From dd0669229582bd8f23723b68514cfb953e3bf573 Mon Sep 17 00:00:00 2001 From: Barry Cape Date: Wed, 23 Sep 2026 00:34:15 -0700 Subject: [PATCH] Redesign the 404 page: lost terminals drifting across the relay Reuse the homepage marquee's agent terminals, cut loose onto diagonal lanes that cross the page at different angles and depths. Each card hesitates, backs up and carries on, wobbles, carries a "lost" status tag, and sheds spinning Claude Code marks. The 0 in 404 is a spinning Claude mark, a delivery-log terminal shows the missing path, and clicking the page sends out a burst of marks. Pointer parallax by depth; reduced motion gets a static scatter. HeroTerminalMarquee now exports HeroTerminalCard and its card data so the 404 can render the same terminals. Co-Authored-By: Claude Opus 5.5 --- web/app/not-found.tsx | 67 +- web/app/not-found/LostField.tsx | 370 +++++++ web/app/not-found/LostMessages.tsx | 98 -- web/app/not-found/RouteReadout.tsx | 46 + web/app/not-found/not-found.module.css | 1049 ++++++++++--------- web/components/home/HeroTerminalMarquee.tsx | 210 ++-- 6 files changed, 1124 insertions(+), 716 deletions(-) create mode 100644 web/app/not-found/LostField.tsx delete mode 100644 web/app/not-found/LostMessages.tsx create mode 100644 web/app/not-found/RouteReadout.tsx diff --git a/web/app/not-found.tsx b/web/app/not-found.tsx index 00c778d0..67f487e9 100644 --- a/web/app/not-found.tsx +++ b/web/app/not-found.tsx @@ -2,12 +2,14 @@ import type { Metadata } from 'next'; import Link from 'next/link'; import { ArrowLeft, BookOpen } from 'lucide-react'; +import { AgentToolLogo } from '../components/AgentToolLogos'; import { LogoIcon, LogoWordmark } from '../components/SiteNav'; -import { LostMessages } from './not-found/LostMessages'; +import { LostField } from './not-found/LostField'; +import { RouteReadout } from './not-found/RouteReadout'; import s from './not-found/not-found.module.css'; export const metadata: Metadata = { - title: 'Message not delivered', + title: 'Lost in the relay', description: 'This Agent Relay route could not be found.', robots: { index: false, @@ -18,42 +20,51 @@ export const metadata: Metadata = { export default function NotFound() { return (
+ +
); diff --git a/web/app/not-found/LostField.tsx b/web/app/not-found/LostField.tsx new file mode 100644 index 00000000..5b8db66e --- /dev/null +++ b/web/app/not-found/LostField.tsx @@ -0,0 +1,370 @@ +'use client'; + +import { useCallback, useEffect, useRef, useState, type CSSProperties } from 'react'; + +import { AgentToolLogo } from '../../components/AgentToolLogos'; +import { + AGENT_META, + HeroTerminalCard, + ROW_ONE, + ROW_THREE, + ROW_TWO, + type TerminalCard, +} from '../../components/home/HeroTerminalMarquee'; +import s from './not-found.module.css'; + +type Depth = 'far' | 'mid' | 'near'; + +type Drifter = { + card: TerminalCard; + /** Where the lane crosses the viewport, as a percentage of each axis. */ + x: number; + y: number; + /** Direction of travel. Every lane is diagonal, none agree with each other. */ + angle: number; + /** Seconds for one full crossing, and how far into it the card starts. */ + duration: number; + delay: number; + depth: Depth; + /** What the agent muttered on its way past. */ + tag: string; + /** Travel in reverse, because some agents are sure it was the other way. */ + reverse?: boolean; + /** Overrides the card's resting tilt, which otherwise leans into its lane. */ + tilt?: number; +}; + +type Spark = { + /** Launch point on the card, in px from the card's top-left. */ + ox: number; + oy: number; + /** Where it flies to, relative to the launch point. */ + sx: number; + sy: number; + spin: number; + size: number; + delay: number; + duration: number; +}; + +type Burst = { id: number; x: number; y: number }; + +const CARDS = [...ROW_ONE, ...ROW_TWO, ...ROW_THREE]; + +const card = (repo: string, agent: TerminalCard['agent']) => + CARDS.find((candidate) => candidate.repo === repo && candidate.agent === agent) ?? CARDS[0]; + +// Ordered so that the first six still read well on a phone, where the rest +// are hidden. +const DRIFTERS: Drifter[] = [ + { + card: card('agentrelay/web', 'claude'), + x: 18, + y: 22, + angle: 24, + duration: 46, + delay: -9, + depth: 'near', + tag: '? /ship-it was right here', + }, + { + card: card('relay/router', 'codex'), + x: 80, + y: 74, + angle: -32, + duration: 52, + delay: -30, + depth: 'near', + tag: '↺ rerouting · attempt 14', + reverse: true, + }, + { + card: card('relay/docs', 'grok'), + x: 84, + y: 18, + angle: 148, + duration: 58, + delay: -21, + depth: 'mid', + tag: 'which channel was it', + }, + { + card: card('relay/infra', 'claude'), + x: 14, + y: 80, + angle: -22, + duration: 50, + delay: -40, + depth: 'mid', + tag: '404 · retry 3/∞', + }, + { + card: card('relay/sdk', 'opencode'), + x: 58, + y: 8, + angle: 38, + duration: 64, + delay: -12, + depth: 'far', + tag: 'asked #general. no reply', + }, + { + card: card('relay/tests', 'claude'), + x: 44, + y: 94, + angle: -48, + duration: 60, + delay: -44, + depth: 'far', + tag: 'pretty sure it was left', + reverse: true, + }, + { + card: card('relay/billing', 'codex'), + x: 4, + y: 48, + angle: 62, + duration: 70, + delay: -5, + depth: 'far', + tag: '⌁ signal lost', + }, + { + card: card('relay/web', 'grok'), + x: 96, + y: 44, + angle: -118, + duration: 56, + delay: -33, + depth: 'mid', + tag: '→ DM to 404: hello?', + }, + { + card: card('relay/cli', 'opencode'), + x: 30, + y: 58, + angle: 16, + duration: 74, + delay: -60, + depth: 'far', + tag: 'reading the map upside down', + tilt: 172, + }, + { + card: card('relay/api', 'claude'), + x: 70, + y: 40, + angle: -14, + duration: 68, + delay: -52, + depth: 'far', + tag: 'handoff acked by nobody', + reverse: true, + }, +]; + +// Claude Code sparks fling off every card. The launch points sit near the +// title bar, where each terminal shows its agent mark. +const SPARKS: Spark[] = [ + { + ox: 44, + oy: 14, + sx: -120, + sy: -150, + spin: 540, + size: 22, + delay: 0, + duration: 3.4, + }, + { + ox: 210, + oy: 12, + sx: 160, + sy: -110, + spin: -620, + size: 16, + delay: 1.1, + duration: 3.1, + }, + { + ox: 260, + oy: 150, + sx: 180, + sy: 120, + spin: 720, + size: 26, + delay: 2.2, + duration: 3.8, + }, + { + ox: 30, + oy: 140, + sx: -170, + sy: 90, + spin: -480, + size: 14, + delay: 2.9, + duration: 2.9, + }, +]; + +const BURST_ICONS = 12; +const BURST_LIFETIME_MS = 1400; + +type DrifterStyle = CSSProperties & Record<`--${string}`, string | number>; + +/** Lean each card a little toward where it is heading, without flipping it. */ +function restingTilt(drifter: Drifter) { + if (drifter.tilt !== undefined) return drifter.tilt; + const heading = ((((drifter.angle + 90) % 180) + 180) % 180) - 90; + return Math.max(-18, Math.min(18, heading * 0.5)); +} + +function drifterStyle(drifter: Drifter, index: number): DrifterStyle { + return { + '--tilt': `${restingTilt(drifter)}deg`, + '--x': `${drifter.x}%`, + '--y': `${drifter.y}%`, + '--angle': `${drifter.angle}deg`, + '--duration': `${drifter.duration}s`, + '--delay': `${drifter.delay}s`, + '--wobble-duration': `${5.2 + (index % 4) * 0.9}s`, + '--wobble-delay': `${-index * 1.3}s`, + }; +} + +function sparkStyle(spark: Spark, index: number): DrifterStyle { + return { + '--ox': `${spark.ox}px`, + '--oy': `${spark.oy}px`, + '--sx': `${spark.sx}px`, + '--sy': `${spark.sy}px`, + '--spin': `${spark.spin}deg`, + '--size': `${spark.size}px`, + '--spark-delay': `${-(spark.delay + index * 0.47)}s`, + '--spark-duration': `${spark.duration}s`, + }; +} + +/** + * The 404 backdrop: the homepage's agent terminals, cut loose from their + * marquee rows and drifting diagonally across the page in every direction. + * They stop, back up, and carry on, shedding spinning Claude Code marks as + * they go. Clicking the empty page sends out a burst of more marks. + * + * Pure decoration, so it is aria-hidden and holds nothing focusable. + */ +export function LostField() { + const fieldRef = useRef(null); + const [bursts, setBursts] = useState([]); + const nextBurst = useRef(0); + + useEffect(() => { + const field = fieldRef.current; + if (!field) return; + + const motion = window.matchMedia('(prefers-reduced-motion: reduce)'); + let frame = 0; + + const onMove = (event: PointerEvent) => { + if (motion.matches || event.pointerType === 'touch') return; + cancelAnimationFrame(frame); + frame = requestAnimationFrame(() => { + const px = event.clientX / window.innerWidth - 0.5; + const py = event.clientY / window.innerHeight - 0.5; + field.style.setProperty('--px', px.toFixed(3)); + field.style.setProperty('--py', py.toFixed(3)); + }); + }; + + window.addEventListener('pointermove', onMove, { passive: true }); + return () => { + cancelAnimationFrame(frame); + window.removeEventListener('pointermove', onMove); + }; + }, []); + + const burst = useCallback((event: PointerEvent) => { + if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; + const target = event.target as Element | null; + if (target?.closest('a, button, [data-no-burst]')) return; + + const id = nextBurst.current++; + setBursts((current) => [...current.slice(-5), { id, x: event.clientX, y: event.clientY }]); + window.setTimeout(() => { + setBursts((current) => current.filter((item) => item.id !== id)); + }, BURST_LIFETIME_MS); + }, []); + + useEffect(() => { + window.addEventListener('pointerdown', burst); + return () => window.removeEventListener('pointerdown', burst); + }, [burst]); + + return ( + <> + + + + + ); +} diff --git a/web/app/not-found/LostMessages.tsx b/web/app/not-found/LostMessages.tsx deleted file mode 100644 index 2f69bb52..00000000 --- a/web/app/not-found/LostMessages.tsx +++ /dev/null @@ -1,98 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import { RotateCcw } from 'lucide-react'; - -import s from './not-found.module.css'; - -const attempts = [ - { - status: 'No route to channel', - note: 'Builder sent /ship-it to a room that does not exist.', - messages: ['anyone here?', 'wrong channel', 'hello?'], - }, - { - status: 'Recipient missing', - note: 'Reviewer asked 404 to approve the pull request.', - messages: ['please review', 'still there?', 'nudge'], - }, - { - status: 'Context wandered off', - note: 'Scout found the page, then forgot where it put it.', - messages: ['found it!', 'wait', 'lost it'], - }, -] as const; - -export function LostMessages() { - const [attempt, setAttempt] = useState(0); - const current = attempts[attempt]; - - return ( -
-
-
- Live misrouting -

