feat(typography): bake IBM Plex Sans + Source Serif 4 into every tool

System-default font stack ('-apple-system, BlinkMacSystemFont, Segoe UI,
…') is the textbook generic admin-tool look. The tools have a real point
of view (engineering documents, traceability, immutability); the
typography should reflect that.

Picks:
  --font          → IBM Plex Sans (400 + 600). UI body text. Distinctive
                    engineering sans with tabular nums and proper figures.
  --font-display  → Source Serif 4 (600). Headings, page titles,
                    .app-header__title. Reads as "document" not "UI label."
  --font-mono     → unchanged. Platform mono fonts are already excellent
                    and engineering tools rarely benefit from a custom mono.

Wiring:
  - Raw .woff2 files live in shared/fonts/ (~60 KB total, latin subset,
    SIL OFL 1.1 — both families)
  - shared/fonts.css is base64-inlined data URIs for those three fonts
    (~80 KB after b64 overhead). Generated once from the snippet in
    shared/fonts/README.md.
  - Every tool's build.sh prepends shared/fonts.css before shared/base.css
    so @font-face is parsed before any rule references the family names.
  - Headings (h1-h6) and .app-header__title now use var(--font-display);
    .app-header__title bumped 17→18px and letter-spacing reset since the
    serif doesn't need the original sans-text tightening.
  - table/code/.tabular-nums get font-variant-numeric: tabular-nums so
    tracking-number columns align vertically.

"Ship the record player with the record": zero CDN dependency at render
time. Tools render identically offline and online. Per-tool dist sizes
grew by ~80 KB.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZDDC 2026-05-10 20:09:59 -05:00
parent 8be6c4d98b
commit 6260aa4860
15 changed files with 187 additions and 13 deletions

View file

@ -19,6 +19,7 @@ trap cleanup EXIT
# CSS files to concatenate in order # CSS files to concatenate in order
concat_files \ concat_files \
"../shared/fonts.css" \
"../shared/base.css" \ "../shared/base.css" \
"../shared/toast.css" \ "../shared/toast.css" \
"../shared/nav.css" \ "../shared/nav.css" \

View file

@ -21,6 +21,7 @@ trap cleanup EXIT
# is bundled because the markdown plugin uses Toast UI inside the # is bundled because the markdown plugin uses Toast UI inside the
# preview pane (.md files render as a full editor). # preview pane (.md files render as a full editor).
concat_files \ concat_files \
"../shared/fonts.css" \
"../shared/base.css" \ "../shared/base.css" \
"../shared/toast.css" \ "../shared/toast.css" \
"../shared/nav.css" \ "../shared/nav.css" \

View file

@ -19,6 +19,7 @@ trap cleanup EXIT
# CSS files to concatenate in order # CSS files to concatenate in order
concat_files \ concat_files \
"../shared/fonts.css" \
"../shared/base.css" \ "../shared/base.css" \
"../shared/toast.css" \ "../shared/toast.css" \
"../shared/nav.css" \ "../shared/nav.css" \

View file

@ -18,6 +18,7 @@ cleanup() { rm -f "$css_temp" "$js_raw" "$js_temp"; }
trap cleanup EXIT trap cleanup EXIT
concat_files \ concat_files \
"../shared/fonts.css" \
"../shared/base.css" \ "../shared/base.css" \
"../shared/toast.css" \ "../shared/toast.css" \
"../shared/nav.css" \ "../shared/nav.css" \

View file

@ -18,6 +18,7 @@ cleanup() { rm -f "$css_temp" "$js_raw" "$js_temp"; }
trap cleanup EXIT trap cleanup EXIT
concat_files \ concat_files \
"../shared/fonts.css" \
"../shared/base.css" \ "../shared/base.css" \
"../shared/toast.css" \ "../shared/toast.css" \
"../shared/nav.css" \ "../shared/nav.css" \

View file

@ -29,6 +29,7 @@ trap cleanup EXIT
# CSS files to concatenate in order # CSS files to concatenate in order
concat_files \ concat_files \
"css/tailwind-utils.css" \ "css/tailwind-utils.css" \
"../shared/fonts.css" \
"../shared/base.css" \ "../shared/base.css" \
"../shared/toast.css" \ "../shared/toast.css" \
"../shared/nav.css" \ "../shared/nav.css" \

View file

@ -35,9 +35,17 @@
/* Shape */ /* Shape */
--radius: 4px; --radius: 4px;
/* Typography */ /* Typography. --font-display covers headings (Source Serif 4 a refined
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; transitional serif that reads as "engineering / document / serious"
--font-mono: 'SF Mono', 'Fira Code', 'Consolas', 'Courier New', monospace; without being academic). --font is body UI text (IBM Plex Sans
distinctive engineering sans, with proper figures and tabular nums).
Both are base64-inlined via shared/fonts.css; system fallbacks kick in
when fonts.css isn't loaded (e.g. unbuilt component preview). --font-mono
stays as a system stack; engineering tools rarely benefit from a custom
mono and platform mono fonts are already excellent. */
--font-display: 'Source Serif 4', ui-serif, Charter, 'Iowan Old Style', Georgia, serif;
--font: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--font-mono: 'SF Mono', 'Fira Code', 'Consolas', 'Courier New', monospace;
} }
/* ── Dark mode tokens ─────────────────────────────────────────────────────── */ /* ── Dark mode tokens ─────────────────────────────────────────────────────── */
@ -103,8 +111,19 @@ html, body {
/* ── Typography ───────────────────────────────────────────────────────────── */ /* ── Typography ───────────────────────────────────────────────────────────── */
h1, h2, h3, h4, h5, h6 { h1, h2, h3, h4, h5, h6 {
font-family: var(--font-display);
font-weight: 600; font-weight: 600;
line-height: 1.2; line-height: 1.2;
/* Source Serif 4 has subtle optical sizing; let the browser opt in
where supported (modern Chromium/Firefox). */
font-optical-sizing: auto;
}
/* Tracking numbers and other engineering identifiers should align in
columns when stacked vertically. Apply tabular figures wherever we
render structured numeric data. */
table, .tabular-nums, code {
font-variant-numeric: tabular-nums;
} }
a { a {
@ -292,12 +311,14 @@ a:hover {
gap: 0.5rem; gap: 0.5rem;
} }
/* Tool name inside the header */ /* Tool name inside the header. Renders in the display serif so the
tool's identity reads as a document title, not a UI label. */
.app-header__title { .app-header__title {
font-size: 17px; font-family: var(--font-display);
font-size: 18px;
font-weight: 600; font-weight: 600;
color: var(--text); color: var(--text);
letter-spacing: 0.01em; letter-spacing: 0;
white-space: nowrap; white-space: nowrap;
} }

30
shared/fonts.css Normal file

File diff suppressed because one or more lines are too long

63
shared/fonts/README.md Normal file
View file

@ -0,0 +1,63 @@
# shared/fonts/
Source `.woff2` files for the typography baked into every tool. These are
the *raw* font bytes; the actual `@font-face` declarations live in
`shared/fonts.css` as base64 data URIs so single-file HTML tools render
identically offline (`file://`) and online with no CDN dependency.
## Files
- `ibm-plex-sans-400.woff2` — UI body text (regular weight)
- `ibm-plex-sans-600.woff2` — UI emphasis (semibold)
- `source-serif-4-600.woff2` — display/headings (semibold)
Latin subsets only. ~60 KB total raw; ~80 KB once base64-inlined.
## Regenerating `shared/fonts.css`
Run from the repo root:
```sh
python3 - <<'PY'
import base64, pathlib
fonts = [
('IBM Plex Sans', 400, 'ibm-plex-sans-400.woff2'),
('IBM Plex Sans', 600, 'ibm-plex-sans-600.woff2'),
('Source Serif 4', 600, 'source-serif-4-600.woff2'),
]
out = pathlib.Path('shared/fonts.css')
lines = ['/* shared/fonts.css — base64-inlined woff2. Generated by',
' * shared/fonts/README.md instructions; do NOT edit by hand. */', '']
for family, weight, fn in fonts:
b64 = base64.b64encode((pathlib.Path('shared/fonts') / fn).read_bytes()).decode('ascii')
lines += ['@font-face {',
f" font-family: '{family}';",
' font-style: normal',
f' font-weight: {weight};',
' font-display: swap;',
f" src: url(data:font/woff2;base64,{b64}) format('woff2');",
'}', '']
out.write_text('\n'.join(lines))
PY
```
## Adding or swapping a font
1. Download the new `.woff2` into this directory (latin subset only — keep
the bundle small).
2. Update the `fonts` list in the snippet above to include the new family
+ weight + filename.
3. Re-run the regen snippet.
4. Update `--font` or `--font-display` tokens in `shared/base.css`.
5. `./build` and verify every tool's `dist/*.html` still includes
`@font-face` (three by default).
## Sourcing
Originally downloaded from the `@fontsource` npm packages via jsdelivr:
- IBM Plex Sans 400/600: `@fontsource/ibm-plex-sans@5.0.18`
- Source Serif 4 600: `@fontsource/source-serif-4@5.0.5`
Both licenses are SIL Open Font License 1.1. Fontsource bundles only the
latin subset by default, which is exactly what we want.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -18,6 +18,7 @@ cleanup() { rm -f "$css_temp" "$js_raw" "$js_temp"; }
trap cleanup EXIT trap cleanup EXIT
concat_files \ concat_files \
"../shared/fonts.css" \
"../shared/base.css" \ "../shared/base.css" \
"../shared/toast.css" \ "../shared/toast.css" \
"../shared/nav.css" \ "../shared/nav.css" \

View file

@ -22,6 +22,7 @@ trap cleanup EXIT
# CSS files to concatenate in order # CSS files to concatenate in order
concat_files \ concat_files \
"../shared/fonts.css" \
"../shared/base.css" \ "../shared/base.css" \
"../shared/toast.css" \ "../shared/toast.css" \
"../shared/nav.css" \ "../shared/nav.css" \

File diff suppressed because one or more lines are too long