Forge. Developer Tools landing page.

A Geist-style developer-tool landing page: near-black canvas, 1px hairlines, one ember accent. A terminal types out a real push-to-merge run, a PR timeline draws itself with scroll, an annotated diff gets auto-review comments that pop in (and a suggestion you can commit), a sticky Gantt scene scrubs the 90-second merge against legacy CI, an integrations grid lights its hairlines with a cursor spotlight, and pricing has a live per-seat calculator. One file, React only.

  • Terminal typing hero
  • Scroll-drawn PR timeline
  • Auto-review comments pop in
  • Scroll-scrubbed pipeline Gantt
  • Cursor spotlight on hairlines
  • Spring billing toggle
  • Live seat calculator
Full screen

The preview is interactive — scroll inside it to run the animations.

The whole page. One file, nothing left out.

1,571 lines · 81.9 KB · imports React only. This is the exact file behind the preview above — every section, interaction and style.

  1. 01Save it as forge-devtool.tsx in any React 18+ project.
  2. 02Render <Page /> on a route of its own — it brings its own nav and footer.
  3. 03Swap the copy and tokens. Keep the DESIGN.md next to it so your AI agent follows the system.
forge-devtool.tsx
1/**
2 * Forge — code review & CI landing page (dark, hairline, one accent).
3 *
4 * A complete, self-contained single page: React is the only import and every
5 * style lives in the <style> block at the bottom of this file (classes are
6 * prefixed `fd-frg-`). Drop it into any React 18+ project and render <Page />.
7 *
8 * Motion: terminal typing hero, a PR timeline whose line draws with scroll,
9 * an annotated diff where review comments pop in (and a suggestion can be
10 * committed), a sticky "90-second merge" scene scrubbed by scroll, a bento of
11 * live micro-visuals, an integrations grid with a cursor-follow spotlight on
12 * its hairlines, a per-seat pricing calculator and staggered reveals.
13 *
14 * Browser support: evergreen browsers (sticky, IntersectionObserver, CSS
15 * custom properties, mask-image). Accessibility: landmarks, real buttons and
16 * range input, aria-live terminal summary, focus rings, reduced-motion and
17 * prefers-contrast fallbacks. SSR-safe (no window access during render).
18 */
19import { useEffect, useRef, useState, type CSSProperties, type ReactNode } from "react";
20
21/* ─────────────────────────── helpers ─────────────────────────── */
22
23const reducedMotion = () =>
24 typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
25const clamp01 = (n: number) => (n < 0 ? 0 : n > 1 ? 1 : n);
26
27function useReveal(root: React.RefObject<HTMLElement | null>) {
28 useEffect(() => {
29 const el = root.current;
30 if (!el) return;
31 el.setAttribute("data-js", "");
32 const items = Array.from(el.querySelectorAll<HTMLElement>("[data-reveal]"));
33 if (typeof IntersectionObserver === "undefined" || reducedMotion()) {
34 items.forEach((i) => i.setAttribute("data-in", ""));
35 return;
36 }
37 const io = new IntersectionObserver(
38 (entries) => {
39 for (const e of entries)
40 if (e.isIntersecting) {
41 e.target.setAttribute("data-in", "");
42 io.unobserve(e.target);
43 }
44 },
45 { rootMargin: "0px 0px -8% 0px" },
46 );
47 items.forEach((i) => io.observe(i));
48 return () => io.disconnect();
49 }, [root]);
50}
51
52/** rAF-throttled scroll/resize callback, active only while `ref` is near the viewport. */
53function useScrollFrame(ref: React.RefObject<HTMLElement | null>, fn: () => void) {
54 const fnRef = useRef(fn);
55 fnRef.current = fn;
56 useEffect(() => {
57 const el = ref.current;
58 if (!el) return;
59 let raf = 0;
60 let active = true;
61 const run = () => {
62 raf = 0;
63 if (active) fnRef.current();
64 };
65 const queue = () => {
66 if (!raf) raf = requestAnimationFrame(run);
67 };
68 const io =
69 typeof IntersectionObserver !== "undefined"
70 ? new IntersectionObserver(
71 ([e]) => {
72 active = e.isIntersecting;
73 if (active) queue();
74 },
75 { rootMargin: "200px 0px" },
76 )
77 : undefined;
78 io?.observe(el);
79 queue();
80 window.addEventListener("scroll", queue, { passive: true });
81 window.addEventListener("resize", queue);
82 return () => {
83 io?.disconnect();
84 window.removeEventListener("scroll", queue);
85 window.removeEventListener("resize", queue);
86 if (raf) cancelAnimationFrame(raf);
87 };
88 }, [ref]);
89}
90
91/** Fires once when `ref` enters the viewport (immediately under reduced motion). */
92function useInView(ref: React.RefObject<HTMLElement | null>, threshold = 0.35) {
93 const [seen, setSeen] = useState(false);
94 useEffect(() => {
95 const el = ref.current;
96 if (!el) return;
97 if (typeof IntersectionObserver === "undefined" || reducedMotion()) {
98 setSeen(true);
99 return;
100 }
101 const io = new IntersectionObserver(
102 ([e]) => {
103 if (e.isIntersecting) {
104 setSeen(true);
105 io.disconnect();
106 }
107 },
108 { threshold },
109 );
110 io.observe(el);
111 return () => io.disconnect();
112 }, [ref, threshold]);
113 return seen;
114}
115
116/* ─────────────────────────── icons ─────────────────────────── */
117
118function Logo({ size = 20 }: { size?: number }) {
119 return (
120 <svg className="fd-frg-logo" viewBox="0 0 24 24" width={size} height={size} aria-hidden="true">
121 <path d="M12 2.5l8.5 4.9v9.2L12 21.5l-8.5-4.9V7.4z" fill="none" stroke="currentColor" strokeWidth="1.5" />
122 <path d="M12 7.5v9M8 10l4-2.5 4 2.5" fill="none" stroke="var(--fd-frg-accent)" strokeWidth="1.5" />
123 </svg>
124 );
125}
126function Arrow() {
127 return (
128 <svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true" className="fd-frg-arrow">
129 <path d="M3 8h9.5M8.5 3.5L13 8l-4.5 4.5" fill="none" stroke="currentColor" strokeWidth="1.5" />
130 </svg>
131 );
132}
133function Tick() {
134 return (
135 <svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true">
136 <path d="M3.5 8.5l3 3 6-7" fill="none" stroke="currentColor" strokeWidth="1.6" />
137 </svg>
138 );
139}
140function Branch() {
141 return (
142 <svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true">
143 <circle cx="4.5" cy="3.5" r="1.6" fill="none" stroke="currentColor" />
144 <circle cx="4.5" cy="12.5" r="1.6" fill="none" stroke="currentColor" />
145 <circle cx="11.5" cy="5.5" r="1.6" fill="none" stroke="currentColor" />
146 <path d="M4.5 5.1v5.8M11.5 7.1c0 2.4-2 3-7 3.8" fill="none" stroke="currentColor" />
147 </svg>
148 );
149}
150
151/* ─────────────────────────── nav ─────────────────────────── */
152
153const NAV = [
154 ["#how", "How it works"],
155 ["#review", "Review"],
156 ["#speed", "Speed"],
157 ["#integrations", "Integrations"],
158 ["#pricing", "Pricing"],
159];
160
161function Nav() {
162 const [scrolled, setScrolled] = useState(false);
163 const [open, setOpen] = useState(false);
164 useEffect(() => {
165 const on = () => setScrolled(window.scrollY > 12);
166 on();
167 window.addEventListener("scroll", on, { passive: true });
168 return () => window.removeEventListener("scroll", on);
169 }, []);
170 return (
171 <header className="fd-frg-nav" data-scrolled={scrolled || undefined} data-open={open || undefined}>
172 <div className="fd-frg-nav-in">
173 <a href="#top" className="fd-frg-brand" aria-label="Forge home">
174 <Logo />
175 <span>Forge</span>
176 </a>
177 <nav className="fd-frg-nav-links" aria-label="Primary">
178 {NAV.map(([h, l]) => (
179 <a key={h} href={h}>
180 {l}
181 </a>
182 ))}
183 </nav>
184 <div className="fd-frg-nav-right">
185 <a className="fd-frg-nav-ghost" href="#top">
186 Docs
187 </a>
188 <a className="fd-frg-btn fd-frg-btn-sm" href="#pricing">
189 Start free
190 </a>
191 <button
192 type="button"
193 className="fd-frg-burger"
194 aria-expanded={open}
195 aria-label={open ? "Close menu" : "Open menu"}
196 onClick={() => setOpen((o) => !o)}
197 >
198 <span />
199 <span />
200 </button>
201 </div>
202 </div>
203 <div className="fd-frg-sheet" hidden={!open}>
204 {NAV.map(([h, l]) => (
205 <a key={h} href={h} onClick={() => setOpen(false)}>
206 {l}
207 </a>
208 ))}
209 </div>
210 </header>
211 );
212}
213
214/* ─────────────────────────── hero terminal ─────────────────────────── */
215
216type TLine = { kind: "cmd" | "out" | "ok" | "warn" | "dim" | "sum"; text: string; pause?: number };
217
218const SCRIPT: TLine[] = [
219 { kind: "cmd", text: "git push origin feat/session-rotation" },
220 { kind: "dim", text: "→ forge: PR #4182 opened · 7 files · +214 −38", pause: 380 },
221 { kind: "out", text: " selecting tests 214 of 9,812 touch this diff", pause: 420 },
222 { kind: "ok", text: " ✓ build cached 0:11", pause: 260 },
223 { kind: "ok", text: " ✓ unit 214 passed 0:38", pause: 260 },
224 { kind: "ok", text: " ✓ integration 41 passed 0:52", pause: 260 },
225 { kind: "warn", text: " ◆ review 2 suggestions on src/auth/session.ts", pause: 520 },
226 { kind: "ok", text: " ✓ approved by jane (staff eng)", pause: 360 },
227 { kind: "sum", text: "merged to main in 1m 28s · deployed to prod-us-east", pause: 0 },
228];
229
230function Terminal() {
231 const [shown, setShown] = useState<{ line: number; chars: number }>({ line: 0, chars: 0 });
232 const [run, setRun] = useState(0);
233 const boxRef = useRef<HTMLDivElement>(null);
234 const done = shown.line >= SCRIPT.length;
235
236 useEffect(() => {
237 if (reducedMotion()) {
238 setShown({ line: SCRIPT.length, chars: 0 });
239 return;
240 }
241 let line = 0;
242 let chars = 0;
243 let t = 0;
244 let cancelled = false;
245 setShown({ line: 0, chars: 0 });
246 const tick = () => {
247 if (cancelled) return;
248 const cur = SCRIPT[line];
249 if (!cur) return;
250 if (cur.kind === "cmd" && chars < cur.text.length) {
251 chars += 1;
252 setShown({ line, chars });
253 t = window.setTimeout(tick, 28 + Math.random() * 46);
254 return;
255 }
256 line += 1;
257 chars = 0;
258 setShown({ line, chars: 0 });
259 t = window.setTimeout(tick, cur.pause ?? 300);
260 };
261 t = window.setTimeout(tick, 700);
262 return () => {
263 cancelled = true;
264 window.clearTimeout(t);
265 };
266 }, [run]);
267
268 return (
269 <div className="fd-frg-term" ref={boxRef}>
270 <div className="fd-frg-term-bar">
271 <span className="fd-frg-term-dots" aria-hidden="true">
272 <i />
273 <i />
274 <i />
275 </span>
276 <span className="fd-frg-term-title">~/acme/api — zsh</span>
277 <button type="button" className="fd-frg-term-replay" onClick={() => setRun((r) => r + 1)} disabled={!done}>
278 Replay
279 </button>
280 </div>
281 <div className="fd-frg-term-body" aria-hidden="true">
282 {SCRIPT.map((l, i) => {
283 if (i > shown.line) return null;
284 const typing = l.kind === "cmd" && i === shown.line;
285 const text = typing ? l.text.slice(0, shown.chars) : l.text;
286 if (i === shown.line && !typing) return null;
287 return (
288 <div key={`${run}-${i}`} className={`fd-frg-tl fd-frg-tl-${l.kind}`}>
289 {l.kind === "cmd" && <span className="fd-frg-prompt">$</span>}
290 {l.kind === "sum" && <span className="fd-frg-sum-dot" />}
291 <span>{text}</span>
292 {typing && <span className="fd-frg-caret" />}
293 </div>
294 );
295 })}
296 {done && (
297 <div className="fd-frg-tl fd-frg-tl-cmd">
298 <span className="fd-frg-prompt">$</span>
299 <span className="fd-frg-caret" />
300 </div>
301 )}
302 </div>
303 <p className="fd-frg-sr" aria-live="polite">
304 {done ? "Example run: pull request 4182 merged to main in 1 minute 28 seconds." : ""}
305 </p>
306 </div>
307 );
308}
309
310function Hero() {
311 return (
312 <section className="fd-frg-hero" id="top">
313 <div className="fd-frg-hero-grid" aria-hidden="true" />
314 <div className="fd-frg-wrap fd-frg-hero-in">
315 <a className="fd-frg-pill fd-frg-in" href="#speed" style={{ "--d": "0ms" } as CSSProperties}>
316 <span className="fd-frg-pill-new">v4.2</span> <span className="fd-frg-pill-txt">Merge queues now rebase stacked PRs</span> <Arrow />
317 </a>
318 <h1 className="fd-frg-h1">
319 <span className="fd-frg-in" style={{ "--d": "80ms" } as CSSProperties}>
320 Ship the PR.
321 </span>
322 <span className="fd-frg-in fd-frg-h1-mute" style={{ "--d": "180ms" } as CSSProperties}>
323 Skip the wait.
324 </span>
325 </h1>
326 <p className="fd-frg-lede fd-frg-in" style={{ "--d": "280ms" } as CSSProperties}>
327 Forge runs only the tests your diff touches, reviews every line before a human does, and lands the merge in
328 about ninety seconds. Works with the GitHub repo you already have.
329 </p>
330 <div className="fd-frg-ctas fd-frg-in" style={{ "--d": "360ms" } as CSSProperties}>
331 <a className="fd-frg-btn" href="#pricing">
332 Start free <Arrow />
333 </a>
334 <CopyCmd cmd="npx forge init" />
335 </div>
336 <div className="fd-frg-hero-term fd-frg-in" style={{ "--d": "460ms" } as CSSProperties}>
337 <Terminal />
338 </div>
339 </div>
340 </section>
341 );
342}
343
344function CopyCmd({ cmd }: { cmd: string }) {
345 const [copied, setCopied] = useState(false);
346 return (
347 <button
348 type="button"
349 className="fd-frg-cmd"
350 onClick={async () => {
351 try {
352 await navigator.clipboard.writeText(cmd);
353 setCopied(true);
354 window.setTimeout(() => setCopied(false), 1600);
355 } catch {
356 /* clipboard unavailable */
357 }
358 }}
359 aria-label={`Copy command: ${cmd}`}
360 >
361 <span className="fd-frg-cmd-p">$</span>
362 <span>{cmd}</span>
363 <span className="fd-frg-cmd-c">{copied ? "Copied" : "Copy"}</span>
364 </button>
365 );
366}
367
368/* ─────────────────────────── logos ─────────────────────────── */
369
370function Logos() {
371 const names = ["Linear Labs", "northwind", "PARCEL", "Halcyon", "tessellate", "Orbital", "kiln.dev", "Quanta"];
372 return (
373 <section className="fd-frg-logos" aria-label="Customers">
374 <div className="fd-frg-wrap">
375 <p className="fd-frg-mini" data-reveal="">
376 Merging 2.1 million PRs a month for 3,400 engineering teams
377 </p>
378 <ul>
379 {names.map((n, i) => (
380 <li key={n} data-reveal="" style={{ "--i": i } as CSSProperties} className={`fd-frg-wm fd-frg-wm-${i % 4}`}>
381 {n}
382 </li>
383 ))}
384 </ul>
385 </div>
386 </section>
387 );
388}
389
390/* ─────────────────────────── PR timeline (draws on scroll) ─────────────────────────── */
391
392const STEPS: { t: string; time: string; title: string; body: string; meta: ReactNode }[] = [
393 {
394 t: "00:00",
395 time: "Push",
396 title: "PR opened from your branch",
397 body: "Forge reads the diff, the dependency graph and the CODEOWNERS file before CI even starts.",
398 meta: (
399 <span className="fd-frg-chip">
400 <Branch /> feat/session-rotation → main
401 </span>
402 ),
403 },
404 {
405 t: "00:04",
406 time: "Select",
407 title: "214 of 9,812 tests selected",
408 body: "Test impact analysis maps every changed symbol to the tests that exercise it. The other 9,598 stay cached.",
409 meta: (
410 <span className="fd-frg-meter" aria-label="2.2 percent of the suite">
411 <i />
412 <b>2.2% of suite</b>
413 </span>
414 ),
415 },
416 {
417 t: "00:11",
418 time: "Build",
419 title: "Remote cache hit, 11 seconds",
420 body: "Content-addressed build artifacts are shared across every branch, so the second build is almost free.",
421 meta: <span className="fd-frg-chip">cache · 96.4% hit rate</span>,
422 },
423 {
424 t: "00:52",
425 time: "Review",
426 title: "Auto-review leaves two suggestions",
427 body: "Forge comments like a careful staff engineer: idempotency, missing indexes, unhandled errors. Never style nits.",
428 meta: <span className="fd-frg-chip fd-frg-chip-acc">2 suggestions · 0 nits</span>,
429 },
430 {
431 t: "01:14",
432 time: "Approve",
433 title: "A human signs off",
434 body: "Reviewers are routed by ownership and load. Hot files with incident history pull in a second reviewer automatically.",
435 meta: (
436 <span className="fd-frg-avs" aria-label="Approved by Jane and Nikhil">
437 <img className="" src="/img/people/p9.webp" alt="Jane Lindqvist, staff engineer, a woman with natural curly hair in a black sweater" width={28} height={28} loading="lazy" decoding="async" />
438 <img className="" src="/img/people/p7.webp" alt="Nikhil Sharma, reviewer, a bearded man in round glasses and a black sweater" width={28} height={28} loading="lazy" decoding="async" />
439 </span>
440 ),
441 },
442 {
443 t: "01:28",
444 time: "Merge",
445 title: "Queued, rebased, merged, deployed",
446 body: "The merge queue batches compatible PRs, re-runs only what changed after rebase and promotes to production.",
447 meta: <span className="fd-frg-chip">prod-us-east · v2026.10.29-4182</span>,
448 },
449];
450
451function Timeline() {
452 const ref = useRef<HTMLElement>(null);
453 const list = useRef<HTMLOListElement>(null);
454 useScrollFrame(ref, () => {
455 const l = list.current;
456 if (!l) return;
457 const r = l.getBoundingClientRect();
458 const vh = window.innerHeight;
459 const p = reducedMotion() ? 1 : clamp01((vh * 0.62 - r.top) / r.height);
460 l.style.setProperty("--draw", p.toFixed(4));
461 const lineY = p * r.height;
462 l.querySelectorAll<HTMLElement>("[data-step]").forEach((li) => {
463 if (li.offsetTop + 14 <= lineY + 1) li.setAttribute("data-on", "");
464 else li.removeAttribute("data-on");
465 });
466 });
467 return (
468 <section className="fd-frg-sec" id="how" ref={ref}>
469 <div className="fd-frg-wrap fd-frg-tl-grid">
470 <div className="fd-frg-sticky-head" data-reveal="">
471 <p className="fd-frg-eyebrow">01 · Anatomy of a merge</p>
472 <h2 className="fd-frg-h2">
473 From push to prod,
474 <br />
475 <span className="fd-frg-h2-mute">one minute twenty-eight.</span>
476 </h2>
477 <p className="fd-frg-p">
478 Every step below is a real span from PR #4182 in the acme/api monorepo — 1.4M lines, 61 services, 180
479 engineers.
480 </p>
481 </div>
482 <ol className="fd-frg-steps" ref={list}>
483 <span className="fd-frg-rail" aria-hidden="true">
484 <i />
485 </span>
486 {STEPS.map((s) => (
487 <li key={s.t} data-step="">
488 <span className="fd-frg-node" aria-hidden="true" />
489 <div className="fd-frg-step-head">
490 <span className="fd-frg-mono fd-frg-step-t">{s.t}</span>
491 <span className="fd-frg-mono fd-frg-step-k">{s.time}</span>
492 </div>
493 <h3>{s.title}</h3>
494 <p>{s.body}</p>
495 <div className="fd-frg-step-meta">{s.meta}</div>
496 </li>
497 ))}
498 </ol>
499 </div>
500 </section>
501 );
502}
503
504/* ─────────────────────────── diff with auto-review ─────────────────────────── */
505
506type DL = { n: number | null; o: number | null; k: " " | "+" | "-"; code: string };
507const DIFF: DL[] = [
508 { o: 41, n: 41, k: " ", code: "export async function rotateSession(userId: string) {" },
509 { o: 42, n: 42, k: " ", code: " const current = await db.session.findActive(userId);" },
510 { o: 43, n: null, k: "-", code: " await db.session.revoke(current.id);" },
511 { o: 44, n: null, k: "-", code: " return db.session.create({ userId });" },
512 { o: null, n: 43, k: "+", code: " return db.transaction(async (tx) => {" },
513 { o: null, n: 44, k: "+", code: " await tx.session.revoke(current.id);" },
514 { o: null, n: 45, k: "+", code: " const next = await tx.session.create({ userId });" },
515 { o: null, n: 46, k: "+", code: " audit.emit(\"session.rotated\", { userId });" },
516 { o: null, n: 47, k: "+", code: " return next;" },
517 { o: null, n: 48, k: "+", code: " });" },
518 { o: 45, n: 49, k: " ", code: "}" },
519];
520
521function Diff() {
522 const ref = useRef<HTMLDivElement>(null);
523 const seen = useInView(ref, 0.3);
524 const [stage, setStage] = useState(0);
525 const [applied, setApplied] = useState(false);
526
527 useEffect(() => {
528 if (!seen) return;
529 if (reducedMotion()) {
530 setStage(4);
531 return;
532 }
533 const ts = [400, 1500, 2600, 3500].map((ms, i) => window.setTimeout(() => setStage(i + 1), ms));
534 return () => ts.forEach((t) => window.clearTimeout(t));
535 }, [seen]);
536
537 return (
538 <section className="fd-frg-sec fd-frg-sec-alt" id="review">
539 <div className="fd-frg-wrap">
540 <div className="fd-frg-head-row" data-reveal="">
541 <div>
542 <p className="fd-frg-eyebrow">02 · Auto-review</p>
543 <h2 className="fd-frg-h2">
544 Reviews that read the code,
545 <br />
546 <span className="fd-frg-h2-mute">not the linter output.</span>
547 </h2>
548 </div>
549 <p className="fd-frg-p fd-frg-head-p">
550 Forge checks for the bugs that page people at 3 a.m. — race conditions, missing idempotency, unbounded
551 queries — and stays silent about semicolons.
552 </p>
553 </div>
554
555 <div className="fd-frg-diff" ref={ref} data-stage={stage} data-reveal="">
556 <div className="fd-frg-diff-bar">
557 <span className="fd-frg-mono">src/auth/session.ts</span>
558 <span className="fd-frg-diff-stat">
559 <b className="fd-frg-add">+6</b> <b className="fd-frg-del">−2</b>
560 </span>
561 <span className="fd-frg-diff-status" data-on={stage >= 1 || undefined}>
562 <span className="fd-frg-spin" aria-hidden="true" />
563 {stage >= 4 ? "Review complete · 2 suggestions" : stage >= 1 ? "Forge is reviewing…" : "Queued"}
564 </span>
565 </div>
566 <div className="fd-frg-diff-body">
567 <div className="fd-frg-code" role="table" aria-label="Diff of src/auth/session.ts">
568 {DIFF.map((l, i) => (
569 <div key={i} role="row" className={`fd-frg-dl fd-frg-dl-${l.k === "+" ? "add" : l.k === "-" ? "del" : "ctx"}`} data-scan={stage >= 1 || undefined} style={{ "--i": i } as CSSProperties}>
570 <span role="cell" className="fd-frg-ln">{l.o ?? ""}</span>
571 <span role="cell" className="fd-frg-ln">{l.n ?? ""}</span>
572 <span role="cell" className="fd-frg-sign">{l.k}</span>
573 <span role="cell" className="fd-frg-src">
574 {applied && i === 7 ? " audit.emit(\"session.rotated\", { userId, prev: current.id });" : l.code}
575 </span>
576 </div>
577 ))}
578 </div>
579 <div className="fd-frg-comments">
580 <article className="fd-frg-cmt" data-show={stage >= 2 || undefined}>
581 <header>
582 <span className="fd-frg-bot">
583 <Logo size={14} />
584 </span>
585 <b>forge-review</b>
586 <span className="fd-frg-mono">L43 · correctness</span>
587 </header>
588 <p>
589 Good call wrapping revoke + create in a transaction. Retries from the mobile client will still mint
590 two sessions though — key the transaction on <code>request.idempotencyKey</code>.
591 </p>
592 </article>
593 <article className="fd-frg-cmt" data-show={stage >= 3 || undefined}>
594 <header>
595 <span className="fd-frg-bot">
596 <Logo size={14} />
597 </span>
598 <b>forge-review</b>
599 <span className="fd-frg-mono">L46 · observability</span>
600 </header>
601 <p>
602 Include the previous session id so dashboards can split rotations from logouts.
603 </p>
604 <div className="fd-frg-sugg">
605 <span className="fd-frg-mono">Suggested change</span>
606 <code>- audit.emit("session.rotated", {"{ userId }"});</code>
607 <code>+ audit.emit("session.rotated", {"{ userId, prev: current.id }"});</code>
608 </div>
609 <button
610 type="button"
611 className="fd-frg-btn fd-frg-btn-sm fd-frg-btn-ghost"
612 onClick={() => setApplied(true)}
613 disabled={applied}
614 >
615 {applied ? (
616 <>
617 <Tick /> Committed · 9c41f2a
618 </>
619 ) : (
620 "Commit suggestion"
621 )}
622 </button>
623 </article>
624 <article className="fd-frg-cmt fd-frg-cmt-human" data-show={stage >= 4 || undefined}>
625 <header>
626 <img className="fd-frg-av" src="/img/people/p9.webp" alt="Jane Lindqvist, staff engineer, a woman with natural curly hair in a black sweater" width={22} height={22} loading="lazy" decoding="async" />
627 <b>jane</b>
628 <span className="fd-frg-mono">staff eng</span>
629 </header>
630 <p>This file had 14 incidents last quarter — glad it pulled me in. Approving once the key lands.</p>
631 </article>
632 </div>
633 </div>
634 </div>
635 </div>
636 </section>
637 );
638}
639
640/* ─────────────────────────── 90-second merge (scroll-scrubbed) ─────────────────────────── */
641
642const JOBS = [
643 { name: "Checkout + diff", start: 0, dur: 4 },
644 { name: "Build (cached)", start: 4, dur: 11 },
645 { name: "Unit · 214 tests", start: 15, dur: 38 },
646 { name: "Integration · 41", start: 15, dur: 52 },
647 { name: "Auto-review", start: 15, dur: 37 },
648 { name: "Human approval", start: 52, dur: 22 },
649 { name: "Merge queue + deploy", start: 74, dur: 14 },
650];
651const TOTAL = 88;
652
653function Speed() {
654 const ref = useRef<HTMLElement>(null);
655 const clock = useRef<HTMLSpanElement>(null);
656 const legacy = useRef<HTMLSpanElement>(null);
657 useScrollFrame(ref, () => {
658 const el = ref.current;
659 if (!el) return;
660 const r = el.getBoundingClientRect();
661 const vh = window.innerHeight;
662 const travel = Math.max(1, r.height - vh);
663 const p = reducedMotion() ? 1 : clamp01((-r.top) / travel);
664 const t = p * TOTAL;
665 el.style.setProperty("--p", p.toFixed(4));
666 el.querySelectorAll<HTMLElement>("[data-job]").forEach((row) => {
667 const s = Number(row.dataset.start);
668 const d = Number(row.dataset.dur);
669 const f = clamp01((t - s) / d);
670 row.style.setProperty("--f", f.toFixed(4));
671 row.dataset.state = f >= 1 ? "done" : f > 0 ? "run" : "wait";
672 });
673 if (clock.current) {
674 const sec = Math.round(t);
675 clock.current.textContent = `${Math.floor(sec / 60)}:${String(sec % 60).padStart(2, "0")}`;
676 }
677 if (legacy.current) {
678 const lt = Math.round(p * 852);
679 legacy.current.textContent = `${Math.floor(lt / 60)}:${String(lt % 60).padStart(2, "0")}`;
680 }
681 });
682 return (
683 <section className="fd-frg-speed" id="speed" ref={ref}>
684 <div className="fd-frg-speed-stick">
685 <div className="fd-frg-wrap fd-frg-speed-in">
686 <div className="fd-frg-speed-head">
687 <div>
688 <p className="fd-frg-eyebrow">03 · The ninety-second merge</p>
689 <h2 className="fd-frg-h2">
690 Same PR. <span className="fd-frg-h2-mute">Ten times sooner.</span>
691 </h2>
692 </div>
693 <p className="fd-frg-mono fd-frg-speed-hint" aria-hidden="true">Scroll to run the pipeline ↓</p>
694 </div>
695 <div className="fd-frg-clock-row">
696 <div className="fd-frg-clock">
697 <span className="fd-frg-mono">Forge</span>
698 <span ref={clock} className="fd-frg-clock-n">
699 0:00
700 </span>
701 </div>
702 <div className="fd-frg-clock fd-frg-clock-legacy">
703 <span className="fd-frg-mono">Same PR on full-suite CI</span>
704 <span ref={legacy} className="fd-frg-clock-n">
705 0:00
706 </span>
707 </div>
708 </div>
709 <div className="fd-frg-gantt" role="img" aria-label="Pipeline: seven jobs complete in 88 seconds, versus 14 minutes 12 seconds on full-suite CI">
710 <div className="fd-frg-gantt-axis" aria-hidden="true">
711 {[0, 15, 30, 45, 60, 75, 88].map((s) => (
712 <span key={s} style={{ left: `${(s / TOTAL) * 100}%` }}>
713 {s}s
714 </span>
715 ))}
716 </div>
717 {JOBS.map((j) => (
718 <div key={j.name} className="fd-frg-job" data-job="" data-start={j.start} data-dur={j.dur} data-state="wait">
719 <span className="fd-frg-job-name">
720 <i aria-hidden="true" />
721 {j.name}
722 </span>
723 <span className="fd-frg-job-track">
724 <span
725 className="fd-frg-job-bar"
726 style={{ left: `${(j.start / TOTAL) * 100}%`, width: `${(j.dur / TOTAL) * 100}%` }}
727 >
728 <i />
729 </span>
730 </span>
731 <span className="fd-frg-job-dur fd-frg-mono">{j.dur}s</span>
732 </div>
733 ))}
734 <span className="fd-frg-playhead" aria-hidden="true" />
735 </div>
736 </div>
737 </div>
738 </section>
739 );
740}
741
742/* ─────────────────────────── bento ─────────────────────────── */
743
744function Bento() {
745 return (
746 <section className="fd-frg-sec" aria-label="Features">
747 <div className="fd-frg-wrap">
748 <div className="fd-frg-head-row" data-reveal="">
749 <div>
750 <p className="fd-frg-eyebrow">04 · Built for the merge queue</p>
751 <h2 className="fd-frg-h2">
752 Less waiting.
753 <br />
754 <span className="fd-frg-h2-mute">More shipping.</span>
755 </h2>
756 </div>
757 </div>
758 <div className="fd-frg-bento">
759 <article className="fd-frg-tile fd-frg-tile-wide" data-reveal="">
760 <div className="fd-frg-tile-vis fd-frg-vis-stack" aria-hidden="true">
761 {["#4180 schema: add sessions.prev_id", "#4181 api: rotate on refresh", "#4182 auth: idempotent rotation"].map((t, i) => (
762 <div key={t} style={{ "--i": i } as CSSProperties}>
763 <Branch />
764 <span>{t}</span>
765 <em>{i === 2 ? "rebasing" : "ready"}</em>
766 </div>
767 ))}
768 </div>
769 <h3>Stacked PRs that stay stacked</h3>
770 <p>Forge rebases, re-checks and re-routes reviewers as the stack changes. Land the bottom, the rest follows.</p>
771 </article>
772 <article className="fd-frg-tile" data-reveal="" style={{ "--i": 1 } as CSSProperties}>
773 <div className="fd-frg-tile-vis fd-frg-vis-flake" aria-hidden="true">
774 {Array.from({ length: 48 }, (_, i) => (
775 <i key={i} data-f={i === 13 || i === 29 || i === 38 ? "" : undefined} />
776 ))}
777 </div>
778 <h3>Flake quarantine</h3>
779 <p>Tests that fail then pass on retry are isolated, owned and ticketed — never blocking your merge.</p>
780 </article>
781 <article className="fd-frg-tile" data-reveal="" style={{ "--i": 2 } as CSSProperties}>
782 <div className="fd-frg-tile-vis fd-frg-vis-cache" aria-hidden="true">
783 <span>96.4%</span>
784 <small>remote cache hit rate</small>
785 </div>
786 <h3>Shared build cache</h3>
787 <p>Content-addressed artifacts across every branch and laptop. Cold builds become a rounding error.</p>
788 </article>
789 <article className="fd-frg-tile fd-frg-tile-wide" data-reveal="" style={{ "--i": 3 } as CSSProperties}>
790 <div className="fd-frg-tile-vis fd-frg-vis-chart" aria-hidden="true">
791 <svg aria-hidden="true" focusable="false" viewBox="0 0 400 110" preserveAspectRatio="none">
792 <path d="M0 18 L40 22 L80 20 L120 30 L160 46 L200 62 L240 76 L280 84 L320 88 L360 92 L400 93" fill="none" stroke="var(--fd-frg-accent)" strokeWidth="2" />
793 <path d="M0 18 L40 22 L80 20 L120 30 L160 46 L200 62 L240 76 L280 84 L320 88 L360 92 L400 93 L400 110 L0 110Z" fill="var(--fd-frg-accent)" opacity="0.08" />
794 <path d="M160 0V110" stroke="rgba(255,255,255,0.2)" strokeDasharray="3 4" />
795 </svg>
796 <span className="fd-frg-chart-tag">Forge enabled · Jun 3</span>
797 </div>
798 <h3>Cycle time you’ll actually open on Monday</h3>
799 <p>PR cycle time, review latency and flake rate per team — measured from the same spans that run your CI.</p>
800 </article>
801 </div>
802 </div>
803 </section>
804 );
805}
806
807/* ─────────────────────────── integrations (spotlight) ─────────────────────────── */
808
809const INTEGRATIONS: [string, string, string][] = [
810 ["GitHub", "Checks, merge queue, CODEOWNERS", "M8 1.5a6.5 6.5 0 00-2 12.7c.3 0 .4-.2.4-.4v-1.3c-1.8.4-2.2-.8-2.2-.8-.3-.8-.7-1-.7-1-.6-.4 0-.4 0-.4.7 0 1 .7 1 .7.6 1 1.6.7 2 .5 0-.4.2-.7.4-.9-1.4-.2-3-.7-3-3.2 0-.7.3-1.3.7-1.8 0-.2-.3-.8.1-1.7 0 0 .5-.2 1.8.7a6 6 0 013.2 0c1.3-.9 1.8-.7 1.8-.7.4.9.1 1.5.1 1.7.4.5.7 1.1.7 1.8 0 2.5-1.5 3-3 3.2.3.2.5.6.5 1.2v1.8c0 .2.1.4.4.4A6.5 6.5 0 008 1.5z"],
811 ["GitLab", "MR pipelines and approvals", "M8 14.5L2 9.8l1.6-6.3 1.5 4.3h5.8l1.5-4.3L14 9.8z"],
812 ["Slack", "Review requests that respect focus time", "M4 9.5a1.3 1.3 0 11-1.3-1.3H4zm.7 0a1.3 1.3 0 012.6 0v3.3a1.3 1.3 0 11-2.6 0zM6 4a1.3 1.3 0 111.3-1.3V4zm0 .7a1.3 1.3 0 010 2.6H2.7a1.3 1.3 0 110-2.6zM11.5 6a1.3 1.3 0 111.3 1.3h-1.3zm-.7 0a1.3 1.3 0 01-2.6 0V2.7a1.3 1.3 0 112.6 0zM9.5 11.5a1.3 1.3 0 11-1.3 1.3v-1.3zm0-.7a1.3 1.3 0 010-2.6h3.3a1.3 1.3 0 110 2.6z"],
813 ["Linear", "Issue status follows the PR", "M2 9.5l4.5 4.5M2 6.8l7.2 7.2M3 4.4l8.6 8.6M5 2.6l8.4 8.4M8 1.6l6.4 6.4"],
814 ["Jira", "Transitions on merge and deploy", "M8 1.5L14.5 8 8 14.5 1.5 8zM8 5L11 8 8 11 5 8z"],
815 ["PagerDuty", "Incident history weights reviews", "M4 14.5v-13h4.5a3.5 3.5 0 010 7H4"],
816 ["Datadog", "Deploy markers and test traces", "M3 12l2-7 3 3 3-5 2 9z"],
817 ["Sentry", "Regressions linked to the PR", "M8 2l6 11H10a5 5 0 00-5-5M8 2L2 13h3"],
818 ["Vercel", "Preview URLs on every commit", "M8 2.5l6.5 11h-13z"],
819 ["AWS", "OIDC deploys to ECS and Lambda", "M2 10c3 2.5 9 2.5 12 0M11.5 10.8l2.5-.8-.6 2.5"],
820 ["Bazel", "Remote execution and cache", "M8 1.5l3 3-3 3-3-3zM4.5 5l3 3-3 3-3-3zM11.5 5l3 3-3 3-3-3zM8 8.5l3 3-3 3-3-3z"],
821 ["Terraform", "Plan diffs as review comments", "M2 2.5l3.8 2.2v4.4L2 6.9zM6.5 5.1l3.8 2.2v4.4L6.5 9.5zM11 7.3l3-1.7v4.4l-3 1.7zM6.5 10.3l3.8 2.2v1.9"],
822];
823
824function Integrations() {
825 const grid = useRef<HTMLDivElement>(null);
826 useEffect(() => {
827 const g = grid.current;
828 if (!g) return;
829 let raf = 0;
830 let ex = 0;
831 let ey = 0;
832 const apply = () => {
833 raf = 0;
834 const gr = g.getBoundingClientRect();
835 g.style.setProperty("--mx", `${ex - gr.left}px`);
836 g.style.setProperty("--my", `${ey - gr.top}px`);
837 g.querySelectorAll<HTMLElement>("[data-cell]").forEach((c) => {
838 const r = c.getBoundingClientRect();
839 c.style.setProperty("--mx", `${ex - r.left}px`);
840 c.style.setProperty("--my", `${ey - r.top}px`);
841 });
842 };
843 const move = (e: PointerEvent) => {
844 ex = e.clientX;
845 ey = e.clientY;
846 g.setAttribute("data-hot", "");
847 if (!raf) raf = requestAnimationFrame(apply);
848 };
849 const leave = () => g.removeAttribute("data-hot");
850 g.addEventListener("pointermove", move);
851 g.addEventListener("pointerleave", leave);
852 return () => {
853 g.removeEventListener("pointermove", move);
854 g.removeEventListener("pointerleave", leave);
855 if (raf) cancelAnimationFrame(raf);
856 };
857 }, []);
858 return (
859 <section className="fd-frg-sec fd-frg-sec-alt" id="integrations">
860 <div className="fd-frg-wrap">
861 <div className="fd-frg-head-row" data-reveal="">
862 <div>
863 <p className="fd-frg-eyebrow">05 · Integrations</p>
864 <h2 className="fd-frg-h2">
865 Fits the stack
866 <br />
867 <span className="fd-frg-h2-mute">you won’t migrate.</span>
868 </h2>
869 </div>
870 <p className="fd-frg-p fd-frg-head-p">
871 Forge is a GitHub App and a single YAML file. No runners to babysit, no agents in your VPC unless you want
872 them.
873 </p>
874 </div>
875 <div className="fd-frg-int" ref={grid} data-reveal="">
876 {INTEGRATIONS.map(([name, desc, d]) => (
877 <a key={name} href="#integrations" className="fd-frg-int-cell" data-cell="">
878 <svg viewBox="0 0 16 16" width="22" height="22" aria-hidden="true">
879 <path d={d} fill="none" stroke="currentColor" strokeWidth="1.1" strokeLinejoin="round" />
880 </svg>
881 <b>{name}</b>
882 <span>{desc}</span>
883 </a>
884 ))}
885 </div>
886 </div>
887 </section>
888 );
889}
890
891/* ─────────────────────────── stats + quote ─────────────────────────── */
892
893function Proof() {
894 return (
895 <section className="fd-frg-sec" aria-label="Results">
896 <div className="fd-frg-wrap">
897 <dl className="fd-frg-stats">
898 {[
899 ["14m → 90s", "median pipeline after enabling test selection"],
900 ["−41%", "PR cycle time in the first 30 days"],
901 ["0.3%", "of auto-review comments dismissed as noise"],
902 ["SOC 2", "Type II · code never used for training"],
903 ].map(([k, v], i) => (
904 <div key={k} data-reveal="" style={{ "--i": i } as CSSProperties}>
905 <dt>{k}</dt>
906 <dd>{v}</dd>
907 </div>
908 ))}
909 </dl>
910 <figure className="fd-frg-quote" data-reveal="">
911 <blockquote>
912 “We used to batch merges at 4 p.m. because CI took fifteen minutes. Now people merge whenever they’re done,
913 and the queue just… keeps up.”
914 </blockquote>
915 <figcaption>
916 <img className="fd-frg-av fd-frg-av-lg" src="/img/people/p6.webp" alt="Priya Anand, customer, a woman with long wavy dark hair in a black sweater" width={40} height={40} loading="lazy" decoding="async" />
917 <span>
918 <b>Priya Anand</b>
919 <small>Head of Platform, Tessellate · 240 engineers</small>
920 </span>
921 </figcaption>
922 </figure>
923 </div>
924 </section>
925 );
926}
927
928/* ─────────────────────────── pricing (seat calculator) ─────────────────────────── */
929
930function Pricing() {
931 const [seats, setSeats] = useState(24);
932 const [annual, setAnnual] = useState(true);
933 const per = annual ? 19 : 24;
934 const total = seats * per;
935 const pct = ((seats - 1) / 199) * 100;
936 return (
937 <section className="fd-frg-sec" id="pricing">
938 <div className="fd-frg-wrap">
939 <div className="fd-frg-head-row" data-reveal="">
940 <div>
941 <p className="fd-frg-eyebrow">06 · Pricing</p>
942 <h2 className="fd-frg-h2">
943 Per seat.
944 <br />
945 <span className="fd-frg-h2-mute">CI minutes included.</span>
946 </h2>
947 </div>
948 <div className="fd-frg-toggle" role="group" aria-label="Billing period">
949 <button type="button" aria-pressed={!annual} onClick={() => setAnnual(false)}>
950 Monthly
951 </button>
952 <button type="button" aria-pressed={annual} onClick={() => setAnnual(true)}>
953 Yearly <em>−20%</em>
954 </button>
955 <i aria-hidden="true" data-right={annual || undefined} />
956 </div>
957 </div>
958 <div className="fd-frg-plans">
959 <article className="fd-frg-plan" data-reveal="">
960 <h3>Hobby</h3>
961 <p className="fd-frg-plan-p">Open source and side projects.</p>
962 <div className="fd-frg-price">
963 <span>$0</span>
964 </div>
965 <ul>
966 {["Public repos, unlimited", "3 private repos", "2,000 CI minutes / mo", "Auto-review on 50 PRs / mo"].map((f) => (
967 <li key={f}>
968 <Tick />
969 {f}
970 </li>
971 ))}
972 </ul>
973 <a href="#top" className="fd-frg-btn fd-frg-btn-ghost">
974 Start free
975 </a>
976 </article>
977 <article className="fd-frg-plan fd-frg-plan-hot" data-reveal="" style={{ "--i": 1 } as CSSProperties}>
978 <div className="fd-frg-plan-top">
979 <h3>Team</h3>
980 <span className="fd-frg-chip fd-frg-chip-acc">Most teams</span>
981 </div>
982 <p className="fd-frg-plan-p">Merge queues, test selection, unlimited review.</p>
983 <div className="fd-frg-price">
984 <span>${total.toLocaleString("en-US")}</span>
985 <small>/ mo</small>
986 </div>
987 <p className="fd-frg-plan-calc fd-frg-mono">
988 {seats} seats × ${per} {annual ? "billed yearly" : "billed monthly"}
989 </p>
990 <label className="fd-frg-range" style={{ "--pct": `${pct}%` } as CSSProperties}>
991 <span className="fd-frg-sr">Number of seats</span>
992 <input type="range" min={1} max={200} value={seats} onChange={(e) => setSeats(Number(e.target.value))} />
993 </label>
994 <ul>
995 {["Unlimited private repos", "25,000 CI minutes / seat pool", "Smart test selection", "Merge queue + stacked PRs", "SAML SSO"].map((f) => (
996 <li key={f}>
997 <Tick />
998 {f}
999 </li>
1000 ))}
1001 </ul>
1002 <a href="#top" className="fd-frg-btn">
1003 Start 14-day trial <Arrow />
1004 </a>
1005 </article>
1006 <article className="fd-frg-plan" data-reveal="" style={{ "--i": 2 } as CSSProperties}>
1007 <h3>Enterprise</h3>
1008 <p className="fd-frg-plan-p">Self-hosted runners and custom retention.</p>
1009 <div className="fd-frg-price">
1010 <span>Custom</span>
1011 </div>
1012 <ul>
1013 {["Runners in your VPC", "SCIM, audit log streaming", "99.95% uptime SLA", "Dedicated solutions engineer"].map((f) => (
1014 <li key={f}>
1015 <Tick />
1016 {f}
1017 </li>
1018 ))}
1019 </ul>
1020 <a href="#top" className="fd-frg-btn fd-frg-btn-ghost">
1021 Talk to sales
1022 </a>
1023 </article>
1024 </div>
1025 </div>
1026 </section>
1027 );
1028}
1029
1030/* ─────────────────────────── CTA + footer ─────────────────────────── */
1031
1032function Cta() {
1033 return (
1034 <section className="fd-frg-cta">
1035 <div className="fd-frg-hero-grid" aria-hidden="true" />
1036 <div className="fd-frg-wrap fd-frg-cta-in" data-reveal="">
1037 <Logo size={40} />
1038 <h2 className="fd-frg-cta-h">Your next PR could already be merged.</h2>
1039 <p className="fd-frg-p">Install the GitHub App, add one YAML file, push. Most teams see their first sub-two-minute merge the same afternoon.</p>
1040 <div className="fd-frg-ctas">
1041 <a className="fd-frg-btn" href="#pricing">
1042 Start free <Arrow />
1043 </a>
1044 <CopyCmd cmd="npx forge init" />
1045 </div>
1046 </div>
1047 </section>
1048 );
1049}
1050
1051function Footer() {
1052 const cols: [string, string[]][] = [
1053 ["Product", ["Review", "CI", "Merge queue", "Insights", "Changelog"]],
1054 ["Developers", ["Docs", "CLI", "API", "Status", "forge.yml reference"]],
1055 ["Company", ["About", "Blog", "Careers", "Security", "Contact"]],
1056 ];
1057 return (
1058 <footer className="fd-frg-foot">
1059 <div className="fd-frg-wrap">
1060 <div className="fd-frg-foot-top">
1061 <div>
1062 <a href="#top" className="fd-frg-brand" aria-label="Forge home">
1063 <Logo />
1064 <span>Forge</span>
1065 </a>
1066 <p className="fd-frg-foot-p">Code review and CI for teams that merge on Fridays.</p>
1067 <span className="fd-frg-status">
1068 <i aria-hidden="true" /> All systems operational
1069 </span>
1070 </div>
1071 {cols.map(([h, items]) => (
1072 <div key={h} className="fd-frg-foot-col">
1073 <h4>{h}</h4>
1074 <ul>
1075 {items.map((it) => (
1076 <li key={it}>
1077 <a href="#top">{it}</a>
1078 </li>
1079 ))}
1080 </ul>
1081 </div>
1082 ))}
1083 </div>
1084 <div className="fd-frg-foot-bot">
1085 <span>© 2026 Forge Labs, Inc.</span>
1086 <span className="fd-frg-mono">build 2026.10.29 · 4f1c9e2</span>
1087 </div>
1088 </div>
1089 </footer>
1090 );
1091}
1092
1093/* ─────────────────────────── page ─────────────────────────── */
1094
1095export function Page() {
1096 const root = useRef<HTMLDivElement>(null);
1097 useReveal(root);
1098 return (
1099 <div className="fd-frg" ref={root}>
1100 <style>{css}</style>
1101 <Nav />
1102 <main>
1103 <Hero />
1104 <Logos />
1105 <Timeline />
1106 <Diff />
1107 <Speed />
1108 <Bento />
1109 <Integrations />
1110 <Proof />
1111 <Pricing />
1112 <Cta />
1113 </main>
1114 <Footer />
1115 </div>
1116 );
1117}
1118
1119export default Page;
1120
1121/* ─────────────────────────── styles ─────────────────────────── */
1122
1123const css = `
1124.fd-frg {
1125 --fd-ease-out: cubic-bezier(0.22, 1, 0.36, 1);
1126 --fd-ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
1127 --fd-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
1128 --fd-dur-1: 120ms;
1129 --fd-dur-2: 240ms;
1130 --fd-dur-3: 420ms;
1131 --fd-dur-4: 700ms;
1132
1133 --fd-frg-bg: #0a0a0a;
1134 --fd-frg-bg-2: #0f0f10;
1135 --fd-frg-surface: #141415;
1136 --fd-frg-surface-2: #1b1b1d;
1137 --fd-frg-fg: #ededed;
1138 --fd-frg-mute: #a1a1a1;
1139 --fd-frg-dim: #8a8a8a;
1140 --fd-frg-line: rgba(255, 255, 255, 0.08);
1141 --fd-frg-line-2: rgba(255, 255, 255, 0.14);
1142 --fd-frg-accent: #ff6b2c;
1143 --fd-frg-accent-soft: rgba(255, 107, 44, 0.12);
1144 --fd-frg-add: #3fb950;
1145 --fd-frg-del: #f2555a;
1146 --fd-frg-sans: "Geist", -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter Variable", "Inter", "Segoe UI", Roboto, sans-serif;
1147 --fd-frg-mono-f: "Geist Mono", ui-monospace, "SF Mono", "JetBrains Mono Variable", "JetBrains Mono", Menlo, Consolas, monospace;
1148
1149 position: relative; display: block; margin: 0; padding: 0;
1150 background: var(--fd-frg-bg); color: var(--fd-frg-fg);
1151 font: 400 1rem/1.5 var(--fd-frg-sans); letter-spacing: -0.006em; text-align: left;
1152 -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
1153 overflow-x: clip; color-scheme: dark;
1154}
1155.fd-frg, .fd-frg *, .fd-frg *::before, .fd-frg *::after { box-sizing: border-box; }
1156.fd-frg :where(h1, h2, h3, h4, p, ul, ol, li, dl, dt, dd, figure, figcaption, blockquote, main, header, footer, section, nav, article, label) {
1157 margin: 0; padding: 0; border: 0; font: inherit; color: inherit; letter-spacing: inherit; background: none;
1158}
1159.fd-frg :where(ul, ol) { list-style: none; }
1160.fd-frg :where(a) { color: inherit; text-decoration: none; }
1161.fd-frg :where(button) { appearance: none; -webkit-appearance: none; margin: 0; padding: 0; border: 0; background: none; font: inherit; color: inherit; letter-spacing: inherit; cursor: pointer; text-align: inherit; line-height: inherit; }
1162.fd-frg :where(button:disabled) { cursor: default; }
1163.fd-frg :where(svg) { display: block; overflow: visible; }
1164.fd-frg :where(code) { font: 400 0.92em/1.5 var(--fd-frg-mono-f); background: none; padding: 0; color: inherit; }
1165.fd-frg :where(b, strong) { font-weight: 600; }
1166.fd-frg :where(em) { font-style: normal; }
1167.fd-frg :where(small) { font-size: inherit; }
1168.fd-frg :where(a, button, input, [tabindex]):focus-visible { outline: 2px solid rgba(255, 107, 44, 0.7); outline-offset: 2px; border-radius: 6px; }
1169.fd-frg-sr { position: absolute !important; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
1170
1171.fd-frg-wrap { width: 100%; max-width: 1200px; margin: 0 auto; padding: 0 32px; }
1172.fd-frg-mono { font: 400 0.75rem/1.4 var(--fd-frg-mono-f); letter-spacing: 0; }
1173.fd-frg-mini { font: 400 0.8125rem/1.4 var(--fd-frg-sans); color: var(--fd-frg-mute); text-align: center; }
1174.fd-frg-eyebrow { font: 500 0.8125rem/1.4 var(--fd-frg-mono-f); color: var(--fd-frg-accent); letter-spacing: 0; }
1175.fd-frg-h2 { margin-top: 14px; font: 600 clamp(2rem, 4.2vw, 3.25rem)/1.04 var(--fd-frg-sans); letter-spacing: -0.04em; }
1176.fd-frg-h2-mute { color: var(--fd-frg-dim); }
1177.fd-frg-p { margin-top: 18px; max-width: 34em; font: 400 1.0625rem/1.55 var(--fd-frg-sans); color: var(--fd-frg-mute); }
1178.fd-frg-head-row { display: flex; justify-content: space-between; align-items: flex-end; gap: 32px; flex-wrap: wrap; }
1179.fd-frg-head-p { max-width: 26em; margin: 0; }
1180
1181.fd-frg-btn {
1182 display: inline-flex; align-items: center; justify-content: center; gap: 8px; height: 44px; padding: 0 18px;
1183 border: 1px solid var(--fd-frg-fg); border-radius: 9999px; background: var(--fd-frg-fg); color: var(--fd-frg-bg);
1184 font: 500 0.9375rem/1 var(--fd-frg-sans); letter-spacing: -0.01em; white-space: nowrap;
1185 transition: transform var(--fd-dur-1) var(--fd-ease-out), background-color var(--fd-dur-2) var(--fd-ease-out), border-color var(--fd-dur-2) var(--fd-ease-out), color var(--fd-dur-2) var(--fd-ease-out);
1186}
1187.fd-frg-btn:hover { background: #fff; }
1188.fd-frg-btn:active { transform: scale(0.96); }
1189.fd-frg-btn .fd-frg-arrow { transition: transform var(--fd-dur-2) var(--fd-spring); }
1190.fd-frg-btn:hover .fd-frg-arrow { transform: translateX(3px); }
1191.fd-frg-btn-sm { height: 34px; padding: 0 14px; font-size: 0.8125rem; }
1192.fd-frg-btn-ghost { background: transparent; color: var(--fd-frg-fg); border-color: var(--fd-frg-line-2); }
1193.fd-frg-btn-ghost:hover { background: var(--fd-frg-surface-2); border-color: rgba(255,255,255,0.3); }
1194.fd-frg-btn-ghost:disabled { color: var(--fd-frg-mute); }
1195.fd-frg-cmd {
1196 display: inline-flex; align-items: center; gap: 12px; height: 44px; padding: 0 8px 0 16px; border: 1px solid var(--fd-frg-line-2); border-radius: 9999px;
1197 background: var(--fd-frg-bg-2); font: 400 0.875rem/1 var(--fd-frg-mono-f); color: var(--fd-frg-fg);
1198 transition: border-color var(--fd-dur-2) var(--fd-ease-out), transform var(--fd-dur-1) var(--fd-ease-out);
1199}
1200.fd-frg-cmd:hover { border-color: rgba(255,255,255,0.3); }
1201.fd-frg-cmd:active { transform: scale(0.97); }
1202.fd-frg-cmd-p { color: var(--fd-frg-accent); }
1203.fd-frg-cmd-c { display: inline-flex; align-items: center; height: 28px; padding: 0 10px; border-radius: 9999px; background: var(--fd-frg-surface-2); font: 500 0.75rem/1 var(--fd-frg-sans); color: var(--fd-frg-mute); }
1204.fd-frg-chip { display: inline-flex; align-items: center; gap: 6px; height: 26px; padding: 0 10px; border: 1px solid var(--fd-frg-line-2); border-radius: 9999px; font: 400 0.75rem/1 var(--fd-frg-mono-f); color: var(--fd-frg-mute); white-space: nowrap; }
1205.fd-frg-chip-acc { color: var(--fd-frg-accent); border-color: rgba(255,107,44,0.35); background: var(--fd-frg-accent-soft); }
1206
1207/* nav */
1208.fd-frg-nav { position: sticky; top: 0; z-index: 50; display: block; border-bottom: 1px solid transparent; transition: background-color var(--fd-dur-3) var(--fd-ease-out), border-color var(--fd-dur-3) var(--fd-ease-out); }
1209.fd-frg-nav[data-scrolled], .fd-frg-nav[data-open] { background: rgba(10,10,10,0.78); border-bottom-color: var(--fd-frg-line); -webkit-backdrop-filter: saturate(160%) blur(14px); backdrop-filter: saturate(160%) blur(14px); }
1210.fd-frg-nav-in { display: flex; align-items: center; gap: 28px; max-width: 1200px; height: 64px; margin: 0 auto; padding: 0 32px; transition: height var(--fd-dur-3) var(--fd-ease-out); }
1211.fd-frg-nav[data-scrolled] .fd-frg-nav-in { height: 54px; }
1212.fd-frg-brand { display: inline-flex; align-items: center; gap: 9px; font: 600 1rem/1 var(--fd-frg-sans); letter-spacing: -0.02em; color: var(--fd-frg-fg); }
1213.fd-frg-nav-links { display: flex; gap: 4px; }
1214.fd-frg-nav-links a, .fd-frg-nav-ghost { display: inline-flex; align-items: center; height: 32px; padding: 0 12px; border-radius: 9999px; font: 400 0.875rem/1 var(--fd-frg-sans); color: var(--fd-frg-mute); transition: color var(--fd-dur-1) linear, background-color var(--fd-dur-1) linear; }
1215.fd-frg-nav-links a:hover, .fd-frg-nav-ghost:hover { color: var(--fd-frg-fg); background: var(--fd-frg-surface-2); }
1216.fd-frg-nav-right { display: flex; align-items: center; gap: 8px; margin-left: auto; }
1217.fd-frg-burger { display: none; position: relative; width: 40px; height: 40px; }
1218.fd-frg-burger span { position: absolute; left: 11px; right: 11px; height: 1.5px; background: var(--fd-frg-fg); transition: transform var(--fd-dur-3) var(--fd-spring); }
1219.fd-frg-burger span:first-child { top: 16px; }
1220.fd-frg-burger span:last-child { top: 23px; }
1221.fd-frg-nav[data-open] .fd-frg-burger span:first-child { transform: translateY(3.5px) rotate(45deg); }
1222.fd-frg-nav[data-open] .fd-frg-burger span:last-child { transform: translateY(-3.5px) rotate(-45deg); }
1223.fd-frg-sheet { display: grid; padding: 4px 20px 16px; }
1224.fd-frg-sheet[hidden] { display: none; }
1225.fd-frg-sheet a { padding: 14px 0; border-bottom: 1px solid var(--fd-frg-line); font: 500 1.125rem/1.2 var(--fd-frg-sans); }
1226
1227/* hero */
1228.fd-frg-hero { position: relative; padding: 96px 0 120px; margin-top: -64px; padding-top: 160px; overflow: hidden; isolation: isolate; }
1229.fd-frg-hero-grid {
1230 position: absolute; inset: 0; z-index: -1; pointer-events: none;
1231 background-image: linear-gradient(to right, var(--fd-frg-line) 1px, transparent 1px), linear-gradient(to bottom, var(--fd-frg-line) 1px, transparent 1px);
1232 background-size: 64px 64px; background-position: center -1px;
1233 -webkit-mask-image: radial-gradient(ellipse 60% 55% at 50% 30%, #000 0%, transparent 75%); mask-image: radial-gradient(ellipse 60% 55% at 50% 30%, #000 0%, transparent 75%);
1234}
1235.fd-frg-hero::after { content: ""; position: absolute; left: 50%; top: 220px; width: 900px; height: 500px; transform: translateX(-50%); z-index: -1; background: radial-gradient(closest-side, rgba(255,107,44,0.14), transparent); pointer-events: none; }
1236.fd-frg-hero-in { display: flex; flex-direction: column; align-items: center; text-align: center; }
1237.fd-frg-pill { display: inline-flex; align-items: center; gap: 10px; height: 32px; padding: 0 12px 0 4px; border: 1px solid var(--fd-frg-line-2); border-radius: 9999px; background: rgba(20,20,21,0.8); font: 400 0.8125rem/1 var(--fd-frg-sans); color: var(--fd-frg-mute); transition: border-color var(--fd-dur-2) var(--fd-ease-out), color var(--fd-dur-2); }
1238.fd-frg-pill:hover { border-color: rgba(255,255,255,0.3); color: var(--fd-frg-fg); }
1239.fd-frg-pill-new { display: inline-flex; align-items: center; height: 24px; padding: 0 9px; border-radius: 9999px; background: var(--fd-frg-accent-soft); color: var(--fd-frg-accent); font: 500 0.75rem/1 var(--fd-frg-mono-f); }
1240.fd-frg-h1 { display: flex; flex-direction: column; margin-top: 28px; font: 600 clamp(3rem, 8vw, 6.5rem)/0.96 var(--fd-frg-sans); letter-spacing: -0.055em; }
1241.fd-frg-h1-mute { color: var(--fd-frg-dim); }
1242.fd-frg-in { animation: fd-frg-in 1000ms var(--fd-ease-out) both; animation-delay: var(--d, 0ms); }
1243@keyframes fd-frg-in { from { opacity: 0; transform: translateY(16px); filter: blur(6px); } to { opacity: 1; transform: none; filter: none; } }
1244.fd-frg-lede { max-width: 36em; margin-top: 26px; font: 400 clamp(1.0625rem, 1.5vw, 1.25rem)/1.5 var(--fd-frg-sans); color: var(--fd-frg-mute); }
1245.fd-frg-ctas { display: flex; flex-wrap: wrap; justify-content: center; gap: 12px; margin-top: 34px; }
1246.fd-frg-hero-term { width: 100%; max-width: 820px; margin-top: 64px; text-align: left; }
1247
1248/* terminal */
1249.fd-frg-term { position: relative; border: 1px solid var(--fd-frg-line-2); border-radius: 14px; background: linear-gradient(180deg, #121213, #0c0c0d); box-shadow: 0 0 0 1px rgba(0,0,0,0.6), 0 40px 80px -30px rgba(0,0,0,0.9), 0 0 120px -40px rgba(255,107,44,0.25); overflow: hidden; }
1250.fd-frg-term-bar { display: flex; align-items: center; gap: 12px; height: 40px; padding: 0 12px 0 14px; border-bottom: 1px solid var(--fd-frg-line); }
1251.fd-frg-term-dots { display: flex; gap: 6px; }
1252.fd-frg-term-dots i { width: 10px; height: 10px; border-radius: 50%; background: var(--fd-frg-surface-2); border: 1px solid var(--fd-frg-line-2); }
1253.fd-frg-term-title { flex: 1; font: 400 0.75rem/1 var(--fd-frg-mono-f); color: var(--fd-frg-dim); text-align: center; }
1254.fd-frg-term-replay { height: 24px; padding: 0 10px; border-radius: 9999px; border: 1px solid var(--fd-frg-line-2); font: 500 0.6875rem/1 var(--fd-frg-sans); color: var(--fd-frg-mute); transition: opacity var(--fd-dur-2); }
1255.fd-frg-term-replay:disabled { opacity: 0.35; }
1256.fd-frg-term-body { height: 320px; padding: 18px 20px; font: 400 0.8125rem/1.85 var(--fd-frg-mono-f); color: var(--fd-frg-fg); overflow: hidden; }
1257.fd-frg-tl { display: flex; gap: 10px; align-items: center; white-space: pre; animation: fd-frg-line 280ms var(--fd-ease-out) both; }
1258@keyframes fd-frg-line { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
1259.fd-frg-tl-cmd { animation: none; }
1260.fd-frg-tl-dim { color: var(--fd-frg-dim); }
1261.fd-frg-tl-out { color: var(--fd-frg-mute); }
1262.fd-frg-tl-ok { color: var(--fd-frg-fg); }
1263.fd-frg-tl-warn { color: var(--fd-frg-accent); }
1264.fd-frg-tl-sum { margin-top: 6px; color: var(--fd-frg-fg); font-weight: 500; }
1265.fd-frg-sum-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--fd-frg-add); box-shadow: 0 0 0 4px rgba(63,185,80,0.15); }
1266.fd-frg-prompt { color: var(--fd-frg-accent); }
1267.fd-frg-caret { display: inline-block; width: 8px; height: 1.1em; margin-left: -6px; background: var(--fd-frg-fg); animation: fd-frg-blink 1s steps(2, start) infinite; }
1268@keyframes fd-frg-blink { to { visibility: hidden; } }
1269
1270/* logos */
1271.fd-frg-logos { padding: 0 0 96px; }
1272.fd-frg-logos ul { display: grid; grid-template-columns: repeat(8, minmax(0, 1fr)); margin-top: 28px; border-top: 1px solid var(--fd-frg-line); border-bottom: 1px solid var(--fd-frg-line); }
1273.fd-frg-logos li { display: grid; place-items: center; height: 84px; color: var(--fd-frg-mute); border-right: 1px solid var(--fd-frg-line); white-space: nowrap; transition: color var(--fd-dur-2) var(--fd-ease-out); }
1274.fd-frg-logos li:last-child { border-right: 0; }
1275.fd-frg-logos li:hover { color: var(--fd-frg-fg); }
1276.fd-frg-wm-0 { font: 600 1.0625rem/1 var(--fd-frg-sans); letter-spacing: -0.03em; }
1277.fd-frg-wm-1 { font: 400 1.125rem/1 var(--fd-frg-sans); letter-spacing: -0.04em; }
1278.fd-frg-wm-2 { font: 600 0.875rem/1 var(--fd-frg-mono-f); letter-spacing: 0.08em; }
1279.fd-frg-wm-3 { font: italic 500 1.125rem/1 Georgia, serif; }
1280
1281/* sections */
1282.fd-frg-sec { padding: 128px 0; border-top: 1px solid var(--fd-frg-line); }
1283.fd-frg-sec-alt { background: var(--fd-frg-bg-2); }
1284
1285/* timeline */
1286.fd-frg-tl-grid { display: grid; grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr); gap: 72px; align-items: start; }
1287.fd-frg-sticky-head { position: sticky; top: 120px; }
1288.fd-frg-steps { --draw: 0; position: relative; padding-left: 44px; }
1289.fd-frg-rail { position: absolute; left: 11px; top: 14px; bottom: 14px; width: 1px; background: var(--fd-frg-line-2); }
1290.fd-frg-rail i { position: absolute; inset: 0; background: linear-gradient(to bottom, var(--fd-frg-accent), #ffb08a); transform-origin: top; transform: scaleY(var(--draw)); box-shadow: 0 0 12px rgba(255,107,44,0.6); }
1291.fd-frg-steps li { position: relative; padding: 0 0 56px; opacity: 0.4; transition: opacity var(--fd-dur-3) var(--fd-ease-out); }
1292.fd-frg-steps li:last-child { padding-bottom: 0; }
1293.fd-frg-steps li[data-on] { opacity: 1; }
1294.fd-frg-node { position: absolute; left: -38px; top: 8px; width: 11px; height: 11px; border-radius: 50%; background: var(--fd-frg-bg); border: 1px solid var(--fd-frg-line-2); transition: background-color var(--fd-dur-2) var(--fd-ease-out), border-color var(--fd-dur-2), box-shadow var(--fd-dur-3) var(--fd-ease-out), transform var(--fd-dur-3) var(--fd-spring); }
1295.fd-frg-steps li[data-on] .fd-frg-node { background: var(--fd-frg-accent); border-color: var(--fd-frg-accent); box-shadow: 0 0 0 5px var(--fd-frg-accent-soft); transform: scale(1.1); }
1296.fd-frg-step-head { display: flex; gap: 12px; align-items: center; }
1297.fd-frg-step-t { color: var(--fd-frg-fg); font-variant-numeric: tabular-nums; }
1298.fd-frg-step-k { color: var(--fd-frg-dim); text-transform: uppercase; letter-spacing: 0.06em; }
1299.fd-frg-steps h3 { margin-top: 8px; font: 600 1.375rem/1.25 var(--fd-frg-sans); letter-spacing: -0.025em; }
1300.fd-frg-steps p { margin-top: 8px; max-width: 32em; font: 400 0.9688rem/1.55 var(--fd-frg-sans); color: var(--fd-frg-mute); }
1301.fd-frg-step-meta { margin-top: 14px; }
1302.fd-frg-meter { display: inline-flex; align-items: center; gap: 10px; }
1303.fd-frg-meter > i { display: grid; grid-template-columns: repeat(46, 1fr); gap: 2px; width: 230px; height: 14px; background: repeating-linear-gradient(to right, var(--fd-frg-surface-2) 0 3px, transparent 3px 5px); }
1304.fd-frg-meter > i::before { content: ""; width: 3px; height: 100%; background: var(--fd-frg-accent); box-shadow: 0 0 8px var(--fd-frg-accent); }
1305.fd-frg-meter b { font: 400 0.75rem/1 var(--fd-frg-mono-f); color: var(--fd-frg-mute); white-space: nowrap; }
1306.fd-frg-avs { display: inline-flex; }
1307.fd-frg-avs img, .fd-frg-av { display: inline-block; flex: none; width: 28px; height: 28px; border-radius: 50%; object-fit: cover; object-position: center top; background: var(--fd-frg-surface-2); border: 1px solid var(--fd-frg-line-2); }
1308.fd-frg-avs img + img { margin-left: -8px; }
1309
1310/* diff */
1311.fd-frg-diff { margin-top: 56px; border: 1px solid var(--fd-frg-line-2); border-radius: 14px; background: var(--fd-frg-bg); overflow: hidden; }
1312.fd-frg-diff-bar { display: flex; align-items: center; gap: 16px; height: 46px; padding: 0 16px; border-bottom: 1px solid var(--fd-frg-line); color: var(--fd-frg-fg); }
1313.fd-frg-diff-stat { font: 500 0.75rem/1 var(--fd-frg-mono-f); }
1314.fd-frg-add { color: var(--fd-frg-add); }
1315.fd-frg-del { color: var(--fd-frg-del); }
1316.fd-frg-diff-status { display: inline-flex; align-items: center; gap: 8px; margin-left: auto; font: 400 0.8125rem/1 var(--fd-frg-sans); color: var(--fd-frg-mute); }
1317.fd-frg-spin { width: 12px; height: 12px; border-radius: 50%; border: 1.5px solid var(--fd-frg-line-2); border-top-color: var(--fd-frg-accent); }
1318.fd-frg-diff-status[data-on] .fd-frg-spin { animation: fd-frg-spin 0.8s linear infinite; }
1319.fd-frg-diff[data-stage="4"] .fd-frg-spin { animation: none; border-color: var(--fd-frg-accent); background: var(--fd-frg-accent); }
1320@keyframes fd-frg-spin { to { transform: rotate(360deg); } }
1321.fd-frg-diff-body { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(0, 0.75fr); }
1322.fd-frg-code { padding: 12px 0; font: 400 0.8125rem/1.9 var(--fd-frg-mono-f); overflow-x: auto; border-right: 1px solid var(--fd-frg-line); }
1323.fd-frg-dl { position: relative; display: grid; grid-template-columns: 40px 40px 22px max-content; min-width: 100%; white-space: pre; }
1324.fd-frg-dl::after { content: ""; position: absolute; inset: 0; background: linear-gradient(90deg, transparent, rgba(255,107,44,0.18), transparent); opacity: 0; pointer-events: none; }
1325.fd-frg-dl[data-scan]::after { animation: fd-frg-scan 900ms var(--fd-ease-out) both; animation-delay: calc(var(--i) * 70ms); }
1326@keyframes fd-frg-scan { 0% { opacity: 0; transform: translateX(-30%); } 40% { opacity: 1; } 100% { opacity: 0; transform: translateX(30%); } }
1327.fd-frg-ln { padding-right: 12px; color: #5f5f63; text-align: right; user-select: none; font-variant-numeric: tabular-nums; }
1328.fd-frg-sign { color: var(--fd-frg-dim); user-select: none; }
1329.fd-frg-src { padding-right: 20px; color: #d6d6d6; }
1330.fd-frg-dl-add { background: rgba(63,185,80,0.1); }
1331.fd-frg-dl-add .fd-frg-sign, .fd-frg-dl-add .fd-frg-ln { color: var(--fd-frg-add); }
1332.fd-frg-dl-del { background: rgba(242,85,90,0.1); }
1333.fd-frg-dl-del .fd-frg-sign, .fd-frg-dl-del .fd-frg-ln { color: var(--fd-frg-del); }
1334.fd-frg-dl-del .fd-frg-src { color: #b9b9b9; }
1335.fd-frg-comments { display: flex; flex-direction: column; gap: 12px; padding: 16px; background: var(--fd-frg-bg-2); }
1336.fd-frg-cmt { padding: 14px; border: 1px solid var(--fd-frg-line-2); border-radius: 12px; background: var(--fd-frg-surface); opacity: 0; transform: translateY(10px) scale(0.96); transform-origin: top left; transition: opacity var(--fd-dur-3) var(--fd-ease-out), transform var(--fd-dur-3) var(--fd-spring); }
1337.fd-frg-cmt[data-show] { opacity: 1; transform: none; }
1338.fd-frg-cmt header { display: flex; align-items: center; gap: 8px; font: 400 0.8125rem/1 var(--fd-frg-sans); }
1339.fd-frg-cmt header .fd-frg-mono { margin-left: auto; color: var(--fd-frg-dim); font-size: 0.6875rem; }
1340.fd-frg-bot { display: grid; place-items: center; width: 22px; height: 22px; border-radius: 6px; background: var(--fd-frg-accent-soft); color: var(--fd-frg-accent); }
1341.fd-frg-cmt .fd-frg-av { width: 22px; height: 22px; font-size: 0.5625rem; }
1342.fd-frg-cmt p { margin-top: 10px; font: 400 0.875rem/1.5 var(--fd-frg-sans); color: #c9c9c9; }
1343.fd-frg-cmt p code { color: var(--fd-frg-accent); }
1344.fd-frg-sugg { display: grid; gap: 2px; margin: 12px 0; padding: 10px; border: 1px solid var(--fd-frg-line); border-radius: 8px; background: var(--fd-frg-bg); overflow-x: auto; }
1345.fd-frg-sugg .fd-frg-mono { color: var(--fd-frg-dim); margin-bottom: 4px; font-size: 0.6875rem; }
1346.fd-frg-sugg code { white-space: pre-wrap; word-break: break-word; font-size: 0.75rem; }
1347.fd-frg-sugg code:nth-of-type(1) { color: var(--fd-frg-del); }
1348.fd-frg-sugg code:nth-of-type(2) { color: var(--fd-frg-add); }
1349.fd-frg-cmt-human { border-style: dashed; }
1350
1351/* speed */
1352.fd-frg-speed { --p: 0; position: relative; height: 320vh; border-top: 1px solid var(--fd-frg-line); background: var(--fd-frg-bg); }
1353.fd-frg-speed-stick { position: sticky; top: 0; height: 100vh; display: flex; align-items: center; overflow: hidden; }
1354.fd-frg-speed-in { display: grid; gap: 36px; }
1355.fd-frg-speed-head { display: flex; justify-content: space-between; align-items: flex-end; flex-wrap: wrap; gap: 16px; }
1356.fd-frg-speed-hint { color: var(--fd-frg-dim); }
1357.fd-frg-clock-row { display: grid; grid-template-columns: 1.3fr 1fr; border: 1px solid var(--fd-frg-line-2); border-radius: 14px; overflow: hidden; }
1358.fd-frg-clock { display: flex; flex-direction: column; gap: 8px; padding: 20px 24px; }
1359.fd-frg-clock .fd-frg-mono { color: var(--fd-frg-accent); text-transform: uppercase; letter-spacing: 0.06em; }
1360.fd-frg-clock-n { font: 600 clamp(3.5rem, 9vw, 7.5rem)/0.9 var(--fd-frg-sans); letter-spacing: -0.06em; font-variant-numeric: tabular-nums; }
1361.fd-frg-clock-legacy { border-left: 1px solid var(--fd-frg-line); background: repeating-linear-gradient(135deg, transparent 0 8px, rgba(255,255,255,0.025) 8px 9px); }
1362.fd-frg-clock-legacy .fd-frg-mono { color: var(--fd-frg-dim); }
1363.fd-frg-clock-legacy .fd-frg-clock-n { color: #5f5f63; }
1364.fd-frg-gantt { position: relative; display: grid; gap: 8px; padding: 30px 0 4px; }
1365.fd-frg-gantt-axis { position: absolute; left: 190px; right: 56px; top: 0; height: 18px; }
1366.fd-frg-gantt-axis span { position: absolute; transform: translateX(-50%); font: 400 0.6875rem/1 var(--fd-frg-mono-f); color: var(--fd-frg-dim); }
1367.fd-frg-job { --f: 0; display: grid; grid-template-columns: 190px 1fr 56px; align-items: center; height: 30px; }
1368.fd-frg-job-name { display: flex; align-items: center; gap: 10px; font: 400 0.875rem/1 var(--fd-frg-sans); color: var(--fd-frg-mute); transition: color var(--fd-dur-2); }
1369.fd-frg-job-name i { width: 8px; height: 8px; border-radius: 50%; border: 1px solid var(--fd-frg-line-2); transition: background-color var(--fd-dur-2), border-color var(--fd-dur-2), box-shadow var(--fd-dur-2); }
1370.fd-frg-job[data-state="run"] .fd-frg-job-name { color: var(--fd-frg-fg); }
1371.fd-frg-job[data-state="run"] .fd-frg-job-name i { border-color: var(--fd-frg-accent); box-shadow: 0 0 0 3px var(--fd-frg-accent-soft); }
1372.fd-frg-job[data-state="done"] .fd-frg-job-name { color: var(--fd-frg-fg); }
1373.fd-frg-job[data-state="done"] .fd-frg-job-name i { background: var(--fd-frg-fg); border-color: var(--fd-frg-fg); }
1374.fd-frg-job-track { position: relative; height: 100%; border-left: 1px solid var(--fd-frg-line); background-image: linear-gradient(to right, var(--fd-frg-line) 1px, transparent 1px); background-size: calc(100% / 88 * 15) 100%; }
1375.fd-frg-job-bar { position: absolute; top: 6px; bottom: 6px; border-radius: 5px; background: var(--fd-frg-surface-2); border: 1px solid var(--fd-frg-line-2); overflow: hidden; }
1376.fd-frg-job-bar i { position: absolute; inset: 0; background: linear-gradient(90deg, rgba(255,107,44,0.7), var(--fd-frg-accent)); transform-origin: left; transform: scaleX(var(--f)); }
1377.fd-frg-job[data-state="done"] .fd-frg-job-bar i { background: #e6e6e6; }
1378.fd-frg-job-dur { text-align: right; color: var(--fd-frg-dim); }
1379.fd-frg-playhead { position: absolute; top: 22px; bottom: 0; left: calc(190px + (100% - 190px - 56px) * var(--p)); width: 1px; background: var(--fd-frg-accent); box-shadow: 0 0 10px var(--fd-frg-accent); }
1380
1381/* bento */
1382.fd-frg-bento { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1px; margin-top: 56px; border: 1px solid var(--fd-frg-line); border-radius: 16px; background: var(--fd-frg-line); overflow: hidden; }
1383.fd-frg-tile { display: flex; flex-direction: column; padding: 28px; background: var(--fd-frg-bg); min-height: 360px; }
1384.fd-frg-tile-wide { grid-column: span 2; }
1385.fd-frg-tile h3 { margin-top: auto; padding-top: 24px; font: 600 1.1875rem/1.3 var(--fd-frg-sans); letter-spacing: -0.02em; }
1386.fd-frg-tile p { margin-top: 8px; max-width: 34em; font: 400 0.9375rem/1.55 var(--fd-frg-sans); color: var(--fd-frg-mute); }
1387.fd-frg-tile-vis { flex: 1; min-height: 160px; }
1388.fd-frg-vis-stack { display: flex; flex-direction: column; justify-content: center; gap: 8px; }
1389.fd-frg-vis-stack div { min-width: 0; display: flex; align-items: center; gap: 10px; height: 44px; padding: 0 14px; border: 1px solid var(--fd-frg-line-2); border-radius: 10px; background: var(--fd-frg-surface); margin-left: calc(var(--i) * 28px); font: 400 0.8125rem/1 var(--fd-frg-mono-f); color: var(--fd-frg-fg); transition: transform var(--fd-dur-3) var(--fd-spring); }
1390.fd-frg-tile:hover .fd-frg-vis-stack div { transform: translateX(calc(var(--i) * 6px)); }
1391.fd-frg-vis-stack svg { color: var(--fd-frg-dim); flex: none; }
1392.fd-frg-vis-stack span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1393.fd-frg-vis-stack em { margin-left: auto; font-size: 0.6875rem; color: var(--fd-frg-dim); }
1394.fd-frg-vis-stack div:last-child em { color: var(--fd-frg-accent); }
1395.fd-frg-vis-flake { display: grid; grid-template-columns: repeat(12, 1fr); gap: 6px; align-content: center; }
1396.fd-frg-vis-flake i { aspect-ratio: 1; border-radius: 3px; background: var(--fd-frg-surface-2); }
1397.fd-frg-vis-flake i[data-f] { background: var(--fd-frg-accent); animation: fd-frg-flake 2.4s var(--fd-ease-in-out) infinite; }
1398@keyframes fd-frg-flake { 50% { opacity: 0.35; } }
1399.fd-frg-vis-cache { display: flex; flex-direction: column; justify-content: center; }
1400.fd-frg-vis-cache span { font: 600 4.5rem/1 var(--fd-frg-sans); letter-spacing: -0.06em; background: linear-gradient(180deg, #fff, #6b6b6b); -webkit-background-clip: text; background-clip: text; color: transparent; }
1401.fd-frg-vis-cache small { margin-top: 8px; font: 400 0.75rem/1 var(--fd-frg-mono-f); color: var(--fd-frg-dim); }
1402.fd-frg-vis-chart { position: relative; }
1403.fd-frg-vis-chart svg { width: 100%; height: 100%; min-height: 160px; }
1404.fd-frg-chart-tag { position: absolute; left: 41%; top: 6px; font: 400 0.6875rem/1 var(--fd-frg-mono-f); color: var(--fd-frg-mute); }
1405
1406/* integrations */
1407.fd-frg-int {
1408 --mx: -999px; --my: -999px; position: relative; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1px; margin-top: 56px;
1409 border-radius: 16px; overflow: hidden; padding: 1px;
1410 background: radial-gradient(360px circle at var(--mx) var(--my), rgba(255,107,44,0.75), transparent 60%), var(--fd-frg-line);
1411}
1412.fd-frg-int-cell { --mx: -999px; --my: -999px; position: relative; display: flex; flex-direction: column; gap: 6px; min-height: 148px; padding: 22px; background: var(--fd-frg-bg-2); color: var(--fd-frg-fg); transition: background-color var(--fd-dur-2) var(--fd-ease-out); }
1413.fd-frg-int-cell::before { content: ""; position: absolute; inset: 0; background: radial-gradient(260px circle at var(--mx) var(--my), rgba(255,107,44,0.09), transparent 65%); opacity: 0; transition: opacity var(--fd-dur-3) var(--fd-ease-out); pointer-events: none; }
1414.fd-frg-int[data-hot] .fd-frg-int-cell::before { opacity: 1; }
1415.fd-frg-int-cell svg { margin-bottom: auto; color: var(--fd-frg-mute); transition: color var(--fd-dur-2), transform var(--fd-dur-3) var(--fd-spring); }
1416.fd-frg-int-cell:hover svg { color: var(--fd-frg-accent); transform: scale(1.08); }
1417.fd-frg-int-cell b { position: relative; margin-top: 22px; font: 500 0.9688rem/1.2 var(--fd-frg-sans); letter-spacing: -0.01em; }
1418.fd-frg-int-cell span { position: relative; font: 400 0.8125rem/1.4 var(--fd-frg-sans); color: var(--fd-frg-mute); }
1419
1420/* proof */
1421.fd-frg-stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); border-top: 1px solid var(--fd-frg-line-2); border-bottom: 1px solid var(--fd-frg-line); }
1422.fd-frg-stats div { padding: 28px 24px 28px 0; }
1423.fd-frg-stats div + div { padding-left: 24px; border-left: 1px solid var(--fd-frg-line); }
1424.fd-frg-stats dt { font: 600 clamp(1.75rem, 3vw, 2.5rem)/1 var(--fd-frg-sans); letter-spacing: -0.045em; white-space: nowrap; }
1425.fd-frg-stats dd { margin-top: 10px; font: 400 0.875rem/1.45 var(--fd-frg-sans); color: var(--fd-frg-mute); }
1426.fd-frg-quote { max-width: 900px; margin: 96px auto 0; text-align: center; }
1427.fd-frg-quote blockquote { font: 500 clamp(1.5rem, 2.8vw, 2.25rem)/1.25 var(--fd-frg-sans); letter-spacing: -0.03em; }
1428.fd-frg-quote figcaption { display: inline-flex; align-items: center; gap: 12px; margin-top: 32px; text-align: left; }
1429.fd-frg-av-lg { width: 40px; height: 40px; font-size: 0.75rem; }
1430.fd-frg-quote b { display: block; font: 500 0.9375rem/1.3 var(--fd-frg-sans); }
1431.fd-frg-quote small { display: block; font: 400 0.8125rem/1.3 var(--fd-frg-sans); color: var(--fd-frg-mute); }
1432
1433/* pricing */
1434.fd-frg-toggle { position: relative; display: inline-grid; grid-template-columns: 1fr 1fr; padding: 3px; border: 1px solid var(--fd-frg-line-2); border-radius: 9999px; background: var(--fd-frg-bg-2); }
1435.fd-frg-toggle button { position: relative; z-index: 1; height: 34px; padding: 0 16px; border-radius: 9999px; font: 500 0.8125rem/1 var(--fd-frg-sans); color: var(--fd-frg-mute); transition: color var(--fd-dur-2); }
1436.fd-frg-toggle button[aria-pressed="true"] { color: var(--fd-frg-bg); }
1437.fd-frg-toggle em { color: var(--fd-frg-accent); font-size: 0.75rem; }
1438.fd-frg-toggle button[aria-pressed="true"] em { color: #b5420f; }
1439.fd-frg-toggle i { position: absolute; top: 3px; bottom: 3px; left: 3px; width: calc(50% - 3px); border-radius: 9999px; background: var(--fd-frg-fg); transition: transform var(--fd-dur-3) var(--fd-spring); }
1440.fd-frg-toggle i[data-right] { transform: translateX(100%); }
1441.fd-frg-plans { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; margin-top: 56px; align-items: stretch; }
1442.fd-frg-plan { display: flex; flex-direction: column; padding: 28px; border: 1px solid var(--fd-frg-line-2); border-radius: 16px; background: var(--fd-frg-bg-2); }
1443.fd-frg-plan-hot { border-color: rgba(255,107,44,0.45); background: linear-gradient(180deg, rgba(255,107,44,0.07), transparent 40%), var(--fd-frg-bg-2); box-shadow: 0 0 0 1px rgba(255,107,44,0.15), 0 30px 80px -40px rgba(255,107,44,0.4); }
1444.fd-frg-plan-top { display: flex; justify-content: space-between; align-items: center; }
1445.fd-frg-plan h3 { font: 600 1.125rem/1.2 var(--fd-frg-sans); letter-spacing: -0.02em; }
1446.fd-frg-plan-p { margin-top: 6px; font: 400 0.875rem/1.45 var(--fd-frg-sans); color: var(--fd-frg-mute); }
1447.fd-frg-price { display: flex; align-items: baseline; gap: 6px; margin-top: 28px; }
1448.fd-frg-price span { font: 600 2.75rem/1 var(--fd-frg-sans); letter-spacing: -0.05em; font-variant-numeric: tabular-nums; }
1449.fd-frg-price small { font: 400 0.9375rem/1 var(--fd-frg-sans); color: var(--fd-frg-mute); }
1450.fd-frg-plan-calc { margin-top: 8px; color: var(--fd-frg-dim); }
1451.fd-frg-range { --pct: 50%; display: block; margin-top: 18px; }
1452.fd-frg-range input { -webkit-appearance: none; appearance: none; display: block; width: 100%; height: 22px; margin: 0; background: transparent; cursor: pointer; }
1453.fd-frg-range input::-webkit-slider-runnable-track { height: 4px; border-radius: 9999px; background: linear-gradient(to right, var(--fd-frg-accent) var(--pct), var(--fd-frg-surface-2) var(--pct)); }
1454.fd-frg-range input::-moz-range-track { height: 4px; border-radius: 9999px; background: linear-gradient(to right, var(--fd-frg-accent) var(--pct), var(--fd-frg-surface-2) var(--pct)); }
1455.fd-frg-range input::-webkit-slider-thumb { -webkit-appearance: none; width: 18px; height: 18px; margin-top: -7px; border-radius: 50%; background: #fff; border: 0; box-shadow: 0 0 0 4px var(--fd-frg-accent-soft), 0 2px 6px rgba(0,0,0,0.5); transition: transform var(--fd-dur-2) var(--fd-spring); }
1456.fd-frg-range input:active::-webkit-slider-thumb { transform: scale(1.15); }
1457.fd-frg-range input::-moz-range-thumb { width: 18px; height: 18px; border-radius: 50%; background: #fff; border: 0; box-shadow: 0 0 0 4px var(--fd-frg-accent-soft); }
1458.fd-frg-plan ul { display: grid; gap: 10px; margin: 24px 0 28px; }
1459.fd-frg-plan li { display: flex; gap: 10px; align-items: center; font: 400 0.875rem/1.4 var(--fd-frg-sans); color: #d0d0d0; }
1460.fd-frg-plan li svg { color: var(--fd-frg-accent); flex: none; }
1461.fd-frg-plan .fd-frg-btn { margin-top: auto; width: 100%; }
1462
1463/* cta */
1464.fd-frg-cta { position: relative; padding: 160px 0; border-top: 1px solid var(--fd-frg-line); overflow: hidden; isolation: isolate; }
1465.fd-frg-cta::after { content: ""; position: absolute; left: 50%; top: 50%; width: 800px; height: 420px; transform: translate(-50%, -50%); background: radial-gradient(closest-side, rgba(255,107,44,0.16), transparent); z-index: -1; }
1466.fd-frg-cta-in { display: flex; flex-direction: column; align-items: center; text-align: center; }
1467.fd-frg-cta-in .fd-frg-p { margin-left: auto; margin-right: auto; }
1468.fd-frg-cta-h { max-width: 14em; margin-top: 28px; font: 600 clamp(2.25rem, 5.5vw, 4.5rem)/1 var(--fd-frg-sans); letter-spacing: -0.05em; }
1469
1470/* footer */
1471.fd-frg-foot { padding: 72px 0 32px; border-top: 1px solid var(--fd-frg-line); background: var(--fd-frg-bg); }
1472.fd-frg-foot-top { display: grid; grid-template-columns: 1.6fr repeat(3, minmax(0, 1fr)); gap: 32px; }
1473.fd-frg-foot-p { margin-top: 14px; max-width: 22em; font: 400 0.875rem/1.5 var(--fd-frg-sans); color: var(--fd-frg-mute); }
1474.fd-frg-status { display: inline-flex; align-items: center; gap: 8px; margin-top: 18px; height: 28px; padding: 0 12px; border: 1px solid var(--fd-frg-line-2); border-radius: 9999px; font: 400 0.75rem/1 var(--fd-frg-sans); color: var(--fd-frg-mute); }
1475.fd-frg-status i { width: 7px; height: 7px; border-radius: 50%; background: var(--fd-frg-add); }
1476.fd-frg-foot-col h4 { font: 500 0.8125rem/1 var(--fd-frg-sans); color: var(--fd-frg-fg); }
1477.fd-frg-foot-col ul { display: grid; gap: 10px; margin-top: 16px; }
1478.fd-frg-foot-col a { font: 400 0.875rem/1.3 var(--fd-frg-sans); color: var(--fd-frg-mute); transition: color var(--fd-dur-1) linear; }
1479.fd-frg-foot-col a:hover { color: var(--fd-frg-fg); }
1480.fd-frg-foot-bot { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 12px; margin-top: 64px; padding-top: 20px; border-top: 1px solid var(--fd-frg-line); font: 400 0.8125rem/1.3 var(--fd-frg-sans); color: var(--fd-frg-dim); }
1481
1482/* reveal */
1483.fd-frg[data-js] [data-reveal] { opacity: 0; transform: translateY(18px); transition: opacity var(--fd-dur-4) var(--fd-ease-out), transform 900ms var(--fd-ease-out); transition-delay: calc(var(--i, 0) * 70ms); }
1484.fd-frg[data-js] [data-reveal][data-in] { opacity: 1; transform: none; }
1485
1486/* responsive */
1487@media (max-width: 1000px) {
1488 .fd-frg-nav-links { display: none; }
1489 .fd-frg-burger { display: block; }
1490 .fd-frg-logos ul { grid-template-columns: repeat(4, minmax(0, 1fr)); }
1491 .fd-frg-logos li:nth-child(4) { border-right: 0; }
1492 .fd-frg-logos li:nth-child(-n + 4) { border-bottom: 1px solid var(--fd-frg-line); }
1493 .fd-frg-tl-grid { grid-template-columns: 1fr; gap: 48px; }
1494 .fd-frg-sticky-head { position: static; }
1495 .fd-frg-diff-body { grid-template-columns: 1fr; }
1496 .fd-frg-code { border-right: 0; border-bottom: 1px solid var(--fd-frg-line); }
1497 .fd-frg-int { grid-template-columns: repeat(3, minmax(0, 1fr)); }
1498 .fd-frg-plans { grid-template-columns: 1fr; max-width: 520px; }
1499 .fd-frg-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
1500 .fd-frg-stats div:nth-child(3) { padding-left: 0; border-left: 0; }
1501 .fd-frg-stats div:nth-child(n + 3) { border-top: 1px solid var(--fd-frg-line); }
1502}
1503@media (max-width: 720px) {
1504 .fd-frg-wrap, .fd-frg-nav-in { padding: 0 20px; }
1505 .fd-frg-nav-ghost { display: none; }
1506 .fd-frg-hero { padding-top: 120px; padding-bottom: 88px; }
1507 .fd-frg-hero-term { margin-top: 44px; }
1508 .fd-frg-term-body { height: 300px; padding: 14px; font-size: 0.6875rem; line-height: 1.8; }
1509 .fd-frg-term-title { display: none; }
1510 .fd-frg-term-bar { justify-content: space-between; }
1511 .fd-frg-sec { padding: 88px 0; }
1512 .fd-frg-logos ul { grid-template-columns: repeat(2, minmax(0, 1fr)); }
1513 .fd-frg-logos li { height: 64px; border-bottom: 1px solid var(--fd-frg-line); }
1514 .fd-frg-logos li:nth-child(2n) { border-right: 0; }
1515 .fd-frg-logos li:nth-child(4) { border-right: 0; }
1516 .fd-frg-logos li:nth-child(n + 7) { border-bottom: 0; }
1517 .fd-frg-steps { padding-left: 36px; }
1518 .fd-frg-node { left: -30px; }
1519 .fd-frg-rail { left: 10px; }
1520 .fd-frg-meter > i { width: 150px; }
1521 .fd-frg-diff-bar { flex-wrap: wrap; height: auto; padding: 12px 14px; row-gap: 8px; }
1522 .fd-frg-diff-status { margin-left: 0; width: 100%; }
1523 .fd-frg-code { font-size: 0.6875rem; }
1524 .fd-frg-dl { grid-template-columns: 28px 28px 16px max-content; }
1525 .fd-frg-speed { height: 300vh; }
1526 .fd-frg-speed-in { gap: 24px; }
1527 .fd-frg-speed-hint { color: var(--fd-frg-dim); }
1528.fd-frg-clock-row { grid-template-columns: 1fr 1fr; }
1529 .fd-frg-clock { padding: 14px 16px; }
1530 .fd-frg-clock-n { font-size: 2.75rem; }
1531 .fd-frg-clock .fd-frg-mono { font-size: 0.625rem; }
1532 .fd-frg-gantt-axis { left: 104px; right: 0; }
1533 .fd-frg-job { grid-template-columns: 104px 1fr 0; height: 28px; }
1534 .fd-frg-job-name { font-size: 0.6875rem; gap: 6px; }
1535 .fd-frg-job-dur { display: none; }
1536 .fd-frg-playhead { left: calc(104px + (100% - 104px) * var(--p)); }
1537 .fd-frg-bento { grid-template-columns: 1fr; }
1538 .fd-frg-tile-wide { grid-column: auto; }
1539 .fd-frg-tile { min-height: 300px; padding: 22px; }
1540 .fd-frg-vis-stack div { margin-left: calc(var(--i) * 12px); padding: 0 10px; gap: 8px; font-size: 0.6875rem; min-width: 0; }
1541 .fd-frg-vis-stack em { display: none; }
1542 .fd-frg-vis-flake { grid-template-columns: repeat(8, 1fr); }
1543 .fd-frg-vis-flake i:nth-child(n + 33) { display: none; }
1544 .fd-frg-pill { max-width: 100%; }
1545 .fd-frg-pill-txt { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1546 .fd-frg-int { grid-template-columns: repeat(2, minmax(0, 1fr)); }
1547 .fd-frg-int-cell { min-height: 132px; padding: 16px; }
1548 .fd-frg-stats { grid-template-columns: 1fr; }
1549 .fd-frg-stats div, .fd-frg-stats div + div { padding: 20px 0; border-left: 0; }
1550 .fd-frg-stats div + div { border-top: 1px solid var(--fd-frg-line); }
1551 .fd-frg-quote { margin-top: 64px; }
1552 .fd-frg-cta { padding: 104px 0; }
1553 .fd-frg-foot-top { grid-template-columns: repeat(2, minmax(0, 1fr)); }
1554 .fd-frg-foot-top > div:first-child { grid-column: 1 / -1; }
1555}
1556@media (prefers-reduced-motion: reduce) {
1557 .fd-frg *, .fd-frg *::before, .fd-frg *::after { animation: none !important; }
1558 .fd-frg[data-js] [data-reveal] { transform: none; transition: opacity 120ms linear; transition-delay: 0ms; }
1559 .fd-frg-cmt { transform: none; transition: opacity 120ms linear; }
1560 .fd-frg-speed { height: auto; padding: 96px 0; }
1561 .fd-frg-speed-stick { position: relative; height: auto; }
1562 .fd-frg-caret { display: none; }
1563}
1564@media (prefers-contrast: more) {
1565 .fd-frg { --fd-frg-mute: #cfcfcf; --fd-frg-dim: #b0b0b0; --fd-frg-line: rgba(255,255,255,0.3); --fd-frg-line-2: rgba(255,255,255,0.45); }
1566}
1567@media (prefers-reduced-transparency: reduce) {
1568 .fd-frg-nav[data-scrolled], .fd-frg-nav[data-open] { background: var(--fd-frg-bg); -webkit-backdrop-filter: none; backdrop-filter: none; }
1569}
1570`;

