Home/Sample reports/TYPOGRAPHY.md

Typography Audit — TYPOGRAPHY.md

A real report, written by 55 · Typography Audit, run against this repo — dogfood output, committed unedited. This is the artifact every brief ends in: findings that cite their evidence, ranked by severity, with a fix sketch each.

Read-only inventory of every way this product sets type, across the four style sources: template.html (landing), build.pySITE_CSS (detail pages), studio.html, and vitals.html. Every number below is grepped from those files; each carries its selector and line.

Headline: two families, sanely chosen, with genuinely good measure — but the size scale has accreted to ~27 distinct sizes, six of them separated by a single indistinguishable half-pixel, and the font economics run exactly backwards: the build ships and precaches a mono weight no rule uses (plexmono-500, 14.9 KB) while eleven selectors request a mono weight that isn't shipped (700 → faux bold). The deliverable is a smaller system.

Backlog reconciliation (2026-07-09). This audit predates the font redesign (Archivo → Schibsted Grotesk for display, IBM Plex Sans added for UI), so its family names and per-size line numbers are stale. Dispositions: T1 (mono @700 faux-bold) — FIXED: the three surviving --mono @700 badges retargeted to the shipped 600. T2 (unused plexmono-500) — ALREADY DONE: the 500 face was dropped in the redesign; only 400/600 ship. T4 (same role typed 3 ways) — FIXED: body line-height unified to one --lh-body token (was 1.6/1.62/1.5 across stylesheets). T3/T5 (font-size + line-height value sprawl) — FIXED (2026-07-09, round 3): every half-pixel size was folded to its nearest integer across all four style sources (69 declarations — 11.5/12.5→12, 13.5→13, 14.5→14, 15.5/16.5→16, 9.5→10, 10.5→11; each move ≤0.5px, imperceptible), so no size sits a half-pixel from its neighbor — the exact accretion T3 named. Near- duplicate line-heights were folded to their dominant neighbor (1.62/1.65→1.6, 1.55→1.5, 1.4→1.45, 1.03/1.04→1.05), 15→11 values; the survivors are deliberate per-role leadings (display .98/1.0, headings 1.05/1.15/1.22, sub 1.3/1.35, body 1.45/1.5/1.6, notes 1.7), not drift. The aggressive 5-step collapse (14/15/16→one 15) was declined: those full-pixel steps are perceptible and folding them would shift running body text — the defect the audit named was the indistinguishable half-steps, and those are gone. New minor issue found & FIXED: .drop-big requested --sans @700 where Plex Sans ships only 400/600 (a fresh faux-bold) — retargeted to --disp (Schibsted Grotesk, real 700). The Studio checkbox glyph at 700 is left as-is (an imperceptible synthesized weight on one symbol). See FIXLOG.md.


1 · The inventory (measured)

Families — 2, well-formed

TokenStackLoaded as
--sans'Archivo', system-ui, -apple-system, 'Segoe UI', sans-serifone variable woff2, font-weight:100 900 (build.py:316)
--mono'IBM Plex Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospacethree static woff2: 400/500/600 (build.py:317–319)

Defined identically in all four files (template.html:33, studio.html:32, vitals.html:30, build.py:324). Fallback stacks are metric-adjacent and sensible. font-display:swap on all 16 faces.

Font-size — ~27 distinct values (the problem)

Fixed px (18 values), with usage counts:

12px ×33   13px ×23   12.5px ×17   11.5px ×16   14px ×13   16px ×10
13.5px ×10 11px ×10   15px ×9      10.5px ×8    15.5px ×7   14.5px ×5
20px ×3    19px ×3    17px ×3      17.5px ×1    10px ×1     9.5px ×1

Fluid clamp() (8 ranges): 44/9vw/72 (×2, studio.html:56,vitals.html:54), 38/6vw/62 (template.html:86), 30/5.6vw/50 (build.py:376), 24/4vw/38 (template.html:74), 22/3.4vw/30 (build.py:384), 20/3vw/26 (build.py:486), 16/2.4vw/20 (build.py:377), 16/2.2vw/19 (template.html:88). Plus .88em for code (×2).

The tell-tale of accretion — six half-pixel pairs, each visually identical: 11/11.5, 12/12.5, 13/13.5, 14/14.5, 15/15.5, 17/17.5. There is no ratio; the 11–17.5px band holds 15+ separate decisions.

Font-weight — 6 real weights

700 ×27, 600 ×23, 800 ×10, 500 ×9, 900 ×5, 400 ×4. (The 100 ×4 hits are only the font-weight:100 900 bound in the four Archivo @font-face blocks — 100 is never applied to text.) All six render from the Archivo variable font. Mono weights requested: 400, 600, and 700 — see §4.

Line-height — 15 distinct values

1.5 ×13, 1.45 ×6, 1.7 ×2, 1.62 ×2, 1.6 ×2, .95 ×2, then singletons 1.65, 1.55, 1.4, 1.35, 1.3, 1.25, 1.05, 1.03, 1.0. The body role alone carries three: 1.6 (template.html:46), 1.62 (build.py:330), 1.5 (studio.html:36) — the three stylesheets have drifted.

Letter-spacing — 12 distinct values

Headings negative (-.01, -.015, -.02, -.03em), eyebrows/kickers positive (.1, .12, .14, .16em). Discipline is directionally right (tight display, tracked-out uppercase) but four near-identical negatives could be two.


2 · The proposed scale (map every value onto it)

Keep the two families and the fluid-heading idea. Collapse the fixed-size sprawl to five UI steps and the eight clamps to four. ~27 sizes → 9.

Fixed UI scale (≈1.18 ratio, no half-pixels):