{current.status}

-
- undelivered -
- -
-
-
-
- - - - - -
- ? - missing-page - never connected -
- - {current.messages.map((message, index) => ( - - {message} - - ))} - -
- DEAD LETTER - 404 -
-
- -
-

{current.note}

- -
-
- ); -} - -function AgentNode({ - className, - initials, - name, - status, -}: { - className: string; - initials: string; - name: string; - status: string; -}) { - return ( -
- {initials} - - {name} - {status} - -
- ); -} diff --git a/web/app/not-found/RouteReadout.tsx b/web/app/not-found/RouteReadout.tsx new file mode 100644 index 00000000..4fb6d3c5 --- /dev/null +++ b/web/app/not-found/RouteReadout.tsx @@ -0,0 +1,46 @@ +'use client'; + +import { usePathname } from 'next/navigation'; + +import s from './not-found.module.css'; + +/** + * A little Claude Code-style session that tries to deliver to the missing + * path, fails, and admits what happened to the search party. + */ +export function RouteReadout() { + const pathname = usePathname() || '/'; + + return ( +
+
+ + + + + + relay · delivery log +
+
+

+ ❯ + + relay deliver {pathname} + +

+

+ ⎿ + resolving route across 10 agents… +

+

+ ✗ + 404 · nobody is subscribed to this route +

+

+ ↗ + search party dispatched. none have reported back. +

+
+
+ ); +} diff --git a/web/app/not-found/not-found.module.css b/web/app/not-found/not-found.module.css index ec555c8d..961bf855 100644 --- a/web/app/not-found/not-found.module.css +++ b/web/app/not-found/not-found.module.css @@ -1,713 +1,750 @@ +/* ── 404: lost in the relay ─────────────────────────────────────────────── + The homepage marquee's terminals, cut loose. Each card rides its own + diagonal lane across the viewport, hesitates, backs up, and carries on, + while Claude Code marks spin off it. The layers, back to front: + field (lanes) → vignette → nav / content / footer → click bursts. */ + .page { --lost-accent: #74b8e2; + --lost-claude: #d97855; + --lost-error: #f0b39f; + --lost-ink: #edf4fb; + --lost-muted: #a8b8c8; + --lost-bg: #07111a; position: relative; display: grid; grid-template-rows: auto 1fr auto; min-height: 100dvh; overflow: hidden; background: - radial-gradient(circle at 77% 46%, rgba(116, 184, 226, 0.12), transparent 28%), - linear-gradient(145deg, #08111a 0%, #0b1825 46%, #10283a 100%); - color: #edf4fb; + radial-gradient(circle at 50% 46%, rgba(217, 120, 85, 0.09), transparent 34%), + radial-gradient(circle at 18% 12%, rgba(116, 184, 226, 0.12), transparent 38%), + linear-gradient(160deg, #07111a 0%, #0a1826 48%, #0e2536 100%); + color: var(--lost-ink); isolation: isolate; } .page::before { position: fixed; inset: 0; - z-index: -1; - background-image: radial-gradient(rgba(159, 212, 245, 0.17) 0.75px, transparent 0.75px); - background-size: 24px 24px; - mask-image: linear-gradient(to right, transparent, black 30%, black 80%, transparent); + z-index: 0; + background-image: radial-gradient(rgba(159, 212, 245, 0.16) 0.75px, transparent 0.75px); + background-size: 26px 26px; + mask-image: radial-gradient(ellipse at center, black 20%, transparent 78%); content: ''; pointer-events: none; } -.nav, -.footer { - position: relative; - z-index: 2; - display: flex; - width: min(100% - 48px, 1320px); - margin-inline: auto; - align-items: center; - justify-content: space-between; -} +/* ── Lanes ─────────────────────────────────────────────────────────────── */ -.nav { - min-height: 76px; - border-bottom: 1px solid rgba(116, 184, 226, 0.16); +.field { + --px: 0; + --py: 0; + --field-scale: 1; + position: fixed; + inset: 0; + z-index: 0; + overflow: hidden; + pointer-events: none; } -.brand { - display: flex; - align-items: center; - gap: 10px; - color: #edf4fb; - text-decoration: none; +.lane { + position: absolute; + top: var(--y); + left: var(--x); + width: 0; + height: 0; + opacity: var(--opacity); + /* Parallax sits outside the lane rotation so the pointer always pushes the + field in screen space, deeper layers moving less. */ + transform: translate3d( + calc(var(--px) * var(--shift) * -1px), + calc(var(--py) * var(--shift) * -1px), + 0 + ); + transition: transform 900ms var(--ease-out-quint); +} + +.far { + --scale: 0.6; + --shift: 18; + --opacity: 0.5; + z-index: 1; + filter: blur(1.4px) saturate(0.8); } -.brand > svg:first-child { - width: 30px; - height: 25px; - color: var(--lost-accent); +.mid { + --scale: 0.8; + --shift: 36; + --opacity: 0.78; + z-index: 2; } -.brand > svg:last-child { - width: auto; - height: 21px; +.near { + --scale: 1; + --shift: 60; + --opacity: 0.92; + z-index: 3; } -.routeCode, -.liveLabel, -.deliveryState, -.deadLetter span, -.footer { - font-family: var(--font-geist-mono), monospace; +.heading { + transform: rotate(var(--angle)) scale(calc(var(--scale) * var(--field-scale))); } -.routeCode { - color: rgba(168, 184, 200, 0.72); - font-size: 0.72rem; - letter-spacing: 0.08em; +.travel { + animation: lostTravel var(--duration) ease-in-out var(--delay) infinite; } -.layout { - display: grid; - grid-template-columns: minmax(0, 0.82fr) minmax(520px, 1.18fr); - width: min(100% - 48px, 1320px); - margin: auto; - align-items: center; - gap: clamp(48px, 7vw, 112px); - padding-block: 36px; +.lane[data-reverse] .travel { + animation-direction: reverse; } -.copy { +.wobble { + --rest: calc(var(--tilt) - var(--angle)); position: relative; - z-index: 1; - padding-block: 58px; + width: max-content; + transform: translate(-50%, -50%) rotate(var(--rest)); + animation: lostWobble var(--wobble-duration) ease-in-out var(--wobble-delay) infinite alternate; } -.errorCode { - position: absolute; - top: -0.42em; - left: -0.08em; - z-index: -1; - color: rgba(116, 184, 226, 0.055); - font-family: var(--font-heading), sans-serif; - font-size: clamp(10rem, 21vw, 20rem); - font-weight: 700; - letter-spacing: -0.09em; - line-height: 1; - user-select: none; +.wobble .lostCard { + margin: 0; + box-shadow: + 0 0 0 1px rgba(116, 184, 226, 0.08), + 0 28px 70px rgba(0, 0, 0, 0.45); } -.kicker { - margin: 0 0 22px; - color: var(--lost-accent); - font-family: var(--font-geist-mono), monospace; - font-size: 0.78rem; - font-weight: 600; - letter-spacing: 0.12em; - text-transform: uppercase; +.wobble .lostCard[data-agent='claude'] { + border-color: rgba(217, 120, 85, 0.34); + box-shadow: + 0 0 0 1px rgba(217, 120, 85, 0.1), + 0 0 44px rgba(217, 120, 85, 0.14), + 0 28px 70px rgba(0, 0, 0, 0.45); } -.copy h1 { - max-width: 10ch; - margin: 0; - color: #f1f7fc; - font-family: var(--font-heading), sans-serif; - font-size: clamp(3rem, 5.5vw, 5.9rem); - font-weight: 560; - letter-spacing: -0.065em; - line-height: 0.98; - text-wrap: balance; +.lostTag { + position: absolute; + bottom: 100%; + left: 10px; + display: inline-flex; + align-items: center; + gap: 6px; + margin-bottom: 9px; + padding: 5px 9px; + border: 1px dashed rgba(240, 179, 159, 0.45); + border-radius: 999px; + background: rgba(52, 23, 22, 0.55); + color: var(--lost-error); + font-family: var(--font-geist-mono), monospace; + font-size: 0.66rem; + letter-spacing: 0.02em; + white-space: nowrap; } -.lede { - max-width: 430px; - margin: 26px 0 0; - color: #a8b8c8; - font-size: clamp(1rem, 1.25vw, 1.14rem); - line-height: 1.65; +.lostTag::after { + width: 6px; + height: 11px; + background: currentColor; + content: ''; + animation: lostCaret 1s steps(2, end) infinite; } -.actions { - display: flex; - flex-wrap: wrap; - gap: 12px; - margin-top: 34px; +/* ── Claude Code sparks ────────────────────────────────────────────────── */ + +.spark { + position: absolute; + top: var(--oy); + left: var(--ox); + width: var(--size); + height: var(--size); + margin: calc(var(--size) / -2) 0 0 calc(var(--size) / -2); + opacity: 0; + filter: drop-shadow(0 0 7px rgba(217, 120, 85, 0.75)); + animation: sparkFly var(--spark-duration) cubic-bezier(0.18, 0.7, 0.3, 1) var(--spark-delay) + infinite; + will-change: transform, opacity; } -.primaryAction, -.secondaryAction { - display: inline-flex; - min-height: 48px; - align-items: center; - justify-content: center; - gap: 9px; - padding: 0 20px; - border: 1px solid transparent; - border-radius: 10px; - font-size: 0.94rem; - font-weight: 650; - text-decoration: none; - white-space: nowrap; - transition: - transform 180ms var(--ease-out-quint), - background 180ms ease, - border-color 180ms ease; +.sparkIcon { + display: block; + width: 100%; + height: 100%; } -.primaryAction { - border-color: #90c9eb; - background: #74b8e2; - color: #07111a; +.sparkIcon path { + fill: var(--lost-claude); } -.secondaryAction { - border-color: rgba(159, 212, 245, 0.24); - background: rgba(8, 17, 26, 0.35); - color: #dbe9f4; +.bursts { + position: fixed; + inset: 0; + z-index: 20; + pointer-events: none; } -.primaryAction:hover, -.secondaryAction:hover { - transform: translateY(-2px); +.burst { + position: absolute; + width: 0; + height: 0; } -.primaryAction:hover { - background: #94cbef; +.burstIcon { + position: absolute; + top: 0; + left: 0; + width: 24px; + height: 24px; + filter: drop-shadow(0 0 10px rgba(217, 120, 85, 0.85)); + animation: burstFly 1.2s cubic-bezier(0.16, 1, 0.3, 1) forwards; } -.secondaryAction:hover { - border-color: rgba(159, 212, 245, 0.52); - background: rgba(116, 184, 226, 0.08); +.burstIcon:nth-child(3n) { + width: 16px; + height: 16px; } -.primaryAction:active, -.secondaryAction:active { - transform: scale(0.98); +.burstIcon:nth-child(4n + 1) { + width: 30px; + height: 30px; } -.primaryAction:focus-visible, -.secondaryAction:focus-visible, -.networkFooter button:focus-visible { - outline: 3px solid rgba(237, 244, 251, 0.9); - outline-offset: 3px; +/* ── Foreground ────────────────────────────────────────────────────────── */ + +.vignette { + position: fixed; + inset: 0; + z-index: 1; + background: radial-gradient( + ellipse 44% 56% at 50% 52%, + rgba(7, 17, 26, 0.94) 0%, + rgba(7, 17, 26, 0.8) 42%, + rgba(7, 17, 26, 0) 76% + ); + pointer-events: none; } -.network { - overflow: hidden; - border: 1px solid rgba(116, 184, 226, 0.2); - border-radius: 14px; - background: rgba(8, 17, 26, 0.78); - box-shadow: - inset 0 1px 0 rgba(255, 255, 255, 0.05), - 0 36px 100px rgba(0, 0, 0, 0.3); - backdrop-filter: blur(18px); +.nav, +.content, +.footer { + position: relative; + z-index: 2; } -.networkHeader, -.networkFooter { +.nav, +.footer { display: flex; + width: min(100% - 48px, 1320px); + margin-inline: auto; align-items: center; justify-content: space-between; - gap: 24px; - padding: 17px 19px; + gap: 16px; } -.networkHeader { - border-bottom: 1px solid rgba(116, 184, 226, 0.14); +.nav { + min-height: 76px; } -.networkHeader p, -.networkFooter p { - margin: 0; +.brand { + display: flex; + align-items: center; + gap: 10px; + color: var(--lost-ink); + text-decoration: none; +} + +.brand > svg:first-child { + width: 30px; + height: 25px; + color: var(--lost-accent); } -.networkHeader p { - margin-top: 4px; - color: #dceaf5; - font-size: 0.92rem; - font-weight: 600; +.brand > svg:last-child { + width: auto; + height: 21px; } -.liveLabel { - color: #8296aa; - font-size: 0.67rem; - letter-spacing: 0.1em; - text-transform: uppercase; +.brand:focus-visible { + border-radius: 6px; + outline: 3px solid rgba(237, 244, 251, 0.9); + outline-offset: 4px; } -.deliveryState { - border: 1px solid rgba(240, 179, 159, 0.3); - border-radius: 999px; - background: rgba(193, 103, 75, 0.11); - color: #f0b39f; - font-size: 0.67rem; - letter-spacing: 0.06em; - padding: 6px 9px; +.footer, +.readout { + font-family: var(--font-geist-mono), monospace; } -.stage { - position: relative; - height: 420px; - overflow: hidden; - background: - linear-gradient(rgba(116, 184, 226, 0.055) 1px, transparent 1px), - linear-gradient(90deg, rgba(116, 184, 226, 0.055) 1px, transparent 1px); - background-size: 48px 48px; +.content { + display: flex; + flex-direction: column; + align-items: center; + width: min(100% - 32px, 640px); + margin: auto; + padding-block: 24px 40px; + text-align: center; } -.stage::after { - position: absolute; - inset: 0; - background: radial-gradient(circle at 54% 49%, transparent 0, rgba(8, 17, 26, 0.22) 70%); - content: ''; - pointer-events: none; +/* The 0 in 404 is the Claude Code mark, spinning inside a dashed orbit. */ +.code { + display: flex; + align-items: center; + justify-content: center; + gap: 0.04em; + color: var(--lost-ink); + font-family: var(--font-heading), sans-serif; + font-size: clamp(6.5rem, 17vw, 12.5rem); + font-weight: 700; + letter-spacing: -0.06em; + line-height: 0.9; + user-select: none; } -.connection { - position: absolute; - z-index: 0; - height: 1px; - overflow: hidden; - background: rgba(116, 184, 226, 0.22); - transform-origin: left center; +.digit { + position: relative; + display: inline-block; + text-shadow: 0 10px 60px rgba(116, 184, 226, 0.25); } -.connection::after { +.digit::before, +.digit::after { position: absolute; inset: 0; - background: linear-gradient(90deg, transparent, #74b8e2, transparent); - content: ''; - transform: translateX(-100%); - animation: signal 2.8s var(--ease-out-quint) infinite; + content: attr(data-text); + opacity: 0; } -.connectionOne { - top: 104px; - left: 132px; - width: 310px; - transform: rotate(17deg); +.digit::before { + color: var(--lost-accent); + animation: glitchA 4.6s steps(1, end) infinite; } -.connectionTwo { - top: 296px; - left: 132px; - width: 312px; - transform: rotate(-18deg); +.digit::after { + color: var(--lost-claude); + animation: glitchB 4.6s steps(1, end) infinite; } -.connectionTwo::after { - animation-delay: 0.8s; +.digit + .zero + .digit::before { + animation-delay: -1.7s; } -.connectionThree { - top: 209px; - right: 102px; - width: 240px; - transform: rotate(180deg); +.digit + .zero + .digit::after { + animation-delay: -1.7s; } -.connectionThree::after { - animation-delay: 1.5s; +.zero { + position: relative; + display: grid; + width: 0.78em; + height: 0.78em; + margin-inline: 0.06em; + place-items: center; } -.agent, -.missingNode { +.zero::before, +.zero::after { position: absolute; - z-index: 2; + border-radius: 50%; + content: ''; } -.agent { - display: flex; - align-items: center; - gap: 10px; - min-width: 126px; - padding: 10px; - border: 1px solid rgba(116, 184, 226, 0.19); - border-radius: 12px; - background: rgba(15, 31, 46, 0.94); - box-shadow: 0 12px 28px rgba(0, 0, 0, 0.2); +.zero::before { + inset: -2%; + border: 2px dashed rgba(116, 184, 226, 0.35); + animation: orbit 22s linear infinite reverse; } -.agent > span:last-child { - display: grid; - gap: 3px; +.zero::after { + inset: 16%; + background: radial-gradient(circle, rgba(217, 120, 85, 0.35), transparent 70%); + filter: blur(18px); + animation: zeroPulse 3.2s ease-in-out infinite; } -.agent strong, -.missingNode strong { - color: #dceaf5; - font-size: 0.78rem; +.zeroMark { + position: relative; + z-index: 1; + width: 74%; + height: 74%; + filter: drop-shadow(0 0 22px rgba(217, 120, 85, 0.55)); + animation: orbit 12s linear infinite; } -.agent small, -.missingNode small { - color: #8296aa; - font-family: var(--font-geist-mono), monospace; - font-size: 0.62rem; +.zeroMark path { + fill: var(--lost-claude); } -.avatar { - display: grid; - width: 34px; - height: 34px; - flex: 0 0 auto; - place-items: center; - border-radius: 8px; - background: #18344b; - color: #a8d8f5; - font-family: var(--font-geist-mono), monospace; - font-size: 0.66rem; - font-weight: 700; +.title { + max-width: 16ch; + margin: 28px 0 0; + color: #f1f7fc; + font-family: var(--font-heading), sans-serif; + font-size: clamp(2.1rem, 4.4vw, 3.5rem); + font-weight: 560; + letter-spacing: -0.05em; + line-height: 1.02; + text-wrap: balance; } -.builder { - top: 68px; - left: 24px; +.lede { + max-width: 440px; + margin: 18px 0 0; + color: var(--lost-muted); + font-size: clamp(1rem, 1.25vw, 1.1rem); + line-height: 1.6; + text-wrap: pretty; } -.scout { - right: 24px; - bottom: 55px; +.readout { + width: min(100%, 520px); + margin-top: 30px; + overflow: hidden; + border: 1px solid rgba(116, 184, 226, 0.2); + border-radius: 10px; + background: rgba(8, 22, 36, 0.86); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.04), + 0 30px 80px rgba(0, 0, 0, 0.4); + text-align: left; + backdrop-filter: blur(14px); } -.reviewer { - bottom: 54px; - left: 24px; +.readoutBar { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 12px; + border-bottom: 1px solid rgba(116, 184, 226, 0.14); + background: rgba(15, 33, 48, 0.94); + color: #8296aa; + font-size: 0.68rem; } -.missingNode { - top: 156px; - left: 50%; - display: grid; - width: 134px; - height: 112px; - place-items: center; - align-content: center; - gap: 3px; - border: 1px dashed rgba(240, 179, 159, 0.44); - border-radius: 12px; - background: rgba(52, 23, 22, 0.27); - transform: translateX(-50%); +.readoutTraffic { + display: inline-flex; + gap: 5px; } -.missingNode > span { - display: grid; - width: 31px; - height: 31px; - margin-bottom: 4px; - place-items: center; +.readoutTraffic span { + width: 8px; + height: 8px; border-radius: 50%; - background: rgba(193, 103, 75, 0.19); - color: #f0b39f; - font-family: var(--font-geist-mono), monospace; - font-weight: 700; + background: rgba(159, 212, 245, 0.18); } -.packet { - position: absolute; - z-index: 3; - padding: 7px 9px; - border: 1px solid rgba(116, 184, 226, 0.32); - border-radius: 8px; - background: #142c40; - color: #b9dff5; - font-family: var(--font-geist-mono), monospace; - font-size: 0.61rem; - box-shadow: 0 8px 20px rgba(0, 0, 0, 0.28); - opacity: 0; +.readoutTraffic span:first-child { + background: #e0715b; +} + +.readoutBody { + display: grid; + gap: 7px; + padding: 14px 16px 16px; + font-size: 0.78rem; + line-height: 1.4; } -.packet1 { - top: 102px; - left: 154px; - animation: packetOne 4.2s var(--ease-out-expo) infinite; +.readoutBody p { + display: grid; + grid-template-columns: 14px minmax(0, 1fr); + gap: 8px; + margin: 0; + animation: lineIn 0.5s var(--ease-out-expo) both; } -.packet2 { - bottom: 107px; - left: 142px; - animation: packetTwo 4.2s 1.35s var(--ease-out-expo) infinite; +.readoutBody p > span:last-child { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } -.packet3 { - right: 126px; - bottom: 97px; - animation: packetThree 4.2s 2.6s var(--ease-out-expo) infinite; +.readoutPrompt { + color: var(--lost-ink); + animation-delay: 0.35s !important; } -.deadLetter { - position: absolute; - top: 26px; - right: 24px; - z-index: 2; - display: grid; - justify-items: end; - color: rgba(240, 179, 159, 0.78); +.readoutPrompt > span:first-child { + color: var(--lost-claude); } -.deadLetter span { - font-size: 0.58rem; - letter-spacing: 0.12em; +.readoutPrompt code { + color: var(--lost-claude); + font-family: inherit; } -.deadLetter strong { - color: rgba(240, 179, 159, 0.28); - font-family: var(--font-heading), sans-serif; - font-size: 3.2rem; - letter-spacing: -0.08em; - line-height: 0.9; +.readoutTool { + color: #8fa3b5; + animation-delay: 0.95s !important; } -.networkFooter { - min-height: 68px; - border-top: 1px solid rgba(116, 184, 226, 0.14); +.readoutError { + color: var(--lost-error); + animation-delay: 1.65s !important; } -.networkFooter p { - max-width: 380px; - color: #8fa3b5; - font-size: 0.78rem; - line-height: 1.45; +.readoutRelay { + color: var(--lost-accent); + animation-delay: 2.3s !important; } -.networkFooter button { - display: inline-flex; - flex: 0 0 auto; - align-items: center; - gap: 7px; - padding: 9px 11px; - border: 1px solid rgba(116, 184, 226, 0.23); - border-radius: 8px; - background: rgba(116, 184, 226, 0.07); - color: #c7dceb; - font-family: inherit; - font-size: 0.73rem; - font-weight: 600; - cursor: pointer; - transition: - background 160ms ease, - transform 160ms var(--ease-out-quint); +.readoutRelay > span:last-child::after { + display: inline-block; + width: 7px; + height: 13px; + margin-left: 6px; + vertical-align: -2px; + background: var(--lost-accent); + content: ''; + animation: lostCaret 1s steps(2, end) infinite; } -.networkFooter button:hover { - background: rgba(116, 184, 226, 0.14); +.actions { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 28px; } -.networkFooter button:active { - transform: scale(0.97); +/* Back to home is the site's own primary button; the docs link keeps a dark + glass treatment so it reads on this page whatever the site theme is. */ +.primaryAction { + composes: btn btn-primary from global; +} + +.secondaryAction { + composes: btn from global; + border: 1.5px solid rgba(159, 212, 245, 0.24); + background: rgba(8, 17, 26, 0.6); + color: #dbe9f4; + backdrop-filter: blur(8px); } -.networkFooter button:hover svg { - transform: rotate(-24deg); +.secondaryAction:hover { + border-color: rgba(159, 212, 245, 0.52); + background: rgba(116, 184, 226, 0.1); } -.networkFooter button svg { - transition: transform 220ms var(--ease-out-quint); +.primaryAction:focus-visible, +.secondaryAction:focus-visible { + outline: 3px solid rgba(237, 244, 251, 0.9); + outline-offset: 3px; } .footer { min-height: 58px; - border-top: 1px solid rgba(116, 184, 226, 0.13); color: rgba(143, 163, 181, 0.72); - font-size: 0.65rem; + font-size: 0.66rem; } -@keyframes signal { - 45%, - 100% { - transform: translateX(100%); - } +.hint { + color: rgba(217, 120, 85, 0.8); + animation: hintPulse 2.8s ease-in-out infinite; } -@keyframes packetOne { - 0%, - 8% { - opacity: 0; - transform: translate3d(0, 0, 0) rotate(0deg); +/* ── Keyframes ─────────────────────────────────────────────────────────── */ + +/* Off one edge, most of the way across, a confused step back, then off the + far edge. ease-in-out per segment makes each leg start and stop. */ +@keyframes lostTravel { + 0% { + transform: translate3d(-68vmax, 0, 0); } - 15% { - opacity: 1; + 38% { + transform: translate3d(-6vmax, 0, 0); } - 54% { - opacity: 1; - transform: translate3d(205px, 57px, 0) rotate(4deg); + 47% { + transform: translate3d(-16vmax, 0, 0); + } + 55% { + transform: translate3d(-11vmax, 0, 0); } - 62%, 100% { - opacity: 0; - transform: translate3d(205px, 84px, 0) rotate(9deg); + transform: translate3d(68vmax, 0, 0); } } -@keyframes packetTwo { - 0%, - 8% { - opacity: 0; - transform: translate3d(0, 0, 0) rotate(0deg); +@keyframes lostWobble { + from { + transform: translate(-50%, calc(-50% - 12px)) rotate(calc(var(--rest) - 5deg)); } - 15% { - opacity: 1; - } - 54% { - opacity: 1; - transform: translate3d(215px, -70px, 0) rotate(-3deg); - } - 62%, - 100% { - opacity: 0; - transform: translate3d(215px, -43px, 0) rotate(-8deg); + to { + transform: translate(-50%, calc(-50% + 12px)) rotate(calc(var(--rest) + 5deg)); } } -@keyframes packetThree { - 0%, - 8% { +@keyframes sparkFly { + 0% { opacity: 0; - transform: translate3d(0, 0, 0) rotate(0deg); + transform: translate3d(0, 0, 0) rotate(0deg) scale(0.2); } - 15% { + 12% { opacity: 1; + transform: translate3d(calc(var(--sx) * 0.12), calc(var(--sy) * 0.12), 0) + rotate(calc(var(--spin) * 0.15)) scale(1.1); } - 54% { - opacity: 1; - transform: translate3d(-142px, -87px, 0) rotate(3deg); + 70% { + opacity: 0.85; } - 62%, 100% { opacity: 0; - transform: translate3d(-142px, -61px, 0) rotate(7deg); + transform: translate3d(var(--sx), var(--sy), 0) rotate(var(--spin)) scale(0.35); } } -@media (max-width: 1040px) { - .page { - overflow: auto; +@keyframes burstFly { + from { + opacity: 1; + transform: translate(-50%, -50%) rotate(0deg) scale(0.2); } - - .layout { - grid-template-columns: 1fr; - max-width: 760px; - gap: 22px; - padding-block: 24px 48px; + to { + opacity: 0; + transform: translate(calc(-50% + var(--bx)), calc(-50% + var(--by))) rotate(var(--bspin)) + scale(1); } +} - .copy { - padding-block: 54px 20px; +@keyframes orbit { + to { + transform: rotate(360deg); } +} - .copy h1 { - max-width: 12ch; +@keyframes zeroPulse { + 0%, + 100% { + opacity: 0.55; + transform: scale(0.9); } - - .errorCode { - top: -0.28em; - font-size: clamp(9rem, 30vw, 15rem); + 50% { + opacity: 1; + transform: scale(1.12); } } -@media (max-width: 640px) { - .nav, - .layout, - .footer { - width: min(100% - 32px, 1320px); +@keyframes glitchA { + 0%, + 90%, + 100% { + opacity: 0; + transform: none; + clip-path: inset(0 0 0 0); } - - .nav { - min-height: 66px; + 91% { + opacity: 0.9; + transform: translate(-6px, 2px); + clip-path: inset(12% 0 58% 0); } - - .routeCode { - display: none; + 93% { + opacity: 0.9; + transform: translate(5px, -2px); + clip-path: inset(62% 0 10% 0); } - - .copy { - padding-top: 46px; + 95% { + opacity: 0.8; + transform: translate(-3px, 0); + clip-path: inset(34% 0 40% 0); } +} - .copy h1 { - font-size: clamp(2.8rem, 14vw, 4.4rem); +@keyframes glitchB { + 0%, + 90%, + 100% { + opacity: 0; + transform: none; + clip-path: inset(0 0 0 0); } - - .lede { - margin-top: 20px; + 92% { + opacity: 0.85; + transform: translate(6px, -1px); + clip-path: inset(48% 0 22% 0); } - - .actions { - display: grid; - grid-template-columns: 1fr; - margin-top: 28px; + 94% { + opacity: 0.85; + transform: translate(-4px, 2px); + clip-path: inset(6% 0 70% 0); } +} - .networkHeader, - .networkFooter { - align-items: flex-start; +@keyframes lineIn { + from { + opacity: 0; + transform: translateX(-8px); + } + to { + opacity: 1; + transform: none; } +} - .stage { - height: 430px; +@keyframes lostCaret { + 50% { + opacity: 0; } +} - .builder { - top: 52px; - left: 14px; +@keyframes hintPulse { + 0%, + 100% { + opacity: 0.45; } + 50% { + opacity: 1; + } +} - .reviewer { - bottom: 45px; - left: 14px; +/* ── Responsive ────────────────────────────────────────────────────────── */ + +@media (max-width: 900px) { + .field { + --field-scale: 0.8; } - .scout { - right: 14px; - bottom: 45px; + .lane:nth-child(n + 9) { + display: none; } +} - .missingNode { - top: 160px; +@media (max-width: 640px) { + .field { + --field-scale: 0.7; } - .connectionOne { - top: 91px; - left: 113px; - width: 44vw; - transform: rotate(30deg); + .lane:nth-child(n + 7) { + display: none; } - .connectionTwo { - top: 338px; - left: 108px; - width: 44vw; - transform: rotate(-35deg); + .vignette { + background: radial-gradient( + ellipse 90% 60% at 50% 50%, + rgba(7, 17, 26, 0.88) 0%, + rgba(7, 17, 26, 0.66) 50%, + rgba(7, 17, 26, 0.1) 100% + ); } - .connectionThree { - display: none; + .nav, + .footer { + width: min(100% - 32px, 1320px); } - .packet1, - .packet2, - .packet3 { - display: none; + .nav { + min-height: 66px; } - .deadLetter { - top: 21px; - right: 16px; + .title { + margin-top: 22px; } - .networkFooter { - flex-direction: column; - gap: 13px; + .readoutBody { + font-size: 0.72rem; } - .networkFooter button { + .actions { + display: grid; width: 100%; - justify-content: center; + grid-template-columns: 1fr; } .footer { @@ -719,32 +756,36 @@ } } +/* No drifting, no sparks, no glitching: the cards rest where their lanes + cross the page and the log is shown in full. */ @media (prefers-reduced-motion: reduce) { - .connection::after, - .packet { + .travel, + .wobble, + .digit::before, + .digit::after, + .zero::before, + .zero::after, + .zeroMark, + .lostTag::after, + .readoutRelay > span:last-child::after, + .hint { animation: none; } - .packet { - opacity: 1; - } - - .packet1 { - transform: translate3d(110px, 31px, 0); + .wobble { + transform: translate(-50%, -50%) rotate(var(--rest)); } - .packet2 { - transform: translate3d(110px, -36px, 0); + .spark, + .bursts { + display: none; } - .packet3 { - transform: translate3d(-80px, -45px, 0); + .readoutBody p { + animation: none; } - .primaryAction, - .secondaryAction, - .networkFooter button, - .networkFooter button svg { + .lane { transition: none; } } diff --git a/web/components/home/HeroTerminalMarquee.tsx b/web/components/home/HeroTerminalMarquee.tsx index 13b320db..11d2cc40 100644 --- a/web/components/home/HeroTerminalMarquee.tsx +++ b/web/components/home/HeroTerminalMarquee.tsx @@ -15,7 +15,7 @@ type TerminalLine = { text: string; }; -type TerminalCard = { +export 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. */ @@ -26,7 +26,7 @@ type TerminalCard = { size: 'sm' | 'md' | 'lg'; }; -type TerminalStyle = CSSProperties & { +export type TerminalStyle = CSSProperties & { '--line-delay'?: string; '--term-cycle'?: string; '--term-phase'?: string; @@ -41,7 +41,7 @@ type RelayRoute = { target: { x: number; y: number }; }; -const AGENT_META: Record< +export const AGENT_META: Record< TerminalAgent, { activityGlyph: string; @@ -170,7 +170,7 @@ const RELAY_ROUTES: RelayRoute[] = [ * 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[] = [ +export const ROW_ONE: TerminalCard[] = [ { agent: 'claude', repo: 'agentrelay/web', @@ -223,7 +223,7 @@ const ROW_ONE: TerminalCard[] = [ }, ]; -const ROW_TWO: TerminalCard[] = [ +export const ROW_TWO: TerminalCard[] = [ { agent: 'grok', repo: 'relay/web', @@ -266,7 +266,7 @@ const ROW_TWO: TerminalCard[] = [ }, ]; -const ROW_THREE: TerminalCard[] = [ +export const ROW_THREE: TerminalCard[] = [ { agent: 'grok', repo: 'relay/search', @@ -427,16 +427,10 @@ function GrokTerminalBody({ card }: { card: TerminalCard }) { {terminalLineCopy(prompt)}
- + ◆ user_prompt_submit - + ◆ Thought for 3.2s +
+ + + + + + + + {meta.label} + + {showAvatar && avatar && ( + {avatar.name} + )} +
+ + {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} + +
+
+ )} + + ); +} + const TEAM_AVATARS = [ { src: '/authors/will.png', name: 'Will Washburn' }, { src: '/authors/khaliq.jpeg', name: 'Khaliq Gant' }, @@ -673,78 +758,16 @@ function TerminalRow({ }; return ( -
-
- - - - - - - - {meta.label} - - {showAvatars && ( - {avatar.name} - )} -
- - {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} - -
-
- )} -
+ /> ); - }) + }), )}
@@ -755,9 +778,24 @@ export function HeroTerminalMarquee({ showAvatars = false }: { showAvatars?: boo return ( ); }