Hero-to-logos is canon

The design system.

Calm surface, live machinery underneath. A working reference for the site — tokens, patterns, and the why behind every decision. Source of truth lives in src/index.css and docs/design-system/; this page renders both.

overviewcanonicalsrc/index.css

LangWatch design system

A working reference for the look and feel of the LangWatch site. Hero-to-logos is canon — everything documented here is shipping in src/components/variants/VariantHero.tsx and src/components/CustomerLogos.tsx today. Sections below the customer logos are not in scope yet (they are still being worked out in code and shouldn't be normalized prematurely).

The design system is two-faced on purpose:

  • Humans browse the rendered page at /design-system — swatches, live patterns, prose explanations.
  • AI / LLMs read these markdown files directly. Every file starts with frontmatter (status, code references, token list) so a model can answer "what is canon?" without parsing prose.

The CSS variables in src/index.css @theme are the only source of truth for tokens. This documentation describes how to use them, why they exist, and where they show up. If the docs and @theme disagree, the CSS wins.

Identity in one line

Calm surface, live machinery underneath.

A serious engineering tool presented the way a premium consumer product would be: mostly white space, quiet ink text, one living gradient, and a real working simulation of the product as the centerpiece (not a screenshot). The page feels still and confident; the proof of capability is alive.

The three references

  • Anthropic — warm paper white, charcoal ink, restraint, text-first surfaces. The Claude demo windows quote Anthropic's own aesthetic (terminal black with clay orange, Cowork paper with serif narration) with near-pixel fidelity.
  • Apple — obsession with physical detail. The nav is real refractive glass (an actual displacement map, not a blur), and its text and logo invert per-pixel as they pass over dark content. If a material can't be simulated properly, it isn't used at all.
  • Editorial serif over mesh (the Orchid-style treatment) — a large, quiet serif statement, left-aligned, floating over a slowly drifting mesh gradient that fades into the page.

Personality words: precise, warm, alive, understated. Playful only in hidden corners.

What's documented here

The hero

The site ships a single hero (VariantHero, the Orchid treatment): the announcement pill, heading copy, subhead, CTAs, and customer logos over a pink mesh with protectSide="left" and fadeBottom. Earlier comparison variants were collapsed away; this is now the canonical and only hero. A blue mesh accent (#91b2f2) is kept as a side-by-side comparison option for the mesh hue only.

The patterns in this document describe that hero.

Reading the frontmatter

Every doc in this folder starts with a YAML block. Keys you'll see:

  • status — canonical (shipping and decided), provisional (in flux), or debt (known wrong, see tokens-* or Known debt sections).
  • code — path:lines references where this pattern lives. Treat these as authoritative; the prose is commentary.
  • tokens-used — which @theme tokens this pattern depends on. Useful for "if I change this token, what breaks?"
  • related — sibling docs worth reading together.

If you're an AI assistant editing this codebase: read the frontmatter to find the live code, read the live code, then come back to the prose only for the why. Don't trust prose over code.

Out of scope (deliberately)

  • Sections below the customer logos (SectionWhy, SectionLangy, SectionScenario, SectionPillars, SectionGovernance, SectionGateway, SectionBasics, SectionEnterprise, SectionCustomers, SectionCTA). These are still being designed; documenting them now would freeze decisions that aren't made yet.
  • Storybook stories (already partially set up via *.stories.tsx). This design system complements Storybook — patterns link to their stories where they exist, but stories are not duplicated here.
  • Email, marketing assets, social cards. Out of scope; this is the website design system.

Known debt

The brief committed alongside this hero recorded these known gaps. They are not fixed in this revision (intentionally — we are documenting reality, not aspiration). See the per-token "Known debt" sections for detail.

  1. --font-sans lists Inter first; Inter is not loaded. The body currently renders in the system stack. Geist Variable is bundled but not in the token. See tokens/type.md.
  2. --font-mono lists JetBrains Mono (not loaded), falls back to system mono. See tokens/type.md.
  3. The announcement teal (rgba(20,184,166,0.13) + rgb(15,118,110)) and the subhead ink-blue (#39445c) are hardcoded in VariantHero.tsx. They should be tokens. See tokens/color.md and patterns/announcement-pill.md.
  4. The Claude demo palette (terminal #15130f + clay #d97757, Cowork paper #faf9f5) is hardcoded inside AgentWorkspace.tsx. Reserved namespace, not site UI tokens. See patterns/agent-workspace.md.
  5. Per-logo optical sizing (h + mt) is currently inline in CustomerLogos.tsx. Documented as a pattern at patterns/customer-logos.md, not yet abstracted.
principlescanonical

Principles

Ten rules. Each one has a why (the reason it exists) and a when to break it (the edge case). If you're making a judgment call, the why matters more than the rule.


1. Show the product, never a picture of it

The hero demo (AgentWorkspace) is a faithful functional recreation: real component structure, real data shapes, typing animation, output that streams in while the agent works. Screenshots age and read as marketing; a living demo is evidence.

Why — Engineers identify marketing screenshots in a glance. A real running UI says "we know what we're doing" without saying it. The cost is real components instead of static images; the payoff is the visitor seeing the product work.

When to break it — Never inside the hero band. Acceptable below the fold for sections that need a context shot (e.g., a screenshot of a dashboard inside an editorial card), but the demo above the fold is non-negotiable.

In code — src/components/AgentWorkspace.tsx, src/components/ClaudeTerminal.tsx, src/components/ClaudeCowork.tsx. See patterns/agent-workspace.md.


2. One living element per view

The mesh gradient drifts continuously; it carries the brand energy. Everything else is still until it has a reason to move (the assistant typing, the output streaming, a selector pill sliding). Never two competing animated focal points.

Why — More than one ambient animation in view splits attention and reads as decoration. One drift = one heartbeat = one focal point.

When to break it — A second motion is fine if it is triggered (a CTA hover, a tab switch). Ambient motion is the rule.

In code — MeshHeroBackground (continuous), ClaudeTerminal / ClaudeCowork (responsive, plays through once). See patterns/mesh-background.md.


3. Soft transitions, no hard edges

Backgrounds dissolve into page white: a huge blurry radial protects the text side, a linear fade ends the mesh at the bottom. A visible straight line where a background stops is a bug. Hairline borders are reserved for surfaces that genuinely need an edge (cards, window chrome).

Why — Hard cuts make the page feel assembled from rectangles. Soft exits make the mesh feel like atmosphere instead of a chunk of color.

When to break it — A 1px ink-100 hairline IS allowed on cards, table rows, and window chrome. The rule is about background termination, not about edges between surfaces.

In code — MeshHeroBackground.tsx:55-72 (protectSide radial + fadeBottom linear). See patterns/mesh-background.md.


4. Optical over mathematical

Equal pixel values are not equal perceived sizes. The logo strip gives every mark its own tuned height (21px to 41px) plus an optional vertical nudge so they feel the same size. A uniform height makes heavy wordmarks shout and light ones disappear.

Why — Designers see what the eye sees, not what the spec says. Trusting the optical decision means trusting the designer over a layout grid.

When to break it — Body type uses fixed sizes. Optical sizing is for brand marks, icons, and display type where the literal pixel count misleads.

In code — src/components/CustomerLogos.tsx:15-23. See patterns/customer-logos.md.


5. Real physics or nothing

The glass nav refracts through a canvas-generated displacement map fed to an SVG feDisplacementMap. A plain backdrop-filter: blur() reads as fake glass immediately. Material effects are either physically convincing or omitted.

Why — Cheap material effects are uncanny. Real refraction crosses a line where it stops looking like CSS and starts looking like an object. If you can't cross that line, don't try.

When to break it — A frosted-blur fallback is acceptable in browsers that can't run the displacement map (mostly non-Chromium). The rule is: not a decorative fake; functional fallback is fine.

In code — src/components/GlassSurface.tsx, used by VariantNav.tsx:175 and AgentWorkspace.tsx:191. See patterns/nav.md.


6. Chrome adapts to content, not the reverse

Any dark surface declares data-nav-dark. Nav links hit-test against those regions and flip to white individually; the logo splits black/white mid-letter via clip-path. Content never has to avoid the nav.

Why — A nav that requires content to give it space is a tax on every section below it. A nav that adapts pays its own cost once.

When to break it — Don't. This is a hard contract: every new dark section, window, or CTA must declare data-nav-dark.

In code — src/components/variants/VariantNav.tsx:67-112 (isOverDark, darkClipForLogo). See patterns/nav.md.


7. Color is meaning, not decoration

The base is white and ink. Color enters only as: the mesh (hero energy), one violet brand accent for emphasis (brand-700), teal strictly for announcements, clay orange strictly inside Claude surfaces, and a few semantic accents. If a color doesn't mean something, it doesn't appear.

Why — A constrained palette makes the rare color hit feel intentional. A wide palette numbs the eye to all of it.

When to break it — Inside a demo window that is faithfully quoting another product's UI (Claude Code, Claude Cowork, the LangWatch traces table), use that product's palette. Those are quotations, not site palette. See patterns/agent-workspace.md.

In code — src/index.css:53-83 (the @theme palette). See tokens/color.md.


8. Delight is hidden, not shouted

Right-clicking the logo opens a brand menu with assets and shader toys. Easter eggs reward curiosity without taxing the default experience.

Why — Surface-level whimsy reads as desperate. Buried delight reads as confident — we don't need to brag about it.

When to break it — Never on a primary action. Never blocking the read. Hidden delights are off the critical path.

In code — src/components/variants/BrandMenu.tsx. See patterns/hero-statement.md.


9. Type does the talking

A big quiet serif statement (Sentient 400, tracking -0.03em, a slight vertical stretch (scaleY 1.10), line height ~0.98) plus small, dense, functional sans for everything else. Few sizes, strong hierarchy, generous line height on prose only.

Why — One voice that is unmistakably the brand's. Three faces (serif/sans/mono) for three roles (statement / interface / machine output) — let the role pick the face, not the designer's mood.

When to break it — The Claude demos use their own typography (Cowork uses a Georgia fallback to quote Anthropic's serif). Quotation is permitted; site UI is not.

In code — src/index.css (Sentient @font-face, self-hosted woff2), the @theme faces block (--font-serif), and the .display treatment. See tokens/type.md.


10. Motion is respectful

Default state is still. Movement means one of three things: something is being created (typing, streaming), something is responding (selector pill sliding), or it is the single ambient brand element (the mesh). Demos play once and hold; replay is a deliberate user action. Loops pause off-screen. prefers-reduced-motion collapses everything.

Why — Motion that doesn't respect the visitor is hostile. The site has to keep working for someone who is not in the mood for animation.

When to break it — Never. prefers-reduced-motion: reduce is a hard rule, enforced at src/index.css:211-218.

In code — src/components/AgentWorkspace.tsx:65-82 (independent-clock output loop), src/index.css:160-179 (animations), src/index.css:211-218 (reduced-motion override). See tokens/motion.md.

Foundations

Tokens

colorcanonicalsrc/index.css:54-83

Color

The full palette lives in @theme at src/index.css:54-83. This file documents what each group is for, when to pick a particular step, and the rules color is held to (see principle 7).

Paper

Page surfaces. White, with two warmer fills for inactive states and soft fills.

TokenValueRole
--color-paper#ffffffPage background. Bodies, cards, popovers.
--color-paper-soft#efeee9Subtle fills: code chips, inactive tab tracks, paper-soft hovers on btn-secondary.
--color-paper-deep#e3e2dcDeeper fills when paper-soft is too pale next to the surrounding ink.

Why warm whites instead of #fff everywhere

#ffffff is the page; paper-soft is a warmer white (#efeee9), not a gray. This keeps the page feeling like paper rather than a void when a section needs a subtle distinction from the body.

Ink (warm charcoal)

The text scale. Cool charcoal labelled "warm" in the brief — the actual hex shows a warm-leaning neutral (#36352f, #26261f) rather than a true blue-gray. This is intentional. Cool grays against #ffffff would feel medical; the warm ramp keeps the page feeling like paper.

TokenValueRole
--color-ink-50#f0f0eeHairlines, dividers (rare — usually whitespace)
--color-ink-100#e3e2ddBorders on cards, window chrome, table rows
--color-ink-200#c6c4bfDisabled state, placeholder ghosts
--color-ink-300#99988fFaint icons, very-tertiary text
--color-ink-400#6d6c64Tertiary text (uppercase kickers, meta labels)
--color-ink-500#4d4d46Secondary text
--color-ink-600#36352fBody text, nav links at rest
--color-ink-700#26261fHovers, dark fills inside light surfaces
--color-ink-800#1b1b15Dark fills (heavier than 700)
--color-ink-900#141417Primary text, primary buttons (btn-primary)
--color-ink-950#0a0a0cDark sections (always with data-nav-dark)

Why an 11-step ink scale

The body sits at ink-600, nav at ink-600 → ink-900 on hover, secondary text at ink-500, kickers at ink-400. The fine-grain steps let hierarchy be expressed in value (darkness) before weight or size. A flatter scale (3–5 steps) forces you to reach for weight/size earlier, which crowds the type system.

Why ink-950 not ink-900 for dark sections

ink-900 is reserved for primary text/buttons on light surfaces. Dark sections want a value the eye reads as "true dark" — that's ink-950. Any element using ink-950 must declare data-nav-dark so the adaptive nav (see patterns/nav.md) flips correctly.

Brand — deep violet, single accent

Used sparingly. The brand exists for one emphasis at a time (the highlighted phrase in a heading, a soft icon fill, the selection tint).

TokenValueRole
--color-brand-200#cbc0eeSoft fills inside selection (brand-500/22 is the selection mix at src/index.css:103)
--color-brand-300#b1a3e8—
--color-brand-400#8a76de—
--color-brand-500#6e57d2Selection tint base; brand-500/10 for soft icon fills
--color-brand-600#5b41c2—
--color-brand-700#4a33a4The text-emphasis violet. The highlighted phrase in a heading; icon emphasis.
--color-brand-800#3a2680Heaviest violet, rarely needed.

Why a 7-step ramp for a single-accent color

The hover, active, and on-dark variants of "the violet" still need different values. The full ramp gives us room to pick the right step for the surface without ever inventing a new color.

Semantic accents (sparing)

TokenValueUse
--color-moss-accent#5b7a4aSuccesses, governance approvals
--color-amber-accent#c97b3aWarnings, mid-flight states
--color-rust-accent#b85240Failures, hard-deny states

These are never decorative. If you reach for moss-accent because "the section needs green," you've already lost — pull it back.

Hardcoded colors (debt)

Two colors that should be tokens but are inline in VariantHero.tsx:

Inline valueWhereShould become
rgba(20, 184, 166, 0.13) (bg) + rgb(15, 118, 110) (fg)VariantHero.tsx:18 (announcement pill)--color-announce-bg / --color-announce-fg
#39445cVariantHero.tsx:126 (subhead)--color-subhead-ink

These are flagged as canon in their pattern docs (the values are right; only their form is wrong). See patterns/announcement-pill.md and patterns/hero-subhead-ctas.md.

Claude quote palette (DEMO ONLY, never site UI)

These exist so the demo windows look exactly like real Claude products. They are quotations, not site palette.

Terminal (ClaudeTerminal)

UseValue
Terminal background#15130f
Clay orange (Anthropic accent)#d97757
Slash-command blue#7aa2d6
PASS green#8fb87a
FAIL red#cf9b8e
Muted text ramp#8b887f / #6f6c63 / #56534b

Cowork (ClaudeCowork)

UseValue
Paper background#faf9f5
Progress blue#3f5ad6
Chip rust#a8503c
Slash blue#3b6fd4

Never use these for site UI. They live inside AgentWorkspace.tsx, ClaudeTerminal.tsx, ClaudeCowork.tsx only. See patterns/agent-workspace.md.

Hero mesh colors

The mesh gradient takes four stops: two whites, the accent, and #ff8c2e (orange).

['#ffffff', '#ffffff', accent, '#ff8c2e']
  • Accent (canonical) — #f391ec (magenta-pink). Used at /2.
  • Accent (comparison) — #91b2f2 (blue). Used at /3.

The two whites keep the cloud mostly white so color stays an edge treatment rather than a flood. See patterns/mesh-background.md.

Selection

The ::selection rule mixes brand-500 at 22% with transparency, keeping selected text at ink-900. Defined at src/index.css:102-105.

::selection {
  background: color-mix(in srgb, var(--color-brand-500) 22%, transparent);
  color: var(--color-ink-900);
}

This is the only place the violet appears as a background by default. It feels brand-aware without being loud.

Known debt

  1. The announcement teal pair is hardcoded in VariantHero.tsx:18 and VariantHero.tsx:126. Should be promoted to tokens like --color-announce-{bg,fg} and --color-subhead-ink. Risk: low — touching the token requires editing VariantHero.tsx to consume it. Effort: small.
  2. The Claude quote palettes are scattered as inline hex inside ClaudeTerminal.tsx / ClaudeCowork.tsx. They should be promoted to a clearly-namespaced demo-only token group (e.g., --demo-claude-terminal-bg) so it is obvious they are not site UI. Risk: medium — many touch points. Effort: medium.

Do / Don't

  • Do pick from the ink-* ramp by value, not by name. If you want "the gray that looks one step darker than this neighbor," walk the ramp.
  • Do use brand-700 exactly once per heading at most. Two violet emphases in one heading is too many.
  • Don't introduce a new color without a meaning. If you can't say what it means, it doesn't belong.
  • Don't mix the Claude palette with site palette. If the surface is the site, it gets ink + paper; if the surface is a demo window, it gets the Claude palette. Don't bridge them.
Color — live
Paper
paper#ffffff
paper-soft#efeee9
paper-deep#e3e2dc
Ink (warm charcoal)
ink-50#f0f0ee
ink-100#e3e2dd
ink-200#c6c4bf
ink-300#99988f
ink-400#6d6c64
ink-500#4d4d46
ink-600#36352f
ink-700#26261f
ink-800#1b1b15
ink-900#141417
ink-950#0a0a0c
Brand (deep violet — single accent)
brand-200#ffe2cc
brand-300#ffb380
brand-400#ff8a3d
brand-500#f56b1a
brand-600#d65209
brand-700#a83e05
brand-800#762c04
Semantic accents (sparing)
moss-accent#5b7a4a
amber-accent#c97b3a
rust-accent#b85240
typecanonicalhas debtsrc/index.css (@font-face block, @theme faces, .display)

Type

Three faces, three roles. Serif for what we believe; sans for the interface; mono for what the machine says.

Faces

TokenValue (as declared in @theme)What's actually loadedRole
--font-sans"Inter", ui-sans-serif, system-ui, sans-serifInter is NOT loaded. Geist Variable is bundled (@fontsource-variable/geist at src/index.css:1) but not listed in the token. The body currently renders in the system sans-serif.Everything functional: nav, buttons, body, labels.
--font-serif"Sentient", "Charter", "Source Serif Pro", ui-serif, Georgia, serifSentient IS loaded in src/index.css, self-hosted woff2 (weights 200/300/400/500/700, plus italics). A high-contrast literary serif from Fontshare.The voice of the brand: statements, headings. The .display class consumes this.
--font-mono"JetBrains Mono", ui-monospace, "SFMono-Regular", monospaceJetBrains Mono is NOT loaded. Falls back to system mono.The machine: terminal text, code chips, data, uppercase meta labels.

Why three faces

  • Serif for statements — anything we believe in headlines. Sentient at 400 weight, tight negative tracking (-0.03em), a slight vertical stretch (scaleY 1.10), tight leading. This is the brand's voice.
  • Sans for interface — body, nav, buttons, controls. Functional. Reads dense and clean at 13–17px.
  • Mono for machine output — terminal text, code, fixed-width data, uppercase kickers with letter-spacing. This is the machine speaking.

Don't let the roles bleed. A serif button is wrong; a mono heading is wrong; a sans-serif statement is fine but loses brand character.

The display class

.display is the heading treatment, and it carries the whole voice in one class. It sets font-family: var(--font-serif) (Sentient) plus the weight, tracking, stretch, and leading:

.display {
  font-family: var(--font-serif); /* Sentient */
  font-weight: 400;               /* Sentient regular */
  letter-spacing: -0.03em;        /* tight tracking */
  line-height: 0.98;
  scale: 1 1.1;                   /* slight vertical stretch — scaleY 1.10 */
}

Headings are written as display alone (no paired font-serif class): the family, weight 400, the -0.03em tracking, the scaleY 1.10 stretch, and the 0.98 line height all come from this single rule.

The hero statement uses it directly (VariantHero.tsx, the OrchidHero h1):

className="display text-[56px] text-ink-900"

Scale in use

SizeFace / weightUseWhere
56pxSentient 400, lh ~1.0Hero statementVariantHero.tsx (OrchidHero h1)
~35 to 52pxSentient 400Section headings(provisional — sections below logos)
~22 to 26pxSentient 400Card headings(provisional)
17 to 21px, lh 1.6+Sans 400Subheads and leads (ink-500 or #39445c)VariantHero.tsx:126 (17px), VariantHero.tsx:98 (18–21px responsive)
15 to 15.5px, lh ~1.65Sans 400Body (ink-600)(provisional, applied below logos)
13.5pxSans 500Nav linksVariantNav.tsx:204
13pxSans 500Buttons (btn-primary, btn-secondary)src/index.css:137-142
11.5 to 12.5pxSans 500 / MonoPills, meta, uppercase kickers (tracking 0.18em to 0.22em)VariantHero.tsx:17 (11.5px pill), CustomerLogos.tsx:28 (11px tracking 0.22em kicker)

Why negative tracking only on display sizes

Negative tracking on 14px text squeezes letters together until they read tight and cramped. On a display-size heading (the 56px hero), -0.03em tightens the optical rhythm exactly enough to look intentional. Small text never gets squeezed.

Why generous line height on prose only

The subhead at VariantHero.tsx:126 is leading-[1.65]. Headings at .display are 0.98. Buttons sit at default (~1.2). The principle: more lines = more leading. Single-line UI doesn't need breathing room; multi-line prose does.

Voice rules

  • Headlines: short situational truths in serif. No terminal period ("When your agents get complex" — no period).
  • Subheads: pair a concrete capability chain with stakes ("Surface patterns in production, turn them into scenarios, and improve quality with every release. For teams that can't afford to get it wrong.").
  • Demo microcopy mirrors the real product's tone (lowercase terminal prompts).
  • Never use em-dashes anywhere — use commas, colons, or periods.
  • Never name real customers in examples (use ACME).
  • Sentence case for UI labels; product nouns capitalized (Voice Agent Simulations).

Known debt

  1. --font-sans lists a face that isn't loaded. Inter is the first item in the stack in the @theme block of src/index.css. The site bundles Geist Variable (@fontsource-variable/geist) but does not put it in the token. Result: every sans-serif element on the page falls through to the system stack (SF Pro on Apple, Segoe UI on Windows, etc.). Resolution path (not applied in this revision): change --font-sans to lead with "Geist Variable". This is a one-line change but it visually shifts every body-text element on the site, which is why it's flagged here for a deliberate decision rather than slipped in.
  2. --font-mono lists a face that isn't loaded. JetBrains Mono is first in the stack but is not bundled, so mono text falls back to the system mono. Pick a face that is loaded (bundle and consume a mono if mono matters), or prune the token down to the system stack.

Do / Don't

  • Do write headings as display alone: the class carries the family, weight, tracking, stretch, and leading.
  • Do keep the serif/sans/mono role split clean: serif for statements, sans for interface, mono for machine output.
  • Don't add a new face. The system is three faces by intent.
  • Don't apply negative tracking below ~24px.
  • Don't style mono text without uppercase + letter-spacing — bare mono in lowercase reads accidental.
Type — live
Display — Sentient 400 @ 64px

When your agents get complex

Section heading — Sentient 400 @ 40px

The reasoning behind every decision.

Subhead — Sans 400 @ 17px, #39445c, lh 1.65

Surface patterns in production, turn them into scenarios, and improve quality with every release. For teams that can't afford to get it wrong.

Body — Sans 400 @ 15px, ink-600, lh 1.65

Three faces, three roles. Serif for what we believe, sans for the interface, mono for what the machine says. Few sizes, strong hierarchy, generous line height on prose only.

Kicker — Sans 500 @ 11px, ink-400, uppercase tracking 0.22em

Trusted in production by

Mono — system mono fallback @ 13px, ink-600

npx vitest run scenarios/voice-agent.scenario.ts

spacecanonicalsrc/index.css:119-131

Space

Containers, vertical rhythm, and the rule that whitespace replaces dividers for almost everything. The system has three container widths and a small handful of vertical-rhythm rules; nothing more.

Containers

Defined at src/index.css:119-131.

ClassMax widthPadding (mobile / md+)Use
.container-wide1440pxpx-6 / md:px-10Nav, hero, AgentWorkspace, dark sections
.container-page1120pxpx-6 / md:px-8Standard content sections (logo strip, "why" cards, etc.)
.container-narrow760pxpx-6 / md:px-8Prose blocks (when we need to cap measure)

Why three widths and not one

The hero and the AgentWorkspace want 1440px because the demo wants room to breathe (two 540px panes + selector wings = ~1180px effective, plus side air). Standard sections want 1120px because content cards stop reading as a row past ~1200px. Prose wants 760px because that's roughly the comfortable measure for a 17px body face.

A single container width forces you to fight it in opposite directions: the hero feels cramped, or the body sections feel sparse. Three widths solve that without proliferating.

Why px-6 md:px-10 on .container-wide

The wide container gets extra horizontal padding on desktop (md:px-10) because at 1440px max-width on a 1920px screen, the gutter is small — and the nav's liquid-glass pill (which sits inside .container-wide) needs visual margin from the screen edge.

The measure rule

ElementMax widthWhere
Hero heading680pxVariantHero.tsx
Hero subhead610pxVariantHero.tsx
Hero block (overall)680pxVariantHero.tsx
Section intros~720px(provisional, below logos)
Body prose760px (.container-narrow)src/index.css:126-128

Why measure is capped

A 56px serif statement read across a 1400px container is impossible to scan. The eye can't track that line length. The cap forces line breaks where they should naturally fall.

Full-container text lines are forbidden. This is a hard rule.

Hero rhythm

The variant 2 hero stacks: announcement pill → heading → subhead → CTAs → mesh-band → AgentWorkspace.

┌─────────────────────────────────────────┐
│  [Announcement pill]                    │  ← `mb-[22px]` to heading
│                                         │
│  Hero heading (56px Sentient)           │  ← `max-w-[680px]`
│  on two lines                           │
│                                         │
│  Subhead (17px, #39445c, lh 1.65)       │  ← `mt-8` (32px), `max-w-[610px]`
│                                         │
│  [Book a demo]  [Self-host ▾]           │  ← `mt-8` (32px) from subhead
│                                         │
│             (mesh band continues)        │  ← `pt-28 pb-[430px]` band height
│                                         │
│  ┌─────────────────────────────────┐    │  ← AgentWorkspace pulled up `-mt-[350px]`
│  │     AgentWorkspace (680px)      │    │
│  └─────────────────────────────────┘    │
└─────────────────────────────────────────┘

Reference values:

GappxWhere
Pill → heading22VariantHero.tsx:17 (mb-[22px])
Heading → subhead32VariantHero.tsx:126 (mt-8)
Subhead → CTAs32VariantHero.tsx:28 (mt-8 on Actions)
Hero block top (under nav)112VariantHero.tsx:116 (pt-28)
Mesh band bottom padding430VariantHero.tsx:116 (pb-[430px])
AgentWorkspace pull-up−350VariantHero.tsx:135 (-mt-[350px])

Why the pull-up

The mesh fades to white at its bottom via fadeBottom (a 360px linear fade). Pulling the AgentWorkspace up into the mesh by 350px means the mesh ends behind the top of the window — so the window appears to emerge from a colored cloud rather than sitting on a flat band. Layered, not stacked. This is the depth principle (principle 3) in spatial form.

Section padding

Section spacing runs py-14 to py-32 by weight of the section. Used at App.tsx:51 (the customer logos section: py-14 md:py-16).

ClassVertical paddingUse
py-1456pxLight sections (the logo strip)
py-1664pxDefault content sections
py-2080pxHeavier content sections
py-2496pxMajor sections
py-32128pxThe heaviest sections (provisional, below logos)

Why no section dividers

Section boundaries are whitespace, not lines. Hairline borders are reserved for surfaces that genuinely need an edge (cards, window chrome, table rows — see principle 3).

The logo strip used to have borders bracketing it; they were removed deliberately. Look at App.tsx:51 and CustomerLogos.tsx — no hairlines. The section is bounded by vertical padding alone.

Do / Don't

  • Do pick a container width by content, not by personal preference. Hero/demo → wide. Cards/grid → page. Prose → narrow.
  • Do use the rhythm values (22, 32, 8, 14, 16, 20, 24, 32) from the rhythm table. Inventing one-off gaps fragments the rhythm.
  • Don't use a hairline border between sections. If a section feels too connected to the next, add padding.
  • Don't let a text line reach the container edge. Cap the measure.
Space — live
Containers (max-widths)
container-narrow760px
container-page1120px
container-wide1440px
Vertical rhythm (hero)
pill
22px — pill → heading
heading
32px — heading → subhead
subhead
32px — subhead → CTAs
CTAs
shapecanonicalsrc/index.css:85-87,119-146

Shape

The site has a clear shape language: the smaller and more interactive a thing is, the rounder it gets. Interactive chrome (buttons, pills, selector tabs, nav links) takes the full radius-pill. Cards take radius-card. Windows take rounded-2xl (16px). Large content surfaces stay calmest.

Radii

TokenValueUse
--radius-pill999pxButtons, nav links, selector tabs, pills, the announcement, pagination dots — anything interactive and small
--radius-card14pxCards, content surfaces, popovers
rounded-2xl (Tailwind)16pxWindow outer frames (AgentWorkspace)
rounded-xl (Tailwind)12pxInner floating panels (the output panel inside the AgentWorkspace)

Defined at src/index.css:85-87.

Why the pill is the signature

The fully-round pill says interactive. Every actionable surface is round: buttons, tabs, links-as-pills, the announcement chip, even the GitHub icon button wrapper (p-1.5 rounded-full).

This means: round → click. Square → display. A visitor's eye can pre-attentively pick out what is actionable on the page just from the curvature. The Apple-style design taste call.

Why cards at 14px, not 12 or 16

14px is the largest curvature where a card still reads as a surface rather than a button. At 16px+ a card reads as a pill candidate; at 12px or less it reads as sharp. 14px is the sweet spot for content cards.

Window curvature (16px and 12px)

The AgentWorkspace window has two curvatures:

┌─────────────────────────────────────┐
│  (outer frame, rounded-2xl = 16px)  │
│  ┌───────────┬──────────────────┐   │
│  │ assistant │ ┌──────────────┐ │   │
│  │           │ │ output panel │ │   │   ← floating inner panel,
│  │           │ │ rounded-xl   │ │   │     rounded-xl = 12px
│  │           │ │   = 12px     │ │   │
│  │           │ └──────────────┘ │   │
│  └───────────┴──────────────────┘   │
└─────────────────────────────────────┘

The outer frame is 16px because it is a page-level window — it's the biggest curved thing on the page after the section corners. The inner output panel is 12px because it sits inside another curved surface, and matching its parent's 16px would feel oversized. Smaller inside larger reads natural.

Code: AgentWorkspace.tsx:110 (rounded-2xl on the frame), AgentWorkspace.tsx:124 (rounded-xl on the inner panel).

Pill components

.pill (status / meta tag)

.pill {
  @apply inline-flex items-center gap-2 rounded-full
         border border-ink-100 bg-paper
         px-3 py-1 text-[11px] font-medium
         uppercase tracking-[0.18em] text-ink-500;
}

A small ink-100 hairline pill used for status / meta tags ("PROVISIONAL", "DEBT", category labels). Defined at src/index.css:133-135.

.btn-primary (the dark action)

.btn-primary {
  @apply inline-flex items-center justify-center gap-2 rounded-full
         bg-ink-900 px-5 py-3 text-sm font-medium text-paper
         transition hover:bg-ink-700 active:translate-y-px;
}

A round, dark pill at ink-900. Hover lifts to ink-700. Active state translates down 1px (the "press" affordance — a touchpad-friendly tactile feedback that doesn't need shadow tricks). Defined at src/index.css:137-139.

Always declares data-nav-dark on its container when used inline with the nav, because the button itself is a dark surface (principle 6).

.btn-secondary (the paper-soft action)

.btn-secondary {
  @apply inline-flex items-center justify-center gap-2 rounded-full
         border border-ink-200 bg-paper px-5 py-3 text-sm font-medium
         text-ink-900 transition
         hover:border-ink-300 hover:bg-paper-soft active:translate-y-px;
}

The lighter pair. White surface, ink-200 border, hover deepens to paper-soft fill. Same active-press translate. Defined at src/index.css:140-142.

Cards

.card {
  @apply rounded-[var(--radius-card)] border border-ink-100 bg-paper;
}

Just three rules: radius-card, ink-100 hairline, paper background. Cards are containers — they don't carry visual decoration on their own. The content inside provides the interest. Defined at src/index.css:144-146.

Borders

ink-100 (#e3e2dd) is the hairline value, used only where surfaces truly meet — card outlines, window chrome, table rows.

The .rule class (src/index.css:155-157) is a 1px bg-ink-100 horizontal line, used for table-style separators inside compact surfaces. Not used as a section divider — sections separate by whitespace.

Shadows

Three tiers of shadow, all soft and low-opacity. No hard material-design elevation steps.

Demo window (heaviest)

0 30px 80px -30px rgba(30,20,40,0.3),
0 10px 30px -15px rgba(30,20,40,0.12)

Two layers: a far soft glow plus a closer harder shadow. Used on the AgentWorkspace frame (AgentWorkspace.tsx:110).

Floating inner panel (medium)

0 16px 36px -14px rgba(20,15,30,0.35)

Single layer, tighter, slightly darker tint. Used on the output panel inside the workspace (AgentWorkspace.tsx:124).

Selector pill (light, with "wet" highlights)

inset 0 0 0 1px rgba(255,255,255,0.7),
inset 1.5px 2px 0 -1px rgba(255,255,255,0.9),
0 4px 12px -4px rgba(20,20,23,0.2),
0 1px 3px rgba(20,20,23,0.08)

Tiny crisp shadow + inset 1px white highlights (giving the pill a wet look — like a wet pebble). Used on the active output-selector pill in the AgentWorkspace (AgentWorkspace.tsx:199-200).

Why two-layer shadows on big surfaces

A single big soft shadow looks like a paper cutout. A close hard shadow + far soft glow creates physical depth — same trick a product photographer uses. Important for the AgentWorkspace because it's the page's centerpiece.

Do / Don't

  • Do reach for rounded-full for anything actionable below ~200px wide.
  • Do use rounded-2xl (16px) for a window-class surface and rounded-xl (12px) for a panel inside it.
  • Don't invent a shadow. Pick from the three tiers above. If a surface needs a new shadow, justify it.
  • Don't sprinkle border on every card — card already includes the hairline.
Shape — live
Pills (radius-pill = 999px)
Status metaAnnouncement
Cards (radius-card = 14px)
Standard card
Hairline at ink-100, paper background, radius 14px.
Window (rounded-2xl, 16px)
Page-level surfaces. Inner panels step down to rounded-xl (12px).
motioncanonicalsrc/index.css:160-218

Motion

Default state is still. Movement means one of three things:

  1. Something is being created — typing, streaming rows, characters arriving.
  2. Something is responding — a selector pill sliding, a nav link flipping color, a button pressing.
  3. It is the single ambient brand element — the mesh drift. There is only ever one of these on a view.

Anything else moving is a bug.

Animation tokens

Defined in @keyframes at src/index.css:160-179.

ClassKeyframeTimingUse
.animate-blinkblink1.4s ease-in-out infiniteTerminal caret
.animate-fade-infade-in (4px rise + opacity)600ms ease-outContent arriving (rows streaming in, chips appearing)
.animate-grow-xgrow-x (scaleX 0→1)650ms cubic-bezier(0.22, 1, 0.36, 1)Bars filling, progress reveals
.animate-spin-slowspin-slow (360° rotation)2.6s linear infiniteDemo spinners (tool-loop indicator, command-running)

Why these specific durations

  • 1.4s caret blink — slow enough to not be twitchy, fast enough to read as a cursor. The terminal cursor blink speed across most OS terminals.
  • 600ms fade-in — long enough that the rise (translateY(4px)) is visible (so the content arrives rather than just appearing), short enough that you don't sit waiting.
  • 650ms grow-x with the (0.22, 1, 0.36, 1) curve — the curve is a soft ease-out that decelerates hard at the end. Bars feel like they're settling into their final length rather than slamming into it.
  • 2.6s spinner — slower than a typical loading spinner (usually 1s). Reflects that the work behind it is real (compiler-grade), not network latency.

Selector pill spring

The active-selector pill in the AgentWorkspace uses motion/react's layoutId for cross-tab transitions:

transition: { type: 'spring', stiffness: 500, damping: 38 }

Defined at AgentWorkspace.tsx:188. The high stiffness + medium damping gives a snappy but settled feel — fast travel, no overshoot ringing.

Reduced motion override: when useReducedMotion() is true, this collapses to { duration: 0 }. The pill jumps instantly. See AgentWorkspace.tsx:154,186-189.

Color transitions

300ms is the default for all color transitions on nav links, hover states, button surfaces. Defined inline via Tailwind's transition-colors duration-300 (e.g., VariantNav.tsx:204).

Why 300ms

Faster than the human perception threshold for "I changed something" (~100ms) but slow enough that the transition reads as deliberate. Anything under 200ms feels twitchy on hover; anything over 400ms feels lazy.

Nav glass morph

The nav transitions to glass at 24px of scroll over 500ms ease-out (VariantNav.tsx:158-166). Container padding shifts (pt-2 when scrolled, pt-0 at top); the inner pill changes height (h-14 scrolled, h-16 at top) and side padding (px-3 md:px-5 scrolled, px-0 at top).

The glass surface (GlassSurface) fades in over the same 500ms (VariantNav.tsx:171-177).

Why 24px and 500ms

24px is enough scroll that the visitor has decided to scroll — not an accidental wheel-tick. 500ms is the longest transition on the page; it feels deliberate (the glass forms) rather than reactive. The slow morph is part of what makes the glass read as physical (see principle 5).

Demo timings (AgentWorkspace output loop)

Defined at AgentWorkspace.tsx:65-82.

PhaseDurationDetail
Initial pause450msEmpty state before streaming begins
Row reveal620ms eachThe output streams in row by row
Hold3400msThe finished list sits still
Advance to next(instant)Output type cycles: Simulations → Experiments → Traces → Simulations

This is the independent-clock output loop — it runs on its own timer, not the assistant's. The assistant plays through once and holds; the output keeps cycling. See patterns/agent-workspace.md.

Mesh drift

The mesh shader runs continuously at speed={0.74} (MeshHeroBackground.tsx:45). This is calibrated to feel like atmospheric drift rather than churn.

See patterns/mesh-background.md.

Off-screen pause (IntersectionObserver)

Every self-animating demo component (assistants, Live outputs) uses an IntersectionObserver to pause its loop when scrolled off-screen. The cancellation flows through every sleep via a tick.cancelled token, so unmount/visibility-change tears the loop down cleanly with no orphaned timers.

This matters because there are ~5 looping components + the WebGL mesh on one page. Letting them all run when nobody's watching burns battery for no reason.

Implementation: tick = { cancelled: false }; sleep(ms, tick) resolves immediately if tick.cancelled; the effect cleanup sets tick.cancelled = true on unmount or visibility change. See AgentWorkspace.tsx:49-53,80-82.

Reduced motion (global override)

Defined at src/index.css:211-218:

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    scroll-behavior: auto !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

This is a hard rule. All durations collapse to ~0; all loops run once; smooth scroll disables. The mesh shader and the spring transitions check useReducedMotion() separately (they can't be killed by CSS alone) and respond.

If you add a new animation, you don't need to add a reduced-motion check unless you're using JS-driven animation (motion/react, requestAnimationFrame, WebGL). For CSS animations and transitions, the global override catches you.

Do / Don't

  • Do make new motion fall into one of the three categories (creating / responding / ambient). If it doesn't fit, the motion is decoration and shouldn't exist.
  • Do pause loops off-screen.
  • Do use the existing duration tokens (300ms color, 500ms glass morph, 600ms content arrival, 620ms row stream) before inventing new ones.
  • Don't loop a motion in two places at once on the same view (two spinners side by side, two pulsing dots, etc.). Pick the one that carries the meaning and kill the others.
  • Don't auto-replay a demo. Demos play once and hold. Replay is a user action.
Motion — live
Animations (CSS classes)
animate-fade-in (600ms)
content arriving…
animate-grow-x (650ms)
animate-blink (1.4s, caret)
▎|
animate-spin-slow (2.6s)
Canon

Patterns (hero → logos)

navcanonicalpath: src/components/GlassSurface.tsx

Nav

The site's nav. Transparent at the top of the page; morphs into a refractive liquid-glass pill once the visitor has scrolled 24px. Adaptive inversion: each link, the GitHub icon, and the wordmark flip to white per element when their center sits over a dark region. The wordmark splits mid-letter via clip-path where it crosses a dark/light boundary.

Anatomy

At the top of the page (scroll = 0):
┌─────────────────────────────────────────────────────────────────────────┐
│  [Logo]   Platform▾  Solutions  Enterprise  Customers  Docs  Pricing   │   [GH] [Sales] [Sign in] [Sign Up]
└─────────────────────────────────────────────────────────────────────────┘
   ↑ transparent — the mesh runs behind it

After 24px scroll:
   ┌───────────────────────────────────────────────────────────────────┐
   │ [Logo]  Platform▾  Solutions  Enterprise  ...    [GH] [Sales] ...│   ← liquid glass pill
   └───────────────────────────────────────────────────────────────────┘
   ↑ refractive glass; container shifts down by `pt-2`; pill takes `px-3 md:px-5`

Adaptive inversion (over a dark section):
┌──────────────────────────────────────────────────────────────────────┐
│ [█████]  Platform  ...                       [GH]  ...     ███████  │
└──────────────────────────────────────────────────────────────────────┘
                            ↑ each link's color depends on what's under its center
                            ↑ logo wordmark splits black/white at the boundary

Tokens used

  • Type — text-[13.5px] font-medium for nav links (VariantNav.tsx:204).
  • Color — ink-600 at rest → ink-900 on hover (light region); white/90 → white (dark region).
  • Shape — rounded-full on every nav link, the GitHub icon button, and the Sign Up CTA. Pill-language all the way through.
  • Motion — 500ms ease-out for the scroll morph; 300ms for color transitions; the GlassSurface fades in over 500ms (VariantNav.tsx:171-177).

Decisions

Why a transparent top state

The hero mesh has to run behind the nav. A solid-colored nav at the top of the page cuts the mesh into a horizontal stripe and tells the visitor "this is a band, not a field." Transparent at top makes the mesh feel like atmosphere; the nav forms only when the visitor scrolls into content where the glass needs to live.

Why glass instead of backdrop-blur

The GlassSurface uses a canvas-generated displacement map fed to an SVG feDisplacementMap. That's refraction — content behind the nav genuinely warps as it passes. backdrop-filter: blur() would be cheaper but reads as fake glass immediately. See principle 5.

Fallback for non-Chromium browsers: a clear frosted blur. Still functional, just less dramatic.

Why per-link adaptive inversion (not a whole-nav toggle)

A whole-nav toggle would flip every link the moment any dark surface appears under the nav band. That breaks down at the boundary — when only half the nav sits over a dark section, you want only those links to flip.

The implementation (VariantNav.tsx:67-82) hit-tests each link's center against every [data-nav-dark] region in the DOM. Each link flips independently. This is the Apple-nav trick, ported per-pixel.

Why the wordmark splits mid-letter (clip-path: inset(...))

A wordmark is a piece of art, not a row of letters. Toggling its color whole-hog at the boundary looks abrupt. The actual solution (darkClipForLogo, VariantNav.tsx:84-112):

  • The same logo image is rendered twice: black + a copy whitened via filter: brightness(0) invert(1).
  • The whitened copy is clipped via clip-path: inset(0 right 0 left) to expose only the horizontal slice that overlaps any [data-nav-dark] region.
  • Result: the wordmark appears to split black/white at the exact pixel column where the dark backdrop begins.

This is Apple.com-level chrome adapting to content. It costs ~30 lines and a requestAnimationFrame loop, and visibly elevates the nav from "good" to "intentional."

Why data-nav-dark as a hard contract

The nav can't predict what content sits under it. Every dark surface — sections, buttons, demo windows — must declare data-nav-dark so the nav can hit-test against it. This is enforced by convention, not the type system. A new dark surface that forgets to declare it will have a broken nav over it, with no warning.

In the canonical hero, data-nav-dark is set on:

  • The btn-primary "Book a demo" CTA (VariantHero.tsx:31)
  • The AgentWorkspace window (when in Code Assistant mode — terminal background #15130f)
  • Any new dark section added below

See principle 6.

Why two RAFs on scroll, not a debounce

The update fires inside a requestAnimationFrame, then a cancelAnimationFrame if scroll fires again before paint. This is the right pattern for "I need this to update each visible frame, but not faster" — see VariantNav.tsx:140-143. Throttle would skip frames the visitor can see; debounce would lag the inversion behind the scroll. RAF coalescing hits both.

Why the Platform dropdown is a hover-only group

The Platform link opens a 600px-wide grid of platform pages (Agentic AI Testing, LLM Evaluation, LLM Observability, Prompt Optimizer, Voice AI, LLM-Red-teaming). It uses Tailwind's group-hover for the open state (VariantNav.tsx:222), no JS state machine.

The dropdown is information-dense — six tiles with icon + title + description each — so it makes sense to surface on hover (a deliberate-feeling interaction) rather than auto-open. On touch, it opens on tap because there is no hover signal; the group-hover and Tailwind's pointer-events-none handling let the dropdown be visible-but-not-clickable until the user actually opens it.

Code references

ConcernFile:lines
Whole navsrc/components/variants/VariantNav.tsx:114-313
isOverDark (per-link hit-test)src/components/variants/VariantNav.tsx:67-82
darkClipForLogo (per-letter clip)src/components/variants/VariantNav.tsx:84-112
Scroll listener + RAF coalescersrc/components/variants/VariantNav.tsx:127-152
Glass pill morphsrc/components/variants/VariantNav.tsx:155-177
Platform dropdown gridsrc/components/variants/VariantNav.tsx:209-249
GlassSurface primitivesrc/components/GlassSurface.tsx
Brand-menu (right-click on logo)src/components/variants/BrandMenu.tsx

Hidden delights

  • Right-click the logo → opens the BrandMenu (copy SVG, full brand-assets dialog, shader easter eggs). VariantNav.tsx:183 (onContextMenu={brand.onContextMenu}).
announcement pillcanonicalpath: src/components/variants/VariantHero.tsx

Announcement pill

A small teal pill, 22px above the hero heading. It announces; it never competes. Reserved for "Introducing ..." moments only. Currently announces Voice Agent Simulations.

Anatomy

        ┌──────────────────────────────────────────────────┐
        │ Introducing Voice Agent Simulations  →           │
        └──────────────────────────────────────────────────┘
            ↑ 11.5px font, teal bg (rgba(20,184,166,0.13)),
              teal fg (rgb(15,118,110)), rounded-full,
              padding px-2.5 py-1, gap-1.5 to the arrow
              tracking -0.005em

           ↓ 22px gap (mb-[22px] on the pill)

        ┌──────────────────────────────────────────────────┐
        │  When your agents                                │   ← hero heading (Sentient 56px)
        │  get complex                                     │
        └──────────────────────────────────────────────────┘

Tokens used

  • Shape — rounded-full (the pill shape language).
  • Type — text-[11.5px] font-medium tracking-[-0.005em]. Slight negative tracking to compensate for the all-cap-height feel of the sentence; less squeeze than display copy.
  • Color — see Known debt: currently uses inline rgba(20, 184, 166, 0.13) / rgb(15, 118, 110) (a teal pair).
  • Spacing — px-2.5 py-1 for the pill itself; mb-[22px] to the heading below.

Decisions

Why teal specifically

The site's identity (principle 7) holds color to a meaning. The brand violet means emphasis; the moss / amber / rust means semantic state (success / warning / failure); paper and ink are the page.

Teal sits outside all of those categories. That's the point. It's a new color hit, and the only place on the page where it appears. Pre-attentively, the visitor's eye lands on it because it's the one color they haven't seen yet. That's how an announcement should read.

If teal becomes a regular color in the design (used for hovers, links, decorations), the announcement loses its bite. Hold the line — teal is announcements only.

Why 11.5px and not 12px

11.5px reads as kicker / meta rather than body. 12px would tip toward "this is text you should read carefully." 11.5px says "this is a tag — your peripheral vision is enough." The visitor doesn't have to commit attention to it.

Why a 22px gap to the heading (not 24 or 32)

22px reads as attached to the heading — the pill is a kicker, not a section. 32px would create a section break ("Here's an announcement. ... Here's a separate headline."). 22px keeps them in the same composition.

This is VariantHero.tsx:17 — mb-[22px] is a tuned value, not mb-6.

Why ArrowUpRight and not ArrowRight

The diagonal arrow says outbound — clicking goes somewhere else (the announcement post, the product page). ArrowRight would say next (in a flow). The pill is a link to elsewhere, not the next step of a journey.

h-3.5 w-3.5 (14px) — small enough to read as a decoration on the pill rather than competing with the text.

Why two whitespace separators in the inline content

In the canonical pill text, the structure is:

Introducing | Voice Agent Simulations | →

— a label, the thing being introduced, the arrow. The gap-1.5 between them gives the eye three things to parse, in the right order. Without spacing, the pill reads as a single phrase; with spacing, it reads as "[label] [name] [link]" which is the intended hierarchy.

(The earlier brief shows a version with a vertical text-ink-400 | h-3 w-px bg-ink-200 | divider between "Announcing" and the title — that was the Hero.tsx orphan, not the canonical VariantHero.tsx pill. The canon pill has no divider.)

Code references

ConcernFile:lines
Componentsrc/components/variants/VariantHero.tsx:13-24
Used in OrchidHerosrc/components/variants/VariantHero.tsx:120

Voice rules for the announcement text

  • Always starts with "Introducing" (or "Announcing" in earlier iterations — but the canon is "Introducing").
  • Title case for the thing being announced (Voice Agent Simulations).
  • No period, no exclamation.
  • Short. If it can't fit in one line at whitespace-nowrap, the pill is wrong, not the design.

Known debt

  • The teal pair is hardcoded:
    • backgroundColor: 'rgba(20, 184, 166, 0.13)'
    • color: 'rgb(15, 118, 110)'
  • Should become tokens: --color-announce-bg and --color-announce-fg.
  • Why not yet: the design system documents reality, not aspiration. The values are right; only the form is wrong. Resolution requires a token addition in index.css and a style={{...}} → className swap in VariantHero.tsx. Trivial; held back here to keep this revision documentation-only.

See tokens/color.md#hardcoded-colors-debt.

Announcement pill — live
hero statementcanonicalpath: src/components/variants/VariantHero.tsx

Hero statement

The 56px serif heading: "When your agents get complex" (Sentient 400, on two lines, no terminal period, no italics). It is plain text in the heading font, including the word "complex".

Anatomy

┌──────────────────────────────────────────────┐
│  When your agents                            │   ← Sentient 400, 56px,
│  get complex                                 │     letter-spacing -0.03em,
└──────────────────────────────────────────────┘     scaleY 1.10, line-height 0.98,
                                                     color ink-900
                                                     max-w-[680px]

Tokens used

  • Type — display, which carries the heading treatment in one class: font-family: var(--font-serif) (Sentient), font-weight: 400 (regular), letter-spacing: -0.03em, a slight vertical stretch (scale: 1 1.1, scaleY 1.10), and line-height: 0.98. Size: literal text-[56px].
  • Color — text-ink-900.
  • Space — max-w-[680px] so the line wraps after "agents" (the visitor sees two lines, not one).

Decisions

Why 56px

56px lets Sentient anchor the hero on a 1440px container without dominating the page so completely that the rest of the hero loses voice. It tilts the hierarchy toward the heading while leaving room for the subhead and the demo to share the space. See the OrchidHero h1 in VariantHero.tsx.

Why Sentient 400 (not 500, not 700)

The heading voice now comes from Sentient at 400 (regular), not from a heavier weight. Sentient is a refined, high-contrast literary serif: at 400 it already carries enough thick/thin contrast to read as a confident statement, where a medium or bold weight (500/700) would thicken the thins and read as aggressive rather than quiet. Presence comes from the treatment, not the weight: tight tracking (-0.03em) pulls the line into a single anchored shape, and a subtle vertical stretch (scaleY 1.10) gives the letters height and gravity without bulking their strokes. The result stays light and editorial while still owning the page. This is the brand's voice setting.

Why no terminal period

The hero says "When your agents get complex" — no period.

The brief locks this in (§10): "Headlines are short situational truths in serif, no terminal period, no italic styling tricks."

The period suggests closure. A statement without a period stays open — the visitor's eye lands on it and waits for the rest of the page to complete the sentence. That's the right effect for a hero.

Code references

ConcernFile:lines
Hero h1src/components/variants/VariantHero.tsx:50-54
.display treatment classsrc/index.css (in the components layer)

Do / Don't

  • Do keep headings to a max of two visual lines (the max-w-[680px] enforces this).
  • Do write headings as display alone: the class carries the family, weight, tracking, stretch, and leading.
  • Don't italicize the heading. The hero statement is plain upright Sentient.
  • Don't add a terminal period to the heading.
Hero statement — live

When your agents
get complex

hero subhead + ctascanonicalpath: src/components/variants/SelfHostMenu.tsx

Hero subhead and CTAs

The lead paragraph below the heading, then the two-pill action row. Sized and colored to support the heading, not compete with it.

Anatomy

  When your agents
  get complex                ← (heading — see hero-statement.md)

  ↓ 32px gap (mt-8 on the subhead)

  Surface patterns in production, turn them into
  scenarios, and improve quality with every release.
  For teams that can't afford to get it wrong.
        ↑ 17px sans, max-w-[610px], color #39445c,
          line-height 1.65, no negative tracking

  ↓ 32px gap (mt-8 on the Actions row)

  ┌──────────────┐  ┌──────────────────┐
  │ Book a demo  │  │ Self-host  ▾     │
  └──────────────┘  └──────────────────┘
      btn-primary       SelfHostMenu (popover trigger)
      data-nav-dark     (light pill, ink-100 hairline)
      = true

The heroBody copy

const heroBody =
  "Surface patterns in production, turn them into scenarios, and improve quality with every release. " +
  "For teams that can't afford to get it wrong."

Two sentences, by design:

  1. What the product does — three verbs in a chain (surface → turn into → improve). This is the capability sentence.
  2. Who it's for — short, raises the stakes. This is the stakes sentence.

(VariantHero.tsx:10-11)

Tokens used

  • Type — text-[17px] leading-[1.65].
  • Color — the subhead is text-[#39445c] (a cool ink-blue), the only place that color appears on the page. The buttons use text-paper (white) on bg-ink-900 for primary, text-ink-900 on bg-paper for secondary.
  • Shape — primary and secondary buttons are both rounded-full pills.

Decisions

Why #39445c for the subhead (instead of ink-500 or ink-600)

ink-500 (#4d4d46) and ink-600 (#36352f) are both warm charcoals. The subhead intentionally drifts cooler — #39445c is a slight ink-blue — to create a value contrast with the warm heading above it. Same darkness (it reads at the same heaviness), different temperature. The visitor's eye stops briefly between heading and subhead because they don't feel like the same family.

This is a single hex value used in exactly one place (VariantHero.tsx:126). It's a debt; it should be promoted to --color-subhead-ink so the relationship is named.

Why a 32px gap to the heading and another 32px to the CTAs

The rhythm is pill -22px-> heading -32px-> subhead -32px-> CTAs. Even gaps between the subhead and its neighbors give the subhead equal weight on both sides, so it reads as a balanced bridge between heading and action. 32px is wide enough to break the elements into distinct lines of the composition (no longer one block) while staying tight enough to read as a single hero unit.

Why max-w-[610px] on the subhead (heading is 680px)

The subhead is narrower than the heading. Two reasons:

  1. Optical hierarchy — making the subhead narrower than the heading pushes the heading forward visually, even though the subhead has more total ink (more characters). Same trick a newspaper uses: pull-quote narrower than body.
  2. Measure — 610px at 17px is approximately the comfortable measure for body text (~70 characters per line). 680px would let it run longer and feel like prose; 540px would feel cramped.

Why two CTAs, not one or three

One CTA reads as the only path forward — and at the top of a marketing site that's a stronger claim than we want to make. Three CTAs flatten the hierarchy and the visitor has to choose.

Two — one primary + one self-service alternative — is the right split for a product like this:

  • Book a demo → enterprise / governance path (talking to sales)
  • Self-host → open-source / try-it-yourself path (the popover surfaces Docker, GitHub clone, etc.)

That's not "two CTAs" so much as "two kinds of buyers acknowledged in one row."

Why primary is dark (ink-900) and secondary is paper

The primary action is the heaviest visual element in the hero CTA row. By making it bg-ink-900 text-paper, it pulls the eye like a deep punctuation mark on a page of light copy. The secondary action is a paper pill with an ink-200 hairline — it's visible but doesn't compete.

Both pills sit at the same height; the only difference is value. (src/index.css:137-142)

Why data-nav-dark on btn-primary

The primary button is itself a dark surface. As the visitor scrolls past it, the nav's adaptive inversion needs to flip the wordmark + links to white over the button. Setting data-nav-dark={inverse ? undefined : true} (VariantHero.tsx:31) tells the nav: "I'm a dark region; hit-test against me." See patterns/nav.md.

Why active:translate-y-px (not a box-shadow press)

btn-primary and btn-secondary both translate down 1 pixel on active. No box-shadow change, no fill change. The translate is a touchpad-friendly tactile feedback — the visitor feels the click without the design needing to invent a press state.

Material design's elevation drop on press would require shadow tokens and a press state. The 1px translate is one rule, zero tokens, same psychological effect.

Self-host popover (briefly)

SelfHostMenu (rendered at VariantHero.tsx:40) is a click-to-open popover anchored to its button. It surfaces self-hosting options (Docker, source clone, etc.). It is a pill at rest; on hover it gets a chevron rotation hint.

Full pattern is covered in its own component file (src/components/variants/SelfHostMenu.tsx) — see the source. Not duplicated here because it's a secondary CTA mechanism, not a hero-band canon element.

Code references

ConcernFile:lines
Shared body copysrc/components/variants/VariantHero.tsx:10-11
Actions componentsrc/components/variants/VariantHero.tsx:26-43
Subheadsrc/components/variants/VariantHero.tsx:126-130
Button stylessrc/index.css:137-142
SelfHostMenusrc/components/variants/SelfHostMenu.tsx

Known debt

  • #39445c hardcoded at VariantHero.tsx:126. Should become --color-subhead-ink. Reasoning is documented above (cool against warm ink); making it a token names the relationship. See tokens/color.md#hardcoded-colors-debt.

Voice rules

  • The subhead is two sentences max, both short. If a third sentence creeps in, the heading isn't doing enough work.
  • The first sentence is capability (what the product does). The second is stakes (who it's for / why it matters).
  • The primary button verb is Book a demo — singular action, present tense, no ellipsis. The secondary is Self-host ▾.
Hero subhead + CTAs — live

Surface patterns in production, turn them into scenarios, and improve quality with every release. For teams that can't afford to get it wrong.

mesh backgroundcanonicalpath: src/components/variants/VariantHero.tsx

Mesh background (OrchidHero)

A slow, continuous drift of color across an off-white field — the single ambient brand element on the hero. Two whites + one accent + orange, churned by @paper-design/shaders-react's MeshGradient. Paired with two visibility protections: a giant blurry white radial keeping one half clean for text, and an optional linear fade at the bottom so the mesh dissolves into page white rather than ending on a hard horizontal cut.

Anatomy

┌──────────────────────────────────────────────────────────────────┐
│                                                                  │
│ ●●●● PROTECTED HALF ●●●●           ╭──── DRIFTING CLOUD ────╮    │
│ ●●●● radial-gradient at ●●●●       │ MeshGradient            │   │
│ ●●●● 14% 45%, 58vw,    ●●●●        │   colors: 2× white,    │   │
│ ●●●● white→white→white ●●●●         │     accent, #ff8c2e    │   │
│ ●●●● 0%   42%   transparent●●●●     │   distortion 0.66      │   │
│ ●●●● ●●●●●●●●●●●●●●●●●●●●●●        │   swirl 0              │   │
│                                     │   speed 0.74           │   │
│  ↑ hero copy (text)                 │   scale 0.96           │   │
│    sits inside the radial,          │   rotation 148         │   │
│    fully readable                   │   offsetX 0.38         │   │
│                                     ╰────────────────────────╯    │
│                                                                  │
│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─    │
│  ↑ fadeBottom (360px linear gradient to white)                   │
└──────────────────────────────────────────────────────────────────┘

Props

type Props = {
  protectSide?: 'left' | 'right' | 'none'   // default 'left'
  className?: string
  accent?: string                            // default '#f391ec' (magenta-pink)
  fadeBottom?: boolean                       // default false
}

(MeshHeroBackground.tsx:20-32)

Decisions

Why speed={0.74} and swirl={0}

The mesh hero runs at speed={0.74} with swirl={0} (zero rotational churn). The combination produces a calm continuous drift rather than a tight swirl.

The math: at this speed and zero swirl, color regions translate slowly across the canvas rather than rotating. The visitor's peripheral vision registers something is moving without their focal vision noticing the movement. The mesh becomes atmosphere.

If you raised swirl (>0.1), the mesh would churn — distracting. If you lowered speed (<0.3), the mesh would feel frozen — boring. 0.74 + 0 is the calibrated sweet spot. See MeshHeroBackground.tsx:45-47.

Why colors=['#ffffff', '#ffffff', accent, '#ff8c2e']

Two of the four color stops are white. That's intentional:

  • The MeshGradient interpolates between its stops. With two whites adjacent, the mesh stays mostly white — color appears as an edge treatment at the boundary between white and the accent, not as a flood across the surface.
  • The accent (magenta-pink or blue) and the orange are the signature: they're what makes the visitor say "this site has a pink mesh thing." But they're a small fraction of the visible canvas.

If we used four distinct colors, the mesh would read as a full-bleed color field — which would fight the readability of the hero text. Two whites keep the canvas chromatically calm. See MeshHeroBackground.tsx:40.

Why a 58vw radial blur on the protected half

The text side of the hero (protectSide="left" means the radial sits at 14% 45%) gets a huge soft white circle laid over the mesh. The math:

radial-gradient(
  circle 58vw at 14% 45%,
  #ffffff 0%,
  #ffffff 42%,
  rgba(255,255,255,0.75) 64%,
  rgba(255,255,255,0) 92%
)

(MeshHeroBackground.tsx:58)

  • 58vw — the radial is ~58% of the viewport wide. At 1440px viewport, that's an ~835px-radius circle. Big enough that its center is solid white (no mesh visible) and its edges fade slowly.
  • at 14% 45% — the center sits at 14% from the left, slightly above vertical center. The hero text block (max-w-[680px] inside container-wide 1440px, left-aligned) sits inside that protected zone.
  • The 4-stop interpolation (0% solid → 42% solid → 64% 75%-opacity → 92% transparent) gives a long soft edge — the protection doesn't end abruptly, so the visitor's eye doesn't see a circle. They see "the text side is calm; the right side has color."

Why fadeBottom instead of a hard cut

Without fadeBottom, the mesh ends at its container's bottom edge — a hard horizontal line. That line would tell the visitor "this is a band of color." Wrong story.

With fadeBottom, a 360px-tall linear gradient (MeshHeroBackground.tsx:67-72) dissolves the mesh into page white at the bottom:

linear-gradient(to bottom,
  rgba(255,255,255,0) 0%,
  rgba(255,255,255,0.7) 60%,
  #ffffff 100%
)

The mesh appears to dissolve into the page. This is the principle of soft transitions, no hard edges made literal.

In the canonical hero composition, the AgentWorkspace is pulled up into this fade region (-mt-[350px]) — so the visitor sees the workspace window emerging from the dissipating mesh. Layered, not stacked.

Why one WebGL context per page (a budget)

MeshGradient and GlassSurface (and the shader easter eggs in the brand menu) are each their own WebGL context. Browsers limit total WebGL contexts (~16 on Chrome). Each context costs setup time and GPU memory. The site needs to budget them deliberately.

The hero gets one (MeshHeroBackground). The nav glass pill is another (GlassSurface). The selector pill in the AgentWorkspace is a third. Shader easter eggs mount only while visible (their stories or the brand menu being open).

If you add a new mesh/glass/shader effect, count contexts first. See principle 5.

Why protectSide="left" for the canonical hero

The OrchidHero text block is max-w-[680px] inside container-wide (1440px), left-aligned. So the text occupies roughly the left third of the viewport. Putting the radial protection on the left (14% 45%) shields the text; the mesh's color signature shows on the right.

This is also why the AgentWorkspace, which spans full container-wide width, is pulled up under the mesh: by the time the workspace appears, the mesh has faded enough that the windows aren't drowning in pink.

Code references

ConcernFile:lines
Whole componentsrc/components/MeshHeroBackground.tsx
MeshGradient propssrc/components/MeshHeroBackground.tsx:39-50
Radial protectorsrc/components/MeshHeroBackground.tsx:54-61
fadeBottom linearsrc/components/MeshHeroBackground.tsx:65-72
Composition in OrchidHerosrc/components/variants/VariantHero.tsx:115-119
AgentWorkspace pull-up into the fadesrc/components/variants/VariantHero.tsx:135-137

Variants

  • /2 — canonical OrchidHero with accent="#f391ec" (default magenta-pink), protectSide="left", fadeBottom.
  • /3 — same shape, accent overridden to #91b2f2 (blue) for comparison.

Do / Don't

  • Do put content in a relative z-10 wrapper above the mesh. The mesh is z-0 by class.
  • Do keep the mesh paired with at least one of its protections (protectSide and/or fadeBottom). Bare mesh under text reads as decoration, not atmosphere.
  • Don't add a second mesh on the same page. One ambient drift, ever. See principle 2.
  • Don't raise swirl to "add more visual interest." That breaks the calm — and calm is what makes it work.
Mesh background — live

Live mesh — protectSide left, fadeBottom on

Atmosphere, not decoration.

agent workspacecanonicalpath: src/components/LiveSimulations.tsxpath: src/components/LiveExperiments.tsxpath: src/components/LiveTraceTable.tsx

Agent workspace

The hero's centerpiece. A two-pane window: an Assistant working on the left (Claude Code terminal or Claude Cowork conversation), and an Output streaming results on the right (Traces, Experiments, or Simulations). The assistant plays through once and holds; the output runs on its own clock and cycles through all three types.

This is the embodiment of principle 1: show the product, never a picture of it.

Anatomy

              [Chat Assistant | Code Assistant]   [Simulations | Experiments | Traces]
                    ↑ wet-white pill on active        ↑ glass pill on active

  ┌──────────────────────────────────────────────────────────────────────────┐
  │  ╔════════════════════════════════════════╗  ╭───────────────────────╮  │
  │  ║                                        ║  │                       │  │
  │  ║  Assistant pane (one of:)              ║  │  Output panel         │  │
  │  ║  - Claude Code terminal                ║  │  (one of:)            │  │
  │  ║  - Claude Cowork conversation          ║  │  - LiveSimulations    │  │
  │  ║                                        ║  │  - LiveExperiments    │  │
  │  ║  fills the window background:          ║  │  - LiveTraceTable     │  │
  │  ║  #15130f (terminal) or                 ║  │                       │  │
  │  ║  #faf9f5 (cowork paper)                ║  │  floats as inset      │  │
  │  ║                                        ║  │  white card, 10px     │  │
  │  ║  680px row (fixed)                     ║  │  gutter from frame    │  │
  │  ║                                        ║  │  rounded-xl (12px)    │  │
  │  ╚════════════════════════════════════════╝  ╰───────────────────────╯  │
  └──────────────────────────────────────────────────────────────────────────┘
   ↑ outer frame: rounded-2xl (16px), border-white/10 (terminal) or border-ink-200/70 (cowork),
     dual-layer drop shadow

Tokens used

  • Shape — outer frame rounded-2xl (16px), inner output panel rounded-xl (12px). See tokens/shape.md#window-curvature.
  • Shadow — outer 0 30px 80px -30px rgba(30,20,40,0.3), 0 10px 30px -15px rgba(30,20,40,0.12); inner 0 16px 36px -14px rgba(20,15,30,0.35). Two-layer for the outer, single-layer for the inner. See tokens/shape.md#shadows.
  • Selector pill — rounded-full border black/[0.06], white inset highlight (inset 0 0 0 1px rgba(255,255,255,0.7), inset 1.5px 2px 0 -1px rgba(255,255,255,0.9), 0 4px 12px -4px rgba(20,20,23,0.2), 0 1px 3px rgba(20,20,23,0.08)). The active output pill uses GlassSurface (refractive) instead — see patterns/nav.md.
  • Color — see Claude quote palette below; the workspace background is one of two hardcoded colors depending on assistant.

Decisions

Why a two-axis selector (assistant × output)

A linear demo (one assistant, one output) can show what the assistant does OR what the output looks like, not both. A two-axis selector shows the combinatorial story: any assistant can produce any output type. The visitor sees the matrix without us having to spell it out.

The two pills:

  • Assistant — Chat Assistant (Cowork) / Code Assistant (Claude Code). The "who's running this?" axis.
  • Output — Simulations / Experiments / Traces. The "what's it producing?" axis.

The implementation puts them above their respective panes ("tabs wing out at the top, one centered over each pane" — AgentWorkspace.tsx:86-103).

Why glass on the output pill, wet-white on the assistant pill

The two pills are visually differentiated to reinforce the axis distinction. The output pill (glass, refractive) sits over the output pane — its translucency lets the pane underneath show through, reinforcing "this controls what's below me." The assistant pill (wet-white, opaque) sits over the assistant pane — solid because the assistant pane's content (terminal black, cowork paper) is too visually busy for a transparent control above it.

This is calibrated, not arbitrary. See AgentWorkspace.tsx:90-100.

Why independent clocks

The session build-log walks through the full arc on this decision:

  1. First attempt (5f67b25) — independent timer for the output. Drifted from the assistant after iteration 1.
  2. Master clock (6b18aba, 56b533a) — the assistant emits onPhase('reset' | 'runTests') and the output advances in sync. Drift-free. This was elegant.
  3. Final shipped (this revision) — the master-clock coupling was removed. The output runs on its own clock again. Now reads as the simpler of the two, and accepts a small loss (the strict frame-accuracy is gone).

Why the team chose the looser version:

  • The strict master-clock made the assistant restart every ~15s, which was the right thing for sync but the wrong thing for calm. A demo that restarts every 15s reads as nervous; one that plays through once and holds reads as deliberate.
  • The output keeps cycling on its own (Simulations → Experiments → Traces, every ~6s per phase) so the visitor still sees motion even after the assistant has gone quiet.
  • Switching assistants reloads the assistant; the output keeps doing its thing.

This is documented in tokens/motion.md#demo-timings. The shipped output loop is AgentWorkspace.tsx:65-82.

Why a fixed 680px row height

CSS grid auto-rows size to the tallest item. With outputs of different heights (Experiments ~660px, Traces ~390px), the whole frame would jump as outputs swapped — and worse, the taller output would push the other column's pinned input bar below the overflow-hidden clip.

The fix (AgentWorkspace.tsx:110): md:grid-rows-[680px]. An explicit track. Both columns become 680px regardless of content; the output scrolls inside its panel; the assistant flexes (flex-1 overflow-y-auto + min-h-0) so its input bar stays pinned at the bottom.

680px was chosen to fit the tallest output (Experiments with 3 model rows) without scroll, accepting whitespace on the short ones. This is one of those trade-offs that can't be made smaller without picking the wrong horn: either things jump, or you scroll, or you waste space. 680px chooses the waste.

Why the output panel floats inset with a 10px gutter

The output panel doesn't fill its grid cell; it sits inside it with a p-2.5 (10px) gutter on the right pane (AgentWorkspace.tsx:123). The window frame's color (terminal black or cowork paper) shows around the panel's edge — so the panel reads as a card resting inside the frame rather than a continuation of the frame.

This is a small detail but it changes the reading. Without the gutter, the output looks attached to the assistant; with the gutter, the output looks like a separate piece of furniture inside the window.

Why the frame takes the assistant's background color

AgentWorkspace.tsx:110-114:

assistant === 'code' ? 'border-white/10 bg-[#15130f]'
                     : 'border-ink-200/70 bg-[#faf9f5]'

The window frame's background = the assistant's background. This is part of what makes the window feel like one continuous surface with the assistant pane: there's no visual boundary between the pane content and the frame around it.

Switching assistant changes the whole window background. The output panel sits as a paper-white card on top of either terminal black or cowork paper. The contrast tells the visitor which mode they're in.

Why the Code Assistant declares data-nav-dark

When the Code Assistant is active, the workspace background is #15130f (terminal black). The nav scrolling over it must flip to white. The workspace doesn't currently mark its terminal-mode container as data-nav-dark — this is a small bug worth flagging. The terminal pane should declare it, or the workspace should toggle a data-nav-dark attribute on the outer frame based on assistant === 'code'. Not blocking; cosmetic.

Why the selector pill spring is stiffness 500 / damping 38

The pill slides between tabs on layoutId (motion/react). stiffness: 500 gives it a fast travel; damping: 38 settles it without overshoot ringing. See AgentWorkspace.tsx:188. Higher stiffness = faster snap; lower damping = more bounce. The current pair lands the pill cleanly without a visible spring effect — snappy but settled.

useReducedMotion() collapses this to { duration: 0 } — the pill jumps instantly when reduced motion is preferred. See AgentWorkspace.tsx:154,186-189.

Claude quote palette

The Claude Code terminal and Claude Cowork panes use the real Claude product palettes:

Terminal (ClaudeTerminal)

UseValue
Background#15130f
Clay orange (Anthropic accent)#d97757
Slash-command blue#7aa2d6
PASS green#8fb87a
FAIL red#cf9b8e
Muted text ramp#8b887f, #6f6c63, #56534b

Cowork (ClaudeCowork)

UseValue
Paper background#faf9f5
Progress blue#3f5ad6
Chip rust#a8503c
Slash blue#3b6fd4

These are quotations of Anthropic's UI. They are NOT site palette. Do not use them anywhere outside the assistant components. See tokens/color.md#claude-quote-palette-demo-only-never-site-ui.

Known debt: they should live in a clearly-namespaced token group (e.g., --demo-claude-terminal-bg) so it's obvious at usage sites that they belong to the demo, not the site.

The three outputs (briefly)

All three share VOICE_SIMULATION_TURNS data from src/data/voiceSimulation.ts, so totals stay consistent.

ComponentTotalWhat it shows
LiveSimulationsSIMULATION_TOTAL = turns.length = 4Turn-by-turn voice conversation — agent left (voice player + transcript + "View Trace"), user right-aligned
LiveExperimentsVOICE_EXPERIMENT_TOTAL = 3Speech-to-speech model comparison (gemini-3.1-flash-live, gpt-realtime-2, …) scored on voice qualities; a climbing ⚡ N/40 counter
LiveTraceTableTRACE_TOTAL = turns.length - 1 = 3Faithful LangWatch traces list — Scenario Turn rows, blue Input / green Output previews, Simulation origin badge, expand into a span waterfall

Each output's units prop drives its streaming reveal — the count of rows currently visible. The output loop (AgentWorkspace.tsx:65-82) advances units 0 → MAX over 620ms steps, holds 3.4s, then cycles to the next type.

Code references

ConcernFile:lines
Whole workspacesrc/components/AgentWorkspace.tsx
Independent-clock output loopsrc/components/AgentWorkspace.tsx:65-82
Selector (with glass / wet-white variants)src/components/AgentWorkspace.tsx:141-216
Code Assistantsrc/components/ClaudeTerminal.tsx
Chat Assistantsrc/components/ClaudeCowork.tsx (in compact mode for the workspace)
Simulations outputsrc/components/LiveSimulations.tsx
Experiments outputsrc/components/LiveExperiments.tsx
Traces outputsrc/components/LiveTraceTable.tsx
Shared mock datasrc/data/voiceSimulation.ts

Composition with the hero

The workspace sits in OrchidHero (canonical) pulled up -mt-[350px] into the mesh's fade region (VariantHero.tsx:135). This is what makes the workspace appear to emerge from the dissipating mesh rather than sit below it as a separate band.

See patterns/mesh-background.md.

Do / Don't

  • Do keep both assistants and all three outputs pixel-faithful to their reference UIs. Fidelity is the feature.
  • Do pause the loop off-screen (already implemented via tick.cancelled pattern).
  • Don't restart the assistant uninvited. The shipped behavior is: play through once, hold. Replay is a user action.
  • Don't introduce a third selector axis. Two is the right number of dimensions for this demo.
  • Don't use the Claude palette colors anywhere outside ClaudeTerminal / ClaudeCowork / AgentWorkspace.
Agent workspace — live
Scroll into view to mount the live workspace…
customer logoscanonicalpath: src/App.tsx

Customer logos

A single horizontal row of brand wordmarks below the hero. The end of the hero band. Each logo is grayscale + 70% opacity at rest; true color and full opacity on hover.

The strip teaches a quiet rule: every logo gets its own tuned height so they all feel the same size, even though their actual pixel heights span 21 → 41 (a 2× range). This is principle 4 (optical over mathematical) in action.

Anatomy

                       TRUSTED IN PRODUCTION BY
                       ↑ 11px, uppercase, tracking 0.22em, ink-400, centered

   ┌──────────────────────────────────────────────────────────────────┐
   │                                                                  │
   │   [Backbase] [PagBank] [Visma] [Deloitte] [Altura] [Vinny] [Freeday]
   │                                                                  │
   │     22px      37px     21px    25px       26px     34px    41px
   │      0px      0px      0px     -2px       0px      0px     0px      ← mt (vertical nudge)
   │                                                                  │
   └──────────────────────────────────────────────────────────────────┘
                  ↑ optical heights (px), individually tuned

   At rest: opacity-70 + brightness-0 (true grayscale)
   On hover: opacity-100 + brightness-100 (true color restored)

   No borders bracketing the strip. Section padding alone separates it.

Tokens used

  • Type (kicker) — text-[11px] font-medium uppercase tracking-[0.22em] text-ink-400. The kicker reads as peripheral context, not as content.
  • Color — ink-400 for the kicker; logos at brightness-0 opacity-70 (grayscale) by default.
  • Space — section padding is py-14 md:py-16 (set on the wrapper section in App.tsx:51). Horizontal gap inside the strip is gap-x-6 md:gap-x-8 (CustomerLogos.tsx:31).
  • Container — .container-page (1120px max-width) — this is a content section, not a hero band.

Decisions

Why per-logo optical heights (21 → 41 px)

A uniform height treats logos like data. They aren't. A heavy, all-caps wordmark (Visma, Deloitte) at the same pixel height as a light or two-line one (Vinny, Freeday) reads as larger — heavier letters fill more of the bounding box.

The fix isn't "make them the same size." It's "make them feel the same size." The current per-logo heights (CustomerLogos.tsx:15-23):

Logoh (px)mt (px)Why
Backbase220Light wordmark, modest size
PagBank370Stacked icon-and-text composition; visually balanced at 37
Visma210Heavy all-caps; the smallest pixel height feels appropriate weight
Deloitte25-2Wordmark + small dot; the dot pushes the optical center down, so we nudge mt: -2
Altura260Mid-weight wordmark
Vinny340Light, narrow wordmark; needs more height
Freeday410Two-line composition (logo above name); needs significant height to read as part of the row

The 2× range (21 → 41) tells you how much optical work is going on. A single shared height would visibly fail.

Why grayscale + 70% opacity at rest

The customer logos are not the point of the section. The point is "real teams ship on this." The logos provide that signal, but they shouldn't compete with the hero above them or the sections below.

Grayscale + 70% opacity says "these are real, and we'd like you to recognize them, but we're not selling them." Hovering one (hover:opacity-100 hover:brightness-100) restores true color — a small invitation to engage, returning the logo to its actual identity if the visitor leans in. See CustomerLogos.tsx:62-63.

Why a graceful text fallback

The strip uses <img onError> to fall back to a styled wordmark when an asset file is missing (CustomerLogos.tsx:41-54):

if (failed) {
  return <span style={{ fontSize: Math.round(h * 0.72) }}
              className="shrink-0 font-semibold tracking-tight
                         text-ink-900 opacity-70
                         transition duration-300 hover:opacity-100">
    {name}
  </span>
}

Two reasons:

  1. The layout never breaks if a logo file is missing — the wordmark takes the same slot at roughly the same optical size (h * 0.72, calibrated so a text wordmark reads at the same weight as the image).
  2. The dev experience is better — you can add a new customer by adding their name + a placeholder filename, and ship before the actual logo file lands.

This is a tiny piece of code (10 lines) that prevents an entire class of layout breakage. Worth keeping.

Why "Trusted in production by" (and not "In production at")

The orphaned Hero.tsx used "In production at". The canonical strip says "Trusted in production by" (CustomerLogos.tsx:29).

The difference matters: "Trusted in production by" puts the companies' trust as the noun. "In production at" puts the deployment as the noun. The former is a stronger signal (these teams chose us); the latter is descriptive.

Why no borders bracketing the strip

Earlier iterations had hairline borders top and bottom of the logo strip. They were removed. The strip is bounded by vertical padding alone (py-14 md:py-16 on the section in App.tsx:51). Whitespace replaces dividers — see principle 3.

Why centered kicker but justified logo row

The kicker is text-center (CustomerLogos.tsx:28) — small, symmetric, a label sitting above the content.

The logos are flex flex-nowrap items-center justify-between (CustomerLogos.tsx:31) — they spread to fill the container width evenly. justify-between means equal gaps between logos, not equal column widths — so the per-logo optical sizing still gets to do its work without being boxed.

Centering the kicker but justifying the row is the inversion of a typical "centered group with centered text" pattern — and it works because the kicker is meta (label) while the logos are content (the actual evidence). Different roles, different alignment.

Why gap-x-6 md:gap-x-8 (24 / 32 px gutters)

The strip has to fit seven logos at 1120px on desktop. At gap-x-8 (32px gutters), six gaps × 32 = 192px of horizontal space goes to gutters, leaving ~928px for seven logos = ~132px each. The logo widths (when their heights are tuned) come in around 80–130px, so this leaves a small amount of slack — enough that justify-between distributes them evenly without crowding.

On mobile (gap-x-6 = 24px), the strip becomes tighter. At narrow widths, it'll begin to overflow — for a future revision, consider letting the strip become a marquee or wrap below ~768px.

Code references

ConcernFile:lines
CustomerLogos componentsrc/components/CustomerLogos.tsx
logos array (per-logo h + mt)src/components/CustomerLogos.tsx:15-23
Text fallback (image onError)src/components/CustomerLogos.tsx:41-54
Composition in App.tsxsrc/App.tsx:51-55

Voice rules

  • Always real customers in the canonical strip (Backbase, PagBank, Visma, Deloitte, Altura, Vinny, Freeday). The orphaned Hero.tsx showed satire names; that was not canonical. If you have to fake a logo for a screenshot, use ACME — never the satire list.
  • The kicker is fixed copy: "Trusted in production by". Don't experiment with variants here; this label is settled.

Do / Don't

  • Do tune each new logo's h (and mt if it doesn't sit on its optical center). Don't ship a logo at a default height — measure with the eye, not the bounding box.
  • Do include a graceful fallback (onError) when adding logos.
  • Don't add borders, dividers, or a card around the strip. The strip is bounded by whitespace.
  • Don't swap colors to "match the brand" — the grayscale + hover-to-color pattern is canon for all customer logos.
Customer logos — live
Trusted in production by
BackbasePagBankVismaDeloitteAlturaVinnyFreeday
Canon

Feature carousels

feature cardcanonicalpath: src/components/sections/SectionFeatures.tsx

Feature card

One item in a feature carousel. A toned visual well holding a live product replica, then a title, a one-line body, and a glass CTA that deep-links into the relevant docs. Fixed width (300px, 380px at sm), so the carousel paces predictably.

The card is deliberately thin: it is a frame around a product-replica visual plus three lines of text. All the character comes from the visual and the tone behind it.

Anatomy

  ┌──────────────────────────────────┐   ← visual well
  │                                  │      aspect-[4/3], rounded-2xl (16px)
  │      { card.visual }             │      ring-1 ring-ink-950/5
  │      mounted by <LazyVisual>     │      background: TONE[card.tone]
  │                                  │      dark tones add inset ring-white/10
  └──────────────────────────────────┘
    Title of the capability              ← h3, 19px, font-semibold, ink-950, mt-5
    One line that says what it does      ← p, 14.5px, ink-600, lh 1.55, max 360px, mt-2
    and stops before it wraps twice.
  ┌─────────────────────────┐
  │  Read the docs   ↗       │            ← <GlassCta>, mt-4
  └─────────────────────────┘

  Card width: w-[300px] sm:w-[380px], shrink-0

The five tones

TONE (SectionFeatures.tsx:60) maps a CardTone to the gradient painted behind the visual. Four are dark, one is light. The tone is chosen to flatter the replica in front of it (a dark trace list wants an ink surface; a light workbench wants the light mesh), and to give the carousel rhythm as you scroll.

ToneFamilyReads asUsed behind
periwinklelight blue radialcalm, product-forwardthe multi-turn simulation thread
inknear-black radial (#1b1b22 → #070709)terminal, backendClaude Code, cmd-K, waterfall, cost
maroondark wine radial (#2a0f1c → #4a1327)adversarial, riskred-teaming
lightMeshlight mesh + blue & clay bloomsairy, canvasevaluation workbenches
tealdark teal radial (#0f3b38 → #071e22)measured, analyticalonline evals, analytics

Dark tones get an inset ring-white/10 so the visual's edge separates from the surface; the single light tone (lightMesh) does not. See principle 7 (color is meaning) — the tone is a category signal, not decoration.

The visual well

The well is aspect-[4/3], rounded-2xl (16px, matching the site's window curvature — see shape.md), overflow-hidden, hairline ring-ink-950/5. Its aspect-[4/3] is what makes deferral free: the box holds its size whether or not the visual inside has mounted, so there is zero layout shift when LazyVisual swaps the content in.

LazyVisual (SectionFeatures.tsx:1720) wraps every card.visual. It renders an empty absolutely-positioned layer until an IntersectionObserver (rootMargin 300px) reports the card is near the viewport, then mounts the real component and disconnects. This keeps the homepage's initial DOM small — nineteen live product replicas would otherwise all mount at once, well below the fold. It is the card-level counterpart of the off-screen pause used elsewhere.

GlassCta — the bevel-glass pill

GlassCta (SectionFeatures.tsx:1748) is a standalone, reusable link styled as a raised piece of wet-white glass:

background: rgba(255,255,255,0.85)
box-shadow:
  inset 0 0 0 1px  rgba(255,255,255,0.7)     ← crisp inner edge
  inset 1.5px 2px 0 -1px rgba(255,255,255,0.9) ← top-left highlight (the bevel)
  0 4px 12px -4px rgba(20,20,23,0.2)          ← lift
  0 1px 3px rgba(20,20,23,0.08)               ← contact shadow

It is the same treatment as the hero's wet-white selector pill (see nav.md), pulled out so any surface can reuse it. Props: href, children, external (default true → adds target=_blank rel=noopener noreferrer), and className for layout. A trailing ArrowUpRight nudges on hover, and the whole pill lifts 1px. Because the fill is 0.85 opaque white, it sits legibly on all five tones without per-tone tuning.

On a feature card the CTA points at docs, not a landing page — the visitor is deep enough in the story to want the real thing.

Do / Don't

  • Do put a real component in the well. The card is a frame; the visual is the content. See principle 1.
  • Do keep the body to one line that stops before wrapping twice. max-w-[360px] is the guardrail; write to it.
  • Do wrap any deferred visual in LazyVisual and give its container a fixed aspect so nothing shifts.
  • Do reuse GlassCta for wet-white pill links anywhere, not just cards.
  • Don't invent a sixth tone without a reason. Five cover the categories; more dilutes the signal.
  • Don't drop the inset ring-white/10 on dark tones — the visual's edge disappears into the surface without it.

Code references

ConcernFile:lines
Cardsrc/components/sections/SectionFeatures.tsx:1779-1803
GlassCtasrc/components/sections/SectionFeatures.tsx:1748-1777
TONE (five gradients)src/components/sections/SectionFeatures.tsx:60-69
LazyVisual (defer + no CLS)src/components/sections/SectionFeatures.tsx:1720-1743
CardTone / FeatureCard typessrc/components/sections/SectionFeatures.tsx:40-50
Feature card — live
Tones — the surface behind each visual
periwinkle
calm, product-forward
ink
terminal, backend
maroon
adversarial, risk
lightMesh
airy, canvas
teal
measured, analytical
navy
infrastructure, routing
Card — the real component, one full item

Simulate real users

Text and voice conversations from a simulated user that pushes your agent turn after turn, like the real world does.

Run a scenario
GlassCta — legible wet-white on every tone
product-replica visualscanonicalpath: src/components/sections/SectionFeatures.tsx

Product-replica visuals

The nineteen components that live inside the feature cards. Each one is a cleaner-but-verbatim replica of a real LangWatch app screen — a simulation thread, an evaluation grid, a trace list, a cost breakdown, a command palette. Not a screenshot: a rebuild, in the same JSX/Tailwind vocabulary as the rest of the site, trimmed to the one idea the card is selling.

This is principle 1 (show the product, never a picture of it) applied nineteen times. The reason to rebuild rather than screenshot: replicas stay crisp at any DPI, restyle with the tokens, animate where it helps, and never go stale against a product screenshot from six months ago. When you build a new landing page, this catalogue is the parts bin — lift a visual, or copy the technique.

Shared primitives

Four pieces are factored out and reused across the visuals. These are the genuinely reusable exports — the individual Viz* components are specific replicas, but these are building blocks for any product surface.

EvalWorkbench (SectionFeatures.tsx:926)

The evaluations-v3 grid, generic over its input type. Props: inputHeader, inputType ('image' | 'text'), cols (WbCol[] — each a candidate with a score badge + latency clock), rows (WbRow[] — an input and one cell per column, each cell carrying a green/red evaluator dot), and evaluator (the label of the judge running down the columns). Two cards use it: Notebook or UI feeds it text (a RAG question → answer) and Multimodal feeds it images (product photos → category). Same component, two data shapes — the workbench is the reusable thing.

ProviderIcon (SectionFeatures.tsx:1224)

The OpenAI (#0b0b0d) and Anthropic (#c96442) wordless marks, as inline SVG paths so they're CSP-safe (no external asset). Used anywhere a trace, cost row, or model comparison needs to show which provider ran. Props: provider, className.

SPAN_COLORS (SectionFeatures.tsx:1216)

The span-type palette shared by every trace-flavored visual, so llm/tool/agent/rag are the same color in the trace list, the waterfall, and the tool timeline:

TypeHex
llm#5b8defblue
tool#8fb87amoss
agent#a855f7violet
rag#3fd0b6teal

This is principle 7 (color is meaning): the color is the span type, consistently, so the eye learns it once.

Spark (SectionFeatures.tsx:1088)

A tiny inline SVG sparkline — pts (a number array) and color. Used per-metric in Online evaluations. Cheap enough to drop several on a card without weight.

Catalogue

Nineteen visuals, three carousels. Tone is the card surface behind each.

Simulate

#CardComponentToneReplicates
1Simulate real usersVizSimulateRealUsersperiwinkleThe live LiveSimulations thread — a multi-turn refund conversation, agent left / user right
2Write scenarios in Claude CodeVizWriteScenarioinkA Claude Code terminal transcript: /scenarios prompt, a "Cogitated" line, Read/Write tool rows, a green scenario diff
3Local and CIVizLocalCIlightMeshA PR-checks panel whose jobs resolve to green out of order (parallelism), then fade and replay — animation gated off-screen
4Red teamingVizRedTeammaroonAn attack conversation: a social-engineering probe and the agent's refusal ("system prompt never leaked")
5More than a judgeVizJudgeinkA JudgeAgent verdict panel; the middle criterion expands into nested Explore/Read tool-call rows and its reasoning
6Tools, skills, MCPVizToolsMCPperiwinkleA trace timeline of tool calls; the stripe·mcp row expands to show its Arguments/Result JSON
7Whitebox and blackboxVizTargetslightMeshAn interactive integration picker — Whitebox (frameworks) / Blackbox (transports); the chips swap when you flip modes

Evaluate

#CardComponentToneReplicates
8LLM as a judgeVizEvaluatorPickerlightMeshThe "Add an evaluator" chooser — LLM-as-a-judge selected, custom-code and workflow alternates
9Notebook or UIVizRagWorkbenchinkEvalWorkbench (text): a RAG question → answer over an agriculture manual, scored by "LLM answer match" across gpt/claude columns
10Online evaluationsVizOnlineEvalstealBusiness-metric monitors with Spark lines — agent refusals, annoyed-user signal (growing), the escalate_to_human tool, p95 latency
11PairwiseVizPairwiselightMeshA three-column compare — two candidates and a pairwise winner column, with a running win/tie tally
12MultimodalVizImageWorkbenchperiwinkleEvalWorkbench (image): product photos → category, mostly-green with one red miss, gpt/claude columns

Observe

#CardComponentToneReplicates
13OpenTelemetry nativeVizTraceTablelightMeshThe traces list — ProviderIcon + model, an origin badge (Simulation/Evaluation/Production), a p95 duration bar, cost; scrolls on overflow
14At the speed of thoughtVizCommandPaletteinkThe Cmd-K palette — recent trace + evaluation, a five-item navigation list, keyboard-hint footer
15Every agent, every tokenVizCostinkA cost breakdown — expected vs bundled-plan spend, a model pareto with ProviderIcon, a cache read/write/hit-rate footer
16Custom views and AI searchVizAiSearchperiwinkleAn "Ask AI" search over the most expensive traces, cost-sorted, each with ProviderIcon and a p95 bar
17See it every wayVizWaterfallinkWaterfall/Flame/Sequence tabs — a flame graph on top, a minimal sequence diagram (SequenceMini) below
18Topic clusteringVizTopicslightMeshA clustered-topics bar list
19Plot any metricVizAnalyticstealEscalations-to-humans by topic, as colored vertical bars

A note on quoted palettes

Some visuals quote another product's UI colors — the Claude Code terminal in Write scenarios uses Anthropic's clay/blue/green palette, exactly as ClaudeTerminal does in the agent workspace. Those are demo-only quotations, never site palette. See color.md. The same discipline applies to ProviderIcon's brand hexes: they belong to the mark, not the site.

Do / Don't

  • Do rebuild the product in JSX; don't drop a screenshot. Fidelity plus token-native styling is the whole point.
  • Do reuse EvalWorkbench, ProviderIcon, SPAN_COLORS, and Spark before writing a new one — a fifth trace visual should share the span colors, not invent them.
  • Do keep SPAN_COLORS the single source for span-type color across every trace-flavored visual.
  • Do trim each replica to the one idea its card sells. These are not the full app.
  • Don't promote a quoted palette (Claude terminal, provider brand hexes) into a site token.
  • Don't animate a visual that doesn't earn it. Only three do (the simulation thread, the CI checks, the online-eval sparklines); the rest are still. See principle 2.

Code references

ConcernFile:lines
Simulate visualssrc/components/sections/SectionFeatures.tsx:91-664
Evaluate visualssrc/components/sections/SectionFeatures.tsx:876-1214
Observe visualssrc/components/sections/SectionFeatures.tsx:1268-1719
EvalWorkbench + WbCol/WbRowsrc/components/sections/SectionFeatures.tsx:923-1017
ProviderIconsrc/components/sections/SectionFeatures.tsx:1224-1243
SPAN_COLORSsrc/components/sections/SectionFeatures.tsx:1216-1221
Sparksrc/components/sections/SectionFeatures.tsx:1088
Product-replica visuals — live
Shared primitives
ProviderIcon
Spark
All nineteen visuals (live, deferred as you scroll)
Simulate real users
periwinkle
Write scenarios in Claude Code
ink
Local and CI
lightMesh
Red teaming
maroon
More than a judge
ink
Tools, skills, MCP
periwinkle
Whitebox and blackbox
lightMesh
LLM as a judge
lightMesh
Notebook or UI
ink
Online evaluations
teal
Pairwise
lightMesh
Multimodal
periwinkle
OpenTelemetry native
lightMesh
At the speed of thought
ink
Every agent, every token
ink
Custom views and AI search
periwinkle
See it every way
ink
Topic clustering
lightMesh
Plot any metric
teal
landing-page replicascanonicalpath: src/pages/ClaudeCodeUsagePage.tsx

Landing-page product replicas

Page-scale rebuilds of real LangWatch screens, in DOM, for landing pages. Two hard lessons from the first attempt are now rules:

  1. Light mode. The app is light; the home page is light. Dark panels for data-dense replicas read as generic AI-slop and hide the product. Replica surfaces are white with border-ink-100, exactly like the app.
  2. Replicate a screenshot, not an idea. The v1 replicas were plausible abstractions and got trashed. The v2 trace explorer was rebuilt against a specific app screenshot, chrome and all (Ask AI chip, tab strip, keyboard-hint kbds, filter chips, view pills, the selected row). Fidelity to a real screen is what makes it read as the product.

The pieces

  • Trace explorer composite: TracesListPanel (columns TIME / TRACE (SUMMARY) / ORIGIN / DURATION / COST / TOKENS / SPANS / MODEL, expanded rows with mono blue Input / green Output lines) with TraceDetailDrawer absolutely positioned to overlap it from the right on lg+, stacking below on smaller screens. The drawer keeps natural height (pin top/right only); pinning both ends squeezes the bottom span panel.
  • Popover visuals: the app's hover-detail cards (cost breakdown, token classes) staged as if hovered from the real trace-detail chips: the HeaderChip row on top, compact Popovers hanging under their chips, never covering them.
  • ClawdSticker: the mascot on a white rounded card with a thick white border, hard offset shadows, and a slight static rotate/skew tilt, Paper Mario style. (A die-cut contour-border variant with paper-twist motion was tried and rolled back; the plain card read better.)
  • CliCommand: dark terminal pill with $ prefix, click-to-select-all text, and a copy button; the CTA on dev pages.

Rules

  • Feed replicas real data. The trace rows are real sessions from building LangWatch, the founder's own asks included. Never lorem, never invented round numbers.
  • Span colors are meaning: blue = llm, green = tool, purple = agent / Bundled. Same palette as the app and SPAN_COLORS.
  • Reuse the home card anatomy (TONE panels, GlassCta, LazyVisual from SectionFeatures) but as a wrapping grid on landing pages (4-up at xl, 3 at lg), aligned to container-wide: no carousel, no rail offset. Visuals mount through LazyVisual, so screenshot scripts must scroll the page before capturing, and tests assert static copy, not visual internals.
  • Responsive by hiding columns, not shrinking type: keep summary + one number on mobile; strips that overflow get overflow-x-auto with hidden scrollbars.
  • Static and prerender-safe: plain DOM, no client-only APIs at render.
Landing-page replicas — live
Ask AI
+
Trace (summary)Duration
2hI did the gcloud auth login hopefully you can continue?2307.1s

Input  I did the gcloud auth login hopefully you can continue?

Output The scan came back empty: **no project in your account has the Google Ads API enabled at all**, which actually pins the diagnosis. Modern Ads API accounts declare their Cloud project explicitly in th…

5hI have several design change requests here [Image #7]5623.4s

Input  I have several design change requests here [Image #7]

Output All three are in: clawd is a die-cut sticker on the right with the slow wobble, the trace colors match the app tokens, and the span tree groups repeats with ×N pills…

1dok merged! but /reuse-worktree cuz already found a bug513.9s

Input  ok merged! but /reuse-worktree cuz already found a bug

Output Worktree reset to latest main on a fresh branch. The bug reproduces: the retry queue drops the last event when the worker restarts. Writing the failing test first, fix goes on the same PR…

3dawesome, this is merged, but apply gave those errors:1543.9s

Input  awesome, this is merged, but apply gave those errors:

Output Both errors share one root cause: the migration runs before the enum exists. Reordered the migrations, apply is clean, and the seed passes on a fresh database…

4dcan you get rid of all those stale artifacts? clean them up, maybe commit first…1534.3s

Input  can you get rid of all those stale artifacts? clean them up, maybe commit first…

Output Committed the WIP first, then removed 14 stale artifacts: 9 orphaned preview deploys, 3 dangling worktrees, 2 cache volumes. 4.2GB freed, nothing CI references…

6dnah why would we keep it in the cluster? why not just ignore it? it was done on…374.9s

Input  nah why would we keep it in the cluster? why not just ignore it? it was done on…

Output Right, dropped it from the cluster and added it to the ignore list instead. The sync job no longer touches it and the drift alert that kept firing is gone…

I did the gcloud auth login hopefully you can …
3 / 41
Duration2307.1sSpans557CostBundledTokens76.8K in · 411.9K outModelclaude-opus-4-8Reasoning effortmax
Service claude-codeOrigin coding_agentEval Presidio PII Detection 0.00
Summary 0Trace TConversation C2h ago
Conversation contextturn 3 of 41
Span
0ms2307.1s
I did the gcloud auth logi…2307.1s
claude-opus-4-8×91 repeatedavg 24.2s · 4.5s–155.4s
Edit32ms
Bash×37 repeatedavg 2.7s · 4ms–33.3s
Bash2.0s
mcp_tool×33 repeatedavg 666ms · 12ms–2.9s
mcp_tool214ms
away_summaryclaude-opus-4-8$0.546.9s
ToolBash2.0s
Input and outputPrettyTextJSONMarkdown
{
  "command": "gcloud services list --enabled | grep -i googleads",
  "description": "Check which projects have the Ads API enabled"
}
FLATJSON

Span attributes

span_id9a01f7d34cc8b52a
claude_code.cost_usd_micros0
claude_code.duration_ms2000
claude_code.event.nametool_result
claude_code.event.sequence3947
claude_code.query_sourcerepl_main_thread
claude_code.speednormal
claude_code.terminal.typetmux
copy: the dev registercanonical

Copy: the dev register

How to write landing pages aimed at developers. Learned the hard way: the first /claude-code-usage shipped with marketing value-talk and got thrown out whole. These rules exist so the next page does not repeat it.

The rule

Say what it IS and what it DOES, in technical terms. Developers reading a tool page want the mechanism; value framing reads as selling and gets the tab closed. ccusage is the calibration: its site says "Analyze your Claude Code token usage from local JSONL files", never "know your true spend".

Banned on dev pages

  • Value adjectives and framing: "honest", "killer", "the story", "actually", "what a plan is really worth". If a phrase argues why the reader should care, cut it; state the fact and let them decide.
  • Benefit-first headings: "What a flat plan is really worth" is marketing; "Bundled plans, still priced" states the feature.
  • Long subheads: one or two lines. A 3-4 line hero paragraph is a tell that it is arguing instead of describing.
  • Everything in the sitewide writing rules (no em dashes, no negation-reframes, no "X, not Y" closers, numbers carry their consequence). Those apply here doubly.

What to do instead

  • Command as CTA. If the product starts with a command, the command is the call to action: $ npx langwatch claude in a terminal block beats a "Start free" button on a dev page.
  • Nouns from the product. Titles name the UI element or the mechanism: token classes, spans, cache reads, ×33 repeated. If the reader can match the title to a pixel in the screenshot, it is the right title.
  • Real data over invented data. The strongest copy on the rebuilt page is not copy: it is real sessions in the trace list, inputs and outputs included. Quote the actual artifact.
  • Numbers with their unit and context: "Cache read 8,398,963" from a real popover says more than any adjective.
  • Founder-verbatim wins. When the founder writes a line ("Or Codex, OpenCode, Copilot, Cursor and Pi."), ship it untouched; the voice is the point.

Register boundaries

Dev pages never say "governance", "compliance", or procurement vocabulary; that language belongs to buyer-register surfaces (the enterprise page, the category pages). One quiet link can bridge registers; the copy never mixes them. An RTL test enforces this on /claude-code-usage (tests/landing-pages.test.tsx).