Design System
Typography
White Tree Digital runs on three finalized variable fonts — Archivo for display, Figtree for body, Aleo for mono — each self-hosted as a single variable WOFF2 per style, with the actual weight/width "cut" set per element through font-variation-settings rather than separate weight files.
That last detail is the mental model: you do not ship a regular/medium/bold file per family. You ship one variable file (plus its italic), and the design picks a precise point in the weight/width space per element in global.css. This keeps the whole type system to 6 files total and keeps the rendered weight under your control regardless of which Tailwind font-* class lands on the element.
This page covers the three families and their roles, the per-element cuts, the @theme tokens that name them, the self-hosting rules, and the one Tailwind sizing convention. For the color tokens that pair with type see Brand tokens & theme; for the broader Tailwind v4 rules see Tailwind v4; for font loading/preload mechanics see Images & fonts.
The three families
| Role | Family | Axes | Used for |
|---|---|---|---|
| Display | Archivo | wght 100–900, wdth 62–125 (declared as font-stretch %), + italic | Headlines, hero, wordmark, stats |
| Body | Figtree | wght 300–900, + italic | Paragraphs, nav, buttons |
| Mono | Aleo | wght 100–900, + italic | Eyebrows, section numbers, captions, meta |
These are finalized — the earlier brand-guidelines draft named Inter Tight (body) and IBM Plex Mono (mono); the live global.css is the source of truth and ships Figtree and Aleo instead. Aleo is a slab serif standing in for the "mono" role (eyebrows, numbers, meta labels); the token is still called --font-mono.
The @theme tokens
The families are named once in the @theme block of src/styles/global.css, which is what makes font-display / font-sans / font-mono available as Tailwind utilities:
--font-display: "Archivo", system-ui, sans-serif;
--font-sans: "Figtree", system-ui, sans-serif;
--font-mono: "Aleo", ui-monospace, monospace;
--font-sans is the document default — body sets font-family: var(--font-sans). Use font-display on headings/stats, font-mono on eyebrows and meta labels, and leave body text to inherit the sans default.
Per-element cuts via font-variation-settings
Because every family is one variable file, the specific weight/width each element renders at is set with font-variation-settings rules in global.css, not by loading a heavier file. Three cuts cover the whole site:
/* H1 + the wordmark get their own light, wide cut */
h1, .wordmark {
font-variation-settings: "wght" 445, "wdth" 120;
}
/* All other display headings + rich-text emphasis: one bolder, slightly narrower cut */
.font-display:not(h1, .wordmark),
h2, h3, h4, h5, h6,
.rich-body blockquote,
.rich-body--editorial p:first-of-type {
font-variation-settings: "wght" 700, "wdth" 107;
}
/* Body (Figtree) weight, inherited from <body> */
body {
font-variation-settings: "wght" 575;
}
So: H1 and the wordmark render Archivo at wght 445 / wdth 120 (light and wide); every other heading renders Archivo at wght 700 / wdth 107; body copy renders Figtree at wght 575. Eyebrows pick up their own weight from the .eyebrow rule (font-family: var(--font-mono); font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase).
font-variation-settings beats Tailwind font-* classes
font-variation-settings overrides the high-level font-weight, which is what Tailwind's font-medium / font-bold set. That means adding a font-bold class to body text or a heading does nothing visible — the variation-settings cut wins. If you genuinely need a different weight on an element, change (or add) a font-variation-settings rule; don't reach for a Tailwind weight utility and expect it to take. This is intentional: it locks each family to one cut per role so the type system stays consistent.
Tracking is left to the type scale: H1 already gets -0.04em from --text-h1--letter-spacing, and everything else keeps its own tracking-* utilities — the font-variation-settings rules touch weight/width only.
Self-hosted WOFF2 — no CDN, 6-file cap
Every face is self-hosted from website/public/fonts/ and declared with @font-face + font-display: swap. There is one variable WOFF2 per style (roman + italic) per family, so:
| Family | Roman file | Italic file |
|---|---|---|
| Archivo | archivo-vf.woff2 | archivo-italic-vf.woff2 |
| Figtree | figtree-vf.woff2 | figtree-italic-vf.woff2 |
| Aleo | Aleo-VF.woff2 | aleo-italic-vf.woff2 |
That is 6 files, the hard cap (enforced by tailwind.md and the performance budgets). The roman @font-face for Archivo, for reference:
@font-face {
font-family: "Archivo";
src: url("/fonts/archivo-vf.woff2") format("woff2");
font-weight: 100 900;
font-stretch: 62% 125%;
font-style: normal;
font-display: swap;
}
Note the font-weight range and the font-stretch range — that's how the single file exposes its full axis to CSS. (Figtree omits font-stretch because it has no width axis; Aleo's roman file is Aleo-VF.woff2 with a capital — match the real filename exactly.)
Never load these from Google Fonts
The fonts are self-hosted on purpose: no Google Fonts CDN, ever. The older Brand Guidelines.html shows Google <link> preconnects from a draft phase — those are historical, not the live setup. Adding a Google CDN <link> would defeat the self-hosting, add a third-party request against the performance budget, and is a privacy/consistency regression. If a new face is ever needed, add a self-hosted WOFF2 and stay within the 6-file cap.
Only the three roman files (Archivo, Figtree, Aleo) are rel="preload"-ed in Layout.astro because they're the only faces above the fold; the italics load on demand. See Images & fonts for the preload markup and the fonts.ready gate the hero animation waits on.
Sizing: text-{size}/{leading}, never leading-*
The fluid type scale is defined in @theme as clamp-based --text-h1 … --text-h6 tokens (320 → 1280px viewport), each with its own --text-h*--line-height and (for H1) --text-h*--letter-spacing. Because the line-height is baked into the size token, set leading with the modifier syntax, not a standalone utility:
<!-- Right: size carries its paired leading -->
<h2 class="text-h2/tight">…</h2>
<p class="text-base/relaxed">…</p>
<!-- Wrong: never use a standalone leading utility -->
<p class="text-base leading-relaxed">…</p>
Use text-{size}/{leading} and never leading-*. The scale itself (with pixel ranges per step) lives in the Tailwind v4 typography rules; the raw clamp() values live in global.css.
The hero headline is the one element that sizes outside the H1 token — .hero-headline uses its own larger clamp(2.99rem, 2.237rem + 3.767vw, 5.25rem) so it can push well past the page H1. See Hero headline animation.
Where this lives
website/src/styles/global.css— the source of truth: the six@font-facedeclarations, the--font-display/--font-sans/--font-mono@themetokens, thefont-variation-settingscuts (h1, .wordmark; the other-headings group;body), the fluid--text-h*scale, and the.eyebrowrule.website/public/fonts/— the six self-hosted variable WOFF2 files (archivo-vf,archivo-italic-vf,figtree-vf,figtree-italic-vf,Aleo-VF,aleo-italic-vf).website/src/layouts/Layout.astro— preloads the three roman fonts above the fold.website/CLAUDE.md— the "Typography" section: family/role table, 6-file cap, no-CDN rule, and thetext-{size}/{leading}convention.website/tailwind.md— the typography sizing rules and the file-count cap.website/Brand Guidelines.html— the visual type-scale specimen (Display → Eyebrow); note its family names and font<link>s predate the finalized self-hosted setup.