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 ( ); }