Typography
Two type families doing distinct jobs — Geist for nearly everything, Sharp Grotesk for the page title — plus Geist Mono for true technical values. Set type with the standard text-* / font-* utilities, never inline px. This page maps every utility to its rendered value, explains the rules, and shows them in context.
Principles
Two jobs, sparingly applied
The system leans on Geist and earns hierarchy from size, weight and colour — not from reaching for a different typeface. Two rules carry most of the weight:
Mono is for machines, not labels
A monospace face is only doing a job when characters must align in a grid or be read one-by-one — IDs, API keys, hashes, versions, code, shortcuts. On an eyebrow or a header it adds no function, only a borrowed “developer-tool” signal, and it reads slower. So uppercase labels stay in Geist; mono is reserved for technical values.
The display face is for the page title
Every mature product UI (Carbon’s “productive” set, Stripe, Linear, Vercel) keeps one neutral family for the app and walls the expressive face off to marketing. Sharp Grotesk carries the page / hero title; section headers, card titles, metric numbers and pills are Geist. Numbers use tabular-nums to align — never a special display face.
Families
font-* utilities
Geist is the default and does almost all the work; Sharp Grotesk (the display cuts) carries the page title; Geist Mono is for technical values only.
font-defaultGeist
next/font (Google)
The default for almost everything — body, UI, buttons, table data, metric numbers, AND every uppercase label/eyebrow. Hierarchy comes from size, weight and colour.
font-displaySharp Grotesk DB Semibold · width 21
self-hosted (licensed)
The page title ONLY — one display heading per view. Section headers, card titles and metric values are Geist (see the rules below). The wider width-25 cut (font-display-wide) is marketing-only and isn't used in the app.
font-monoGeist Mono
next/font (Google)
Technical values only — IDs, API keys, hashes, version strings, code, keyboard shortcuts. Things you'd copy-paste or read character-by-character. NOT eyebrows or labels.
font-stickerSharp Grotesk DB Semibold · width 15
self-hosted (licensed)
Expressive brand badge / pill labels only (the Badge brand tones).
Scale
The app type scale — 6 steps
Design with this closed set of seven role-based steps, not the raw ramp. The app lives almost entirely on Caption (12px) and Body (14px); everything above is a deliberate, role-mapped step. The full migration to semantic role tokens (text-caption …) is tracked in GOATS-415; today each role maps onto the existing text-* utility shown.
Caption 12px
text-xsCaptions, eyebrows, table headers, KPI labels, chips
Body 14px
text-smDefault UI text — body, buttons, inputs, table cells
Body large 16px
text-baseLead paragraphs, card body, emphasized body
Title 18px
text-lgCard / list-item titles (h3) and section headers (h2 = + weight + eyebrow)
Page 24px
text-2xlPage titles (h1)
Display 30px
text-3xlHero metric / big stat — the one big step
Tokens
Underlying text-* utilities
The raw Tailwind tokens the roles above map onto, at our 100% (16px) base. Sizes are rem-based, so they scale together if the base changes. There is no text-md — the 16px step is text-base. Only the six in-scale sizes are shown: text-2xs (10px, below the 12px floor), text-xl (20px, collapsed into Title) and the marketing ramp (text-4xl–text-7xl, 36–72px) are not part of the app scale.
text-xs0.75rem · 12px
lh 16px
text-sm0.875rem · 14px
lh 20px
text-base1rem · 16px
lh 24px
text-lg1.125rem · 18px
lh 28px
text-2xl1.5rem · 24px
lh 32px
text-3xl1.875rem · 30px
lh 36px
Weight
font-* weight utilities
Geist carries a real 400–700 range — lean on it for emphasis (bold for metrics and key values, regular elsewhere). The Sharp Grotesk display family ships a single Semibold cut declared across 500–800, so every weight utility renders the same glyph: on display type, vary tone, not weight.
Geist (font-default)
Sharp Grotesk (font-display)
Guide
How to use it — role recipes
Reach for these combinations rather than assembling sizes by hand. Each row is a live render of the exact class string beside it; copy it verbatim. Headings h1–h3 also pick up -0.02em tracking from the base layer automatically.
Page title
The ONE display-face heading on a page. Use PageHeader, which bakes this in.
font-display text-2xl font-semibold text-content-emphasisSection header (H2)
Groups within a page. Geist — the display face stops at the page title.
font-default text-lg font-semibold text-content-emphasisCard title (H3)
Title inside a card / list row. Geist, semibold for weight-based emphasis.
font-default text-sm font-semibold text-content-emphasisMetric value
Big KPI numbers (the Display step, 30px). Geist (not the display face) + tabular-nums so digits align; bold carries the emphasis.
font-default text-3xl font-bold tabular-nums tracking-[-0.02em] text-content-emphasisLead / large body
Intro paragraphs and emphasis copy (the Title step at regular weight; 20px is retired).
font-default text-lg text-content-defaultBody
Default reading text (brand Body 2 · 16px).
font-default text-base text-content-defaultDense UI text
The app workhorse (brand Body 3 · 14px) — buttons, inputs, table cells, sidebar, compact UI.
font-default text-sm text-content-defaultCaption / helper
Secondary helper text, timestamps, sub-labels.
font-default text-xs text-content-subtleUppercase label / eyebrow
KPI captions, overlines, section eyebrows. Geist — NOT mono. Case + tracking + tone do the work. Use the Eyebrow component. (Table headers are sentence case — see below.)
font-default text-xs font-medium uppercase tracking-wider text-content-subtleTechnical value (mono)
The ONLY place mono belongs: IDs, API keys, hashes, versions, shortcuts, code — values read character-by-character.
font-mono text-xs text-content-defaultBadge sticker
Expressive pill labels (Badge component).
font-sticker text-xs font-semiboldDemo
What it looks like in the app
A composed slice of a dashboard, using the recipes above. One display heading (the page title); everything else — eyebrows, metric numbers, card titles, the table, the rank pill, the buttons — is Geist, with mono kept to the one technical value (the ad ID).
Overview
Top creatives
Spend
$12,450
Impressions
1,284,902
CTR
3.18%
Performance breakdown
| Creative | Spend | CTR | ROAS |
|---|---|---|---|
| Summer Sale 2026 | $4,210 | 3.9% | 4.1× |
| Back to School | $3,180 | 2.7% | 3.4× |
| Evergreen — UGC | $2,060 | 3.1% | 2.9× |
Summer Sale 2026
ad_120214… · 23877291
Rules
Do & don't
- Do
- Use semantic size/family utilities (
text-sm,font-display) — never inlinestylepx. - Keep the display face (
font-display) for the page / hero title only; section and card titles arefont-default. - Set numbers and metrics in Geist with
tabular-nums; reach forfont-boldto make a value read as important. - Set uppercase labels in
font-defaultwithtracking-wider(the Eyebrow component). - Sentence case in prose and headings.
- Don't
- Use
font-monofor eyebrows, headers or labels — mono is for IDs, keys, hashes, versions, code and shortcuts only. - Put section headers, card titles, metric numbers or pills in the display face — that re-imports the marketing ramp into dense UI.
- Reach for
text-md— it doesn't exist; the 16px step istext-base. - Expect
font-boldto look heavier thanfont-semiboldon display type — same single cut. - Use pure black; ink is the content-emphasis token.