The design brief. For you and your AI agent.

Tokens, type, layout, components and motion — written from the real values in the file, so a model can extend the page without drifting.

Forge — Design Brief

A dark developer-tool landing page in the Geist tradition: near-black canvas, 1px hairlines, pill buttons, sans + mono, and exactly one accent — ember #ff6b2c — reserved for "Forge is doing something" (the prompt, the timeline glow, running jobs, the review bot, the hot plan). Everything else is greyscale.

Source of truth: Page.tsx in this folder.


1. Principles

  1. Hairlines over fills. Cards are #0a0a0a / #0f0f10 with a

rgba(255,255,255,0.08–0.14) 1px border. Grids (logos, bento, integrations) are built with gap: 1px over a hairline-colored background, so the lines are the layout.

  1. One accent, with meaning. Ember marks activity and Forge's own voice.

Diff green #3fb950 and red #f2555a appear only inside code.

  1. Two-tone headlines. Every H2 is a white line followed by a muted line

(#8a8a8a): "From push to prod, / one minute twenty-eight."

  1. Real artifacts, not illustrations. A terminal, a diff, a Gantt chart

and a merge timeline carry the story. Copy uses believable numbers (214 of 9,812 tests, 1m 28s, 96.4% cache hits).

  1. Motion is a demo. Every animation shows the product working: typing,

reviewing, running, drawing.


2. Tokens

--fd-frg-bg:          #0a0a0a;   /* page */
--fd-frg-bg-2:        #0f0f10;   /* alt sections, recessed panels */
--fd-frg-surface:     #141415;   /* comments, stacked PR rows */
--fd-frg-surface-2:   #1b1b1d;   /* hover, tracks, chips */
--fd-frg-fg:          #ededed;   /* primary text, primary button */
--fd-frg-mute:        #a1a1a1;   /* body copy (AA on bg) */
--fd-frg-dim:         #8a8a8a;   /* second headline line, meta (AA on bg) */
--fd-frg-line:        rgba(255,255,255,0.08);
--fd-frg-line-2:      rgba(255,255,255,0.14);
--fd-frg-accent:      #ff6b2c;
--fd-frg-accent-soft: rgba(255,107,44,0.12);
--fd-frg-add:         #3fb950;
--fd-frg-del:         #f2555a;

Motion tokens: --fd-ease-out (0.22,1,0.36,1), --fd-ease-in-out (0.65,0,0.35,1), --fd-spring (0.34,1.56,0.64,1), durations 120 / 240 / 420 / 700ms.


3. Type

RoleSpec
Sans"Geist", -apple-system, "SF Pro Text", "Inter", … (falls back cleanly without Geist installed)
Mono"Geist Mono", ui-monospace, "SF Mono", "JetBrains Mono", …
H1600, clamp(3rem, 8vw, 6.5rem)/0.96, −0.055em, centered, second line dim
H2600, clamp(2rem, 4.2vw, 3.25rem)/1.04, −0.04em, two-tone
Eyebrowmono 500 0.8125rem, accent, numbered 01 · Anatomy of a merge
Body400 1.0625rem/1.55, mute
Big clock600 clamp(3.5rem, 9vw, 7.5rem), −0.06em, tabular

4. Layout

  • Container 1200px, 32px gutters (20px ≤ 720px).
  • Sections: Nav · Hero (centered, terminal) · Logos (8-cell hairline row) ·

Timeline (sticky heading left, steps right) · Auto-review diff · Speed (sticky, 320vh) · Bento (3 columns, two wide tiles) · Integrations (4×3 hairline grid) · Proof (stat row + quote) · Pricing (3 plans) · CTA · Footer.

  • Section padding 128px (88px ≤ 720px); every section has a top hairline;

bg-2 alternates for Review and Integrations.

  • Breakpoints: 1000 (burger nav, single-column timeline, stacked diff),

720 (mobile).


5. Components

  • Button: pill, 44px, white fill / black text; ghost = hairline border.

Press scale(0.96), arrow nudges on the spring.

  • Copy command: pill with $ prompt in accent and an inline Copy chip

(writes to the clipboard).

  • Terminal: 14px radius, dark gradient, traffic-light dots, a Replay

button, fixed 320px body so typing never shifts layout.

  • Timeline step: node dot (fills accent + 5px soft ring when reached),

mono timestamp, title, body, a meta chip / meter / avatar stack.

  • Diff: grid columns old# / new# / sign / code; add/del tinted rows; a

side column of review comments (bot avatar in accent-soft, human comment dashed).

  • Gantt row: name with state dot, track with 15s gridlines, bar whose

fill is scaleX(--f); done bars turn light grey.

  • Integration cell: icon, name, one-line purpose; the grid's 1px gaps are

lit by a cursor-following radial gradient.

  • Seat calculator: native range input restyled (accent track fill via

--pct), live total = seats × per-seat price.


6. Motion choreography

WhereWhatHow
HeroBlur-up entranceopacity 0, translateY(16px), blur(6px) → none, 1000ms, staggered
TerminalTypingcommand typed 28–74ms per char, then output lines appear with per-line pauses; ends on a summary line and a blinking caret. Replay restarts it
NavCondense64 → 54px, 78% black + blur(14px) + hairline after 12px
TimelineLine draws on scroll--draw = (0.62·vh − top) / height; rail scaleY(--draw) with ember glow; each step lights when the line passes its node
DiffAuto-reviewon view: a scan highlight sweeps lines (70ms stagger), then comments pop in at 1.5s / 2.6s / 3.5s (opacity + translate + spring scale); "Commit suggestion" rewrites line 46 in place
SpeedScrubbed pipelinep = −top / (height − vh) maps to t = p × 88s; each job's fill --f = (t − start) / dur; a playhead and two clocks (Forge vs. 14:12 legacy CI) follow p
IntegrationsSpotlightpointermove (rAF-throttled) sets --mx/--my on the grid (lights the hairlines) and on each cell (soft inner glow)
PricingTogglewhite thumb slides on the spring
BlocksReveal[data-reveal] → data-in, 700/900ms, stagger --i × 70ms

7. Accessibility and fallbacks

  • Terminal output is aria-hidden; a polite live region announces the final

result. The diff uses table roles.

  • prefers-reduced-motion: no typing (full log shown), comments appear

instantly, the speed scene is unpinned and shows the finished pipeline, no keyframes.

  • prefers-contrast: more: lighter mute/dim, stronger hairlines.
  • Focus ring: 2px ember at 70%, offset 2px.

8. Do / don't

  • Do keep ember for activity only; a static decoration in ember is a bug.
  • Do write every H2 as white line + dim line.
  • Don't add shadows to cards (the terminal and hot plan are the only glows).
  • Don't use more than one accent; status colors stay inside code.

Assets

Reviewer avatars (approval step, review comment, quote): Placeholder portraits live in public/img/people/ (p1.webp to p9.webp, 328×328). Copy the folder next to the page so root-relative /img/people/... URLs resolve, or swap in your own photos. They are lazy-loaded and cropped with object-fit: cover; object-position: center top.

Questions about Forge

Dark, hairline dev-tool site where the CI pipeline runs as you scroll. It is a free developer tools landing page for React with a matching DESIGN.md brief.

Open the page, click Copy full source or Download .tsx, and drop the single Page file into your React project. Copy the public/img folder next to it for the demo photography, or swap in your own images.

Yes. It is MIT-licensed: free for personal and commercial projects, no signup and no attribution required.

Yes. The DESIGN.md describes tokens, type scale, layout, components and motion so Claude Code, Cursor, Lovable or v0 can extend the page without drifting from its look.