Demonstrates the canonical print/slide token roles. Each row labels the token name (left), the print pt + web rem mapping (mono), and a render at the rem value. Floor/ceiling rows are marked with their relationship to the default.
Same content, different read distance. Print is read at 18 inches; slides project at 6-30 feet. Slide context overrides body inheritance to 18pt at the slide root, with body content at 22pt (body-display). Caption / caption-strong / micro are scope-overridden inside .vd-slide to slide pt values.
Telemetry stops being a feature the moment another system depends on its cadence to be safe. The thesis of this brief is narrow and load-bearing.
Telemetry stops being a feature the moment another system depends on its cadence to be safe.
Tokens with cell variation ship as a band: floor (min), default, ceiling (max). Authors pick within the band based on cover length, audience, or read distance. Validators check the rendered size sits inside the band.
2rem / 32pt
cover
2.375rem / 38pt
3rem / 48pt
Typography sizes, weights, line-heights, and letter-spacing are identical across light and dark mode. Only colors differ. Brand teal #0fc8c3 at lightness 0.75 has 2.085:1 contrast on white (fails WCAG AA-large 3:1, not just AA-body 4.5:1), and 9.54:1 on canonical dark #09090b (passes). For text on light backgrounds at every size — body, caption, AND headline accent — use brand.verdigris-on-light (#007571, 5.55:1 on white, passes AA). Numbers verified by npm run validate:wcag; CI blocks merges that introduce regressions.
brand.verdigris-on-lightbrand.verdigrisDocumentation
Typography
Font Stack
Inter (Body — shared)
Inter is the body font across both codebases. It’s a versatile, highly legible sans-serif designed for screens, with excellent support for tabular numbers and multiple weights.
- Usage: Body text, labels, navigation, buttons, data tables
- Weights: 400 (regular), 500 (medium), 600 (semibold), 700 (bold)
- Loading: Google Fonts or self-hosted via
next/font(Patina) or<link>(www)
Lato (Display/Headings — www only)
Lato provides visual contrast for marketing headings. It’s slightly wider and rounder than Inter, creating a more approachable feel for hero text. Patina does not use a display font — it uses Inter everywhere.
- Usage: H1, H2, H3 headings on marketing pages
- Weights: 700 (bold) only — headings don’t need medium/regular
- Where: www applies
font-displayclass to allh1–h6via@layer base
JetBrains Mono (Code — Patina canonical)
Patina uses JetBrains Mono for all monospace contexts. The www site falls back to system monospace. JetBrains Mono has excellent ligatures and is designed for code readability.
- Usage: Code blocks, terminal output, metrics, IDs, timestamps, CLI commands
- Weights: 400 only
Font Evaluation (Inter+Lato vs Alternatives)
The current Inter+Lato pairing is the baseline to evaluate against. Potential directions:
Option A: Keep Inter + Lato (current www)
- Pros: No migration cost, familiar to users, Lato is widely available
- Cons: Lato is ubiquitous (Google’s #3 most popular font), limited brand differentiation
- Verdict: Safe, not distinctive
Option B: Inter for everything (current Patina)
- Pros: Simplest stack, one font to load, consistent across app and marketing
- Cons: No typographic contrast between headings and body — marketing pages feel flat
- Verdict: Works for apps, insufficient for marketing
Option C: Inter + a geometric display font
- Candidates: Plus Jakarta Sans, General Sans, Satoshi, Cabinet Grotesk
- Pros: More personality than Lato, still clean/tech-forward
- Cons: Requires evaluation and licensing review
- Verdict: Worth exploring in Phase 3
Option D: A single variable font with wide weight range
- Candidates: Instrument Sans, Space Grotesk
- Pros: One font file, weight variation creates hierarchy
- Cons: Less typographic contrast than a serif/sans pairing
- Verdict: Modern but may not differentiate enough
Evaluation Results (2026-03-27)
Evaluated all four options against these criteria:
| Criteria | Weight | Inter+Lato (A) | Inter-only (B) | Inter+Geometric (C) | Single Variable (D) |
|---|---|---|---|---|---|
| Brand fit | High | Good — clean, approachable | Flat — no hierarchy contrast | Excellent — fresh, distinctive | Good — modern |
| Migration cost | High | Zero | Low (remove Lato) | Medium (add new font, test) | Medium |
| Performance | Medium | 2 fonts, well-cached (Google #3 + #1) | 1 font, fastest | 2 fonts, may need self-hosting | 1 font |
| Patina alignment | Medium | Divergent (Patina = Inter-only) | Perfect alignment | Divergent | Divergent |
| Licensing | Low | Free (OFL) | Free (OFL) | Varies — some need licensing | Varies |
Option C deep dive: Plus Jakarta Sans and Satoshi were the strongest candidates. Both offer more geometric personality than Lato. However:
- Neither is available on Google Fonts (requires self-hosting or CDN setup)
- Introducing a new display font mid-sprint adds risk with no user-facing upside yet
- The token architecture makes a future font swap trivial — update
fontFamily.displayintokens/typography/font-family.jsonand rebuild
Decision: Lock Inter + Lato.
Rationale:
- Zero migration cost — already implemented in www, already in the token system
- Performance — both fonts are top-10 on Google Fonts, maximizing cache hits across the web
- Good enough — Lato at 700 weight provides sufficient heading contrast for marketing pages
- Future-proof — the token system decouples the font choice from every consumer. If we want to explore a geometric display font later, it’s a single JSON change + rebuild. No reason to block the website upgrade sprint on a font decision.
- Patina stays Inter-only — the display font is a justified www deviation (marketing needs font contrast that an app dashboard doesn’t)
Locked fonts:
- Body:
Inter(shared across all surfaces) - Display:
Lato(www marketing headings only) - Mono:
JetBrains Mono(code, metrics, data)
Review trigger: Revisit if/when Verdigris rebrands or if user research indicates the typography feels generic. The token architecture ensures a swap is low-cost when the time comes.
Type Scale
All values from production CSS. This repo is the canonical source of truth.
Headings
| Level | Desktop | Mobile (<1024px) | Weight | Line Height | Letter Spacing |
|---|---|---|---|---|---|
| H1 | 4rem (64px) | 2.75rem (44px) | 700 | 1.1 | -0.02em |
| H2 | 3rem (48px) | 2rem (32px) | 700 | 1.2 | -0.02em |
| H3 | 2rem (32px) | 1.5rem (24px) | 700 | 1.3 | — |
Note: www currently uses 991px as its mobile breakpoint. The canonical target is 1024px (Tailwind lg) pending migration.
Body
| Name | Size | Line Height | Usage |
|---|---|---|---|
| Large | 1.25rem (20px) | 1.6 | Hero subtext, feature descriptions |
| Medium | 1.125rem (18px) | 1.6 | Cards, summaries |
| Regular | 1rem (16px) | 1.6 | Body paragraphs, lists |
| Small | 0.875rem (14px) | 1.6 | Captions, metadata, fine print |
1.6 is the base default. Use 1.65-1.75 on tinted/dark backgrounds per the coupling rules, and 1.7 for Narrate prose. See composition.persuade-web-page.coupling.tinted-line-height and composition.narrate-web-page.coupling.long-form-line-height.
Print Type Scale
Print stylesheets (cover, whitepaper-body, case-study, one-pager, slides) consume parallel pt values from build/dist/typography/print.css. One scale.json source; two output emissions (rem for web, pt for print). The token names are identical across surfaces; the pt mapping resolves the read-distance principle.
Display (cover headlines)
| Token | Print pt | Role |
|---|---|---|
display.cover-min |
32pt | Long covers compress to this floor |
display.cover |
38pt | Whitepaper cover headline default |
display.cover-max |
48pt | Single-line ceo_brief opens to this ceiling |
Display (slide headlines, projected)
| Token | Print pt | Role |
|---|---|---|
display.slide-h1-min |
48pt | Long title-slide line floor |
display.slide-h1 |
56pt | Title-slide H1 / close H2 default |
display.slide-h1-max |
64pt | Auditorium one-word title ceiling |
Headline (page-level)
| Token | Print pt | Role |
|---|---|---|
headline.page-min |
26pt | One-pager headline |
headline.page |
30pt | Case-study page H1 |
headline.page-max |
38pt | Cover headline upper bound (= display.cover) |
headline.section-min |
16pt | Pull-quote tier |
headline.section |
18pt | H2 across whitepaper-body, case-study, cover paper |
headline.section-max |
36pt | Body-slide H1 default at slide root |
headline.slide-min |
40pt | Body-slide H1 floor for long-distance projection (auditorium opt-in) |
headline.slide |
42pt | Body-slide H1 auditorium default — opt in via [data-projection="auditorium"] on <body> or .vd-slide ancestor |
headline.slide-max |
44pt | Body-slide H1 ceiling for auditorium |
headline.sub |
14pt | H3 in whitepaper-body |
Deck (subtitle / supporting line)
| Token | Print pt | Role |
|---|---|---|
deck.page-min |
12pt | One-pager deck |
deck.page |
13pt | Cover deck, case-study deck, CTA strip |
deck.page-max |
14pt | Whitepaper-body H3 / lab_tradition deck |
deck.slide-min |
18pt | Body-slide deck floor |
deck.slide |
20pt | Body-slide deck-line default |
deck.slide-max |
22pt | Body-slide deck ceiling |
deck.slide-title-min |
22pt | Title-slide deck floor |
deck.slide-title |
24pt | Title-slide deck default |
deck.slide-title-max |
28pt | Title-slide deck ceiling |
Body (print)
| Token | Print pt | Role |
|---|---|---|
body |
11pt | Canonical baseline across all print cells |
body-small |
9.5pt | Affil, table thead, references |
body-display |
22pt | Slide-context body (long-form reading at projection distance) |
Print body is normalized to 11pt across all cells (cover, whitepaper-body, case-study, one-pager). Prior cover.css and whitepaper-body.css inherited 10.5pt by accident; that 0.5pt drift is below WCAG perceptual threshold but above evaluator detectability and surfaced every cohesion-audit run. 11pt aligns with the explicit case-study spec and editorial-register references (Brookings, Stripe Press, Anthropic Core Views).
Caption / Micro
| Token | Print pt | Role |
|---|---|---|
caption |
9pt | Footer, eyebrow, figcaption — the most common small-text role |
caption-strong |
9.5pt | Number-prefixed eyebrow (visual weight against the number) |
micro-min |
7.5pt | Site-header chrome only; not content |
micro |
8pt | Disclosure, ORCID, figure-credit |
micro-max |
9pt | Ceiling for the micro tier — above this, use caption |
Metric (anchor numbers)
| Token | Print pt | Role |
|---|---|---|
metric.deck |
17pt | Case-study pull-quote tier |
metric.callout |
24pt | One-pager 3-up grid AND body-slide 3-up grid |
metric.anchor-min |
40pt | Compact callout floor |
metric.anchor |
56pt | Case-study anchor / single-anchor slide |
metric.anchor-max |
72pt | Auditorium close-slide ceiling |
Anchor metrics use Lato 700 + tnum + tracking 0 + line-height 1.0. A 56pt headline uses tracking -0.02em + line-height 1.10. Same size, different typographic treatment, different role — that is why metric is its own tier, not a headline alias.
Foot-gun: don’t use fontSize.h1 in print stylesheets
fontSize.h1 is the legacy web token at 4rem (64px). It is preserved verbatim for Patina and www consumer compatibility. Print stylesheets must not reach for it; reach for display.cover (38pt) or headline.page (30pt) directly. Aliasing display.cover to fontSize.h1 would couple the cover headline to every web h1 consumer and silently move it on any future tweak.
Slide-root override
The slide stylesheet sets a slide-context body inheritance (18pt) at the slide root via .vd-slide { font-size: 18pt }, distinct from the 11pt print body inherited by cover, whitepaper-body, case-study, and one-pager. This codifies the read-distance principle: print is read at 18 inches; slides project to 6-30 feet. Slide-context tokens (display.slide-h1, headline.section-max, deck.slide, body-display, metric.callout) carry the projection-scaled pt values.
In addition, three canonical tokens are scoped-overridden at .vd-slide:
| Token | Print pt | Slide pt | Why |
|---|---|---|---|
caption-strong |
9.5pt | 12pt | Slide eyebrow + callout label legible at projection distance |
caption |
9pt | 11pt | Footer chrome + figure-label readable from row N of the room |
micro |
8pt | 10pt | Figure credit at slide minimum |
Print stylesheets loaded in the same document keep their print pt values for these tokens; the override is scoped to .vd-slide descendants only.
Body Measure (Line Length)
The number of characters per line of body text. This is one of the highest-leverage typography decisions — it controls reading rhythm more than font choice, size, or leading does.
Canonical measure: 65ch (default) / 75ch (maximum).
| Surface | Target | Hard maximum |
|---|---|---|
| Platform / product / case-study body | 65-68ch | 75ch |
| Narrate / inform (editorial) | 65ch | 70ch |
| Tinted or dark surface | 65ch | 65ch (explicit) |
| Marketing hero subtext | 50-55ch | 60ch |
| Dashboard / tabular | 45-55ch | (not enforced — data rules dominate) |
At our type scale (Inter 16px body), 1ch ≈ 8px. So:
| Measure | Roughly |
|---|---|
| 45ch | 360px |
| 65ch | 520px — comfortable reading |
| 68ch | 544px — our default |
| 75ch | 600px — absolute upper bound |
| 90ch | 720px — too wide, rejected |
Why these numbers
The Quiet Reader + Precision Instrument model demands reading comfort first. Marketing pages that feel like product docs (which ours do — technical audience, specification-heavy copy) must honor the typography literature. A 100ch column says “this is brochure copy nobody will read end-to-end.” A 65ch column says “I expect you to read this.”
Three sources converge on the 65-75 range:
- Apple Human Interface Guidelines — “Aim for an average of 50–75 characters per line of body text” for reading comfort in long-form content.
- Bringhurst, Elements of Typographic Style — “Anything from 45 to 75 characters is widely regarded as a satisfactory length of line for a single-column page set in a serifed text face in a text size. The 66-character line (counting both letters and spaces) is widely regarded as ideal.”
- Butterick, Practical Typography — “The optimal line length … is between 45 and 90 characters, with 60–80 characters being preferable.”
Every respected editorial reference lands within this range. There is no position that “100+ characters is fine for readable body text.”
Why not wider
The eye saccades (jumps ahead) in chunks of roughly 7-9 characters. Beyond ~75ch, the return sweep to the start of the next line crosses enough horizontal distance that the reader can lose their place. This is measurable: reading speed drops, comprehension drops, re-reads increase. Marketing research shows conversion drops too — people skim rather than read.
Wider lines are appropriate for scanning (tables, dashboards, data-dense displays), not for reading.
Evaluator enforcement
rules/visual-rules.yml → typography.line-length.body enforces 75ch as a hard maximum. A configured contentMaxWidth on any content page that resolves to more than 75ch is a rule violation, not a warning — meaning evaluator runs should block merges.
Consumers should pin their content column using ch units, not px or rem, so the measure stays tied to font character width regardless of type scale changes. Example:
.prose-column { max-width: 68ch; margin-inline: auto; }
When body becomes figure: the break-out patterns
Body text at 65ch is narrow. Data visualizations, tables with more than 4 columns, and hero-scale illustrations often need more horizontal room. Three canonical patterns let content escape the reading column without abandoning it. See categories/visualizations/interactive-viz.md for full specs.
Short version:
- Inline — matches prose measure. Default for small figures, code blocks, inline formulas.
- Breakout — widens to the content column (up to a section max, typically ~900px), centered on the viewport. Default for Canvas visualizations, charts, wide comparison tables.
- Full-bleed — spans the full viewport width edge-to-edge. Reserved for Demonstrate-arc pages where a visualization IS the hero of a section. Use
<FullBleedSection>.
Pattern choice is a design decision per surface, not a component-level default. The evaluator does not dictate the choice; it does flag ad-hoc widths that aren’t one of these three patterns.
Notes
- CTA text-transform (uppercase) is not enforced. Buttons use sentence case.
- Patina uses Inter for headings (no display font). The www display font (Lato) is a justified deviation for marketing contexts.
- All values in this document are canonical. The design token JSON files are the machine-readable source of truth.