StepSize / LHRoleAbsorbs today
t-micro11px / 1.4uppercase mono eyebrows, kickers, labels (tracking carries them)9.5, 10, 10.5, 11
t-meta12px / 1.45chips, meta rows, captions, code-in-UI11.5, 12, 12.5
t-sec13px / 1.5taglines, secondary body, helper text13, 13.5
t-body15px / 1.6UI base / running body14, 14.5, 15, 15.5, 16
t-sub18px / 1.3card titles, sub-heads (H3/H4)17, 17.5, 19, 20

Fluid headings (4 clamps):

StepClampRoleAbsorbs
t-ledeclamp(16px,2.4vw,20px) / 1.5ledes/subs16/2.2vw/19
t-h2clamp(22px,3.4vw,30px) / 1.05section H220/3vw/26, 24/4vw/38
t-titleclamp(30px,5.6vw,50px) / 1.03page/detail H1
t-displayclamp(38px,6vw,62px) / 1.0landing/hero display44/9vw/72 (unify Studio & Vitals onto this)

Weights: four is plenty — 400 body, 600 mono/UI emphasis, 700 sans strong, 900 display. Retire the sparse 500 (9 uses → fold into 600 or 400) and 800 (10 uses → fold into 700 or 900) unless a specific pairing needs them.

Deletions: ~13 fixed sizes, 4 clamps, ~10 line-heights, ~4 letter-spacing values, and (see §4) one whole font file. Nothing above changes what a reader perceives — the half-steps were never distinguishable.


3 · Findings (ranked by reading improved per change)

T1 — Faux bold: mono @700 requested, never shipped (most visible defect)

IBM Plex Mono ships only 400/500/600 (build.py:317–319, fonts/), yet eleven selectors set var(--mono) at font-weight:700, forcing the browser to synthesize bold — smeared, uneven stems, worst at the 11–13px sizes where these live: .nav-cta (template.html:60,build.py:344), .stepno (:246), .badge (template.html:166), .art .sev (:104), .pnode .dot (:129), .phase .step (build.py:397), .lens .i (:405), .seqstep .node (:466), .topnav .cta (studio.html:48, vitals.html:46). Fix: retarget all to 600 (shipped) — or ship a 700 face. Retargeting is the smaller system.

T2 — Font economics run backwards (free bytes)

plexmono-latin-500.woff2 (14,888 B) is declared (build.py:318), precached by the service worker (build.py:994sw.js), yet zero rules request mono @500 — every font-weight:500 in the codebase inherits sans (.eyebrow b, .quote .q, .hero .micro b, .empty b). So the build ships + downloads a face nobody renders, while omitting the 700 face eleven selectors ask for (T1). Drop the 500 face → reclaim ~14.9 KB (~18% of the ~80 KB font payload) with no visual change.

T3 — Half-pixel size accretion (scale has no spine)

The 11–17.5px band holds 15+ sizes including six 0.5px-apart pairs (11/11.5, 12/12.5, 13/13.5, 14/14.5, 15/15.5, 17/17.5; counts in §1). No two adjacent steps are distinguishable, so the "hierarchy" they imply doesn't read. Collapse to the five-step UI scale in §2.

T4 — The same role is typed three different ways across stylesheets

The three hand-authored stylesheets have drifted on shared roles:

  • Body line-height: 1.6 (template.html:46) vs 1.62 (build.py:330)

vs 1.5 (studio.html:36).

  • .badge weight: 700 (template.html:166) vs 600 (build.py:366).
  • Page-H1 display: clamp(38,6vw,62) (landing) vs clamp(44,9vw,72)

(Studio :56, Vitals :54) — same role, two sizes. One shared token block would end the drift.

T5 — Line-height sprawl (15 values, §1)

Fold to five roles: display 1.0, heading 1.05, sub 1.3, body 1.6, mono/code 1.5. Removes ~10 improvised values.

What's already good (leave it)

  • Measure is disciplined. Nearly every prose block is ch-constrained

inside the 45–75 target: .hero p.sub 52ch (:88), .sub 58ch (studio.html:58), .lead 60ch (:75) / 64ch (build.py:385), .prose 68ch (:386), .turn 70ch (:122), .dhero .lede 60ch (:377). Keep.

  • The neglected text isn't neglected: form labels (.ctxbody label

template.html:208), captions (.drop-small studio.html:73), code, and empty states are each deliberately styled, not inherited.

  • Fluid headings already form a real scale — just too many steps (§2).

4 · Font economics — loaded vs. used

FileBytesStatus
archivo-latin-var.woff234,928used — one variable file covers every sans weight (400/500/600/700/800/900). Efficient.
plexmono-latin-400.woff214,708used — mono body/labels default weight. Preloaded (build.py:536).
plexmono-latin-600.woff215,620used — mono emphasis (buttons, kickers, badges).
plexmono-latin-500.woff214,888UNUSED — declared + precached, requested by no rule. Reclaim.
(mono 700)MISSING — requested by 11 selectors (T1); currently faux-synthesized.

Total shipped ≈ 80.1 KB. Immediately reclaimable ≈ 14.9 KB by deleting the 500 face (drop @font-face build.py:318 + the precache entry build.py:994 + the file). Net after also fixing T1 (retarget 700→600): –14.9 KB and zero faux bold, no faces added. Only preload the two above-the-fold faces (already done: archivo-var + plexmono-400).


Report only — measured values only, nothing edited. The two highest-leverage, lowest-risk changes are T1 (retarget the 11 mono-700 selectors to 600 — kills all faux bold) and T2 (drop the unused plexmono-500 — reclaims ~14.9 KB); they're one small commit together. Which fixes should I make?