--- title: "Typography" subtitle: "The type system as web tokens" description: "How BlackDoctor's typefaces fill the design system's font slots on the web, how each is loaded, and the heading, body and label sizes the engine renders." author: "Studio" section: "Website" subsection: "Foundations" order: 2 status: "published" --- The brand typefaces are loaded once and assigned to the design system's font slots. The type scale itself — the sizes, the leading, the breakpoints at which headings step down — is the engine's and is not a brand decision; the brand supplies the faces and says which slot each fills. ## Font slots | Slot | Face | Loaded by | | --- | --- | --- | | `--font-primary` | Inter | A Google Fonts link the generator emits from `brand.json` | | `--font-secondary` | Erode | An `@font-face` rule in `theme.css` pointing at `assets/fonts/`, preloaded from `brand.json` | | `--font-tertiary` | Inter | Aliased to `--font-primary` in `theme.css`, because the engine's badge reads it | | `--font-quaternary` | engine default (a system monospace stack) | Not a brand slot; still dresses every `code`, `pre`, `kbd` and token tag | | `--font-display`, `--font-playful`, `--font-script` | engine defaults | Not brand slots; nothing on the site uses them | Headings read `--title-family`, which the theme points at `--font-secondary`, so every `h1` to `h6` renders in Erode. Body text and interface read `--font-primary`, with three engine exceptions: pull quotes and drop caps read `--font-secondary` (a `blockquote` is Erode italic, the one place the italic file loads), badges read `--font-tertiary`, and code reads `--font-quaternary`. Page code references the slots, never a family name. ## Web hierarchy Sizes are stepped rem values, shown here in pixels at desktop and at 959 px and below, where the headings step down one size. | Element | Size | Leading | | --- | --- | --- | | `h1` | 48 → 36 | 1.2 | | `h2` | 40 → 32 | 1.2 | | `h3` | 36 → 28 | 1.2 | | `h4` | 32 → 24 | 1.2 | | `h5` | 28 → 22 | 1.2 | | `h6` | 24 → 20 | 1.4 | | Body | 16 | 1.6 | | Lead (`--text-size-l`) | 20 | | | Small (`--text-size-s`) | 14 | | | `.eyebrow` | 14, uppercase, tracked 0.12em | 1.2 | Type follows browser zoom and the reader's font-size preference because every step is a rem value; there is no viewport-relative type, and no page writes its own breakpoint typography. ## Where the web differs from the design file - **Body is 16 px on the web, 20 px in the design file.** The 20 px setting is the lead paragraph, `--text-size-l`. - **Size names do not line up.** The design file calls 20 px `font-m` and 24 px `font-l`; the engine's `--font-m` is 18 px and `--font-l` is 20 px. Read the pixel value in an inspect panel, never the name. - **Eyebrow tracking is narrower on the web.** The design file tracks the 18 px eyebrow by 8 px (0.44em); `.eyebrow` renders at 14 px with 0.12em, because wider tracking wraps uppercase text at small sizes. - **The 94 px display size has no engine step.** The largest is `--font-10xl` at 72 px, reached through a display class rather than a heading element. ## Loading Inter arrives from Google Fonts through the `googleFontsUrl` in `cms/blackdoctor/brand.json`; the generator writes the link into every page head. Erode is self-hosted: the file sits in `assets/fonts/`, `theme.css` declares it with `@font-face`, and `brand.json` lists it under `fontPreload` so the browser fetches it before the stylesheet asks for it. The fallback stack is Georgia, so a page reads as a serif heading over sans text even before the file lands.