Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
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.tsxBlame3073 lines · 4 contributors
fc1817aClaude1import type { FC, PropsWithChildren } from "hono/jsx";
06d5ffeClaude2import type { User } from "../db/schema";
3import { hljsThemeCss } from "../lib/highlight";
45e31d0Claude4import { clientJs } from "./client-js";
05cdb85Claude5import { getBuildInfo } from "../lib/build-info";
fc1817aClaude6
06d5ffeClaude7export const Layout: FC<
3ef4c9dClaude8 PropsWithChildren<{
9 title?: string;
10 user?: User | null;
11 notificationCount?: number;
6fc53bdClaude12 theme?: "dark" | "light";
5f2e749Claude13 // Block L10 — additive SEO + Open Graph fields. All optional;
14 // omission preserves the prior render exactly (regression-safe).
15 fullTitle?: string;
16 description?: string;
17 ogTitle?: string;
18 ogDescription?: string;
19 ogType?: string;
20 twitterCard?: "summary" | "summary_large_image";
c63b860Claude21 // Block O3 — site-wide footer banner stripe. When non-empty,
22 // renders below the footer-bottom row; wired from
23 // admin.system_flags.site_banner_text.
24 siteBannerText?: string;
25 siteBannerLevel?: "info" | "warn" | "error";
3ef4c9dClaude26 }>
5f2e749Claude27> = ({
28 children,
29 title,
30 user,
31 notificationCount,
32 theme,
33 fullTitle,
34 description,
35 ogTitle,
36 ogDescription,
37 ogType,
38 twitterCard,
c63b860Claude39 siteBannerText,
40 siteBannerLevel,
5f2e749Claude41}) => {
81201ccTest User42 // Default to "light" — feedback from operators was the dark default
43 // felt too gamer-ish and not what senior platform engineers expect from
44 // a tool they'd evaluate alongside Vercel / Linear / Stripe. Users who
45 // explicitly want dark can flip via the theme toggle (cookie persists).
46 const initialTheme = theme === "dark" ? "dark" : "light";
05cdb85Claude47 const build = getBuildInfo();
5f2e749Claude48 // L10 — when `fullTitle` is provided, use it verbatim (no " — gluecron"
49 // suffix); otherwise fall back to the existing `title` + suffix behaviour.
50 const renderedTitle = fullTitle
51 ? fullTitle
52 : title
53 ? `${title} — gluecron`
54 : "gluecron";
fc1817aClaude55 return (
6fc53bdClaude56 <html lang="en" data-theme={initialTheme}>
fc1817aClaude57 <head>
58 <meta charset="UTF-8" />
59 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
eae38d1Claude60 <meta name="theme-color" content="#0d1117" />
3a5755eClaude61 {/* 2026 polish — load Inter + Inter Tight + JetBrains Mono for
62 crisp modern typography. `preconnect` keeps the handshake
63 cost off the critical path; `display=swap` means we never
64 block first paint waiting on fonts. */}
65 <link rel="preconnect" href="https://fonts.googleapis.com" />
66 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="" />
67 <link
68 rel="stylesheet"
69 href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Inter+Tight:wght@600;700;800&family=JetBrains+Mono:wght@400;500&display=swap"
70 />
44fe49bClaude71 {/* PWA removed 2026-05-16 — repeated reload-loop bugs (admin
72 dashboard, deploy pill, admin-screen flash). A git host has
73 no use for service workers or install-as-app. Manifest link
74 and SW registrations are gone permanently. */}
eae38d1Claude75 <link rel="icon" type="image/svg+xml" href="/icon.svg" />
5f2e749Claude76 <title>{renderedTitle}</title>
77 {description && <meta name="description" content={description} />}
78 {(ogTitle || fullTitle || title) && (
79 <meta property="og:title" content={ogTitle ?? fullTitle ?? renderedTitle} />
80 )}
81 {(ogDescription || description) && (
82 <meta
83 property="og:description"
84 content={ogDescription ?? description ?? ""}
85 />
86 )}
87 {ogType && <meta property="og:type" content={ogType} />}
88 {twitterCard && <meta name="twitter:card" content={twitterCard} />}
fa880f2Claude89 <script dangerouslySetInnerHTML={{ __html: themeInitScript }} />
90 <style dangerouslySetInnerHTML={{ __html: css }} />
91 <style dangerouslySetInnerHTML={{ __html: hljsThemeCss }} />
fc1817aClaude92 </head>
93 <body>
4a52a98Claude94 <div class="prelaunch-banner" role="status" aria-live="polite">
95 Pre-launch &mdash; Gluecron is in final validation. Public signups
96 and git hosting for non-owner users open after launch review.
97 </div>
cd4f63bTest User98 {/* Block Q3 — Playground banner. Renders only when the active
99 user is a playground account with a future expiry; the small
100 inline script counts down once per minute. Strip is dismissible
101 per-page-load (re-appears on next nav) — gentle, not noisy. */}
102 {user && (user as any).isPlayground && (user as any).playgroundExpiresAt && (
103 <div
104 class="playground-banner"
105 role="status"
106 aria-live="polite"
107 data-playground-expires={
108 ((user as any).playgroundExpiresAt instanceof Date
109 ? (user as any).playgroundExpiresAt.toISOString()
110 : String((user as any).playgroundExpiresAt))
111 }
112 >
113 <span class="playground-banner-icon" aria-hidden="true">{"\u{1F3AE}"}</span>
114 <span class="playground-banner-text">
115 Playground account &mdash;{" "}
116 <span class="playground-banner-countdown">expires soon</span>.{" "}
117 <a href="/play/claim" class="playground-banner-cta">
118 Save your work &rarr;
119 </a>
120 </span>
121 <button
122 type="button"
123 class="playground-banner-dismiss"
124 aria-label="Dismiss"
125 data-playground-dismiss="1"
126 >
127 {"×"}
128 </button>
129 <script
130 dangerouslySetInnerHTML={{
131 __html: /* js */ `
132 (function () {
133 var el = document.currentScript && document.currentScript.parentElement;
134 if (!el) return;
135 var iso = el.getAttribute('data-playground-expires');
136 if (!iso) return;
137 var target = Date.parse(iso);
138 if (isNaN(target)) return;
139 var out = el.querySelector('.playground-banner-countdown');
140 function render() {
141 var ms = target - Date.now();
142 if (!out) return;
143 if (ms <= 0) { out.textContent = 'expired'; return; }
144 var mins = Math.floor(ms / 60000);
145 var hrs = Math.floor(mins / 60);
146 if (hrs > 1) out.textContent = hrs + ' hours left';
147 else if (hrs === 1) out.textContent = '1 hour left';
148 else if (mins > 1) out.textContent = mins + ' minutes left';
149 else out.textContent = 'less than a minute left';
150 }
151 render();
152 setInterval(render, 60000);
153 var dismiss = el.querySelector('[data-playground-dismiss="1"]');
154 if (dismiss) {
155 dismiss.addEventListener('click', function () {
156 el.style.display = 'none';
157 });
158 }
159 })();
160 `,
161 }}
162 />
163 </div>
164 )}
fc1817aClaude165 <header>
166 <nav>
167 <a href="/" class="logo">
168 gluecron
169 </a>
3ef4c9dClaude170 <div class="nav-search">
001af43Claude171 <form method="get" action="/search">
3ef4c9dClaude172 <input
173 type="search"
174 name="q"
175 placeholder="Search (press /)"
176 aria-label="Search"
177 />
178 </form>
179 </div>
06d5ffeClaude180 <div class="nav-right">
f764c07Claude181 {/* Block N3 — site-admin platform-deploy status pill. Hidden
182 by default; revealed client-side once /admin/deploys/latest.json
183 responds 200 (non-admins get 401/403 and the pill stays
184 hidden). Subscribes to the `platform:deploys` SSE topic
185 for live updates. See `deployPillScript` below. */}
186 {user && (
187 <a
188 id="deploy-pill"
189 href="/admin/deploys"
190 class="nav-deploy-pill"
191 style="display:none"
192 aria-label="Platform deploy status"
193 title="Platform deploy status"
194 >
195 <span class="deploy-pill-dot" />
196 <span class="deploy-pill-text">Deploys</span>
197 </a>
198 )}
6fc53bdClaude199 <a
200 href="/theme/toggle"
201 class="nav-link nav-theme"
202 title="Toggle theme"
203 aria-label="Toggle theme"
204 >
205 <span class="theme-icon-dark">{"\u263E"}</span>
206 <span class="theme-icon-light">{"\u2600"}</span>
207 </a>
c81ab7aClaude208 <a href="/explore" class="nav-link">
209 Explore
210 </a>
06d5ffeClaude211 {user ? (
212 <>
f1ab587Claude213 <a href="/dashboard" class="nav-link" style="font-weight: 600">
214 Dashboard
215 </a>
bdbd0deClaude216 <a href="/import" class="nav-link">
217 Import
218 </a>
06d5ffeClaude219 <a href="/new" class="btn btn-sm btn-primary">
220 + New
221 </a>
222 <a href={`/${user.username}`} class="nav-user">
223 {user.displayName || user.username}
224 </a>
225 <a href="/settings" class="nav-link">
226 Settings
227 </a>
228 <a href="/logout" class="nav-link">
229 Sign out
230 </a>
231 </>
232 ) : (
233 <>
234 <a href="/login" class="nav-link">
235 Sign in
236 </a>
237 <a href="/register" class="btn btn-sm btn-primary">
238 Register
239 </a>
240 </>
241 )}
242 </div>
fc1817aClaude243 </nav>
244 </header>
45e31d0Claude245 <main id="main-content">{children}</main>
cf9178bTest User246 {/* Global toast host — populated by the toastScript below from
247 ?success= / ?error= / ?toast= query params. Replaces the
248 per-page banner pattern with one polished slide-in. */}
249 <div
250 id="toast-host"
251 aria-live="polite"
252 aria-atomic="true"
253 style="position:fixed;top:calc(var(--header-h) + 12px);right:16px;z-index:var(--z-toast,10000);display:flex;flex-direction:column;gap:8px;pointer-events:none"
254 />
fc1817aClaude255 <footer>
958d26aClaude256 <div class="footer-inner">
257 <div class="footer-brand">
258 <a href="/" class="logo">gluecron</a>
259 <p class="footer-tag">
260 AI-native code intelligence. Self-hosted git, automated CI,
261 push-time gates. Software that ships itself.
262 </p>
263 </div>
264 <div class="footer-links">
265 <div class="footer-col">
266 <div class="footer-col-title">Product</div>
b0148e9Claude267 <a href="/features">Features</a>
268 <a href="/pricing">Pricing</a>
958d26aClaude269 <a href="/explore">Explore</a>
270 <a href="/marketplace">Marketplace</a>
271 </div>
272 <div class="footer-col">
273 <div class="footer-col-title">Platform</div>
b0148e9Claude274 <a href="/help">Quickstart</a>
958d26aClaude275 <a href="/status">Status</a>
276 <a href="/api/graphql">GraphQL</a>
277 <a href="/mcp">MCP server</a>
278 </div>
279 <div class="footer-col">
b0148e9Claude280 <div class="footer-col-title">Company</div>
281 <a href="/about">About</a>
958d26aClaude282 <a href="/terms">Terms</a>
283 <a href="/privacy">Privacy</a>
284 <a href="/acceptable-use">Acceptable use</a>
285 </div>
286 </div>
287 </div>
288 <div class="footer-bottom">
289 <span>&copy; {new Date().getFullYear()} gluecron</span>
05cdb85Claude290 <span class="footer-build" title={`commit ${build.shaFull}\nbuilt ${build.builtAt}`}>
291 <span class="footer-build-dot" aria-hidden="true" />
292 {build.sha} · {build.branch}
293 </span>
36b4cbdClaude294 </div>
c63b860Claude295 {siteBannerText ? (
296 <div
297 class={`footer-banner footer-banner-${siteBannerLevel || "info"}`}
298 role="status"
299 aria-live="polite"
300 >
301 {siteBannerText}
302 </div>
303 ) : null}
fc1817aClaude304 </footer>
05cdb85Claude305 {/* Live update poller — checks /api/version every 15s, prompts
306 reload when the running sha changes. Pure progressive-
307 enhancement; degrades to nothing if JS is off. */}
308 <div
309 id="version-banner"
310 style="display:none;position:fixed;bottom:18px;left:50%;transform:translateX(-50%);z-index:9999;background:var(--bg-elevated);border:1px solid rgba(140,109,255,0.45);border-radius:9999px;padding:8px 14px 8px 14px;font-size:13px;color:var(--text-strong);box-shadow:0 12px 28px -8px rgba(0,0,0,0.55),0 0 24px -6px rgba(140,109,255,0.40);font-family:var(--font-sans);align-items:center;gap:10px"
311 >
312 <span style="display:inline-flex;align-items:center;gap:8px">
313 <span style="width:8px;height:8px;border-radius:50%;background:#34d399;box-shadow:0 0 10px rgba(52,211,153,0.6)" />
314 <span>New version available</span>
315 </span>
316 <button
317 type="button"
318 id="version-banner-reload"
319 style="background:linear-gradient(135deg,#8c6dff 0%,#36c5d6 100%);color:#fff;border:0;border-radius:9999px;padding:5px 12px;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit"
320 >
321 Reload
322 </button>
323 </div>
fa880f2Claude324 <script dangerouslySetInnerHTML={{ __html: versionPollerScript }} />
f764c07Claude325 {/* Block N3 — site-admin deploy status pill (script-only). The pill
326 container is rendered above for authed users; this script bootstraps
327 it by fetching /admin/deploys/latest.json. Non-admins get 401/403
328 and the pill stays display:none — zero leak. */}
329 {user && (
330 <script dangerouslySetInnerHTML={{ __html: deployPillScript }} />
331 )}
699e5c7Claude332 {/* Block I4 — Command palette shell (hidden by default) */}
333 <div
334 id="cmdk-backdrop"
335 style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:9998"
336 />
337 <div
338 id="cmdk-panel"
339 style="display:none;position:fixed;top:10%;left:50%;transform:translateX(-50%);width:min(560px,92vw);background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius);box-shadow:0 12px 32px rgba(0,0,0,0.4);z-index:9999;overflow:hidden"
340 >
341 <input
342 id="cmdk-input"
343 type="text"
344 placeholder="Type a command..."
345 aria-label="Command palette"
dc26881CC LABS App346 style="width:100%;padding:var(--space-3) var(--space-4);background:transparent;color:var(--text);border:0;border-bottom:1px solid var(--border);outline:none;font-size:14px"
699e5c7Claude347 />
348 <div id="cmdk-list" style="max-height:60vh;overflow-y:auto" />
349 </div>
fa880f2Claude350 <script dangerouslySetInnerHTML={{ __html: clientJs }} />
44fe49bClaude351 {/* PWA-kill script: actively unregisters any service worker
352 previously installed under gluecron.com. Recovers any browser
353 still trapped in the SW reload loop from the legacy registrations. */}
354 <script dangerouslySetInnerHTML={{ __html: pwaKillSwitchScript }} />
cf9178bTest User355 <script dangerouslySetInnerHTML={{ __html: toastScript }} />
fa880f2Claude356 <script dangerouslySetInnerHTML={{ __html: navScript }} />
fc1817aClaude357 </body>
358 </html>
359 );
360};
361
05cdb85Claude362// Live version poller. Checks /api/version every 15s; if the sha differs
363// from the one we booted with, reveal the floating 'New version' pill so
364// the user can reload onto the new code without manually refreshing.
365const versionPollerScript = `
366 (function(){
367 var loadedSha = null;
368 var banner, btn;
369 function poll(){
370 fetch('/api/version', { cache: 'no-store' })
371 .then(function(r){ return r.ok ? r.json() : null; })
372 .then(function(j){
373 if (!j || !j.sha) return;
374 if (loadedSha === null) { loadedSha = j.sha; return; }
375 if (j.sha !== loadedSha && banner) {
376 banner.style.display = 'inline-flex';
377 }
378 })
379 .catch(function(){});
380 }
381 function init(){
382 banner = document.getElementById('version-banner');
383 btn = document.getElementById('version-banner-reload');
384 if (btn) btn.addEventListener('click', function(){ window.location.reload(); });
385 poll();
386 setInterval(poll, 15000);
387 }
388 if (document.readyState === 'loading') {
389 document.addEventListener('DOMContentLoaded', init);
390 } else {
391 init();
392 }
393 })();
394`;
395
f764c07Claude396// Block N3 — site-admin deploy status pill. Fetches /admin/deploys/latest.json;
397// if 200, reveals the pill in the nav and subscribes to the `platform:deploys`
398// SSE topic for live status updates. Non-admins get 401/403 and the pill stays
399// display:none — there's no leakage of admin-only data to other users.
400//
401// Pill states (CSS classes on the container drive colour):
402// .deploy-pill-success 🟢 "Deployed 12s ago"
403// .deploy-pill-progress 🟡 "Deploying… 14s" (pulsing dot)
404// .deploy-pill-failed 🔴 "Deploy failed 1m ago"
405// .deploy-pill-empty ⚪ "No deploys yet"
406//
407// Relative-time auto-refreshes every 15s without re-fetching.
408export const deployPillScript = `
409 (function(){
410 var pill, dot, text;
411 var state = { latest: null, asOf: null };
412
413 function classifyAge(ms){
414 if (ms < 0) return 'just now';
415 var s = Math.floor(ms / 1000);
416 if (s < 5) return 'just now';
417 if (s < 60) return s + 's ago';
418 var m = Math.floor(s / 60);
419 if (m < 60) return m + 'm ago';
420 var h = Math.floor(m / 60);
421 if (h < 24) return h + 'h ago';
422 var d = Math.floor(h / 24);
423 return d + 'd ago';
424 }
425 function elapsed(ms){
426 if (ms < 0) ms = 0;
427 var s = Math.floor(ms / 1000);
428 if (s < 60) return s + 's';
429 var m = Math.floor(s / 60);
430 var rem = s - m * 60;
431 return m + 'm ' + rem + 's';
432 }
433
434 function render(){
435 if (!pill || !text || !dot) return;
436 var d = state.latest;
81201ccTest User437 // When there's no deploy event yet, keep the pill HIDDEN. Showing
438 // "No deploys yet" was visible noise on every admin page load and
439 // flashed during reconnect cycles — admins don't need a placeholder.
440 // The pill reveals itself the first time a real deploy fires.
f764c07Claude441 if (!d) {
81201ccTest User442 pill.style.display = 'none';
f764c07Claude443 return;
444 }
445 pill.style.display = 'inline-flex';
446 var now = Date.now();
447 if (d.status === 'in_progress') {
448 pill.className = 'nav-deploy-pill deploy-pill-progress';
449 var started = Date.parse(d.started_at) || now;
450 text.textContent = 'Deploying… ' + elapsed(now - started);
451 } else if (d.status === 'succeeded') {
452 pill.className = 'nav-deploy-pill deploy-pill-success';
453 var ref = Date.parse(d.finished_at || d.started_at) || now;
454 text.textContent = 'Deployed ' + classifyAge(now - ref);
455 } else if (d.status === 'failed') {
456 pill.className = 'nav-deploy-pill deploy-pill-failed';
457 var refF = Date.parse(d.finished_at || d.started_at) || now;
458 text.textContent = 'Deploy failed ' + classifyAge(now - refF);
459 } else {
460 pill.className = 'nav-deploy-pill';
461 text.textContent = d.status;
462 }
463 }
464
465 function fetchLatest(){
466 fetch('/admin/deploys/latest.json', { cache: 'no-store', credentials: 'same-origin' })
467 .then(function(r){ if (!r.ok) return null; return r.json(); })
468 .then(function(j){
469 if (!j || j.ok !== true) return;
470 state.latest = j.latest;
471 state.asOf = j.asOf;
472 render();
473 subscribe();
474 })
475 .catch(function(){});
476 }
477
478 var subscribed = false;
479 function subscribe(){
480 if (subscribed) return;
481 if (typeof EventSource === 'undefined') return;
482 subscribed = true;
483 var es;
bf19c50Test User484 // Exponential backoff with cap. Previously a tight 1500ms reconnect
485 // produced visible looping in the nav whenever the proxy timed out
486 // or the connection blipped — the bottom-of-page deploy pill
487 // re-rendered the placeholder every 1.5s. Cap at 60s and reset on
488 // successful message receipt.
489 var delay = 2000;
490 var DELAY_MAX = 60000;
491 function bump(){
492 delay = Math.min(delay * 2, DELAY_MAX);
493 }
494 function resetDelay(){
495 delay = 2000;
496 }
f764c07Claude497 function connect(){
498 try { es = new EventSource('/live-events/platform:deploys'); }
bf19c50Test User499 catch(e){ bump(); setTimeout(connect, delay); return; }
f764c07Claude500 es.onmessage = function(m){
bf19c50Test User501 resetDelay();
f764c07Claude502 try {
503 var d = JSON.parse(m.data);
504 if (d && d.run_id) {
505 if (!state.latest || state.latest.run_id === d.run_id ||
506 Date.parse(d.started_at) >= Date.parse(state.latest.started_at)) {
507 state.latest = d;
508 render();
509 }
510 }
511 } catch(e){}
512 };
513 es.onerror = function(){
514 try { es.close(); } catch(e){}
bf19c50Test User515 bump();
f764c07Claude516 setTimeout(connect, delay);
517 };
518 }
519 connect();
520 }
521
522 function init(){
523 pill = document.getElementById('deploy-pill');
524 if (!pill) return;
525 dot = pill.querySelector('.deploy-pill-dot');
526 text = pill.querySelector('.deploy-pill-text');
527 fetchLatest();
528 // Refresh relative-time labels every 15s so "12s ago" → "27s ago"
529 // without a fresh fetch.
530 setInterval(render, 15000);
531 }
532 if (document.readyState === 'loading') {
533 document.addEventListener('DOMContentLoaded', init);
534 } else {
535 init();
536 }
537 })();
538`;
539
6fc53bdClaude540// Runs before paint — reads the theme cookie and flips data-theme so there's
81201ccTest User541// no light-to-dark flash on load. SSR default is "light"; cookie-set "dark"
542// is honoured for users who explicitly opted in.
6fc53bdClaude543const themeInitScript = `
544 (function(){
545 try {
546 var m = document.cookie.match(/(?:^|; )theme=([^;]+)/);
81201ccTest User547 var t = m ? decodeURIComponent(m[1]) : 'light';
548 if (t !== 'light' && t !== 'dark') t = 'light';
6fc53bdClaude549 document.documentElement.setAttribute('data-theme', t);
550 } catch(_){}
551 })();
552`;
553
eae38d1Claude554// Block G1 — register service worker for offline / install support.
555// Kept inline (and tiny) so we don't block first paint.
b1be050CC LABS App556//
cf9178bTest User557// Global toast notifications — reads ?success=, ?error=, ?toast= from the
558// URL on page load and surfaces a polished slide-in toast instead of the
559// per-page banner divs that crowded the layout. Toasts auto-dismiss after
560// 4.5s; query params are scrubbed from the URL via history.replaceState
561// so a subsequent Refresh doesn't re-fire the same toast.
562//
563// Variants: ?success=…, ?error=…, ?toast=info:…, ?toast=warn:… All values
564// must be URI-encoded (callers already do this via encodeURIComponent
565// in c.redirect()).
566const toastScript = `
567 (function(){
568 function showToast(kind, message){
569 if (!message) return;
570 var host = document.getElementById('toast-host');
571 if (!host) return;
572 var el = document.createElement('div');
573 el.className = 'gx-toast gx-toast--' + kind;
574 el.setAttribute('role', kind === 'error' ? 'alert' : 'status');
575 var icon = document.createElement('span');
576 icon.className = 'gx-toast__icon';
577 icon.textContent = kind === 'success' ? '\\u2713'
578 : kind === 'error' ? '\\u00D7'
579 : kind === 'warn' ? '!'
580 : 'i';
581 el.appendChild(icon);
582 var text = document.createElement('span');
583 text.className = 'gx-toast__text';
584 text.textContent = message;
585 el.appendChild(text);
586 var close = document.createElement('button');
587 close.type = 'button';
588 close.className = 'gx-toast__close';
589 close.setAttribute('aria-label', 'Dismiss notification');
590 close.textContent = '\\u00D7';
591 close.addEventListener('click', function(){ dismiss(); });
592 el.appendChild(close);
593 host.appendChild(el);
594 // Force a reflow then add the visible class so the slide-in transitions.
595 void el.offsetWidth;
596 el.classList.add('gx-toast--in');
597 var timer = setTimeout(dismiss, 4500);
598 function dismiss(){
599 clearTimeout(timer);
600 el.classList.remove('gx-toast--in');
601 el.classList.add('gx-toast--out');
602 setTimeout(function(){
603 if (el.parentNode) el.parentNode.removeChild(el);
604 }, 220);
605 }
606 }
607 try {
608 var url = new URL(window.location.href);
609 var hits = 0;
610 var s = url.searchParams.get('success');
611 if (s) { showToast('success', s); url.searchParams.delete('success'); hits++; }
612 var e = url.searchParams.get('error');
613 if (e) { showToast('error', e); url.searchParams.delete('error'); hits++; }
614 var t = url.searchParams.get('toast');
615 if (t) {
616 var ix = t.indexOf(':');
617 var kind = ix > 0 ? t.slice(0, ix) : 'info';
618 var msg = ix > 0 ? t.slice(ix + 1) : t;
619 showToast(kind, msg);
620 url.searchParams.delete('toast');
621 hits++;
622 }
623 if (hits > 0 && window.history && window.history.replaceState) {
624 window.history.replaceState({}, '', url.pathname + (url.searchParams.toString() ? '?' + url.searchParams.toString() : '') + url.hash);
625 }
626 } catch(_) {}
627 })();
628`;
629
44fe49bClaude630// PWA kill-switch (2026-05-16) — replaces the previous pwaRegisterScript
631// and pwaInstallBannerScript. Those two scripts registered /sw.js and
632// /sw-push.js at the same scope, causing a reload loop that made the
633// admin dashboard unusable (deploy pill flashing, typing wiped, buttons
634// uncllickable). Per the SW spec, only one SW can control a scope; two
635// different script URLs at the same scope keep replacing each other.
6345c3eTest User636//
44fe49bClaude637// PWA is gone for good. A git host has no use for service workers,
638// install-as-app, or push notifications via SW. This script actively
639// unregisters every previously installed SW on the gluecron.com origin
640// so any browser still trapped in the loop recovers on the very next
641// page load — without needing the user to clear site data or open
642// DevTools. Idempotent and safe to keep running forever; once all
643// browsers have been cleaned, it's a no-op.
644const pwaKillSwitchScript = `
534f04aClaude645(function(){
646 try {
44fe49bClaude647 if (!('serviceWorker' in navigator)) return;
648 if (!navigator.serviceWorker.getRegistrations) return;
649 navigator.serviceWorker.getRegistrations().then(function(regs){
650 if (!regs || regs.length === 0) return;
651 regs.forEach(function(reg){
652 try { reg.unregister(); } catch(_){}
653 });
654 }).catch(function(){});
655 // Also drop any caches the old SWs left behind so the user gets
656 // truly fresh HTML on every page load. Restricted to gluecron-*
657 // namespaced caches so we don't trample anything a future opt-in
658 // feature might create under a different name.
659 if ('caches' in self) {
660 caches.keys().then(function(keys){
661 keys.forEach(function(k){
662 if (typeof k === 'string' && k.indexOf('gluecron') === 0) {
663 try { caches.delete(k); } catch(_){}
d7ba05dClaude664 }
665 });
666 }).catch(function(){});
534f04aClaude667 }
668 } catch(_) {}
669})();
670`;
671
3ef4c9dClaude672const navScript = `
673 (function(){
674 var chord = null;
675 var chordTimer = null;
676 function isTyping(t){
677 t = t || {};
678 var tag = (t.tagName || '').toLowerCase();
679 return tag === 'input' || tag === 'textarea' || t.isContentEditable;
680 }
71cd5ecClaude681
682 // ---------- Block I4 — Command palette ----------
683 var COMMANDS = [
684 { label: 'Go to Dashboard', href: '/dashboard', kw: 'home' },
685 { label: 'Go to Explore', href: '/explore', kw: 'browse discover' },
686 { label: 'Go to Notifications', href: '/notifications', kw: 'inbox' },
687 { label: 'Go to Ask AI', href: '/ask', kw: 'chat assistant' },
688 { label: 'Create new repository', href: '/new', kw: 'add create' },
689 { label: 'Marketplace', href: '/marketplace', kw: 'apps store' },
690 { label: 'Installed apps', href: '/settings/apps', kw: 'my apps' },
691 { label: 'Register new app', href: '/developer/apps-new', kw: 'developer create' },
692 { label: 'Keyboard shortcuts', href: '/shortcuts', kw: 'help keys' },
693 { label: 'Settings (profile)', href: '/settings', kw: 'account' },
694 { label: '2FA settings', href: '/settings/2fa', kw: 'two factor security' },
695 { label: 'Passkeys settings', href: '/settings/passkeys', kw: 'webauthn' },
696 { label: 'Personal access tokens', href: '/settings/tokens', kw: 'pat api' },
697 { label: 'Billing + quotas', href: '/settings/billing', kw: 'plans money' },
698 { label: 'Audit log (personal)', href: '/settings/audit', kw: 'history' },
699 { label: 'Gists', href: '/gists', kw: 'snippets' },
700 { label: 'GraphQL explorer', href: '/api/graphql', kw: 'api query' },
701 { label: 'Admin dashboard', href: '/admin', kw: 'superuser' },
702 { label: 'Toggle theme', href: '/theme/toggle', kw: 'dark light mode' }
703 ];
704
705 function fuzzyMatch(item, q){
706 if (!q) return true;
707 var hay = (item.label + ' ' + (item.kw||'') + ' ' + item.href).toLowerCase();
708 q = q.toLowerCase();
709 var qi = 0;
710 for (var i = 0; i < hay.length && qi < q.length; i++) {
711 if (hay[i] === q[qi]) qi++;
712 }
713 return qi === q.length;
714 }
715
716 var backdrop, panel, input, list, selected = 0, filtered = COMMANDS.slice();
717
718 function render(){
719 if (!list) return;
720 var html = '';
721 for (var i = 0; i < filtered.length; i++) {
722 var item = filtered[i];
723 var cls = i === selected ? 'cmdk-item cmdk-active' : 'cmdk-item';
724 var bg = i === selected ? 'background:var(--bg);' : '';
ea52715copilot-swe-agent[bot]725 html += '<div class="' + cls + '" data-idx="' + i + '" data-url="' + item.href + '"' +
dc26881CC LABS App726 ' style="padding:var(--space-2) var(--space-4);cursor:pointer;border-bottom:1px solid var(--border);' + bg + '">' +
71cd5ecClaude727 '<div>' + item.label + '</div>' +
728 '<div style="font-size:11px;color:var(--text-muted)">' + item.href + '</div>' +
729 '</div>';
730 }
731 if (filtered.length === 0) {
dc26881CC LABS App732 html = '<div style="padding:var(--space-4);color:var(--text-muted);text-align:center">No matches.</div>';
71cd5ecClaude733 }
734 list.innerHTML = html;
735 }
736
737 function openPalette(){
738 backdrop = document.getElementById('cmdk-backdrop');
739 panel = document.getElementById('cmdk-panel');
740 input = document.getElementById('cmdk-input');
741 list = document.getElementById('cmdk-list');
742 if (!backdrop || !panel) return;
743 backdrop.style.display = 'block';
744 panel.style.display = 'block';
745 input.value = '';
746 selected = 0;
747 filtered = COMMANDS.slice();
748 render();
749 input.focus();
750 }
751 function closePalette(){
752 if (backdrop) backdrop.style.display = 'none';
753 if (panel) panel.style.display = 'none';
754 }
755 function go(href){ closePalette(); window.location.href = href; }
756
757 document.addEventListener('click', function(e){
758 var t = e.target;
759 if (t && t.id === 'cmdk-backdrop') { closePalette(); return; }
760 var item = t && t.closest && t.closest('.cmdk-item');
ea52715copilot-swe-agent[bot]761 if (item) { go(item.getAttribute('data-url')); }
71cd5ecClaude762 });
763
764 document.addEventListener('input', function(e){
765 if (e.target && e.target.id === 'cmdk-input') {
766 var q = e.target.value;
767 filtered = COMMANDS.filter(function(c){ return fuzzyMatch(c, q); });
768 selected = 0;
769 render();
770 }
771 });
772
3ef4c9dClaude773 document.addEventListener('keydown', function(e){
71cd5ecClaude774 // Palette-scoped keys take priority when open
775 if (panel && panel.style.display === 'block') {
776 if (e.key === 'Escape') { e.preventDefault(); closePalette(); return; }
777 if (e.key === 'ArrowDown') {
778 e.preventDefault();
779 selected = Math.min(filtered.length - 1, selected + 1);
780 render();
781 return;
782 }
783 if (e.key === 'ArrowUp') {
784 e.preventDefault();
785 selected = Math.max(0, selected - 1);
786 render();
787 return;
788 }
789 if (e.key === 'Enter') {
790 e.preventDefault();
791 var item = filtered[selected];
792 if (item) go(item.href);
793 return;
794 }
795 return;
796 }
797
3ef4c9dClaude798 if (isTyping(e.target)) return;
799 // Single key shortcuts
800 if (e.key === '/') {
801 var el = document.querySelector('.nav-search input');
802 if (el) { e.preventDefault(); el.focus(); return; }
803 }
804 if ((e.metaKey||e.ctrlKey) && e.key.toLowerCase() === 'k') {
71cd5ecClaude805 e.preventDefault();
806 openPalette();
807 return;
3ef4c9dClaude808 }
809 if (e.key === '?' && !e.ctrlKey && !e.metaKey) {
810 e.preventDefault(); window.location.href = '/shortcuts'; return;
811 }
812 if (e.key === 'n' && !e.ctrlKey && !e.metaKey) {
813 e.preventDefault(); window.location.href = '/new'; return;
814 }
815 // "g" chord
816 if (e.key === 'g' && !e.ctrlKey && !e.metaKey) {
817 chord = 'g';
818 clearTimeout(chordTimer);
819 chordTimer = setTimeout(function(){ chord = null; }, 1200);
820 return;
821 }
822 if (chord === 'g') {
823 if (e.key === 'd') { e.preventDefault(); window.location.href = '/dashboard'; }
824 else if (e.key === 'n') { e.preventDefault(); window.location.href = '/notifications'; }
825 else if (e.key === 'e') { e.preventDefault(); window.location.href = '/explore'; }
826 else if (e.key === 'a') { e.preventDefault(); window.location.href = '/ask'; }
827 chord = null;
828 }
829 });
830 })();
831`;
832
fc1817aClaude833const css = `
2ce1d0bClaude834 /* ================================================================
958d26aClaude835 * Gluecron design system — 2026.05 "Editorial-Technical"
836 * Slate-noir base · refined violet signature · hairline geometry ·
837 * mono-as-feature · cinematic motion · Inter Tight + JetBrains Mono.
838 * All class names preserved for back-compat across 50+ route views.
2ce1d0bClaude839 * ============================================================== */
6fc53bdClaude840 :root, :root[data-theme='dark'] {
958d26aClaude841 /* Surfaces — slate, not black. More depth, less crush. */
842 --bg: #08090f;
843 --bg-secondary: #0c0d14;
844 --bg-tertiary: #11131c;
845 --bg-elevated: #0f111a;
846 --bg-surface: #161826;
847 --bg-hover: rgba(255,255,255,0.04);
848 --bg-active: rgba(255,255,255,0.08);
849 --bg-inset: rgba(0,0,0,0.30);
850
851 /* Borders — three weights, used deliberately */
852 --border: rgba(255,255,255,0.06);
853 --border-subtle: rgba(255,255,255,0.035);
854 --border-strong: rgba(255,255,255,0.13);
855 --border-focus: rgba(140,109,255,0.55);
856
857 /* Text */
858 --text: #ededf2;
859 --text-strong: #f7f7fb;
860 --text-muted: #8b8c9c;
861 --text-faint: #555665;
862 --text-link: #b69dff;
863
864 /* Accent — refined violet (less candy), warm amber as secondary signal */
865 --accent: #8c6dff;
866 --accent-2: #36c5d6;
867 --accent-warm: #ffb45e;
868 --accent-hover: #a48bff;
869 --accent-pressed:#7559e8;
870 --accent-gradient: linear-gradient(135deg, #8c6dff 0%, #36c5d6 100%);
871 --accent-gradient-soft: linear-gradient(135deg, rgba(140,109,255,0.18) 0%, rgba(54,197,214,0.18) 100%);
872 --accent-gradient-faint: linear-gradient(135deg, rgba(140,109,255,0.07) 0%, rgba(54,197,214,0.07) 100%);
873 --accent-glow: 0 0 24px rgba(140,109,255,0.28);
874
875 /* Semantic */
876 --green: #34d399;
877 --red: #f87171;
878 --yellow: #fbbf24;
879 --amber: #fbbf24;
880 --blue: #60a5fa;
881
3a5755eClaude882 /* Type — 2026 polish pass. Inter is the primary sans, Inter Tight for
883 display (headlines), JetBrains Mono for code. All three loaded from
884 Google Fonts with display:swap so we never block first paint. System
885 fallbacks remain in place — if the CDN is unreachable the site still
886 renders cleanly with Segoe UI (Win) / SF (Mac) / Roboto (Android). */
887 --font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', 'Cascadia Code', 'Cascadia Mono', Menlo, Consolas, 'Courier New', monospace;
888 --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI Variable', 'Segoe UI', system-ui, Roboto, 'Helvetica Neue', Arial, sans-serif;
889 --font-display: 'Inter Tight', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI Variable', 'Segoe UI', system-ui, Roboto, 'Helvetica Neue', Arial, sans-serif;
958d26aClaude890 --mono-feat: 'calt', 'liga', 'ss01';
891
892 /* Radius — sharper than before */
893 --r-sm: 5px;
894 --r: 7px;
895 --r-md: 9px;
896 --r-lg: 12px;
897 --r-xl: 16px;
898 --r-2xl: 22px;
899 --r-full: 9999px;
900 --radius: 7px;
901
902 /* Type scale — bigger display sizes for editorial feel */
903 --t-xs: 11px;
904 --t-sm: 13px;
905 --t-base: 14px;
906 --t-md: 16px;
907 --t-lg: 20px;
908 --t-xl: 28px;
909 --t-2xl: 40px;
910 --t-3xl: 56px;
911 --t-display: 72px;
912 --t-display-lg:96px;
913
914 /* Spacing — 4px base */
2ce1d0bClaude915 --s-0: 0;
958d26aClaude916 --s-1: 4px; --s-2: 8px; --s-3: 12px; --s-4: 16px;
917 --s-5: 20px; --s-6: 24px; --s-7: 28px; --s-8: 32px;
918 --s-10:40px; --s-12:48px; --s-14:56px; --s-16:64px;
919 --s-20:80px; --s-24:96px; --s-32:128px;
920
921 /* Elevation — softer + more layered */
922 --elev-0: 0 0 0 1px var(--border);
923 --elev-1: 0 1px 2px rgba(0,0,0,0.50), 0 0 0 1px var(--border);
924 --elev-2: 0 8px 24px -8px rgba(0,0,0,0.60), 0 0 0 1px var(--border);
925 --elev-3: 0 20px 48px -12px rgba(0,0,0,0.70), 0 0 0 1px var(--border-strong);
926 --elev-glow: 0 0 0 1px rgba(140,109,255,0.40), 0 0 32px -4px rgba(140,109,255,0.30);
927 --ring: 0 0 0 3px rgba(140,109,255,0.28);
928 --ring-warn: 0 0 0 3px rgba(251,191,36,0.28);
929 --ring-err: 0 0 0 3px rgba(248,113,113,0.28);
930
931 /* Motion */
932 --ease: cubic-bezier(0.16, 1, 0.3, 1);
933 --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
934 --ease-out-expo: cubic-bezier(0.19, 1, 0.22, 1);
935 --ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1);
936 --t-fast: 120ms;
937 --t-base: 200ms;
938 --t-slow: 360ms;
939 --t-slower:560ms;
940
941 --header-h: 60px;
c63b860Claude942
943 /* Block O3 — visual coherence: named token aliases (additive). */
944 --space-1: var(--s-1);
945 --space-2: var(--s-2);
946 --space-3: var(--s-3);
947 --space-4: var(--s-4);
948 --space-5: var(--s-5);
949 --space-6: var(--s-6);
950 --space-8: var(--s-8);
951 --space-10: var(--s-10);
952 --space-12: var(--s-12);
953 --space-16: var(--s-16);
954 --space-20: var(--s-20);
955 --space-24: var(--s-24);
956 --radius-sm: var(--r-sm);
957 --radius-md: var(--r-md);
958 --radius-lg: var(--r-lg);
959 --radius-xl: var(--r-xl);
960 --radius-full: var(--r-full);
961 --font-size-xs: var(--t-xs);
962 --font-size-sm: var(--t-sm);
963 --font-size-base: var(--t-base);
964 --font-size-md: var(--t-md);
965 --font-size-lg: var(--t-lg);
966 --font-size-xl: var(--t-xl);
967 --font-size-2xl: var(--t-2xl);
968 --font-size-3xl: var(--t-3xl);
969 --font-size-hero: var(--t-display);
970 --leading-tight: 1.2;
971 --leading-snug: 1.35;
972 --leading-normal: 1.5;
973 --leading-relaxed: 1.6;
974 --leading-loose: 1.7;
975 --z-base: 1;
976 --z-nav: 10;
977 --z-sticky: 50;
978 --z-overlay: 100;
979 --z-modal: 1000;
980 --z-toast: 10000;
fc1817aClaude981 }
982
6fc53bdClaude983 :root[data-theme='light'] {
958d26aClaude984 --bg: #fbfbfc;
4c47454Claude985 --bg-secondary: #ffffff;
958d26aClaude986 --bg-tertiary: #f3f3f6;
987 --bg-elevated: #ffffff;
988 --bg-surface: #f6f6f9;
989 --bg-hover: rgba(0,0,0,0.035);
990 --bg-active: rgba(0,0,0,0.07);
991 --bg-inset: rgba(0,0,0,0.04);
992
993 --border: rgba(15,16,28,0.08);
994 --border-subtle: rgba(15,16,28,0.04);
995 --border-strong: rgba(15,16,28,0.16);
996
997 --text: #0e1020;
998 --text-strong: #050617;
999 --text-muted: #5a5b70;
1000 --text-faint: #8a8b9e;
1001 --text-link: #6d4dff;
1002
1003 --accent: #6d4dff;
1004 --accent-2: #0891b2;
1005 --accent-hover: #5a3df0;
1006 --accent-pressed:#4a30d6;
1007 --accent-glow: 0 0 24px rgba(109,77,255,0.18);
1008
1009 --green: #059669;
1010 --red: #dc2626;
4c47454Claude1011 --yellow: #d97706;
958d26aClaude1012
1013 --elev-1: 0 1px 2px rgba(15,16,28,0.06), 0 0 0 1px var(--border);
1014 --elev-2: 0 8px 24px -10px rgba(15,16,28,0.10), 0 0 0 1px var(--border);
1015 --elev-3: 0 20px 48px -16px rgba(15,16,28,0.14), 0 0 0 1px var(--border-strong);
6fc53bdClaude1016 }
1017
1018 /* Theme toggle — show the icon for the *opposite* theme so users see what they'll switch to. */
958d26aClaude1019 .nav-theme { display: inline-flex; align-items: center; font-size: 15px; line-height: 1; opacity: 0.85; }
1020 .nav-theme:hover { opacity: 1; }
6fc53bdClaude1021 :root[data-theme='dark'] .theme-icon-dark { display: none; }
1022 :root[data-theme='light'] .theme-icon-light { display: none; }
1023
fc1817aClaude1024 * { margin: 0; padding: 0; box-sizing: border-box; }
958d26aClaude1025 *::selection { background: rgba(140,109,255,0.32); color: var(--text-strong); }
2ce1d0bClaude1026
958d26aClaude1027 html { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-rendering: optimizeLegibility; }
fc1817aClaude1028
1029 body {
1030 font-family: var(--font-sans);
1031 background: var(--bg);
1032 color: var(--text);
cf9178bTest User1033 font-size: 15px;
2ce1d0bClaude1034 line-height: 1.55;
958d26aClaude1035 letter-spacing: -0.011em;
fc1817aClaude1036 min-height: 100vh;
1037 display: flex;
1038 flex-direction: column;
958d26aClaude1039 font-feature-settings: 'cv11', 'ss01', 'ss03', 'calt';
cf9178bTest User1040 /* Subtle: prefers grayscale font smoothing on macOS for thin text,
1041 and disables automatic synthesis of bold/italic which can produce
1042 muddier rendering on certain weights. */
1043 -webkit-font-smoothing: antialiased;
1044 -moz-osx-font-smoothing: grayscale;
1045 font-synthesis: none;
1046 }
1047 /* Tighten heading rhythm — the body is 15/1.55, headings step down
1048 line-height inversely with size so display text doesn't feel airy. */
1049 h1, h2, h3, h4, h5, h6 {
1050 font-family: var(--font-display);
1051 color: var(--text-strong);
1052 letter-spacing: -0.022em;
1053 line-height: 1.18;
1054 font-weight: 650;
1055 margin-top: 0;
1056 }
1057 h1 { font-size: 28px; letter-spacing: -0.028em; }
1058 h2 { font-size: 22px; }
1059 h3 { font-size: 18px; letter-spacing: -0.018em; }
1060 h4 { font-size: 15.5px; letter-spacing: -0.012em; font-weight: 600; }
1061 /* Link refinement — underline only on hover/focus, never by default
1062 on internal nav. Prevents the "blue underline soup" look. */
1063 a { color: var(--text-link); text-decoration: none; transition: color var(--t-fast) var(--ease); }
1064 a:hover { color: var(--accent-hover); text-decoration: underline; text-underline-offset: 3px; }
1065 a:focus-visible {
1066 outline: none;
1067 box-shadow: 0 0 0 3px rgba(109,77,255,0.32);
1068 border-radius: 3px;
fc1817aClaude1069 }
1070
958d26aClaude1071 /* Whole-page atmosphere: very subtle gradient + dot-grid layered behind everything.
1072 Keeps every page feeling like part of the same product without competing with hero art. */
2ce1d0bClaude1073 body::before {
1074 content: '';
1075 position: fixed;
1076 inset: 0;
1077 pointer-events: none;
958d26aClaude1078 z-index: -2;
2ce1d0bClaude1079 background:
958d26aClaude1080 radial-gradient(70% 55% at 85% -20%, rgba(140,109,255,0.07), transparent 65%),
1081 radial-gradient(55% 45% at -10% 115%, rgba(54,197,214,0.05), transparent 65%);
1082 }
1083 body::after {
1084 content: '';
1085 position: fixed;
1086 inset: 0;
1087 pointer-events: none;
1088 z-index: -1;
1089 background-image: radial-gradient(rgba(255,255,255,0.025) 1px, transparent 1px);
1090 background-size: 28px 28px;
1091 background-position: 0 0;
1092 opacity: 0.55;
1093 mask-image: radial-gradient(ellipse at 50% 0%, #000 0%, transparent 70%);
1094 -webkit-mask-image: radial-gradient(ellipse at 50% 0%, #000 0%, transparent 70%);
1095 }
1096 :root[data-theme='light'] body::before { opacity: 0.55; }
1097 :root[data-theme='light'] body::after {
1098 background-image: radial-gradient(rgba(15,16,28,0.06) 1px, transparent 1px);
1099 opacity: 0.4;
fc1817aClaude1100 }
2ce1d0bClaude1101
1102 a { color: var(--text-link); text-decoration: none; transition: color var(--t-fast) var(--ease); }
1103 a:hover { color: var(--accent-hover); text-decoration: none; }
fc1817aClaude1104
958d26aClaude1105 /* Heading scale — confident, editorial, tight tracking */
1106 h1, h2, h3, h4, h5, h6 {
1107 font-family: var(--font-display);
2ce1d0bClaude1108 font-weight: 600;
958d26aClaude1109 letter-spacing: -0.022em;
1110 line-height: 1.18;
1111 color: var(--text-strong);
1112 }
1113 h1 { font-size: var(--t-xl); letter-spacing: -0.028em; }
1114 h2 { font-size: var(--t-lg); letter-spacing: -0.022em; }
1115 h3 { font-size: var(--t-md); font-weight: 600; letter-spacing: -0.015em; }
1116 h4 { font-size: var(--t-base); font-weight: 600; letter-spacing: -0.01em; }
1117 h5, h6 { font-size: var(--t-sm); font-weight: 600; letter-spacing: -0.005em; }
1118
1119 /* Editorial display heading utility — used by landing + marketing pages */
1120 .display {
1121 font-family: var(--font-display);
1122 font-size: clamp(40px, 7.5vw, 96px);
1123 line-height: 0.98;
1124 letter-spacing: -0.04em;
1125 font-weight: 600;
1126 color: var(--text-strong);
2ce1d0bClaude1127 }
fc1817aClaude1128
958d26aClaude1129 /* Eyebrow — uppercase mono label that sits above section headings */
1130 .eyebrow {
1131 display: inline-flex;
1132 align-items: center;
1133 gap: 8px;
1134 font-family: var(--font-mono);
1135 font-size: 11px;
1136 font-weight: 500;
1137 letter-spacing: 0.14em;
1138 text-transform: uppercase;
1139 color: var(--accent);
1140 margin-bottom: var(--s-3);
1141 }
1142 .eyebrow::before {
1143 content: '';
1144 width: 18px;
1145 height: 1px;
1146 background: currentColor;
1147 opacity: 0.6;
1148 }
1149
1150 /* Section header — paired eyebrow + title + lede */
1151 .section-header { max-width: 720px; margin: 0 auto var(--s-10); text-align: center; }
1152 .section-header.left { text-align: left; margin-left: 0; margin-right: auto; }
1153 .section-header h2 {
1154 font-size: clamp(28px, 4vw, 44px);
1155 line-height: 1.05;
1156 letter-spacing: -0.028em;
1157 margin-bottom: var(--s-3);
1158 }
1159 .section-header p {
1160 color: var(--text-muted);
1161 font-size: var(--t-md);
1162 line-height: 1.6;
1163 max-width: 580px;
1164 margin: 0 auto;
1165 }
1166 .section-header.left p { margin-left: 0; }
fc1817aClaude1167
958d26aClaude1168 code, kbd, samp {
2ce1d0bClaude1169 font-family: var(--font-mono);
958d26aClaude1170 font-feature-settings: var(--mono-feat);
1171 }
1172 code {
1173 font-size: 0.9em;
2ce1d0bClaude1174 background: var(--bg-tertiary);
958d26aClaude1175 border: 1px solid var(--border-subtle);
2ce1d0bClaude1176 padding: 1px 6px;
1177 border-radius: var(--r-sm);
1178 color: var(--text);
1179 }
958d26aClaude1180 kbd, .kbd {
1181 display: inline-flex;
1182 align-items: center;
1183 padding: 1px 6px;
1184 font-family: var(--font-mono);
1185 font-size: 11px;
1186 background: var(--bg-elevated);
1187 border: 1px solid var(--border);
1188 border-bottom-width: 2px;
1189 border-radius: 4px;
1190 color: var(--text);
1191 line-height: 1.5;
1192 vertical-align: middle;
1193 }
2ce1d0bClaude1194
958d26aClaude1195 /* Mono utility for technical chrome (paths, IDs, dates) */
1196 .mono { font-family: var(--font-mono); font-feature-settings: var(--mono-feat); font-size: 0.96em; }
1197 .meta-mono { font-family: var(--font-mono); font-size: var(--t-xs); color: var(--text-muted); letter-spacing: 0; }
1198
1199 /* Pre-launch banner — slim, refined, mono caption */
4a52a98Claude1200 .prelaunch-banner {
958d26aClaude1201 position: relative;
2ce1d0bClaude1202 background:
958d26aClaude1203 linear-gradient(180deg, rgba(251,191,36,0.10), rgba(251,191,36,0.03)),
2ce1d0bClaude1204 var(--bg);
958d26aClaude1205 border-bottom: 1px solid rgba(251,191,36,0.28);
4a52a98Claude1206 color: var(--yellow);
958d26aClaude1207 padding: 7px 24px;
1208 font-family: var(--font-mono);
1209 font-size: 11px;
4a52a98Claude1210 font-weight: 500;
1211 text-align: center;
2ce1d0bClaude1212 line-height: 1.5;
958d26aClaude1213 letter-spacing: 0.04em;
1214 text-transform: uppercase;
1215 }
1216 .prelaunch-banner::before {
1217 content: '◆';
1218 margin-right: 8px;
1219 font-size: 9px;
1220 opacity: 0.7;
1221 vertical-align: 1px;
4a52a98Claude1222 }
1223
cd4f63bTest User1224 /* Block Q3 — Playground banner. Brighter than the prelaunch strip so
1225 visitors don't miss the "save your work" CTA, but slim enough to
1226 not feel like a modal. */
1227 .playground-banner {
1228 position: relative;
1229 display: flex;
1230 align-items: center;
1231 justify-content: center;
1232 gap: 8px;
1233 background:
1234 linear-gradient(180deg, rgba(251,191,36,0.20), rgba(251,191,36,0.06)),
1235 var(--bg);
1236 border-bottom: 1px solid rgba(251,191,36,0.45);
1237 color: var(--yellow, #fbbf24);
1238 padding: 8px 40px 8px 24px;
1239 font-size: 13px;
1240 font-weight: 500;
1241 text-align: center;
1242 line-height: 1.4;
1243 }
1244 .playground-banner-icon { font-size: 14px; }
1245 .playground-banner-text { color: var(--text-strong, #e6edf3); }
1246 .playground-banner-countdown { font-weight: 600; }
1247 .playground-banner-cta {
1248 margin-left: 4px;
1249 color: var(--yellow, #fbbf24);
1250 text-decoration: underline;
1251 font-weight: 600;
1252 }
1253 .playground-banner-cta:hover { opacity: 0.85; }
1254 .playground-banner-dismiss {
1255 position: absolute;
1256 top: 50%;
1257 right: 12px;
1258 transform: translateY(-50%);
1259 background: transparent;
1260 border: none;
1261 color: var(--text-muted, #8b949e);
1262 font-size: 18px;
1263 line-height: 1;
1264 cursor: pointer;
1265 padding: 0 4px;
1266 }
1267 .playground-banner-dismiss:hover { color: var(--text-strong, #e6edf3); }
1268
958d26aClaude1269 /* Header — sticky, blurred, hairline border, taller for breathing room */
fc1817aClaude1270 header {
2ce1d0bClaude1271 position: sticky;
1272 top: 0;
1273 z-index: 100;
fc1817aClaude1274 border-bottom: 1px solid var(--border);
2ce1d0bClaude1275 padding: 0 24px;
1276 height: var(--header-h);
958d26aClaude1277 background: rgba(8,9,15,0.72);
1278 backdrop-filter: saturate(180%) blur(18px);
1279 -webkit-backdrop-filter: saturate(180%) blur(18px);
fc1817aClaude1280 }
958d26aClaude1281 :root[data-theme='light'] header { background: rgba(251,251,252,0.78); }
fc1817aClaude1282
06d5ffeClaude1283 header nav {
1284 display: flex;
1285 align-items: center;
958d26aClaude1286 gap: 18px;
1287 max-width: 1240px;
06d5ffeClaude1288 margin: 0 auto;
2ce1d0bClaude1289 height: 100%;
1290 }
1291 .logo {
958d26aClaude1292 font-family: var(--font-display);
1293 font-size: 16px;
2ce1d0bClaude1294 font-weight: 700;
958d26aClaude1295 letter-spacing: -0.025em;
1296 color: var(--text-strong);
2ce1d0bClaude1297 display: inline-flex;
1298 align-items: center;
958d26aClaude1299 gap: 9px;
1300 transition: opacity var(--t-fast) var(--ease);
06d5ffeClaude1301 }
2ce1d0bClaude1302 .logo::before {
1303 content: '';
958d26aClaude1304 width: 20px; height: 20px;
1305 border-radius: 6px;
2ce1d0bClaude1306 background: var(--accent-gradient);
958d26aClaude1307 box-shadow:
1308 inset 0 1px 0 rgba(255,255,255,0.25),
1309 0 0 0 1px rgba(140,109,255,0.45),
1310 0 0 20px rgba(140,109,255,0.30);
2ce1d0bClaude1311 flex-shrink: 0;
958d26aClaude1312 transition: transform var(--t-base) var(--ease-spring), box-shadow var(--t-base) var(--ease);
1313 }
1314 .logo:hover { text-decoration: none; color: var(--text-strong); }
1315 .logo:hover::before {
1316 transform: rotate(8deg) scale(1.05);
1317 box-shadow:
1318 inset 0 1px 0 rgba(255,255,255,0.30),
1319 0 0 0 1px rgba(140,109,255,0.55),
1320 0 0 28px rgba(140,109,255,0.45);
06d5ffeClaude1321 }
fc1817aClaude1322
2ce1d0bClaude1323 .nav-search {
1324 flex: 1;
958d26aClaude1325 max-width: 360px;
1326 margin: 0 4px 0 8px;
1327 position: relative;
2ce1d0bClaude1328 }
1329 .nav-search input {
1330 width: 100%;
958d26aClaude1331 padding: 7px 12px 7px 32px;
2ce1d0bClaude1332 background: var(--bg-tertiary);
1333 border: 1px solid var(--border);
1334 border-radius: var(--r-sm);
1335 color: var(--text);
1336 font-family: var(--font-sans);
1337 font-size: var(--t-sm);
958d26aClaude1338 transition: border-color var(--t-fast) var(--ease), background var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
1339 }
1340 .nav-search::before {
1341 content: '';
1342 position: absolute;
1343 left: 11px; top: 50%;
1344 transform: translateY(-50%);
1345 width: 12px; height: 12px;
1346 border: 1.5px solid var(--text-faint);
1347 border-radius: 50%;
1348 pointer-events: none;
1349 }
1350 .nav-search::after {
1351 content: '';
1352 position: absolute;
1353 left: 19px; top: calc(50% + 4px);
1354 width: 5px; height: 1.5px;
1355 background: var(--text-faint);
1356 transform: rotate(45deg);
1357 pointer-events: none;
2ce1d0bClaude1358 }
1359 .nav-search input::placeholder { color: var(--text-faint); }
1360 .nav-search input:focus {
1361 outline: none;
1362 background: var(--bg-secondary);
958d26aClaude1363 border-color: var(--border-focus);
1364 box-shadow: var(--ring);
2ce1d0bClaude1365 }
06d5ffeClaude1366
958d26aClaude1367 .nav-right { display: flex; align-items: center; gap: 2px; margin-left: auto; }
f764c07Claude1368
1369 /* Block N3 — site-admin deploy status pill */
1370 .nav-deploy-pill {
1371 display: inline-flex;
1372 align-items: center;
1373 gap: 6px;
1374 padding: 4px 10px;
1375 margin: 0 6px 0 0;
1376 border-radius: 9999px;
1377 font-size: 11px;
1378 font-weight: 600;
1379 line-height: 1;
1380 color: var(--text-strong);
1381 background: var(--bg-elevated);
1382 border: 1px solid var(--border);
1383 text-decoration: none;
1384 transition: background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
1385 }
1386 .nav-deploy-pill:hover { background: var(--bg-hover); text-decoration: none; }
1387 .nav-deploy-pill .deploy-pill-dot {
1388 display: inline-block;
1389 width: 8px; height: 8px;
1390 border-radius: 50%;
1391 background: #6b7280;
1392 }
1393 .deploy-pill-success .deploy-pill-dot { background: #34d399; box-shadow: 0 0 6px rgba(52,211,153,0.45); }
1394 .deploy-pill-failed .deploy-pill-dot { background: #f87171; box-shadow: 0 0 6px rgba(248,113,113,0.45); }
1395 .deploy-pill-failed { border-color: rgba(248,113,113,0.4); }
1396 .deploy-pill-progress .deploy-pill-dot {
1397 background: #fbbf24;
1398 box-shadow: 0 0 6px rgba(251,191,36,0.55);
1399 animation: deployPillPulse 1.2s ease-in-out infinite;
1400 }
1401 @keyframes deployPillPulse {
1402 0%, 100% { opacity: 1; transform: scale(1); }
1403 50% { opacity: 0.45; transform: scale(0.8); }
1404 }
1405 .deploy-pill-empty .deploy-pill-dot { background: #9ca3af; }
1406
2ce1d0bClaude1407 .nav-link {
958d26aClaude1408 position: relative;
2ce1d0bClaude1409 color: var(--text-muted);
1410 font-size: var(--t-sm);
1411 font-weight: 500;
958d26aClaude1412 padding: 7px 11px;
2ce1d0bClaude1413 border-radius: var(--r-sm);
958d26aClaude1414 transition: color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
2ce1d0bClaude1415 }
1416 .nav-link:hover {
958d26aClaude1417 color: var(--text-strong);
2ce1d0bClaude1418 background: var(--bg-hover);
1419 text-decoration: none;
1420 }
958d26aClaude1421 .nav-link.active { color: var(--text-strong); }
1422 .nav-link.active::after {
1423 content: '';
1424 position: absolute;
1425 left: 11px; right: 11px;
1426 bottom: -1px;
1427 height: 2px;
1428 background: var(--accent-gradient);
1429 border-radius: 2px;
1430 }
2ce1d0bClaude1431 .nav-user {
958d26aClaude1432 color: var(--text-strong);
2ce1d0bClaude1433 font-weight: 600;
1434 font-size: var(--t-sm);
1435 padding: 6px 10px;
1436 border-radius: var(--r-sm);
958d26aClaude1437 margin-left: 6px;
2ce1d0bClaude1438 transition: background var(--t-fast) var(--ease);
1439 }
958d26aClaude1440 .nav-user::before {
1441 content: '';
1442 display: inline-block;
1443 width: 8px; height: 8px;
1444 background: var(--green);
1445 border-radius: 50%;
1446 margin-right: 7px;
1447 box-shadow: 0 0 8px rgba(52,211,153,0.5);
1448 vertical-align: 1px;
1449 }
2ce1d0bClaude1450 .nav-user:hover { background: var(--bg-hover); text-decoration: none; }
fc1817aClaude1451
2ce1d0bClaude1452 main {
958d26aClaude1453 max-width: 1240px;
2ce1d0bClaude1454 margin: 0 auto;
958d26aClaude1455 padding: 36px 24px 80px;
2ce1d0bClaude1456 flex: 1;
1457 width: 100%;
ed6e438Claude1458 /* 2026 polish — subtle entrance animation on every page load.
1459 Content fades up 4px over 360ms, giving the site that "alive"
1460 quality that 2026 SaaS expects. Honors prefers-reduced-motion. */
1461 animation: gxMainEnter 360ms cubic-bezier(0.22, 1, 0.36, 1) both;
1462 }
1463 @keyframes gxMainEnter {
1464 from { opacity: 0; transform: translateY(4px); }
1465 to { opacity: 1; transform: translateY(0); }
1466 }
1467 @media (prefers-reduced-motion: reduce) {
1468 main { animation: none; }
1469 }
1470 /* 2026 polish — accent focus ring across all focusable elements.
1471 The default browser ring is utilitarian; this is the same width
1472 but tinted to the brand accent so keyboard navigation feels
1473 intentional, not jarring. :focus-visible only — mouse users
1474 never see it. */
1475 :focus-visible {
1476 outline: none;
1477 }
1478 a:focus-visible,
1479 button:focus-visible,
1480 input:focus-visible,
1481 select:focus-visible,
1482 textarea:focus-visible,
1483 [tabindex]:focus-visible {
1484 outline: 2px solid rgba(140, 109, 255, 0.55);
1485 outline-offset: 2px;
1486 border-radius: 4px;
2ce1d0bClaude1487 }
fc1817aClaude1488
958d26aClaude1489 /* Editorial footer: link grid + tagline row */
fc1817aClaude1490 footer {
1491 border-top: 1px solid var(--border);
958d26aClaude1492 padding: 56px 24px 40px;
fc1817aClaude1493 color: var(--text-muted);
958d26aClaude1494 font-size: var(--t-sm);
1495 background:
1496 linear-gradient(180deg, transparent 0%, rgba(140,109,255,0.025) 100%),
1497 var(--bg);
1498 }
1499 footer .footer-inner {
1500 max-width: 1240px;
1501 margin: 0 auto;
1502 display: grid;
1503 grid-template-columns: 1fr auto;
1504 gap: 48px;
1505 align-items: start;
1506 }
1507 footer .footer-brand .logo { margin-bottom: var(--s-3); }
1508 footer .footer-tag {
1509 color: var(--text-muted);
1510 font-size: var(--t-sm);
1511 max-width: 320px;
1512 line-height: 1.55;
1513 }
1514 footer .footer-links {
1515 display: grid;
1516 grid-template-columns: repeat(3, minmax(120px, auto));
1517 gap: 0 56px;
1518 }
1519 footer .footer-col-title {
1520 font-family: var(--font-mono);
1521 font-size: 11px;
1522 text-transform: uppercase;
1523 letter-spacing: 0.14em;
1524 color: var(--text-faint);
1525 margin-bottom: var(--s-3);
1526 }
1527 footer .footer-col a {
1528 display: block;
1529 color: var(--text-muted);
1530 font-size: var(--t-sm);
1531 padding: 5px 0;
1532 transition: color var(--t-fast) var(--ease);
1533 }
1534 footer .footer-col a:hover { color: var(--text-strong); text-decoration: none; }
1535 footer .footer-bottom {
1536 max-width: 1240px;
1537 margin: 40px auto 0;
1538 padding-top: 24px;
1539 border-top: 1px solid var(--border-subtle);
1540 display: flex;
1541 justify-content: space-between;
1542 align-items: center;
2ce1d0bClaude1543 color: var(--text-faint);
1544 font-size: var(--t-xs);
958d26aClaude1545 font-family: var(--font-mono);
1546 letter-spacing: 0.02em;
1547 }
1548 footer .footer-bottom a { color: var(--text-faint); }
1549 footer .footer-bottom a:hover { color: var(--text-muted); text-decoration: none; }
05cdb85Claude1550 footer .footer-build {
1551 display: inline-flex;
1552 align-items: center;
1553 gap: 6px;
1554 color: var(--text-faint);
1555 font-family: var(--font-mono);
1556 font-size: 11px;
1557 cursor: help;
1558 }
1559 footer .footer-build-dot {
1560 width: 6px; height: 6px;
1561 border-radius: 50%;
1562 background: var(--green);
1563 box-shadow: 0 0 6px rgba(52,211,153,0.55);
1564 animation: footer-build-pulse 2.4s ease-in-out infinite;
1565 }
1566 @keyframes footer-build-pulse {
1567 0%, 100% { opacity: 0.6; }
1568 50% { opacity: 1; }
1569 }
958d26aClaude1570 @media (max-width: 768px) {
1571 footer .footer-inner { grid-template-columns: 1fr; gap: 32px; }
1572 footer .footer-links { grid-template-columns: repeat(2, 1fr); gap: 24px 32px; }
1573 footer .footer-bottom { flex-direction: column; gap: 8px; text-align: center; }
fc1817aClaude1574 }
1575
2ce1d0bClaude1576 /* ============================================================ */
1577 /* Buttons */
1578 /* ============================================================ */
dc26881CC LABS App1579 /* ============================================================ */
1580 /* Buttons — Block U2 senior polish pass. */
1581 /* Rules: */
1582 /* · hover lifts every .btn by 1px + soft drop shadow (180ms) */
1583 /* · active presses back down to 0 (80ms — faster on press) */
1584 /* · focus-visible uses a soft box-shadow ring (no outline) */
1585 /* · primary gradient shifts position on hover for a slow */
1586 /* 600ms shimmer */
1587 /* · disabled never lifts and never animates */
1588 /* ============================================================ */
06d5ffeClaude1589 .btn {
1590 display: inline-flex;
1591 align-items: center;
2ce1d0bClaude1592 justify-content: center;
958d26aClaude1593 gap: 7px;
2ce1d0bClaude1594 padding: 7px 14px;
1595 border-radius: var(--r-sm);
958d26aClaude1596 font-family: var(--font-sans);
2ce1d0bClaude1597 font-size: var(--t-sm);
06d5ffeClaude1598 font-weight: 500;
1599 border: 1px solid var(--border);
2ce1d0bClaude1600 background: var(--bg-elevated);
06d5ffeClaude1601 color: var(--text);
1602 cursor: pointer;
1603 text-decoration: none;
958d26aClaude1604 line-height: 1.25;
1605 letter-spacing: -0.008em;
dc26881CC LABS App1606 /* U2 — hover/transform settles in 180ms; press snaps in 80ms.
1607 Listed once on the base rule so :active can override only the
1608 transform/duration without re-typing the colour/bg transitions. */
2ce1d0bClaude1609 transition:
dc26881CC LABS App1610 background 180ms ease,
1611 border-color 180ms ease,
1612 transform 180ms ease,
1613 box-shadow 180ms ease,
1614 color 180ms ease;
2ce1d0bClaude1615 user-select: none;
1616 white-space: nowrap;
958d26aClaude1617 position: relative;
06d5ffeClaude1618 }
2ce1d0bClaude1619 .btn:hover {
1620 background: var(--bg-surface);
1621 border-color: var(--border-strong);
958d26aClaude1622 color: var(--text-strong);
2ce1d0bClaude1623 text-decoration: none;
dc26881CC LABS App1624 /* U2 — universal hover lift + soft accent drop shadow. */
1625 transform: translateY(-1px);
1626 box-shadow: 0 4px 12px -4px rgba(15, 17, 26, 0.45);
1627 }
1628 .btn:active {
1629 transform: translateY(0);
1630 transition-duration: 80ms;
1631 }
1632 /* U2 — soft modern focus ring via box-shadow, not outline. */
1633 .btn:focus-visible {
1634 outline: none;
1635 box-shadow: 0 0 0 3px rgba(140, 109, 255, 0.35);
2ce1d0bClaude1636 }
1637
1638 .btn-primary {
1639 background: var(--accent-gradient);
dc26881CC LABS App1640 background-size: 200% 100%;
1641 background-position: 0% 50%;
2ce1d0bClaude1642 border-color: transparent;
1643 color: #fff;
1644 font-weight: 600;
958d26aClaude1645 text-shadow: 0 1px 0 rgba(0,0,0,0.15);
2ce1d0bClaude1646 box-shadow:
958d26aClaude1647 inset 0 1px 0 rgba(255,255,255,0.22),
1648 inset 0 -1px 0 rgba(0,0,0,0.10),
1649 0 1px 2px rgba(0,0,0,0.40),
1650 0 0 0 1px rgba(140,109,255,0.30);
dc26881CC LABS App1651 /* U2 — slower 600ms transition on background-position so the
1652 primary CTA shimmers when the cursor lands. */
1653 transition:
1654 background-position 600ms ease,
1655 transform 180ms ease,
1656 box-shadow 180ms ease,
1657 color 180ms ease;
06d5ffeClaude1658 }
958d26aClaude1659 .btn-primary::before {
1660 content: '';
1661 position: absolute;
1662 inset: 0;
1663 border-radius: inherit;
1664 background: linear-gradient(180deg, rgba(255,255,255,0.18), transparent 60%);
1665 opacity: 0;
1666 transition: opacity var(--t-fast) var(--ease);
1667 pointer-events: none;
2ce1d0bClaude1668 }
1669 .btn-primary:hover {
958d26aClaude1670 color: #fff;
2ce1d0bClaude1671 background: var(--accent-gradient);
dc26881CC LABS App1672 background-size: 200% 100%;
1673 background-position: 100% 50%;
958d26aClaude1674 border-color: transparent;
dc26881CC LABS App1675 transform: translateY(-1px);
2ce1d0bClaude1676 box-shadow:
958d26aClaude1677 inset 0 1px 0 rgba(255,255,255,0.30),
1678 inset 0 -1px 0 rgba(0,0,0,0.10),
1679 0 6px 18px -4px rgba(140,109,255,0.45),
1680 0 0 0 1px rgba(140,109,255,0.45);
2ce1d0bClaude1681 }
958d26aClaude1682 .btn-primary:hover::before { opacity: 1; }
dc26881CC LABS App1683 .btn-primary:active { transform: translateY(0); transition-duration: 80ms; }
1684 /* U2 — primary focus ring uses the same accent box-shadow recipe. */
1685 .btn-primary:focus-visible {
1686 box-shadow:
1687 inset 0 1px 0 rgba(255,255,255,0.22),
1688 0 0 0 3px rgba(140,109,255,0.35);
1689 }
2ce1d0bClaude1690
1691 .btn-danger {
1692 background: transparent;
958d26aClaude1693 border-color: rgba(248,113,113,0.40);
2ce1d0bClaude1694 color: var(--red);
1695 }
1696 .btn-danger:hover {
958d26aClaude1697 background: rgba(248,113,113,0.08);
2ce1d0bClaude1698 border-color: var(--red);
958d26aClaude1699 color: var(--red);
dc26881CC LABS App1700 transform: translateY(-1px);
1701 box-shadow: 0 4px 12px -4px rgba(248,113,113,0.30);
2ce1d0bClaude1702 }
dc26881CC LABS App1703 .btn-danger:focus-visible { box-shadow: 0 0 0 3px rgba(248,113,113,0.35); }
2ce1d0bClaude1704
1705 .btn-ghost {
1706 background: transparent;
1707 border-color: transparent;
1708 color: var(--text-muted);
1709 }
1710 .btn-ghost:hover {
1711 background: var(--bg-hover);
958d26aClaude1712 color: var(--text-strong);
2ce1d0bClaude1713 border-color: var(--border);
dc26881CC LABS App1714 transform: translateY(-1px);
1715 box-shadow: 0 4px 12px -4px rgba(15, 17, 26, 0.30);
2ce1d0bClaude1716 }
1717
958d26aClaude1718 .btn-secondary {
1719 background: var(--bg-elevated);
1720 border-color: var(--border-strong);
1721 color: var(--text-strong);
1722 }
1723 .btn-secondary:hover {
1724 background: var(--bg-surface);
1725 border-color: var(--border-strong);
dc26881CC LABS App1726 transform: translateY(-1px);
1727 box-shadow: 0 4px 12px -4px rgba(15, 17, 26, 0.35);
958d26aClaude1728 }
1729
1730 .btn-sm { padding: 4px 10px; font-size: var(--t-xs); border-radius: var(--r-sm); gap: 5px; }
1731 .btn-lg { padding: 11px 22px; font-size: var(--t-base); border-radius: var(--r); }
1732 .btn-xl { padding: 14px 28px; font-size: var(--t-md); border-radius: var(--r); font-weight: 600; }
1733 .btn-block { width: 100%; }
2ce1d0bClaude1734
dc26881CC LABS App1735 /* U2 — disabled never lifts, never shimmers, never glows. */
1736 .btn:disabled,
1737 .btn[aria-disabled='true'],
1738 .btn:disabled:hover,
1739 .btn[aria-disabled='true']:hover {
1740 opacity: 0.5;
2ce1d0bClaude1741 cursor: not-allowed;
1742 pointer-events: none;
dc26881CC LABS App1743 transform: none;
1744 box-shadow: none;
1745 }
1746 @media (prefers-reduced-motion: reduce) {
1747 .btn,
1748 .btn:hover,
1749 .btn:active,
1750 .btn-primary,
1751 .btn-primary:hover {
1752 transform: none;
1753 transition: background-color 80ms linear, color 80ms linear;
1754 }
2ce1d0bClaude1755 }
1756
1757 /* ============================================================ */
1758 /* Forms */
1759 /* ============================================================ */
1760 .form-group { margin-bottom: 20px; }
1761 .form-group label {
1762 display: block;
1763 font-size: var(--t-sm);
1764 font-weight: 500;
1765 margin-bottom: 6px;
1766 color: var(--text);
1767 letter-spacing: -0.005em;
1768 }
1769 .form-group input,
1770 .form-group textarea,
1771 .form-group select,
1772 input[type='text'], input[type='email'], input[type='password'],
1773 input[type='url'], input[type='search'], input[type='number'],
1774 textarea, select {
06d5ffeClaude1775 width: 100%;
2ce1d0bClaude1776 padding: 9px 12px;
1777 background: var(--bg-secondary);
06d5ffeClaude1778 border: 1px solid var(--border);
2ce1d0bClaude1779 border-radius: var(--r-sm);
06d5ffeClaude1780 color: var(--text);
2ce1d0bClaude1781 font-size: var(--t-sm);
06d5ffeClaude1782 font-family: var(--font-sans);
2ce1d0bClaude1783 transition:
1784 border-color var(--t-fast) var(--ease),
1785 background var(--t-fast) var(--ease),
1786 box-shadow var(--t-fast) var(--ease);
06d5ffeClaude1787 }
2ce1d0bClaude1788 .form-group input::placeholder, textarea::placeholder, input::placeholder {
1789 color: var(--text-faint);
06d5ffeClaude1790 }
2ce1d0bClaude1791 .form-group input:hover, textarea:hover, select:hover,
1792 input[type='text']:hover, input[type='email']:hover, input[type='password']:hover {
1793 border-color: var(--border-strong);
1794 }
1795 .form-group input:focus, .form-group textarea:focus, .form-group select:focus,
1796 input:focus, textarea:focus, select:focus {
06d5ffeClaude1797 outline: none;
2ce1d0bClaude1798 background: var(--bg);
1799 border-color: var(--border-focus);
1800 box-shadow: var(--ring);
06d5ffeClaude1801 }
2ce1d0bClaude1802 textarea { font-family: var(--font-mono); font-size: var(--t-sm); line-height: 1.55; }
06d5ffeClaude1803 .input-disabled { opacity: 0.5; cursor: not-allowed; }
1804
2ce1d0bClaude1805 /* ============================================================ */
1806 /* Auth (register / login / verify) */
1807 /* ============================================================ */
1808 .auth-container {
98f45b4Claude1809 /* 2026 polish — wider, more generous, with a subtle accent-glow
1810 border and gradient top-edge so it reads as a premium product
1811 gateway, not a stock form. The 480px width feels intentional
1812 (not cramped, not endless). */
1813 max-width: 480px;
1814 margin: 72px auto;
1815 padding: 40px 40px 36px;
2ce1d0bClaude1816 background: var(--bg-elevated);
1817 border: 1px solid var(--border);
98f45b4Claude1818 border-radius: 16px;
1819 box-shadow:
1820 var(--elev-2),
1821 0 24px 64px -16px rgba(140, 109, 255, 0.12);
1822 position: relative;
1823 overflow: hidden;
1824 }
1825 .auth-container::before {
1826 /* Hairline gradient accent on the top edge — signals 'AI-native'
1827 without shouting. Pointer-events disabled so it never interferes
1828 with form interactions. */
1829 content: '';
1830 position: absolute;
1831 top: 0;
1832 left: 0;
1833 right: 0;
1834 height: 2px;
1835 background: linear-gradient(90deg, transparent 0%, #8c6dff 30%, #36c5d6 70%, transparent 100%);
1836 opacity: 0.7;
1837 pointer-events: none;
2ce1d0bClaude1838 }
1839 .auth-container h2 {
98f45b4Claude1840 margin: 0 0 8px;
1841 font-size: 28px;
1842 font-weight: 700;
1843 font-family: var(--font-display);
1844 letter-spacing: -0.025em;
1845 color: var(--text-strong);
1846 line-height: 1.15;
1847 }
1848 .auth-container .auth-subtitle {
1849 color: var(--text-muted);
1850 font-size: 14.5px;
1851 line-height: 1.5;
1852 margin: 0 0 24px;
2ce1d0bClaude1853 }
1854 .auth-container > p {
1855 color: var(--text-muted);
1856 font-size: var(--t-sm);
1857 margin-bottom: 24px;
1858 }
98f45b4Claude1859 .auth-container .btn-primary {
1860 width: 100%;
1861 padding: 12px 16px;
1862 font-size: 15px;
1863 font-weight: 600;
1864 margin-top: 4px;
1865 }
06d5ffeClaude1866 .auth-error {
958d26aClaude1867 background: rgba(248,113,113,0.08);
1868 border: 1px solid rgba(248,113,113,0.35);
06d5ffeClaude1869 color: var(--red);
2ce1d0bClaude1870 padding: 10px 14px;
1871 border-radius: var(--r-sm);
06d5ffeClaude1872 margin-bottom: 16px;
2ce1d0bClaude1873 font-size: var(--t-sm);
06d5ffeClaude1874 }
1875 .auth-success {
958d26aClaude1876 background: rgba(52,211,153,0.08);
1877 border: 1px solid rgba(52,211,153,0.35);
06d5ffeClaude1878 color: var(--green);
2ce1d0bClaude1879 padding: 10px 14px;
1880 border-radius: var(--r-sm);
06d5ffeClaude1881 margin-bottom: 16px;
2ce1d0bClaude1882 font-size: var(--t-sm);
1883 }
1884 .auth-switch {
1885 margin-top: 20px;
1886 font-size: var(--t-sm);
1887 color: var(--text-muted);
1888 text-align: center;
1889 }
1890 .banner {
1891 background: var(--accent-gradient-faint);
958d26aClaude1892 border: 1px solid rgba(140,109,255,0.35);
2ce1d0bClaude1893 color: var(--text);
1894 padding: 10px 14px;
1895 border-radius: var(--r-sm);
1896 font-size: var(--t-sm);
06d5ffeClaude1897 }
1898
2ce1d0bClaude1899 /* ============================================================ */
1900 /* Settings */
1901 /* ============================================================ */
1902 .settings-container { max-width: 720px; }
1903 .settings-container h2 { margin-bottom: 8px; font-size: var(--t-xl); letter-spacing: -0.02em; }
1904 .settings-container > h2 + p { color: var(--text-muted); font-size: var(--t-sm); margin-bottom: 24px; }
1905 .settings-container h3 { font-size: var(--t-md); margin-bottom: 12px; margin-top: 28px; }
1906 .settings-container h3:first-of-type { margin-top: 0; }
06d5ffeClaude1907 .ssh-keys-list { margin-bottom: 24px; }
1908 .ssh-key-item {
1909 display: flex;
1910 justify-content: space-between;
1911 align-items: center;
2ce1d0bClaude1912 padding: 14px 16px;
06d5ffeClaude1913 border: 1px solid var(--border);
2ce1d0bClaude1914 border-radius: var(--r-md);
06d5ffeClaude1915 margin-bottom: 8px;
2ce1d0bClaude1916 background: var(--bg-elevated);
1917 transition: border-color var(--t-fast) var(--ease);
06d5ffeClaude1918 }
2ce1d0bClaude1919 .ssh-key-item:hover { border-color: var(--border-strong); }
1920 .ssh-key-meta { font-size: var(--t-xs); color: var(--text-muted); margin-top: 4px; }
1921 .ssh-key-meta code { font-size: var(--t-xs); background: var(--bg-tertiary); padding: 1px 6px; border-radius: 3px; margin-right: 8px; }
06d5ffeClaude1922
2ce1d0bClaude1923 /* ============================================================ */
1924 /* Repo header + nav */
1925 /* ============================================================ */
fc1817aClaude1926 .repo-header {
1927 display: flex;
1928 align-items: center;
debcf27Claude1929 gap: 12px;
1930 margin-bottom: 22px;
1931 }
1932 .repo-header-title {
1933 display: flex;
1934 align-items: center;
1935 gap: 10px;
1936 font-family: var(--font-display);
1937 font-size: 24px;
1938 letter-spacing: -0.025em;
1939 flex-wrap: wrap;
1940 }
1941 .repo-header .owner {
1942 color: var(--text-muted);
1943 font-weight: 500;
1944 transition: color var(--t-fast) var(--ease);
1945 }
1946 .repo-header .owner:hover { color: var(--text-link); text-decoration: none; }
1947 .repo-header .separator { color: var(--text-faint); font-weight: 300; }
1948 .repo-header .name {
1949 color: var(--text-strong);
1950 font-weight: 700;
1951 letter-spacing: -0.028em;
fc1817aClaude1952 }
2ce1d0bClaude1953 .repo-header .name:hover { color: var(--text-link); text-decoration: none; }
debcf27Claude1954 .repo-header-fork {
1955 font-family: var(--font-mono);
1956 font-size: 11px;
1957 color: var(--text-muted);
1958 margin-top: 4px;
1959 letter-spacing: 0.01em;
1960 }
1961 .repo-header-fork a { color: var(--text-muted); }
1962 .repo-header-fork a:hover { color: var(--accent); }
1963 .repo-header-pill {
1964 display: inline-flex;
1965 align-items: center;
1966 padding: 2px 10px;
1967 border-radius: var(--r-full);
1968 font-family: var(--font-mono);
1969 font-size: 10px;
1970 font-weight: 600;
1971 letter-spacing: 0.12em;
1972 text-transform: uppercase;
1973 line-height: 1.6;
1974 vertical-align: 4px;
1975 }
1976 .repo-header-pill-archived {
1977 background: rgba(251,191,36,0.10);
1978 color: var(--yellow);
1979 border: 1px solid rgba(251,191,36,0.30);
1980 }
1981 .repo-header-pill-template {
1982 background: var(--accent-gradient-faint);
1983 color: var(--accent);
1984 border: 1px solid rgba(140,109,255,0.30);
fc1817aClaude1985 }
06d5ffeClaude1986 .repo-header-actions { margin-left: auto; display: flex; gap: 8px; align-items: center; }
fc1817aClaude1987
1988 .repo-nav {
1989 display: flex;
debcf27Claude1990 gap: 1px;
fc1817aClaude1991 border-bottom: 1px solid var(--border);
debcf27Claude1992 margin-bottom: 28px;
2ce1d0bClaude1993 overflow-x: auto;
1994 scrollbar-width: thin;
fc1817aClaude1995 }
2ce1d0bClaude1996 .repo-nav::-webkit-scrollbar { height: 0; }
fc1817aClaude1997 .repo-nav a {
debcf27Claude1998 position: relative;
1999 padding: 11px 14px;
fc1817aClaude2000 color: var(--text-muted);
2001 border-bottom: 2px solid transparent;
2ce1d0bClaude2002 font-size: var(--t-sm);
2003 font-weight: 500;
2004 margin-bottom: -1px;
debcf27Claude2005 transition: color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
2ce1d0bClaude2006 white-space: nowrap;
2007 }
debcf27Claude2008 .repo-nav a:hover { text-decoration: none; color: var(--text-strong); background: var(--bg-hover); }
2ce1d0bClaude2009 .repo-nav a.active {
debcf27Claude2010 color: var(--text-strong);
2011 font-weight: 600;
2012 }
2013 .repo-nav a.active::after {
2014 content: '';
2015 position: absolute;
2016 left: 14px;
2017 right: 14px;
2018 bottom: -1px;
2019 height: 2px;
2020 background: var(--accent-gradient);
2021 border-radius: 2px;
2022 }
2023 /* AI links in the right-side cluster — sparkle accent */
2024 .repo-nav-ai {
2025 color: var(--accent) !important;
2026 font-weight: 500;
2027 }
2028 .repo-nav-ai:hover {
2029 color: var(--accent-hover) !important;
2030 background: var(--accent-gradient-faint) !important;
2031 }
2032 .repo-nav-ai.active {
2033 color: var(--accent-hover) !important;
2ce1d0bClaude2034 font-weight: 600;
fc1817aClaude2035 }
2036
2ce1d0bClaude2037 .breadcrumb {
2038 display: flex;
debcf27Claude2039 gap: 6px;
2ce1d0bClaude2040 align-items: center;
debcf27Claude2041 margin-bottom: 18px;
2ce1d0bClaude2042 color: var(--text-muted);
2043 font-size: var(--t-sm);
2044 font-family: var(--font-mono);
debcf27Claude2045 font-feature-settings: var(--mono-feat);
2ce1d0bClaude2046 }
2047 .breadcrumb a { color: var(--text-link); font-weight: 500; }
2048 .breadcrumb a:hover { color: var(--accent-hover); }
debcf27Claude2049 .breadcrumb strong {
2050 color: var(--text-strong);
2051 font-weight: 600;
fc1817aClaude2052 }
2053
debcf27Claude2054 /* Page header — eyebrow + title + optional actions row.
2055 Use on dashboard, settings, admin, any "section landing" page. */
2056 .page-header {
2057 display: flex;
2058 align-items: flex-end;
2059 justify-content: space-between;
2060 gap: 16px;
2061 margin-bottom: 28px;
2062 padding-bottom: 20px;
2063 border-bottom: 1px solid var(--border-subtle);
2064 flex-wrap: wrap;
2065 }
2066 .page-header-text { flex: 1; min-width: 280px; }
2067 .page-header .eyebrow { margin-bottom: var(--s-2); }
2068 .page-header h1 {
2069 font-family: var(--font-display);
2070 font-size: clamp(24px, 3vw, 36px);
2071 line-height: 1.1;
2072 letter-spacing: -0.028em;
2073 margin-bottom: 6px;
2074 }
2075 .page-header p {
2076 color: var(--text-muted);
2077 font-size: var(--t-sm);
2078 line-height: 1.55;
2079 max-width: 640px;
2080 }
2081 .page-header-actions { display: flex; gap: 8px; align-items: center; }
fc1817aClaude2082
2ce1d0bClaude2083 /* ============================================================ */
2084 /* File browser table */
2085 /* ============================================================ */
2086 .file-table {
2087 width: 100%;
2088 border: 1px solid var(--border);
2089 border-radius: var(--r-md);
2090 overflow: hidden;
2091 background: var(--bg-elevated);
debcf27Claude2092 border-collapse: collapse;
2ce1d0bClaude2093 }
debcf27Claude2094 .file-table tr { border-bottom: 1px solid var(--border-subtle); transition: background var(--t-fast) var(--ease); }
fc1817aClaude2095 .file-table tr:last-child { border-bottom: none; }
debcf27Claude2096 .file-table td {
2097 padding: 9px 16px;
2098 font-size: var(--t-sm);
2099 font-family: var(--font-mono);
2100 font-feature-settings: var(--mono-feat);
2101 }
2ce1d0bClaude2102 .file-table tr:hover { background: var(--bg-hover); }
debcf27Claude2103 .file-icon {
2104 width: 22px;
2105 color: var(--text-faint);
2106 font-size: 13px;
2107 text-align: center;
2108 }
2109 .file-name a {
2110 color: var(--text);
2111 font-weight: 500;
2112 transition: color var(--t-fast) var(--ease);
2113 }
2114 .file-name a:hover { color: var(--accent); text-decoration: none; }
fc1817aClaude2115
2ce1d0bClaude2116 /* ============================================================ */
2117 /* Blob view */
2118 /* ============================================================ */
fc1817aClaude2119 .blob-view {
2120 border: 1px solid var(--border);
2ce1d0bClaude2121 border-radius: var(--r-md);
fc1817aClaude2122 overflow: hidden;
2ce1d0bClaude2123 background: var(--bg-elevated);
fc1817aClaude2124 }
2125 .blob-header {
2126 background: var(--bg-secondary);
2ce1d0bClaude2127 padding: 10px 16px;
fc1817aClaude2128 border-bottom: 1px solid var(--border);
2ce1d0bClaude2129 font-size: var(--t-sm);
fc1817aClaude2130 color: var(--text-muted);
06d5ffeClaude2131 display: flex;
2132 justify-content: space-between;
2133 align-items: center;
fc1817aClaude2134 }
2135 .blob-code {
2136 overflow-x: auto;
2137 font-family: var(--font-mono);
2ce1d0bClaude2138 font-size: var(--t-sm);
2139 line-height: 1.65;
fc1817aClaude2140 }
2141 .blob-code table { width: 100%; border-collapse: collapse; }
2142 .blob-code .line-num {
2143 width: 1%;
2ce1d0bClaude2144 min-width: 56px;
2145 padding: 0 14px;
fc1817aClaude2146 text-align: right;
2ce1d0bClaude2147 color: var(--text-faint);
fc1817aClaude2148 user-select: none;
2149 white-space: nowrap;
2150 border-right: 1px solid var(--border);
2151 }
2ce1d0bClaude2152 .blob-code .line-content { padding: 0 16px; white-space: pre; }
2153 .blob-code tr:hover { background: var(--bg-hover); }
fc1817aClaude2154
2ce1d0bClaude2155 /* ============================================================ */
2156 /* Commits + diffs */
2157 /* ============================================================ */
2158 .commit-list {
2159 border: 1px solid var(--border);
2160 border-radius: var(--r-md);
2161 overflow: hidden;
2162 background: var(--bg-elevated);
2163 }
fc1817aClaude2164 .commit-item {
2165 display: flex;
2166 justify-content: space-between;
2167 align-items: center;
debcf27Claude2168 padding: 14px 18px;
2169 border-bottom: 1px solid var(--border-subtle);
2ce1d0bClaude2170 transition: background var(--t-fast) var(--ease);
fc1817aClaude2171 }
2172 .commit-item:last-child { border-bottom: none; }
2ce1d0bClaude2173 .commit-item:hover { background: var(--bg-hover); }
debcf27Claude2174 .commit-message {
2175 font-size: var(--t-sm);
2176 font-weight: 500;
2177 line-height: 1.45;
2178 color: var(--text-strong);
2179 letter-spacing: -0.005em;
2180 }
2181 .commit-meta {
2182 font-family: var(--font-mono);
2183 font-size: 11px;
2184 color: var(--text-muted);
2185 margin-top: 5px;
2186 letter-spacing: 0.01em;
2187 }
fc1817aClaude2188 .commit-sha {
2189 font-family: var(--font-mono);
debcf27Claude2190 font-feature-settings: var(--mono-feat);
2191 font-size: 11px;
2192 padding: 4px 9px;
fc1817aClaude2193 background: var(--bg-tertiary);
2194 border: 1px solid var(--border);
2ce1d0bClaude2195 border-radius: var(--r-sm);
debcf27Claude2196 color: var(--accent);
2197 font-weight: 600;
2198 letter-spacing: 0.02em;
2ce1d0bClaude2199 transition: all var(--t-fast) var(--ease);
fc1817aClaude2200 }
debcf27Claude2201 .commit-sha:hover {
2202 border-color: rgba(140,109,255,0.40);
2203 background: var(--accent-gradient-faint);
2204 color: var(--accent-hover);
2205 text-decoration: none;
fc1817aClaude2206 }
2207
2208 .diff-view { margin-top: 16px; }
2209 .diff-file {
2210 border: 1px solid var(--border);
2ce1d0bClaude2211 border-radius: var(--r-md);
fc1817aClaude2212 margin-bottom: 16px;
2213 overflow: hidden;
2ce1d0bClaude2214 background: var(--bg-elevated);
fc1817aClaude2215 }
2216 .diff-file-header {
2217 background: var(--bg-secondary);
2ce1d0bClaude2218 padding: 10px 16px;
fc1817aClaude2219 border-bottom: 1px solid var(--border);
2220 font-family: var(--font-mono);
2ce1d0bClaude2221 font-size: var(--t-sm);
2222 font-weight: 500;
fc1817aClaude2223 }
2224 .diff-content {
2225 overflow-x: auto;
2226 font-family: var(--font-mono);
2ce1d0bClaude2227 font-size: var(--t-sm);
2228 line-height: 1.65;
fc1817aClaude2229 }
958d26aClaude2230 .diff-content .line-add { background: rgba(52,211,153,0.10); color: var(--green); }
2231 .diff-content .line-del { background: rgba(248,113,113,0.08); color: var(--red); }
2232 .diff-content .line-hunk { background: rgba(140,109,255,0.06); color: var(--text-link); }
2ce1d0bClaude2233 .diff-content .line { padding: 0 16px; white-space: pre; display: block; }
fc1817aClaude2234
2235 .stat-add { color: var(--green); font-weight: 600; }
2236 .stat-del { color: var(--red); font-weight: 600; }
2237
2ce1d0bClaude2238 /* ============================================================ */
2239 /* Empty state */
2240 /* ============================================================ */
fc1817aClaude2241 .empty-state {
2242 text-align: center;
958d26aClaude2243 padding: 96px 24px;
fc1817aClaude2244 color: var(--text-muted);
2ce1d0bClaude2245 border: 1px dashed var(--border);
2246 border-radius: var(--r-lg);
958d26aClaude2247 background:
2248 radial-gradient(60% 60% at 50% 0%, rgba(140,109,255,0.05), transparent 70%),
2249 var(--bg-elevated);
2250 position: relative;
2251 overflow: hidden;
2252 }
2253 .empty-state::before {
2254 content: '';
2255 position: absolute;
2256 inset: 0;
2257 background-image: radial-gradient(rgba(255,255,255,0.025) 1px, transparent 1px);
2258 background-size: 24px 24px;
2259 opacity: 0.5;
2260 pointer-events: none;
2261 mask-image: radial-gradient(ellipse at center, #000 30%, transparent 70%);
2262 -webkit-mask-image: radial-gradient(ellipse at center, #000 30%, transparent 70%);
2263 }
2264 :root[data-theme='light'] .empty-state::before {
2265 background-image: radial-gradient(rgba(15,16,28,0.08) 1px, transparent 1px);
fc1817aClaude2266 }
958d26aClaude2267 .empty-state > * { position: relative; z-index: 1; }
2ce1d0bClaude2268 .empty-state h2 {
958d26aClaude2269 font-size: var(--t-xl);
2ce1d0bClaude2270 margin-bottom: 8px;
958d26aClaude2271 color: var(--text-strong);
2272 letter-spacing: -0.022em;
2ce1d0bClaude2273 }
958d26aClaude2274 .empty-state p { font-size: var(--t-md); max-width: 480px; margin: 0 auto; line-height: 1.55; }
fc1817aClaude2275 .empty-state pre {
2276 text-align: left;
2277 display: inline-block;
2278 background: var(--bg-secondary);
958d26aClaude2279 padding: 18px 24px;
2280 border-radius: var(--r-md);
fc1817aClaude2281 border: 1px solid var(--border);
2282 font-family: var(--font-mono);
958d26aClaude2283 font-feature-settings: var(--mono-feat);
2ce1d0bClaude2284 font-size: var(--t-sm);
958d26aClaude2285 margin-top: 24px;
fc1817aClaude2286 line-height: 1.8;
2ce1d0bClaude2287 color: var(--text);
958d26aClaude2288 box-shadow: var(--elev-1);
fc1817aClaude2289 }
2290
2ce1d0bClaude2291 /* ============================================================ */
2292 /* Badges */
2293 /* ============================================================ */
fc1817aClaude2294 .badge {
2ce1d0bClaude2295 display: inline-flex;
2296 align-items: center;
2297 gap: 4px;
2298 padding: 2px 9px;
2299 border-radius: var(--r-full);
2300 font-size: var(--t-xs);
fc1817aClaude2301 font-weight: 500;
2302 background: var(--bg-tertiary);
2303 border: 1px solid var(--border);
2304 color: var(--text-muted);
2ce1d0bClaude2305 line-height: 1.5;
fc1817aClaude2306 }
2307
2ce1d0bClaude2308 /* ============================================================ */
2309 /* Branch dropdown */
2310 /* ============================================================ */
fc1817aClaude2311 .branch-selector {
2312 display: inline-flex;
2313 align-items: center;
2ce1d0bClaude2314 gap: 6px;
2315 padding: 6px 12px;
2316 background: var(--bg-elevated);
fc1817aClaude2317 border: 1px solid var(--border);
2ce1d0bClaude2318 border-radius: var(--r-sm);
2319 font-size: var(--t-sm);
fc1817aClaude2320 color: var(--text);
2321 margin-bottom: 12px;
2ce1d0bClaude2322 transition: border-color var(--t-fast) var(--ease);
fc1817aClaude2323 }
2ce1d0bClaude2324 .branch-selector:hover { border-color: var(--border-strong); }
fc1817aClaude2325
06d5ffeClaude2326 .branch-dropdown {
2327 position: relative;
2328 display: inline-block;
2329 margin-bottom: 12px;
2330 }
2331 .branch-dropdown-content {
2332 display: none;
2333 position: absolute;
2ce1d0bClaude2334 top: calc(100% + 4px);
06d5ffeClaude2335 left: 0;
2336 z-index: 10;
2ce1d0bClaude2337 min-width: 220px;
2338 background: var(--bg-elevated);
2339 border: 1px solid var(--border-strong);
2340 border-radius: var(--r-md);
2341 overflow: hidden;
2342 box-shadow: var(--elev-3);
06d5ffeClaude2343 }
2344 .branch-dropdown:hover .branch-dropdown-content,
2345 .branch-dropdown:focus-within .branch-dropdown-content { display: block; }
2346 .branch-dropdown-content a {
2347 display: block;
2ce1d0bClaude2348 padding: 9px 14px;
2349 font-size: var(--t-sm);
06d5ffeClaude2350 color: var(--text);
2351 border-bottom: 1px solid var(--border);
2ce1d0bClaude2352 transition: background var(--t-fast) var(--ease);
06d5ffeClaude2353 }
2354 .branch-dropdown-content a:last-child { border-bottom: none; }
2ce1d0bClaude2355 .branch-dropdown-content a:hover { background: var(--bg-hover); text-decoration: none; }
2356 .branch-dropdown-content a.active-branch { color: var(--text-link); font-weight: 600; background: var(--accent-gradient-faint); }
06d5ffeClaude2357
2ce1d0bClaude2358 /* ============================================================ */
2359 /* Card grid */
2360 /* ============================================================ */
2361 .card-grid {
2362 display: grid;
2363 grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
2364 gap: 16px;
2365 }
fc1817aClaude2366 .card {
2367 border: 1px solid var(--border);
2ce1d0bClaude2368 border-radius: var(--r-md);
958d26aClaude2369 padding: 20px;
2ce1d0bClaude2370 background: var(--bg-elevated);
2371 transition:
958d26aClaude2372 border-color var(--t-base) var(--ease),
2373 transform var(--t-base) var(--ease-out-quart),
2ce1d0bClaude2374 box-shadow var(--t-base) var(--ease);
2375 position: relative;
2376 overflow: hidden;
958d26aClaude2377 isolation: isolate;
2378 }
2379 .card::before {
2380 content: '';
2381 position: absolute;
2382 inset: 0;
2383 background: linear-gradient(135deg, rgba(140,109,255,0.06), transparent 50%);
2384 opacity: 0;
2385 transition: opacity var(--t-base) var(--ease);
2386 pointer-events: none;
2387 z-index: -1;
2ce1d0bClaude2388 }
2389 .card:hover {
2390 border-color: var(--border-strong);
2391 box-shadow: var(--elev-2);
958d26aClaude2392 transform: translateY(-2px);
2ce1d0bClaude2393 }
958d26aClaude2394 .card:hover::before { opacity: 1; }
2395 .card h3 { font-size: var(--t-md); margin-bottom: 6px; letter-spacing: -0.012em; }
2ce1d0bClaude2396 .card h3 a { color: var(--text); font-weight: 600; }
2397 .card h3 a:hover { color: var(--text-link); }
2398 .card p { font-size: var(--t-sm); color: var(--text-muted); line-height: 1.5; }
2399 .card-meta {
2400 display: flex;
2401 gap: 14px;
2402 margin-top: 14px;
2403 padding-top: 14px;
2404 border-top: 1px solid var(--border);
2405 font-size: var(--t-xs);
2406 color: var(--text-muted);
2407 }
2408 .card-meta span { display: flex; align-items: center; gap: 5px; }
2409
2410 /* ============================================================ */
2411 /* Stat card / box */
2412 /* ============================================================ */
2413 .stat-card {
2414 background: var(--bg-elevated);
2415 border: 1px solid var(--border);
2416 border-radius: var(--r-md);
fc1817aClaude2417 padding: 16px;
2ce1d0bClaude2418 transition: border-color var(--t-fast) var(--ease);
2419 }
2420 .stat-card:hover { border-color: var(--border-strong); }
2421 .stat-label {
2422 font-size: var(--t-xs);
2423 color: var(--text-muted);
2424 text-transform: uppercase;
2425 letter-spacing: 0.06em;
2426 font-weight: 500;
2427 }
2428 .stat-value {
2429 font-size: var(--t-xl);
2430 font-weight: 700;
2431 margin-top: 4px;
2432 letter-spacing: -0.025em;
2433 color: var(--text);
2434 font-feature-settings: 'tnum';
fc1817aClaude2435 }
06d5ffeClaude2436
2ce1d0bClaude2437 /* ============================================================ */
2438 /* Star button */
2439 /* ============================================================ */
06d5ffeClaude2440 .star-btn {
2441 display: inline-flex;
2442 align-items: center;
2443 gap: 6px;
2ce1d0bClaude2444 padding: 5px 12px;
2445 background: var(--bg-elevated);
06d5ffeClaude2446 border: 1px solid var(--border);
2ce1d0bClaude2447 border-radius: var(--r-sm);
06d5ffeClaude2448 color: var(--text);
2ce1d0bClaude2449 font-size: var(--t-sm);
2450 font-weight: 500;
06d5ffeClaude2451 cursor: pointer;
2ce1d0bClaude2452 transition: all var(--t-fast) var(--ease);
2453 }
2454 .star-btn:hover { background: var(--bg-surface); border-color: var(--border-strong); text-decoration: none; }
2455 .star-btn.starred {
2456 color: var(--yellow);
958d26aClaude2457 border-color: rgba(251,191,36,0.4);
2458 background: rgba(251,191,36,0.08);
06d5ffeClaude2459 }
2460
2ce1d0bClaude2461 /* ============================================================ */
2462 /* User profile */
2463 /* ============================================================ */
06d5ffeClaude2464 .user-profile {
2465 display: flex;
2466 gap: 32px;
2467 margin-bottom: 32px;
2ce1d0bClaude2468 padding: 24px;
2469 background: var(--bg-elevated);
2470 border: 1px solid var(--border);
2471 border-radius: var(--r-lg);
06d5ffeClaude2472 }
2473 .user-avatar {
2474 width: 96px;
2475 height: 96px;
2ce1d0bClaude2476 border-radius: var(--r-full);
2477 background: var(--accent-gradient-soft);
2478 border: 1px solid var(--border-strong);
06d5ffeClaude2479 display: flex;
2480 align-items: center;
2481 justify-content: center;
2ce1d0bClaude2482 font-size: 36px;
2483 font-weight: 600;
2484 color: var(--text);
06d5ffeClaude2485 flex-shrink: 0;
2ce1d0bClaude2486 letter-spacing: -0.02em;
06d5ffeClaude2487 }
2ce1d0bClaude2488 .user-info h2 { font-size: var(--t-xl); margin-bottom: 2px; letter-spacing: -0.025em; }
2489 .user-info .username { font-size: var(--t-md); color: var(--text-muted); }
2490 .user-info .bio { font-size: var(--t-sm); color: var(--text-muted); margin-top: 10px; line-height: 1.55; }
06d5ffeClaude2491
2ce1d0bClaude2492 /* ============================================================ */
2493 /* New repo form */
2494 /* ============================================================ */
2495 .new-repo-form { max-width: 640px; }
2496 .new-repo-form h2 { margin-bottom: 24px; font-size: var(--t-xl); letter-spacing: -0.025em; }
2497 .visibility-options { display: flex; gap: 12px; margin-bottom: 20px; }
06d5ffeClaude2498 .visibility-option {
2499 flex: 1;
2ce1d0bClaude2500 padding: 16px;
06d5ffeClaude2501 border: 1px solid var(--border);
2ce1d0bClaude2502 border-radius: var(--r-md);
2503 background: var(--bg-elevated);
06d5ffeClaude2504 cursor: pointer;
2ce1d0bClaude2505 text-align: left;
2506 transition: all var(--t-fast) var(--ease);
2507 }
2508 .visibility-option:hover { border-color: var(--border-strong); background: var(--bg-surface); }
2509 .visibility-option:has(input:checked) {
2510 border-color: var(--accent);
2511 background: var(--accent-gradient-faint);
2512 box-shadow: 0 0 0 1px var(--accent);
06d5ffeClaude2513 }
2514 .visibility-option input { display: none; }
2ce1d0bClaude2515 .visibility-option .vis-label { font-size: var(--t-sm); font-weight: 600; margin-bottom: 4px; }
2516 .visibility-option .vis-desc { font-size: var(--t-xs); color: var(--text-muted); }
79136bbClaude2517
2ce1d0bClaude2518 /* ============================================================ */
2519 /* Issues */
2520 /* ============================================================ */
2521 .issue-tabs { display: flex; gap: 4px; }
2522 .issue-tabs a {
2523 color: var(--text-muted);
2524 font-size: var(--t-sm);
2525 font-weight: 500;
2526 padding: 6px 12px;
2527 border-radius: var(--r-sm);
2528 transition: all var(--t-fast) var(--ease);
2529 }
2530 .issue-tabs a:hover { color: var(--text); background: var(--bg-hover); text-decoration: none; }
2531 .issue-tabs a.active { color: var(--text); background: var(--bg-elevated); }
79136bbClaude2532
2ce1d0bClaude2533 .issue-list {
2534 border: 1px solid var(--border);
2535 border-radius: var(--r-md);
2536 overflow: hidden;
2537 background: var(--bg-elevated);
2538 }
79136bbClaude2539 .issue-item {
2540 display: flex;
2ce1d0bClaude2541 gap: 14px;
79136bbClaude2542 align-items: flex-start;
2ce1d0bClaude2543 padding: 14px 16px;
79136bbClaude2544 border-bottom: 1px solid var(--border);
2ce1d0bClaude2545 transition: background var(--t-fast) var(--ease);
79136bbClaude2546 }
2547 .issue-item:last-child { border-bottom: none; }
2ce1d0bClaude2548 .issue-item:hover { background: var(--bg-hover); }
2549 .issue-state-icon {
2550 font-size: 14px;
2551 padding-top: 2px;
2552 width: 18px;
2553 height: 18px;
2554 display: inline-flex;
2555 align-items: center;
2556 justify-content: center;
2557 border-radius: var(--r-full);
2558 flex-shrink: 0;
2559 }
79136bbClaude2560 .state-open { color: var(--green); }
958d26aClaude2561 .state-closed { color: #b69dff; }
2ce1d0bClaude2562 .issue-title {
debcf27Claude2563 font-family: var(--font-display);
2564 font-size: var(--t-md);
2ce1d0bClaude2565 font-weight: 600;
debcf27Claude2566 line-height: 1.35;
2567 letter-spacing: -0.012em;
2568 }
2569 .issue-title a { color: var(--text-strong); transition: color var(--t-fast) var(--ease); }
2570 .issue-title a:hover { color: var(--accent); text-decoration: none; }
2571 .issue-meta {
2572 font-family: var(--font-mono);
2573 font-size: 11px;
2574 color: var(--text-muted);
2575 margin-top: 5px;
2576 letter-spacing: 0.01em;
2ce1d0bClaude2577 }
79136bbClaude2578
2579 .issue-badge {
2580 display: inline-flex;
2581 align-items: center;
2ce1d0bClaude2582 gap: 6px;
79136bbClaude2583 padding: 4px 12px;
2ce1d0bClaude2584 border-radius: var(--r-full);
2585 font-size: var(--t-sm);
79136bbClaude2586 font-weight: 500;
2ce1d0bClaude2587 line-height: 1.4;
79136bbClaude2588 }
958d26aClaude2589 .badge-open { background: rgba(52,211,153,0.10); color: var(--green); border: 1px solid rgba(52,211,153,0.35); }
2590 .badge-closed { background: rgba(182,157,255,0.10); color: #b69dff; border: 1px solid rgba(182,157,255,0.35); }
2591 .badge-merged { background: rgba(140,109,255,0.10); color: var(--accent); border: 1px solid rgba(140,109,255,0.35); }
2ce1d0bClaude2592 .state-merged { color: var(--accent); }
958d26aClaude2593 .ai-review { border-color: var(--accent); box-shadow: 0 0 0 1px rgba(140,109,255,0.3); }
79136bbClaude2594
2ce1d0bClaude2595 .issue-detail { max-width: 920px; }
79136bbClaude2596 .issue-comment-box {
2597 border: 1px solid var(--border);
2ce1d0bClaude2598 border-radius: var(--r-md);
79136bbClaude2599 margin-bottom: 16px;
2600 overflow: hidden;
2ce1d0bClaude2601 background: var(--bg-elevated);
79136bbClaude2602 }
2603 .comment-header {
2604 background: var(--bg-secondary);
2ce1d0bClaude2605 padding: 10px 16px;
79136bbClaude2606 border-bottom: 1px solid var(--border);
2ce1d0bClaude2607 font-size: var(--t-sm);
79136bbClaude2608 color: var(--text-muted);
2609 }
2610
2ce1d0bClaude2611 /* ============================================================ */
2612 /* Panel — flexible container used across many pages */
2613 /* ============================================================ */
2614 .panel {
2615 background: var(--bg-elevated);
2616 border: 1px solid var(--border);
2617 border-radius: var(--r-md);
2618 overflow: hidden;
2619 }
2620 .panel-item {
2621 display: flex;
2622 align-items: center;
2623 gap: 12px;
2624 padding: 12px 16px;
79136bbClaude2625 border-bottom: 1px solid var(--border);
2ce1d0bClaude2626 transition: background var(--t-fast) var(--ease);
2627 }
2628 .panel-item:last-child { border-bottom: none; }
2629 .panel-item:hover { background: var(--bg-hover); }
2630 .panel-empty {
2631 padding: 24px;
2632 text-align: center;
79136bbClaude2633 color: var(--text-muted);
2ce1d0bClaude2634 font-size: var(--t-sm);
79136bbClaude2635 }
2636
2ce1d0bClaude2637 /* ============================================================ */
2638 /* Search */
2639 /* ============================================================ */
79136bbClaude2640 .search-results .diff-file { margin-bottom: 12px; }
16b325cClaude2641
2ce1d0bClaude2642 /* ============================================================ */
2643 /* Timeline */
2644 /* ============================================================ */
2645 .timeline { position: relative; padding-left: 28px; }
16b325cClaude2646 .timeline::before {
2647 content: '';
2648 position: absolute;
2649 left: 4px;
2650 top: 8px;
2651 bottom: 8px;
2652 width: 2px;
2ce1d0bClaude2653 background: linear-gradient(180deg, var(--border) 0%, transparent 100%);
16b325cClaude2654 }
2ce1d0bClaude2655 .timeline-item { position: relative; padding-bottom: 16px; }
16b325cClaude2656 .timeline-dot {
2657 position: absolute;
2ce1d0bClaude2658 left: -28px;
2659 top: 8px;
2660 width: 12px;
2661 height: 12px;
2662 border-radius: var(--r-full);
2663 background: var(--accent-gradient);
16b325cClaude2664 border: 2px solid var(--bg);
2ce1d0bClaude2665 box-shadow: 0 0 0 1px var(--border-strong);
16b325cClaude2666 }
2667 .timeline-content {
2ce1d0bClaude2668 background: var(--bg-elevated);
16b325cClaude2669 border: 1px solid var(--border);
2ce1d0bClaude2670 border-radius: var(--r-md);
2671 padding: 14px 16px;
16b325cClaude2672 }
f1ab587Claude2673
2ce1d0bClaude2674 /* ============================================================ */
2675 /* Toggle switch */
2676 /* ============================================================ */
f1ab587Claude2677 .toggle-switch {
2678 position: relative;
2679 display: inline-block;
2ce1d0bClaude2680 width: 40px;
2681 height: 22px;
f1ab587Claude2682 flex-shrink: 0;
2683 margin-left: 16px;
2684 }
2685 .toggle-switch input { opacity: 0; width: 0; height: 0; }
2686 .toggle-slider {
2687 position: absolute;
2688 cursor: pointer;
2689 top: 0; left: 0; right: 0; bottom: 0;
2690 background: var(--bg-tertiary);
2691 border: 1px solid var(--border);
2ce1d0bClaude2692 border-radius: var(--r-full);
2693 transition: all var(--t-base) var(--ease);
f1ab587Claude2694 }
2695 .toggle-slider::before {
2696 content: '';
2697 position: absolute;
2ce1d0bClaude2698 height: 16px;
2699 width: 16px;
f1ab587Claude2700 left: 2px;
2701 bottom: 2px;
2702 background: var(--text-muted);
2ce1d0bClaude2703 border-radius: var(--r-full);
2704 transition: all var(--t-base) var(--ease);
f1ab587Claude2705 }
2706 .toggle-switch input:checked + .toggle-slider {
2ce1d0bClaude2707 background: var(--accent-gradient);
2708 border-color: transparent;
958d26aClaude2709 box-shadow: 0 0 0 1px rgba(140,109,255,0.4);
f1ab587Claude2710 }
2711 .toggle-switch input:checked + .toggle-slider::before {
2ce1d0bClaude2712 transform: translateX(18px);
f1ab587Claude2713 background: #fff;
2714 }
ef8d378Claude2715
2716 /* ============================================================
2717 * 2026 polish layer (purely additive — no layout changes).
2718 * Improves typography rendering, focus states, hover affordances,
2719 * and adds the gradient brand cue to primary buttons. Anything
2720 * that could alter dimensions stays in the rules above.
2721 * ============================================================ */
2722 html { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
2723 body { letter-spacing: -0.005em; font-feature-settings: 'cv11', 'ss01', 'ss03'; }
2724 *::selection { background: rgba(168,85,247,0.35); color: var(--text); }
2725
2726 h1, h2, h3, h4 { letter-spacing: -0.018em; }
2727 h1 { letter-spacing: -0.025em; }
2728
2729 /* Smoother colour transitions everywhere links live */
2730 a { transition: color 120ms cubic-bezier(0.16,1,0.3,1); }
2731
2732 /* Buttons: focus rings + smoother transitions; primary gets the gradient */
2733 .btn {
2734 transition:
2735 background 120ms cubic-bezier(0.16,1,0.3,1),
2736 border-color 120ms cubic-bezier(0.16,1,0.3,1),
2737 transform 120ms cubic-bezier(0.16,1,0.3,1),
2738 box-shadow 120ms cubic-bezier(0.16,1,0.3,1);
2739 }
2740 .btn:active { transform: translateY(0.5px); }
2741 .btn:focus-visible {
2742 outline: none;
2743 box-shadow: 0 0 0 3px rgba(168,85,247,0.30);
2744 }
2745 .btn-primary {
2746 background: linear-gradient(135deg, #a855f7 0%, #06b6d4 100%);
2747 border-color: transparent;
2748 box-shadow:
2749 inset 0 1px 0 rgba(255,255,255,0.15),
2750 0 1px 2px rgba(168,85,247,0.25);
2751 }
2752 .btn-primary:hover {
2753 background: linear-gradient(135deg, #b766f8 0%, #22cce0 100%);
2754 filter: none;
2755 box-shadow:
2756 inset 0 1px 0 rgba(255,255,255,0.20),
2757 0 4px 12px rgba(168,85,247,0.30);
2758 }
2759
2760 /* Inputs: cleaner focus ring + hover */
2761 .form-group input:hover,
2762 .form-group textarea:hover,
2763 .form-group select:hover {
2764 border-color: rgba(255,255,255,0.14);
2765 }
2766 .form-group input:focus,
2767 .form-group textarea:focus,
2768 .form-group select:focus {
2769 border-color: rgba(168,85,247,0.55);
2770 box-shadow: 0 0 0 3px rgba(168,85,247,0.22);
2771 }
2772 :root[data-theme='light'] .form-group input:hover,
2773 :root[data-theme='light'] .form-group textarea:hover,
2774 :root[data-theme='light'] .form-group select:hover {
2775 border-color: rgba(0,0,0,0.18);
2776 }
2777
2778 /* Cards: subtle hover lift */
2779 .card {
2780 transition:
2781 border-color 160ms cubic-bezier(0.16,1,0.3,1),
2782 transform 160ms cubic-bezier(0.16,1,0.3,1),
2783 box-shadow 200ms cubic-bezier(0.16,1,0.3,1);
2784 }
2785 .card:hover {
2786 border-color: rgba(255,255,255,0.18);
2787 transform: translateY(-1px);
2788 box-shadow: 0 8px 24px rgba(0,0,0,0.30);
2789 }
2790 :root[data-theme='light'] .card:hover {
2791 border-color: rgba(0,0,0,0.18);
2792 box-shadow: 0 8px 24px rgba(0,0,0,0.08);
2793 }
2794
2795 /* Issue / commit / panel rows: smoother hover */
2796 .issue-item, .commit-item {
2797 transition: background 120ms cubic-bezier(0.16,1,0.3,1);
2798 }
2799 .repo-nav a {
2800 transition: color 120ms cubic-bezier(0.16,1,0.3,1),
2801 border-bottom-color 120ms cubic-bezier(0.16,1,0.3,1);
2802 }
2803 .nav-link {
2804 transition: color 120ms cubic-bezier(0.16,1,0.3,1);
2805 }
2806
2807 /* Auth card: subtle elevation so register/login feel premium */
2808 .auth-container {
2809 background: var(--bg-secondary);
2810 border: 1px solid var(--border);
2811 border-radius: var(--r-lg, 12px);
2812 padding: 32px;
2813 box-shadow: 0 4px 16px rgba(0,0,0,0.30), 0 0 0 1px var(--border);
2814 }
2815 :root[data-theme='light'] .auth-container {
2816 box-shadow: 0 4px 16px rgba(0,0,0,0.06), 0 0 0 1px var(--border);
2817 }
2818 .auth-container h2 { letter-spacing: -0.025em; }
2819
2820 /* Empty state: dashed border, generous padding */
2821 .empty-state {
2822 border: 1px dashed var(--border);
2823 border-radius: var(--r-lg, 12px);
2824 background: var(--bg);
2825 }
2826
2827 /* Badges + commit-sha: smoother transition */
2828 .commit-sha, .badge { transition: all 120ms cubic-bezier(0.16,1,0.3,1); }
2829
2830 /* Gradient text utility — matches landing's accent treatment */
2831 .gradient-text {
2832 background: linear-gradient(135deg, #a855f7 0%, #06b6d4 100%);
2833 -webkit-background-clip: text;
2834 background-clip: text;
2835 -webkit-text-fill-color: transparent;
2836 }
2837
2838 /* Custom scrollbars (subtle, themed) */
2839 ::-webkit-scrollbar { width: 10px; height: 10px; }
2840 ::-webkit-scrollbar-track { background: transparent; }
2841 ::-webkit-scrollbar-thumb {
2842 background: rgba(255,255,255,0.06);
2843 border: 2px solid var(--bg);
2844 border-radius: 9999px;
2845 }
2846 ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.14); }
2847 :root[data-theme='light'] ::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.10); }
2848 :root[data-theme='light'] ::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,0.18); }
2849
2850 /* Honour reduced-motion preference */
2851 @media (prefers-reduced-motion: reduce) {
2852 *, *::before, *::after {
2853 animation-duration: 0.01ms !important;
2854 animation-iteration-count: 1 !important;
2855 transition-duration: 0.01ms !important;
2856 }
2857 }
c63b860Claude2858
2859 /* Block O3 — visual coherence additive rules. */
2860 .card.card-p-none { padding: 0; }
2861 .card.card-p-sm { padding: var(--space-3); }
2862 .card.card-p-md { padding: var(--space-4); }
2863 .card.card-p-lg { padding: var(--space-6); }
2864 .card.card-elevated { box-shadow: var(--elev-2); }
2865 .card.card-gradient {
2866 background:
2867 linear-gradient(135deg, rgba(140,109,255,0.05), transparent 60%),
2868 var(--bg-elevated);
2869 }
2870 .card.card-gradient::before { opacity: 1; }
2871 .notice {
2872 padding: var(--space-3) var(--space-4);
2873 border-radius: var(--radius-md);
2874 border: 1px solid var(--border);
2875 background: var(--bg-elevated);
2876 color: var(--text);
2877 font-size: var(--font-size-sm);
2878 margin-bottom: var(--space-6);
2879 line-height: var(--leading-normal);
2880 }
2881 .notice-info { border-color: rgba(96,165,250,0.40); background: rgba(96,165,250,0.08); color: var(--text); }
2882 .notice-success { border-color: var(--green); background: rgba(52,211,153,0.08); color: var(--text); }
2883 .notice-warn { border-color: var(--yellow); background: rgba(251,191,36,0.10); color: var(--yellow); }
2884 .notice-error { border-color: var(--red); background: rgba(248,113,113,0.10); color: var(--red); }
2885 .notice-accent { border-color: var(--accent); background: rgba(140,109,255,0.10); color: var(--text); }
2886 .email-preview {
2887 padding: var(--space-5);
2888 background: #fff;
2889 color: #111;
2890 border-radius: var(--radius-md);
2891 }
2892 .code-block {
2893 margin: var(--space-2) 0 0;
2894 padding: var(--space-3);
2895 background: var(--bg-tertiary);
2896 border: 1px solid var(--border-subtle);
2897 border-radius: var(--radius-sm);
2898 font-family: var(--font-mono);
2899 font-size: var(--font-size-xs);
2900 line-height: var(--leading-normal);
2901 overflow-x: auto;
2902 }
2903 .status-pill-operational {
2904 display: inline-flex;
2905 align-items: center;
2906 padding: var(--space-1) var(--space-3);
2907 border-radius: var(--radius-full);
2908 font-size: var(--font-size-xs);
2909 font-weight: 600;
2910 background: rgba(52,211,153,0.15);
2911 color: var(--green);
2912 }
2913 .api-tag {
2914 display: inline-flex;
2915 align-items: center;
2916 font-size: var(--font-size-xs);
2917 padding: 2px var(--space-2);
2918 border-radius: var(--radius-sm);
2919 font-family: var(--font-mono);
2920 }
2921 .api-tag-auth { background: rgba(96,165,250,0.15); color: var(--accent); }
2922 .api-tag-scope { background: rgba(52,211,153,0.15); color: var(--green); }
2923 .stat-number {
2924 font-size: var(--font-size-xl);
2925 font-weight: 700;
2926 color: var(--text-strong);
2927 line-height: var(--leading-tight);
2928 }
2929 .stat-number-accent { color: var(--accent); }
2930 .stat-number-blue { color: var(--blue); }
2931 .stat-number-purple { color: var(--text-link); }
2932 footer .footer-tag-sub {
2933 margin-top: var(--space-2);
2934 font-size: var(--font-size-xs);
2935 color: var(--text-faint);
2936 line-height: var(--leading-normal);
2937 }
2938 footer .footer-version-pill {
2939 display: inline-flex;
2940 align-items: center;
2941 gap: var(--space-2);
2942 padding: var(--space-1) var(--space-3);
2943 border-radius: var(--radius-full);
2944 border: 1px solid var(--border);
2945 background: var(--bg-elevated);
2946 color: var(--text-faint);
2947 font-family: var(--font-mono);
2948 font-size: var(--font-size-xs);
2949 cursor: help;
2950 }
2951 footer .footer-banner {
2952 max-width: 1240px;
2953 margin: var(--space-6) auto 0;
2954 padding: var(--space-3) var(--space-4);
2955 border-radius: var(--radius-md);
2956 border: 1px solid var(--border);
2957 background: var(--bg-elevated);
2958 color: var(--text);
2959 font-family: var(--font-mono);
2960 font-size: var(--font-size-xs);
2961 letter-spacing: 0.04em;
2962 text-transform: uppercase;
2963 text-align: center;
2964 }
2965 footer .footer-banner-info { border-color: rgba(96,165,250,0.40); color: var(--blue); }
2966 footer .footer-banner-warn { border-color: rgba(251,191,36,0.40); color: var(--yellow); }
2967 footer .footer-banner-error { border-color: rgba(248,113,113,0.45); color: var(--red); }
dc26881CC LABS App2968
cf9178bTest User2969 /* ============================================================ */
2970 /* Global toast notifications. */
2971 /* Slide-in from right, auto-dismiss, ARIA-live for SR users. */
2972 /* ============================================================ */
2973 .gx-toast {
2974 pointer-events: auto;
2975 display: inline-flex;
2976 align-items: flex-start;
2977 gap: 10px;
2978 min-width: 280px;
2979 max-width: min(440px, calc(100vw - 32px));
2980 padding: 12px 14px 12px 12px;
2981 background: var(--bg-elevated);
2982 border: 1px solid var(--border);
2983 border-radius: 10px;
2984 box-shadow:
2985 0 12px 36px -10px rgba(15,16,28,0.18),
2986 0 2px 6px rgba(15,16,28,0.04),
2987 0 0 0 1px rgba(15,16,28,0.02);
2988 color: var(--text);
2989 font-size: 13.5px;
2990 line-height: 1.45;
2991 opacity: 0;
2992 transform: translateX(16px);
2993 transition:
2994 opacity 220ms cubic-bezier(0.2, 0.8, 0.2, 1),
2995 transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
2996 }
2997 .gx-toast--in { opacity: 1; transform: translateX(0); }
2998 .gx-toast--out { opacity: 0; transform: translateX(16px); }
2999 .gx-toast__icon {
3000 flex-shrink: 0;
3001 width: 20px; height: 20px;
3002 border-radius: 50%;
3003 display: inline-flex;
3004 align-items: center;
3005 justify-content: center;
3006 font-size: 12px;
3007 font-weight: 700;
3008 line-height: 1;
3009 margin-top: 1px;
3010 }
3011 .gx-toast__text { flex: 1 1 auto; min-width: 0; padding-top: 2px; word-wrap: break-word; }
3012 .gx-toast__close {
3013 flex-shrink: 0;
3014 width: 22px; height: 22px;
3015 border: 0;
3016 background: transparent;
3017 color: var(--text-muted);
3018 font-size: 18px;
3019 line-height: 1;
3020 cursor: pointer;
3021 border-radius: 4px;
3022 padding: 0;
3023 margin: -2px -2px 0 0;
3024 transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
3025 }
3026 .gx-toast__close:hover { background: var(--bg-hover); color: var(--text); }
3027 .gx-toast--success .gx-toast__icon { background: rgba(5,150,105,0.14); color: var(--green); }
3028 .gx-toast--error .gx-toast__icon { background: rgba(220,38,38,0.14); color: var(--red); }
3029 .gx-toast--warn .gx-toast__icon { background: rgba(217,119,6,0.16); color: var(--yellow); }
3030 .gx-toast--info .gx-toast__icon { background: rgba(109,77,255,0.14); color: var(--accent); }
3031 @media (prefers-reduced-motion: reduce) {
3032 .gx-toast { transition: opacity 60ms linear; transform: none; }
3033 .gx-toast--in, .gx-toast--out { transform: none; }
3034 }
3035
dc26881CC LABS App3036 /* ============================================================ */
3037 /* Block U4 — cross-document view transitions. */
3038 /* Chrome 126+, Edge, Safari 18.2+ get a soft 200ms fade on */
3039 /* every same-origin navigation. Older browsers ignore these */
3040 /* rules entirely — no JS shim, no breakage. */
3041 /* prefers-reduced-motion disables the animation honourably. */
3042 /* ============================================================ */
3043 @view-transition {
3044 navigation: auto;
3045 }
3046 ::view-transition-old(root),
3047 ::view-transition-new(root) {
3048 animation-duration: 200ms;
3049 animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
3050 }
3051 ::view-transition-old(root) {
3052 animation-name: vt-fade-out;
3053 }
3054 ::view-transition-new(root) {
3055 animation-name: vt-fade-in;
3056 }
3057 @keyframes vt-fade-out {
3058 to { opacity: 0; }
3059 }
3060 @keyframes vt-fade-in {
3061 from { opacity: 0; }
3062 }
3063 @media (prefers-reduced-motion: reduce) {
3064 ::view-transition-old(root),
3065 ::view-transition-new(root) {
3066 animation-duration: 0s;
3067 }
3068 }
3069 /* The transition system picks up its root subject from this rule. */
3070 body {
3071 view-transition-name: root;
3072 }
fc1817aClaude3073`;