Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

landing.tsx

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

landing.tsxBlame3011 lines · 3 contributors
2b821b7Claude1/**
2 * Marketing landing page for logged-out visitors.
3 *
958d26aClaude4 * Editorial-Technical redesign — 2026.05.
5 * Hero · trust strip · feature grid · workflow walkthrough ·
6 * comparison · terminal · pricing teaser · closing CTA.
2b821b7Claude7 *
5f2e749Claude8 * Block L10 — hero rewrite. The hero now lands the Block L positioning
9 * ("the git host built around Claude"): gradient headline, one-line
10 * install snippet w/ copy button, three CTAs (Sign up / Demo / vs-GitHub),
11 * and a four-line "what just happened" rail driven off the L4 publicStats
12 * payload. The L4 counters tile section and L5 vs-GitHub CTA are both
13 * preserved — additive only.
14 *
15 * Also adds two new editorial sections below the L4 counters:
16 * - "Three reasons to switch" (Sleep Mode / Migrate / Demo)
17 * - "How is this different from GitHub?" pull-quote → /vs-github
18 *
958d26aClaude19 * Pure presentational. Drops into <Layout user={null}>.
20 * All styles scoped under `.landing-` so they don't bleed into app views.
2b821b7Claude21 */
22
23import type { FC } from "hono/jsx";
52ad8b1Claude24import type { PublicStats } from "../lib/public-stats";
534f04aClaude25import { DEMO_USERNAME } from "../lib/demo-seed";
26
27export interface LandingLiveFeedQueued {
28 repo: string;
29 number: number;
30 title: string;
31 createdAt: string | Date;
32}
33
34export interface LandingLiveFeedMerge {
35 repo: string;
36 number: number;
37 title: string;
38 mergedAt: string | Date;
39}
40
41export interface LandingLiveFeedReview {
42 repo: string;
43 prNumber: number;
44 commentSnippet: string;
45 createdAt: string | Date;
46}
47
48export interface LandingLiveFeedEntry {
49 kind: "auto_merge.merged" | "ai_build.dispatched" | "ai_review.posted";
50 repo: string;
51 ref: { type: "issue" | "pr"; number: number };
52 at: string | Date;
53}
54
55/**
56 * Block M1 — server-rendered snapshot of the live-now feed. The same
57 * fields are also fetched client-side every 30s from
58 * `/api/v2/demo/{queued,merges,reviews,activity}`. Optional so existing
59 * call-sites (and tests that don't care about the live block) keep
60 * compiling.
61 */
62export interface LandingLiveFeed {
63 queued: LandingLiveFeedQueued[];
64 merges: LandingLiveFeedMerge[];
65 reviews: LandingLiveFeedReview[];
66 reviewCount: number;
67 feed: LandingLiveFeedEntry[];
68}
2b821b7Claude69
70export interface LandingPageProps {
71 stats?: {
72 publicRepos?: number;
73 users?: number;
74 };
52ad8b1Claude75 /**
76 * Block L4 — full public-stats payload (lifetime + trailing-7-day
77 * AI-highlight counters). When present, the hero renders an animated
78 * six-tile social-proof row beneath the eyebrow.
79 */
80 publicStats?: PublicStats | null;
534f04aClaude81 /**
82 * Block M1 — initial SSR snapshot for the live-now feed block.
83 * The same endpoints poll client-side every 30s. When undefined the
84 * section still renders, but with empty-state copy until the first
85 * client poll lands.
86 */
87 liveFeed?: LandingLiveFeed | null;
2b821b7Claude88}
89
534f04aClaude90export const LandingHero: FC<LandingPageProps> = ({
91 stats,
92 publicStats,
93 liveFeed,
94} = {}) => {
8e9f1d9Claude95 const hasStats =
96 stats &&
97 ((stats.publicRepos !== undefined && stats.publicRepos > 0) ||
98 (stats.users !== undefined && stats.users > 0));
4c47454Claude99
52ad8b1Claude100 // Block L4 — six-tile social proof row. Rendered only when the
101 // cached public-stats payload is available; absent → fall back to
102 // the small text-only `landing-stats` row.
103 const tiles = publicStats
104 ? buildSocialProofTiles(publicStats)
105 : null;
106
534f04aClaude107 // Block M1 — SSR-friendly fallbacks so the no-JS path still renders
108 // a populated block. The client-side poller will overwrite these
109 // every 30s anyway.
110 const liveQueued = liveFeed?.queued ?? [];
111 const liveMerges = liveFeed?.merges ?? [];
112 const liveReviews = liveFeed?.reviews ?? [];
113 const liveReviewCount = liveFeed?.reviewCount ?? 0;
114 const liveEntries = liveFeed?.feed ?? [];
115
2b821b7Claude116 return (
117 <>
fa880f2Claude118 <style dangerouslySetInnerHTML={{ __html: landingCss }} />
2b821b7Claude119
4c47454Claude120 <div class="landing-root">
dc26881CC LABS App121 {/* ---------- 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 */}
4c47454Claude135 <section class="landing-hero">
958d26aClaude136 <div class="landing-hero-bg" aria-hidden="true">
137 <div class="landing-hero-blob landing-hero-blob-1" />
138 <div class="landing-hero-blob landing-hero-blob-2" />
dc26881CC LABS App139 <div class="landing-hero-blob landing-hero-orb" />
958d26aClaude140 <div class="landing-hero-grid" />
2b821b7Claude141 </div>
958d26aClaude142
143 <div class="landing-hero-inner stagger">
144 <div class="eyebrow landing-hero-eyebrow">
145 <span class="landing-hero-pulse" />
146 v1 · pre-launch · {new Date().getFullYear()}
147 </div>
148
149 <h1 class="landing-hero-title display">
5f2e749Claude150 <span class="gradient-text">The git host built around Claude.</span>
958d26aClaude151 </h1>
152
153 <p class="landing-hero-sub">
5f2e749Claude154 Label an issue. Walk away. Wake up to a merged PR.
958d26aClaude155 </p>
156
dc26881CC LABS App157 {/* U1 — primary CTA row, demoted to 2 buttons. */}
158 <div class="landing-hero-ctas" data-testid="hero-primary-ctas">
958d26aClaude159 <a href="/register" class="btn btn-primary btn-xl landing-cta-primary">
5f2e749Claude160 Sign up free
958d26aClaude161 <span class="landing-cta-arrow" aria-hidden="true">{"→"}</span>
162 </a>
dc26881CC LABS App163 {/* BLOCK Q1 — one-click Claude Desktop install. */}
93fe97eClaude164 <a
165 href="/gluecron.dxt"
166 class="btn btn-xl landing-cta-dxt"
167 download
168 data-testid="cta-dxt"
169 >
170 Add to Claude Desktop
171 <span class="landing-cta-arrow" aria-hidden="true">{"→"}</span>
172 </a>
dc26881CC LABS App173 </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">
5f2e749Claude182 Try the live demo
dc26881CC LABS App183 <span aria-hidden="true">{" →"}</span>
958d26aClaude184 </a>
dc26881CC LABS App185 <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">
52ad8b1Claude187 Compare to GitHub
dc26881CC LABS App188 <span aria-hidden="true">{" →"}</span>
52ad8b1Claude189 </a>
dc26881CC LABS App190 <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
cd4f63bTest User193 <span aria-hidden="true">{" →"}</span>
194 </a>
195 </div>
196
dc26881CC LABS App197 {/* 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. */}
5f2e749Claude222 {publicStats && (
223 <ul class="landing-hero-rail" aria-label="What just happened on Gluecron">
224 <li>
225 <strong>{publicStats.weeklyPrsAutoMerged.toLocaleString()}</strong>
dc26881CC LABS App226 <span class="landing-hero-rail-label">PRs auto-merged</span>
5f2e749Claude227 </li>
228 <li>
229 <strong>{publicStats.weeklyIssuesBuiltByAi.toLocaleString()}</strong>
dc26881CC LABS App230 <span class="landing-hero-rail-label">issues built by AI</span>
5f2e749Claude231 </li>
232 <li>
233 <strong>{publicStats.weeklyDeploysShipped.toLocaleString()}</strong>
dc26881CC LABS App234 <span class="landing-hero-rail-label">deploys overnight</span>
5f2e749Claude235 </li>
236 <li>
dc26881CC LABS App237 <strong>{`~${Math.round(publicStats.weeklyHoursSaved).toLocaleString()}`}</strong>
238 <span class="landing-hero-rail-label">hours saved by AI</span>
5f2e749Claude239 </li>
240 </ul>
241 )}
242
243 {/* L8 — free-tier reassurance link. Keeps anxiety low for the AI-curious. */}
244 <p class="landing-hero-freenote">
245 Free forever for the AI-curious.{" "}
246 <a href="/pricing" class="landing-hero-freenote-link">
247 See pricing &rarr;
248 </a>
249 </p>
250
958d26aClaude251 <p class="landing-hero-caption">
252 Already have a repo?
253 <span class="landing-hero-cmd">
254 <span class="kbd">git</span>
255 <span class="kbd">remote</span>
256 <span class="kbd">add</span>
257 <span class="kbd">gluecron</span>
258 <span class="landing-hero-arrow">{"→"}</span>
259 <span class="kbd">git push</span>
260 </span>
261 </p>
262
263 {hasStats && (
264 <p class="landing-stats">
265 {stats!.publicRepos !== undefined && stats!.publicRepos > 0 && (
266 <span>
267 <strong>{stats!.publicRepos.toLocaleString()}</strong>
268 {stats!.publicRepos === 1 ? " repo" : " repos"}
269 </span>
270 )}
271 {stats!.publicRepos !== undefined &&
272 stats!.publicRepos > 0 &&
273 stats!.users !== undefined &&
274 stats!.users > 0 && <span class="landing-stats-sep">·</span>}
275 {stats!.users !== undefined && stats!.users > 0 && (
276 <span>
277 <strong>{stats!.users.toLocaleString()}</strong>
278 {stats!.users === 1 ? " developer" : " developers"}
279 </span>
280 )}
281 <span class="landing-stats-sep">·</span>
4c47454Claude282 <span>
958d26aClaude283 <strong>100%</strong> AI-native
4c47454Claude284 </span>
958d26aClaude285 </p>
286 )}
287 </div>
c963db5Claude288 </section>
c475ee6Claude289
534f04aClaude290 {/* ---------- Block M1 — Live-now demo feed ---------- */}
291 <LiveNowSection
292 queued={liveQueued}
293 merges={liveMerges}
294 reviews={liveReviews}
295 reviewCount={liveReviewCount}
296 feed={liveEntries}
297 />
298
52ad8b1Claude299 {/* ---------- L4 social-proof counters (animated count-up) ---------- */}
300 {tiles && (
301 <section class="landing-counters" aria-label="Gluecron live counters">
302 <div class="landing-counters-grid">
303 {tiles.map((t) => (
304 <div class="landing-counter">
305 <div
306 class="landing-counter-num"
307 data-counter-target={String(t.value)}
308 data-counter-suffix={t.suffix ?? ""}
309 data-counter-prefix={t.prefix ?? ""}
310 >
311 {t.prefix ?? ""}
312 {t.value.toLocaleString()}
313 {t.suffix ?? ""}
314 </div>
315 <div class="landing-counter-label">{t.label}</div>
316 </div>
317 ))}
318 </div>
319 <script dangerouslySetInnerHTML={{ __html: landingCountersJs }} />
320 </section>
321 )}
322
5f2e749Claude323 {/* ---------- L10 — Three reasons to switch ---------- */}
324 <section class="landing-section landing-reasons" aria-label="Three reasons to switch">
325 <div class="section-header">
326 <div class="eyebrow">Three reasons to switch</div>
327 <h2>Built so Claude can do the work.</h2>
328 </div>
329 <div class="landing-reasons-grid">
330 <ReasonCard
331 icon={
332 <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
333 <path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" />
334 </svg>
335 }
336 title="Toggle Sleep Mode"
337 body="Claude does the work overnight. You get a 9 AM digest of what shipped — PRs merged, deploys live, incidents triaged."
338 link={{ href: "/sleep-mode", label: "Turn on Sleep Mode" }}
339 />
340 <ReasonCard
341 icon={
342 <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
343 <polyline points="4 17 10 11 4 5" />
344 <line x1="12" y1="19" x2="20" y2="19" />
345 </svg>
346 }
347 title="One command to migrate"
348 body="Drop a single curl into your shell. Gluecron rehosts your repo, your issues, your branches — no SaaS rip-and-replace project required."
349 extra={
350 <code class="landing-reasons-code">curl -sSL gluecron.com/install | bash</code>
351 }
352 link={{ href: "/import", label: "Or import from GitHub" }}
353 />
354 <ReasonCard
355 icon={
356 <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
357 <polygon points="5 3 19 12 5 21 5 3" />
358 </svg>
359 }
360 title="Open the demo, watch it work"
361 body="The demo repo is real. Label an issue, hit refresh, see Claude open the PR. Inspect the diff. Approve the merge. Zero setup, zero credit card."
362 link={{ href: "/demo", label: "Open the live demo" }}
363 />
364 </div>
365 </section>
366
c963db5Claude367 {/* ---------- Capability strip — uppercase tracked grid (crontech-style) ---------- */}
368 <section class="landing-caps">
369 <div class="landing-caps-grid">
370 <span class="landing-cap">Claude-powered AI</span>
371 <span class="landing-cap">Spec-to-PR</span>
372 <span class="landing-cap">Auto-repair</span>
373 <span class="landing-cap">Real-time gates</span>
374 <span class="landing-cap">MCP-native</span>
375 <span class="landing-cap">Workflow runner</span>
376 <span class="landing-cap">Self-hostable</span>
377 <span class="landing-cap">Branch protection</span>
378 <span class="landing-cap">Bun + Hono</span>
379 <span class="landing-cap">Drizzle + Postgres</span>
380 <span class="landing-cap">JSX server-rendered</span>
381 <span class="landing-cap">Type-safe end to end</span>
c475ee6Claude382 </div>
958d26aClaude383 </section>
384
c963db5Claude385 {/* ---------- Big stat row (crontech-style hero closer) ---------- */}
386 <section class="landing-bigstats">
387 <div class="landing-bigstats-grid">
388 <div class="landing-bigstat">
389 <div class="landing-bigstat-num">Claude-powered</div>
390 <div class="landing-bigstat-label">The best AI, native</div>
391 </div>
392 <div class="landing-bigstat">
393 <div class="landing-bigstat-num">Self-hosted</div>
394 <div class="landing-bigstat-label">On your hardware</div>
395 </div>
396 <div class="landing-bigstat">
397 <div class="landing-bigstat-num">MCP-native</div>
398 <div class="landing-bigstat-label">Claude · Cursor · Code</div>
399 </div>
400 <div class="landing-bigstat">
401 <div class="landing-bigstat-num">Real-time</div>
402 <div class="landing-bigstat-label">SSE everywhere</div>
403 </div>
958d26aClaude404 </div>
4c47454Claude405 </section>
406
407 {/* ---------- Feature grid ---------- */}
958d26aClaude408 <section class="landing-section">
409 <div class="section-header">
410 <div class="eyebrow">The platform</div>
411 <h2>An IDE for your repo, not just a host.</h2>
412 <p>
413 Gluecron ships the surfaces GitHub charges extra for, and the
414 ones it never built. AI is a teammate with its own commits, not
415 a sidebar.
2b821b7Claude416 </p>
417 </div>
4c47454Claude418
958d26aClaude419 <div class="landing-features stagger">
420 <FeatureCard
421 icon={
422 <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
423 <path d="M12 3l1.9 4.6L18.5 9l-3.6 3 1 4.8L12 14.5 8.1 16.8l1-4.8L5.5 9l4.6-1.4z" />
424 </svg>
425 }
426 title="AI as a teammate"
427 desc="Spec-to-PR drafts entire features from plain English. Auto-explain reviews every diff. The AI commits with its own bot account, visible in your history."
428 />
429 <FeatureCard
430 icon={
431 <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
432 <path d="M12 2.5l8 3.5v6c0 5-3.5 8.5-8 9.5-4.5-1-8-4.5-8-9.5v-6z" />
433 <path d="M9 12l2 2 4-4" />
434 </svg>
435 }
436 title="Quality gate that learns"
437 desc="GateTest scans every push. Auto-repair fixes regressions before you see them. Required checks block bad PRs from merging. Your software self-corrects."
438 />
439 <FeatureCard
440 icon={
441 <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
442 <path d="M13 2L4 14h7l-1 8 9-12h-7z" />
443 </svg>
444 }
445 title="Real-time everything"
446 desc="Live workflow logs over SSE. Live PR review presence. Live deploys you watch happen. No polling, no refresh, no waiting on a CI tab."
447 />
448 <FeatureCard
449 icon={
450 <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
451 <rect x="3" y="3" width="18" height="18" rx="3" />
452 <path d="M3 9h18M9 21V9" />
453 </svg>
454 }
455 title="Workflow runner"
456 desc="Drop a yaml in `.gluecron/workflows/` and it runs on every push. Cron triggers, secret substitution, matrix runs, artifacts. No SaaS provider in the loop."
457 />
458 <FeatureCard
459 icon={
460 <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
461 <circle cx="12" cy="12" r="9" />
462 <path d="M3 12h18M12 3a14 14 0 010 18M12 3a14 14 0 000 18" />
463 </svg>
464 }
465 title="MCP-native"
466 desc="Claude, Cursor, Code — they speak Model Context Protocol. Gluecron exposes search, file read, issues, codebase explain as MCP tools by default."
467 />
468 <FeatureCard
469 icon={
470 <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
471 <path d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
472 <path d="M12 7v5l3 2" />
473 </svg>
474 }
475 title="Yours, on your hardware"
476 desc="Single-binary Bun runtime. Postgres + bare git repos on a volume. Deploy to Fly, Railway, your own VPS. No vendor lock, no surprise bills."
477 />
2b821b7Claude478 </div>
958d26aClaude479 </section>
2b821b7Claude480
958d26aClaude481 {/* ---------- Workflow walkthrough ---------- */}
482 <section class="landing-section landing-walk">
483 <div class="section-header">
484 <div class="eyebrow">How it works</div>
485 <h2>Push code. Watch it ship.</h2>
486 <p>
487 Every push triggers the same pipeline whether the commit came
488 from you, from CI, or from an AI agent.
4c47454Claude489 </p>
490 </div>
958d26aClaude491
492 <div class="landing-walk-grid">
493 <WalkStep n="01" title="Push" desc="git push to gluecron — Smart-HTTP, SSH, or via the web editor." />
494 <WalkStep n="02" title="Gate" desc="GateTest runs. Secret scanner runs. AI security review posts inline comments." />
495 <WalkStep n="03" title="Repair" desc="If a gate fails, auto-repair tries to fix it. New commit gets re-gated." />
496 <WalkStep n="04" title="Ship" desc="Green push to default branch fires deploy webhook. Crontech, Fly, your prod." />
497 </div>
4c47454Claude498 </section>
2b821b7Claude499
4c47454Claude500 {/* ---------- Terminal block ---------- */}
958d26aClaude501 <section class="landing-section landing-terminal-section">
502 <div class="landing-terminal-wrap">
503 <div class="landing-terminal" role="img" aria-label="Example git push to gluecron with passing gates">
504 <div class="landing-terminal-chrome">
505 <span class="landing-terminal-dot landing-terminal-dot-r" />
506 <span class="landing-terminal-dot landing-terminal-dot-y" />
507 <span class="landing-terminal-dot landing-terminal-dot-g" />
508 <span class="landing-terminal-title">~/your-repo &mdash; zsh</span>
509 </div>
510 <div class="landing-terminal-body">
511 <div class="landing-term-line">
512 <span class="landing-term-prompt">$</span>
513 <span>git remote add gluecron https://gluecron.com/you/your-repo.git</span>
514 </div>
515 <div class="landing-term-line">
516 <span class="landing-term-prompt">$</span>
517 <span>git push -u gluecron main</span>
518 </div>
519 <div class="landing-term-line landing-term-out">
520 <span class="landing-term-meta">remote:</span>
521 <span>Resolving deltas… 100% (24/24)</span>
522 </div>
523 <div class="landing-term-line landing-term-out landing-term-ok-line">
524 <span class="landing-term-ok">{"✓"}</span>
525 <span>pushed to gluecron.com/you/your-repo</span>
526 </div>
527 <div class="landing-term-line landing-term-out landing-term-ok-line">
528 <span class="landing-term-ok">{"✓"}</span>
529 <span>GateTest passed (12 rules, 0 violations)</span>
530 </div>
531 <div class="landing-term-line landing-term-out landing-term-ok-line">
532 <span class="landing-term-ok">{"✓"}</span>
533 <span>AI review posted (2 suggestions, 0 blockers)</span>
534 </div>
535 <div class="landing-term-line landing-term-out landing-term-ok-line">
536 <span class="landing-term-ok">{"✓"}</span>
537 <span>deployed to your-repo.gluecron.com <span class="landing-term-meta">(4.1s)</span></span>
538 </div>
539 <div class="landing-term-line landing-term-cursor">
540 <span class="landing-term-prompt">$</span>
541 <span class="landing-term-blink">▍</span>
542 </div>
543 </div>
4c47454Claude544 </div>
958d26aClaude545 </div>
546 </section>
547
548 {/* ---------- Comparison ---------- */}
549 <section class="landing-section">
550 <div class="section-header">
551 <div class="eyebrow">vs the incumbent</div>
552 <h2>Everything GitHub charges for. And the parts they didn't build.</h2>
553 </div>
554
555 <div class="landing-compare">
556 <CompareRow feature="Git hosting + Smart-HTTP push" them="✓" us="✓" />
557 <CompareRow feature="Issues, PRs, code review" them="✓" us="✓" />
558 <CompareRow feature="Workflow runner (Actions-equivalent)" them="paid minutes" us="self-hosted, unmetered" highlight />
559 <CompareRow feature="AI code review on every PR" them="Copilot subscription" us="built in" highlight />
560 <CompareRow feature="Spec-to-PR (NL feature → draft PR)" them="—" us="✓" highlight />
561 <CompareRow feature="Auto-repair on failed gates" them="—" us="✓" highlight />
562 <CompareRow feature="Real-time SSE for logs + PRs" them="polling" us="streaming" highlight />
563 <CompareRow feature="MCP server (Claude / Cursor)" them="—" us="✓" highlight />
564 <CompareRow feature="Self-host on your own infra" them="enterprise tier" us="single binary" highlight />
565 <CompareRow feature="Pre-receive policy enforcement" them="rulesets (GHE)" us="✓" />
566 </div>
567 </section>
568
569 {/* ---------- Pricing teaser ---------- */}
570 <section class="landing-section">
571 <div class="section-header">
572 <div class="eyebrow">Pricing</div>
573 <h2>Free to start. Honest at scale.</h2>
574 <p>
575 Self-hosting is free forever. Hosted plans price the AI calls,
576 not the seats.
577 </p>
578 </div>
579
580 <div class="landing-pricing">
581 <PricingCard
582 tier="Free"
583 price="$0"
584 cadence="forever"
585 desc="For personal projects + open source. Public + private repos, full AI suite, fair quotas."
586 features={["Unlimited public repos", "3 private repos", "5K AI calls / mo", "Community support"]}
587 cta="Start free"
588 href="/register"
589 />
590 <PricingCard
591 tier="Pro"
592 price="$12"
593 cadence="per user / mo"
594 desc="For working developers. Lifts every quota, adds priority routing, no Gluecron branding on deploys."
595 features={["Unlimited private repos", "100K AI calls / mo", "Priority queue", "Custom domains"]}
596 cta="Go Pro"
597 href="/settings/billing"
598 highlight
599 />
600 <PricingCard
601 tier="Team"
602 price="Talk to us"
603 cadence="custom"
604 desc="For orgs running production on Gluecron. SSO, audit retention, enterprise SLA, on-prem."
605 features={["SSO + SCIM", "On-prem deploy", "Dedicated capacity", "24/7 incident response"]}
606 cta="Contact"
607 href="mailto:hello@gluecron.com"
608 />
609 </div>
610 </section>
611
5f2e749Claude612 {/* ---------- L10 — "How is this different?" pull-quote ---------- */}
613 <section class="landing-pullquote-section" aria-label="How is this different from GitHub?">
614 <figure class="landing-pullquote">
615 <div class="landing-pullquote-eyebrow">How is this different from GitHub?</div>
616 <blockquote class="landing-pullquote-text">
617 Every other host bolts AI on as a sidecar. Gluecron is the first
618 git host where Claude is a first-class developer. Built to be
619 operated by AI agents, not just augmented by them.
620 </blockquote>
621 <a href="/vs-github" class="landing-pullquote-link">
622 See the full comparison
623 <span class="landing-cta-arrow" aria-hidden="true">{"→"}</span>
624 </a>
625 </figure>
626 </section>
627
958d26aClaude628 {/* ---------- Closing CTA ---------- */}
629 <section class="landing-cta-section">
630 <div class="landing-cta-card">
631 <div class="landing-cta-bg" aria-hidden="true" />
632 <div class="eyebrow">Ready when you are</div>
633 <h2 class="landing-cta-title">
634 Stop maintaining the platform.<br />
635 <span class="gradient-text">Start shipping the product.</span>
636 </h2>
637 <p class="landing-cta-sub">
638 Free to start, self-hosted-friendly, MCP-native. Migrate from
639 GitHub in one click.
640 </p>
641 <div class="landing-cta-buttons">
642 <a href="/register" class="btn btn-primary btn-xl">
643 Create your account
644 <span class="landing-cta-arrow" aria-hidden="true">{"→"}</span>
645 </a>
646 <a href="/import" class="btn btn-ghost btn-xl">
647 Migrate a repo
648 </a>
4c47454Claude649 </div>
650 </div>
651 </section>
5f2e749Claude652
653 {/* L10 — clipboard copy script for the hero install snippet. */}
654 <script dangerouslySetInnerHTML={{ __html: landingCopyJs }} />
4c47454Claude655 </div>
2b821b7Claude656 </>
657 );
658};
659
958d26aClaude660const FeatureCard: FC<{ icon: any; title: string; desc: string }> = ({
661 icon,
662 title,
663 desc,
664}) => (
665 <div class="landing-feature">
666 <div class="landing-feature-icon" aria-hidden="true">
667 {icon}
668 </div>
669 <h3 class="landing-feature-title">{title}</h3>
670 <p class="landing-feature-desc">{desc}</p>
671 </div>
672);
673
5f2e749Claude674// Block L10 — "Three reasons to switch" column.
675const ReasonCard: FC<{
676 icon: any;
677 title: string;
678 body: string;
679 link: { href: string; label: string };
680 extra?: any;
681}> = ({ icon, title, body, link, extra }) => (
682 <div class="landing-reason">
683 <div class="landing-reason-icon" aria-hidden="true">
684 {icon}
685 </div>
686 <h3 class="landing-reason-title">{title}</h3>
687 <p class="landing-reason-body">{body}</p>
688 {extra}
689 <a href={link.href} class="landing-reason-link">
690 {link.label}
691 <span class="landing-cta-arrow" aria-hidden="true">{"→"}</span>
692 </a>
693 </div>
694);
695
958d26aClaude696const WalkStep: FC<{ n: string; title: string; desc: string }> = ({
697 n,
698 title,
699 desc,
700}) => (
701 <div class="landing-walk-step">
702 <div class="landing-walk-num">{n}</div>
703 <h3 class="landing-walk-title">{title}</h3>
704 <p class="landing-walk-desc">{desc}</p>
705 </div>
706);
707
708const CompareRow: FC<{
709 feature: string;
710 them: string;
711 us: string;
712 highlight?: boolean;
713}> = ({ feature, them, us, highlight }) => (
714 <div class={`landing-compare-row${highlight ? " landing-compare-hl" : ""}`}>
715 <div class="landing-compare-feature">{feature}</div>
716 <div class="landing-compare-them">{them}</div>
717 <div class="landing-compare-us">{us === "✓" ? "✓" : us}</div>
718 </div>
719);
720
721const PricingCard: FC<{
722 tier: string;
723 price: string;
724 cadence: string;
725 desc: string;
726 features: string[];
727 cta: string;
728 href: string;
729 highlight?: boolean;
730}> = ({ tier, price, cadence, desc, features, cta, href, highlight }) => (
731 <div class={`landing-price-card${highlight ? " landing-price-hl" : ""}`}>
732 {highlight && <div class="landing-price-badge">Most popular</div>}
733 <div class="landing-price-tier">{tier}</div>
734 <div class="landing-price-amount">
735 <span class="landing-price-num">{price}</span>
736 <span class="landing-price-cad">{cadence}</span>
737 </div>
738 <p class="landing-price-desc">{desc}</p>
739 <ul class="landing-price-features">
740 {features.map((f) => (
741 <li>
742 <span class="landing-price-check" aria-hidden="true">{"✓"}</span>
743 {f}
744 </li>
745 ))}
746 </ul>
747 <a
748 href={href}
749 class={`btn ${highlight ? "btn-primary" : "btn-secondary"} btn-block landing-price-cta`}
750 >
751 {cta}
752 </a>
753 </div>
754);
755
52ad8b1Claude756// ─────────────────────────────────────────────────────────────────
757// Block L4 — social-proof tile builder.
758//
759// Pure: takes the cached PublicStats payload and emits the six
760// landing-page tiles in render order. Exported so tests / future
761// surfaces (dashboard, /about, …) can share the exact same copy.
762// ─────────────────────────────────────────────────────────────────
763
764export interface SocialProofTile {
765 label: string;
766 value: number;
767 prefix?: string;
768 suffix?: string;
769}
770
771export function buildSocialProofTiles(s: PublicStats): SocialProofTile[] {
772 return [
773 { label: "Public repos", value: s.totalPublicRepos },
774 { label: "Developers", value: s.totalUsers },
775 {
776 label: "PRs auto-merged this week",
777 value: s.weeklyPrsAutoMerged,
778 },
779 {
780 label: "Issues built by AI this week",
781 value: s.weeklyIssuesBuiltByAi,
782 },
783 {
784 label: "Deploys shipped this week",
785 value: s.weeklyDeploysShipped,
786 },
787 {
788 label: "Hours saved this week",
789 // Round to whole hours for the tile — the precise 0.1 figure
790 // lives on the dashboard widget; the marketing surface keeps
791 // the number scannable.
792 value: Math.round(s.weeklyHoursSaved),
793 prefix: "~",
794 suffix: "h",
795 },
796 ];
797}
798
534f04aClaude799// ─────────────────────────────────────────────────────────────────
800// Block M1 — Live-now demo feed.
801//
802// A 4-tile row inserted between the hero and the L4 counters tile
803// section, surfacing real autopilot activity from the seeded `demo`
804// owner's repos:
805// 1. Issues queued for AI build (ai:build label, open)
806// 2. PRs auto-merged in the last 24h
807// 3. AI reviews posted today (count + latest 3)
808// 4. Combined activity feed (last 10)
809//
810// SSR-renders an initial snapshot, then re-fetches every 30s via the
811// L3 JSON endpoints so the page feels alive without a websocket.
812// Pure presentational; the route layer owns the DB reads.
813// ─────────────────────────────────────────────────────────────────
814
815/**
816 * Render an ISO timestamp (or Date) as a coarse "about N units ago"
817 * string. Tolerates strings, Dates, NaN, and future timestamps.
818 *
819 * Exported for unit testing.
820 */
821export function relativeTimeFromNow(
822 value: string | Date | number | null | undefined,
823 now: number = Date.now()
824): string {
825 if (value === null || value === undefined) return "just now";
826 let t: number;
827 if (value instanceof Date) {
828 t = value.getTime();
829 } else if (typeof value === "number") {
830 t = value;
831 } else {
832 t = new Date(value).getTime();
833 }
834 if (!Number.isFinite(t)) return "just now";
835 const delta = now - t;
836 // Future timestamps (clock skew) — treat as "just now" rather than
837 // surfacing a confusing negative.
838 if (delta < 0) return "just now";
839 const s = Math.floor(delta / 1000);
840 if (s < 60) return "just now";
841 const m = Math.floor(s / 60);
842 if (m < 60) return `about ${m} minute${m === 1 ? "" : "s"} ago`;
843 const h = Math.floor(m / 60);
844 if (h < 24) return `about ${h} hour${h === 1 ? "" : "s"} ago`;
845 const d = Math.floor(h / 24);
846 return `about ${d} day${d === 1 ? "" : "s"} ago`;
847}
848
849function feedEntryId(e: LandingLiveFeedEntry): string {
850 const at = e.at instanceof Date ? e.at.toISOString() : String(e.at);
851 return `${e.kind}|${e.repo}|${e.ref.type}|${e.ref.number}|${at}`;
852}
853
854function feedEntryLabel(kind: LandingLiveFeedEntry["kind"]): string {
855 switch (kind) {
856 case "auto_merge.merged":
857 return "auto-merged";
858 case "ai_build.dispatched":
859 return "AI-build queued";
860 case "ai_review.posted":
861 return "AI review posted";
862 }
863}
864
865interface LiveNowSectionProps {
866 queued: LandingLiveFeedQueued[];
867 merges: LandingLiveFeedMerge[];
868 reviews: LandingLiveFeedReview[];
869 reviewCount: number;
870 feed: LandingLiveFeedEntry[];
871}
872
873const LiveNowSection: FC<LiveNowSectionProps> = ({
874 queued,
875 merges,
876 reviews,
877 reviewCount,
878 feed,
879}) => {
880 return (
881 <section class="landing-livenow" aria-labelledby="landing-livenow-h">
882 <div class="landing-livenow-head">
883 <div class="landing-livenow-eyebrow">
884 <span class="landing-livenow-pulse" aria-hidden="true" />
885 Live now
886 </div>
887 <h2 id="landing-livenow-h" class="landing-livenow-title">
888 Claude is working on demo repos as you read this.
889 </h2>
890 <p class="landing-livenow-sub">
891 Every card below is real data from the public{" "}
892 <code>{DEMO_USERNAME}/*</code> repos. Refreshes every 30 seconds.
893 </p>
894 </div>
895
896 <div class="landing-livenow-grid" data-livenow-grid>
897 {/* Card 1 — queued issues */}
898 <article class="landing-livecard" aria-labelledby="lc-queued-h">
899 <header class="landing-livecard-head">
900 <span class="landing-livecard-dot" aria-hidden="true" />
901 <h3 id="lc-queued-h" class="landing-livecard-title">
902 Issues queued for AI
903 </h3>
904 </header>
905 <ul class="landing-livecard-list" data-livecard="queued">
906 {queued.length === 0 ? (
907 <li class="landing-livecard-empty">
908 No queued AI builds — quiet right now.
909 </li>
910 ) : (
911 queued.slice(0, 3).map((i) => (
912 <li
913 class="landing-livecard-row"
914 data-row-id={`queued|${i.repo}|${i.number}`}
915 >
916 <a
917 class="landing-livecard-link"
918 href={`/${DEMO_USERNAME}/${i.repo}/issues/${i.number}`}
919 >
920 <span class="landing-livecard-num">#{i.number}</span>{" "}
921 <span class="landing-livecard-title-text">{i.title}</span>
922 </a>
923 <div class="landing-livecard-meta">
924 <span class="landing-livecard-repo">{i.repo}</span>
925 </div>
926 </li>
927 ))
928 )}
929 </ul>
930 </article>
931
932 {/* Card 2 — recently merged */}
933 <article class="landing-livecard" aria-labelledby="lc-merges-h">
934 <header class="landing-livecard-head">
935 <span class="landing-livecard-dot" aria-hidden="true" />
936 <h3 id="lc-merges-h" class="landing-livecard-title">
937 Recently merged by AI
938 </h3>
939 </header>
940 <ul class="landing-livecard-list" data-livecard="merges">
941 {merges.length === 0 ? (
942 <li class="landing-livecard-empty">
943 No auto-merges in the last 24h.
944 </li>
945 ) : (
946 merges.slice(0, 3).map((m) => (
947 <li
948 class="landing-livecard-row"
949 data-row-id={`merges|${m.repo}|${m.number}`}
950 >
951 <a
952 class="landing-livecard-link"
953 href={`/${DEMO_USERNAME}/${m.repo}/pulls/${m.number}`}
954 >
955 <span class="landing-livecard-num">#{m.number}</span>{" "}
956 <span class="landing-livecard-title-text">{m.title}</span>
957 </a>
958 <div class="landing-livecard-meta">
959 AI merged in{" "}
960 <span class="landing-livecard-repo">{m.repo}</span>{" "}
961 <span
962 class="landing-livecard-rel"
963 data-rel={
964 m.mergedAt instanceof Date
965 ? m.mergedAt.toISOString()
966 : String(m.mergedAt)
967 }
968 >
969 {relativeTimeFromNow(m.mergedAt)}
970 </span>
971 </div>
972 </li>
973 ))
974 )}
975 </ul>
976 </article>
977
978 {/* Card 3 — AI reviews */}
979 <article class="landing-livecard" aria-labelledby="lc-reviews-h">
980 <header class="landing-livecard-head">
981 <span class="landing-livecard-dot" aria-hidden="true" />
982 <h3 id="lc-reviews-h" class="landing-livecard-title">
983 AI reviews posted
984 </h3>
985 </header>
986 <div class="landing-livecard-bignum">
987 <span
988 class="landing-livecard-bignum-n"
989 data-livecard-count="reviews"
990 data-tick-target={String(reviewCount)}
991 >
992 {reviewCount.toLocaleString()}
993 </span>
994 <span class="landing-livecard-bignum-label">reviews today</span>
995 </div>
996 <ul class="landing-livecard-list" data-livecard="reviews">
997 {reviews.length === 0 ? (
998 <li class="landing-livecard-empty">
999 No AI reviews in the last 24h.
1000 </li>
1001 ) : (
1002 reviews.slice(0, 3).map((r) => (
1003 <li
1004 class="landing-livecard-row"
1005 data-row-id={`reviews|${r.repo}|${r.prNumber}`}
1006 >
1007 <a
1008 class="landing-livecard-link"
1009 href={`/${DEMO_USERNAME}/${r.repo}/pulls/${r.prNumber}`}
1010 >
1011 <span class="landing-livecard-num">#{r.prNumber}</span>{" "}
1012 <span class="landing-livecard-snippet">
1013 {r.commentSnippet}
1014 </span>
1015 </a>
1016 <div class="landing-livecard-meta">
1017 <span class="landing-livecard-repo">{r.repo}</span>
1018 </div>
1019 </li>
1020 ))
1021 )}
1022 </ul>
1023 </article>
1024
1025 {/* Card 4 — activity feed */}
1026 <article class="landing-livecard" aria-labelledby="lc-feed-h">
1027 <header class="landing-livecard-head">
1028 <span class="landing-livecard-dot" aria-hidden="true" />
1029 <h3 id="lc-feed-h" class="landing-livecard-title">
1030 Activity feed
1031 </h3>
1032 </header>
1033 <ul class="landing-livecard-list landing-livecard-feed" data-livecard="feed">
1034 {feed.length === 0 ? (
1035 <li class="landing-livecard-empty">
1036 Quiet right now — check back in a minute.
1037 </li>
1038 ) : (
1039 feed.slice(0, 10).map((e) => {
1040 const path = e.ref.type === "pr" ? "pulls" : "issues";
1041 const id = feedEntryId(e);
1042 return (
1043 <li class="landing-livecard-feedrow" data-row-id={id}>
1044 <span
1045 class={`landing-livecard-kind landing-livecard-kind-${e.kind.replace(/\./g, "-")}`}
1046 >
1047 {feedEntryLabel(e.kind)}
1048 </span>{" "}
1049 <a
1050 class="landing-livecard-link"
1051 href={`/${DEMO_USERNAME}/${e.repo}/${path}/${e.ref.number}`}
1052 >
1053 {e.repo} #{e.ref.number}
1054 </a>{" "}
1055 <span
1056 class="landing-livecard-rel"
1057 data-rel={
1058 e.at instanceof Date ? e.at.toISOString() : String(e.at)
1059 }
1060 >
1061 {relativeTimeFromNow(e.at)}
1062 </span>
1063 </li>
1064 );
1065 })
1066 )}
1067 </ul>
1068 </article>
1069 </div>
1070
1071 <div class="landing-livenow-cta">
1072 <span class="landing-livenow-cta-text">
1073 Want this for your repos?
1074 </span>
1075 <a class="landing-livenow-cta-link" href="/register">
1076 Sign up free
1077 <span class="landing-cta-arrow" aria-hidden="true">{"→"}</span>
1078 </a>
1079 <span class="landing-livenow-cta-sep" aria-hidden="true">·</span>
1080 <a class="landing-livenow-cta-link" href="/demo">
1081 Try the live demo
1082 <span class="landing-cta-arrow" aria-hidden="true">{"→"}</span>
1083 </a>
1084 </div>
1085
1086 <script dangerouslySetInnerHTML={{ __html: liveNowJs }} />
1087 </section>
1088 );
1089};
1090
1091// Inline poller. Plain JS so we don't ship a separate bundle. Hits the
1092// four L3 JSON endpoints every 30s, re-renders the four cards, ticks
1093// the big number, refreshes relative timestamps, flashes new rows.
1094const liveNowJs = `
1095(function(){
1096try{
1097 var DEMO=${JSON.stringify(DEMO_USERNAME)};
1098 var INTERVAL=30000;
1099 function esc(s){return String(s==null?'':s).replace(/[&<>"']/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c];});}
1100 function rel(v){
1101 if(v==null) return 'just now';
1102 var t=(v instanceof Date)?v.getTime():(typeof v==='number'?v:new Date(v).getTime());
1103 if(!isFinite(t)) return 'just now';
1104 var d=Date.now()-t;
1105 if(d<0) return 'just now';
1106 var s=Math.floor(d/1000);
1107 if(s<60) return 'just now';
1108 var m=Math.floor(s/60);
1109 if(m<60) return 'about '+m+' minute'+(m===1?'':'s')+' ago';
1110 var h=Math.floor(m/60);
1111 if(h<24) return 'about '+h+' hour'+(h===1?'':'s')+' ago';
1112 var dd=Math.floor(h/24);
1113 return 'about '+dd+' day'+(dd===1?'':'s')+' ago';
1114 }
1115 function tickNumber(el,target){
1116 if(!el) return;
1117 var start=parseInt(el.getAttribute('data-tick-current')||'0',10)||0;
1118 if(start===target){el.textContent=target.toLocaleString();el.setAttribute('data-tick-current',String(target));return;}
1119 var dur=800,t0=performance.now();
1120 function step(now){
1121 var p=Math.min(1,(now-t0)/dur);
1122 var eased=1-Math.pow(1-p,3);
1123 var v=Math.round(start+(target-start)*eased);
1124 el.textContent=v.toLocaleString();
1125 if(p<1) requestAnimationFrame(step); else el.setAttribute('data-tick-current',String(target));
1126 }
1127 requestAnimationFrame(step);
1128 }
1129 function flashRow(li){
1130 if(!li) return;
1131 li.classList.add('landing-livecard-flash');
1132 setTimeout(function(){li.classList.remove('landing-livecard-flash');},1100);
1133 }
1134 function diffMount(ul,newHtml,newIds){
1135 if(!ul) return;
1136 var prev={};
1137 var nodes=ul.querySelectorAll('[data-row-id]');
1138 for(var i=0;i<nodes.length;i++){prev[nodes[i].getAttribute('data-row-id')]=true;}
1139 ul.innerHTML=newHtml;
1140 var fresh=ul.querySelectorAll('[data-row-id]');
1141 for(var j=0;j<fresh.length;j++){
1142 var id=fresh[j].getAttribute('data-row-id');
1143 if(id && !prev[id]) flashRow(fresh[j]);
1144 }
1145 }
1146 function pollQueued(){
1147 return fetch('/api/v2/demo/queued',{credentials:'omit'}).then(function(r){return r.json();}).then(function(d){
1148 var ul=document.querySelector('[data-livecard="queued"]');if(!ul) return;
1149 var items=(d&&d.items)||[];
1150 if(items.length===0){ul.innerHTML='<li class="landing-livecard-empty">No queued AI builds — quiet right now.</li>';return;}
1151 var ids=[];
1152 var html=items.slice(0,3).map(function(i){
1153 var id='queued|'+i.repo+'|'+i.number;ids.push(id);
1154 return '<li class="landing-livecard-row" data-row-id="'+esc(id)+'">'+
1155 '<a class="landing-livecard-link" href="/'+esc(DEMO)+'/'+esc(i.repo)+'/issues/'+i.number+'">'+
1156 '<span class="landing-livecard-num">#'+i.number+'</span> '+
1157 '<span class="landing-livecard-title-text">'+esc(i.title)+'</span></a>'+
1158 '<div class="landing-livecard-meta"><span class="landing-livecard-repo">'+esc(i.repo)+'</span></div></li>';
1159 }).join('');
1160 diffMount(ul,html,ids);
1161 }).catch(function(){});
1162 }
1163 function pollMerges(){
1164 return fetch('/api/v2/demo/merges',{credentials:'omit'}).then(function(r){return r.json();}).then(function(d){
1165 var ul=document.querySelector('[data-livecard="merges"]');if(!ul) return;
1166 var items=(d&&d.items)||[];
1167 if(items.length===0){ul.innerHTML='<li class="landing-livecard-empty">No auto-merges in the last 24h.</li>';return;}
1168 var ids=[];
1169 var html=items.slice(0,3).map(function(m){
1170 var id='merges|'+m.repo+'|'+m.number;ids.push(id);
1171 return '<li class="landing-livecard-row" data-row-id="'+esc(id)+'">'+
1172 '<a class="landing-livecard-link" href="/'+esc(DEMO)+'/'+esc(m.repo)+'/pulls/'+m.number+'">'+
1173 '<span class="landing-livecard-num">#'+m.number+'</span> '+
1174 '<span class="landing-livecard-title-text">'+esc(m.title)+'</span></a>'+
1175 '<div class="landing-livecard-meta">AI merged in <span class="landing-livecard-repo">'+esc(m.repo)+'</span> '+
1176 '<span class="landing-livecard-rel" data-rel="'+esc(m.mergedAt)+'">'+esc(rel(m.mergedAt))+'</span></div></li>';
1177 }).join('');
1178 diffMount(ul,html,ids);
1179 }).catch(function(){});
1180 }
1181 function pollReviews(){
1182 return fetch('/api/v2/demo/reviews',{credentials:'omit'}).then(function(r){return r.json();}).then(function(d){
1183 var ul=document.querySelector('[data-livecard="reviews"]');if(!ul) return;
1184 var bigEl=document.querySelector('[data-livecard-count="reviews"]');
1185 var n=(d&&typeof d.count==='number')?d.count:0;
1186 if(bigEl) tickNumber(bigEl,n);
1187 var items=(d&&d.items)||[];
1188 if(items.length===0){ul.innerHTML='<li class="landing-livecard-empty">No AI reviews in the last 24h.</li>';return;}
1189 var ids=[];
1190 var html=items.slice(0,3).map(function(r){
1191 var id='reviews|'+r.repo+'|'+r.prNumber;ids.push(id);
1192 return '<li class="landing-livecard-row" data-row-id="'+esc(id)+'">'+
1193 '<a class="landing-livecard-link" href="/'+esc(DEMO)+'/'+esc(r.repo)+'/pulls/'+r.prNumber+'">'+
1194 '<span class="landing-livecard-num">#'+r.prNumber+'</span> '+
1195 '<span class="landing-livecard-snippet">'+esc(r.commentSnippet)+'</span></a>'+
1196 '<div class="landing-livecard-meta"><span class="landing-livecard-repo">'+esc(r.repo)+'</span></div></li>';
1197 }).join('');
1198 diffMount(ul,html,ids);
1199 }).catch(function(){});
1200 }
1201 function pollFeed(){
1202 return fetch('/api/v2/demo/activity',{credentials:'omit'}).then(function(r){return r.json();}).then(function(d){
1203 var ul=document.querySelector('[data-livecard="feed"]');if(!ul) return;
1204 var entries=(d&&d.entries)||[];
1205 if(entries.length===0){ul.innerHTML='<li class="landing-livecard-empty">Quiet right now — check back in a minute.</li>';return;}
1206 var ids=[];
1207 var html=entries.slice(0,10).map(function(e){
1208 var path=(e.ref&&e.ref.type==='pr')?'pulls':'issues';
1209 var num=(e.ref&&e.ref.number)||0;
1210 var label=e.kind==='auto_merge.merged'?'auto-merged':(e.kind==='ai_build.dispatched'?'AI-build queued':'AI review posted');
1211 var kindCls=String(e.kind||'').replace(/\\./g,'-');
1212 var id=e.kind+'|'+e.repo+'|'+(e.ref&&e.ref.type)+'|'+num+'|'+e.at;ids.push(id);
1213 return '<li class="landing-livecard-feedrow" data-row-id="'+esc(id)+'">'+
1214 '<span class="landing-livecard-kind landing-livecard-kind-'+esc(kindCls)+'">'+esc(label)+'</span> '+
1215 '<a class="landing-livecard-link" href="/'+esc(DEMO)+'/'+esc(e.repo)+'/'+path+'/'+num+'">'+esc(e.repo)+' #'+num+'</a> '+
1216 '<span class="landing-livecard-rel" data-rel="'+esc(e.at)+'">'+esc(rel(e.at))+'</span></li>';
1217 }).join('');
1218 diffMount(ul,html,ids);
1219 }).catch(function(){});
1220 }
1221 function refreshRel(){
1222 var spans=document.querySelectorAll('.landing-livecard-rel[data-rel]');
1223 for(var i=0;i<spans.length;i++){
1224 spans[i].textContent=rel(spans[i].getAttribute('data-rel'));
1225 }
1226 }
1227 function tickAll(){pollQueued();pollMerges();pollReviews();pollFeed();}
1228 // Initial counter tick (count-up from 0) on first paint.
1229 var bigEl0=document.querySelector('[data-livecard-count="reviews"]');
1230 if(bigEl0){
1231 var target=parseInt(bigEl0.getAttribute('data-tick-target')||'0',10)||0;
1232 bigEl0.textContent='0';
1233 tickNumber(bigEl0,target);
1234 }
1235 refreshRel();
1236 setInterval(tickAll,INTERVAL);
1237 setInterval(refreshRel,INTERVAL);
1238 document.addEventListener('visibilitychange',function(){
1239 if(document.visibilityState==='visible'){tickAll();refreshRel();}
1240 });
1241}catch(_){}})();
1242`.trim();
1243
4c47454Claude1244// Backwards-compatible default — web.tsx imports `LandingPage`.
1245export const LandingPage: FC<LandingPageProps> = (props) => (
1246 <LandingHero {...props} />
2b821b7Claude1247);
1248
4c47454Claude1249export default LandingPage;
1250
2b821b7Claude1251const landingCss = `
958d26aClaude1252 /* ============================================================ */
1253 /* Landing — Editorial-Technical 2026.05 */
1254 /* ============================================================ */
4c47454Claude1255 .landing-root {
1256 position: relative;
958d26aClaude1257 max-width: 1180px;
4c47454Claude1258 margin: 0 auto;
1259 padding: 0 16px;
958d26aClaude1260 }
1261 .landing-root > section { position: relative; }
1262
1263 /* ---------- Hero ---------- */
1264 .landing-hero {
1265 position: relative;
c475ee6Claude1266 padding: var(--s-16) 0 var(--s-20);
958d26aClaude1267 text-align: center;
4c47454Claude1268 overflow: hidden;
1269 }
c475ee6Claude1270 .landing-hero-blob-1 {
1271 animation: hero-blob-drift-1 18s var(--ease, ease) infinite alternate;
1272 }
1273 .landing-hero-blob-2 {
1274 animation: hero-blob-drift-2 22s var(--ease, ease) infinite alternate;
1275 }
1276 @keyframes hero-blob-drift-1 {
1277 0% { transform: translate(0, 0) scale(1); opacity: 0.55; }
1278 100% { transform: translate(8%, 6%) scale(1.18); opacity: 0.75; }
1279 }
1280 @keyframes hero-blob-drift-2 {
1281 0% { transform: translate(0, 0) scale(1); opacity: 0.40; }
1282 100% { transform: translate(-10%, -4%) scale(1.25); opacity: 0.60; }
1283 }
1284
1285 /* ---------- Hero product visual: live AI PR review card ---------- */
1286 .landing-hero-visual {
1287 position: relative;
1288 max-width: 760px;
1289 margin: var(--s-12) auto 0;
1290 padding: 0 16px;
1291 perspective: 1400px;
1292 z-index: 2;
1293 opacity: 0;
1294 animation: hero-visual-in 700ms var(--ease-out-expo, cubic-bezier(0.19, 1, 0.22, 1)) 400ms forwards;
1295 }
1296 @keyframes hero-visual-in {
1297 from { opacity: 0; transform: translateY(20px); }
1298 to { opacity: 1; transform: translateY(0); }
1299 }
1300 .hero-pr-card {
1301 position: relative;
1302 background: linear-gradient(180deg, rgba(15,17,26,0.96) 0%, rgba(8,9,15,0.96) 100%);
1303 border: 1px solid var(--border-strong);
1304 border-radius: var(--r-xl);
1305 overflow: hidden;
1306 text-align: left;
1307 box-shadow:
1308 0 30px 80px -20px rgba(0,0,0,0.65),
1309 0 0 0 1px rgba(140,109,255,0.18),
1310 0 0 60px -10px rgba(140,109,255,0.30);
1311 transform: rotateX(2deg) rotateY(-2deg);
1312 transition: transform 600ms var(--ease, ease);
1313 backdrop-filter: blur(12px);
1314 -webkit-backdrop-filter: blur(12px);
1315 }
1316 .landing-hero-visual:hover .hero-pr-card {
1317 transform: rotateX(0deg) rotateY(0deg);
1318 }
1319 .hero-pr-card::before {
1320 content: '';
1321 position: absolute;
1322 inset: 0;
1323 background: linear-gradient(135deg, rgba(140,109,255,0.10), transparent 35%, transparent 65%, rgba(54,197,214,0.08));
1324 pointer-events: none;
1325 }
1326 .hero-pr-header {
1327 display: flex;
1328 align-items: center;
1329 gap: 12px;
1330 padding: 14px 18px;
1331 border-bottom: 1px solid rgba(255,255,255,0.06);
1332 background: rgba(255,255,255,0.025);
1333 font-size: 13px;
1334 }
1335 .hero-pr-dot {
1336 width: 10px; height: 10px;
1337 border-radius: 50%;
1338 background: var(--green);
1339 box-shadow: 0 0 10px rgba(52,211,153,0.6);
1340 flex-shrink: 0;
1341 }
1342 .hero-pr-title {
1343 color: var(--text-strong);
1344 font-weight: 600;
1345 flex: 1;
1346 overflow: hidden;
1347 text-overflow: ellipsis;
1348 white-space: nowrap;
1349 }
1350 .hero-pr-num {
1351 color: var(--text-faint);
1352 font-family: var(--font-mono);
1353 font-weight: 500;
1354 margin-right: 8px;
1355 }
1356 .hero-pr-status {
1357 display: inline-flex;
1358 align-items: center;
1359 gap: 6px;
1360 padding: 3px 10px;
1361 border-radius: var(--r-full);
1362 background: var(--accent-gradient-faint);
1363 border: 1px solid rgba(140,109,255,0.30);
1364 color: var(--accent);
1365 font-family: var(--font-mono);
1366 font-size: 11px;
1367 letter-spacing: 0.04em;
1368 flex-shrink: 0;
1369 }
1370 .hero-pr-status-pulse {
1371 width: 6px; height: 6px;
1372 border-radius: 50%;
1373 background: var(--accent);
1374 box-shadow: 0 0 0 0 rgba(140,109,255,0.6);
1375 animation: hero-pulse 1.6s ease-out infinite;
1376 }
1377 @keyframes hero-pulse {
1378 0% { box-shadow: 0 0 0 0 rgba(140,109,255,0.55); }
1379 70% { box-shadow: 0 0 0 8px rgba(140,109,255,0); }
1380 100% { box-shadow: 0 0 0 0 rgba(140,109,255,0); }
1381 }
1382
1383 .hero-pr-body {
1384 padding: 0;
1385 }
1386 .hero-pr-file {
1387 display: flex;
1388 align-items: center;
1389 gap: 10px;
1390 padding: 10px 18px;
1391 border-bottom: 1px solid rgba(255,255,255,0.05);
1392 font-family: var(--font-mono);
1393 font-size: 12px;
1394 background: rgba(255,255,255,0.012);
1395 }
1396 .hero-pr-file-icon { color: var(--accent-2); }
1397 .hero-pr-file-name { color: var(--text); flex: 1; }
1398 .hero-pr-file-stats { display: inline-flex; gap: 8px; }
1399 .hero-pr-add { color: var(--green); font-weight: 600; }
1400 .hero-pr-del { color: var(--red); font-weight: 600; }
1401
1402 .hero-pr-diff {
1403 padding: 12px 18px;
1404 font-family: var(--font-mono);
1405 font-feature-settings: var(--mono-feat, 'calt');
1406 font-size: 12.5px;
1407 line-height: 1.7;
1408 color: rgba(237,237,242,0.85);
1409 overflow-x: auto;
1410 }
1411 .hero-pr-hunk {
1412 color: rgba(140,109,255,0.85);
1413 background: rgba(140,109,255,0.06);
1414 padding: 2px 8px;
1415 margin: 0 -8px 4px;
1416 border-radius: 4px;
1417 }
1418 .hero-pr-line-add {
1419 background: rgba(52,211,153,0.08);
1420 color: rgba(167,243,208,0.95);
1421 padding: 0 8px;
1422 margin: 0 -8px;
1423 border-left: 2px solid var(--green);
1424 padding-left: 8px;
1425 }
1426
1427 .hero-pr-comment {
1428 margin: 14px 18px;
1429 padding: 14px 16px;
1430 background: linear-gradient(135deg, rgba(140,109,255,0.08), rgba(54,197,214,0.05));
1431 border: 1px solid rgba(140,109,255,0.25);
1432 border-radius: var(--r-md);
1433 }
1434 .hero-pr-bot-row {
1435 display: flex;
1436 align-items: center;
1437 gap: 8px;
1438 margin-bottom: 8px;
1439 font-size: 12px;
1440 }
1441 .hero-pr-bot-avatar {
1442 width: 22px; height: 22px;
1443 display: inline-flex;
1444 align-items: center;
1445 justify-content: center;
1446 border-radius: 50%;
1447 background: var(--accent-gradient);
1448 font-size: 11px;
1449 box-shadow: 0 0 12px rgba(140,109,255,0.40);
1450 }
1451 .hero-pr-bot-name {
1452 color: var(--text-strong);
1453 font-weight: 600;
1454 }
1455 .hero-pr-bot-meta {
1456 color: var(--text-faint);
1457 font-family: var(--font-mono);
1458 }
1459 .hero-pr-bot-text {
1460 color: var(--text);
1461 font-size: 13px;
1462 line-height: 1.55;
1463 margin: 0;
1464 }
1465 .hero-pr-bot-text code {
1466 background: rgba(255,255,255,0.06);
1467 border: 1px solid rgba(255,255,255,0.10);
1468 padding: 1px 6px;
1469 border-radius: 4px;
1470 font-size: 11.5px;
1471 color: var(--accent);
1472 }
1473 .hero-pr-bot-link { color: var(--accent-2); text-decoration: underline; text-decoration-style: dotted; }
1474
1475 .hero-pr-gates {
1476 display: flex;
1477 flex-wrap: wrap;
1478 gap: 6px;
1479 padding: 12px 18px 16px;
1480 border-top: 1px solid rgba(255,255,255,0.06);
1481 background: rgba(255,255,255,0.012);
1482 }
1483 .hero-pr-gate {
1484 display: inline-flex;
1485 align-items: center;
1486 gap: 6px;
1487 padding: 4px 10px;
1488 border-radius: var(--r-full);
1489 font-family: var(--font-mono);
1490 font-size: 11px;
1491 border: 1px solid;
1492 }
1493 .hero-pr-gate-pass {
1494 color: var(--green);
1495 background: rgba(52,211,153,0.08);
1496 border-color: rgba(52,211,153,0.30);
1497 }
1498 .hero-pr-gate-running {
1499 color: var(--accent);
1500 background: var(--accent-gradient-faint);
1501 border-color: rgba(140,109,255,0.40);
1502 }
1503 .hero-pr-gate-spin {
1504 width: 9px; height: 9px;
1505 border: 1.5px solid rgba(140,109,255,0.30);
1506 border-top-color: var(--accent);
1507 border-radius: 50%;
1508 animation: hero-spin 800ms linear infinite;
1509 }
1510 @keyframes hero-spin {
1511 to { transform: rotate(360deg); }
1512 }
1513
1514 /* Floating accent badges around the card */
1515 .hero-float {
1516 position: absolute;
1517 display: inline-flex;
1518 align-items: center;
1519 gap: 6px;
1520 padding: 6px 12px;
1521 background: rgba(15,17,26,0.92);
1522 border: 1px solid rgba(140,109,255,0.35);
1523 border-radius: var(--r-full);
1524 font-family: var(--font-mono);
1525 font-size: 11px;
1526 color: var(--text);
1527 box-shadow: 0 12px 24px -8px rgba(0,0,0,0.5), 0 0 18px -4px rgba(140,109,255,0.30);
1528 backdrop-filter: blur(8px);
1529 -webkit-backdrop-filter: blur(8px);
1530 }
1531 .hero-float-icon { color: var(--accent); }
1532 .hero-float-1 {
1533 top: -14px;
1534 left: -8px;
1535 animation: hero-float-bob-1 5s var(--ease, ease) infinite alternate;
1536 }
1537 .hero-float-2 {
1538 bottom: -14px;
1539 right: -8px;
1540 animation: hero-float-bob-2 6s var(--ease, ease) infinite alternate;
1541 }
1542 @keyframes hero-float-bob-1 {
1543 from { transform: translate(0, 0); }
1544 to { transform: translate(-8px, -10px); }
1545 }
1546 @keyframes hero-float-bob-2 {
1547 from { transform: translate(0, 0); }
1548 to { transform: translate(8px, 8px); }
1549 }
1550
1551 @media (max-width: 720px) {
1552 .landing-hero-visual { padding: 0 8px; }
1553 .hero-pr-card { transform: none; }
1554 .hero-pr-title { font-size: 12px; }
1555 .hero-pr-diff { font-size: 11px; line-height: 1.6; }
1556 .hero-float { display: none; }
1557 }
958d26aClaude1558 .landing-hero-bg {
1559 position: absolute;
1560 inset: -10% -20%;
1561 pointer-events: none;
1562 z-index: 0;
4c47454Claude1563 }
958d26aClaude1564 .landing-hero-blob {
1565 position: absolute;
1566 border-radius: 50%;
1567 filter: blur(80px);
1568 opacity: 0.55;
3a5755eClaude1569 will-change: transform;
958d26aClaude1570 }
1571 .landing-hero-blob-1 {
1572 top: -10%;
1573 left: 30%;
1574 width: 480px;
1575 height: 480px;
3a5755eClaude1576 background: radial-gradient(circle, rgba(140,109,255,0.65), transparent 65%);
1577 /* 2026 polish — slow drift gives the hero "this is a live product"
1578 feel without being distracting. 24s loop, eased, contained motion. */
1579 animation: landingBlobDrift1 24s ease-in-out infinite;
958d26aClaude1580 }
1581 .landing-hero-blob-2 {
1582 top: 10%;
1583 left: 50%;
1584 width: 380px;
1585 height: 380px;
3a5755eClaude1586 background: radial-gradient(circle, rgba(54,197,214,0.50), transparent 65%);
1587 animation: landingBlobDrift2 28s ease-in-out infinite;
958d26aClaude1588 }
dc26881CC LABS App1589 /* U1 — subtle, low-opacity accent-gradient orb behind the headline.
1590 Sits dead-centre, very blurred, so the hero reads as a real product
3a5755eClaude1591 surface rather than flat-bg + text. 2026 polish — gentle breathing
1592 pulse to give the surface a soft heartbeat. */
dc26881CC LABS App1593 .landing-hero-orb {
1594 top: 18%;
1595 left: 50%;
1596 transform: translateX(-50%);
1597 width: 720px;
1598 height: 720px;
3a5755eClaude1599 background: radial-gradient(circle, rgba(140,109,255,0.28), rgba(54,197,214,0.16) 45%, transparent 70%);
dc26881CC LABS App1600 filter: blur(120px);
3a5755eClaude1601 opacity: 0.6;
dc26881CC LABS App1602 z-index: 0;
3a5755eClaude1603 animation: landingOrbBreath 12s ease-in-out infinite;
dc26881CC LABS App1604 }
1605 :root[data-theme='light'] .landing-hero-orb {
1606 opacity: 0.32;
1607 }
3a5755eClaude1608 @keyframes landingBlobDrift1 {
1609 0%, 100% { transform: translate(0, 0) scale(1); }
1610 33% { transform: translate(40px, -30px) scale(1.08); }
1611 66% { transform: translate(-30px, 25px) scale(0.95); }
1612 }
1613 @keyframes landingBlobDrift2 {
1614 0%, 100% { transform: translate(0, 0) scale(1); }
1615 50% { transform: translate(-50px, 35px) scale(1.12); }
1616 }
1617 @keyframes landingOrbBreath {
1618 0%, 100% { opacity: 0.55; transform: translateX(-50%) scale(1); }
1619 50% { opacity: 0.72; transform: translateX(-50%) scale(1.06); }
1620 }
dc26881CC LABS App1621 @media (prefers-reduced-motion: reduce) {
1622 .landing-hero-blob-1,
1623 .landing-hero-blob-2,
1624 .landing-hero-orb {
1625 animation: none;
1626 }
1627 }
958d26aClaude1628 .landing-hero-grid {
4c47454Claude1629 position: absolute;
1630 inset: 0;
958d26aClaude1631 background-image:
1632 linear-gradient(to right, rgba(255,255,255,0.04) 1px, transparent 1px),
1633 linear-gradient(to bottom, rgba(255,255,255,0.04) 1px, transparent 1px);
1634 background-size: 60px 60px;
1635 mask-image: radial-gradient(ellipse 50% 50% at 50% 30%, #000 0%, transparent 75%);
1636 -webkit-mask-image: radial-gradient(ellipse 50% 50% at 50% 30%, #000 0%, transparent 75%);
1637 }
1638 :root[data-theme='light'] .landing-hero-grid {
1639 background-image:
1640 linear-gradient(to right, rgba(15,16,28,0.06) 1px, transparent 1px),
1641 linear-gradient(to bottom, rgba(15,16,28,0.06) 1px, transparent 1px);
4c47454Claude1642 }
1643
958d26aClaude1644 .landing-hero-inner {
1645 position: relative;
1646 z-index: 1;
1647 max-width: 960px;
2b821b7Claude1648 margin: 0 auto;
1649 }
dc26881CC LABS App1650 /* U1 — every block below the headline obeys a single rhythm. */
958d26aClaude1651 .landing-hero-eyebrow {
dc26881CC LABS App1652 margin: 0 auto var(--space-6);
958d26aClaude1653 color: var(--accent);
1654 }
1655 .landing-hero-eyebrow::before { display: none; }
1656 .landing-hero-pulse {
1657 width: 7px;
1658 height: 7px;
1659 border-radius: 50%;
1660 background: var(--accent);
1661 box-shadow: 0 0 0 0 rgba(140,109,255,0.6);
1662 animation: pulse 1.8s ease-out infinite;
1663 flex-shrink: 0;
1664 }
1665 @keyframes pulse {
1666 0% { box-shadow: 0 0 0 0 rgba(140,109,255,0.55); }
1667 70% { box-shadow: 0 0 0 10px rgba(140,109,255,0); }
1668 100% { box-shadow: 0 0 0 0 rgba(140,109,255,0); }
1669 }
1670
2b821b7Claude1671 .landing-hero-title {
3a5755eClaude1672 /* 2026 polish — bigger, bolder, tighter. Inter Tight at 800 weight
1673 with -0.03em tracking is the modern "AI-startup hero" look that
1674 Vercel/Linear/Cursor all use. clamp() scales gracefully on mobile. */
1675 font-size: clamp(40px, 7vw, 84px);
1676 line-height: 1.02;
1677 letter-spacing: -0.032em;
1678 font-weight: 800;
1679 font-family: var(--font-display);
dc26881CC LABS App1680 margin: 0 0 var(--space-6);
958d26aClaude1681 color: var(--text-strong);
2b821b7Claude1682 }
c963db5Claude1683 .landing-hero-title .gradient-text {
3a5755eClaude1684 /* Richer gradient with a third stop for more depth. Drop-shadow
1685 gives the impression of subtle glow without overpowering the type. */
1686 background-image: linear-gradient(135deg, #c2a8ff 0%, #8c6dff 40%, #5d3dff 70%, #36c5d6 100%);
c963db5Claude1687 -webkit-background-clip: text;
1688 background-clip: text;
1689 -webkit-text-fill-color: transparent;
1690 color: transparent;
3a5755eClaude1691 filter: drop-shadow(0 4px 32px rgba(140, 109, 255, 0.18));
c963db5Claude1692 }
958d26aClaude1693
2b821b7Claude1694 .landing-hero-sub {
3a5755eClaude1695 font-size: clamp(16px, 1.8vw, 22px);
2b821b7Claude1696 color: var(--text-muted);
958d26aClaude1697 max-width: 680px;
dc26881CC LABS App1698 margin: 0 auto var(--space-6);
3a5755eClaude1699 line-height: 1.5;
1700 letter-spacing: -0.008em;
1701 font-weight: 400;
2b821b7Claude1702 }
4c47454Claude1703
2b821b7Claude1704 .landing-hero-ctas {
1705 display: flex;
dc26881CC LABS App1706 gap: var(--space-3);
2b821b7Claude1707 justify-content: center;
1708 flex-wrap: wrap;
dc26881CC LABS App1709 margin-top: 0;
1710 margin-bottom: var(--space-4);
2b821b7Claude1711 }
958d26aClaude1712 .landing-cta-arrow {
1713 transition: transform var(--t-base) var(--ease-spring);
1714 display: inline-block;
2b821b7Claude1715 }
cd4f63bTest User1716
dc26881CC LABS App1717 /* U1 — tertiary text-link row.
1718 Sits directly under the 2-button primary CTA row. Smaller, muted,
1719 so it reads as "by the way" rather than competing for the eye. */
1720 .landing-hero-tertiary {
1721 margin-top: 0;
1722 margin-bottom: var(--space-6);
cd4f63bTest User1723 text-align: center;
dc26881CC LABS App1724 display: inline-flex;
1725 flex-wrap: wrap;
1726 gap: var(--space-2) var(--space-3);
1727 justify-content: center;
1728 align-items: baseline;
1729 width: 100%;
cd4f63bTest User1730 font-size: 13px;
1731 color: var(--text-muted);
dc26881CC LABS App1732 }
1733 .landing-hero-tertiary-link {
1734 color: var(--text-muted);
cd4f63bTest User1735 text-decoration: none;
1736 border-bottom: 1px dashed transparent;
1737 padding-bottom: 1px;
dc26881CC LABS App1738 transition: color var(--t-fast) var(--ease),
1739 border-color var(--t-fast) var(--ease);
cd4f63bTest User1740 }
dc26881CC LABS App1741 .landing-hero-tertiary-link:hover {
1742 color: var(--text-strong);
cd4f63bTest User1743 border-bottom-color: var(--text-muted);
1744 }
dc26881CC LABS App1745 .landing-hero-tertiary-sep {
1746 color: var(--text-faint);
1747 user-select: none;
1748 }
958d26aClaude1749 .btn:hover .landing-cta-arrow,
1750 .landing-cta-primary:hover .landing-cta-arrow {
1751 transform: translateX(4px);
8e9f1d9Claude1752 }
2b821b7Claude1753
93fe97eClaude1754 /* BLOCK Q1 — flagship "Add to Claude Desktop" CTA.
1755 Gradient-bordered + accent text so it reads as a peer of the primary
bc7654bCC LABS App1756 Sign-up CTA, not a third secondary. Theme-aware: inner fill uses
1757 --bg-elevated so it's white on light and dark on dark, never the
1758 jarring near-black on white we shipped first time. Subtle elevation
1759 on hover; static when the visitor opts out of motion. */
93fe97eClaude1760 .landing-cta-dxt {
1761 position: relative;
bc7654bCC LABS App1762 background: var(--bg-elevated);
1763 color: var(--text-strong);
93fe97eClaude1764 border: 1px solid transparent;
1765 background-image:
bc7654bCC LABS App1766 linear-gradient(var(--bg-elevated), var(--bg-elevated)),
93fe97eClaude1767 linear-gradient(90deg, #8c6dff 0%, #36c5d6 100%);
1768 background-origin: border-box;
1769 background-clip: padding-box, border-box;
1770 transition: transform var(--t-base, 180ms) var(--ease-spring, ease),
1771 box-shadow var(--t-base, 180ms) var(--ease-spring, ease);
1772 }
1773 .landing-cta-dxt:hover {
1774 transform: translateY(-2px);
1775 box-shadow: 0 8px 24px -8px rgba(140, 109, 255, 0.45);
1776 }
1777 @media (prefers-reduced-motion: reduce) {
1778 .landing-cta-dxt,
1779 .landing-cta-dxt:hover {
1780 transform: none;
1781 transition: none;
1782 }
1783 }
1784
dc26881CC LABS App1785 /* L8 — free-tier reassurance link beneath the CTA row.
1786 U1 — rhythm snapped to var(--space-6). */
5f2e749Claude1787 .landing-hero-freenote {
dc26881CC LABS App1788 margin: 0 auto var(--space-6);
5f2e749Claude1789 font-size: var(--t-sm);
1790 color: var(--text-muted);
1791 text-align: center;
1792 }
1793 .landing-hero-freenote-link {
1794 color: var(--accent);
1795 text-decoration: none;
1796 font-weight: 500;
1797 border-bottom: 1px dotted rgba(140,109,255,0.4);
1798 transition: color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
1799 }
1800 .landing-hero-freenote-link:hover {
1801 color: var(--text-strong);
1802 border-bottom-color: var(--accent);
1803 }
1804
4c47454Claude1805 .landing-hero-caption {
dc26881CC LABS App1806 margin: 0 auto var(--space-6);
958d26aClaude1807 font-size: var(--t-sm);
4c47454Claude1808 color: var(--text-muted);
958d26aClaude1809 display: flex;
1810 flex-wrap: wrap;
1811 align-items: center;
1812 justify-content: center;
dc26881CC LABS App1813 gap: var(--space-3);
2b821b7Claude1814 }
958d26aClaude1815 .landing-hero-cmd {
1816 display: inline-flex;
1817 align-items: center;
1818 gap: 4px;
1819 padding: 6px 12px;
1820 background: var(--bg-elevated);
4c47454Claude1821 border: 1px solid var(--border);
958d26aClaude1822 border-radius: var(--r-full);
1823 box-shadow: var(--elev-1);
1824 }
1825 .landing-hero-cmd .kbd {
1826 border: 0;
1827 background: transparent;
1828 padding: 0 4px;
1829 color: var(--text-muted);
1830 font-size: 12px;
1831 }
1832 .landing-hero-cmd .kbd:nth-last-of-type(1) { color: var(--accent); }
1833 .landing-hero-arrow {
1834 color: var(--text-faint);
1835 font-size: 13px;
1836 margin: 0 2px;
2b821b7Claude1837 }
4c47454Claude1838
1839 .landing-stats {
dc26881CC LABS App1840 margin: 0 auto;
958d26aClaude1841 font-family: var(--font-mono);
1842 font-size: 12px;
1843 color: var(--text-muted);
1844 display: flex;
1845 align-items: center;
1846 justify-content: center;
dc26881CC LABS App1847 gap: var(--space-2);
958d26aClaude1848 flex-wrap: wrap;
1849 letter-spacing: 0.02em;
1850 }
1851 .landing-stats strong {
1852 color: var(--text-strong);
1853 font-weight: 600;
1854 font-feature-settings: 'tnum';
1855 }
1856 .landing-stats-sep { opacity: 0.4; }
1857
c963db5Claude1858 /* ---------- Capability grid (crontech-style uppercase tracked) ---------- */
1859 .landing-caps {
1860 margin: var(--s-12) auto var(--s-16);
1861 max-width: 1080px;
1862 padding: var(--s-7) var(--s-4);
958d26aClaude1863 border-top: 1px solid var(--border-subtle);
1864 border-bottom: 1px solid var(--border-subtle);
c963db5Claude1865 }
1866 .landing-caps-grid {
1867 display: grid;
1868 grid-template-columns: repeat(4, 1fr);
1869 gap: 24px 16px;
958d26aClaude1870 text-align: center;
1871 }
c963db5Claude1872 .landing-cap {
958d26aClaude1873 font-family: var(--font-mono);
1874 font-size: 11px;
c963db5Claude1875 font-weight: 600;
958d26aClaude1876 text-transform: uppercase;
c963db5Claude1877 letter-spacing: 0.16em;
1878 color: var(--text-muted);
1879 transition: color var(--t-fast) var(--ease);
958d26aClaude1880 }
c963db5Claude1881 .landing-cap:hover { color: var(--text-strong); }
1882 @media (max-width: 800px) {
1883 .landing-caps-grid { grid-template-columns: repeat(2, 1fr); gap: 18px 12px; }
1884 }
1885 @media (max-width: 480px) {
1886 .landing-caps-grid { grid-template-columns: 1fr; }
1887 }
1888
1889 /* ---------- Big stat row (crontech-style hero closer) ---------- */
1890 .landing-bigstats {
1891 margin: var(--s-10) auto var(--s-20);
1892 max-width: 1180px;
1893 padding: 0 var(--s-4);
1894 }
1895 .landing-bigstats-grid {
1896 display: grid;
1897 grid-template-columns: repeat(4, 1fr);
1898 gap: 32px;
1899 text-align: left;
958d26aClaude1900 }
c963db5Claude1901 .landing-bigstat {
1902 padding: var(--s-2) 0;
1903 }
1904 .landing-bigstat-num {
958d26aClaude1905 font-family: var(--font-display);
c963db5Claude1906 font-size: clamp(28px, 3.5vw, 44px);
1907 line-height: 1.05;
1908 letter-spacing: -0.03em;
1909 font-weight: 700;
1910 color: var(--text-strong);
1911 margin-bottom: var(--s-2);
1912 }
1913 .landing-bigstat-label {
1914 font-family: var(--font-mono);
1915 font-size: 11px;
958d26aClaude1916 font-weight: 500;
c963db5Claude1917 text-transform: uppercase;
1918 letter-spacing: 0.14em;
1919 color: var(--text-faint);
2b821b7Claude1920 }
c963db5Claude1921 @media (max-width: 800px) {
1922 .landing-bigstats-grid { grid-template-columns: repeat(2, 1fr); gap: 28px 16px; }
1923 }
1924 @media (max-width: 480px) {
1925 .landing-bigstats-grid { grid-template-columns: 1fr; gap: 24px; }
958d26aClaude1926 }
1927
1928 /* ---------- Section base ---------- */
1929 .landing-section { margin: var(--s-20) auto; }
2b821b7Claude1930
4c47454Claude1931 /* ---------- Feature grid ---------- */
1932 .landing-features {
2b821b7Claude1933 display: grid;
1934 grid-template-columns: repeat(3, 1fr);
958d26aClaude1935 gap: 16px;
2b821b7Claude1936 }
1937 .landing-feature {
958d26aClaude1938 background: var(--bg-elevated);
2b821b7Claude1939 border: 1px solid var(--border);
958d26aClaude1940 border-radius: var(--r-lg);
1941 padding: var(--s-7);
1942 position: relative;
1943 overflow: hidden;
1944 isolation: isolate;
1945 transition:
1946 transform var(--t-base) var(--ease-out-quart),
1947 border-color var(--t-base) var(--ease),
1948 box-shadow var(--t-base) var(--ease);
1949 }
1950 .landing-feature::before {
1951 content: '';
1952 position: absolute;
1953 inset: 0;
1954 background: radial-gradient(120% 100% at 0% 0%, rgba(140,109,255,0.08), transparent 55%);
1955 opacity: 0;
1956 transition: opacity var(--t-base) var(--ease);
1957 z-index: -1;
4c47454Claude1958 }
1959 .landing-feature:hover {
958d26aClaude1960 transform: translateY(-3px);
1961 border-color: var(--border-strong);
1962 box-shadow: var(--elev-2);
2b821b7Claude1963 }
958d26aClaude1964 .landing-feature:hover::before { opacity: 1; }
2b821b7Claude1965 .landing-feature-icon {
4c47454Claude1966 display: inline-flex;
1967 align-items: center;
1968 justify-content: center;
958d26aClaude1969 width: 40px;
1970 height: 40px;
1971 border-radius: var(--r);
1972 background: var(--accent-gradient-soft);
1973 color: var(--accent);
1974 margin-bottom: var(--s-4);
1975 border: 1px solid rgba(140,109,255,0.20);
2b821b7Claude1976 }
1977 .landing-feature-title {
958d26aClaude1978 font-family: var(--font-display);
1979 font-size: 19px;
1980 font-weight: 600;
1981 letter-spacing: -0.018em;
1982 margin: 0 0 var(--s-2);
1983 color: var(--text-strong);
2b821b7Claude1984 }
1985 .landing-feature-desc {
958d26aClaude1986 font-size: var(--t-sm);
1987 color: var(--text-muted);
1988 line-height: 1.6;
1989 margin: 0;
1990 }
1991
1992 /* ---------- Walkthrough ---------- */
1993 .landing-walk-grid {
1994 display: grid;
1995 grid-template-columns: repeat(4, 1fr);
1996 gap: 16px;
1997 counter-reset: walk;
1998 }
1999 .landing-walk-step {
2000 position: relative;
2001 padding: var(--s-7) var(--s-6) var(--s-6);
2002 background: var(--bg-elevated);
2003 border: 1px solid var(--border);
2004 border-radius: var(--r-lg);
2005 }
2006 .landing-walk-step::after {
2007 content: '';
2008 position: absolute;
2009 top: 50%;
2010 right: -12px;
2011 width: 12px;
2012 height: 1px;
2013 background: var(--border-strong);
2014 }
2015 .landing-walk-step:last-child::after { display: none; }
2016 .landing-walk-num {
2017 display: inline-block;
2018 font-family: var(--font-mono);
2019 font-size: 11px;
2020 color: var(--accent);
2021 background: var(--accent-gradient-faint);
2022 border: 1px solid rgba(140,109,255,0.30);
2023 padding: 3px 8px;
2024 border-radius: var(--r-full);
2025 letter-spacing: 0.06em;
2026 margin-bottom: var(--s-3);
2027 }
2028 .landing-walk-title {
2029 font-family: var(--font-display);
2030 font-size: 22px;
2031 font-weight: 600;
2032 letter-spacing: -0.022em;
2033 margin: 0 0 var(--s-2);
2034 color: var(--text-strong);
2035 }
2036 .landing-walk-desc {
2037 font-size: var(--t-sm);
2b821b7Claude2038 color: var(--text-muted);
2039 line-height: 1.55;
2040 margin: 0;
2041 }
2042
958d26aClaude2043 /* ---------- Terminal ---------- */
2044 .landing-terminal-section { margin-top: var(--s-16); }
4c47454Claude2045 .landing-terminal-wrap {
2046 display: flex;
2b821b7Claude2047 justify-content: center;
2048 }
4c47454Claude2049 .landing-terminal {
2050 width: 100%;
958d26aClaude2051 max-width: 820px;
2052 background: linear-gradient(180deg, #0a0b12 0%, #06070c 100%);
2053 border: 1px solid var(--border-strong);
2054 border-radius: var(--r-lg);
2055 overflow: hidden;
2056 box-shadow: var(--elev-3), 0 0 60px -10px rgba(140,109,255,0.18);
4c47454Claude2057 text-align: left;
2b821b7Claude2058 }
958d26aClaude2059 :root[data-theme='light'] .landing-terminal {
2060 background: linear-gradient(180deg, #0f111a 0%, #06070c 100%);
2061 }
2062 .landing-terminal-chrome {
2063 display: flex;
2064 align-items: center;
2065 gap: 7px;
2066 padding: 11px 14px;
2067 background: rgba(255,255,255,0.025);
2068 border-bottom: 1px solid rgba(255,255,255,0.06);
2069 position: relative;
2070 }
2071 .landing-terminal-dot {
2072 width: 11px;
2073 height: 11px;
2074 border-radius: 50%;
2075 flex-shrink: 0;
2076 }
2077 .landing-terminal-dot-r { background: #ff5f57; }
2078 .landing-terminal-dot-y { background: #febc2e; }
2079 .landing-terminal-dot-g { background: #28c840; }
2080 .landing-terminal-title {
2081 position: absolute;
2082 left: 50%;
2083 transform: translateX(-50%);
2084 font-family: var(--font-mono);
2085 font-size: 11px;
2086 color: rgba(237,237,242,0.55);
2087 letter-spacing: 0.01em;
2088 }
2089 .landing-terminal-body {
2090 padding: var(--s-6) var(--s-7);
2091 font-family: var(--font-mono);
2092 font-feature-settings: var(--mono-feat);
2093 font-size: 13.5px;
2094 line-height: 1.85;
2095 color: rgba(237,237,242,0.92);
2096 }
4c47454Claude2097 .landing-term-line {
2098 display: flex;
2099 gap: 10px;
2100 white-space: pre-wrap;
2101 word-break: break-all;
2b821b7Claude2102 }
958d26aClaude2103 .landing-term-out { color: rgba(237,237,242,0.7); }
2104 .landing-term-prompt { color: rgba(140,109,255,0.85); user-select: none; flex-shrink: 0; }
2105 .landing-term-meta { color: rgba(237,237,242,0.45); }
2106 .landing-term-ok { color: var(--green); user-select: none; flex-shrink: 0; }
2107 .landing-term-ok-line { color: rgba(237,237,242,0.92); }
2108 .landing-term-cursor { margin-top: 4px; }
2109 .landing-term-blink {
2110 animation: blink 1.05s steps(2) infinite;
2111 color: var(--accent);
2112 }
2113 @keyframes blink { 50% { opacity: 0; } }
2114
2115 /* ---------- Comparison ---------- */
2116 .landing-compare {
2117 max-width: 920px;
2118 margin: 0 auto;
2119 border: 1px solid var(--border);
2120 border-radius: var(--r-lg);
2121 overflow: hidden;
2122 background: var(--bg-elevated);
2123 }
2124 .landing-compare-row {
2125 display: grid;
2126 grid-template-columns: 1fr 180px 180px;
2127 align-items: center;
2128 padding: 14px 20px;
2129 border-bottom: 1px solid var(--border-subtle);
2130 font-size: var(--t-sm);
2131 transition: background var(--t-fast) var(--ease);
2132 }
2133 .landing-compare-row:last-child { border-bottom: none; }
2134 .landing-compare-row:hover { background: var(--bg-hover); }
2135 .landing-compare-feature {
2136 color: var(--text-strong);
2137 font-weight: 500;
2138 }
2139 .landing-compare-them, .landing-compare-us {
2140 text-align: center;
2141 font-family: var(--font-mono);
2142 font-size: 12px;
2143 color: var(--text-muted);
2144 }
2145 .landing-compare-us { color: var(--green); font-weight: 500; }
2146 .landing-compare-hl .landing-compare-us {
2147 color: var(--accent);
2148 font-weight: 600;
2b821b7Claude2149 }
958d26aClaude2150 .landing-compare-hl .landing-compare-feature::after {
2151 content: 'NEW';
2152 margin-left: 8px;
2153 padding: 1px 6px;
2154 border-radius: 4px;
2155 background: var(--accent-gradient-faint);
2156 color: var(--accent);
2157 font-family: var(--font-mono);
2158 font-size: 9px;
2159 letter-spacing: 0.1em;
2160 font-weight: 600;
2161 vertical-align: 1px;
2162 }
2163 @media (max-width: 720px) {
2164 .landing-compare-row { grid-template-columns: 1fr 80px 80px; padding: 12px 14px; }
2165 .landing-compare-hl .landing-compare-feature::after { display: none; }
2166 }
2167
2168 /* ---------- Pricing ---------- */
2169 .landing-pricing {
2170 display: grid;
2171 grid-template-columns: repeat(3, 1fr);
2172 gap: 16px;
2173 max-width: 1080px;
2174 margin: 0 auto;
2175 align-items: stretch;
2176 }
2177 .landing-price-card {
2178 position: relative;
2179 background: var(--bg-elevated);
2180 border: 1px solid var(--border);
2181 border-radius: var(--r-lg);
2182 padding: var(--s-7);
2183 display: flex;
2184 flex-direction: column;
2185 gap: var(--s-4);
2186 transition: border-color var(--t-base) var(--ease), transform var(--t-base) var(--ease);
2187 }
2188 .landing-price-card:hover {
2189 border-color: var(--border-strong);
2190 transform: translateY(-2px);
2191 }
2192 .landing-price-hl {
2193 border-color: rgba(140,109,255,0.35);
2194 box-shadow: var(--elev-2), 0 0 0 1px rgba(140,109,255,0.25);
2195 background:
2196 linear-gradient(180deg, rgba(140,109,255,0.05), transparent 50%),
2197 var(--bg-elevated);
2198 }
2199 .landing-price-hl:hover { border-color: rgba(140,109,255,0.55); }
2200 .landing-price-badge {
2201 position: absolute;
2202 top: -10px;
2203 left: 50%;
2204 transform: translateX(-50%);
2205 padding: 3px 12px;
2206 background: var(--accent-gradient);
2207 color: #fff;
2208 font-family: var(--font-mono);
2209 font-size: 10px;
2210 letter-spacing: 0.1em;
2211 text-transform: uppercase;
2212 font-weight: 600;
2213 border-radius: var(--r-full);
2214 box-shadow: 0 4px 12px -2px rgba(140,109,255,0.4);
2215 }
2216 .landing-price-tier {
2217 font-family: var(--font-mono);
2218 font-size: 11px;
2219 text-transform: uppercase;
2220 letter-spacing: 0.16em;
2221 color: var(--text-muted);
2222 }
2223 .landing-price-amount {
2224 display: flex;
2225 align-items: baseline;
2226 gap: 8px;
2227 }
2228 .landing-price-num {
2229 font-family: var(--font-display);
2230 font-size: 40px;
2231 font-weight: 600;
2232 letter-spacing: -0.03em;
2233 color: var(--text-strong);
2234 }
2235 .landing-price-cad {
2236 font-size: var(--t-sm);
2237 color: var(--text-faint);
2238 }
2239 .landing-price-desc {
2240 font-size: var(--t-sm);
2241 color: var(--text-muted);
2242 line-height: 1.55;
2243 margin: 0;
2244 }
2245 .landing-price-features {
2246 list-style: none;
2247 padding: 0;
2248 margin: 0;
2249 display: flex;
2250 flex-direction: column;
2251 gap: 8px;
2252 font-size: var(--t-sm);
2253 color: var(--text);
2254 }
2255 .landing-price-features li {
2256 display: flex;
2257 align-items: center;
2258 gap: 9px;
2259 }
2260 .landing-price-check {
2261 color: var(--accent);
2262 font-weight: 600;
4c47454Claude2263 flex-shrink: 0;
2b821b7Claude2264 }
958d26aClaude2265 .landing-price-cta { margin-top: auto; }
2266
2267 /* ---------- Closing CTA ---------- */
2268 .landing-cta-section { margin: var(--s-20) auto var(--s-16); }
2269 .landing-cta-card {
2270 position: relative;
2271 text-align: center;
2272 padding: var(--s-16) var(--s-7);
2273 border: 1px solid var(--border-strong);
2274 border-radius: var(--r-2xl);
2275 background: var(--bg-elevated);
2276 overflow: hidden;
2277 isolation: isolate;
2278 }
2279 .landing-cta-bg {
2280 position: absolute;
2281 inset: 0;
2282 z-index: -1;
2283 background:
2284 radial-gradient(60% 100% at 50% 0%, rgba(140,109,255,0.16), transparent 65%),
2285 radial-gradient(40% 80% at 80% 100%, rgba(54,197,214,0.10), transparent 65%);
2286 }
2287 .landing-cta-card::after {
2288 content: '';
2289 position: absolute;
2290 inset: 0;
2291 z-index: -1;
2292 background-image: radial-gradient(rgba(255,255,255,0.04) 1px, transparent 1px);
2293 background-size: 24px 24px;
2294 mask-image: radial-gradient(ellipse at center, #000 0%, transparent 65%);
2295 -webkit-mask-image: radial-gradient(ellipse at center, #000 0%, transparent 65%);
2296 opacity: 0.6;
2297 }
2298 :root[data-theme='light'] .landing-cta-card::after {
2299 background-image: radial-gradient(rgba(15,16,28,0.07) 1px, transparent 1px);
2300 }
2301 .landing-cta-card .eyebrow { justify-content: center; }
2302 .landing-cta-title {
2303 font-family: var(--font-display);
2304 font-size: clamp(28px, 4.4vw, 56px);
2305 line-height: 1.05;
2306 letter-spacing: -0.03em;
2307 font-weight: 600;
2308 margin: var(--s-3) 0 var(--s-4);
2309 color: var(--text-strong);
2310 }
2311 .landing-cta-sub {
2312 font-size: var(--t-md);
2313 color: var(--text-muted);
2314 max-width: 560px;
2315 margin: 0 auto var(--s-8);
2316 line-height: 1.55;
2317 }
2318 .landing-cta-buttons {
2319 display: flex;
2320 gap: 12px;
2321 justify-content: center;
2322 flex-wrap: wrap;
2323 }
2b821b7Claude2324
2325 /* ---------- Responsive ---------- */
958d26aClaude2326 @media (max-width: 960px) {
2327 .landing-features { grid-template-columns: repeat(2, 1fr); }
2328 .landing-walk-grid { grid-template-columns: repeat(2, 1fr); }
2329 .landing-walk-step::after { display: none; }
2330 .landing-pricing { grid-template-columns: 1fr; max-width: 480px; }
2331 }
2332 @media (max-width: 640px) {
2333 .landing-hero { padding: var(--s-14) 0 var(--s-10); }
2334 .landing-hero-cmd { flex-wrap: wrap; justify-content: center; }
2335 .landing-hero-ctas { flex-direction: column; align-items: stretch; }
2336 .landing-hero-ctas .btn { width: 100%; justify-content: center; }
2337 .landing-features { grid-template-columns: 1fr; }
2338 .landing-walk-grid { grid-template-columns: 1fr; }
2339 .landing-section { margin: var(--s-12) auto; }
2340 .landing-cta-card { padding: var(--s-10) var(--s-5); }
2341 .landing-cta-buttons .btn { width: 100%; justify-content: center; }
2b821b7Claude2342 }
52ad8b1Claude2343
2344 /* ---------- L4 social-proof counters ---------- */
2345 .landing-counters {
2346 margin: var(--s-10) auto var(--s-12);
2347 max-width: 1180px;
2348 padding: 0 var(--s-4);
2349 }
2350 .landing-counters-grid {
2351 display: grid;
2352 grid-template-columns: repeat(6, 1fr);
2353 gap: 20px;
2354 text-align: left;
2355 }
2356 .landing-counter {
2357 padding: var(--s-3) 0;
2358 border-top: 1px solid var(--border-subtle);
2359 }
2360 .landing-counter-num {
2361 font-family: var(--font-display);
2362 font-size: clamp(24px, 3vw, 38px);
2363 line-height: 1.05;
2364 letter-spacing: -0.03em;
2365 font-weight: 700;
2366 margin-bottom: 6px;
2367 font-feature-settings: 'tnum';
2368 background-image: var(--accent-gradient);
2369 -webkit-background-clip: text;
2370 background-clip: text;
2371 -webkit-text-fill-color: transparent;
2372 color: transparent;
2373 }
2374 .landing-counter-label {
2375 font-family: var(--font-mono);
2376 font-size: 10.5px;
2377 font-weight: 500;
2378 text-transform: uppercase;
2379 letter-spacing: 0.12em;
2380 color: var(--text-faint);
2381 line-height: 1.4;
2382 }
2383 @media (max-width: 960px) {
2384 .landing-counters-grid { grid-template-columns: repeat(3, 1fr); gap: 20px 16px; }
2385 }
2386 @media (max-width: 540px) {
2387 .landing-counters-grid { grid-template-columns: repeat(2, 1fr); gap: 18px 12px; }
2388 }
5f2e749Claude2389
dc26881CC LABS App2390 /* ---------- L10/U1 hero install snippet ----------
2391 U1: wrapped in a labelled "power users" panel and re-located
2392 beneath the CTA + tertiary rows so it no longer competes with
2393 the primary calls to action. */
2394 .landing-hero-install-wrap {
2395 margin: 0 auto var(--space-6);
2396 text-align: center;
2397 }
2398 .landing-hero-install-label {
2399 display: inline-block;
2400 margin-bottom: var(--space-2);
2401 font-family: var(--font-mono);
2402 font-size: 11px;
2403 letter-spacing: 0.08em;
2404 text-transform: uppercase;
2405 color: var(--text-faint);
2406 }
5f2e749Claude2407 .landing-hero-install {
2408 display: inline-flex;
2409 align-items: stretch;
2410 gap: 0;
dc26881CC LABS App2411 margin: 0 auto;
5f2e749Claude2412 background: var(--bg-elevated);
2413 border: 1px solid var(--border-strong);
2414 border-radius: var(--r);
2415 box-shadow: var(--elev-1);
2416 overflow: hidden;
2417 max-width: 100%;
2418 font-family: var(--font-mono);
2419 }
2420 .landing-hero-install-code {
2421 display: inline-flex;
2422 align-items: center;
2423 gap: 10px;
2424 padding: 10px 14px;
2425 font-size: 13.5px;
2426 color: var(--text-strong);
2427 background: transparent;
2428 border: 0;
2429 white-space: nowrap;
2430 overflow-x: auto;
2431 }
2432 .landing-hero-install-prompt {
2433 color: var(--accent);
2434 user-select: none;
2435 }
2436 .landing-hero-install-copy {
2437 appearance: none;
2438 border: 0;
2439 border-left: 1px solid var(--border);
2440 background: transparent;
2441 color: var(--text-muted);
2442 font-family: var(--font-mono);
2443 font-size: 12px;
2444 font-weight: 600;
2445 letter-spacing: 0.06em;
2446 text-transform: uppercase;
2447 padding: 0 16px;
2448 cursor: pointer;
2449 transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
2450 }
2451 .landing-hero-install-copy:hover {
2452 background: var(--accent-gradient-faint);
2453 color: var(--accent);
2454 }
2455 .landing-hero-install-copy[data-copied="1"] {
2456 color: var(--green, #34d399);
2457 }
2458
dc26881CC LABS App2459 /* ---------- L10/U1 hero "what just happened" rail ----------
2460 U1 — tightened into a single horizontal strip. The 1px gradient
2461 rule on top is the same accent the headline uses, so the rail
2462 reads as part of the hero composition rather than a stray list. */
5f2e749Claude2463 .landing-hero-rail {
2464 list-style: none;
dc26881CC LABS App2465 padding: var(--space-4) 0 0;
2466 margin: 0 auto var(--space-6);
5f2e749Claude2467 display: flex;
2468 flex-wrap: wrap;
2469 justify-content: center;
dc26881CC LABS App2470 gap: var(--space-2) var(--space-6);
5f2e749Claude2471 font-family: var(--font-sans);
dc26881CC LABS App2472 font-size: 12px;
2473 color: var(--text-faint);
5f2e749Claude2474 max-width: 760px;
dc26881CC LABS App2475 position: relative;
2476 }
2477 .landing-hero-rail::before {
2478 content: '';
2479 position: absolute;
2480 top: 0;
2481 left: 50%;
2482 transform: translateX(-50%);
2483 width: 120px;
2484 height: 1px;
2485 background: var(--accent-gradient);
2486 opacity: 0.45;
2487 border-radius: 9999px;
5f2e749Claude2488 }
2489 .landing-hero-rail li {
2490 display: inline-flex;
dc26881CC LABS App2491 align-items: baseline;
2492 gap: var(--space-2);
5f2e749Claude2493 line-height: 1.4;
2494 }
2495 .landing-hero-rail strong {
2496 color: var(--text-strong);
2497 font-weight: 600;
2498 font-feature-settings: 'tnum';
dc26881CC LABS App2499 font-size: 14px;
2500 letter-spacing: -0.01em;
5f2e749Claude2501 }
dc26881CC LABS App2502 .landing-hero-rail-label {
2503 color: var(--text-muted);
2504 letter-spacing: 0.01em;
5f2e749Claude2505 }
dc26881CC LABS App2506 /* Backwards-compat: nothing references this any more but if a stale
2507 fragment lingers it's still hidden cleanly rather than orphaned. */
2508 .landing-hero-rail-check { display: none; }
5f2e749Claude2509
2510 /* ---------- L10 three-reasons section ---------- */
2511 .landing-reasons { margin-top: var(--s-12); }
2512 .landing-reasons-grid {
2513 display: grid;
2514 grid-template-columns: repeat(3, 1fr);
2515 gap: 16px;
2516 }
2517 .landing-reason {
2518 background: var(--bg-elevated);
2519 border: 1px solid var(--border);
2520 border-radius: var(--r-lg);
2521 padding: var(--s-7);
2522 display: flex;
2523 flex-direction: column;
2524 gap: var(--s-3);
2525 transition: border-color var(--t-base) var(--ease), transform var(--t-base) var(--ease);
2526 }
2527 .landing-reason:hover {
2528 border-color: var(--border-strong);
2529 transform: translateY(-2px);
2530 }
2531 .landing-reason-icon {
2532 display: inline-flex;
2533 align-items: center;
2534 justify-content: center;
2535 width: 40px;
2536 height: 40px;
2537 border-radius: var(--r);
2538 background: var(--accent-gradient-soft);
2539 color: var(--accent);
2540 border: 1px solid rgba(140,109,255,0.20);
2541 }
2542 .landing-reason-title {
2543 font-family: var(--font-display);
2544 font-size: 20px;
2545 font-weight: 600;
2546 letter-spacing: -0.02em;
2547 margin: 0;
2548 color: var(--text-strong);
2549 }
2550 .landing-reason-body {
2551 font-size: var(--t-sm);
2552 color: var(--text-muted);
2553 line-height: 1.55;
2554 margin: 0;
2555 }
2556 .landing-reasons-code {
2557 display: block;
2558 font-family: var(--font-mono);
2559 font-size: 12px;
2560 color: var(--text-strong);
2561 background: var(--bg);
2562 border: 1px solid var(--border);
2563 border-radius: var(--r);
2564 padding: 8px 12px;
2565 overflow-x: auto;
2566 white-space: nowrap;
2567 }
2568 .landing-reason-link {
2569 margin-top: auto;
2570 display: inline-flex;
2571 align-items: center;
2572 gap: 6px;
2573 color: var(--accent);
2574 font-size: var(--t-sm);
2575 font-weight: 500;
2576 text-decoration: none;
2577 }
2578 .landing-reason-link:hover { text-decoration: underline; }
2579 @media (max-width: 960px) {
2580 .landing-reasons-grid { grid-template-columns: 1fr; max-width: 520px; margin: 0 auto; }
2581 }
2582
2583 /* ---------- L10 "How is this different" pull-quote ---------- */
2584 .landing-pullquote-section {
2585 margin: var(--s-20) auto var(--s-12);
2586 max-width: 920px;
2587 padding: 0 var(--s-4);
2588 text-align: center;
2589 }
2590 .landing-pullquote {
2591 margin: 0;
2592 padding: var(--s-10) var(--s-7);
2593 background:
2594 radial-gradient(80% 100% at 50% 0%, rgba(140,109,255,0.10), transparent 65%),
2595 var(--bg-elevated);
2596 border: 1px solid var(--border-strong);
2597 border-radius: var(--r-xl);
2598 position: relative;
2599 overflow: hidden;
2600 }
2601 .landing-pullquote-eyebrow {
2602 font-family: var(--font-mono);
2603 font-size: 11px;
2604 font-weight: 600;
2605 letter-spacing: 0.16em;
2606 text-transform: uppercase;
2607 color: var(--accent);
2608 margin-bottom: var(--s-4);
2609 }
2610 .landing-pullquote-text {
2611 font-family: var(--font-display);
2612 font-size: clamp(20px, 2.4vw, 28px);
2613 line-height: 1.4;
2614 letter-spacing: -0.018em;
2615 color: var(--text-strong);
2616 margin: 0 auto;
2617 max-width: 760px;
2618 quotes: "\\201C" "\\201D";
2619 }
2620 .landing-pullquote-text::before { content: open-quote; color: var(--accent); margin-right: 4px; }
2621 .landing-pullquote-text::after { content: close-quote; color: var(--accent); margin-left: 4px; }
2622 .landing-pullquote-link {
2623 display: inline-flex;
2624 align-items: center;
2625 gap: 6px;
2626 margin-top: var(--s-6);
2627 color: var(--accent);
2628 font-size: var(--t-sm);
2629 font-weight: 500;
2630 text-decoration: none;
2631 }
2632 .landing-pullquote-link:hover { text-decoration: underline; }
2633
2634 /* ---------- L10 hero responsive overrides ---------- */
2635 @media (max-width: 640px) {
2636 .landing-hero-install { width: 100%; }
2637 .landing-hero-install-code { flex: 1; font-size: 12px; }
2638 .landing-hero-rail { flex-direction: column; align-items: flex-start; gap: 6px; padding: 0 var(--s-3); }
2639 .landing-hero-rail li { width: 100%; }
2640 }
534f04aClaude2641
2642 /* ============================================================ */
2643 /* Block M1 — Live-now demo feed */
2644 /* ============================================================ */
2645 .landing-livenow {
2646 margin: var(--s-8) 0 var(--s-6);
2647 padding: var(--s-6) 0 var(--s-4);
2648 }
2649 .landing-livenow-head {
2650 text-align: center;
2651 margin-bottom: var(--s-6);
2652 }
2653 .landing-livenow-eyebrow {
2654 display: inline-flex;
2655 align-items: center;
2656 gap: 8px;
2657 padding: 4px 12px;
2658 border-radius: var(--r-full);
2659 background: rgba(52,211,153,0.08);
2660 border: 1px solid rgba(52,211,153,0.25);
2661 color: var(--green);
2662 font-family: var(--font-mono, ui-monospace, monospace);
2663 font-size: 11px;
2664 letter-spacing: 0.06em;
2665 text-transform: uppercase;
2666 margin-bottom: var(--s-3);
2667 }
2668 .landing-livenow-pulse {
2669 width: 8px; height: 8px;
2670 border-radius: 50%;
2671 background: var(--green);
2672 box-shadow: 0 0 0 0 rgba(52,211,153,0.6);
2673 animation: landing-livenow-pulse 1.6s ease-out infinite;
2674 }
2675 @keyframes landing-livenow-pulse {
2676 0% { box-shadow: 0 0 0 0 rgba(52,211,153,0.55); transform: scale(1); }
2677 70% { box-shadow: 0 0 0 10px rgba(52,211,153,0); transform: scale(1.05); }
2678 100% { box-shadow: 0 0 0 0 rgba(52,211,153,0); transform: scale(1); }
2679 }
2680 .landing-livenow-title {
2681 font-size: 22px;
2682 line-height: 1.25;
2683 margin: 0 auto;
2684 max-width: 720px;
2685 color: var(--text-strong);
2686 font-weight: 600;
2687 letter-spacing: -0.01em;
2688 }
2689 .landing-livenow-sub {
2690 margin: var(--s-2) auto 0;
2691 color: var(--text-muted);
2692 font-size: 13px;
2693 max-width: 560px;
2694 }
2695 .landing-livenow-sub code {
2696 background: rgba(255,255,255,0.05);
2697 border: 1px solid var(--border);
2698 padding: 1px 6px;
2699 border-radius: 4px;
2700 font-size: 12px;
2701 color: var(--accent);
2702 }
2703
2704 .landing-livenow-grid {
2705 display: grid;
2706 grid-template-columns: repeat(2, minmax(0, 1fr));
2707 gap: var(--s-3);
2708 }
2709 @media (min-width: 980px) {
2710 .landing-livenow-grid {
2711 grid-template-columns: repeat(4, minmax(0, 1fr));
2712 }
2713 }
2714
2715 .landing-livecard {
2716 background: linear-gradient(180deg, rgba(255,255,255,0.025), rgba(255,255,255,0.005));
2717 border: 1px solid var(--border);
2718 border-radius: var(--r-md, 10px);
2719 padding: 14px 14px 12px;
2720 display: flex;
2721 flex-direction: column;
2722 min-height: 180px;
2723 position: relative;
2724 overflow: hidden;
2725 }
2726 .landing-livecard::before {
2727 content: '';
2728 position: absolute;
2729 inset: 0;
2730 background: var(--accent-gradient-faint);
2731 opacity: 0;
2732 transition: opacity 200ms var(--ease, ease);
2733 pointer-events: none;
2734 }
2735 .landing-livecard:hover::before { opacity: 1; }
2736
2737 .landing-livecard-head {
2738 display: flex;
2739 align-items: center;
2740 gap: 8px;
2741 margin-bottom: 10px;
2742 }
2743 .landing-livecard-dot {
2744 width: 7px; height: 7px;
2745 border-radius: 50%;
2746 background: var(--green);
2747 box-shadow: 0 0 8px rgba(52,211,153,0.55);
2748 animation: landing-livenow-pulse 1.8s ease-out infinite;
2749 flex-shrink: 0;
2750 }
2751 .landing-livecard-title {
2752 margin: 0;
2753 font-size: 12px;
2754 font-weight: 600;
2755 text-transform: uppercase;
2756 letter-spacing: 0.06em;
2757 color: var(--text-muted);
2758 }
2759
2760 .landing-livecard-bignum {
2761 display: flex;
2762 align-items: baseline;
2763 gap: 8px;
2764 margin: 4px 0 10px;
2765 }
2766 .landing-livecard-bignum-n {
2767 font-size: 30px;
2768 font-weight: 700;
2769 color: var(--text-strong);
2770 font-variant-numeric: tabular-nums;
2771 background: var(--accent-gradient);
2772 -webkit-background-clip: text;
2773 background-clip: text;
2774 -webkit-text-fill-color: transparent;
2775 color: transparent;
2776 }
2777 .landing-livecard-bignum-label {
2778 font-size: 12px;
2779 color: var(--text-muted);
2780 }
2781
2782 .landing-livecard-list {
2783 list-style: none;
2784 margin: 0;
2785 padding: 0;
2786 font-size: 13px;
2787 line-height: 1.45;
2788 flex: 1;
2789 }
2790 .landing-livecard-row, .landing-livecard-feedrow {
2791 padding: 6px 0;
2792 border-bottom: 1px dashed rgba(255,255,255,0.05);
2793 transition: background-color 1s var(--ease, ease);
2794 border-radius: 4px;
2795 margin: 0 -4px;
2796 padding-left: 4px;
2797 padding-right: 4px;
2798 }
2799 .landing-livecard-row:last-child,
2800 .landing-livecard-feedrow:last-child { border-bottom: 0; }
2801 .landing-livecard-feedrow { padding: 4px; font-size: 12.5px; }
2802
2803 .landing-livecard-flash {
2804 background-color: rgba(52,211,153,0.18) !important;
2805 animation: landing-livecard-flash-fade 1.1s ease-out forwards;
2806 }
2807 @keyframes landing-livecard-flash-fade {
2808 0% { background-color: rgba(52,211,153,0.22); }
2809 100% { background-color: rgba(52,211,153,0); }
2810 }
2811
2812 .landing-livecard-link {
2813 color: var(--text-strong);
2814 text-decoration: none;
2815 display: inline-block;
2816 max-width: 100%;
2817 overflow: hidden;
2818 text-overflow: ellipsis;
2819 white-space: nowrap;
2820 }
2821 .landing-livecard-link:hover { color: var(--accent); }
2822 .landing-livecard-num {
2823 font-family: var(--font-mono, ui-monospace, monospace);
2824 color: var(--text-faint);
2825 font-weight: 500;
2826 font-size: 12px;
2827 }
2828 .landing-livecard-title-text { color: var(--text-strong); }
2829 .landing-livecard-snippet {
2830 color: var(--text-muted);
2831 font-style: italic;
2832 font-size: 12px;
2833 }
2834 .landing-livecard-meta {
2835 font-size: 11px;
2836 color: var(--text-faint);
2837 margin-top: 2px;
2838 font-family: var(--font-mono, ui-monospace, monospace);
2839 }
2840 .landing-livecard-repo {
2841 color: var(--accent-2);
2842 }
2843 .landing-livecard-rel {
2844 color: var(--text-muted);
2845 }
2846 .landing-livecard-empty {
2847 color: var(--text-faint);
2848 font-size: 12px;
2849 font-style: italic;
2850 padding: 8px 0;
2851 }
2852 .landing-livecard-kind {
2853 display: inline-block;
2854 padding: 1px 7px;
2855 border-radius: var(--r-full);
2856 font-family: var(--font-mono, ui-monospace, monospace);
2857 font-size: 10px;
2858 letter-spacing: 0.04em;
2859 text-transform: uppercase;
2860 font-weight: 600;
2861 }
2862 .landing-livecard-kind-auto_merge-merged,
2863 .landing-livecard-kind-auto-merge-merged {
2864 background: rgba(52,211,153,0.12);
2865 color: var(--green);
2866 border: 1px solid rgba(52,211,153,0.25);
2867 }
2868 .landing-livecard-kind-ai_build-dispatched,
2869 .landing-livecard-kind-ai-build-dispatched {
2870 background: rgba(140,109,255,0.12);
2871 color: var(--accent);
2872 border: 1px solid rgba(140,109,255,0.30);
2873 }
2874 .landing-livecard-kind-ai_review-posted,
2875 .landing-livecard-kind-ai-review-posted {
2876 background: rgba(54,197,214,0.12);
2877 color: var(--accent-2);
2878 border: 1px solid rgba(54,197,214,0.30);
2879 }
2880
2881 .landing-livenow-cta {
2882 margin-top: var(--s-5);
2883 display: flex;
2884 flex-wrap: wrap;
2885 align-items: center;
2886 justify-content: center;
2887 gap: 10px;
2888 font-size: 13px;
2889 color: var(--text-muted);
2890 }
2891 .landing-livenow-cta-link {
2892 color: var(--accent);
2893 text-decoration: none;
2894 font-weight: 500;
2895 display: inline-flex;
2896 align-items: center;
2897 gap: 4px;
2898 }
2899 .landing-livenow-cta-link:hover { color: var(--accent-hover); }
2900 .landing-livenow-cta-sep { color: var(--text-faint); }
2901
2902 @media (prefers-reduced-motion: reduce) {
2903 .landing-livenow-pulse,
2904 .landing-livecard-dot { animation: none; }
2905 .landing-livecard-flash { animation: none; background-color: transparent !important; }
2906 }
52ad8b1Claude2907`;
2908
2909/**
2910 * Block L4 — count-up animation.
2911 *
2912 * Reads each `[data-counter-target]` and animates the in-DOM text from
2913 * 0 → target over ~1.2s when the element first scrolls into view.
2914 *
2915 * Render-once semantics: each tile already contains the final value as
2916 * HTML, so visitors with JS disabled — or anyone before the script
2917 * loads — sees the correct number. The script just animates the text.
2918 *
2919 * Falls back to the static value (no animation) when IntersectionObserver
2920 * isn't available, or when the user prefers reduced motion.
2921 */
2922const landingCountersJs = `
2923(function(){
2924 try {
2925 var els = document.querySelectorAll('[data-counter-target]');
2926 if (!els.length) return;
2927 var reduced = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
2928 if (reduced || typeof IntersectionObserver !== 'function') return;
2929
2930 function animate(el) {
2931 var target = parseInt(el.getAttribute('data-counter-target') || '0', 10);
2932 if (!isFinite(target) || target <= 0) return;
2933 var prefix = el.getAttribute('data-counter-prefix') || '';
2934 var suffix = el.getAttribute('data-counter-suffix') || '';
2935 var duration = 1200;
2936 var start = performance.now();
2937 function frame(now) {
2938 var t = Math.min(1, (now - start) / duration);
2939 // ease-out cubic
2940 var eased = 1 - Math.pow(1 - t, 3);
2941 var v = Math.floor(eased * target);
2942 el.textContent = prefix + v.toLocaleString() + suffix;
2943 if (t < 1) requestAnimationFrame(frame);
2944 else el.textContent = prefix + target.toLocaleString() + suffix;
2945 }
2946 // Reset to zero before animating in.
2947 el.textContent = prefix + '0' + suffix;
2948 requestAnimationFrame(frame);
2949 }
2950
2951 var io = new IntersectionObserver(function(entries) {
2952 entries.forEach(function(entry){
2953 if (entry.isIntersecting) {
2954 animate(entry.target);
2955 io.unobserve(entry.target);
2956 }
2957 });
2958 }, { threshold: 0.4 });
2959 els.forEach(function(el){ io.observe(el); });
2960 } catch (_) { /* swallow — static numbers remain */ }
2961})();
2b821b7Claude2962`;
5f2e749Claude2963
2964/**
2965 * Block L10 — clipboard copy for the hero install snippet.
2966 *
2967 * Pure progressive enhancement. Without JS the user can still
2968 * triple-click + Cmd/Ctrl-C the snippet — the button is the
2969 * speed-bump, not the only path.
2970 */
2971const landingCopyJs = `
2972(function(){
2973 try {
2974 var btns = document.querySelectorAll('[data-copy-target]');
2975 if (!btns.length) return;
2976 btns.forEach(function(btn){
2977 btn.addEventListener('click', function(){
2978 var id = btn.getAttribute('data-copy-target') || '';
2979 var src = document.getElementById(id);
2980 if (!src) return;
2981 var text = src.textContent || '';
2982 var done = function(){
2983 var prev = btn.textContent;
2984 btn.textContent = 'Copied';
2985 btn.setAttribute('data-copied', '1');
2986 setTimeout(function(){
2987 btn.textContent = prev || 'Copy';
2988 btn.removeAttribute('data-copied');
2989 }, 1500);
2990 };
2991 if (navigator.clipboard && navigator.clipboard.writeText) {
2992 navigator.clipboard.writeText(text).then(done, function(){ done(); });
2993 } else {
2994 // Legacy fallback — temp textarea + execCommand.
2995 try {
2996 var ta = document.createElement('textarea');
2997 ta.value = text;
2998 ta.style.position = 'fixed';
2999 ta.style.opacity = '0';
3000 document.body.appendChild(ta);
3001 ta.select();
3002 document.execCommand('copy');
3003 document.body.removeChild(ta);
3004 done();
3005 } catch (_) {}
3006 }
3007 });
3008 });
3009 } catch (_) { /* swallow */ }
3010})();
3011`;