diff --git a/.gitignore b/.gitignore index 40dee73..28fa614 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,6 @@ web/waitlist.json # OS .DS_Store + +# Palate website-builder skill telemetry, written at the repo root during agent sessions +build-manifest.json diff --git a/web/app/DurableDeliveryTimeline.tsx b/web/app/DurableDeliveryTimeline.tsx index 1a95046..02527de 100644 --- a/web/app/DurableDeliveryTimeline.tsx +++ b/web/app/DurableDeliveryTimeline.tsx @@ -1,6 +1,7 @@ 'use client'; import { useEffect, useState } from 'react'; +import { Bot, Check, CircleAlert, Database, FileOutput, RotateCcw, UserRoundCheck, Webhook } from 'lucide-react'; import { AgentToolLogo, type AgentTool } from '../components/AgentToolLogos'; import s from './landing.module.css'; @@ -170,3 +171,101 @@ export function DurableDeliveryTimeline() { ); } + +type WorkflowTraceItem = { + detail: string; + kind: 'checkpoint' | 'complete' | 'human' | 'model' | 'output' | 'retry' | 'trigger'; + title: string; +}; + +const WORKFLOW_TRACE: readonly WorkflowTraceItem[] = [ + { kind: 'trigger', title: 'repo.push received', detail: 'event · us-west' }, + { kind: 'checkpoint', title: 'Input persisted', detail: 'checkpoint_01' }, + { kind: 'model', title: 'Claude Code', detail: 'wrote release-plan.md' }, + { kind: 'human', title: 'Approval recorded', detail: 'artifact accepted' }, + { kind: 'retry', title: 'Deploy step', detail: 'attempt 2 · idempotent' }, + { kind: 'output', title: 'Output persisted', detail: 'build_418' }, + { kind: 'complete', title: 'Run completed', detail: '6 transitions · 1 retry' }, +]; + +const WORKFLOW_STEP_DELAYS_MS = [1100, 1250, 1950, 1550, 2100, 1250, 3200] as const; + +function WorkflowTraceIcon({ kind }: { kind: WorkflowTraceItem['kind'] }) { + const iconProps = { 'aria-hidden': true, size: 15 } as const; + + switch (kind) { + case 'trigger': + return ; + case 'checkpoint': + return ; + case 'model': + return ; + case 'human': + return ; + case 'retry': + return ; + case 'output': + return ; + case 'complete': + return ; + } +} + +export function DurableWorkflowTrace() { + const [activeStep, setActiveStep] = useState(0); + + useEffect(() => { + const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + if (reducedMotion) { + setActiveStep(WORKFLOW_TRACE.length - 1); + return undefined; + } + + const timeoutId = window.setTimeout(() => { + setActiveStep((current) => (current + 1) % WORKFLOW_TRACE.length); + }, WORKFLOW_STEP_DELAYS_MS[activeStep]); + + return () => window.clearTimeout(timeoutId); + }, [activeStep]); + + return ( +
+
+ run_01J7 + {activeStep === WORKFLOW_TRACE.length - 1 ? 'completed' : 'running'} +
+ +
+ {WORKFLOW_TRACE.map((item, index) => { + const state = index < activeStep ? 'complete' : index === activeStep ? 'active' : 'pending'; + const kindClass = item.kind === 'retry' + ? s.workflowTraceKindRetry + : item.kind === 'human' + ? s.workflowTraceKindHuman + : ''; + return ( +
+ + + + + {item.title} + {item.detail} + + + {state === 'complete' ? +
+ ); + })} +
+
+ ); +} diff --git a/web/app/globals.css b/web/app/globals.css index 5411fa8..e49a309 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -32,20 +32,60 @@ a { color: inherit; } +/* ── Focus ── + One rule for the whole site. The 2px offset puts the ring on the page + ground rather than on the control, so it is measured against the + background: >= 12:1 on every surface the site uses. */ +:focus-visible { + outline: 3px solid var(--focus-ring); + outline-offset: 2px; +} + +/* Keyboard-only skip target, revealed on focus. */ +.skip-link { + position: absolute; + top: 10px; + left: 10px; + z-index: 200; + padding: 12px 20px; + min-height: 44px; + display: inline-flex; + align-items: center; + background: var(--fg); + color: var(--bg); + font-family: var(--font-geist-sans), sans-serif; + font-size: 0.9375rem; + font-weight: 600; + text-decoration: none; + border-radius: var(--r-md); + transform: translateY(-200%); + transition: transform 0.18s var(--ease-out-quint); +} + +.skip-link:focus-visible { + transform: none; +} + /* ── Buttons ── Canonical button design, shared across the site. Landing-page CSS modules pull these in with `composes: btn btn-primary from global;` - so the primary button looks identical everywhere. */ + so the primary button looks identical everywhere. + + Shape comes from --r-md: the tail-corner message radius that every + interactive element on the site shares. min-height keeps every button a + comfortable target on touch (>= 44px) without per-page overrides. */ .btn { display: inline-flex; align-items: center; justify-content: center; - gap: 6px; - font-size: 0.92rem; + gap: 8px; + font-family: var(--font-geist-sans), sans-serif; + font-size: 0.9375rem; font-weight: 600; - letter-spacing: 0.01em; - padding: 12px 28px; - border-radius: 100px; + letter-spacing: -0.005em; + padding: 13px 26px; + min-height: 48px; + border-radius: var(--r-md); text-decoration: none; cursor: pointer; transition: diff --git a/web/app/landing.module.css b/web/app/landing.module.css index 30f48b4..7bc08f7 100644 --- a/web/app/landing.module.css +++ b/web/app/landing.module.css @@ -1943,7 +1943,11 @@ background: var(--agent-tools-band-bg); } -.messagingPage > :not(:first-child) { +/* Lift the page content above the hero wash. Targeted at the two content + siblings by name so the sticky nav (and the keyboard skip link that now + precedes it) keep their own positioning. */ +.messagingPage > main, +.messagingPage > footer { position: relative; z-index: 1; } @@ -1981,23 +1985,6 @@ z-index: 2; } -/* Hero communication graph and contour background */ -.heroBgSvg { - position: absolute; - inset: 0; - width: 100%; - height: 100%; - z-index: 0; - pointer-events: none; - opacity: 0.96; - filter: saturate(1.1) contrast(1.04); -} - -:global(html[data-theme='dark']) .heroBgSvg { - opacity: 0.88; - filter: none; -} - .hero { display: grid; grid-template-columns: 1fr 1.2fr; @@ -2017,235 +2004,1440 @@ padding-top: 56px; } -.heroCopyGroup { - display: grid; - gap: 10px; - max-width: 610px; -} - .heroRight { position: relative; align-self: stretch; min-height: 580px; } -.badge { - display: inline-flex; +/* ── Home hero: one centred column over the terminal marquee ───────────── + The product pages keep the two-column `.hero` above; the home page runs a + single centred column so the marquee band underneath gets the full width. */ +.heroCenter { + display: flex; + justify-content: center; + width: 100%; + padding: 72px 40px 0; +} + +.heroCenterColumn { + display: flex; + flex-direction: column; align-items: center; - gap: 8px; - background: var(--bg-elevated); - border: 1px solid var(--line); - padding: 6px 16px; - border-radius: 100px; - font-size: 0.75rem; - font-weight: 600; - letter-spacing: 0.06em; - color: var(--primary); - width: fit-content; + gap: 22px; + width: 100%; + max-width: 880px; + text-align: center; } -.badgeDot { - width: 7px; - height: 7px; - border-radius: 50%; - background: var(--primary); - box-shadow: 0 0 6px color-mix(in srgb, var(--primary) 50%, transparent); - animation: pulse 2s ease-in-out infinite; +/* One step down from the product pages' `.headline`: this line is five words, + not three, and at the shared scale it filled the fold on its own and pushed + the marquee off the bottom of the screen. The column prefix is load-bearing — + `.headline` and `.subtitle` are declared further down this file, so a bare + class here would lose the cascade to them. */ +.heroCenterColumn .heroCenterHeadline { + font-size: clamp(2.6rem, 5.4vw, 4.5rem); + max-width: 20ch; + text-wrap: balance; } -@keyframes pulse { - 0%, - 100% { - opacity: 1; +.heroCenterColumn .heroCenterSubtitle { + max-width: 52ch; + margin: -4px auto 0; + text-wrap: pretty; +} + +.heroCenterCtas { + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + gap: 14px; + margin-top: 2px; +} + +/* ── Terminal marquee ──────────────────────────────────────────────────── + Three full-bleed rows of agent sessions sliding past. Each track holds two + identical copies of its cards and slides exactly one copy's width, so the + loop is seamless; the spacing lives in each card's margin-right rather than + a flex gap so that -50% lands on a card boundary rather than half a gap + short. Rows differ in duration, direction and negative delay (a phase + offset) so the card seams never line up across the band. The rows also hold + different numbers of cards, so their tracks are different lengths and the + seams drift instead of beating in step. */ +.heroMarquee { + position: relative; + isolation: isolate; + display: flex; + flex-direction: column; + gap: 14px; + width: 100%; + margin-top: 68px; + padding-bottom: 26px; + /* clip, not hidden: the band never adds page scroll, and the cards' shadows + still breathe above and below the row. */ + overflow-x: clip; + overflow-y: visible; +} + +.heroRelayNetwork { + position: absolute; + inset: 0 0 26px; + z-index: 0; + width: 100%; + height: calc(100% - 26px); + overflow: visible; + pointer-events: none; +} + +.heroRelayRoute { + fill: none; + stroke: none; +} + +.heroRelayPacket, +.heroRelayOrigin, +.heroRelayArrival { + display: none; +} + +.heroRelayPacketTail { + fill: none; + stroke: rgba(125, 194, 233, 0.82); + stroke-linecap: round; + stroke-width: 2.1; + vector-effect: non-scaling-stroke; +} + +.heroRelayPacketHalo, +.heroRelayOrigin, +.heroRelayArrival { + fill: none; + stroke: rgba(125, 194, 233, 0.58); + stroke-width: 1.4; + vector-effect: non-scaling-stroke; +} + +.heroRelayPacketCore { + fill: rgba(166, 219, 247, 0.96); +} + +.heroMarqueeRow { + position: relative; + z-index: 1; + width: 100%; + overflow-x: clip; + overflow-y: visible; +} + +.heroMarqueeTrack { + display: flex; + align-items: stretch; + width: max-content; +} + +@media (prefers-reduced-motion: no-preference) { + .heroRelayPacket, + .heroRelayOrigin, + .heroRelayArrival { + display: inline; } - 50% { - opacity: 0.4; + + .heroMarqueeTrack { + will-change: transform; + animation: heroMarqueeSlide linear infinite; + } + + .heroMarqueeTrackOne { + animation-duration: 74s; + animation-delay: -11s; + } + + .heroMarqueeTrackTwo { + animation-duration: 88s; + animation-delay: -37s; + animation-direction: reverse; + } + + .heroMarqueeTrackThree { + animation-duration: 66s; + animation-delay: -23s; } } -.headline { - margin: 0; - font-family: var(--font-heading), sans-serif; - font-size: clamp(3.2rem, 7vw, 5.5rem); - font-weight: 500; - line-height: 1.05; - letter-spacing: -0.03em; - color: var(--fg); - opacity: 0.85; +@keyframes heroMarqueeSlide { + from { + transform: translate3d(0, 0, 0); + } + to { + transform: translate3d(-50%, 0, 0); + } } -:global(html[data-theme='dark']) .headline { - opacity: 1; +/* The second and third rows start part-way along so the seams stagger even + when motion is off. */ +.heroMarqueeTrackTwo { + margin-left: -132px; } -.subtitle { - margin: 0; - font-size: 1.1rem; - line-height: 1.65; - color: var(--fg-muted); - max-width: 48ch; - font-weight: 400; +.heroMarqueeTrackThree { + margin-left: -258px; } -.ctas { +.heroTerm { + --term-accent: #74b8e2; display: flex; - align-items: center; - gap: 16px; - padding-top: 4px; + flex-direction: column; + flex: none; + margin-right: 14px; + min-height: 158px; + border: 1px solid rgba(116, 184, 226, 0.16); + border-radius: 10px; + background: rgba(8, 22, 36, 0.9); + box-shadow: 0 6px 18px rgba(2, 10, 20, 0.28); + overflow: hidden; } -.heroCommandGroup { - display: grid; - gap: 10px; - width: min(100%, 610px); - margin-top: 0; +.heroTermSm { + width: 268px; } -.heroCommandIntro { - color: rgba(184, 211, 232, 0.62); - font-size: 0.78rem; - font-weight: 500; - letter-spacing: 0; - line-height: 1.25; - max-width: 56ch; +.heroTermMd { + width: 312px; } -.heroCommandCta { - appearance: none; - position: relative; +.heroTermLg { + width: 352px; +} + +.heroTermBar { display: flex; align-items: center; - gap: 12px; - width: 100%; - margin-top: 0; - min-height: 54px; - padding: 10px 12px 10px 16px; - border: 1px solid rgba(116, 184, 226, 0.2); - border-radius: 10px; - background: - linear-gradient(180deg, rgba(116, 184, 226, 0.08), rgba(116, 184, 226, 0.02)), - rgba(4, 18, 31, 0.76); - box-shadow: - inset 0 1px 0 rgba(237, 247, 255, 0.06), - inset 0 -1px 0 rgba(116, 184, 226, 0.04); - -webkit-backdrop-filter: blur(12px) saturate(125%); - backdrop-filter: blur(12px) saturate(125%); - color: inherit; - cursor: pointer; - font: inherit; - text-align: left; - transition: - border-color 0.2s, - background 0.2s; + gap: 8px; + padding: 6px 10px; + background: rgba(15, 33, 48, 0.94); + border-bottom: 1px solid rgba(116, 184, 226, 0.14); } -.heroCommandCta::before { - content: none; +.heroTermTraffic { + display: inline-flex; + flex: none; + align-items: center; + gap: 4px; } -.heroCommandPrompt, -.heroCommandText, -.heroCommandCopyButton { - position: relative; - z-index: 1; +.heroTermTraffic span { + width: 7px; + height: 7px; + border-radius: 999px; } -.heroCommandPrompt { - color: rgba(184, 211, 232, 0.62); - font-family: var(--font-geist-mono), monospace; - font-size: 1rem; - line-height: 1; - flex-shrink: 0; +.heroTermTraffic span:nth-child(1) { + background: #ff5f57; } -.heroCommandText { - min-width: 0; - flex: 1; - overflow-x: auto; - scrollbar-width: none; - color: rgba(244, 250, 255, 0.94); - font-family: var(--font-geist-mono), monospace; - font-size: clamp(0.84rem, 1.5vw, 1rem); - font-weight: 520; - letter-spacing: 0; - line-height: 1.45; - white-space: nowrap; +.heroTermTraffic span:nth-child(2) { + background: #febc2e; } -.heroCommandText::-webkit-scrollbar { - display: none; +.heroTermTraffic span:nth-child(3) { + background: #28c840; } -.heroCommandCopyButton { +.heroTermTitle { display: inline-flex; + flex: 1; align-items: center; - justify-content: center; - width: 38px; - height: 38px; + gap: 6px; + min-width: 0; + font-family: var(--font-geist-mono), monospace; + font-size: 0.68rem; + font-weight: 500; + letter-spacing: 0.01em; + color: rgba(206, 227, 243, 0.82); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.heroTermMark { + width: 12px; + height: 12px; flex-shrink: 0; - border: 1px solid rgba(116, 184, 226, 0.28); - border-radius: 8px; - background: rgba(9, 24, 40, 0.54); - color: rgba(222, 240, 255, 0.78); - cursor: inherit; - opacity: 0; - pointer-events: none; - transition: - opacity 0.2s, - color 0.2s, - border-color 0.2s, - background 0.2s; + color: rgba(219, 233, 244, 0.9); } -.heroCommandCopyButton svg { - width: 17px; - height: 17px; - stroke-width: 1.8; +.heroTermBody { + display: flex; + flex-direction: column; + min-height: 128px; + padding: 9px 12px 8px; } -.heroCommandCopied { - position: absolute; - width: 1px; - height: 1px; +.heroTermSession { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 1px 8px; + min-width: 0; + margin-bottom: 7px; + font-family: var(--font-geist-mono), monospace; +} + +.heroTermSessionName { + overflow: hidden; + color: rgba(218, 232, 244, 0.9); + font-size: 0.62rem; + font-weight: 650; + text-overflow: ellipsis; + white-space: nowrap; +} + +.heroTermMode { + color: rgba(160, 184, 204, 0.72); + font-size: 0.56rem; + white-space: nowrap; +} + +.heroTermRepo { + grid-column: 1 / -1; + min-width: 0; overflow: hidden; - clip: rect(0 0 0 0); + color: rgba(147, 171, 193, 0.7); + font-size: 0.57rem; + text-overflow: ellipsis; white-space: nowrap; } -.heroCommandCta:hover .heroCommandCopyButton, -.heroCommandCta:focus-visible .heroCommandCopyButton { - border-color: rgba(116, 184, 226, 0.28); - background: rgba(9, 24, 40, 0.58); - color: rgba(222, 240, 255, 0.76); +.heroTermTranscript { + display: flex; + flex-direction: column; + gap: 2px; + min-height: 52px; +} + +.heroTermLine { + position: relative; + display: flex; + align-items: baseline; + gap: 6px; + min-width: 0; + font-family: var(--font-geist-mono), monospace; + font-size: 0.64rem; + line-height: 1.35; + letter-spacing: 0; + white-space: nowrap; + overflow: hidden; +} + +.heroTermLineMarker { + width: 9px; + flex: none; + color: currentColor; + font-weight: 700; + text-align: center; +} + +.heroTermLineCopy { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; +} + +.heroTermPrompt { + margin-inline: -4px; + padding: 2px 4px; + color: rgba(241, 247, 252, 0.96); + background: rgba(226, 237, 246, 0.1); +} + +@media (prefers-reduced-motion: no-preference) { + .heroTermLine { + animation: heroTerminalLineCycle var(--term-cycle) cubic-bezier(0.16, 1, 0.3, 1) infinite both; + animation-delay: calc(var(--term-phase) + var(--line-delay)); + will-change: clip-path, opacity, transform; + } + + .heroTermActivityGlyph { + animation: heroTerminalActivity 1.7s ease-in-out infinite; + } + + .heroTermCursor { + animation: heroTerminalCursorBlink 920ms steps(2, start) infinite; + } +} + +@keyframes heroTerminalLineCycle { + 0%, + 7% { + opacity: 0; + clip-path: inset(0 100% 0 0); + transform: translateY(3px); + } + 19%, + 76% { + opacity: 1; + clip-path: inset(0 0 0 0); + transform: translateY(0); + } + 91%, + 100% { + opacity: 0; + clip-path: inset(0 0 0 0); + transform: translateY(-2px); + } +} + +@keyframes heroTerminalActivity { + 0%, + 100% { + opacity: 0.48; + transform: rotate(0deg) scale(0.86); + } + 50% { + opacity: 1; + transform: rotate(180deg) scale(1); + } +} + +@keyframes heroTerminalCursorBlink { + 0%, + 48% { + opacity: 1; + } + 49%, + 100% { + opacity: 0; + } +} + +.heroTermTool { + color: rgba(160, 184, 204, 0.82); +} + +.heroTermOk { + color: #7fd3a0; +} + +.heroTermRelay { + color: #85c6ec; +} + +.heroTermComposer { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + margin-top: auto; + padding-top: 5px; + border-top: 1px solid rgba(116, 184, 226, 0.12); +} + +.heroTermLivePrompt { + display: inline-flex; + align-items: center; + gap: 5px; + height: 10px; + color: var(--term-accent); + font-family: var(--font-geist-mono), monospace; + font-size: 0.65rem; + line-height: 1; +} + +.heroTermState { + display: inline-flex; + flex: none; + align-items: center; + gap: 4px; + color: rgba(183, 207, 226, 0.72); + font-family: var(--font-geist-mono), monospace; + font-size: 0.55rem; + font-weight: 600; + white-space: nowrap; +} + +.heroTermActivityGlyph { + display: inline-block; + min-width: 9px; + color: var(--term-accent); + transform-origin: center; + text-align: center; +} + +.heroGrokBody { + position: relative; + gap: 0; + padding-top: 8px; +} + +.heroGrokBody::before { + position: absolute; + top: 0; + right: 0; + left: 0; + height: 1px; + background: rgba(116, 184, 226, 0.72); + content: ""; + transform: scaleX(0.38); + transform-origin: left; +} + +.heroGrokContext { + display: flex; + align-items: center; + gap: 5px; + min-width: 0; + margin-bottom: 6px; + color: rgba(145, 162, 177, 0.66); + font-family: var(--font-geist-mono), monospace; + font-size: 0.56rem; +} + +.heroGrokBranch { + flex: none; + font-size: 0.68rem; +} + +.heroGrokPath { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.heroGrokContextMode { + flex: none; + margin-left: auto; + color: rgba(193, 210, 223, 0.74); +} + +.heroGrokPrompt { + display: flex; + align-items: baseline; + gap: 7px; + min-width: 0; + margin-bottom: 5px; + padding: 4px 6px; + color: rgba(239, 244, 247, 0.92); + background: rgba(226, 237, 246, 0.1); + font-family: var(--font-geist-mono), monospace; + font-size: 0.63rem; + white-space: nowrap; + overflow: hidden; +} + +.heroGrokPrompt span:last-child { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; +} + +.heroGrokEvents { + display: flex; + flex-direction: column; + gap: 1px; + min-height: 43px; + padding-inline: 7px; + color: rgba(147, 166, 181, 0.7); + font-family: var(--font-geist-mono), monospace; + font-size: 0.56rem; + line-height: 1.25; + white-space: nowrap; + overflow: hidden; +} + +.heroGrokReply { + margin-top: 2px; + overflow: hidden; + color: rgba(217, 228, 236, 0.84); + text-overflow: ellipsis; +} + +.heroGrokFooter { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; + margin-top: auto; + margin-bottom: 4px; + color: rgba(187, 203, 214, 0.78); + font-family: var(--font-geist-mono), monospace; + font-size: 0.54rem; + white-space: nowrap; +} + +.heroGrokProgress { + display: inline-flex; + flex: none; + align-items: center; + gap: 4px; +} + +.heroGrokProgressGlyph { + display: inline-block; + color: rgba(216, 229, 238, 0.9); +} + +.heroGrokResult { + min-width: 0; + overflow: hidden; + color: rgba(133, 198, 236, 0.86); + text-overflow: ellipsis; +} + +.heroGrokComposer { + display: flex; + align-items: center; + gap: 5px; + min-height: 18px; + padding: 3px 5px; + border: 1px solid rgba(180, 199, 214, 0.24); + border-radius: 3px; + color: rgba(228, 238, 245, 0.88); + font-family: var(--font-geist-mono), monospace; + font-size: 0.55rem; +} + +.heroGrokModel { + margin-left: auto; + color: rgba(157, 174, 188, 0.68); + white-space: nowrap; +} + +@media (prefers-reduced-motion: no-preference) { + .heroGrokBody::before { + animation: heroGrokProgressRail var(--term-cycle) ease-in-out infinite both; + animation-delay: var(--term-phase); + } + + .heroGrokCycleLine { + animation: heroTerminalLineCycle var(--term-cycle) cubic-bezier(0.16, 1, 0.3, 1) infinite both; + animation-delay: calc(var(--term-phase) + var(--line-delay)); + will-change: clip-path, opacity, transform; + } + + .heroGrokProgressGlyph { + animation: heroTerminalActivity 1.1s ease-in-out infinite; + } +} + +@keyframes heroGrokProgressRail { + 0%, + 100% { + opacity: 0.36; + transform: scaleX(0.12); + } + 58% { + opacity: 0.92; + transform: scaleX(0.78); + } +} + +.heroOpenCodeBody { + gap: 0; + padding-top: 8px; +} + +.heroOpenCodePrompt { + min-width: 0; + margin-bottom: 7px; + padding: 5px 7px; + border-left: 2px solid rgba(89, 166, 238, 0.92); + background: rgba(226, 237, 246, 0.08); + color: rgba(237, 244, 249, 0.94); + font-family: var(--font-geist-mono), monospace; + font-size: 0.63rem; + line-height: 1.3; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +.heroOpenCodeActivity { + display: flex; + flex-direction: column; + gap: 2px; + min-height: 42px; + padding-inline: 8px; + color: rgba(155, 176, 192, 0.76); + font-family: var(--font-geist-mono), monospace; + font-size: 0.56rem; + line-height: 1.25; + white-space: nowrap; + overflow: hidden; +} + +.heroOpenCodeThought { + color: rgba(214, 164, 93, 0.92); +} + +.heroOpenCodeResult { + overflow: hidden; + color: rgba(220, 231, 239, 0.86); + text-overflow: ellipsis; +} + +.heroOpenCodeMode { + display: flex; + align-items: center; + gap: 6px; + margin: 1px 8px 0; + color: rgba(148, 169, 185, 0.68); + font-family: var(--font-geist-mono), monospace; + font-size: 0.55rem; +} + +.heroOpenCodeMode > span:first-child, +.heroOpenCodeMode strong { + color: rgba(89, 166, 238, 0.9); +} + +.heroOpenCodeMode strong { + font-weight: 650; +} + +.heroOpenCodeComposer { + display: flex; + align-items: center; + gap: 6px; + min-height: 22px; + margin-top: auto; + padding: 4px 6px; + border-left: 2px solid rgba(89, 166, 238, 0.92); + background: rgba(226, 237, 246, 0.08); + color: rgba(151, 171, 187, 0.66); + font-family: var(--font-geist-mono), monospace; + font-size: 0.54rem; + white-space: nowrap; +} + +.heroOpenCodeComposerMode { + margin-left: auto; + color: rgba(89, 166, 238, 0.94); +} + +.heroOpenCodeLoaderRow { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; + min-height: 14px; + color: rgba(169, 186, 199, 0.72); + font-family: var(--font-geist-mono), monospace; + font-size: 0.51rem; + white-space: nowrap; +} + +.heroOpenCodeLoader { + display: inline-flex; + flex: none; + align-items: center; + gap: 4px; + height: 9px; +} + +.heroOpenCodeLoaderDot { + display: inline-block; + flex: none; + width: 5px; + height: 5px; + background: rgba(89, 166, 238, 0.82); + transform-origin: center; +} + +.heroOpenCodeShortcut { + min-width: 0; + margin-left: auto; + overflow: hidden; + text-overflow: ellipsis; +} + +@media (prefers-reduced-motion: no-preference) { + .heroOpenCodeCycleLine { + animation: heroTerminalLineCycle var(--term-cycle) cubic-bezier(0.16, 1, 0.3, 1) infinite both; + animation-delay: calc(var(--term-phase) + var(--line-delay)); + will-change: clip-path, opacity, transform; + } + + .heroClaudeAddedLine { + animation-duration: var(--term-cycle); + animation-timing-function: step-end; + animation-delay: var(--term-phase); + animation-iteration-count: infinite; + animation-fill-mode: both; + } + + .heroClaudeBody .heroClaudeAddedLine:nth-child(1) { + animation-name: heroTerminalLineAddOne; + } + + .heroClaudeBody .heroClaudeAddedLine:nth-child(2) { + animation-name: heroTerminalLineAddTwo; + } + + .heroClaudeBody .heroClaudeAddedLine:nth-child(3) { + animation-name: heroTerminalLineAddThree; + } + + .heroOpenCodeLoaderDot { + animation: heroOpenCodeLoaderPulse 1.28s ease-in-out infinite both; + } + + .heroOpenCodeLoaderDot:nth-child(2) { + animation-delay: 100ms; + } + + .heroOpenCodeLoaderDot:nth-child(3) { + animation-delay: 200ms; + } + + .heroOpenCodeLoaderDot:nth-child(4) { + animation-delay: 300ms; + } + + .heroOpenCodeLoaderDot:nth-child(5) { + animation-delay: 400ms; + } + + .heroOpenCodeLoaderDot:nth-child(6) { + animation-delay: 500ms; + } + + .heroOpenCodeLoaderDot:nth-child(7) { + animation-delay: 600ms; + } + + .heroOpenCodeLoaderDot:nth-child(8) { + animation-delay: 700ms; + } +} + +@keyframes heroTerminalLineAddOne { + 0%, + 18% { + visibility: hidden; + clip-path: inset(0 100% 0 0); + } + 19%, + 100% { + visibility: visible; + clip-path: inset(0 0 0 0); + } +} + +@keyframes heroTerminalLineAddTwo { + 0%, + 39% { + visibility: hidden; + clip-path: inset(0 100% 0 0); + } + 40%, + 100% { + visibility: visible; + clip-path: inset(0 0 0 0); + } +} + +@keyframes heroTerminalLineAddThree { + 0%, + 61% { + visibility: hidden; + clip-path: inset(0 100% 0 0); + } + 62%, + 100% { + visibility: visible; + clip-path: inset(0 0 0 0); + } +} + +@keyframes heroOpenCodeLoaderPulse { + 0%, + 100% { + opacity: 0.3; + transform: scale(0.68); + } + 45% { + opacity: 1; + transform: scale(1.28); + } +} + +.heroClaudeBody { + gap: 0; + padding-top: 7px; +} + +.heroClaudeScrollViewport { + position: relative; + height: 78px; + flex: none; + overflow: hidden; +} + +.heroClaudeScrollTrack, +.heroClaudeScrollSequence { + display: flex; + flex-direction: column; +} + +.heroClaudeScrollSequence { + flex: none; + min-height: 102px; + padding-bottom: 8px; +} + +.heroClaudeIdentity { + display: grid; + grid-template-columns: 50px minmax(0, 1fr); + align-items: center; + gap: 8px; + min-width: 0; + min-height: 40px; + margin-bottom: 6px; +} + +.heroClaudeMascot { + position: relative; + display: block; + width: 48px; + height: 36px; + justify-self: center; + background: #d97855; + clip-path: polygon( + 12.5% 0, + 87.5% 0, + 87.5% 28%, + 100% 28%, + 100% 64%, + 87.5% 64%, + 87.5% 78%, + 75% 78%, + 75% 100%, + 62.5% 100%, + 62.5% 78%, + 50% 78%, + 50% 100%, + 37.5% 100%, + 37.5% 78%, + 25% 78%, + 25% 100%, + 12.5% 100%, + 12.5% 78%, + 0 78%, + 0 28%, + 12.5% 28% + ); +} + +.heroClaudeMascot::after { + position: absolute; + top: 9px; + left: 13px; + width: 4px; + height: 8px; + background: rgb(8 22 36); + box-shadow: 27px 0 0 rgb(8 22 36); + content: ''; +} + +.heroClaudeIdentityCopy { + display: flex; + flex-direction: column; + gap: 1px; + min-width: 0; + font-family: var(--font-geist-mono), monospace; + line-height: 1.18; +} + +.heroClaudeIdentityTitle { + overflow: hidden; + color: rgba(222, 230, 246, 0.94); + font-size: 0.58rem; + font-weight: 680; + letter-spacing: 0.02em; + text-overflow: ellipsis; + white-space: nowrap; +} + +.heroClaudeIdentityTitle span { + color: rgba(158, 169, 184, 0.7); + font-weight: 500; +} + +.heroClaudeModel, +.heroClaudePath { + min-width: 0; + overflow: hidden; + color: rgba(177, 189, 204, 0.76); + font-size: 0.49rem; + text-overflow: ellipsis; + white-space: nowrap; +} + +.heroClaudePath { + color: rgba(161, 177, 194, 0.68); +} + +.heroClaudeBody .heroTermTranscript { + min-height: 48px; +} + +.heroClaudeBody .heroTermLine { + font-size: 0.59rem; +} + +.heroCodexBody { + gap: 0; + padding-top: 7px; +} + +.heroCodexScrollViewport { + position: relative; + height: 80px; + flex: none; + margin-bottom: 3px; + overflow: hidden; +} + +.heroCodexScrollTrack, +.heroCodexScrollSequence { + display: flex; + flex-direction: column; +} + +.heroCodexScrollSequence { + flex: none; + min-height: 116px; + padding-bottom: 8px; +} + +.heroCodexIdentity { + display: flex; + flex-direction: column; + gap: 1px; + min-width: 0; + margin-bottom: 5px; + padding: 4px 6px; + border: 1px solid rgba(160, 184, 204, 0.28); + border-radius: 3px; + color: rgba(196, 215, 232, 0.84); + font-family: var(--font-geist-mono), monospace; + font-size: 0.5rem; + line-height: 1.18; +} + +.heroCodexIdentityTitle { + display: inline-flex; + align-items: center; + gap: 5px; + margin-bottom: 1px; + color: rgba(223, 235, 246, 0.94); + font-size: 0.58rem; + font-weight: 680; + letter-spacing: 0.02em; +} + +.heroCodexIdentityTitle span { + color: rgba(141, 170, 198, 0.76); +} + +.heroCodexIdentityRow { + display: flex; + align-items: baseline; + gap: 5px; + min-width: 0; + white-space: nowrap; +} + +.heroCodexIdentityRow b { + color: rgba(132, 202, 238, 0.92); + font-weight: 600; +} + +.heroCodexIdentityLabel { + width: 46px; + flex: none; + color: rgba(137, 159, 180, 0.68); +} + +.heroCodexIdentityPath { + min-width: 0; + overflow: hidden; + color: rgba(162, 208, 178, 0.86); + text-overflow: ellipsis; +} + +.heroCodexPrompt, +.heroCodexReply, +.heroCodexRun, +.heroCodexWorking, +.heroCodexComposer, +.heroCodexFooter { + font-family: var(--font-geist-mono), monospace; +} + +.heroCodexPrompt { + display: flex; + align-items: baseline; + gap: 6px; + min-width: 0; + margin-bottom: 4px; + padding: 4px 5px; + background: rgba(226, 237, 246, 0.11); + color: rgba(229, 238, 247, 0.94); + font-size: 0.58rem; + line-height: 1.25; + white-space: nowrap; + overflow: hidden; +} + +.heroCodexPrompt span:last-child { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; +} + +.heroCodexReply { + display: flex; + align-items: baseline; + gap: 6px; + min-width: 0; + margin-bottom: 3px; + color: rgba(199, 216, 232, 0.82); + font-size: 0.51rem; + line-height: 1.25; + white-space: nowrap; + overflow: hidden; +} + +.heroCodexReply span:last-child { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; +} + +.heroCodexRun { + display: grid; + grid-template-columns: 8px minmax(0, 1fr); + min-width: 0; + margin-bottom: 3px; + color: rgba(155, 179, 201, 0.75); + font-size: 0.5rem; + line-height: 1.22; +} + +.heroCodexRunMarker { + color: rgba(169, 218, 112, 0.92); +} + +.heroCodexRunCopy, +.heroCodexRunResult { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.heroCodexRunCopy strong { + color: rgba(215, 230, 241, 0.9); + font-weight: 650; +} + +.heroCodexRunResult { + grid-column: 2; + color: rgba(137, 162, 184, 0.68); +} + +.heroCodexWorking { + display: flex; + align-items: center; + gap: 5px; + margin-top: auto; + margin-bottom: 3px; + color: rgba(145, 168, 190, 0.66); + font-size: 0.49rem; + white-space: nowrap; +} + +.heroCodexScrollSequence .heroCodexWorking { + margin-top: 0; +} + +.heroCodexWorking strong { + color: rgba(193, 212, 229, 0.82); + font-weight: 650; +} + +.heroCodexWorkingMarker { + display: inline-block; + color: rgba(130, 185, 226, 0.9); +} + +.heroCodexComposer { + display: flex; + align-items: center; + gap: 5px; + min-width: 0; + min-height: 19px; + padding: 3px 5px; + background: rgba(226, 237, 246, 0.11); + color: rgba(220, 232, 242, 0.86); + font-size: 0.52rem; +} + +.heroCodexComposerHint { + min-width: 0; + overflow: hidden; + color: rgba(155, 177, 197, 0.66); + text-overflow: ellipsis; + white-space: nowrap; +} + +.heroCodexFooter { + display: flex; + align-items: center; + gap: 5px; + min-width: 0; + padding: 3px 5px 0; + color: rgba(181, 202, 219, 0.72); + font-size: 0.47rem; + line-height: 1; + white-space: nowrap; +} + +.heroCodexFooter strong { + color: rgba(132, 202, 238, 0.92); + font-weight: 600; +} + +.heroCodexFooterPath { + min-width: 0; + margin-left: auto; + overflow: hidden; + color: rgba(162, 208, 178, 0.82); + text-overflow: ellipsis; +} + +@media (prefers-reduced-motion: no-preference) { + .heroCodexScrollTrack { + animation: heroCodexTranscriptSteps var(--term-cycle) step-end infinite both; + animation-delay: var(--term-phase); + will-change: transform; + } + + .heroCodexAddedLine { + animation-duration: var(--term-cycle); + animation-timing-function: step-end; + animation-delay: var(--term-phase); + animation-iteration-count: infinite; + animation-fill-mode: both; + } + + .heroCodexPrompt.heroCodexAddedLine { + animation-name: heroCodexLineAddPrompt; + } + + .heroCodexReply.heroCodexAddedLine { + animation-name: heroCodexLineAddReply; + } + + .heroCodexRun.heroCodexAddedLine { + animation-name: heroCodexLineAddRun; + } + + .heroCodexWorking.heroCodexAddedLine { + animation-name: heroCodexLineAddWorking; + } + + .heroClaudeScrollTrack { + animation: heroClaudeTranscriptSteps var(--term-cycle) step-end infinite both; + animation-delay: var(--term-phase); + will-change: transform; + } + + .heroCodexWorkingMarker { + animation: heroCodexWorkingPulse 1.25s ease-in-out infinite; + } +} + +@keyframes heroCodexTranscriptSteps { + 0%, + 37% { + transform: translate3d(0, 0, 0); + } + 38%, + 57% { + transform: translate3d(0, -12px, 0); + } + 58%, + 77% { + transform: translate3d(0, -34px, 0); + } + 78%, + 100% { + transform: translate3d(0, -48px, 0); + } +} + +@keyframes heroCodexLineAddPrompt { + 0%, + 17% { + visibility: hidden; + clip-path: inset(0 100% 0 0); + } + 18%, + 100% { + visibility: visible; + clip-path: inset(0 0 0 0); + } +} + +@keyframes heroCodexLineAddReply { + 0%, + 37% { + visibility: hidden; + clip-path: inset(0 100% 0 0); + } + 38%, + 100% { + visibility: visible; + clip-path: inset(0 0 0 0); + } +} + +@keyframes heroCodexLineAddRun { + 0%, + 57% { + visibility: hidden; + clip-path: inset(0 100% 0 0); + } + 58%, + 100% { + visibility: visible; + clip-path: inset(0 0 0 0); + } +} + +@keyframes heroCodexLineAddWorking { + 0%, + 77% { + visibility: hidden; + clip-path: inset(0 100% 0 0); + } + 78%, + 100% { + visibility: visible; + clip-path: inset(0 0 0 0); + } +} + +@keyframes heroClaudeTranscriptSteps { + 0%, + 31% { + transform: translate3d(0, 0, 0); + } + 32%, + 52% { + transform: translate3d(0, -14px, 0); + } + 53%, + 73% { + transform: translate3d(0, -28px, 0); + } + 74%, + 100% { + transform: translate3d(0, -44px, 0); + } +} + +@keyframes heroCodexWorkingPulse { + 0%, + 100% { + opacity: 0.38; + transform: scale(0.72); + } + 50% { + opacity: 1; + transform: scale(1); + } +} + +.heroTermPromptGlyph { + min-width: 8px; + font-weight: 700; +} + +.heroTermCursor { + display: inline-block; + width: 6px; + height: 9px; + background: color-mix(in srgb, var(--term-accent) 82%, white 18%); + opacity: 0.78; +} + +.badge { + display: inline-flex; + align-items: center; + gap: 8px; + background: var(--bg-elevated); + border: 1px solid var(--line); + padding: 6px 16px; + border-radius: 100px; + font-size: 0.75rem; + font-weight: 600; + letter-spacing: 0.06em; + color: var(--primary); + width: fit-content; +} + +.badgeDot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--primary); + box-shadow: 0 0 6px color-mix(in srgb, var(--primary) 50%, transparent); + animation: pulse 2s ease-in-out infinite; } -.heroCommandCta:focus-visible { - outline: 2px solid color-mix(in srgb, var(--primary) 70%, white 18%); - outline-offset: 3px; +@keyframes pulse { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.4; + } } -.heroCommandCta:focus-within .heroCommandCopyButton { - opacity: 1; +.headline { + margin: 0; + font-family: var(--font-heading), sans-serif; + font-size: clamp(3.2rem, 7vw, 5.5rem); + font-weight: 500; + line-height: 1.05; + letter-spacing: -0.03em; + color: var(--fg); + opacity: 0.85; } -@media (hover: hover) and (pointer: fine) { - .heroCommandCta:hover { - border-color: rgba(116, 184, 226, 0.36); - background: - linear-gradient(180deg, rgba(116, 184, 226, 0.1), rgba(116, 184, 226, 0.025)), - rgba(4, 18, 31, 0.8); - } +:global(html[data-theme='dark']) .headline { + opacity: 1; +} - .heroCommandCta:hover .heroCommandCopyButton, - .heroCommandCta:focus-within .heroCommandCopyButton { - opacity: 1; - } +.subtitle { + margin: 0; + font-size: 1.1rem; + line-height: 1.65; + color: var(--fg-muted); + max-width: 48ch; + font-weight: 400; } -@media (hover: none), (pointer: coarse) { - .heroCommandCopyButton { - opacity: 1; - } +.ctas { + display: flex; + align-items: center; + gap: 16px; + padding-top: 4px; } .installCommand { @@ -3106,6 +4298,10 @@ transition: background 0.3s ease; position: relative; z-index: 3; + /* Feature reveals can enter from beyond the centered content grid. Clip at + the viewport-wide wrapper so full-bleed section backgrounds remain + visible while off-screen motion never creates horizontal scroll. */ + overflow-x: clip; } .featuresSection { @@ -3550,36 +4746,343 @@ color: rgba(226, 232, 240, 0.58); } -.webhookCapabilityPreview { +.sharedFilesCapabilityPreview { background: #07111e; } -.webhookCapabilityPreview .webhookPreview { - padding: 0; - justify-content: stretch; -} +.sharedFileMount { + position: absolute; + inset: 0; + z-index: 1; + display: grid; + grid-template-rows: auto auto minmax(0, 1fr) auto; + gap: 10px; + padding: 14px; + overflow: hidden; + background: + radial-gradient(circle at 50% -30%, rgba(73, 157, 214, 0.2), transparent 52%), + #07111e; + color: rgba(226, 232, 240, 0.86); + font-family: var(--font-geist-mono), monospace; +} + +.mountTopbar, +.mountAgent, +.mountTree { + border: 1px solid rgba(156, 207, 255, 0.15); + background: rgba(14, 23, 41, 0.9); + box-shadow: 0 10px 24px rgba(2, 7, 18, 0.22); +} + +.mountTopbar { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: 8px; + min-height: 30px; + padding: 7px 9px; + border-radius: 8px; + color: rgba(181, 205, 226, 0.76); + font-size: 0.61rem; +} + +.mountTopbar code { + overflow: hidden; + color: rgba(241, 245, 249, 0.9); + font: inherit; + text-overflow: ellipsis; + white-space: nowrap; +} + +.mountStatus { + display: inline-flex; + align-items: center; + gap: 5px; + color: #78d5ad; + white-space: nowrap; +} + +.mountStatus > span { + width: 5px; + height: 5px; + border-radius: 50%; + background: currentColor; +} + +.mountAgentRow { + display: grid; + grid-template-columns: max-content minmax(36px, 1fr) max-content; + align-items: center; + gap: 8px; + min-width: 0; +} + +.mountAgent { + display: inline-flex; + align-items: center; + gap: 6px; + min-height: 28px; + padding: 6px 8px; + border-radius: 7px; + color: rgba(226, 232, 240, 0.78); + font-size: 0.58rem; + white-space: nowrap; +} + +.mountAgentClaude svg { + color: #df7b58; +} + +.mountAgentCodex svg { + color: #91b7d4; +} + +.mountTransport { + position: relative; + height: 10px; + overflow: hidden; +} + +.mountTransport::before { + content: ''; + position: absolute; + top: 50%; + right: 0; + left: 0; + height: 1px; + background: rgba(116, 184, 226, 0.32); + transform: translateY(-50%); +} + +.mountPulseWrite, +.mountPulseRead { + position: absolute; + inset: 0; + opacity: 0; + background: linear-gradient( + 90deg, + transparent 40%, + rgba(116, 184, 226, 0.2) 46%, + #9ccfff 50%, + rgba(116, 184, 226, 0.2) 54%, + transparent 60% + ); + will-change: transform, opacity; +} + +.mountPulseWrite { + transform: translateX(-100%); +} + +.mountPulseRead { + transform: translateX(100%); +} + +.mountTree { + min-height: 0; + overflow: hidden; + border-radius: 9px; +} + +.mountTreeHead, +.mountFile { + position: relative; + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: 7px; +} + +.mountTreeHead { + min-height: 28px; + padding: 6px 9px; + border-bottom: 1px solid rgba(156, 207, 255, 0.12); + color: rgba(241, 245, 249, 0.88); + font-size: 0.61rem; + font-weight: 700; +} + +.mountTreeHead small { + color: rgba(120, 213, 173, 0.78); + font: inherit; + font-weight: 500; +} + +.mountFile { + min-height: 27px; + padding: 6px 9px 6px 20px; + color: rgba(148, 163, 184, 0.76); + font-size: 0.56rem; +} + +.mountFile + .mountFile { + border-top: 1px solid rgba(156, 207, 255, 0.08); +} + +.mountFile::before { + content: ''; + position: absolute; + inset: 2px 4px; + border: 1px solid rgba(116, 184, 226, 0.22); + border-radius: 5px; + background: rgba(50, 119, 169, 0.16); + opacity: 0; +} + +.mountFile > * { + position: relative; + z-index: 1; +} + +.mountFile code { + overflow: hidden; + color: rgba(218, 229, 239, 0.84); + font: inherit; + text-overflow: ellipsis; + white-space: nowrap; +} + +.mountFile > span { + color: rgba(116, 184, 226, 0.86); +} + +.mountActivity { + position: relative; + display: grid; + min-height: 14px; + padding-left: 13px; + color: rgba(173, 192, 210, 0.72); + font-size: 0.56rem; + line-height: 14px; +} + +.mountActivity::before { + content: ''; + position: absolute; + top: 5px; + left: 1px; + width: 5px; + height: 5px; + border-radius: 50%; + background: #78d5ad; +} + +.mountActivity span { + grid-area: 1 / 1; + opacity: 0; +} + +.mountActivity span:first-child { + opacity: 1; +} + +@keyframes sharedMountWrite { + 0%, + 8% { + opacity: 0; + transform: translateX(-100%); + } + + 12%, + 34% { + opacity: 1; + } + + 38%, + 100% { + opacity: 0; + transform: translateX(100%); + } +} + +@keyframes sharedMountRead { + 0%, + 50% { + opacity: 0; + transform: translateX(100%); + } + + 54%, + 76% { + opacity: 1; + } + + 80%, + 100% { + opacity: 0; + transform: translateX(-100%); + } +} + +@keyframes sharedMountFileSync { + 0%, + 20%, + 100% { + opacity: 0; + } + + 22%, + 42% { + opacity: 1; + } + + 44% { + opacity: 0; + } +} + +@keyframes sharedMountActivityFirst { + 0%, + 47% { + opacity: 1; + } + + 50%, + 100% { + opacity: 0; + } +} + +@keyframes sharedMountActivitySecond { + 0%, + 47% { + opacity: 0; + } + + 50%, + 100% { + opacity: 1; + } +} + +@media (prefers-reduced-motion: no-preference) { + .mountPulseWrite { + animation: sharedMountWrite 5.6s steps(9, end) infinite; + } + + .mountPulseRead { + animation: sharedMountRead 5.6s steps(9, end) infinite; + } + + .mountFile::before { + animation: sharedMountFileSync 5.6s steps(1, end) infinite; + } + + .mountFile:nth-child(3)::before { + animation-delay: 0.65s; + } + + .mountFile:nth-child(4)::before { + animation-delay: 1.3s; + } -.webhookCapabilityPreview .webhookCodeTitle { - border-top: 0; - border-right: 0; - border-left: 0; - border-radius: 0; - border-bottom-color: rgba(156, 207, 255, 0.2); - background: linear-gradient(180deg, rgba(74, 144, 194, 0.34), rgba(22, 47, 72, 0.56)); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.16); - color: rgba(248, 250, 252, 0.82); - backdrop-filter: blur(16px) saturate(1.35); - -webkit-backdrop-filter: blur(16px) saturate(1.35); -} + .mountActivity span:first-child { + animation: sharedMountActivityFirst 5.6s steps(1, end) infinite; + } -.webhookCapabilityPreview .webhookCodeSnippet { - flex: 1; - min-height: 0; - border-right: 0; - border-bottom: 0; - border-left: 0; - border-radius: 0; - box-shadow: none; + .mountActivity span:last-child { + animation: sharedMountActivitySecond 5.6s steps(1, end) infinite; + } } .capabilityPreview .searchPreview { @@ -4286,83 +5789,6 @@ backdrop-filter: blur(12px); } -.webhookPreview { - position: absolute; - inset: 0; - z-index: 1; - display: flex; - flex-direction: column; - justify-content: center; - padding: 20px 0 20px 20px; - color: #f7fafc; -} - -.webhookCodeTitle { - display: flex; - align-items: center; - gap: 10px; - width: 100%; - padding: 11px 16px; - border: 1px solid rgba(255, 255, 255, 0.14); - border-right: 0; - border-radius: 10px 0 0 0; - background: #111a24; - color: rgba(226, 232, 240, 0.72); - font-family: var(--font-geist-mono), monospace; - font-size: 0.68rem; - line-height: 1; -} - -.webhookCodeDots { - display: inline-flex; - align-items: center; - gap: 6px; - flex: 0 0 auto; -} - -.webhookCodeDots span { - width: 9px; - height: 9px; - border-radius: 50%; -} - -.webhookCodeDots span:nth-child(1) { - background: #ff5f57; -} - -.webhookCodeDots span:nth-child(2) { - background: #febc2e; -} - -.webhookCodeDots span:nth-child(3) { - background: #28c840; -} - -.webhookCodeSnippet { - width: 100%; - min-height: 174px; - margin: 0; - padding: 18px 16px 20px; - border: 1px solid rgba(255, 255, 255, 0.14); - border-top: 0; - border-right: 0; - border-radius: 0 0 0 10px; - background: #07111e; - box-shadow: -10px 12px 30px rgba(2, 7, 18, 0.28); - color: rgba(248, 250, 252, 0.9); - overflow: hidden; -} - -.webhookCodeSnippet code { - overflow: hidden; - font-family: var(--font-geist-mono), monospace; - font-size: 0.7rem; - font-weight: 700; - line-height: 1.7; - text-overflow: ellipsis; - white-space: pre-wrap; -} - .commandPrompt code { overflow: hidden; font-family: var(--font-geist-mono), monospace; @@ -5060,11 +6486,206 @@ } } +.workflowTracePreview { + position: relative; + height: 100%; + overflow: hidden; + padding: 12px 18px 12px 44px; + font-family: var(--font-geist-mono), monospace; +} + +.workflowTraceHeader { + display: flex; + align-items: center; + justify-content: space-between; + min-height: 26px; + margin-bottom: 6px; + padding: 0 10px; + border: 1px solid rgba(156, 207, 255, 0.13); + border-radius: 8px; + background: rgba(15, 27, 45, 0.68); + color: rgba(190, 206, 224, 0.72); + font-size: 0.58rem; +} + +.workflowTraceHeader strong { + color: #74b8e2; + font-size: 0.54rem; + font-weight: 700; +} + +.workflowTraceRail { + position: absolute; + top: 45px; + bottom: 28px; + left: 27px; + width: 1px; + overflow: hidden; + background: rgba(126, 184, 218, 0.16); +} + +.workflowTraceRail span { + display: block; + width: 100%; + min-height: 4px; + background: #2e6a9c; + box-shadow: 0 0 10px rgba(74, 144, 194, 0.38); + transition: height 520ms cubic-bezier(0.16, 1, 0.3, 1); +} + +.workflowTraceRows { + display: grid; + gap: 4px; +} + +.workflowTraceRow { + position: relative; + display: grid; + grid-template-columns: 22px minmax(0, 1fr) auto; + align-items: center; + min-height: 35px; + padding: 3px 9px; + overflow: hidden; + border: 1px solid rgba(156, 207, 255, 0.12); + border-radius: 8px; + background: rgba(15, 27, 45, 0.7); + color: rgba(245, 248, 255, 0.94); + box-shadow: 0 8px 22px rgba(2, 7, 18, 0.18); + transition: + opacity 360ms ease, + border-color 360ms ease, + background-color 360ms ease, + transform 360ms cubic-bezier(0.16, 1, 0.3, 1); +} + +.workflowTraceRow::before { + position: absolute; + top: 50%; + left: -22px; + width: 7px; + height: 7px; + border: 1px solid rgba(116, 184, 226, 0.45); + border-radius: 50%; + background: #07111f; + content: ''; + transform: translateY(-50%); +} + +.workflowTraceRow_pending { + opacity: 0.22; + transform: translateY(2px); +} + +.workflowTraceRow_active { + border-color: rgba(116, 184, 226, 0.56); + background: rgba(19, 38, 61, 0.9); + opacity: 1; + transform: translateY(0); +} + +.workflowTraceRow_active::after { + position: absolute; + inset: 0; + background: linear-gradient(90deg, transparent 0%, rgba(116, 184, 226, 0.1) 48%, transparent 76%); + content: ''; + pointer-events: none; + transform: translateX(-100%); + animation: workflowTraceScan 1.35s ease-in-out infinite; +} + +.workflowTraceRow_active::before, +.workflowTraceRow_complete::before { + border-color: #2e6a9c; + background: #2e6a9c; +} + +.workflowTraceRow_complete { + opacity: 0.72; +} + +.workflowTraceKindRetry.workflowTraceRow_active { + border-color: rgba(216, 135, 111, 0.58); + background: rgba(55, 34, 36, 0.78); +} + +.workflowTraceKindRetry.workflowTraceRow_active::after { + background: linear-gradient(90deg, transparent 0%, rgba(216, 135, 111, 0.12) 48%, transparent 76%); +} + +.workflowTraceIcon { + display: grid; + place-items: center; + width: 18px; + height: 18px; + color: #74b8e2; +} + +.workflowTraceKindRetry .workflowTraceIcon, +.workflowTraceKindHuman .workflowTraceIcon { + color: #d8876f; +} + +.workflowTraceCopy { + min-width: 0; +} + +.workflowTraceCopy strong, +.workflowTraceCopy small { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.workflowTraceCopy strong { + color: rgba(245, 248, 255, 0.94); + font-size: 0.61rem; + line-height: 1.2; +} + +.workflowTraceCopy small { + margin-top: 1px; + color: rgba(190, 206, 224, 0.6); + font-size: 0.45rem; + line-height: 1.15; +} + +.workflowTraceStatus { + position: relative; + z-index: 1; + display: flex; + align-items: center; + gap: 4px; + color: #74b8e2; + font-size: 0.48rem; + font-weight: 700; +} + +.workflowTraceKindRetry .workflowTraceStatus { + color: #d8876f; +} + +@keyframes workflowTraceScan { + 0% { + transform: translateX(-100%); + } + 70%, + 100% { + transform: translateX(100%); + } +} + @media (prefers-reduced-motion: reduce) { .durableTimelineRow, - .durableDelivered { + .durableDelivered, + .workflowTraceRow_active::after { animation: none; } + + .workflowTraceRail span, + .workflowTraceRow { + transition: none; + } } .deliveryTableHead { @@ -5597,10 +7218,6 @@ gap: 9px; } - .webhookPreview { - padding: 16px 0 16px 16px; - } - .commandsPreview { inset: 0; } @@ -5609,20 +7226,6 @@ padding: 20px; } - .webhookCodeTitle { - padding: 10px 14px; - } - - .webhookCodeSnippet { - min-height: 168px; - padding: 15px 14px 16px; - } - - .webhookCodeSnippet code { - font-size: 0.66rem; - line-height: 1.65; - } - .searchPreview { padding-top: 0; } @@ -6073,13 +7676,14 @@ min-height: auto; } + /* min-width: 0 stops the single grid track from being floored by the install + command's min-content width, which pushed the hero column ~34px past the + gutter and clipped the right edge of the copy button on phones. The + command itself already scrolls inside the button. */ .heroLeft { gap: 28px; padding-top: 0; - } - - .heroCopyGroup { - gap: 10px; + min-width: 0; } .heroRight { @@ -6090,6 +7694,23 @@ font-size: clamp(2.85rem, 10.5vw, 4.35rem); } + .heroCenter { + padding: 40px 32px 0; + } + + .heroCenterColumn { + gap: 20px; + max-width: 640px; + } + + .heroCenterColumn .heroCenterHeadline { + font-size: clamp(2.5rem, 7.6vw, 3.9rem); + } + + .heroMarquee { + margin-top: 52px; + } + .sdkSection { grid-template-columns: 1fr; padding: 60px 24px 80px; @@ -6195,15 +7816,76 @@ gap: 26px; } - .heroCopyGroup { - gap: 8px; - } - .headline { font-size: clamp(3.05rem, 11.5vw, 4.5rem); line-height: 1.08; } + /* Phones: the same architecture, one step smaller. The headline is four + words on four lines at this width, so the column tightens and the band + comes up to meet it. */ + .heroCenter { + padding: 34px 24px 0; + } + + .heroCenterColumn { + gap: 18px; + } + + .heroCenterColumn .heroCenterHeadline { + font-size: clamp(2.3rem, 10.2vw, 3.5rem); + max-width: none; + } + + .heroCenterColumn .heroCenterSubtitle { + max-width: 40ch; + } + + .heroCenterCtas { + align-self: stretch; + flex-direction: column; + align-items: stretch; + gap: 10px; + } + + .heroMarquee { + gap: 10px; + margin-top: 40px; + padding-bottom: 18px; + } + + .heroTerm { + margin-right: 10px; + } + + .heroTermSm { + width: 228px; + } + + .heroTermMd { + width: 262px; + } + + .heroTermLg { + width: 292px; + } + + .heroTermTitle { + font-size: 0.64rem; + } + + .heroTermLine { + font-size: 0.66rem; + } + + .heroMarqueeTrackTwo { + margin-left: -96px; + } + + .heroMarqueeTrackThree { + margin-left: -188px; + } + .ctas { flex-direction: column; align-items: stretch; @@ -6222,8 +7904,22 @@ line-height: 1.62; } + /* Phone prose floor. The responsive scale steps running copy down to + 14–15.7px, which is under the 16px body minimum exactly where the column + is narrowest and the text is hardest to read. Labels, captions, code and + the mock chat UI keep their own smaller sizes — this is only the + sentence-length prose. */ + .featureList, + .capabilityCopy p, + .installSubtitle, + .deployCardText { + font-size: 1rem; + } + .heroRight { - display: none; + min-height: 0; + align-self: center; + width: 100%; } .installSection { @@ -6420,17 +8116,18 @@ radial-gradient(circle at 50% 0%, color-mix(in srgb, var(--primary) 22%, transparent), transparent 72%), #0e2031; box-shadow: - 0 0 0 6px color-mix(in srgb, var(--primary) 8%, transparent), + 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: -22px; + inset: -14px; z-index: -1; border-radius: 50%; - background: radial-gradient(circle, color-mix(in srgb, var(--primary) 16%, transparent), transparent 68%); + background: radial-gradient(circle, color-mix(in srgb, var(--primary) 9%, transparent), transparent 70%); pointer-events: none; } @@ -6490,8 +8187,8 @@ border-radius: 50%; background: var(--c); box-shadow: - 0 0 0 1px color-mix(in srgb, var(--c) 35%, transparent), - 0 0 14px 2px var(--c); + 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 @@ -6748,7 +8445,7 @@ border-radius: 12px; border: 1px solid var(--line); background: color-mix(in srgb, var(--card-bg) 55%, #060f1a 45%); - /* Mono brand marks (OpenCode, Cursor, GitHub, Hermes) inherit this. */ + /* Mono brand marks (Codex, OpenCode, Cursor, GitHub, Hermes) inherit this. */ color: #e6edf5; transition: transform 0.2s, diff --git a/web/app/layout.tsx b/web/app/layout.tsx index 0dd8e93..02956b7 100644 --- a/web/app/layout.tsx +++ b/web/app/layout.tsx @@ -11,16 +11,22 @@ import './globals.css'; const inter = Inter({ variable: '--font-geist-sans', subsets: ['latin'], + display: 'swap', }); const sora = Sora({ variable: '--font-heading', subsets: ['latin'], + display: 'swap', }); +// Mono only ever appears below or beside the LCP element, so it stays off the +// critical path: two preloaded families instead of three. const geistMono = Geist_Mono({ variable: '--font-geist-mono', subsets: ['latin'], + display: 'swap', + preload: false, }); export const metadata: Metadata = { diff --git a/web/app/messaging/page.tsx b/web/app/messaging/page.tsx index 246b828..1e430dd 100644 --- a/web/app/messaging/page.tsx +++ b/web/app/messaging/page.tsx @@ -1,29 +1,27 @@ 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 { absoluteUrl } from '../../lib/site'; export const metadata: Metadata = { - title: 'Agent Relay Messaging — Headless Slack for agents.', - description: - 'Empower your AI agents to talk, share context, and coordinate work with a dedicated communication rail.', + title: HOME_HERO_TITLE, + description: HOME_HERO_DESCRIPTION, alternates: { canonical: absoluteUrl('/'), }, openGraph: { - title: 'Agent Relay Messaging — Headless Slack for Agents', - description: - 'Channels, threads, DMs, reactions, and real-time events — everything you’d expect from Slack, exposed as an SDK.', + title: HOME_HERO_TITLE, + description: HOME_HERO_DESCRIPTION, url: absoluteUrl('/'), type: 'website', - images: [ogImage(HOME_OG_IMAGE_PATH, 'Agent Relay Messaging — Headless Slack for Agents')], + images: [ogImage(HOME_OG_IMAGE_PATH, HOME_HERO_TITLE)], }, twitter: { card: 'summary_large_image', - title: 'Agent Relay Messaging — Headless Slack for Agents', - description: - 'Channels, threads, DMs, reactions, and real-time events — everything you’d expect from Slack, exposed as an SDK.', + title: HOME_HERO_TITLE, + description: HOME_HERO_DESCRIPTION, images: [absoluteUrl(HOME_OG_IMAGE_PATH)], }, }; diff --git a/web/app/page.tsx b/web/app/page.tsx index ac1c483..156a9ac 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -1,29 +1,27 @@ 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 { absoluteUrl } from '../lib/site'; export const metadata: Metadata = { - title: 'Agent Relay Messaging - Headless Slack for agents.', - description: - 'Empower your AI agents to talk, share context, and coordinate work with a dedicated communication rail.', + title: HOME_HERO_TITLE, + description: HOME_HERO_DESCRIPTION, alternates: { canonical: absoluteUrl('/'), }, openGraph: { - title: 'Agent Relay Messaging - Headless Slack for Agents', - description: - 'Channels, threads, DMs, reactions, and real-time events. Everything you would expect from Slack, exposed as an SDK.', + title: HOME_HERO_TITLE, + description: HOME_HERO_DESCRIPTION, url: absoluteUrl('/'), type: 'website', - images: [ogImage(HOME_OG_IMAGE_PATH, 'Agent Relay Messaging - Headless Slack for Agents')], + images: [ogImage(HOME_OG_IMAGE_PATH, HOME_HERO_TITLE)], }, twitter: { card: 'summary_large_image', - title: 'Agent Relay Messaging - Headless Slack for Agents', - description: - 'Channels, threads, DMs, reactions, and real-time events. Everything you would expect from Slack, exposed as an SDK.', + title: HOME_HERO_TITLE, + description: HOME_HERO_DESCRIPTION, images: [absoluteUrl(HOME_OG_IMAGE_PATH)], }, }; diff --git a/web/components/MessageRelayAnimation.tsx b/web/components/MessageRelayAnimation.tsx index e96cf07..4c8ecac 100644 --- a/web/components/MessageRelayAnimation.tsx +++ b/web/components/MessageRelayAnimation.tsx @@ -326,11 +326,22 @@ function buildCenters(nodes: AgentNode[], w: number, h: number): NodeCenter[] { })); } +/* Brand blue, one value per theme. The network reads as the site's own colour + rather than the leftover green it was drawn in. */ +const NETWORK_RGB_DARK = '116, 184, 226'; +const NETWORK_RGB_LIGHT = '45, 106, 156'; + +function networkRgb() { + return isDarkMode() ? NETWORK_RGB_DARK : NETWORK_RGB_LIGHT; +} + function drawConnectionLines( ctx: CanvasRenderingContext2D, centers: NodeCenter[], connections: [number, number][] ) { + const rgb = networkRgb(); + for (const [i, j] of connections) { const ci = centers[i]; const cj = centers[j]; @@ -338,7 +349,7 @@ function drawConnectionLines( continue; } - const lineOpacity = Math.min(ci.opacity, cj.opacity) * 0.1; + const lineOpacity = Math.min(ci.opacity, cj.opacity) * 0.3; if (lineOpacity < 0.01) { continue; } @@ -346,8 +357,10 @@ function drawConnectionLines( ctx.beginPath(); ctx.moveTo(ci.cx, ci.cy); ctx.lineTo(cj.cx, cj.cy); - ctx.strokeStyle = `rgba(45, 79, 62, ${lineOpacity})`; - ctx.lineWidth = 1; + /* No shadowBlur here on purpose: the line carries its own weight at this + opacity, and a blur behind every edge is what turned the network neon. */ + ctx.strokeStyle = `rgba(${rgb}, ${lineOpacity})`; + ctx.lineWidth = 1.1; ctx.stroke(); } } @@ -573,6 +586,8 @@ function drawMessages( } function drawGlowRings(ctx: CanvasRenderingContext2D, nodes: AgentNode[], centers: NodeCenter[]) { + const rgb = networkRgb(); + for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; if (node.glowOpacity <= 0.01 || node.opacity <= 0.1) { @@ -581,10 +596,13 @@ function drawGlowRings(ctx: CanvasRenderingContext2D, nodes: AgentNode[], center const center = centers[i]; const radius = 56; - const alpha = node.glowOpacity * node.opacity * 0.07; - const grad = ctx.createRadialGradient(center.cx, center.cy, radius * 0.6, center.cx, center.cy, radius); - grad.addColorStop(0, `rgba(45, 79, 62, ${alpha})`); - grad.addColorStop(1, 'rgba(45, 79, 62, 0)'); + /* A tight, low halo that only says "this node is busy". Starting the ramp + late (0.62) keeps it hugging the card instead of blooming into the + background. */ + const alpha = node.glowOpacity * node.opacity * 0.09; + const grad = ctx.createRadialGradient(center.cx, center.cy, radius * 0.62, center.cx, center.cy, radius); + grad.addColorStop(0, `rgba(${rgb}, ${alpha})`); + grad.addColorStop(1, `rgba(${rgb}, 0)`); ctx.beginPath(); ctx.arc(center.cx, center.cy, radius, 0, Math.PI * 2); ctx.fillStyle = grad; @@ -592,7 +610,7 @@ function drawGlowRings(ctx: CanvasRenderingContext2D, nodes: AgentNode[], center } } -function ClaudeLogo() { +export function ClaudeLogo() { return (
- + @@ -126,7 +126,7 @@ export function SiteFooter() { aria-label="GitHub" className={s.social} > - + @@ -137,7 +137,7 @@ export function SiteFooter() { aria-label="X" className={s.social} > - + @@ -148,7 +148,7 @@ export function SiteFooter() { aria-label="Discord" className={s.social} > - + diff --git a/web/components/SiteNav.tsx b/web/components/SiteNav.tsx index da7bfb4..baab760 100644 --- a/web/components/SiteNav.tsx +++ b/web/components/SiteNav.tsx @@ -9,7 +9,14 @@ import s from './site-nav.module.css'; export function LogoIcon() { return ( - +
{/* Mobile hamburger */} - -
- ); -} diff --git a/web/components/home/HeroGraph.tsx b/web/components/home/HeroGraph.tsx deleted file mode 100644 index d0a27e6..0000000 --- a/web/components/home/HeroGraph.tsx +++ /dev/null @@ -1,49 +0,0 @@ -'use client'; - -import { useEffect, useState } from 'react'; - -import { MessageRelayAnimation } from '../MessageRelayAnimation'; - -// Matches the mobile breakpoint in landing.module.css where .heroRight is hidden. -const MOBILE_QUERY = '(max-width: 600px)'; - -type MediaQueryListWithLegacyListeners = MediaQueryList & { - addListener?: (listener: () => void) => void; - removeListener?: (listener: () => void) => void; -}; - -function subscribeToMediaQuery(mql: MediaQueryList, listener: () => void) { - if (typeof mql.addEventListener === 'function') { - mql.addEventListener('change', listener); - return () => mql.removeEventListener('change', listener); - } - - const legacyMql = mql as MediaQueryListWithLegacyListeners; - legacyMql.addListener?.(listener); - return () => legacyMql.removeListener?.(listener); -} - -/** - * Renders the hero's animated node graph only above the mobile breakpoint. - * - * The graph is a canvas animation with a continuous requestAnimationFrame loop, - * so on mobile we avoid mounting it entirely (rather than just hiding it with - * CSS) to keep the page lightweight on phones. - */ -export function HeroGraph() { - const [isDesktop, setIsDesktop] = useState(false); - - useEffect(() => { - const mql = window.matchMedia(MOBILE_QUERY); - const update = () => setIsDesktop(!mql.matches); - - update(); - return subscribeToMediaQuery(mql, update); - }, []); - - if (!isDesktop) { - return null; - } - - return ; -} diff --git a/web/components/home/HeroTerminalMarquee.tsx b/web/components/home/HeroTerminalMarquee.tsx new file mode 100644 index 0000000..745b78e --- /dev/null +++ b/web/components/home/HeroTerminalMarquee.tsx @@ -0,0 +1,737 @@ +import type { CSSProperties } from 'react'; + +import Grok from '@lobehub/icons/es/Grok'; + +import { AgentToolLogo, type AgentTool } from '../AgentToolLogos'; +import s from '../../app/landing.module.css'; + +type TerminalAgent = Extract | 'grok'; + +type LineTone = 'prompt' | 'tool' | 'ok' | 'relay'; + +type TerminalLine = { + tone: LineTone; + text: string; +}; + +type TerminalCard = { + /** Which agent's terminal this is; drives the name and mark in the title bar. */ + agent: TerminalAgent; + /** Repository context shown inside the terminal session. */ + repo: string; + /** Three or four lines: a prompt, what the agent did, where it went. */ + lines: TerminalLine[]; + /** Card width step. The rows read as sessions, not as a grid, so widths vary. */ + size: 'sm' | 'md' | 'lg'; +}; + +type TerminalStyle = CSSProperties & { + '--line-delay'?: string; + '--term-cycle'?: string; + '--term-phase'?: string; +}; + +type RelayRoute = { + d: string; + delay: string; + duration: string; + id: string; + origin: { x: number; y: number }; + target: { x: number; y: number }; +}; + +const AGENT_META: Record< + TerminalAgent, + { + activityGlyph: string; + cycle: string; + label: string; + mode: string; + prompt: string; + session: string; + state: string; + } +> = { + claude: { + activityGlyph: '✻', + cycle: '7.8s', + label: 'Claude Code', + mode: 'trusted', + prompt: '❯', + session: 'Claude Max', + state: 'Thinking', + }, + codex: { + activityGlyph: '◌', + cycle: '7.2s', + label: 'OpenAI Codex', + mode: 'workspace-write', + prompt: '›', + session: 'Codex session', + state: 'Running', + }, + grok: { + activityGlyph: '◆', + cycle: '7.5s', + label: 'Grok', + mode: 'auto', + prompt: '>', + session: 'Grok Code', + state: 'Working', + }, + opencode: { + activityGlyph: '▣', + cycle: '6.9s', + label: 'OpenCode', + mode: 'build', + prompt: '>', + session: 'OpenCode build', + state: 'Editing', + }, +}; + +const TONE_CLASS: Record = { + prompt: s.heroTermPrompt, + tool: s.heroTermTool, + ok: s.heroTermOk, + relay: s.heroTermRelay, +}; + +const LINE_MARKER: Record = { + prompt: '❯', + tool: '└', + ok: '✓', + relay: '↗', +}; + +function terminalLineCopy(line: TerminalLine) { + if (line.tone === 'prompt') { + return line.text.match(/^\$ \S+ "(.+)"$/)?.[1] ?? line.text.replace(/^\$ /, ''); + } + + return line.text.replace(/^(?:⎿|✓|↑|→)\s*/, ''); +} + +const RELAY_ROUTES: RelayRoute[] = [ + { + id: 'hero-relay-route-one', + d: 'M 318 62 C 410 62 438 202 552 202', + duration: '10.6s', + delay: '-1.4s', + origin: { x: 318, y: 62 }, + target: { x: 552, y: 202 }, + }, + { + id: 'hero-relay-route-two', + d: 'M 654 202 C 758 202 782 344 904 344', + duration: '12.2s', + delay: '-6.8s', + origin: { x: 654, y: 202 }, + target: { x: 904, y: 344 }, + }, + { + id: 'hero-relay-route-three', + d: 'M 956 344 C 1078 344 1080 62 1206 62', + duration: '11.3s', + delay: '-4.1s', + origin: { x: 956, y: 344 }, + target: { x: 1206, y: 62 }, + }, + { + id: 'hero-relay-route-four', + d: 'M 1184 62 C 1088 62 1030 202 872 202', + duration: '13.1s', + delay: '-9.3s', + origin: { x: 1184, y: 62 }, + target: { x: 872, y: 202 }, + }, + { + id: 'hero-relay-route-five', + d: 'M 338 344 C 450 344 476 62 624 62', + duration: '11.8s', + delay: '-11.1s', + origin: { x: 338, y: 344 }, + target: { x: 624, y: 62 }, + }, +]; + +/** + * Three rows of Claude Code, Codex, Grok and OpenCode sessions. Each title bar + * names the agent, while repository and activity context stay in the terminal. + * The animated lines show the prompt, what the tool did, and where the result + * went, which for Agent Relay means a channel, a DM or a PR. + * + * The cards are illustrative chrome, so the whole band is aria-hidden and holds + * nothing focusable; the hero's real content is the column above it. + */ +const ROW_ONE: TerminalCard[] = [ + { + agent: 'claude', + repo: 'agentrelay/web', + size: 'md', + lines: [ + { tone: 'prompt', text: '$ claude "fix the flaky retry test"' }, + { tone: 'tool', text: '⎿ ran 26 tests - 26 passed' }, + { tone: 'relay', text: '↑ pushed fix-retry → PR #482' }, + ], + }, + { + agent: 'codex', + repo: 'relay/router', + size: 'lg', + lines: [ + { tone: 'prompt', text: '$ codex "trace the dropped socket"' }, + { tone: 'tool', text: '⎿ found reconnect gap at 30s' }, + { tone: 'relay', text: '→ relayed to #build-router' }, + ], + }, + { + agent: 'opencode', + repo: 'relay/sdk', + size: 'sm', + lines: [ + { tone: 'prompt', text: '$ opencode "threads for py sdk"' }, + { tone: 'tool', text: '⎿ wrote sdk/python/threads.py' }, + { tone: 'ok', text: '✓ typecheck clean' }, + ], + }, + { + agent: 'grok', + repo: 'relay/docs', + size: 'md', + lines: [ + { tone: 'prompt', text: '$ grok "refresh the quickstart"' }, + { tone: 'tool', text: '⎿ edited docs/quickstart.mdx' }, + { tone: 'relay', text: '→ DM to Reviewer: ready for a pass' }, + ], + }, + { + agent: 'claude', + repo: 'relay/tests', + size: 'sm', + lines: [ + { tone: 'prompt', text: '$ claude "unflake the ws suite"' }, + { tone: 'tool', text: '⎿ retried 3× - stable' }, + { tone: 'relay', text: '→ note left in #build-router' }, + ], + }, +]; + +const ROW_TWO: TerminalCard[] = [ + { + agent: 'grok', + repo: 'relay/web', + size: 'lg', + lines: [ + { tone: 'prompt', text: '$ grok "port nav to the new tokens"' }, + { tone: 'tool', text: '⎿ 4 files changed, 61 insertions' }, + { tone: 'relay', text: '↑ opened PR #491 · asked Reviewer' }, + ], + }, + { + agent: 'claude', + repo: 'relay/api', + size: 'sm', + lines: [ + { tone: 'tool', text: '⎿ relay.send_message(#build-web)' }, + { tone: 'tool', text: '⎿ Reviewer joined the thread' }, + { tone: 'ok', text: '✓ handoff acked in 1.2s' }, + ], + }, + { + agent: 'codex', + repo: 'relay/billing', + size: 'md', + lines: [ + { tone: 'prompt', text: '$ codex "add the usage export"' }, + { tone: 'tool', text: '⎿ ran migration 0042' }, + { tone: 'ok', text: '✓ backfilled 1,204 rows' }, + ], + }, + { + agent: 'opencode', + repo: 'relay/cli', + size: 'md', + lines: [ + { tone: 'prompt', text: '$ opencode "make relay login retry"' }, + { tone: 'tool', text: '⎿ patched cli/auth.ts' }, + { tone: 'relay', text: '→ posted the diff in #build-cli' }, + ], + }, +]; + +const ROW_THREE: TerminalCard[] = [ + { + agent: 'grok', + repo: 'relay/search', + size: 'md', + lines: [ + { tone: 'prompt', text: '$ grok "index the thread history"' }, + { tone: 'tool', text: '⎿ reindexed 84,102 messages' }, + { tone: 'relay', text: '→ summary in #build-search' }, + ], + }, + { + agent: 'claude', + repo: 'relay/infra', + size: 'sm', + lines: [ + { tone: 'tool', text: '⎿ waiting on Planner in #ship-it' }, + { tone: 'relay', text: '→ Planner: "go after the freeze"' }, + { tone: 'ok', text: '✓ resumed after 41m idle' }, + ], + }, + { + agent: 'claude', + repo: 'agentrelay/web', + size: 'lg', + lines: [ + { tone: 'prompt', text: '$ claude "review PR #482"' }, + { tone: 'tool', text: '⎿ read 6 files · left 3 notes' }, + { tone: 'relay', text: '→ DM to Coder: one nit, then merge' }, + ], + }, + { + agent: 'codex', + repo: 'relay/router', + size: 'md', + lines: [ + { tone: 'tool', text: '⎿ picked up task from #build-router' }, + { tone: 'prompt', text: '$ codex "add backpressure to fanout"' }, + { tone: 'ok', text: '✓ p99 480ms → 120ms' }, + ], + }, + { + agent: 'opencode', + repo: 'relay/web', + size: 'lg', + lines: [ + { tone: 'prompt', text: '$ opencode "wire up the pricing table"' }, + { tone: 'tool', text: '⎿ 2 components, 1 story' }, + { tone: 'relay', text: '↑ PR #494 → handed to Reviewer' }, + ], + }, +]; + +const SIZE_CLASS = { + sm: s.heroTermSm, + md: s.heroTermMd, + lg: s.heroTermLg, +} as const; + +function TerminalAgentLogo({ agent, idPrefix }: { agent: TerminalAgent; idPrefix: string }) { + if (agent === 'grok') return ; + + return ; +} + +function RelayNetwork() { + return ( + + {RELAY_ROUTES.map((route) => ( + + + + + + + + + + + + + + + + + + + ))} + + ); +} + +function GrokTerminalBody({ card }: { card: TerminalCard }) { + const prompt = card.lines.find((line) => line.tone === 'prompt') ?? card.lines[0]; + const activity = card.lines.find((line) => line.tone === 'tool') ?? card.lines[1]; + const result = card.lines.find((line) => line.tone === 'relay' || line.tone === 'ok'); + + return ( +
+
+ + + codex/{card.repo.replace('/', '-')} ~/{card.repo} + + auto +
+
+ + {terminalLineCopy(prompt)} +
+
+ + ◆ user_prompt_submit + + + ◆ Thought for 3.2s + + + {terminalLineCopy(activity)} + +
+
+ + + Preparing tools (3)… + + {result ? {terminalLineCopy(result)} : null} +
+
+ + + Grok Code (high) +
+
+ ); +} + +function OpenCodeLoader() { + return ( + + {[0, 1, 2, 3, 4, 5, 6, 7].map((segment) => ( + + ))} + + ); +} + +function OpenCodeTerminalBody({ card }: { card: TerminalCard }) { + const prompt = card.lines.find((line) => line.tone === 'prompt') ?? card.lines[0]; + const activity = card.lines.find((line) => line.tone === 'tool') ?? card.lines[1]; + const result = card.lines.find((line) => line.tone === 'relay' || line.tone === 'ok'); + + return ( +
+
+ {terminalLineCopy(prompt)} +
+
+ + + Thought: 683ms + + + * {terminalLineCopy(activity)} + + + ~ {result ? terminalLineCopy(result) : 'Finding files…'} + +
+
+ + Build + OpenCode +
+
+ + Build + OpenCode session +
+
+ + esc interrupt + tab agents +
+
+ ); +} + +function CodexTerminalBody({ card }: { card: TerminalCard }) { + const prompt = card.lines.find((line) => line.tone === 'prompt') ?? card.lines[0]; + const activity = card.lines.find((line) => line.tone === 'tool') ?? card.lines[1]; + const result = card.lines.find((line) => line.tone === 'relay' || line.tone === 'ok'); + + return ( +
+
+
+
+
+ + >_ OpenAI Codex + + + model: + coding model + high + fast + + + directory: + ~/{card.repo} + +
+
+ + {terminalLineCopy(prompt)} +
+
+ + I’ll inspect the project, then make the smallest safe change. +
+
+ + + Ran {terminalLineCopy(activity)} + + {result ? ( + └ {terminalLineCopy(result)} + ) : null} +
+
+ + Working + esc to interrupt +
+
+
+
+
+ + + Ask Codex to do anything +
+
+ coding model + high + fast + ~/{card.repo} +
+
+ ); +} + +function ClaudeTerminalBody({ card }: { card: TerminalCard }) { + const meta = AGENT_META.claude; + + return ( +
+
+
+
+
+
+
+ {card.lines.map((line) => ( + + {LINE_MARKER[line.tone]} + {terminalLineCopy(line)} + + ))} +
+
+
+
+
+ + {meta.prompt} + + + + {meta.activityGlyph} + {meta.state} + +
+
+ ); +} + +function TerminalRow({ + cards, + rowClass, + rowKey, +}: { + cards: TerminalCard[]; + rowClass: string; + rowKey: string; +}) { + // Two identical copies make the loop seamless: the track slides exactly one + // copy's width (translateX(-50%)) and snaps back with the second copy already + // sitting where the first one started. + const copies = ['a', 'b']; + const rowPhase = rowKey === 'one' ? 0 : rowKey === 'two' ? 2.2 : 4.4; + + return ( +
+
+ {copies.map((copy, copyIndex) => + cards.map((card, index) => { + const meta = AGENT_META[card.agent]; + const phase = -(rowPhase + index * 0.83 + copyIndex * 0.37); + const terminalStyle: TerminalStyle = { + '--term-cycle': meta.cycle, + '--term-phase': `${phase}s`, + }; + + return ( +
+
+ + + + + + + + {meta.label} + +
+ + {card.agent === 'grok' ? ( + + ) : card.agent === 'opencode' ? ( + + ) : card.agent === 'codex' ? ( + + ) : card.agent === 'claude' ? ( + + ) : ( +
+
+ {meta.session} + {meta.mode} + ~/{card.repo} +
+
+ {card.lines.map((line, lineIndex) => ( + + {LINE_MARKER[line.tone]} + {terminalLineCopy(line)} + + ))} +
+
+ + {meta.prompt} + + + + {meta.activityGlyph} + {meta.state} + +
+
+ )} +
+ ); + }) + )} +
+
+ ); +} + +export function HeroTerminalMarquee() { + return ( + + ); +} diff --git a/web/components/home/HowItWorks.tsx b/web/components/home/HowItWorks.tsx index 60732a7..e2094df 100644 --- a/web/components/home/HowItWorks.tsx +++ b/web/components/home/HowItWorks.tsx @@ -24,7 +24,9 @@ interface LogoItem { /** CLI coding agents that Relay drives over a PTY. */ const CLI_LOGOS: readonly LogoItem[] = [ { key: 'claude-code', label: 'Claude Code', node: }, - { key: 'codex', label: 'Codex', node: }, + // Mono, not .Color: the coloured Codex mark is near-black and disappears + // against the chip on this dark band. + { key: 'codex', label: 'Codex', node: }, { key: 'opencode', label: 'OpenCode', node: }, { key: 'cursor', label: 'Cursor', node: }, { key: 'grok', label: 'Grok', node: }, diff --git a/web/components/home/MessagingFeature.tsx b/web/components/home/MessagingFeature.tsx index bb16783..b26a4b3 100644 --- a/web/components/home/MessagingFeature.tsx +++ b/web/components/home/MessagingFeature.tsx @@ -1,7 +1,6 @@ import { ChannelMessagesPreview } from '../ChannelMessagesPreview'; import { FadeIn } from '../FadeIn'; import s from '../../app/landing.module.css'; -import { ScribbleUnderline } from './icons'; export function MessagingFeature() { return ( @@ -13,14 +12,7 @@ export function MessagingFeature() {
-

- Everything Slack has. For{' '} - - agents. - - - -

+

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.
  • diff --git a/web/components/home/MessagingLandingPage.tsx b/web/components/home/MessagingLandingPage.tsx index 2fa96bd..06ff59e 100644 --- a/web/components/home/MessagingLandingPage.tsx +++ b/web/components/home/MessagingLandingPage.tsx @@ -18,28 +18,43 @@ import s from '../../app/landing.module.css'; export function MessagingLandingPage() { return (
    + + Skip to content + + } /> - +
    + -
    -
    - - - - - - - - -
    -
    +
    +
    + + + + + + + + +
    +
    -
    - -
    +
    + +
    - + +
    diff --git a/web/components/home/QuickStart.tsx b/web/components/home/QuickStart.tsx index 9c87ebd..2fc0b1e 100644 --- a/web/components/home/QuickStart.tsx +++ b/web/components/home/QuickStart.tsx @@ -23,8 +23,8 @@ export function QuickStart() {

- Use the Agent Relay SDK for channels, DMs, threads, and realtime events inside your product - or infrastructure. + Build agent coordination, shared files, approvals, and durable workflows into your product or + infrastructure.

diff --git a/web/components/home/icons.tsx b/web/components/home/icons.tsx index 16961ff..1e33f47 100644 --- a/web/components/home/icons.tsx +++ b/web/components/home/icons.tsx @@ -216,98 +216,3 @@ export function WaveDivider({ ); } - -/** - * Decorative communication-graph and contour backdrop behind the hero. - * Inline so it paints with the first server-rendered HTML — no extra request - * and no above-the-fold flash. - */ -export function HeroBackdrop() { - return ( - - ); -} diff --git a/web/components/site-footer.module.css b/web/components/site-footer.module.css index 0e7d2a3..2331a38 100644 --- a/web/components/site-footer.module.css +++ b/web/components/site-footer.module.css @@ -1,8 +1,14 @@ .footer { background: var(--footer-bg); color: var(--footer-fg); - padding: 64px 40px 32px; + padding: 72px 40px 28px; transition: background 0.3s ease; + /* The footer carries its own ground, which is dark on every theme — including + the light homepage, where the page-level ring is near-black ink. Inheriting + that ring would draw dark-on-dark and leave 23 links with no visible focus + state, so the ring is re-pointed at the footer's own foreground, which the + --footer-bg / --footer-fg pair already guarantees contrasts with it. */ + --focus-ring: var(--footer-fg); } .inner { @@ -11,26 +17,30 @@ gap: 48px; max-width: 1280px; margin: 0 auto; - padding-bottom: 48px; + padding-bottom: 40px; border-bottom: 1px solid var(--footer-line); } .brand { display: flex; flex-direction: column; - gap: 14px; + gap: 12px; } +/* The logo is a link, so it gets a comfortable target of its own rather than + being exactly as tall as the wordmark. */ .logo { - display: flex; + display: inline-flex; align-items: center; gap: 10px; + min-height: 44px; + width: fit-content; text-decoration: none; color: var(--footer-fg); } .logo svg { - height: 22px; + height: 26px; width: auto; color: var(--footer-fg); } @@ -41,7 +51,7 @@ } .tagline { - font-size: 0.88rem; + font-size: 1rem; color: var(--footer-muted); line-height: 1.5; max-width: 28ch; @@ -51,42 +61,52 @@ .columns { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); - gap: 32px; + gap: 28px; } .col { display: flex; flex-direction: column; - gap: 10px; } .colTitle { - font-size: 0.72rem; + font-size: 0.8125rem; font-weight: 700; - letter-spacing: 0.1em; + letter-spacing: 0.12em; text-transform: uppercase; color: var(--footer-faint); - margin: 0 0 4px; + margin: 0 0 10px; } +/* Every footer link is a 44px row, not a 17px word: the whole column was the + tap-target failure on mobile. The tail-corner hover ground is the same shape + rule the rest of the site uses. */ .link { display: inline-flex; align-items: center; - gap: 0.35rem; + gap: 0.4rem; width: fit-content; - font-size: 0.88rem; + max-width: 100%; + min-height: 44px; + padding: 4px 10px; + margin-left: -10px; + border-radius: var(--r-sm, 12px); + font-size: 0.9375rem; color: var(--footer-muted); text-decoration: none; - transition: color 0.15s; + transition: + color 0.15s, + background 0.15s; } .link:hover { color: var(--footer-fg); + background: color-mix(in srgb, var(--footer-fg) 8%, transparent); } .externalIcon { - width: 0.8rem; - height: 0.8rem; + width: 0.85rem; + height: 0.85rem; flex-shrink: 0; stroke-width: 1.8; } @@ -95,9 +115,10 @@ display: flex; align-items: center; justify-content: space-between; + gap: 16px; max-width: 1280px; margin: 0 auto; - padding-top: 24px; + padding-top: 20px; } .bottomMeta { @@ -108,33 +129,45 @@ } .copy { - font-size: 0.78rem; + font-size: 0.875rem; color: var(--footer-faint); margin: 0; } .socials { display: flex; - gap: 16px; + gap: 10px; } .social { - color: var(--footer-faint); - transition: color 0.15s; + display: inline-flex; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + border-radius: var(--r-sm, 12px); + border: 1px solid var(--footer-line); + color: var(--footer-muted); + transition: + color 0.15s, + border-color 0.15s, + background 0.15s; } .social:hover { color: var(--footer-fg); + border-color: color-mix(in srgb, var(--footer-fg) 34%, transparent); + background: color-mix(in srgb, var(--footer-fg) 8%, transparent); } @media (max-width: 960px) { .footer { - padding: 48px 24px 24px; + padding: 52px 24px 24px; } .inner { grid-template-columns: 1fr; - gap: 36px; + gap: 32px; } .bottom { @@ -148,9 +181,9 @@ } } -@media (max-width: 560px) { +@media (max-width: 620px) { .columns { grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 28px 24px; + gap: 24px 16px; } } diff --git a/web/components/site-nav.module.css b/web/components/site-nav.module.css index bc8c215..fca784d 100644 --- a/web/components/site-nav.module.css +++ b/web/components/site-nav.module.css @@ -10,7 +10,8 @@ margin: 0 auto; background: var(--nav-solid-bg); border: 1px solid var(--nav-border); - border-radius: 14px; + /* Same tail-corner shape as every other surface on the site. */ + border-radius: var(--r-lg, 24px); transition: border-color 0.2s ease, box-shadow 0.2s ease; @@ -20,12 +21,18 @@ inset 0 1px 0 var(--nav-inset); } +/* The gradient is translucent so the bar picks up whatever it floats over. + That only works where page and bar share a value range; a page that scrolls + dark bands under a light bar sets --nav-scrolled-base to an opaque ground, + which paints as the last background layer underneath the gradient. It + defaults to transparent, so every other route is unchanged. */ .navBarScrolled { background: linear-gradient( - 135deg, - color-mix(in srgb, var(--nav-bg) 78%, transparent) 0%, - color-mix(in srgb, var(--nav-bg) 88%, transparent) 100% - ); + 135deg, + color-mix(in srgb, var(--nav-bg) 78%, transparent) 0%, + color-mix(in srgb, var(--nav-bg) 88%, transparent) 100% + ) + var(--nav-scrolled-base, transparent); backdrop-filter: blur(24px) saturate(2) brightness(1.05); -webkit-backdrop-filter: blur(24px) saturate(2) brightness(1.05); border-color: var(--nav-border); @@ -38,7 +45,8 @@ display: flex; align-items: center; justify-content: space-between; - padding: 10px 24px; + padding: 10px 20px; + min-height: 62px; } .navCenter { @@ -67,6 +75,8 @@ display: flex; align-items: center; gap: 10px; + min-height: 44px; + padding-right: 6px; } .logoIcon { @@ -91,14 +101,17 @@ .agentLink { font-family: var(--font-geist-sans), sans-serif; - font-size: 0.72rem; + font-size: 0.8125rem; font-weight: 500; color: inherit; text-decoration: none; opacity: 0.85; - padding: 5px 12px; + display: inline-flex; + align-items: center; + min-height: 40px; + padding: 8px 14px; border: 1px solid var(--nav-border); - border-radius: 6px; + border-radius: var(--r-xs, 8px); transition: opacity 0.2s, border-color 0.2s; @@ -111,13 +124,16 @@ .link { font-family: var(--font-geist-sans), sans-serif; - font-size: 0.78rem; + font-size: 0.8125rem; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: inherit; text-decoration: none; - padding-bottom: 4px; + display: inline-flex; + align-items: center; + min-height: 44px; + padding-top: 4px; border-bottom: 2.5px solid transparent; transition: color 0.2s, @@ -138,11 +154,12 @@ } .navBar { - border-radius: 10px; + border-radius: var(--r-md, 16px); } .nav { - padding: 8px 14px; + padding: 8px 12px; + min-height: 56px; } .logoIcon { @@ -173,13 +190,13 @@ display: none; align-items: center; justify-content: center; - width: 36px; - height: 36px; + width: 44px; + height: 44px; border: none; background: transparent; color: inherit; cursor: pointer; - border-radius: 8px; + border-radius: var(--r-sm, 12px); transition: background 0.15s; } @@ -198,7 +215,7 @@ .mobileMenu { display: flex; flex-direction: column; - padding: 6px 14px 16px; + padding: 4px 10px 14px; gap: 4px; max-height: min(72vh, calc(100vh - 88px)); overflow-y: auto; @@ -206,9 +223,11 @@ } .mobileLink { - display: block; + display: flex; + align-items: center; + min-height: 52px; padding: 12px 14px; - border-radius: 8px; + border-radius: var(--r-sm, 12px); font-size: 1.0625rem; font-weight: 500; color: inherit; @@ -272,9 +291,10 @@ .dropdownItem { display: block; - padding: 10px 24px; + padding: 12px 24px; + min-height: 44px; font-family: var(--font-geist-sans), sans-serif; - font-size: 0.78rem; + font-size: 0.8125rem; font-weight: 500; letter-spacing: 0.08em; text-transform: uppercase; @@ -291,11 +311,11 @@ .dropdownMenu a:first-child { border-top: 1px solid var(--nav-border); - border-radius: 8px 8px 0 0; + border-radius: 12px 12px 0 0; } .dropdownMenu a:last-child { - border-radius: 0 0 8px 8px; + border-radius: 0 0 12px 2px; } .dropdownItem:hover { @@ -310,8 +330,9 @@ align-items: center; justify-content: space-between; width: 100%; + min-height: 52px; padding: 12px 14px; - border-radius: 8px; + border-radius: var(--r-sm, 12px); font-size: 1.0625rem; font-weight: 500; color: inherit; diff --git a/web/components/waitlist.module.css b/web/components/waitlist.module.css index 6c3881c..fcb30ef 100644 --- a/web/components/waitlist.module.css +++ b/web/components/waitlist.module.css @@ -2,13 +2,13 @@ position: relative; display: flex; align-items: center; - gap: 0; + gap: 8px; max-width: 520px; margin: 0 auto; background: var(--card-bg); - border: 1px solid var(--line); - border-radius: 100px; - padding: 5px 5px 5px 24px; + border: 1.5px solid var(--line); + border-radius: var(--r-md, 16px); + padding: 6px 6px 6px 8px; transition: border-color 0.2s, box-shadow 0.2s; @@ -16,7 +16,19 @@ .form:focus-within { border-color: var(--primary); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 10%, transparent); +} + +/* Visually hidden: the input had only a placeholder for a name. */ +.label { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; + border: 0; } .input { @@ -26,9 +38,12 @@ background: transparent; color: var(--fg); font-family: var(--font-geist-sans), sans-serif; - font-size: 0.92rem; - outline: none; - padding: 10px 0; + font-size: 1rem; + /* No `outline: none` here: the global :focus-visible rule is the only focus + treatment on the site and it must be allowed to draw. */ + padding: 13px 12px; + border-radius: var(--r-sm, 12px); + min-height: 48px; } .input::placeholder { @@ -37,30 +52,26 @@ .button { flex-shrink: 0; - padding: 10px 24px; - background: #2d6a9c; + padding: 13px 22px; + min-height: 48px; + background: var(--primary-600, #2d6a9c); color: var(--primary-fg); - border: 1.5px solid #234969; - border-radius: 100px; + border: 1.5px solid transparent; + border-radius: var(--r-md, 16px); font-family: var(--font-geist-sans), sans-serif; - font-size: 0.85rem; + font-size: 0.9375rem; font-weight: 600; - letter-spacing: 0.01em; + letter-spacing: -0.005em; cursor: pointer; - box-shadow: none; transition: background 0.2s, border-color 0.2s, - transform 0.15s, - box-shadow 0.2s; + transform 0.15s; white-space: nowrap; } .button:hover:not(:disabled) { - background: #4a90c2; - border-color: #234969; - transform: translateY(-1px); - box-shadow: none; + background: var(--primary-hover, #4a90c2); } .button:disabled { @@ -72,9 +83,9 @@ max-width: 520px; margin: 0 auto; padding: 32px 28px; - border-radius: 16px; - border: 1px solid color-mix(in srgb, var(--primary) 15%, transparent); - background: color-mix(in srgb, var(--primary) 5%, transparent); + border-radius: var(--r-lg, 24px); + border: 1px solid color-mix(in srgb, var(--primary) 24%, transparent); + background: color-mix(in srgb, var(--primary) 7%, transparent); text-align: center; animation: popIn 0.35s cubic-bezier(0.34, 1.56, 0.64, 1); } @@ -96,22 +107,23 @@ justify-content: center; width: 48px; height: 48px; - border-radius: 50%; - background: color-mix(in srgb, var(--primary) 10%, transparent); + border-radius: var(--r-md, 16px); + background: color-mix(in srgb, var(--primary) 14%, transparent); color: var(--primary); margin: 0 auto 16px; } .successTitle { font-family: var(--font-heading), sans-serif; - font-size: 1.2rem; + font-size: 1.3rem; font-weight: 700; + letter-spacing: -0.02em; color: var(--primary); margin: 0 0 8px; } .successText { - font-size: 0.9rem; + font-size: 1rem; line-height: 1.6; color: var(--fg-muted); margin: 0; @@ -125,8 +137,8 @@ .error { position: absolute; bottom: -28px; - left: 24px; - font-size: 0.78rem; + left: 12px; + font-size: 0.8125rem; color: #c44; margin: 0; } @@ -134,8 +146,7 @@ @media (max-width: 500px) { .form { flex-direction: column; - border-radius: 16px; - padding: 12px; + padding: 10px; gap: 8px; } @@ -146,5 +157,6 @@ .button { width: 100%; + min-height: 52px; } } diff --git a/web/lib/home-copy.ts b/web/lib/home-copy.ts new file mode 100644 index 0000000..6485145 --- /dev/null +++ b/web/lib/home-copy.ts @@ -0,0 +1,6 @@ +export const HOME_HERO_TITLE_LINES = ['Turn coding agents', 'into a working team.'] as const; + +export const HOME_HERO_TITLE = HOME_HERO_TITLE_LINES.join(' '); + +export const HOME_HERO_DESCRIPTION = + 'Coordinate Claude, Codex, and other coding agents across shared work, approvals, and long-running workflows'; diff --git a/web/lib/og-meta.ts b/web/lib/og-meta.ts index 5632f4e..9eff529 100644 --- a/web/lib/og-meta.ts +++ b/web/lib/og-meta.ts @@ -49,7 +49,7 @@ 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-v20260610.png'; +export const HOME_OG_IMAGE_PATH = '/og/agent-relay-home-v20260831.png'; export const DEFAULT_OG_IMAGE_PATH = '/og/agent-relay-default-v20260610.png'; /** The site-wide default card. */ diff --git a/web/lib/og/template.tsx b/web/lib/og/template.tsx index 8ae14c0..8415f8a 100644 --- a/web/lib/og/template.tsx +++ b/web/lib/og/template.tsx @@ -12,6 +12,8 @@ */ import type { ReactElement } from 'react'; +import { HOME_HERO_DESCRIPTION, HOME_HERO_TITLE_LINES } from '../home-copy'; + export const OG_SIZE = { width: 1200, height: 630 } as const; export const OG_CONTENT_TYPE = 'image/png'; @@ -792,14 +794,17 @@ export function LandingVariant({ flexDirection: 'column', fontFamily: headingFamily, fontWeight: 800, - fontSize: 76, + fontSize: 48, lineHeight: 1.0, letterSpacing: '-0.045em', color: PALETTE.fg, }} > - Let your - agents talk + {HOME_HERO_TITLE_LINES.map((line) => ( + + {line} + + ))}
- Give Claude, Codex or any agent DMs, channels, and a searchable chat history — without the glue. + {HOME_HERO_DESCRIPTION}
diff --git a/web/next.config.mjs b/web/next.config.mjs index fdf4431..c846d2f 100644 --- a/web/next.config.mjs +++ b/web/next.config.mjs @@ -7,6 +7,16 @@ const __dirname = path.dirname(__filename); /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, + experimental: { + // Default ("loose") chunking merged unrelated route CSS into one + // render-blocking file: the homepage was shipping the /brand, /enterprise, + // /telemetry and /legal stylesheets — about 60% of its critical CSS — + // before a byte of its own. 'strict' still merges them; opting out entirely + // gives each CSS module its own chunk, loaded only where it is used, which + // takes the homepage's render-blocking CSS from 253 KB to 82 KB raw + // (51 KB → 20 KB gzip) at the cost of a couple more HTTP/2 requests. + cssChunking: false, + }, outputFileTracingRoot: path.resolve(__dirname, '..'), outputFileTracingIncludes: { '/*': ['content/docs/**/*', 'content/blog/**/*'], diff --git a/web/public/brand.css b/web/public/brand.css index 8ec9151..6062484 100644 --- a/web/public/brand.css +++ b/web/public/brand.css @@ -8,6 +8,26 @@ */ :root { + /* ── Shape ── + Agent Relay's one shape rule: every interactive element and every content + surface is a message — a rounded rectangle with a single squared "tail" + corner at bottom-left. Nothing is a plain rounded rect, nothing is a pill. + The `-in` variants mirror the tail to bottom-right, used only where the + direction means something (an incoming message vs an outgoing one). */ + --r-xs: 8px 8px 8px 2px; + --r-sm: 12px 12px 12px 2px; + --r-md: 16px 16px 16px 3px; + --r-lg: 24px 24px 24px 4px; + --r-xl: 34px 34px 34px 6px; + --r-xs-in: 8px 8px 2px 8px; + --r-sm-in: 12px 12px 2px 12px; + --r-md-in: 16px 16px 3px 16px; + --r-lg-in: 24px 24px 4px 24px; + + /* Focus ring. Drawn with a 2px outline-offset so the ring sits on the page + ground rather than on the control, which is what it is measured against. */ + --focus-ring: #1c1512; + /* Logo */ --logo-primary: #4a90c2; --logo-secondary: rgba(74, 144, 194, 0.5); @@ -152,6 +172,7 @@ } :root[data-theme='dark'] { + --focus-ring: #ffd9c4; --bg: #08111a; --bg-elevated: rgba(12, 20, 30, 0.82); --fg: #edf4fb; diff --git a/web/public/og/agent-relay-home-v20260831.png b/web/public/og/agent-relay-home-v20260831.png new file mode 100644 index 0000000..35666b4 Binary files /dev/null and b/web/public/og/agent-relay-home-v20260831.png differ