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.tsxBlame2888 lines · 2 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">
121 {/* ---------- Hero ---------- */}
122 <section class="landing-hero">
958d26aClaude123 <div class="landing-hero-bg" aria-hidden="true">
124 <div class="landing-hero-blob landing-hero-blob-1" />
125 <div class="landing-hero-blob landing-hero-blob-2" />
126 <div class="landing-hero-grid" />
2b821b7Claude127 </div>
958d26aClaude128
129 <div class="landing-hero-inner stagger">
130 <div class="eyebrow landing-hero-eyebrow">
131 <span class="landing-hero-pulse" />
132 v1 · pre-launch · {new Date().getFullYear()}
133 </div>
134
135 <h1 class="landing-hero-title display">
5f2e749Claude136 <span class="gradient-text">The git host built around Claude.</span>
958d26aClaude137 </h1>
138
139 <p class="landing-hero-sub">
5f2e749Claude140 Label an issue. Walk away. Wake up to a merged PR.
958d26aClaude141 </p>
142
5f2e749Claude143 {/* L10 — one-line install snippet with copy button. */}
144 <div class="landing-hero-install" aria-label="One-line install">
145 <code class="landing-hero-install-code">
146 <span class="landing-hero-install-prompt" aria-hidden="true">$</span>
147 <span id="landing-install-text">curl -sSL gluecron.com/install | bash</span>
148 </code>
149 <button
150 type="button"
151 class="landing-hero-install-copy"
152 data-copy-target="landing-install-text"
153 aria-label="Copy install command"
154 >
155 Copy
156 </button>
157 </div>
158
958d26aClaude159 <div class="landing-hero-ctas">
160 <a href="/register" class="btn btn-primary btn-xl landing-cta-primary">
5f2e749Claude161 Sign up free
958d26aClaude162 <span class="landing-cta-arrow" aria-hidden="true">{"→"}</span>
163 </a>
93fe97eClaude164 {/* BLOCK Q1 — one-click Claude Desktop install. Gradient
165 border + accent so it reads as a distinct flagship CTA,
166 not just a third secondary option. */}
167 <a
168 href="/gluecron.dxt"
169 class="btn btn-xl landing-cta-dxt"
170 download
171 data-testid="cta-dxt"
172 >
173 Add to Claude Desktop
174 <span class="landing-cta-arrow" aria-hidden="true">{"→"}</span>
175 </a>
5f2e749Claude176 <a href="/demo" class="btn btn-secondary btn-xl">
177 Try the live demo
178 <span class="landing-cta-arrow" aria-hidden="true">{"→"}</span>
958d26aClaude179 </a>
52ad8b1Claude180 <a href="/vs-github" class="btn btn-ghost btn-xl">
181 Compare to GitHub
5f2e749Claude182 <span class="landing-cta-arrow" aria-hidden="true">{"→"}</span>
52ad8b1Claude183 </a>
958d26aClaude184 </div>
185
cd4f63bTest User186 {/* Block Q3 — tertiary "try it without signing up" link.
187 Visually subordinate to the buttons above; renders as a
188 small ghost text link so it doesn't crowd the CTA row. */}
189 <div class="landing-hero-play">
190 <a href="/play" class="landing-hero-play-link" data-testid="cta-play">
191 Or try it without signing up
192 <span aria-hidden="true">{" →"}</span>
193 </a>
194 </div>
195
5f2e749Claude196 {/* L10 — "what just happened" rail. Mini, secondary,
197 separate from the BIG L4 counters tile section below. */}
198 {publicStats && (
199 <ul class="landing-hero-rail" aria-label="What just happened on Gluecron">
200 <li>
201 <span class="landing-hero-rail-check" aria-hidden="true">{"✓"}</span>
202 <strong>{publicStats.weeklyPrsAutoMerged.toLocaleString()}</strong>
203 {" PRs auto-merged this week"}
204 </li>
205 <li>
206 <span class="landing-hero-rail-check" aria-hidden="true">{"✓"}</span>
207 <strong>{publicStats.weeklyIssuesBuiltByAi.toLocaleString()}</strong>
208 {" issues built by AI"}
209 </li>
210 <li>
211 <span class="landing-hero-rail-check" aria-hidden="true">{"✓"}</span>
212 <strong>{publicStats.weeklyDeploysShipped.toLocaleString()}</strong>
213 {" deploys shipped overnight"}
214 </li>
215 <li>
216 <span class="landing-hero-rail-check" aria-hidden="true">{"✓"}</span>
217 {"~"}
218 <strong>{Math.round(publicStats.weeklyHoursSaved).toLocaleString()}</strong>
219 {" hours saved by AI"}
220 </li>
221 </ul>
222 )}
223
224 {/* L8 — free-tier reassurance link. Keeps anxiety low for the AI-curious. */}
225 <p class="landing-hero-freenote">
226 Free forever for the AI-curious.{" "}
227 <a href="/pricing" class="landing-hero-freenote-link">
228 See pricing &rarr;
229 </a>
230 </p>
231
958d26aClaude232 <p class="landing-hero-caption">
233 Already have a repo?
234 <span class="landing-hero-cmd">
235 <span class="kbd">git</span>
236 <span class="kbd">remote</span>
237 <span class="kbd">add</span>
238 <span class="kbd">gluecron</span>
239 <span class="landing-hero-arrow">{"→"}</span>
240 <span class="kbd">git push</span>
241 </span>
242 </p>
243
244 {hasStats && (
245 <p class="landing-stats">
246 {stats!.publicRepos !== undefined && stats!.publicRepos > 0 && (
247 <span>
248 <strong>{stats!.publicRepos.toLocaleString()}</strong>
249 {stats!.publicRepos === 1 ? " repo" : " repos"}
250 </span>
251 )}
252 {stats!.publicRepos !== undefined &&
253 stats!.publicRepos > 0 &&
254 stats!.users !== undefined &&
255 stats!.users > 0 && <span class="landing-stats-sep">·</span>}
256 {stats!.users !== undefined && stats!.users > 0 && (
257 <span>
258 <strong>{stats!.users.toLocaleString()}</strong>
259 {stats!.users === 1 ? " developer" : " developers"}
260 </span>
261 )}
262 <span class="landing-stats-sep">·</span>
4c47454Claude263 <span>
958d26aClaude264 <strong>100%</strong> AI-native
4c47454Claude265 </span>
958d26aClaude266 </p>
267 )}
268 </div>
c963db5Claude269 </section>
c475ee6Claude270
534f04aClaude271 {/* ---------- Block M1 — Live-now demo feed ---------- */}
272 <LiveNowSection
273 queued={liveQueued}
274 merges={liveMerges}
275 reviews={liveReviews}
276 reviewCount={liveReviewCount}
277 feed={liveEntries}
278 />
279
52ad8b1Claude280 {/* ---------- L4 social-proof counters (animated count-up) ---------- */}
281 {tiles && (
282 <section class="landing-counters" aria-label="Gluecron live counters">
283 <div class="landing-counters-grid">
284 {tiles.map((t) => (
285 <div class="landing-counter">
286 <div
287 class="landing-counter-num"
288 data-counter-target={String(t.value)}
289 data-counter-suffix={t.suffix ?? ""}
290 data-counter-prefix={t.prefix ?? ""}
291 >
292 {t.prefix ?? ""}
293 {t.value.toLocaleString()}
294 {t.suffix ?? ""}
295 </div>
296 <div class="landing-counter-label">{t.label}</div>
297 </div>
298 ))}
299 </div>
300 <script dangerouslySetInnerHTML={{ __html: landingCountersJs }} />
301 </section>
302 )}
303
5f2e749Claude304 {/* ---------- L10 — Three reasons to switch ---------- */}
305 <section class="landing-section landing-reasons" aria-label="Three reasons to switch">
306 <div class="section-header">
307 <div class="eyebrow">Three reasons to switch</div>
308 <h2>Built so Claude can do the work.</h2>
309 </div>
310 <div class="landing-reasons-grid">
311 <ReasonCard
312 icon={
313 <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">
314 <path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" />
315 </svg>
316 }
317 title="Toggle Sleep Mode"
318 body="Claude does the work overnight. You get a 9 AM digest of what shipped — PRs merged, deploys live, incidents triaged."
319 link={{ href: "/sleep-mode", label: "Turn on Sleep Mode" }}
320 />
321 <ReasonCard
322 icon={
323 <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">
324 <polyline points="4 17 10 11 4 5" />
325 <line x1="12" y1="19" x2="20" y2="19" />
326 </svg>
327 }
328 title="One command to migrate"
329 body="Drop a single curl into your shell. Gluecron rehosts your repo, your issues, your branches — no SaaS rip-and-replace project required."
330 extra={
331 <code class="landing-reasons-code">curl -sSL gluecron.com/install | bash</code>
332 }
333 link={{ href: "/import", label: "Or import from GitHub" }}
334 />
335 <ReasonCard
336 icon={
337 <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">
338 <polygon points="5 3 19 12 5 21 5 3" />
339 </svg>
340 }
341 title="Open the demo, watch it work"
342 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."
343 link={{ href: "/demo", label: "Open the live demo" }}
344 />
345 </div>
346 </section>
347
c963db5Claude348 {/* ---------- Capability strip — uppercase tracked grid (crontech-style) ---------- */}
349 <section class="landing-caps">
350 <div class="landing-caps-grid">
351 <span class="landing-cap">Claude-powered AI</span>
352 <span class="landing-cap">Spec-to-PR</span>
353 <span class="landing-cap">Auto-repair</span>
354 <span class="landing-cap">Real-time gates</span>
355 <span class="landing-cap">MCP-native</span>
356 <span class="landing-cap">Workflow runner</span>
357 <span class="landing-cap">Self-hostable</span>
358 <span class="landing-cap">Branch protection</span>
359 <span class="landing-cap">Bun + Hono</span>
360 <span class="landing-cap">Drizzle + Postgres</span>
361 <span class="landing-cap">JSX server-rendered</span>
362 <span class="landing-cap">Type-safe end to end</span>
c475ee6Claude363 </div>
958d26aClaude364 </section>
365
c963db5Claude366 {/* ---------- Big stat row (crontech-style hero closer) ---------- */}
367 <section class="landing-bigstats">
368 <div class="landing-bigstats-grid">
369 <div class="landing-bigstat">
370 <div class="landing-bigstat-num">Claude-powered</div>
371 <div class="landing-bigstat-label">The best AI, native</div>
372 </div>
373 <div class="landing-bigstat">
374 <div class="landing-bigstat-num">Self-hosted</div>
375 <div class="landing-bigstat-label">On your hardware</div>
376 </div>
377 <div class="landing-bigstat">
378 <div class="landing-bigstat-num">MCP-native</div>
379 <div class="landing-bigstat-label">Claude · Cursor · Code</div>
380 </div>
381 <div class="landing-bigstat">
382 <div class="landing-bigstat-num">Real-time</div>
383 <div class="landing-bigstat-label">SSE everywhere</div>
384 </div>
958d26aClaude385 </div>
4c47454Claude386 </section>
387
388 {/* ---------- Feature grid ---------- */}
958d26aClaude389 <section class="landing-section">
390 <div class="section-header">
391 <div class="eyebrow">The platform</div>
392 <h2>An IDE for your repo, not just a host.</h2>
393 <p>
394 Gluecron ships the surfaces GitHub charges extra for, and the
395 ones it never built. AI is a teammate with its own commits, not
396 a sidebar.
2b821b7Claude397 </p>
398 </div>
4c47454Claude399
958d26aClaude400 <div class="landing-features stagger">
401 <FeatureCard
402 icon={
403 <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
404 <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" />
405 </svg>
406 }
407 title="AI as a teammate"
408 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."
409 />
410 <FeatureCard
411 icon={
412 <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
413 <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" />
414 <path d="M9 12l2 2 4-4" />
415 </svg>
416 }
417 title="Quality gate that learns"
418 desc="GateTest scans every push. Auto-repair fixes regressions before you see them. Required checks block bad PRs from merging. Your software self-corrects."
419 />
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="M13 2L4 14h7l-1 8 9-12h-7z" />
424 </svg>
425 }
426 title="Real-time everything"
427 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."
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 <rect x="3" y="3" width="18" height="18" rx="3" />
433 <path d="M3 9h18M9 21V9" />
434 </svg>
435 }
436 title="Workflow runner"
437 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."
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 <circle cx="12" cy="12" r="9" />
443 <path d="M3 12h18M12 3a14 14 0 010 18M12 3a14 14 0 000 18" />
444 </svg>
445 }
446 title="MCP-native"
447 desc="Claude, Cursor, Code — they speak Model Context Protocol. Gluecron exposes search, file read, issues, codebase explain as MCP tools by default."
448 />
449 <FeatureCard
450 icon={
451 <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
452 <path d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
453 <path d="M12 7v5l3 2" />
454 </svg>
455 }
456 title="Yours, on your hardware"
457 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."
458 />
2b821b7Claude459 </div>
958d26aClaude460 </section>
2b821b7Claude461
958d26aClaude462 {/* ---------- Workflow walkthrough ---------- */}
463 <section class="landing-section landing-walk">
464 <div class="section-header">
465 <div class="eyebrow">How it works</div>
466 <h2>Push code. Watch it ship.</h2>
467 <p>
468 Every push triggers the same pipeline whether the commit came
469 from you, from CI, or from an AI agent.
4c47454Claude470 </p>
471 </div>
958d26aClaude472
473 <div class="landing-walk-grid">
474 <WalkStep n="01" title="Push" desc="git push to gluecron — Smart-HTTP, SSH, or via the web editor." />
475 <WalkStep n="02" title="Gate" desc="GateTest runs. Secret scanner runs. AI security review posts inline comments." />
476 <WalkStep n="03" title="Repair" desc="If a gate fails, auto-repair tries to fix it. New commit gets re-gated." />
477 <WalkStep n="04" title="Ship" desc="Green push to default branch fires deploy webhook. Crontech, Fly, your prod." />
478 </div>
4c47454Claude479 </section>
2b821b7Claude480
4c47454Claude481 {/* ---------- Terminal block ---------- */}
958d26aClaude482 <section class="landing-section landing-terminal-section">
483 <div class="landing-terminal-wrap">
484 <div class="landing-terminal" role="img" aria-label="Example git push to gluecron with passing gates">
485 <div class="landing-terminal-chrome">
486 <span class="landing-terminal-dot landing-terminal-dot-r" />
487 <span class="landing-terminal-dot landing-terminal-dot-y" />
488 <span class="landing-terminal-dot landing-terminal-dot-g" />
489 <span class="landing-terminal-title">~/your-repo &mdash; zsh</span>
490 </div>
491 <div class="landing-terminal-body">
492 <div class="landing-term-line">
493 <span class="landing-term-prompt">$</span>
494 <span>git remote add gluecron https://gluecron.com/you/your-repo.git</span>
495 </div>
496 <div class="landing-term-line">
497 <span class="landing-term-prompt">$</span>
498 <span>git push -u gluecron main</span>
499 </div>
500 <div class="landing-term-line landing-term-out">
501 <span class="landing-term-meta">remote:</span>
502 <span>Resolving deltas… 100% (24/24)</span>
503 </div>
504 <div class="landing-term-line landing-term-out landing-term-ok-line">
505 <span class="landing-term-ok">{"✓"}</span>
506 <span>pushed to gluecron.com/you/your-repo</span>
507 </div>
508 <div class="landing-term-line landing-term-out landing-term-ok-line">
509 <span class="landing-term-ok">{"✓"}</span>
510 <span>GateTest passed (12 rules, 0 violations)</span>
511 </div>
512 <div class="landing-term-line landing-term-out landing-term-ok-line">
513 <span class="landing-term-ok">{"✓"}</span>
514 <span>AI review posted (2 suggestions, 0 blockers)</span>
515 </div>
516 <div class="landing-term-line landing-term-out landing-term-ok-line">
517 <span class="landing-term-ok">{"✓"}</span>
518 <span>deployed to your-repo.gluecron.com <span class="landing-term-meta">(4.1s)</span></span>
519 </div>
520 <div class="landing-term-line landing-term-cursor">
521 <span class="landing-term-prompt">$</span>
522 <span class="landing-term-blink">▍</span>
523 </div>
524 </div>
4c47454Claude525 </div>
958d26aClaude526 </div>
527 </section>
528
529 {/* ---------- Comparison ---------- */}
530 <section class="landing-section">
531 <div class="section-header">
532 <div class="eyebrow">vs the incumbent</div>
533 <h2>Everything GitHub charges for. And the parts they didn't build.</h2>
534 </div>
535
536 <div class="landing-compare">
537 <CompareRow feature="Git hosting + Smart-HTTP push" them="✓" us="✓" />
538 <CompareRow feature="Issues, PRs, code review" them="✓" us="✓" />
539 <CompareRow feature="Workflow runner (Actions-equivalent)" them="paid minutes" us="self-hosted, unmetered" highlight />
540 <CompareRow feature="AI code review on every PR" them="Copilot subscription" us="built in" highlight />
541 <CompareRow feature="Spec-to-PR (NL feature → draft PR)" them="—" us="✓" highlight />
542 <CompareRow feature="Auto-repair on failed gates" them="—" us="✓" highlight />
543 <CompareRow feature="Real-time SSE for logs + PRs" them="polling" us="streaming" highlight />
544 <CompareRow feature="MCP server (Claude / Cursor)" them="—" us="✓" highlight />
545 <CompareRow feature="Self-host on your own infra" them="enterprise tier" us="single binary" highlight />
546 <CompareRow feature="Pre-receive policy enforcement" them="rulesets (GHE)" us="✓" />
547 </div>
548 </section>
549
550 {/* ---------- Pricing teaser ---------- */}
551 <section class="landing-section">
552 <div class="section-header">
553 <div class="eyebrow">Pricing</div>
554 <h2>Free to start. Honest at scale.</h2>
555 <p>
556 Self-hosting is free forever. Hosted plans price the AI calls,
557 not the seats.
558 </p>
559 </div>
560
561 <div class="landing-pricing">
562 <PricingCard
563 tier="Free"
564 price="$0"
565 cadence="forever"
566 desc="For personal projects + open source. Public + private repos, full AI suite, fair quotas."
567 features={["Unlimited public repos", "3 private repos", "5K AI calls / mo", "Community support"]}
568 cta="Start free"
569 href="/register"
570 />
571 <PricingCard
572 tier="Pro"
573 price="$12"
574 cadence="per user / mo"
575 desc="For working developers. Lifts every quota, adds priority routing, no Gluecron branding on deploys."
576 features={["Unlimited private repos", "100K AI calls / mo", "Priority queue", "Custom domains"]}
577 cta="Go Pro"
578 href="/settings/billing"
579 highlight
580 />
581 <PricingCard
582 tier="Team"
583 price="Talk to us"
584 cadence="custom"
585 desc="For orgs running production on Gluecron. SSO, audit retention, enterprise SLA, on-prem."
586 features={["SSO + SCIM", "On-prem deploy", "Dedicated capacity", "24/7 incident response"]}
587 cta="Contact"
588 href="mailto:hello@gluecron.com"
589 />
590 </div>
591 </section>
592
5f2e749Claude593 {/* ---------- L10 — "How is this different?" pull-quote ---------- */}
594 <section class="landing-pullquote-section" aria-label="How is this different from GitHub?">
595 <figure class="landing-pullquote">
596 <div class="landing-pullquote-eyebrow">How is this different from GitHub?</div>
597 <blockquote class="landing-pullquote-text">
598 Every other host bolts AI on as a sidecar. Gluecron is the first
599 git host where Claude is a first-class developer. Built to be
600 operated by AI agents, not just augmented by them.
601 </blockquote>
602 <a href="/vs-github" class="landing-pullquote-link">
603 See the full comparison
604 <span class="landing-cta-arrow" aria-hidden="true">{"→"}</span>
605 </a>
606 </figure>
607 </section>
608
958d26aClaude609 {/* ---------- Closing CTA ---------- */}
610 <section class="landing-cta-section">
611 <div class="landing-cta-card">
612 <div class="landing-cta-bg" aria-hidden="true" />
613 <div class="eyebrow">Ready when you are</div>
614 <h2 class="landing-cta-title">
615 Stop maintaining the platform.<br />
616 <span class="gradient-text">Start shipping the product.</span>
617 </h2>
618 <p class="landing-cta-sub">
619 Free to start, self-hosted-friendly, MCP-native. Migrate from
620 GitHub in one click.
621 </p>
622 <div class="landing-cta-buttons">
623 <a href="/register" class="btn btn-primary btn-xl">
624 Create your account
625 <span class="landing-cta-arrow" aria-hidden="true">{"→"}</span>
626 </a>
627 <a href="/import" class="btn btn-ghost btn-xl">
628 Migrate a repo
629 </a>
4c47454Claude630 </div>
631 </div>
632 </section>
5f2e749Claude633
634 {/* L10 — clipboard copy script for the hero install snippet. */}
635 <script dangerouslySetInnerHTML={{ __html: landingCopyJs }} />
4c47454Claude636 </div>
2b821b7Claude637 </>
638 );
639};
640
958d26aClaude641const FeatureCard: FC<{ icon: any; title: string; desc: string }> = ({
642 icon,
643 title,
644 desc,
645}) => (
646 <div class="landing-feature">
647 <div class="landing-feature-icon" aria-hidden="true">
648 {icon}
649 </div>
650 <h3 class="landing-feature-title">{title}</h3>
651 <p class="landing-feature-desc">{desc}</p>
652 </div>
653);
654
5f2e749Claude655// Block L10 — "Three reasons to switch" column.
656const ReasonCard: FC<{
657 icon: any;
658 title: string;
659 body: string;
660 link: { href: string; label: string };
661 extra?: any;
662}> = ({ icon, title, body, link, extra }) => (
663 <div class="landing-reason">
664 <div class="landing-reason-icon" aria-hidden="true">
665 {icon}
666 </div>
667 <h3 class="landing-reason-title">{title}</h3>
668 <p class="landing-reason-body">{body}</p>
669 {extra}
670 <a href={link.href} class="landing-reason-link">
671 {link.label}
672 <span class="landing-cta-arrow" aria-hidden="true">{"→"}</span>
673 </a>
674 </div>
675);
676
958d26aClaude677const WalkStep: FC<{ n: string; title: string; desc: string }> = ({
678 n,
679 title,
680 desc,
681}) => (
682 <div class="landing-walk-step">
683 <div class="landing-walk-num">{n}</div>
684 <h3 class="landing-walk-title">{title}</h3>
685 <p class="landing-walk-desc">{desc}</p>
686 </div>
687);
688
689const CompareRow: FC<{
690 feature: string;
691 them: string;
692 us: string;
693 highlight?: boolean;
694}> = ({ feature, them, us, highlight }) => (
695 <div class={`landing-compare-row${highlight ? " landing-compare-hl" : ""}`}>
696 <div class="landing-compare-feature">{feature}</div>
697 <div class="landing-compare-them">{them}</div>
698 <div class="landing-compare-us">{us === "✓" ? "✓" : us}</div>
699 </div>
700);
701
702const PricingCard: FC<{
703 tier: string;
704 price: string;
705 cadence: string;
706 desc: string;
707 features: string[];
708 cta: string;
709 href: string;
710 highlight?: boolean;
711}> = ({ tier, price, cadence, desc, features, cta, href, highlight }) => (
712 <div class={`landing-price-card${highlight ? " landing-price-hl" : ""}`}>
713 {highlight && <div class="landing-price-badge">Most popular</div>}
714 <div class="landing-price-tier">{tier}</div>
715 <div class="landing-price-amount">
716 <span class="landing-price-num">{price}</span>
717 <span class="landing-price-cad">{cadence}</span>
718 </div>
719 <p class="landing-price-desc">{desc}</p>
720 <ul class="landing-price-features">
721 {features.map((f) => (
722 <li>
723 <span class="landing-price-check" aria-hidden="true">{"✓"}</span>
724 {f}
725 </li>
726 ))}
727 </ul>
728 <a
729 href={href}
730 class={`btn ${highlight ? "btn-primary" : "btn-secondary"} btn-block landing-price-cta`}
731 >
732 {cta}
733 </a>
734 </div>
735);
736
52ad8b1Claude737// ─────────────────────────────────────────────────────────────────
738// Block L4 — social-proof tile builder.
739//
740// Pure: takes the cached PublicStats payload and emits the six
741// landing-page tiles in render order. Exported so tests / future
742// surfaces (dashboard, /about, …) can share the exact same copy.
743// ─────────────────────────────────────────────────────────────────
744
745export interface SocialProofTile {
746 label: string;
747 value: number;
748 prefix?: string;
749 suffix?: string;
750}
751
752export function buildSocialProofTiles(s: PublicStats): SocialProofTile[] {
753 return [
754 { label: "Public repos", value: s.totalPublicRepos },
755 { label: "Developers", value: s.totalUsers },
756 {
757 label: "PRs auto-merged this week",
758 value: s.weeklyPrsAutoMerged,
759 },
760 {
761 label: "Issues built by AI this week",
762 value: s.weeklyIssuesBuiltByAi,
763 },
764 {
765 label: "Deploys shipped this week",
766 value: s.weeklyDeploysShipped,
767 },
768 {
769 label: "Hours saved this week",
770 // Round to whole hours for the tile — the precise 0.1 figure
771 // lives on the dashboard widget; the marketing surface keeps
772 // the number scannable.
773 value: Math.round(s.weeklyHoursSaved),
774 prefix: "~",
775 suffix: "h",
776 },
777 ];
778}
779
534f04aClaude780// ─────────────────────────────────────────────────────────────────
781// Block M1 — Live-now demo feed.
782//
783// A 4-tile row inserted between the hero and the L4 counters tile
784// section, surfacing real autopilot activity from the seeded `demo`
785// owner's repos:
786// 1. Issues queued for AI build (ai:build label, open)
787// 2. PRs auto-merged in the last 24h
788// 3. AI reviews posted today (count + latest 3)
789// 4. Combined activity feed (last 10)
790//
791// SSR-renders an initial snapshot, then re-fetches every 30s via the
792// L3 JSON endpoints so the page feels alive without a websocket.
793// Pure presentational; the route layer owns the DB reads.
794// ─────────────────────────────────────────────────────────────────
795
796/**
797 * Render an ISO timestamp (or Date) as a coarse "about N units ago"
798 * string. Tolerates strings, Dates, NaN, and future timestamps.
799 *
800 * Exported for unit testing.
801 */
802export function relativeTimeFromNow(
803 value: string | Date | number | null | undefined,
804 now: number = Date.now()
805): string {
806 if (value === null || value === undefined) return "just now";
807 let t: number;
808 if (value instanceof Date) {
809 t = value.getTime();
810 } else if (typeof value === "number") {
811 t = value;
812 } else {
813 t = new Date(value).getTime();
814 }
815 if (!Number.isFinite(t)) return "just now";
816 const delta = now - t;
817 // Future timestamps (clock skew) — treat as "just now" rather than
818 // surfacing a confusing negative.
819 if (delta < 0) return "just now";
820 const s = Math.floor(delta / 1000);
821 if (s < 60) return "just now";
822 const m = Math.floor(s / 60);
823 if (m < 60) return `about ${m} minute${m === 1 ? "" : "s"} ago`;
824 const h = Math.floor(m / 60);
825 if (h < 24) return `about ${h} hour${h === 1 ? "" : "s"} ago`;
826 const d = Math.floor(h / 24);
827 return `about ${d} day${d === 1 ? "" : "s"} ago`;
828}
829
830function feedEntryId(e: LandingLiveFeedEntry): string {
831 const at = e.at instanceof Date ? e.at.toISOString() : String(e.at);
832 return `${e.kind}|${e.repo}|${e.ref.type}|${e.ref.number}|${at}`;
833}
834
835function feedEntryLabel(kind: LandingLiveFeedEntry["kind"]): string {
836 switch (kind) {
837 case "auto_merge.merged":
838 return "auto-merged";
839 case "ai_build.dispatched":
840 return "AI-build queued";
841 case "ai_review.posted":
842 return "AI review posted";
843 }
844}
845
846interface LiveNowSectionProps {
847 queued: LandingLiveFeedQueued[];
848 merges: LandingLiveFeedMerge[];
849 reviews: LandingLiveFeedReview[];
850 reviewCount: number;
851 feed: LandingLiveFeedEntry[];
852}
853
854const LiveNowSection: FC<LiveNowSectionProps> = ({
855 queued,
856 merges,
857 reviews,
858 reviewCount,
859 feed,
860}) => {
861 return (
862 <section class="landing-livenow" aria-labelledby="landing-livenow-h">
863 <div class="landing-livenow-head">
864 <div class="landing-livenow-eyebrow">
865 <span class="landing-livenow-pulse" aria-hidden="true" />
866 Live now
867 </div>
868 <h2 id="landing-livenow-h" class="landing-livenow-title">
869 Claude is working on demo repos as you read this.
870 </h2>
871 <p class="landing-livenow-sub">
872 Every card below is real data from the public{" "}
873 <code>{DEMO_USERNAME}/*</code> repos. Refreshes every 30 seconds.
874 </p>
875 </div>
876
877 <div class="landing-livenow-grid" data-livenow-grid>
878 {/* Card 1 — queued issues */}
879 <article class="landing-livecard" aria-labelledby="lc-queued-h">
880 <header class="landing-livecard-head">
881 <span class="landing-livecard-dot" aria-hidden="true" />
882 <h3 id="lc-queued-h" class="landing-livecard-title">
883 Issues queued for AI
884 </h3>
885 </header>
886 <ul class="landing-livecard-list" data-livecard="queued">
887 {queued.length === 0 ? (
888 <li class="landing-livecard-empty">
889 No queued AI builds — quiet right now.
890 </li>
891 ) : (
892 queued.slice(0, 3).map((i) => (
893 <li
894 class="landing-livecard-row"
895 data-row-id={`queued|${i.repo}|${i.number}`}
896 >
897 <a
898 class="landing-livecard-link"
899 href={`/${DEMO_USERNAME}/${i.repo}/issues/${i.number}`}
900 >
901 <span class="landing-livecard-num">#{i.number}</span>{" "}
902 <span class="landing-livecard-title-text">{i.title}</span>
903 </a>
904 <div class="landing-livecard-meta">
905 <span class="landing-livecard-repo">{i.repo}</span>
906 </div>
907 </li>
908 ))
909 )}
910 </ul>
911 </article>
912
913 {/* Card 2 — recently merged */}
914 <article class="landing-livecard" aria-labelledby="lc-merges-h">
915 <header class="landing-livecard-head">
916 <span class="landing-livecard-dot" aria-hidden="true" />
917 <h3 id="lc-merges-h" class="landing-livecard-title">
918 Recently merged by AI
919 </h3>
920 </header>
921 <ul class="landing-livecard-list" data-livecard="merges">
922 {merges.length === 0 ? (
923 <li class="landing-livecard-empty">
924 No auto-merges in the last 24h.
925 </li>
926 ) : (
927 merges.slice(0, 3).map((m) => (
928 <li
929 class="landing-livecard-row"
930 data-row-id={`merges|${m.repo}|${m.number}`}
931 >
932 <a
933 class="landing-livecard-link"
934 href={`/${DEMO_USERNAME}/${m.repo}/pulls/${m.number}`}
935 >
936 <span class="landing-livecard-num">#{m.number}</span>{" "}
937 <span class="landing-livecard-title-text">{m.title}</span>
938 </a>
939 <div class="landing-livecard-meta">
940 AI merged in{" "}
941 <span class="landing-livecard-repo">{m.repo}</span>{" "}
942 <span
943 class="landing-livecard-rel"
944 data-rel={
945 m.mergedAt instanceof Date
946 ? m.mergedAt.toISOString()
947 : String(m.mergedAt)
948 }
949 >
950 {relativeTimeFromNow(m.mergedAt)}
951 </span>
952 </div>
953 </li>
954 ))
955 )}
956 </ul>
957 </article>
958
959 {/* Card 3 — AI reviews */}
960 <article class="landing-livecard" aria-labelledby="lc-reviews-h">
961 <header class="landing-livecard-head">
962 <span class="landing-livecard-dot" aria-hidden="true" />
963 <h3 id="lc-reviews-h" class="landing-livecard-title">
964 AI reviews posted
965 </h3>
966 </header>
967 <div class="landing-livecard-bignum">
968 <span
969 class="landing-livecard-bignum-n"
970 data-livecard-count="reviews"
971 data-tick-target={String(reviewCount)}
972 >
973 {reviewCount.toLocaleString()}
974 </span>
975 <span class="landing-livecard-bignum-label">reviews today</span>
976 </div>
977 <ul class="landing-livecard-list" data-livecard="reviews">
978 {reviews.length === 0 ? (
979 <li class="landing-livecard-empty">
980 No AI reviews in the last 24h.
981 </li>
982 ) : (
983 reviews.slice(0, 3).map((r) => (
984 <li
985 class="landing-livecard-row"
986 data-row-id={`reviews|${r.repo}|${r.prNumber}`}
987 >
988 <a
989 class="landing-livecard-link"
990 href={`/${DEMO_USERNAME}/${r.repo}/pulls/${r.prNumber}`}
991 >
992 <span class="landing-livecard-num">#{r.prNumber}</span>{" "}
993 <span class="landing-livecard-snippet">
994 {r.commentSnippet}
995 </span>
996 </a>
997 <div class="landing-livecard-meta">
998 <span class="landing-livecard-repo">{r.repo}</span>
999 </div>
1000 </li>
1001 ))
1002 )}
1003 </ul>
1004 </article>
1005
1006 {/* Card 4 — activity feed */}
1007 <article class="landing-livecard" aria-labelledby="lc-feed-h">
1008 <header class="landing-livecard-head">
1009 <span class="landing-livecard-dot" aria-hidden="true" />
1010 <h3 id="lc-feed-h" class="landing-livecard-title">
1011 Activity feed
1012 </h3>
1013 </header>
1014 <ul class="landing-livecard-list landing-livecard-feed" data-livecard="feed">
1015 {feed.length === 0 ? (
1016 <li class="landing-livecard-empty">
1017 Quiet right now — check back in a minute.
1018 </li>
1019 ) : (
1020 feed.slice(0, 10).map((e) => {
1021 const path = e.ref.type === "pr" ? "pulls" : "issues";
1022 const id = feedEntryId(e);
1023 return (
1024 <li class="landing-livecard-feedrow" data-row-id={id}>
1025 <span
1026 class={`landing-livecard-kind landing-livecard-kind-${e.kind.replace(/\./g, "-")}`}
1027 >
1028 {feedEntryLabel(e.kind)}
1029 </span>{" "}
1030 <a
1031 class="landing-livecard-link"
1032 href={`/${DEMO_USERNAME}/${e.repo}/${path}/${e.ref.number}`}
1033 >
1034 {e.repo} #{e.ref.number}
1035 </a>{" "}
1036 <span
1037 class="landing-livecard-rel"
1038 data-rel={
1039 e.at instanceof Date ? e.at.toISOString() : String(e.at)
1040 }
1041 >
1042 {relativeTimeFromNow(e.at)}
1043 </span>
1044 </li>
1045 );
1046 })
1047 )}
1048 </ul>
1049 </article>
1050 </div>
1051
1052 <div class="landing-livenow-cta">
1053 <span class="landing-livenow-cta-text">
1054 Want this for your repos?
1055 </span>
1056 <a class="landing-livenow-cta-link" href="/register">
1057 Sign up free
1058 <span class="landing-cta-arrow" aria-hidden="true">{"→"}</span>
1059 </a>
1060 <span class="landing-livenow-cta-sep" aria-hidden="true">·</span>
1061 <a class="landing-livenow-cta-link" href="/demo">
1062 Try the live demo
1063 <span class="landing-cta-arrow" aria-hidden="true">{"→"}</span>
1064 </a>
1065 </div>
1066
1067 <script dangerouslySetInnerHTML={{ __html: liveNowJs }} />
1068 </section>
1069 );
1070};
1071
1072// Inline poller. Plain JS so we don't ship a separate bundle. Hits the
1073// four L3 JSON endpoints every 30s, re-renders the four cards, ticks
1074// the big number, refreshes relative timestamps, flashes new rows.
1075const liveNowJs = `
1076(function(){
1077try{
1078 var DEMO=${JSON.stringify(DEMO_USERNAME)};
1079 var INTERVAL=30000;
1080 function esc(s){return String(s==null?'':s).replace(/[&<>"']/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c];});}
1081 function rel(v){
1082 if(v==null) return 'just now';
1083 var t=(v instanceof Date)?v.getTime():(typeof v==='number'?v:new Date(v).getTime());
1084 if(!isFinite(t)) return 'just now';
1085 var d=Date.now()-t;
1086 if(d<0) return 'just now';
1087 var s=Math.floor(d/1000);
1088 if(s<60) return 'just now';
1089 var m=Math.floor(s/60);
1090 if(m<60) return 'about '+m+' minute'+(m===1?'':'s')+' ago';
1091 var h=Math.floor(m/60);
1092 if(h<24) return 'about '+h+' hour'+(h===1?'':'s')+' ago';
1093 var dd=Math.floor(h/24);
1094 return 'about '+dd+' day'+(dd===1?'':'s')+' ago';
1095 }
1096 function tickNumber(el,target){
1097 if(!el) return;
1098 var start=parseInt(el.getAttribute('data-tick-current')||'0',10)||0;
1099 if(start===target){el.textContent=target.toLocaleString();el.setAttribute('data-tick-current',String(target));return;}
1100 var dur=800,t0=performance.now();
1101 function step(now){
1102 var p=Math.min(1,(now-t0)/dur);
1103 var eased=1-Math.pow(1-p,3);
1104 var v=Math.round(start+(target-start)*eased);
1105 el.textContent=v.toLocaleString();
1106 if(p<1) requestAnimationFrame(step); else el.setAttribute('data-tick-current',String(target));
1107 }
1108 requestAnimationFrame(step);
1109 }
1110 function flashRow(li){
1111 if(!li) return;
1112 li.classList.add('landing-livecard-flash');
1113 setTimeout(function(){li.classList.remove('landing-livecard-flash');},1100);
1114 }
1115 function diffMount(ul,newHtml,newIds){
1116 if(!ul) return;
1117 var prev={};
1118 var nodes=ul.querySelectorAll('[data-row-id]');
1119 for(var i=0;i<nodes.length;i++){prev[nodes[i].getAttribute('data-row-id')]=true;}
1120 ul.innerHTML=newHtml;
1121 var fresh=ul.querySelectorAll('[data-row-id]');
1122 for(var j=0;j<fresh.length;j++){
1123 var id=fresh[j].getAttribute('data-row-id');
1124 if(id && !prev[id]) flashRow(fresh[j]);
1125 }
1126 }
1127 function pollQueued(){
1128 return fetch('/api/v2/demo/queued',{credentials:'omit'}).then(function(r){return r.json();}).then(function(d){
1129 var ul=document.querySelector('[data-livecard="queued"]');if(!ul) return;
1130 var items=(d&&d.items)||[];
1131 if(items.length===0){ul.innerHTML='<li class="landing-livecard-empty">No queued AI builds — quiet right now.</li>';return;}
1132 var ids=[];
1133 var html=items.slice(0,3).map(function(i){
1134 var id='queued|'+i.repo+'|'+i.number;ids.push(id);
1135 return '<li class="landing-livecard-row" data-row-id="'+esc(id)+'">'+
1136 '<a class="landing-livecard-link" href="/'+esc(DEMO)+'/'+esc(i.repo)+'/issues/'+i.number+'">'+
1137 '<span class="landing-livecard-num">#'+i.number+'</span> '+
1138 '<span class="landing-livecard-title-text">'+esc(i.title)+'</span></a>'+
1139 '<div class="landing-livecard-meta"><span class="landing-livecard-repo">'+esc(i.repo)+'</span></div></li>';
1140 }).join('');
1141 diffMount(ul,html,ids);
1142 }).catch(function(){});
1143 }
1144 function pollMerges(){
1145 return fetch('/api/v2/demo/merges',{credentials:'omit'}).then(function(r){return r.json();}).then(function(d){
1146 var ul=document.querySelector('[data-livecard="merges"]');if(!ul) return;
1147 var items=(d&&d.items)||[];
1148 if(items.length===0){ul.innerHTML='<li class="landing-livecard-empty">No auto-merges in the last 24h.</li>';return;}
1149 var ids=[];
1150 var html=items.slice(0,3).map(function(m){
1151 var id='merges|'+m.repo+'|'+m.number;ids.push(id);
1152 return '<li class="landing-livecard-row" data-row-id="'+esc(id)+'">'+
1153 '<a class="landing-livecard-link" href="/'+esc(DEMO)+'/'+esc(m.repo)+'/pulls/'+m.number+'">'+
1154 '<span class="landing-livecard-num">#'+m.number+'</span> '+
1155 '<span class="landing-livecard-title-text">'+esc(m.title)+'</span></a>'+
1156 '<div class="landing-livecard-meta">AI merged in <span class="landing-livecard-repo">'+esc(m.repo)+'</span> '+
1157 '<span class="landing-livecard-rel" data-rel="'+esc(m.mergedAt)+'">'+esc(rel(m.mergedAt))+'</span></div></li>';
1158 }).join('');
1159 diffMount(ul,html,ids);
1160 }).catch(function(){});
1161 }
1162 function pollReviews(){
1163 return fetch('/api/v2/demo/reviews',{credentials:'omit'}).then(function(r){return r.json();}).then(function(d){
1164 var ul=document.querySelector('[data-livecard="reviews"]');if(!ul) return;
1165 var bigEl=document.querySelector('[data-livecard-count="reviews"]');
1166 var n=(d&&typeof d.count==='number')?d.count:0;
1167 if(bigEl) tickNumber(bigEl,n);
1168 var items=(d&&d.items)||[];
1169 if(items.length===0){ul.innerHTML='<li class="landing-livecard-empty">No AI reviews in the last 24h.</li>';return;}
1170 var ids=[];
1171 var html=items.slice(0,3).map(function(r){
1172 var id='reviews|'+r.repo+'|'+r.prNumber;ids.push(id);
1173 return '<li class="landing-livecard-row" data-row-id="'+esc(id)+'">'+
1174 '<a class="landing-livecard-link" href="/'+esc(DEMO)+'/'+esc(r.repo)+'/pulls/'+r.prNumber+'">'+
1175 '<span class="landing-livecard-num">#'+r.prNumber+'</span> '+
1176 '<span class="landing-livecard-snippet">'+esc(r.commentSnippet)+'</span></a>'+
1177 '<div class="landing-livecard-meta"><span class="landing-livecard-repo">'+esc(r.repo)+'</span></div></li>';
1178 }).join('');
1179 diffMount(ul,html,ids);
1180 }).catch(function(){});
1181 }
1182 function pollFeed(){
1183 return fetch('/api/v2/demo/activity',{credentials:'omit'}).then(function(r){return r.json();}).then(function(d){
1184 var ul=document.querySelector('[data-livecard="feed"]');if(!ul) return;
1185 var entries=(d&&d.entries)||[];
1186 if(entries.length===0){ul.innerHTML='<li class="landing-livecard-empty">Quiet right now — check back in a minute.</li>';return;}
1187 var ids=[];
1188 var html=entries.slice(0,10).map(function(e){
1189 var path=(e.ref&&e.ref.type==='pr')?'pulls':'issues';
1190 var num=(e.ref&&e.ref.number)||0;
1191 var label=e.kind==='auto_merge.merged'?'auto-merged':(e.kind==='ai_build.dispatched'?'AI-build queued':'AI review posted');
1192 var kindCls=String(e.kind||'').replace(/\\./g,'-');
1193 var id=e.kind+'|'+e.repo+'|'+(e.ref&&e.ref.type)+'|'+num+'|'+e.at;ids.push(id);
1194 return '<li class="landing-livecard-feedrow" data-row-id="'+esc(id)+'">'+
1195 '<span class="landing-livecard-kind landing-livecard-kind-'+esc(kindCls)+'">'+esc(label)+'</span> '+
1196 '<a class="landing-livecard-link" href="/'+esc(DEMO)+'/'+esc(e.repo)+'/'+path+'/'+num+'">'+esc(e.repo)+' #'+num+'</a> '+
1197 '<span class="landing-livecard-rel" data-rel="'+esc(e.at)+'">'+esc(rel(e.at))+'</span></li>';
1198 }).join('');
1199 diffMount(ul,html,ids);
1200 }).catch(function(){});
1201 }
1202 function refreshRel(){
1203 var spans=document.querySelectorAll('.landing-livecard-rel[data-rel]');
1204 for(var i=0;i<spans.length;i++){
1205 spans[i].textContent=rel(spans[i].getAttribute('data-rel'));
1206 }
1207 }
1208 function tickAll(){pollQueued();pollMerges();pollReviews();pollFeed();}
1209 // Initial counter tick (count-up from 0) on first paint.
1210 var bigEl0=document.querySelector('[data-livecard-count="reviews"]');
1211 if(bigEl0){
1212 var target=parseInt(bigEl0.getAttribute('data-tick-target')||'0',10)||0;
1213 bigEl0.textContent='0';
1214 tickNumber(bigEl0,target);
1215 }
1216 refreshRel();
1217 setInterval(tickAll,INTERVAL);
1218 setInterval(refreshRel,INTERVAL);
1219 document.addEventListener('visibilitychange',function(){
1220 if(document.visibilityState==='visible'){tickAll();refreshRel();}
1221 });
1222}catch(_){}})();
1223`.trim();
1224
4c47454Claude1225// Backwards-compatible default — web.tsx imports `LandingPage`.
1226export const LandingPage: FC<LandingPageProps> = (props) => (
1227 <LandingHero {...props} />
2b821b7Claude1228);
1229
4c47454Claude1230export default LandingPage;
1231
2b821b7Claude1232const landingCss = `
958d26aClaude1233 /* ============================================================ */
1234 /* Landing — Editorial-Technical 2026.05 */
1235 /* ============================================================ */
4c47454Claude1236 .landing-root {
1237 position: relative;
958d26aClaude1238 max-width: 1180px;
4c47454Claude1239 margin: 0 auto;
1240 padding: 0 16px;
958d26aClaude1241 }
1242 .landing-root > section { position: relative; }
1243
1244 /* ---------- Hero ---------- */
1245 .landing-hero {
1246 position: relative;
c475ee6Claude1247 padding: var(--s-16) 0 var(--s-20);
958d26aClaude1248 text-align: center;
4c47454Claude1249 overflow: hidden;
1250 }
c475ee6Claude1251 .landing-hero-blob-1 {
1252 animation: hero-blob-drift-1 18s var(--ease, ease) infinite alternate;
1253 }
1254 .landing-hero-blob-2 {
1255 animation: hero-blob-drift-2 22s var(--ease, ease) infinite alternate;
1256 }
1257 @keyframes hero-blob-drift-1 {
1258 0% { transform: translate(0, 0) scale(1); opacity: 0.55; }
1259 100% { transform: translate(8%, 6%) scale(1.18); opacity: 0.75; }
1260 }
1261 @keyframes hero-blob-drift-2 {
1262 0% { transform: translate(0, 0) scale(1); opacity: 0.40; }
1263 100% { transform: translate(-10%, -4%) scale(1.25); opacity: 0.60; }
1264 }
1265
1266 /* ---------- Hero product visual: live AI PR review card ---------- */
1267 .landing-hero-visual {
1268 position: relative;
1269 max-width: 760px;
1270 margin: var(--s-12) auto 0;
1271 padding: 0 16px;
1272 perspective: 1400px;
1273 z-index: 2;
1274 opacity: 0;
1275 animation: hero-visual-in 700ms var(--ease-out-expo, cubic-bezier(0.19, 1, 0.22, 1)) 400ms forwards;
1276 }
1277 @keyframes hero-visual-in {
1278 from { opacity: 0; transform: translateY(20px); }
1279 to { opacity: 1; transform: translateY(0); }
1280 }
1281 .hero-pr-card {
1282 position: relative;
1283 background: linear-gradient(180deg, rgba(15,17,26,0.96) 0%, rgba(8,9,15,0.96) 100%);
1284 border: 1px solid var(--border-strong);
1285 border-radius: var(--r-xl);
1286 overflow: hidden;
1287 text-align: left;
1288 box-shadow:
1289 0 30px 80px -20px rgba(0,0,0,0.65),
1290 0 0 0 1px rgba(140,109,255,0.18),
1291 0 0 60px -10px rgba(140,109,255,0.30);
1292 transform: rotateX(2deg) rotateY(-2deg);
1293 transition: transform 600ms var(--ease, ease);
1294 backdrop-filter: blur(12px);
1295 -webkit-backdrop-filter: blur(12px);
1296 }
1297 .landing-hero-visual:hover .hero-pr-card {
1298 transform: rotateX(0deg) rotateY(0deg);
1299 }
1300 .hero-pr-card::before {
1301 content: '';
1302 position: absolute;
1303 inset: 0;
1304 background: linear-gradient(135deg, rgba(140,109,255,0.10), transparent 35%, transparent 65%, rgba(54,197,214,0.08));
1305 pointer-events: none;
1306 }
1307 .hero-pr-header {
1308 display: flex;
1309 align-items: center;
1310 gap: 12px;
1311 padding: 14px 18px;
1312 border-bottom: 1px solid rgba(255,255,255,0.06);
1313 background: rgba(255,255,255,0.025);
1314 font-size: 13px;
1315 }
1316 .hero-pr-dot {
1317 width: 10px; height: 10px;
1318 border-radius: 50%;
1319 background: var(--green);
1320 box-shadow: 0 0 10px rgba(52,211,153,0.6);
1321 flex-shrink: 0;
1322 }
1323 .hero-pr-title {
1324 color: var(--text-strong);
1325 font-weight: 600;
1326 flex: 1;
1327 overflow: hidden;
1328 text-overflow: ellipsis;
1329 white-space: nowrap;
1330 }
1331 .hero-pr-num {
1332 color: var(--text-faint);
1333 font-family: var(--font-mono);
1334 font-weight: 500;
1335 margin-right: 8px;
1336 }
1337 .hero-pr-status {
1338 display: inline-flex;
1339 align-items: center;
1340 gap: 6px;
1341 padding: 3px 10px;
1342 border-radius: var(--r-full);
1343 background: var(--accent-gradient-faint);
1344 border: 1px solid rgba(140,109,255,0.30);
1345 color: var(--accent);
1346 font-family: var(--font-mono);
1347 font-size: 11px;
1348 letter-spacing: 0.04em;
1349 flex-shrink: 0;
1350 }
1351 .hero-pr-status-pulse {
1352 width: 6px; height: 6px;
1353 border-radius: 50%;
1354 background: var(--accent);
1355 box-shadow: 0 0 0 0 rgba(140,109,255,0.6);
1356 animation: hero-pulse 1.6s ease-out infinite;
1357 }
1358 @keyframes hero-pulse {
1359 0% { box-shadow: 0 0 0 0 rgba(140,109,255,0.55); }
1360 70% { box-shadow: 0 0 0 8px rgba(140,109,255,0); }
1361 100% { box-shadow: 0 0 0 0 rgba(140,109,255,0); }
1362 }
1363
1364 .hero-pr-body {
1365 padding: 0;
1366 }
1367 .hero-pr-file {
1368 display: flex;
1369 align-items: center;
1370 gap: 10px;
1371 padding: 10px 18px;
1372 border-bottom: 1px solid rgba(255,255,255,0.05);
1373 font-family: var(--font-mono);
1374 font-size: 12px;
1375 background: rgba(255,255,255,0.012);
1376 }
1377 .hero-pr-file-icon { color: var(--accent-2); }
1378 .hero-pr-file-name { color: var(--text); flex: 1; }
1379 .hero-pr-file-stats { display: inline-flex; gap: 8px; }
1380 .hero-pr-add { color: var(--green); font-weight: 600; }
1381 .hero-pr-del { color: var(--red); font-weight: 600; }
1382
1383 .hero-pr-diff {
1384 padding: 12px 18px;
1385 font-family: var(--font-mono);
1386 font-feature-settings: var(--mono-feat, 'calt');
1387 font-size: 12.5px;
1388 line-height: 1.7;
1389 color: rgba(237,237,242,0.85);
1390 overflow-x: auto;
1391 }
1392 .hero-pr-hunk {
1393 color: rgba(140,109,255,0.85);
1394 background: rgba(140,109,255,0.06);
1395 padding: 2px 8px;
1396 margin: 0 -8px 4px;
1397 border-radius: 4px;
1398 }
1399 .hero-pr-line-add {
1400 background: rgba(52,211,153,0.08);
1401 color: rgba(167,243,208,0.95);
1402 padding: 0 8px;
1403 margin: 0 -8px;
1404 border-left: 2px solid var(--green);
1405 padding-left: 8px;
1406 }
1407
1408 .hero-pr-comment {
1409 margin: 14px 18px;
1410 padding: 14px 16px;
1411 background: linear-gradient(135deg, rgba(140,109,255,0.08), rgba(54,197,214,0.05));
1412 border: 1px solid rgba(140,109,255,0.25);
1413 border-radius: var(--r-md);
1414 }
1415 .hero-pr-bot-row {
1416 display: flex;
1417 align-items: center;
1418 gap: 8px;
1419 margin-bottom: 8px;
1420 font-size: 12px;
1421 }
1422 .hero-pr-bot-avatar {
1423 width: 22px; height: 22px;
1424 display: inline-flex;
1425 align-items: center;
1426 justify-content: center;
1427 border-radius: 50%;
1428 background: var(--accent-gradient);
1429 font-size: 11px;
1430 box-shadow: 0 0 12px rgba(140,109,255,0.40);
1431 }
1432 .hero-pr-bot-name {
1433 color: var(--text-strong);
1434 font-weight: 600;
1435 }
1436 .hero-pr-bot-meta {
1437 color: var(--text-faint);
1438 font-family: var(--font-mono);
1439 }
1440 .hero-pr-bot-text {
1441 color: var(--text);
1442 font-size: 13px;
1443 line-height: 1.55;
1444 margin: 0;
1445 }
1446 .hero-pr-bot-text code {
1447 background: rgba(255,255,255,0.06);
1448 border: 1px solid rgba(255,255,255,0.10);
1449 padding: 1px 6px;
1450 border-radius: 4px;
1451 font-size: 11.5px;
1452 color: var(--accent);
1453 }
1454 .hero-pr-bot-link { color: var(--accent-2); text-decoration: underline; text-decoration-style: dotted; }
1455
1456 .hero-pr-gates {
1457 display: flex;
1458 flex-wrap: wrap;
1459 gap: 6px;
1460 padding: 12px 18px 16px;
1461 border-top: 1px solid rgba(255,255,255,0.06);
1462 background: rgba(255,255,255,0.012);
1463 }
1464 .hero-pr-gate {
1465 display: inline-flex;
1466 align-items: center;
1467 gap: 6px;
1468 padding: 4px 10px;
1469 border-radius: var(--r-full);
1470 font-family: var(--font-mono);
1471 font-size: 11px;
1472 border: 1px solid;
1473 }
1474 .hero-pr-gate-pass {
1475 color: var(--green);
1476 background: rgba(52,211,153,0.08);
1477 border-color: rgba(52,211,153,0.30);
1478 }
1479 .hero-pr-gate-running {
1480 color: var(--accent);
1481 background: var(--accent-gradient-faint);
1482 border-color: rgba(140,109,255,0.40);
1483 }
1484 .hero-pr-gate-spin {
1485 width: 9px; height: 9px;
1486 border: 1.5px solid rgba(140,109,255,0.30);
1487 border-top-color: var(--accent);
1488 border-radius: 50%;
1489 animation: hero-spin 800ms linear infinite;
1490 }
1491 @keyframes hero-spin {
1492 to { transform: rotate(360deg); }
1493 }
1494
1495 /* Floating accent badges around the card */
1496 .hero-float {
1497 position: absolute;
1498 display: inline-flex;
1499 align-items: center;
1500 gap: 6px;
1501 padding: 6px 12px;
1502 background: rgba(15,17,26,0.92);
1503 border: 1px solid rgba(140,109,255,0.35);
1504 border-radius: var(--r-full);
1505 font-family: var(--font-mono);
1506 font-size: 11px;
1507 color: var(--text);
1508 box-shadow: 0 12px 24px -8px rgba(0,0,0,0.5), 0 0 18px -4px rgba(140,109,255,0.30);
1509 backdrop-filter: blur(8px);
1510 -webkit-backdrop-filter: blur(8px);
1511 }
1512 .hero-float-icon { color: var(--accent); }
1513 .hero-float-1 {
1514 top: -14px;
1515 left: -8px;
1516 animation: hero-float-bob-1 5s var(--ease, ease) infinite alternate;
1517 }
1518 .hero-float-2 {
1519 bottom: -14px;
1520 right: -8px;
1521 animation: hero-float-bob-2 6s var(--ease, ease) infinite alternate;
1522 }
1523 @keyframes hero-float-bob-1 {
1524 from { transform: translate(0, 0); }
1525 to { transform: translate(-8px, -10px); }
1526 }
1527 @keyframes hero-float-bob-2 {
1528 from { transform: translate(0, 0); }
1529 to { transform: translate(8px, 8px); }
1530 }
1531
1532 @media (max-width: 720px) {
1533 .landing-hero-visual { padding: 0 8px; }
1534 .hero-pr-card { transform: none; }
1535 .hero-pr-title { font-size: 12px; }
1536 .hero-pr-diff { font-size: 11px; line-height: 1.6; }
1537 .hero-float { display: none; }
1538 }
958d26aClaude1539 .landing-hero-bg {
1540 position: absolute;
1541 inset: -10% -20%;
1542 pointer-events: none;
1543 z-index: 0;
4c47454Claude1544 }
958d26aClaude1545 .landing-hero-blob {
1546 position: absolute;
1547 border-radius: 50%;
1548 filter: blur(80px);
1549 opacity: 0.55;
1550 }
1551 .landing-hero-blob-1 {
1552 top: -10%;
1553 left: 30%;
1554 width: 480px;
1555 height: 480px;
1556 background: radial-gradient(circle, rgba(140,109,255,0.55), transparent 65%);
1557 }
1558 .landing-hero-blob-2 {
1559 top: 10%;
1560 left: 50%;
1561 width: 380px;
1562 height: 380px;
1563 background: radial-gradient(circle, rgba(54,197,214,0.40), transparent 65%);
1564 }
1565 .landing-hero-grid {
4c47454Claude1566 position: absolute;
1567 inset: 0;
958d26aClaude1568 background-image:
1569 linear-gradient(to right, rgba(255,255,255,0.04) 1px, transparent 1px),
1570 linear-gradient(to bottom, rgba(255,255,255,0.04) 1px, transparent 1px);
1571 background-size: 60px 60px;
1572 mask-image: radial-gradient(ellipse 50% 50% at 50% 30%, #000 0%, transparent 75%);
1573 -webkit-mask-image: radial-gradient(ellipse 50% 50% at 50% 30%, #000 0%, transparent 75%);
1574 }
1575 :root[data-theme='light'] .landing-hero-grid {
1576 background-image:
1577 linear-gradient(to right, rgba(15,16,28,0.06) 1px, transparent 1px),
1578 linear-gradient(to bottom, rgba(15,16,28,0.06) 1px, transparent 1px);
4c47454Claude1579 }
1580
958d26aClaude1581 .landing-hero-inner {
1582 position: relative;
1583 z-index: 1;
1584 max-width: 960px;
2b821b7Claude1585 margin: 0 auto;
1586 }
958d26aClaude1587 .landing-hero-eyebrow {
1588 margin: 0 auto var(--s-6);
1589 color: var(--accent);
1590 }
1591 .landing-hero-eyebrow::before { display: none; }
1592 .landing-hero-pulse {
1593 width: 7px;
1594 height: 7px;
1595 border-radius: 50%;
1596 background: var(--accent);
1597 box-shadow: 0 0 0 0 rgba(140,109,255,0.6);
1598 animation: pulse 1.8s ease-out infinite;
1599 flex-shrink: 0;
1600 }
1601 @keyframes pulse {
1602 0% { box-shadow: 0 0 0 0 rgba(140,109,255,0.55); }
1603 70% { box-shadow: 0 0 0 10px rgba(140,109,255,0); }
1604 100% { box-shadow: 0 0 0 0 rgba(140,109,255,0); }
1605 }
1606
2b821b7Claude1607 .landing-hero-title {
93fe97eClaude1608 font-size: clamp(32px, 5.5vw, 64px);
1609 line-height: 1.05;
1610 letter-spacing: -0.025em;
c963db5Claude1611 font-weight: 700;
1612 margin: 0 0 var(--s-7);
958d26aClaude1613 color: var(--text-strong);
2b821b7Claude1614 }
c963db5Claude1615 .landing-hero-title .gradient-text {
1616 background-image: linear-gradient(135deg, #a48bff 0%, #8c6dff 50%, #6d4dff 100%);
1617 -webkit-background-clip: text;
1618 background-clip: text;
1619 -webkit-text-fill-color: transparent;
1620 color: transparent;
1621 }
958d26aClaude1622
2b821b7Claude1623 .landing-hero-sub {
958d26aClaude1624 font-size: clamp(15px, 1.6vw, 19px);
2b821b7Claude1625 color: var(--text-muted);
958d26aClaude1626 max-width: 680px;
1627 margin: 0 auto;
4c47454Claude1628 line-height: 1.55;
958d26aClaude1629 letter-spacing: -0.005em;
2b821b7Claude1630 }
4c47454Claude1631
2b821b7Claude1632 .landing-hero-ctas {
1633 display: flex;
958d26aClaude1634 gap: 12px;
2b821b7Claude1635 justify-content: center;
1636 flex-wrap: wrap;
958d26aClaude1637 margin-top: var(--s-10);
2b821b7Claude1638 }
958d26aClaude1639 .landing-cta-arrow {
1640 transition: transform var(--t-base) var(--ease-spring);
1641 display: inline-block;
2b821b7Claude1642 }
cd4f63bTest User1643
1644 /* Block Q3 — tertiary "try it without signing up" link.
1645 Sits under the primary CTA row as a small, low-contrast text link
1646 so it doesn't crowd the main calls-to-action. */
1647 .landing-hero-play {
1648 margin-top: 14px;
1649 text-align: center;
1650 }
1651 .landing-hero-play-link {
1652 font-size: 13px;
1653 color: var(--text-muted);
1654 text-decoration: none;
1655 border-bottom: 1px dashed transparent;
1656 padding-bottom: 1px;
1657 transition: color var(--t-base), border-color var(--t-base);
1658 }
1659 .landing-hero-play-link:hover {
1660 color: var(--text-strong, var(--text));
1661 border-bottom-color: var(--text-muted);
1662 }
958d26aClaude1663 .btn:hover .landing-cta-arrow,
1664 .landing-cta-primary:hover .landing-cta-arrow {
1665 transform: translateX(4px);
8e9f1d9Claude1666 }
2b821b7Claude1667
93fe97eClaude1668 /* BLOCK Q1 — flagship "Add to Claude Desktop" CTA.
1669 Gradient-bordered + accent text so it reads as a peer of the primary
3a6c5ecTest User1670 Sign-up CTA, not a third secondary. Theme-aware: inner fill uses
1671 --bg-elevated so it's white on light and dark on dark, never the
1672 jarring near-black on white we shipped first time. Subtle elevation
1673 on hover; static when the visitor opts out of motion. */
93fe97eClaude1674 .landing-cta-dxt {
1675 position: relative;
3a6c5ecTest User1676 background: var(--bg-elevated);
1677 color: var(--text-strong);
93fe97eClaude1678 border: 1px solid transparent;
1679 background-image:
3a6c5ecTest User1680 linear-gradient(var(--bg-elevated), var(--bg-elevated)),
93fe97eClaude1681 linear-gradient(90deg, #8c6dff 0%, #36c5d6 100%);
1682 background-origin: border-box;
1683 background-clip: padding-box, border-box;
1684 transition: transform var(--t-base, 180ms) var(--ease-spring, ease),
1685 box-shadow var(--t-base, 180ms) var(--ease-spring, ease);
1686 }
1687 .landing-cta-dxt:hover {
1688 transform: translateY(-2px);
1689 box-shadow: 0 8px 24px -8px rgba(140, 109, 255, 0.45);
1690 }
1691 @media (prefers-reduced-motion: reduce) {
1692 .landing-cta-dxt,
1693 .landing-cta-dxt:hover {
1694 transform: none;
1695 transition: none;
1696 }
1697 }
1698
5f2e749Claude1699 /* L8 — free-tier reassurance link beneath the CTA row. */
1700 .landing-hero-freenote {
1701 margin-top: var(--s-5);
1702 font-size: var(--t-sm);
1703 color: var(--text-muted);
1704 text-align: center;
1705 }
1706 .landing-hero-freenote-link {
1707 color: var(--accent);
1708 text-decoration: none;
1709 font-weight: 500;
1710 border-bottom: 1px dotted rgba(140,109,255,0.4);
1711 transition: color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
1712 }
1713 .landing-hero-freenote-link:hover {
1714 color: var(--text-strong);
1715 border-bottom-color: var(--accent);
1716 }
1717
4c47454Claude1718 .landing-hero-caption {
958d26aClaude1719 margin-top: var(--s-8);
1720 font-size: var(--t-sm);
4c47454Claude1721 color: var(--text-muted);
958d26aClaude1722 display: flex;
1723 flex-wrap: wrap;
1724 align-items: center;
1725 justify-content: center;
1726 gap: 12px;
2b821b7Claude1727 }
958d26aClaude1728 .landing-hero-cmd {
1729 display: inline-flex;
1730 align-items: center;
1731 gap: 4px;
1732 padding: 6px 12px;
1733 background: var(--bg-elevated);
4c47454Claude1734 border: 1px solid var(--border);
958d26aClaude1735 border-radius: var(--r-full);
1736 box-shadow: var(--elev-1);
1737 }
1738 .landing-hero-cmd .kbd {
1739 border: 0;
1740 background: transparent;
1741 padding: 0 4px;
1742 color: var(--text-muted);
1743 font-size: 12px;
1744 }
1745 .landing-hero-cmd .kbd:nth-last-of-type(1) { color: var(--accent); }
1746 .landing-hero-arrow {
1747 color: var(--text-faint);
1748 font-size: 13px;
1749 margin: 0 2px;
2b821b7Claude1750 }
4c47454Claude1751
1752 .landing-stats {
958d26aClaude1753 margin-top: var(--s-7);
1754 font-family: var(--font-mono);
1755 font-size: 12px;
1756 color: var(--text-muted);
1757 display: flex;
1758 align-items: center;
1759 justify-content: center;
1760 gap: 10px;
1761 flex-wrap: wrap;
1762 letter-spacing: 0.02em;
1763 }
1764 .landing-stats strong {
1765 color: var(--text-strong);
1766 font-weight: 600;
1767 font-feature-settings: 'tnum';
1768 }
1769 .landing-stats-sep { opacity: 0.4; }
1770
c963db5Claude1771 /* ---------- Capability grid (crontech-style uppercase tracked) ---------- */
1772 .landing-caps {
1773 margin: var(--s-12) auto var(--s-16);
1774 max-width: 1080px;
1775 padding: var(--s-7) var(--s-4);
958d26aClaude1776 border-top: 1px solid var(--border-subtle);
1777 border-bottom: 1px solid var(--border-subtle);
c963db5Claude1778 }
1779 .landing-caps-grid {
1780 display: grid;
1781 grid-template-columns: repeat(4, 1fr);
1782 gap: 24px 16px;
958d26aClaude1783 text-align: center;
1784 }
c963db5Claude1785 .landing-cap {
958d26aClaude1786 font-family: var(--font-mono);
1787 font-size: 11px;
c963db5Claude1788 font-weight: 600;
958d26aClaude1789 text-transform: uppercase;
c963db5Claude1790 letter-spacing: 0.16em;
1791 color: var(--text-muted);
1792 transition: color var(--t-fast) var(--ease);
958d26aClaude1793 }
c963db5Claude1794 .landing-cap:hover { color: var(--text-strong); }
1795 @media (max-width: 800px) {
1796 .landing-caps-grid { grid-template-columns: repeat(2, 1fr); gap: 18px 12px; }
1797 }
1798 @media (max-width: 480px) {
1799 .landing-caps-grid { grid-template-columns: 1fr; }
1800 }
1801
1802 /* ---------- Big stat row (crontech-style hero closer) ---------- */
1803 .landing-bigstats {
1804 margin: var(--s-10) auto var(--s-20);
1805 max-width: 1180px;
1806 padding: 0 var(--s-4);
1807 }
1808 .landing-bigstats-grid {
1809 display: grid;
1810 grid-template-columns: repeat(4, 1fr);
1811 gap: 32px;
1812 text-align: left;
958d26aClaude1813 }
c963db5Claude1814 .landing-bigstat {
1815 padding: var(--s-2) 0;
1816 }
1817 .landing-bigstat-num {
958d26aClaude1818 font-family: var(--font-display);
c963db5Claude1819 font-size: clamp(28px, 3.5vw, 44px);
1820 line-height: 1.05;
1821 letter-spacing: -0.03em;
1822 font-weight: 700;
1823 color: var(--text-strong);
1824 margin-bottom: var(--s-2);
1825 }
1826 .landing-bigstat-label {
1827 font-family: var(--font-mono);
1828 font-size: 11px;
958d26aClaude1829 font-weight: 500;
c963db5Claude1830 text-transform: uppercase;
1831 letter-spacing: 0.14em;
1832 color: var(--text-faint);
2b821b7Claude1833 }
c963db5Claude1834 @media (max-width: 800px) {
1835 .landing-bigstats-grid { grid-template-columns: repeat(2, 1fr); gap: 28px 16px; }
1836 }
1837 @media (max-width: 480px) {
1838 .landing-bigstats-grid { grid-template-columns: 1fr; gap: 24px; }
958d26aClaude1839 }
1840
1841 /* ---------- Section base ---------- */
1842 .landing-section { margin: var(--s-20) auto; }
2b821b7Claude1843
4c47454Claude1844 /* ---------- Feature grid ---------- */
1845 .landing-features {
2b821b7Claude1846 display: grid;
1847 grid-template-columns: repeat(3, 1fr);
958d26aClaude1848 gap: 16px;
2b821b7Claude1849 }
1850 .landing-feature {
958d26aClaude1851 background: var(--bg-elevated);
2b821b7Claude1852 border: 1px solid var(--border);
958d26aClaude1853 border-radius: var(--r-lg);
1854 padding: var(--s-7);
1855 position: relative;
1856 overflow: hidden;
1857 isolation: isolate;
1858 transition:
1859 transform var(--t-base) var(--ease-out-quart),
1860 border-color var(--t-base) var(--ease),
1861 box-shadow var(--t-base) var(--ease);
1862 }
1863 .landing-feature::before {
1864 content: '';
1865 position: absolute;
1866 inset: 0;
1867 background: radial-gradient(120% 100% at 0% 0%, rgba(140,109,255,0.08), transparent 55%);
1868 opacity: 0;
1869 transition: opacity var(--t-base) var(--ease);
1870 z-index: -1;
4c47454Claude1871 }
1872 .landing-feature:hover {
958d26aClaude1873 transform: translateY(-3px);
1874 border-color: var(--border-strong);
1875 box-shadow: var(--elev-2);
2b821b7Claude1876 }
958d26aClaude1877 .landing-feature:hover::before { opacity: 1; }
2b821b7Claude1878 .landing-feature-icon {
4c47454Claude1879 display: inline-flex;
1880 align-items: center;
1881 justify-content: center;
958d26aClaude1882 width: 40px;
1883 height: 40px;
1884 border-radius: var(--r);
1885 background: var(--accent-gradient-soft);
1886 color: var(--accent);
1887 margin-bottom: var(--s-4);
1888 border: 1px solid rgba(140,109,255,0.20);
2b821b7Claude1889 }
1890 .landing-feature-title {
958d26aClaude1891 font-family: var(--font-display);
1892 font-size: 19px;
1893 font-weight: 600;
1894 letter-spacing: -0.018em;
1895 margin: 0 0 var(--s-2);
1896 color: var(--text-strong);
2b821b7Claude1897 }
1898 .landing-feature-desc {
958d26aClaude1899 font-size: var(--t-sm);
1900 color: var(--text-muted);
1901 line-height: 1.6;
1902 margin: 0;
1903 }
1904
1905 /* ---------- Walkthrough ---------- */
1906 .landing-walk-grid {
1907 display: grid;
1908 grid-template-columns: repeat(4, 1fr);
1909 gap: 16px;
1910 counter-reset: walk;
1911 }
1912 .landing-walk-step {
1913 position: relative;
1914 padding: var(--s-7) var(--s-6) var(--s-6);
1915 background: var(--bg-elevated);
1916 border: 1px solid var(--border);
1917 border-radius: var(--r-lg);
1918 }
1919 .landing-walk-step::after {
1920 content: '';
1921 position: absolute;
1922 top: 50%;
1923 right: -12px;
1924 width: 12px;
1925 height: 1px;
1926 background: var(--border-strong);
1927 }
1928 .landing-walk-step:last-child::after { display: none; }
1929 .landing-walk-num {
1930 display: inline-block;
1931 font-family: var(--font-mono);
1932 font-size: 11px;
1933 color: var(--accent);
1934 background: var(--accent-gradient-faint);
1935 border: 1px solid rgba(140,109,255,0.30);
1936 padding: 3px 8px;
1937 border-radius: var(--r-full);
1938 letter-spacing: 0.06em;
1939 margin-bottom: var(--s-3);
1940 }
1941 .landing-walk-title {
1942 font-family: var(--font-display);
1943 font-size: 22px;
1944 font-weight: 600;
1945 letter-spacing: -0.022em;
1946 margin: 0 0 var(--s-2);
1947 color: var(--text-strong);
1948 }
1949 .landing-walk-desc {
1950 font-size: var(--t-sm);
2b821b7Claude1951 color: var(--text-muted);
1952 line-height: 1.55;
1953 margin: 0;
1954 }
1955
958d26aClaude1956 /* ---------- Terminal ---------- */
1957 .landing-terminal-section { margin-top: var(--s-16); }
4c47454Claude1958 .landing-terminal-wrap {
1959 display: flex;
2b821b7Claude1960 justify-content: center;
1961 }
4c47454Claude1962 .landing-terminal {
1963 width: 100%;
958d26aClaude1964 max-width: 820px;
1965 background: linear-gradient(180deg, #0a0b12 0%, #06070c 100%);
1966 border: 1px solid var(--border-strong);
1967 border-radius: var(--r-lg);
1968 overflow: hidden;
1969 box-shadow: var(--elev-3), 0 0 60px -10px rgba(140,109,255,0.18);
4c47454Claude1970 text-align: left;
2b821b7Claude1971 }
958d26aClaude1972 :root[data-theme='light'] .landing-terminal {
1973 background: linear-gradient(180deg, #0f111a 0%, #06070c 100%);
1974 }
1975 .landing-terminal-chrome {
1976 display: flex;
1977 align-items: center;
1978 gap: 7px;
1979 padding: 11px 14px;
1980 background: rgba(255,255,255,0.025);
1981 border-bottom: 1px solid rgba(255,255,255,0.06);
1982 position: relative;
1983 }
1984 .landing-terminal-dot {
1985 width: 11px;
1986 height: 11px;
1987 border-radius: 50%;
1988 flex-shrink: 0;
1989 }
1990 .landing-terminal-dot-r { background: #ff5f57; }
1991 .landing-terminal-dot-y { background: #febc2e; }
1992 .landing-terminal-dot-g { background: #28c840; }
1993 .landing-terminal-title {
1994 position: absolute;
1995 left: 50%;
1996 transform: translateX(-50%);
1997 font-family: var(--font-mono);
1998 font-size: 11px;
1999 color: rgba(237,237,242,0.55);
2000 letter-spacing: 0.01em;
2001 }
2002 .landing-terminal-body {
2003 padding: var(--s-6) var(--s-7);
2004 font-family: var(--font-mono);
2005 font-feature-settings: var(--mono-feat);
2006 font-size: 13.5px;
2007 line-height: 1.85;
2008 color: rgba(237,237,242,0.92);
2009 }
4c47454Claude2010 .landing-term-line {
2011 display: flex;
2012 gap: 10px;
2013 white-space: pre-wrap;
2014 word-break: break-all;
2b821b7Claude2015 }
958d26aClaude2016 .landing-term-out { color: rgba(237,237,242,0.7); }
2017 .landing-term-prompt { color: rgba(140,109,255,0.85); user-select: none; flex-shrink: 0; }
2018 .landing-term-meta { color: rgba(237,237,242,0.45); }
2019 .landing-term-ok { color: var(--green); user-select: none; flex-shrink: 0; }
2020 .landing-term-ok-line { color: rgba(237,237,242,0.92); }
2021 .landing-term-cursor { margin-top: 4px; }
2022 .landing-term-blink {
2023 animation: blink 1.05s steps(2) infinite;
2024 color: var(--accent);
2025 }
2026 @keyframes blink { 50% { opacity: 0; } }
2027
2028 /* ---------- Comparison ---------- */
2029 .landing-compare {
2030 max-width: 920px;
2031 margin: 0 auto;
2032 border: 1px solid var(--border);
2033 border-radius: var(--r-lg);
2034 overflow: hidden;
2035 background: var(--bg-elevated);
2036 }
2037 .landing-compare-row {
2038 display: grid;
2039 grid-template-columns: 1fr 180px 180px;
2040 align-items: center;
2041 padding: 14px 20px;
2042 border-bottom: 1px solid var(--border-subtle);
2043 font-size: var(--t-sm);
2044 transition: background var(--t-fast) var(--ease);
2045 }
2046 .landing-compare-row:last-child { border-bottom: none; }
2047 .landing-compare-row:hover { background: var(--bg-hover); }
2048 .landing-compare-feature {
2049 color: var(--text-strong);
2050 font-weight: 500;
2051 }
2052 .landing-compare-them, .landing-compare-us {
2053 text-align: center;
2054 font-family: var(--font-mono);
2055 font-size: 12px;
2056 color: var(--text-muted);
2057 }
2058 .landing-compare-us { color: var(--green); font-weight: 500; }
2059 .landing-compare-hl .landing-compare-us {
2060 color: var(--accent);
2061 font-weight: 600;
2b821b7Claude2062 }
958d26aClaude2063 .landing-compare-hl .landing-compare-feature::after {
2064 content: 'NEW';
2065 margin-left: 8px;
2066 padding: 1px 6px;
2067 border-radius: 4px;
2068 background: var(--accent-gradient-faint);
2069 color: var(--accent);
2070 font-family: var(--font-mono);
2071 font-size: 9px;
2072 letter-spacing: 0.1em;
2073 font-weight: 600;
2074 vertical-align: 1px;
2075 }
2076 @media (max-width: 720px) {
2077 .landing-compare-row { grid-template-columns: 1fr 80px 80px; padding: 12px 14px; }
2078 .landing-compare-hl .landing-compare-feature::after { display: none; }
2079 }
2080
2081 /* ---------- Pricing ---------- */
2082 .landing-pricing {
2083 display: grid;
2084 grid-template-columns: repeat(3, 1fr);
2085 gap: 16px;
2086 max-width: 1080px;
2087 margin: 0 auto;
2088 align-items: stretch;
2089 }
2090 .landing-price-card {
2091 position: relative;
2092 background: var(--bg-elevated);
2093 border: 1px solid var(--border);
2094 border-radius: var(--r-lg);
2095 padding: var(--s-7);
2096 display: flex;
2097 flex-direction: column;
2098 gap: var(--s-4);
2099 transition: border-color var(--t-base) var(--ease), transform var(--t-base) var(--ease);
2100 }
2101 .landing-price-card:hover {
2102 border-color: var(--border-strong);
2103 transform: translateY(-2px);
2104 }
2105 .landing-price-hl {
2106 border-color: rgba(140,109,255,0.35);
2107 box-shadow: var(--elev-2), 0 0 0 1px rgba(140,109,255,0.25);
2108 background:
2109 linear-gradient(180deg, rgba(140,109,255,0.05), transparent 50%),
2110 var(--bg-elevated);
2111 }
2112 .landing-price-hl:hover { border-color: rgba(140,109,255,0.55); }
2113 .landing-price-badge {
2114 position: absolute;
2115 top: -10px;
2116 left: 50%;
2117 transform: translateX(-50%);
2118 padding: 3px 12px;
2119 background: var(--accent-gradient);
2120 color: #fff;
2121 font-family: var(--font-mono);
2122 font-size: 10px;
2123 letter-spacing: 0.1em;
2124 text-transform: uppercase;
2125 font-weight: 600;
2126 border-radius: var(--r-full);
2127 box-shadow: 0 4px 12px -2px rgba(140,109,255,0.4);
2128 }
2129 .landing-price-tier {
2130 font-family: var(--font-mono);
2131 font-size: 11px;
2132 text-transform: uppercase;
2133 letter-spacing: 0.16em;
2134 color: var(--text-muted);
2135 }
2136 .landing-price-amount {
2137 display: flex;
2138 align-items: baseline;
2139 gap: 8px;
2140 }
2141 .landing-price-num {
2142 font-family: var(--font-display);
2143 font-size: 40px;
2144 font-weight: 600;
2145 letter-spacing: -0.03em;
2146 color: var(--text-strong);
2147 }
2148 .landing-price-cad {
2149 font-size: var(--t-sm);
2150 color: var(--text-faint);
2151 }
2152 .landing-price-desc {
2153 font-size: var(--t-sm);
2154 color: var(--text-muted);
2155 line-height: 1.55;
2156 margin: 0;
2157 }
2158 .landing-price-features {
2159 list-style: none;
2160 padding: 0;
2161 margin: 0;
2162 display: flex;
2163 flex-direction: column;
2164 gap: 8px;
2165 font-size: var(--t-sm);
2166 color: var(--text);
2167 }
2168 .landing-price-features li {
2169 display: flex;
2170 align-items: center;
2171 gap: 9px;
2172 }
2173 .landing-price-check {
2174 color: var(--accent);
2175 font-weight: 600;
4c47454Claude2176 flex-shrink: 0;
2b821b7Claude2177 }
958d26aClaude2178 .landing-price-cta { margin-top: auto; }
2179
2180 /* ---------- Closing CTA ---------- */
2181 .landing-cta-section { margin: var(--s-20) auto var(--s-16); }
2182 .landing-cta-card {
2183 position: relative;
2184 text-align: center;
2185 padding: var(--s-16) var(--s-7);
2186 border: 1px solid var(--border-strong);
2187 border-radius: var(--r-2xl);
2188 background: var(--bg-elevated);
2189 overflow: hidden;
2190 isolation: isolate;
2191 }
2192 .landing-cta-bg {
2193 position: absolute;
2194 inset: 0;
2195 z-index: -1;
2196 background:
2197 radial-gradient(60% 100% at 50% 0%, rgba(140,109,255,0.16), transparent 65%),
2198 radial-gradient(40% 80% at 80% 100%, rgba(54,197,214,0.10), transparent 65%);
2199 }
2200 .landing-cta-card::after {
2201 content: '';
2202 position: absolute;
2203 inset: 0;
2204 z-index: -1;
2205 background-image: radial-gradient(rgba(255,255,255,0.04) 1px, transparent 1px);
2206 background-size: 24px 24px;
2207 mask-image: radial-gradient(ellipse at center, #000 0%, transparent 65%);
2208 -webkit-mask-image: radial-gradient(ellipse at center, #000 0%, transparent 65%);
2209 opacity: 0.6;
2210 }
2211 :root[data-theme='light'] .landing-cta-card::after {
2212 background-image: radial-gradient(rgba(15,16,28,0.07) 1px, transparent 1px);
2213 }
2214 .landing-cta-card .eyebrow { justify-content: center; }
2215 .landing-cta-title {
2216 font-family: var(--font-display);
2217 font-size: clamp(28px, 4.4vw, 56px);
2218 line-height: 1.05;
2219 letter-spacing: -0.03em;
2220 font-weight: 600;
2221 margin: var(--s-3) 0 var(--s-4);
2222 color: var(--text-strong);
2223 }
2224 .landing-cta-sub {
2225 font-size: var(--t-md);
2226 color: var(--text-muted);
2227 max-width: 560px;
2228 margin: 0 auto var(--s-8);
2229 line-height: 1.55;
2230 }
2231 .landing-cta-buttons {
2232 display: flex;
2233 gap: 12px;
2234 justify-content: center;
2235 flex-wrap: wrap;
2236 }
2b821b7Claude2237
2238 /* ---------- Responsive ---------- */
958d26aClaude2239 @media (max-width: 960px) {
2240 .landing-features { grid-template-columns: repeat(2, 1fr); }
2241 .landing-walk-grid { grid-template-columns: repeat(2, 1fr); }
2242 .landing-walk-step::after { display: none; }
2243 .landing-pricing { grid-template-columns: 1fr; max-width: 480px; }
2244 }
2245 @media (max-width: 640px) {
2246 .landing-hero { padding: var(--s-14) 0 var(--s-10); }
2247 .landing-hero-cmd { flex-wrap: wrap; justify-content: center; }
2248 .landing-hero-ctas { flex-direction: column; align-items: stretch; }
2249 .landing-hero-ctas .btn { width: 100%; justify-content: center; }
2250 .landing-features { grid-template-columns: 1fr; }
2251 .landing-walk-grid { grid-template-columns: 1fr; }
2252 .landing-section { margin: var(--s-12) auto; }
2253 .landing-cta-card { padding: var(--s-10) var(--s-5); }
2254 .landing-cta-buttons .btn { width: 100%; justify-content: center; }
2b821b7Claude2255 }
52ad8b1Claude2256
2257 /* ---------- L4 social-proof counters ---------- */
2258 .landing-counters {
2259 margin: var(--s-10) auto var(--s-12);
2260 max-width: 1180px;
2261 padding: 0 var(--s-4);
2262 }
2263 .landing-counters-grid {
2264 display: grid;
2265 grid-template-columns: repeat(6, 1fr);
2266 gap: 20px;
2267 text-align: left;
2268 }
2269 .landing-counter {
2270 padding: var(--s-3) 0;
2271 border-top: 1px solid var(--border-subtle);
2272 }
2273 .landing-counter-num {
2274 font-family: var(--font-display);
2275 font-size: clamp(24px, 3vw, 38px);
2276 line-height: 1.05;
2277 letter-spacing: -0.03em;
2278 font-weight: 700;
2279 margin-bottom: 6px;
2280 font-feature-settings: 'tnum';
2281 background-image: var(--accent-gradient);
2282 -webkit-background-clip: text;
2283 background-clip: text;
2284 -webkit-text-fill-color: transparent;
2285 color: transparent;
2286 }
2287 .landing-counter-label {
2288 font-family: var(--font-mono);
2289 font-size: 10.5px;
2290 font-weight: 500;
2291 text-transform: uppercase;
2292 letter-spacing: 0.12em;
2293 color: var(--text-faint);
2294 line-height: 1.4;
2295 }
2296 @media (max-width: 960px) {
2297 .landing-counters-grid { grid-template-columns: repeat(3, 1fr); gap: 20px 16px; }
2298 }
2299 @media (max-width: 540px) {
2300 .landing-counters-grid { grid-template-columns: repeat(2, 1fr); gap: 18px 12px; }
2301 }
5f2e749Claude2302
2303 /* ---------- L10 hero install snippet ---------- */
2304 .landing-hero-install {
2305 display: inline-flex;
2306 align-items: stretch;
2307 gap: 0;
2308 margin: var(--s-8) auto 0;
2309 background: var(--bg-elevated);
2310 border: 1px solid var(--border-strong);
2311 border-radius: var(--r);
2312 box-shadow: var(--elev-1);
2313 overflow: hidden;
2314 max-width: 100%;
2315 font-family: var(--font-mono);
2316 }
2317 .landing-hero-install-code {
2318 display: inline-flex;
2319 align-items: center;
2320 gap: 10px;
2321 padding: 10px 14px;
2322 font-size: 13.5px;
2323 color: var(--text-strong);
2324 background: transparent;
2325 border: 0;
2326 white-space: nowrap;
2327 overflow-x: auto;
2328 }
2329 .landing-hero-install-prompt {
2330 color: var(--accent);
2331 user-select: none;
2332 }
2333 .landing-hero-install-copy {
2334 appearance: none;
2335 border: 0;
2336 border-left: 1px solid var(--border);
2337 background: transparent;
2338 color: var(--text-muted);
2339 font-family: var(--font-mono);
2340 font-size: 12px;
2341 font-weight: 600;
2342 letter-spacing: 0.06em;
2343 text-transform: uppercase;
2344 padding: 0 16px;
2345 cursor: pointer;
2346 transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
2347 }
2348 .landing-hero-install-copy:hover {
2349 background: var(--accent-gradient-faint);
2350 color: var(--accent);
2351 }
2352 .landing-hero-install-copy[data-copied="1"] {
2353 color: var(--green, #34d399);
2354 }
2355
2356 /* ---------- L10 hero "what just happened" rail ---------- */
2357 .landing-hero-rail {
2358 list-style: none;
2359 padding: 0;
2360 margin: var(--s-7) auto 0;
2361 display: flex;
2362 flex-wrap: wrap;
2363 justify-content: center;
2364 gap: 8px 22px;
2365 font-family: var(--font-sans);
2366 font-size: var(--t-sm);
2367 color: var(--text-muted);
2368 max-width: 760px;
2369 }
2370 .landing-hero-rail li {
2371 display: inline-flex;
2372 align-items: center;
2373 gap: 7px;
2374 line-height: 1.4;
2375 }
2376 .landing-hero-rail strong {
2377 color: var(--text-strong);
2378 font-weight: 600;
2379 font-feature-settings: 'tnum';
2380 }
2381 .landing-hero-rail-check {
2382 color: var(--accent);
2383 font-weight: 700;
2384 flex-shrink: 0;
2385 }
2386
2387 /* ---------- L10 three-reasons section ---------- */
2388 .landing-reasons { margin-top: var(--s-12); }
2389 .landing-reasons-grid {
2390 display: grid;
2391 grid-template-columns: repeat(3, 1fr);
2392 gap: 16px;
2393 }
2394 .landing-reason {
2395 background: var(--bg-elevated);
2396 border: 1px solid var(--border);
2397 border-radius: var(--r-lg);
2398 padding: var(--s-7);
2399 display: flex;
2400 flex-direction: column;
2401 gap: var(--s-3);
2402 transition: border-color var(--t-base) var(--ease), transform var(--t-base) var(--ease);
2403 }
2404 .landing-reason:hover {
2405 border-color: var(--border-strong);
2406 transform: translateY(-2px);
2407 }
2408 .landing-reason-icon {
2409 display: inline-flex;
2410 align-items: center;
2411 justify-content: center;
2412 width: 40px;
2413 height: 40px;
2414 border-radius: var(--r);
2415 background: var(--accent-gradient-soft);
2416 color: var(--accent);
2417 border: 1px solid rgba(140,109,255,0.20);
2418 }
2419 .landing-reason-title {
2420 font-family: var(--font-display);
2421 font-size: 20px;
2422 font-weight: 600;
2423 letter-spacing: -0.02em;
2424 margin: 0;
2425 color: var(--text-strong);
2426 }
2427 .landing-reason-body {
2428 font-size: var(--t-sm);
2429 color: var(--text-muted);
2430 line-height: 1.55;
2431 margin: 0;
2432 }
2433 .landing-reasons-code {
2434 display: block;
2435 font-family: var(--font-mono);
2436 font-size: 12px;
2437 color: var(--text-strong);
2438 background: var(--bg);
2439 border: 1px solid var(--border);
2440 border-radius: var(--r);
2441 padding: 8px 12px;
2442 overflow-x: auto;
2443 white-space: nowrap;
2444 }
2445 .landing-reason-link {
2446 margin-top: auto;
2447 display: inline-flex;
2448 align-items: center;
2449 gap: 6px;
2450 color: var(--accent);
2451 font-size: var(--t-sm);
2452 font-weight: 500;
2453 text-decoration: none;
2454 }
2455 .landing-reason-link:hover { text-decoration: underline; }
2456 @media (max-width: 960px) {
2457 .landing-reasons-grid { grid-template-columns: 1fr; max-width: 520px; margin: 0 auto; }
2458 }
2459
2460 /* ---------- L10 "How is this different" pull-quote ---------- */
2461 .landing-pullquote-section {
2462 margin: var(--s-20) auto var(--s-12);
2463 max-width: 920px;
2464 padding: 0 var(--s-4);
2465 text-align: center;
2466 }
2467 .landing-pullquote {
2468 margin: 0;
2469 padding: var(--s-10) var(--s-7);
2470 background:
2471 radial-gradient(80% 100% at 50% 0%, rgba(140,109,255,0.10), transparent 65%),
2472 var(--bg-elevated);
2473 border: 1px solid var(--border-strong);
2474 border-radius: var(--r-xl);
2475 position: relative;
2476 overflow: hidden;
2477 }
2478 .landing-pullquote-eyebrow {
2479 font-family: var(--font-mono);
2480 font-size: 11px;
2481 font-weight: 600;
2482 letter-spacing: 0.16em;
2483 text-transform: uppercase;
2484 color: var(--accent);
2485 margin-bottom: var(--s-4);
2486 }
2487 .landing-pullquote-text {
2488 font-family: var(--font-display);
2489 font-size: clamp(20px, 2.4vw, 28px);
2490 line-height: 1.4;
2491 letter-spacing: -0.018em;
2492 color: var(--text-strong);
2493 margin: 0 auto;
2494 max-width: 760px;
2495 quotes: "\\201C" "\\201D";
2496 }
2497 .landing-pullquote-text::before { content: open-quote; color: var(--accent); margin-right: 4px; }
2498 .landing-pullquote-text::after { content: close-quote; color: var(--accent); margin-left: 4px; }
2499 .landing-pullquote-link {
2500 display: inline-flex;
2501 align-items: center;
2502 gap: 6px;
2503 margin-top: var(--s-6);
2504 color: var(--accent);
2505 font-size: var(--t-sm);
2506 font-weight: 500;
2507 text-decoration: none;
2508 }
2509 .landing-pullquote-link:hover { text-decoration: underline; }
2510
2511 /* ---------- L10 hero responsive overrides ---------- */
2512 @media (max-width: 640px) {
2513 .landing-hero-install { width: 100%; }
2514 .landing-hero-install-code { flex: 1; font-size: 12px; }
2515 .landing-hero-rail { flex-direction: column; align-items: flex-start; gap: 6px; padding: 0 var(--s-3); }
2516 .landing-hero-rail li { width: 100%; }
2517 }
534f04aClaude2518
2519 /* ============================================================ */
2520 /* Block M1 — Live-now demo feed */
2521 /* ============================================================ */
2522 .landing-livenow {
2523 margin: var(--s-8) 0 var(--s-6);
2524 padding: var(--s-6) 0 var(--s-4);
2525 }
2526 .landing-livenow-head {
2527 text-align: center;
2528 margin-bottom: var(--s-6);
2529 }
2530 .landing-livenow-eyebrow {
2531 display: inline-flex;
2532 align-items: center;
2533 gap: 8px;
2534 padding: 4px 12px;
2535 border-radius: var(--r-full);
2536 background: rgba(52,211,153,0.08);
2537 border: 1px solid rgba(52,211,153,0.25);
2538 color: var(--green);
2539 font-family: var(--font-mono, ui-monospace, monospace);
2540 font-size: 11px;
2541 letter-spacing: 0.06em;
2542 text-transform: uppercase;
2543 margin-bottom: var(--s-3);
2544 }
2545 .landing-livenow-pulse {
2546 width: 8px; height: 8px;
2547 border-radius: 50%;
2548 background: var(--green);
2549 box-shadow: 0 0 0 0 rgba(52,211,153,0.6);
2550 animation: landing-livenow-pulse 1.6s ease-out infinite;
2551 }
2552 @keyframes landing-livenow-pulse {
2553 0% { box-shadow: 0 0 0 0 rgba(52,211,153,0.55); transform: scale(1); }
2554 70% { box-shadow: 0 0 0 10px rgba(52,211,153,0); transform: scale(1.05); }
2555 100% { box-shadow: 0 0 0 0 rgba(52,211,153,0); transform: scale(1); }
2556 }
2557 .landing-livenow-title {
2558 font-size: 22px;
2559 line-height: 1.25;
2560 margin: 0 auto;
2561 max-width: 720px;
2562 color: var(--text-strong);
2563 font-weight: 600;
2564 letter-spacing: -0.01em;
2565 }
2566 .landing-livenow-sub {
2567 margin: var(--s-2) auto 0;
2568 color: var(--text-muted);
2569 font-size: 13px;
2570 max-width: 560px;
2571 }
2572 .landing-livenow-sub code {
2573 background: rgba(255,255,255,0.05);
2574 border: 1px solid var(--border);
2575 padding: 1px 6px;
2576 border-radius: 4px;
2577 font-size: 12px;
2578 color: var(--accent);
2579 }
2580
2581 .landing-livenow-grid {
2582 display: grid;
2583 grid-template-columns: repeat(2, minmax(0, 1fr));
2584 gap: var(--s-3);
2585 }
2586 @media (min-width: 980px) {
2587 .landing-livenow-grid {
2588 grid-template-columns: repeat(4, minmax(0, 1fr));
2589 }
2590 }
2591
2592 .landing-livecard {
2593 background: linear-gradient(180deg, rgba(255,255,255,0.025), rgba(255,255,255,0.005));
2594 border: 1px solid var(--border);
2595 border-radius: var(--r-md, 10px);
2596 padding: 14px 14px 12px;
2597 display: flex;
2598 flex-direction: column;
2599 min-height: 180px;
2600 position: relative;
2601 overflow: hidden;
2602 }
2603 .landing-livecard::before {
2604 content: '';
2605 position: absolute;
2606 inset: 0;
2607 background: var(--accent-gradient-faint);
2608 opacity: 0;
2609 transition: opacity 200ms var(--ease, ease);
2610 pointer-events: none;
2611 }
2612 .landing-livecard:hover::before { opacity: 1; }
2613
2614 .landing-livecard-head {
2615 display: flex;
2616 align-items: center;
2617 gap: 8px;
2618 margin-bottom: 10px;
2619 }
2620 .landing-livecard-dot {
2621 width: 7px; height: 7px;
2622 border-radius: 50%;
2623 background: var(--green);
2624 box-shadow: 0 0 8px rgba(52,211,153,0.55);
2625 animation: landing-livenow-pulse 1.8s ease-out infinite;
2626 flex-shrink: 0;
2627 }
2628 .landing-livecard-title {
2629 margin: 0;
2630 font-size: 12px;
2631 font-weight: 600;
2632 text-transform: uppercase;
2633 letter-spacing: 0.06em;
2634 color: var(--text-muted);
2635 }
2636
2637 .landing-livecard-bignum {
2638 display: flex;
2639 align-items: baseline;
2640 gap: 8px;
2641 margin: 4px 0 10px;
2642 }
2643 .landing-livecard-bignum-n {
2644 font-size: 30px;
2645 font-weight: 700;
2646 color: var(--text-strong);
2647 font-variant-numeric: tabular-nums;
2648 background: var(--accent-gradient);
2649 -webkit-background-clip: text;
2650 background-clip: text;
2651 -webkit-text-fill-color: transparent;
2652 color: transparent;
2653 }
2654 .landing-livecard-bignum-label {
2655 font-size: 12px;
2656 color: var(--text-muted);
2657 }
2658
2659 .landing-livecard-list {
2660 list-style: none;
2661 margin: 0;
2662 padding: 0;
2663 font-size: 13px;
2664 line-height: 1.45;
2665 flex: 1;
2666 }
2667 .landing-livecard-row, .landing-livecard-feedrow {
2668 padding: 6px 0;
2669 border-bottom: 1px dashed rgba(255,255,255,0.05);
2670 transition: background-color 1s var(--ease, ease);
2671 border-radius: 4px;
2672 margin: 0 -4px;
2673 padding-left: 4px;
2674 padding-right: 4px;
2675 }
2676 .landing-livecard-row:last-child,
2677 .landing-livecard-feedrow:last-child { border-bottom: 0; }
2678 .landing-livecard-feedrow { padding: 4px; font-size: 12.5px; }
2679
2680 .landing-livecard-flash {
2681 background-color: rgba(52,211,153,0.18) !important;
2682 animation: landing-livecard-flash-fade 1.1s ease-out forwards;
2683 }
2684 @keyframes landing-livecard-flash-fade {
2685 0% { background-color: rgba(52,211,153,0.22); }
2686 100% { background-color: rgba(52,211,153,0); }
2687 }
2688
2689 .landing-livecard-link {
2690 color: var(--text-strong);
2691 text-decoration: none;
2692 display: inline-block;
2693 max-width: 100%;
2694 overflow: hidden;
2695 text-overflow: ellipsis;
2696 white-space: nowrap;
2697 }
2698 .landing-livecard-link:hover { color: var(--accent); }
2699 .landing-livecard-num {
2700 font-family: var(--font-mono, ui-monospace, monospace);
2701 color: var(--text-faint);
2702 font-weight: 500;
2703 font-size: 12px;
2704 }
2705 .landing-livecard-title-text { color: var(--text-strong); }
2706 .landing-livecard-snippet {
2707 color: var(--text-muted);
2708 font-style: italic;
2709 font-size: 12px;
2710 }
2711 .landing-livecard-meta {
2712 font-size: 11px;
2713 color: var(--text-faint);
2714 margin-top: 2px;
2715 font-family: var(--font-mono, ui-monospace, monospace);
2716 }
2717 .landing-livecard-repo {
2718 color: var(--accent-2);
2719 }
2720 .landing-livecard-rel {
2721 color: var(--text-muted);
2722 }
2723 .landing-livecard-empty {
2724 color: var(--text-faint);
2725 font-size: 12px;
2726 font-style: italic;
2727 padding: 8px 0;
2728 }
2729 .landing-livecard-kind {
2730 display: inline-block;
2731 padding: 1px 7px;
2732 border-radius: var(--r-full);
2733 font-family: var(--font-mono, ui-monospace, monospace);
2734 font-size: 10px;
2735 letter-spacing: 0.04em;
2736 text-transform: uppercase;
2737 font-weight: 600;
2738 }
2739 .landing-livecard-kind-auto_merge-merged,
2740 .landing-livecard-kind-auto-merge-merged {
2741 background: rgba(52,211,153,0.12);
2742 color: var(--green);
2743 border: 1px solid rgba(52,211,153,0.25);
2744 }
2745 .landing-livecard-kind-ai_build-dispatched,
2746 .landing-livecard-kind-ai-build-dispatched {
2747 background: rgba(140,109,255,0.12);
2748 color: var(--accent);
2749 border: 1px solid rgba(140,109,255,0.30);
2750 }
2751 .landing-livecard-kind-ai_review-posted,
2752 .landing-livecard-kind-ai-review-posted {
2753 background: rgba(54,197,214,0.12);
2754 color: var(--accent-2);
2755 border: 1px solid rgba(54,197,214,0.30);
2756 }
2757
2758 .landing-livenow-cta {
2759 margin-top: var(--s-5);
2760 display: flex;
2761 flex-wrap: wrap;
2762 align-items: center;
2763 justify-content: center;
2764 gap: 10px;
2765 font-size: 13px;
2766 color: var(--text-muted);
2767 }
2768 .landing-livenow-cta-link {
2769 color: var(--accent);
2770 text-decoration: none;
2771 font-weight: 500;
2772 display: inline-flex;
2773 align-items: center;
2774 gap: 4px;
2775 }
2776 .landing-livenow-cta-link:hover { color: var(--accent-hover); }
2777 .landing-livenow-cta-sep { color: var(--text-faint); }
2778
2779 @media (prefers-reduced-motion: reduce) {
2780 .landing-livenow-pulse,
2781 .landing-livecard-dot { animation: none; }
2782 .landing-livecard-flash { animation: none; background-color: transparent !important; }
2783 }
52ad8b1Claude2784`;
2785
2786/**
2787 * Block L4 — count-up animation.
2788 *
2789 * Reads each `[data-counter-target]` and animates the in-DOM text from
2790 * 0 → target over ~1.2s when the element first scrolls into view.
2791 *
2792 * Render-once semantics: each tile already contains the final value as
2793 * HTML, so visitors with JS disabled — or anyone before the script
2794 * loads — sees the correct number. The script just animates the text.
2795 *
2796 * Falls back to the static value (no animation) when IntersectionObserver
2797 * isn't available, or when the user prefers reduced motion.
2798 */
2799const landingCountersJs = `
2800(function(){
2801 try {
2802 var els = document.querySelectorAll('[data-counter-target]');
2803 if (!els.length) return;
2804 var reduced = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
2805 if (reduced || typeof IntersectionObserver !== 'function') return;
2806
2807 function animate(el) {
2808 var target = parseInt(el.getAttribute('data-counter-target') || '0', 10);
2809 if (!isFinite(target) || target <= 0) return;
2810 var prefix = el.getAttribute('data-counter-prefix') || '';
2811 var suffix = el.getAttribute('data-counter-suffix') || '';
2812 var duration = 1200;
2813 var start = performance.now();
2814 function frame(now) {
2815 var t = Math.min(1, (now - start) / duration);
2816 // ease-out cubic
2817 var eased = 1 - Math.pow(1 - t, 3);
2818 var v = Math.floor(eased * target);
2819 el.textContent = prefix + v.toLocaleString() + suffix;
2820 if (t < 1) requestAnimationFrame(frame);
2821 else el.textContent = prefix + target.toLocaleString() + suffix;
2822 }
2823 // Reset to zero before animating in.
2824 el.textContent = prefix + '0' + suffix;
2825 requestAnimationFrame(frame);
2826 }
2827
2828 var io = new IntersectionObserver(function(entries) {
2829 entries.forEach(function(entry){
2830 if (entry.isIntersecting) {
2831 animate(entry.target);
2832 io.unobserve(entry.target);
2833 }
2834 });
2835 }, { threshold: 0.4 });
2836 els.forEach(function(el){ io.observe(el); });
2837 } catch (_) { /* swallow — static numbers remain */ }
2838})();
2b821b7Claude2839`;
5f2e749Claude2840
2841/**
2842 * Block L10 — clipboard copy for the hero install snippet.
2843 *
2844 * Pure progressive enhancement. Without JS the user can still
2845 * triple-click + Cmd/Ctrl-C the snippet — the button is the
2846 * speed-bump, not the only path.
2847 */
2848const landingCopyJs = `
2849(function(){
2850 try {
2851 var btns = document.querySelectorAll('[data-copy-target]');
2852 if (!btns.length) return;
2853 btns.forEach(function(btn){
2854 btn.addEventListener('click', function(){
2855 var id = btn.getAttribute('data-copy-target') || '';
2856 var src = document.getElementById(id);
2857 if (!src) return;
2858 var text = src.textContent || '';
2859 var done = function(){
2860 var prev = btn.textContent;
2861 btn.textContent = 'Copied';
2862 btn.setAttribute('data-copied', '1');
2863 setTimeout(function(){
2864 btn.textContent = prev || 'Copy';
2865 btn.removeAttribute('data-copied');
2866 }, 1500);
2867 };
2868 if (navigator.clipboard && navigator.clipboard.writeText) {
2869 navigator.clipboard.writeText(text).then(done, function(){ done(); });
2870 } else {
2871 // Legacy fallback — temp textarea + execCommand.
2872 try {
2873 var ta = document.createElement('textarea');
2874 ta.value = text;
2875 ta.style.position = 'fixed';
2876 ta.style.opacity = '0';
2877 document.body.appendChild(ta);
2878 ta.select();
2879 document.execCommand('copy');
2880 document.body.removeChild(ta);
2881 done();
2882 } catch (_) {}
2883 }
2884 });
2885 });
2886 } catch (_) { /* swallow */ }
2887})();
2888`;