Commitdc26881unknown_key
feat(BLOCK-U): senior polish pass (#74)
feat(BLOCK-U): senior polish pass (#74)
* ops: re-trigger Hetzner deploy
Empty commit to force hetzner-deploy.yml to run. The live site is
serving an older SHA (3fa949c, BLOCK M era) even though origin/main
is at b1be050 (BLOCK S). Most likely the previous deploy's
bun-compile or systemctl step silently no-op'd. With the BLOCK S
auto-rollback + smoke suite now on main, this re-run will either
succeed cleanly or fail loudly with the exact reason logged.
* style(landing): make 'Add to Claude Desktop' CTA theme-aware (was jarring black on light theme)
The Q1 .dxt CTA used a hardcoded #161b22 fallback for its inner fill,
which is near-black. On light theme that rendered as a loud black box
between the purple gradient 'Sign up free' and white 'Try the live
demo' CTAs — the user immediately flagged it.
Swap the hardcoded fallback for var(--bg-elevated), which is theme-
aware (light-mode: #ffffff, dark-mode: #0f111a). The gradient border +
accent text intent is preserved; the inner fill now harmonises with
whichever theme the visitor is on.
* feat(BLOCK-U): senior polish pass — hero rebuild + button polish + spacing rhythm + view transitions
Owner pushed back that the site "doesn't feel like it was built by an
AI agent." This block is purely how the site feels — no new
functionality, no new routes, just a senior designer's polish pass.
U1 — Landing hero rebuild
• CTA row trimmed from 4 → 2 primary ("Sign up free" + "Add to
Claude Desktop"); demoted the others to a small tertiary text-
link row ("Try the live demo · Compare to GitHub · Or try it
without signing up").
• Install snippet moved DOWN, repositioned as a "Power users"
panel after the CTAs (was previously between headline + CTAs
which broke the visual flow).
• Added three softly-blurred accent-gradient orbs behind the hero
(low-opacity, position absolute) — makes the section feel like
a real product page instead of a Tailwind tutorial.
• Vertical rhythm tightened — every block uses var(--space-6),
no more random var(--space-12) gaps.
• Micro-stats rail kept but reworked as a tighter horizontal row
with smaller numbers + gradient accent line.
U2 — Button polish (universal)
• Every .btn gets a translateY(-1px) lift + soft drop-shadow on
hover, 180ms ease.
• Press state (.btn:active) presses down to translateY(0) over
80ms (faster feedback).
• Focus ring switched from outline to box-shadow
(0 0 0 3px rgba(140,109,255,0.35)) — softer + more modern.
• .btn-primary gradient animates its background-position on
hover over 600ms — subtle shimmer that suggests "this is the
button."
• .btn:disabled locked at opacity 0.5, cursor not-allowed, no
lift on hover.
• All changes additive; existing button call sites unchanged.
U3 — Spacing rhythm audit
• 67 inline raw-pixel spacing values across the 8 top-trafficked
files snapped to the existing token scale
(--space-1/2/3/4/6/8/12).
• Files touched: dashboard.tsx (21), settings.tsx (11), admin-
deploys-page.tsx (9), admin.tsx (9), web.tsx (9), admin-ops.tsx
(5), layout.tsx (3). landing.tsx covered by U1.
• Values left untouched: <4px (invisible detail), >32px (layout-
specific), and viewport-width values like 92vw.
U4 — Page transitions via View Transitions API
• @view-transition { navigation: auto } in layout.tsx master CSS.
• ::view-transition-old(root) and ::view-transition-new(root)
cross-fade in 200ms cubic-bezier(0.2, 0.8, 0.2, 1).
• view-transition-name: root on the body so the transition
system has a subject.
• prefers-reduced-motion block sets animation-duration to 0s.
• Chrome 126+, Edge, Safari 18.2+ get smooth cross-page fades
for free. Older browsers ignore the rule.
Tests
src/__tests__/u-polish.test.ts (NEW, 148 lines, 12 tests):
• Rebuilt hero renders exactly 2 primary CTAs
• Tertiary text-link row present below
• Button CSS includes hover-lift transform
• Focus ring is box-shadow not outline
• Master CSS includes @view-transition block
• prefers-reduced-motion block disables transitions
bun test → 1963 / 1 fail / 2 skip. The 1 fail remains the
pre-existing scheduled-workflows artifact (passes in isolation).
Deviations
• Q3 /play "without signing up" link re-added to tertiary row —
first pass dropped it which would have broken playground tests.
• Tertiary copy stayed verbatim ("Try the live demo · Compare to
GitHub") to preserve the L10 regression-test invariant; the
brief's suggested copy was illustrative, not mandatory.
---------
Co-authored-by: Test User <test@gluecron.com>9 files changed+514−169dc26881b995aa2685e57c84f17007be385d087b4
9 changed files+514−169
Addedsrc/__tests__/u-polish.test.ts+148−0View fileUnifiedSplit
@@ -0,0 +1,148 @@
1/**
2 * Block U — Senior polish pass smoke tests.
3 *
4 * U1: hero rebuild — exactly two primary CTAs in the buttons row,
5 * tertiary text-link row sits underneath with the demoted demo +
6 * vs-github affordances, install snippet sits BELOW the CTAs
7 * wrapped in the "For power users" panel.
8 * U2: button CSS — hover lifts every .btn by translateY(-1px) with a
9 * soft drop shadow; focus-visible uses box-shadow (not outline);
10 * primary CTA shimmers via background-position; disabled buttons
11 * never lift.
12 * U4: master CSS contains the @view-transition block and the
13 * prefers-reduced-motion guard.
14 *
15 * No DB stubs, no mock pollution — pure rendering checks against the
16 * already-mounted Hono app.
17 */
18import { describe, it, expect } from "bun:test";
19import app from "../app";
20
21const HOME = "/";
22
23async function fetchHomeHtml(): Promise<string> {
24 const res = await app.request(HOME);
25 expect(res.status).toBe(200);
26 return await res.text();
27}
28
29describe("Block U1 — landing hero rebuild", () => {
30 it("renders exactly two primary CTAs in the main button row", async () => {
31 const body = await fetchHomeHtml();
32 // Extract the primary CTA row using the data-testid we shipped.
33 const start = body.indexOf('data-testid="hero-primary-ctas"');
34 expect(start).toBeGreaterThan(-1);
35 // The wrapper opens with that attribute on a div; find the matching
36 // closing </div>. Heuristic: it's the first </div> after a tight
37 // window of <a … class="btn …"> elements.
38 const tail = body.slice(start, start + 2400);
39 // Count the <a class="btn …"> anchors inside the row.
40 const anchorMatches = tail.match(/<a[^>]*class="btn[^"]*"/g) || [];
41 expect(anchorMatches.length).toBeGreaterThanOrEqual(2);
42 // Sanity-check the two CTAs we explicitly preserve:
43 expect(tail).toContain('href="/register"');
44 expect(tail).toContain('href="/gluecron.dxt"');
45 // The tertiary row is OUTSIDE this slice; we assert the demoted
46 // demo + vs-github hrefs are NOT in the primary slice.
47 const primaryRowEnd = tail.indexOf("hero-tertiary-row");
48 const primaryOnly =
49 primaryRowEnd > 0 ? tail.slice(0, primaryRowEnd) : tail.slice(0, 800);
50 expect(primaryOnly).not.toContain('href="/demo"');
51 expect(primaryOnly).not.toContain('href="/vs-github"');
52 });
53
54 it("renders the tertiary text-link row with demo + vs-github affordances", async () => {
55 const body = await fetchHomeHtml();
56 expect(body).toContain('data-testid="hero-tertiary-row"');
57 expect(body).toContain('data-testid="cta-tertiary-demo"');
58 expect(body).toContain('data-testid="cta-tertiary-vs"');
59 // The legacy labels survive on the demoted row so L10 regression
60 // guards keep passing.
61 expect(body).toContain("Try the live demo");
62 expect(body).toContain("Compare to GitHub");
63 });
64
65 it("places the install snippet inside a 'For power users' panel below the CTAs", async () => {
66 const body = await fetchHomeHtml();
67 const ctas = body.indexOf('data-testid="hero-primary-ctas"');
68 const installPanel = body.indexOf("Power users install panel");
69 const installLabel = body.indexOf("For power users");
70 expect(ctas).toBeGreaterThan(-1);
71 expect(installPanel).toBeGreaterThan(-1);
72 expect(installLabel).toBeGreaterThan(-1);
73 // Install panel sits AFTER the CTAs — that's the whole U1 point.
74 expect(installPanel).toBeGreaterThan(ctas);
75 });
76});
77
78describe("Block U2 — button polish", () => {
79 it("includes the universal hover-lift transform on .btn", async () => {
80 const body = await fetchHomeHtml();
81 // The master CSS is inlined in <style> by Layout. We assert on the
82 // hover-rule shape: `.btn:hover { … transform: translateY(-1px); … }`.
83 expect(body).toMatch(/\.btn:hover\b[\s\S]*?transform:\s*translateY\(-1px\)/);
84 });
85
86 it("uses a soft drop shadow on .btn:hover (not just border)", async () => {
87 const body = await fetchHomeHtml();
88 expect(body).toMatch(/\.btn:hover\b[\s\S]*?box-shadow:\s*0\s+4px\s+12px/);
89 });
90
91 it("uses box-shadow (not outline) for the .btn focus ring", async () => {
92 const body = await fetchHomeHtml();
93 // Focus-visible rule must set box-shadow with the soft accent rgba.
94 expect(body).toMatch(
95 /\.btn:focus-visible\b[\s\S]*?box-shadow:\s*0\s+0\s+0\s+3px\s+rgba\(140,\s*109,\s*255,\s*0\.35\)/
96 );
97 // And must NOT fall back to outline:2px on .btn anywhere.
98 expect(body).not.toMatch(/\.btn:focus-visible[^{]*\{\s*outline:\s*2px/);
99 });
100
101 it("disables hover-lift on disabled buttons", async () => {
102 const body = await fetchHomeHtml();
103 // The disabled rule sets transform:none AND opacity:0.5.
104 expect(body).toMatch(/\.btn:disabled[\s\S]*?transform:\s*none/);
105 expect(body).toMatch(/\.btn:disabled[\s\S]*?opacity:\s*0\.5/);
106 });
107
108 it("shimmers the primary CTA via background-position transition", async () => {
109 const body = await fetchHomeHtml();
110 // Primary button declares background-size 200% so the position
111 // animation has somewhere to travel.
112 expect(body).toMatch(/\.btn-primary\b[\s\S]*?background-size:\s*200%/);
113 // The transition lists background-position with the 600ms duration.
114 expect(body).toMatch(
115 /\.btn-primary\b[\s\S]*?transition:[\s\S]*?background-position\s+600ms/
116 );
117 });
118});
119
120describe("Block U4 — view transitions", () => {
121 it("includes the @view-transition opt-in", async () => {
122 const body = await fetchHomeHtml();
123 expect(body).toContain("@view-transition");
124 expect(body).toMatch(/@view-transition\s*\{\s*navigation:\s*auto;?\s*\}/);
125 });
126
127 it("declares both fade-out and fade-in keyframes for ::view-transition-*", async () => {
128 const body = await fetchHomeHtml();
129 expect(body).toContain("::view-transition-old(root)");
130 expect(body).toContain("::view-transition-new(root)");
131 expect(body).toContain("@keyframes vt-fade-out");
132 expect(body).toContain("@keyframes vt-fade-in");
133 });
134
135 it("disables the transition under prefers-reduced-motion", async () => {
136 const body = await fetchHomeHtml();
137 // The reduced-motion block must mention the view-transition
138 // pseudos and set animation-duration: 0s.
139 expect(body).toMatch(
140 /@media\s*\(prefers-reduced-motion:\s*reduce\)\s*\{[\s\S]*?::view-transition-(old|new)\(root\)[\s\S]*?animation-duration:\s*0s/
141 );
142 });
143
144 it("attaches view-transition-name: root to body", async () => {
145 const body = await fetchHomeHtml();
146 expect(body).toMatch(/\bbody\b\s*\{[^}]*view-transition-name:\s*root/);
147 });
148});
Modifiedsrc/routes/admin-deploys-page.tsx+10−10View fileUnifiedSplit
@@ -252,13 +252,13 @@ page.get("/admin/deploys", async (c) => {
252252 </form>
253253 </div>
254254
255 <div style="background:var(--bg-elevated);border:1px solid var(--border);border-radius:8px;padding:14px 16px;margin-bottom:18px">
255 <div style="background:var(--bg-elevated);border:1px solid var(--border);border-radius:8px;padding:var(--space-4);margin-bottom:var(--space-4)">
256256 {lastSuccess ? (
257257 <div>
258258 <div style="font-size:12px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.06em">
259259 Last successful deploy
260260 </div>
261 <div style="margin-top:4px;font-size:14px">
261 <div style="margin-top:var(--space-1);font-size:14px">
262262 <code class="meta-mono">{shortSha(lastSuccess.sha)}</code>
263263 {" · "}
264264 <span title={lastSuccess.startedAt.toISOString()}>
@@ -293,7 +293,7 @@ page.get("/admin/deploys", async (c) => {
293293 <tr>
294294 <td
295295 colspan={6}
296 style="padding:18px 6px;color:var(--text-muted);text-align:center"
296 style="padding:var(--space-4) 6px;color:var(--text-muted);text-align:center"
297297 >
298298 No deploys recorded yet — they'll appear here when the next
299299 push to <code>main</code> runs hetzner-deploy.yml.
@@ -314,7 +314,7 @@ page.get("/admin/deploys", async (c) => {
314314 : "#fbbf24"
315315 }`}
316316 />
317 <span style="margin-left:8px">{row.status}</span>
317 <span style="margin-left:var(--space-2)">{row.status}</span>
318318 </td>
319319 <td style="padding:8px 6px">
320320 <code class="meta-mono">{shortSha(row.sha)}</code>
@@ -340,7 +340,7 @@ page.get("/admin/deploys", async (c) => {
340340 </tbody>
341341 </table>
342342
343 <p style="margin-top:18px;font-size:12px;color:var(--text-muted)">
343 <p style="margin-top:var(--space-4);font-size:12px;color:var(--text-muted)">
344344 Manual trigger (CLI shortcut — the button above is wired to the N4
345345 POST /admin/deploys/trigger handler):{" "}
346346 <code class="meta-mono">
@@ -394,9 +394,9 @@ function renderDeployModal(
394394 role="dialog"
395395 aria-modal="true"
396396 aria-labelledby="deploy-modal-title"
397 style="background:var(--bg-elevated);border:1px solid var(--border);border-radius:10px;max-width:600px;width:92vw;padding:18px 20px;box-shadow:0 24px 64px rgba(0,0,0,0.5);font-size:14px;color:var(--text);max-height:80vh;overflow:auto"
397 style="background:var(--bg-elevated);border:1px solid var(--border);border-radius:10px;max-width:600px;width:92vw;padding:var(--space-4) var(--space-5);box-shadow:0 24px 64px rgba(0,0,0,0.5);font-size:14px;color:var(--text);max-height:80vh;overflow:auto"
398398 >
399 <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px;gap:8px">
399 <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--space-3);gap:var(--space-2)">
400400 <h3
401401 id="deploy-modal-title"
402402 style="margin:0;font-size:16px;font-weight:600"
@@ -410,14 +410,14 @@ function renderDeployModal(
410410 type="button"
411411 id="deploy-modal-close"
412412 aria-label="Close"
413 style="background:transparent;border:0;color:var(--text-muted);cursor:pointer;font-size:18px;line-height:1;padding:4px 8px"
413 style="background:transparent;border:0;color:var(--text-muted);cursor:pointer;font-size:18px;line-height:1;padding:var(--space-1) var(--space-2)"
414414 >
415415 ×
416416 </button>
417417 </div>
418418 <ol
419419 id="deploy-modal-steps"
420 style="list-style:none;padding:0;margin:0 0 12px;display:flex;flex-direction:column;gap:6px"
420 style="list-style:none;padding:0;margin:0 0 var(--space-3);display:flex;flex-direction:column;gap:6px"
421421 >
422422 {R2_STEP_ORDER.map((step) => {
423423 const s = stepStatus[step.name];
@@ -450,7 +450,7 @@ function renderDeployModal(
450450 <li
451451 data-step={step.name}
452452 data-status={s || "pending"}
453 style="display:flex;align-items:center;gap:10px;padding:6px 8px;border-radius:6px;background:rgba(255,255,255,0.02)"
453 style="display:flex;align-items:center;gap:var(--space-2);padding:6px var(--space-2);border-radius:6px;background:rgba(255,255,255,0.02)"
454454 >
455455 <span
456456 class="step-icon"
Modifiedsrc/routes/admin-ops.tsx+6−6View fileUnifiedSplit
@@ -241,7 +241,7 @@ function CardShell({
241241}) {
242242 return (
243243 <div
244 style="background:var(--bg-elevated);border:1px solid var(--border);border-radius:8px;padding:16px 18px;margin-bottom:16px"
244 style="background:var(--bg-elevated);border:1px solid var(--border);border-radius:8px;padding:var(--space-4);margin-bottom:var(--space-4)"
245245 >
246246 <h3
247247 style="margin:0 0 12px 0;font-size:14px;letter-spacing:0.04em;text-transform:uppercase;color:var(--text-muted)"
@@ -319,7 +319,7 @@ ops.get("/admin/ops", async (c) => {
319319
320320 return c.html(
321321 <Layout title="Operations — admin" user={user}>
322 <div style="max-width:880px;margin:0 auto;padding:24px 16px">
322 <div style="max-width:880px;margin:0 auto;padding:var(--space-6) var(--space-4)">
323323 <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:18px">
324324 <h1 style="margin:0">Operations</h1>
325325 <a href="/admin" class="btn btn-sm">
@@ -344,7 +344,7 @@ ops.get("/admin/ops", async (c) => {
344344
345345 {/* ---- Auto-merge card ---- */}
346346 <CardShell title="AI auto-merge on main">
347 <div style="display:flex;align-items:center;gap:10px;margin-bottom:12px">
347 <div style="display:flex;align-items:center;gap:var(--space-2);margin-bottom:var(--space-3)">
348348 <span style="font-size:13px;color:var(--text-muted)">Status:</span>
349349 <Pill
350350 ok={autoMergeState.enabled}
@@ -385,7 +385,7 @@ ops.get("/admin/ops", async (c) => {
385385 </ul>
386386 </div>
387387
388 <div style="display:flex;gap:8px;align-items:center">
388 <div style="display:flex;gap:var(--space-2);align-items:center">
389389 {autoMergeState.enabled ? (
390390 <form
391391 method="post"
@@ -447,7 +447,7 @@ ops.get("/admin/ops", async (c) => {
447447 </span>
448448 )}
449449 </div>
450 <div style="display:flex;gap:8px;align-items:center">
450 <div style="display:flex;gap:var(--space-2);align-items:center">
451451 <form
452452 method="post"
453453 action="/admin/ops/deploy/trigger"
@@ -483,7 +483,7 @@ ops.get("/admin/ops", async (c) => {
483483 </span>
484484 )}
485485 </div>
486 <div style="display:flex;gap:8px;align-items:center">
486 <div style="display:flex;gap:var(--space-2);align-items:center">
487487 <form
488488 method="post"
489489 action="/admin/ops/rollback"
Modifiedsrc/routes/admin.tsx+9−9View fileUnifiedSplit
@@ -97,20 +97,20 @@ admin.get("/admin", async (c) => {
9797 </div>
9898 )}
9999
100 <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-bottom:20px">
101 <div class="panel" style="padding:12px;text-align:center">
100 <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:var(--space-3);margin-bottom:var(--space-5)">
101 <div class="panel" style="padding:var(--space-3);text-align:center">
102102 <div style="font-size:22px;font-weight:700">{Number(uc?.n || 0)}</div>
103103 <div style="font-size:11px;color:var(--text-muted);text-transform:uppercase">
104104 Users
105105 </div>
106106 </div>
107 <div class="panel" style="padding:12px;text-align:center">
107 <div class="panel" style="padding:var(--space-3);text-align:center">
108108 <div style="font-size:22px;font-weight:700">{Number(rc?.n || 0)}</div>
109109 <div style="font-size:11px;color:var(--text-muted);text-transform:uppercase">
110110 Repos
111111 </div>
112112 </div>
113 <div class="panel" style="padding:12px;text-align:center">
113 <div class="panel" style="padding:var(--space-3);text-align:center">
114114 <div style="font-size:22px;font-weight:700">{admins.length}</div>
115115 <div style="font-size:11px;color:var(--text-muted);text-transform:uppercase">
116116 Site admins
@@ -118,7 +118,7 @@ admin.get("/admin", async (c) => {
118118 </div>
119119 </div>
120120
121 <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:8px;margin-bottom:20px">
121 <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:var(--space-2);margin-bottom:var(--space-5)">
122122 <a href="/admin/ops" class="btn btn-primary">
123123 Operations
124124 </a>
@@ -415,7 +415,7 @@ admin.get("/admin/flags", async (c) => {
415415 method="post"
416416 action="/admin/flags"
417417 class="panel"
418 style="padding:16px"
418 style="padding:var(--space-4)"
419419 >
420420 {keys.map((k) => {
421421 const current = existingMap.get(k) ?? (KNOWN_FLAGS as any)[k];
@@ -502,7 +502,7 @@ admin.get("/admin/digests", async (c) => {
502502 <div class="auth-error">{decodeURIComponent(error)}</div>
503503 )}
504504
505 <div class="panel" style="padding:16px;margin-bottom:20px">
505 <div class="panel" style="padding:var(--space-4);margin-bottom:var(--space-5)">
506506 <div style="font-size:13px;color:var(--text-muted);margin-bottom:8px">
507507 {opted} user{opted === 1 ? "" : "s"} opted into the weekly digest.
508508 </div>
@@ -630,7 +630,7 @@ admin.get("/admin/autopilot", async (c) => {
630630 const isErr = !!c.req.query("error");
631631 return c.html(
632632 <Layout title="Autopilot — admin" user={user}>
633 <div style="max-width: 960px; margin: 0 auto; padding: 24px 16px">
633 <div style="max-width: 960px; margin: 0 auto; padding: var(--space-6) var(--space-4)">
634634 <h1 style="margin-bottom: 8px">Autopilot</h1>
635635 <p style="color: var(--text-muted); margin-bottom: 24px">
636636 Periodic platform-maintenance loop — mirror sync, merge-queue
@@ -646,7 +646,7 @@ admin.get("/admin/autopilot", async (c) => {
646646 </div>
647647 )}
648648 <div
649 style="display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin-bottom: 24px"
649 style="display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: var(--space-3); margin-bottom: var(--space-6)"
650650 >
651651 <div class="stat-card">
652652 <div class="stat-label">Status</div>
Modifiedsrc/routes/dashboard.tsx+27−27View fileUnifiedSplit
@@ -181,7 +181,7 @@ dashboard.get("/dashboard", requireAuth, async (c) => {
181181 <Layout title="Command Center" user={user}>
182182 {showVerifyBanner && (
183183 <div
184 style="background: rgba(210, 153, 34, 0.12); border: 1px solid rgba(210, 153, 34, 0.45); color: #e3b341; padding: 12px 16px; border-radius: 8px; margin-bottom: 16px; display: flex; align-items: center; justify-content: space-between; gap: 12px; font-size: 14px"
184 style="background: rgba(210, 153, 34, 0.12); border: 1px solid rgba(210, 153, 34, 0.45); color: #e3b341; padding: var(--space-3) var(--space-4); border-radius: 8px; margin-bottom: var(--space-4); display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); font-size: 14px"
185185 data-p2-verify-banner=""
186186 >
187187 <div style="flex: 1 1 auto; min-width: 0">
@@ -204,7 +204,7 @@ dashboard.get("/dashboard", requireAuth, async (c) => {
204204 <form
205205 method="post"
206206 action="/verify-email/resend"
207 style="display: inline-flex; gap: 8px; align-items: center; margin: 0"
207 style="display: inline-flex; gap: var(--space-2); align-items: center; margin: 0"
208208 >
209209 <input
210210 type="hidden"
@@ -236,7 +236,7 @@ dashboard.get("/dashboard", requireAuth, async (c) => {
236236 Real-time overview of all your repositories
237237 </p>
238238 </div>
239 <div style="display: flex; gap: 8px">
239 <div style="display: flex; gap: var(--space-2)">
240240 <a href="/new" class="btn btn-primary">+ New repo</a>
241241 <a href="/settings" class="btn">Settings</a>
242242 </div>
@@ -246,7 +246,7 @@ dashboard.get("/dashboard", requireAuth, async (c) => {
246246 <AiHoursSavedWidget week={savingsWeek} lifetime={savingsLifetime} />
247247
248248 {/* ─── Stats Bar ─── */}
249 <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin-bottom: 32px">
249 <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: var(--space-3); margin-bottom: var(--space-8)">
250250 <StatBox
251251 label="Repositories"
252252 value={String(repos.length)}
@@ -281,7 +281,7 @@ dashboard.get("/dashboard", requireAuth, async (c) => {
281281 {/* ─── Repo Grid ─── */}
282282 <h2 style="font-size: 18px; margin-bottom: 16px">Your Repositories</h2>
283283 {repos.length === 0 ? (
284 <div class="empty-state" style="text-align:left;padding:24px">
284 <div class="empty-state" style="text-align:left;padding:var(--space-6)">
285285 <div style="text-align:center;margin-bottom:20px">
286286 <h2 style="margin-bottom:6px">Get started</h2>
287287 <p style="color:var(--text-muted);font-size:14px;margin:0">
@@ -289,7 +289,7 @@ dashboard.get("/dashboard", requireAuth, async (c) => {
289289 </p>
290290 </div>
291291 <div class="panel" style="margin-bottom:20px;text-align:left">
292 <div class="panel-item" style="justify-content:space-between;padding:16px;gap:12px">
292 <div class="panel-item" style="justify-content:space-between;padding:var(--space-4);gap:var(--space-3)">
293293 <div style="flex:1">
294294 <div style="font-size:15px;font-weight:600">Create a new repository</div>
295295 <div style="font-size:13px;color:var(--text-muted);margin-top:2px">
@@ -298,7 +298,7 @@ dashboard.get("/dashboard", requireAuth, async (c) => {
298298 </div>
299299 <a href="/new" class="btn btn-primary">Create repo</a>
300300 </div>
301 <div class="panel-item" style="justify-content:space-between;padding:16px;gap:12px">
301 <div class="panel-item" style="justify-content:space-between;padding:var(--space-4);gap:var(--space-3)">
302302 <div style="flex:1">
303303 <div style="font-size:15px;font-weight:600">Import from GitHub</div>
304304 <div style="font-size:13px;color:var(--text-muted);margin-top:2px">
@@ -307,7 +307,7 @@ dashboard.get("/dashboard", requireAuth, async (c) => {
307307 </div>
308308 <a href="/import" class="btn">Import repo</a>
309309 </div>
310 <div class="panel-item" style="justify-content:space-between;padding:16px;gap:12px">
310 <div class="panel-item" style="justify-content:space-between;padding:var(--space-4);gap:var(--space-3)">
311311 <div style="flex:1">
312312 <div style="font-size:15px;font-weight:600">Browse public repos</div>
313313 <div style="font-size:13px;color:var(--text-muted);margin-top:2px">
@@ -317,7 +317,7 @@ dashboard.get("/dashboard", requireAuth, async (c) => {
317317 <a href="/explore" class="btn">Browse</a>
318318 </div>
319319 </div>
320 <div style="background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius);padding:14px 16px">
320 <div style="background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius);padding:var(--space-4)">
321321 <div style="font-size:12px;color:var(--text-muted);margin-bottom:6px;text-transform:uppercase;letter-spacing:0.5px">
322322 Push an existing project (preview)
323323 </div>
@@ -327,14 +327,14 @@ git push -u gluecron main</code></pre>
327327 </div>
328328 </div>
329329 ) : (
330 <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 16px; margin-bottom: 32px">
330 <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: var(--space-4); margin-bottom: var(--space-8)">
331331 {repoData.map(({ repo, healthScore, healthGrade, recentCommits, branchCount, ciConfig }) => (
332332 <div class="card" style="padding: 0; overflow: hidden">
333333 {/* Health bar at top */}
334334 <div
335335 style={`height: 4px; background: ${gradeColor(healthGrade)}; width: ${healthScore}%; transition: width 0.3s`}
336336 />
337 <div style="padding: 16px">
337 <div style="padding: var(--space-4)">
338338 <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 8px">
339339 <div>
340340 <h3 style="font-size: 16px; margin-bottom: 2px">
@@ -358,14 +358,14 @@ git push -u gluecron main</code></pre>
358358 </div>
359359 </div>
360360
361 <div style="display: flex; gap: 16px; font-size: 12px; color: var(--text-muted); margin-top: 8px">
361 <div style="display: flex; gap: var(--space-4); font-size: 12px; color: var(--text-muted); margin-top: var(--space-2)">
362362 <span>{branchCount} branch{branchCount !== 1 ? "es" : ""}</span>
363363 <span>{"\u2606"} {repo.starCount}</span>
364364 {repo.isPrivate && <span class="badge" style="font-size: 10px">Private</span>}
365365 </div>
366366
367367 {ciConfig && ciConfig.commands.length > 0 && (
368 <div style="margin-top: 8px; display: flex; gap: 6px; flex-wrap: wrap">
368 <div style="margin-top: var(--space-2); display: flex; gap: 6px; flex-wrap: wrap">
369369 {ciConfig.detected.slice(0, 3).map((d) => (
370370 <span
371371 class="badge"
@@ -377,7 +377,7 @@ git push -u gluecron main</code></pre>
377377 </div>
378378 )}
379379
380 <div style="display: flex; gap: 6px; margin-top: 12px">
380 <div style="display: flex; gap: 6px; margin-top: var(--space-3)">
381381 <a
382382 href={`/${user.username}/${repo.name}/health`}
383383 class="btn btn-sm"
@@ -420,7 +420,7 @@ git push -u gluecron main</code></pre>
420420 <div class="issue-list">
421421 {recentActivity.map((a) => (
422422 <div class="issue-item">
423 <div style="display: flex; gap: 8px; align-items: center">
423 <div style="display: flex; gap: var(--space-2); align-items: center">
424424 <ActivityIcon action={a.action} />
425425 <div>
426426 <span style="font-size: 14px">
@@ -447,7 +447,7 @@ git push -u gluecron main</code></pre>
447447 <LiveFeed topic={`user:${user.id}`} title="Live activity" />
448448
449449 {/* ─── Quick Links ─── */}
450 <div style="margin-top: 32px; display: flex; gap: 16px; flex-wrap: wrap">
450 <div style="margin-top: var(--space-8); display: flex; gap: var(--space-4); flex-wrap: wrap">
451451 <a href="/explore" class="btn">Browse public repos</a>
452452 <a href="/settings/tokens" class="btn">API tokens</a>
453453 <a href="/settings/keys" class="btn">SSH keys</a>
@@ -579,7 +579,7 @@ dashboard.get("/:owner/:repo/pushes", softAuth, async (c) => {
579579 return (
580580 <div class="issue-item" style="flex-direction: column; align-items: stretch">
581581 <div style="display: flex; justify-content: space-between; align-items: start">
582 <div style="display: flex; gap: 8px; align-items: start">
582 <div style="display: flex; gap: var(--space-2); align-items: start">
583583 {isRepair ? (
584584 <span
585585 style="color: var(--green); font-size: 16px; flex-shrink: 0; margin-top: 2px"
@@ -628,7 +628,7 @@ dashboard.get("/:owner/:repo/pushes", softAuth, async (c) => {
628628 </div>
629629 {isRepair && (
630630 <div
631 style="margin-top: 8px; padding: 8px 12px; background: rgba(63, 185, 80, 0.1); border-radius: var(--radius); font-size: 12px; color: var(--green)"
631 style="margin-top: var(--space-2); padding: var(--space-2) var(--space-3); background: rgba(63, 185, 80, 0.1); border-radius: var(--radius); font-size: 12px; color: var(--green)"
632632 >
633633 Automatically repaired by gluecron
634634 </div>
@@ -685,8 +685,8 @@ const AiHoursSavedWidget = ({
685685 class="card ai-hours-saved-widget"
686686 style="margin-bottom: 24px; padding: 0; overflow: hidden; position: relative; background: var(--accent-gradient-faint, var(--bg-secondary)); border-color: var(--accent)"
687687 >
688 <div style="padding: 24px 24px 20px 24px">
689 <div style="display:flex;justify-content:space-between;align-items:flex-start;gap:16px;flex-wrap:wrap">
688 <div style="padding: var(--space-6) var(--space-6) var(--space-5) var(--space-6)">
689 <div style="display:flex;justify-content:space-between;align-items:flex-start;gap:var(--space-4);flex-wrap:wrap">
690690 <div style="flex:1;min-width:240px">
691691 <div style="font-size: 12px; text-transform: uppercase; letter-spacing: 0.6px; color: var(--text-muted); margin-bottom: 4px">
692692 AI working for you
@@ -734,7 +734,7 @@ const AiHoursSavedWidget = ({
734734 </div>
735735
736736 {hasAnyWeek ? (
737 <div style="display:flex;flex-wrap:wrap;gap:8px;margin-top:16px">
737 <div style="display:flex;flex-wrap:wrap;gap:var(--space-2);margin-top:var(--space-4)">
738738 {weekPills.map((p) => (
739739 <span
740740 class="badge"
@@ -836,7 +836,7 @@ const HealthCoach = ({
836836 return (
837837 <div
838838 class="card"
839 style="margin-bottom: 32px; padding: 16px; background: rgba(63,185,80,0.08); border-color: var(--green)"
839 style="margin-bottom: var(--space-8); padding: var(--space-4); background: rgba(63,185,80,0.08); border-color: var(--green)"
840840 >
841841 <h3 style="margin: 0 0 4px; font-size: 15px">
842842 {"✨"} AI Health Coach
@@ -853,14 +853,14 @@ const HealthCoach = ({
853853 style="margin-bottom: 32px; padding: 0; overflow: hidden"
854854 >
855855 <div
856 style="padding: 12px 16px; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between"
856 style="padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between"
857857 >
858858 <div>
859859 <h3 style="margin: 0; font-size: 15px">
860860 {"✨"} AI Health Coach
861861 </h3>
862862 <p
863 style="margin: 4px 0 0; color: var(--text-muted); font-size: 12px"
863 style="margin: var(--space-1) 0 0; color: var(--text-muted); font-size: 12px"
864864 >
865865 Top {picks.length} repos that would benefit from attention
866866 this week.
@@ -870,7 +870,7 @@ const HealthCoach = ({
870870 <ul style="list-style: none; margin: 0; padding: 0">
871871 {picks.map((p) => (
872872 <li
873 style="padding: 12px 16px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 12px"
873 style="padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: var(--space-3)"
874874 >
875875 <div
876876 style={`min-width: 40px; padding: 4px 8px; border-radius: 4px; text-align: center; font-weight: 600; color: var(--bg); background: ${moduleGradeColor(p.healthGrade)}`}
@@ -916,7 +916,7 @@ const StatBox = ({
916916 color: string;
917917}) => (
918918 <div
919 style="background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; text-align: center"
919 style="background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--space-4); text-align: center"
920920 >
921921 <div style={`font-size: 28px; font-weight: 700; color: ${color}`}>
922922 {value}
@@ -939,7 +939,7 @@ const ToggleSetting = ({
939939 defaultChecked: boolean;
940940}) => (
941941 <div
942 style="display: flex; justify-content: space-between; align-items: start; padding: 16px 0; border-bottom: 1px solid var(--border)"
942 style="display: flex; justify-content: space-between; align-items: start; padding: var(--space-4) 0; border-bottom: 1px solid var(--border)"
943943 >
944944 <div style="flex: 1">
945945 <div style="font-size: 15px; font-weight: 600">{label}</div>
Modifiedsrc/routes/settings.tsx+15−15View fileUnifiedSplit
@@ -102,7 +102,7 @@ settings.get("/settings", (c) => {
102102 </p>
103103 <form method="post" action="/settings/notifications">
104104 <label
105 style="display: flex; gap: 8px; align-items: center; margin-bottom: 8px; font-size: 14px"
105 style="display: flex; gap: var(--space-2); align-items: center; margin-bottom: var(--space-2); font-size: 14px"
106106 >
107107 <input
108108 type="checkbox"
@@ -116,7 +116,7 @@ settings.get("/settings", (c) => {
116116 </span>
117117 </label>
118118 <label
119 style="display: flex; gap: 8px; align-items: center; margin-bottom: 8px; font-size: 14px"
119 style="display: flex; gap: var(--space-2); align-items: center; margin-bottom: var(--space-2); font-size: 14px"
120120 >
121121 <input
122122 type="checkbox"
@@ -128,7 +128,7 @@ settings.get("/settings", (c) => {
128128 <span>I am assigned to an issue or PR</span>
129129 </label>
130130 <label
131 style="display: flex; gap: 8px; align-items: center; margin-bottom: 8px; font-size: 14px"
131 style="display: flex; gap: var(--space-2); align-items: center; margin-bottom: var(--space-2); font-size: 14px"
132132 >
133133 <input
134134 type="checkbox"
@@ -140,7 +140,7 @@ settings.get("/settings", (c) => {
140140 <span>A gate fails on one of my repositories</span>
141141 </label>
142142 <label
143 style="display: flex; gap: 8px; align-items: center; margin-bottom: 12px; font-size: 14px"
143 style="display: flex; gap: var(--space-2); align-items: center; margin-bottom: var(--space-3); font-size: 14px"
144144 >
145145 <input
146146 type="checkbox"
@@ -163,7 +163,7 @@ settings.get("/settings", (c) => {
163163 <a href="/sleep-mode">Learn more</a>.
164164 </p>
165165 <label
166 style="display: flex; gap: 8px; align-items: center; margin-bottom: 8px; font-size: 14px"
166 style="display: flex; gap: var(--space-2); align-items: center; margin-bottom: var(--space-2); font-size: 14px"
167167 >
168168 <input
169169 type="checkbox"
@@ -175,7 +175,7 @@ settings.get("/settings", (c) => {
175175 <span>Enable Sleep Mode (daily “overnight” digest)</span>
176176 </label>
177177 <label
178 style="display: flex; gap: 8px; align-items: center; margin-bottom: 12px; font-size: 14px"
178 style="display: flex; gap: var(--space-2); align-items: center; margin-bottom: var(--space-3); font-size: 14px"
179179 >
180180 <span>Send my morning digest at (UTC hour, 0-23):</span>
181181 <input
@@ -198,7 +198,7 @@ settings.get("/settings", (c) => {
198198 control which notification kinds trigger a push.
199199 </p>
200200 <label
201 style="display: flex; gap: 8px; align-items: center; margin-bottom: 8px; font-size: 14px"
201 style="display: flex; gap: var(--space-2); align-items: center; margin-bottom: var(--space-2); font-size: 14px"
202202 >
203203 <input
204204 type="checkbox"
@@ -212,7 +212,7 @@ settings.get("/settings", (c) => {
212212 </span>
213213 </label>
214214 <label
215 style="display: flex; gap: 8px; align-items: center; margin-bottom: 8px; font-size: 14px"
215 style="display: flex; gap: var(--space-2); align-items: center; margin-bottom: var(--space-2); font-size: 14px"
216216 >
217217 <input
218218 type="checkbox"
@@ -224,7 +224,7 @@ settings.get("/settings", (c) => {
224224 <span>I am assigned to an issue or PR</span>
225225 </label>
226226 <label
227 style="display: flex; gap: 8px; align-items: center; margin-bottom: 8px; font-size: 14px"
227 style="display: flex; gap: var(--space-2); align-items: center; margin-bottom: var(--space-2); font-size: 14px"
228228 >
229229 <input
230230 type="checkbox"
@@ -236,7 +236,7 @@ settings.get("/settings", (c) => {
236236 <span>Someone requests a review from me</span>
237237 </label>
238238 <label
239 style="display: flex; gap: 8px; align-items: center; margin-bottom: 12px; font-size: 14px"
239 style="display: flex; gap: var(--space-2); align-items: center; margin-bottom: var(--space-3); font-size: 14px"
240240 >
241241 <input
242242 type="checkbox"
@@ -253,7 +253,7 @@ settings.get("/settings", (c) => {
253253 </form>
254254 <div
255255 id="gc-push-device"
256 style="margin-top:16px;padding:12px;border:1px solid var(--border);border-radius:6px;background:var(--bg-elevated,transparent)"
256 style="margin-top:var(--space-4);padding:var(--space-3);border:1px solid var(--border);border-radius:6px;background:var(--bg-elevated,transparent)"
257257 >
258258 <div
259259 id="gc-push-status"
@@ -261,7 +261,7 @@ settings.get("/settings", (c) => {
261261 >
262262 Push status: checking…
263263 </div>
264 <div style="margin-top:10px;display:flex;gap:8px;flex-wrap:wrap">
264 <div style="margin-top:var(--space-2);display:flex;gap:var(--space-2);flex-wrap:wrap">
265265 <button type="button" id="gc-push-subscribe" class="btn btn-sm btn-primary">
266266 Subscribe on this device
267267 </button>
@@ -334,7 +334,7 @@ function renderDeleteAccountSection(args: {
334334 </p>
335335 <form method="post" action="/settings/delete-account">
336336 <input type="hidden" name="_csrf" value={csrfToken || ""} />
337 <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
337 <div style="display:flex;gap:var(--space-2);align-items:center;flex-wrap:wrap">
338338 <input
339339 type="text"
340340 name="confirm_username"
@@ -405,7 +405,7 @@ settings.get("/settings/sleep-mode/preview", async (c) => {
405405 {report.gateFailuresAutoRepaired} · Hours saved:{" "}
406406 {report.hoursSaved} · Total events: {total}
407407 </p>
408 <div class="panel" style="padding:20px;background:#fff;color:#111">
408 <div class="panel" style="padding:var(--space-5);background:#fff;color:#111">
409409 {raw(rendered.html)}
410410 </div>
411411 <p style="margin-top:20px">
@@ -443,7 +443,7 @@ settings.get("/settings/digest/preview", async (c) => {
443443 </p>
444444 <div
445445 class="panel"
446 style="padding:20px;background:#fff;color:#111"
446 style="padding:var(--space-5);background:#fff;color:#111"
447447 >
448448 {raw(body.html)}
449449 </div>
Modifiedsrc/routes/web.tsx+9−9View fileUnifiedSplit
@@ -359,7 +359,7 @@ web.get("/:owner", async (c) => {
359359 <div class="username">@{ownerName}</div>
360360 {ownerUser?.bio && <div class="bio">{ownerUser.bio}</div>}
361361 <div
362 style="margin-top:8px;display:flex;gap:12px;align-items:center;flex-wrap:wrap;font-size:13px"
362 style="margin-top:var(--space-2);display:flex;gap:var(--space-3);align-items:center;flex-wrap:wrap;font-size:13px"
363363 >
364364 <a
365365 href={`/${ownerName}/followers`}
@@ -402,7 +402,7 @@ web.get("/:owner", async (c) => {
402402 {profileReadmeHtml && (
403403 <div
404404 class="markdown-body"
405 style="background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius);padding:20px 24px;margin-bottom:24px"
405 style="background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius);padding:var(--space-5) var(--space-6);margin-bottom:var(--space-6)"
406406 dangerouslySetInnerHTML={{ __html: profileReadmeHtml }}
407407 />
408408 )}
@@ -615,7 +615,7 @@ git push -u gluecron main`}</pre>
615615 {isTemplate && user && user.username !== owner && (
616616 <div
617617 class="panel"
618 style="margin-bottom:16px;padding:12px;display:flex;align-items:center;justify-content:space-between;gap:12px"
618 style="margin-bottom:var(--space-4);padding:var(--space-3);display:flex;align-items:center;justify-content:space-between;gap:var(--space-3)"
619619 >
620620 <div style="font-size:13px">
621621 <strong>Template repository.</strong> Create a new repository from
@@ -624,7 +624,7 @@ git push -u gluecron main`}</pre>
624624 <form
625625 method="post"
626626 action={`/${owner}/${repo}/use-template`}
627 style="display:flex;gap:8px;align-items:center"
627 style="display:flex;gap:var(--space-2);align-items:center"
628628 >
629629 <input
630630 type="text"
@@ -780,7 +780,7 @@ web.get("/:owner/:repo/blob/:ref{.+$}", async (c) => {
780780 <div class="blob-view">
781781 <div class="blob-header">
782782 <span>{fileName} — {blob.size} bytes</span>
783 <span style="display: flex; gap: 12px">
783 <span style="display: flex; gap: var(--space-3)">
784784 <a href={`/${owner}/${repo}/raw/${ref}/${filePath}`} style="font-size: 12px">
785785 Raw
786786 </a>
@@ -798,7 +798,7 @@ web.get("/:owner/:repo/blob/:ref{.+$}", async (c) => {
798798 </span>
799799 </div>
800800 {blob.isBinary ? (
801 <div style="padding: 16px; color: var(--text-muted)">
801 <div style="padding: var(--space-4); color: var(--text-muted)">
802802 Binary file not shown.
803803 </div>
804804 ) : (() => {
@@ -1003,7 +1003,7 @@ web.get("/:owner/:repo/commit/:sha", async (c) => {
10031003 <Layout title={`${commit.message} — ${owner}/${repo}`} user={user}>
10041004 <RepoHeader owner={owner} repo={repo} />
10051005 <div
1006 style="background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; margin-bottom: 20px"
1006 style="background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--space-4); margin-bottom: var(--space-5)"
10071007 >
10081008 <div style="font-size: 18px; font-weight: 600; margin-bottom: 8px">
10091009 {commit.message}
@@ -1252,14 +1252,14 @@ web.get("/:owner/:repo/search", async (c) => {
12521252 action={`/${owner}/${repo}/search`}
12531253 style="margin-bottom: 20px"
12541254 >
1255 <div style="display: flex; gap: 8px">
1255 <div style="display: flex; gap: var(--space-2)">
12561256 <input
12571257 type="text"
12581258 name="q"
12591259 value={q}
12601260 placeholder="Search code..."
12611261 aria-label="Search code"
1262 style="flex: 1; padding: 8px 12px; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text); font-size: 14px"
1262 style="flex: 1; padding: var(--space-2) var(--space-3); background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text); font-size: 14px"
12631263 />
12641264 <button type="submit" class="btn btn-primary">
12651265 Search
Modifiedsrc/views/landing.tsx+175−80View fileUnifiedSplit
@@ -118,11 +118,25 @@ export const LandingHero: FC<LandingPageProps> = ({
118118 <style dangerouslySetInnerHTML={{ __html: landingCss }} />
119119
120120 <div class="landing-root">
121 {/* ---------- Hero ---------- */}
121 {/* ---------- Hero ----------
122 Block U1 — Senior polish pass. Rebuilt for tighter rhythm:
123 · 2 primary CTAs (sign up + Claude Desktop)
124 · "Try the live demo" / "Compare to GitHub" demoted to
125 a tertiary text-link row
126 · Install snippet moved BELOW the CTAs as a "power users"
127 panel — used to sit above and crowded the buttons
128 · 4-stat rail kept but rendered as a tighter horizontal
129 strip with the gradient accent rule
130 · One new muted gradient orb absolutely positioned behind
131 everything so the section reads as a product page, not
132 a tutorial
133 · vertical rhythm = var(--space-6) between every block
134 */}
122135 <section class="landing-hero">
123136 <div class="landing-hero-bg" aria-hidden="true">
124137 <div class="landing-hero-blob landing-hero-blob-1" />
125138 <div class="landing-hero-blob landing-hero-blob-2" />
139 <div class="landing-hero-blob landing-hero-orb" />
126140 <div class="landing-hero-grid" />
127141 </div>
128142
@@ -140,30 +154,13 @@ export const LandingHero: FC<LandingPageProps> = ({
140154 Label an issue. Walk away. Wake up to a merged PR.
141155 </p>
142156
143 {/* L10 — one-line install snippet with copy button. */}
144 <div class="landing-hero-install" aria-label="One-line install">
145 <code class="landing-hero-install-code">
146 <span class="landing-hero-install-prompt" aria-hidden="true">$</span>
147 <span id="landing-install-text">curl -sSL gluecron.com/install | bash</span>
148 </code>
149 <button
150 type="button"
151 class="landing-hero-install-copy"
152 data-copy-target="landing-install-text"
153 aria-label="Copy install command"
154 >
155 Copy
156 </button>
157 </div>
158
159 <div class="landing-hero-ctas">
157 {/* U1 — primary CTA row, demoted to 2 buttons. */}
158 <div class="landing-hero-ctas" data-testid="hero-primary-ctas">
160159 <a href="/register" class="btn btn-primary btn-xl landing-cta-primary">
161160 Sign up free
162161 <span class="landing-cta-arrow" aria-hidden="true">{"→"}</span>
163162 </a>
164 {/* BLOCK Q1 — one-click Claude Desktop install. Gradient
165 border + accent so it reads as a distinct flagship CTA,
166 not just a third secondary option. */}
163 {/* BLOCK Q1 — one-click Claude Desktop install. */}
167164 <a
168165 href="/gluecron.dxt"
169166 class="btn btn-xl landing-cta-dxt"
@@ -173,50 +170,72 @@ export const LandingHero: FC<LandingPageProps> = ({
173170 Add to Claude Desktop
174171 <span class="landing-cta-arrow" aria-hidden="true">{"→"}</span>
175172 </a>
176 <a href="/demo" class="btn btn-secondary btn-xl">
173 </div>
174
175 {/* U1 — tertiary text-link row.
176 Visually subordinate to the buttons above. Keeps the
177 "Try the live demo" + "Compare to GitHub" affordances
178 from L10 + the Q3 "try without signing up" /play link
179 without crowding the primary CTA row. */}
180 <div class="landing-hero-tertiary" data-testid="hero-tertiary-row">
181 <a href="/demo" class="landing-hero-tertiary-link" data-testid="cta-tertiary-demo">
177182 Try the live demo
178 <span class="landing-cta-arrow" aria-hidden="true">{"→"}</span>
183 <span aria-hidden="true">{" →"}</span>
179184 </a>
180 <a href="/vs-github" class="btn btn-ghost btn-xl">
185 <span class="landing-hero-tertiary-sep" aria-hidden="true">·</span>
186 <a href="/vs-github" class="landing-hero-tertiary-link" data-testid="cta-tertiary-vs">
181187 Compare to GitHub
182 <span class="landing-cta-arrow" aria-hidden="true">{"→"}</span>
188 <span aria-hidden="true">{" →"}</span>
183189 </a>
184 </div>
185
186 {/* Block Q3 — tertiary "try it without signing up" link.
187 Visually subordinate to the buttons above; renders as a
188 small ghost text link so it doesn't crowd the CTA row. */}
189 <div class="landing-hero-play">
190 <a href="/play" class="landing-hero-play-link" data-testid="cta-play">
191 Or try it without signing up
190 <span class="landing-hero-tertiary-sep" aria-hidden="true">·</span>
191 <a href="/play" class="landing-hero-tertiary-link" data-testid="cta-play">
192 Try it without signing up
192193 <span aria-hidden="true">{" →"}</span>
193194 </a>
194195 </div>
195196
196 {/* L10 — "what just happened" rail. Mini, secondary,
197 separate from the BIG L4 counters tile section below. */}
197 {/* U1 — power-users install snippet panel.
198 Moved BELOW the CTAs so it doesn't compete for the eye
199 with the primary "Sign up free" button. */}
200 <div class="landing-hero-install-wrap" aria-label="Power users install panel">
201 <div class="landing-hero-install-label">For power users</div>
202 <div class="landing-hero-install" aria-label="One-line install">
203 <code class="landing-hero-install-code">
204 <span class="landing-hero-install-prompt" aria-hidden="true">$</span>
205 <span id="landing-install-text">curl -sSL gluecron.com/install | bash</span>
206 </code>
207 <button
208 type="button"
209 class="landing-hero-install-copy"
210 data-copy-target="landing-install-text"
211 aria-label="Copy install command"
212 >
213 Copy
214 </button>
215 </div>
216 </div>
217
218 {/* U1 — tightened "what just happened" rail.
219 Same data as before, rendered as a single horizontal
220 rule with the gradient accent line on top. Numbers
221 smaller, copy still scannable. */}
198222 {publicStats && (
199223 <ul class="landing-hero-rail" aria-label="What just happened on Gluecron">
200224 <li>
201 <span class="landing-hero-rail-check" aria-hidden="true">{"✓"}</span>
202225 <strong>{publicStats.weeklyPrsAutoMerged.toLocaleString()}</strong>
203 {" PRs auto-merged this week"}
226 <span class="landing-hero-rail-label">PRs auto-merged</span>
204227 </li>
205228 <li>
206 <span class="landing-hero-rail-check" aria-hidden="true">{"✓"}</span>
207229 <strong>{publicStats.weeklyIssuesBuiltByAi.toLocaleString()}</strong>
208 {" issues built by AI"}
230 <span class="landing-hero-rail-label">issues built by AI</span>
209231 </li>
210232 <li>
211 <span class="landing-hero-rail-check" aria-hidden="true">{"✓"}</span>
212233 <strong>{publicStats.weeklyDeploysShipped.toLocaleString()}</strong>
213 {" deploys shipped overnight"}
234 <span class="landing-hero-rail-label">deploys overnight</span>
214235 </li>
215236 <li>
216 <span class="landing-hero-rail-check" aria-hidden="true">{"✓"}</span>
217 {"~"}
218 <strong>{Math.round(publicStats.weeklyHoursSaved).toLocaleString()}</strong>
219 {" hours saved by AI"}
237 <strong>{`~${Math.round(publicStats.weeklyHoursSaved).toLocaleString()}`}</strong>
238 <span class="landing-hero-rail-label">hours saved by AI</span>
220239 </li>
221240 </ul>
222241 )}
@@ -1562,6 +1581,30 @@ const landingCss = `
15621581 height: 380px;
15631582 background: radial-gradient(circle, rgba(54,197,214,0.40), transparent 65%);
15641583 }
1584 /* U1 — subtle, low-opacity accent-gradient orb behind the headline.
1585 Sits dead-centre, very blurred, so the hero reads as a real product
1586 surface rather than flat-bg + text. */
1587 .landing-hero-orb {
1588 top: 18%;
1589 left: 50%;
1590 transform: translateX(-50%);
1591 width: 720px;
1592 height: 720px;
1593 background: radial-gradient(circle, rgba(140,109,255,0.22), rgba(54,197,214,0.12) 45%, transparent 70%);
1594 filter: blur(120px);
1595 opacity: 0.55;
1596 z-index: 0;
1597 }
1598 :root[data-theme='light'] .landing-hero-orb {
1599 opacity: 0.32;
1600 }
1601 @media (prefers-reduced-motion: reduce) {
1602 .landing-hero-blob-1,
1603 .landing-hero-blob-2,
1604 .landing-hero-orb {
1605 animation: none;
1606 }
1607 }
15651608 .landing-hero-grid {
15661609 position: absolute;
15671610 inset: 0;
@@ -1584,8 +1627,9 @@ const landingCss = `
15841627 max-width: 960px;
15851628 margin: 0 auto;
15861629 }
1630 /* U1 — every block below the headline obeys a single rhythm. */
15871631 .landing-hero-eyebrow {
1588 margin: 0 auto var(--s-6);
1632 margin: 0 auto var(--space-6);
15891633 color: var(--accent);
15901634 }
15911635 .landing-hero-eyebrow::before { display: none; }
@@ -1609,7 +1653,7 @@ const landingCss = `
16091653 line-height: 1.05;
16101654 letter-spacing: -0.025em;
16111655 font-weight: 700;
1612 margin: 0 0 var(--s-7);
1656 margin: 0 0 var(--space-6);
16131657 color: var(--text-strong);
16141658 }
16151659 .landing-hero-title .gradient-text {
@@ -1624,42 +1668,56 @@ const landingCss = `
16241668 font-size: clamp(15px, 1.6vw, 19px);
16251669 color: var(--text-muted);
16261670 max-width: 680px;
1627 margin: 0 auto;
1671 margin: 0 auto var(--space-6);
16281672 line-height: 1.55;
16291673 letter-spacing: -0.005em;
16301674 }
16311675
16321676 .landing-hero-ctas {
16331677 display: flex;
1634 gap: 12px;
1678 gap: var(--space-3);
16351679 justify-content: center;
16361680 flex-wrap: wrap;
1637 margin-top: var(--s-10);
1681 margin-top: 0;
1682 margin-bottom: var(--space-4);
16381683 }
16391684 .landing-cta-arrow {
16401685 transition: transform var(--t-base) var(--ease-spring);
16411686 display: inline-block;
16421687 }
16431688
1644 /* Block Q3 — tertiary "try it without signing up" link.
1645 Sits under the primary CTA row as a small, low-contrast text link
1646 so it doesn't crowd the main calls-to-action. */
1647 .landing-hero-play {
1648 margin-top: 14px;
1689 /* U1 — tertiary text-link row.
1690 Sits directly under the 2-button primary CTA row. Smaller, muted,
1691 so it reads as "by the way" rather than competing for the eye. */
1692 .landing-hero-tertiary {
1693 margin-top: 0;
1694 margin-bottom: var(--space-6);
16491695 text-align: center;
1650 }
1651 .landing-hero-play-link {
1696 display: inline-flex;
1697 flex-wrap: wrap;
1698 gap: var(--space-2) var(--space-3);
1699 justify-content: center;
1700 align-items: baseline;
1701 width: 100%;
16521702 font-size: 13px;
16531703 color: var(--text-muted);
1704 }
1705 .landing-hero-tertiary-link {
1706 color: var(--text-muted);
16541707 text-decoration: none;
16551708 border-bottom: 1px dashed transparent;
16561709 padding-bottom: 1px;
1657 transition: color var(--t-base), border-color var(--t-base);
1710 transition: color var(--t-fast) var(--ease),
1711 border-color var(--t-fast) var(--ease);
16581712 }
1659 .landing-hero-play-link:hover {
1660 color: var(--text-strong, var(--text));
1713 .landing-hero-tertiary-link:hover {
1714 color: var(--text-strong);
16611715 border-bottom-color: var(--text-muted);
16621716 }
1717 .landing-hero-tertiary-sep {
1718 color: var(--text-faint);
1719 user-select: none;
1720 }
16631721 .btn:hover .landing-cta-arrow,
16641722 .landing-cta-primary:hover .landing-cta-arrow {
16651723 transform: translateX(4px);
@@ -1696,9 +1754,10 @@ const landingCss = `
16961754 }
16971755 }
16981756
1699 /* L8 — free-tier reassurance link beneath the CTA row. */
1757 /* L8 — free-tier reassurance link beneath the CTA row.
1758 U1 — rhythm snapped to var(--space-6). */
17001759 .landing-hero-freenote {
1701 margin-top: var(--s-5);
1760 margin: 0 auto var(--space-6);
17021761 font-size: var(--t-sm);
17031762 color: var(--text-muted);
17041763 text-align: center;
@@ -1716,14 +1775,14 @@ const landingCss = `
17161775 }
17171776
17181777 .landing-hero-caption {
1719 margin-top: var(--s-8);
1778 margin: 0 auto var(--space-6);
17201779 font-size: var(--t-sm);
17211780 color: var(--text-muted);
17221781 display: flex;
17231782 flex-wrap: wrap;
17241783 align-items: center;
17251784 justify-content: center;
1726 gap: 12px;
1785 gap: var(--space-3);
17271786 }
17281787 .landing-hero-cmd {
17291788 display: inline-flex;
@@ -1750,14 +1809,14 @@ const landingCss = `
17501809 }
17511810
17521811 .landing-stats {
1753 margin-top: var(--s-7);
1812 margin: 0 auto;
17541813 font-family: var(--font-mono);
17551814 font-size: 12px;
17561815 color: var(--text-muted);
17571816 display: flex;
17581817 align-items: center;
17591818 justify-content: center;
1760 gap: 10px;
1819 gap: var(--space-2);
17611820 flex-wrap: wrap;
17621821 letter-spacing: 0.02em;
17631822 }
@@ -2300,12 +2359,28 @@ const landingCss = `
23002359 .landing-counters-grid { grid-template-columns: repeat(2, 1fr); gap: 18px 12px; }
23012360 }
23022361
2303 /* ---------- L10 hero install snippet ---------- */
2362 /* ---------- L10/U1 hero install snippet ----------
2363 U1: wrapped in a labelled "power users" panel and re-located
2364 beneath the CTA + tertiary rows so it no longer competes with
2365 the primary calls to action. */
2366 .landing-hero-install-wrap {
2367 margin: 0 auto var(--space-6);
2368 text-align: center;
2369 }
2370 .landing-hero-install-label {
2371 display: inline-block;
2372 margin-bottom: var(--space-2);
2373 font-family: var(--font-mono);
2374 font-size: 11px;
2375 letter-spacing: 0.08em;
2376 text-transform: uppercase;
2377 color: var(--text-faint);
2378 }
23042379 .landing-hero-install {
23052380 display: inline-flex;
23062381 align-items: stretch;
23072382 gap: 0;
2308 margin: var(--s-8) auto 0;
2383 margin: 0 auto;
23092384 background: var(--bg-elevated);
23102385 border: 1px solid var(--border-strong);
23112386 border-radius: var(--r);
@@ -2353,36 +2428,56 @@ const landingCss = `
23532428 color: var(--green, #34d399);
23542429 }
23552430
2356 /* ---------- L10 hero "what just happened" rail ---------- */
2431 /* ---------- L10/U1 hero "what just happened" rail ----------
2432 U1 — tightened into a single horizontal strip. The 1px gradient
2433 rule on top is the same accent the headline uses, so the rail
2434 reads as part of the hero composition rather than a stray list. */
23572435 .landing-hero-rail {
23582436 list-style: none;
2359 padding: 0;
2360 margin: var(--s-7) auto 0;
2437 padding: var(--space-4) 0 0;
2438 margin: 0 auto var(--space-6);
23612439 display: flex;
23622440 flex-wrap: wrap;
23632441 justify-content: center;
2364 gap: 8px 22px;
2442 gap: var(--space-2) var(--space-6);
23652443 font-family: var(--font-sans);
2366 font-size: var(--t-sm);
2367 color: var(--text-muted);
2444 font-size: 12px;
2445 color: var(--text-faint);
23682446 max-width: 760px;
2447 position: relative;
2448 }
2449 .landing-hero-rail::before {
2450 content: '';
2451 position: absolute;
2452 top: 0;
2453 left: 50%;
2454 transform: translateX(-50%);
2455 width: 120px;
2456 height: 1px;
2457 background: var(--accent-gradient);
2458 opacity: 0.45;
2459 border-radius: 9999px;
23692460 }
23702461 .landing-hero-rail li {
23712462 display: inline-flex;
2372 align-items: center;
2373 gap: 7px;
2463 align-items: baseline;
2464 gap: var(--space-2);
23742465 line-height: 1.4;
23752466 }
23762467 .landing-hero-rail strong {
23772468 color: var(--text-strong);
23782469 font-weight: 600;
23792470 font-feature-settings: 'tnum';
2471 font-size: 14px;
2472 letter-spacing: -0.01em;
23802473 }
2381 .landing-hero-rail-check {
2382 color: var(--accent);
2383 font-weight: 700;
2384 flex-shrink: 0;
2474 .landing-hero-rail-label {
2475 color: var(--text-muted);
2476 letter-spacing: 0.01em;
23852477 }
2478 /* Backwards-compat: nothing references this any more but if a stale
2479 fragment lingers it's still hidden cleanly rather than orphaned. */
2480 .landing-hero-rail-check { display: none; }
23862481
23872482 /* ---------- L10 three-reasons section ---------- */
23882483 .landing-reasons { margin-top: var(--s-12); }
Modifiedsrc/views/layout.tsx+115−13View fileUnifiedSplit
@@ -317,7 +317,7 @@ export const Layout: FC<
317317 type="text"
318318 placeholder="Type a command..."
319319 aria-label="Command palette"
320 style="width:100%;padding:12px 16px;background:transparent;color:var(--text);border:0;border-bottom:1px solid var(--border);outline:none;font-size:14px"
320 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"
321321 />
322322 <div id="cmdk-list" style="max-height:60vh;overflow-y:auto" />
323323 </div>
@@ -668,13 +668,13 @@ const navScript = `
668668 var cls = i === selected ? 'cmdk-item cmdk-active' : 'cmdk-item';
669669 var bg = i === selected ? 'background:var(--bg);' : '';
670670 html += '<div class="' + cls + '" data-idx="' + i + '" data-url="' + item.href + '"' +
671 ' style="padding:10px 16px;cursor:pointer;border-bottom:1px solid var(--border);' + bg + '">' +
671 ' style="padding:var(--space-2) var(--space-4);cursor:pointer;border-bottom:1px solid var(--border);' + bg + '">' +
672672 '<div>' + item.label + '</div>' +
673673 '<div style="font-size:11px;color:var(--text-muted)">' + item.href + '</div>' +
674674 '</div>';
675675 }
676676 if (filtered.length === 0) {
677 html = '<div style="padding:16px;color:var(--text-muted);text-align:center">No matches.</div>';
677 html = '<div style="padding:var(--space-4);color:var(--text-muted);text-align:center">No matches.</div>';
678678 }
679679 list.innerHTML = html;
680680 }
@@ -1460,6 +1460,16 @@ const css = `
14601460 /* ============================================================ */
14611461 /* Buttons */
14621462 /* ============================================================ */
1463 /* ============================================================ */
1464 /* Buttons — Block U2 senior polish pass. */
1465 /* Rules: */
1466 /* · hover lifts every .btn by 1px + soft drop shadow (180ms) */
1467 /* · active presses back down to 0 (80ms — faster on press) */
1468 /* · focus-visible uses a soft box-shadow ring (no outline) */
1469 /* · primary gradient shifts position on hover for a slow */
1470 /* 600ms shimmer */
1471 /* · disabled never lifts and never animates */
1472 /* ============================================================ */
14631473 .btn {
14641474 display: inline-flex;
14651475 align-items: center;
@@ -1477,12 +1487,15 @@ const css = `
14771487 text-decoration: none;
14781488 line-height: 1.25;
14791489 letter-spacing: -0.008em;
1490 /* U2 — hover/transform settles in 180ms; press snaps in 80ms.
1491 Listed once on the base rule so :active can override only the
1492 transform/duration without re-typing the colour/bg transitions. */
14801493 transition:
1481 background var(--t-fast) var(--ease),
1482 border-color var(--t-fast) var(--ease),
1483 transform var(--t-fast) var(--ease),
1484 box-shadow var(--t-fast) var(--ease),
1485 color var(--t-fast) var(--ease);
1494 background 180ms ease,
1495 border-color 180ms ease,
1496 transform 180ms ease,
1497 box-shadow 180ms ease,
1498 color 180ms ease;
14861499 user-select: none;
14871500 white-space: nowrap;
14881501 position: relative;
@@ -1492,12 +1505,24 @@ const css = `
14921505 border-color: var(--border-strong);
14931506 color: var(--text-strong);
14941507 text-decoration: none;
1508 /* U2 — universal hover lift + soft accent drop shadow. */
1509 transform: translateY(-1px);
1510 box-shadow: 0 4px 12px -4px rgba(15, 17, 26, 0.45);
1511 }
1512 .btn:active {
1513 transform: translateY(0);
1514 transition-duration: 80ms;
1515 }
1516 /* U2 — soft modern focus ring via box-shadow, not outline. */
1517 .btn:focus-visible {
1518 outline: none;
1519 box-shadow: 0 0 0 3px rgba(140, 109, 255, 0.35);
14951520 }
1496 .btn:active { transform: translateY(1px); }
1497 .btn:focus-visible { outline: none; box-shadow: var(--ring); }
14981521
14991522 .btn-primary {
15001523 background: var(--accent-gradient);
1524 background-size: 200% 100%;
1525 background-position: 0% 50%;
15011526 border-color: transparent;
15021527 color: #fff;
15031528 font-weight: 600;
@@ -1507,6 +1532,13 @@ const css = `
15071532 inset 0 -1px 0 rgba(0,0,0,0.10),
15081533 0 1px 2px rgba(0,0,0,0.40),
15091534 0 0 0 1px rgba(140,109,255,0.30);
1535 /* U2 — slower 600ms transition on background-position so the
1536 primary CTA shimmers when the cursor lands. */
1537 transition:
1538 background-position 600ms ease,
1539 transform 180ms ease,
1540 box-shadow 180ms ease,
1541 color 180ms ease;
15101542 }
15111543 .btn-primary::before {
15121544 content: '';
@@ -1521,7 +1553,10 @@ const css = `
15211553 .btn-primary:hover {
15221554 color: #fff;
15231555 background: var(--accent-gradient);
1556 background-size: 200% 100%;
1557 background-position: 100% 50%;
15241558 border-color: transparent;
1559 transform: translateY(-1px);
15251560 box-shadow:
15261561 inset 0 1px 0 rgba(255,255,255,0.30),
15271562 inset 0 -1px 0 rgba(0,0,0,0.10),
@@ -1529,6 +1564,13 @@ const css = `
15291564 0 0 0 1px rgba(140,109,255,0.45);
15301565 }
15311566 .btn-primary:hover::before { opacity: 1; }
1567 .btn-primary:active { transform: translateY(0); transition-duration: 80ms; }
1568 /* U2 — primary focus ring uses the same accent box-shadow recipe. */
1569 .btn-primary:focus-visible {
1570 box-shadow:
1571 inset 0 1px 0 rgba(255,255,255,0.22),
1572 0 0 0 3px rgba(140,109,255,0.35);
1573 }
15321574
15331575 .btn-danger {
15341576 background: transparent;
@@ -1539,8 +1581,10 @@ const css = `
15391581 background: rgba(248,113,113,0.08);
15401582 border-color: var(--red);
15411583 color: var(--red);
1584 transform: translateY(-1px);
1585 box-shadow: 0 4px 12px -4px rgba(248,113,113,0.30);
15421586 }
1543 .btn-danger:focus-visible { box-shadow: var(--ring-err); }
1587 .btn-danger:focus-visible { box-shadow: 0 0 0 3px rgba(248,113,113,0.35); }
15441588
15451589 .btn-ghost {
15461590 background: transparent;
@@ -1551,6 +1595,8 @@ const css = `
15511595 background: var(--bg-hover);
15521596 color: var(--text-strong);
15531597 border-color: var(--border);
1598 transform: translateY(-1px);
1599 box-shadow: 0 4px 12px -4px rgba(15, 17, 26, 0.30);
15541600 }
15551601
15561602 .btn-secondary {
@@ -1561,6 +1607,8 @@ const css = `
15611607 .btn-secondary:hover {
15621608 background: var(--bg-surface);
15631609 border-color: var(--border-strong);
1610 transform: translateY(-1px);
1611 box-shadow: 0 4px 12px -4px rgba(15, 17, 26, 0.35);
15641612 }
15651613
15661614 .btn-sm { padding: 4px 10px; font-size: var(--t-xs); border-radius: var(--r-sm); gap: 5px; }
@@ -1568,10 +1616,26 @@ const css = `
15681616 .btn-xl { padding: 14px 28px; font-size: var(--t-md); border-radius: var(--r); font-weight: 600; }
15691617 .btn-block { width: 100%; }
15701618
1571 .btn:disabled, .btn[aria-disabled='true'] {
1572 opacity: 0.45;
1619 /* U2 — disabled never lifts, never shimmers, never glows. */
1620 .btn:disabled,
1621 .btn[aria-disabled='true'],
1622 .btn:disabled:hover,
1623 .btn[aria-disabled='true']:hover {
1624 opacity: 0.5;
15731625 cursor: not-allowed;
15741626 pointer-events: none;
1627 transform: none;
1628 box-shadow: none;
1629 }
1630 (prefers-reduced-motion: reduce) {
1631 .btn,
1632 .btn:hover,
1633 .btn:active,
1634 .btn-primary,
1635 .btn-primary:hover {
1636 transform: none;
1637 transition: background-color 80ms linear, color 80ms linear;
1638 }
15751639 }
15761640
15771641 /* ============================================================ */
@@ -2747,4 +2811,42 @@ const css = `
27472811 footer .footer-banner-info { border-color: rgba(96,165,250,0.40); color: var(--blue); }
27482812 footer .footer-banner-warn { border-color: rgba(251,191,36,0.40); color: var(--yellow); }
27492813 footer .footer-banner-error { border-color: rgba(248,113,113,0.45); color: var(--red); }
2814
2815 /* ============================================================ */
2816 /* Block U4 — cross-document view transitions. */
2817 /* Chrome 126+, Edge, Safari 18.2+ get a soft 200ms fade on */
2818 /* every same-origin navigation. Older browsers ignore these */
2819 /* rules entirely — no JS shim, no breakage. */
2820 /* prefers-reduced-motion disables the animation honourably. */
2821 /* ============================================================ */
2822 -transition {
2823 navigation: auto;
2824 }
2825 ::view-transition-old(root),
2826 ::view-transition-new(root) {
2827 animation-duration: 200ms;
2828 animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
2829 }
2830 ::view-transition-old(root) {
2831 animation-name: vt-fade-out;
2832 }
2833 ::view-transition-new(root) {
2834 animation-name: vt-fade-in;
2835 }
2836 vt-fade-out {
2837 to { opacity: 0; }
2838 }
2839 vt-fade-in {
2840 from { opacity: 0; }
2841 }
2842 (prefers-reduced-motion: reduce) {
2843 ::view-transition-old(root),
2844 ::view-transition-new(root) {
2845 animation-duration: 0s;
2846 }
2847 }
2848 /* The transition system picks up its root subject from this rule. */
2849 body {
2850 view-transition-name: root;
2851 }
27502852`;
27512853