Koder Design — Typography

mandatory

Closed vocabulary of typeface roles, canonical CSS variables, hosting rules, weights, scale, fallback chain, and tests for typography in every Koder surface (web/landing, Flutter mobile/desktop, CLI/TUI). Self-hosted-first: webfonts ship as `.woff2` from the consumer's own origin; Google Fonts / jsDelivr / unpkg are forbidden.

Canonical typography for the Koder Stack and every Koder product UI. The set of typeface roles is closed; the implementations behind each role evolve (Wave 1 reuses Inter + JetBrains Mono; Wave 2 introduces Koder Display; Wave 3 ships proprietary Koder Sans + Koder Mono). References to "the body font" or "the code font" in any other spec or component MUST resolve to a role defined here, not to a specific typeface name.

Roles (closed vocabulary)

Role Wave 1 implementation Wave 2 target Wave 2 ETA Purpose
sans Inter Koder Sans 2027 UI body, controls, navigation, default everywhere
mono JetBrains Mono Koder Mono 2027 Code, terminal output, numeric tables, IDs, hashes
display Inter @ 700/800 Koder Display 2026-Q4 Headlines, hero text, posters, OG images
serif ui-serif, Georgia, serif (unchanged) Long-form reading mode (optional, opt-in per surface)

Wave 1 reuses upstream open-source typefaces under SIL Open Font License 1.1; no Koder-owned typeface ships yet. Wave 2 introduces the full Koder family (Sans + Mono + Display), designed in-house by the owner with AI assistance, shipped under OFL with Reserved Font Name. See meta/brand/koder-design#003-#030 for the roadmap; supersedes the earlier commissioned-foundry plan in projects/koder-stack#128.

Canonical CSS variables (KDS-prefixed)

Every Koder web surface declares these once at :root and resolves every font-family through them. Hard-coding the typeface name in component CSS is forbidden — components consume the variable so the Wave-2/3 swap is a single declaration change.

:root {
  --kds-font-sans:    'Inter', system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --kds-font-mono:    'JetBrains Mono', ui-monospace, "SF Mono", Menlo, Consolas, "DejaVu Sans Mono", monospace;
  --kds-font-display: var(--kds-font-sans);
  --kds-font-serif:   ui-serif, Georgia, "Times New Roman", serif;
}

body, button, input, select, textarea { font-family: var(--kds-font-sans); }
code, kbd, pre, samp, .mono           { font-family: var(--kds-font-mono); }
h1, h2, h3, .display                   { font-family: var(--kds-font-display); }

The legacy --kdr-* variables in tools/design-gen/assets/css/base.css remain for non-typeface tokens until the broader rename sweep; for typography always prefer --kds-font-*.

Hosting — self-hosted-first

  • Webfont files MUST ship as .woff2 from the consumer's own origin

    (e.g. kds.koder.dev/assets/fonts/, <product>.koder.dev/assets/fonts/).

  • @font-face declarations MUST point to a relative path on the same

    origin or to a Koder-owned *.koder.dev host. The following hosts are forbidden anywhere a Koder product loads: fonts.googleapis.com, fonts.gstatic.com, cdn.jsdelivr.net, unpkg.com, cdnjs.cloudflare.com, and every other third-party font CDN.

  • font-display: swap is mandatory (system fallback shows immediately

    while the webfont streams in — mitigates FOIT).

  • For pages that critically depend on the webfont (hero / above-fold

    text), the page shell MUST emit <link rel="preload" as="font" type="font/woff2" crossorigin>.

Rationale: privacy (policies/analytics.kmd forbids third-party beacons; Google Fonts is one), reliability (no third-party outage in the critical path), and policies/self-hosted-first.kmd G1.

Weights

Role Required weights Notes
sans 400 (body), 500 (medium), 600 (UI emphasis), 700 (heading) Variable font preferred when available
mono 400 (code), 700 (highlight) Variable font preferred
display 700, 800 Wave 2 may ship dedicated display face
serif 400, 700 Reader-mode only

A surface MAY skip an unused weight; it MUST NOT introduce weights outside this table without amending this spec.

Variable fonts

Variable single-file .woff2 (e.g. Inter VF, JetBrains Mono VF) is preferred over multiple static weight files: smaller total payload, single HTTP request, finer weight control. When a variable file is shipped, declare it with font-weight: 100 900; in @font-face and drop the per-weight static files from the same family.

Subsetting

  • Default subset: latin + latin-ext (covers en-US + pt-BR + every

    European Latin-script locale).

  • Extra scripts (cyrillic, greek, vietnamese, thai, …) ship as

    separate @font-face unicode-range declarations only when a product ships a locale that requires them; otherwise they MUST NOT ship.

  • Subset payload budget: ≤ 50 KB per weight per script. Larger

    payloads require an exception comment in the consumer's CSS.

Type scale

