diff --git a/web/app/about/page.tsx b/web/app/about/page.tsx
index 2e6833d1..139f861f 100644
--- a/web/app/about/page.tsx
+++ b/web/app/about/page.tsx
@@ -6,7 +6,7 @@ import { HomePosterText } from '../../components/home/HomePosterText';
import { ScribbleUnderline } from '../../components/home/icons';
import { SiteFooter } from '../../components/SiteFooter';
import { SiteNav } from '../../components/SiteNav';
-import { HOME_OG_IMAGE_PATH, ogImage } from '../../lib/og-meta';
+import { DEFAULT_OG_IMAGE_PATH, ogImage } from '../../lib/og-meta';
import { absoluteUrl } from '../../lib/site';
import s from '../landing.module.css';
@@ -53,14 +53,14 @@ export const metadata: Metadata = {
'Communication infrastructure for software teams moving from human-centered tools to agent-centered engineering.',
url: absoluteUrl('/about'),
type: 'website',
- images: [ogImage(HOME_OG_IMAGE_PATH, 'Agent Relay')],
+ images: [ogImage(DEFAULT_OG_IMAGE_PATH, 'Agent Relay')],
},
twitter: {
card: 'summary_large_image',
title: 'Agent Relay',
description:
'Communication infrastructure for software teams moving from human-centered tools to agent-centered engineering.',
- images: [absoluteUrl(HOME_OG_IMAGE_PATH)],
+ images: [absoluteUrl(DEFAULT_OG_IMAGE_PATH)],
},
};
diff --git a/web/app/flows/FlowExamples.tsx b/web/app/flows/FlowExamples.tsx
new file mode 100644
index 00000000..1d8f6870
--- /dev/null
+++ b/web/app/flows/FlowExamples.tsx
@@ -0,0 +1,13 @@
+import type { ReactNode } from 'react';
+import { highlightCode } from '../../lib/syntax';
+import { flowExamples } from './flow-examples';
+import { FlowScriptCarousel } from './FlowScriptCarousel';
+
+export async function FlowExamples({ introductoryCode }: { introductoryCode: ReactNode }) {
+ const examples = await Promise.all(flowExamples.map(async ({ code, ...example }, index) => ({
+ ...example,
+ codeHtml: index === 0 ? '' : (await highlightCode(code, 'typescript')).codeHtml,
+ })));
+
+ return ;
+}
diff --git a/web/app/flows/FlowScriptCarousel.tsx b/web/app/flows/FlowScriptCarousel.tsx
new file mode 100644
index 00000000..82437b8d
--- /dev/null
+++ b/web/app/flows/FlowScriptCarousel.tsx
@@ -0,0 +1,86 @@
+'use client';
+
+import { useId, useState, type ReactNode } from 'react';
+import Link from 'next/link';
+import { ArrowRight } from 'lucide-react';
+import s from './flows.module.css';
+
+type Example = {
+ id: string;
+ label: string;
+ title: string;
+ description: string;
+ filename: string;
+ codeHtml: string;
+};
+
+export function FlowScriptCarousel({ examples, introductoryCode }: {
+ examples: Example[];
+ introductoryCode: ReactNode;
+}) {
+ const [active, setActive] = useState(0);
+ const id = useId();
+ const example = examples[active];
+
+ return (
+
+
+
+
{active === 0 ? 'A Flow is a script that runs your agents.' : example.title}
+
+ {active === 0
+ ? 'You write the steps in TypeScript: which agent does the work, what it should produce, and what to check before continuing.'
+ : example.description}
+
+
+ When the flow runs, you've got a dependable, auditable script that babysits your agents for you.
+
+
+
+
+ Explore the docs
+
+
+
+
+
+
+
{introductoryCode}
+
+
+ {active === 0 ? introductoryCode : (
+
+
+
+ )}
+
+
+
+
+
Explore use cases
+
{example.label}
+
+ {examples.map((item, index) => (
+ setActive(index)}
+ >
+ {String(index + 1).padStart(2, '0')}
+ {item.label}
+
+ ))}
+
+
+
+ );
+}
diff --git a/web/app/flows/IntegrationMarquee.tsx b/web/app/flows/IntegrationMarquee.tsx
new file mode 100644
index 00000000..a3fd1ea4
--- /dev/null
+++ b/web/app/flows/IntegrationMarquee.tsx
@@ -0,0 +1,84 @@
+'use client';
+
+import { useLayoutEffect, useRef, useState, type CSSProperties } from 'react';
+import Image from 'next/image';
+import { DatabaseBackup } from 'lucide-react';
+import { SiPostgresql, SiRedis, SiPosthog } from 'react-icons/si';
+import s from './flows.module.css';
+
+function brand(name: string, slug: string) {
+ return { name, logo: };
+}
+
+const integrations = [
+ brand('GitHub', 'github'),
+ brand('Linear', 'linear'),
+ brand('Slack', 'slack'),
+ brand('Jira', 'jira'),
+ brand('Notion', 'notion'),
+ brand('Stripe', 'stripe'),
+ brand('HubSpot', 'hubspot'),
+ brand('Intercom', 'intercom'),
+ { name: 'Postgres', logo: },
+ { name: 'Redis', logo: },
+ { name: 'S3', logo: },
+ brand('Cloudflare', 'cloudflare'),
+ brand('Sendgrid', 'sendgrid'),
+ brand('Mailgun', 'mailgun'),
+ brand('Gmail', 'gmail'),
+ brand('Google Calendar', 'google-calendar'),
+ brand('Segment', 'segment'),
+ { name: 'PostHog', logo: },
+];
+
+function IntegrationRow({ row }: { row: typeof integrations }) {
+ const trackRef = useRef(null);
+ const [copies, setCopies] = useState(2);
+
+ useLayoutEffect(() => {
+ const track = trackRef.current;
+ const window = track?.parentElement;
+ const group = track?.firstElementChild;
+ if (!track || !window || !group) return;
+
+ const resize = () => {
+ const groupWidth = group.getBoundingClientRect().width;
+ if (groupWidth > 0) {
+ setCopies(Math.max(2, Math.ceil(window.clientWidth / groupWidth) + 1));
+ }
+ };
+ resize();
+ const observer = new ResizeObserver(resize);
+ observer.observe(window);
+ observer.observe(group);
+ return () => observer.disconnect();
+ }, []);
+
+ return (
+
+ {Array.from({ length: copies }, (_, copy) => (
+
0 || undefined}>
+ {row.map(({ name, logo }) => (
+
+ {logo}
+ {name}
+
+ ))}
+
+ ))}
+
+ );
+}
+
+export function IntegrationMarquee() {
+ return (
+
+
+ {[integrations.slice(0, 9), integrations.slice(9)].map((row, index) => (
+
+ ))}
+
+
+ );
+}
diff --git a/web/app/flows/flow-examples.ts b/web/app/flows/flow-examples.ts
new file mode 100644
index 00000000..ef4291ad
--- /dev/null
+++ b/web/app/flows/flow-examples.ts
@@ -0,0 +1,66 @@
+export const flowExamples = [
+ {
+ "id": "software-factory",
+ "label": "Software factory",
+ "title": "Turn a ticket into a pull request.",
+ "description": "Plan the change, write the code, and run the tests before opening a PR.",
+ "filename": "software-factory.flow.ts",
+ "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n repo: string;\n ticket: string;\n};\n\nexport default flow (\n \"software-factory\",\n { budget: \"$5/run\" },\n async (f, input) => {\n await f.agent(\"planner\", {\n task: `${input.ticket} Write a plan to plan.md.`,\n }).gate((r) => r.artifacts.includes(\"plan.md\"));\n\n await f.agent(\"implementer\", {\n task: \"Read plan.md. Implement it on branch flow/fix. \" +\n \"Write the PR description to summary.md.\",\n }).gate((r) => r.artifacts.includes(\"summary.md\"));\n\n // The tests run outside the agent.\n // The agent cannot lie about the exit code.\n await f.run(\"git checkout flow/fix && npm test\");\n\n await f.agent(\"reviewer\", {\n task: \"Review the diff against main. \" +\n \"Write review.passed only if ready for a PR.\",\n }).gate((r) => r.artifacts.includes(\"review.passed\"));\n\n // Deterministic step, not an agent decision.\n await f.github.createPullRequest({\n repo: input.repo,\n head: \"flow/fix\",\n base: \"main\",\n bodyPath: \"summary.md\",\n });\n f.done(\"success\");\n },\n);"
+ },
+ {
+ "id": "pr-review",
+ "label": "Code review",
+ "title": "Give every PR a second opinion.",
+ "description": "Review a diff for security, correctness, and performance in parallel. Then reconcile the findings.",
+ "filename": "pr-review.flow.ts",
+ "code": "import { flow } from \"@relayflows/surface\";\n\nexport default flow(\n \"pr-review\",\n { budget: \"$5/run\" },\n async (f) => {\n const diff = await f.run(\"git diff main...HEAD\");\n const lenses = [\"security\", \"correctness\", \"performance\"];\n\n await Promise.all(lenses.map((lens) =>\n f.agent(`${lens}-reviewer`, {\n task: `Review this diff for ${lens}: ${diff}. ` +\n `Write findings to review/${lens}.md.`,\n workspace: \"review/: readwrite\",\n }).gate((r) =>\n r.artifacts.includes(`review/${lens}.md`)\n )\n ));\n\n // Resolve disagreements between reviewers.\n await f.agent(\"reconciler\", {\n task: \"Read the three reviews in review/. \" +\n \"Resolve disagreements, flag unresolved issues, \" +\n \"and write review/consensus.md.\",\n workspace: \"review/: readwrite\",\n }).gate((r) =>\n r.artifacts.includes(\"review/consensus.md\")\n );\n f.done(\"success\");\n },\n);"
+ },
+ {
+ "id": "support-triage",
+ "label": "Customer support",
+ "title": "Draft the reply. Let a human decide.",
+ "description": "Draft a response to a support thread, get approval, and reply in Slack.",
+ "filename": "support-triage.flow.ts",
+ "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n message: string;\n channel: string;\n threadTs: string;\n approver: string;\n};\n\nexport default flow (\n \"support-triage\",\n { budget: \"$5/run\" },\n async (f, input) => {\n await f.agent(\"triage\", {\n task: `Classify this request: ${input.message}. ` +\n \"Write the category and urgency to triage.md.\",\n }).gate((r) => r.artifacts.includes(\"triage.md\"));\n\n await f.agent(\"writer\", {\n task: `Read triage.md and draft a reply to: ` +\n `${input.message}. Write only the reply to reply.md.`,\n }).gate((r) => r.artifacts.includes(\"reply.md\"));\n\n const reply = await f.run(\"cat reply.md\");\n const approved = await f.human(\n `Send this reply?\\n\\n${reply}`,\n { to: input.approver },\n );\n if (!approved) return f.done(\"canceled\");\n\n // Send only after a human approves.\n await f.slack.reply(input.channel, input.threadTs, reply);\n f.done(\"success\");\n },\n);"
+ },
+ {
+ "id": "content-pipeline",
+ "label": "Content publishing",
+ "title": "Put a fact-check between draft and publish.",
+ "description": "Research, draft, and check a post before a human approves sharing it in Slack.",
+ "filename": "content-pipeline.flow.ts",
+ "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n topic: string;\n channel: string;\n approver: string;\n};\n\nexport default flow (\n \"content-pipeline\",\n { budget: \"$5/run\" },\n async (f, input) => {\n await f.agent(\"researcher\", {\n task: `Research ${input.topic}. Cite your sources. ` +\n \"Write research.md.\",\n }).gate((r) => r.artifacts.includes(\"research.md\"));\n\n await f.agent(\"writer\", {\n task: \"Use research.md to write post.md. \" +\n \"Keep the source links with each claim.\",\n }).gate((r) => r.artifacts.includes(\"post.md\"));\n\n await f.agent(\"fact-checker\", {\n task: \"Check post.md against its sources. \" +\n \"Write checked.passed only if all claims hold up.\",\n }).gate((r) => r.artifacts.includes(\"checked.passed\"));\n\n const post = await f.run(\"cat post.md\");\n const approved = await f.human(\n `Publish this post?\\n\\n${post}`,\n { to: input.approver },\n );\n if (!approved) return f.done(\"canceled\");\n\n await f.slack.post(input.channel, post);\n f.done(\"success\");\n },\n);"
+ },
+ {
+ "id": "repo-migration",
+ "label": "Repo migrations",
+ "title": "Make the same change across repositories.",
+ "description": "Apply a migration in each local checkout, test it, and open a PR.",
+ "filename": "repo-migration.flow.ts",
+ "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n repos: { path: string; slug: string }[];\n instructions: string;\n};\n\nexport default flow (\n \"repo-migration\",\n { budget: \"$5/run\" },\n async (f, input) => {\n for (const repo of input.repos) {\n // Quote the local checkout path for shell commands.\n const dir = \"'\" + repo.path.replaceAll(\"'\", \"'\\\"'\\\"'\") + \"'\";\n\n await f.agent(`migrate-${repo.slug}`, {\n cwd: repo.path,\n task: `${input.instructions} ` +\n \"Use branch flow/migration and commit the change. \" +\n \"Write migration.md with a summary.\",\n }).gate((r) => r.artifacts.includes(\"migration.md\"));\n\n await f.run(`cd ${dir} && npm test`);\n\n await f.agent(`review-${repo.slug}`, {\n cwd: repo.path,\n task: \"Review this migration against main. \" +\n \"Write review.passed only if it is ready.\",\n }).gate((r) => r.artifacts.includes(\"review.passed\"));\n\n // Each checkout opens its own pull request.\n await f.run(\n `cd ${dir} && gh pr create --base main ` +\n '--head flow/migration --title \"Apply migration\" ' +\n '--body-file migration.md'\n );\n }\n f.done(\"success\");\n },\n);"
+ },
+ {
+ "id": "voicemail-follow-up",
+ "label": "Voicemail follow-up",
+ "title": "Turn a voicemail into a callback brief.",
+ "description": "Use a transcript to prepare the callback, then send the approved brief to a teammate.",
+ "filename": "voicemail-follow-up.flow.ts",
+ "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n transcript: string;\n approver: string;\n callbackOwner: string;\n};\n\nexport default flow (\n \"voicemail-follow-up\",\n { budget: \"$5/run\" },\n async (f, input) => {\n await f.agent(\"triage\", {\n task: `Read this voicemail: ${input.transcript}. ` +\n \"Identify urgency and the caller's request. \" +\n \"Write triage.md.\",\n }).gate((r) => r.artifacts.includes(\"triage.md\"));\n\n await f.agent(\"callback-writer\", {\n task: \"Read triage.md. Prepare a callback brief \" +\n \"with the questions we need to answer. \" +\n \"Write callback.md.\",\n }).gate((r) => r.artifacts.includes(\"callback.md\"));\n\n const brief = await f.run(\"cat callback.md\");\n const approved = await f.human(\n `Ready for a callback?\\n\\n${brief}`,\n { to: input.approver },\n );\n if (!approved) return f.done(\"canceled\");\n\n await f.slack.dm(input.callbackOwner, brief);\n f.done(\"success\");\n },\n);"
+ },
+ {
+ "id": "research-report",
+ "label": "Research reports",
+ "title": "Bring independent research into one report.",
+ "description": "Give researchers different angles, combine their findings, and ask another agent to check the citations.",
+ "filename": "research-report.flow.ts",
+ "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n question: string;\n};\n\nexport default flow (\n \"research-report\",\n { budget: \"$5/run\" },\n async (f, input) => {\n const angles = [\"technical\", \"commercial\", \"risks\"];\n\n await Promise.all(angles.map((angle) =>\n f.agent(`research-${angle}`, {\n task: `Research ${input.question} from a ${angle} ` +\n `angle. Cite sources in research/${angle}.md.`,\n workspace: \"research/: readwrite\",\n }).gate((r) =>\n r.artifacts.includes(`research/${angle}.md`)\n )\n ));\n\n await f.agent(\"editor\", {\n task: \"Read research/*.md. Combine the findings, \" +\n \"keep disagreements visible, and cite sources. \" +\n \"Write report.md.\",\n }).gate((r) => r.artifacts.includes(\"report.md\"));\n\n await f.agent(\"citation-reviewer\", {\n task: \"Open every source in report.md and check \" +\n \"the claims it supports. Write citations.passed \" +\n \"only when every citation checks out.\",\n }).gate((r) => r.artifacts.includes(\"citations.passed\"));\n f.done(\"success\");\n },\n);"
+ },
+ {
+ "id": "redacted-summary",
+ "label": "Sensitive data",
+ "title": "Choose what the summarizer gets to see.",
+ "description": "Select the fields needed for the task before putting a record into the agent prompt.",
+ "filename": "redacted-summary.flow.ts",
+ "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n record: { category: string; status: string; email: string };\n approver: string;\n};\n\nexport default flow (\n \"redacted-summary\",\n { budget: \"$5/run\" },\n async (f, input) => {\n // Select fields in code before building the prompt.\n // The email address is excluded from this input.\n const selected = {\n category: input.record.category,\n status: input.record.status,\n };\n\n await f.agent(\"summarizer\", {\n task: `Summarize this record: ` +\n `${JSON.stringify(selected)}. Write summary.md.`,\n }).gate((r) => r.artifacts.includes(\"summary.md\"));\n\n const summary = await f.run(\"cat summary.md\");\n const approved = await f.human(\n `Review this summary before sharing:\\n\\n${summary}`,\n { to: input.approver },\n );\n if (!approved) return f.done(\"canceled\");\n\n f.done(\"success\");\n },\n);"
+ }
+];
diff --git a/web/app/flows/flows.module.css b/web/app/flows/flows.module.css
new file mode 100644
index 00000000..b6890fa8
--- /dev/null
+++ b/web/app/flows/flows.module.css
@@ -0,0 +1,1356 @@
+.page {
+ --flows-bg: #071724;
+ --flows-surface: rgba(13, 32, 49, 0.76);
+ --flows-surface-strong: rgba(18, 43, 63, 0.92);
+ --flows-line: rgba(116, 184, 226, 0.18);
+ --flows-line-strong: rgba(116, 184, 226, 0.32);
+ --flows-text: #edf4fb;
+ --flows-muted: #a8b8c8;
+ --flows-faint: #77879a;
+ --flows-accent: #74b8e2;
+ --flows-accent-strong: #9fd4f5;
+ --flows-orange: #d97757;
+ --flows-band-blue:
+ radial-gradient(circle at 18% 24%, rgba(116, 184, 226, 0.16), transparent 32%),
+ linear-gradient(180deg, #10273a 0%, #17364c 52%, #0d2638 100%);
+ --flows-band-dark: #0b1c2a;
+ --flows-band-deep: #081723;
+ min-height: 100vh;
+ overflow-x: hidden;
+ background: linear-gradient(180deg, #10273a 0%, #0d2638 38%, #0b1c2a 100%);
+ color: var(--flows-text);
+}
+
+.main {
+ position: relative;
+ width: min(100%, 1180px);
+ margin: 0 auto;
+ padding: 0 1.25rem 6.5rem;
+}
+
+.hero {
+ position: relative;
+ display: flex;
+ justify-content: center;
+ min-height: 0;
+ padding: clamp(3.5rem, 7vw, 5.5rem) 0 3.5rem;
+ text-align: center;
+ isolation: isolate;
+}
+
+.heroCopy {
+ position: relative;
+ z-index: 1;
+ display: grid;
+ justify-items: center;
+ gap: 1.35rem;
+ width: min(100%, 920px);
+ margin: 0 auto;
+}
+
+.hero::before,
+.hero::after {
+ position: absolute;
+ inset: 0 calc(50% - 50vw);
+ pointer-events: none;
+ content: '';
+}
+
+.hero::before {
+ z-index: -2;
+ background: var(--flows-band-blue);
+}
+
+.hero::after {
+ z-index: -1;
+ opacity: 0.22;
+ background:
+ linear-gradient(90deg, transparent 0%, rgba(116, 184, 226, 0.2) 48%, transparent 74%),
+ linear-gradient(180deg, rgba(116, 184, 226, 0.09) 1px, transparent 1px);
+ background-size:
+ 460px 100%,
+ 100% 96px;
+ transform: skewY(-3deg) translateY(18%);
+}
+
+.hero h1,
+.sectionIntro h2,
+.assetCopy h2,
+.planCopy h2,
+.managerHead h2,
+.finalCta h2 {
+ margin: 0;
+ font-family: var(--font-heading), sans-serif;
+ font-weight: 720;
+ letter-spacing: 0;
+ color: var(--flows-text);
+}
+
+.hero h1 {
+ max-width: 22ch;
+ font-size: clamp(2.25rem, 4.5vw, 3.8rem);
+ line-height: 1.12;
+ letter-spacing: -0.035em;
+}
+
+.headlineLine {
+ display: block;
+ white-space: normal;
+}
+
+.heroLead {
+ max-width: 35rem;
+ margin: 0;
+ color: var(--flows-muted);
+ font-size: clamp(1.05rem, 1.8vw, 1.28rem);
+ line-height: 1.55;
+}
+
+.ctaRow {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.85rem;
+ align-items: center;
+ justify-content: center;
+ margin-top: 0.35rem;
+}
+
+.ctaPrimary {
+ composes: btn btn-primary from global;
+ white-space: nowrap;
+}
+
+.ctaSecondary {
+ composes: btn btn-secondary from global;
+ white-space: nowrap;
+}
+
+.waveBreak {
+ position: relative;
+ z-index: 3;
+ width: 100vw;
+ height: 84px;
+ margin: -38px calc(50% - 50vw);
+ overflow: hidden;
+ pointer-events: none;
+}
+
+.waveBreak::before {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 126vw;
+ height: 1px;
+ background: linear-gradient(90deg, transparent, currentColor 48%, transparent);
+ opacity: 0.34;
+ transform: translate(-50%, -50%) rotate(2deg);
+ content: '';
+}
+
+.waveBreak svg {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 132vw;
+ height: 92px;
+ overflow: visible;
+ color: currentColor;
+ opacity: 0.5;
+ transform: translate(-50%, -50%) rotate(2deg);
+}
+
+.waveBreak path {
+ fill: none;
+ stroke: currentColor;
+ stroke-linecap: round;
+ stroke-width: 1.2;
+ vector-effect: non-scaling-stroke;
+}
+
+.waveBreak path:nth-child(2) {
+ opacity: 0.42;
+}
+
+.waveBreakBlue {
+ color: var(--flows-accent);
+}
+
+.waveBreakOrange {
+ color: var(--flows-orange);
+}
+
+.sectionBand {
+ position: relative;
+ z-index: 1;
+ margin-top: 0;
+ padding: clamp(5.5rem, 10vw, 7.5rem) 0;
+ isolation: isolate;
+}
+
+.sectionBand::before,
+.sectionBand::after {
+ position: absolute;
+ inset: 0 calc(50% - 50vw);
+ pointer-events: none;
+ content: '';
+}
+
+.sectionBand::before {
+ z-index: -2;
+ background: var(--section-bg, var(--flows-band-dark));
+ border-top: 1px solid rgba(237, 244, 251, 0.05);
+ border-bottom: 1px solid rgba(237, 244, 251, 0.05);
+}
+
+.sectionBand::after {
+ z-index: -1;
+ opacity: var(--section-pattern-opacity, 0.16);
+ background: var(
+ --section-pattern,
+ linear-gradient(90deg, rgba(116, 184, 226, 0.12) 1px, transparent 1px)
+ );
+ background-size: var(--section-pattern-size, 96px 96px);
+}
+
+.bandCapture {
+ --section-bg: var(--flows-band-dark);
+ --section-pattern:
+ linear-gradient(90deg, rgba(116, 184, 226, 0.08) 1px, transparent 1px),
+ linear-gradient(180deg, rgba(116, 184, 226, 0.06) 1px, transparent 1px);
+ --section-pattern-size:
+ 92px 92px,
+ 92px 92px;
+}
+
+.bandLearn {
+ --section-bg: linear-gradient(180deg, #10273a 0%, #0d2638 100%);
+ --section-pattern:
+ radial-gradient(circle at 18% 12%, rgba(217, 119, 87, 0.18), transparent 22rem),
+ linear-gradient(120deg, rgba(217, 119, 87, 0.12), transparent 32%);
+ --section-pattern-opacity: 0.44;
+}
+
+.bandProof {
+ --section-bg: var(--flows-band-deep);
+ --section-pattern:
+ linear-gradient(130deg, transparent 0 38%, rgba(116, 184, 226, 0.09) 38% 39%, transparent 39% 100%),
+ linear-gradient(90deg, rgba(237, 244, 251, 0.05) 1px, transparent 1px);
+ --section-pattern-size:
+ 160px 160px,
+ 80px 80px;
+}
+
+.bandPlan {
+ --section-bg: var(--flows-band-blue);
+ --section-pattern:
+ radial-gradient(circle at 80% 20%, rgba(116, 184, 226, 0.2), transparent 24rem),
+ linear-gradient(90deg, rgba(237, 244, 251, 0.08) 1px, transparent 1px);
+ --section-pattern-opacity: 0.35;
+ --section-pattern-size:
+ auto,
+ 74px 74px;
+}
+
+.bandPair {
+ --section-bg: #0a1926;
+ --section-pattern:
+ linear-gradient(90deg, transparent 0%, rgba(217, 119, 87, 0.12) 50%, transparent 100%),
+ linear-gradient(180deg, rgba(116, 184, 226, 0.05) 1px, transparent 1px);
+ --section-pattern-size:
+ 520px 100%,
+ 100% 86px;
+}
+
+.bandManager {
+ --section-bg: linear-gradient(180deg, #10273a 0%, #0b1c2a 100%);
+ --section-pattern:
+ radial-gradient(circle at 8% 18%, rgba(116, 184, 226, 0.18), transparent 20rem),
+ radial-gradient(circle at 92% 24%, rgba(217, 119, 87, 0.14), transparent 22rem);
+ --section-pattern-opacity: 0.72;
+}
+
+.bandFinal {
+ --section-bg: #071724;
+ --section-pattern:
+ linear-gradient(90deg, rgba(116, 184, 226, 0.08) 1px, transparent 1px),
+ linear-gradient(180deg, rgba(116, 184, 226, 0.05) 1px, transparent 1px);
+ --section-pattern-size:
+ 70px 70px,
+ 70px 70px;
+}
+
+.captureSection,
+.learnSection,
+.pairSection,
+.managerSection {
+ margin-top: 0;
+}
+
+.sectionIntro {
+ display: grid;
+ gap: 1rem;
+ max-width: 48rem;
+}
+
+.sectionIntro h2,
+.assetCopy h2,
+.planCopy h2,
+.managerHead h2,
+.finalCta h2 {
+ font-size: clamp(2.3rem, 5vw, 4.2rem);
+ line-height: 1;
+}
+
+.sectionIntro p,
+.assetCopy p,
+.planCopy p,
+.managerHead p {
+ max-width: 43rem;
+ margin: 0;
+ color: var(--flows-muted);
+ font-size: 1.04rem;
+ line-height: 1.68;
+}
+
+.captureGrid {
+ display: grid;
+ grid-template-columns: minmax(0, 1.05fr) minmax(300px, 0.7fr);
+ gap: 1rem;
+ margin-top: 2rem;
+}
+
+.signalPanel,
+.privacyPanel,
+.patternCard,
+.promptPanel,
+.warningCard {
+ border: 1px solid var(--flows-line);
+ border-radius: 22px;
+ background: var(--flows-surface);
+}
+
+.signalPanel {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 0.75rem;
+ padding: 1rem;
+}
+
+.signalRow {
+ display: flex;
+ align-items: center;
+ min-height: 78px;
+ padding: 1rem;
+ border: 1px solid rgba(116, 184, 226, 0.12);
+ border-radius: 16px;
+ background: rgba(7, 17, 26, 0.42);
+ color: var(--flows-text);
+ font-weight: 660;
+}
+
+.privacyPanel {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ gap: 1rem;
+ padding: 1.3rem;
+ background:
+ radial-gradient(circle at 86% 10%, rgba(193, 103, 75, 0.18), transparent 14rem),
+ var(--flows-surface-strong);
+}
+
+.privacyPanel svg,
+.assetCopy svg,
+.finalCta svg {
+ color: var(--flows-accent);
+}
+
+.privacyPanel h3,
+.patternCard h3 {
+ margin: 0;
+ color: var(--flows-text);
+ font-family: var(--font-heading), sans-serif;
+ font-size: 1.45rem;
+ line-height: 1.12;
+ letter-spacing: 0;
+}
+
+.privacyPanel p,
+.patternCard p,
+.warningCard p {
+ margin: 0;
+ color: var(--flows-muted);
+ line-height: 1.62;
+}
+
+.patternGrid {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 1rem;
+ margin-top: 2rem;
+}
+
+.patternCard {
+ min-height: 250px;
+ padding: 1.25rem;
+}
+
+.patternCard svg {
+ margin-bottom: 2rem;
+ color: var(--flows-accent);
+}
+
+.visualProof {
+ display: grid;
+ grid-template-columns: minmax(0, 1.1fr) minmax(300px, 0.7fr);
+ gap: clamp(1rem, 4vw, 2.5rem);
+ align-items: center;
+ margin-top: 0;
+}
+
+.assetFrame {
+ position: relative;
+ overflow: hidden;
+ border: 1px solid var(--flows-line);
+ border-radius: 24px;
+ padding: 0 34px 14px 0;
+ background: linear-gradient(135deg, #7eb8da 0%, #4a90c2 48%, #2d6a9c 100%);
+ box-shadow: 0 24px 80px rgba(0, 0, 0, 0.24);
+ isolation: isolate;
+}
+
+.assetFrame::before,
+.assetFrame::after {
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ content: '';
+}
+
+.assetFrame::before {
+ z-index: -2;
+ background:
+ radial-gradient(ellipse 50% 70% at 18% 26%, rgba(255, 255, 255, 0.16) 0%, transparent 70%),
+ radial-gradient(ellipse 42% 50% at 82% 76%, rgba(255, 255, 255, 0.12) 0%, transparent 66%);
+}
+
+.assetFrame::after {
+ z-index: -1;
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 320'%3E%3Cpath d='M-20,70 Q110,20 250,110 T500,80' fill='none' stroke='rgba(255,255,255,0.24)' stroke-width='2'/%3E%3Cpath d='M-20,130 Q120,70 280,160 T520,130' fill='none' stroke='rgba(255,255,255,0.16)' stroke-width='1.5'/%3E%3Cpath d='M-20,210 Q80,160 220,235 T500,210' fill='none' stroke='rgba(255,255,255,0.12)' stroke-width='1.25'/%3E%3C/svg%3E");
+ background-size: cover;
+}
+
+.skillMemoryVisual {
+ position: relative;
+ display: grid;
+ gap: 1rem;
+ min-height: 440px;
+ padding: clamp(1.25rem, 3vw, 1.65rem) clamp(1rem, 2.8vw, 1.28rem) clamp(1rem, 2.8vw, 1.28rem);
+ overflow: hidden;
+ border: 1px solid color-mix(in srgb, #9ccfff 16%, transparent);
+ border-radius: 0 0 14px 0;
+ background: #07111f;
+ box-shadow: 10px 10px 28px color-mix(in srgb, #020712 42%, transparent);
+ isolation: isolate;
+}
+
+.skillMemoryVisual::before {
+ position: absolute;
+ inset: 0;
+ z-index: 0;
+ pointer-events: none;
+ background:
+ radial-gradient(circle at 72% 18%, rgba(74, 144, 194, 0.16), transparent 34%),
+ linear-gradient(180deg, rgba(255, 255, 255, 0.035), transparent 42%),
+ linear-gradient(90deg, rgba(126, 184, 218, 0.07) 1px, transparent 1px),
+ linear-gradient(180deg, rgba(126, 184, 218, 0.045) 1px, transparent 1px);
+ background-size: 48px 48px;
+ opacity: 1;
+ content: '';
+}
+
+.skillMemoryGrid {
+ position: relative;
+ z-index: 1;
+}
+
+.skillMemoryGrid {
+ display: grid;
+ grid-template-columns: minmax(0, 1.28fr) minmax(168px, 0.58fr);
+ gap: 0.85rem;
+ align-items: stretch;
+}
+
+.skillTreePane,
+.skillSignalPane {
+ border: 1px solid rgba(156, 207, 255, 0.13);
+ border-radius: 16px;
+ background: rgba(5, 13, 21, 0.68);
+ backdrop-filter: blur(10px);
+}
+
+.skillTreePane {
+ position: relative;
+ padding: 1rem 1rem 1rem 2.75rem;
+}
+
+.skillTreePane::before {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 1.5rem;
+ width: 1px;
+ background: linear-gradient(
+ 180deg,
+ rgba(126, 184, 218, 0.28),
+ rgba(126, 184, 218, 0.36) 10%,
+ rgba(126, 184, 218, 0.2) 55%,
+ rgba(126, 184, 218, 0.24) 82%,
+ rgba(126, 184, 218, 0.18)
+ );
+ content: '';
+}
+
+.treeRoot {
+ display: inline-flex;
+ padding: 0.42rem 0.58rem;
+ border: 1px solid rgba(116, 184, 226, 0.16);
+ border-radius: 10px;
+ background: rgba(116, 184, 226, 0.08);
+ color: var(--flows-accent-strong);
+ font-family: var(--font-geist-mono), monospace;
+ font-size: 0.78rem;
+ font-weight: 800;
+}
+
+.fileTree {
+ display: grid;
+ gap: 0.7rem;
+ margin: 1rem 0 0;
+ padding: 0;
+ list-style: none;
+}
+
+.fileTreeItem {
+ position: relative;
+ display: grid;
+ gap: 0.24rem;
+ padding: 0.78rem 0.86rem;
+ border: 1px solid rgba(156, 207, 255, 0.13);
+ border-radius: 14px;
+ background: rgba(15, 27, 45, 0.72);
+ box-shadow: 0 10px 24px rgba(2, 7, 18, 0.22);
+}
+
+.fileTreeItem::before {
+ position: absolute;
+ top: 50%;
+ left: -2rem;
+ display: grid;
+ place-items: center;
+ width: 1rem;
+ height: 1rem;
+ color: #28c840;
+ font-family: var(--font-geist-mono), monospace;
+ font-size: 0.9rem;
+ font-weight: 850;
+ line-height: 1;
+ transform: translateY(-50%);
+ content: '\2713';
+}
+
+.filePath {
+ overflow: hidden;
+ color: var(--flows-text);
+ font-family: var(--font-geist-mono), monospace;
+ font-size: clamp(0.74rem, 1vw, 0.84rem);
+ font-weight: 760;
+ line-height: 1.35;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.fileMeta {
+ color: var(--flows-muted);
+ font-size: 0.82rem;
+ line-height: 1.45;
+}
+
+.skillSignalPane {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+ justify-content: flex-end;
+ padding: 1rem;
+ background:
+ radial-gradient(circle at 100% 0%, rgba(126, 184, 218, 0.18), transparent 9rem),
+ rgba(15, 27, 45, 0.7);
+}
+
+.skillSignalPane span {
+ color: color-mix(in srgb, var(--flows-accent) 74%, white 26%);
+ font-size: 0.74rem;
+ font-weight: 850;
+ text-transform: uppercase;
+}
+
+.skillSignalPane p {
+ margin: 0;
+ padding: 0.72rem 0.78rem;
+ border: 1px solid rgba(156, 207, 255, 0.13);
+ border-radius: 12px;
+ background: rgba(15, 27, 45, 0.72);
+ color: var(--flows-text);
+ font-size: 0.84rem;
+ line-height: 1.45;
+}
+
+.assetCopy,
+.planCopy,
+.managerHead {
+ display: grid;
+ gap: 1rem;
+}
+
+.planSection {
+ display: grid;
+ grid-template-columns: minmax(280px, 0.68fr) minmax(0, 1fr);
+ gap: clamp(1.5rem, 5vw, 4rem);
+ align-items: center;
+ margin-top: 0;
+}
+
+.promptPanel {
+ position: relative;
+ overflow: hidden;
+ padding: 0;
+ background:
+ linear-gradient(180deg, rgba(13, 18, 26, 0.98), rgba(4, 8, 13, 0.99)),
+ #070b10;
+ box-shadow:
+ inset 0 1px 0 rgba(237, 244, 251, 0.07),
+ inset 0 -1px 0 rgba(116, 184, 226, 0.08),
+ 0 24px 72px rgba(0, 0, 0, 0.36);
+}
+
+.promptPanel::before {
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ background:
+ linear-gradient(90deg, rgba(116, 184, 226, 0.045) 1px, transparent 1px),
+ linear-gradient(180deg, rgba(116, 184, 226, 0.035) 1px, transparent 1px);
+ background-size: 44px 44px;
+ opacity: 0.24;
+ content: '';
+}
+
+.promptPanel::after {
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ background: linear-gradient(110deg, transparent 0 42%, rgba(159, 212, 245, 0.12) 50%, transparent 58% 100%);
+ opacity: 0;
+ content: '';
+}
+
+.terminalHeader,
+.terminalBody {
+ position: relative;
+ z-index: 1;
+}
+
+.terminalHeader {
+ display: grid;
+ grid-template-columns: 5.8rem minmax(0, 1fr) auto;
+ gap: 0.75rem;
+ align-items: center;
+ min-height: 3rem;
+ padding: 0.72rem 1rem;
+ border-bottom: 1px solid rgba(237, 244, 251, 0.08);
+ background:
+ linear-gradient(180deg, rgba(18, 23, 31, 0.98), rgba(8, 12, 18, 0.98)),
+ #0a0d13;
+ font-family: var(--font-geist-mono), monospace;
+}
+
+.terminalChrome {
+ display: flex;
+ gap: 0.48rem;
+ align-items: center;
+}
+
+.terminalChrome span {
+ width: 0.76rem;
+ height: 0.76rem;
+ border: 1px solid rgba(0, 0, 0, 0.34);
+ border-radius: 999px;
+ box-shadow:
+ inset 0 1px 0 rgba(255, 255, 255, 0.45),
+ 0 0 0 1px rgba(255, 255, 255, 0.03);
+}
+
+.terminalChrome span:nth-child(1) {
+ background: #ff5f57;
+}
+
+.terminalChrome span:nth-child(2) {
+ background: #ffbd2e;
+}
+
+.terminalChrome span:nth-child(3) {
+ background: #28c840;
+}
+
+.terminalTitle {
+ justify-self: center;
+ color: rgba(237, 244, 251, 0.76);
+ font-size: 0.75rem;
+ font-weight: 700;
+}
+
+.terminalState {
+ padding: 0.28rem 0.5rem;
+ border: 1px solid rgba(116, 184, 226, 0.18);
+ border-radius: 999px;
+ background: rgba(116, 184, 226, 0.07);
+ color: var(--flows-accent-strong);
+ font-size: 0.68rem;
+ font-weight: 700;
+}
+
+.terminalBody {
+ display: grid;
+ gap: 0.8rem;
+ padding: clamp(1rem, 3vw, 1.3rem);
+ background: rgba(3, 8, 13, 0.62);
+ font-family: var(--font-geist-mono), monospace;
+}
+
+.terminalLine {
+ display: grid;
+ grid-template-columns: auto minmax(0, 1fr) auto;
+ gap: 0.75rem;
+ align-items: center;
+ min-height: 3rem;
+ padding: 0.78rem 0.9rem;
+ border: 1px solid rgba(116, 184, 226, 0.13);
+ border-radius: 14px;
+ background: rgba(9, 18, 28, 0.9);
+ color: var(--flows-text);
+ font-size: clamp(0.78rem, 1.35vw, 0.94rem);
+ line-height: 1.45;
+}
+
+.terminalSearch {
+ min-height: 2.65rem;
+ background: rgba(5, 11, 18, 0.86);
+ color: var(--flows-muted);
+}
+
+.terminalPrompt {
+ color: var(--flows-accent);
+ font-weight: 800;
+}
+
+.terminalCursor {
+ width: 0.55rem;
+ height: 1.05rem;
+ border-radius: 2px;
+ background: var(--flows-accent-strong);
+ box-shadow: 0 0 18px rgba(116, 184, 226, 0.34);
+}
+
+.terminalAnswer {
+ padding: 1rem 1.05rem;
+ border: 1px solid rgba(116, 184, 226, 0.1);
+ border-radius: 16px;
+ background: rgba(7, 12, 18, 0.82);
+}
+
+.terminalAnswer p {
+ margin: 0 0 0.85rem;
+ color: var(--flows-muted);
+ font-size: clamp(0.82rem, 1.25vw, 0.94rem);
+ line-height: 1.6;
+}
+
+.terminalAnswer ol {
+ counter-reset: terminal-step;
+ display: grid;
+ gap: 0.72rem;
+ margin: 0;
+ padding: 0;
+ color: var(--flows-text);
+ list-style: none;
+}
+
+.terminalAnswer li {
+ counter-increment: terminal-step;
+ display: grid;
+ grid-template-columns: auto minmax(0, 1fr);
+ gap: 0.75rem;
+ align-items: start;
+ color: var(--flows-text);
+ font-size: clamp(0.82rem, 1.25vw, 0.94rem);
+ line-height: 1.55;
+}
+
+.terminalAnswer li::before {
+ color: var(--flows-accent);
+ font-weight: 800;
+ content: '0' counter(terminal-step);
+}
+
+.terminalAnswer li span {
+ min-width: 0;
+}
+
+.terminalCommand,
+.terminalSearch,
+.terminalAnswer p,
+.terminalAnswer li {
+ opacity: 1;
+ transform: none;
+}
+
+.terminalAnswer li::marker {
+ color: var(--flows-accent);
+ font-weight: 800;
+}
+
+.warningGrid {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 1rem;
+ margin-top: 2rem;
+}
+
+.warningCard {
+ padding: 1.15rem;
+ background: rgba(9, 24, 38, 0.72);
+}
+
+.warningCard svg {
+ margin-bottom: 1.4rem;
+ color: var(--flows-accent);
+}
+
+.managerSection {
+ display: grid;
+ grid-template-columns: minmax(0, 0.95fr) minmax(320px, 0.8fr);
+ gap: clamp(1.5rem, 5vw, 4rem);
+ align-items: start;
+ padding: clamp(5.5rem, 10vw, 7.5rem) 0;
+ border: 0;
+ border-radius: 0;
+ background: transparent;
+}
+
+.questionGrid {
+ display: grid;
+ gap: 0.75rem;
+}
+
+.questionItem {
+ padding: 0.95rem 1rem;
+ border: 1px solid rgba(116, 184, 226, 0.14);
+ border-radius: 16px;
+ background: rgba(7, 17, 26, 0.48);
+ color: var(--flows-text);
+ font-weight: 650;
+}
+
+.finalCta {
+ display: flex;
+ gap: 1.5rem;
+ align-items: center;
+ justify-content: space-between;
+ margin-top: 0;
+ padding: clamp(5.5rem, 10vw, 7.5rem) 0;
+ border: 0;
+ border-radius: 0;
+ background: transparent;
+}
+
+.finalCta > div {
+ display: grid;
+ gap: 1rem;
+}
+
+.finalCta h2 {
+ max-width: 12ch;
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ .hero::after {
+ animation: reflexHeroDrift 15s ease-in-out infinite alternate;
+ will-change: transform;
+ }
+
+ .waveBreak svg {
+ animation: reflexWaveDrift 9s ease-in-out infinite alternate;
+ will-change: transform;
+ }
+
+ .sectionBand::after {
+ animation: reflexBandDrift 18s ease-in-out infinite alternate;
+ will-change: transform;
+ }
+
+ .promptPanel::after {
+ animation: terminalSweep 4.8s ease-in-out 1.2s infinite;
+ }
+
+ .terminalCommand,
+ .terminalSearch,
+ .terminalAnswer p,
+ .terminalAnswer li {
+ opacity: 0;
+ transform: translateY(10px);
+ animation: terminalLineIn 0.55s var(--ease-out-quint) forwards;
+ will-change: transform, opacity;
+ }
+
+ .terminalCommand {
+ animation-delay: 0.12s;
+ }
+
+ .terminalSearch {
+ animation-delay: 0.72s;
+ }
+
+ .terminalAnswer p {
+ animation-delay: 1.18s;
+ }
+
+ .terminalAnswer li:nth-child(1) {
+ animation-delay: 1.58s;
+ }
+
+ .terminalAnswer li:nth-child(2) {
+ animation-delay: 1.82s;
+ }
+
+ .terminalAnswer li:nth-child(3) {
+ animation-delay: 2.06s;
+ }
+
+ .terminalAnswer li:nth-child(4) {
+ animation-delay: 2.3s;
+ }
+
+ .terminalCursor {
+ animation: terminalCursorBlink 1s steps(2, end) infinite;
+ }
+
+ .patternCard,
+ .warningCard,
+ .fileTreeItem,
+ .skillSignalPane p,
+ .signalRow,
+ .questionItem {
+ transition:
+ transform 0.25s var(--ease-out-quint),
+ border-color 0.25s,
+ background 0.25s;
+ }
+
+ .patternCard:hover,
+ .warningCard:hover,
+ .fileTreeItem:hover,
+ .skillSignalPane p:hover,
+ .signalRow:hover,
+ .questionItem:hover {
+ transform: translateY(-2px);
+ border-color: var(--flows-line-strong);
+ background-color: rgba(16, 39, 58, 0.82);
+ }
+}
+
+@keyframes reflexHeroDrift {
+ from {
+ transform: skewY(-3deg) translate3d(-18px, 18%, 0);
+ }
+ to {
+ transform: skewY(-3deg) translate3d(22px, 18%, 0);
+ }
+}
+
+@keyframes reflexWaveDrift {
+ from {
+ transform: translate(-51%, -50%) rotate(2deg);
+ }
+ to {
+ transform: translate(-49%, -50%) rotate(2deg);
+ }
+}
+
+@keyframes reflexBandDrift {
+ from {
+ transform: translate3d(-18px, 0, 0);
+ }
+ to {
+ transform: translate3d(18px, 0, 0);
+ }
+}
+
+@keyframes terminalLineIn {
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@keyframes terminalCursorBlink {
+ 0%,
+ 46% {
+ opacity: 1;
+ }
+ 47%,
+ 100% {
+ opacity: 0;
+ }
+}
+
+@keyframes terminalSweep {
+ 0%,
+ 52% {
+ opacity: 0;
+ transform: translateX(-120%);
+ }
+ 62% {
+ opacity: 1;
+ }
+ 82%,
+ 100% {
+ opacity: 0;
+ transform: translateX(120%);
+ }
+}
+
+@media (max-width: 980px) {
+ .captureGrid,
+ .visualProof,
+ .planSection,
+ .managerSection {
+ grid-template-columns: 1fr;
+ }
+
+ .hero h1 {
+ max-width: 22ch;
+ }
+
+ .patternGrid,
+ .warningGrid {
+ grid-template-columns: 1fr;
+ }
+}
+
+@media (max-width: 680px) {
+ .main {
+ padding-inline: 1rem;
+ }
+
+ .hero {
+ gap: 2rem;
+ }
+
+ .hero h1 {
+ font-size: clamp(2.15rem, 7.2vw, 3.1rem);
+ }
+
+ .headlineLine {
+ white-space: normal;
+ }
+
+ .signalPanel {
+ grid-template-columns: 1fr;
+ }
+
+ .skillMemoryGrid {
+ grid-template-columns: 1fr;
+ }
+
+ .finalCta {
+ align-items: flex-start;
+ flex-direction: column;
+ }
+}
+
+/* ── flows-specific: use case grid, code block, gate strip ── */
+
+.textLink { display: inline-flex; align-items: center; gap: 0.5rem; width: fit-content; margin-top: 0.5rem; color: var(--flows-accent-strong); font-size: 0.95rem; }
+.textLink:hover { text-decoration: underline; text-underline-offset: 0.3em; }
+.textLink:focus-visible { outline: 2px solid var(--flows-accent-strong); outline-offset: 5px; border-radius: 2px; }
+
+.codeSection {
+ display: grid;
+ grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
+ gap: clamp(1rem, 4vw, 2.5rem);
+ align-items: center;
+}
+
+.codeCopy {
+ display: grid;
+ gap: 1rem;
+ max-width: 32rem;
+}
+
+.codeCopy h2,
+.bandIntegrations .sectionIntro h2,
+.workflowSection > div > div > h3 {
+ margin: 0;
+ font-family: var(--font-heading), sans-serif;
+ font-weight: 620;
+ font-size: clamp(2rem, 3.4vw, 3rem);
+ line-height: 1.16;
+ letter-spacing: -0.035em;
+ color: var(--flows-text);
+}
+
+.codeCopy p {
+ margin: 0;
+ color: var(--flows-muted);
+ font-size: 1.04rem;
+ line-height: 1.66;
+}
+
+.codeBlock {
+ overflow: hidden;
+ border: 1px solid var(--flows-line);
+ border-radius: 20px;
+ background: #050f18;
+ min-width: 0;
+}
+
+.codeHeader {
+ display: flex;
+ align-items: center;
+ gap: 0.6rem;
+ padding: 0.7rem 1rem;
+ border-bottom: 1px solid var(--flows-line);
+ background: rgba(255, 255, 255, 0.02);
+ color: var(--flows-faint);
+ font-size: 0.82rem;
+ font-family: var(--font-geist-mono), monospace;
+}
+
+.codeChrome {
+ display: flex;
+ gap: 0.35rem;
+}
+
+.codeChrome span {
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+}
+
+.codeChrome span:nth-child(1) { background: #ff5f57; }
+.codeChrome span:nth-child(2) { background: #febc2e; }
+.codeChrome span:nth-child(3) { background: #28c840; }
+
+.codePre {
+ margin: 0;
+ padding: 1.15rem 1.35rem;
+ overflow-x: auto;
+ color: var(--flows-text);
+ font-family: var(--font-geist-mono), 'JetBrains Mono', 'Fira Code', monospace;
+ font-size: 0.85rem;
+ line-height: 1.62;
+ white-space: pre;
+ tab-size: 2;
+}
+
+.codeKw { color: #d97757; }
+.codeFn { color: #9fd4f5; }
+.codeStr { color: #a8dda2; }
+.codeCmt { color: var(--flows-faint); font-style: italic; }
+.codeMuted { color: var(--flows-muted); }
+
+.gateStrip {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 1rem;
+ margin-top: 2rem;
+}
+
+.gateCard {
+ padding: 1.4rem;
+ border: 1px solid var(--flows-line);
+ border-radius: 22px;
+ background: var(--flows-surface);
+}
+
+.gateCard h3 {
+ margin: 0 0 0.55rem;
+ color: var(--flows-text);
+ font-family: var(--font-heading), sans-serif;
+ font-size: 1.3rem;
+ line-height: 1.15;
+}
+
+.gateCard p {
+ margin: 0;
+ color: var(--flows-muted);
+ line-height: 1.62;
+}
+
+.gateBad h3 { color: #e6957a; }
+.gateGood h3 { color: #b1e0c7; }
+
+.integrationsMarquee { margin-top: 2rem; width: 100vw; margin-inline: calc(50% - 50vw); }
+.integrationWindow { overflow: hidden; }
+.integrationTrack { display: flex; width: max-content; animation: integrationScroll 55s linear infinite; }
+.integrationTrack + .integrationTrack { margin-top: 0.5rem; animation-direction: reverse; animation-duration: 65s; }
+.integrationGroup { flex-shrink: 0; display: flex; gap: 0.5rem; list-style: none; padding: 0 0.5rem 0 0; margin: 0; }
+.integrationLogo {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 0.6rem;
+ width: 124px;
+ min-height: 96px;
+ border: 1px solid var(--flows-line);
+ border-radius: 9px;
+ background: rgba(7, 17, 26, 0.45);
+ color: var(--flows-accent-strong);
+}
+.integrationLogo img { object-fit: contain; }
+.integrationLogo img, .integrationLogo > svg { width: 28px; height: 28px; }
+.integrationLogo span { color: var(--flows-muted); font-size: 0.75rem; white-space: nowrap; }
+.integrationWindow:hover .integrationTrack { animation-play-state: paused; }
+@keyframes integrationScroll { to { transform: translateX(calc(-100% / var(--integration-copies, 2))); } }
+@media (max-width: 680px) {
+ .integrationLogo { width: 112px; min-height: 88px; }
+}
+@media (prefers-reduced-motion: reduce) {
+ .integrationWindow { mask-image: none; }
+ .integrationTrack { animation: none; width: 100%; }
+ .integrationGroup { flex-wrap: wrap; justify-content: center; width: 100%; padding: 0; }
+ .integrationGroup[aria-hidden='true'] { display: none; }
+ .integrationLogo { flex: 1 0 112px; }
+}
+
+.bandCases {
+ --section-bg: var(--flows-band-dark);
+ --section-pattern:
+ linear-gradient(90deg, rgba(116, 184, 226, 0.08) 1px, transparent 1px),
+ linear-gradient(180deg, rgba(116, 184, 226, 0.06) 1px, transparent 1px);
+ --section-pattern-size:
+ 92px 92px,
+ 92px 92px;
+}
+
+.bandCode {
+ --section-bg: linear-gradient(180deg, #10273a 0%, #0d2638 100%);
+ --section-pattern:
+ radial-gradient(circle at 18% 12%, rgba(116, 184, 226, 0.18), transparent 22rem),
+ linear-gradient(120deg, rgba(116, 184, 226, 0.1), transparent 32%);
+ --section-pattern-opacity: 0.44;
+}
+
+.bandGate {
+ --section-bg: var(--flows-band-deep);
+ --section-pattern:
+ radial-gradient(circle at 80% 20%, rgba(217, 119, 87, 0.16), transparent 24rem),
+ linear-gradient(90deg, rgba(237, 244, 251, 0.05) 1px, transparent 1px);
+ --section-pattern-opacity: 0.5;
+}
+
+.bandIntegrations {
+ --section-bg: #21435B;
+ --section-pattern-opacity: 0;
+ --section-pattern:
+ linear-gradient(90deg, transparent 0%, rgba(116, 184, 226, 0.1) 50%, transparent 100%),
+ linear-gradient(180deg, rgba(116, 184, 226, 0.05) 1px, transparent 1px);
+ --section-pattern-size:
+ 520px 100%,
+ 100% 86px;
+}
+
+@media (max-width: 980px) {
+ .codeSection {
+ grid-template-columns: 1fr;
+ }
+ .gateStrip {
+ grid-template-columns: 1fr;
+ }
+}
+
+.finalCtaLead {
+ max-width: 34rem;
+ margin: 0.9rem 0 0;
+ color: var(--flows-muted);
+ font-size: 1.02rem;
+ line-height: 1.62;
+}
+
+.gateCard > svg { margin-bottom: 1rem; color: var(--flows-accent); }
+
+@media (max-width: 680px) {
+ .codePre { padding: 1rem; font-size: 0.72rem; }
+}
+
+.scriptCarousel { min-width: 0; }
+.selectedExplanation { display: grid; gap: 1rem; }
+.useCasePicker { grid-column: 1 / -1; display: grid; gap: 1rem; min-width: 0; }
+.useCaseLabel { margin: 0; color: var(--flows-text); font-size: 1rem; font-weight: 500; text-align: left; }
+.useCaseGrid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0.75rem; }
+.useCaseOption {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ min-height: 64px;
+ padding: 1rem;
+ border: 1px solid var(--flows-line);
+ border-radius: 10px;
+ background: rgba(5, 15, 24, 0.5);
+ color: var(--flows-muted);
+ font: inherit;
+ font-size: 0.92rem;
+ text-align: left;
+ cursor: pointer;
+ transition: background 150ms ease, border-color 150ms ease, color 150ms ease;
+}
+.useCaseOption:hover { border-color: var(--flows-line-strong); background: #19394e; color: var(--flows-text); }
+.useCaseOption[aria-pressed='true'] { background: #2d6b9c; border-color: #74b8e2; color: #fff; }
+.useCaseNumber { font-family: var(--font-geist-mono), monospace; font-size: 0.75rem; opacity: 0.7; }
+@media (prefers-reduced-motion: reduce) { .useCaseOption { transition: none; } }
+.scriptCarousel button:focus-visible,
+.carouselCode:focus-visible { outline: 2px solid var(--flows-accent-strong); outline-offset: 3px; }
+.carouselCode { min-height: 40rem; overflow: auto; }
+.carouselCode span { color: var(--shiki-dark, inherit) !important; }
+.visuallyHidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
+
+@media (max-width: 980px) {
+ .scriptCarousel { gap: 2rem; }
+ .useCaseGrid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+}
+@media (max-width: 680px) {
+ .useCaseOption { padding: 0.75rem; gap: 0.5rem; font-size: 0.85rem; }
+ .carouselCode { min-height: 30rem; }
+}
+
+.capabilityWaveBreak {
+ position: relative;
+ z-index: 3;
+ height: 84px;
+ margin: -38px 0;
+ pointer-events: none;
+}
+
+.capabilityWaveBreak > div {
+ height: 84px;
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+.scriptViewport { position: relative; min-width: 0; }
+.scriptSizeReference { visibility: hidden; pointer-events: none; }
+/* Let scrolling continue onto the page at the script's top and bottom edges. */
+.scriptScroll { position: absolute; inset: 0; overflow: auto; overscroll-behavior-x: contain; overscroll-behavior-y: auto; }
+.scriptScroll > pre { min-height: 100%; overflow: visible; box-sizing: border-box; }
+.scriptScroll:focus-visible { outline: 2px solid var(--flows-accent-strong); outline-offset: -2px; }
diff --git a/web/app/flows/page.tsx b/web/app/flows/page.tsx
new file mode 100644
index 00000000..d09f5dea
--- /dev/null
+++ b/web/app/flows/page.tsx
@@ -0,0 +1,190 @@
+import { DeploymentCall } from '../../components/home/DeploymentCall';
+import { WaveDivider } from '../../components/home/icons';
+import { FlowGatePreview } from '../../components/home/FlowGatePreview';
+import type { Metadata } from 'next';
+import { ArrowRight } from 'lucide-react';
+
+import { MessagingFeature } from '../../components/home/MessagingFeature';
+import { DurableWorkflowFeature } from '../../components/home/DeliveryFeature';
+import { InvestorStrip } from '../../components/InvestorStrip';
+
+import { SiteFooter } from '../../components/SiteFooter';
+import { SiteNav } from '../../components/SiteNav';
+import { HOME_OG_IMAGE_PATH, ogImage } from '../../lib/og-meta';
+import { absoluteUrl } from '../../lib/site';
+import home from '../landing.module.css';
+import s from './flows.module.css';
+import { FlowExamples } from './FlowExamples';
+import { IntegrationMarquee } from './IntegrationMarquee';
+
+export const metadata: Metadata = {
+ title: 'Relayflows: Script your agents',
+ description:
+ 'Write the steps once. Relayflows runs your agents in order, checks their work, and pauses for human approval.',
+ alternates: {
+ canonical: absoluteUrl('/flows'),
+ },
+ openGraph: {
+ title: 'Relayflows: Script your agents',
+ description:
+ 'Write the steps once. Relayflows runs your agents in order, checks their work, and pauses for human approval.',
+ url: absoluteUrl('/flows'),
+ type: 'website',
+ images: [ogImage(HOME_OG_IMAGE_PATH, 'Relayflows by Agent Relay')],
+ },
+ twitter: {
+ card: 'summary_large_image',
+ title: 'Relayflows: Script your agents',
+ description: 'Write the steps once. Relayflows runs your agents and checks their work before moving on.',
+ images: [absoluteUrl(HOME_OG_IMAGE_PATH)],
+ },
+};
+
+// The cloud app mounts its Google OAuth entry point under /cloud.
+const FLOWS_SIGN_IN_URL =
+ 'https://agentrelay.com/cloud/api/auth/google/start?next=%2Fdashboard&ref=flows&utm_source=agentrelay.com&utm_medium=flows_landing&utm_campaign=flows';
+
+function flowsSignInHref(utmContent: string) {
+ return `${FLOWS_SIGN_IN_URL}&utm_content=${utmContent}`;
+}
+
+function WaveBreak({ tone = 'blue' }: { tone?: 'blue' | 'orange' }) {
+ return (
+
+ );
+}
+
+
+
+export default function FlowsPage() {
+ const navGetStartedLink = (
+
+ Try Free with Cloud
+
+ );
+ const mobileGetStartedLink = (
+
+ Try Free with Cloud
+
+ );
+
+ return (
+
+
+
+
+
+
+
+ Stop babysitting agents. {' '}
+ Script them.
+
+
+ Use Agent Relay Flows to define complex sequences of tasks instead
+ of relying on the vibes of an agent. Predictable, auditable and dependable.
+
+
+
+
+
+
+
+
+
+
+
+{`export default `}flow {`(
+ `}"software-factory" {`,
+ { budget: `}"$8/run" {` },
+ `}async {` (f, input) => {
+ `}await {` f
+ .`}agent {`(`}"planner" {`, { model: `}"claude-sonnet-5" {`, task: ... })
+ .`}gate {`((r) => r.artifacts.includes(`}"plan/plan.md" {`));
+
+ `}await {` f
+ .`}agent {`(`}"implementer" {`, { model: `}"claude-opus-5" {`, task: ... })
+ .`}gate {`((r) => r.artifacts.includes(`}"impl/branch.txt" {`));
+
+ `}{`// The tests run outside the agent.
+ // The agent cannot lie about the exit code.`} {`
+ `}await {` f
+ .`}run {`(`}{`\`git checkout $(cat impl/branch.txt) && npm test\``} {`)
+ .`}gate {`((out) => /failed:\\s*0/.test(out));
+
+ `}{`// Different model. Its only job is to break the PR.`} {`
+ `}await {` f
+ .`}agent {`(`}"adversary" {`, { model: `}"gpt-5" {`, task: ... })
+ .`}gate {`((r) => r.artifacts.includes(`}"adversary/clean" {`));
+
+ `}{`// Deterministic step, not an agent decision.`} {`
+ `}await {` f.github.`}createPullRequest {`({
+ repo: input.repo,
+ head: `}{`\`feat/\${input.ticketId}\``} {`,
+ base: input.baseBranch,
+ bodyPath: `}"impl/summary.md" {`,
+ });
+
+ f.`}done {`(`}"success" {`);
+ },
+);`}
+
+ } />
+
+
+
+
+
+ }
+ items={[
+ 'Require files, run scripts, and check results before the next step starts.',
+ 'Enforce requirements in code instead of relying on an agent to follow markdown instructions.',
+ 'Save tokens by running deterministic steps directly, without asking and hoping the LLM does them.',
+ ]}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
Connect the tools you already use.
+
+ Built on top of Agent Relay, so your Flows can open pull requests, send Slack messages,
+ and update tickets in the tools your team already uses.
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/web/app/landing.module.css b/web/app/landing.module.css
index 2d9481d1..8e848c84 100644
--- a/web/app/landing.module.css
+++ b/web/app/landing.module.css
@@ -1955,6 +1955,7 @@
/* ---- HERO ---- */
.heroSection {
+ --investor-strip-padding-top: 3rem;
position: relative;
background: var(--agent-tools-band-bg);
transition: background 0.3s ease;
@@ -8022,7 +8023,7 @@
}
}
-/* ---- WORKS WITH EVERY AGENT (pyramid diagram) ---- */
+/* ---- ALL AGENTS WORK ON THE RELAY ---- */
/* Same band fill + separation as the surrounding feature bands. */
.howItWorks {
@@ -8031,7 +8032,7 @@
display: flex;
flex-direction: column;
align-items: center;
- padding: 88px 0 92px;
+ padding: 64px 0 68px;
isolation: isolate;
}
@@ -8051,8 +8052,8 @@
}
.howHeader {
- max-width: 660px;
- margin: 0 0 48px;
+ max-width: 800px;
+ margin: 0 0 32px;
text-align: center;
}
@@ -8074,387 +8075,29 @@
max-width: 56ch;
}
-/* pyramid: relay (top) -> connector pipes -> two agent groups (bottom) */
-.howStage {
- display: flex;
- flex-direction: column;
- align-items: center;
- width: 100%;
- max-width: 680px;
- margin: 0 auto;
-}
-
-/* apex zone: relay box (top) over the connector; wires + dots span both so a
- message can travel up behind the relay box and pop back out */
-.howApex {
- position: relative;
- display: flex;
- flex-direction: column;
- align-items: center;
- width: 100%;
-}
-
-/* --- relay hub at the apex (sits above the dots so they hide behind it) --- */
-.howCore {
- position: relative;
- z-index: 2;
- display: flex;
- justify-content: center;
-}
-
-.howCoreNode {
- position: relative;
+.howIconList {
display: flex;
align-items: center;
justify-content: center;
- gap: 14px;
- height: 72px;
- padding: 0 28px;
- border-radius: 18px;
- border: 1px solid color-mix(in srgb, var(--primary) 42%, transparent);
- background:
- radial-gradient(circle at 50% 0%, color-mix(in srgb, var(--primary) 22%, transparent), transparent 72%),
- #0e2031;
- box-shadow:
- 0 0 0 4px color-mix(in srgb, var(--primary) 6%, transparent),
- 0 16px 44px color-mix(in srgb, #04101c 52%, transparent);
-}
-
-/* A close, low halo that separates the hub from the band without blooming. */
-.howCoreNode::before {
- content: '';
- position: absolute;
- inset: -14px;
- z-index: -1;
- border-radius: 50%;
- background: radial-gradient(circle, color-mix(in srgb, var(--primary) 9%, transparent), transparent 70%);
- pointer-events: none;
-}
-
-.howCoreMark {
- width: 40px;
- height: auto;
-}
-
-.howCoreWordmark {
- width: 138px;
- height: auto;
-}
-
-/* --- connector spacer: fixed height so pipes + dots align at any width.
- Combined with the 72px relay box, the apex zone is a known height. --- */
-.howLink {
- width: 100%;
- height: 72px;
-}
-
-.howWires {
- position: absolute;
- inset: 0;
- z-index: 0;
- width: 100%;
- height: 100%;
-}
-
-.howWire {
- fill: none;
- stroke: color-mix(in srgb, var(--primary) 38%, transparent);
- stroke-width: 1.5;
- stroke-linejoin: round;
- stroke-linecap: round;
- vector-effect: non-scaling-stroke;
-}
-
-.howMsgs {
- position: absolute;
- inset: 0;
- z-index: 1;
- pointer-events: none;
- /* Size container so the dots can be positioned with container-query units
- (cqw/cqh) driven through GPU-composited transforms instead of animating
- left/top. The box is sized by `inset: 0` (not its contents), so size
- containment is safe here. */
- container-type: size;
-}
-
-.howMsg {
- position: absolute;
- top: 0;
- left: 0;
- width: 10px;
- height: 10px;
- margin: -5px 0 0 -5px;
- border-radius: 50%;
- background: var(--c);
- box-shadow:
- 0 0 0 1px color-mix(in srgb, var(--c) 30%, transparent),
- 0 0 7px 1px color-mix(in srgb, var(--c) 55%, transparent);
- opacity: 0;
- /* Position is driven by the individual `translate` property and the size is
- driven by the individual `scale` property; both composite on the GPU and
- interpolate independently, mirroring the old left/top vs transform split. */
- will-change: translate, scale, opacity;
-}
-
-@media (prefers-reduced-motion: reduce) {
- .howMsg {
- animation: none;
- }
-}
-
-/* Three messages, each its own colour, routed up to the relay and back down. */
-.howMsg1 {
- --c: #74b8e2; /* blue — CLI -> custom */
- animation: howMsg_m1 4.6s linear 0s infinite;
-}
-.howMsg2 {
- --c: #d97757; /* Claude orange — custom -> CLI */
- animation: howMsg_m2 5s linear 1.8s infinite;
-}
-.howMsg3 {
- --c: #a6d388; /* green — CLI -> CLI */
- animation: howMsg_m3 4.4s linear 3.6s infinite;
-}
-
-@keyframes howMsg_m1 {
- 0% {
- translate: 25cqw 100cqh;
- opacity: 0;
- scale: 1;
- }
- 4% {
- opacity: 1;
- }
- 9% {
- translate: 25cqw 75cqh;
- }
- 22% {
- translate: 50cqw 75cqh;
- animation-timing-function: cubic-bezier(0.6, 0, 0.9, 0.2);
- }
- 37% {
- translate: 50cqw 50cqh;
- opacity: 1;
- scale: 0.5;
- }
- 43% {
- translate: 50cqw 40cqh;
- opacity: 0;
- scale: 0.25;
- }
- 49% {
- translate: 50cqw 40cqh;
- opacity: 0;
- scale: 0.25;
- }
- 50% {
- translate: 50cqw 50cqh;
- opacity: 1;
- scale: 1.55;
- animation-timing-function: cubic-bezier(0.2, 0.8, 0.3, 1);
- }
- 56% {
- translate: 50cqw 57cqh;
- scale: 1;
- }
- 66% {
- translate: 50cqw 75cqh;
- }
- 82% {
- translate: 75cqw 75cqh;
- }
- 86% {
- opacity: 1;
- }
- 91% {
- translate: 75cqw 100cqh;
- opacity: 0;
- }
- 100% {
- opacity: 0;
- }
-}
-
-@keyframes howMsg_m2 {
- 0% {
- translate: 75cqw 100cqh;
- opacity: 0;
- scale: 1;
- }
- 4% {
- opacity: 1;
- }
- 9% {
- translate: 75cqw 75cqh;
- }
- 22% {
- translate: 50cqw 75cqh;
- animation-timing-function: cubic-bezier(0.6, 0, 0.9, 0.2);
- }
- 37% {
- translate: 50cqw 50cqh;
- opacity: 1;
- scale: 0.5;
- }
- 43% {
- translate: 50cqw 40cqh;
- opacity: 0;
- scale: 0.25;
- }
- 49% {
- translate: 50cqw 40cqh;
- opacity: 0;
- scale: 0.25;
- }
- 50% {
- translate: 50cqw 50cqh;
- opacity: 1;
- scale: 1.55;
- animation-timing-function: cubic-bezier(0.2, 0.8, 0.3, 1);
- }
- 56% {
- translate: 50cqw 57cqh;
- scale: 1;
- }
- 66% {
- translate: 50cqw 75cqh;
- }
- 82% {
- translate: 25cqw 75cqh;
- }
- 86% {
- opacity: 1;
- }
- 91% {
- translate: 25cqw 100cqh;
- opacity: 0;
- }
- 100% {
- opacity: 0;
- }
-}
-
-@keyframes howMsg_m3 {
- 0% {
- translate: 25cqw 100cqh;
- opacity: 0;
- scale: 1;
- }
- 4% {
- opacity: 1;
- }
- 9% {
- translate: 25cqw 75cqh;
- }
- 22% {
- translate: 50cqw 75cqh;
- animation-timing-function: cubic-bezier(0.6, 0, 0.9, 0.2);
- }
- 37% {
- translate: 50cqw 50cqh;
- opacity: 1;
- scale: 0.5;
- }
- 43% {
- translate: 50cqw 40cqh;
- opacity: 0;
- scale: 0.25;
- }
- 49% {
- translate: 50cqw 40cqh;
- opacity: 0;
- scale: 0.25;
- }
- 50% {
- translate: 50cqw 50cqh;
- opacity: 1;
- scale: 1.55;
- animation-timing-function: cubic-bezier(0.2, 0.8, 0.3, 1);
- }
- 56% {
- translate: 50cqw 57cqh;
- scale: 1;
- }
- 66% {
- translate: 50cqw 75cqh;
- }
- 82% {
- translate: 25cqw 75cqh;
- }
- 86% {
- opacity: 1;
- }
- 91% {
- translate: 25cqw 100cqh;
- opacity: 0;
- }
- 100% {
- opacity: 0;
- }
-}
-
-/* --- agent groups at the base --- */
-.howRow {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: clamp(16px, 4vw, 40px);
- width: 100%;
-}
-
-.howCol {
- display: flex;
- justify-content: center;
-}
-
-.howGroup {
- flex: 1;
- display: flex;
- flex-direction: column;
- align-items: center;
+ flex-wrap: wrap;
gap: 12px;
- padding: 22px 18px;
- border-radius: 18px;
- border: 1px solid var(--line);
- background: color-mix(in srgb, var(--card-bg) 92%, #07111c 8%);
- box-shadow: 0 14px 38px color-mix(in srgb, #04101c 36%, transparent);
-}
-
-.howGroupLabel {
- font-family: var(--font-heading), sans-serif;
- font-size: 1.05rem;
- font-weight: 700;
- letter-spacing: -0.01em;
- color: var(--fg);
- text-align: center;
+ padding: 0;
+ margin: 0;
+ list-style: none;
+ max-width: 100%;
}
-.howGroupLogos {
+.howIcon {
display: flex;
align-items: center;
justify-content: center;
- gap: 10px;
- flex-wrap: wrap;
-}
-
-.howChip {
- display: inline-flex;
- align-items: center;
- justify-content: center;
width: 44px;
height: 44px;
- border-radius: 12px;
- border: 1px solid var(--line);
- background: color-mix(in srgb, var(--card-bg) 55%, #060f1a 45%);
- /* Mono brand marks (Codex, OpenCode, Cursor, GitHub, Hermes) inherit this. */
color: #e6edf5;
- transition:
- transform 0.2s,
- border-color 0.2s;
}
-.howChip:hover {
- transform: translateY(-2px);
- border-color: var(--card-hover-border);
+.howIcon > span {
+ display: flex;
}
.howChipSvg {
@@ -8462,90 +8105,21 @@
height: 24px;
}
-.howGroupCaption {
- margin: 0;
- font-size: 0.8rem;
- line-height: 1.45;
- color: var(--fg-faint);
- text-align: center;
-}
-
@media (max-width: 600px) {
.howItWorks {
- padding: 64px 0 72px;
- }
-
- .howHeader {
- margin-bottom: 38px;
- }
-
- .howCoreWordmark {
- width: 120px;
- }
-
- /* Keep the relay box and connector equal so the apex coordinate math
- (relay bottom 50%, bus 75%) holds at this breakpoint too. */
- .howCoreNode {
- height: 60px;
- }
-
- .howLink {
- height: 60px;
- }
-
- .howRow {
- gap: 12px;
- }
-
- .howGroup {
- padding: 16px 8px;
- gap: 10px;
- }
-
- /* Cap the logo row at three chips wide so the five icons wrap evenly to a
- 3-over-2 grid (the bottom two stay centred). */
- .howGroupLogos {
- gap: 8px;
- max-width: 140px;
- margin: 0 auto;
+ padding: 48px 0;
}
- .howChip {
- width: 41px;
- height: 41px;
- }
-
- .howChipSvg {
- width: 24px;
- height: 24px;
- }
-
- .howGroupLabel {
- font-size: 0.95rem;
- }
-
- .howGroupCaption {
- font-size: 0.74rem;
+ .howIconList {
+ display: grid;
+ grid-template-columns: repeat(5, 44px);
+ gap: 8px 12px;
}
}
-/* Reduced motion: park the three messages on the pipes instead of animating. */
-@media (prefers-reduced-motion: reduce) {
- .howMsg {
- animation: none;
- opacity: 1;
- transform: none;
- }
- .howMsg1 {
- left: 25%;
- top: 88%;
- }
- .howMsg2 {
- left: 50%;
- top: 62%;
- }
- .howMsg3 {
- left: 75%;
- top: 88%;
- }
+.featureDescription {
+ margin: 0;
+ color: var(--fg-muted);
+ font-size: 1.05rem;
+ line-height: 1.7;
}
diff --git a/web/app/og-home.png/route.tsx b/web/app/og-home.png/route.tsx
index d7f06b4d..19450fd9 100644
--- a/web/app/og-home.png/route.tsx
+++ b/web/app/og-home.png/route.tsx
@@ -1,27 +1,9 @@
-import { ImageResponse } from 'next/og';
+import { HOME_OG_IMAGE_PATH } from '../../lib/og-meta';
+import { absoluteUrl } from '../../lib/site';
-import { LandingVariant, loadBrandFonts, OG_SIZE } from '../../lib/og/template';
-
-export const runtime = 'nodejs';
-// Prerender at build time: the homepage card never changes between deploys, so
-// there is no reason to re-render (and re-fetch the brand fonts) per request.
export const dynamic = 'force-static';
-/**
- * The homepage's rich Open Graph card: the landing variant (logo + wordmark,
- * hero headline, tagline, and the live-style chat panel). Served as a `.png`
- * route handler so the URL ends in a real image extension — some scrapers key
- * off the extension rather than the `Content-Type`. `app/page.tsx` points its
- * `openGraph.images` here.
- */
-export async function GET() {
- const { fonts, headingFamily, bodyFamily } = await loadBrandFonts();
-
- return new ImageResponse( , {
- ...OG_SIZE,
- // Render 👍 (and any emoji) as Twemoji graphics rather than relying on a
- // system emoji font satori may not have.
- emoji: 'twemoji',
- ...(fonts.length > 0 ? { fonts } : {}),
- });
+// Keep the legacy URL aligned with the static card used by homepage metadata.
+export function GET() {
+ return Response.redirect(absoluteUrl(HOME_OG_IMAGE_PATH), 307);
}
diff --git a/web/app/page.tsx b/web/app/page.tsx
index 156a9acf..8f0b8f41 100644
--- a/web/app/page.tsx
+++ b/web/app/page.tsx
@@ -2,7 +2,7 @@ import type { Metadata } from 'next';
import { MessagingLandingPage } from '../components/home/MessagingLandingPage';
import { HOME_HERO_DESCRIPTION, HOME_HERO_TITLE } from '../lib/home-copy';
-import { HOME_OG_IMAGE_PATH, ogImage } from '../lib/og-meta';
+import { HOME_OG_ALT, HOME_OG_IMAGE_PATH, ogImage } from '../lib/og-meta';
import { absoluteUrl } from '../lib/site';
export const metadata: Metadata = {
@@ -16,7 +16,7 @@ export const metadata: Metadata = {
description: HOME_HERO_DESCRIPTION,
url: absoluteUrl('/'),
type: 'website',
- images: [ogImage(HOME_OG_IMAGE_PATH, HOME_HERO_TITLE)],
+ images: [ogImage(HOME_OG_IMAGE_PATH, HOME_OG_ALT)],
},
twitter: {
card: 'summary_large_image',
@@ -27,5 +27,5 @@ export const metadata: Metadata = {
};
export default function HomePage() {
- return ;
+ return ;
}
diff --git a/web/components/InvestorStrip.tsx b/web/components/InvestorStrip.tsx
new file mode 100644
index 00000000..ba9f2ff3
--- /dev/null
+++ b/web/components/InvestorStrip.tsx
@@ -0,0 +1,25 @@
+import Image from 'next/image';
+import s from './investor-strip.module.css';
+
+export function InvestorStrip() {
+ return (
+
+ Backed by
+
+
+
+
+ Y Combinator
+
+
+
+
+
+
+
+ YONDER
+
+
+
+ );
+}
diff --git a/web/components/SiteFooter.tsx b/web/components/SiteFooter.tsx
index bead4f65..b6025660 100644
--- a/web/components/SiteFooter.tsx
+++ b/web/components/SiteFooter.tsx
@@ -21,8 +21,8 @@ const footerColumns: FooterColumn[] = [
title: 'Products',
links: [
{ label: 'Relay Cloud', href: 'https://agentrelay.com/cloud' },
- { label: 'Enterprise', href: '/enterprise' },
- { label: 'Reflex', href: '/reflex' },
+ { label: 'Flows', href: '/flows' },
+ { label: 'Skip', href: 'https://heyskip.dev' },
{ label: 'Pear', href: '/pear' },
],
},
@@ -32,7 +32,7 @@ const footerColumns: FooterColumn[] = [
{ label: 'Relay', href: '/messaging' },
{ label: 'RelayFile', href: 'https://github.com/AgentWorkforce/relayfile' },
{ label: 'RelayAuth', href: 'https://github.com/AgentWorkforce/relayauth' },
- { label: 'RelayFlows', href: 'https://github.com/AgentWorkforce/relayflows' },
+ { label: 'RelayFlows', href: 'https://github.com/agentworkforce/flows' },
{ label: 'Agents', href: '/agents' },
],
},
diff --git a/web/components/blog/blog.module.css b/web/components/blog/blog.module.css
index fa36d1f5..add2943d 100644
--- a/web/components/blog/blog.module.css
+++ b/web/components/blog/blog.module.css
@@ -5,7 +5,7 @@
--blog-band-bg:
radial-gradient(circle at 18% 24%, rgba(116, 184, 226, 0.16), transparent 32%),
linear-gradient(180deg, #10273a 0%, #17364c 52%, #0d2638 100%);
- --blog-shell-bg: #08111a;
+ --blog-shell-bg: #0b1c2a;
--blog-post-hero-bg: #0b1c2a;
min-height: 100vh;
background: var(--blog-band-bg);
diff --git a/web/components/home/DeliveryFeature.tsx b/web/components/home/DeliveryFeature.tsx
index f38673b1..c07be8a0 100644
--- a/web/components/home/DeliveryFeature.tsx
+++ b/web/components/home/DeliveryFeature.tsx
@@ -50,10 +50,12 @@ export function DeliveryFeature({
title = 'The hard parts of delivery, handled',
items = DEFAULT_ITEMS,
previewVariant = 'delivery',
+ showCapabilities = true,
}: {
title?: string;
items?: readonly string[];
previewVariant?: 'delivery' | 'workflow';
+ showCapabilities?: boolean;
} = {}) {
return (
@@ -96,7 +98,26 @@ export function DeliveryFeature({
-
+ {showCapabilities && }
);
}
+
+export function DurableWorkflowFeature({
+ showCapabilities = true,
+ title = 'Build durable workflows',
+}: { showCapabilities?: boolean; title?: string }) {
+ return (
+
+ );
+}
diff --git a/web/components/home/DeploymentCall.tsx b/web/components/home/DeploymentCall.tsx
new file mode 100644
index 00000000..f0d751b0
--- /dev/null
+++ b/web/components/home/DeploymentCall.tsx
@@ -0,0 +1,51 @@
+import { FlowFileAnimation } from './FlowFileAnimation';
+import Image from 'next/image';
+import { ArrowRight, Check } from 'lucide-react';
+import { FadeIn } from '../FadeIn';
+import s from '../../app/enterprise/enterprise.module.css';
+import layout from './deployment-call.module.css';
+
+export function DeploymentCall() {
+ return (
+
+
+
+
Become a design partner
+
+ We're actively looking for design partners who are tired of agents letting them down. You'll have direct access to our team and great discounts on all paid plans.
+
+
+
+
+
+ Walk us through a flaky workflow your humans or agents handle
+
+
+
+ We'll help you write your flows and deploy them
+
+
+
+ Review hosted, private cloud, or self-managed options
+
+
+
+ Leave with a clear path from discovery to production
+
+
+
+
+
+
+ Khaliq Gant
+ Founder and CTO
+
+
Book Call
+
+
+
+
+
+
+ );
+}
diff --git a/web/components/home/FlowFileAnimation.tsx b/web/components/home/FlowFileAnimation.tsx
new file mode 100644
index 00000000..f903c325
--- /dev/null
+++ b/web/components/home/FlowFileAnimation.tsx
@@ -0,0 +1,76 @@
+'use client';
+
+import { useEffect, useRef, useState } from 'react';
+import s from './flow-file-animation.module.css';
+
+const prefix = `import { flow } from "@relayflows/surface";
+
+export default flow(
+ "customer-reply",
+ { budget: "$5/run" },
+ async (f) => {
+ await f.agent("researcher", {
+ task: "Research and write findings.md",
+ });
+
+ `;
+const correction = 'await f.agent("send-reply");';
+const ending = `// Verify before drafting.
+ await f.run("test -s findings.md");
+
+ await f.agent("responder", {
+ task: "Use findings.md to draft reply.md",
+ });
+
+ f.done("success");
+ },
+);`;
+const finalCode = prefix + ending;
+const frames: { text: string; delay: number }[] = [{ text: '', delay: 600 }];
+for (let i = 1; i <= prefix.length + correction.length; i++) {
+ frames.push({ text: (prefix + correction).slice(0, i), delay: i === prefix.length + correction.length ? 1100 : 22 });
+}
+for (let i = correction.length - 1; i >= 0; i--) frames.push({ text: prefix + correction.slice(0, i), delay: i === 0 ? 450 : 38 });
+for (let i = 1; i <= ending.length; i++) frames.push({ text: prefix + ending.slice(0, i), delay: i === ending.length ? 4500 : 25 });
+
+function highlight(text: string) {
+ return text.split(/("[^"\n]*"|\/\/[^\n]*|\b(?:import|from|export|default|async|await|const)\b)/g).map((part, i) => (
+ {part}
+ ));
+}
+
+export function FlowFileAnimation() {
+ const root = useRef(null);
+ const [code, setCode] = useState('');
+ useEffect(() => {
+ const motion = matchMedia('(prefers-reduced-motion: reduce)');
+ let frame = 0;
+ let visible = false;
+ let timer: ReturnType | undefined;
+ const schedule = () => {
+ clearTimeout(timer);
+ if (motion.matches) { setCode(finalCode); return; }
+ if (!visible || document.hidden) return;
+ timer = setTimeout(() => {
+ frame = (frame + 1) % frames.length;
+ setCode(frames[frame].text);
+ schedule();
+ }, frames[frame].delay);
+ };
+ const observer = new IntersectionObserver(([entry]) => { visible = entry.isIntersecting; schedule(); }, { threshold: 0.15 });
+ if (root.current) observer.observe(root.current);
+ motion.addEventListener('change', schedule);
+ document.addEventListener('visibilitychange', schedule);
+ return () => { clearTimeout(timer); observer.disconnect(); motion.removeEventListener('change', schedule); document.removeEventListener('visibilitychange', schedule); };
+ }, []);
+ return (
+
+
+
+
{finalCode.split('\n').map((_, i) => {i + 1} )}
+
{highlight(code)}
+
+
{code === finalCode ? 'Saved' : 'Editing'} UTF-8
+
+ );
+}
diff --git a/web/components/home/FlowGatePreview.tsx b/web/components/home/FlowGatePreview.tsx
new file mode 100644
index 00000000..f54d9441
--- /dev/null
+++ b/web/components/home/FlowGatePreview.tsx
@@ -0,0 +1,96 @@
+'use client';
+
+import { useEffect, useRef, useState } from 'react';
+import { Check, FileCheck2, LockKeyhole, Terminal } from 'lucide-react';
+import ClaudeCode from '@lobehub/icons/es/ClaudeCode';
+import Image from 'next/image';
+import Grok from '@lobehub/icons/es/Grok';
+import Codex from '@lobehub/icons/es/Codex';
+import s from './flow-gate-preview.module.css';
+
+export function FlowGatePreview() {
+ const [phase, setPhase] = useState(0);
+ const root = useRef(null);
+ useEffect(() => {
+ const media = window.matchMedia('(prefers-reduced-motion: reduce)');
+ let timer: ReturnType | undefined;
+ let visible = false;
+ let current = 0;
+ const delays = [1200, 1400, 3200, 1400, 2200, 2400];
+ const start = () => {
+ clearTimeout(timer);
+ if (media.matches) { setPhase(2); return; }
+ if (!visible || document.hidden) return;
+ timer = setTimeout(() => {
+ current = (current + 1) % delays.length;
+ setPhase(current);
+ start();
+ }, delays[current]);
+ };
+ const observer = new IntersectionObserver(([entry]) => { visible = entry.isIntersecting; start(); }, { threshold: 0.2 });
+ if (root.current) observer.observe(root.current);
+ media.addEventListener('change', start);
+ document.addEventListener('visibilitychange', start);
+ return () => { clearTimeout(timer); observer.disconnect(); media.removeEventListener('change', start); document.removeEventListener('visibilitychange', start); };
+ }, []);
+ const passed = phase === 5;
+ const stages = [
+ { label: 'Gmail', task: 'Receive mail', step: 0, x: 7, y: 45, icon: },
+ { label: 'Script', task: 'Classify', step: 1, x: 23, y: 45, icon: },
+ { label: 'Grok', task: 'Research competitors', step: 2, x: 43, y: 0, icon: },
+ { label: 'Claude', task: 'Review backlog', step: 2, x: 43, y: 90, icon: },
+ { label: 'Script', task: 'Verify conclusions', step: 3, x: 70, y: 45, icon: },
+ { label: 'Codex', task: 'Draft response', step: 4, x: 92, y: 45, icon: },
+ ];
+ const messages = [
+ ['GMAIL', 'Customer email received.'],
+ ['SCRIPT', 'Classifying the request.'],
+ ['AGENTS', 'Researching competitors and reviewing backlog.'],
+ ['SCRIPT', 'Verifying conclusions from both agents.'],
+ ['CODEX', 'Drafting a response from the findings.'],
+ ['CODEX', 'Response draft ready.'],
+ ];
+ const status = [
+ 'Email received. Classification is next.',
+ 'Running the classification script.',
+ 'Verification waits for both agent outputs.',
+ 'Requirements met. Codex can start.',
+ 'Codex is preparing the response.',
+ 'Response draft ready. Flow complete.',
+ ];
+ return (
+
+
+
+
+ = 1 ? s.pathDone : ''} d="M7 63 L23 63" />
+ = 2 ? s.pathDone : ''} d="M23 63 L43 18 M23 63 L43 108" />
+ = 3 ? s.pathDone : ''} d="M43 18 L70 63 M43 108 L70 63" />
+ = 4 ? s.pathDone : ''} d="M70 63 L92 63" />
+
+ {stages.map((stage) => (
+
+ {stage.icon}
+ {stage.label} {stage.task}
+
+ ))}
+
+
{messages[phase][0]} {messages[phase][1]}
+
+ {[
+ { file: 'classification.json', ready: phase >= 2, waiting: 'Classifying' },
+ { file: 'research + backlog', ready: phase >= 3, waiting: 'Awaiting both' },
+ { file: 'response.md', ready: phase >= 5, waiting: 'Pending' },
+ ].map((item) => (
+
+ {item.file}{item.ready ? <> Ready> : item.waiting}
+
+ ))}
+
+
+ {passed ? : }{status[phase]}
+
+
+
+ );
+}
diff --git a/web/components/home/Hero.tsx b/web/components/home/Hero.tsx
index a4d37125..a563e938 100644
--- a/web/components/home/Hero.tsx
+++ b/web/components/home/Hero.tsx
@@ -1,4 +1,5 @@
import Link from 'next/link';
+import { InvestorStrip } from '../InvestorStrip';
import { ArrowRight } from 'lucide-react';
import { HOME_HERO_DESCRIPTION, HOME_HERO_TITLE } from '../../lib/home-copy';
@@ -6,7 +7,7 @@ import { HeroTerminalMarquee } from './HeroTerminalMarquee';
import s from '../../app/landing.module.css';
import { GitHubIcon } from './icons';
-export function Hero() {
+export function Hero({ showInvestors = false }: { showInvestors?: boolean }) {
return (
@@ -35,6 +36,7 @@ export function Hero() {
+ {showInvestors && }
);
diff --git a/web/components/home/HowItWorks.tsx b/web/components/home/HowItWorks.tsx
index e2094dfa..40a92cef 100644
--- a/web/components/home/HowItWorks.tsx
+++ b/web/components/home/HowItWorks.tsx
@@ -41,110 +41,25 @@ const CUSTOM_LOGOS: readonly LogoItem[] = [
{ key: 'github', label: 'GitHub', node: },
];
-/**
- * Pyramid layout: the Agent Relay hub sits at the top; the two agent groups
- * sit below it. A fixed-height connector zone between them carries the
- * orthogonal pipes (relay → bus → each group) and the animated messages.
- * Coordinates are percentages of that connector zone, so the pipes and dots
- * line up at every width. Three message dots ({@link MESSAGE_CLASSES}) travel
- * the pipes up to the relay and back down to another agent (CLI → custom,
- * custom → CLI, CLI → CLI).
- */
-// Apex coords (% of the relay+connector zone): relay bottom 50%, bus 75%,
-// card tops 100%. The pipes connect the relay box down to each agent group.
-const WIRES: readonly string[] = [
- '50,50 50,75', // relay down to the bus
- '25,75 75,75', // bus across
- '25,75 25,100', // bus down to CLI agents
- '75,75 75,100', // bus down to custom agents
-];
-
-const MESSAGE_CLASSES = [s.howMsg1, s.howMsg2, s.howMsg3] as const;
-
-function AgentGroup({
- label,
- logos,
- caption,
-}: {
- label: string;
- logos: readonly LogoItem[];
- caption: string;
-}) {
- return (
-
-
{label}
-
- {logos.map((logo) => (
-
- {logo.node}
-
- ))}
-
-
{caption}
-
- );
-}
-
export function HowItWorks() {
return (
- Works with all of them
+ All agents work on the relay
It's not a harness, and it's not a framework. Our PTY based driver can power any CLI agent or you
can drop in our SDK for your custom orchestrator.
-
-
- {/* Apex zone = relay box (top) + connector (below). Wires and message
- dots span the whole zone so a dot can travel up behind the relay
- box and pop back out. */}
-
-
- {WIRES.map((points, i) => (
-
- ))}
-
-
- {MESSAGE_CLASSES.map((cls, i) => (
-
- ))}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ {[...CLI_LOGOS, ...CUSTOM_LOGOS].map((logo) => (
+
+ {logo.node}
+
+ ))}
+
);
}
diff --git a/web/components/home/MessagingFeature.tsx b/web/components/home/MessagingFeature.tsx
index b26a4b39..67dd4518 100644
--- a/web/components/home/MessagingFeature.tsx
+++ b/web/components/home/MessagingFeature.tsx
@@ -1,24 +1,34 @@
+import type { ReactNode } from 'react';
import { ChannelMessagesPreview } from '../ChannelMessagesPreview';
import { FadeIn } from '../FadeIn';
import s from '../../app/landing.module.css';
-export function MessagingFeature() {
+const DEFAULT_ITEMS = [
+ 'Channels and messages to coordinate work in shared spaces.',
+ 'Threads and reactions to keep decisions attached to the right context.',
+ 'DMs and @mentions to route handoffs to the right agent.',
+ 'Searchable history so agents can recover decisions without asking humans.',
+];
+
+export function MessagingFeature({
+ title = 'Let your agents talk. Directly.',
+ items = DEFAULT_ITEMS,
+ description,
+ preview,
+}: { title?: string; items?: readonly string[]; description?: string; preview?: ReactNode } = {}) {
return (
-
Let your agents talk. Directly.
-
- Channels and messages to coordinate work in shared spaces.
- Threads and reactions to keep decisions attached to the right context.
- DMs and @mentions to route handoffs to the right agent.
- Searchable history so agents can recover decisions without asking humans.
-
+
{title}
+ {description ?
{description}
:
+ {items.map((item) => {item} )}
+ }
);
diff --git a/web/components/home/MessagingLandingPage.tsx b/web/components/home/MessagingLandingPage.tsx
index 06ff59e3..de961183 100644
--- a/web/components/home/MessagingLandingPage.tsx
+++ b/web/components/home/MessagingLandingPage.tsx
@@ -1,3 +1,4 @@
+import { DurableWorkflowFeature } from './DeliveryFeature';
import { GitHubStarsBadge } from '../GitHubStars';
import { SiteFooter } from '../SiteFooter';
import { SiteNav } from '../SiteNav';
@@ -5,7 +6,6 @@ import {
A2AFeature,
AgentToolsFeature,
Deploy,
- DeliveryFeature,
Hero,
HowItWorks,
MessagingFeature,
@@ -15,7 +15,7 @@ import {
} from './index';
import s from '../../app/landing.module.css';
-export function MessagingLandingPage() {
+export function MessagingLandingPage({ showInvestors = false }: { showInvestors?: boolean }) {
return (
@@ -25,22 +25,13 @@ export function MessagingLandingPage() {
} />
-
+
-
+
diff --git a/web/components/home/deployment-call.module.css b/web/components/home/deployment-call.module.css
new file mode 100644
index 00000000..a758e4bc
--- /dev/null
+++ b/web/components/home/deployment-call.module.css
@@ -0,0 +1,18 @@
+.section {
+ --enterprise-line: rgba(116, 184, 226, 0.2);
+ --enterprise-line-strong: rgba(116, 184, 226, 0.34);
+ --enterprise-text: #edf4fb;
+ --enterprise-muted: #a8b8c8;
+ --enterprise-faint: #77879a;
+ --enterprise-accent: #74b8e2;
+ width: 100vw;
+ margin-inline: calc(50% - 50vw);
+ box-sizing: border-box;
+}
+
+.owner { flex-wrap: wrap; }
+.owner > img { flex-shrink: 0; }
+.bookCall { margin-left: auto; }
+@media (max-width: 420px) { .bookCall { margin-left: 0; } }
+
+section.section { background: #0B1C2A; }
diff --git a/web/components/home/flow-file-animation.module.css b/web/components/home/flow-file-animation.module.css
new file mode 100644
index 00000000..1c1711d1
--- /dev/null
+++ b/web/components/home/flow-file-animation.module.css
@@ -0,0 +1,19 @@
+.editor { min-width: 0; width: 100%; border: 1px solid #29475d; border-radius: 14px; background: #07131e; overflow: hidden; color: #d3e3ef; box-shadow: 0 24px 65px #020a1040; }
+.header { height: 48px; padding: 0 18px; display: flex; align-items: center; gap: 16px; border-bottom: 1px solid #243a4c; background: #102130; font: 12px monospace; }
+.header small { margin-left: auto; color: #7995ac; font-size: 10px; }
+.dots { display: flex; gap: 6px; }
+.dots i { width: 9px; height: 9px; border-radius: 50%; background: #ff5f57; }
+.dots i:nth-child(2) { background: #febc2e; }
+.dots i:nth-child(3) { background: #28c840; }
+.body { display: flex; gap: 14px; padding: 22px 16px; height: 482px; box-sizing: border-box; overflow-x: auto; }
+.numbers { display: flex; flex-direction: column; color: #435e74; text-align: right; user-select: none; }
+.body pre, .numbers { margin: 0; font: 11px/20px var(--font-geist-mono), monospace; }
+.body pre { tab-size: 2; }
+.keyword { color: #74b8e2; }
+.string { color: #cd866a; }
+.comment { color: #70899d; }
+.cursor { display: inline-block; height: 13px; width: 2px; vertical-align: -2px; background: #9fd4f5; animation: blink 1s steps(1) infinite; }
+.footer { border-top: 1px solid #243a4c; display: flex; justify-content: space-between; padding: 10px 18px; color: #7995ac; font: 10px monospace; }
+@keyframes blink { 50% { opacity: 0; } }
+@media (prefers-reduced-motion: reduce) { .cursor { animation: none; display: none; } }
+@media (max-width: 600px) { .body { gap: 9px; padding-inline: 10px; } .body pre, .numbers { font-size: 10px; } }
diff --git a/web/components/home/flow-gate-preview.module.css b/web/components/home/flow-gate-preview.module.css
new file mode 100644
index 00000000..d4614c38
--- /dev/null
+++ b/web/components/home/flow-gate-preview.module.css
@@ -0,0 +1,73 @@
+.panel { height: 100%; color: #dce9f4; font-family: var(--font-sans), sans-serif; }
+.content { height: 100%; display: flex; flex-direction: column; gap: clamp(10px, 1.5vw, 20px); padding: 4px 8px; }
+.nodes { display: flex; align-items: center; padding: 7px 9px 0; }
+.node { display: grid; justify-items: center; gap: 8px; min-width: 58px; }
+.node > span { display: grid; place-items: center; width: 40px; height: 40px; border: 1px solid; border-radius: 50%; transition: color .4s, background .4s, border-color .4s; }
+.node b { font-size: 11px; font-weight: 500; }
+.complete { color: #74b8e2; background: #173449; border-color: #427fa5 !important; }
+.active { color: #cd866a; background: #3b2926; border-color: #c1674b !important; }
+.locked { color: #8092a5; background: #162232; border-color: #354558 !important; }
+.connector { flex: 1; height: 2px; background: #354558; margin: -39px 0 0; position: relative; overflow: hidden; transition: background .6s; }
+.lit { background: #427fa5; }
+.lit::after { content: ''; position: absolute; inset: 0; width: 18px; background: #9fd4f5; animation: travel 1.6s ease-in-out infinite; }
+.agent { display: flex; align-items: center; gap: 10px; padding: 10px 0; border-bottom: 1px solid #2b4053; font-size: 12px; }
+.agent > span:first-child { font: 9px monospace; color: #8ea5b8; }
+.agent svg { margin-left: auto; color: #74b8e2; }
+.requirements { display: grid; gap: 4px; }
+.caption { font: 9px/1.5 monospace; letter-spacing: .08em; color: #8ea5b8; margin-bottom: 5px; }
+.row { display: flex; justify-content: space-between; align-items: center; gap: 8px; padding: 10px; border: 1px solid transparent; border-radius: 5px; transition: background .4s, border-color .4s; }
+.row code { font-size: 12px; }
+.row > span { display: flex; align-items: center; gap: 5px; font-size: 11px; white-space: nowrap; }
+.good { color: #74b8e2; }
+.pending { color: #cd866a; }
+.missing { background: #cd866a10; border-color: #cd866a38; }
+.status { display: flex; align-items: center; gap: 9px; margin-top: auto; padding: 10px 0 3px; color: #cd866a; font-size: 11px; animation: appear .3s ease-out; }
+.success { color: #74b8e2; }
+@keyframes travel { from { transform: translateX(-20px); } to { transform: translateX(150px); } }
+@keyframes appear { from { opacity: .3; } to { opacity: 1; } }
+@media (max-width: 600px) { .content { gap: 9px; padding: 0; } .row { padding: 7px; } .nodes { padding-top: 2px; } .agent { font-size: 11px; } }
+@media (prefers-reduced-motion: reduce) { .panel *, .panel *::after { animation: none; transition: none; } }
+
+.node small { font-size: 9px; line-height: 1.3; color: #a7bed0; white-space: nowrap; }
+
+/* Fixed tracks keep every stage in place throughout the animation. */
+.panel { width: 100%; min-width: 0; }
+.content { width: 100%; box-sizing: border-box; min-width: 0; gap: 14px; }
+.nodes { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); padding: 4px 0 0; flex: 0 0 82px; }
+.node { position: relative; min-width: 0; grid-template-rows: 40px 14px 12px; gap: 4px; align-content: start; }
+.node > span { position: relative; z-index: 1; box-sizing: border-box; }
+.node:not(:last-child)::after { content: ''; position: absolute; height: 2px; top: 19px; left: calc(50% + 20px); width: calc(100% - 40px); background: #354558; transition: background .4s; }
+.node.done::after { background: #427fa5; }
+.node small { font-size: 9px; }
+.agent { display: grid; grid-template-columns: 44px minmax(0, 1fr) 14px; box-sizing: border-box; height: 45px; flex: 0 0 45px; gap: 6px; padding: 5px 0; font-size: 11px; }
+.agent > span:nth-child(2) { min-width: 0; }
+.row { box-sizing: border-box; height: 37px; }
+.status { min-height: 32px; flex-shrink: 0; box-sizing: border-box; }
+.status svg { flex-shrink: 0; }
+@media (max-width: 600px) { .content { gap: 7px; } .nodes { flex-basis: 77px; } .node b { font-size: 10px; } .agent { font-size: 10px; height: 38px; flex-basis: 38px; } .row { height: 33px; } }
+
+.branchNodes { position: relative; width: 100%; height: 156px; flex: 0 0 156px; }
+.branchLines { position: absolute; inset: 0; width: 100%; height: 156px; overflow: visible; }
+.branchLines path { fill: none; stroke: #354558; stroke-width: 1.5; vector-effect: non-scaling-stroke; transition: stroke .4s; }
+.branchLines .pathDone { stroke: #427fa5; }
+.branchNode { position: absolute; width: 20%; transform: translateX(-50%); }
+.branchNode::after { display: none; }
+.content { gap: 8px; }
+@media (max-width: 600px) { .content { gap: 3px; } .branchNodes { margin-bottom: -8px; } .agent { height: 30px; flex-basis: 30px; } .row { height: 28px; padding-top: 3px; padding-bottom: 3px; } .status { padding-top: 3px; font-size: 10px; } }
+
+.branchNodes { height: 168px; flex-basis: 168px; width: calc(100% - 16px); }
+.branchLines { height: 168px; }
+.branchNode { width: 16%; grid-template-rows: 36px 14px 26px; gap: 3px; }
+.branchNode > span { width: 36px; height: 36px; }
+.branchNode b { font-size: 10px; }
+.branchNode small { white-space: normal; text-align: center; font-size: 8px; line-height: 1.3; width: 100%; }
+.agent { font-size: 10px; }
+@media (max-width: 600px) { .branchNode b { font-size: 9px; } .branchNode small { font-size: 7px; } .branchNode > span { width: 30px; height: 30px; margin-top: 3px; } }
+
+@media (max-width: 680px) {
+ .content { justify-content: center; }
+ .branchNodes { margin-bottom: 0; }
+ .agent,
+ .requirements,
+ .status { display: none; }
+}
diff --git a/web/components/investor-strip.module.css b/web/components/investor-strip.module.css
new file mode 100644
index 00000000..90606f4b
--- /dev/null
+++ b/web/components/investor-strip.module.css
@@ -0,0 +1,55 @@
+.backers {
+ width: min(calc(100% - 2rem), 1140px);
+ margin-inline: auto;
+ position: relative;
+ z-index: 2;
+ padding: var(--investor-strip-padding-top, 1.5rem) 0 3.5rem;
+ text-align: center;
+}
+
+.backers > p {
+ margin: 0 0 1.75rem;
+ color: #a8b8c8;
+ font-size: 0.75rem;
+ font-weight: 500;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+}
+
+.investorGroup {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ align-items: center;
+ gap: 1.75rem clamp(1.5rem, 4vw, 3.5rem);
+}
+
+.investorGroup > img { display: block; height: auto; object-fit: contain; }
+.investorHustle { width: 148px; }
+.investorActive { width: 140px; }
+.investorCortical { width: 155px; filter: grayscale(1) brightness(0) invert(1); }
+.investorYc,
+.investorYonder { display: inline-flex; align-items: center; gap: 0.5rem; white-space: nowrap; }
+.investorYc > span {
+ display: grid;
+ place-items: center;
+ width: 29px;
+ height: 29px;
+ background: #ff5a1f;
+ color: white;
+ font: 22px/1 Arial, sans-serif;
+}
+.investorYc strong { color: #ff936f; font: 17px/1 Arial, sans-serif; }
+.investorYonder { color: #6acf95; }
+.investorYonder svg { width: 25px; height: 25px; }
+.investorYonder strong { font-size: 1rem; font-weight: 720; letter-spacing: 0.015em; }
+
+@media (max-width: 680px) {
+ .backers { padding-bottom: 3rem; }
+ .investorGroup { gap: 1.5rem 1.25rem; }
+ .investorHustle { width: 125px; }
+ .investorActive { width: 122px; }
+ .investorCortical { width: 130px; }
+ .investorYc strong { font-size: 15px; }
+ .investorYonder strong { font-size: 0.9rem; }
+}
diff --git a/web/content/blog/promptgramming.mdx b/web/content/blog/promptgramming.mdx
new file mode 100644
index 00000000..4b293204
--- /dev/null
+++ b/web/content/blog/promptgramming.mdx
@@ -0,0 +1,159 @@
+---
+title: 'Promptgramming'
+description: 'Many have said this before but STOP BABYSITTING YOUR AGENTS and stop using glued together systems. Instead script your agents. Script your systems.'
+date: '2026-09-12'
+author: 'Khaliq Gant'
+category: 'Opinion'
+tags:
+ - Promptgramming
+ - Relayflows
+ - Agent Relay
+ - Workflows
+ - Multi-agent orchestration
+coverImage: '/blog-images/promptgramming-cover.png'
+coverImageAlt: 'A frustrated developer at a desk staring at a terminal chat agent'
+---
+
+I have been a developer for over 15 years. Back in my day we used to write the code by hand! Way before agents started putting their slimy hands all in the codebase!
+
+At this point most agree that the process of creating software has changed, but also as a result the entire industry is having to rethink first principles and reinvent a lot of age old practices. As the pendulum continues to swing and settle on how to best create software I'd like to try and push us all towards scripting out prompts or what I'd like to call promptgramming.
+
+
+
+This is a lot of us today, frantically prompting and steering agents as they keep telling us the fix is just one deploy away and that we're "right to push back on that". It's a frustratingly exhilarating roller coaster of an experience that very quickly makes for a nice looking demo. Anyone that has gone deep knows that to make a high quality product takes real work and iterations. Agents are confidently wrong and as my fellow AI Vampires know that when it gets to 1 or 2 am, the inclination to tell the agent yes to every question gets higher with every underslept minute.
+
+Let's take a few scenarios. One: you make an amazing plan and are feeling super hyped about it and three hours into the agent session it has deviated away from it so far that you even yourself forgot what the original intent was. Two: you're CTO of a nursing platform. The customer success team has three different LLM tools: one tags tickets, one drafts replies, and one handles voicemail callbacks. Between these tools are six humans copy-pasting text from one window to the next, with no verification but rather humans using a "system" that has been glued together.
+
+Many have said this before but STOP BABYSITTING YOUR AGENTS and stop using glued together systems. Instead script your agents. Script your systems. Produce prompts that are part of a deterministic script and gate the outcomes with deterministic checks. A skill is a suggestion, a recommendation and only that. My three year old happily takes a suggestion and tosses it right into the trash. A hard line, a gate is a mandatory requirement that if doesn't get met fails the entire operation. A plan that isn't enforced isn't a plan at all but just an outdated artifact causing future confusion for your agent. If you promptgram, you can not only add in deterministic checks but also add in different agents from different models and harnesses to verify and review the agents work. Those things you end up always telling your agent? Stop doing that and script it out and let your agent run in a script autonomously.
+
+So what does this look like? It is a workflow. Claude released [dynamic workflows](https://code.claude.com/docs/en/workflows) earlier this year which is a super powerful abstraction but I'm talking about writing workflows intentionally ahead of time with proper planning and even stack workflows to create rich applications.
+
+```ts
+import { flow } from "@relayflows/surface";
+
+export interface SocialPostInput {
+ topic: string;
+ brand: string;
+}
+
+export default flow(
+ "social-post-pipeline",
+ { budget: "$5/run" },
+ async (f, input) => {
+ const research = await f
+ .agent("researcher", {
+ cli: "claude",
+ model: "claude-sonnet-5
+ task:
+ `Research current, verifiable facts about "${input.topic}" for ` +
+ `${input.brand}. Cite a source for every claim. Write your findings ` +
+ `to research/notes.md.`,
+ permissions: { fileGlobs: ["research/**"], accessPreset: "readwrite" },
+ })
+ .gate(
+ (r) => r.artifacts.includes("research/notes.md"),
+ "the researcher must write research/notes.md, not just summarize in chat",
+ );
+
+ const draft = await f
+ .agent("writer", {
+ cli: "codex",
+ model: "gpt-6-astra",
+ task:
+ `Read research/notes.md and draft one social post for ${input.brand} ` +
+ `about "${input.topic}". Do not state anything the research does not ` +
+ `support. Write the draft to drafts/post.md.`,
+ permissions: { fileGlobs: ["drafts/**"], accessPreset: "readwrite" },
+ })
+ .gate(
+ (r) => r.artifacts.includes("drafts/post.md"),
+ "the writer must write drafts/post.md",
+ );
+ },
+);
+```
+
+The flow is multi-agent and also supports any model and harness. The gate is a runtime verification against the actual filesystem. Many other systems gate on the agent's own output, for instance a substring in the message, or a specific schema in its reply.
+
+Workflows are the underpinnings of a lot of our infrastructure. Imagine you're building an autonomous software factory. There are a sequence of steps that the agent or agents will go through to successfully pick up a ticket and satisfy the requirements. You could try and achieve this with a single agent and skills but why prompt and pray? Promptgram and profit, I say.
+
+```ts
+import { flow } from "@relayflows/surface";
+
+export interface TicketInput {
+ owner: string;
+ repo: string;
+ ticketId: string;
+ requirements: string;
+ baseBranch: string;
+}
+
+export default flow(
+ "software-factory",
+ { budget: "$8/run" },
+ async (f, input) => {
+ await f
+ .agent("planner", {
+ model: "claude-sonnet-5",
+ task:
+ `Decompose the ticket. Each step names the file it touches and ` +
+ `the test that proves it works. Write plan/plan.md.\n\n` +
+ `${input.requirements}`,
+ permissions: { fileGlobs: ["plan/**"], accessPreset: "readwrite" },
+ })
+ .gate((r) => r.artifacts.includes("plan/plan.md"), "no plan, no code");
+
+ await f
+ .agent("implementer", {
+ model: "claude-opus-5",
+ cli: "claude",
+ task:
+ `Follow plan/plan.md. Branch feat/${input.ticketId} off ` +
+ `${input.baseBranch}. Write code, tests, and a PR body in ` +
+ `impl/summary.md. Record the branch in impl/branch.txt.`,
+ permissions: {
+ fileGlobs: ["impl/**", "src/**", "tests/**"],
+ accessPreset: "readwrite",
+ },
+ })
+ .gate((r) => r.artifacts.includes("impl/branch.txt"), "no branch, no PR");
+
+ // The runtime runs the tests and checks the exit code. The agent cannot lie about it.
+ await f.run(
+ `branch=$(cat impl/branch.txt); ` +
+ `case "$branch" in feat/*) ;; *) exit 1 ;; esac; ` +
+ `git checkout "$branch" && npm test`,
+ );
+
+ // Different model. Its only job is to break the PR.
+ await f
+ .agent("adversary", {
+ model: "gpt-5.6-sol",
+ task:
+ `Try to sink branch $(cat impl/branch.txt). Find one concrete ` +
+ `failure the tests miss. If you can't after honest effort, ` +
+ `write adversary/clean.`,
+ permissions: { fileGlobs: ["adversary/**"], accessPreset: "readwrite" },
+ })
+ .gate((r) => r.artifacts.includes("adversary/clean"), "adversary found an issue");
+
+ // Deterministic step, not an agent decision. Journal-backed and idempotent.
+ const summary = await f.run("cat impl/summary.md");
+ await f.github.createPullRequest({
+ owner: input.owner,
+ repo: input.repo,
+ title: input.ticketId,
+ head: `feat/${input.ticketId}`,
+ base: input.baseBranch,
+ body: summary,
+ });
+
+ f.done("success");
+ },
+);
+```
+
+Sow are we using relayflows today? We have a relayflow that iterates over every single feature in our [cli relay surface](https://github.com/AgentWorkforce/relay/blob/main/workflows/verify-features.ts). It uses agent intelligence and deterministic checks to run a full end to end QA of every command and mapped feature. We built a [proactive Hacker news agent](https://github.com/AgentWorkforce/agents/blob/main/hn-monitor/workflows/scheduled-digest.ts) that fetches the latest from the site, filters by topics we're interested in and surfaces what we're interested in and posts a summary of those noteworthy posts to slack.
+Deterministic and consistent output with an agent doing its thing to do the hard part.
+
+If a task is well defined and can be decomposed into a series of well defined steps then you should be reaching for a relayflow. Hard constraints win over soft ones every time. Let's get back to programming, but with agents aka promptgramming. Get started: https://agentrelay.com/docs/relayflows/introduction
diff --git a/web/lib/og-meta.ts b/web/lib/og-meta.ts
index 9eff5293..5417552e 100644
--- a/web/lib/og-meta.ts
+++ b/web/lib/og-meta.ts
@@ -49,7 +49,8 @@ export function ogImage(path: string, alt: string): OgImage {
export const DEFAULT_OG_ALT = 'Agent Relay — Headless Slack for Agents';
/** Static public card paths. Change the version suffix when the visual changes to bust social caches. */
-export const HOME_OG_IMAGE_PATH = '/og/agent-relay-home-v20260831.png';
+export const HOME_OG_ALT = 'Agent Relay Flows — a TypeScript script that runs agents and checks their work';
+export const HOME_OG_IMAGE_PATH = '/og/agent-relay-flows-v20260913.png';
export const DEFAULT_OG_IMAGE_PATH = '/og/agent-relay-default-v20260610.png';
/** The site-wide default card. */
diff --git a/web/next.config.mjs b/web/next.config.mjs
index a39610ab..707a07ee 100644
--- a/web/next.config.mjs
+++ b/web/next.config.mjs
@@ -104,6 +104,12 @@ const nextConfig = {
},
async redirects() {
return [
+ // Mirror the router's calendar alias for direct Next.js and local dev requests.
+ {
+ source: '/khaliq',
+ destination: 'https://calendly.com/khaliq-agent-relay/30min',
+ permanent: false,
+ },
// In-person event banner QR code → homepage, tagged so the traffic is
// attributable to the physical banner at the current event. Temporary
// (not permanent) so /banner can be repointed at the next event without
diff --git a/web/public/blog-images/promptgramming-cover.png b/web/public/blog-images/promptgramming-cover.png
new file mode 100644
index 00000000..adcf1529
Binary files /dev/null and b/web/public/blog-images/promptgramming-cover.png differ
diff --git a/web/public/investors/active-capital.svg b/web/public/investors/active-capital.svg
new file mode 100644
index 00000000..6b74dafd
--- /dev/null
+++ b/web/public/investors/active-capital.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/web/public/investors/cortical-ventures.webp b/web/public/investors/cortical-ventures.webp
new file mode 100644
index 00000000..efd1b539
Binary files /dev/null and b/web/public/investors/cortical-ventures.webp differ
diff --git a/web/public/investors/hustle-fund.svg b/web/public/investors/hustle-fund.svg
new file mode 100644
index 00000000..ffc1b8d8
--- /dev/null
+++ b/web/public/investors/hustle-fund.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/public/og/agent-relay-flows-v20260913.png b/web/public/og/agent-relay-flows-v20260913.png
new file mode 100644
index 00000000..03110f51
Binary files /dev/null and b/web/public/og/agent-relay-flows-v20260913.png differ
diff --git a/web/scripts/generate-flows-og.mjs b/web/scripts/generate-flows-og.mjs
new file mode 100644
index 00000000..50b51685
--- /dev/null
+++ b/web/scripts/generate-flows-og.mjs
@@ -0,0 +1,85 @@
+// Run from web: node scripts/generate-flows-og.mjs
+// Local asset rendering only; no server or deployed preview is needed.
+import { chromium } from '@playwright/test';
+import { codeToHtml } from 'shiki';
+import { fileURLToPath } from 'node:url';
+import { createJiti } from 'jiti';
+import React from 'react';
+import { renderToStaticMarkup } from 'react-dom/server';
+
+const jiti = createJiti(import.meta.url, { jsx: { runtime: 'automatic' } });
+const { BrandLockup, SwoopLines, loadBrandFonts } = await jiti.import('../lib/og/template.tsx');
+const { fonts } = await loadBrandFonts();
+if (!fonts.some(font => font.name === 'Sora') || !fonts.some(font => font.name === 'Inter')) {
+ throw new Error('Brand fonts are required to preserve the original card design');
+}
+const fontCss = fonts.map(font => `@font-face { font-family: ${font.name}; font-weight: ${font.weight}; src: url(data:font/woff;base64,${Buffer.from(font.data).toString('base64')}); }`).join('\n');
+const brand = renderToStaticMarkup(React.createElement(BrandLockup, { scale: 1.4 }));
+const waves = renderToStaticMarkup(React.createElement(SwoopLines, { top: 420, opacity: 0.18 }));
+
+const script = `export default flow(
+ "software-factory",
+ { budget: "$8/run" },
+ async (f, input) => {
+ await f
+ .agent("planner", { task: "Write the plan" })
+ .gate(r => r.artifacts.includes("plan/plan.md"));
+
+ await f
+ .agent("implementer", { task: "Build the plan" });
+
+ // Tests run outside the agent.
+ await f.run("npm test")
+ .gate(out => /failed:\\s*0/.test(out));
+
+ await f
+ .agent("adversary", { task: "Review changes" })
+ .gate(r => r.artifacts.includes("adversary/clean"));
+
+ f.done("success");
+ },
+);`;
+
+const code = await codeToHtml(script, { lang: 'typescript', theme: 'github-dark' });
+const browser = await chromium.launch();
+try {
+ const page = await browser.newPage({ viewport: { width: 1200, height: 630 }, deviceScaleFactor: 1 });
+ await page.setContent(`${waves}
+ ${brand}
+ A Flow is a script that runs your agents.
+ Coordinate Claude, Codex, and other coding agents across shared work, approvals, and long-running workflows
+
+
+
+ TS software-factory.flow.ts ${code}
+
+ `);
+ await page.evaluate(() => document.fonts.ready);
+ const fits = await page.evaluate(() => {
+ const code = document.querySelector('pre');
+ const panel = document.querySelector('section');
+ return code.scrollWidth <= panel.clientWidth && code.getBoundingClientRect().bottom <= panel.getBoundingClientRect().bottom;
+ });
+ if (!fits) throw new Error('Script overflows the OG image');
+ await page.screenshot({ path: fileURLToPath(new URL('../public/og/agent-relay-flows-v20260913.png', import.meta.url)) });
+} finally {
+ await browser.close();
+}