# DHK Design System

**Version 2.0.1 · Electric Cobalt · Light theme only**
Published at https://dhk.io/design-system.md · Source of truth: this file.

Supersedes the full root `design-system.md` (green `#16a34a` / Barlow / weight 300, pre-May 2026). The root path now contains only a compatibility stub. Any surface still using that palette is legacy — see [Exceptions](#exceptions).

Token tables in [Colour](#colour), [Typography](#typography), [Spacing](#spacing) and [Layout](#layout) are **generated from `src/styles/global.css`** by `scripts/gen-design-tokens.mjs`. Do not hand-edit between the `GENERATED` markers; change the CSS and re-run.

---

## Contents

1. [Principles](#principles)
2. [Voice and content](#voice-and-content)
3. [Colour](#colour)
4. [Typography](#typography)
5. [Spacing](#spacing)
6. [Layout](#layout)
7. [Elevation and borders](#elevation-and-borders)
8. [Motion](#motion)
9. [Interaction states](#interaction-states)
10. [Iconography](#iconography)
11. [Components](#components)
12. [Forms](#forms)
13. [Tables](#tables)
14. [Data visualisation](#data-visualisation)
15. [Prose and long-form](#prose-and-long-form)
16. [Responsive](#responsive)
17. [Print](#print)
18. [Accessibility](#accessibility)
19. [Component map](#component-map)
20. [Exceptions](#exceptions)
21. [Known drift](#known-drift)
22. [Never](#never)
23. [Changelog](#changelog)

---

## Principles

Five sentences that decide most arguments.

1. **The page is type and rules.** Structure comes from hairline borders and a three-step grey ladder, never from shadow, gradient, or illustration.
2. **One accent.** Electric Cobalt carries brand, action, and link. The other three accents are semantic labels, not decoration.
3. **Density over air.** Calm comes from specificity and rhythm, not from size. A big headline with one clause in a field of white is a billboard, not confidence.
4. **Restraint in motion.** Fast and short by default. Expressive easing is reserved for revealing data.
5. **Say what is missing.** Empty states name the gap and the thing that would fill it. Never pad a gap with a plausible placeholder.

---

## Voice and content

The layer most likely to drift, and the one that was previously undocumented.

**Register.** Declarative, compressed, confident. A claim, then a turn: *"Demos are impressive. Equipping is impactful."* State the idea and trust the reader.

**Person.** Mostly third person and imperative. Product pages address "you" directly. First person is rare and never chatty.

**Casing.** Sentence case for all prose and headings. **UPPERCASE only in mono chrome** — tags, nav, dates, section-divider labels, step numbers, button labels. Never uppercase a headline.

**Structure.** Problem → mechanism → implication. Essays open by naming a hidden assumption. Tools open with *"## The problem"* before *"## What it does."*

**Titles.** Either a plain noun phrase ("The Hidden Structure of Policy Networks") or a crisp opposition ("Showing vs. Shipping").

**Honesty tells.** Capabilities are *"working, not finished."* Hype gets deflated. Prefer *equipping* to *showing*.

**Punctuation.** The em dash and the mid-sentence period do the emphasis. No emoji, anywhere. Effectively no exclamation marks.

**Do not.** Italicise the punchline of a heading. Use "actually", "just", or "simply" — each is arguing with someone. Ask a rhetorical question in a heading. Compare the work to "most tools"; show the artifact instead.

---

## Colour

<!-- GENERATED:colour -->
**Backgrounds**

| Token | Value | Usage |
|---|---|---|
| `--bg` | `#f5f6fa` | Page background — near-white blue-grey |
| `--bg2` | `#eceef5` | Elevated surfaces, code blocks, input backgrounds |
| `--bg3` | `#e0e3f0` | Card hover state, inline code |

**Borders**

| Token | Value | Usage |
|---|---|---|
| `--border` | `#c8cde0` | Default borders, dividers |
| `--border-light` | `#d8dbeb` | Subdivision inside a bordered container |

**Text**

| Token | Value | Usage |
|---|---|---|
| `--text` | `#0d0f1a` | Headings, strong — darkest |
| `--text-muted` | `#2a2d42` | Body copy, descriptions — always lighter than --text |
| `--text-dim` | `#5a5d78` | Metadata, dates, labels, inactive nav |

**Accents**

| Token | Value | Usage |
|---|---|---|
| `--accent` | `#2b50e8` | Electric Cobalt — brand, CTA, hover borders |
| `--accent-hover` | `#1a3bd4` | hover state for accent elements |
| `--accent-purple` | `#8b2adc` | commentary tags, graph/network work |
| `--accent-orange` | `#e05c2a` | trend/time-series, project tags |
| `--accent-teal` | `#0ea5b0` | data viz, study tags |

**Deprecated aliases — do not use in new code**

| Token | Value | Usage |
|---|---|---|
| `--accent-blue` | `var(--accent-teal)` |  |
| `--text-head` | `var(--text)` |  |
<!-- /GENERATED:colour -->

**Rules.**

- Never hardcode a hex value outside `global.css`. The one sanctioned exception is a JS template literal that cannot read a CSS variable — and those must be the `--viz-*` values, commented as mirrors.
- `--text-muted` must always be visually lighter than `--text`.
- No new accent colours. The four-accent set is closed.
- Secondary accents are **semantic**: purple = commentary, tools, graph and network work · orange = trends, time series, projects · teal = studies, data visualisation.
- `--accent-blue` and `--text-head` are backward-compatibility aliases. Do not use them in new code.

**Tag tints** are 10–12% alpha of the semantic accent, paired with the accent at full strength as the text colour.

---

## Typography

<!-- GENERATED:type -->
**Font stacks**

| Token | Value | Usage |
|---|---|---|
| `--font-sans` | `system-ui, -apple-system, 'Segoe UI', sans-serif` | All headings and body prose |
| `--font-mono` | `'DM Mono', monospace` | UI chrome only — tags, dates, nav, labels, code |
<!-- /GENERATED:type -->

**Type scale.**

| Element | Size | Weight | Notes |
|---|---|---|---|
| h1 | `clamp(42px, 5vw, 64px)` | 700 | `letter-spacing: -0.02em`, `line-height: 1.1` |
| h2 | 28px | 600 | `-0.01em` |
| h3 | 20px | 600 | — |
| p | 16px | 400 | `line-height: 1.75` |
| `.meta`, dates | 12px | 400 | Mono, `0.04em` |
| `.tag` | 11px | 400 | Mono, uppercase, `0.08em` |
| nav links | 11px | 400 | Mono, uppercase, `0.1em` |
| button labels | 11px | 400 | Mono, uppercase, `0.1em` |

**Rules.**

- No custom heading font. No serif. No condensed. `--font-cond` survives only as an alias to `--font-sans`; new code should not reference it.
- `--font-mono` is UI chrome only — tags, dates, nav, labels, code, prompts. **Never body prose.**
- Body weight minimum 400. Weight 300 is never used for prose. DM Mono ships a 300 face; it is loaded for legacy reasons and should not be used.
- No UI text below 11px (WCAG 1.4.4).

---

## Spacing

<!-- GENERATED:spacing -->
**Scale (4px base)**

| Token | Value | Usage |
|---|---|---|
| `--sp1` | `4px` |  |
| `--sp2` | `8px` |  |
| `--sp3` | `12px` |  |
| `--sp4` | `16px` |  |
| `--sp5` | `24px` |  |
| `--sp6` | `32px` |  |
| `--sp7` | `48px` |  |
| `--sp8` | `64px` |  |
| `--sp9` | `80px` |  |
| `--sp10` | `96px` |  |
| `--sp11` | `128px` |  |
<!-- /GENERATED:spacing -->

Express spacing with flex/grid `gap` on the parent. **Never** use margins between sibling components — gap survives reordering and deletion; margins do not. Margins are for prose rhythm inside `.prose` only.

`--space-N` are backward-compatibility aliases for `--spN`. Do not use them in new code.

---

## Layout

<!-- GENERATED:layout -->
**Layout**

| Token | Value | Usage |
|---|---|---|
| `--max-width` | `1200px` | `.container` cap |
| `--header-height` | `56px` | Sticky header |
| `--radius` | `4px` | The only radius in the system |
<!-- /GENERATED:layout -->

`.container` — max-width wrapper, auto margins, `--sp5` side padding.

Page skeleton: `Base.astro` → `Header.astro` + `<main>` + `Footer.astro`.

Header is sticky, 56px, with `backdrop-filter: blur(8px)` over a translucent `--bg`. Nav order: Writing · Lab · Listen · Signal · About.

Content pages: `.post-header.container` (breadcrumb, h1, excerpt) then `.container.page-body.prose`.

---

## Elevation and borders

**There are no shadows.** Not subtle ones. Depth is signalled by surface colour and hairlines:

| Level | Surface | Used for |
|---|---|---|
| 0 | `--bg` | Page, and cards at rest |
| 1 | `--bg2` | Elevated panels, code blocks, input backgrounds, note strips |
| 2 | `--bg3` | Hover state, inline code |

Borders are always **1px** — `--border` for structure, `--border-light` for subdivision inside an already-bordered container. Dashed borders are for wireframes and annotation only, never in production UI.

**One radius: 4px** (`--radius`). Fully round corners are reserved for the 7px brand dot and 20×3px legend pills.

The **border-gap grid** is the house pattern for feature cells: a 1px grid `gap` over a parent whose background is `--border`, giving hairline dividers with no per-cell borders.

The only permitted blur in the system is the sticky header's `backdrop-filter`.

---

## Motion

```css
--motion-fast:   0.15s;                        /* colour, background, border-color */
--motion-reveal: 1s cubic-bezier(0.22, 1, 0.36, 1);  /* data reveals only */
```

- **Default:** `0.15s` on `color`, `background`, `border-color`. Nothing else animates on hover.
- **Data reveals:** the longer easing is for bars filling, graph edges drawing in, series entering. Not for UI.
- No bounce, no parallax, no scroll-jacking, no entrance animation on page load.
- Every animation must be suppressed under reduced motion:

```css
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
```

---

## Interaction states

Every interactive element defines all five.

| State | Treatment |
|---|---|
| Rest | `--bg` surface, `--text-dim` for chrome, `--text-muted` for body |
| Hover | Surfaces → `--bg3`. Primary buttons → `opacity: 0.85`. Ghost buttons and nav → accent border / darker text |
| Active (nav) | Accent colour **and** a 2px cobalt bottom border — never colour alone |
| Focus-visible | `2px solid var(--accent)` outline. `outline-offset: -2px` inside a surface, `3px` outside |
| Disabled | `--text-dim` text on `--bg2`, `cursor: not-allowed`, `opacity: 1` — dim it with tokens, not transparency |

No press-shrink, no transform on click.

**Loading.** A mono `--text-dim` label stating what is loading. No spinners, no skeleton shimmer.

**Empty.** One line naming what is absent and one naming the action that would fill it. Never a generic placeholder, never an illustration.

**Error.** `--accent-orange` for the label, plain sentence for the cause, and the next action. Errors are stated, not apologised for.

A global focus baseline belongs in `global.css`:

```css
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
:focus:not(:focus-visible) { outline: none; }
```

Every page needs a skip link to `<main>`, visually hidden until focused.

---

## Iconography

DHK is **near-iconless by design**. There is no icon font, no sprite, no Lucide or Heroicons dependency, and none should be added.

- **Unicode arrows stand in for icons.** `↗` for external and "follow". `→` for sequence and forward motion.
- **Ad-hoc inline SVG** only where a glyph is genuinely functional — the 8×8 play/pause triangles in podcast rows, the force-graph nodes. These are illustrative, not a reusable set.
- **Brand mark:** a 7px filled cobalt circle before the wordmark. DHK has no pictorial logo; the brand is the wordmark.
- If an icon is unavoidable, use a 1–1.5px stroke matching the hairline weight, and **flag the substitution** in the PR — it is not native to the system.

---

## Components

The site is content-first and defines these in CSS rather than as a component library. The canonical CSS lives in `global.css`; this section states intent and the rules a variant must not break.

### Card
Flat surface, no shadow, no heavy container. Bottom hairline for separation. Hover lifts to `--bg3`. Optional tag, date, title, excerpt. Focus-visible gets an inset cobalt outline and the hover surface.

### Tag
Mono, uppercase, 11px, `0.08em`, 4px radius, tinted by content kind. The tint and the text colour come from the same semantic accent.

### Section divider
A mono uppercase label on the left, a hairline rule filling the remaining width. Opens every section. `padding: --sp7 0 --sp6`, `border-top: 1px solid var(--border)`.

### Nav link
Dim at rest → `--text` on hover → cobalt with a 2px cobalt underline when active.

### Button — primary and ghost
Mono uppercase 11px, `0.1em`, `--sp3 --sp5` padding, 4px radius. Primary is solid cobalt on white text, hovering to `0.85` opacity. Ghost is a `--border` outline that becomes cobalt on hover. No third variant.

### Feature grid / feature card
The border-gap grid, `auto-fill` at a 220px minimum column. Card title is 13px 600 cobalt; body is 14px `--text-muted` at 1.6.

### Meta
Mono 12px `--text-dim`, `0.04em`. Dates, breadcrumbs, counts.

### Post header
Breadcrumb in cobalt mono, h1, excerpt at 17px `--text-dim`, closing hairline.

### Pull quote
`3px solid var(--accent)` left border, `--sp5` left padding, italic `--text-muted`.

### Prompt block
`--bg2` surface, `2px solid var(--accent)` left border, 4px radius, a mono uppercase `--text-dim` label, and the quoted text in mono at 14px. Used wherever the reader is given an exact sentence to type.

---

## Forms

Previously unspecified. Inputs sit on `--bg2` so they read as recessed, not raised.

```css
.input, .textarea, .select {
  font-family: var(--font-sans);
  font-size: 15px;
  color: var(--text);
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--sp3) var(--sp4);
  transition: border-color 0.15s;
}
.input:hover { border-color: var(--text-dim); }
.input:focus-visible { outline: 2px solid var(--accent); outline-offset: -1px; border-color: var(--accent); }
.input::placeholder { color: var(--text-dim); }
.input[aria-invalid="true"] { border-color: var(--accent-orange); }
.input:disabled { color: var(--text-dim); cursor: not-allowed; }
```

- **Label** above the field, mono uppercase 11px `--text-dim`. Never a placeholder as the label.
- **Help text** below, 13px `--text-muted`. **Error text** below, 13px `--accent-orange`, naming the fix.
- **Checkbox and radio:** 16px box, 1px `--border`, 4px radius (radio fully round), cobalt fill when checked, cobalt focus ring. Hit target 44px minimum including padding.
- Field groups use `gap`, never margins. Required fields are marked in the label, not by colour alone.

---

## Tables

Data studies need a specified table.

```css
.table { width: 100%; border-collapse: collapse; }
.table th {
  font-family: var(--font-mono); font-size: 11px; font-weight: 400;
  text-transform: uppercase; letter-spacing: 0.1em;
  color: var(--text-dim); text-align: left;
  padding: var(--sp3) var(--sp4);
  border-bottom: 1px solid var(--border);
}
.table td {
  font-size: 15px; color: var(--text-muted);
  padding: var(--sp4);
  border-bottom: 1px solid var(--border-light);
}
.table tbody tr:hover { background: var(--bg3); }
.table .num { font-family: var(--font-mono); text-align: right; font-variant-numeric: tabular-nums; }
```

- Header row is mono uppercase; body is sans. No zebra striping — hairlines do the work.
- Numeric columns are mono, right-aligned, tabular.
- No vertical rules. No borders around the table.
- A caption above the table, mono `--text-dim`, naming the source and the date.

---

## Data visualisation

The only imagery in the system is functional.

Six-country tokens for multi-series charts, mirrored as hex in JS where CSS variables cannot reach:

<!-- GENERATED:viz -->
**Six-country series**

| Token | Value | Usage |
|---|---|---|
| `--viz-us` | `#2b50e8` | United States  — cobalt (= --accent) |
| `--viz-se` | `#e8a030` | Sweden         — amber |
| `--viz-gb` | `#d94040` | United Kingdom — red |
| `--viz-fr` | `#0ea58a` | France         — teal |
| `--viz-jp` | `#9b42d8` | Japan          — purple |
| `--viz-in` | `#c47830` | India          — sienna |
<!-- /GENERATED:viz -->

- Line stroke 2px minimum; 2.5px for `--viz-se` to compensate for lower contrast.
- Grid lines `--border-light` at 1px. Axis labels mono 11px `--text-dim`.
- Legend pills 20×3px rounded, not square swatches.
- Series must be distinguishable without colour — direct labels at the line end, or dash patterns.
- Reveal animations use `--motion-reveal`, and are suppressed under reduced motion.

---

## Prose and long-form

Carried forward from v1; this rhythm was lost in the interim and is restored.

`.prose` is capped at **680px**.

| Element | Top margin | Bottom margin |
|---|---|---|
| `p` | — | `--sp5` |
| `h2` | `--sp7` | `--sp4` |
| `h3` | `--sp6` | `--sp3` |
| `ul`, `ol` | — | `--sp5` |
| `li` | — | `--sp2` |
| `blockquote` | `--sp6` | `--sp6` |
| `pre` | — | `--sp5` |

Inline code is mono 13px on `--bg3` in cobalt. Code blocks are `--bg2` with a `--border` outline. Links are cobalt, hovering to `--accent-hover`. Blockquotes take the pull-quote treatment.

---

## Responsive

Previously undocumented. Three breakpoints, mobile-last (the system is authored desktop-first with fluid type).

| Name | Width | Behaviour |
|---|---|---|
| `sm` | `≤ 640px` | Single column. Multi-column grids collapse to one. Nav becomes a single mono row, horizontally scrollable. Side padding `--sp4` |
| `md` | `641–1024px` | Two columns maximum. Three-column card grids become two. Sidebars stack under content |
| `lg` | `≥ 1025px` | Full layout at `--max-width` |

- Type scales fluidly via `clamp()`; do not add breakpoint-specific font sizes.
- Hit targets never below 44px at `sm`.
- Tables scroll horizontally inside a bordered container rather than reflowing.
- No layout may clip content at 1024px — test every fixed-width third column.

---

## Print

Every article and spec should print without a redesign.

```css
@media print {
  :root { --bg: #fff; --bg2: #fff; --bg3: #fff; }
  header, footer, nav, .no-print { display: none; }
  body { color: #000; }
  a { color: #000; text-decoration: underline; }
  a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 10pt; }
  .prose { max-width: none; }
  h2, h3 { break-after: avoid; }
  pre, blockquote, .table { break-inside: avoid; }
}
```

Body text prints at 11pt minimum. Backgrounds drop out; hairlines stay.

---

## Accessibility

- **Contrast.** Approved pairs: `--text` and `--text-muted` on `--bg`, `--bg2`, `--bg3`. `--accent` on `--bg` and `--bg2`. White on `--accent`. **Forbidden:** `--text-dim` on `--bg3` for body copy (it is for chrome only, at 11–12px, and even then check it), any accent on any other accent, `--accent-teal` or `--accent-orange` as body text.
- **Never colour alone.** Verdicts, series, and states carry a text label as well as a colour.
- No UI text below 11px.
- Focus-visible is mandatory and must never be removed. A skip link on every page.
- Every interactive element reachable and operable by keyboard, in visual order.
- Reduced motion respected globally.
- Images carry alt text; decorative SVG carries `aria-hidden="true"`.

---

## Component map

Keep this table current — it is what tells the next reader where a pattern lives.

| Pattern | CSS | Implementation |
|---|---|---|
| Base page | `global.css` resets | `src/layouts/Base.astro` |
| Header, nav | `.nav-link` | `src/components/Header.astro` |
| Footer | — | `src/components/Footer.astro` |
| Card | `.card` | used by writing / work / signal listings |
| Tag | `.tag`, `.tag-*` | listings and post headers |
| Section divider | `.section-divider` | every section opener |
| Container | `.container` | all pages |
| Meta | `.meta`, `.meta-link` | dates, breadcrumbs |
| Prose | `.prose` | `src/pages/**` content bodies |
| Feature grid | `.feature-grid`, `.feature-card` | product and tool pages |
| Charts | `--viz-*` | React islands in `src/components/*.tsx` |

React islands mount `client:load` when immediately interactive, `client:visible` when below the fold, and are styled through `:global()` in the parent `.astro` wrapper.

---

## Exceptions

Deliberately off-system surfaces. **These are not bugs. Do not "fix" them.**

| Surface | Palette | Why |
|---|---|---|
| Life Expectancy Explorer (`/work/life-expectancy-misconception`) | IBM Plex Mono, Cormorant Garamond, `#080808` base | Self-contained editorial piece with its own internal palette |
| Twine product page | Legacy green `#16a34a` + Barlow / Barlow Condensed | Predates the Electric Cobalt migration; not yet ported |
| Writing-techniques study (`/work/writing-techniques`) | Electric Cobalt, plus its own magnitude and polarity scales | Generated from dhk/alexandria by `npm run sync:study`; nothing here authors it, and a fix applied here is lost on the next sync. It restates the tokens as literals so the same file also opens standalone in the research repo, and its dense tables use 10.5px mono labels and a diagonal hatch for "no value published" |
| Seven Grids study (`/work/seven-grids`) | Toned paper, Instrument Serif / Newsreader / IBM Plex Mono, brass, and seven survey-plat inks | Generated from dhk/alexandria by `npm run sync:study`; nothing here authors it, and a fix applied here is lost on the next sync. The map is drawn in the idiom of a 19th-century hand-tinted survey plate, which is the argument of the piece rather than a styling choice — its 10px mono legend labels, hairline plate shadow and literal hex tokens all belong to that idiom. The source page is theme-aware because it also publishes as a standalone artifact; `sync-study.mjs` strips its dark-theme rules for this site and fails if any token would be left unresolved. |

Anything added to this list needs a one-line reason. Anything not on this list uses the tokens.

**Other repositories.** Surfaces outside this site (dhk.github.io, tricorder's pages) use the same system and the same linter, run from their own root. Their deliberate exceptions live in a `.design-system-exceptions` file in that repository, one path per line with a reason, not in this table. A page that cannot read `global.css` may restate a token with its exact value; see `scripts/README.md`.

---

## Known drift

Real disagreements between this document and the code, as of v2.0.

**Resolved — the code changes.**

1. **Active nav underline.** `.nav-link.active` gains a 2px cobalt bottom border. Colour alone is not an acceptable state signal — the accessibility rule in this document forbids it, and an underline is the only affordance that survives a monochrome print or a colour-vision difference. Apply to `global.css`:

   ```css
   .nav-link.active {
     color: var(--accent);
     border-bottom: 2px solid var(--accent);
     padding-bottom: 2px;
   }
   ```

2. **`--max-width` stays at 1200px.** The code is right and the v1 figure is the stale one. Nothing needs to reflow: every content measure is capped independently — `.prose` at 680px, post excerpts at 720px — so the container width only governs full-bleed grids and listings, which are fluid by construction. Changing it would reflow every page to fix a number nobody sees.

**Outstanding — mechanical.**

3. **Favicon.** `public/favicon.svg` remains a separate brand asset and is not token-generated.
4. **Legacy surfaces.** Twine and the Tab Tamer compatibility privacy page still use pre-v2 typography and colour. They are explicit checker exceptions above, not evidence that the main system is compliant.
5. **Deprecated aliases.** Remove remaining uses from legacy surfaces before removing aliases from `global.css`.

---

## Never

The review list. `scripts/lint-design-system.mjs` automatically checks the syntactic rules it can identify reliably; reviewers must still check sibling spacing, colour contrast, interaction coverage, and whether a new token is semantically justified.

- No hardcoded hex values outside `global.css` (except commented `--viz-*` mirrors in JS).
- No serif fonts.
- No condensed fonts. No `Barlow`.
- No font size below 11px for UI text.
- No `font-weight: 300` — and no `font-weight` below 400 for prose.
- No Tailwind classes.
- No `--font-mono` on body prose.
- No `--text-muted` darker than `--text`.
- No new accent colours.
- No `box-shadow`, no `linear-gradient`, no `radial-gradient`.
- No `border-radius` other than `4px`, `50%`, or a pill.
- No margins for spacing between sibling components — use `gap`.
- No `outline: none` without a replacement focus style.
- No `--accent-blue`, `--text-head`, `--space-N`, `--border-radius`, `--font-cond` in new code (deprecated aliases).
- No dark-mode media query. There is no dark theme.

---

## Changelog

**2.0.1 — 2026-10-10.** The linter can run against other repositories: per-repository exceptions in `.design-system-exceptions`, and exact token mirrors allowed where a page cannot read `global.css`. No rules changed.

**2.0 — merged 2026-08-07 ([PR #15](https://github.com/dhk/DHK-website/pull/15)).** Replaced the full root `design-system.md` with a compatibility stub; `public/design-system.md` is the single source of truth. Added voice and content, motion, interaction states, iconography, forms, tables, responsive, print, accessibility, component map, known drift. Restored the prose rhythm table and the exceptions register from v1. Token tables now generated from `global.css`. The `Never` list is now machine-checked.

**1.1 — May 2026.** Electric Cobalt migration. Green → cobalt, Barlow → system-ui, weight 300 → 400. Published at `public/design-system.md`. The root document was not updated and became the stale copy.

**1.0.** Green accent, Barlow and Barlow Condensed, warm greys, body weight 300.