Use these px values for font-size; map to CSS custom properties when the surface has more than ~5 sizes.

12 (caption) · 14 (small) · 16 (body, base) · 18 (lead)
24 (h4) · 32 (h3) · 48 (h2) · 64 (h1 / display)

Line-heights:

  • 1.5 — body, paragraphs
  • 1.4 — controls, dense UI
  • 1.2 — headlines, display
  • 1.6 — long-form reading

Fallback chain — required

Every font-family declaration MUST end in a system fallback chain so a webfont failure degrades gracefully. The chains above (--kds-font-*) are the canonical chains; do not invent new ones per component.

Forbidden anywhere: Comic Sans MS, Papyrus, Times New Roman as a primary face (Times is OK only inside the serif fallback chain).

Tests

ID Test
T1 grep -rn 'fonts.googleapis|fonts.gstatic|jsdelivr.*font|unpkg.*font' <consumer>/ → empty
T2 Every font-family in CSSDarttempl resolves to a role in the table above or its fallback chain
T3 @font-face blocks include font-display: swap
T4 Hero/critical pages emit <link rel="preload" as="font" ...> for the body face
T5 Each shipped .woff2 ≤ 50 KB per subset/weight (or carries an exception comment)
T6 Variable face used when the upstream provides one (Inter VF / JetBrains Mono VF)
T7 AAA contrast preserved across the fallback chain (system fonts pass tools/a11y-test/)

Cross-surface integration

  • Web / landing / templ pages@font-face + --kds-font-* vars at

    :root (see tools/design-gen/assets/css/base.css for the canonical KDS implementation).

  • Flutter (koder_kit, mobile/desktop) — bundle the same .woff2

    files under assets/fonts/ and declare them in pubspec.yaml fonts: blocks; expose KdsTextStyles.sans/mono/display/serif helpers matching the role table.

  • JS (koderwebkit) — re-export the --kds-font-* values via

    KdsTokens.font.sans/mono/... for consumers that bypass CSS.

  • CLI/TUI — terminals use the OS default mono; no spec is needed

    beyond "rely on the user's terminal font."

  • Terminal emulators (kterm, …) — the same §CLI/TUI rule applies to

    the grid (the user's shell output): it follows the user's OS terminal font (e.g. GNOME monospace-font-name), NOT the mono role. The "terminal output" cell in the role table above is about Koder chrome rendering mono text (UI labels, code snippets, IDs, hashes, the badge) — that uses the mono role (JetBrains Mono). Forcing the role face into a terminal grid is a misread of the table; the grid is the user's font.

Roadmap

  • Wave 1 (now, 2026-05-11): Inter + JetBrains Mono self-hosted, OFL.

    Spec captured here; KDS landing reflects the canonical chains.

  • Wave 2 (2026-2027): Koder family — owner-led Sans + Mono +

    Display, designed in-house with AI assistance, OFL with Reserved Font Name. Implemented in three phases:

    • Phase 1 (4-6 months): Koder Display ships in --kds-font-display

      (headlines, hero, OG, splash). First visible ROI of the family.

    • Phase 2 (6-10 months): Koder Mono ships in --kds-font-mono,

      substituindo JetBrains Mono. Koda-optimized (disambiguation, programming ligatures, tabular figures).

    • Phase 3 (10-18 months): Koder Sans ships in --kds-font-sans,

      substituindo Inter. Workhorse body face, variable wght + opsz axes.

Tracked in meta/brand/koder-design/backlog/{pending,in-progress,done}/ tickets #003-#030. Supersedes the earlier commissioned-foundry plan in projects/koder-stack#128.

References

  • meta/docs/stack/policies/self-hosted-first.kmd G1 (no third-party origin in critical path)
  • meta/docs/stack/policies/analytics.kmd (no Google Fonts beacon)
  • meta/docs/stack/rfcs/design-RFC-001-koder-design-system.kmd
  • meta/brand/koder-design/backlog/pending/003-pivot-owner-led-typeface-family.kmd — Wave 2 pivot decision record
  • meta/brand/koder-design/backlog/pending/ — full Wave 2 roadmap (#003-#030)
  • projects/koder-stack/backlog/done/128-koder-display-commissioned-typeface-wave-2.md — superseded Wave 2 commission proposal
  • projects/koder-stack/backlog/pending/098-foundry-fonts-typography-store-scoping.md — the marketplace of fonts (distinct from this spec, which is the Koder Stack's own typography)
  • tools/design-gen/assets/css/base.css — canonical KDS web implementation
  • tools/design-gen/assets/fonts/ — hosted .woff2 files
  • meta/docs/stack/policies/headless-first.kmd R4 — text rendering in

    self-hosted fonts (Inter, JetBrains Mono) is golden-tested at exact match (tolerance 0.0), no per-run drift; a headless capture of glyph output must be bit-identical across runs (R5 determinism)