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(
x
); + const el = container.querySelector('[data-ui="section"]'); + expect(el?.getAttribute('data-surface')).toBe('signal'); + }); + it('defaults to canvas', () => { const { container } = render(
x
); 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(
x
); + 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;