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

RoleFamilyAxesUsed for
DisplayArchivowght 100–900, wdth 62–125 (declared as font-stretch %), + italicHeadlines, hero, wordmark, stats
BodyFigtreewght 300–900, + italicParagraphs, nav, buttons
MonoAleowght 100–900, + italicEyebrows, 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:

FamilyRoman fileItalic file
Archivoarchivo-vf.woff2archivo-italic-vf.woff2
Figtreefigtree-vf.woff2figtree-italic-vf.woff2
AleoAleo-VF.woff2aleo-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-face declarations, the --font-display / --font-sans / --font-mono @theme tokens, the font-variation-settings cuts (h1, .wordmark; the other-headings group; body), the fluid --text-h* scale, and the .eyebrow rule.
  • 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 the text-{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.
Previous
Tailwind v4