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.tsxBlame2873 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" />
61 <link rel="manifest" href="/manifest.webmanifest" />
62 <link rel="icon" type="image/svg+xml" href="/icon.svg" />
5f2e749Claude63 <title>{renderedTitle}</title>
64 {description && <meta name="description" content={description} />}
65 {(ogTitle || fullTitle || title) && (
66 <meta property="og:title" content={ogTitle ?? fullTitle ?? renderedTitle} />
67 )}
68 {(ogDescription || description) && (
69 <meta
70 property="og:description"
71 content={ogDescription ?? description ?? ""}
72 />
73 )}
74 {ogType && <meta property="og:type" content={ogType} />}
75 {twitterCard && <meta name="twitter:card" content={twitterCard} />}
fa880f2Claude76 <script dangerouslySetInnerHTML={{ __html: themeInitScript }} />
77 <style dangerouslySetInnerHTML={{ __html: css }} />
78 <style dangerouslySetInnerHTML={{ __html: hljsThemeCss }} />
fc1817aClaude79 </head>
80 <body>
4a52a98Claude81 <div class="prelaunch-banner" role="status" aria-live="polite">
82 Pre-launch &mdash; Gluecron is in final validation. Public signups
83 and git hosting for non-owner users open after launch review.
84 </div>
cd4f63bTest User85 {/* Block Q3 — Playground banner. Renders only when the active
86 user is a playground account with a future expiry; the small
87 inline script counts down once per minute. Strip is dismissible
88 per-page-load (re-appears on next nav) — gentle, not noisy. */}
89 {user && (user as any).isPlayground && (user as any).playgroundExpiresAt && (
90 <div
91 class="playground-banner"
92 role="status"
93 aria-live="polite"
94 data-playground-expires={
95 ((user as any).playgroundExpiresAt instanceof Date
96 ? (user as any).playgroundExpiresAt.toISOString()
97 : String((user as any).playgroundExpiresAt))
98 }
99 >
100 <span class="playground-banner-icon" aria-hidden="true">{"\u{1F3AE}"}</span>
101 <span class="playground-banner-text">
102 Playground account &mdash;{" "}
103 <span class="playground-banner-countdown">expires soon</span>.{" "}
104 <a href="/play/claim" class="playground-banner-cta">
105 Save your work &rarr;
106 </a>
107 </span>
108 <button
109 type="button"
110 class="playground-banner-dismiss"
111 aria-label="Dismiss"
112 data-playground-dismiss="1"
113 >
114 {"×"}
115 </button>
116 <script
117 dangerouslySetInnerHTML={{
118 __html: /* js */ `
119 (function () {
120 var el = document.currentScript && document.currentScript.parentElement;
121 if (!el) return;
122 var iso = el.getAttribute('data-playground-expires');
123 if (!iso) return;
124 var target = Date.parse(iso);
125 if (isNaN(target)) return;
126 var out = el.querySelector('.playground-banner-countdown');
127 function render() {
128 var ms = target - Date.now();
129 if (!out) return;
130 if (ms <= 0) { out.textContent = 'expired'; return; }
131 var mins = Math.floor(ms / 60000);
132 var hrs = Math.floor(mins / 60);
133 if (hrs > 1) out.textContent = hrs + ' hours left';
134 else if (hrs === 1) out.textContent = '1 hour left';
135 else if (mins > 1) out.textContent = mins + ' minutes left';
136 else out.textContent = 'less than a minute left';
137 }
138 render();
139 setInterval(render, 60000);
140 var dismiss = el.querySelector('[data-playground-dismiss="1"]');
141 if (dismiss) {
142 dismiss.addEventListener('click', function () {
143 el.style.display = 'none';
144 });
145 }
146 })();
147 `,
148 }}
149 />
150 </div>
151 )}
fc1817aClaude152 <header>
153 <nav>
154 <a href="/" class="logo">
155 gluecron
156 </a>
3ef4c9dClaude157 <div class="nav-search">
001af43Claude158 <form method="get" action="/search">
3ef4c9dClaude159 <input
160 type="search"
161 name="q"
162 placeholder="Search (press /)"
163 aria-label="Search"
164 />
165 </form>
166 </div>
06d5ffeClaude167 <div class="nav-right">
f764c07Claude168 {/* Block N3 — site-admin platform-deploy status pill. Hidden
169 by default; revealed client-side once /admin/deploys/latest.json
170 responds 200 (non-admins get 401/403 and the pill stays
171 hidden). Subscribes to the `platform:deploys` SSE topic
172 for live updates. See `deployPillScript` below. */}
173 {user && (
174 <a
175 id="deploy-pill"
176 href="/admin/deploys"
177 class="nav-deploy-pill"
178 style="display:none"
179 aria-label="Platform deploy status"
180 title="Platform deploy status"
181 >
182 <span class="deploy-pill-dot" />
183 <span class="deploy-pill-text">Deploys</span>
184 </a>
185 )}
6fc53bdClaude186 <a
187 href="/theme/toggle"
188 class="nav-link nav-theme"
189 title="Toggle theme"
190 aria-label="Toggle theme"
191 >
192 <span class="theme-icon-dark">{"\u263E"}</span>
193 <span class="theme-icon-light">{"\u2600"}</span>
194 </a>
c81ab7aClaude195 <a href="/explore" class="nav-link">
196 Explore
197 </a>
06d5ffeClaude198 {user ? (
199 <>
f1ab587Claude200 <a href="/dashboard" class="nav-link" style="font-weight: 600">
201 Dashboard
202 </a>
bdbd0deClaude203 <a href="/import" class="nav-link">
204 Import
205 </a>
06d5ffeClaude206 <a href="/new" class="btn btn-sm btn-primary">
207 + New
208 </a>
209 <a href={`/${user.username}`} class="nav-user">
210 {user.displayName || user.username}
211 </a>
212 <a href="/settings" class="nav-link">
213 Settings
214 </a>
215 <a href="/logout" class="nav-link">
216 Sign out
217 </a>
218 </>
219 ) : (
220 <>
221 <a href="/login" class="nav-link">
222 Sign in
223 </a>
224 <a href="/register" class="btn btn-sm btn-primary">
225 Register
226 </a>
227 </>
228 )}
229 </div>
fc1817aClaude230 </nav>
231 </header>
45e31d0Claude232 <main id="main-content">{children}</main>
fc1817aClaude233 <footer>
958d26aClaude234 <div class="footer-inner">
235 <div class="footer-brand">
236 <a href="/" class="logo">gluecron</a>
237 <p class="footer-tag">
238 AI-native code intelligence. Self-hosted git, automated CI,
239 push-time gates. Software that ships itself.
240 </p>
241 </div>
242 <div class="footer-links">
243 <div class="footer-col">
244 <div class="footer-col-title">Product</div>
b0148e9Claude245 <a href="/features">Features</a>
246 <a href="/pricing">Pricing</a>
958d26aClaude247 <a href="/explore">Explore</a>
248 <a href="/marketplace">Marketplace</a>
249 </div>
250 <div class="footer-col">
251 <div class="footer-col-title">Platform</div>
b0148e9Claude252 <a href="/help">Quickstart</a>
958d26aClaude253 <a href="/status">Status</a>
254 <a href="/api/graphql">GraphQL</a>
255 <a href="/mcp">MCP server</a>
256 </div>
257 <div class="footer-col">
b0148e9Claude258 <div class="footer-col-title">Company</div>
259 <a href="/about">About</a>
958d26aClaude260 <a href="/terms">Terms</a>
261 <a href="/privacy">Privacy</a>
262 <a href="/acceptable-use">Acceptable use</a>
263 </div>
264 </div>
265 </div>
266 <div class="footer-bottom">
267 <span>&copy; {new Date().getFullYear()} gluecron</span>
05cdb85Claude268 <span class="footer-build" title={`commit ${build.shaFull}\nbuilt ${build.builtAt}`}>
269 <span class="footer-build-dot" aria-hidden="true" />
270 {build.sha} · {build.branch}
271 </span>
36b4cbdClaude272 </div>
c63b860Claude273 {siteBannerText ? (
274 <div
275 class={`footer-banner footer-banner-${siteBannerLevel || "info"}`}
276 role="status"
277 aria-live="polite"
278 >
279 {siteBannerText}
280 </div>
281 ) : null}
fc1817aClaude282 </footer>
05cdb85Claude283 {/* Live update poller — checks /api/version every 15s, prompts
284 reload when the running sha changes. Pure progressive-
285 enhancement; degrades to nothing if JS is off. */}
286 <div
287 id="version-banner"
288 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"
289 >
290 <span style="display:inline-flex;align-items:center;gap:8px">
291 <span style="width:8px;height:8px;border-radius:50%;background:#34d399;box-shadow:0 0 10px rgba(52,211,153,0.6)" />
292 <span>New version available</span>
293 </span>
294 <button
295 type="button"
296 id="version-banner-reload"
297 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"
298 >
299 Reload
300 </button>
301 </div>
fa880f2Claude302 <script dangerouslySetInnerHTML={{ __html: versionPollerScript }} />
f764c07Claude303 {/* Block N3 — site-admin deploy status pill (script-only). The pill
304 container is rendered above for authed users; this script bootstraps
305 it by fetching /admin/deploys/latest.json. Non-admins get 401/403
306 and the pill stays display:none — zero leak. */}
307 {user && (
308 <script dangerouslySetInnerHTML={{ __html: deployPillScript }} />
309 )}
699e5c7Claude310 {/* Block I4 — Command palette shell (hidden by default) */}
311 <div
312 id="cmdk-backdrop"
313 style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:9998"
314 />
315 <div
316 id="cmdk-panel"
317 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"
318 >
319 <input
320 id="cmdk-input"
321 type="text"
322 placeholder="Type a command..."
323 aria-label="Command palette"
dc26881CC LABS App324 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"
699e5c7Claude325 />
326 <div id="cmdk-list" style="max-height:60vh;overflow-y:auto" />
327 </div>
fa880f2Claude328 <script dangerouslySetInnerHTML={{ __html: clientJs }} />
329 <script dangerouslySetInnerHTML={{ __html: pwaRegisterScript }} />
534f04aClaude330 {/* Block M2 — smart install banner + push-SW bootstrap. Authed
331 users only; the banner respects a 3+ visits + 14-day cooldown
332 heuristic. The push SW is registered on the same gate so we
333 don't ask anonymous visitors to opt into anything. */}
334 {user && (
335 <script
336 dangerouslySetInnerHTML={{ __html: pwaInstallBannerScript }}
337 />
338 )}
fa880f2Claude339 <script dangerouslySetInnerHTML={{ __html: navScript }} />
fc1817aClaude340 </body>
341 </html>
342 );
343};
344
05cdb85Claude345// Live version poller. Checks /api/version every 15s; if the sha differs
346// from the one we booted with, reveal the floating 'New version' pill so
347// the user can reload onto the new code without manually refreshing.
348const versionPollerScript = `
349 (function(){
350 var loadedSha = null;
351 var banner, btn;
352 function poll(){
353 fetch('/api/version', { cache: 'no-store' })
354 .then(function(r){ return r.ok ? r.json() : null; })
355 .then(function(j){
356 if (!j || !j.sha) return;
357 if (loadedSha === null) { loadedSha = j.sha; return; }
358 if (j.sha !== loadedSha && banner) {
359 banner.style.display = 'inline-flex';
360 }
361 })
362 .catch(function(){});
363 }
364 function init(){
365 banner = document.getElementById('version-banner');
366 btn = document.getElementById('version-banner-reload');
367 if (btn) btn.addEventListener('click', function(){ window.location.reload(); });
368 poll();
369 setInterval(poll, 15000);
370 }
371 if (document.readyState === 'loading') {
372 document.addEventListener('DOMContentLoaded', init);
373 } else {
374 init();
375 }
376 })();
377`;
378
f764c07Claude379// Block N3 — site-admin deploy status pill. Fetches /admin/deploys/latest.json;
380// if 200, reveals the pill in the nav and subscribes to the `platform:deploys`
381// SSE topic for live status updates. Non-admins get 401/403 and the pill stays
382// display:none — there's no leakage of admin-only data to other users.
383//
384// Pill states (CSS classes on the container drive colour):
385// .deploy-pill-success 🟢 "Deployed 12s ago"
386// .deploy-pill-progress 🟡 "Deploying… 14s" (pulsing dot)
387// .deploy-pill-failed 🔴 "Deploy failed 1m ago"
388// .deploy-pill-empty ⚪ "No deploys yet"
389//
390// Relative-time auto-refreshes every 15s without re-fetching.
391export const deployPillScript = `
392 (function(){
393 var pill, dot, text;
394 var state = { latest: null, asOf: null };
395
396 function classifyAge(ms){
397 if (ms < 0) return 'just now';
398 var s = Math.floor(ms / 1000);
399 if (s < 5) return 'just now';
400 if (s < 60) return s + 's ago';
401 var m = Math.floor(s / 60);
402 if (m < 60) return m + 'm ago';
403 var h = Math.floor(m / 60);
404 if (h < 24) return h + 'h ago';
405 var d = Math.floor(h / 24);
406 return d + 'd ago';
407 }
408 function elapsed(ms){
409 if (ms < 0) ms = 0;
410 var s = Math.floor(ms / 1000);
411 if (s < 60) return s + 's';
412 var m = Math.floor(s / 60);
413 var rem = s - m * 60;
414 return m + 'm ' + rem + 's';
415 }
416
417 function render(){
418 if (!pill || !text || !dot) return;
419 var d = state.latest;
81201ccTest User420 // When there's no deploy event yet, keep the pill HIDDEN. Showing
421 // "No deploys yet" was visible noise on every admin page load and
422 // flashed during reconnect cycles — admins don't need a placeholder.
423 // The pill reveals itself the first time a real deploy fires.
f764c07Claude424 if (!d) {
81201ccTest User425 pill.style.display = 'none';
f764c07Claude426 return;
427 }
428 pill.style.display = 'inline-flex';
429 var now = Date.now();
430 if (d.status === 'in_progress') {
431 pill.className = 'nav-deploy-pill deploy-pill-progress';
432 var started = Date.parse(d.started_at) || now;
433 text.textContent = 'Deploying… ' + elapsed(now - started);
434 } else if (d.status === 'succeeded') {
435 pill.className = 'nav-deploy-pill deploy-pill-success';
436 var ref = Date.parse(d.finished_at || d.started_at) || now;
437 text.textContent = 'Deployed ' + classifyAge(now - ref);
438 } else if (d.status === 'failed') {
439 pill.className = 'nav-deploy-pill deploy-pill-failed';
440 var refF = Date.parse(d.finished_at || d.started_at) || now;
441 text.textContent = 'Deploy failed ' + classifyAge(now - refF);
442 } else {
443 pill.className = 'nav-deploy-pill';
444 text.textContent = d.status;
445 }
446 }
447
448 function fetchLatest(){
449 fetch('/admin/deploys/latest.json', { cache: 'no-store', credentials: 'same-origin' })
450 .then(function(r){ if (!r.ok) return null; return r.json(); })
451 .then(function(j){
452 if (!j || j.ok !== true) return;
453 state.latest = j.latest;
454 state.asOf = j.asOf;
455 render();
456 subscribe();
457 })
458 .catch(function(){});
459 }
460
461 var subscribed = false;
462 function subscribe(){
463 if (subscribed) return;
464 if (typeof EventSource === 'undefined') return;
465 subscribed = true;
466 var es;
bf19c50Test User467 // Exponential backoff with cap. Previously a tight 1500ms reconnect
468 // produced visible looping in the nav whenever the proxy timed out
469 // or the connection blipped — the bottom-of-page deploy pill
470 // re-rendered the placeholder every 1.5s. Cap at 60s and reset on
471 // successful message receipt.
472 var delay = 2000;
473 var DELAY_MAX = 60000;
474 function bump(){
475 delay = Math.min(delay * 2, DELAY_MAX);
476 }
477 function resetDelay(){
478 delay = 2000;
479 }
f764c07Claude480 function connect(){
481 try { es = new EventSource('/live-events/platform:deploys'); }
bf19c50Test User482 catch(e){ bump(); setTimeout(connect, delay); return; }
f764c07Claude483 es.onmessage = function(m){
bf19c50Test User484 resetDelay();
f764c07Claude485 try {
486 var d = JSON.parse(m.data);
487 if (d && d.run_id) {
488 if (!state.latest || state.latest.run_id === d.run_id ||
489 Date.parse(d.started_at) >= Date.parse(state.latest.started_at)) {
490 state.latest = d;
491 render();
492 }
493 }
494 } catch(e){}
495 };
496 es.onerror = function(){
497 try { es.close(); } catch(e){}
bf19c50Test User498 bump();
f764c07Claude499 setTimeout(connect, delay);
500 };
501 }
502 connect();
503 }
504
505 function init(){
506 pill = document.getElementById('deploy-pill');
507 if (!pill) return;
508 dot = pill.querySelector('.deploy-pill-dot');
509 text = pill.querySelector('.deploy-pill-text');
510 fetchLatest();
511 // Refresh relative-time labels every 15s so "12s ago" → "27s ago"
512 // without a fresh fetch.
513 setInterval(render, 15000);
514 }
515 if (document.readyState === 'loading') {
516 document.addEventListener('DOMContentLoaded', init);
517 } else {
518 init();
519 }
520 })();
521`;
522
6fc53bdClaude523// Runs before paint — reads the theme cookie and flips data-theme so there's
81201ccTest User524// no light-to-dark flash on load. SSR default is "light"; cookie-set "dark"
525// is honoured for users who explicitly opted in.
6fc53bdClaude526const themeInitScript = `
527 (function(){
528 try {
529 var m = document.cookie.match(/(?:^|; )theme=([^;]+)/);
81201ccTest User530 var t = m ? decodeURIComponent(m[1]) : 'light';
531 if (t !== 'light' && t !== 'dark') t = 'light';
6fc53bdClaude532 document.documentElement.setAttribute('data-theme', t);
533 } catch(_){}
534 })();
535`;
536
eae38d1Claude537// Block G1 — register service worker for offline / install support.
538// Kept inline (and tiny) so we don't block first paint.
b1be050CC LABS App539//
540// Block S2 extension — when an updated SW activates we force a single
541// reload so the user picks up the fresh HTML immediately instead of
542// being stuck on the previous deploy's cached page (the "I saw /login's
543// old version even after merging" bug that triggered S2 in the first
544// place). The reload only fires when `controller` is already set, which
545// means this is NOT the first-ever install (no double-load on first
546// visit). A page-scoped guard prevents reload loops if the SW updates
547// twice in quick succession.
eae38d1Claude548const pwaRegisterScript = `
549 if ('serviceWorker' in navigator) {
550 window.addEventListener('load', function(){
b1be050CC LABS App551 navigator.serviceWorker.register('/sw.js').then(function(reg){
552 var reloaded = false;
553 reg.addEventListener('updatefound', function(){
554 var newSW = reg.installing;
555 if (!newSW) return;
556 newSW.addEventListener('statechange', function(){
557 if (newSW.state === 'activated' && navigator.serviceWorker.controller && !reloaded) {
558 reloaded = true;
559 window.location.reload();
560 }
561 });
562 });
563 }).catch(function(){});
eae38d1Claude564 });
565 }
566`;
567
534f04aClaude568// Block M2 — smart install banner (authed users only). Three heuristics:
569// 1. user is authenticated (gated server-side via the `{user &&}` JSX)
570// 2. visit counter (localStorage) ≥ 3
571// 3. dismissed-cooldown < 14 days ago
572// Also bootstraps the push + offline SW at /sw-push.js. Idempotent — safe
573// to load on every page; only inserts DOM nodes if all conditions match.
574const pwaInstallBannerScript = `
575(function(){
576 try {
577 var DISMISS_KEY = 'gc:pwa-banner-dismissed-at';
578 var VISITS_KEY = 'gc:pwa-visit-count';
579 var DISMISS_MS = 14 * 24 * 60 * 60 * 1000;
580 var visits = parseInt(localStorage.getItem(VISITS_KEY) || '0', 10) || 0;
581 visits++;
582 try { localStorage.setItem(VISITS_KEY, String(visits)); } catch(_){}
583 // Bootstrap the push + offline SW (separate from /sw.js's locked behaviour).
584 if ('serviceWorker' in navigator) {
585 window.addEventListener('load', function(){
586 navigator.serviceWorker.register('/sw-push.js', { scope: '/' })
587 .catch(function(){});
588 });
589 }
590 var deferredPrompt = null;
591 window.addEventListener('beforeinstallprompt', function(e){
592 e.preventDefault();
593 deferredPrompt = e;
594 maybeShow();
595 });
596 function maybeShow(){
597 if (!deferredPrompt) return;
598 if (visits < 3) return;
599 var dismissedAt = parseInt(localStorage.getItem(DISMISS_KEY) || '0', 10) || 0;
600 if (dismissedAt && (Date.now() - dismissedAt) < DISMISS_MS) return;
601 if (document.getElementById('gc-install-banner')) return;
602 var bar = document.createElement('div');
603 bar.id = 'gc-install-banner';
604 bar.setAttribute('role', 'dialog');
605 bar.setAttribute('aria-label', 'Install Gluecron');
606 bar.style.cssText = 'position:fixed;left:12px;right:12px;bottom:12px;z-index:9997;'
607 + 'background:var(--bg-elevated,#161b22);color:var(--text,#c9d1d9);'
608 + 'border:1px solid var(--border,#30363d);border-radius:8px;padding:12px 14px;'
609 + 'display:flex;gap:10px;align-items:center;justify-content:space-between;'
610 + 'box-shadow:0 8px 24px rgba(0,0,0,0.45);font-size:13px;line-height:1.3;'
611 + 'max-width:560px;margin:0 auto';
612 bar.innerHTML = '<span style="flex:1">'
613 + '<strong>Install Gluecron</strong> to keep working when offline + get push notifications.'
614 + '</span>'
615 + '<button type="button" id="gc-install-go" style="background:#238636;color:#fff;'
616 + 'border:0;border-radius:6px;padding:6px 12px;font-size:12px;cursor:pointer;'
617 + 'font-family:inherit">Install</button>'
618 + '<button type="button" id="gc-install-no" style="background:transparent;color:inherit;'
619 + 'border:1px solid var(--border,#30363d);border-radius:6px;padding:6px 12px;'
620 + 'font-size:12px;cursor:pointer;font-family:inherit">Not now</button>';
621 document.body.appendChild(bar);
622 var go = bar.querySelector('#gc-install-go');
623 var no = bar.querySelector('#gc-install-no');
624 if (go) go.addEventListener('click', function(){
625 try { deferredPrompt.prompt(); } catch(_){}
626 bar.parentNode && bar.parentNode.removeChild(bar);
627 deferredPrompt = null;
628 });
629 if (no) no.addEventListener('click', function(){
630 try { localStorage.setItem(DISMISS_KEY, String(Date.now())); } catch(_){}
631 bar.parentNode && bar.parentNode.removeChild(bar);
632 });
633 }
634 } catch(_) {}
635})();
636`;
637
3ef4c9dClaude638const navScript = `
639 (function(){
640 var chord = null;
641 var chordTimer = null;
642 function isTyping(t){
643 t = t || {};
644 var tag = (t.tagName || '').toLowerCase();
645 return tag === 'input' || tag === 'textarea' || t.isContentEditable;
646 }
71cd5ecClaude647
648 // ---------- Block I4 — Command palette ----------
649 var COMMANDS = [
650 { label: 'Go to Dashboard', href: '/dashboard', kw: 'home' },
651 { label: 'Go to Explore', href: '/explore', kw: 'browse discover' },
652 { label: 'Go to Notifications', href: '/notifications', kw: 'inbox' },
653 { label: 'Go to Ask AI', href: '/ask', kw: 'chat assistant' },
654 { label: 'Create new repository', href: '/new', kw: 'add create' },
655 { label: 'Marketplace', href: '/marketplace', kw: 'apps store' },
656 { label: 'Installed apps', href: '/settings/apps', kw: 'my apps' },
657 { label: 'Register new app', href: '/developer/apps-new', kw: 'developer create' },
658 { label: 'Keyboard shortcuts', href: '/shortcuts', kw: 'help keys' },
659 { label: 'Settings (profile)', href: '/settings', kw: 'account' },
660 { label: '2FA settings', href: '/settings/2fa', kw: 'two factor security' },
661 { label: 'Passkeys settings', href: '/settings/passkeys', kw: 'webauthn' },
662 { label: 'Personal access tokens', href: '/settings/tokens', kw: 'pat api' },
663 { label: 'Billing + quotas', href: '/settings/billing', kw: 'plans money' },
664 { label: 'Audit log (personal)', href: '/settings/audit', kw: 'history' },
665 { label: 'Gists', href: '/gists', kw: 'snippets' },
666 { label: 'GraphQL explorer', href: '/api/graphql', kw: 'api query' },
667 { label: 'Admin dashboard', href: '/admin', kw: 'superuser' },
668 { label: 'Toggle theme', href: '/theme/toggle', kw: 'dark light mode' }
669 ];
670
671 function fuzzyMatch(item, q){
672 if (!q) return true;
673 var hay = (item.label + ' ' + (item.kw||'') + ' ' + item.href).toLowerCase();
674 q = q.toLowerCase();
675 var qi = 0;
676 for (var i = 0; i < hay.length && qi < q.length; i++) {
677 if (hay[i] === q[qi]) qi++;
678 }
679 return qi === q.length;
680 }
681
682 var backdrop, panel, input, list, selected = 0, filtered = COMMANDS.slice();
683
684 function render(){
685 if (!list) return;
686 var html = '';
687 for (var i = 0; i < filtered.length; i++) {
688 var item = filtered[i];
689 var cls = i === selected ? 'cmdk-item cmdk-active' : 'cmdk-item';
690 var bg = i === selected ? 'background:var(--bg);' : '';
ea52715copilot-swe-agent[bot]691 html += '<div class="' + cls + '" data-idx="' + i + '" data-url="' + item.href + '"' +
dc26881CC LABS App692 ' style="padding:var(--space-2) var(--space-4);cursor:pointer;border-bottom:1px solid var(--border);' + bg + '">' +
71cd5ecClaude693 '<div>' + item.label + '</div>' +
694 '<div style="font-size:11px;color:var(--text-muted)">' + item.href + '</div>' +
695 '</div>';
696 }
697 if (filtered.length === 0) {
dc26881CC LABS App698 html = '<div style="padding:var(--space-4);color:var(--text-muted);text-align:center">No matches.</div>';
71cd5ecClaude699 }
700 list.innerHTML = html;
701 }
702
703 function openPalette(){
704 backdrop = document.getElementById('cmdk-backdrop');
705 panel = document.getElementById('cmdk-panel');
706 input = document.getElementById('cmdk-input');
707 list = document.getElementById('cmdk-list');
708 if (!backdrop || !panel) return;
709 backdrop.style.display = 'block';
710 panel.style.display = 'block';
711 input.value = '';
712 selected = 0;
713 filtered = COMMANDS.slice();
714 render();
715 input.focus();
716 }
717 function closePalette(){
718 if (backdrop) backdrop.style.display = 'none';
719 if (panel) panel.style.display = 'none';
720 }
721 function go(href){ closePalette(); window.location.href = href; }
722
723 document.addEventListener('click', function(e){
724 var t = e.target;
725 if (t && t.id === 'cmdk-backdrop') { closePalette(); return; }
726 var item = t && t.closest && t.closest('.cmdk-item');
ea52715copilot-swe-agent[bot]727 if (item) { go(item.getAttribute('data-url')); }
71cd5ecClaude728 });
729
730 document.addEventListener('input', function(e){
731 if (e.target && e.target.id === 'cmdk-input') {
732 var q = e.target.value;
733 filtered = COMMANDS.filter(function(c){ return fuzzyMatch(c, q); });
734 selected = 0;
735 render();
736 }
737 });
738
3ef4c9dClaude739 document.addEventListener('keydown', function(e){
71cd5ecClaude740 // Palette-scoped keys take priority when open
741 if (panel && panel.style.display === 'block') {
742 if (e.key === 'Escape') { e.preventDefault(); closePalette(); return; }
743 if (e.key === 'ArrowDown') {
744 e.preventDefault();
745 selected = Math.min(filtered.length - 1, selected + 1);
746 render();
747 return;
748 }
749 if (e.key === 'ArrowUp') {
750 e.preventDefault();
751 selected = Math.max(0, selected - 1);
752 render();
753 return;
754 }
755 if (e.key === 'Enter') {
756 e.preventDefault();
757 var item = filtered[selected];
758 if (item) go(item.href);
759 return;
760 }
761 return;
762 }
763
3ef4c9dClaude764 if (isTyping(e.target)) return;
765 // Single key shortcuts
766 if (e.key === '/') {
767 var el = document.querySelector('.nav-search input');
768 if (el) { e.preventDefault(); el.focus(); return; }
769 }
770 if ((e.metaKey||e.ctrlKey) && e.key.toLowerCase() === 'k') {
71cd5ecClaude771 e.preventDefault();
772 openPalette();
773 return;
3ef4c9dClaude774 }
775 if (e.key === '?' && !e.ctrlKey && !e.metaKey) {
776 e.preventDefault(); window.location.href = '/shortcuts'; return;
777 }
778 if (e.key === 'n' && !e.ctrlKey && !e.metaKey) {
779 e.preventDefault(); window.location.href = '/new'; return;
780 }
781 // "g" chord
782 if (e.key === 'g' && !e.ctrlKey && !e.metaKey) {
783 chord = 'g';
784 clearTimeout(chordTimer);
785 chordTimer = setTimeout(function(){ chord = null; }, 1200);
786 return;
787 }
788 if (chord === 'g') {
789 if (e.key === 'd') { e.preventDefault(); window.location.href = '/dashboard'; }
790 else if (e.key === 'n') { e.preventDefault(); window.location.href = '/notifications'; }
791 else if (e.key === 'e') { e.preventDefault(); window.location.href = '/explore'; }
792 else if (e.key === 'a') { e.preventDefault(); window.location.href = '/ask'; }
793 chord = null;
794 }
795 });
796 })();
797`;
798
fc1817aClaude799const css = `
2ce1d0bClaude800 /* ================================================================
958d26aClaude801 * Gluecron design system — 2026.05 "Editorial-Technical"
802 * Slate-noir base · refined violet signature · hairline geometry ·
803 * mono-as-feature · cinematic motion · Inter Tight + JetBrains Mono.
804 * All class names preserved for back-compat across 50+ route views.
2ce1d0bClaude805 * ============================================================== */
6fc53bdClaude806 :root, :root[data-theme='dark'] {
958d26aClaude807 /* Surfaces — slate, not black. More depth, less crush. */
808 --bg: #08090f;
809 --bg-secondary: #0c0d14;
810 --bg-tertiary: #11131c;
811 --bg-elevated: #0f111a;
812 --bg-surface: #161826;
813 --bg-hover: rgba(255,255,255,0.04);
814 --bg-active: rgba(255,255,255,0.08);
815 --bg-inset: rgba(0,0,0,0.30);
816
817 /* Borders — three weights, used deliberately */
818 --border: rgba(255,255,255,0.06);
819 --border-subtle: rgba(255,255,255,0.035);
820 --border-strong: rgba(255,255,255,0.13);
821 --border-focus: rgba(140,109,255,0.55);
822
823 /* Text */
824 --text: #ededf2;
825 --text-strong: #f7f7fb;
826 --text-muted: #8b8c9c;
827 --text-faint: #555665;
828 --text-link: #b69dff;
829
830 /* Accent — refined violet (less candy), warm amber as secondary signal */
831 --accent: #8c6dff;
832 --accent-2: #36c5d6;
833 --accent-warm: #ffb45e;
834 --accent-hover: #a48bff;
835 --accent-pressed:#7559e8;
836 --accent-gradient: linear-gradient(135deg, #8c6dff 0%, #36c5d6 100%);
837 --accent-gradient-soft: linear-gradient(135deg, rgba(140,109,255,0.18) 0%, rgba(54,197,214,0.18) 100%);
838 --accent-gradient-faint: linear-gradient(135deg, rgba(140,109,255,0.07) 0%, rgba(54,197,214,0.07) 100%);
839 --accent-glow: 0 0 24px rgba(140,109,255,0.28);
840
841 /* Semantic */
842 --green: #34d399;
843 --red: #f87171;
844 --yellow: #fbbf24;
845 --amber: #fbbf24;
846 --blue: #60a5fa;
847
fb71554Claude848 /* Type — system fonts FIRST so we never depend on Google Fonts loading.
849 Segoe UI (Win), -apple-system / SF (Mac), Roboto (Android), Inter as
850 optional upgrade if the user already has it. NEVER falls back to serif. */
851 --font-mono: ui-monospace, 'SF Mono', 'Cascadia Code', 'Cascadia Mono', Menlo, Consolas, 'Courier New', monospace;
852 --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI Variable', 'Segoe UI', system-ui, Roboto, 'Helvetica Neue', Arial, 'Inter', sans-serif;
853 --font-display: -apple-system, BlinkMacSystemFont, 'Segoe UI Variable', 'Segoe UI', system-ui, Roboto, 'Helvetica Neue', Arial, 'Inter Tight', 'Inter', sans-serif;
958d26aClaude854 --mono-feat: 'calt', 'liga', 'ss01';
855
856 /* Radius — sharper than before */
857 --r-sm: 5px;
858 --r: 7px;
859 --r-md: 9px;
860 --r-lg: 12px;
861 --r-xl: 16px;
862 --r-2xl: 22px;
863 --r-full: 9999px;
864 --radius: 7px;
865
866 /* Type scale — bigger display sizes for editorial feel */
867 --t-xs: 11px;
868 --t-sm: 13px;
869 --t-base: 14px;
870 --t-md: 16px;
871 --t-lg: 20px;
872 --t-xl: 28px;
873 --t-2xl: 40px;
874 --t-3xl: 56px;
875 --t-display: 72px;
876 --t-display-lg:96px;
877
878 /* Spacing — 4px base */
2ce1d0bClaude879 --s-0: 0;
958d26aClaude880 --s-1: 4px; --s-2: 8px; --s-3: 12px; --s-4: 16px;
881 --s-5: 20px; --s-6: 24px; --s-7: 28px; --s-8: 32px;
882 --s-10:40px; --s-12:48px; --s-14:56px; --s-16:64px;
883 --s-20:80px; --s-24:96px; --s-32:128px;
884
885 /* Elevation — softer + more layered */
886 --elev-0: 0 0 0 1px var(--border);
887 --elev-1: 0 1px 2px rgba(0,0,0,0.50), 0 0 0 1px var(--border);
888 --elev-2: 0 8px 24px -8px rgba(0,0,0,0.60), 0 0 0 1px var(--border);
889 --elev-3: 0 20px 48px -12px rgba(0,0,0,0.70), 0 0 0 1px var(--border-strong);
890 --elev-glow: 0 0 0 1px rgba(140,109,255,0.40), 0 0 32px -4px rgba(140,109,255,0.30);
891 --ring: 0 0 0 3px rgba(140,109,255,0.28);
892 --ring-warn: 0 0 0 3px rgba(251,191,36,0.28);
893 --ring-err: 0 0 0 3px rgba(248,113,113,0.28);
894
895 /* Motion */
896 --ease: cubic-bezier(0.16, 1, 0.3, 1);
897 --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
898 --ease-out-expo: cubic-bezier(0.19, 1, 0.22, 1);
899 --ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1);
900 --t-fast: 120ms;
901 --t-base: 200ms;
902 --t-slow: 360ms;
903 --t-slower:560ms;
904
905 --header-h: 60px;
c63b860Claude906
907 /* Block O3 — visual coherence: named token aliases (additive). */
908 --space-1: var(--s-1);
909 --space-2: var(--s-2);
910 --space-3: var(--s-3);
911 --space-4: var(--s-4);
912 --space-5: var(--s-5);
913 --space-6: var(--s-6);
914 --space-8: var(--s-8);
915 --space-10: var(--s-10);
916 --space-12: var(--s-12);
917 --space-16: var(--s-16);
918 --space-20: var(--s-20);
919 --space-24: var(--s-24);
920 --radius-sm: var(--r-sm);
921 --radius-md: var(--r-md);
922 --radius-lg: var(--r-lg);
923 --radius-xl: var(--r-xl);
924 --radius-full: var(--r-full);
925 --font-size-xs: var(--t-xs);
926 --font-size-sm: var(--t-sm);
927 --font-size-base: var(--t-base);
928 --font-size-md: var(--t-md);
929 --font-size-lg: var(--t-lg);
930 --font-size-xl: var(--t-xl);
931 --font-size-2xl: var(--t-2xl);
932 --font-size-3xl: var(--t-3xl);
933 --font-size-hero: var(--t-display);
934 --leading-tight: 1.2;
935 --leading-snug: 1.35;
936 --leading-normal: 1.5;
937 --leading-relaxed: 1.6;
938 --leading-loose: 1.7;
939 --z-base: 1;
940 --z-nav: 10;
941 --z-sticky: 50;
942 --z-overlay: 100;
943 --z-modal: 1000;
944 --z-toast: 10000;
fc1817aClaude945 }
946
6fc53bdClaude947 :root[data-theme='light'] {
958d26aClaude948 --bg: #fbfbfc;
4c47454Claude949 --bg-secondary: #ffffff;
958d26aClaude950 --bg-tertiary: #f3f3f6;
951 --bg-elevated: #ffffff;
952 --bg-surface: #f6f6f9;
953 --bg-hover: rgba(0,0,0,0.035);
954 --bg-active: rgba(0,0,0,0.07);
955 --bg-inset: rgba(0,0,0,0.04);
956
957 --border: rgba(15,16,28,0.08);
958 --border-subtle: rgba(15,16,28,0.04);
959 --border-strong: rgba(15,16,28,0.16);
960
961 --text: #0e1020;
962 --text-strong: #050617;
963 --text-muted: #5a5b70;
964 --text-faint: #8a8b9e;
965 --text-link: #6d4dff;
966
967 --accent: #6d4dff;
968 --accent-2: #0891b2;
969 --accent-hover: #5a3df0;
970 --accent-pressed:#4a30d6;
971 --accent-glow: 0 0 24px rgba(109,77,255,0.18);
972
973 --green: #059669;
974 --red: #dc2626;
4c47454Claude975 --yellow: #d97706;
958d26aClaude976
977 --elev-1: 0 1px 2px rgba(15,16,28,0.06), 0 0 0 1px var(--border);
978 --elev-2: 0 8px 24px -10px rgba(15,16,28,0.10), 0 0 0 1px var(--border);
979 --elev-3: 0 20px 48px -16px rgba(15,16,28,0.14), 0 0 0 1px var(--border-strong);
6fc53bdClaude980 }
981
982 /* Theme toggle — show the icon for the *opposite* theme so users see what they'll switch to. */
958d26aClaude983 .nav-theme { display: inline-flex; align-items: center; font-size: 15px; line-height: 1; opacity: 0.85; }
984 .nav-theme:hover { opacity: 1; }
6fc53bdClaude985 :root[data-theme='dark'] .theme-icon-dark { display: none; }
986 :root[data-theme='light'] .theme-icon-light { display: none; }
987
fc1817aClaude988 * { margin: 0; padding: 0; box-sizing: border-box; }
958d26aClaude989 *::selection { background: rgba(140,109,255,0.32); color: var(--text-strong); }
2ce1d0bClaude990
958d26aClaude991 html { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-rendering: optimizeLegibility; }
fc1817aClaude992
993 body {
994 font-family: var(--font-sans);
995 background: var(--bg);
996 color: var(--text);
2ce1d0bClaude997 line-height: 1.55;
958d26aClaude998 letter-spacing: -0.011em;
fc1817aClaude999 min-height: 100vh;
1000 display: flex;
1001 flex-direction: column;
958d26aClaude1002 font-feature-settings: 'cv11', 'ss01', 'ss03', 'calt';
fc1817aClaude1003 }
1004
958d26aClaude1005 /* Whole-page atmosphere: very subtle gradient + dot-grid layered behind everything.
1006 Keeps every page feeling like part of the same product without competing with hero art. */
2ce1d0bClaude1007 body::before {
1008 content: '';
1009 position: fixed;
1010 inset: 0;
1011 pointer-events: none;
958d26aClaude1012 z-index: -2;
2ce1d0bClaude1013 background:
958d26aClaude1014 radial-gradient(70% 55% at 85% -20%, rgba(140,109,255,0.07), transparent 65%),
1015 radial-gradient(55% 45% at -10% 115%, rgba(54,197,214,0.05), transparent 65%);
1016 }
1017 body::after {
1018 content: '';
1019 position: fixed;
1020 inset: 0;
1021 pointer-events: none;
1022 z-index: -1;
1023 background-image: radial-gradient(rgba(255,255,255,0.025) 1px, transparent 1px);
1024 background-size: 28px 28px;
1025 background-position: 0 0;
1026 opacity: 0.55;
1027 mask-image: radial-gradient(ellipse at 50% 0%, #000 0%, transparent 70%);
1028 -webkit-mask-image: radial-gradient(ellipse at 50% 0%, #000 0%, transparent 70%);
1029 }
1030 :root[data-theme='light'] body::before { opacity: 0.55; }
1031 :root[data-theme='light'] body::after {
1032 background-image: radial-gradient(rgba(15,16,28,0.06) 1px, transparent 1px);
1033 opacity: 0.4;
fc1817aClaude1034 }
2ce1d0bClaude1035
1036 a { color: var(--text-link); text-decoration: none; transition: color var(--t-fast) var(--ease); }
1037 a:hover { color: var(--accent-hover); text-decoration: none; }
fc1817aClaude1038
958d26aClaude1039 /* Heading scale — confident, editorial, tight tracking */
1040 h1, h2, h3, h4, h5, h6 {
1041 font-family: var(--font-display);
2ce1d0bClaude1042 font-weight: 600;
958d26aClaude1043 letter-spacing: -0.022em;
1044 line-height: 1.18;
1045 color: var(--text-strong);
1046 }
1047 h1 { font-size: var(--t-xl); letter-spacing: -0.028em; }
1048 h2 { font-size: var(--t-lg); letter-spacing: -0.022em; }
1049 h3 { font-size: var(--t-md); font-weight: 600; letter-spacing: -0.015em; }
1050 h4 { font-size: var(--t-base); font-weight: 600; letter-spacing: -0.01em; }
1051 h5, h6 { font-size: var(--t-sm); font-weight: 600; letter-spacing: -0.005em; }
1052
1053 /* Editorial display heading utility — used by landing + marketing pages */
1054 .display {
1055 font-family: var(--font-display);
1056 font-size: clamp(40px, 7.5vw, 96px);
1057 line-height: 0.98;
1058 letter-spacing: -0.04em;
1059 font-weight: 600;
1060 color: var(--text-strong);
2ce1d0bClaude1061 }
fc1817aClaude1062
958d26aClaude1063 /* Eyebrow — uppercase mono label that sits above section headings */
1064 .eyebrow {
1065 display: inline-flex;
1066 align-items: center;
1067 gap: 8px;
1068 font-family: var(--font-mono);
1069 font-size: 11px;
1070 font-weight: 500;
1071 letter-spacing: 0.14em;
1072 text-transform: uppercase;
1073 color: var(--accent);
1074 margin-bottom: var(--s-3);
1075 }
1076 .eyebrow::before {
1077 content: '';
1078 width: 18px;
1079 height: 1px;
1080 background: currentColor;
1081 opacity: 0.6;
1082 }
1083
1084 /* Section header — paired eyebrow + title + lede */
1085 .section-header { max-width: 720px; margin: 0 auto var(--s-10); text-align: center; }
1086 .section-header.left { text-align: left; margin-left: 0; margin-right: auto; }
1087 .section-header h2 {
1088 font-size: clamp(28px, 4vw, 44px);
1089 line-height: 1.05;
1090 letter-spacing: -0.028em;
1091 margin-bottom: var(--s-3);
1092 }
1093 .section-header p {
1094 color: var(--text-muted);
1095 font-size: var(--t-md);
1096 line-height: 1.6;
1097 max-width: 580px;
1098 margin: 0 auto;
1099 }
1100 .section-header.left p { margin-left: 0; }
fc1817aClaude1101
958d26aClaude1102 code, kbd, samp {
2ce1d0bClaude1103 font-family: var(--font-mono);
958d26aClaude1104 font-feature-settings: var(--mono-feat);
1105 }
1106 code {
1107 font-size: 0.9em;
2ce1d0bClaude1108 background: var(--bg-tertiary);
958d26aClaude1109 border: 1px solid var(--border-subtle);
2ce1d0bClaude1110 padding: 1px 6px;
1111 border-radius: var(--r-sm);
1112 color: var(--text);
1113 }
958d26aClaude1114 kbd, .kbd {
1115 display: inline-flex;
1116 align-items: center;
1117 padding: 1px 6px;
1118 font-family: var(--font-mono);
1119 font-size: 11px;
1120 background: var(--bg-elevated);
1121 border: 1px solid var(--border);
1122 border-bottom-width: 2px;
1123 border-radius: 4px;
1124 color: var(--text);
1125 line-height: 1.5;
1126 vertical-align: middle;
1127 }
2ce1d0bClaude1128
958d26aClaude1129 /* Mono utility for technical chrome (paths, IDs, dates) */
1130 .mono { font-family: var(--font-mono); font-feature-settings: var(--mono-feat); font-size: 0.96em; }
1131 .meta-mono { font-family: var(--font-mono); font-size: var(--t-xs); color: var(--text-muted); letter-spacing: 0; }
1132
1133 /* Pre-launch banner — slim, refined, mono caption */
4a52a98Claude1134 .prelaunch-banner {
958d26aClaude1135 position: relative;
2ce1d0bClaude1136 background:
958d26aClaude1137 linear-gradient(180deg, rgba(251,191,36,0.10), rgba(251,191,36,0.03)),
2ce1d0bClaude1138 var(--bg);
958d26aClaude1139 border-bottom: 1px solid rgba(251,191,36,0.28);
4a52a98Claude1140 color: var(--yellow);
958d26aClaude1141 padding: 7px 24px;
1142 font-family: var(--font-mono);
1143 font-size: 11px;
4a52a98Claude1144 font-weight: 500;
1145 text-align: center;
2ce1d0bClaude1146 line-height: 1.5;
958d26aClaude1147 letter-spacing: 0.04em;
1148 text-transform: uppercase;
1149 }
1150 .prelaunch-banner::before {
1151 content: '◆';
1152 margin-right: 8px;
1153 font-size: 9px;
1154 opacity: 0.7;
1155 vertical-align: 1px;
4a52a98Claude1156 }
1157
cd4f63bTest User1158 /* Block Q3 — Playground banner. Brighter than the prelaunch strip so
1159 visitors don't miss the "save your work" CTA, but slim enough to
1160 not feel like a modal. */
1161 .playground-banner {
1162 position: relative;
1163 display: flex;
1164 align-items: center;
1165 justify-content: center;
1166 gap: 8px;
1167 background:
1168 linear-gradient(180deg, rgba(251,191,36,0.20), rgba(251,191,36,0.06)),
1169 var(--bg);
1170 border-bottom: 1px solid rgba(251,191,36,0.45);
1171 color: var(--yellow, #fbbf24);
1172 padding: 8px 40px 8px 24px;
1173 font-size: 13px;
1174 font-weight: 500;
1175 text-align: center;
1176 line-height: 1.4;
1177 }
1178 .playground-banner-icon { font-size: 14px; }
1179 .playground-banner-text { color: var(--text-strong, #e6edf3); }
1180 .playground-banner-countdown { font-weight: 600; }
1181 .playground-banner-cta {
1182 margin-left: 4px;
1183 color: var(--yellow, #fbbf24);
1184 text-decoration: underline;
1185 font-weight: 600;
1186 }
1187 .playground-banner-cta:hover { opacity: 0.85; }
1188 .playground-banner-dismiss {
1189 position: absolute;
1190 top: 50%;
1191 right: 12px;
1192 transform: translateY(-50%);
1193 background: transparent;
1194 border: none;
1195 color: var(--text-muted, #8b949e);
1196 font-size: 18px;
1197 line-height: 1;
1198 cursor: pointer;
1199 padding: 0 4px;
1200 }
1201 .playground-banner-dismiss:hover { color: var(--text-strong, #e6edf3); }
1202
958d26aClaude1203 /* Header — sticky, blurred, hairline border, taller for breathing room */
fc1817aClaude1204 header {
2ce1d0bClaude1205 position: sticky;
1206 top: 0;
1207 z-index: 100;
fc1817aClaude1208 border-bottom: 1px solid var(--border);
2ce1d0bClaude1209 padding: 0 24px;
1210 height: var(--header-h);
958d26aClaude1211 background: rgba(8,9,15,0.72);
1212 backdrop-filter: saturate(180%) blur(18px);
1213 -webkit-backdrop-filter: saturate(180%) blur(18px);
fc1817aClaude1214 }
958d26aClaude1215 :root[data-theme='light'] header { background: rgba(251,251,252,0.78); }
fc1817aClaude1216
06d5ffeClaude1217 header nav {
1218 display: flex;
1219 align-items: center;
958d26aClaude1220 gap: 18px;
1221 max-width: 1240px;
06d5ffeClaude1222 margin: 0 auto;
2ce1d0bClaude1223 height: 100%;
1224 }
1225 .logo {
958d26aClaude1226 font-family: var(--font-display);
1227 font-size: 16px;
2ce1d0bClaude1228 font-weight: 700;
958d26aClaude1229 letter-spacing: -0.025em;
1230 color: var(--text-strong);
2ce1d0bClaude1231 display: inline-flex;
1232 align-items: center;
958d26aClaude1233 gap: 9px;
1234 transition: opacity var(--t-fast) var(--ease);
06d5ffeClaude1235 }
2ce1d0bClaude1236 .logo::before {
1237 content: '';
958d26aClaude1238 width: 20px; height: 20px;
1239 border-radius: 6px;
2ce1d0bClaude1240 background: var(--accent-gradient);
958d26aClaude1241 box-shadow:
1242 inset 0 1px 0 rgba(255,255,255,0.25),
1243 0 0 0 1px rgba(140,109,255,0.45),
1244 0 0 20px rgba(140,109,255,0.30);
2ce1d0bClaude1245 flex-shrink: 0;
958d26aClaude1246 transition: transform var(--t-base) var(--ease-spring), box-shadow var(--t-base) var(--ease);
1247 }
1248 .logo:hover { text-decoration: none; color: var(--text-strong); }
1249 .logo:hover::before {
1250 transform: rotate(8deg) scale(1.05);
1251 box-shadow:
1252 inset 0 1px 0 rgba(255,255,255,0.30),
1253 0 0 0 1px rgba(140,109,255,0.55),
1254 0 0 28px rgba(140,109,255,0.45);
06d5ffeClaude1255 }
fc1817aClaude1256
2ce1d0bClaude1257 .nav-search {
1258 flex: 1;
958d26aClaude1259 max-width: 360px;
1260 margin: 0 4px 0 8px;
1261 position: relative;
2ce1d0bClaude1262 }
1263 .nav-search input {
1264 width: 100%;
958d26aClaude1265 padding: 7px 12px 7px 32px;
2ce1d0bClaude1266 background: var(--bg-tertiary);
1267 border: 1px solid var(--border);
1268 border-radius: var(--r-sm);
1269 color: var(--text);
1270 font-family: var(--font-sans);
1271 font-size: var(--t-sm);
958d26aClaude1272 transition: border-color var(--t-fast) var(--ease), background var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
1273 }
1274 .nav-search::before {
1275 content: '';
1276 position: absolute;
1277 left: 11px; top: 50%;
1278 transform: translateY(-50%);
1279 width: 12px; height: 12px;
1280 border: 1.5px solid var(--text-faint);
1281 border-radius: 50%;
1282 pointer-events: none;
1283 }
1284 .nav-search::after {
1285 content: '';
1286 position: absolute;
1287 left: 19px; top: calc(50% + 4px);
1288 width: 5px; height: 1.5px;
1289 background: var(--text-faint);
1290 transform: rotate(45deg);
1291 pointer-events: none;
2ce1d0bClaude1292 }
1293 .nav-search input::placeholder { color: var(--text-faint); }
1294 .nav-search input:focus {
1295 outline: none;
1296 background: var(--bg-secondary);
958d26aClaude1297 border-color: var(--border-focus);
1298 box-shadow: var(--ring);
2ce1d0bClaude1299 }
06d5ffeClaude1300
958d26aClaude1301 .nav-right { display: flex; align-items: center; gap: 2px; margin-left: auto; }
f764c07Claude1302
1303 /* Block N3 — site-admin deploy status pill */
1304 .nav-deploy-pill {
1305 display: inline-flex;
1306 align-items: center;
1307 gap: 6px;
1308 padding: 4px 10px;
1309 margin: 0 6px 0 0;
1310 border-radius: 9999px;
1311 font-size: 11px;
1312 font-weight: 600;
1313 line-height: 1;
1314 color: var(--text-strong);
1315 background: var(--bg-elevated);
1316 border: 1px solid var(--border);
1317 text-decoration: none;
1318 transition: background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
1319 }
1320 .nav-deploy-pill:hover { background: var(--bg-hover); text-decoration: none; }
1321 .nav-deploy-pill .deploy-pill-dot {
1322 display: inline-block;
1323 width: 8px; height: 8px;
1324 border-radius: 50%;
1325 background: #6b7280;
1326 }
1327 .deploy-pill-success .deploy-pill-dot { background: #34d399; box-shadow: 0 0 6px rgba(52,211,153,0.45); }
1328 .deploy-pill-failed .deploy-pill-dot { background: #f87171; box-shadow: 0 0 6px rgba(248,113,113,0.45); }
1329 .deploy-pill-failed { border-color: rgba(248,113,113,0.4); }
1330 .deploy-pill-progress .deploy-pill-dot {
1331 background: #fbbf24;
1332 box-shadow: 0 0 6px rgba(251,191,36,0.55);
1333 animation: deployPillPulse 1.2s ease-in-out infinite;
1334 }
1335 @keyframes deployPillPulse {
1336 0%, 100% { opacity: 1; transform: scale(1); }
1337 50% { opacity: 0.45; transform: scale(0.8); }
1338 }
1339 .deploy-pill-empty .deploy-pill-dot { background: #9ca3af; }
1340
2ce1d0bClaude1341 .nav-link {
958d26aClaude1342 position: relative;
2ce1d0bClaude1343 color: var(--text-muted);
1344 font-size: var(--t-sm);
1345 font-weight: 500;
958d26aClaude1346 padding: 7px 11px;
2ce1d0bClaude1347 border-radius: var(--r-sm);
958d26aClaude1348 transition: color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
2ce1d0bClaude1349 }
1350 .nav-link:hover {
958d26aClaude1351 color: var(--text-strong);
2ce1d0bClaude1352 background: var(--bg-hover);
1353 text-decoration: none;
1354 }
958d26aClaude1355 .nav-link.active { color: var(--text-strong); }
1356 .nav-link.active::after {
1357 content: '';
1358 position: absolute;
1359 left: 11px; right: 11px;
1360 bottom: -1px;
1361 height: 2px;
1362 background: var(--accent-gradient);
1363 border-radius: 2px;
1364 }
2ce1d0bClaude1365 .nav-user {
958d26aClaude1366 color: var(--text-strong);
2ce1d0bClaude1367 font-weight: 600;
1368 font-size: var(--t-sm);
1369 padding: 6px 10px;
1370 border-radius: var(--r-sm);
958d26aClaude1371 margin-left: 6px;
2ce1d0bClaude1372 transition: background var(--t-fast) var(--ease);
1373 }
958d26aClaude1374 .nav-user::before {
1375 content: '';
1376 display: inline-block;
1377 width: 8px; height: 8px;
1378 background: var(--green);
1379 border-radius: 50%;
1380 margin-right: 7px;
1381 box-shadow: 0 0 8px rgba(52,211,153,0.5);
1382 vertical-align: 1px;
1383 }
2ce1d0bClaude1384 .nav-user:hover { background: var(--bg-hover); text-decoration: none; }
fc1817aClaude1385
2ce1d0bClaude1386 main {
958d26aClaude1387 max-width: 1240px;
2ce1d0bClaude1388 margin: 0 auto;
958d26aClaude1389 padding: 36px 24px 80px;
2ce1d0bClaude1390 flex: 1;
1391 width: 100%;
1392 }
fc1817aClaude1393
958d26aClaude1394 /* Editorial footer: link grid + tagline row */
fc1817aClaude1395 footer {
1396 border-top: 1px solid var(--border);
958d26aClaude1397 padding: 56px 24px 40px;
fc1817aClaude1398 color: var(--text-muted);
958d26aClaude1399 font-size: var(--t-sm);
1400 background:
1401 linear-gradient(180deg, transparent 0%, rgba(140,109,255,0.025) 100%),
1402 var(--bg);
1403 }
1404 footer .footer-inner {
1405 max-width: 1240px;
1406 margin: 0 auto;
1407 display: grid;
1408 grid-template-columns: 1fr auto;
1409 gap: 48px;
1410 align-items: start;
1411 }
1412 footer .footer-brand .logo { margin-bottom: var(--s-3); }
1413 footer .footer-tag {
1414 color: var(--text-muted);
1415 font-size: var(--t-sm);
1416 max-width: 320px;
1417 line-height: 1.55;
1418 }
1419 footer .footer-links {
1420 display: grid;
1421 grid-template-columns: repeat(3, minmax(120px, auto));
1422 gap: 0 56px;
1423 }
1424 footer .footer-col-title {
1425 font-family: var(--font-mono);
1426 font-size: 11px;
1427 text-transform: uppercase;
1428 letter-spacing: 0.14em;
1429 color: var(--text-faint);
1430 margin-bottom: var(--s-3);
1431 }
1432 footer .footer-col a {
1433 display: block;
1434 color: var(--text-muted);
1435 font-size: var(--t-sm);
1436 padding: 5px 0;
1437 transition: color var(--t-fast) var(--ease);
1438 }
1439 footer .footer-col a:hover { color: var(--text-strong); text-decoration: none; }
1440 footer .footer-bottom {
1441 max-width: 1240px;
1442 margin: 40px auto 0;
1443 padding-top: 24px;
1444 border-top: 1px solid var(--border-subtle);
1445 display: flex;
1446 justify-content: space-between;
1447 align-items: center;
2ce1d0bClaude1448 color: var(--text-faint);
1449 font-size: var(--t-xs);
958d26aClaude1450 font-family: var(--font-mono);
1451 letter-spacing: 0.02em;
1452 }
1453 footer .footer-bottom a { color: var(--text-faint); }
1454 footer .footer-bottom a:hover { color: var(--text-muted); text-decoration: none; }
05cdb85Claude1455 footer .footer-build {
1456 display: inline-flex;
1457 align-items: center;
1458 gap: 6px;
1459 color: var(--text-faint);
1460 font-family: var(--font-mono);
1461 font-size: 11px;
1462 cursor: help;
1463 }
1464 footer .footer-build-dot {
1465 width: 6px; height: 6px;
1466 border-radius: 50%;
1467 background: var(--green);
1468 box-shadow: 0 0 6px rgba(52,211,153,0.55);
1469 animation: footer-build-pulse 2.4s ease-in-out infinite;
1470 }
1471 @keyframes footer-build-pulse {
1472 0%, 100% { opacity: 0.6; }
1473 50% { opacity: 1; }
1474 }
958d26aClaude1475 @media (max-width: 768px) {
1476 footer .footer-inner { grid-template-columns: 1fr; gap: 32px; }
1477 footer .footer-links { grid-template-columns: repeat(2, 1fr); gap: 24px 32px; }
1478 footer .footer-bottom { flex-direction: column; gap: 8px; text-align: center; }
fc1817aClaude1479 }
1480
2ce1d0bClaude1481 /* ============================================================ */
1482 /* Buttons */
1483 /* ============================================================ */
dc26881CC LABS App1484 /* ============================================================ */
1485 /* Buttons — Block U2 senior polish pass. */
1486 /* Rules: */
1487 /* · hover lifts every .btn by 1px + soft drop shadow (180ms) */
1488 /* · active presses back down to 0 (80ms — faster on press) */
1489 /* · focus-visible uses a soft box-shadow ring (no outline) */
1490 /* · primary gradient shifts position on hover for a slow */
1491 /* 600ms shimmer */
1492 /* · disabled never lifts and never animates */
1493 /* ============================================================ */
06d5ffeClaude1494 .btn {
1495 display: inline-flex;
1496 align-items: center;
2ce1d0bClaude1497 justify-content: center;
958d26aClaude1498 gap: 7px;
2ce1d0bClaude1499 padding: 7px 14px;
1500 border-radius: var(--r-sm);
958d26aClaude1501 font-family: var(--font-sans);
2ce1d0bClaude1502 font-size: var(--t-sm);
06d5ffeClaude1503 font-weight: 500;
1504 border: 1px solid var(--border);
2ce1d0bClaude1505 background: var(--bg-elevated);
06d5ffeClaude1506 color: var(--text);
1507 cursor: pointer;
1508 text-decoration: none;
958d26aClaude1509 line-height: 1.25;
1510 letter-spacing: -0.008em;
dc26881CC LABS App1511 /* U2 — hover/transform settles in 180ms; press snaps in 80ms.
1512 Listed once on the base rule so :active can override only the
1513 transform/duration without re-typing the colour/bg transitions. */
2ce1d0bClaude1514 transition:
dc26881CC LABS App1515 background 180ms ease,
1516 border-color 180ms ease,
1517 transform 180ms ease,
1518 box-shadow 180ms ease,
1519 color 180ms ease;
2ce1d0bClaude1520 user-select: none;
1521 white-space: nowrap;
958d26aClaude1522 position: relative;
06d5ffeClaude1523 }
2ce1d0bClaude1524 .btn:hover {
1525 background: var(--bg-surface);
1526 border-color: var(--border-strong);
958d26aClaude1527 color: var(--text-strong);
2ce1d0bClaude1528 text-decoration: none;
dc26881CC LABS App1529 /* U2 — universal hover lift + soft accent drop shadow. */
1530 transform: translateY(-1px);
1531 box-shadow: 0 4px 12px -4px rgba(15, 17, 26, 0.45);
1532 }
1533 .btn:active {
1534 transform: translateY(0);
1535 transition-duration: 80ms;
1536 }
1537 /* U2 — soft modern focus ring via box-shadow, not outline. */
1538 .btn:focus-visible {
1539 outline: none;
1540 box-shadow: 0 0 0 3px rgba(140, 109, 255, 0.35);
2ce1d0bClaude1541 }
1542
1543 .btn-primary {
1544 background: var(--accent-gradient);
dc26881CC LABS App1545 background-size: 200% 100%;
1546 background-position: 0% 50%;
2ce1d0bClaude1547 border-color: transparent;
1548 color: #fff;
1549 font-weight: 600;
958d26aClaude1550 text-shadow: 0 1px 0 rgba(0,0,0,0.15);
2ce1d0bClaude1551 box-shadow:
958d26aClaude1552 inset 0 1px 0 rgba(255,255,255,0.22),
1553 inset 0 -1px 0 rgba(0,0,0,0.10),
1554 0 1px 2px rgba(0,0,0,0.40),
1555 0 0 0 1px rgba(140,109,255,0.30);
dc26881CC LABS App1556 /* U2 — slower 600ms transition on background-position so the
1557 primary CTA shimmers when the cursor lands. */
1558 transition:
1559 background-position 600ms ease,
1560 transform 180ms ease,
1561 box-shadow 180ms ease,
1562 color 180ms ease;
06d5ffeClaude1563 }
958d26aClaude1564 .btn-primary::before {
1565 content: '';
1566 position: absolute;
1567 inset: 0;
1568 border-radius: inherit;
1569 background: linear-gradient(180deg, rgba(255,255,255,0.18), transparent 60%);
1570 opacity: 0;
1571 transition: opacity var(--t-fast) var(--ease);
1572 pointer-events: none;
2ce1d0bClaude1573 }
1574 .btn-primary:hover {
958d26aClaude1575 color: #fff;
2ce1d0bClaude1576 background: var(--accent-gradient);
dc26881CC LABS App1577 background-size: 200% 100%;
1578 background-position: 100% 50%;
958d26aClaude1579 border-color: transparent;
dc26881CC LABS App1580 transform: translateY(-1px);
2ce1d0bClaude1581 box-shadow:
958d26aClaude1582 inset 0 1px 0 rgba(255,255,255,0.30),
1583 inset 0 -1px 0 rgba(0,0,0,0.10),
1584 0 6px 18px -4px rgba(140,109,255,0.45),
1585 0 0 0 1px rgba(140,109,255,0.45);
2ce1d0bClaude1586 }
958d26aClaude1587 .btn-primary:hover::before { opacity: 1; }
dc26881CC LABS App1588 .btn-primary:active { transform: translateY(0); transition-duration: 80ms; }
1589 /* U2 — primary focus ring uses the same accent box-shadow recipe. */
1590 .btn-primary:focus-visible {
1591 box-shadow:
1592 inset 0 1px 0 rgba(255,255,255,0.22),
1593 0 0 0 3px rgba(140,109,255,0.35);
1594 }
2ce1d0bClaude1595
1596 .btn-danger {
1597 background: transparent;
958d26aClaude1598 border-color: rgba(248,113,113,0.40);
2ce1d0bClaude1599 color: var(--red);
1600 }
1601 .btn-danger:hover {
958d26aClaude1602 background: rgba(248,113,113,0.08);
2ce1d0bClaude1603 border-color: var(--red);
958d26aClaude1604 color: var(--red);
dc26881CC LABS App1605 transform: translateY(-1px);
1606 box-shadow: 0 4px 12px -4px rgba(248,113,113,0.30);
2ce1d0bClaude1607 }
dc26881CC LABS App1608 .btn-danger:focus-visible { box-shadow: 0 0 0 3px rgba(248,113,113,0.35); }
2ce1d0bClaude1609
1610 .btn-ghost {
1611 background: transparent;
1612 border-color: transparent;
1613 color: var(--text-muted);
1614 }
1615 .btn-ghost:hover {
1616 background: var(--bg-hover);
958d26aClaude1617 color: var(--text-strong);
2ce1d0bClaude1618 border-color: var(--border);
dc26881CC LABS App1619 transform: translateY(-1px);
1620 box-shadow: 0 4px 12px -4px rgba(15, 17, 26, 0.30);
2ce1d0bClaude1621 }
1622
958d26aClaude1623 .btn-secondary {
1624 background: var(--bg-elevated);
1625 border-color: var(--border-strong);
1626 color: var(--text-strong);
1627 }
1628 .btn-secondary:hover {
1629 background: var(--bg-surface);
1630 border-color: var(--border-strong);
dc26881CC LABS App1631 transform: translateY(-1px);
1632 box-shadow: 0 4px 12px -4px rgba(15, 17, 26, 0.35);
958d26aClaude1633 }
1634
1635 .btn-sm { padding: 4px 10px; font-size: var(--t-xs); border-radius: var(--r-sm); gap: 5px; }
1636 .btn-lg { padding: 11px 22px; font-size: var(--t-base); border-radius: var(--r); }
1637 .btn-xl { padding: 14px 28px; font-size: var(--t-md); border-radius: var(--r); font-weight: 600; }
1638 .btn-block { width: 100%; }
2ce1d0bClaude1639
dc26881CC LABS App1640 /* U2 — disabled never lifts, never shimmers, never glows. */
1641 .btn:disabled,
1642 .btn[aria-disabled='true'],
1643 .btn:disabled:hover,
1644 .btn[aria-disabled='true']:hover {
1645 opacity: 0.5;
2ce1d0bClaude1646 cursor: not-allowed;
1647 pointer-events: none;
dc26881CC LABS App1648 transform: none;
1649 box-shadow: none;
1650 }
1651 @media (prefers-reduced-motion: reduce) {
1652 .btn,
1653 .btn:hover,
1654 .btn:active,
1655 .btn-primary,
1656 .btn-primary:hover {
1657 transform: none;
1658 transition: background-color 80ms linear, color 80ms linear;
1659 }
2ce1d0bClaude1660 }
1661
1662 /* ============================================================ */
1663 /* Forms */
1664 /* ============================================================ */
1665 .form-group { margin-bottom: 20px; }
1666 .form-group label {
1667 display: block;
1668 font-size: var(--t-sm);
1669 font-weight: 500;
1670 margin-bottom: 6px;
1671 color: var(--text);
1672 letter-spacing: -0.005em;
1673 }
1674 .form-group input,
1675 .form-group textarea,
1676 .form-group select,
1677 input[type='text'], input[type='email'], input[type='password'],
1678 input[type='url'], input[type='search'], input[type='number'],
1679 textarea, select {
06d5ffeClaude1680 width: 100%;
2ce1d0bClaude1681 padding: 9px 12px;
1682 background: var(--bg-secondary);
06d5ffeClaude1683 border: 1px solid var(--border);
2ce1d0bClaude1684 border-radius: var(--r-sm);
06d5ffeClaude1685 color: var(--text);
2ce1d0bClaude1686 font-size: var(--t-sm);
06d5ffeClaude1687 font-family: var(--font-sans);
2ce1d0bClaude1688 transition:
1689 border-color var(--t-fast) var(--ease),
1690 background var(--t-fast) var(--ease),
1691 box-shadow var(--t-fast) var(--ease);
06d5ffeClaude1692 }
2ce1d0bClaude1693 .form-group input::placeholder, textarea::placeholder, input::placeholder {
1694 color: var(--text-faint);
06d5ffeClaude1695 }
2ce1d0bClaude1696 .form-group input:hover, textarea:hover, select:hover,
1697 input[type='text']:hover, input[type='email']:hover, input[type='password']:hover {
1698 border-color: var(--border-strong);
1699 }
1700 .form-group input:focus, .form-group textarea:focus, .form-group select:focus,
1701 input:focus, textarea:focus, select:focus {
06d5ffeClaude1702 outline: none;
2ce1d0bClaude1703 background: var(--bg);
1704 border-color: var(--border-focus);
1705 box-shadow: var(--ring);
06d5ffeClaude1706 }
2ce1d0bClaude1707 textarea { font-family: var(--font-mono); font-size: var(--t-sm); line-height: 1.55; }
06d5ffeClaude1708 .input-disabled { opacity: 0.5; cursor: not-allowed; }
1709
2ce1d0bClaude1710 /* ============================================================ */
1711 /* Auth (register / login / verify) */
1712 /* ============================================================ */
1713 .auth-container {
1714 max-width: 420px;
1715 margin: 64px auto;
1716 padding: 32px;
1717 background: var(--bg-elevated);
1718 border: 1px solid var(--border);
1719 border-radius: var(--r-lg);
1720 box-shadow: var(--elev-2);
1721 }
1722 .auth-container h2 {
1723 margin-bottom: 6px;
1724 font-size: var(--t-lg);
1725 letter-spacing: -0.02em;
1726 }
1727 .auth-container > p {
1728 color: var(--text-muted);
1729 font-size: var(--t-sm);
1730 margin-bottom: 24px;
1731 }
1732 .auth-container .btn-primary { width: 100%; padding: 10px 16px; }
06d5ffeClaude1733 .auth-error {
958d26aClaude1734 background: rgba(248,113,113,0.08);
1735 border: 1px solid rgba(248,113,113,0.35);
06d5ffeClaude1736 color: var(--red);
2ce1d0bClaude1737 padding: 10px 14px;
1738 border-radius: var(--r-sm);
06d5ffeClaude1739 margin-bottom: 16px;
2ce1d0bClaude1740 font-size: var(--t-sm);
06d5ffeClaude1741 }
1742 .auth-success {
958d26aClaude1743 background: rgba(52,211,153,0.08);
1744 border: 1px solid rgba(52,211,153,0.35);
06d5ffeClaude1745 color: var(--green);
2ce1d0bClaude1746 padding: 10px 14px;
1747 border-radius: var(--r-sm);
06d5ffeClaude1748 margin-bottom: 16px;
2ce1d0bClaude1749 font-size: var(--t-sm);
1750 }
1751 .auth-switch {
1752 margin-top: 20px;
1753 font-size: var(--t-sm);
1754 color: var(--text-muted);
1755 text-align: center;
1756 }
1757 .banner {
1758 background: var(--accent-gradient-faint);
958d26aClaude1759 border: 1px solid rgba(140,109,255,0.35);
2ce1d0bClaude1760 color: var(--text);
1761 padding: 10px 14px;
1762 border-radius: var(--r-sm);
1763 font-size: var(--t-sm);
06d5ffeClaude1764 }
1765
2ce1d0bClaude1766 /* ============================================================ */
1767 /* Settings */
1768 /* ============================================================ */
1769 .settings-container { max-width: 720px; }
1770 .settings-container h2 { margin-bottom: 8px; font-size: var(--t-xl); letter-spacing: -0.02em; }
1771 .settings-container > h2 + p { color: var(--text-muted); font-size: var(--t-sm); margin-bottom: 24px; }
1772 .settings-container h3 { font-size: var(--t-md); margin-bottom: 12px; margin-top: 28px; }
1773 .settings-container h3:first-of-type { margin-top: 0; }
06d5ffeClaude1774 .ssh-keys-list { margin-bottom: 24px; }
1775 .ssh-key-item {
1776 display: flex;
1777 justify-content: space-between;
1778 align-items: center;
2ce1d0bClaude1779 padding: 14px 16px;
06d5ffeClaude1780 border: 1px solid var(--border);
2ce1d0bClaude1781 border-radius: var(--r-md);
06d5ffeClaude1782 margin-bottom: 8px;
2ce1d0bClaude1783 background: var(--bg-elevated);
1784 transition: border-color var(--t-fast) var(--ease);
06d5ffeClaude1785 }
2ce1d0bClaude1786 .ssh-key-item:hover { border-color: var(--border-strong); }
1787 .ssh-key-meta { font-size: var(--t-xs); color: var(--text-muted); margin-top: 4px; }
1788 .ssh-key-meta code { font-size: var(--t-xs); background: var(--bg-tertiary); padding: 1px 6px; border-radius: 3px; margin-right: 8px; }
06d5ffeClaude1789
2ce1d0bClaude1790 /* ============================================================ */
1791 /* Repo header + nav */
1792 /* ============================================================ */
fc1817aClaude1793 .repo-header {
1794 display: flex;
1795 align-items: center;
debcf27Claude1796 gap: 12px;
1797 margin-bottom: 22px;
1798 }
1799 .repo-header-title {
1800 display: flex;
1801 align-items: center;
1802 gap: 10px;
1803 font-family: var(--font-display);
1804 font-size: 24px;
1805 letter-spacing: -0.025em;
1806 flex-wrap: wrap;
1807 }
1808 .repo-header .owner {
1809 color: var(--text-muted);
1810 font-weight: 500;
1811 transition: color var(--t-fast) var(--ease);
1812 }
1813 .repo-header .owner:hover { color: var(--text-link); text-decoration: none; }
1814 .repo-header .separator { color: var(--text-faint); font-weight: 300; }
1815 .repo-header .name {
1816 color: var(--text-strong);
1817 font-weight: 700;
1818 letter-spacing: -0.028em;
fc1817aClaude1819 }
2ce1d0bClaude1820 .repo-header .name:hover { color: var(--text-link); text-decoration: none; }
debcf27Claude1821 .repo-header-fork {
1822 font-family: var(--font-mono);
1823 font-size: 11px;
1824 color: var(--text-muted);
1825 margin-top: 4px;
1826 letter-spacing: 0.01em;
1827 }
1828 .repo-header-fork a { color: var(--text-muted); }
1829 .repo-header-fork a:hover { color: var(--accent); }
1830 .repo-header-pill {
1831 display: inline-flex;
1832 align-items: center;
1833 padding: 2px 10px;
1834 border-radius: var(--r-full);
1835 font-family: var(--font-mono);
1836 font-size: 10px;
1837 font-weight: 600;
1838 letter-spacing: 0.12em;
1839 text-transform: uppercase;
1840 line-height: 1.6;
1841 vertical-align: 4px;
1842 }
1843 .repo-header-pill-archived {
1844 background: rgba(251,191,36,0.10);
1845 color: var(--yellow);
1846 border: 1px solid rgba(251,191,36,0.30);
1847 }
1848 .repo-header-pill-template {
1849 background: var(--accent-gradient-faint);
1850 color: var(--accent);
1851 border: 1px solid rgba(140,109,255,0.30);
fc1817aClaude1852 }
06d5ffeClaude1853 .repo-header-actions { margin-left: auto; display: flex; gap: 8px; align-items: center; }
fc1817aClaude1854
1855 .repo-nav {
1856 display: flex;
debcf27Claude1857 gap: 1px;
fc1817aClaude1858 border-bottom: 1px solid var(--border);
debcf27Claude1859 margin-bottom: 28px;
2ce1d0bClaude1860 overflow-x: auto;
1861 scrollbar-width: thin;
fc1817aClaude1862 }
2ce1d0bClaude1863 .repo-nav::-webkit-scrollbar { height: 0; }
fc1817aClaude1864 .repo-nav a {
debcf27Claude1865 position: relative;
1866 padding: 11px 14px;
fc1817aClaude1867 color: var(--text-muted);
1868 border-bottom: 2px solid transparent;
2ce1d0bClaude1869 font-size: var(--t-sm);
1870 font-weight: 500;
1871 margin-bottom: -1px;
debcf27Claude1872 transition: color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
2ce1d0bClaude1873 white-space: nowrap;
1874 }
debcf27Claude1875 .repo-nav a:hover { text-decoration: none; color: var(--text-strong); background: var(--bg-hover); }
2ce1d0bClaude1876 .repo-nav a.active {
debcf27Claude1877 color: var(--text-strong);
1878 font-weight: 600;
1879 }
1880 .repo-nav a.active::after {
1881 content: '';
1882 position: absolute;
1883 left: 14px;
1884 right: 14px;
1885 bottom: -1px;
1886 height: 2px;
1887 background: var(--accent-gradient);
1888 border-radius: 2px;
1889 }
1890 /* AI links in the right-side cluster — sparkle accent */
1891 .repo-nav-ai {
1892 color: var(--accent) !important;
1893 font-weight: 500;
1894 }
1895 .repo-nav-ai:hover {
1896 color: var(--accent-hover) !important;
1897 background: var(--accent-gradient-faint) !important;
1898 }
1899 .repo-nav-ai.active {
1900 color: var(--accent-hover) !important;
2ce1d0bClaude1901 font-weight: 600;
fc1817aClaude1902 }
1903
2ce1d0bClaude1904 .breadcrumb {
1905 display: flex;
debcf27Claude1906 gap: 6px;
2ce1d0bClaude1907 align-items: center;
debcf27Claude1908 margin-bottom: 18px;
2ce1d0bClaude1909 color: var(--text-muted);
1910 font-size: var(--t-sm);
1911 font-family: var(--font-mono);
debcf27Claude1912 font-feature-settings: var(--mono-feat);
2ce1d0bClaude1913 }
1914 .breadcrumb a { color: var(--text-link); font-weight: 500; }
1915 .breadcrumb a:hover { color: var(--accent-hover); }
debcf27Claude1916 .breadcrumb strong {
1917 color: var(--text-strong);
1918 font-weight: 600;
fc1817aClaude1919 }
1920
debcf27Claude1921 /* Page header — eyebrow + title + optional actions row.
1922 Use on dashboard, settings, admin, any "section landing" page. */
1923 .page-header {
1924 display: flex;
1925 align-items: flex-end;
1926 justify-content: space-between;
1927 gap: 16px;
1928 margin-bottom: 28px;
1929 padding-bottom: 20px;
1930 border-bottom: 1px solid var(--border-subtle);
1931 flex-wrap: wrap;
1932 }
1933 .page-header-text { flex: 1; min-width: 280px; }
1934 .page-header .eyebrow { margin-bottom: var(--s-2); }
1935 .page-header h1 {
1936 font-family: var(--font-display);
1937 font-size: clamp(24px, 3vw, 36px);
1938 line-height: 1.1;
1939 letter-spacing: -0.028em;
1940 margin-bottom: 6px;
1941 }
1942 .page-header p {
1943 color: var(--text-muted);
1944 font-size: var(--t-sm);
1945 line-height: 1.55;
1946 max-width: 640px;
1947 }
1948 .page-header-actions { display: flex; gap: 8px; align-items: center; }
fc1817aClaude1949
2ce1d0bClaude1950 /* ============================================================ */
1951 /* File browser table */
1952 /* ============================================================ */
1953 .file-table {
1954 width: 100%;
1955 border: 1px solid var(--border);
1956 border-radius: var(--r-md);
1957 overflow: hidden;
1958 background: var(--bg-elevated);
debcf27Claude1959 border-collapse: collapse;
2ce1d0bClaude1960 }
debcf27Claude1961 .file-table tr { border-bottom: 1px solid var(--border-subtle); transition: background var(--t-fast) var(--ease); }
fc1817aClaude1962 .file-table tr:last-child { border-bottom: none; }
debcf27Claude1963 .file-table td {
1964 padding: 9px 16px;
1965 font-size: var(--t-sm);
1966 font-family: var(--font-mono);
1967 font-feature-settings: var(--mono-feat);
1968 }
2ce1d0bClaude1969 .file-table tr:hover { background: var(--bg-hover); }
debcf27Claude1970 .file-icon {
1971 width: 22px;
1972 color: var(--text-faint);
1973 font-size: 13px;
1974 text-align: center;
1975 }
1976 .file-name a {
1977 color: var(--text);
1978 font-weight: 500;
1979 transition: color var(--t-fast) var(--ease);
1980 }
1981 .file-name a:hover { color: var(--accent); text-decoration: none; }
fc1817aClaude1982
2ce1d0bClaude1983 /* ============================================================ */
1984 /* Blob view */
1985 /* ============================================================ */
fc1817aClaude1986 .blob-view {
1987 border: 1px solid var(--border);
2ce1d0bClaude1988 border-radius: var(--r-md);
fc1817aClaude1989 overflow: hidden;
2ce1d0bClaude1990 background: var(--bg-elevated);
fc1817aClaude1991 }
1992 .blob-header {
1993 background: var(--bg-secondary);
2ce1d0bClaude1994 padding: 10px 16px;
fc1817aClaude1995 border-bottom: 1px solid var(--border);
2ce1d0bClaude1996 font-size: var(--t-sm);
fc1817aClaude1997 color: var(--text-muted);
06d5ffeClaude1998 display: flex;
1999 justify-content: space-between;
2000 align-items: center;
fc1817aClaude2001 }
2002 .blob-code {
2003 overflow-x: auto;
2004 font-family: var(--font-mono);
2ce1d0bClaude2005 font-size: var(--t-sm);
2006 line-height: 1.65;
fc1817aClaude2007 }
2008 .blob-code table { width: 100%; border-collapse: collapse; }
2009 .blob-code .line-num {
2010 width: 1%;
2ce1d0bClaude2011 min-width: 56px;
2012 padding: 0 14px;
fc1817aClaude2013 text-align: right;
2ce1d0bClaude2014 color: var(--text-faint);
fc1817aClaude2015 user-select: none;
2016 white-space: nowrap;
2017 border-right: 1px solid var(--border);
2018 }
2ce1d0bClaude2019 .blob-code .line-content { padding: 0 16px; white-space: pre; }
2020 .blob-code tr:hover { background: var(--bg-hover); }
fc1817aClaude2021
2ce1d0bClaude2022 /* ============================================================ */
2023 /* Commits + diffs */
2024 /* ============================================================ */
2025 .commit-list {
2026 border: 1px solid var(--border);
2027 border-radius: var(--r-md);
2028 overflow: hidden;
2029 background: var(--bg-elevated);
2030 }
fc1817aClaude2031 .commit-item {
2032 display: flex;
2033 justify-content: space-between;
2034 align-items: center;
debcf27Claude2035 padding: 14px 18px;
2036 border-bottom: 1px solid var(--border-subtle);
2ce1d0bClaude2037 transition: background var(--t-fast) var(--ease);
fc1817aClaude2038 }
2039 .commit-item:last-child { border-bottom: none; }
2ce1d0bClaude2040 .commit-item:hover { background: var(--bg-hover); }
debcf27Claude2041 .commit-message {
2042 font-size: var(--t-sm);
2043 font-weight: 500;
2044 line-height: 1.45;
2045 color: var(--text-strong);
2046 letter-spacing: -0.005em;
2047 }
2048 .commit-meta {
2049 font-family: var(--font-mono);
2050 font-size: 11px;
2051 color: var(--text-muted);
2052 margin-top: 5px;
2053 letter-spacing: 0.01em;
2054 }
fc1817aClaude2055 .commit-sha {
2056 font-family: var(--font-mono);
debcf27Claude2057 font-feature-settings: var(--mono-feat);
2058 font-size: 11px;
2059 padding: 4px 9px;
fc1817aClaude2060 background: var(--bg-tertiary);
2061 border: 1px solid var(--border);
2ce1d0bClaude2062 border-radius: var(--r-sm);
debcf27Claude2063 color: var(--accent);
2064 font-weight: 600;
2065 letter-spacing: 0.02em;
2ce1d0bClaude2066 transition: all var(--t-fast) var(--ease);
fc1817aClaude2067 }
debcf27Claude2068 .commit-sha:hover {
2069 border-color: rgba(140,109,255,0.40);
2070 background: var(--accent-gradient-faint);
2071 color: var(--accent-hover);
2072 text-decoration: none;
fc1817aClaude2073 }
2074
2075 .diff-view { margin-top: 16px; }
2076 .diff-file {
2077 border: 1px solid var(--border);
2ce1d0bClaude2078 border-radius: var(--r-md);
fc1817aClaude2079 margin-bottom: 16px;
2080 overflow: hidden;
2ce1d0bClaude2081 background: var(--bg-elevated);
fc1817aClaude2082 }
2083 .diff-file-header {
2084 background: var(--bg-secondary);
2ce1d0bClaude2085 padding: 10px 16px;
fc1817aClaude2086 border-bottom: 1px solid var(--border);
2087 font-family: var(--font-mono);
2ce1d0bClaude2088 font-size: var(--t-sm);
2089 font-weight: 500;
fc1817aClaude2090 }
2091 .diff-content {
2092 overflow-x: auto;
2093 font-family: var(--font-mono);
2ce1d0bClaude2094 font-size: var(--t-sm);
2095 line-height: 1.65;
fc1817aClaude2096 }
958d26aClaude2097 .diff-content .line-add { background: rgba(52,211,153,0.10); color: var(--green); }
2098 .diff-content .line-del { background: rgba(248,113,113,0.08); color: var(--red); }
2099 .diff-content .line-hunk { background: rgba(140,109,255,0.06); color: var(--text-link); }
2ce1d0bClaude2100 .diff-content .line { padding: 0 16px; white-space: pre; display: block; }
fc1817aClaude2101
2102 .stat-add { color: var(--green); font-weight: 600; }
2103 .stat-del { color: var(--red); font-weight: 600; }
2104
2ce1d0bClaude2105 /* ============================================================ */
2106 /* Empty state */
2107 /* ============================================================ */
fc1817aClaude2108 .empty-state {
2109 text-align: center;
958d26aClaude2110 padding: 96px 24px;
fc1817aClaude2111 color: var(--text-muted);
2ce1d0bClaude2112 border: 1px dashed var(--border);
2113 border-radius: var(--r-lg);
958d26aClaude2114 background:
2115 radial-gradient(60% 60% at 50% 0%, rgba(140,109,255,0.05), transparent 70%),
2116 var(--bg-elevated);
2117 position: relative;
2118 overflow: hidden;
2119 }
2120 .empty-state::before {
2121 content: '';
2122 position: absolute;
2123 inset: 0;
2124 background-image: radial-gradient(rgba(255,255,255,0.025) 1px, transparent 1px);
2125 background-size: 24px 24px;
2126 opacity: 0.5;
2127 pointer-events: none;
2128 mask-image: radial-gradient(ellipse at center, #000 30%, transparent 70%);
2129 -webkit-mask-image: radial-gradient(ellipse at center, #000 30%, transparent 70%);
2130 }
2131 :root[data-theme='light'] .empty-state::before {
2132 background-image: radial-gradient(rgba(15,16,28,0.08) 1px, transparent 1px);
fc1817aClaude2133 }
958d26aClaude2134 .empty-state > * { position: relative; z-index: 1; }
2ce1d0bClaude2135 .empty-state h2 {
958d26aClaude2136 font-size: var(--t-xl);
2ce1d0bClaude2137 margin-bottom: 8px;
958d26aClaude2138 color: var(--text-strong);
2139 letter-spacing: -0.022em;
2ce1d0bClaude2140 }
958d26aClaude2141 .empty-state p { font-size: var(--t-md); max-width: 480px; margin: 0 auto; line-height: 1.55; }
fc1817aClaude2142 .empty-state pre {
2143 text-align: left;
2144 display: inline-block;
2145 background: var(--bg-secondary);
958d26aClaude2146 padding: 18px 24px;
2147 border-radius: var(--r-md);
fc1817aClaude2148 border: 1px solid var(--border);
2149 font-family: var(--font-mono);
958d26aClaude2150 font-feature-settings: var(--mono-feat);
2ce1d0bClaude2151 font-size: var(--t-sm);
958d26aClaude2152 margin-top: 24px;
fc1817aClaude2153 line-height: 1.8;
2ce1d0bClaude2154 color: var(--text);
958d26aClaude2155 box-shadow: var(--elev-1);
fc1817aClaude2156 }
2157
2ce1d0bClaude2158 /* ============================================================ */
2159 /* Badges */
2160 /* ============================================================ */
fc1817aClaude2161 .badge {
2ce1d0bClaude2162 display: inline-flex;
2163 align-items: center;
2164 gap: 4px;
2165 padding: 2px 9px;
2166 border-radius: var(--r-full);
2167 font-size: var(--t-xs);
fc1817aClaude2168 font-weight: 500;
2169 background: var(--bg-tertiary);
2170 border: 1px solid var(--border);
2171 color: var(--text-muted);
2ce1d0bClaude2172 line-height: 1.5;
fc1817aClaude2173 }
2174
2ce1d0bClaude2175 /* ============================================================ */
2176 /* Branch dropdown */
2177 /* ============================================================ */
fc1817aClaude2178 .branch-selector {
2179 display: inline-flex;
2180 align-items: center;
2ce1d0bClaude2181 gap: 6px;
2182 padding: 6px 12px;
2183 background: var(--bg-elevated);
fc1817aClaude2184 border: 1px solid var(--border);
2ce1d0bClaude2185 border-radius: var(--r-sm);
2186 font-size: var(--t-sm);
fc1817aClaude2187 color: var(--text);
2188 margin-bottom: 12px;
2ce1d0bClaude2189 transition: border-color var(--t-fast) var(--ease);
fc1817aClaude2190 }
2ce1d0bClaude2191 .branch-selector:hover { border-color: var(--border-strong); }
fc1817aClaude2192
06d5ffeClaude2193 .branch-dropdown {
2194 position: relative;
2195 display: inline-block;
2196 margin-bottom: 12px;
2197 }
2198 .branch-dropdown-content {
2199 display: none;
2200 position: absolute;
2ce1d0bClaude2201 top: calc(100% + 4px);
06d5ffeClaude2202 left: 0;
2203 z-index: 10;
2ce1d0bClaude2204 min-width: 220px;
2205 background: var(--bg-elevated);
2206 border: 1px solid var(--border-strong);
2207 border-radius: var(--r-md);
2208 overflow: hidden;
2209 box-shadow: var(--elev-3);
06d5ffeClaude2210 }
2211 .branch-dropdown:hover .branch-dropdown-content,
2212 .branch-dropdown:focus-within .branch-dropdown-content { display: block; }
2213 .branch-dropdown-content a {
2214 display: block;
2ce1d0bClaude2215 padding: 9px 14px;
2216 font-size: var(--t-sm);
06d5ffeClaude2217 color: var(--text);
2218 border-bottom: 1px solid var(--border);
2ce1d0bClaude2219 transition: background var(--t-fast) var(--ease);
06d5ffeClaude2220 }
2221 .branch-dropdown-content a:last-child { border-bottom: none; }
2ce1d0bClaude2222 .branch-dropdown-content a:hover { background: var(--bg-hover); text-decoration: none; }
2223 .branch-dropdown-content a.active-branch { color: var(--text-link); font-weight: 600; background: var(--accent-gradient-faint); }
06d5ffeClaude2224
2ce1d0bClaude2225 /* ============================================================ */
2226 /* Card grid */
2227 /* ============================================================ */
2228 .card-grid {
2229 display: grid;
2230 grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
2231 gap: 16px;
2232 }
fc1817aClaude2233 .card {
2234 border: 1px solid var(--border);
2ce1d0bClaude2235 border-radius: var(--r-md);
958d26aClaude2236 padding: 20px;
2ce1d0bClaude2237 background: var(--bg-elevated);
2238 transition:
958d26aClaude2239 border-color var(--t-base) var(--ease),
2240 transform var(--t-base) var(--ease-out-quart),
2ce1d0bClaude2241 box-shadow var(--t-base) var(--ease);
2242 position: relative;
2243 overflow: hidden;
958d26aClaude2244 isolation: isolate;
2245 }
2246 .card::before {
2247 content: '';
2248 position: absolute;
2249 inset: 0;
2250 background: linear-gradient(135deg, rgba(140,109,255,0.06), transparent 50%);
2251 opacity: 0;
2252 transition: opacity var(--t-base) var(--ease);
2253 pointer-events: none;
2254 z-index: -1;
2ce1d0bClaude2255 }
2256 .card:hover {
2257 border-color: var(--border-strong);
2258 box-shadow: var(--elev-2);
958d26aClaude2259 transform: translateY(-2px);
2ce1d0bClaude2260 }
958d26aClaude2261 .card:hover::before { opacity: 1; }
2262 .card h3 { font-size: var(--t-md); margin-bottom: 6px; letter-spacing: -0.012em; }
2ce1d0bClaude2263 .card h3 a { color: var(--text); font-weight: 600; }
2264 .card h3 a:hover { color: var(--text-link); }
2265 .card p { font-size: var(--t-sm); color: var(--text-muted); line-height: 1.5; }
2266 .card-meta {
2267 display: flex;
2268 gap: 14px;
2269 margin-top: 14px;
2270 padding-top: 14px;
2271 border-top: 1px solid var(--border);
2272 font-size: var(--t-xs);
2273 color: var(--text-muted);
2274 }
2275 .card-meta span { display: flex; align-items: center; gap: 5px; }
2276
2277 /* ============================================================ */
2278 /* Stat card / box */
2279 /* ============================================================ */
2280 .stat-card {
2281 background: var(--bg-elevated);
2282 border: 1px solid var(--border);
2283 border-radius: var(--r-md);
fc1817aClaude2284 padding: 16px;
2ce1d0bClaude2285 transition: border-color var(--t-fast) var(--ease);
2286 }
2287 .stat-card:hover { border-color: var(--border-strong); }
2288 .stat-label {
2289 font-size: var(--t-xs);
2290 color: var(--text-muted);
2291 text-transform: uppercase;
2292 letter-spacing: 0.06em;
2293 font-weight: 500;
2294 }
2295 .stat-value {
2296 font-size: var(--t-xl);
2297 font-weight: 700;
2298 margin-top: 4px;
2299 letter-spacing: -0.025em;
2300 color: var(--text);
2301 font-feature-settings: 'tnum';
fc1817aClaude2302 }
06d5ffeClaude2303
2ce1d0bClaude2304 /* ============================================================ */
2305 /* Star button */
2306 /* ============================================================ */
06d5ffeClaude2307 .star-btn {
2308 display: inline-flex;
2309 align-items: center;
2310 gap: 6px;
2ce1d0bClaude2311 padding: 5px 12px;
2312 background: var(--bg-elevated);
06d5ffeClaude2313 border: 1px solid var(--border);
2ce1d0bClaude2314 border-radius: var(--r-sm);
06d5ffeClaude2315 color: var(--text);
2ce1d0bClaude2316 font-size: var(--t-sm);
2317 font-weight: 500;
06d5ffeClaude2318 cursor: pointer;
2ce1d0bClaude2319 transition: all var(--t-fast) var(--ease);
2320 }
2321 .star-btn:hover { background: var(--bg-surface); border-color: var(--border-strong); text-decoration: none; }
2322 .star-btn.starred {
2323 color: var(--yellow);
958d26aClaude2324 border-color: rgba(251,191,36,0.4);
2325 background: rgba(251,191,36,0.08);
06d5ffeClaude2326 }
2327
2ce1d0bClaude2328 /* ============================================================ */
2329 /* User profile */
2330 /* ============================================================ */
06d5ffeClaude2331 .user-profile {
2332 display: flex;
2333 gap: 32px;
2334 margin-bottom: 32px;
2ce1d0bClaude2335 padding: 24px;
2336 background: var(--bg-elevated);
2337 border: 1px solid var(--border);
2338 border-radius: var(--r-lg);
06d5ffeClaude2339 }
2340 .user-avatar {
2341 width: 96px;
2342 height: 96px;
2ce1d0bClaude2343 border-radius: var(--r-full);
2344 background: var(--accent-gradient-soft);
2345 border: 1px solid var(--border-strong);
06d5ffeClaude2346 display: flex;
2347 align-items: center;
2348 justify-content: center;
2ce1d0bClaude2349 font-size: 36px;
2350 font-weight: 600;
2351 color: var(--text);
06d5ffeClaude2352 flex-shrink: 0;
2ce1d0bClaude2353 letter-spacing: -0.02em;
06d5ffeClaude2354 }
2ce1d0bClaude2355 .user-info h2 { font-size: var(--t-xl); margin-bottom: 2px; letter-spacing: -0.025em; }
2356 .user-info .username { font-size: var(--t-md); color: var(--text-muted); }
2357 .user-info .bio { font-size: var(--t-sm); color: var(--text-muted); margin-top: 10px; line-height: 1.55; }
06d5ffeClaude2358
2ce1d0bClaude2359 /* ============================================================ */
2360 /* New repo form */
2361 /* ============================================================ */
2362 .new-repo-form { max-width: 640px; }
2363 .new-repo-form h2 { margin-bottom: 24px; font-size: var(--t-xl); letter-spacing: -0.025em; }
2364 .visibility-options { display: flex; gap: 12px; margin-bottom: 20px; }
06d5ffeClaude2365 .visibility-option {
2366 flex: 1;
2ce1d0bClaude2367 padding: 16px;
06d5ffeClaude2368 border: 1px solid var(--border);
2ce1d0bClaude2369 border-radius: var(--r-md);
2370 background: var(--bg-elevated);
06d5ffeClaude2371 cursor: pointer;
2ce1d0bClaude2372 text-align: left;
2373 transition: all var(--t-fast) var(--ease);
2374 }
2375 .visibility-option:hover { border-color: var(--border-strong); background: var(--bg-surface); }
2376 .visibility-option:has(input:checked) {
2377 border-color: var(--accent);
2378 background: var(--accent-gradient-faint);
2379 box-shadow: 0 0 0 1px var(--accent);
06d5ffeClaude2380 }
2381 .visibility-option input { display: none; }
2ce1d0bClaude2382 .visibility-option .vis-label { font-size: var(--t-sm); font-weight: 600; margin-bottom: 4px; }
2383 .visibility-option .vis-desc { font-size: var(--t-xs); color: var(--text-muted); }
79136bbClaude2384
2ce1d0bClaude2385 /* ============================================================ */
2386 /* Issues */
2387 /* ============================================================ */
2388 .issue-tabs { display: flex; gap: 4px; }
2389 .issue-tabs a {
2390 color: var(--text-muted);
2391 font-size: var(--t-sm);
2392 font-weight: 500;
2393 padding: 6px 12px;
2394 border-radius: var(--r-sm);
2395 transition: all var(--t-fast) var(--ease);
2396 }
2397 .issue-tabs a:hover { color: var(--text); background: var(--bg-hover); text-decoration: none; }
2398 .issue-tabs a.active { color: var(--text); background: var(--bg-elevated); }
79136bbClaude2399
2ce1d0bClaude2400 .issue-list {
2401 border: 1px solid var(--border);
2402 border-radius: var(--r-md);
2403 overflow: hidden;
2404 background: var(--bg-elevated);
2405 }
79136bbClaude2406 .issue-item {
2407 display: flex;
2ce1d0bClaude2408 gap: 14px;
79136bbClaude2409 align-items: flex-start;
2ce1d0bClaude2410 padding: 14px 16px;
79136bbClaude2411 border-bottom: 1px solid var(--border);
2ce1d0bClaude2412 transition: background var(--t-fast) var(--ease);
79136bbClaude2413 }
2414 .issue-item:last-child { border-bottom: none; }
2ce1d0bClaude2415 .issue-item:hover { background: var(--bg-hover); }
2416 .issue-state-icon {
2417 font-size: 14px;
2418 padding-top: 2px;
2419 width: 18px;
2420 height: 18px;
2421 display: inline-flex;
2422 align-items: center;
2423 justify-content: center;
2424 border-radius: var(--r-full);
2425 flex-shrink: 0;
2426 }
79136bbClaude2427 .state-open { color: var(--green); }
958d26aClaude2428 .state-closed { color: #b69dff; }
2ce1d0bClaude2429 .issue-title {
debcf27Claude2430 font-family: var(--font-display);
2431 font-size: var(--t-md);
2ce1d0bClaude2432 font-weight: 600;
debcf27Claude2433 line-height: 1.35;
2434 letter-spacing: -0.012em;
2435 }
2436 .issue-title a { color: var(--text-strong); transition: color var(--t-fast) var(--ease); }
2437 .issue-title a:hover { color: var(--accent); text-decoration: none; }
2438 .issue-meta {
2439 font-family: var(--font-mono);
2440 font-size: 11px;
2441 color: var(--text-muted);
2442 margin-top: 5px;
2443 letter-spacing: 0.01em;
2ce1d0bClaude2444 }
79136bbClaude2445
2446 .issue-badge {
2447 display: inline-flex;
2448 align-items: center;
2ce1d0bClaude2449 gap: 6px;
79136bbClaude2450 padding: 4px 12px;
2ce1d0bClaude2451 border-radius: var(--r-full);
2452 font-size: var(--t-sm);
79136bbClaude2453 font-weight: 500;
2ce1d0bClaude2454 line-height: 1.4;
79136bbClaude2455 }
958d26aClaude2456 .badge-open { background: rgba(52,211,153,0.10); color: var(--green); border: 1px solid rgba(52,211,153,0.35); }
2457 .badge-closed { background: rgba(182,157,255,0.10); color: #b69dff; border: 1px solid rgba(182,157,255,0.35); }
2458 .badge-merged { background: rgba(140,109,255,0.10); color: var(--accent); border: 1px solid rgba(140,109,255,0.35); }
2ce1d0bClaude2459 .state-merged { color: var(--accent); }
958d26aClaude2460 .ai-review { border-color: var(--accent); box-shadow: 0 0 0 1px rgba(140,109,255,0.3); }
79136bbClaude2461
2ce1d0bClaude2462 .issue-detail { max-width: 920px; }
79136bbClaude2463 .issue-comment-box {
2464 border: 1px solid var(--border);
2ce1d0bClaude2465 border-radius: var(--r-md);
79136bbClaude2466 margin-bottom: 16px;
2467 overflow: hidden;
2ce1d0bClaude2468 background: var(--bg-elevated);
79136bbClaude2469 }
2470 .comment-header {
2471 background: var(--bg-secondary);
2ce1d0bClaude2472 padding: 10px 16px;
79136bbClaude2473 border-bottom: 1px solid var(--border);
2ce1d0bClaude2474 font-size: var(--t-sm);
79136bbClaude2475 color: var(--text-muted);
2476 }
2477
2ce1d0bClaude2478 /* ============================================================ */
2479 /* Panel — flexible container used across many pages */
2480 /* ============================================================ */
2481 .panel {
2482 background: var(--bg-elevated);
2483 border: 1px solid var(--border);
2484 border-radius: var(--r-md);
2485 overflow: hidden;
2486 }
2487 .panel-item {
2488 display: flex;
2489 align-items: center;
2490 gap: 12px;
2491 padding: 12px 16px;
79136bbClaude2492 border-bottom: 1px solid var(--border);
2ce1d0bClaude2493 transition: background var(--t-fast) var(--ease);
2494 }
2495 .panel-item:last-child { border-bottom: none; }
2496 .panel-item:hover { background: var(--bg-hover); }
2497 .panel-empty {
2498 padding: 24px;
2499 text-align: center;
79136bbClaude2500 color: var(--text-muted);
2ce1d0bClaude2501 font-size: var(--t-sm);
79136bbClaude2502 }
2503
2ce1d0bClaude2504 /* ============================================================ */
2505 /* Search */
2506 /* ============================================================ */
79136bbClaude2507 .search-results .diff-file { margin-bottom: 12px; }
16b325cClaude2508
2ce1d0bClaude2509 /* ============================================================ */
2510 /* Timeline */
2511 /* ============================================================ */
2512 .timeline { position: relative; padding-left: 28px; }
16b325cClaude2513 .timeline::before {
2514 content: '';
2515 position: absolute;
2516 left: 4px;
2517 top: 8px;
2518 bottom: 8px;
2519 width: 2px;
2ce1d0bClaude2520 background: linear-gradient(180deg, var(--border) 0%, transparent 100%);
16b325cClaude2521 }
2ce1d0bClaude2522 .timeline-item { position: relative; padding-bottom: 16px; }
16b325cClaude2523 .timeline-dot {
2524 position: absolute;
2ce1d0bClaude2525 left: -28px;
2526 top: 8px;
2527 width: 12px;
2528 height: 12px;
2529 border-radius: var(--r-full);
2530 background: var(--accent-gradient);
16b325cClaude2531 border: 2px solid var(--bg);
2ce1d0bClaude2532 box-shadow: 0 0 0 1px var(--border-strong);
16b325cClaude2533 }
2534 .timeline-content {
2ce1d0bClaude2535 background: var(--bg-elevated);
16b325cClaude2536 border: 1px solid var(--border);
2ce1d0bClaude2537 border-radius: var(--r-md);
2538 padding: 14px 16px;
16b325cClaude2539 }
f1ab587Claude2540
2ce1d0bClaude2541 /* ============================================================ */
2542 /* Toggle switch */
2543 /* ============================================================ */
f1ab587Claude2544 .toggle-switch {
2545 position: relative;
2546 display: inline-block;
2ce1d0bClaude2547 width: 40px;
2548 height: 22px;
f1ab587Claude2549 flex-shrink: 0;
2550 margin-left: 16px;
2551 }
2552 .toggle-switch input { opacity: 0; width: 0; height: 0; }
2553 .toggle-slider {
2554 position: absolute;
2555 cursor: pointer;
2556 top: 0; left: 0; right: 0; bottom: 0;
2557 background: var(--bg-tertiary);
2558 border: 1px solid var(--border);
2ce1d0bClaude2559 border-radius: var(--r-full);
2560 transition: all var(--t-base) var(--ease);
f1ab587Claude2561 }
2562 .toggle-slider::before {
2563 content: '';
2564 position: absolute;
2ce1d0bClaude2565 height: 16px;
2566 width: 16px;
f1ab587Claude2567 left: 2px;
2568 bottom: 2px;
2569 background: var(--text-muted);
2ce1d0bClaude2570 border-radius: var(--r-full);
2571 transition: all var(--t-base) var(--ease);
f1ab587Claude2572 }
2573 .toggle-switch input:checked + .toggle-slider {
2ce1d0bClaude2574 background: var(--accent-gradient);
2575 border-color: transparent;
958d26aClaude2576 box-shadow: 0 0 0 1px rgba(140,109,255,0.4);
f1ab587Claude2577 }
2578 .toggle-switch input:checked + .toggle-slider::before {
2ce1d0bClaude2579 transform: translateX(18px);
f1ab587Claude2580 background: #fff;
2581 }
ef8d378Claude2582
2583 /* ============================================================
2584 * 2026 polish layer (purely additive — no layout changes).
2585 * Improves typography rendering, focus states, hover affordances,
2586 * and adds the gradient brand cue to primary buttons. Anything
2587 * that could alter dimensions stays in the rules above.
2588 * ============================================================ */
2589 html { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
2590 body { letter-spacing: -0.005em; font-feature-settings: 'cv11', 'ss01', 'ss03'; }
2591 *::selection { background: rgba(168,85,247,0.35); color: var(--text); }
2592
2593 h1, h2, h3, h4 { letter-spacing: -0.018em; }
2594 h1 { letter-spacing: -0.025em; }
2595
2596 /* Smoother colour transitions everywhere links live */
2597 a { transition: color 120ms cubic-bezier(0.16,1,0.3,1); }
2598
2599 /* Buttons: focus rings + smoother transitions; primary gets the gradient */
2600 .btn {
2601 transition:
2602 background 120ms cubic-bezier(0.16,1,0.3,1),
2603 border-color 120ms cubic-bezier(0.16,1,0.3,1),
2604 transform 120ms cubic-bezier(0.16,1,0.3,1),
2605 box-shadow 120ms cubic-bezier(0.16,1,0.3,1);
2606 }
2607 .btn:active { transform: translateY(0.5px); }
2608 .btn:focus-visible {
2609 outline: none;
2610 box-shadow: 0 0 0 3px rgba(168,85,247,0.30);
2611 }
2612 .btn-primary {
2613 background: linear-gradient(135deg, #a855f7 0%, #06b6d4 100%);
2614 border-color: transparent;
2615 box-shadow:
2616 inset 0 1px 0 rgba(255,255,255,0.15),
2617 0 1px 2px rgba(168,85,247,0.25);
2618 }
2619 .btn-primary:hover {
2620 background: linear-gradient(135deg, #b766f8 0%, #22cce0 100%);
2621 filter: none;
2622 box-shadow:
2623 inset 0 1px 0 rgba(255,255,255,0.20),
2624 0 4px 12px rgba(168,85,247,0.30);
2625 }
2626
2627 /* Inputs: cleaner focus ring + hover */
2628 .form-group input:hover,
2629 .form-group textarea:hover,
2630 .form-group select:hover {
2631 border-color: rgba(255,255,255,0.14);
2632 }
2633 .form-group input:focus,
2634 .form-group textarea:focus,
2635 .form-group select:focus {
2636 border-color: rgba(168,85,247,0.55);
2637 box-shadow: 0 0 0 3px rgba(168,85,247,0.22);
2638 }
2639 :root[data-theme='light'] .form-group input:hover,
2640 :root[data-theme='light'] .form-group textarea:hover,
2641 :root[data-theme='light'] .form-group select:hover {
2642 border-color: rgba(0,0,0,0.18);
2643 }
2644
2645 /* Cards: subtle hover lift */
2646 .card {
2647 transition:
2648 border-color 160ms cubic-bezier(0.16,1,0.3,1),
2649 transform 160ms cubic-bezier(0.16,1,0.3,1),
2650 box-shadow 200ms cubic-bezier(0.16,1,0.3,1);
2651 }
2652 .card:hover {
2653 border-color: rgba(255,255,255,0.18);
2654 transform: translateY(-1px);
2655 box-shadow: 0 8px 24px rgba(0,0,0,0.30);
2656 }
2657 :root[data-theme='light'] .card:hover {
2658 border-color: rgba(0,0,0,0.18);
2659 box-shadow: 0 8px 24px rgba(0,0,0,0.08);
2660 }
2661
2662 /* Issue / commit / panel rows: smoother hover */
2663 .issue-item, .commit-item {
2664 transition: background 120ms cubic-bezier(0.16,1,0.3,1);
2665 }
2666 .repo-nav a {
2667 transition: color 120ms cubic-bezier(0.16,1,0.3,1),
2668 border-bottom-color 120ms cubic-bezier(0.16,1,0.3,1);
2669 }
2670 .nav-link {
2671 transition: color 120ms cubic-bezier(0.16,1,0.3,1);
2672 }
2673
2674 /* Auth card: subtle elevation so register/login feel premium */
2675 .auth-container {
2676 background: var(--bg-secondary);
2677 border: 1px solid var(--border);
2678 border-radius: var(--r-lg, 12px);
2679 padding: 32px;
2680 box-shadow: 0 4px 16px rgba(0,0,0,0.30), 0 0 0 1px var(--border);
2681 }
2682 :root[data-theme='light'] .auth-container {
2683 box-shadow: 0 4px 16px rgba(0,0,0,0.06), 0 0 0 1px var(--border);
2684 }
2685 .auth-container h2 { letter-spacing: -0.025em; }
2686
2687 /* Empty state: dashed border, generous padding */
2688 .empty-state {
2689 border: 1px dashed var(--border);
2690 border-radius: var(--r-lg, 12px);
2691 background: var(--bg);
2692 }
2693
2694 /* Badges + commit-sha: smoother transition */
2695 .commit-sha, .badge { transition: all 120ms cubic-bezier(0.16,1,0.3,1); }
2696
2697 /* Gradient text utility — matches landing's accent treatment */
2698 .gradient-text {
2699 background: linear-gradient(135deg, #a855f7 0%, #06b6d4 100%);
2700 -webkit-background-clip: text;
2701 background-clip: text;
2702 -webkit-text-fill-color: transparent;
2703 }
2704
2705 /* Custom scrollbars (subtle, themed) */
2706 ::-webkit-scrollbar { width: 10px; height: 10px; }
2707 ::-webkit-scrollbar-track { background: transparent; }
2708 ::-webkit-scrollbar-thumb {
2709 background: rgba(255,255,255,0.06);
2710 border: 2px solid var(--bg);
2711 border-radius: 9999px;
2712 }
2713 ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.14); }
2714 :root[data-theme='light'] ::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.10); }
2715 :root[data-theme='light'] ::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,0.18); }
2716
2717 /* Honour reduced-motion preference */
2718 @media (prefers-reduced-motion: reduce) {
2719 *, *::before, *::after {
2720 animation-duration: 0.01ms !important;
2721 animation-iteration-count: 1 !important;
2722 transition-duration: 0.01ms !important;
2723 }
2724 }
c63b860Claude2725
2726 /* Block O3 — visual coherence additive rules. */
2727 .card.card-p-none { padding: 0; }
2728 .card.card-p-sm { padding: var(--space-3); }
2729 .card.card-p-md { padding: var(--space-4); }
2730 .card.card-p-lg { padding: var(--space-6); }
2731 .card.card-elevated { box-shadow: var(--elev-2); }
2732 .card.card-gradient {
2733 background:
2734 linear-gradient(135deg, rgba(140,109,255,0.05), transparent 60%),
2735 var(--bg-elevated);
2736 }
2737 .card.card-gradient::before { opacity: 1; }
2738 .notice {
2739 padding: var(--space-3) var(--space-4);
2740 border-radius: var(--radius-md);
2741 border: 1px solid var(--border);
2742 background: var(--bg-elevated);
2743 color: var(--text);
2744 font-size: var(--font-size-sm);
2745 margin-bottom: var(--space-6);
2746 line-height: var(--leading-normal);
2747 }
2748 .notice-info { border-color: rgba(96,165,250,0.40); background: rgba(96,165,250,0.08); color: var(--text); }
2749 .notice-success { border-color: var(--green); background: rgba(52,211,153,0.08); color: var(--text); }
2750 .notice-warn { border-color: var(--yellow); background: rgba(251,191,36,0.10); color: var(--yellow); }
2751 .notice-error { border-color: var(--red); background: rgba(248,113,113,0.10); color: var(--red); }
2752 .notice-accent { border-color: var(--accent); background: rgba(140,109,255,0.10); color: var(--text); }
2753 .email-preview {
2754 padding: var(--space-5);
2755 background: #fff;
2756 color: #111;
2757 border-radius: var(--radius-md);
2758 }
2759 .code-block {
2760 margin: var(--space-2) 0 0;
2761 padding: var(--space-3);
2762 background: var(--bg-tertiary);
2763 border: 1px solid var(--border-subtle);
2764 border-radius: var(--radius-sm);
2765 font-family: var(--font-mono);
2766 font-size: var(--font-size-xs);
2767 line-height: var(--leading-normal);
2768 overflow-x: auto;
2769 }
2770 .status-pill-operational {
2771 display: inline-flex;
2772 align-items: center;
2773 padding: var(--space-1) var(--space-3);
2774 border-radius: var(--radius-full);
2775 font-size: var(--font-size-xs);
2776 font-weight: 600;
2777 background: rgba(52,211,153,0.15);
2778 color: var(--green);
2779 }
2780 .api-tag {
2781 display: inline-flex;
2782 align-items: center;
2783 font-size: var(--font-size-xs);
2784 padding: 2px var(--space-2);
2785 border-radius: var(--radius-sm);
2786 font-family: var(--font-mono);
2787 }
2788 .api-tag-auth { background: rgba(96,165,250,0.15); color: var(--accent); }
2789 .api-tag-scope { background: rgba(52,211,153,0.15); color: var(--green); }
2790 .stat-number {
2791 font-size: var(--font-size-xl);
2792 font-weight: 700;
2793 color: var(--text-strong);
2794 line-height: var(--leading-tight);
2795 }
2796 .stat-number-accent { color: var(--accent); }
2797 .stat-number-blue { color: var(--blue); }
2798 .stat-number-purple { color: var(--text-link); }
2799 footer .footer-tag-sub {
2800 margin-top: var(--space-2);
2801 font-size: var(--font-size-xs);
2802 color: var(--text-faint);
2803 line-height: var(--leading-normal);
2804 }
2805 footer .footer-version-pill {
2806 display: inline-flex;
2807 align-items: center;
2808 gap: var(--space-2);
2809 padding: var(--space-1) var(--space-3);
2810 border-radius: var(--radius-full);
2811 border: 1px solid var(--border);
2812 background: var(--bg-elevated);
2813 color: var(--text-faint);
2814 font-family: var(--font-mono);
2815 font-size: var(--font-size-xs);
2816 cursor: help;
2817 }
2818 footer .footer-banner {
2819 max-width: 1240px;
2820 margin: var(--space-6) auto 0;
2821 padding: var(--space-3) var(--space-4);
2822 border-radius: var(--radius-md);
2823 border: 1px solid var(--border);
2824 background: var(--bg-elevated);
2825 color: var(--text);
2826 font-family: var(--font-mono);
2827 font-size: var(--font-size-xs);
2828 letter-spacing: 0.04em;
2829 text-transform: uppercase;
2830 text-align: center;
2831 }
2832 footer .footer-banner-info { border-color: rgba(96,165,250,0.40); color: var(--blue); }
2833 footer .footer-banner-warn { border-color: rgba(251,191,36,0.40); color: var(--yellow); }
2834 footer .footer-banner-error { border-color: rgba(248,113,113,0.45); color: var(--red); }
dc26881CC LABS App2835
2836 /* ============================================================ */
2837 /* Block U4 — cross-document view transitions. */
2838 /* Chrome 126+, Edge, Safari 18.2+ get a soft 200ms fade on */
2839 /* every same-origin navigation. Older browsers ignore these */
2840 /* rules entirely — no JS shim, no breakage. */
2841 /* prefers-reduced-motion disables the animation honourably. */
2842 /* ============================================================ */
2843 @view-transition {
2844 navigation: auto;
2845 }
2846 ::view-transition-old(root),
2847 ::view-transition-new(root) {
2848 animation-duration: 200ms;
2849 animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
2850 }
2851 ::view-transition-old(root) {
2852 animation-name: vt-fade-out;
2853 }
2854 ::view-transition-new(root) {
2855 animation-name: vt-fade-in;
2856 }
2857 @keyframes vt-fade-out {
2858 to { opacity: 0; }
2859 }
2860 @keyframes vt-fade-in {
2861 from { opacity: 0; }
2862 }
2863 @media (prefers-reduced-motion: reduce) {
2864 ::view-transition-old(root),
2865 ::view-transition-new(root) {
2866 animation-duration: 0s;
2867 }
2868 }
2869 /* The transition system picks up its root subject from this rule. */
2870 body {
2871 view-transition-name: root;
2872 }
fc1817aClaude2873`;