Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

layout.tsx

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

layout.tsxBlame3139 lines · 4 contributors
fc1817aClaude1import type { FC, PropsWithChildren } from "hono/jsx";
06d5ffeClaude2import type { User } from "../db/schema";
3import { hljsThemeCss } from "../lib/highlight";
45e31d0Claude4import { clientJs } from "./client-js";
05cdb85Claude5import { getBuildInfo } from "../lib/build-info";
fc1817aClaude6
06d5ffeClaude7export const Layout: FC<
3ef4c9dClaude8 PropsWithChildren<{
9 title?: string;
10 user?: User | null;
11 notificationCount?: number;
6fc53bdClaude12 theme?: "dark" | "light";
5f2e749Claude13 // Block L10 — additive SEO + Open Graph fields. All optional;
14 // omission preserves the prior render exactly (regression-safe).
15 fullTitle?: string;
16 description?: string;
17 ogTitle?: string;
18 ogDescription?: string;
19 ogType?: string;
20 twitterCard?: "summary" | "summary_large_image";
c63b860Claude21 // Block O3 — site-wide footer banner stripe. When non-empty,
22 // renders below the footer-bottom row; wired from
23 // admin.system_flags.site_banner_text.
24 siteBannerText?: string;
25 siteBannerLevel?: "info" | "warn" | "error";
3ef4c9dClaude26 }>
5f2e749Claude27> = ({
28 children,
29 title,
30 user,
31 notificationCount,
32 theme,
33 fullTitle,
34 description,
35 ogTitle,
36 ogDescription,
37 ogType,
38 twitterCard,
c63b860Claude39 siteBannerText,
40 siteBannerLevel,
5f2e749Claude41}) => {
81201ccTest User42 // Default to "light" — feedback from operators was the dark default
43 // felt too gamer-ish and not what senior platform engineers expect from
44 // a tool they'd evaluate alongside Vercel / Linear / Stripe. Users who
45 // explicitly want dark can flip via the theme toggle (cookie persists).
46 const initialTheme = theme === "dark" ? "dark" : "light";
05cdb85Claude47 const build = getBuildInfo();
5f2e749Claude48 // L10 — when `fullTitle` is provided, use it verbatim (no " — gluecron"
49 // suffix); otherwise fall back to the existing `title` + suffix behaviour.
50 const renderedTitle = fullTitle
51 ? fullTitle
52 : title
53 ? `${title} — gluecron`
54 : "gluecron";
fc1817aClaude55 return (
6fc53bdClaude56 <html lang="en" data-theme={initialTheme}>
fc1817aClaude57 <head>
58 <meta charset="UTF-8" />
59 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
eae38d1Claude60 <meta name="theme-color" content="#0d1117" />
3a5755eClaude61 {/* 2026 polish — load Inter + Inter Tight + JetBrains Mono for
62 crisp modern typography. `preconnect` keeps the handshake
63 cost off the critical path; `display=swap` means we never
64 block first paint waiting on fonts. */}
65 <link rel="preconnect" href="https://fonts.googleapis.com" />
66 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="" />
67 <link
68 rel="stylesheet"
69 href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Inter+Tight:wght@600;700;800&family=JetBrains+Mono:wght@400;500&display=swap"
70 />
44fe49bClaude71 {/* PWA removed 2026-05-16 — repeated reload-loop bugs (admin
72 dashboard, deploy pill, admin-screen flash). A git host has
73 no use for service workers or install-as-app. Manifest link
74 and SW registrations are gone permanently. */}
eae38d1Claude75 <link rel="icon" type="image/svg+xml" href="/icon.svg" />
5f2e749Claude76 <title>{renderedTitle}</title>
77 {description && <meta name="description" content={description} />}
78 {(ogTitle || fullTitle || title) && (
79 <meta property="og:title" content={ogTitle ?? fullTitle ?? renderedTitle} />
80 )}
81 {(ogDescription || description) && (
82 <meta
83 property="og:description"
84 content={ogDescription ?? description ?? ""}
85 />
86 )}
87 {ogType && <meta property="og:type" content={ogType} />}
88 {twitterCard && <meta name="twitter:card" content={twitterCard} />}
fa880f2Claude89 <script dangerouslySetInnerHTML={{ __html: themeInitScript }} />
90 <style dangerouslySetInnerHTML={{ __html: css }} />
91 <style dangerouslySetInnerHTML={{ __html: hljsThemeCss }} />
fc1817aClaude92 </head>
93 <body>
4a52a98Claude94 <div class="prelaunch-banner" role="status" aria-live="polite">
95 Pre-launch &mdash; Gluecron is in final validation. Public signups
96 and git hosting for non-owner users open after launch review.
97 </div>
cd4f63bTest User98 {/* Block Q3 — Playground banner. Renders only when the active
99 user is a playground account with a future expiry; the small
100 inline script counts down once per minute. Strip is dismissible
101 per-page-load (re-appears on next nav) — gentle, not noisy. */}
102 {user && (user as any).isPlayground && (user as any).playgroundExpiresAt && (
103 <div
104 class="playground-banner"
105 role="status"
106 aria-live="polite"
107 data-playground-expires={
108 ((user as any).playgroundExpiresAt instanceof Date
109 ? (user as any).playgroundExpiresAt.toISOString()
110 : String((user as any).playgroundExpiresAt))
111 }
112 >
113 <span class="playground-banner-icon" aria-hidden="true">{"\u{1F3AE}"}</span>
114 <span class="playground-banner-text">
115 Playground account &mdash;{" "}
116 <span class="playground-banner-countdown">expires soon</span>.{" "}
117 <a href="/play/claim" class="playground-banner-cta">
118 Save your work &rarr;
119 </a>
120 </span>
121 <button
122 type="button"
123 class="playground-banner-dismiss"
124 aria-label="Dismiss"
125 data-playground-dismiss="1"
126 >
127 {"×"}
128 </button>
129 <script
130 dangerouslySetInnerHTML={{
131 __html: /* js */ `
132 (function () {
133 var el = document.currentScript && document.currentScript.parentElement;
134 if (!el) return;
135 var iso = el.getAttribute('data-playground-expires');
136 if (!iso) return;
137 var target = Date.parse(iso);
138 if (isNaN(target)) return;
139 var out = el.querySelector('.playground-banner-countdown');
140 function render() {
141 var ms = target - Date.now();
142 if (!out) return;
143 if (ms <= 0) { out.textContent = 'expired'; return; }
144 var mins = Math.floor(ms / 60000);
145 var hrs = Math.floor(mins / 60);
146 if (hrs > 1) out.textContent = hrs + ' hours left';
147 else if (hrs === 1) out.textContent = '1 hour left';
148 else if (mins > 1) out.textContent = mins + ' minutes left';
149 else out.textContent = 'less than a minute left';
150 }
151 render();
152 setInterval(render, 60000);
153 var dismiss = el.querySelector('[data-playground-dismiss="1"]');
154 if (dismiss) {
155 dismiss.addEventListener('click', function () {
156 el.style.display = 'none';
157 });
158 }
159 })();
160 `,
161 }}
162 />
163 </div>
164 )}
fc1817aClaude165 <header>
166 <nav>
167 <a href="/" class="logo">
168 gluecron
169 </a>
3ef4c9dClaude170 <div class="nav-search">
001af43Claude171 <form method="get" action="/search">
3ef4c9dClaude172 <input
173 type="search"
174 name="q"
175 placeholder="Search (press /)"
176 aria-label="Search"
177 />
178 </form>
179 </div>
06d5ffeClaude180 <div class="nav-right">
f764c07Claude181 {/* Block N3 — site-admin platform-deploy status pill. Hidden
182 by default; revealed client-side once /admin/deploys/latest.json
183 responds 200 (non-admins get 401/403 and the pill stays
184 hidden). Subscribes to the `platform:deploys` SSE topic
185 for live updates. See `deployPillScript` below. */}
186 {user && (
187 <a
188 id="deploy-pill"
189 href="/admin/deploys"
190 class="nav-deploy-pill"
191 style="display:none"
192 aria-label="Platform deploy status"
193 title="Platform deploy status"
194 >
195 <span class="deploy-pill-dot" />
196 <span class="deploy-pill-text">Deploys</span>
197 </a>
198 )}
6fc53bdClaude199 <a
200 href="/theme/toggle"
201 class="nav-link nav-theme"
202 title="Toggle theme"
203 aria-label="Toggle theme"
204 >
205 <span class="theme-icon-dark">{"\u263E"}</span>
206 <span class="theme-icon-light">{"\u2600"}</span>
207 </a>
c81ab7aClaude208 <a href="/explore" class="nav-link">
209 Explore
210 </a>
06d5ffeClaude211 {user ? (
212 <>
f1ab587Claude213 <a href="/dashboard" class="nav-link" style="font-weight: 600">
214 Dashboard
215 </a>
bdbd0deClaude216 <a href="/import" class="nav-link">
217 Import
218 </a>
06d5ffeClaude219 <a href="/new" class="btn btn-sm btn-primary">
220 + New
221 </a>
222 <a href={`/${user.username}`} class="nav-user">
223 {user.displayName || user.username}
224 </a>
225 <a href="/settings" class="nav-link">
226 Settings
227 </a>
228 <a href="/logout" class="nav-link">
229 Sign out
230 </a>
231 </>
232 ) : (
233 <>
234 <a href="/login" class="nav-link">
235 Sign in
236 </a>
237 <a href="/register" class="btn btn-sm btn-primary">
238 Register
239 </a>
240 </>
241 )}
242 </div>
fc1817aClaude243 </nav>
244 </header>
45e31d0Claude245 <main id="main-content">{children}</main>
cf9178bTest User246 {/* Global toast host — populated by the toastScript below from
247 ?success= / ?error= / ?toast= query params. Replaces the
248 per-page banner pattern with one polished slide-in. */}
249 <div
250 id="toast-host"
251 aria-live="polite"
252 aria-atomic="true"
253 style="position:fixed;top:calc(var(--header-h) + 12px);right:16px;z-index:var(--z-toast,10000);display:flex;flex-direction:column;gap:8px;pointer-events:none"
254 />
fc1817aClaude255 <footer>
958d26aClaude256 <div class="footer-inner">
257 <div class="footer-brand">
258 <a href="/" class="logo">gluecron</a>
259 <p class="footer-tag">
260 AI-native code intelligence. Self-hosted git, automated CI,
261 push-time gates. Software that ships itself.
262 </p>
263 </div>
264 <div class="footer-links">
265 <div class="footer-col">
266 <div class="footer-col-title">Product</div>
b0148e9Claude267 <a href="/features">Features</a>
268 <a href="/pricing">Pricing</a>
958d26aClaude269 <a href="/explore">Explore</a>
270 <a href="/marketplace">Marketplace</a>
271 </div>
272 <div class="footer-col">
273 <div class="footer-col-title">Platform</div>
b0148e9Claude274 <a href="/help">Quickstart</a>
958d26aClaude275 <a href="/status">Status</a>
276 <a href="/api/graphql">GraphQL</a>
277 <a href="/mcp">MCP server</a>
278 </div>
279 <div class="footer-col">
b0148e9Claude280 <div class="footer-col-title">Company</div>
281 <a href="/about">About</a>
958d26aClaude282 <a href="/terms">Terms</a>
283 <a href="/privacy">Privacy</a>
284 <a href="/acceptable-use">Acceptable use</a>
285 </div>
286 </div>
287 </div>
288 <div class="footer-bottom">
289 <span>&copy; {new Date().getFullYear()} gluecron</span>
05cdb85Claude290 <span class="footer-build" title={`commit ${build.shaFull}\nbuilt ${build.builtAt}`}>
291 <span class="footer-build-dot" aria-hidden="true" />
292 {build.sha} · {build.branch}
293 </span>
36b4cbdClaude294 </div>
c63b860Claude295 {siteBannerText ? (
296 <div
297 class={`footer-banner footer-banner-${siteBannerLevel || "info"}`}
298 role="status"
299 aria-live="polite"
300 >
301 {siteBannerText}
302 </div>
303 ) : null}
fc1817aClaude304 </footer>
05cdb85Claude305 {/* Live update poller — checks /api/version every 15s, prompts
306 reload when the running sha changes. Pure progressive-
307 enhancement; degrades to nothing if JS is off. */}
308 <div
309 id="version-banner"
310 style="display:none;position:fixed;bottom:18px;left:50%;transform:translateX(-50%);z-index:9999;background:var(--bg-elevated);border:1px solid rgba(140,109,255,0.45);border-radius:9999px;padding:8px 14px 8px 14px;font-size:13px;color:var(--text-strong);box-shadow:0 12px 28px -8px rgba(0,0,0,0.55),0 0 24px -6px rgba(140,109,255,0.40);font-family:var(--font-sans);align-items:center;gap:10px"
311 >
312 <span style="display:inline-flex;align-items:center;gap:8px">
313 <span style="width:8px;height:8px;border-radius:50%;background:#34d399;box-shadow:0 0 10px rgba(52,211,153,0.6)" />
314 <span>New version available</span>
315 </span>
316 <button
317 type="button"
318 id="version-banner-reload"
319 style="background:linear-gradient(135deg,#8c6dff 0%,#36c5d6 100%);color:#fff;border:0;border-radius:9999px;padding:5px 12px;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit"
320 >
321 Reload
322 </button>
323 </div>
fa880f2Claude324 <script dangerouslySetInnerHTML={{ __html: versionPollerScript }} />
f764c07Claude325 {/* Block N3 — site-admin deploy status pill (script-only). The pill
326 container is rendered above for authed users; this script bootstraps
327 it by fetching /admin/deploys/latest.json. Non-admins get 401/403
328 and the pill stays display:none — zero leak. */}
329 {user && (
330 <script dangerouslySetInnerHTML={{ __html: deployPillScript }} />
331 )}
699e5c7Claude332 {/* Block I4 — Command palette shell (hidden by default) */}
333 <div
334 id="cmdk-backdrop"
335 style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:9998"
336 />
337 <div
338 id="cmdk-panel"
339 style="display:none;position:fixed;top:10%;left:50%;transform:translateX(-50%);width:min(560px,92vw);background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius);box-shadow:0 12px 32px rgba(0,0,0,0.4);z-index:9999;overflow:hidden"
340 >
341 <input
342 id="cmdk-input"
343 type="text"
344 placeholder="Type a command..."
345 aria-label="Command palette"
dc26881CC LABS App346 style="width:100%;padding:var(--space-3) var(--space-4);background:transparent;color:var(--text);border:0;border-bottom:1px solid var(--border);outline:none;font-size:14px"
699e5c7Claude347 />
348 <div id="cmdk-list" style="max-height:60vh;overflow-y:auto" />
349 </div>
fa880f2Claude350 <script dangerouslySetInnerHTML={{ __html: clientJs }} />
44fe49bClaude351 {/* PWA-kill script: actively unregisters any service worker
352 previously installed under gluecron.com. Recovers any browser
353 still trapped in the SW reload loop from the legacy registrations. */}
354 <script dangerouslySetInnerHTML={{ __html: pwaKillSwitchScript }} />
cf9178bTest User355 <script dangerouslySetInnerHTML={{ __html: toastScript }} />
fa880f2Claude356 <script dangerouslySetInnerHTML={{ __html: navScript }} />
fc1817aClaude357 </body>
358 </html>
359 );
360};
361
05cdb85Claude362// Live version poller. Checks /api/version every 15s; if the sha differs
363// from the one we booted with, reveal the floating 'New version' pill so
364// the user can reload onto the new code without manually refreshing.
365const versionPollerScript = `
366 (function(){
367 var loadedSha = null;
368 var banner, btn;
369 function poll(){
370 fetch('/api/version', { cache: 'no-store' })
371 .then(function(r){ return r.ok ? r.json() : null; })
372 .then(function(j){
373 if (!j || !j.sha) return;
374 if (loadedSha === null) { loadedSha = j.sha; return; }
375 if (j.sha !== loadedSha && banner) {
376 banner.style.display = 'inline-flex';
377 }
378 })
379 .catch(function(){});
380 }
381 function init(){
382 banner = document.getElementById('version-banner');
383 btn = document.getElementById('version-banner-reload');
384 if (btn) btn.addEventListener('click', function(){ window.location.reload(); });
385 poll();
386 setInterval(poll, 15000);
387 }
388 if (document.readyState === 'loading') {
389 document.addEventListener('DOMContentLoaded', init);
390 } else {
391 init();
392 }
393 })();
394`;
395
f764c07Claude396// Block N3 — site-admin deploy status pill. Fetches /admin/deploys/latest.json;
397// if 200, reveals the pill in the nav and subscribes to the `platform:deploys`
398// SSE topic for live status updates. Non-admins get 401/403 and the pill stays
399// display:none — there's no leakage of admin-only data to other users.
400//
401// Pill states (CSS classes on the container drive colour):
402// .deploy-pill-success 🟢 "Deployed 12s ago"
403// .deploy-pill-progress 🟡 "Deploying… 14s" (pulsing dot)
404// .deploy-pill-failed 🔴 "Deploy failed 1m ago"
405// .deploy-pill-empty ⚪ "No deploys yet"
406//
407// Relative-time auto-refreshes every 15s without re-fetching.
408export const deployPillScript = `
409 (function(){
410 var pill, dot, text;
411 var state = { latest: null, asOf: null };
412
413 function classifyAge(ms){
414 if (ms < 0) return 'just now';
415 var s = Math.floor(ms / 1000);
416 if (s < 5) return 'just now';
417 if (s < 60) return s + 's ago';
418 var m = Math.floor(s / 60);
419 if (m < 60) return m + 'm ago';
420 var h = Math.floor(m / 60);
421 if (h < 24) return h + 'h ago';
422 var d = Math.floor(h / 24);
423 return d + 'd ago';
424 }
425 function elapsed(ms){
426 if (ms < 0) ms = 0;
427 var s = Math.floor(ms / 1000);
428 if (s < 60) return s + 's';
429 var m = Math.floor(s / 60);
430 var rem = s - m * 60;
431 return m + 'm ' + rem + 's';
432 }
433
434 function render(){
435 if (!pill || !text || !dot) return;
436 var d = state.latest;
81201ccTest User437 // When there's no deploy event yet, keep the pill HIDDEN. Showing
438 // "No deploys yet" was visible noise on every admin page load and
439 // flashed during reconnect cycles — admins don't need a placeholder.
440 // The pill reveals itself the first time a real deploy fires.
f764c07Claude441 if (!d) {
81201ccTest User442 pill.style.display = 'none';
f764c07Claude443 return;
444 }
445 pill.style.display = 'inline-flex';
446 var now = Date.now();
447 if (d.status === 'in_progress') {
448 pill.className = 'nav-deploy-pill deploy-pill-progress';
449 var started = Date.parse(d.started_at) || now;
450 text.textContent = 'Deploying… ' + elapsed(now - started);
451 } else if (d.status === 'succeeded') {
452 pill.className = 'nav-deploy-pill deploy-pill-success';
453 var ref = Date.parse(d.finished_at || d.started_at) || now;
454 text.textContent = 'Deployed ' + classifyAge(now - ref);
455 } else if (d.status === 'failed') {
456 pill.className = 'nav-deploy-pill deploy-pill-failed';
457 var refF = Date.parse(d.finished_at || d.started_at) || now;
458 text.textContent = 'Deploy failed ' + classifyAge(now - refF);
459 } else {
460 pill.className = 'nav-deploy-pill';
461 text.textContent = d.status;
462 }
463 }
464
465 function fetchLatest(){
466 fetch('/admin/deploys/latest.json', { cache: 'no-store', credentials: 'same-origin' })
467 .then(function(r){ if (!r.ok) return null; return r.json(); })
468 .then(function(j){
469 if (!j || j.ok !== true) return;
470 state.latest = j.latest;
471 state.asOf = j.asOf;
472 render();
473 subscribe();
474 })
475 .catch(function(){});
476 }
477
478 var subscribed = false;
479 function subscribe(){
480 if (subscribed) return;
481 if (typeof EventSource === 'undefined') return;
482 subscribed = true;
483 var es;
bf19c50Test User484 // Exponential backoff with cap. Previously a tight 1500ms reconnect
485 // produced visible looping in the nav whenever the proxy timed out
486 // or the connection blipped — the bottom-of-page deploy pill
487 // re-rendered the placeholder every 1.5s. Cap at 60s and reset on
488 // successful message receipt.
489 var delay = 2000;
490 var DELAY_MAX = 60000;
491 function bump(){
492 delay = Math.min(delay * 2, DELAY_MAX);
493 }
494 function resetDelay(){
495 delay = 2000;
496 }
f764c07Claude497 function connect(){
498 try { es = new EventSource('/live-events/platform:deploys'); }
bf19c50Test User499 catch(e){ bump(); setTimeout(connect, delay); return; }
f764c07Claude500 es.onmessage = function(m){
bf19c50Test User501 resetDelay();
f764c07Claude502 try {
503 var d = JSON.parse(m.data);
504 if (d && d.run_id) {
505 if (!state.latest || state.latest.run_id === d.run_id ||
506 Date.parse(d.started_at) >= Date.parse(state.latest.started_at)) {
507 state.latest = d;
508 render();
509 }
510 }
511 } catch(e){}
512 };
513 es.onerror = function(){
514 try { es.close(); } catch(e){}
bf19c50Test User515 bump();
f764c07Claude516 setTimeout(connect, delay);
517 };
518 }
519 connect();
520 }
521
522 function init(){
523 pill = document.getElementById('deploy-pill');
524 if (!pill) return;
525 dot = pill.querySelector('.deploy-pill-dot');
526 text = pill.querySelector('.deploy-pill-text');
527 fetchLatest();
528 // Refresh relative-time labels every 15s so "12s ago" → "27s ago"
529 // without a fresh fetch.
530 setInterval(render, 15000);
531 }
532 if (document.readyState === 'loading') {
533 document.addEventListener('DOMContentLoaded', init);
534 } else {
535 init();
536 }
537 })();
538`;
539
6fc53bdClaude540// Runs before paint — reads the theme cookie and flips data-theme so there's
81201ccTest User541// no light-to-dark flash on load. SSR default is "light"; cookie-set "dark"
542// is honoured for users who explicitly opted in.
6fc53bdClaude543const themeInitScript = `
544 (function(){
545 try {
546 var m = document.cookie.match(/(?:^|; )theme=([^;]+)/);
81201ccTest User547 var t = m ? decodeURIComponent(m[1]) : 'light';
548 if (t !== 'light' && t !== 'dark') t = 'light';
6fc53bdClaude549 document.documentElement.setAttribute('data-theme', t);
550 } catch(_){}
551 })();
552`;
553
eae38d1Claude554// Block G1 — register service worker for offline / install support.
555// Kept inline (and tiny) so we don't block first paint.
b1be050CC LABS App556//
cf9178bTest User557// Global toast notifications — reads ?success=, ?error=, ?toast= from the
558// URL on page load and surfaces a polished slide-in toast instead of the
559// per-page banner divs that crowded the layout. Toasts auto-dismiss after
560// 4.5s; query params are scrubbed from the URL via history.replaceState
561// so a subsequent Refresh doesn't re-fire the same toast.
562//
563// Variants: ?success=…, ?error=…, ?toast=info:…, ?toast=warn:… All values
564// must be URI-encoded (callers already do this via encodeURIComponent
565// in c.redirect()).
566const toastScript = `
567 (function(){
568 function showToast(kind, message){
569 if (!message) return;
570 var host = document.getElementById('toast-host');
571 if (!host) return;
572 var el = document.createElement('div');
573 el.className = 'gx-toast gx-toast--' + kind;
574 el.setAttribute('role', kind === 'error' ? 'alert' : 'status');
575 var icon = document.createElement('span');
576 icon.className = 'gx-toast__icon';
577 icon.textContent = kind === 'success' ? '\\u2713'
578 : kind === 'error' ? '\\u00D7'
579 : kind === 'warn' ? '!'
580 : 'i';
581 el.appendChild(icon);
582 var text = document.createElement('span');
583 text.className = 'gx-toast__text';
584 text.textContent = message;
585 el.appendChild(text);
586 var close = document.createElement('button');
587 close.type = 'button';
588 close.className = 'gx-toast__close';
589 close.setAttribute('aria-label', 'Dismiss notification');
590 close.textContent = '\\u00D7';
591 close.addEventListener('click', function(){ dismiss(); });
592 el.appendChild(close);
593 host.appendChild(el);
594 // Force a reflow then add the visible class so the slide-in transitions.
595 void el.offsetWidth;
596 el.classList.add('gx-toast--in');
597 var timer = setTimeout(dismiss, 4500);
598 function dismiss(){
599 clearTimeout(timer);
600 el.classList.remove('gx-toast--in');
601 el.classList.add('gx-toast--out');
602 setTimeout(function(){
603 if (el.parentNode) el.parentNode.removeChild(el);
604 }, 220);
605 }
606 }
607 try {
608 var url = new URL(window.location.href);
609 var hits = 0;
610 var s = url.searchParams.get('success');
611 if (s) { showToast('success', s); url.searchParams.delete('success'); hits++; }
612 var e = url.searchParams.get('error');
613 if (e) { showToast('error', e); url.searchParams.delete('error'); hits++; }
614 var t = url.searchParams.get('toast');
615 if (t) {
616 var ix = t.indexOf(':');
617 var kind = ix > 0 ? t.slice(0, ix) : 'info';
618 var msg = ix > 0 ? t.slice(ix + 1) : t;
619 showToast(kind, msg);
620 url.searchParams.delete('toast');
621 hits++;
622 }
623 if (hits > 0 && window.history && window.history.replaceState) {
624 window.history.replaceState({}, '', url.pathname + (url.searchParams.toString() ? '?' + url.searchParams.toString() : '') + url.hash);
625 }
626 } catch(_) {}
627 })();
628`;
629
44fe49bClaude630// PWA kill-switch (2026-05-16) — replaces the previous pwaRegisterScript
631// and pwaInstallBannerScript. Those two scripts registered /sw.js and
632// /sw-push.js at the same scope, causing a reload loop that made the
633// admin dashboard unusable (deploy pill flashing, typing wiped, buttons
634// uncllickable). Per the SW spec, only one SW can control a scope; two
635// different script URLs at the same scope keep replacing each other.
6345c3eTest User636//
44fe49bClaude637// PWA is gone for good. A git host has no use for service workers,
638// install-as-app, or push notifications via SW. This script actively
639// unregisters every previously installed SW on the gluecron.com origin
640// so any browser still trapped in the loop recovers on the very next
641// page load — without needing the user to clear site data or open
642// DevTools. Idempotent and safe to keep running forever; once all
643// browsers have been cleaned, it's a no-op.
644const pwaKillSwitchScript = `
534f04aClaude645(function(){
646 try {
44fe49bClaude647 if (!('serviceWorker' in navigator)) return;
648 if (!navigator.serviceWorker.getRegistrations) return;
649 navigator.serviceWorker.getRegistrations().then(function(regs){
650 if (!regs || regs.length === 0) return;
651 regs.forEach(function(reg){
652 try { reg.unregister(); } catch(_){}
653 });
654 }).catch(function(){});
655 // Also drop any caches the old SWs left behind so the user gets
656 // truly fresh HTML on every page load. Restricted to gluecron-*
657 // namespaced caches so we don't trample anything a future opt-in
658 // feature might create under a different name.
659 if ('caches' in self) {
660 caches.keys().then(function(keys){
661 keys.forEach(function(k){
662 if (typeof k === 'string' && k.indexOf('gluecron') === 0) {
663 try { caches.delete(k); } catch(_){}
d7ba05dClaude664 }
665 });
666 }).catch(function(){});
534f04aClaude667 }
668 } catch(_) {}
669})();
670`;
671
3ef4c9dClaude672const navScript = `
673 (function(){
674 var chord = null;
675 var chordTimer = null;
676 function isTyping(t){
677 t = t || {};
678 var tag = (t.tagName || '').toLowerCase();
679 return tag === 'input' || tag === 'textarea' || t.isContentEditable;
680 }
71cd5ecClaude681
682 // ---------- Block I4 — Command palette ----------
683 var COMMANDS = [
684 { label: 'Go to Dashboard', href: '/dashboard', kw: 'home' },
685 { label: 'Go to Explore', href: '/explore', kw: 'browse discover' },
686 { label: 'Go to Notifications', href: '/notifications', kw: 'inbox' },
687 { label: 'Go to Ask AI', href: '/ask', kw: 'chat assistant' },
688 { label: 'Create new repository', href: '/new', kw: 'add create' },
689 { label: 'Marketplace', href: '/marketplace', kw: 'apps store' },
690 { label: 'Installed apps', href: '/settings/apps', kw: 'my apps' },
691 { label: 'Register new app', href: '/developer/apps-new', kw: 'developer create' },
692 { label: 'Keyboard shortcuts', href: '/shortcuts', kw: 'help keys' },
693 { label: 'Settings (profile)', href: '/settings', kw: 'account' },
694 { label: '2FA settings', href: '/settings/2fa', kw: 'two factor security' },
695 { label: 'Passkeys settings', href: '/settings/passkeys', kw: 'webauthn' },
696 { label: 'Personal access tokens', href: '/settings/tokens', kw: 'pat api' },
697 { label: 'Billing + quotas', href: '/settings/billing', kw: 'plans money' },
698 { label: 'Audit log (personal)', href: '/settings/audit', kw: 'history' },
699 { label: 'Gists', href: '/gists', kw: 'snippets' },
700 { label: 'GraphQL explorer', href: '/api/graphql', kw: 'api query' },
701 { label: 'Admin dashboard', href: '/admin', kw: 'superuser' },
702 { label: 'Toggle theme', href: '/theme/toggle', kw: 'dark light mode' }
703 ];
704
705 function fuzzyMatch(item, q){
706 if (!q) return true;
707 var hay = (item.label + ' ' + (item.kw||'') + ' ' + item.href).toLowerCase();
708 q = q.toLowerCase();
709 var qi = 0;
710 for (var i = 0; i < hay.length && qi < q.length; i++) {
711 if (hay[i] === q[qi]) qi++;
712 }
713 return qi === q.length;
714 }
715
716 var backdrop, panel, input, list, selected = 0, filtered = COMMANDS.slice();
717
718 function render(){
719 if (!list) return;
720 var html = '';
721 for (var i = 0; i < filtered.length; i++) {
722 var item = filtered[i];
723 var cls = i === selected ? 'cmdk-item cmdk-active' : 'cmdk-item';
724 var bg = i === selected ? 'background:var(--bg);' : '';
ea52715copilot-swe-agent[bot]725 html += '<div class="' + cls + '" data-idx="' + i + '" data-url="' + item.href + '"' +
dc26881CC LABS App726 ' style="padding:var(--space-2) var(--space-4);cursor:pointer;border-bottom:1px solid var(--border);' + bg + '">' +
71cd5ecClaude727 '<div>' + item.label + '</div>' +
728 '<div style="font-size:11px;color:var(--text-muted)">' + item.href + '</div>' +
729 '</div>';
730 }
731 if (filtered.length === 0) {
dc26881CC LABS App732 html = '<div style="padding:var(--space-4);color:var(--text-muted);text-align:center">No matches.</div>';
71cd5ecClaude733 }
734 list.innerHTML = html;
735 }
736
737 function openPalette(){
738 backdrop = document.getElementById('cmdk-backdrop');
739 panel = document.getElementById('cmdk-panel');
740 input = document.getElementById('cmdk-input');
741 list = document.getElementById('cmdk-list');
742 if (!backdrop || !panel) return;
743 backdrop.style.display = 'block';
744 panel.style.display = 'block';
745 input.value = '';
746 selected = 0;
747 filtered = COMMANDS.slice();
748 render();
749 input.focus();
750 }
751 function closePalette(){
752 if (backdrop) backdrop.style.display = 'none';
753 if (panel) panel.style.display = 'none';
754 }
755 function go(href){ closePalette(); window.location.href = href; }
756
757 document.addEventListener('click', function(e){
758 var t = e.target;
759 if (t && t.id === 'cmdk-backdrop') { closePalette(); return; }
760 var item = t && t.closest && t.closest('.cmdk-item');
ea52715copilot-swe-agent[bot]761 if (item) { go(item.getAttribute('data-url')); }
71cd5ecClaude762 });
763
764 document.addEventListener('input', function(e){
765 if (e.target && e.target.id === 'cmdk-input') {
766 var q = e.target.value;
767 filtered = COMMANDS.filter(function(c){ return fuzzyMatch(c, q); });
768 selected = 0;
769 render();
770 }
771 });
772
3ef4c9dClaude773 document.addEventListener('keydown', function(e){
71cd5ecClaude774 // Palette-scoped keys take priority when open
775 if (panel && panel.style.display === 'block') {
776 if (e.key === 'Escape') { e.preventDefault(); closePalette(); return; }
777 if (e.key === 'ArrowDown') {
778 e.preventDefault();
779 selected = Math.min(filtered.length - 1, selected + 1);
780 render();
781 return;
782 }
783 if (e.key === 'ArrowUp') {
784 e.preventDefault();
785 selected = Math.max(0, selected - 1);
786 render();
787 return;
788 }
789 if (e.key === 'Enter') {
790 e.preventDefault();
791 var item = filtered[selected];
792 if (item) go(item.href);
793 return;
794 }
795 return;
796 }
797
3ef4c9dClaude798 if (isTyping(e.target)) return;
799 // Single key shortcuts
800 if (e.key === '/') {
801 var el = document.querySelector('.nav-search input');
802 if (el) { e.preventDefault(); el.focus(); return; }
803 }
804 if ((e.metaKey||e.ctrlKey) && e.key.toLowerCase() === 'k') {
71cd5ecClaude805 e.preventDefault();
806 openPalette();
807 return;
3ef4c9dClaude808 }
809 if (e.key === '?' && !e.ctrlKey && !e.metaKey) {
810 e.preventDefault(); window.location.href = '/shortcuts'; return;
811 }
812 if (e.key === 'n' && !e.ctrlKey && !e.metaKey) {
813 e.preventDefault(); window.location.href = '/new'; return;
814 }
815 // "g" chord
816 if (e.key === 'g' && !e.ctrlKey && !e.metaKey) {
817 chord = 'g';
818 clearTimeout(chordTimer);
819 chordTimer = setTimeout(function(){ chord = null; }, 1200);
820 return;
821 }
822 if (chord === 'g') {
823 if (e.key === 'd') { e.preventDefault(); window.location.href = '/dashboard'; }
824 else if (e.key === 'n') { e.preventDefault(); window.location.href = '/notifications'; }
825 else if (e.key === 'e') { e.preventDefault(); window.location.href = '/explore'; }
826 else if (e.key === 'a') { e.preventDefault(); window.location.href = '/ask'; }
827 chord = null;
828 }
829 });
830 })();
831`;
832
fc1817aClaude833const css = `
2ce1d0bClaude834 /* ================================================================
958d26aClaude835 * Gluecron design system — 2026.05 "Editorial-Technical"
836 * Slate-noir base · refined violet signature · hairline geometry ·
837 * mono-as-feature · cinematic motion · Inter Tight + JetBrains Mono.
838 * All class names preserved for back-compat across 50+ route views.
2ce1d0bClaude839 * ============================================================== */
6fc53bdClaude840 :root, :root[data-theme='dark'] {
958d26aClaude841 /* Surfaces — slate, not black. More depth, less crush. */
842 --bg: #08090f;
843 --bg-secondary: #0c0d14;
844 --bg-tertiary: #11131c;
845 --bg-elevated: #0f111a;
846 --bg-surface: #161826;
847 --bg-hover: rgba(255,255,255,0.04);
848 --bg-active: rgba(255,255,255,0.08);
849 --bg-inset: rgba(0,0,0,0.30);
850
851 /* Borders — three weights, used deliberately */
852 --border: rgba(255,255,255,0.06);
853 --border-subtle: rgba(255,255,255,0.035);
854 --border-strong: rgba(255,255,255,0.13);
855 --border-focus: rgba(140,109,255,0.55);
856
857 /* Text */
858 --text: #ededf2;
859 --text-strong: #f7f7fb;
860 --text-muted: #8b8c9c;
861 --text-faint: #555665;
862 --text-link: #b69dff;
863
864 /* Accent — refined violet (less candy), warm amber as secondary signal */
865 --accent: #8c6dff;
866 --accent-2: #36c5d6;
867 --accent-warm: #ffb45e;
868 --accent-hover: #a48bff;
869 --accent-pressed:#7559e8;
870 --accent-gradient: linear-gradient(135deg, #8c6dff 0%, #36c5d6 100%);
871 --accent-gradient-soft: linear-gradient(135deg, rgba(140,109,255,0.18) 0%, rgba(54,197,214,0.18) 100%);
872 --accent-gradient-faint: linear-gradient(135deg, rgba(140,109,255,0.07) 0%, rgba(54,197,214,0.07) 100%);
873 --accent-glow: 0 0 24px rgba(140,109,255,0.28);
874
875 /* Semantic */
876 --green: #34d399;
877 --red: #f87171;
878 --yellow: #fbbf24;
879 --amber: #fbbf24;
880 --blue: #60a5fa;
881
3a5755eClaude882 /* Type — 2026 polish pass. Inter is the primary sans, Inter Tight for
883 display (headlines), JetBrains Mono for code. All three loaded from
884 Google Fonts with display:swap so we never block first paint. System
885 fallbacks remain in place — if the CDN is unreachable the site still
886 renders cleanly with Segoe UI (Win) / SF (Mac) / Roboto (Android). */
887 --font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', 'Cascadia Code', 'Cascadia Mono', Menlo, Consolas, 'Courier New', monospace;
888 --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI Variable', 'Segoe UI', system-ui, Roboto, 'Helvetica Neue', Arial, sans-serif;
889 --font-display: 'Inter Tight', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI Variable', 'Segoe UI', system-ui, Roboto, 'Helvetica Neue', Arial, sans-serif;
958d26aClaude890 --mono-feat: 'calt', 'liga', 'ss01';
891
892 /* Radius — sharper than before */
893 --r-sm: 5px;
894 --r: 7px;
895 --r-md: 9px;
896 --r-lg: 12px;
897 --r-xl: 16px;
898 --r-2xl: 22px;
899 --r-full: 9999px;
900 --radius: 7px;
901
902 /* Type scale — bigger display sizes for editorial feel */
903 --t-xs: 11px;
904 --t-sm: 13px;
905 --t-base: 14px;
906 --t-md: 16px;
907 --t-lg: 20px;
908 --t-xl: 28px;
909 --t-2xl: 40px;
910 --t-3xl: 56px;
911 --t-display: 72px;
912 --t-display-lg:96px;
913
914 /* Spacing — 4px base */
2ce1d0bClaude915 --s-0: 0;
958d26aClaude916 --s-1: 4px; --s-2: 8px; --s-3: 12px; --s-4: 16px;
917 --s-5: 20px; --s-6: 24px; --s-7: 28px; --s-8: 32px;
918 --s-10:40px; --s-12:48px; --s-14:56px; --s-16:64px;
919 --s-20:80px; --s-24:96px; --s-32:128px;
920
921 /* Elevation — softer + more layered */
922 --elev-0: 0 0 0 1px var(--border);
923 --elev-1: 0 1px 2px rgba(0,0,0,0.50), 0 0 0 1px var(--border);
924 --elev-2: 0 8px 24px -8px rgba(0,0,0,0.60), 0 0 0 1px var(--border);
925 --elev-3: 0 20px 48px -12px rgba(0,0,0,0.70), 0 0 0 1px var(--border-strong);
926 --elev-glow: 0 0 0 1px rgba(140,109,255,0.40), 0 0 32px -4px rgba(140,109,255,0.30);
927 --ring: 0 0 0 3px rgba(140,109,255,0.28);
928 --ring-warn: 0 0 0 3px rgba(251,191,36,0.28);
929 --ring-err: 0 0 0 3px rgba(248,113,113,0.28);
930
931 /* Motion */
932 --ease: cubic-bezier(0.16, 1, 0.3, 1);
933 --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
934 --ease-out-expo: cubic-bezier(0.19, 1, 0.22, 1);
935 --ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1);
936 --t-fast: 120ms;
937 --t-base: 200ms;
938 --t-slow: 360ms;
939 --t-slower:560ms;
940
941 --header-h: 60px;
c63b860Claude942
943 /* Block O3 — visual coherence: named token aliases (additive). */
944 --space-1: var(--s-1);
945 --space-2: var(--s-2);
946 --space-3: var(--s-3);
947 --space-4: var(--s-4);
948 --space-5: var(--s-5);
949 --space-6: var(--s-6);
950 --space-8: var(--s-8);
951 --space-10: var(--s-10);
952 --space-12: var(--s-12);
953 --space-16: var(--s-16);
954 --space-20: var(--s-20);
955 --space-24: var(--s-24);
956 --radius-sm: var(--r-sm);
957 --radius-md: var(--r-md);
958 --radius-lg: var(--r-lg);
959 --radius-xl: var(--r-xl);
960 --radius-full: var(--r-full);
961 --font-size-xs: var(--t-xs);
962 --font-size-sm: var(--t-sm);
963 --font-size-base: var(--t-base);
964 --font-size-md: var(--t-md);
965 --font-size-lg: var(--t-lg);
966 --font-size-xl: var(--t-xl);
967 --font-size-2xl: var(--t-2xl);
968 --font-size-3xl: var(--t-3xl);
969 --font-size-hero: var(--t-display);
970 --leading-tight: 1.2;
971 --leading-snug: 1.35;
972 --leading-normal: 1.5;
973 --leading-relaxed: 1.6;
974 --leading-loose: 1.7;
975 --z-base: 1;
976 --z-nav: 10;
977 --z-sticky: 50;
978 --z-overlay: 100;
979 --z-modal: 1000;
980 --z-toast: 10000;
fc1817aClaude981 }
982
6fc53bdClaude983 :root[data-theme='light'] {
958d26aClaude984 --bg: #fbfbfc;
4c47454Claude985 --bg-secondary: #ffffff;
958d26aClaude986 --bg-tertiary: #f3f3f6;
987 --bg-elevated: #ffffff;
988 --bg-surface: #f6f6f9;
989 --bg-hover: rgba(0,0,0,0.035);
990 --bg-active: rgba(0,0,0,0.07);
991 --bg-inset: rgba(0,0,0,0.04);
992
993 --border: rgba(15,16,28,0.08);
994 --border-subtle: rgba(15,16,28,0.04);
995 --border-strong: rgba(15,16,28,0.16);
996
997 --text: #0e1020;
998 --text-strong: #050617;
999 --text-muted: #5a5b70;
1000 --text-faint: #8a8b9e;
1001 --text-link: #6d4dff;
1002
1003 --accent: #6d4dff;
1004 --accent-2: #0891b2;
1005 --accent-hover: #5a3df0;
1006 --accent-pressed:#4a30d6;
1007 --accent-glow: 0 0 24px rgba(109,77,255,0.18);
1008
1009 --green: #059669;
1010 --red: #dc2626;
4c47454Claude1011 --yellow: #d97706;
958d26aClaude1012
1013 --elev-1: 0 1px 2px rgba(15,16,28,0.06), 0 0 0 1px var(--border);
1014 --elev-2: 0 8px 24px -10px rgba(15,16,28,0.10), 0 0 0 1px var(--border);
1015 --elev-3: 0 20px 48px -16px rgba(15,16,28,0.14), 0 0 0 1px var(--border-strong);
6fc53bdClaude1016 }
1017
1018 /* Theme toggle — show the icon for the *opposite* theme so users see what they'll switch to. */
958d26aClaude1019 .nav-theme { display: inline-flex; align-items: center; font-size: 15px; line-height: 1; opacity: 0.85; }
1020 .nav-theme:hover { opacity: 1; }
6fc53bdClaude1021 :root[data-theme='dark'] .theme-icon-dark { display: none; }
1022 :root[data-theme='light'] .theme-icon-light { display: none; }
1023
fc1817aClaude1024 * { margin: 0; padding: 0; box-sizing: border-box; }
958d26aClaude1025 *::selection { background: rgba(140,109,255,0.32); color: var(--text-strong); }
2ce1d0bClaude1026
958d26aClaude1027 html { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-rendering: optimizeLegibility; }
fc1817aClaude1028
1029 body {
1030 font-family: var(--font-sans);
1031 background: var(--bg);
1032 color: var(--text);
cf9178bTest User1033 font-size: 15px;
2ce1d0bClaude1034 line-height: 1.55;
958d26aClaude1035 letter-spacing: -0.011em;
fc1817aClaude1036 min-height: 100vh;
1037 display: flex;
1038 flex-direction: column;
958d26aClaude1039 font-feature-settings: 'cv11', 'ss01', 'ss03', 'calt';
cf9178bTest User1040 /* Subtle: prefers grayscale font smoothing on macOS for thin text,
1041 and disables automatic synthesis of bold/italic which can produce
1042 muddier rendering on certain weights. */
1043 -webkit-font-smoothing: antialiased;
1044 -moz-osx-font-smoothing: grayscale;
1045 font-synthesis: none;
1046 }
1047 /* Tighten heading rhythm — the body is 15/1.55, headings step down
1048 line-height inversely with size so display text doesn't feel airy. */
1049 h1, h2, h3, h4, h5, h6 {
1050 font-family: var(--font-display);
1051 color: var(--text-strong);
1052 letter-spacing: -0.022em;
1053 line-height: 1.18;
1054 font-weight: 650;
1055 margin-top: 0;
1056 }
1057 h1 { font-size: 28px; letter-spacing: -0.028em; }
1058 h2 { font-size: 22px; }
1059 h3 { font-size: 18px; letter-spacing: -0.018em; }
1060 h4 { font-size: 15.5px; letter-spacing: -0.012em; font-weight: 600; }
1061 /* Link refinement — underline only on hover/focus, never by default
1062 on internal nav. Prevents the "blue underline soup" look. */
1063 a { color: var(--text-link); text-decoration: none; transition: color var(--t-fast) var(--ease); }
1064 a:hover { color: var(--accent-hover); text-decoration: underline; text-underline-offset: 3px; }
1065 a:focus-visible {
1066 outline: none;
1067 box-shadow: 0 0 0 3px rgba(109,77,255,0.32);
1068 border-radius: 3px;
fc1817aClaude1069 }
1070
958d26aClaude1071 /* Whole-page atmosphere: very subtle gradient + dot-grid layered behind everything.
1072 Keeps every page feeling like part of the same product without competing with hero art. */
2ce1d0bClaude1073 body::before {
1074 content: '';
1075 position: fixed;
1076 inset: 0;
1077 pointer-events: none;
958d26aClaude1078 z-index: -2;
2ce1d0bClaude1079 background:
958d26aClaude1080 radial-gradient(70% 55% at 85% -20%, rgba(140,109,255,0.07), transparent 65%),
1081 radial-gradient(55% 45% at -10% 115%, rgba(54,197,214,0.05), transparent 65%);
1082 }
1083 body::after {
1084 content: '';
1085 position: fixed;
1086 inset: 0;
1087 pointer-events: none;
1088 z-index: -1;
1089 background-image: radial-gradient(rgba(255,255,255,0.025) 1px, transparent 1px);
1090 background-size: 28px 28px;
1091 background-position: 0 0;
1092 opacity: 0.55;
1093 mask-image: radial-gradient(ellipse at 50% 0%, #000 0%, transparent 70%);
1094 -webkit-mask-image: radial-gradient(ellipse at 50% 0%, #000 0%, transparent 70%);
1095 }
1096 :root[data-theme='light'] body::before { opacity: 0.55; }
1097 :root[data-theme='light'] body::after {
1098 background-image: radial-gradient(rgba(15,16,28,0.06) 1px, transparent 1px);
1099 opacity: 0.4;
fc1817aClaude1100 }
2ce1d0bClaude1101
1102 a { color: var(--text-link); text-decoration: none; transition: color var(--t-fast) var(--ease); }
1103 a:hover { color: var(--accent-hover); text-decoration: none; }
fc1817aClaude1104
958d26aClaude1105 /* Heading scale — confident, editorial, tight tracking */
1106 h1, h2, h3, h4, h5, h6 {
1107 font-family: var(--font-display);
2ce1d0bClaude1108 font-weight: 600;
958d26aClaude1109 letter-spacing: -0.022em;
1110 line-height: 1.18;
1111 color: var(--text-strong);
1112 }
1113 h1 { font-size: var(--t-xl); letter-spacing: -0.028em; }
1114 h2 { font-size: var(--t-lg); letter-spacing: -0.022em; }
1115 h3 { font-size: var(--t-md); font-weight: 600; letter-spacing: -0.015em; }
1116 h4 { font-size: var(--t-base); font-weight: 600; letter-spacing: -0.01em; }
1117 h5, h6 { font-size: var(--t-sm); font-weight: 600; letter-spacing: -0.005em; }
1118
1119 /* Editorial display heading utility — used by landing + marketing pages */
1120 .display {
1121 font-family: var(--font-display);
1122 font-size: clamp(40px, 7.5vw, 96px);
1123 line-height: 0.98;
1124 letter-spacing: -0.04em;
1125 font-weight: 600;
1126 color: var(--text-strong);
2ce1d0bClaude1127 }
fc1817aClaude1128
958d26aClaude1129 /* Eyebrow — uppercase mono label that sits above section headings */
1130 .eyebrow {
1131 display: inline-flex;
1132 align-items: center;
1133 gap: 8px;
1134 font-family: var(--font-mono);
1135 font-size: 11px;
1136 font-weight: 500;
1137 letter-spacing: 0.14em;
1138 text-transform: uppercase;
1139 color: var(--accent);
1140 margin-bottom: var(--s-3);
1141 }
1142 .eyebrow::before {
1143 content: '';
1144 width: 18px;
1145 height: 1px;
1146 background: currentColor;
1147 opacity: 0.6;
1148 }
1149
1150 /* Section header — paired eyebrow + title + lede */
1151 .section-header { max-width: 720px; margin: 0 auto var(--s-10); text-align: center; }
1152 .section-header.left { text-align: left; margin-left: 0; margin-right: auto; }
1153 .section-header h2 {
1154 font-size: clamp(28px, 4vw, 44px);
1155 line-height: 1.05;
1156 letter-spacing: -0.028em;
1157 margin-bottom: var(--s-3);
1158 }
1159 .section-header p {
1160 color: var(--text-muted);
1161 font-size: var(--t-md);
1162 line-height: 1.6;
1163 max-width: 580px;
1164 margin: 0 auto;
1165 }
1166 .section-header.left p { margin-left: 0; }
fc1817aClaude1167
958d26aClaude1168 code, kbd, samp {
2ce1d0bClaude1169 font-family: var(--font-mono);
958d26aClaude1170 font-feature-settings: var(--mono-feat);
1171 }
1172 code {
1173 font-size: 0.9em;
2ce1d0bClaude1174 background: var(--bg-tertiary);
958d26aClaude1175 border: 1px solid var(--border-subtle);
2ce1d0bClaude1176 padding: 1px 6px;
1177 border-radius: var(--r-sm);
1178 color: var(--text);
1179 }
958d26aClaude1180 kbd, .kbd {
1181 display: inline-flex;
1182 align-items: center;
1183 padding: 1px 6px;
1184 font-family: var(--font-mono);
1185 font-size: 11px;
1186 background: var(--bg-elevated);
1187 border: 1px solid var(--border);
1188 border-bottom-width: 2px;
1189 border-radius: 4px;
1190 color: var(--text);
1191 line-height: 1.5;
1192 vertical-align: middle;
1193 }
2ce1d0bClaude1194
958d26aClaude1195 /* Mono utility for technical chrome (paths, IDs, dates) */
1196 .mono { font-family: var(--font-mono); font-feature-settings: var(--mono-feat); font-size: 0.96em; }
1197 .meta-mono { font-family: var(--font-mono); font-size: var(--t-xs); color: var(--text-muted); letter-spacing: 0; }
1198
1199 /* Pre-launch banner — slim, refined, mono caption */
4a52a98Claude1200 .prelaunch-banner {
958d26aClaude1201 position: relative;
2ce1d0bClaude1202 background:
958d26aClaude1203 linear-gradient(180deg, rgba(251,191,36,0.10), rgba(251,191,36,0.03)),
2ce1d0bClaude1204 var(--bg);
958d26aClaude1205 border-bottom: 1px solid rgba(251,191,36,0.28);
4a52a98Claude1206 color: var(--yellow);
958d26aClaude1207 padding: 7px 24px;
1208 font-family: var(--font-mono);
1209 font-size: 11px;
4a52a98Claude1210 font-weight: 500;
1211 text-align: center;
2ce1d0bClaude1212 line-height: 1.5;
958d26aClaude1213 letter-spacing: 0.04em;
1214 text-transform: uppercase;
1215 }
1216 .prelaunch-banner::before {
1217 content: '◆';
1218 margin-right: 8px;
1219 font-size: 9px;
1220 opacity: 0.7;
1221 vertical-align: 1px;
4a52a98Claude1222 }
1223
cd4f63bTest User1224 /* Block Q3 — Playground banner. Brighter than the prelaunch strip so
1225 visitors don't miss the "save your work" CTA, but slim enough to
1226 not feel like a modal. */
1227 .playground-banner {
1228 position: relative;
1229 display: flex;
1230 align-items: center;
1231 justify-content: center;
1232 gap: 8px;
1233 background:
1234 linear-gradient(180deg, rgba(251,191,36,0.20), rgba(251,191,36,0.06)),
1235 var(--bg);
1236 border-bottom: 1px solid rgba(251,191,36,0.45);
1237 color: var(--yellow, #fbbf24);
1238 padding: 8px 40px 8px 24px;
1239 font-size: 13px;
1240 font-weight: 500;
1241 text-align: center;
1242 line-height: 1.4;
1243 }
1244 .playground-banner-icon { font-size: 14px; }
1245 .playground-banner-text { color: var(--text-strong, #e6edf3); }
1246 .playground-banner-countdown { font-weight: 600; }
1247 .playground-banner-cta {
1248 margin-left: 4px;
1249 color: var(--yellow, #fbbf24);
1250 text-decoration: underline;
1251 font-weight: 600;
1252 }
1253 .playground-banner-cta:hover { opacity: 0.85; }
1254 .playground-banner-dismiss {
1255 position: absolute;
1256 top: 50%;
1257 right: 12px;
1258 transform: translateY(-50%);
1259 background: transparent;
1260 border: none;
1261 color: var(--text-muted, #8b949e);
1262 font-size: 18px;
1263 line-height: 1;
1264 cursor: pointer;
1265 padding: 0 4px;
1266 }
1267 .playground-banner-dismiss:hover { color: var(--text-strong, #e6edf3); }
1268
958d26aClaude1269 /* Header — sticky, blurred, hairline border, taller for breathing room */
fc1817aClaude1270 header {
2ce1d0bClaude1271 position: sticky;
1272 top: 0;
1273 z-index: 100;
fc1817aClaude1274 border-bottom: 1px solid var(--border);
2ce1d0bClaude1275 padding: 0 24px;
1276 height: var(--header-h);
958d26aClaude1277 background: rgba(8,9,15,0.72);
1278 backdrop-filter: saturate(180%) blur(18px);
1279 -webkit-backdrop-filter: saturate(180%) blur(18px);
fc1817aClaude1280 }
958d26aClaude1281 :root[data-theme='light'] header { background: rgba(251,251,252,0.78); }
fc1817aClaude1282
06d5ffeClaude1283 header nav {
1284 display: flex;
1285 align-items: center;
958d26aClaude1286 gap: 18px;
1287 max-width: 1240px;
06d5ffeClaude1288 margin: 0 auto;
2ce1d0bClaude1289 height: 100%;
1290 }
1291 .logo {
958d26aClaude1292 font-family: var(--font-display);
1293 font-size: 16px;
2ce1d0bClaude1294 font-weight: 700;
958d26aClaude1295 letter-spacing: -0.025em;
1296 color: var(--text-strong);
2ce1d0bClaude1297 display: inline-flex;
1298 align-items: center;
958d26aClaude1299 gap: 9px;
1300 transition: opacity var(--t-fast) var(--ease);
06d5ffeClaude1301 }
2ce1d0bClaude1302 .logo::before {
1303 content: '';
958d26aClaude1304 width: 20px; height: 20px;
1305 border-radius: 6px;
2ce1d0bClaude1306 background: var(--accent-gradient);
958d26aClaude1307 box-shadow:
1308 inset 0 1px 0 rgba(255,255,255,0.25),
1309 0 0 0 1px rgba(140,109,255,0.45),
1310 0 0 20px rgba(140,109,255,0.30);
2ce1d0bClaude1311 flex-shrink: 0;
958d26aClaude1312 transition: transform var(--t-base) var(--ease-spring), box-shadow var(--t-base) var(--ease);
1313 }
1314 .logo:hover { text-decoration: none; color: var(--text-strong); }
1315 .logo:hover::before {
1316 transform: rotate(8deg) scale(1.05);
1317 box-shadow:
1318 inset 0 1px 0 rgba(255,255,255,0.30),
1319 0 0 0 1px rgba(140,109,255,0.55),
1320 0 0 28px rgba(140,109,255,0.45);
06d5ffeClaude1321 }
fc1817aClaude1322
2ce1d0bClaude1323 .nav-search {
1324 flex: 1;
958d26aClaude1325 max-width: 360px;
1326 margin: 0 4px 0 8px;
1327 position: relative;
2ce1d0bClaude1328 }
1329 .nav-search input {
1330 width: 100%;
958d26aClaude1331 padding: 7px 12px 7px 32px;
2ce1d0bClaude1332 background: var(--bg-tertiary);
1333 border: 1px solid var(--border);
1334 border-radius: var(--r-sm);
1335 color: var(--text);
1336 font-family: var(--font-sans);
1337 font-size: var(--t-sm);
958d26aClaude1338 transition: border-color var(--t-fast) var(--ease), background var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
1339 }
1340 .nav-search::before {
1341 content: '';
1342 position: absolute;
1343 left: 11px; top: 50%;
1344 transform: translateY(-50%);
1345 width: 12px; height: 12px;
1346 border: 1.5px solid var(--text-faint);
1347 border-radius: 50%;
1348 pointer-events: none;
1349 }
1350 .nav-search::after {
1351 content: '';
1352 position: absolute;
1353 left: 19px; top: calc(50% + 4px);
1354 width: 5px; height: 1.5px;
1355 background: var(--text-faint);
1356 transform: rotate(45deg);
1357 pointer-events: none;
2ce1d0bClaude1358 }
1359 .nav-search input::placeholder { color: var(--text-faint); }
1360 .nav-search input:focus {
1361 outline: none;
1362 background: var(--bg-secondary);
958d26aClaude1363 border-color: var(--border-focus);
1364 box-shadow: var(--ring);
2ce1d0bClaude1365 }
06d5ffeClaude1366
958d26aClaude1367 .nav-right { display: flex; align-items: center; gap: 2px; margin-left: auto; }
f764c07Claude1368
1369 /* Block N3 — site-admin deploy status pill */
1370 .nav-deploy-pill {
1371 display: inline-flex;
1372 align-items: center;
1373 gap: 6px;
1374 padding: 4px 10px;
1375 margin: 0 6px 0 0;
1376 border-radius: 9999px;
1377 font-size: 11px;
1378 font-weight: 600;
1379 line-height: 1;
1380 color: var(--text-strong);
1381 background: var(--bg-elevated);
1382 border: 1px solid var(--border);
1383 text-decoration: none;
1384 transition: background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
1385 }
1386 .nav-deploy-pill:hover { background: var(--bg-hover); text-decoration: none; }
1387 .nav-deploy-pill .deploy-pill-dot {
1388 display: inline-block;
1389 width: 8px; height: 8px;
1390 border-radius: 50%;
1391 background: #6b7280;
1392 }
1393 .deploy-pill-success .deploy-pill-dot { background: #34d399; box-shadow: 0 0 6px rgba(52,211,153,0.45); }
1394 .deploy-pill-failed .deploy-pill-dot { background: #f87171; box-shadow: 0 0 6px rgba(248,113,113,0.45); }
1395 .deploy-pill-failed { border-color: rgba(248,113,113,0.4); }
1396 .deploy-pill-progress .deploy-pill-dot {
1397 background: #fbbf24;
1398 box-shadow: 0 0 6px rgba(251,191,36,0.55);
1399 animation: deployPillPulse 1.2s ease-in-out infinite;
1400 }
1401 @keyframes deployPillPulse {
1402 0%, 100% { opacity: 1; transform: scale(1); }
1403 50% { opacity: 0.45; transform: scale(0.8); }
1404 }
1405 .deploy-pill-empty .deploy-pill-dot { background: #9ca3af; }
1406
2ce1d0bClaude1407 .nav-link {
958d26aClaude1408 position: relative;
2ce1d0bClaude1409 color: var(--text-muted);
1410 font-size: var(--t-sm);
1411 font-weight: 500;
958d26aClaude1412 padding: 7px 11px;
2ce1d0bClaude1413 border-radius: var(--r-sm);
958d26aClaude1414 transition: color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
2ce1d0bClaude1415 }
1416 .nav-link:hover {
958d26aClaude1417 color: var(--text-strong);
2ce1d0bClaude1418 background: var(--bg-hover);
1419 text-decoration: none;
1420 }
958d26aClaude1421 .nav-link.active { color: var(--text-strong); }
1422 .nav-link.active::after {
1423 content: '';
1424 position: absolute;
1425 left: 11px; right: 11px;
1426 bottom: -1px;
1427 height: 2px;
1428 background: var(--accent-gradient);
1429 border-radius: 2px;
1430 }
2ce1d0bClaude1431 .nav-user {
958d26aClaude1432 color: var(--text-strong);
2ce1d0bClaude1433 font-weight: 600;
1434 font-size: var(--t-sm);
1435 padding: 6px 10px;
1436 border-radius: var(--r-sm);
958d26aClaude1437 margin-left: 6px;
2ce1d0bClaude1438 transition: background var(--t-fast) var(--ease);
1439 }
958d26aClaude1440 .nav-user::before {
1441 content: '';
1442 display: inline-block;
1443 width: 8px; height: 8px;
1444 background: var(--green);
1445 border-radius: 50%;
1446 margin-right: 7px;
1447 box-shadow: 0 0 8px rgba(52,211,153,0.5);
1448 vertical-align: 1px;
1449 }
2ce1d0bClaude1450 .nav-user:hover { background: var(--bg-hover); text-decoration: none; }
fc1817aClaude1451
2ce1d0bClaude1452 main {
958d26aClaude1453 max-width: 1240px;
2ce1d0bClaude1454 margin: 0 auto;
958d26aClaude1455 padding: 36px 24px 80px;
2ce1d0bClaude1456 flex: 1;
1457 width: 100%;
ed6e438Claude1458 /* 2026 polish — subtle entrance animation on every page load.
1459 Content fades up 4px over 360ms, giving the site that "alive"
1460 quality that 2026 SaaS expects. Honors prefers-reduced-motion. */
1461 animation: gxMainEnter 360ms cubic-bezier(0.22, 1, 0.36, 1) both;
1462 }
1463 @keyframes gxMainEnter {
1464 from { opacity: 0; transform: translateY(4px); }
1465 to { opacity: 1; transform: translateY(0); }
1466 }
1467 @media (prefers-reduced-motion: reduce) {
1468 main { animation: none; }
1469 }
1470 /* 2026 polish — accent focus ring across all focusable elements.
1471 The default browser ring is utilitarian; this is the same width
1472 but tinted to the brand accent so keyboard navigation feels
1473 intentional, not jarring. :focus-visible only — mouse users
1474 never see it. */
1475 :focus-visible {
1476 outline: none;
1477 }
1478 a:focus-visible,
1479 button:focus-visible,
1480 input:focus-visible,
1481 select:focus-visible,
1482 textarea:focus-visible,
1483 [tabindex]:focus-visible {
1484 outline: 2px solid rgba(140, 109, 255, 0.55);
1485 outline-offset: 2px;
1486 border-radius: 4px;
2ce1d0bClaude1487 }
fc1817aClaude1488
958d26aClaude1489 /* Editorial footer: link grid + tagline row */
fc1817aClaude1490 footer {
1491 border-top: 1px solid var(--border);
958d26aClaude1492 padding: 56px 24px 40px;
fc1817aClaude1493 color: var(--text-muted);
958d26aClaude1494 font-size: var(--t-sm);
1495 background:
1496 linear-gradient(180deg, transparent 0%, rgba(140,109,255,0.025) 100%),
1497 var(--bg);
1498 }
1499 footer .footer-inner {
1500 max-width: 1240px;
1501 margin: 0 auto;
1502 display: grid;
1503 grid-template-columns: 1fr auto;
1504 gap: 48px;
1505 align-items: start;
1506 }
1507 footer .footer-brand .logo { margin-bottom: var(--s-3); }
1508 footer .footer-tag {
1509 color: var(--text-muted);
1510 font-size: var(--t-sm);
1511 max-width: 320px;
1512 line-height: 1.55;
1513 }
1514 footer .footer-links {
1515 display: grid;
1516 grid-template-columns: repeat(3, minmax(120px, auto));
1517 gap: 0 56px;
1518 }
1519 footer .footer-col-title {
1520 font-family: var(--font-mono);
1521 font-size: 11px;
1522 text-transform: uppercase;
1523 letter-spacing: 0.14em;
1524 color: var(--text-faint);
1525 margin-bottom: var(--s-3);
1526 }
1527 footer .footer-col a {
1528 display: block;
1529 color: var(--text-muted);
1530 font-size: var(--t-sm);
1531 padding: 5px 0;
1532 transition: color var(--t-fast) var(--ease);
1533 }
1534 footer .footer-col a:hover { color: var(--text-strong); text-decoration: none; }
1535 footer .footer-bottom {
1536 max-width: 1240px;
1537 margin: 40px auto 0;
1538 padding-top: 24px;
1539 border-top: 1px solid var(--border-subtle);
1540 display: flex;
1541 justify-content: space-between;
1542 align-items: center;
2ce1d0bClaude1543 color: var(--text-faint);
1544 font-size: var(--t-xs);
958d26aClaude1545 font-family: var(--font-mono);
1546 letter-spacing: 0.02em;
1547 }
1548 footer .footer-bottom a { color: var(--text-faint); }
1549 footer .footer-bottom a:hover { color: var(--text-muted); text-decoration: none; }
05cdb85Claude1550 footer .footer-build {
1551 display: inline-flex;
1552 align-items: center;
1553 gap: 6px;
1554 color: var(--text-faint);
1555 font-family: var(--font-mono);
1556 font-size: 11px;
1557 cursor: help;
1558 }
1559 footer .footer-build-dot {
1560 width: 6px; height: 6px;
1561 border-radius: 50%;
1562 background: var(--green);
1563 box-shadow: 0 0 6px rgba(52,211,153,0.55);
1564 animation: footer-build-pulse 2.4s ease-in-out infinite;
1565 }
1566 @keyframes footer-build-pulse {
1567 0%, 100% { opacity: 0.6; }
1568 50% { opacity: 1; }
1569 }
958d26aClaude1570 @media (max-width: 768px) {
1571 footer .footer-inner { grid-template-columns: 1fr; gap: 32px; }
1572 footer .footer-links { grid-template-columns: repeat(2, 1fr); gap: 24px 32px; }
1573 footer .footer-bottom { flex-direction: column; gap: 8px; text-align: center; }
fc1817aClaude1574 }
1575
2ce1d0bClaude1576 /* ============================================================ */
1577 /* Buttons */
1578 /* ============================================================ */
dc26881CC LABS App1579 /* ============================================================ */
1580 /* Buttons — Block U2 senior polish pass. */
1581 /* Rules: */
1582 /* · hover lifts every .btn by 1px + soft drop shadow (180ms) */
1583 /* · active presses back down to 0 (80ms — faster on press) */
1584 /* · focus-visible uses a soft box-shadow ring (no outline) */
1585 /* · primary gradient shifts position on hover for a slow */
1586 /* 600ms shimmer */
1587 /* · disabled never lifts and never animates */
1588 /* ============================================================ */
06d5ffeClaude1589 .btn {
1590 display: inline-flex;
1591 align-items: center;
2ce1d0bClaude1592 justify-content: center;
958d26aClaude1593 gap: 7px;
2ce1d0bClaude1594 padding: 7px 14px;
1595 border-radius: var(--r-sm);
958d26aClaude1596 font-family: var(--font-sans);
2ce1d0bClaude1597 font-size: var(--t-sm);
06d5ffeClaude1598 font-weight: 500;
1599 border: 1px solid var(--border);
2ce1d0bClaude1600 background: var(--bg-elevated);
06d5ffeClaude1601 color: var(--text);
1602 cursor: pointer;
1603 text-decoration: none;
958d26aClaude1604 line-height: 1.25;
1605 letter-spacing: -0.008em;
dc26881CC LABS App1606 /* U2 — hover/transform settles in 180ms; press snaps in 80ms.
1607 Listed once on the base rule so :active can override only the
1608 transform/duration without re-typing the colour/bg transitions. */
2ce1d0bClaude1609 transition:
dc26881CC LABS App1610 background 180ms ease,
1611 border-color 180ms ease,
1612 transform 180ms ease,
1613 box-shadow 180ms ease,
1614 color 180ms ease;
2ce1d0bClaude1615 user-select: none;
1616 white-space: nowrap;
958d26aClaude1617 position: relative;
06d5ffeClaude1618 }
2ce1d0bClaude1619 .btn:hover {
1620 background: var(--bg-surface);
1621 border-color: var(--border-strong);
958d26aClaude1622 color: var(--text-strong);
2ce1d0bClaude1623 text-decoration: none;
dc26881CC LABS App1624 /* U2 — universal hover lift + soft accent drop shadow. */
1625 transform: translateY(-1px);
1626 box-shadow: 0 4px 12px -4px rgba(15, 17, 26, 0.45);
1627 }
1628 .btn:active {
1629 transform: translateY(0);
1630 transition-duration: 80ms;
1631 }
1632 /* U2 — soft modern focus ring via box-shadow, not outline. */
1633 .btn:focus-visible {
1634 outline: none;
1635 box-shadow: 0 0 0 3px rgba(140, 109, 255, 0.35);
2ce1d0bClaude1636 }
1637
1638 .btn-primary {
1639 background: var(--accent-gradient);
dc26881CC LABS App1640 background-size: 200% 100%;
1641 background-position: 0% 50%;
2ce1d0bClaude1642 border-color: transparent;
1643 color: #fff;
1644 font-weight: 600;
958d26aClaude1645 text-shadow: 0 1px 0 rgba(0,0,0,0.15);
2ce1d0bClaude1646 box-shadow:
958d26aClaude1647 inset 0 1px 0 rgba(255,255,255,0.22),
1648 inset 0 -1px 0 rgba(0,0,0,0.10),
1649 0 1px 2px rgba(0,0,0,0.40),
1650 0 0 0 1px rgba(140,109,255,0.30);
dc26881CC LABS App1651 /* U2 — slower 600ms transition on background-position so the
1652 primary CTA shimmers when the cursor lands. */
1653 transition:
1654 background-position 600ms ease,
1655 transform 180ms ease,
1656 box-shadow 180ms ease,
1657 color 180ms ease;
06d5ffeClaude1658 }
958d26aClaude1659 .btn-primary::before {
1660 content: '';
1661 position: absolute;
1662 inset: 0;
1663 border-radius: inherit;
1664 background: linear-gradient(180deg, rgba(255,255,255,0.18), transparent 60%);
1665 opacity: 0;
1666 transition: opacity var(--t-fast) var(--ease);
1667 pointer-events: none;
2ce1d0bClaude1668 }
1669 .btn-primary:hover {
958d26aClaude1670 color: #fff;
2ce1d0bClaude1671 background: var(--accent-gradient);
dc26881CC LABS App1672 background-size: 200% 100%;
1673 background-position: 100% 50%;
958d26aClaude1674 border-color: transparent;
dc26881CC LABS App1675 transform: translateY(-1px);
2ce1d0bClaude1676 box-shadow:
958d26aClaude1677 inset 0 1px 0 rgba(255,255,255,0.30),
1678 inset 0 -1px 0 rgba(0,0,0,0.10),
1679 0 6px 18px -4px rgba(140,109,255,0.45),
1680 0 0 0 1px rgba(140,109,255,0.45);
2ce1d0bClaude1681 }
958d26aClaude1682 .btn-primary:hover::before { opacity: 1; }
dc26881CC LABS App1683 .btn-primary:active { transform: translateY(0); transition-duration: 80ms; }
1684 /* U2 — primary focus ring uses the same accent box-shadow recipe. */
1685 .btn-primary:focus-visible {
1686 box-shadow:
1687 inset 0 1px 0 rgba(255,255,255,0.22),
1688 0 0 0 3px rgba(140,109,255,0.35);
1689 }
2ce1d0bClaude1690
1691 .btn-danger {
1692 background: transparent;
958d26aClaude1693 border-color: rgba(248,113,113,0.40);
2ce1d0bClaude1694 color: var(--red);
1695 }
1696 .btn-danger:hover {
958d26aClaude1697 background: rgba(248,113,113,0.08);
2ce1d0bClaude1698 border-color: var(--red);
958d26aClaude1699 color: var(--red);
dc26881CC LABS App1700 transform: translateY(-1px);
1701 box-shadow: 0 4px 12px -4px rgba(248,113,113,0.30);
2ce1d0bClaude1702 }
dc26881CC LABS App1703 .btn-danger:focus-visible { box-shadow: 0 0 0 3px rgba(248,113,113,0.35); }
2ce1d0bClaude1704
1705 .btn-ghost {
1706 background: transparent;
1707 border-color: transparent;
1708 color: var(--text-muted);
1709 }
1710 .btn-ghost:hover {
1711 background: var(--bg-hover);
958d26aClaude1712 color: var(--text-strong);
2ce1d0bClaude1713 border-color: var(--border);
dc26881CC LABS App1714 transform: translateY(-1px);
1715 box-shadow: 0 4px 12px -4px rgba(15, 17, 26, 0.30);
2ce1d0bClaude1716 }
1717
958d26aClaude1718 .btn-secondary {
1719 background: var(--bg-elevated);
1720 border-color: var(--border-strong);
1721 color: var(--text-strong);
1722 }
1723 .btn-secondary:hover {
1724 background: var(--bg-surface);
1725 border-color: var(--border-strong);
dc26881CC LABS App1726 transform: translateY(-1px);
1727 box-shadow: 0 4px 12px -4px rgba(15, 17, 26, 0.35);
958d26aClaude1728 }
1729
1730 .btn-sm { padding: 4px 10px; font-size: var(--t-xs); border-radius: var(--r-sm); gap: 5px; }
1731 .btn-lg { padding: 11px 22px; font-size: var(--t-base); border-radius: var(--r); }
1732 .btn-xl { padding: 14px 28px; font-size: var(--t-md); border-radius: var(--r); font-weight: 600; }
1733 .btn-block { width: 100%; }
2ce1d0bClaude1734
dc26881CC LABS App1735 /* U2 — disabled never lifts, never shimmers, never glows. */
1736 .btn:disabled,
1737 .btn[aria-disabled='true'],
1738 .btn:disabled:hover,
1739 .btn[aria-disabled='true']:hover {
1740 opacity: 0.5;
2ce1d0bClaude1741 cursor: not-allowed;
1742 pointer-events: none;
dc26881CC LABS App1743 transform: none;
1744 box-shadow: none;
1745 }
1746 @media (prefers-reduced-motion: reduce) {
1747 .btn,
1748 .btn:hover,
1749 .btn:active,
1750 .btn-primary,
1751 .btn-primary:hover {
1752 transform: none;
1753 transition: background-color 80ms linear, color 80ms linear;
1754 }
2ce1d0bClaude1755 }
1756
1757 /* ============================================================ */
1758 /* Forms */
1759 /* ============================================================ */
1760 .form-group { margin-bottom: 20px; }
1761 .form-group label {
1762 display: block;
1763 font-size: var(--t-sm);
1764 font-weight: 500;
1765 margin-bottom: 6px;
1766 color: var(--text);
1767 letter-spacing: -0.005em;
1768 }
1769 .form-group input,
1770 .form-group textarea,
1771 .form-group select,
1772 input[type='text'], input[type='email'], input[type='password'],
1773 input[type='url'], input[type='search'], input[type='number'],
1774 textarea, select {
06d5ffeClaude1775 width: 100%;
2ce1d0bClaude1776 padding: 9px 12px;
1777 background: var(--bg-secondary);
06d5ffeClaude1778 border: 1px solid var(--border);
2ce1d0bClaude1779 border-radius: var(--r-sm);
06d5ffeClaude1780 color: var(--text);
2ce1d0bClaude1781 font-size: var(--t-sm);
06d5ffeClaude1782 font-family: var(--font-sans);
2ce1d0bClaude1783 transition:
1784 border-color var(--t-fast) var(--ease),
1785 background var(--t-fast) var(--ease),
1786 box-shadow var(--t-fast) var(--ease);
06d5ffeClaude1787 }
2ce1d0bClaude1788 .form-group input::placeholder, textarea::placeholder, input::placeholder {
1789 color: var(--text-faint);
06d5ffeClaude1790 }
2ce1d0bClaude1791 .form-group input:hover, textarea:hover, select:hover,
1792 input[type='text']:hover, input[type='email']:hover, input[type='password']:hover {
1793 border-color: var(--border-strong);
1794 }
1795 .form-group input:focus, .form-group textarea:focus, .form-group select:focus,
1796 input:focus, textarea:focus, select:focus {
06d5ffeClaude1797 outline: none;
2ce1d0bClaude1798 background: var(--bg);
1799 border-color: var(--border-focus);
1800 box-shadow: var(--ring);
06d5ffeClaude1801 }
2ce1d0bClaude1802 textarea { font-family: var(--font-mono); font-size: var(--t-sm); line-height: 1.55; }
06d5ffeClaude1803 .input-disabled { opacity: 0.5; cursor: not-allowed; }
1804
2ce1d0bClaude1805 /* ============================================================ */
1806 /* Auth (register / login / verify) */
1807 /* ============================================================ */
1808 .auth-container {
98f45b4Claude1809 /* 2026 polish — wider, more generous, with a subtle accent-glow
1810 border and gradient top-edge so it reads as a premium product
1811 gateway, not a stock form. The 480px width feels intentional
1812 (not cramped, not endless). */
1813 max-width: 480px;
1814 margin: 72px auto;
1815 padding: 40px 40px 36px;
2ce1d0bClaude1816 background: var(--bg-elevated);
1817 border: 1px solid var(--border);
98f45b4Claude1818 border-radius: 16px;
1819 box-shadow:
1820 var(--elev-2),
1821 0 24px 64px -16px rgba(140, 109, 255, 0.12);
1822 position: relative;
1823 overflow: hidden;
1824 }
1825 .auth-container::before {
1826 /* Hairline gradient accent on the top edge — signals 'AI-native'
1827 without shouting. Pointer-events disabled so it never interferes
1828 with form interactions. */
1829 content: '';
1830 position: absolute;
1831 top: 0;
1832 left: 0;
1833 right: 0;
1834 height: 2px;
1835 background: linear-gradient(90deg, transparent 0%, #8c6dff 30%, #36c5d6 70%, transparent 100%);
1836 opacity: 0.7;
1837 pointer-events: none;
2ce1d0bClaude1838 }
1839 .auth-container h2 {
98f45b4Claude1840 margin: 0 0 8px;
1841 font-size: 28px;
1842 font-weight: 700;
1843 font-family: var(--font-display);
1844 letter-spacing: -0.025em;
1845 color: var(--text-strong);
1846 line-height: 1.15;
1847 }
1848 .auth-container .auth-subtitle {
1849 color: var(--text-muted);
1850 font-size: 14.5px;
1851 line-height: 1.5;
1852 margin: 0 0 24px;
2ce1d0bClaude1853 }
1854 .auth-container > p {
1855 color: var(--text-muted);
1856 font-size: var(--t-sm);
1857 margin-bottom: 24px;
1858 }
98f45b4Claude1859 .auth-container .btn-primary {
1860 width: 100%;
1861 padding: 12px 16px;
1862 font-size: 15px;
1863 font-weight: 600;
1864 margin-top: 4px;
1865 }
582cdacClaude1866
1867 /* OAuth provider buttons (Google / GitHub / SSO). Single-line layout
1868 with a brand-coloured logo on the left and a label centred-trailing.
1869 Hover lifts 1px with a subtle shadow — matches the rest of the
1870 button system but reads as a brand-affiliated action, not a brand-
1871 coloured primary CTA. */
1872 .auth-container .oauth-btn {
1873 display: flex;
1874 align-items: center;
1875 justify-content: center;
1876 gap: 10px;
1877 width: 100%;
1878 padding: 11px 16px;
1879 background: var(--bg-surface, var(--bg-elevated));
1880 border: 1px solid var(--border);
1881 border-radius: var(--r-md, 8px);
1882 color: var(--text-strong);
1883 font-family: var(--font-sans);
1884 font-size: 14.5px;
1885 font-weight: 500;
1886 text-decoration: none;
1887 transition:
1888 transform var(--t-base, 180ms) var(--ease, ease),
1889 box-shadow var(--t-base, 180ms) var(--ease, ease),
1890 background var(--t-fast, 120ms) var(--ease, ease),
1891 border-color var(--t-fast, 120ms) var(--ease, ease);
1892 }
1893 .auth-container .oauth-btn:hover {
1894 transform: translateY(-1px);
1895 background: var(--bg-hover);
1896 border-color: var(--text-muted);
1897 color: var(--text-strong);
1898 box-shadow: 0 4px 14px -4px rgba(0,0,0,0.25);
1899 text-decoration: none;
1900 }
1901 .auth-container .oauth-btn:focus-visible {
1902 outline: 2px solid rgba(140, 109, 255, 0.55);
1903 outline-offset: 2px;
1904 }
1905 .auth-container .oauth-btn .oauth-icon {
1906 flex-shrink: 0;
1907 }
1908 /* GitHub icon adopts the text colour so it reads in both themes. */
1909 .auth-container .oauth-github .oauth-icon {
1910 color: var(--text-strong);
1911 }
1912 /* Google logo uses the official 4-colour treatment via inline SVG fill
1913 attributes — nothing to override here. */
1914 /* "or" divider between the password form and provider buttons */
1915 .auth-container .auth-divider {
1916 display: flex;
1917 align-items: center;
1918 gap: 12px;
1919 margin: 20px 0 12px;
1920 color: var(--text-faint);
1921 font-size: 12px;
1922 letter-spacing: 0.08em;
1923 text-transform: uppercase;
1924 }
1925 .auth-container .auth-divider::before,
1926 .auth-container .auth-divider::after {
1927 content: '';
1928 flex: 1;
1929 height: 1px;
1930 background: var(--border);
1931 }
06d5ffeClaude1932 .auth-error {
958d26aClaude1933 background: rgba(248,113,113,0.08);
1934 border: 1px solid rgba(248,113,113,0.35);
06d5ffeClaude1935 color: var(--red);
2ce1d0bClaude1936 padding: 10px 14px;
1937 border-radius: var(--r-sm);
06d5ffeClaude1938 margin-bottom: 16px;
2ce1d0bClaude1939 font-size: var(--t-sm);
06d5ffeClaude1940 }
1941 .auth-success {
958d26aClaude1942 background: rgba(52,211,153,0.08);
1943 border: 1px solid rgba(52,211,153,0.35);
06d5ffeClaude1944 color: var(--green);
2ce1d0bClaude1945 padding: 10px 14px;
1946 border-radius: var(--r-sm);
06d5ffeClaude1947 margin-bottom: 16px;
2ce1d0bClaude1948 font-size: var(--t-sm);
1949 }
1950 .auth-switch {
1951 margin-top: 20px;
1952 font-size: var(--t-sm);
1953 color: var(--text-muted);
1954 text-align: center;
1955 }
1956 .banner {
1957 background: var(--accent-gradient-faint);
958d26aClaude1958 border: 1px solid rgba(140,109,255,0.35);
2ce1d0bClaude1959 color: var(--text);
1960 padding: 10px 14px;
1961 border-radius: var(--r-sm);
1962 font-size: var(--t-sm);
06d5ffeClaude1963 }
1964
2ce1d0bClaude1965 /* ============================================================ */
1966 /* Settings */
1967 /* ============================================================ */
1968 .settings-container { max-width: 720px; }
1969 .settings-container h2 { margin-bottom: 8px; font-size: var(--t-xl); letter-spacing: -0.02em; }
1970 .settings-container > h2 + p { color: var(--text-muted); font-size: var(--t-sm); margin-bottom: 24px; }
1971 .settings-container h3 { font-size: var(--t-md); margin-bottom: 12px; margin-top: 28px; }
1972 .settings-container h3:first-of-type { margin-top: 0; }
06d5ffeClaude1973 .ssh-keys-list { margin-bottom: 24px; }
1974 .ssh-key-item {
1975 display: flex;
1976 justify-content: space-between;
1977 align-items: center;
2ce1d0bClaude1978 padding: 14px 16px;
06d5ffeClaude1979 border: 1px solid var(--border);
2ce1d0bClaude1980 border-radius: var(--r-md);
06d5ffeClaude1981 margin-bottom: 8px;
2ce1d0bClaude1982 background: var(--bg-elevated);
1983 transition: border-color var(--t-fast) var(--ease);
06d5ffeClaude1984 }
2ce1d0bClaude1985 .ssh-key-item:hover { border-color: var(--border-strong); }
1986 .ssh-key-meta { font-size: var(--t-xs); color: var(--text-muted); margin-top: 4px; }
1987 .ssh-key-meta code { font-size: var(--t-xs); background: var(--bg-tertiary); padding: 1px 6px; border-radius: 3px; margin-right: 8px; }
06d5ffeClaude1988
2ce1d0bClaude1989 /* ============================================================ */
1990 /* Repo header + nav */
1991 /* ============================================================ */
fc1817aClaude1992 .repo-header {
1993 display: flex;
1994 align-items: center;
debcf27Claude1995 gap: 12px;
1996 margin-bottom: 22px;
1997 }
1998 .repo-header-title {
1999 display: flex;
2000 align-items: center;
2001 gap: 10px;
2002 font-family: var(--font-display);
2003 font-size: 24px;
2004 letter-spacing: -0.025em;
2005 flex-wrap: wrap;
2006 }
2007 .repo-header .owner {
2008 color: var(--text-muted);
2009 font-weight: 500;
2010 transition: color var(--t-fast) var(--ease);
2011 }
2012 .repo-header .owner:hover { color: var(--text-link); text-decoration: none; }
2013 .repo-header .separator { color: var(--text-faint); font-weight: 300; }
2014 .repo-header .name {
2015 color: var(--text-strong);
2016 font-weight: 700;
2017 letter-spacing: -0.028em;
fc1817aClaude2018 }
2ce1d0bClaude2019 .repo-header .name:hover { color: var(--text-link); text-decoration: none; }
debcf27Claude2020 .repo-header-fork {
2021 font-family: var(--font-mono);
2022 font-size: 11px;
2023 color: var(--text-muted);
2024 margin-top: 4px;
2025 letter-spacing: 0.01em;
2026 }
2027 .repo-header-fork a { color: var(--text-muted); }
2028 .repo-header-fork a:hover { color: var(--accent); }
2029 .repo-header-pill {
2030 display: inline-flex;
2031 align-items: center;
2032 padding: 2px 10px;
2033 border-radius: var(--r-full);
2034 font-family: var(--font-mono);
2035 font-size: 10px;
2036 font-weight: 600;
2037 letter-spacing: 0.12em;
2038 text-transform: uppercase;
2039 line-height: 1.6;
2040 vertical-align: 4px;
2041 }
2042 .repo-header-pill-archived {
2043 background: rgba(251,191,36,0.10);
2044 color: var(--yellow);
2045 border: 1px solid rgba(251,191,36,0.30);
2046 }
2047 .repo-header-pill-template {
2048 background: var(--accent-gradient-faint);
2049 color: var(--accent);
2050 border: 1px solid rgba(140,109,255,0.30);
fc1817aClaude2051 }
06d5ffeClaude2052 .repo-header-actions { margin-left: auto; display: flex; gap: 8px; align-items: center; }
fc1817aClaude2053
2054 .repo-nav {
2055 display: flex;
debcf27Claude2056 gap: 1px;
fc1817aClaude2057 border-bottom: 1px solid var(--border);
debcf27Claude2058 margin-bottom: 28px;
2ce1d0bClaude2059 overflow-x: auto;
2060 scrollbar-width: thin;
fc1817aClaude2061 }
2ce1d0bClaude2062 .repo-nav::-webkit-scrollbar { height: 0; }
fc1817aClaude2063 .repo-nav a {
debcf27Claude2064 position: relative;
2065 padding: 11px 14px;
fc1817aClaude2066 color: var(--text-muted);
2067 border-bottom: 2px solid transparent;
2ce1d0bClaude2068 font-size: var(--t-sm);
2069 font-weight: 500;
2070 margin-bottom: -1px;
debcf27Claude2071 transition: color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
2ce1d0bClaude2072 white-space: nowrap;
2073 }
debcf27Claude2074 .repo-nav a:hover { text-decoration: none; color: var(--text-strong); background: var(--bg-hover); }
2ce1d0bClaude2075 .repo-nav a.active {
debcf27Claude2076 color: var(--text-strong);
2077 font-weight: 600;
2078 }
2079 .repo-nav a.active::after {
2080 content: '';
2081 position: absolute;
2082 left: 14px;
2083 right: 14px;
2084 bottom: -1px;
2085 height: 2px;
2086 background: var(--accent-gradient);
2087 border-radius: 2px;
2088 }
2089 /* AI links in the right-side cluster — sparkle accent */
2090 .repo-nav-ai {
2091 color: var(--accent) !important;
2092 font-weight: 500;
2093 }
2094 .repo-nav-ai:hover {
2095 color: var(--accent-hover) !important;
2096 background: var(--accent-gradient-faint) !important;
2097 }
2098 .repo-nav-ai.active {
2099 color: var(--accent-hover) !important;
2ce1d0bClaude2100 font-weight: 600;
fc1817aClaude2101 }
2102
2ce1d0bClaude2103 .breadcrumb {
2104 display: flex;
debcf27Claude2105 gap: 6px;
2ce1d0bClaude2106 align-items: center;
debcf27Claude2107 margin-bottom: 18px;
2ce1d0bClaude2108 color: var(--text-muted);
2109 font-size: var(--t-sm);
2110 font-family: var(--font-mono);
debcf27Claude2111 font-feature-settings: var(--mono-feat);
2ce1d0bClaude2112 }
2113 .breadcrumb a { color: var(--text-link); font-weight: 500; }
2114 .breadcrumb a:hover { color: var(--accent-hover); }
debcf27Claude2115 .breadcrumb strong {
2116 color: var(--text-strong);
2117 font-weight: 600;
fc1817aClaude2118 }
2119
debcf27Claude2120 /* Page header — eyebrow + title + optional actions row.
2121 Use on dashboard, settings, admin, any "section landing" page. */
2122 .page-header {
2123 display: flex;
2124 align-items: flex-end;
2125 justify-content: space-between;
2126 gap: 16px;
2127 margin-bottom: 28px;
2128 padding-bottom: 20px;
2129 border-bottom: 1px solid var(--border-subtle);
2130 flex-wrap: wrap;
2131 }
2132 .page-header-text { flex: 1; min-width: 280px; }
2133 .page-header .eyebrow { margin-bottom: var(--s-2); }
2134 .page-header h1 {
2135 font-family: var(--font-display);
2136 font-size: clamp(24px, 3vw, 36px);
2137 line-height: 1.1;
2138 letter-spacing: -0.028em;
2139 margin-bottom: 6px;
2140 }
2141 .page-header p {
2142 color: var(--text-muted);
2143 font-size: var(--t-sm);
2144 line-height: 1.55;
2145 max-width: 640px;
2146 }
2147 .page-header-actions { display: flex; gap: 8px; align-items: center; }
fc1817aClaude2148
2ce1d0bClaude2149 /* ============================================================ */
2150 /* File browser table */
2151 /* ============================================================ */
2152 .file-table {
2153 width: 100%;
2154 border: 1px solid var(--border);
2155 border-radius: var(--r-md);
2156 overflow: hidden;
2157 background: var(--bg-elevated);
debcf27Claude2158 border-collapse: collapse;
2ce1d0bClaude2159 }
debcf27Claude2160 .file-table tr { border-bottom: 1px solid var(--border-subtle); transition: background var(--t-fast) var(--ease); }
fc1817aClaude2161 .file-table tr:last-child { border-bottom: none; }
debcf27Claude2162 .file-table td {
2163 padding: 9px 16px;
2164 font-size: var(--t-sm);
2165 font-family: var(--font-mono);
2166 font-feature-settings: var(--mono-feat);
2167 }
2ce1d0bClaude2168 .file-table tr:hover { background: var(--bg-hover); }
debcf27Claude2169 .file-icon {
2170 width: 22px;
2171 color: var(--text-faint);
2172 font-size: 13px;
2173 text-align: center;
2174 }
2175 .file-name a {
2176 color: var(--text);
2177 font-weight: 500;
2178 transition: color var(--t-fast) var(--ease);
2179 }
2180 .file-name a:hover { color: var(--accent); text-decoration: none; }
fc1817aClaude2181
2ce1d0bClaude2182 /* ============================================================ */
2183 /* Blob view */
2184 /* ============================================================ */
fc1817aClaude2185 .blob-view {
2186 border: 1px solid var(--border);
2ce1d0bClaude2187 border-radius: var(--r-md);
fc1817aClaude2188 overflow: hidden;
2ce1d0bClaude2189 background: var(--bg-elevated);
fc1817aClaude2190 }
2191 .blob-header {
2192 background: var(--bg-secondary);
2ce1d0bClaude2193 padding: 10px 16px;
fc1817aClaude2194 border-bottom: 1px solid var(--border);
2ce1d0bClaude2195 font-size: var(--t-sm);
fc1817aClaude2196 color: var(--text-muted);
06d5ffeClaude2197 display: flex;
2198 justify-content: space-between;
2199 align-items: center;
fc1817aClaude2200 }
2201 .blob-code {
2202 overflow-x: auto;
2203 font-family: var(--font-mono);
2ce1d0bClaude2204 font-size: var(--t-sm);
2205 line-height: 1.65;
fc1817aClaude2206 }
2207 .blob-code table { width: 100%; border-collapse: collapse; }
2208 .blob-code .line-num {
2209 width: 1%;
2ce1d0bClaude2210 min-width: 56px;
2211 padding: 0 14px;
fc1817aClaude2212 text-align: right;
2ce1d0bClaude2213 color: var(--text-faint);
fc1817aClaude2214 user-select: none;
2215 white-space: nowrap;
2216 border-right: 1px solid var(--border);
2217 }
2ce1d0bClaude2218 .blob-code .line-content { padding: 0 16px; white-space: pre; }
2219 .blob-code tr:hover { background: var(--bg-hover); }
fc1817aClaude2220
2ce1d0bClaude2221 /* ============================================================ */
2222 /* Commits + diffs */
2223 /* ============================================================ */
2224 .commit-list {
2225 border: 1px solid var(--border);
2226 border-radius: var(--r-md);
2227 overflow: hidden;
2228 background: var(--bg-elevated);
2229 }
fc1817aClaude2230 .commit-item {
2231 display: flex;
2232 justify-content: space-between;
2233 align-items: center;
debcf27Claude2234 padding: 14px 18px;
2235 border-bottom: 1px solid var(--border-subtle);
2ce1d0bClaude2236 transition: background var(--t-fast) var(--ease);
fc1817aClaude2237 }
2238 .commit-item:last-child { border-bottom: none; }
2ce1d0bClaude2239 .commit-item:hover { background: var(--bg-hover); }
debcf27Claude2240 .commit-message {
2241 font-size: var(--t-sm);
2242 font-weight: 500;
2243 line-height: 1.45;
2244 color: var(--text-strong);
2245 letter-spacing: -0.005em;
2246 }
2247 .commit-meta {
2248 font-family: var(--font-mono);
2249 font-size: 11px;
2250 color: var(--text-muted);
2251 margin-top: 5px;
2252 letter-spacing: 0.01em;
2253 }
fc1817aClaude2254 .commit-sha {
2255 font-family: var(--font-mono);
debcf27Claude2256 font-feature-settings: var(--mono-feat);
2257 font-size: 11px;
2258 padding: 4px 9px;
fc1817aClaude2259 background: var(--bg-tertiary);
2260 border: 1px solid var(--border);
2ce1d0bClaude2261 border-radius: var(--r-sm);
debcf27Claude2262 color: var(--accent);
2263 font-weight: 600;
2264 letter-spacing: 0.02em;
2ce1d0bClaude2265 transition: all var(--t-fast) var(--ease);
fc1817aClaude2266 }
debcf27Claude2267 .commit-sha:hover {
2268 border-color: rgba(140,109,255,0.40);
2269 background: var(--accent-gradient-faint);
2270 color: var(--accent-hover);
2271 text-decoration: none;
fc1817aClaude2272 }
2273
2274 .diff-view { margin-top: 16px; }
2275 .diff-file {
2276 border: 1px solid var(--border);
2ce1d0bClaude2277 border-radius: var(--r-md);
fc1817aClaude2278 margin-bottom: 16px;
2279 overflow: hidden;
2ce1d0bClaude2280 background: var(--bg-elevated);
fc1817aClaude2281 }
2282 .diff-file-header {
2283 background: var(--bg-secondary);
2ce1d0bClaude2284 padding: 10px 16px;
fc1817aClaude2285 border-bottom: 1px solid var(--border);
2286 font-family: var(--font-mono);
2ce1d0bClaude2287 font-size: var(--t-sm);
2288 font-weight: 500;
fc1817aClaude2289 }
2290 .diff-content {
2291 overflow-x: auto;
2292 font-family: var(--font-mono);
2ce1d0bClaude2293 font-size: var(--t-sm);
2294 line-height: 1.65;
fc1817aClaude2295 }
958d26aClaude2296 .diff-content .line-add { background: rgba(52,211,153,0.10); color: var(--green); }
2297 .diff-content .line-del { background: rgba(248,113,113,0.08); color: var(--red); }
2298 .diff-content .line-hunk { background: rgba(140,109,255,0.06); color: var(--text-link); }
2ce1d0bClaude2299 .diff-content .line { padding: 0 16px; white-space: pre; display: block; }
fc1817aClaude2300
2301 .stat-add { color: var(--green); font-weight: 600; }
2302 .stat-del { color: var(--red); font-weight: 600; }
2303
2ce1d0bClaude2304 /* ============================================================ */
2305 /* Empty state */
2306 /* ============================================================ */
fc1817aClaude2307 .empty-state {
2308 text-align: center;
958d26aClaude2309 padding: 96px 24px;
fc1817aClaude2310 color: var(--text-muted);
2ce1d0bClaude2311 border: 1px dashed var(--border);
2312 border-radius: var(--r-lg);
958d26aClaude2313 background:
2314 radial-gradient(60% 60% at 50% 0%, rgba(140,109,255,0.05), transparent 70%),
2315 var(--bg-elevated);
2316 position: relative;
2317 overflow: hidden;
2318 }
2319 .empty-state::before {
2320 content: '';
2321 position: absolute;
2322 inset: 0;
2323 background-image: radial-gradient(rgba(255,255,255,0.025) 1px, transparent 1px);
2324 background-size: 24px 24px;
2325 opacity: 0.5;
2326 pointer-events: none;
2327 mask-image: radial-gradient(ellipse at center, #000 30%, transparent 70%);
2328 -webkit-mask-image: radial-gradient(ellipse at center, #000 30%, transparent 70%);
2329 }
2330 :root[data-theme='light'] .empty-state::before {
2331 background-image: radial-gradient(rgba(15,16,28,0.08) 1px, transparent 1px);
fc1817aClaude2332 }
958d26aClaude2333 .empty-state > * { position: relative; z-index: 1; }
2ce1d0bClaude2334 .empty-state h2 {
958d26aClaude2335 font-size: var(--t-xl);
2ce1d0bClaude2336 margin-bottom: 8px;
958d26aClaude2337 color: var(--text-strong);
2338 letter-spacing: -0.022em;
2ce1d0bClaude2339 }
958d26aClaude2340 .empty-state p { font-size: var(--t-md); max-width: 480px; margin: 0 auto; line-height: 1.55; }
fc1817aClaude2341 .empty-state pre {
2342 text-align: left;
2343 display: inline-block;
2344 background: var(--bg-secondary);
958d26aClaude2345 padding: 18px 24px;
2346 border-radius: var(--r-md);
fc1817aClaude2347 border: 1px solid var(--border);
2348 font-family: var(--font-mono);
958d26aClaude2349 font-feature-settings: var(--mono-feat);
2ce1d0bClaude2350 font-size: var(--t-sm);
958d26aClaude2351 margin-top: 24px;
fc1817aClaude2352 line-height: 1.8;
2ce1d0bClaude2353 color: var(--text);
958d26aClaude2354 box-shadow: var(--elev-1);
fc1817aClaude2355 }
2356
2ce1d0bClaude2357 /* ============================================================ */
2358 /* Badges */
2359 /* ============================================================ */
fc1817aClaude2360 .badge {
2ce1d0bClaude2361 display: inline-flex;
2362 align-items: center;
2363 gap: 4px;
2364 padding: 2px 9px;
2365 border-radius: var(--r-full);
2366 font-size: var(--t-xs);
fc1817aClaude2367 font-weight: 500;
2368 background: var(--bg-tertiary);
2369 border: 1px solid var(--border);
2370 color: var(--text-muted);
2ce1d0bClaude2371 line-height: 1.5;
fc1817aClaude2372 }
2373
2ce1d0bClaude2374 /* ============================================================ */
2375 /* Branch dropdown */
2376 /* ============================================================ */
fc1817aClaude2377 .branch-selector {
2378 display: inline-flex;
2379 align-items: center;
2ce1d0bClaude2380 gap: 6px;
2381 padding: 6px 12px;
2382 background: var(--bg-elevated);
fc1817aClaude2383 border: 1px solid var(--border);
2ce1d0bClaude2384 border-radius: var(--r-sm);
2385 font-size: var(--t-sm);
fc1817aClaude2386 color: var(--text);
2387 margin-bottom: 12px;
2ce1d0bClaude2388 transition: border-color var(--t-fast) var(--ease);
fc1817aClaude2389 }
2ce1d0bClaude2390 .branch-selector:hover { border-color: var(--border-strong); }
fc1817aClaude2391
06d5ffeClaude2392 .branch-dropdown {
2393 position: relative;
2394 display: inline-block;
2395 margin-bottom: 12px;
2396 }
2397 .branch-dropdown-content {
2398 display: none;
2399 position: absolute;
2ce1d0bClaude2400 top: calc(100% + 4px);
06d5ffeClaude2401 left: 0;
2402 z-index: 10;
2ce1d0bClaude2403 min-width: 220px;
2404 background: var(--bg-elevated);
2405 border: 1px solid var(--border-strong);
2406 border-radius: var(--r-md);
2407 overflow: hidden;
2408 box-shadow: var(--elev-3);
06d5ffeClaude2409 }
2410 .branch-dropdown:hover .branch-dropdown-content,
2411 .branch-dropdown:focus-within .branch-dropdown-content { display: block; }
2412 .branch-dropdown-content a {
2413 display: block;
2ce1d0bClaude2414 padding: 9px 14px;
2415 font-size: var(--t-sm);
06d5ffeClaude2416 color: var(--text);
2417 border-bottom: 1px solid var(--border);
2ce1d0bClaude2418 transition: background var(--t-fast) var(--ease);
06d5ffeClaude2419 }
2420 .branch-dropdown-content a:last-child { border-bottom: none; }
2ce1d0bClaude2421 .branch-dropdown-content a:hover { background: var(--bg-hover); text-decoration: none; }
2422 .branch-dropdown-content a.active-branch { color: var(--text-link); font-weight: 600; background: var(--accent-gradient-faint); }
06d5ffeClaude2423
2ce1d0bClaude2424 /* ============================================================ */
2425 /* Card grid */
2426 /* ============================================================ */
2427 .card-grid {
2428 display: grid;
2429 grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
2430 gap: 16px;
2431 }
fc1817aClaude2432 .card {
2433 border: 1px solid var(--border);
2ce1d0bClaude2434 border-radius: var(--r-md);
958d26aClaude2435 padding: 20px;
2ce1d0bClaude2436 background: var(--bg-elevated);
2437 transition:
958d26aClaude2438 border-color var(--t-base) var(--ease),
2439 transform var(--t-base) var(--ease-out-quart),
2ce1d0bClaude2440 box-shadow var(--t-base) var(--ease);
2441 position: relative;
2442 overflow: hidden;
958d26aClaude2443 isolation: isolate;
2444 }
2445 .card::before {
2446 content: '';
2447 position: absolute;
2448 inset: 0;
2449 background: linear-gradient(135deg, rgba(140,109,255,0.06), transparent 50%);
2450 opacity: 0;
2451 transition: opacity var(--t-base) var(--ease);
2452 pointer-events: none;
2453 z-index: -1;
2ce1d0bClaude2454 }
2455 .card:hover {
2456 border-color: var(--border-strong);
2457 box-shadow: var(--elev-2);
958d26aClaude2458 transform: translateY(-2px);
2ce1d0bClaude2459 }
958d26aClaude2460 .card:hover::before { opacity: 1; }
2461 .card h3 { font-size: var(--t-md); margin-bottom: 6px; letter-spacing: -0.012em; }
2ce1d0bClaude2462 .card h3 a { color: var(--text); font-weight: 600; }
2463 .card h3 a:hover { color: var(--text-link); }
2464 .card p { font-size: var(--t-sm); color: var(--text-muted); line-height: 1.5; }
2465 .card-meta {
2466 display: flex;
2467 gap: 14px;
2468 margin-top: 14px;
2469 padding-top: 14px;
2470 border-top: 1px solid var(--border);
2471 font-size: var(--t-xs);
2472 color: var(--text-muted);
2473 }
2474 .card-meta span { display: flex; align-items: center; gap: 5px; }
2475
2476 /* ============================================================ */
2477 /* Stat card / box */
2478 /* ============================================================ */
2479 .stat-card {
2480 background: var(--bg-elevated);
2481 border: 1px solid var(--border);
2482 border-radius: var(--r-md);
fc1817aClaude2483 padding: 16px;
2ce1d0bClaude2484 transition: border-color var(--t-fast) var(--ease);
2485 }
2486 .stat-card:hover { border-color: var(--border-strong); }
2487 .stat-label {
2488 font-size: var(--t-xs);
2489 color: var(--text-muted);
2490 text-transform: uppercase;
2491 letter-spacing: 0.06em;
2492 font-weight: 500;
2493 }
2494 .stat-value {
2495 font-size: var(--t-xl);
2496 font-weight: 700;
2497 margin-top: 4px;
2498 letter-spacing: -0.025em;
2499 color: var(--text);
2500 font-feature-settings: 'tnum';
fc1817aClaude2501 }
06d5ffeClaude2502
2ce1d0bClaude2503 /* ============================================================ */
2504 /* Star button */
2505 /* ============================================================ */
06d5ffeClaude2506 .star-btn {
2507 display: inline-flex;
2508 align-items: center;
2509 gap: 6px;
2ce1d0bClaude2510 padding: 5px 12px;
2511 background: var(--bg-elevated);
06d5ffeClaude2512 border: 1px solid var(--border);
2ce1d0bClaude2513 border-radius: var(--r-sm);
06d5ffeClaude2514 color: var(--text);
2ce1d0bClaude2515 font-size: var(--t-sm);
2516 font-weight: 500;
06d5ffeClaude2517 cursor: pointer;
2ce1d0bClaude2518 transition: all var(--t-fast) var(--ease);
2519 }
2520 .star-btn:hover { background: var(--bg-surface); border-color: var(--border-strong); text-decoration: none; }
2521 .star-btn.starred {
2522 color: var(--yellow);
958d26aClaude2523 border-color: rgba(251,191,36,0.4);
2524 background: rgba(251,191,36,0.08);
06d5ffeClaude2525 }
2526
2ce1d0bClaude2527 /* ============================================================ */
2528 /* User profile */
2529 /* ============================================================ */
06d5ffeClaude2530 .user-profile {
2531 display: flex;
2532 gap: 32px;
2533 margin-bottom: 32px;
2ce1d0bClaude2534 padding: 24px;
2535 background: var(--bg-elevated);
2536 border: 1px solid var(--border);
2537 border-radius: var(--r-lg);
06d5ffeClaude2538 }
2539 .user-avatar {
2540 width: 96px;
2541 height: 96px;
2ce1d0bClaude2542 border-radius: var(--r-full);
2543 background: var(--accent-gradient-soft);
2544 border: 1px solid var(--border-strong);
06d5ffeClaude2545 display: flex;
2546 align-items: center;
2547 justify-content: center;
2ce1d0bClaude2548 font-size: 36px;
2549 font-weight: 600;
2550 color: var(--text);
06d5ffeClaude2551 flex-shrink: 0;
2ce1d0bClaude2552 letter-spacing: -0.02em;
06d5ffeClaude2553 }
2ce1d0bClaude2554 .user-info h2 { font-size: var(--t-xl); margin-bottom: 2px; letter-spacing: -0.025em; }
2555 .user-info .username { font-size: var(--t-md); color: var(--text-muted); }
2556 .user-info .bio { font-size: var(--t-sm); color: var(--text-muted); margin-top: 10px; line-height: 1.55; }
06d5ffeClaude2557
2ce1d0bClaude2558 /* ============================================================ */
2559 /* New repo form */
2560 /* ============================================================ */
2561 .new-repo-form { max-width: 640px; }
2562 .new-repo-form h2 { margin-bottom: 24px; font-size: var(--t-xl); letter-spacing: -0.025em; }
2563 .visibility-options { display: flex; gap: 12px; margin-bottom: 20px; }
06d5ffeClaude2564 .visibility-option {
2565 flex: 1;
2ce1d0bClaude2566 padding: 16px;
06d5ffeClaude2567 border: 1px solid var(--border);
2ce1d0bClaude2568 border-radius: var(--r-md);
2569 background: var(--bg-elevated);
06d5ffeClaude2570 cursor: pointer;
2ce1d0bClaude2571 text-align: left;
2572 transition: all var(--t-fast) var(--ease);
2573 }
2574 .visibility-option:hover { border-color: var(--border-strong); background: var(--bg-surface); }
2575 .visibility-option:has(input:checked) {
2576 border-color: var(--accent);
2577 background: var(--accent-gradient-faint);
2578 box-shadow: 0 0 0 1px var(--accent);
06d5ffeClaude2579 }
2580 .visibility-option input { display: none; }
2ce1d0bClaude2581 .visibility-option .vis-label { font-size: var(--t-sm); font-weight: 600; margin-bottom: 4px; }
2582 .visibility-option .vis-desc { font-size: var(--t-xs); color: var(--text-muted); }
79136bbClaude2583
2ce1d0bClaude2584 /* ============================================================ */
2585 /* Issues */
2586 /* ============================================================ */
2587 .issue-tabs { display: flex; gap: 4px; }
2588 .issue-tabs a {
2589 color: var(--text-muted);
2590 font-size: var(--t-sm);
2591 font-weight: 500;
2592 padding: 6px 12px;
2593 border-radius: var(--r-sm);
2594 transition: all var(--t-fast) var(--ease);
2595 }
2596 .issue-tabs a:hover { color: var(--text); background: var(--bg-hover); text-decoration: none; }
2597 .issue-tabs a.active { color: var(--text); background: var(--bg-elevated); }
79136bbClaude2598
2ce1d0bClaude2599 .issue-list {
2600 border: 1px solid var(--border);
2601 border-radius: var(--r-md);
2602 overflow: hidden;
2603 background: var(--bg-elevated);
2604 }
79136bbClaude2605 .issue-item {
2606 display: flex;
2ce1d0bClaude2607 gap: 14px;
79136bbClaude2608 align-items: flex-start;
2ce1d0bClaude2609 padding: 14px 16px;
79136bbClaude2610 border-bottom: 1px solid var(--border);
2ce1d0bClaude2611 transition: background var(--t-fast) var(--ease);
79136bbClaude2612 }
2613 .issue-item:last-child { border-bottom: none; }
2ce1d0bClaude2614 .issue-item:hover { background: var(--bg-hover); }
2615 .issue-state-icon {
2616 font-size: 14px;
2617 padding-top: 2px;
2618 width: 18px;
2619 height: 18px;
2620 display: inline-flex;
2621 align-items: center;
2622 justify-content: center;
2623 border-radius: var(--r-full);
2624 flex-shrink: 0;
2625 }
79136bbClaude2626 .state-open { color: var(--green); }
958d26aClaude2627 .state-closed { color: #b69dff; }
2ce1d0bClaude2628 .issue-title {
debcf27Claude2629 font-family: var(--font-display);
2630 font-size: var(--t-md);
2ce1d0bClaude2631 font-weight: 600;
debcf27Claude2632 line-height: 1.35;
2633 letter-spacing: -0.012em;
2634 }
2635 .issue-title a { color: var(--text-strong); transition: color var(--t-fast) var(--ease); }
2636 .issue-title a:hover { color: var(--accent); text-decoration: none; }
2637 .issue-meta {
2638 font-family: var(--font-mono);
2639 font-size: 11px;
2640 color: var(--text-muted);
2641 margin-top: 5px;
2642 letter-spacing: 0.01em;
2ce1d0bClaude2643 }
79136bbClaude2644
2645 .issue-badge {
2646 display: inline-flex;
2647 align-items: center;
2ce1d0bClaude2648 gap: 6px;
79136bbClaude2649 padding: 4px 12px;
2ce1d0bClaude2650 border-radius: var(--r-full);
2651 font-size: var(--t-sm);
79136bbClaude2652 font-weight: 500;
2ce1d0bClaude2653 line-height: 1.4;
79136bbClaude2654 }
958d26aClaude2655 .badge-open { background: rgba(52,211,153,0.10); color: var(--green); border: 1px solid rgba(52,211,153,0.35); }
2656 .badge-closed { background: rgba(182,157,255,0.10); color: #b69dff; border: 1px solid rgba(182,157,255,0.35); }
2657 .badge-merged { background: rgba(140,109,255,0.10); color: var(--accent); border: 1px solid rgba(140,109,255,0.35); }
2ce1d0bClaude2658 .state-merged { color: var(--accent); }
958d26aClaude2659 .ai-review { border-color: var(--accent); box-shadow: 0 0 0 1px rgba(140,109,255,0.3); }
79136bbClaude2660
2ce1d0bClaude2661 .issue-detail { max-width: 920px; }
79136bbClaude2662 .issue-comment-box {
2663 border: 1px solid var(--border);
2ce1d0bClaude2664 border-radius: var(--r-md);
79136bbClaude2665 margin-bottom: 16px;
2666 overflow: hidden;
2ce1d0bClaude2667 background: var(--bg-elevated);
79136bbClaude2668 }
2669 .comment-header {
2670 background: var(--bg-secondary);
2ce1d0bClaude2671 padding: 10px 16px;
79136bbClaude2672 border-bottom: 1px solid var(--border);
2ce1d0bClaude2673 font-size: var(--t-sm);
79136bbClaude2674 color: var(--text-muted);
2675 }
2676
2ce1d0bClaude2677 /* ============================================================ */
2678 /* Panel — flexible container used across many pages */
2679 /* ============================================================ */
2680 .panel {
2681 background: var(--bg-elevated);
2682 border: 1px solid var(--border);
2683 border-radius: var(--r-md);
2684 overflow: hidden;
2685 }
2686 .panel-item {
2687 display: flex;
2688 align-items: center;
2689 gap: 12px;
2690 padding: 12px 16px;
79136bbClaude2691 border-bottom: 1px solid var(--border);
2ce1d0bClaude2692 transition: background var(--t-fast) var(--ease);
2693 }
2694 .panel-item:last-child { border-bottom: none; }
2695 .panel-item:hover { background: var(--bg-hover); }
2696 .panel-empty {
2697 padding: 24px;
2698 text-align: center;
79136bbClaude2699 color: var(--text-muted);
2ce1d0bClaude2700 font-size: var(--t-sm);
79136bbClaude2701 }
2702
2ce1d0bClaude2703 /* ============================================================ */
2704 /* Search */
2705 /* ============================================================ */
79136bbClaude2706 .search-results .diff-file { margin-bottom: 12px; }
16b325cClaude2707
2ce1d0bClaude2708 /* ============================================================ */
2709 /* Timeline */
2710 /* ============================================================ */
2711 .timeline { position: relative; padding-left: 28px; }
16b325cClaude2712 .timeline::before {
2713 content: '';
2714 position: absolute;
2715 left: 4px;
2716 top: 8px;
2717 bottom: 8px;
2718 width: 2px;
2ce1d0bClaude2719 background: linear-gradient(180deg, var(--border) 0%, transparent 100%);
16b325cClaude2720 }
2ce1d0bClaude2721 .timeline-item { position: relative; padding-bottom: 16px; }
16b325cClaude2722 .timeline-dot {
2723 position: absolute;
2ce1d0bClaude2724 left: -28px;
2725 top: 8px;
2726 width: 12px;
2727 height: 12px;
2728 border-radius: var(--r-full);
2729 background: var(--accent-gradient);
16b325cClaude2730 border: 2px solid var(--bg);
2ce1d0bClaude2731 box-shadow: 0 0 0 1px var(--border-strong);
16b325cClaude2732 }
2733 .timeline-content {
2ce1d0bClaude2734 background: var(--bg-elevated);
16b325cClaude2735 border: 1px solid var(--border);
2ce1d0bClaude2736 border-radius: var(--r-md);
2737 padding: 14px 16px;
16b325cClaude2738 }
f1ab587Claude2739
2ce1d0bClaude2740 /* ============================================================ */
2741 /* Toggle switch */
2742 /* ============================================================ */
f1ab587Claude2743 .toggle-switch {
2744 position: relative;
2745 display: inline-block;
2ce1d0bClaude2746 width: 40px;
2747 height: 22px;
f1ab587Claude2748 flex-shrink: 0;
2749 margin-left: 16px;
2750 }
2751 .toggle-switch input { opacity: 0; width: 0; height: 0; }
2752 .toggle-slider {
2753 position: absolute;
2754 cursor: pointer;
2755 top: 0; left: 0; right: 0; bottom: 0;
2756 background: var(--bg-tertiary);
2757 border: 1px solid var(--border);
2ce1d0bClaude2758 border-radius: var(--r-full);
2759 transition: all var(--t-base) var(--ease);
f1ab587Claude2760 }
2761 .toggle-slider::before {
2762 content: '';
2763 position: absolute;
2ce1d0bClaude2764 height: 16px;
2765 width: 16px;
f1ab587Claude2766 left: 2px;
2767 bottom: 2px;
2768 background: var(--text-muted);
2ce1d0bClaude2769 border-radius: var(--r-full);
2770 transition: all var(--t-base) var(--ease);
f1ab587Claude2771 }
2772 .toggle-switch input:checked + .toggle-slider {
2ce1d0bClaude2773 background: var(--accent-gradient);
2774 border-color: transparent;
958d26aClaude2775 box-shadow: 0 0 0 1px rgba(140,109,255,0.4);
f1ab587Claude2776 }
2777 .toggle-switch input:checked + .toggle-slider::before {
2ce1d0bClaude2778 transform: translateX(18px);
f1ab587Claude2779 background: #fff;
2780 }
ef8d378Claude2781
2782 /* ============================================================
2783 * 2026 polish layer (purely additive — no layout changes).
2784 * Improves typography rendering, focus states, hover affordances,
2785 * and adds the gradient brand cue to primary buttons. Anything
2786 * that could alter dimensions stays in the rules above.
2787 * ============================================================ */
2788 html { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
2789 body { letter-spacing: -0.005em; font-feature-settings: 'cv11', 'ss01', 'ss03'; }
2790 *::selection { background: rgba(168,85,247,0.35); color: var(--text); }
2791
2792 h1, h2, h3, h4 { letter-spacing: -0.018em; }
2793 h1 { letter-spacing: -0.025em; }
2794
2795 /* Smoother colour transitions everywhere links live */
2796 a { transition: color 120ms cubic-bezier(0.16,1,0.3,1); }
2797
2798 /* Buttons: focus rings + smoother transitions; primary gets the gradient */
2799 .btn {
2800 transition:
2801 background 120ms cubic-bezier(0.16,1,0.3,1),
2802 border-color 120ms cubic-bezier(0.16,1,0.3,1),
2803 transform 120ms cubic-bezier(0.16,1,0.3,1),
2804 box-shadow 120ms cubic-bezier(0.16,1,0.3,1);
2805 }
2806 .btn:active { transform: translateY(0.5px); }
2807 .btn:focus-visible {
2808 outline: none;
2809 box-shadow: 0 0 0 3px rgba(168,85,247,0.30);
2810 }
2811 .btn-primary {
2812 background: linear-gradient(135deg, #a855f7 0%, #06b6d4 100%);
2813 border-color: transparent;
2814 box-shadow:
2815 inset 0 1px 0 rgba(255,255,255,0.15),
2816 0 1px 2px rgba(168,85,247,0.25);
2817 }
2818 .btn-primary:hover {
2819 background: linear-gradient(135deg, #b766f8 0%, #22cce0 100%);
2820 filter: none;
2821 box-shadow:
2822 inset 0 1px 0 rgba(255,255,255,0.20),
2823 0 4px 12px rgba(168,85,247,0.30);
2824 }
2825
2826 /* Inputs: cleaner focus ring + hover */
2827 .form-group input:hover,
2828 .form-group textarea:hover,
2829 .form-group select:hover {
2830 border-color: rgba(255,255,255,0.14);
2831 }
2832 .form-group input:focus,
2833 .form-group textarea:focus,
2834 .form-group select:focus {
2835 border-color: rgba(168,85,247,0.55);
2836 box-shadow: 0 0 0 3px rgba(168,85,247,0.22);
2837 }
2838 :root[data-theme='light'] .form-group input:hover,
2839 :root[data-theme='light'] .form-group textarea:hover,
2840 :root[data-theme='light'] .form-group select:hover {
2841 border-color: rgba(0,0,0,0.18);
2842 }
2843
2844 /* Cards: subtle hover lift */
2845 .card {
2846 transition:
2847 border-color 160ms cubic-bezier(0.16,1,0.3,1),
2848 transform 160ms cubic-bezier(0.16,1,0.3,1),
2849 box-shadow 200ms cubic-bezier(0.16,1,0.3,1);
2850 }
2851 .card:hover {
2852 border-color: rgba(255,255,255,0.18);
2853 transform: translateY(-1px);
2854 box-shadow: 0 8px 24px rgba(0,0,0,0.30);
2855 }
2856 :root[data-theme='light'] .card:hover {
2857 border-color: rgba(0,0,0,0.18);
2858 box-shadow: 0 8px 24px rgba(0,0,0,0.08);
2859 }
2860
2861 /* Issue / commit / panel rows: smoother hover */
2862 .issue-item, .commit-item {
2863 transition: background 120ms cubic-bezier(0.16,1,0.3,1);
2864 }
2865 .repo-nav a {
2866 transition: color 120ms cubic-bezier(0.16,1,0.3,1),
2867 border-bottom-color 120ms cubic-bezier(0.16,1,0.3,1);
2868 }
2869 .nav-link {
2870 transition: color 120ms cubic-bezier(0.16,1,0.3,1);
2871 }
2872
2873 /* Auth card: subtle elevation so register/login feel premium */
2874 .auth-container {
2875 background: var(--bg-secondary);
2876 border: 1px solid var(--border);
2877 border-radius: var(--r-lg, 12px);
2878 padding: 32px;
2879 box-shadow: 0 4px 16px rgba(0,0,0,0.30), 0 0 0 1px var(--border);
2880 }
2881 :root[data-theme='light'] .auth-container {
2882 box-shadow: 0 4px 16px rgba(0,0,0,0.06), 0 0 0 1px var(--border);
2883 }
2884 .auth-container h2 { letter-spacing: -0.025em; }
2885
2886 /* Empty state: dashed border, generous padding */
2887 .empty-state {
2888 border: 1px dashed var(--border);
2889 border-radius: var(--r-lg, 12px);
2890 background: var(--bg);
2891 }
2892
2893 /* Badges + commit-sha: smoother transition */
2894 .commit-sha, .badge { transition: all 120ms cubic-bezier(0.16,1,0.3,1); }
2895
2896 /* Gradient text utility — matches landing's accent treatment */
2897 .gradient-text {
2898 background: linear-gradient(135deg, #a855f7 0%, #06b6d4 100%);
2899 -webkit-background-clip: text;
2900 background-clip: text;
2901 -webkit-text-fill-color: transparent;
2902 }
2903
2904 /* Custom scrollbars (subtle, themed) */
2905 ::-webkit-scrollbar { width: 10px; height: 10px; }
2906 ::-webkit-scrollbar-track { background: transparent; }
2907 ::-webkit-scrollbar-thumb {
2908 background: rgba(255,255,255,0.06);
2909 border: 2px solid var(--bg);
2910 border-radius: 9999px;
2911 }
2912 ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.14); }
2913 :root[data-theme='light'] ::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.10); }
2914 :root[data-theme='light'] ::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,0.18); }
2915
2916 /* Honour reduced-motion preference */
2917 @media (prefers-reduced-motion: reduce) {
2918 *, *::before, *::after {
2919 animation-duration: 0.01ms !important;
2920 animation-iteration-count: 1 !important;
2921 transition-duration: 0.01ms !important;
2922 }
2923 }
c63b860Claude2924
2925 /* Block O3 — visual coherence additive rules. */
2926 .card.card-p-none { padding: 0; }
2927 .card.card-p-sm { padding: var(--space-3); }
2928 .card.card-p-md { padding: var(--space-4); }
2929 .card.card-p-lg { padding: var(--space-6); }
2930 .card.card-elevated { box-shadow: var(--elev-2); }
2931 .card.card-gradient {
2932 background:
2933 linear-gradient(135deg, rgba(140,109,255,0.05), transparent 60%),
2934 var(--bg-elevated);
2935 }
2936 .card.card-gradient::before { opacity: 1; }
2937 .notice {
2938 padding: var(--space-3) var(--space-4);
2939 border-radius: var(--radius-md);
2940 border: 1px solid var(--border);
2941 background: var(--bg-elevated);
2942 color: var(--text);
2943 font-size: var(--font-size-sm);
2944 margin-bottom: var(--space-6);
2945 line-height: var(--leading-normal);
2946 }
2947 .notice-info { border-color: rgba(96,165,250,0.40); background: rgba(96,165,250,0.08); color: var(--text); }
2948 .notice-success { border-color: var(--green); background: rgba(52,211,153,0.08); color: var(--text); }
2949 .notice-warn { border-color: var(--yellow); background: rgba(251,191,36,0.10); color: var(--yellow); }
2950 .notice-error { border-color: var(--red); background: rgba(248,113,113,0.10); color: var(--red); }
2951 .notice-accent { border-color: var(--accent); background: rgba(140,109,255,0.10); color: var(--text); }
2952 .email-preview {
2953 padding: var(--space-5);
2954 background: #fff;
2955 color: #111;
2956 border-radius: var(--radius-md);
2957 }
2958 .code-block {
2959 margin: var(--space-2) 0 0;
2960 padding: var(--space-3);
2961 background: var(--bg-tertiary);
2962 border: 1px solid var(--border-subtle);
2963 border-radius: var(--radius-sm);
2964 font-family: var(--font-mono);
2965 font-size: var(--font-size-xs);
2966 line-height: var(--leading-normal);
2967 overflow-x: auto;
2968 }
2969 .status-pill-operational {
2970 display: inline-flex;
2971 align-items: center;
2972 padding: var(--space-1) var(--space-3);
2973 border-radius: var(--radius-full);
2974 font-size: var(--font-size-xs);
2975 font-weight: 600;
2976 background: rgba(52,211,153,0.15);
2977 color: var(--green);
2978 }
2979 .api-tag {
2980 display: inline-flex;
2981 align-items: center;
2982 font-size: var(--font-size-xs);
2983 padding: 2px var(--space-2);
2984 border-radius: var(--radius-sm);
2985 font-family: var(--font-mono);
2986 }
2987 .api-tag-auth { background: rgba(96,165,250,0.15); color: var(--accent); }
2988 .api-tag-scope { background: rgba(52,211,153,0.15); color: var(--green); }
2989 .stat-number {
2990 font-size: var(--font-size-xl);
2991 font-weight: 700;
2992 color: var(--text-strong);
2993 line-height: var(--leading-tight);
2994 }
2995 .stat-number-accent { color: var(--accent); }
2996 .stat-number-blue { color: var(--blue); }
2997 .stat-number-purple { color: var(--text-link); }
2998 footer .footer-tag-sub {
2999 margin-top: var(--space-2);
3000 font-size: var(--font-size-xs);
3001 color: var(--text-faint);
3002 line-height: var(--leading-normal);
3003 }
3004 footer .footer-version-pill {
3005 display: inline-flex;
3006 align-items: center;
3007 gap: var(--space-2);
3008 padding: var(--space-1) var(--space-3);
3009 border-radius: var(--radius-full);
3010 border: 1px solid var(--border);
3011 background: var(--bg-elevated);
3012 color: var(--text-faint);
3013 font-family: var(--font-mono);
3014 font-size: var(--font-size-xs);
3015 cursor: help;
3016 }
3017 footer .footer-banner {
3018 max-width: 1240px;
3019 margin: var(--space-6) auto 0;
3020 padding: var(--space-3) var(--space-4);
3021 border-radius: var(--radius-md);
3022 border: 1px solid var(--border);
3023 background: var(--bg-elevated);
3024 color: var(--text);
3025 font-family: var(--font-mono);
3026 font-size: var(--font-size-xs);
3027 letter-spacing: 0.04em;
3028 text-transform: uppercase;
3029 text-align: center;
3030 }
3031 footer .footer-banner-info { border-color: rgba(96,165,250,0.40); color: var(--blue); }
3032 footer .footer-banner-warn { border-color: rgba(251,191,36,0.40); color: var(--yellow); }
3033 footer .footer-banner-error { border-color: rgba(248,113,113,0.45); color: var(--red); }
dc26881CC LABS App3034
cf9178bTest User3035 /* ============================================================ */
3036 /* Global toast notifications. */
3037 /* Slide-in from right, auto-dismiss, ARIA-live for SR users. */
3038 /* ============================================================ */
3039 .gx-toast {
3040 pointer-events: auto;
3041 display: inline-flex;
3042 align-items: flex-start;
3043 gap: 10px;
3044 min-width: 280px;
3045 max-width: min(440px, calc(100vw - 32px));
3046 padding: 12px 14px 12px 12px;
3047 background: var(--bg-elevated);
3048 border: 1px solid var(--border);
3049 border-radius: 10px;
3050 box-shadow:
3051 0 12px 36px -10px rgba(15,16,28,0.18),
3052 0 2px 6px rgba(15,16,28,0.04),
3053 0 0 0 1px rgba(15,16,28,0.02);
3054 color: var(--text);
3055 font-size: 13.5px;
3056 line-height: 1.45;
3057 opacity: 0;
3058 transform: translateX(16px);
3059 transition:
3060 opacity 220ms cubic-bezier(0.2, 0.8, 0.2, 1),
3061 transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
3062 }
3063 .gx-toast--in { opacity: 1; transform: translateX(0); }
3064 .gx-toast--out { opacity: 0; transform: translateX(16px); }
3065 .gx-toast__icon {
3066 flex-shrink: 0;
3067 width: 20px; height: 20px;
3068 border-radius: 50%;
3069 display: inline-flex;
3070 align-items: center;
3071 justify-content: center;
3072 font-size: 12px;
3073 font-weight: 700;
3074 line-height: 1;
3075 margin-top: 1px;
3076 }
3077 .gx-toast__text { flex: 1 1 auto; min-width: 0; padding-top: 2px; word-wrap: break-word; }
3078 .gx-toast__close {
3079 flex-shrink: 0;
3080 width: 22px; height: 22px;
3081 border: 0;
3082 background: transparent;
3083 color: var(--text-muted);
3084 font-size: 18px;
3085 line-height: 1;
3086 cursor: pointer;
3087 border-radius: 4px;
3088 padding: 0;
3089 margin: -2px -2px 0 0;
3090 transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
3091 }
3092 .gx-toast__close:hover { background: var(--bg-hover); color: var(--text); }
3093 .gx-toast--success .gx-toast__icon { background: rgba(5,150,105,0.14); color: var(--green); }
3094 .gx-toast--error .gx-toast__icon { background: rgba(220,38,38,0.14); color: var(--red); }
3095 .gx-toast--warn .gx-toast__icon { background: rgba(217,119,6,0.16); color: var(--yellow); }
3096 .gx-toast--info .gx-toast__icon { background: rgba(109,77,255,0.14); color: var(--accent); }
3097 @media (prefers-reduced-motion: reduce) {
3098 .gx-toast { transition: opacity 60ms linear; transform: none; }
3099 .gx-toast--in, .gx-toast--out { transform: none; }
3100 }
3101
dc26881CC LABS App3102 /* ============================================================ */
3103 /* Block U4 — cross-document view transitions. */
3104 /* Chrome 126+, Edge, Safari 18.2+ get a soft 200ms fade on */
3105 /* every same-origin navigation. Older browsers ignore these */
3106 /* rules entirely — no JS shim, no breakage. */
3107 /* prefers-reduced-motion disables the animation honourably. */
3108 /* ============================================================ */
3109 @view-transition {
3110 navigation: auto;
3111 }
3112 ::view-transition-old(root),
3113 ::view-transition-new(root) {
3114 animation-duration: 200ms;
3115 animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
3116 }
3117 ::view-transition-old(root) {
3118 animation-name: vt-fade-out;
3119 }
3120 ::view-transition-new(root) {
3121 animation-name: vt-fade-in;
3122 }
3123 @keyframes vt-fade-out {
3124 to { opacity: 0; }
3125 }
3126 @keyframes vt-fade-in {
3127 from { opacity: 0; }
3128 }
3129 @media (prefers-reduced-motion: reduce) {
3130 ::view-transition-old(root),
3131 ::view-transition-new(root) {
3132 animation-duration: 0s;
3133 }
3134 }
3135 /* The transition system picks up its root subject from this rule. */
3136 body {
3137 view-transition-name: root;
3138 }
fc1817aClaude3139`;