Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
Commitbba3681unknown_key

feat(BLOCK-U): senior polish pass — hero rebuild + button polish + spacing rhythm + view transitions

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.
Test User committed on May 14, 2026Parent: 3a6c5ec
9 files changed+514169bba36814bd02032a399f76971c9b732ceed9a3d2
9 changed files+514−169
Addedsrc/__tests__/u-polish.test.ts+148−0View fileUnifiedSplit
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
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()}>
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.
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>
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">
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"
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];
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
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)"
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">
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}
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"
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"
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
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
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>
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];
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>
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
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
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">
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"
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>
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)}
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">
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">
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">
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">
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>
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">
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"
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"
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">
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>
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"
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>
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
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"
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
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.
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)}`}
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}
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
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"
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"
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"
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"
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"
175175 <span>Enable Sleep Mode (daily &ldquo;overnight&rdquo; 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
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"
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"
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"
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"
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"
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>
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"
405405 {report.gateFailuresAutoRepaired} &middot; Hours saved:{" "}
406406 {report.hoursSaved} &middot; 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">
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
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`}
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 )}
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
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"
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>
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 ) : (() => {
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}
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
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
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"
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 )}
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;
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; }
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 {
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);
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;
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;
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 }
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);
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
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>
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 }
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;
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;
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;
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: '';
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),
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;
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;
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 {
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; }
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 @media (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 /* ============================================================ */
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 @view-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 @keyframes vt-fade-out {
2837 to { opacity: 0; }
2838 }
2839 @keyframes vt-fade-in {
2840 from { opacity: 0; }
2841 }
2842 @media (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