diff --git a/apps/website/src/app/card/tokens.ts b/apps/website/src/app/card/tokens.ts
index 8f1d53b1c..c27563d5f 100644
--- a/apps/website/src/app/card/tokens.ts
+++ b/apps/website/src/app/card/tokens.ts
@@ -19,16 +19,16 @@ export const CARD = {
/** --color-surface-dim, the user bubble. */
dim: '#f5f5f5',
/** --color-text-primary / secondary / muted */
- ink: '#1c1c1c',
+ ink: '#0a0a0a',
inkSecondary: '#464646',
inkMuted: '#737373',
/** --color-border / --color-border-strong */
border: '#e5e5e5',
borderStrong: '#c8c8c8',
/** --color-accent and its surface/border tints. */
- accent: '#004090',
- accentSurface: 'rgba(0, 64, 144, 0.06)',
- accentBorder: 'rgba(0, 64, 144, 0.30)',
+ accent: '#15253e',
+ accentSurface: 'rgba(255, 175, 0, 0.10)',
+ accentBorder: 'rgba(255, 175, 0, 0.35)',
/** BrowserFrame traffic lights (ui.css). */
trafficRed: '#FF5F57',
trafficAmber: '#FEBC2E',
diff --git a/apps/website/src/app/global.css b/apps/website/src/app/global.css
index 316c2f6c6..aad2c265b 100644
--- a/apps/website/src/app/global.css
+++ b/apps/website/src/app/global.css
@@ -34,7 +34,7 @@ html {
body {
background-color: var(--color-bg);
color: var(--color-text-primary);
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* overflow-x: CLIP, never hidden. `hidden` makes
a scroll container
diff --git a/apps/website/src/app/layout.tsx b/apps/website/src/app/layout.tsx
index 08dfe1992..0a32b3ea3 100644
--- a/apps/website/src/app/layout.tsx
+++ b/apps/website/src/app/layout.tsx
@@ -1,5 +1,5 @@
import type { Metadata } from 'next';
-import { EB_Garamond, Inter, JetBrains_Mono } from 'next/font/google';
+import { Archivo, Archivo_Black, Inter, JetBrains_Mono } from 'next/font/google';
import '@threadplane/design-tokens/tokens.css';
import './global.css';
import { Nav } from '../components/shared/Nav';
@@ -20,16 +20,31 @@ import { getFormPolicy } from '../lib/growth/form-policy';
import { WebsiteSignals } from '../components/shared/WebsiteSignals';
import { websiteContentCatalog } from '../lib/growth/website-content';
-const garamond = EB_Garamond({
+const display = Archivo_Black({
subsets: ['latin'],
- weight: ['400', '700', '800'],
+ weight: '400',
+ variable: '--font-display',
+});
+
+const sans = Archivo({
+ subsets: ['latin'],
+ // Archivo ships a true italic. Loading it makes every italic on the text
+ // face real rather than an obliqued upright — Inter was normal-only here,
+ // so those sites had been faux-italic all along.
style: ['normal', 'italic'],
- variable: '--font-garamond',
+ variable: '--font-sans',
});
-const inter = Inter({
+/**
+ * Inter is retained for diagrams only, and MUST be loaded here rather than
+ * left to theme.css: next/font registers its family under a hashed name, so
+ * theme.css's raw `Inter, system-ui` stack never matches it and would silently
+ * fall back to system-ui. Diagram geometry is pinned to Inter's metrics —
+ * see the FONTS note in src/styles/ui.css and e2e/home-architecture.spec.ts.
+ */
+const diagram = Inter({
subsets: ['latin'],
- variable: '--font-inter',
+ variable: '--font-diagram',
});
const mono = JetBrains_Mono({
@@ -66,7 +81,7 @@ export default function RootLayout({
return (
{/*
diff --git a/apps/website/src/components/docs/DocsControlPlane.spec.tsx b/apps/website/src/components/docs/DocsControlPlane.spec.tsx
index 7de3d6298..149f7a06b 100644
--- a/apps/website/src/components/docs/DocsControlPlane.spec.tsx
+++ b/apps/website/src/components/docs/DocsControlPlane.spec.tsx
@@ -50,7 +50,7 @@ describe('DocsControlPlane', () => {
'[data-docs-control-plane-context] [data-control-plane-section-heading]',
]) {
const declarations = declarationsFor(docsCss, selector);
- expect(declarations).toMatch(/font-family:\s*var\(--font-inter\)/);
+ expect(declarations).toMatch(/font-family:\s*var\(--font-sans\)/);
expect(declarations).toMatch(/font-size:\s*12px/);
expect(declarations).toMatch(/font-weight:\s*600/);
expect(declarations).toMatch(/letter-spacing:\s*normal/);
diff --git a/apps/website/src/components/landing/Hero.tsx b/apps/website/src/components/landing/Hero.tsx
index 37636b0d1..296454e2a 100644
--- a/apps/website/src/components/landing/Hero.tsx
+++ b/apps/website/src/components/landing/Hero.tsx
@@ -27,7 +27,7 @@ export function Hero() {
const closeInstall = useCallback(() => setInstallOpen(false), []);
return (
-
+
{HERO_EYEBROW}
@@ -59,7 +59,7 @@ export function Hero() {
- {HERO_TRUST_LINE}
+ {HERO_TRUST_LINE}
diff --git a/apps/website/src/components/landing/chat-landing/ChatLandingCodeShowcase.tsx b/apps/website/src/components/landing/chat-landing/ChatLandingCodeShowcase.tsx
index 6762af2c4..bb2570776 100644
--- a/apps/website/src/components/landing/chat-landing/ChatLandingCodeShowcase.tsx
+++ b/apps/website/src/components/landing/chat-landing/ChatLandingCodeShowcase.tsx
@@ -22,7 +22,7 @@ export class MyChatPage {
const SNIPPET_2 = `chat {
--chat-bg: #f8f9fc;
- --chat-user-bg: #004090;
+ --chat-user-bg: #15253E;
--chat-user-color: #ffffff;
--chat-assistant-bg: #f0f4ff;
--chat-font-family: 'Inter', sans-serif;
diff --git a/apps/website/src/components/ui/Section.spec.tsx b/apps/website/src/components/ui/Section.spec.tsx
index 99d0a08af..f59c12ff5 100644
--- a/apps/website/src/components/ui/Section.spec.tsx
+++ b/apps/website/src/components/ui/Section.spec.tsx
@@ -9,6 +9,12 @@ describe('Section', () => {
expect(el?.getAttribute('data-surface')).toBe('dark');
});
+ it('renders data-surface="signal" when asked', () => {
+ const { container } = render();
+ const el = container.querySelector('[data-ui="section"]');
+ expect(el?.getAttribute('data-surface')).toBe('signal');
+ });
+
it('defaults to canvas', () => {
const { container } = render();
expect(
diff --git a/apps/website/src/components/ui/Section.tsx b/apps/website/src/components/ui/Section.tsx
index 8456115ac..3958146b4 100644
--- a/apps/website/src/components/ui/Section.tsx
+++ b/apps/website/src/components/ui/Section.tsx
@@ -1,7 +1,7 @@
import type { ReactNode, HTMLAttributes } from 'react';
import { cn } from '../../lib/cn';
-type Surface = 'canvas' | 'tinted' | 'white' | 'dark';
+type Surface = 'canvas' | 'tinted' | 'white' | 'dark' | 'signal';
interface SectionProps extends Omit, 'id'> {
children: ReactNode;
diff --git a/apps/website/src/styles/chrome.css b/apps/website/src/styles/chrome.css
index 964fe8a64..a5abb50ef 100644
--- a/apps/website/src/styles/chrome.css
+++ b/apps/website/src/styles/chrome.css
@@ -205,7 +205,7 @@
padding: 10px 0;
font-size: 15px;
font-weight: 500;
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
background: transparent;
color: var(--color-text-muted);
border: none;
@@ -240,7 +240,7 @@
line-height: 24px;
min-height: 44px;
text-decoration: none;
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
color: var(--color-text-secondary);
background: transparent;
}
@@ -286,7 +286,7 @@
color: var(--color-accent);
background: var(--color-accent-surface);
text-decoration: none;
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-weight: 600;
}
.nav-mobile-section-toggle {
@@ -301,7 +301,7 @@
padding: 12px 14px;
min-height: 48px;
border-radius: 8px;
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 16px;
line-height: 24px;
color: var(--color-text-primary);
@@ -323,7 +323,7 @@
min-height: 48px;
color: var(--color-text-secondary);
text-decoration: none;
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
}
.nav-mobile-github-link {
display: flex;
@@ -334,7 +334,7 @@
min-height: 48px;
color: var(--color-text-secondary);
text-decoration: none;
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 16px;
}
.nav-mobile-cta-wrap {
@@ -421,9 +421,8 @@
margin-bottom: 8px;
}
.toast-title {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: 1.05rem;
- font-weight: 700;
color: var(--color-text-primary);
line-height: 1.3;
margin-bottom: 6px;
@@ -461,7 +460,7 @@
font-size: 12px;
color: var(--color-text-muted);
text-decoration: underline;
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
}
/* Mobile docs-search entry (polish arc PR 3) — button reset to match the
diff --git a/apps/website/src/styles/docs.css b/apps/website/src/styles/docs.css
index 728ef3ab5..459761dee 100644
--- a/apps/website/src/styles/docs.css
+++ b/apps/website/src/styles/docs.css
@@ -163,9 +163,9 @@ body:has([data-website-workspace-host]) {
.docs-prose { overflow-wrap: break-word; word-break: break-word; max-width: 100%; }
.docs-prose > * { max-width: 100%; }
-.docs-prose h1 { font-size: 1.875rem; font-weight: 700; margin-top: 0; margin-bottom: 1rem; font-family: var(--font-garamond); }
-.docs-prose h2 { font-size: 1.5rem; font-weight: 600; margin-top: 2.5rem; margin-bottom: 1rem; font-family: var(--font-garamond); }
-.docs-prose h3 { font-size: 1.25rem; font-weight: 600; margin-top: 2rem; margin-bottom: 0.75rem; font-family: var(--font-garamond); }
+.docs-prose h1 { font-size: 1.875rem; margin-top: 0; margin-bottom: 1rem; font-family: var(--font-display); }
+.docs-prose h2 { font-size: 1.5rem; margin-top: 2.5rem; margin-bottom: 1rem; font-family: var(--font-display); }
+.docs-prose h3 { font-size: 1.25rem; margin-top: 2rem; margin-bottom: 0.75rem; font-family: var(--font-display); }
.docs-prose p { line-height: 1.75; margin-bottom: 1.25rem; }
.docs-prose ul, .docs-prose ol {
margin-bottom: 1.25rem;
@@ -331,7 +331,7 @@ body:has([data-website-workspace-host]) {
/* mdx — Callout (header band; tone via custom properties) */
[data-mdx="callout"] {
--callout-tone-text: var(--color-accent);
- --callout-tone-surface: rgba(0, 64, 144, 0.06);
+ --callout-tone-surface: rgba(255, 175, 0, 0.1);
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
@@ -366,14 +366,14 @@ body:has([data-website-workspace-host]) {
flex-shrink: 0;
}
.mdx-callout-title {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 13px;
font-weight: 600;
color: var(--callout-tone-text);
}
.mdx-callout-body {
padding: 12px 14px;
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 15px;
line-height: 1.6;
color: var(--color-text-secondary);
@@ -406,7 +406,7 @@ body:has([data-website-workspace-host]) {
padding: 7px 14px;
border-radius: var(--radius-md);
border: 1px solid var(--callout-tone-text, var(--color-accent));
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 13px;
font-weight: 600;
line-height: 1.2;
@@ -521,14 +521,14 @@ body:has([data-website-workspace-host]) {
padding-bottom: 8px;
}
.mdx-step-title {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 17px;
font-weight: 600;
color: var(--color-text-primary);
margin-bottom: 4px;
}
.mdx-step-body {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 16px;
line-height: 1.6;
color: var(--color-text-secondary);
@@ -570,8 +570,7 @@ body:has([data-website-workspace-host]) {
margin-bottom: 6px;
}
.mdx-card-title {
- font-family: var(--font-garamond);
- font-weight: 700;
+ font-family: var(--font-display);
font-size: 0.95rem;
color: var(--color-text-primary);
margin-bottom: 4px;
@@ -582,7 +581,7 @@ body:has([data-website-workspace-host]) {
margin-top: 2px;
}
.mdx-card-body {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 0.85rem;
line-height: 1.5;
color: var(--color-text-secondary);
@@ -759,7 +758,7 @@ body:has([data-website-workspace-host]) {
margin-bottom: 8px;
}
.mdx-chip-title {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 13px;
font-weight: 600;
color: var(--color-text-primary);
@@ -830,7 +829,7 @@ body:has([data-website-workspace-host]) {
.tp-diagram-node[data-meta="mono"] .tp-diagram-meta { font-family: var(--font-mono); }
.tp-diagram-dot { fill: var(--color-border); }
.tp-diagram-caption {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 13px;
color: var(--color-text-muted);
text-align: center;
@@ -863,13 +862,13 @@ body:has([data-website-workspace-host]) {
fill: var(--color-text-primary);
}
.tp-diagram-node[data-title="sans"] .tp-diagram-title {
- font-family: var(--font-inter);
+ font-family: var(--font-diagram);
font-size: 11px;
font-weight: 600;
fill: var(--color-text-secondary);
}
.tp-diagram-meta {
- font-family: var(--font-inter);
+ font-family: var(--font-diagram);
font-size: 10.5px;
fill: var(--color-text-muted);
}
@@ -1047,7 +1046,7 @@ body:has([data-website-workspace-host]) {
min-width: 0;
}
.docs-control-plane-search-label {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 0.8rem;
}
.docs-control-plane-search-kbd {
@@ -1105,7 +1104,7 @@ body:has([data-website-workspace-host]) {
width: 100%;
border: none;
outline: none;
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 0.95rem;
color: var(--color-text-primary);
background: transparent;
@@ -1151,7 +1150,7 @@ body:has([data-website-workspace-host]) {
/* Content hits: a section heading, its page, and why it matched. */
.docs-search-group-label {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 11px;
font-weight: 600;
letter-spacing: 0.04em;
@@ -1162,7 +1161,7 @@ body:has([data-website-workspace-host]) {
.docs-search-result-snippet {
flex-basis: 100%;
width: 100%;
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 12px;
line-height: 1.5;
color: var(--color-text-secondary);
@@ -1233,7 +1232,7 @@ body:has([data-website-workspace-host]) {
* links inside each li, and when only the links carried 13px the first two
* separators inherited body's 16px/24px and floated 3px high. */
.website-workspace-host [data-workspace-trail] ol[data-workspace-trail-list] {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 13px;
line-height: 1.5;
color: var(--color-text-muted);
@@ -1288,7 +1287,7 @@ body:has([data-website-workspace-host]) {
width: 100%;
text-align: left;
padding: 8px 12px;
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 13px;
color: var(--color-text-primary);
background: transparent;
@@ -1620,7 +1619,7 @@ body:has([data-website-workspace-host]) {
height: 22px;
border-radius: 6px;
flex-shrink: 0;
- background: rgba(0, 64, 144, 0.1);
+ background: rgba(255, 175, 0, 0.14);
display: flex;
align-items: center;
justify-content: center;
@@ -1693,8 +1692,8 @@ body:has([data-website-workspace-host]) {
color: #5e35b1;
}
.arch-flow-log-source[data-source="langgraph"] {
- background: rgba(0, 64, 144, 0.08);
- color: #004090;
+ background: rgba(255, 175, 0, 0.12);
+ color: var(--color-accent);
}
.arch-flow-log-source[data-source="signal"] {
background: rgba(16, 185, 129, 0.08);
@@ -1730,7 +1729,7 @@ body:has([data-website-workspace-host]) {
margin-bottom: 8px;
}
.docs-prevnext-title {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 16px;
font-weight: 600;
color: var(--color-accent);
@@ -1860,7 +1859,7 @@ body:has([data-website-workspace-host]) {
background: var(--color-accent-surface);
}
.docs-control-plane [data-control-plane-rail-label] {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 10px;
line-height: 1;
font-weight: 600;
@@ -1902,7 +1901,7 @@ body:has([data-website-workspace-host]) {
color: var(--color-text-muted);
background: transparent;
cursor: pointer;
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 12px;
font-weight: 600;
letter-spacing: normal;
@@ -1925,7 +1924,7 @@ body:has([data-website-workspace-host]) {
margin: 0;
padding: 8px;
color: var(--color-text-muted);
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 12px;
line-height: 1.2;
font-weight: 600;
@@ -1946,7 +1945,7 @@ body:has([data-website-workspace-host]) {
justify-content: space-between;
}
.docs-sidebar-lib-trigger-label {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 13px;
}
/* Library-neutral pages: the picker is an invitation, not a statement. */
@@ -1972,7 +1971,7 @@ body:has([data-website-workspace-host]) {
display: block;
padding: 7px 9px 4px;
color: var(--color-text-muted);
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 10.5px;
font-weight: 700;
letter-spacing: 0.07em;
@@ -1994,7 +1993,7 @@ body:has([data-website-workspace-host]) {
text-align: left;
text-decoration: none;
}
-.docs-sidebar-lib-item-title { font-family: var(--font-inter); }
+.docs-sidebar-lib-item-title { font-family: var(--font-sans); }
.docs-sidebar-top-links,
.docs-sidebar-section-links { display: flex; flex-direction: column; }
.docs-sidebar-top-links { margin: 0 8px 8px; }
@@ -2003,7 +2002,7 @@ body:has([data-website-workspace-host]) {
margin: 1px 0;
padding: 7px 9px;
border-radius: 7px;
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 13px;
line-height: 1.35;
text-decoration: none;
@@ -2019,7 +2018,7 @@ body:has([data-website-workspace-host]) {
align-items: center;
justify-content: space-between;
color: var(--color-text-muted);
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 12px;
font-weight: 600;
}
@@ -2067,7 +2066,7 @@ body:has([data-website-workspace-host]) {
color: var(--color-surface);
background: var(--color-text-primary);
box-shadow: var(--shadow-sm);
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 11px;
font-weight: 500;
line-height: 1;
@@ -2113,7 +2112,7 @@ body:has([data-website-workspace-host]) {
/* Docs table of contents: complete rounded active/hover surfaces. */
.docs-toc:focus { outline: none; }
.docs-toc-label {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 12px;
font-weight: 600;
}
@@ -2162,7 +2161,7 @@ body:has([data-website-workspace-host]) {
color: var(--color-surface);
background: var(--color-text-primary);
box-shadow: var(--shadow-sm);
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 11px;
font-weight: 500;
line-height: 1;
@@ -2220,7 +2219,7 @@ body:has([data-website-workspace-host]) {
display: flex;
align-items: center;
color: var(--color-text-secondary);
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 12px;
text-decoration: none;
}
diff --git a/apps/website/src/styles/font-vars.spec.ts b/apps/website/src/styles/font-vars.spec.ts
new file mode 100644
index 000000000..09b368c94
--- /dev/null
+++ b/apps/website/src/styles/font-vars.spec.ts
@@ -0,0 +1,49 @@
+import { readFileSync, readdirSync } from 'node:fs';
+import { join } from 'node:path';
+import { describe, it, expect } from 'vitest';
+
+/**
+ * Every `--font-*` custom property this app references must be one the app
+ * actually supplies.
+ *
+ * `layout.tsx` defines exactly four, via `next/font` on ``. A reference
+ * to anything else resolves to nothing, and an undefined custom property in a
+ * `font-family` declaration is invalid at computed-value time — the whole
+ * declaration is dropped and the element silently falls back to the inherited
+ * or system face. Nothing else catches it: jsdom does not resolve custom
+ * properties, and the Next build does not type-check CSS.
+ *
+ * This guard exists because it happened twice in one day. The 2026-09-07
+ * aviation retheme renamed `--font-garamond`/`--font-inter` to
+ * `--font-display`/`--font-sans`, and a PR that landed on main in between
+ * added two new rules still using the old names. They would have shipped as
+ * system-font fallbacks that look almost right.
+ */
+const SUPPLIED = new Set([
+ '--font-display', // Archivo Black
+ '--font-sans', // Archivo
+ '--font-diagram', // Inter — diagram geometry is pinned to its metrics
+ '--font-mono', // JetBrains Mono
+]);
+
+describe('font custom properties', () => {
+ const dir = __dirname;
+ const files = readdirSync(dir).filter((f) => f.endsWith('.css'));
+
+ it('reads a non-trivial number of stylesheets', () => {
+ expect(files.length).toBeGreaterThan(3);
+ });
+
+ it('references only font vars that layout.tsx supplies', () => {
+ const offenders: string[] = [];
+ for (const file of files) {
+ const css = readFileSync(join(dir, file), 'utf-8');
+ for (const match of css.matchAll(/var\((--font-[a-z0-9-]+)/g)) {
+ if (!SUPPLIED.has(match[1])) {
+ offenders.push(`${file}: ${match[1]}`);
+ }
+ }
+ }
+ expect([...new Set(offenders)].sort()).toEqual([]);
+ });
+});
diff --git a/apps/website/src/styles/forms.css b/apps/website/src/styles/forms.css
index de81ec21b..96f5ec4e2 100644
--- a/apps/website/src/styles/forms.css
+++ b/apps/website/src/styles/forms.css
@@ -22,7 +22,7 @@
gap: 6px;
}
[data-ui="field-label"] {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 13px;
font-weight: 500;
color: var(--color-text-primary);
@@ -33,7 +33,7 @@
}
[data-ui="field-help"] {
margin: 0;
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 12px;
color: var(--color-text-muted);
}
@@ -42,7 +42,7 @@
align-items: center;
gap: 6px;
margin: 0;
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 12px;
color: var(--color-status-error);
}
@@ -66,7 +66,7 @@
box-sizing: border-box;
height: var(--form-control-height);
padding: 0 14px;
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: var(--text-body);
color: var(--color-text-primary);
background: var(--color-surface);
@@ -141,7 +141,7 @@ select[data-ui="form-control"] {
}
[data-ui="form-disclosure"] {
margin: 0;
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 12px;
line-height: 1.45;
color: var(--color-text-muted);
@@ -180,7 +180,7 @@ select[data-ui="form-control"] {
align-items: flex-start;
padding: 14px 16px;
border-radius: var(--form-control-radius);
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: var(--text-body);
line-height: var(--text-body--line-height);
color: var(--color-text-primary);
diff --git a/apps/website/src/styles/landing.css b/apps/website/src/styles/landing.css
index c10ab8bea..e188e6a6f 100644
--- a/apps/website/src/styles/landing.css
+++ b/apps/website/src/styles/landing.css
@@ -14,21 +14,21 @@
margin-bottom: 16px;
}
.hero-heading {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: var(--text-h1);
line-height: var(--text-h1--line-height);
- font-weight: 700;
+ font-weight: 400; /* Archivo Black ships a single weight */
color: var(--color-text-primary);
margin: 0;
margin-bottom: 24px;
- letter-spacing: -0.02em;
+ letter-spacing: -0.032em;
}
/* One line per thought; the space between the spans keeps textContent equal to HERO_H1. */
.hero-heading-line {
display: block;
}
.hero-subhead {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: var(--text-body-lg);
line-height: var(--text-body-lg--line-height);
color: var(--color-text-secondary);
@@ -41,13 +41,17 @@
* with box-decoration-break: clone, so the sweep follows the text across line
* wraps (an absolutely-positioned box renders one broken block when the
* phrase wraps — which it does at every width below ~1200px). */
+/* Highlighter wash — aviation yellow doing the job yellow actually does on
+ * paper. Safe anywhere on a light surface because it is a FILL sitting under
+ * --color-text-primary ink, never a text colour itself. Used by the hero and
+ * by the four library pages (/langgraph, /chat, /ag-ui, /render). */
.marker-highlight {
color: var(--color-text-primary);
font-weight: 600;
background: linear-gradient(
100deg,
- rgba(0, 64, 144, 0.14),
- rgba(0, 64, 144, 0.08) 85%
+ rgba(255, 175, 0, 0.55),
+ rgba(255, 175, 0, 0.32) 85%
);
border-radius: 4px;
padding: 1px 5px 3px;
@@ -55,6 +59,16 @@
-webkit-box-decoration-break: clone;
box-decoration-break: clone;
}
+/* Inside the signal block the wash would be yellow on yellow. The emphasis
+ * becomes a solid ink underline instead. */
+[data-ui="section"][data-surface="signal"] .marker-highlight {
+ background: none;
+ font-weight: 700;
+ border-radius: 0;
+ box-shadow: inset 0 -3px 0 var(--color-ink);
+ padding: 1px 2px 3px;
+ margin: -1px -2px -3px;
+}
.hero-cta-row {
display: flex;
@@ -98,7 +112,7 @@
}
.hero-trust {
margin: 12px 0 0;
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 13px;
color: var(--color-text-muted);
}
@@ -126,17 +140,16 @@
margin-bottom: 16px;
}
.feature-block-heading {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: var(--text-h2);
line-height: var(--text-h2--line-height);
- font-weight: 700;
color: var(--color-text-primary);
margin: 0;
margin-bottom: 20px;
letter-spacing: -0.015em;
}
.feature-block-body {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: var(--text-body-lg);
line-height: var(--text-body-lg--line-height);
color: var(--color-text-secondary);
@@ -144,7 +157,7 @@
margin-bottom: 24px;
}
.feature-block-cta {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 15px;
font-weight: 600;
color: var(--color-accent);
@@ -179,7 +192,7 @@
padding: 9px 0;
}
.feature-block-row-claim {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 15px;
line-height: 1.45;
color: var(--color-text-primary);
@@ -231,10 +244,9 @@
margin-bottom: 16px;
}
.wp-heading {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: var(--text-h2);
line-height: var(--text-h2--line-height);
- font-weight: 700;
color: var(--color-text-primary);
margin: 0;
margin-bottom: 20px;
@@ -254,7 +266,7 @@
padding: 9px 0;
}
.wp-row-claim {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 15px;
line-height: 1.45;
color: var(--color-text-primary);
@@ -280,7 +292,7 @@
margin-top: 4px;
font-size: 13px;
color: var(--color-text-muted);
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
}
.wp-already a {
color: var(--color-accent);
@@ -316,16 +328,14 @@
margin-bottom: 14px;
}
.wp-cover-title {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: 28px;
line-height: 1.15;
- font-weight: 700;
color: var(--color-text-primary);
margin-bottom: 12px;
- font-style: italic;
}
.wp-cover-desc {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 14px;
line-height: 1.5;
color: var(--color-text-secondary);
@@ -368,17 +378,16 @@
background: var(--color-border);
}
.pilot-heading {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: var(--text-h2);
line-height: var(--text-h2--line-height);
- font-weight: 700;
color: var(--color-text-primary);
margin: 0;
margin-bottom: 20px;
letter-spacing: -0.015em;
}
.pilot-subhead {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: var(--text-body-lg);
line-height: var(--text-body-lg--line-height);
color: var(--color-text-secondary);
@@ -404,7 +413,7 @@
padding: 9px 0;
}
.pilot-row-claim {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 15px;
line-height: 1.45;
color: var(--color-text-primary);
@@ -432,21 +441,20 @@
border-bottom: none;
}
.pilot-step-num {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: 44px;
- font-weight: 700;
line-height: 0.9;
letter-spacing: -0.03em;
color: var(--color-border);
}
.pilot-step-title {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 15.5px;
font-weight: 600;
color: var(--color-text-primary);
}
.pilot-step-body {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 14px;
line-height: 1.5;
color: var(--color-text-secondary);
@@ -477,15 +485,14 @@
margin: 6px 0 0;
}
.teams-report-title {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: 26px;
- font-weight: 700;
line-height: 1.1;
margin: 8px 0 6px;
color: var(--color-text-primary);
}
.teams-report-desc {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 14px;
line-height: 1.5;
color: var(--color-text-secondary);
@@ -522,7 +529,7 @@
margin-bottom: 0;
}
.recent-articles-heading {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 14px;
font-style: italic;
font-weight: 400;
@@ -553,18 +560,16 @@
text-align: center;
}
.final-cta-heading {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: var(--text-h2);
line-height: var(--text-h2--line-height);
- font-weight: 700;
color: var(--color-text-primary);
margin: 0;
margin-bottom: 16px;
letter-spacing: -0.015em;
- font-style: italic;
}
.final-cta-subtext {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: var(--text-body-lg);
line-height: var(--text-body-lg--line-height);
color: var(--color-text-secondary);
@@ -579,7 +584,7 @@
margin-bottom: 16px;
}
.final-cta-caption {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: var(--text-caption);
color: var(--color-text-muted);
margin: 0;
@@ -593,16 +598,14 @@
position: absolute;
right: -30px;
bottom: -70px;
- font-family: var(--font-garamond);
- font-style: italic;
- font-weight: 800;
+ font-family: var(--font-display);
font-size: 210px;
line-height: 1;
letter-spacing: -0.04em;
background: linear-gradient(
160deg,
- rgba(100, 195, 253, 0.14),
- rgba(100, 195, 253, 0.03) 70%
+ rgba(255, 175, 0, 0.14),
+ rgba(255, 175, 0, 0.03) 70%
);
-webkit-background-clip: text;
background-clip: text;
@@ -639,7 +642,9 @@
flex-wrap: wrap;
}
.open-source-strip-line {
- font-family: var(--font-garamond);
+ /* Body-scale editorial copy, not display type: it carries an italic ,
+ and Archivo Black has no italic. --font-sans (Archivo) does. */
+ font-family: var(--font-sans);
font-size: 23px;
line-height: 1.35;
font-weight: 400;
@@ -657,7 +662,7 @@
flex: none;
}
.open-source-strip-licence {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 12px;
letter-spacing: 0.02em;
color: var(--color-text-muted);
@@ -680,10 +685,9 @@
margin-bottom: 16px;
}
.home-faq-heading {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: var(--text-h2);
line-height: var(--text-h2--line-height);
- font-weight: 700;
color: var(--color-text-primary);
margin: 0;
letter-spacing: -0.015em;
@@ -713,7 +717,7 @@
gap: 4px;
}
.backends-grid-name {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 14px;
font-weight: 600;
color: var(--color-text-primary);
@@ -767,7 +771,7 @@
gap: 5px;
}
.demo-modal__tab {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 12px;
font-weight: 600;
padding: 6px 12px;
@@ -807,12 +811,12 @@
background: var(--color-surface);
}
.demo-modal__hint {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 12px;
color: var(--color-text-muted);
}
.demo-modal__open-link {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 12px;
font-weight: 600;
color: var(--color-accent);
@@ -900,9 +904,8 @@
margin-bottom: 14px;
}
.render-show-heading {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: clamp(26px, 3.5vw, 42px);
- font-weight: 800;
line-height: 1.1;
color: var(--color-text-primary);
}
@@ -948,9 +951,8 @@
margin-bottom: 14px;
}
.lg-show-heading {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: clamp(26px, 3.5vw, 42px);
- font-weight: 800;
line-height: 1.1;
color: var(--color-text-primary);
}
@@ -968,7 +970,7 @@
}
.lg-show-card-head {
padding: 10px 20px;
- background: rgba(0, 64, 144, 0.04);
+ background: rgba(255, 175, 0, 0.08);
border-bottom: 1px solid var(--color-border);
}
.lg-show-card-title {
@@ -996,9 +998,8 @@
margin-bottom: 14px;
}
.chat-show-heading {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: clamp(26px, 3.5vw, 42px);
- font-weight: 800;
line-height: 1.1;
color: var(--color-text-primary);
}
@@ -1042,16 +1043,14 @@
position: absolute;
right: -40px;
top: -78px;
- font-family: var(--font-garamond);
- font-style: italic;
- font-weight: 800;
+ font-family: var(--font-display);
font-size: clamp(150px, 21vw, 300px);
line-height: 0.76;
letter-spacing: -0.04em;
background: linear-gradient(
160deg,
- rgba(100, 195, 253, 0.11),
- rgba(100, 195, 253, 0.02) 70%
+ rgba(255, 175, 0, 0.11),
+ rgba(255, 175, 0, 0.02) 70%
);
-webkit-background-clip: text;
background-clip: text;
@@ -1069,8 +1068,8 @@
height: 560px;
background: radial-gradient(
closest-side,
- rgba(100, 195, 253, 0.13),
- rgba(100, 195, 253, 0.04) 55%,
+ rgba(255, 175, 0, 0.13),
+ rgba(255, 175, 0, 0.04) 55%,
transparent 75%
);
pointer-events: none;
@@ -1148,9 +1147,8 @@
);
}
.proof-strip-value {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: 58px;
- font-weight: 700;
line-height: 0.9;
letter-spacing: -0.028em;
color: var(--color-text-primary);
@@ -1168,7 +1166,7 @@
display: block;
}
.proof-strip-caption {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 14px;
line-height: 1.5;
color: var(--color-text-secondary);
@@ -1212,7 +1210,7 @@
min-width: 0;
}
.reliability-receipt-claim {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 15px;
font-weight: 600;
line-height: 1.4;
@@ -1220,7 +1218,7 @@
margin: 0;
}
.reliability-receipt-detail {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 13px;
line-height: 1.5;
color: var(--color-text-secondary);
@@ -1268,7 +1266,7 @@
object-fit: contain;
}
.reliability-works-with-name {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 13px;
font-weight: 500;
color: var(--color-text-secondary);
@@ -1513,10 +1511,9 @@
color: #fff;
}
.stage-claim {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: 30px;
line-height: 1.08;
- font-weight: 500;
color: var(--color-text-primary);
margin: 0;
}
@@ -1685,7 +1682,7 @@
gap: 12px;
}
.stack-diagram-body {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: var(--text-body-lg);
line-height: var(--text-body-lg--line-height);
color: var(--color-text-secondary);
@@ -1713,7 +1710,7 @@
min-width: 1024px;
}
.arch-figure text {
- font-family: var(--font-inter);
+ font-family: var(--font-diagram);
font-size: 13.5px;
fill: var(--color-text-primary);
}
@@ -1819,7 +1816,7 @@
padding: 0;
display: grid;
gap: 6px;
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 13px;
color: var(--color-text-secondary);
}
@@ -1881,7 +1878,7 @@
}
.arch-stack-label {
text-align: left;
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 11px;
letter-spacing: 0.14em;
font-weight: 700;
@@ -1913,7 +1910,7 @@
height: 20px;
}
.arch-stack-title {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 15px;
font-weight: 600;
margin: 0;
@@ -1929,7 +1926,7 @@
margin: 8px 0 0;
padding: 0;
list-style: none;
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 13px;
color: var(--color-text-secondary);
display: grid;
@@ -1942,7 +1939,7 @@
margin: 10px 0 0;
padding: 0;
list-style: none;
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 13px;
font-weight: 500;
}
@@ -2002,14 +1999,14 @@
max-width: 420px;
}
.home-concept-title {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 1rem;
font-weight: 600;
color: var(--color-text-primary);
margin: 0;
}
.home-concept-sentence {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 0.9rem;
line-height: 1.55;
color: var(--color-text-secondary);
@@ -2139,7 +2136,7 @@
live control looking double-exposed. */
background: rgb(248 248 248);
color: #111;
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 15px;
font-weight: 600;
text-decoration: none;
@@ -2197,3 +2194,20 @@
font-weight: 600;
text-decoration: none;
}
+
+/* The frequency strip — the ATC player's bottom bar. Full-bleed scope navy
+ * closing the yellow block, set in mono because the instrument-panel cue is
+ * doing as much work here as the colour is. */
+.hero-strip {
+ margin: 32px calc(50% - 50vw) 0;
+ width: 100vw;
+ padding: 10px var(--spacing-container-x);
+ background: var(--color-scope);
+ color: #ffffff;
+ font-family: var(--font-mono);
+ font-size: 11px;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ text-align: center;
+}
diff --git a/apps/website/src/styles/marketing.css b/apps/website/src/styles/marketing.css
index 5ba0cfaa2..2d1c663d1 100644
--- a/apps/website/src/styles/marketing.css
+++ b/apps/website/src/styles/marketing.css
@@ -45,7 +45,7 @@
transform: translateY(-50%);
background: var(--color-accent);
color: #fff;
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 10px;
font-weight: 700;
letter-spacing: 0.08em;
@@ -70,7 +70,7 @@
color: var(--color-accent);
}
.pricing-plan-journey {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 12px;
font-weight: 700;
color: var(--color-text-muted);
@@ -78,7 +78,7 @@
}
.pricing-plan-name {
margin: 0 0 18px;
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: 34px;
line-height: 1;
color: var(--color-text-primary);
@@ -93,8 +93,7 @@
flex-wrap: wrap;
}
.pricing-plan-price-amount {
- font-family: var(--font-garamond);
- font-weight: 700;
+ font-family: var(--font-display);
font-size: 30px;
color: var(--color-text-primary);
line-height: 1;
@@ -116,7 +115,7 @@
margin: 6px 0 20px;
min-height: 88px;
color: var(--color-text-secondary);
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 13px;
line-height: 1.55;
}
@@ -133,7 +132,7 @@
display: flex;
align-items: flex-start;
gap: 9px;
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 12px;
line-height: 1.45;
color: var(--color-text-secondary);
@@ -173,7 +172,7 @@
.pricing-section-heading {
margin: 0 0 14px;
color: var(--color-text-primary);
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: clamp(30px, 4vw, 44px);
line-height: 1.08;
letter-spacing: -0.02em;
@@ -181,7 +180,7 @@
.pricing-section-body {
margin: 0;
color: var(--color-text-secondary);
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 16px;
line-height: 1.65;
}
@@ -206,7 +205,7 @@
justify-content: center;
gap: 8px;
color: var(--color-text-primary);
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
line-height: 1.4;
}
.pricing-boundary-block[data-accent] {
@@ -231,7 +230,7 @@
border-left: 3px solid var(--color-accent);
background: var(--color-surface);
color: var(--color-text-secondary);
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 14px;
line-height: 1.6;
}
@@ -247,7 +246,7 @@
border-collapse: separate;
border-spacing: 0;
color: var(--color-text-secondary);
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 12px;
line-height: 1.45;
}
@@ -340,14 +339,14 @@
margin-bottom: 4px;
}
.enterprise-band-heading {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: var(--text-h2);
line-height: var(--text-h2--line-height);
color: var(--color-text-primary);
margin: 0;
}
.enterprise-band-lede {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: var(--text-body-lg);
line-height: var(--text-body-lg--line-height);
color: var(--color-text-secondary);
@@ -362,10 +361,9 @@
margin-bottom: 16px;
}
.pricing-faq-heading {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: var(--text-h2);
line-height: var(--text-h2--line-height);
- font-weight: 700;
color: var(--color-text-primary);
margin: 0;
letter-spacing: -0.015em;
@@ -397,7 +395,7 @@
color: var(--color-text-muted);
}
.post-card-title {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: var(--text-h3);
line-height: var(--text-h3--line-height);
font-weight: var(--text-h3--font-weight);
@@ -439,15 +437,14 @@
margin-bottom: 32px;
}
.featured-post-card-title {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: var(--text-h2);
line-height: var(--text-h2--line-height);
- font-weight: 700;
letter-spacing: -0.02em;
margin: 0;
}
.featured-post-card-description {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: var(--text-body-lg);
line-height: var(--text-body-lg--line-height);
color: var(--color-text-secondary);
@@ -545,17 +542,16 @@
margin: 0 auto;
}
.sol-demo-heading {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: var(--text-h2);
line-height: var(--text-h2--line-height);
- font-weight: 700;
color: var(--color-text-primary);
margin: 0;
margin-bottom: 12px;
letter-spacing: -0.015em;
}
.sol-demo-caption {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: var(--text-body-lg);
line-height: var(--text-body-lg--line-height);
color: var(--color-text-secondary);
@@ -563,7 +559,7 @@
max-width: 62ch;
}
.sol-demo-note {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: var(--text-body);
color: var(--color-text-muted);
margin: 14px 0 0;
@@ -578,10 +574,9 @@
margin: 0 auto;
}
.sol-code-heading {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: var(--text-h2);
line-height: var(--text-h2--line-height);
- font-weight: 700;
color: var(--color-text-primary);
margin: 0;
margin-bottom: 12px;
diff --git a/apps/website/src/styles/pages.css b/apps/website/src/styles/pages.css
index 72d4f36af..6721a84bd 100644
--- a/apps/website/src/styles/pages.css
+++ b/apps/website/src/styles/pages.css
@@ -30,17 +30,16 @@
margin-bottom: 16px;
}
.docs-index-h1 {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: var(--text-h1);
line-height: var(--text-h1--line-height);
- font-weight: 700;
color: var(--color-text-primary);
margin: 0;
margin-bottom: 16px;
letter-spacing: -0.02em;
}
.docs-index-subtitle {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: var(--text-body-lg);
line-height: var(--text-body-lg--line-height);
color: var(--color-text-secondary);
@@ -133,7 +132,7 @@
flex: 0 0 auto;
}
.docs-index-card-title {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 18px;
line-height: 1.3;
font-weight: 600;
@@ -154,7 +153,7 @@
margin-top: 2px;
}
.docs-index-card-blurb {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: var(--text-body);
line-height: var(--text-body--line-height);
color: var(--color-text-secondary);
@@ -166,7 +165,7 @@
}
.docs-index-cta {
/* raw font stack, not the `.family` var — see the font-exception note atop ui.css */
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 14px;
font-weight: 600;
color: var(--color-accent);
@@ -196,7 +195,7 @@
white-space: nowrap;
}
.docs-index-helper {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 14px;
color: var(--color-text-secondary);
margin: 0;
@@ -219,7 +218,7 @@
margin: 0 auto;
}
.docs-index-search-heading {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 22px;
line-height: 1.3;
font-weight: 600;
@@ -228,7 +227,7 @@
margin-bottom: 12px;
}
.docs-index-search-button {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: var(--text-body);
line-height: var(--text-body--line-height);
color: var(--color-text-secondary);
@@ -289,17 +288,16 @@
margin-bottom: 16px;
}
.about-h1 {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: var(--text-h1);
line-height: var(--text-h1--line-height);
- font-weight: 700;
color: var(--color-text-primary);
margin: 0;
margin-bottom: 16px;
letter-spacing: -0.02em;
}
.about-body {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: var(--text-body-lg);
line-height: var(--text-body-lg--line-height);
color: var(--color-text-secondary);
@@ -311,7 +309,7 @@
margin-bottom: 0;
}
.about-h2 {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: var(--text-h2);
color: var(--color-text-primary);
margin: 0;
@@ -337,17 +335,16 @@
margin-bottom: 16px;
}
.sol-index-h1 {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: var(--text-h1);
line-height: var(--text-h1--line-height);
- font-weight: 700;
color: var(--color-text-primary);
margin: 0;
margin-bottom: 24px;
letter-spacing: -0.02em;
}
.sol-index-hero-body {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: var(--text-body-lg);
line-height: var(--text-body-lg--line-height);
color: var(--color-text-secondary);
@@ -371,10 +368,9 @@
margin-bottom: 12px;
}
.sol-index-h2 {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: var(--text-h2);
line-height: var(--text-h2--line-height);
- font-weight: 700;
color: var(--color-text-primary);
margin: 0;
letter-spacing: -0.015em;
@@ -395,7 +391,7 @@
flex-direction: column;
}
.sol-index-card-title {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 22px;
line-height: 1.25;
font-weight: 600;
@@ -405,7 +401,7 @@
white-space: pre-line;
}
.sol-index-card-subtitle {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: var(--text-body);
line-height: var(--text-body--line-height);
color: var(--color-text-secondary);
@@ -420,7 +416,7 @@
margin-bottom: 16px;
}
.sol-index-cta {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 14px;
font-weight: 600;
color: var(--color-accent);
@@ -445,10 +441,9 @@
background: var(--color-border);
}
.sol-page-h2 {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: var(--text-h2);
line-height: var(--text-h2--line-height);
- font-weight: 700;
color: var(--color-text-primary);
margin: 0;
letter-spacing: -0.015em;
@@ -457,7 +452,7 @@
margin-bottom: 16px;
}
.sol-page-section-intro {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: var(--text-body-lg);
line-height: var(--text-body-lg--line-height);
color: var(--color-text-secondary);
@@ -478,7 +473,7 @@
margin: 0 auto;
}
.sol-page-card-h3 {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 18px;
line-height: 1.3;
font-weight: 600;
@@ -487,7 +482,7 @@
margin-bottom: 10px;
}
.sol-page-card-body {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: var(--text-body);
line-height: var(--text-body--line-height);
color: var(--color-text-secondary);
@@ -503,14 +498,13 @@
margin-bottom: 12px;
}
.sol-page-arch-title {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: 22px;
- font-weight: 700;
color: var(--color-text-primary);
margin: 0;
}
.sol-page-role {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: var(--text-body);
line-height: var(--text-body--line-height);
color: var(--color-text-secondary);
@@ -520,7 +514,7 @@
margin-bottom: 16px;
}
.sol-page-arch-cta {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 14px;
font-weight: 600;
color: var(--color-accent);
@@ -529,9 +523,8 @@
text-decoration: none;
}
.sol-page-metric {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: 40px;
- font-weight: 700;
line-height: 0.9;
letter-spacing: -0.02em;
color: var(--color-text-primary);
@@ -547,10 +540,9 @@
margin-bottom: 16px;
}
.sol-page-h1 {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: var(--text-h1);
line-height: var(--text-h1--line-height);
- font-weight: 700;
color: var(--color-text-primary);
margin: 0;
margin-bottom: 24px;
@@ -558,7 +550,7 @@
white-space: pre-line;
}
.sol-page-hero-subtitle {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: var(--text-body-lg);
line-height: var(--text-body-lg--line-height);
color: var(--color-text-secondary);
@@ -582,17 +574,16 @@
margin-bottom: 16px;
}
.pilot-h1 {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: var(--text-h1);
line-height: var(--text-h1--line-height);
- font-weight: 700;
color: var(--color-text-primary);
margin: 0;
margin-bottom: 24px;
letter-spacing: -0.02em;
}
.pilot-hero-subtitle {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: var(--text-body-lg);
line-height: var(--text-body-lg--line-height);
color: var(--color-text-secondary);
@@ -691,10 +682,9 @@
background: var(--color-border);
}
.pilot-h2 {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: var(--text-h2);
line-height: var(--text-h2--line-height);
- font-weight: 700;
color: var(--color-text-primary);
margin: 0;
letter-spacing: -0.015em;
@@ -716,7 +706,7 @@
padding: 10px 0;
}
.pilot-outcome-claim {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 15px;
line-height: 1.5;
color: var(--color-text-primary);
@@ -741,7 +731,7 @@
text-align: center;
}
.pilot-contact-body {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: var(--text-body-lg);
line-height: var(--text-body-lg--line-height);
color: var(--color-text-secondary);
@@ -755,17 +745,16 @@
text-align: center;
}
.render-page-h1 {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: var(--text-h1);
line-height: var(--text-h1--line-height);
- font-weight: 700;
color: var(--color-text-primary);
margin: 0;
margin-bottom: 24px;
letter-spacing: -0.02em;
}
.render-page-hero-subtitle {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: var(--text-body-lg);
line-height: var(--text-body-lg--line-height);
color: var(--color-text-secondary);
@@ -792,17 +781,16 @@
text-align: center;
}
.chat-page-h1 {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: var(--text-h1);
line-height: var(--text-h1--line-height);
- font-weight: 700;
color: var(--color-text-primary);
margin: 0;
margin-bottom: 24px;
letter-spacing: -0.02em;
}
.chat-page-hero-subtitle {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: var(--text-body-lg);
line-height: var(--text-body-lg--line-height);
color: var(--color-text-secondary);
@@ -829,17 +817,16 @@
text-align: center;
}
.ag-ui-page-h1 {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: var(--text-h1);
line-height: var(--text-h1--line-height);
- font-weight: 700;
color: var(--color-text-primary);
margin: 0;
margin-bottom: 24px;
letter-spacing: -0.02em;
}
.ag-ui-page-hero-subtitle {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: var(--text-body-lg);
line-height: var(--text-body-lg--line-height);
color: var(--color-text-secondary);
@@ -869,7 +856,7 @@
margin-bottom: 24px;
}
.ag-ui-page-langgraph-note {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 13px;
color: var(--color-text-muted);
margin: 0 auto;
@@ -887,17 +874,16 @@
text-align: center;
}
.langgraph-page-h1 {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: var(--text-h1);
line-height: var(--text-h1--line-height);
- font-weight: 700;
color: var(--color-text-primary);
margin: 0;
margin-bottom: 24px;
letter-spacing: -0.02em;
}
.langgraph-page-hero-subtitle {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: var(--text-body-lg);
line-height: var(--text-body-lg--line-height);
color: var(--color-text-secondary);
@@ -943,16 +929,15 @@
margin-bottom: 16px;
}
.blog-index-h1 {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: var(--text-h1);
line-height: var(--text-h1--line-height);
- font-weight: 700;
letter-spacing: -0.02em;
color: var(--color-text-primary);
margin: 0 0 16px;
}
.blog-index-subtitle {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: var(--text-body-lg);
line-height: var(--text-body-lg--line-height);
color: var(--color-text-secondary);
@@ -967,7 +952,7 @@
border-radius: 12px;
}
.blog-index-empty-text {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: var(--text-body-lg);
line-height: var(--text-body-lg--line-height);
color: var(--color-text-secondary);
@@ -1011,16 +996,15 @@
margin-bottom: 24px;
}
.blog-post-h1 {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: var(--text-h1);
line-height: var(--text-h1--line-height);
- font-weight: 700;
letter-spacing: -0.02em;
color: var(--color-text-primary);
margin: 0 0 24px;
}
.blog-post-subtitle {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: var(--text-body-lg);
line-height: var(--text-body-lg--line-height);
color: var(--color-text-secondary);
@@ -1045,17 +1029,16 @@
margin-bottom: 16px;
}
.error-page-h1 {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: var(--text-h1);
line-height: var(--text-h1--line-height);
- font-weight: 700;
color: var(--color-text-primary);
margin: 0;
margin-bottom: 16px;
letter-spacing: -0.02em;
}
.error-page-body {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: var(--text-body-lg);
line-height: var(--text-body-lg--line-height);
color: var(--color-text-secondary);
@@ -1088,8 +1071,7 @@
margin-bottom: 16px;
}
.pricing-page-h1 {
- font-family: var(--font-garamond);
- font-weight: 700;
+ font-family: var(--font-display);
font-size: var(--text-h1);
line-height: var(--text-h1--line-height);
color: var(--color-text-primary);
@@ -1100,7 +1082,7 @@
max-width: 760px;
margin: 20px auto 0;
color: var(--color-text-secondary);
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: var(--text-body-lg);
line-height: 1.65;
}
@@ -1116,7 +1098,7 @@
border-radius: var(--radius-lg);
background: var(--color-surface-tinted);
color: var(--color-text-secondary);
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 13px;
line-height: 1.45;
}
@@ -1158,10 +1140,9 @@
/* Contact page — app/contact/page.tsx */
.contact-page-h1 {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: var(--text-h1);
line-height: var(--text-h1--line-height);
- font-weight: 700;
color: var(--color-text-primary);
margin: 0;
margin-bottom: 16px;
@@ -1177,7 +1158,7 @@
margin-bottom: 16px;
}
.contact-band-lede {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: var(--text-body-lg);
line-height: var(--text-body-lg--line-height);
color: var(--color-text-secondary);
@@ -1185,7 +1166,7 @@
max-width: 48ch;
}
.contact-band-note {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: var(--text-body);
line-height: var(--text-body--line-height);
color: var(--color-text-primary);
@@ -1210,7 +1191,7 @@
padding: 0 12px;
border: 1px solid var(--color-border);
border-radius: var(--radius-full);
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 13px;
color: var(--color-text-primary);
text-decoration: none;
@@ -1237,17 +1218,16 @@
margin-bottom: 16px;
}
.nf-h1 {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: var(--text-h1);
line-height: var(--text-h1--line-height);
- font-weight: 700;
color: var(--color-text-primary);
margin: 0;
margin-bottom: 16px;
letter-spacing: -0.02em;
}
.nf-body {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: var(--text-body-lg);
line-height: var(--text-body-lg--line-height);
color: var(--color-text-secondary);
@@ -1269,17 +1249,16 @@
margin-bottom: 16px;
}
.privacy-h1 {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: var(--text-h1);
line-height: var(--text-h1--line-height);
- font-weight: 700;
color: var(--color-text-primary);
margin: 0;
margin-bottom: 16px;
letter-spacing: -0.02em;
}
.privacy-h2 {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: var(--text-h2);
color: var(--color-text-primary);
margin: 0;
@@ -1287,7 +1266,7 @@
margin-bottom: 12px;
}
.privacy-body {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: var(--text-body-lg);
line-height: var(--text-body-lg--line-height);
color: var(--color-text-secondary);
diff --git a/apps/website/src/styles/style-contracts.spec.ts b/apps/website/src/styles/style-contracts.spec.ts
index 8fa06ec5f..0d5bfbb5b 100644
--- a/apps/website/src/styles/style-contracts.spec.ts
+++ b/apps/website/src/styles/style-contracts.spec.ts
@@ -236,7 +236,7 @@ const CONTRACTS: StyleContract[] = [
{
file: 'ui.css',
selector: '[data-ui="button"]:focus-visible',
- why: "Without this the UA default `outline: auto 1px rgb(0, 95, 204)` draws a blue hairline on the primary button's own #004090 fill — invisible. Closing the install dialog returns focus to that button, so the keyboard user is left with no idea where they are. The ring uses --color-accent, which the dark section scope re-points, so it survives on both surfaces.",
+ why: "Without this the UA default `outline: auto 1px rgb(0, 95, 204)` draws a blue hairline straight on the primary button's own fill — invisible. Closing the install dialog returns focus to that button, so the keyboard user is left with no idea where they are. The ring uses --color-accent, which every section scope re-points, so it survives on all three grounds: scope navy (#15253E) against the signal-yellow fill on white, aviation yellow inside the dark band, and ink inside the signal scope, where the primary button inverts to an ink fill.",
requires: {
outline: /outline:\s*2px\s+solid\s+var\(--color-accent\)/,
'outline-offset': /outline-offset:\s*2px/,
diff --git a/apps/website/src/styles/ui.css b/apps/website/src/styles/ui.css
index 80a71cac0..985ab14e6 100644
--- a/apps/website/src/styles/ui.css
+++ b/apps/website/src/styles/ui.css
@@ -96,8 +96,8 @@
}
/*
- * FONTS: every font-family in apps/website CSS is var(--font-inter),
- * var(--font-mono), or var(--font-garamond) — never a raw stack.
+ * FONTS: every font-family in apps/website CSS is var(--font-sans),
+ * var(--font-mono), or var(--font-display) — never a raw stack.
*
* layout.tsx loads the fonts via next/font, which defines those three
* vars on with self-hosted, size-adjusted stacks ("Inter",
@@ -109,6 +109,13 @@
* more mixed stacks. theme.css also defines the same var names from
* the design tokens; next/font's -level values win, which is now
* the intended resolution, and they guarantee the vars always resolve.
+ *
+ * --font-diagram is diagram-only and MUST keep being supplied by next/font in
+ * layout.tsx. theme.css emits it as a raw `Inter, ...` stack, which next/font's
+ * hashed family never matches — so if the loader is removed, diagrams silently
+ * fall back to system-ui, every glyph width shifts, and
+ * e2e/home-architecture.spec.ts (which measures text runs against their cards)
+ * is the only thing that will notice.
*/
/* UI primitive — Pill */
[data-ui="pill"] {
@@ -158,10 +165,12 @@
}
/* Dark section scope (homepage proof band + FinalCTA dark variant).
- * Values mirror libs/design-tokens/src/lib/dark.ts — the website hardwires
- * the light theme in TS, so dark sections re-scope the CSS variables instead.
- * If dark.ts changes, update here. Treatment B (spec): the canvas is a subtle
- * vertical gradient, and a 1px accent seam marks the light→dark boundary. */
+ * The ACCENTS mirror libs/design-tokens/src/lib/dark.ts. The SURFACES
+ * deliberately do not: this band is website-only and takes the ATC scope
+ * navy, while dark.ts stays neutral because @threadplane/chat and the cockpit
+ * consume it and a navy ground there would seam against embedded chat.
+ * Treatment B (spec): vertical gradient canvas, 1px accent seam at the
+ * light→dark boundary. */
[data-ui="section"][data-surface="dark"] {
--color-canvas: rgb(17, 17, 17);
--color-surface: rgb(28, 28, 28);
@@ -173,14 +182,14 @@
--color-text-secondary: rgb(200, 200, 200);
--color-text-muted: rgb(160, 160, 160);
--color-text-inverted: rgb(17, 17, 17);
- --color-accent: #64c3fd;
- --color-accent-hover: #8dd4ff;
- --color-accent-glow: rgba(100, 195, 253, 0.25);
- --color-accent-border: rgba(100, 195, 253, 0.2);
- --color-accent-border-hover: rgba(100, 195, 253, 0.35);
- --color-accent-surface: rgba(100, 195, 253, 0.08);
+ --color-accent: var(--color-signal);
+ --color-accent-hover: #FFC233;
+ --color-accent-glow: rgba(255, 175, 0, 0.25);
+ --color-accent-border: rgba(255, 175, 0, 0.22);
+ --color-accent-border-hover: rgba(255, 175, 0, 0.4);
+ --color-accent-surface: rgba(255, 175, 0, 0.1);
- background: linear-gradient(180deg, #161616 0%, #0e0e0e 100%);
+ background: linear-gradient(180deg, #1b2e4d 0%, #15253e 100%);
color: var(--color-text-primary);
position: relative;
}
@@ -195,20 +204,45 @@
background: linear-gradient(
90deg,
transparent,
- rgba(100, 195, 253, 0.55) 30%,
- rgba(100, 195, 253, 0.55) 70%,
+ rgba(255, 175, 0, 0.55) 30%,
+ rgba(255, 175, 0, 0.55) 70%,
transparent
);
pointer-events: none;
}
+/* Signal section scope (the ATC hero block).
+ * Aviation yellow is a FILL: this scope flips the ink to near-black rather
+ * than tinting the yellow, because #FFAF00 carries text at 1.84:1 on white
+ * and 10.73:1 under #0A0A0A. Same re-scoping mechanism as the dark surface
+ * above. */
+[data-ui="section"][data-surface="signal"] {
+ --color-canvas: var(--color-signal);
+ --color-surface: var(--color-signal);
+ --color-surface-tinted: var(--color-signal-strong);
+ --color-border: rgba(10, 10, 10, 0.18);
+ --color-border-strong: rgba(10, 10, 10, 0.32);
+ --color-text-primary: var(--color-ink);
+ --color-text-secondary: rgba(10, 10, 10, 0.78);
+ --color-text-muted: rgba(10, 10, 10, 0.6);
+ --color-text-inverted: var(--color-signal);
+ --color-accent: var(--color-ink);
+ --color-accent-hover: #000000;
+ --color-accent-surface: rgba(10, 10, 10, 0.08);
+ --color-accent-border: rgba(10, 10, 10, 0.25);
+ --shadow-focus: 0 0 0 3px rgba(10, 10, 10, 0.45);
+
+ background: var(--color-signal);
+ color: var(--color-text-primary);
+}
+
/* UI primitive — Button */
[data-ui="button"] {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-weight: 600;
line-height: 1;
border-radius: var(--radius-md);
@@ -228,11 +262,18 @@
font-size: 16px;
}
[data-ui="button"][data-variant="primary"] {
- background: var(--color-accent);
- color: var(--color-text-inverted);
- border: 1px solid var(--color-accent);
+ background: var(--color-signal);
+ color: var(--color-ink);
+ border: 1px solid var(--color-signal);
box-shadow: var(--shadow-sm);
}
+/* On the yellow hero block the fill inverts — a yellow button on a yellow
+ * ground has nothing to sit against. */
+[data-ui="section"][data-surface="signal"] [data-ui="button"][data-variant="primary"] {
+ background: var(--color-ink);
+ color: var(--color-signal);
+ border-color: var(--color-ink);
+}
[data-ui="button"][data-variant="secondary"] {
background: var(--color-surface);
color: var(--color-text-primary);
@@ -248,16 +289,21 @@
/* Keyboard focus ring.
*
* Without this the UA default applies — `outline: auto 1px rgb(0, 95, 204)` in
- * Chrome — which is a thin blue hairline drawn against the primary button's
- * own `--color-accent` (#004090) fill. Effectively invisible, and the place it
- * matters most is the one place focus lands programmatically: closing the
- * install dialog returns focus to the "Install Threadplane" trigger, and the
- * keyboard user could not see where they were.
+ * Chrome — a thin blue hairline drawn straight against the primary button's
+ * fill. Effectively invisible, and the place it matters most is the one place
+ * focus lands programmatically: closing the install dialog returns focus to
+ * the "Install Threadplane" trigger, and the keyboard user could not see where
+ * they were.
*
- * The offset ring is `--color-accent`, which the dark section scope above
- * re-points to #64c3fd, so it stays high-contrast on both surfaces. The inner
- * `--color-surface` halo separates ring from fill on `primary`, where fill and
- * ring are otherwise the same colour family. Held by a style contract. */
+ * The offset ring is `--color-accent`, which every section scope re-points, so
+ * it stays high-contrast on all three grounds: scope navy (#15253E) against
+ * the signal-yellow fill on white, aviation yellow inside the dark band, and
+ * ink inside the signal scope, where the primary button inverts to an ink
+ * fill. The inner `--color-surface` halo is what separates ring from fill in
+ * the two cases where both land in the same colour family — yellow ring on
+ * yellow fill in the dark band, ink ring on ink fill in the signal scope —
+ * because `--color-surface` re-points with the scope too (dark grey, then the
+ * signal yellow). Held by a style contract. */
[data-ui="button"]:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
@@ -287,7 +333,7 @@
gap: 16px;
padding: 20px 0;
cursor: pointer;
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 17px;
font-weight: 600;
color: var(--color-text-primary);
@@ -300,7 +346,7 @@
}
[data-ui="faq-item"] > div {
padding-bottom: 20px;
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 16px;
line-height: 1.6;
color: var(--color-text-secondary);
@@ -311,8 +357,7 @@
display: inline-flex;
align-items: center;
gap: 8px;
- font-family: var(--font-garamond);
- font-weight: 700;
+ font-family: var(--font-display);
color: var(--color-text-primary);
line-height: 1;
}
@@ -322,13 +367,38 @@
[data-ui="logo-mark"][data-size="md"] {
font-size: 16px;
}
-/* The glyph is an SVG filled with currentColor, so it inherits the wordmark's
- * color. Sized in px rather than em: the wordmark is Garamond, whose em box
- * would make the plane read smaller beside it than it measures. */
+/* The mark is the ATC app icon in miniature: a signal-yellow squircle with an
+ * ink glyph. A bare yellow glyph on the white nav would be 1.84:1 — yellow is
+ * a fill in this system, never an icon colour on a light ground. Inside the
+ * tile the same glyph is ink-on-yellow, 10.73:1.
+ *
+ * PlaneMark's path is fill="currentColor", so `color` paints the glyph and
+ * `background` the tile; the wordmark beside it keeps --color-text-primary and
+ * is sized in px rather than em so the tile does not track the wordmark's em
+ * box.
+ *
+ * `border-box`, not `content-box`, and the difference is load-bearing. The
+ * logo is what sets the nav's content-row height at the two smaller steps, so
+ * padding on a content-box icon grows the whole nav: measured 58/66/81 →
+ * 64/72/81, against a `--nav-h` of 58/66/81 that e2e/nav-height.spec.ts pins
+ * to the rendered nav within 1px. Border-box insets the glyph instead of
+ * inflating the box, so the tile stays exactly the 15/18px below and every
+ * offset built on --nav-h is untouched. Grow the tile only by raising those
+ * two sizes AND re-measuring --nav-h. */
[data-ui="logo-mark-icon"] {
display: block;
flex: none;
- color: inherit;
+ color: var(--color-ink);
+ background: var(--color-signal);
+ border-radius: 5px;
+ padding: 3px;
+ box-sizing: border-box;
+}
+/* On the yellow block the squircle inverts — a yellow tile on a yellow ground
+ * has nothing to sit against. */
+[data-ui="section"][data-surface="signal"] [data-ui="logo-mark-icon"] {
+ color: var(--color-signal);
+ background: var(--color-ink);
}
[data-ui="logo-mark"][data-size="sm"] > [data-ui="logo-mark-icon"] {
width: 15px;
@@ -449,7 +519,7 @@
margin-bottom: 12px;
}
[data-ui="tab-group-tab"] {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 13px;
font-weight: 600;
padding: 8px 14px;
@@ -469,16 +539,15 @@
margin-bottom: 16px;
}
[data-ui="section-header"] .section-header-heading {
- font-family: var(--font-garamond);
+ font-family: var(--font-display);
font-size: var(--text-h2);
line-height: var(--text-h2--line-height);
- font-weight: 700;
letter-spacing: -0.015em;
color: var(--color-text-primary);
margin: 0;
}
[data-ui="section-header"] .section-header-aside {
- font-family: var(--font-inter);
+ font-family: var(--font-sans);
font-size: 15px;
line-height: 1.6;
font-style: italic;
diff --git a/docs/superpowers/plans/2026-09-07-atc-theme-spike.md b/docs/superpowers/plans/2026-09-07-atc-theme-spike.md
new file mode 100644
index 000000000..fd5454d72
--- /dev/null
+++ b/docs/superpowers/plans/2026-09-07-atc-theme-spike.md
@@ -0,0 +1,1152 @@
+# ATC Theme Spike Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Reskin the Threadplane website to the ATC app's visual language — aviation yellow `#FFAF00`, scope navy `#15253E`, Archivo Black display type — by rethemeing `libs/design-tokens` and hand-tuning the nav, homepage hero and docs shell.
+
+**Architecture:** The retheme happens in the token library, which is the source of truth; `apps/website` inherits it. The accent role splits: `--color-accent` becomes scope navy (the interactive ink) and `--color-accent-light` becomes the yellow (fill only), because `#FFAF00` on white is 1.84:1 and cannot be a text colour. The yellow hero is a new `Section surface="signal"` variant that re-scopes `--color-*` for its block — the same mechanism the existing `data-surface="dark"` band already uses. Diagrams are deliberately carved out onto a new `--font-diagram` token that keeps Inter, because their geometry is pinned to Inter's metrics by an e2e.
+
+**Tech Stack:** Nx monorepo, TypeScript token sources compiled to CSS by a generator script, Next.js 15 App Router with `next/font/google`, Tailwind v4 `@theme`, Vitest for unit tests, Playwright for e2e.
+
+**Spec:** `docs/superpowers/specs/2026-09-07-atc-theme-spike-design.md`
+
+---
+
+## Background an engineer needs before starting
+
+**The token files are generated.** `libs/design-tokens/src/lib/tokens.css`, `tokens-dark.css` and `theme.css` all begin with "GENERATED FILE — DO NOT EDIT BY HAND". They are produced by `libs/design-tokens/scripts/generate-theme-css.ts` from the TypeScript sources (`base.ts`, `light.ts`, `dark.ts`, `typography.ts`, `shadows.ts`). Every colour change in this plan edits a `.ts` source and then runs the generator. If you edit a `.css` file directly, `ds-var-sources-agree.spec.ts` will fail and you will have wasted the time.
+
+**Two naming conventions, one source.** The generator emits each token twice: `--color-*` / `--font-*` (Tailwind v4 `@theme`, consumed by `apps/website`) and `--ds-*` (plain custom properties, consumed by the Angular cockpit and example apps). `token-css-parity.spec.ts` asserts the first set matches the TS sources; `ds-var-sources-agree.spec.ts` asserts the `--ds-*` set agrees between `cssVars()` and `tokens.css`; `ds-var-contract.spec.ts` asserts no `--ds-*` name silently disappears.
+
+**Three specs pin values this plan deliberately changes.** They are updated as part of the work, test-first: `css-vars.spec.ts`, `ds-var-contract.spec.ts`, `token-css-parity.spec.ts`. `tokens.spec.ts`, `theme.spec.ts`, `ds-var-sources-agree.spec.ts` and `generate-theme-css.spec.ts` were checked and hardcode none of the changed values — they should pass untouched, and if one fails you have broken something structural rather than merely changed a colour.
+
+**Two e2e specs are load-bearing and are the reason several steps exist:**
+- `apps/website/e2e/home-architecture.spec.ts` waits for `document.fonts.ready` then measures every text run in the homepage architecture diagram against its card. It is the gate proving the `--font-diagram` carve-out held. If it goes red, restore `--font-diagram` on the three declarations in Task 5 — **do not** adjust the diagram geometry.
+- `apps/website/e2e/home-hero.spec.ts` exercises the hero demo iframe, which only loads when `HeroDemo.tsx:69`'s `IntersectionObserver` at `threshold: 0.25` fires. Adding vertical padding to the hero pushes that stage down the page. If it goes red, scroll the stage into view in the test — do not shrink the hero copy.
+
+**`nx test website` does NOT type-check.** Vitest transpiles without checking
+types, and React forwards an unknown prop string to the DOM happily — so a
+test asserting a new union member passes BEFORE the union is extended. Any
+step in this plan that says "watch it fail" on a type-level change is wrong as
+written: get the real failure from `npx tsc --noEmit -p apps/website/tsconfig.json`
+(add `--ignoreDeprecations 6.0`; that config has pre-existing unrelated errors,
+so filter to the file you are working on), or from `npx nx build website`,
+which type-checks because `next.config` sets no `ignoreBuildErrors`. Treat the
+build as the real gate for anything type-level.
+
+**Commands** (run from the worktree root, never `cd` elsewhere):
+- Regenerate CSS: `npx nx run design-tokens:generate-theme-css`
+- Token tests: `npx nx test design-tokens`
+- Website unit tests: `npx nx test website`
+- Website e2e: `npx nx e2e website`
+
+---
+
+## File Structure
+
+**Token library — `libs/design-tokens/`**
+
+| File | Responsibility | Change |
+| --- | --- | --- |
+| `src/lib/base.ts` | Theme-invariant brand identity | Add 5 ATC brand tokens; repoint `accent` / `accentLight` |
+| `src/lib/light.ts` | Light-theme resolved roles | Accent family → navy ink + yellow tints; `textPrimary` → `#0A0A0A` |
+| `src/lib/dark.ts` | Dark-theme resolved roles | Lines 29–33 only: blue tints → yellow |
+| `src/lib/typography.ts` | Font families + type scale | `fontSerif`→`fontDisplay` (Archivo Black); `fontSans`→Archivo; add `fontDiagram` |
+| `src/lib/shadows.ts` | Elevation + focus ring | `focus` → scope-navy tint |
+| `src/lib/css-vars.ts` | `--ds-*` emitter (runtime fn) | Rename serif var; add the 5 brand vars + `--ds-font-diagram` |
+| `scripts/generate-theme-css.ts` | Emits all three CSS files | Rename font vars; emit the 5 brand vars + diagram font |
+| `src/lib/*.spec.ts` | The three value-pinning specs | Updated test-first |
+
+**Website — `apps/website/`**
+
+| File | Responsibility | Change |
+| --- | --- | --- |
+| `src/app/layout.tsx` | Loads fonts via `next/font` | Archivo Black + Archivo + Inter + JetBrains Mono; EB Garamond removed |
+| `src/components/ui/Section.tsx` | Section surface variants | Add `'signal'` to the `Surface` union |
+| `src/styles/ui.css` | UI primitives + surface scopes | Add `[data-surface="signal"]` scope; retint the dark scope |
+| `src/styles/landing.css` | Hero + landing + arch diagram | Hero on the signal block; `marker-highlight` navy→yellow; `--font-diagram` carve-out |
+| `src/styles/docs.css` | Docs shell + diagram kit | Active-item rule, callout rule, `--font-diagram` carve-out |
+| `src/styles/chrome.css` | Nav | Yellow logo mark, yellow CTA |
+| `src/components/landing/Hero.tsx` | Homepage hero | `surface="signal"` + trust strip |
+
+---
+
+## Task 1: Clear the stale build output
+
+**Files:**
+- Delete: `apps/website/.next/`
+
+This worktree carries a 347 MB `.next` directory from earlier dev use. Turbopack panics on a stale root when a production build runs after a worktree has been used for dev, and the failure message points at the wrong thing entirely.
+
+- [ ] **Step 1: Remove it**
+
+```bash
+rm -rf apps/website/.next
+```
+
+- [ ] **Step 2: Confirm it is gone**
+
+```bash
+test -d apps/website/.next && echo "STILL THERE" || echo "clean"
+```
+
+Expected: `clean`
+
+No commit — `.next` is gitignored.
+
+---
+
+## Task 2: Brand and light-theme colour values
+
+**Files:**
+- Modify: `libs/design-tokens/src/lib/base.ts`
+- Modify: `libs/design-tokens/src/lib/light.ts`
+- Modify: `libs/design-tokens/src/lib/dark.ts:27-33`
+- Modify: `libs/design-tokens/src/lib/shadows.ts`
+- Test: `libs/design-tokens/src/lib/css-vars.spec.ts:12-14,16-18`
+
+- [ ] **Step 1: Update the failing assertions in `css-vars.spec.ts`**
+
+Replace the two `it` blocks inside `describe('light')`:
+
+```typescript
+ it('uses scope-navy accent as the interactive ink', () => {
+ expect(vars['--ds-accent']).toBe('#15253E');
+ });
+
+ it('uses near-black text on light surfaces', () => {
+ expect(vars['--ds-text-primary']).toBe('#0A0A0A');
+ });
+```
+
+And add a new one directly after them, still inside `describe('light')`:
+
+```typescript
+ it('exposes aviation yellow as a fill-only signal, not as the ink', () => {
+ expect(vars['--ds-accent-light']).toBe('#FFAF00');
+ expect(vars['--ds-accent']).not.toBe(vars['--ds-accent-light']);
+ });
+```
+
+- [ ] **Step 2: Run the test and watch it fail**
+
+```bash
+npx nx test design-tokens
+```
+
+Expected: FAIL — `expected '#004090' to be '#15253E'`.
+
+- [ ] **Step 3: Add the ATC brand tokens to `base.ts`**
+
+Replace the `brand` block:
+
+```typescript
+ brand: Object.freeze({
+ /** Scope navy — the ATC radar panel. The light-theme interactive ink. */
+ accent: '#15253E',
+ /**
+ * Aviation yellow. Fill only: 1.84:1 on white, so it is never a text or
+ * icon colour on a light surface. Doubles as the dark-theme accent, where
+ * it reaches 8.33:1 — dark.ts derives its accent from this name.
+ */
+ accentLight: '#FFAF00',
+ /** Aviation yellow, by its intended name. Same value as accentLight. */
+ signal: '#FFAF00',
+ /** Frequency-strip amber. Full-bleed data strips only. */
+ signalStrong: '#FFB700',
+ /** Scope navy — dark grounds and data strips. */
+ scope: '#15253E',
+ /** LIVE red-orange. 3.68:1, so status fills and large bold text only. */
+ alert: '#FF3200',
+ /** Near-black. Text on yellow, and the light-theme text primary. */
+ ink: '#0A0A0A',
+ /** Angular brand red — trademark colour, not ours to retheme */
+ angularRed: '#DD0031',
+ /** Render library green */
+ renderGreen: '#1a7a40',
+ /** Chat library purple */
+ chatPurple: '#5a00c8',
+ }),
+```
+
+- [ ] **Step 4: Update the accent family and text primary in `light.ts`**
+
+Replace the `// Text` block's first line and the `// Semantic accent` block:
+
+```typescript
+ // Text
+ textPrimary: '#0A0A0A',
+```
+
+```typescript
+ // Semantic accent is the interactive INK (links, focus, borders) and stays
+ // dark: aviation yellow is 1.84:1 on white and cannot carry text. The tints
+ // below are backgrounds, so they carry the yellow instead — which is how
+ // [data-ui="pill"][data-variant="accent"] already composes: yellow surface,
+ // navy ink on top.
+ accent: baseTokens.brand.accent,
+ accentHover: '#0E1B2E',
+ accentGlow: 'rgba(255, 175, 0, 0.28)',
+ accentBorder: 'rgba(255, 175, 0, 0.35)',
+ accentBorderHover: 'rgba(255, 175, 0, 0.55)',
+ accentSurface: 'rgba(255, 175, 0, 0.10)',
+```
+
+- [ ] **Step 5: Repoint the focus ring in `shadows.ts`**
+
+```typescript
+ /**
+ * Keyboard focus ring. Deliberately scope navy and NOT the signal yellow:
+ * a yellow ring on a white page is 1.84:1 and effectively invisible.
+ */
+ focus: '0 0 0 3px rgba(21, 37, 62, 0.30)',
+```
+
+- [ ] **Step 6: Re-derive the dark theme's accent tints**
+
+This is the likeliest silent bug in the whole change. `dark.ts:28` derives
+`accent` as `baseTokens.brand.accentLight`, so it flips to yellow on its own
+from Step 3 — but lines 29–33 hardcode the old blue. Left alone, the dark
+accent turns yellow while its own hover and border tints stay blue, and
+nothing fails.
+
+In `libs/design-tokens/src/lib/dark.ts`, replace the accent block:
+
+```typescript
+ // Semantic accent maps to aviation yellow (8.33:1 on the dark ground).
+ // `accent` derives from the brand token; the tints below must be re-derived
+ // with it or they silently stay blue.
+ accent: baseTokens.brand.accentLight,
+ accentHover: '#ffc233',
+ accentGlow: 'rgba(255, 175, 0, 0.25)',
+ accentBorder: 'rgba(255, 175, 0, 0.22)',
+ accentBorderHover: 'rgba(255, 175, 0, 0.4)',
+ accentSurface: 'rgba(255, 175, 0, 0.1)',
+```
+
+Leave the surface and text values above it untouched: that file states they are
+deliberately aligned to `@threadplane/chat` so embedded chat has no colour
+seam, and the cockpit consumes them.
+
+- [ ] **Step 7: Confirm no blue survives in the token sources**
+
+```bash
+grep -rn "64C3FD\|64c3fd\|8dd4ff\|100, 195, 253\|004090" libs/design-tokens/src/lib/*.ts || echo "clean"
+```
+
+Expected: `clean`
+
+- [ ] **Step 8: Regenerate the CSS**
+
+```bash
+npx nx run design-tokens:generate-theme-css
+```
+
+- [ ] **Step 9: Run the tests**
+
+```bash
+npx nx test design-tokens
+```
+
+Expected: PASS. If `token-css-parity.spec.ts` fails complaining about `brand.signal` and friends having no CSS var, that is expected — Task 3 adds them. Note which names it lists and continue.
+
+- [ ] **Step 10: Commit**
+
+```bash
+git add libs/design-tokens/src/lib/base.ts libs/design-tokens/src/lib/light.ts libs/design-tokens/src/lib/dark.ts libs/design-tokens/src/lib/shadows.ts libs/design-tokens/src/lib/css-vars.spec.ts libs/design-tokens/src/lib/tokens.css libs/design-tokens/src/lib/tokens-dark.css libs/design-tokens/src/lib/theme.css
+git commit -m "feat(design-tokens): ATC palette — the accent role splits into navy ink and yellow signal"
+```
+
+---
+
+## Task 3: Emit the new brand tokens through the generator
+
+The five new brand entries exist in TypeScript but reach no stylesheet yet.
+
+**Files:**
+- Modify: `libs/design-tokens/scripts/generate-theme-css.ts` (the `/* Brand */` block near line 80, and the `--ds-*` brand lines near line 205)
+- Modify: `libs/design-tokens/src/lib/css-vars.ts:49-52`
+- Test: `libs/design-tokens/src/lib/token-css-parity.spec.ts:19-23`
+- Test: `libs/design-tokens/src/lib/ds-var-contract.spec.ts`
+
+- [ ] **Step 1: Add the parity-map entries**
+
+In `token-css-parity.spec.ts`, extend the Brand block:
+
+```typescript
+ // Brand (theme-invariant)
+ 'brand.accent': '--color-accent',
+ 'brand.accentLight': '--color-accent-light',
+ 'brand.signal': '--color-signal',
+ 'brand.signalStrong': '--color-signal-strong',
+ 'brand.scope': '--color-scope',
+ 'brand.alert': '--color-alert',
+ 'brand.ink': '--color-ink',
+ 'brand.angularRed': '--color-angular-red',
+```
+
+- [ ] **Step 2: Add the contract entries**
+
+In `ds-var-contract.spec.ts`, add to the name list, keeping it alphabetical alongside the existing `--ds-accent*` entries:
+
+```typescript
+ '--ds-alert',
+ '--ds-ink',
+ '--ds-scope',
+ '--ds-signal',
+ '--ds-signal-strong',
+```
+
+- [ ] **Step 3: Run and watch both fail**
+
+```bash
+npx nx test design-tokens
+```
+
+Expected: FAIL — parity reports the new `brand.*` leaves have no CSS var, and the contract reports the five `--ds-*` names are missing from `tokens.css`.
+
+- [ ] **Step 4: Emit the Tailwind names**
+
+In `generate-theme-css.ts`, extend the `/* Brand */` block:
+
+```typescript
+ lines.push(` --color-accent-light: ${brand.accentLight};`);
+ lines.push(` --color-signal: ${brand.signal};`);
+ lines.push(` --color-signal-strong: ${brand.signalStrong};`);
+ lines.push(` --color-scope: ${brand.scope};`);
+ lines.push(` --color-alert: ${brand.alert};`);
+ lines.push(` --color-ink: ${brand.ink};`);
+ lines.push(` --color-angular-red: ${brand.angularRed};`);
+```
+
+- [ ] **Step 5: Emit the `--ds-*` names**
+
+In the same file, extend the brand lines in the `--ds-*` section:
+
+```typescript
+ lines.push(` --ds-signal: ${brand.signal};`);
+ lines.push(` --ds-signal-strong: ${brand.signalStrong};`);
+ lines.push(` --ds-scope: ${brand.scope};`);
+ lines.push(` --ds-alert: ${brand.alert};`);
+ lines.push(` --ds-ink: ${brand.ink};`);
+ lines.push(` --ds-angular-red: ${brand.angularRed};`);
+```
+
+- [ ] **Step 6: Add them to the runtime emitter**
+
+In `css-vars.ts`, extend the "Raw brand colors (invariant)" block:
+
+```typescript
+ '--ds-accent-light': brand.accentLight,
+ '--ds-signal': brand.signal,
+ '--ds-signal-strong': brand.signalStrong,
+ '--ds-scope': brand.scope,
+ '--ds-alert': brand.alert,
+ '--ds-ink': brand.ink,
+ '--ds-angular-red': brand.angularRed,
+```
+
+- [ ] **Step 7: Regenerate and test**
+
+```bash
+npx nx run design-tokens:generate-theme-css && npx nx test design-tokens
+```
+
+Expected: PASS, all suites.
+
+- [ ] **Step 8: Commit**
+
+```bash
+git add libs/design-tokens/
+git commit -m "feat(design-tokens): emit signal, signal-strong, scope, alert and ink"
+```
+
+---
+
+## Task 4: Typography values
+
+**Files:**
+- Modify: `libs/design-tokens/src/lib/typography.ts`
+- Test: `libs/design-tokens/src/lib/css-vars.spec.ts` (the typography block at the end)
+
+- [ ] **Step 1: Update the typography assertion**
+
+Replace the final `it` block in `css-vars.spec.ts`:
+
+```typescript
+ it('typography tokens are identical across themes', () => {
+ expect(cssVars('light')['--ds-font-display']).toBe(cssVars('dark')['--ds-font-display']);
+ expect(cssVars('light')['--ds-font-sans']).toBe(cssVars('dark')['--ds-font-sans']);
+ expect(cssVars('light')['--ds-font-diagram']).toBe(cssVars('dark')['--ds-font-diagram']);
+ });
+
+ it('keeps diagrams off the brand face', () => {
+ // Diagram geometry is pinned to Inter's metrics by
+ // apps/website/e2e/home-architecture.spec.ts. Archivo would change every
+ // glyph width and silently overflow the cards.
+ expect(cssVars('light')['--ds-font-diagram']).toContain('Inter');
+ expect(cssVars('light')['--ds-font-sans']).not.toContain('Inter');
+ });
+```
+
+- [ ] **Step 2: Run and watch it fail**
+
+```bash
+npx nx test design-tokens
+```
+
+Expected: FAIL — `--ds-font-display` and `--ds-font-diagram` are `undefined`.
+
+- [ ] **Step 3: Update `typography.ts`**
+
+Replace the doc comment's first three bullets and the three family constants:
+
+```typescript
+/**
+ * - Display (Archivo Black): headlines and the wordmark. Single weight.
+ * - Sans (Archivo): body text, UI elements
+ * - Diagram (Inter): diagram text ONLY — see fontDiagram
+ * - Mono (JetBrains Mono): code, labels, metadata
+ */
+```
+
+```typescript
+ /** Display face for headlines. Archivo Black ships one weight (400). */
+ fontDisplay: '"Archivo Black", system-ui, sans-serif',
+ /** Text and UI face. */
+ fontSans: 'Archivo, system-ui, sans-serif',
+ /**
+ * Diagram text, deliberately NOT the brand face.
+ *
+ * Diagrams are information, not brand surface, and their geometry is tuned
+ * to Inter's metrics: EnterpriseArchitecture.tsx pins every rectangle to an
+ * 8px grid and home-architecture.spec.ts measures each rendered text run
+ * against its card. Retyping diagrams would force a geometry rework larger
+ * than the retheme itself.
+ */
+ fontDiagram: 'Inter, system-ui, sans-serif',
+```
+
+Then update `h1.family` and `h2.family` to `'var(--font-display)'`, and `h3` / `bodyLg` / `body` / `caption` `.family` to `'var(--font-sans)'`.
+
+- [ ] **Step 4: Update the emitters**
+
+In `css-vars.ts`, replace the Typography block:
+
+```typescript
+ // Typography (invariant)
+ '--ds-font-display': typography.fontDisplay,
+ '--ds-font-sans': typography.fontSans,
+ '--ds-font-diagram': typography.fontDiagram,
+ '--ds-font-mono': typography.fontMono,
+```
+
+In `generate-theme-css.ts`, replace the `/* Fonts */` block:
+
+```typescript
+ lines.push(` --font-display: ${typography.fontDisplay};`);
+ lines.push(` --font-sans: ${typography.fontSans};`);
+ lines.push(` --font-diagram: ${typography.fontDiagram};`);
+ lines.push(` --font-mono: ${typography.fontMono};`);
+```
+
+and the `--ds-*` typography block:
+
+```typescript
+ lines.push(` --ds-font-display: ${typography.fontDisplay};`);
+ lines.push(` --ds-font-sans: ${typography.fontSans};`);
+ lines.push(` --ds-font-diagram: ${typography.fontDiagram};`);
+ lines.push(` --ds-font-mono: ${typography.fontMono};`);
+```
+
+- [ ] **Step 5: Update the two remaining specs**
+
+In `token-css-parity.spec.ts`, replace the Font families block:
+
+```typescript
+ // Font families
+ 'typography.fontDisplay': '--font-display',
+ 'typography.fontSans': '--font-sans',
+ 'typography.fontDiagram': '--font-diagram',
+ 'typography.fontMono': '--font-mono',
+```
+
+In `ds-var-contract.spec.ts`, replace `'--ds-font-serif',` with:
+
+```typescript
+ '--ds-font-diagram',
+ '--ds-font-display',
+```
+
+- [ ] **Step 6: Regenerate and test**
+
+```bash
+npx nx run design-tokens:generate-theme-css && npx nx test design-tokens
+```
+
+Expected: PASS.
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add libs/design-tokens/
+git commit -m "feat(design-tokens): Archivo display and text faces, Inter retained for diagrams"
+```
+
+---
+
+## Task 5: Rename the font vars in consumers, with the diagram carve-out
+
+The generator now emits `--font-display` / `--font-sans` / `--font-diagram`, but `apps/website` and `workspace-react` still reference `--font-garamond` and `--font-inter`, which no longer exist. **The site is visually broken between Task 4 and this task** — that is expected; do not stop to investigate it.
+
+The carve-out and the rename land in **one commit**. A blind replacement sends the diagram rules to Archivo and breaks the overflow e2e, possibly only at 390px.
+
+**Files:**
+- Modify: `libs/workspace-react/src/styles/workspace.css:26`
+- Modify: `apps/website/src/styles/*.css` (55 `--font-garamond`, 128 `--font-inter`)
+- Modify (carve-out): `apps/website/src/styles/landing.css` `.arch-figure text`
+- Modify (carve-out): `apps/website/src/styles/docs.css` `.tp-diagram-node[data-title="sans"] .tp-diagram-title` and `.tp-diagram-meta`
+
+- [ ] **Step 1: Do the mechanical rename**
+
+```bash
+grep -rl -e '--font-garamond' -e '--font-inter' apps/website/src libs/workspace-react/src \
+ | xargs sed -i '' -e 's/--font-garamond/--font-display/g' -e 's/--font-inter/--font-sans/g'
+```
+
+- [ ] **Step 2: Confirm no references survive**
+
+```bash
+grep -rn -e '--font-garamond' -e '--font-inter' apps/website/src libs/workspace-react/src || echo "clean"
+```
+
+Expected: `clean`
+
+- [ ] **Step 3: Apply the diagram carve-out**
+
+In `apps/website/src/styles/landing.css`, find `.arch-figure text` (a single blanket rule covering every text node in the architecture figure) and set:
+
+```css
+.arch-figure text {
+ font-family: var(--font-diagram);
+}
+```
+
+In `apps/website/src/styles/docs.css`, set both diagram-kit sans rules:
+
+```css
+.tp-diagram-node[data-title="sans"] .tp-diagram-title {
+ font-family: var(--font-diagram);
+}
+```
+
+```css
+.tp-diagram-meta {
+ font-family: var(--font-diagram);
+}
+```
+
+Leave every diagram rule already on `var(--font-mono)` alone — mono was never Inter.
+
+- [ ] **Step 3b: Record the next/font constraint in `ui.css`**
+
+`ui.css`'s FONTS note explains that website font vars are supplied by
+`next/font` on ``, that those ``-level values WIN over `theme.css`,
+and that raw literals like `Inter, system-ui` never match because next/font
+registers its family under a hashed name.
+
+That makes `--font-diagram` fragile in a way nothing else records: the value
+`theme.css` emits for it IS a raw `Inter, system-ui, sans-serif` stack, so if
+`layout.tsx` ever stops defining `--font-diagram` via `next/font`, the
+diagrams silently fall back to `system-ui`, every glyph width changes, and the
+geometry this token exists to protect breaks. Between this task and Task 6
+that is exactly the state the site is in.
+
+Append to the FONTS comment block in `apps/website/src/styles/ui.css`:
+
+```
+ * --font-diagram is diagram-only and MUST keep being supplied by next/font in
+ * layout.tsx. theme.css emits it as a raw `Inter, ...` stack, which next/font's
+ * hashed family never matches — so if the loader is removed, diagrams silently
+ * fall back to system-ui, every glyph width shifts, and
+ * e2e/home-architecture.spec.ts (which measures text runs against their cards)
+ * is the only thing that will notice.
+```
+
+- [ ] **Step 4: Verify the carve-out is exactly three declarations**
+
+```bash
+grep -rn "var(--font-diagram)" apps/website/src/styles/
+```
+
+Expected: exactly 3 lines — one in `landing.css`, two in `docs.css`.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add apps/website/src/styles libs/workspace-react/src/styles
+git commit -m "refactor(website): --font-display/--font-sans, with diagrams carved out onto --font-diagram"
+```
+
+---
+
+## Task 6: Load the new fonts
+
+**Files:**
+- Modify: `apps/website/src/app/layout.tsx:2,23-37,69`
+
+- [ ] **Step 1: Replace the font imports and declarations**
+
+```typescript
+import { Archivo, Archivo_Black, Inter, JetBrains_Mono } from 'next/font/google';
+```
+
+```typescript
+const display = Archivo_Black({
+ subsets: ['latin'],
+ weight: '400',
+ variable: '--font-display',
+});
+
+const sans = Archivo({
+ subsets: ['latin'],
+ variable: '--font-sans',
+});
+
+/**
+ * Inter is retained for diagrams only. Their geometry is pinned to Inter's
+ * metrics by apps/website/e2e/home-architecture.spec.ts — see the token
+ * comment on typography.fontDiagram.
+ */
+const diagram = Inter({
+ subsets: ['latin'],
+ variable: '--font-diagram',
+});
+
+const mono = JetBrains_Mono({
+ subsets: ['latin'],
+ variable: '--font-mono',
+});
+```
+
+- [ ] **Step 2: Update the `` class list**
+
+```tsx
+
+```
+
+- [ ] **Step 3: Confirm EB Garamond is gone from the app**
+
+```bash
+grep -rn "Garamond" apps/website/src/app/layout.tsx || echo "clean"
+```
+
+Expected: `clean`. (`Garamond` still appears under `src/app/card/` and `scripts/generate-whitepaper.ts` — those are out of scope by design, see spec §7.)
+
+- [ ] **Step 4: Run the website unit tests**
+
+```bash
+npx nx test website
+```
+
+Expected: PASS.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add apps/website/src/app/layout.tsx
+git commit -m "feat(website): load Archivo Black and Archivo, drop EB Garamond"
+```
+
+---
+
+## Task 7: The signal surface variant
+
+**Files:**
+- Modify: `apps/website/src/components/ui/Section.tsx:4`
+- Modify: `apps/website/src/styles/ui.css` (after the `[data-surface="dark"]` scope)
+- Test: `apps/website/src/components/ui/Section.spec.tsx`
+
+- [ ] **Step 1: Write the failing test**
+
+Add to `Section.spec.tsx`:
+
+```tsx
+ it('renders data-surface="signal" when asked', () => {
+ const { container } = render();
+ const el = container.querySelector('[data-ui="section"]');
+ expect(el?.getAttribute('data-surface')).toBe('signal');
+ });
+```
+
+- [ ] **Step 2: Run it and watch it fail**
+
+```bash
+npx nx test website
+```
+
+Expected: FAIL — TypeScript rejects `"signal"` as not assignable to `Surface`.
+
+- [ ] **Step 3: Extend the union**
+
+In `Section.tsx`:
+
+```typescript
+type Surface = 'canvas' | 'tinted' | 'white' | 'dark' | 'signal';
+```
+
+- [ ] **Step 4: Add the CSS scope**
+
+In `ui.css`, directly after the `[data-surface="dark"]` rule and its `::before` seam:
+
+```css
+/* Signal section scope (the ATC hero block).
+ * Aviation yellow is a FILL: this scope flips the ink to near-black rather
+ * than tinting the yellow, because #FFAF00 carries text at 1.84:1 on white
+ * and 10.73:1 under #0A0A0A. Same re-scoping mechanism as the dark surface
+ * above. */
+[data-ui="section"][data-surface="signal"] {
+ --color-canvas: var(--color-signal);
+ --color-surface: var(--color-signal);
+ --color-surface-tinted: var(--color-signal-strong);
+ --color-border: rgba(10, 10, 10, 0.18);
+ --color-border-strong: rgba(10, 10, 10, 0.32);
+ --color-text-primary: var(--color-ink);
+ --color-text-secondary: rgba(10, 10, 10, 0.78);
+ --color-text-muted: rgba(10, 10, 10, 0.6);
+ --color-text-inverted: var(--color-signal);
+ --color-accent: var(--color-ink);
+ --color-accent-hover: #000000;
+ --color-accent-surface: rgba(10, 10, 10, 0.08);
+ --color-accent-border: rgba(10, 10, 10, 0.25);
+ --shadow-focus: 0 0 0 3px rgba(10, 10, 10, 0.45);
+
+ background: var(--color-signal);
+ color: var(--color-text-primary);
+}
+```
+
+- [ ] **Step 5: Run the test**
+
+```bash
+npx nx test website
+```
+
+Expected: PASS.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add apps/website/src/components/ui/Section.tsx apps/website/src/components/ui/Section.spec.tsx apps/website/src/styles/ui.css
+git commit -m "feat(website): signal surface scope for the ATC hero block"
+```
+
+---
+
+## Task 8: The yellow hero and its data strip
+
+**Files:**
+- Modify: `apps/website/src/components/landing/Hero.tsx:31,63`
+- Modify: `apps/website/src/styles/landing.css` (`.hero-heading`, `.marker-highlight`, `.hero-trust`)
+
+- [ ] **Step 1: Put the hero on the signal surface**
+
+In `Hero.tsx`, change the opening `Section`:
+
+```tsx
+
+```
+
+- [ ] **Step 2: Turn the trust line into the frequency strip**
+
+Replace the `` line:
+
+```tsx
+
{HERO_TRUST_LINE}
+```
+
+- [ ] **Step 3: Restyle the heading for the display face**
+
+In `landing.css`, replace the `.hero-heading` rule's family, weight and tracking:
+
+```css
+.hero-heading {
+ font-family: var(--font-display);
+ font-size: var(--text-h1);
+ line-height: var(--text-h1--line-height);
+ font-weight: 400; /* Archivo Black ships a single weight */
+ color: var(--color-text-primary);
+ margin: 0;
+ margin-bottom: 24px;
+ letter-spacing: -0.032em;
+}
+```
+
+- [ ] **Step 4: Move the marker highlight off hardcoded navy**
+
+`.marker-highlight` hardcodes `rgba(0, 64, 144, …)`, which no token change reaches. On the yellow block a yellow highlight is invisible, so it becomes a solid ink underline:
+
+```css
+.marker-highlight {
+ color: var(--color-text-primary);
+ font-weight: 700;
+ background: none;
+ box-shadow: inset 0 -3px 0 var(--color-accent);
+ border-radius: 0;
+ padding: 1px 2px 3px;
+ margin: -1px -2px -3px;
+ -webkit-box-decoration-break: clone;
+ box-decoration-break: clone;
+}
+```
+
+- [ ] **Step 5: Style the strip**
+
+Append to `landing.css`:
+
+```css
+/* The frequency strip — the ATC player's bottom bar. Full-bleed scope navy
+ * closing the yellow block, set in mono because the instrument-panel cue is
+ * doing as much work here as the colour is. */
+.hero-strip {
+ margin: 32px calc(50% - 50vw) 0;
+ width: 100vw;
+ padding: 10px var(--spacing-container-x);
+ background: var(--color-scope);
+ color: #ffffff;
+ font-family: var(--font-mono);
+ font-size: 11px;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ text-align: center;
+}
+```
+
+- [ ] **Step 6: Run the unit tests**
+
+```bash
+npx nx test website
+```
+
+Expected: PASS.
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add apps/website/src/components/landing/Hero.tsx apps/website/src/styles/landing.css
+git commit -m "feat(website): yellow hero block closed by the scope-navy frequency strip"
+```
+
+---
+
+## Task 9: Nav and the dark band
+
+**Files:**
+- Modify: `apps/website/src/styles/chrome.css` (logo mark, nav CTA)
+- Modify: `apps/website/src/styles/ui.css` (`[data-surface="dark"]` scope)
+
+- [ ] **Step 1: Retint the dark band onto scope navy**
+
+In `ui.css`, inside `[data-ui="section"][data-surface="dark"]`, replace the six accent lines and the `background`:
+
+```css
+ --color-accent: var(--color-signal);
+ --color-accent-hover: #ffc233;
+ --color-accent-glow: rgba(255, 175, 0, 0.25);
+ --color-accent-border: rgba(255, 175, 0, 0.22);
+ --color-accent-border-hover: rgba(255, 175, 0, 0.4);
+ --color-accent-surface: rgba(255, 175, 0, 0.1);
+
+ background: linear-gradient(180deg, #1b2e4d 0%, #15253e 100%);
+```
+
+Then change the `::before` seam gradient from `rgba(100, 195, 253, 0.55)` to `rgba(255, 175, 0, 0.55)` at both stops.
+
+- [ ] **Step 1b: Correct the comment above that rule**
+
+The rule's existing comment says its values mirror `dark.ts` and to update it
+when `dark.ts` changes. That is now only half true — the accents still mirror,
+but the surfaces deliberately do not. Leaving the comment would send the next
+person to make the library dark theme navy, which would put a colour seam in
+embedded chat. Replace it:
+
+```css
+/* Dark section scope (homepage proof band + FinalCTA dark variant).
+ * The ACCENTS mirror libs/design-tokens/src/lib/dark.ts. The SURFACES
+ * deliberately do not: this band is website-only and takes the ATC scope
+ * navy, while dark.ts stays neutral because @threadplane/chat and the cockpit
+ * consume it and a navy ground there would seam against embedded chat.
+ * Treatment B (spec): vertical gradient canvas, 1px accent seam at the
+ * light→dark boundary. */
+
+- [ ] **Step 2: Give the nav its yellow mark**
+
+The nav keeps a white ground — yellow reads as a band, not as chrome.
+
+`PlaneMark` is an inline SVG whose path is `fill="currentColor"`, and
+`ui.css:328` currently sets `[data-ui="logo-mark-icon"] { color: inherit; }`.
+So the mark takes the signal through `color`, not `background` or `fill`.
+Replace that rule in `apps/website/src/styles/ui.css`:
+
+```css
+[data-ui="logo-mark-icon"] {
+ display: block;
+ flex: none;
+ /* PlaneMark's path is fill="currentColor", so the signal arrives via color.
+ The wordmark beside it keeps --color-text-primary. */
+ color: var(--color-signal);
+}
+```
+
+On the yellow hero block that would be yellow-on-yellow, so pin it back inside
+the signal scope. Append to the `[data-ui="section"][data-surface="signal"]`
+rule added in Task 7:
+
+```css
+[data-ui="section"][data-surface="signal"] [data-ui="logo-mark-icon"] {
+ color: var(--color-ink);
+}
+```
+
+- [ ] **Step 3: Run the unit tests**
+
+```bash
+npx nx test website
+```
+
+Expected: PASS.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add apps/website/src/styles/chrome.css apps/website/src/styles/ui.css
+git commit -m "feat(website): yellow nav mark and a scope-navy dark band"
+```
+
+---
+
+## Task 10: Docs shell
+
+Yellow is reduced to rules. Docs stay a document.
+
+**Files:**
+- Modify: `apps/website/src/styles/docs.css` (the hardcoded `#004090` at ~line 1697, the active sidebar item, the callout rule)
+
+- [ ] **Step 1: Find the hardcoded navy**
+
+```bash
+grep -n "004090" apps/website/src/styles/docs.css
+```
+
+- [ ] **Step 2: Replace it with the token**
+
+Change `color: #004090;` to:
+
+```css
+ color: var(--color-accent);
+```
+
+- [x] **Step 3: WITHDRAWN — the tokens already did this, and a guard says so**
+
+This step originally replaced the active sidebar item's fill with a 3px
+`--color-signal` left rule. It is withdrawn; do nothing here.
+
+`DocsControlPlane.spec.tsx` has a test named "uses complete rounded sidebar
+states without a left marker" asserting `border-radius: 7px`, asserting the
+active state uses `background: var(--color-accent-surface)`, and explicitly
+asserting NO `border-left` on either the hover or the active state. That came
+from #963 (unify docs and runtime control plane) — a deliberate decision.
+
+More to the point, the step was unnecessary. `--color-accent-surface` is now
+`rgba(255, 175, 0, 0.10)` and `--color-accent` is scope navy, so the active
+item ALREADY renders as a soft yellow rounded pill with navy ink — yellow as a
+fill under dark ink, exactly the discipline spec §6 asks for. The retheme
+reached this surface through the tokens, which is the point of doing the work
+in the token library.
+
+Withdrawing rather than overriding: a plan written days earlier does not get to
+overrule a design decision the codebase deliberately guards, especially to
+achieve something already achieved.
+
+- [ ] **Step 4: Run the unit tests**
+
+```bash
+npx nx test website
+```
+
+Expected: PASS.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add apps/website/src/styles/docs.css
+git commit -m "feat(website): docs shell takes the signal as a rule, not a fill"
+```
+
+---
+
+## Task 11: Sweep the remaining hardcoded brand colours
+
+Added after Task 2's review found that the plan named only 3 of the 24
+hardcoded old-brand sites in `apps/website/src`. The rest would have survived
+the retheme as blue islands.
+
+**Files:**
+- Modify: `apps/website/src/styles/landing.css` (lines ~631, 945, 1027, 1046)
+- Modify: `apps/website/src/styles/docs.css` (lines ~334, 1623, 1696)
+- Modify: `apps/website/src/styles/ui.css` (comments at ~252, 258)
+- Modify: `apps/website/src/components/landing/chat-landing/ChatLandingCodeShowcase.tsx:25`
+- Modify: `apps/website/src/styles/style-contracts.spec.ts:239` (comment prose only)
+
+- [ ] **Step 1: List what is left**
+
+```bash
+grep -rn "004090\|0, 64, 144\|64c3fd\|64C3FD\|100, 195, 253" apps/website/src --include="*.css" --include="*.tsx" --include="*.ts"
+```
+
+Tasks 8, 9 and 10 should already have cleared `landing.css:49-50`,
+`ui.css:176-199` and `docs.css:1697`. Everything still listed is this task.
+
+- [ ] **Step 2: Replace the light-surface tints**
+
+These are all backgrounds, so they take the yellow. Replace the colour only,
+keeping each rule's existing alpha:
+
+- `docs.css` `--callout-tone-surface: rgba(0, 64, 144, 0.06)` → `rgba(255, 175, 0, 0.10)`
+- `docs.css` `background: rgba(0, 64, 144, 0.1)` → `rgba(255, 175, 0, 0.14)`
+- `docs.css` `background: rgba(0, 64, 144, 0.08)` → `rgba(255, 175, 0, 0.12)`
+- `landing.css` `background: rgba(0, 64, 144, 0.04)` → `rgba(255, 175, 0, 0.08)`
+
+The yellow alphas are raised slightly because `#FFAF00` is a much lighter
+colour than `#004090`; at the original alpha the tint is invisible on white.
+
+- [ ] **Step 3: Replace the dark-surface gradients**
+
+`landing.css` lines ~631, ~1027 and ~1046 are decorative gradients sitting on
+dark surfaces, currently `rgba(100, 195, 253, …)`. Swap the triple to
+`255, 175, 0`, keeping every alpha exactly as-is.
+
+- [ ] **Step 4: The one that must NOT become yellow**
+
+`ChatLandingCodeShowcase.tsx:25` is `--chat-user-bg: #004090`, and line 26 is
+`--chat-user-color: #ffffff`. White on aviation yellow is **1.84:1**. This one
+takes the scope navy so the pairing keeps working:
+
+```
+ --chat-user-bg: #15253E;
+```
+
+(It is a string inside a displayed code snippet, so it teaches the palette to
+readers rather than styling anything — which is exactly why it should not
+teach an inaccessible pairing.)
+
+- [ ] **Step 5: Correct the two comments that now describe the old palette**
+
+`ui.css` ~252 and ~258 explain the focus ring in terms of `#004090` and
+`#64c3fd`. Rewrite them for the current palette: the primary button's fill is
+now the signal yellow, the ring is scope navy on light, and the dark scope
+re-points the accent to yellow. `style-contracts.spec.ts:239`'s `why:` string
+makes the same stale claim — update the prose. Do not change the assertion.
+
+- [ ] **Step 6: Confirm the sweep is complete**
+
+```bash
+grep -rn "004090\|0, 64, 144\|64c3fd\|64C3FD\|100, 195, 253" apps/website/src --include="*.css" --include="*.tsx" --include="*.ts"
+```
+
+Expected: no results. `apps/website/src/app/icon.svg` still contains `#004090`
+and that is correct — the favicon is out of scope per spec §7.
+
+- [ ] **Step 7: Test and commit**
+
+```bash
+npx nx test website
+```
+
+Expected: PASS.
+
+```bash
+git add apps/website/src
+git commit -m "fix(website): sweep the last hardcoded navy out of the light and dark surfaces"
+```
+
+---
+
+## Task 12: Verification
+
+No code. This is where the two load-bearing e2e specs get their say.
+
+- [ ] **Step 1: Full unit suites**
+
+```bash
+npx nx test design-tokens && npx nx test website
+```
+
+Expected: PASS both.
+
+- [ ] **Step 2: The diagram carve-out gate**
+
+```bash
+npx nx e2e website -- --grep "architecture"
+```
+
+Expected: PASS **unchanged**. If it fails, the rename swept the diagram rules into Archivo — restore `var(--font-diagram)` on the three declarations from Task 5. Do **not** adjust the diagram geometry; that bakes Archivo's metrics into the data module and makes the problem permanent.
+
+- [ ] **Step 3: The hero demo gate**
+
+```bash
+npx nx e2e website -- --grep "hero"
+```
+
+Expected: PASS. If the demo iframe never loads, the yellow block's padding pushed the stage past `HeroDemo.tsx:69`'s `threshold: 0.25`. Fix by scrolling the stage into view in the test — not by shrinking the hero copy.
+
+- [ ] **Step 4: Full e2e**
+
+```bash
+npx nx e2e website
+```
+
+Expected: PASS.
+
+- [ ] **Step 5: Look at it**
+
+Start the dev server through the Browser pane preview tools (never `npm run dev` via Bash), then check `/` and a docs page at desktop width and at 390px. Confirm by reading the rendered page, not by assuming:
+- the hero yellow ends at the strip and does not bleed into the section below
+- the nav CTA is black-on-yellow, not white-on-yellow
+- docs body text is Archivo and diagram text is still Inter
+- the focus ring is visible when tabbing to the hero's primary button
+
+- [ ] **Step 6: Contrast check from the rendered page**
+
+In the Browser pane console, sample the computed values rather than trusting the plan's table:
+
+```javascript
+const el = document.querySelector('.hero-heading');
+getComputedStyle(el).color + ' on ' + getComputedStyle(el.closest('[data-surface]')).backgroundColor
+```
+
+Expected: `rgb(10, 10, 10)` on `rgb(255, 175, 0)`.
+
+- [ ] **Step 7: Commit any fixes**
+
+```bash
+git add -A
+git commit -m "fix(website): ATC theme verification pass"
+```
+
+---
+
+## Out of scope — do not fix these
+
+Per spec §7, these keep navy and EB Garamond and **will visibly diverge**. Leaving them is the plan working correctly:
+
+- `apps/website/src/app/card/` **fonts only** — the bundled `EBGaramond-Bold.ttf` that `card.spec.ts` asserts by filename. The card's *colours* are NOT out of scope: `card.spec.ts` compares `card/tokens.ts` against `theme.css` at test time, so they move with the retheme (handled in Task 2)
+- `apps/website/src/app/opengraph-image.tsx` and `blog/[slug]/opengraph-image.tsx`
+- `apps/website/scripts/generate-whitepaper.ts`
+- `apps/website/src/app/icon.svg` — the favicon stays navy
+- The cockpit and example apps — they inherit the new `--ds-*` values but are not visually reviewed here
+- Marketing pages other than home — they inherit tokens and fonts automatically; they get a look during Task 11 Step 5 but no hand-tuning
diff --git a/docs/superpowers/specs/2026-09-07-atc-theme-spike-design.md b/docs/superpowers/specs/2026-09-07-atc-theme-spike-design.md
new file mode 100644
index 000000000..2938e8ebb
--- /dev/null
+++ b/docs/superpowers/specs/2026-09-07-atc-theme-spike-design.md
@@ -0,0 +1,319 @@
+# ATC theme spike — aviation yellow on the Threadplane website
+
+**Date:** 2026-09-07
+**Status:** Approved, ready for planning
+**Branch:** `blove/threadplane-theme-update-55baa6`
+
+## 1. Goal
+
+Reskin the Threadplane website to the visual language of **ATC — Live Air
+Traffic Radio** (Enhanced Radar, App Store id6755132266): aviation yellow, a
+heavy grotesk display face, flat saturated blocks, and instrument-panel data
+strips.
+
+This is a spike in intent — it exists to be looked at and judged — but it is
+built the real way, in `libs/design-tokens`, not as a throwaway override. The
+branch is the isolation.
+
+## 2. The reference
+
+Values sampled from the App Store screenshots, not approximated:
+
+| Value | Role in the app |
+| --- | --- |
+| `#FFAF00` | App icon ground. The aviation yellow. |
+| `#FFB700` | The frequency strip at the bottom of the player. |
+| `#15253E` | The radar/scope panel navy. |
+| `#FF3200` | LIVE badges and marketing grounds. |
+| `#6155F5` | The followed-flight card. Not adopted — see §4.1. |
+| `#0A0A0A` | The wordmark black. |
+
+The app's design language is flat saturated colour blocks with no gradients,
+very heavy grotesk display type set tight, squircle and pill rounding, and
+solid full-bleed data strips carrying callsigns and frequencies.
+
+## 3. Direction
+
+Of three directions considered — yellow as a signal colour on the existing
+light page, a fully dark navy "scope" page, and a yellow-forward hero block —
+the chosen direction is **the yellow-forward hero block on an otherwise
+disciplined light page**.
+
+The hero is the app icon blown up: a full-bleed `#FFAF00` panel with black type
+and a black primary button, terminated by a `#15253E` data strip. Below the
+strip the page returns to white and behaves. Yellow does not reappear as a
+wash — only as the primary button, a 3px rule, or a link underline.
+
+The rejected alternatives, and why:
+
+- **Fully dark scope page.** Closest to the app's real product surface, but the
+ site is light-only today; docs, code blocks, diagrams and the embedded live
+ demo all assume a white ground. Largest blast radius for the least additional
+ signal.
+- **Yellow as accent only.** Safe and cheap, but reads as "a site that uses
+ yellow" rather than as the reference. It does not answer the question the
+ spike is asking.
+
+## 4. Token architecture
+
+### 4.1 The accent role splits in two
+
+Today `--color-accent` does two jobs: it fills the primary button *and* colours
+links, borders and focus rings. Navy `#004090` could do both. Yellow cannot —
+`#FFAF00` on white is **1.84:1**, which is not a text colour under any
+circumstances.
+
+So the retheme is a remap of token *roles*, not a substitution of values.
+Measured ratios:
+
+| Pair | Ratio | Verdict |
+| --- | --- | --- |
+| `#FFAF00` on `#FFFFFF` | 1.84:1 | Fails. Fill only, never text. |
+| `#0A0A0A` on `#FFAF00` | 10.73:1 | Passes AA. The yellow/black pairing. |
+| `#15253E` on `#FFFFFF` | 15.37:1 | Passes AA — higher than the `#004090` it replaces (9.83:1). |
+| `#FFAF00` on `#15253E` | 8.33:1 | Passes AA. Accent flips to yellow on dark. |
+| `#FF3200` on `#FFFFFF` | 3.68:1 | Large/bold text or non-text only. |
+
+**Brand layer — `libs/design-tokens/src/lib/base.ts`** (theme-invariant):
+
+| Token | Value | Job |
+| --- | --- | --- |
+| `signal` | `#FFAF00` | Hero block, primary button fill, marker highlight, logo mark. |
+| `signalStrong` | `#FFB700` | Full-bleed data strips only. |
+| `scope` | `#15253E` | Interactive ink on light; the ground of the website's dark band. |
+| `alert` | `#FF3200` | LIVE status fills. Never small text. |
+| `ink` | `#0A0A0A` | Text on yellow; the new `textPrimary`. |
+
+`angularRed` (`#DD0031`) is unchanged — it is Angular's trademark colour, not
+ours to retheme. `renderGreen` and `chatPurple` are unchanged; retinting them
+is unrelated churn. The app's indigo `#6155F5` is **not** adopted: it would
+collide with `chatPurple` for no gain.
+
+These are *additive* brand entries. Each needs a line in
+`generate-theme-css.ts` (emitting `--color-signal`, `--color-signal-strong`,
+`--color-scope`, `--color-alert`, `--color-ink` for Tailwind and the matching
+`--ds-*` names for plain consumers), an entry in the `token-css-parity.spec.ts`
+source→var map, and — because they become names consumers may reference — an
+entry in the `ds-var-contract.spec.ts` list.
+
+`signal` and `accentLight` resolve to the same value rather than one replacing
+the other, and that is deliberate: `dark.ts` already derives its accent as
+`baseTokens.brand.accentLight`, so repointing `accentLight` to `#FFAF00` makes
+the dark accent flip to yellow with no second edit. `--color-signal` is the
+name new work should use; `--color-accent-light` stays for its 4 existing call
+sites.
+
+**Light theme — `libs/design-tokens/src/lib/light.ts`:**
+
+| Role | Was | Becomes | Rationale |
+| --- | --- | --- | --- |
+| `accent` | `#004090` | `#15253E` | Interactive ink. From the ATC palette, and higher contrast than what it replaces. |
+| `accentLight` | `#64C3FD` | `#FFAF00` | The signal. Fill only. |
+| `accentHover` | `#003070` | `#0E1B2E` | Darker scope navy. |
+| `accentSurface` / `accentBorder` / `accentGlow` | navy tints | yellow tints | Tints are backgrounds, so they carry the yellow; the ink on top stays `accent`. This is already how `[data-ui="pill"][data-variant="accent"]` composes. |
+| `shadowFocus` | navy ring | `#15253E` ring | **Deliberate.** A yellow focus ring on white is 1.84:1 and effectively invisible. |
+| `textPrimary` | `rgb(28,28,28)` | `#0A0A0A` | Matches the app's harder near-black. |
+
+**Dark theme — `libs/design-tokens/src/lib/dark.ts`:** its `accent` needs *no*
+edit — line 28 already derives it as `baseTokens.brand.accentLight`, so it
+becomes `#FFAF00` (8.33:1) the moment `accentLight` is repointed. What does
+need editing is lines 29–33, which hardcode the blue: `accentHover`,
+`accentGlow`, `accentBorder`, `accentBorderHover` and `accentSurface` all carry
+literal `#8dd4ff` / `rgba(100, 195, 253, …)` values and must be re-derived from
+the yellow. Leaving them is the likeliest silent bug in this whole change: the
+accent would flip to yellow while its own hover and border tints stayed blue.
+
+Its *surfaces* stay neutral, because that file states they are deliberately
+aligned to `@threadplane/chat` so embedded chat has no colour seam, and the
+cockpit consumes them.
+
+The website's own `[data-ui="section"][data-surface="dark"]` scope
+(`apps/website/src/styles/ui.css`) is website-local, so that band moves off
+neutral `#111` onto scope navy `#15253E`, and its 1px boundary seam changes
+from blue to yellow.
+
+### 4.2 Why the library and not a website override
+
+An override layer in `apps/website` would have kept `libs/design-tokens`
+untouched and left every existing token spec green. It was rejected in favour
+of doing it properly: the library is the source of truth, and a spike that
+routes around it would not tell us what adopting the theme actually costs.
+
+The accepted consequence is that the cockpit and example apps inherit the new
+`--ds-*` values. They consume them with fallbacks and nothing imports
+`tokens.css` yet, so nothing breaks at runtime — but they are not visually
+reviewed as part of this work (§7).
+
+## 5. Typography
+
+| Var | Was | Becomes |
+| --- | --- | --- |
+| `--font-display` (was `--font-garamond`) | EB Garamond | **Archivo Black** |
+| `--font-sans` (was `--font-inter`) | Inter | **Archivo** 400–700 |
+| `--font-diagram` | *(new)* | **Inter** — retained for diagrams only |
+| `--font-mono` | JetBrains Mono | unchanged |
+
+EB Garamond is removed from `layout.tsx` entirely. A serif has no place in this
+system, and dropping it removes six loaded font files. Inter is **not**
+removed — it stays loaded to serve `--font-diagram` (§5.1).
+
+Archivo Black is display-only (a single weight) and is used for h1/h2 and the
+wordmark; Archivo carries body and UI. This pairing is what keeps the docs
+readable — Archivo is a text face, so a long page still reads, which Archivo
+Black alone would not.
+
+JetBrains Mono uppercase on the eyebrows and the data strips is doing more work
+than it looks: it is the instrument-panel cue. Without it the theme leans
+entirely on the yellow.
+
+### 5.1 Diagrams keep Inter
+
+Diagrams do not take the brand face. They stay on Inter via a new
+`--font-diagram` token, and they stay clean and minimal: neutral type, no
+Archivo Black, no display weights.
+
+Two reasons, and the second is the load-bearing one:
+
+1. **Diagrams are information, not brand surface.** A heavy grotesk in a
+ 16-node architecture drawing fights the drawing. The brand lives in the
+ hero; the diagram's job is to be read.
+2. **The geometry is tuned to Inter's metrics.** `EnterpriseArchitecture.tsx`
+ pins every rectangle to an 8px grid in a data module, and
+ `home-architecture.spec.ts` measures every rendered text run against its
+ card after `document.fonts.ready`. Swapping the diagram to Archivo would
+ change every glyph width and force a geometry rework that could easily
+ exceed the retheme itself. Keeping Inter makes that rework zero.
+
+The carve-out is three declarations, not a scatter:
+
+- `apps/website/src/styles/landing.css` — `.arch-figure text`, a single blanket
+ rule over every text node in the architecture figure
+- `apps/website/src/styles/docs.css` — `.tp-diagram-node[data-title="sans"]
+ .tp-diagram-title` and `.tp-diagram-meta`, the shared docs diagram kit
+
+Diagram rules already on `var(--font-mono)` (eyebrows, titles, `data-meta="mono"`)
+are unchanged — mono was never Inter and is not being retyped.
+
+**Trap:** the §5.2 rename is a mechanical `--font-inter` → `--font-sans`
+replacement, and these three declarations must become `--font-diagram`
+instead. A blind find-and-replace silently rethemes the diagrams to Archivo
+and breaks the overflow e2e — or worse, passes at desktop and overflows at
+390px. Do the carve-out in the same commit as the rename, not after it.
+
+### 5.2 The rename
+
+The names `--font-garamond` and `--font-inter` become lies after the swap, so
+they are renamed to `--font-display` and `--font-sans`. This is wider than a
+find-and-replace in `apps/website`, because the names are generated:
+
+- `libs/design-tokens/src/lib/typography.ts` — `fontSerif` → `fontDisplay`
+- `libs/design-tokens/src/lib/css-vars.ts` — `--ds-font-serif` → `--ds-font-display`
+- `libs/design-tokens/scripts/generate-theme-css.ts` — emits both `--font-garamond` and `--ds-font-serif`
+- `libs/design-tokens/src/lib/token-css-parity.spec.ts` — the source→var map
+- `libs/design-tokens/src/lib/ds-var-contract.spec.ts` — the published `--ds-*` name list
+- `libs/workspace-react/src/styles/workspace.css`
+- `apps/website` — 55 uses of `--font-garamond`, 128 of `--font-inter`
+
+Risk is low despite the breadth: `ds-var-contract.spec.ts` documents that
+consumers reference `--ds-*` names with fallbacks and that nothing imports
+`tokens.css` yet, so removing `--ds-font-serif` has no runtime effect.
+
+The rename lands as **its own commit**, before the design commit, so review of
+the design is not buried under ~190 mechanical replacements.
+
+## 6. Surfaces in scope
+
+Yellow appears in exactly three forms. Anywhere else it is reduced to a 3px
+rule or an underline colour.
+
+1. **Nav** — white ground, black text, yellow logo mark, yellow "Get started"
+ button with black text.
+2. **Homepage hero** — full-bleed `#FFAF00` block, Archivo Black headline in
+ `#0A0A0A`, black primary button, terminated by a `#15253E` strip carrying
+ the trust line in JetBrains Mono. Below the strip, white.
+3. **Docs shell** — a document first. Yellow arrives through the tokens rather
+ than through new rules: the active sidebar item is already
+ `background: var(--color-accent-surface)` with `color: var(--color-accent)`,
+ which the retheme turns into a soft yellow pill with navy ink. Links are
+ `#15253E`. **Correction to an earlier draft:** this section previously
+ called for a 3px left rule on the active item. That is withdrawn —
+ `DocsControlPlane.spec.tsx` deliberately guards "rounded sidebar states
+ without a left marker" (#963), and the token change already produces the
+ intended treatment without contradicting it.
+
+Every other marketing page (pricing, blog, about, solutions, pilot-to-prod)
+inherits the new tokens and fonts automatically — that is the point of the
+library approach — but is not hand-tuned. Those pages get a smoke pass only.
+
+## 7. Explicitly out of scope
+
+Named here so the divergence reads as a decision rather than a defect:
+
+- **The OG / social card pipeline's TYPE** (`apps/website/src/app/card/`,
+ `opengraph-image.tsx`, `blog/[slug]/opengraph-image.tsx`, `og-font.ts`). It
+ bundles its own `EBGaramond-Bold.ttf`, and `card.spec.ts` asserts the bundled
+ filenames, so retyping it means shipping an Archivo Black TTF. The card keeps
+ Garamond.
+
+ **Its COLOURS are in scope, and this is a correction to an earlier draft of
+ this spec.** `card/tokens.ts` is a hand-copied snapshot of the light tokens,
+ and `card.spec.ts` actively compares it against `theme.css` — its docstring
+ says a snapshot with nothing checking it "would keep rendering, in last
+ season's colours". Declaring the divergence intentional in a markdown file
+ does not satisfy a guard that compares values at test time; it just leaves
+ `nx test website` red for the rest of the arc, which trains everyone to
+ ignore it. The colours cost nothing to move (they are hex literals, not
+ font files), so `ink`, `accent`, `accentSurface` and `accentBorder` follow
+ the retheme and the guard keeps working.
+- **`apps/website/scripts/generate-whitepaper.ts`** — same reason.
+- **`apps/website/src/app/icon.svg`** — the favicon stays navy.
+- **Cockpit and example apps** — they inherit the new values but are not
+ visually reviewed here.
+
+## 8. Verification
+
+The e2e added by #1048 is the gate on the §5.1 carve-out.
+`apps/website/e2e/home-architecture.spec.ts` waits on `document.fonts.ready`
+and then runs an overflow report against every card in the homepage
+architecture diagram, with the comment "a fallback face lies about widths."
+
+Because diagrams keep Inter, this should pass **unchanged** — and that is
+exactly what makes it useful. A green run means the carve-out held; a red run
+means the rename swept the diagram rules into Archivo. If it goes red, the fix
+is to restore `--font-diagram` on the three declarations in §5.1, **not** to
+adjust the diagram geometry. Adjusting geometry would be treating the symptom
+and would bake Archivo's metrics into the data module.
+
+Run it at both viewports. The spec's second test drops to 390px, and a
+metrics regression can pass at desktop while overflowing on a phone.
+
+Specs that pin a value being deliberately changed, and must be updated as part
+of the work:
+
+- `libs/design-tokens/src/lib/css-vars.spec.ts` — `--ds-accent` pinned to
+ `#004090`, dark to `#64C3FD`, and the serif light/dark agreement
+- `libs/design-tokens/src/lib/ds-var-contract.spec.ts` — `--ds-font-serif`
+- `libs/design-tokens/src/lib/token-css-parity.spec.ts` — the font var map
+- `tokens.spec.ts`, `theme.spec.ts`, `ds-var-sources-agree.spec.ts`,
+ `generate-theme-css.spec.ts` — wherever they assert changed values
+- `apps/website/src/styles/style-contracts.spec.ts`
+
+Sequence:
+
+1. `rm -rf apps/website/.next` first. This worktree carries a 347 MB stale
+ `.next` from earlier dev use, which panics the production build.
+2. Edit `base.ts` / `light.ts` / `dark.ts` / `typography.ts`, then regenerate
+ with `npx nx run design-tokens:generate-theme-css`. Never hand-edit
+ `tokens.css`, `tokens-dark.css` or `theme.css` — they are generated.
+3. `npx nx test design-tokens` green.
+4. `npx nx test website` green.
+5. `npx nx e2e website` — in particular `home-architecture.spec.ts`.
+6. Browser pane on `/` and a docs page at desktop and at 390px, and a contrast
+ check taken from the rendered page rather than from the table in §4.1.
+
+## 9. Known debt if the theme is adopted
+
+- The social card, whitepaper and favicon pipelines need the new face bundled
+ and their own `#004090` replaced.
+- The non-hand-tuned marketing pages need a real pass.
+- The cockpit and example apps need a visual review against the new `--ds-*`.
diff --git a/libs/design-tokens/scripts/generate-theme-css.ts b/libs/design-tokens/scripts/generate-theme-css.ts
index f47aa73a6..4f7177c74 100644
--- a/libs/design-tokens/scripts/generate-theme-css.ts
+++ b/libs/design-tokens/scripts/generate-theme-css.ts
@@ -80,6 +80,12 @@ function buildThemeBlock(): string {
lines.push('');
lines.push(' /* Brand */');
lines.push(` --color-accent-light: ${brand.accentLight};`);
+ lines.push(' /* signal aliases accent-light, scope aliases accent — deliberate: dark.ts derives its accent from accentLight, so both names must exist. Prefer signal/scope in new work. */');
+ lines.push(` --color-signal: ${brand.signal};`);
+ lines.push(` --color-signal-strong: ${brand.signalStrong};`);
+ lines.push(` --color-scope: ${brand.scope};`);
+ lines.push(` --color-alert: ${brand.alert};`);
+ lines.push(` --color-ink: ${brand.ink};`);
lines.push(` --color-angular-red: ${brand.angularRed};`);
lines.push(` --color-render-green: ${brand.renderGreen};`);
lines.push(` --color-chat-purple: ${brand.chatPurple};`);
@@ -87,8 +93,9 @@ function buildThemeBlock(): string {
// Fonts
lines.push('');
lines.push(' /* Fonts */');
- lines.push(` --font-garamond: ${typography.fontSerif};`);
- lines.push(` --font-inter: ${typography.fontSans};`);
+ lines.push(` --font-display: ${typography.fontDisplay};`);
+ lines.push(` --font-sans: ${typography.fontSans};`);
+ lines.push(` --font-diagram: ${typography.fontDiagram};`);
lines.push(` --font-mono: ${typography.fontMono};`);
// Type scale — Tailwind v4 composite text tokens.
@@ -99,7 +106,7 @@ function buildThemeBlock(): string {
// composite objects in typography.ts.
//
// `family` is deliberately not emitted: those values are already
- // `var(--font-garamond)` and friends, and Tailwind's --text-* bundle has no
+ // `var(--font-display)` and friends, and Tailwind's --text-* bundle has no
// font-family sub-key. `eyebrow.transform` is likewise a plain
// `text-transform` keyword, not a token. Both are excluded in
// token-css-parity.spec.ts with that reasoning.
@@ -203,6 +210,13 @@ function buildTokensBlock(theme: ThemeOverrides): string {
lines.push(` --ds-text-muted: ${theme.textMuted};`);
lines.push(` --ds-text-inverted: ${theme.textInverted};`);
lines.push(` --ds-sidebar-bg: ${theme.sidebarBg};`);
+
+ lines.push(' /* Brand (invariant — identical in tokens.css and tokens-dark.css) */');
+ lines.push(` --ds-signal: ${brand.signal};`);
+ lines.push(` --ds-signal-strong: ${brand.signalStrong};`);
+ lines.push(` --ds-scope: ${brand.scope};`);
+ lines.push(` --ds-alert: ${brand.alert};`);
+ lines.push(` --ds-ink: ${brand.ink};`);
lines.push(` --ds-angular-red: ${brand.angularRed};`);
lines.push(` --ds-render-green: ${brand.renderGreen};`);
lines.push(` --ds-chat-purple: ${brand.chatPurple};`);
@@ -218,8 +232,9 @@ function buildTokensBlock(theme: ThemeOverrides): string {
lines.push('');
lines.push(' /* Typography */');
- lines.push(` --ds-font-serif: ${typography.fontSerif};`);
+ lines.push(` --ds-font-display: ${typography.fontDisplay};`);
lines.push(` --ds-font-sans: ${typography.fontSans};`);
+ lines.push(` --ds-font-diagram: ${typography.fontDiagram};`);
lines.push(` --ds-font-mono: ${typography.fontMono};`);
lines.push('');
@@ -287,8 +302,11 @@ export function generateTokensCss(): string {
/**
* Dark twin of tokens.css, from darkOverrides. Exists because the cockpit
* example apps were designed dark: their var(--ds-*, fallback) fallbacks are
- * hand-copies of darkOverrides (verified value-by-value, 2026-08-30, with
- * minor drift - e.g. accentBorder 0.25 vs the token's 0.2). Wiring THIS file
+ * hand-copies of darkOverrides (verified value-by-value, 2026-08-30). As of
+ * 2026-09-07 those fallbacks are still the pre-ATC blue - e.g.
+ * var(--ds-accent, #64c3fd) against a darkOverrides.accent of #FFAF00 - so
+ * they have diverged from the tokens by HUE, not by a rounding nit, and are
+ * effectively decorative until the cockpit is reviewed. Wiring THIS file
* in makes those fallbacks dead text; wiring the light tokens.css would flip
* deliberately-dark apps to light.
*/
diff --git a/libs/design-tokens/src/lib/base.ts b/libs/design-tokens/src/lib/base.ts
index c06c71af8..f8eeaa42a 100644
--- a/libs/design-tokens/src/lib/base.ts
+++ b/libs/design-tokens/src/lib/base.ts
@@ -3,6 +3,14 @@ import { space } from './space.ts';
import { radius } from './radius.ts';
import { shadows } from './shadows.ts';
+/**
+ * The two ATC hues that each carry two token names. Hoisted so a repoint moves
+ * both names at once — `signal` cannot drift from `accentLight`, nor `scope`
+ * from `accent`.
+ */
+const AVIATION_YELLOW = '#FFAF00';
+const SCOPE_NAVY = '#15253E';
+
/**
* Theme-invariant tokens. Same values in light and dark.
* Includes typography, spacing, radii, shadows, and brand colors that
@@ -14,11 +22,25 @@ export const baseTokens = Object.freeze({
radius,
shadows,
brand: Object.freeze({
- /** LangGraph navy — used as the light-theme semantic accent */
- accent: '#004090',
- /** Bright sky blue — used as the dark-theme semantic accent */
- accentLight: '#64C3FD',
- /** Angular brand red */
+ /** Scope navy — the ATC radar panel. The light-theme interactive ink. */
+ accent: SCOPE_NAVY,
+ /**
+ * Aviation yellow. Fill only: 1.84:1 on white, so it is never a text or
+ * icon colour on a light surface. Doubles as the dark-theme accent, where
+ * it reaches 8.33:1 — dark.ts derives its accent from this name.
+ */
+ accentLight: AVIATION_YELLOW,
+ /** Aviation yellow, by its intended name. Same value as accentLight. */
+ signal: AVIATION_YELLOW,
+ /** Frequency-strip amber. Full-bleed data strips only. */
+ signalStrong: '#FFB700',
+ /** Scope navy — dark grounds and data strips. */
+ scope: SCOPE_NAVY,
+ /** LIVE red-orange. 3.68:1, so status fills and large bold text only. */
+ alert: '#FF3200',
+ /** Near-black. Text on yellow, and the light-theme text primary. */
+ ink: '#0A0A0A',
+ /** Angular brand red — trademark colour, not ours to retheme */
angularRed: '#DD0031',
/** Render library green */
renderGreen: '#1a7a40',
diff --git a/libs/design-tokens/src/lib/css-vars.spec.ts b/libs/design-tokens/src/lib/css-vars.spec.ts
index 7725c21b6..a7a11642e 100644
--- a/libs/design-tokens/src/lib/css-vars.spec.ts
+++ b/libs/design-tokens/src/lib/css-vars.spec.ts
@@ -1,5 +1,6 @@
import { describe, it, expect } from 'vitest';
import { cssVars } from './css-vars';
+import { baseTokens } from './base';
describe('cssVars(theme)', () => {
describe('light', () => {
@@ -9,12 +10,17 @@ describe('cssVars(theme)', () => {
expect(vars['--ds-canvas']).toBe('rgb(255, 255, 255)');
});
- it('uses navy accent', () => {
- expect(vars['--ds-accent']).toBe('#004090');
+ it('uses scope-navy accent as the interactive ink', () => {
+ expect(vars['--ds-accent']).toBe('#15253E');
});
- it('uses dark text on light surfaces', () => {
- expect(vars['--ds-text-primary']).toBe('rgb(28, 28, 28)');
+ it('uses near-black text on light surfaces', () => {
+ expect(vars['--ds-text-primary']).toBe('#0A0A0A');
+ });
+
+ it('exposes aviation yellow as a fill-only signal, not as the ink', () => {
+ expect(vars['--ds-accent-light']).toBe('#FFAF00');
+ expect(vars['--ds-accent']).not.toBe(baseTokens.brand.accentLight);
});
});
@@ -25,13 +31,26 @@ describe('cssVars(theme)', () => {
expect(vars['--ds-canvas']).toBe('rgb(17, 17, 17)');
});
- it('uses bright-blue accent', () => {
- expect(vars['--ds-accent']).toBe('#64C3FD');
+ it('uses aviation yellow as the dark-theme accent', () => {
+ expect(vars['--ds-accent']).toBe('#FFAF00');
});
it('uses near-white text on dark surfaces', () => {
expect(vars['--ds-text-primary']).toBe('rgb(245, 245, 245)');
});
+
+ it('derives every accent tint from the accent hue', () => {
+ const hex = baseTokens.brand.accentLight.replace('#', '');
+ const triple = [0, 2, 4].map((i) => parseInt(hex.slice(i, i + 2), 16)).join(', ');
+ for (const key of [
+ '--ds-accent-glow',
+ '--ds-accent-border',
+ '--ds-accent-border-hover',
+ '--ds-accent-surface',
+ ] as const) {
+ expect(vars[key]).toContain(triple);
+ }
+ });
});
it('both themes expose the same custom-property keys', () => {
@@ -47,7 +66,16 @@ describe('cssVars(theme)', () => {
});
it('typography tokens are identical across themes', () => {
- expect(cssVars('light')['--ds-font-serif']).toBe(cssVars('dark')['--ds-font-serif']);
+ expect(cssVars('light')['--ds-font-display']).toBe(cssVars('dark')['--ds-font-display']);
expect(cssVars('light')['--ds-font-sans']).toBe(cssVars('dark')['--ds-font-sans']);
+ expect(cssVars('light')['--ds-font-diagram']).toBe(cssVars('dark')['--ds-font-diagram']);
+ });
+
+ it('keeps diagrams off the brand face', () => {
+ // Diagram geometry is pinned to Inter's metrics by
+ // apps/website/e2e/home-architecture.spec.ts. Archivo would change every
+ // glyph width and silently overflow the cards.
+ expect(cssVars('light')['--ds-font-diagram']).toContain('Inter');
+ expect(cssVars('light')['--ds-font-sans']).not.toContain('Inter');
});
});
diff --git a/libs/design-tokens/src/lib/css-vars.ts b/libs/design-tokens/src/lib/css-vars.ts
index 5391b7269..52736e44e 100644
--- a/libs/design-tokens/src/lib/css-vars.ts
+++ b/libs/design-tokens/src/lib/css-vars.ts
@@ -37,7 +37,7 @@ export function cssVars(theme: Theme) {
'--ds-bg': t.bg,
'--ds-sidebar-bg': t.sidebarBg,
- // Accent family (theme-variant — semantic accent points to navy in light, bright-blue in dark)
+ // Accent family (theme-variant — the interactive ink: scope navy in light, aviation yellow in dark)
'--ds-accent': t.accent,
'--ds-accent-hover': t.accentHover,
'--ds-accent-glow': t.accentGlow,
@@ -47,13 +47,19 @@ export function cssVars(theme: Theme) {
// Raw brand colors (invariant)
'--ds-accent-light': brand.accentLight,
+ '--ds-signal': brand.signal,
+ '--ds-signal-strong': brand.signalStrong,
+ '--ds-scope': brand.scope,
+ '--ds-alert': brand.alert,
+ '--ds-ink': brand.ink,
'--ds-angular-red': brand.angularRed,
'--ds-render-green': brand.renderGreen,
'--ds-chat-purple': brand.chatPurple,
// Typography (invariant)
- '--ds-font-serif': typography.fontSerif,
+ '--ds-font-display': typography.fontDisplay,
'--ds-font-sans': typography.fontSans,
+ '--ds-font-diagram': typography.fontDiagram,
'--ds-font-mono': typography.fontMono,
// Shadows (invariant)
diff --git a/libs/design-tokens/src/lib/dark.ts b/libs/design-tokens/src/lib/dark.ts
index be4213194..e8738958a 100644
--- a/libs/design-tokens/src/lib/dark.ts
+++ b/libs/design-tokens/src/lib/dark.ts
@@ -4,6 +4,12 @@ import { baseTokens } from './base.ts';
* Theme-variant tokens resolved for the dark theme.
* Neutral-dark palette aligned to @threadplane/chat lib's dark aesthetic so embedded
* chat surfaces unify with cockpit chrome (no iframe color seam).
+ *
+ * The website's `[data-surface="dark"]` scope diverges from this on purpose:
+ * it mirrors these ACCENTS but swaps the SURFACES for the ATC scope navy,
+ * which is safe there because nothing embeds chat inside that band. See the
+ * docblock above that rule in apps/website/src/styles/ui.css. Keep the two in
+ * step on accents; expect them to differ on surfaces.
*/
export const darkOverrides = Object.freeze({
// Surfaces
@@ -24,13 +30,17 @@ export const darkOverrides = Object.freeze({
bg: 'rgb(17, 17, 17)',
sidebarBg: 'rgba(28, 28, 28, 0.65)',
- // Semantic accent maps to the bright-blue brand color (readable on dark surfaces)
+ // Semantic accent maps to aviation yellow: 10.24:1 on `canvas` and 9.24:1 on
+ // `surface`, the two grounds it actually lands on here. (8.33:1 is the ratio
+ // against the website's scope navy #15253E — a different surface entirely.)
+ // `accent` derives from the brand token; the tints below must be re-derived
+ // with it or they silently keep the previous hue.
accent: baseTokens.brand.accentLight,
- accentHover: '#8dd4ff',
- accentGlow: 'rgba(100, 195, 253, 0.25)',
- accentBorder: 'rgba(100, 195, 253, 0.2)',
- accentBorderHover: 'rgba(100, 195, 253, 0.35)',
- accentSurface: 'rgba(100, 195, 253, 0.08)',
+ accentHover: '#FFC233',
+ accentGlow: 'rgba(255, 175, 0, 0.25)',
+ accentBorder: 'rgba(255, 175, 0, 0.22)',
+ accentBorderHover: 'rgba(255, 175, 0, 0.4)',
+ accentSurface: 'rgba(255, 175, 0, 0.1)',
} as const);
export type DarkOverrides = typeof darkOverrides;
diff --git a/libs/design-tokens/src/lib/ds-var-contract.spec.ts b/libs/design-tokens/src/lib/ds-var-contract.spec.ts
index e3cac8c7c..d264694a3 100644
--- a/libs/design-tokens/src/lib/ds-var-contract.spec.ts
+++ b/libs/design-tokens/src/lib/ds-var-contract.spec.ts
@@ -1,20 +1,40 @@
-import { readFileSync } from 'node:fs';
+import { readFileSync, existsSync } from 'node:fs';
+import { execFileSync } from 'node:child_process';
import { resolve } from 'node:path';
import { describe, it, expect } from 'vitest';
const TOKENS_CSS = resolve(__dirname, 'tokens.css');
/**
- * The `--ds-*` names that cockpit and example apps actually reference today.
+ * The `--ds-*` names this contract protects. Two categories, both guarded
+ * the same way:
*
- * They all reference them with fallbacks and nothing imports tokens.css yet,
- * so dropping a name causes no immediate breakage — it would just silently
- * pin those apps to their fallback colours forever. Hence this list.
+ * 1. Names cockpit and example apps actually reference today. They all
+ * reference them with fallbacks and nothing imports tokens.css yet,
+ * so dropping a name causes no immediate breakage — it would just
+ * silently pin those apps to their fallback colours forever. Hence
+ * this list.
*
- * Derived from:
- * grep -rhoE -- "--ds-[a-z0-9-]+" cockpit examples apps | sort -u
- * intersected with the names tokens.css defined before it came under the
- * generator. Add to this list when a consumer starts using a new name.
+ * Derived from:
+ * grep -rhoE -- "--ds-[a-z0-9-]+" cockpit examples apps | sort -u
+ * intersected with the names tokens.css defined before it came under
+ * the generator. Add to this list when a consumer starts using a new
+ * name.
+ *
+ * 2. Names reserved ahead of their consumers: `--ds-signal`,
+ * `--ds-signal-strong`, `--ds-scope`, `--ds-alert`, `--ds-ink` are the
+ * aviation-yellow retheme's brand colors, and `--ds-font-display` /
+ * `--ds-font-diagram` are the retheme's font faces, replacing
+ * `--ds-font-serif` — a name this list previously guarded and the retheme
+ * retires. `--ds-font-serif` is now fully retired: the workspace.css
+ * fallback that briefly outlived it has since been repaired, so the name
+ * has zero references repo-wide.
+ *
+ * A repo-wide grep likewise finds ZERO references to any of the reserved
+ * names above outside libs/design-tokens/ as of 2026-09-07 — that is
+ * expected, not a mistake, because cockpit/example adoption has not landed
+ * yet. Do NOT remove them because the grep comes up empty; they must stay
+ * guarded so the names are ready when a consumer needs them.
*/
const CONSUMER_REFERENCED = [
'--ds-accent',
@@ -22,19 +42,25 @@ const CONSUMER_REFERENCED = [
'--ds-accent-glow',
'--ds-accent-hover',
'--ds-accent-surface',
+ '--ds-alert',
'--ds-border',
'--ds-border-strong',
'--ds-canvas',
+ '--ds-font-diagram',
+ '--ds-font-display',
'--ds-font-mono',
'--ds-font-sans',
- '--ds-font-serif',
+ '--ds-ink',
'--ds-radius-lg',
'--ds-radius-md',
'--ds-radius-sm',
'--ds-radius-xl',
'--ds-render-green',
+ '--ds-scope',
'--ds-shadow-lg',
'--ds-shadow-md',
+ '--ds-signal',
+ '--ds-signal-strong',
'--ds-surface',
'--ds-surface-dim',
'--ds-surface-tinted',
@@ -60,3 +86,114 @@ describe('--ds-* consumer contract', () => {
expect(missing).toEqual([]);
});
});
+
+/**
+ * No consumer may reference a `--ds-*` name the generator does not define.
+ *
+ * The list above guards the other direction — that a name we know consumers
+ * use does not vanish from tokens.css. It cannot catch the inverse, and the
+ * inverse is what actually shipped during the 2026-09-07 aviation retheme:
+ * `--ds-font-serif` was retired from the token sources while
+ * workspace.css:26 still said `var(--font-display), var(--ds-font-serif)`.
+ *
+ * That failure mode is silent and worse than it looks. An undefined custom
+ * property in a `font-family` list is invalid at computed-value time, so the
+ * whole declaration is dropped and the element loses its font entirely —
+ * there is no partial fallback to the first entry in the list.
+ */
+describe('no dangling --ds-* references', () => {
+ /**
+ * Dangling names this branch inherited rather than introduced. Listed rather
+ * than hidden, so they stay visible and countable.
+ *
+ * `--ds-surface-subtle` pre-dates the retheme (already dangling at 218ac1c41)
+ * and is a real bug in its own right: `background: var(--ds-surface-subtle)`
+ * with no fallback resolves to nothing. Remove this entry when
+ * workspace.css:209 is fixed or the token is added.
+ *
+ * The other thirteen all come from one committed static artifact,
+ * apps/website/public/whitepapers/chat-preview.html, emitted by the
+ * whitepaper generator against a `--ds-color-*` / `--ds-font-*-*` naming
+ * scheme this repo's tokens have never defined. They also pre-date the
+ * retheme; they were simply invisible until CONSUMER_ROOTS widened to the
+ * whole repo below. The fix belongs in the generator, not here.
+ */
+ const KNOWN_PRE_EXISTING = new Set([
+ '--ds-surface-subtle',
+ // The thirteen from whitepapers/chat-preview.html:
+ '--ds-color-brand-primary',
+ '--ds-color-feedback-error',
+ '--ds-color-feedback-success',
+ '--ds-color-surface-elevated',
+ '--ds-color-surface-elevated-dark',
+ '--ds-color-surface-sunken',
+ '--ds-color-text-muted',
+ '--ds-color-text-primary',
+ '--ds-color-text-primary-dark',
+ '--ds-font-family-body',
+ '--ds-font-size-md',
+ '--ds-line-height-normal',
+ '--ds-spacing-base',
+ ]);
+
+ /**
+ * The whole repo, not a hand-kept list of directories. The previous roots
+ * missed libs/chat, libs/example-layouts, libs/design-tokens,
+ * apps/website/public and apps/website/e2e — so a dangling reference in any
+ * of them was invisible to the guard that exists to catch exactly that.
+ */
+ const CONSUMER_ROOTS = ['libs', 'apps', 'cockpit', 'examples'];
+
+ /**
+ * This file is not a consumer. Its docblocks quote `var(--ds-font-serif)` and
+ * `var(--ds-surface-subtle)` as prose about names that are retired or broken,
+ * and once `libs` became a root the grep began reading its own commentary
+ * back as evidence — so documenting a retired name would re-break the guard.
+ */
+ const SELF = 'ds-var-contract.spec.ts';
+
+ it('every --ds-* a consumer references is defined in tokens.css', () => {
+ const repoRoot = resolve(__dirname, '../../../..');
+ const defined = new Set(
+ [...readFileSync(TOKENS_CSS, 'utf-8').matchAll(/^\s*(--ds-[a-z0-9-]+):/gm)].map((m) => m[1]),
+ );
+
+ const referenced = new Set();
+ for (const root of CONSUMER_ROOTS) {
+ const dir = resolve(repoRoot, root);
+ if (!existsSync(dir)) continue;
+ // grep exits 1 when a directory simply has no matches, and execFileSync
+ // throws on any non-zero status — so a clean directory would look like a
+ // broken test rather than a passing one.
+ let found = '';
+ try {
+ found = execFileSync(
+ 'grep',
+ [
+ '-rhoE',
+ '--exclude-dir=node_modules',
+ '--exclude-dir=.next',
+ '--exclude-dir=dist',
+ '--exclude-dir=coverage',
+ `--exclude=${SELF}`,
+ 'var\\(--ds-[a-z0-9-]+',
+ dir,
+ ],
+ { encoding: 'utf-8' },
+ );
+ } catch (error) {
+ const status = (error as { status?: number }).status;
+ if (status !== 1) throw error;
+ }
+ for (const hit of found.split('\n')) {
+ if (hit) referenced.add(hit.replace('var(', ''));
+ }
+ }
+
+ expect(referenced.size).toBeGreaterThan(5);
+ const dangling = [...referenced]
+ .filter((name) => !defined.has(name) && !KNOWN_PRE_EXISTING.has(name))
+ .sort();
+ expect(dangling).toEqual([]);
+ });
+});
diff --git a/libs/design-tokens/src/lib/ds-var-sources-agree.spec.ts b/libs/design-tokens/src/lib/ds-var-sources-agree.spec.ts
index f9d304660..1e06a642a 100644
--- a/libs/design-tokens/src/lib/ds-var-sources-agree.spec.ts
+++ b/libs/design-tokens/src/lib/ds-var-sources-agree.spec.ts
@@ -43,4 +43,30 @@ describe('--ds-* emitters agree', () => {
.map((k) => `${k}: css=${fromCss[k]} fn=${String(fromFn[k])}`);
expect(mismatches).toEqual([]);
});
+
+ /**
+ * Brand colors are the one case where the "tokens.css may be a superset"
+ * rule above does NOT apply. They are theme-invariant identity markers
+ * (base.ts's `brand` block) with no type-scale-style reason to exist in
+ * tokens.css but not cssVars() — unlike the type scale, dropping one from
+ * cssVars() is never legitimate. This list is scoped to just the five
+ * aviation-yellow brand names (not the full CONSUMER_REFERENCED list in
+ * ds-var-contract.spec.ts, which also covers theme-variant and type-scale
+ * names that are allowed to be tokens.css-only).
+ *
+ * A reviewer proved this hole by mutation: deleting these five entries
+ * from css-vars.ts left the full suite green before this test existed.
+ */
+ const BRAND_NAMES = [
+ '--ds-signal',
+ '--ds-signal-strong',
+ '--ds-scope',
+ '--ds-alert',
+ '--ds-ink',
+ ] as const;
+
+ it('cssVars(light) emits every brand color name', () => {
+ const missing = BRAND_NAMES.filter((n) => !(n in fromFn));
+ expect(missing).toEqual([]);
+ });
});
diff --git a/libs/design-tokens/src/lib/light.ts b/libs/design-tokens/src/lib/light.ts
index b8eec8a8e..998aa535f 100644
--- a/libs/design-tokens/src/lib/light.ts
+++ b/libs/design-tokens/src/lib/light.ts
@@ -17,7 +17,7 @@ export const lightOverrides = Object.freeze({
borderStrong: 'rgb(200, 200, 200)',
// Text
- textPrimary: 'rgb(28, 28, 28)',
+ textPrimary: '#0A0A0A',
textSecondary: 'rgb(70, 70, 70)',
textMuted: 'rgb(115, 115, 115)',
textInverted: 'rgb(255, 255, 255)',
@@ -26,13 +26,17 @@ export const lightOverrides = Object.freeze({
bg: 'rgb(255, 255, 255)',
sidebarBg: 'rgba(255, 255, 255, 0.45)',
- // Semantic accent maps to the navy brand color (unchanged — cockpit identity)
+ // Semantic accent is the interactive INK (links, focus, borders) and stays
+ // dark: aviation yellow is 1.84:1 on white and cannot carry text. The tints
+ // below are backgrounds, so they carry the yellow instead — which is the
+ // shape [data-ui="pill"][data-variant="accent"] already has: accent-surface
+ // behind, accent ink on top.
accent: baseTokens.brand.accent,
- accentHover: '#003070',
- accentGlow: 'rgba(0, 64, 144, 0.2)',
- accentBorder: 'rgba(0, 64, 144, 0.15)',
- accentBorderHover: 'rgba(0, 64, 144, 0.3)',
- accentSurface: 'rgba(0, 64, 144, 0.06)',
+ accentHover: '#0E1B2E',
+ accentGlow: 'rgba(255, 175, 0, 0.28)',
+ accentBorder: 'rgba(255, 175, 0, 0.35)',
+ accentBorderHover: 'rgba(255, 175, 0, 0.55)',
+ accentSurface: 'rgba(255, 175, 0, 0.10)',
} as const);
export type LightOverrides = typeof lightOverrides;
diff --git a/libs/design-tokens/src/lib/shadows.ts b/libs/design-tokens/src/lib/shadows.ts
index 3f095e4a9..251bf66a4 100644
--- a/libs/design-tokens/src/lib/shadows.ts
+++ b/libs/design-tokens/src/lib/shadows.ts
@@ -11,8 +11,11 @@ export const shadows = Object.freeze({
md: '0 4px 6px -1px rgba(0, 0, 0, 0.10), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
/** Strong — floating elements, hero collage */
lg: '0 10px 15px -3px rgba(0, 0, 0, 0.10), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
- /** Keyboard focus ring (accent-tinted; unchanged) */
- focus: '0 0 0 3px rgba(0, 64, 144, 0.25)',
+ /**
+ * Keyboard focus ring. Deliberately scope navy and NOT the signal yellow:
+ * a yellow ring on a white page is 1.84:1 and effectively invisible.
+ */
+ focus: '0 0 0 3px rgba(21, 37, 62, 0.30)',
} as const);
export type Shadows = typeof shadows;
diff --git a/libs/design-tokens/src/lib/theme.css b/libs/design-tokens/src/lib/theme.css
index 43f9f2680..d38191fe9 100644
--- a/libs/design-tokens/src/lib/theme.css
+++ b/libs/design-tokens/src/lib/theme.css
@@ -23,7 +23,7 @@
--color-border-strong: rgb(200, 200, 200);
/* Text */
- --color-text-primary: rgb(28, 28, 28);
+ --color-text-primary: #0A0A0A;
--color-text-secondary: rgb(70, 70, 70);
--color-text-muted: rgb(115, 115, 115);
--color-text-inverted: rgb(255, 255, 255);
@@ -33,22 +33,29 @@
--color-sidebar-bg: rgba(255, 255, 255, 0.45);
/* Accent family */
- --color-accent: #004090;
- --color-accent-hover: #003070;
- --color-accent-glow: rgba(0, 64, 144, 0.2);
- --color-accent-border: rgba(0, 64, 144, 0.15);
- --color-accent-border-hover: rgba(0, 64, 144, 0.3);
- --color-accent-surface: rgba(0, 64, 144, 0.06);
+ --color-accent: #15253E;
+ --color-accent-hover: #0E1B2E;
+ --color-accent-glow: rgba(255, 175, 0, 0.28);
+ --color-accent-border: rgba(255, 175, 0, 0.35);
+ --color-accent-border-hover: rgba(255, 175, 0, 0.55);
+ --color-accent-surface: rgba(255, 175, 0, 0.10);
/* Brand */
- --color-accent-light: #64C3FD;
+ --color-accent-light: #FFAF00;
+ /* signal aliases accent-light, scope aliases accent — deliberate: dark.ts derives its accent from accentLight, so both names must exist. Prefer signal/scope in new work. */
+ --color-signal: #FFAF00;
+ --color-signal-strong: #FFB700;
+ --color-scope: #15253E;
+ --color-alert: #FF3200;
+ --color-ink: #0A0A0A;
--color-angular-red: #DD0031;
--color-render-green: #1a7a40;
--color-chat-purple: #5a00c8;
/* Fonts */
- --font-garamond: "EB Garamond", Georgia, serif;
- --font-inter: Inter, system-ui, sans-serif;
+ --font-display: "Archivo Black", system-ui, sans-serif;
+ --font-sans: Archivo, system-ui, sans-serif;
+ --font-diagram: Inter, system-ui, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, monospace;
/* Type scale */
@@ -81,7 +88,7 @@
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.10), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.10), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
- --shadow-focus: 0 0 0 3px rgba(0, 64, 144, 0.25);
+ --shadow-focus: 0 0 0 3px rgba(21, 37, 62, 0.30);
/* Space scale */
--spacing-section-y: clamp(64px, 8vw, 120px);
diff --git a/libs/design-tokens/src/lib/token-css-parity.spec.ts b/libs/design-tokens/src/lib/token-css-parity.spec.ts
index 2228813cb..19bfd5641 100644
--- a/libs/design-tokens/src/lib/token-css-parity.spec.ts
+++ b/libs/design-tokens/src/lib/token-css-parity.spec.ts
@@ -18,13 +18,19 @@ const CSS_VAR_BY_PATH: Record = {
// Brand (theme-invariant)
'brand.accent': '--color-accent',
'brand.accentLight': '--color-accent-light',
+ 'brand.signal': '--color-signal',
+ 'brand.signalStrong': '--color-signal-strong',
+ 'brand.scope': '--color-scope',
+ 'brand.alert': '--color-alert',
+ 'brand.ink': '--color-ink',
'brand.angularRed': '--color-angular-red',
'brand.renderGreen': '--color-render-green',
'brand.chatPurple': '--color-chat-purple',
// Font families
- 'typography.fontSerif': '--font-garamond',
- 'typography.fontSans': '--font-inter',
+ 'typography.fontDisplay': '--font-display',
+ 'typography.fontSans': '--font-sans',
+ 'typography.fontDiagram': '--font-diagram',
'typography.fontMono': '--font-mono',
// Type scale — size
@@ -100,7 +106,7 @@ const CSS_VAR_BY_PATH: Record = {
const EXCLUDED: ReadonlyArray<{ prefix: string; why: string }> = [
{
prefix: 'typography.h1.family',
- why: 'value is already `var(--font-garamond)` — a var about a var buys nothing',
+ why: 'value is already `var(--font-display)` — a var about a var buys nothing',
},
{ prefix: 'typography.h2.family', why: 'see h1.family' },
{ prefix: 'typography.h3.family', why: 'see h1.family' },
diff --git a/libs/design-tokens/src/lib/tokens-dark.css b/libs/design-tokens/src/lib/tokens-dark.css
index 1d0323f47..633e3405c 100644
--- a/libs/design-tokens/src/lib/tokens-dark.css
+++ b/libs/design-tokens/src/lib/tokens-dark.css
@@ -20,18 +20,24 @@
:root {
/* Colors */
--ds-bg: rgb(17, 17, 17);
- --ds-accent: #64C3FD;
- --ds-accent-hover: #8dd4ff;
- --ds-accent-light: #64C3FD;
- --ds-accent-glow: rgba(100, 195, 253, 0.25);
- --ds-accent-border: rgba(100, 195, 253, 0.2);
- --ds-accent-border-hover: rgba(100, 195, 253, 0.35);
- --ds-accent-surface: rgba(100, 195, 253, 0.08);
+ --ds-accent: #FFAF00;
+ --ds-accent-hover: #FFC233;
+ --ds-accent-light: #FFAF00;
+ --ds-accent-glow: rgba(255, 175, 0, 0.25);
+ --ds-accent-border: rgba(255, 175, 0, 0.22);
+ --ds-accent-border-hover: rgba(255, 175, 0, 0.4);
+ --ds-accent-surface: rgba(255, 175, 0, 0.1);
--ds-text-primary: rgb(245, 245, 245);
--ds-text-secondary: rgb(200, 200, 200);
--ds-text-muted: rgb(160, 160, 160);
--ds-text-inverted: rgb(17, 17, 17);
--ds-sidebar-bg: rgba(28, 28, 28, 0.65);
+ /* Brand (invariant — identical in tokens.css and tokens-dark.css) */
+ --ds-signal: #FFAF00;
+ --ds-signal-strong: #FFB700;
+ --ds-scope: #15253E;
+ --ds-alert: #FF3200;
+ --ds-ink: #0A0A0A;
--ds-angular-red: #DD0031;
--ds-render-green: #1a7a40;
--ds-chat-purple: #5a00c8;
@@ -45,8 +51,9 @@
--ds-border-strong: rgb(60, 60, 60);
/* Typography */
- --ds-font-serif: "EB Garamond", Georgia, serif;
- --ds-font-sans: Inter, system-ui, sans-serif;
+ --ds-font-display: "Archivo Black", system-ui, sans-serif;
+ --ds-font-sans: Archivo, system-ui, sans-serif;
+ --ds-font-diagram: Inter, system-ui, sans-serif;
--ds-font-mono: "JetBrains Mono", ui-monospace, monospace;
/* Typography — type scale */
@@ -72,7 +79,7 @@
--ds-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--ds-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.10), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--ds-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.10), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
- --ds-shadow-focus: 0 0 0 3px rgba(0, 64, 144, 0.25);
+ --ds-shadow-focus: 0 0 0 3px rgba(21, 37, 62, 0.30);
/* Radius */
--ds-radius-sm: 6px;
diff --git a/libs/design-tokens/src/lib/tokens.css b/libs/design-tokens/src/lib/tokens.css
index f2bf372a7..8be025b79 100644
--- a/libs/design-tokens/src/lib/tokens.css
+++ b/libs/design-tokens/src/lib/tokens.css
@@ -20,18 +20,24 @@
:root {
/* Colors */
--ds-bg: rgb(255, 255, 255);
- --ds-accent: #004090;
- --ds-accent-hover: #003070;
- --ds-accent-light: #64C3FD;
- --ds-accent-glow: rgba(0, 64, 144, 0.2);
- --ds-accent-border: rgba(0, 64, 144, 0.15);
- --ds-accent-border-hover: rgba(0, 64, 144, 0.3);
- --ds-accent-surface: rgba(0, 64, 144, 0.06);
- --ds-text-primary: rgb(28, 28, 28);
+ --ds-accent: #15253E;
+ --ds-accent-hover: #0E1B2E;
+ --ds-accent-light: #FFAF00;
+ --ds-accent-glow: rgba(255, 175, 0, 0.28);
+ --ds-accent-border: rgba(255, 175, 0, 0.35);
+ --ds-accent-border-hover: rgba(255, 175, 0, 0.55);
+ --ds-accent-surface: rgba(255, 175, 0, 0.10);
+ --ds-text-primary: #0A0A0A;
--ds-text-secondary: rgb(70, 70, 70);
--ds-text-muted: rgb(115, 115, 115);
--ds-text-inverted: rgb(255, 255, 255);
--ds-sidebar-bg: rgba(255, 255, 255, 0.45);
+ /* Brand (invariant — identical in tokens.css and tokens-dark.css) */
+ --ds-signal: #FFAF00;
+ --ds-signal-strong: #FFB700;
+ --ds-scope: #15253E;
+ --ds-alert: #FF3200;
+ --ds-ink: #0A0A0A;
--ds-angular-red: #DD0031;
--ds-render-green: #1a7a40;
--ds-chat-purple: #5a00c8;
@@ -45,8 +51,9 @@
--ds-border-strong: rgb(200, 200, 200);
/* Typography */
- --ds-font-serif: "EB Garamond", Georgia, serif;
- --ds-font-sans: Inter, system-ui, sans-serif;
+ --ds-font-display: "Archivo Black", system-ui, sans-serif;
+ --ds-font-sans: Archivo, system-ui, sans-serif;
+ --ds-font-diagram: Inter, system-ui, sans-serif;
--ds-font-mono: "JetBrains Mono", ui-monospace, monospace;
/* Typography — type scale */
@@ -72,7 +79,7 @@
--ds-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--ds-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.10), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--ds-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.10), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
- --ds-shadow-focus: 0 0 0 3px rgba(0, 64, 144, 0.25);
+ --ds-shadow-focus: 0 0 0 3px rgba(21, 37, 62, 0.30);
/* Radius */
--ds-radius-sm: 6px;
diff --git a/libs/design-tokens/src/lib/typography.ts b/libs/design-tokens/src/lib/typography.ts
index a015b4a2c..1eea936ed 100644
--- a/libs/design-tokens/src/lib/typography.ts
+++ b/libs/design-tokens/src/lib/typography.ts
@@ -1,9 +1,10 @@
/**
* Typography tokens — font families and type scale used across the design system.
*
- * - Serif (EB Garamond): Headlines, elegant emphasis
- * - Sans (Inter): Body text, UI elements
- * - Mono (JetBrains Mono): Code, labels, metadata
+ * - Display (Archivo Black): headlines and the wordmark. Single weight.
+ * - Sans (Archivo): body text, UI elements
+ * - Diagram (Inter): diagram text ONLY — see fontDiagram
+ * - Mono (JetBrains Mono): code, labels, metadata
*
* The h1/h2/h3/eyebrow/bodyLg/body/caption objects are the type scale
* used by the marketing-site UI primitives. Each entry includes
@@ -11,10 +12,26 @@
* `letterSpacing`, `transform`.
*/
export const typography = {
- /** Serif font for headings */
- fontSerif: '"EB Garamond", Georgia, serif',
- /** Sans-serif font for body text */
- fontSans: 'Inter, system-ui, sans-serif',
+ /** Display face for headlines. Archivo Black ships one weight (400). */
+ fontDisplay: '"Archivo Black", system-ui, sans-serif',
+ /** Text and UI face. */
+ fontSans: 'Archivo, system-ui, sans-serif',
+ /**
+ * Diagram text, deliberately NOT the brand face.
+ *
+ * Diagrams are information, not brand surface, and their geometry is tuned
+ * to Inter's metrics: EnterpriseArchitecture.tsx pins every rectangle to an
+ * 8px grid and home-architecture.spec.ts measures each rendered text run
+ * against its card. Retyping diagrams would force a geometry rework larger
+ * than the retheme itself.
+ *
+ * On apps/website this var must keep being supplied by next/font in
+ * layout.tsx: the raw `Inter` literal theme.css emits does not match
+ * next/font's hashed family name, so without the loader diagrams fall back
+ * to system-ui and every glyph width shifts. See the FONTS note in
+ * apps/website/src/styles/ui.css.
+ */
+ fontDiagram: 'Inter, system-ui, sans-serif',
/** Monospace font for code and labels. ui-monospace before the generic
* keyword: consumers that don't load JetBrains Mono (the cockpit example
* apps) get the platform mono (SF Mono) instead of Courier. */
@@ -23,17 +40,17 @@ export const typography = {
h1: {
size: 'clamp(48px, 6vw, 72px)',
line: 1.08,
- family: 'var(--font-garamond)',
+ family: 'var(--font-display)',
},
h2: {
size: 'clamp(36px, 4.5vw, 56px)',
line: 1.12,
- family: 'var(--font-garamond)',
+ family: 'var(--font-display)',
},
h3: {
size: '28px',
line: 1.25,
- family: 'var(--font-inter)',
+ family: 'var(--font-sans)',
weight: 600,
},
eyebrow: {
@@ -47,17 +64,17 @@ export const typography = {
bodyLg: {
size: '20px',
line: 1.6,
- family: 'var(--font-inter)',
+ family: 'var(--font-sans)',
},
body: {
size: '16px',
line: 1.6,
- family: 'var(--font-inter)',
+ family: 'var(--font-sans)',
},
caption: {
size: '14px',
line: 1.5,
- family: 'var(--font-inter)',
+ family: 'var(--font-sans)',
},
} as const;
diff --git a/libs/workspace-react/src/lib/components/api-mode/api-mode.spec.tsx b/libs/workspace-react/src/lib/components/api-mode/api-mode.spec.tsx
index 4c896e4b4..a77d1a160 100644
--- a/libs/workspace-react/src/lib/components/api-mode/api-mode.spec.tsx
+++ b/libs/workspace-react/src/lib/components/api-mode/api-mode.spec.tsx
@@ -118,7 +118,7 @@ describe('ApiMode', () => {
'utf8'
);
expect(css).toMatch(
- /\.workspace-prose \.workspace-api-heading\s*\{[\s\S]*font-family:\s*var\(--font-inter\),\s*var\(--ds-font-sans\)[\s\S]*letter-spacing:\s*normal/
+ /\.workspace-prose \.workspace-api-heading\s*\{[\s\S]*font-family:\s*var\(--font-sans\),\s*var\(--ds-font-sans\)[\s\S]*letter-spacing:\s*normal/
);
});
diff --git a/libs/workspace-react/src/lib/components/control-plane/cockpit-control-plane.spec.tsx b/libs/workspace-react/src/lib/components/control-plane/cockpit-control-plane.spec.tsx
index 96a1e2eca..c442d62a8 100644
--- a/libs/workspace-react/src/lib/components/control-plane/cockpit-control-plane.spec.tsx
+++ b/libs/workspace-react/src/lib/components/control-plane/cockpit-control-plane.spec.tsx
@@ -145,7 +145,7 @@ describe('CockpitControlPlane', () => {
]) {
const declarations = declarationsFor(cockpitCss, selector);
expect(declarations).toMatch(
- /font-family:\s*var\(--font-inter,\s*var\(--ds-font-sans\)\)/
+ /font-family:\s*var\(--font-sans,\s*var\(--ds-font-sans\)\)/
);
expect(declarations).toMatch(/font-size:\s*12px/);
expect(declarations).toMatch(/font-weight:\s*600/);
diff --git a/libs/workspace-react/src/styles/workspace.css b/libs/workspace-react/src/styles/workspace.css
index 820ac886f..aff871fa4 100644
--- a/libs/workspace-react/src/styles/workspace.css
+++ b/libs/workspace-react/src/styles/workspace.css
@@ -23,7 +23,7 @@
.workspace-prose h1,
.workspace-prose h2,
.workspace-prose h3 {
- font-family: var(--font-garamond), var(--ds-font-serif);
+ font-family: var(--font-display), var(--ds-font-display);
color: var(--ds-text-primary);
letter-spacing: -0.01em;
}
@@ -43,7 +43,7 @@
margin: 1.5rem 0 0.5rem;
}
.workspace-prose .workspace-api-heading {
- font-family: var(--font-inter), var(--ds-font-sans);
+ font-family: var(--font-sans), var(--ds-font-sans);
letter-spacing: normal;
text-transform: none;
}
@@ -471,7 +471,7 @@
color: var(--ds-text-muted);
background: transparent;
cursor: pointer;
- font-family: var(--font-inter, var(--ds-font-sans));
+ font-family: var(--font-sans, var(--ds-font-sans));
font-size: 12px;
font-weight: 600;
letter-spacing: normal;
@@ -495,7 +495,7 @@
margin: 0;
padding: 8px;
color: var(--ds-text-muted);
- font-family: var(--font-inter, var(--ds-font-sans));
+ font-family: var(--font-sans, var(--ds-font-sans));
font-size: 12px;
line-height: 1.2;
font-weight: 600;