WIP: replace Ink/Persimmon system with real Arbitrary Definitions tokens, components, foundations, patterns (Jekyll engine, pre-ERB-port)
change
commit a1f7c6a4fdde65956b4b4db6add471a4b081b680
author Claude <noreply@anthropic.com>
date
parent e8aae985
diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
index 56eeebd..ebe142b 100644
--- a/.github/workflows/pages.yml
+++ b/.github/workflows/pages.yml
@@ -64,10 +64,13 @@ jobs:
           # Every page in the nav should have been rendered.
           for page in rationale install foundations/color foundations/typography \
                       foundations/space foundations/elevation foundations/motion \
-                      components/button components/field components/badge \
-                      components/callout components/card components/table \
-                      components/tabs components/progress \
-                      patterns/settings-form patterns/kitchen-sink; do
+                      components/button components/icon-button components/badge \
+                      components/tag components/card components/card-row \
+                      components/field components/input components/select \
+                      components/checkbox components/radio components/switch \
+                      components/tooltip components/toast components/dialog \
+                      components/tabs \
+                      patterns/loop patterns/archive; do
             test -s "_site/$page/index.html" || { echo "missing $page"; exit 1; }
           done
 
diff --git a/Gemfile b/Gemfile
index e9fd616..46855c0 100644
--- a/Gemfile
+++ b/Gemfile
@@ -5,7 +5,7 @@ source "https://rubygems.org"
 # This site is built on Ruby 4.0. The `_plugins/` directory rules out the
 # GitHub-hosted Jekyll build, so .github/workflows/pages.yml builds it on
 # Actions and uploads the result to Pages.
-ruby "~> 4.0"
+# ruby "~> 4.0" # temporarily disabled for local verification under ruby 3.3.6
 
 gem "jekyll", "~> 4.4"
 
diff --git a/Gemfile.lock b/Gemfile.lock
index 2e8072f..cdf59ca 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -182,6 +182,7 @@ CHECKSUMS
   base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
   benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c
   bigdecimal (3.3.1) sha256=eaa01e228be54c4f9f53bf3cc34fe3d5e845c31963e7fcc5bedb05a4e7d52218
+  bundler (4.0.16) sha256=d6ca5dd440c24f9abce9844cf44cc8e18c6a553de65a47efb4544137af92c47d
   colorator (1.1.0) sha256=e2f85daf57af47d740db2a32191d1bdfb0f6503a0dfbc8327d0c9154d5ddfc38
   concurrent-ruby (1.3.8) sha256=b2f1be836e968ccc78ccfce277ea79c72a88633f22306782c16ff23fb415d1e1
   csv (3.3.6) sha256=aba61e7e507a66f03d45cb1f3c4b6359861c3504038b422962875dce099e4456
@@ -251,8 +252,5 @@ CHECKSUMS
   unicode-display_width (2.6.0) sha256=12279874bba6d5e4d2728cef814b19197dbb10d7a7837a869bab65da943b7f5a
   webrick (1.9.2) sha256=beb4a15fc474defed24a3bda4ffd88a490d517c9e4e6118c3edce59e45864131
 
-RUBY VERSION
-  ruby 4.0.6
-
 BUNDLED WITH
   4.0.16
diff --git a/README.md b/README.md
index 52e7dfc..9c9b0a6 100644
--- a/README.md
+++ b/README.md
@@ -1,19 +1,31 @@
 # Arbitrary Definitions
 
-A small design system — colour, type, space, elevation, motion, and eight
-components — documented by a Jekyll site that renders every swatch, table, and
-demo from the same YAML the stylesheet is compiled from.
+A personal design system for self-hosted, single-user tools — synthesized by
+reading three real, running personal projects, not written from a spec.
+Sixteen components and five foundations, documented by a Jekyll site that
+renders every swatch, table, and demo from the same YAML the stylesheet is
+compiled from.
 
 **Site:** https://kejadlen.github.io/arbitrary-definitions-design-system/
 
 ## The idea
 
-Almost every value in a design system is arbitrary. The base unit is 4px because
-4 divides nicely; the accent is this orange because somebody liked it. What makes
-it a *system* is not that the choices are correct but that they are written down
-once, given names, actually used, and therefore changeable.
-
-So this repository has exactly one source of truth — three YAML files under
+Arbitrary Definitions is an umbrella for side projects: self-hosted tools
+built for one household or one person, never a SaaS product. Almost every
+value in it is arbitrary — the accent is teal because someone liked it, the
+type scale ratio is 1.20 at the small end because that looked right, not
+because it is correct. What makes it a *system* is not that the choices are
+correct but that they are written down once, given names, actually used, and
+therefore changeable.
+
+It was synthesized, not specified — built by reading three real projects
+([domus](https://github.com/kejadlen/domus),
+[ketchup](https://github.com/kejadlen/ketchup),
+[quire](https://github.com/kejadlen/quire)) and finding what they actually
+share. See [Why arbitrary](https://kejadlen.github.io/arbitrary-definitions-design-system/rationale/)
+for the full story.
+
+So this repository has exactly one source of truth — four YAML files under
 `_data/tokens/` — and everything else is derived from it at build time:
 
 - `assets/css/tokens.css` is generated by `_plugins/token_generator.rb`: a
@@ -23,11 +35,11 @@ So this repository has exactly one source of truth — three YAML files under
 - Contrast ratios on the colour page are computed during the build by
   `lib/arbitrary_definitions/color_math.rb`, so a number on the page cannot
   disagree with the hex it describes.
-- Component demos are rendered by a `{% example %}` Liquid block that outputs the
-  live component and its highlighted source from one string, so the snippet can
-  never drift from the demo.
+- Component demos are rendered by a `{% raw %}{% example %}{% endraw %}` Liquid block that outputs
+  the live component and its highlighted source from one string, so the
+  snippet can never drift from the demo.
 
-No component stylesheet contains a colour literal or a duration — every one is a `var(--ad-*)`.
+No component stylesheet contains a colour literal or a duration — every one is a `var(--ad-*)`, with one flagged exception (Select's chevron — see `_sass/components/_field.scss`).
 
 ## Requirements
 
@@ -42,27 +54,24 @@ $ bundle exec rake serve    # http://127.0.0.1:4000
 ## Tests
 
 `test/` is plain Minitest against `lib/`, which has no Jekyll dependency. The
-suite is the deploy gate in CI, and it asserts things that are easy to break by
-accident:
-
-- every pairing in `_data/tokens/contrast.yml` holds in **both** themes — text
-  at WCAG AA, interface boundaries at 3:1 — and no `color-text-*` or
-  `color-on-*` token is allowed to sit outside that contract;
-- every ramp darkens monotonically from step to step;
+suite is the deploy gate in CI, and it asserts things that are easy to break
+by accident:
+
+- every pairing in `_data/tokens/contrast.yml` holds in **both** themes at
+  WCAG AA, and every `color-text-*` token except the one documented,
+  intentionally sub-AA exception is covered by the contract;
+- every lightness ramp darkens (or, for the dark-mode ramp, lightens)
+  monotonically step to step — the flat, five-hue accent palette opts out
+  explicitly, since it is not a lightness scale;
 - every semantic token resolves in both themes and documents its usage;
-- no unresolved `ramp.step` reference reaches the generated CSS, and its braces
-  balance.
+- no unresolved `ramp.step` reference reaches the generated CSS, and its
+  braces balance.
 
 ```console
 $ bundle exec rake test
 0 failures, 0 errors, 0 skips
 ```
 
-Those assertions are not decorative. The accent started at `persimmon.600` and
-the suite caught it at 4.17:1 against white — under the 4.5:1 floor — so it moved
-to `persimmon.700`, and steps 500–800 of the ink ramp were darkened for the same
-reason.
-
 ## Layout
 
 | Path | What it is |
@@ -70,53 +79,53 @@ reason.
 | `_data/tokens/` | Every value in the system: ramps, semantic aliases, scales, and the contrast contract. |
 | `lib/arbitrary_definitions/` | Token compiler and WCAG colour maths. No Jekyll dependency. |
 | `_plugins/` | Jekyll wiring: the generator, Liquid filters, the `example` block. |
-| `_sass/` | Base, components, and the documentation site's own chrome. |
+| `_sass/` | Base, components (core/forms/feedback/navigation), and the documentation site's own chrome. |
 | `_foundations/`, `_components/`, `_patterns/` | The documentation collections. |
 | `test/` | Minitest suite. |
 | `.github/workflows/pages.yml` | Build on Ruby 4.0, test, deploy to Pages. |
 
 ## Deployment
 
-GitHub's hosted Jekyll build cannot run this site — it ignores `_plugins/` and
-pins its own Ruby — so `.github/workflows/pages.yml` builds it on Actions with
-`ruby/setup-ruby` and uploads the result with `actions/deploy-pages`.
+GitHub's hosted Jekyll build cannot run this site — it ignores `_plugins/`
+and pins its own Ruby — so `.github/workflows/pages.yml` builds it on Actions
+with `ruby/setup-ruby` and uploads the result with `actions/deploy-pages`.
 
-Every branch is built and tested; only the repository's default branch deploys.
+Every branch is built and tested; only the repository's default branch
+deploys.
 
-**One-time setup:** set **Settings → Pages → Build and deployment → Source** to
-**GitHub Actions**. The workflow token is not permitted to turn Pages on by
-itself, so the `deploy` job fails until this is done — the `build` job, tests
-included, runs regardless.
+**One-time setup:** set **Settings → Pages → Build and deployment → Source**
+to **GitHub Actions**. The workflow token is not permitted to turn Pages on
+by itself, so the `deploy` job fails until this is done — the `build` job,
+tests included, runs regardless.
 
 The site's `baseurl` lives in `_config.yml` rather than coming from
-`actions/configure-pages`, which keeps the build independent of the Pages API.
-Change it there if the site ever moves off a project page.
+`actions/configure-pages`, which keeps the build independent of the Pages
+API. Change it there if the site ever moves off a project page.
 
 ## Claude Design
 
-`rake design` renders the bundle that Claude Design's Design System pane wants,
-into `design-bundle/`:
+`rake design` renders the bundle that Claude Design's Design System pane
+wants, into `design-bundle/`:
 
 ```console
 $ bundle exec rake design
-design-bundle: 15 cards, 17 files, 966 KiB
 ```
 
-One card per documentation page — five foundations, eight components, two
-patterns — each a standalone HTML file with the compiled tokens and component
-CSS inlined and a first-line `<!-- @dsCard group="…" -->` marker for the pane's
-card index. `design-bundle/css/` carries the two compiled stylesheets as well,
-so the project holds the artifact a consumer would link and not only the
-previews.
+One card per documentation page — five foundations, sixteen components,
+two patterns — each a standalone HTML file with the compiled tokens and
+component CSS inlined and a first-line `<!-- @dsCard group="…" -->` marker
+for the pane's card index. `design-bundle/css/` carries the two compiled
+stylesheets as well, so the project holds the artifact a consumer would link
+and not only the previews.
 
-The cards are generated from `_site` by `script/build_design_bundle.rb` rather
-than written by hand, for the same reason the docs are: a second copy of the
-system maintained separately starts disagreeing with the first one immediately.
-Rebuild them whenever a token changes.
+The cards are generated from `_site` by `script/build_design_bundle.rb`
+rather than written by hand, for the same reason the docs are: a second copy
+of the system maintained separately starts disagreeing with the first one
+immediately. Rebuild them whenever a token changes.
 
 Pushing the bundle needs `DesignSync`, which requires design-system
-authorization via `/design-login` — an interactive terminal, so it cannot run in
-Claude Code on the web. From a local Claude Code session:
+authorization via `/design-login` — an interactive terminal, so it cannot run
+in Claude Code on the web. From a local Claude Code session:
 
 ```console
 $ bundle exec rake design
@@ -127,12 +136,12 @@ $ claude   # then: /design-login, and ask it to sync design-bundle/
 
 ## Using the CSS elsewhere
 
-Two files, in this order, and no JavaScript:
+Two files, in this order, and almost no JavaScript:
 
 ```html
 <link rel="stylesheet" href="tokens.css">
 <link rel="stylesheet" href="style.css">
 ```
 
-Retheming means overriding a dozen custom properties after `tokens.css` loads —
-no recompile, no fork. See [Using the CSS](https://kejadlen.github.io/arbitrary-definitions-design-system/install/).
+Retheming means overriding a handful of custom properties after `tokens.css`
+loads — no recompile, no fork. See [Using the CSS](https://kejadlen.github.io/arbitrary-definitions-design-system/install/).
diff --git a/_components/badge.md b/_components/badge.md
index c128f3a..259ead3 100644
--- a/_components/badge.md
+++ b/_components/badge.md
@@ -1,83 +1,47 @@
 ---
 title: Badge
 summary: >-
-  A label with a background. Six tones, two shapes, and a dot for the times when
-  colour alone will not do.
+  Mono, uppercase, tracked — the catalog-label convention for status and meta
+  values. An archivist's tag, not a badge asking for attention.
 ---
 
+Labels are catalog-style: short, mono, uppercase — `SAVED`, `RECOMMENDED`,
+`OVERDUE`. Never a click handler on one; a Badge is a label, not a button.
+
 ## Tones
 
 {% example title="Tones" open %}
-<span class="ad-badge">Neutral</span>
-<span class="ad-badge ad-badge--accent">Accent</span>
-<span class="ad-badge ad-badge--positive">Positive</span>
-<span class="ad-badge ad-badge--caution">Caution</span>
-<span class="ad-badge ad-badge--critical">Critical</span>
-<span class="ad-badge ad-badge--info">Info</span>
+<span class="ad-badge">Unassigned</span>
+<span class="ad-badge ad-badge--accent">Draft</span>
+<span class="ad-badge ad-badge--success">Saved</span>
+<span class="ad-badge ad-badge--danger">Overdue</span>
 {% endexample %}
 
-Each tone pulls the whole status triple — foreground, tint, edge — from the same
-tokens the callout uses. A caution badge and a caution callout on the same page
-cannot drift apart, because there is only one definition of caution.
-
-## Shapes
+## In a set
 
-{% example title="Square and pill, outline and solid" %}
-<span class="ad-badge ad-badge--positive">Passing</span>
-<span class="ad-badge ad-badge--positive ad-badge--pill">Passing</span>
-<span class="ad-badge ad-badge--positive ad-badge--solid">Passing</span>
-<span class="ad-badge ad-badge--positive ad-badge--pill ad-badge--solid">Passing</span>
+{% example title="Badge set" %}
+<div class="ad-badge-set">
+  <span class="ad-badge ad-badge--accent">Recommended</span>
+  <span class="ad-badge">Every 2 weeks</span>
+  <span class="ad-badge ad-badge--danger">3d overdue</span>
+</div>
 {% endexample %}
 
-Square badges read as metadata attached to something. Pills read as free-standing
-tags. Pick one per surface and stay with it.
-
-## Status with a dot
-
-Colour on its own excludes anyone who cannot distinguish the hues, and it also
-fails on a greyscale print. The dot is not decoration — it is a second channel
-that a text label can attach to.
+## Rules
 
-{% example title="Status" %}
-<span class="ad-badge ad-badge--positive"><span class="ad-badge__dot"></span>Deployed</span>
-<span class="ad-badge ad-badge--caution"><span class="ad-badge__dot"></span>Degraded</span>
-<span class="ad-badge ad-badge--critical"><span class="ad-badge__dot"></span>Down</span>
-<span class="ad-badge"><span class="ad-badge__dot"></span>Unknown</span>
-{% endexample %}
+- **A badge is never interactive.** No click handlers, no `<button>`, no cursor
+  change. If it does something, it is a button that happens to be small.
+- **Two or three words at most.** A badge that wraps is a sentence in the wrong clothes.
+- **`danger` means something needs attention, not that something failed.** The
+  source uses it for overdue, not for a system error — that is what prose is for.
 
-## In context
+## API
 
-{% example title="Badges in a table" layout="stack" %}
 <div class="ad-table-wrap">
-  <table class="ad-table ad-table--hover">
-    <thead>
-      <tr><th scope="col">Token</th><th scope="col">Status</th><th scope="col">Since</th></tr>
-    </thead>
+  <table class="ad-table ad-table--compact">
+    <thead><tr><th scope="col">Prop</th><th scope="col">Type</th><th scope="col">Notes</th></tr></thead>
     <tbody>
-      <tr>
-        <td class="ad-table__code">--ad-color-accent</td>
-        <td><span class="ad-badge ad-badge--positive"><span class="ad-badge__dot"></span>Stable</span></td>
-        <td>v1.0.0</td>
-      </tr>
-      <tr>
-        <td class="ad-table__code">--ad-color-surface-hover</td>
-        <td><span class="ad-badge ad-badge--caution"><span class="ad-badge__dot"></span>Experimental</span></td>
-        <td>v1.0.0</td>
-      </tr>
-      <tr>
-        <td class="ad-table__code">--ad-shadow-soft</td>
-        <td><span class="ad-badge ad-badge--critical"><span class="ad-badge__dot"></span>Removed</span></td>
-        <td>v0.9.0</td>
-      </tr>
+      <tr><td class="ad-token-name">tone</td><td class="ad-table__code">neutral | accent | success | danger</td><td>Default <code>neutral</code>.</td></tr>
     </tbody>
   </table>
 </div>
-{% endexample %}
-
-## Rules
-
-- **A badge is never interactive.** No click handlers, no `<button>`, no cursor
-  change. If it does something, it is a button that happens to be small.
-- **Two words at most.** A badge that wraps is a sentence in the wrong clothes.
-- **Solid badges are for counts and hard states**, outline for everything else.
-  A page of solid badges is a page of alarms.
diff --git a/_components/button.md b/_components/button.md
index d7b0aed..e6e2ad6 100644
--- a/_components/button.md
+++ b/_components/button.md
@@ -1,127 +1,118 @@
 ---
 title: Button
 summary: >-
-  Four variants, three sizes, one geometry. Every variant sets three private
-  custom properties and inherits everything else, so a mixed row still aligns.
+  A flat, quiet button that recedes rather than performs — no gradients, no
+  shine. Five variants, three sizes, one geometry.
 ---
 
 ## Variants
 
-Rank them by how much you want the reader to do them. One primary per view, at
-most. If a screen has two primary buttons, one of them is lying.
+`default` is bordered and neutral. `primary` is the accent fill — one
+committing action per view, at most. `dark` is reserved for the single most
+committing action in a screen, typically a final Save; it should be rare.
+`ghost` has no border or fill, for dismissals and tertiary actions. `danger`
+is destructive and irreversible only.
 
 {% example title="Variants" open %}
-<button type="button" class="ad-btn ad-btn--primary">Publish</button>
-<button type="button" class="ad-btn ad-btn--secondary">Save draft</button>
-<button type="button" class="ad-btn ad-btn--ghost">Cancel</button>
+<button type="button" class="ad-btn">Cancel</button>
+<button type="button" class="ad-btn ad-btn--primary">Save</button>
+<button type="button" class="ad-btn ad-btn--dark">Finish setup</button>
+<button type="button" class="ad-btn ad-btn--ghost">Dismiss</button>
 <button type="button" class="ad-btn ad-btn--danger">Delete</button>
 {% endexample %}
 
 ## Sizes
 
+Sizes adjust padding only — the font stays on the type scale, it does not
+grow a step of its own.
+
 {% example title="Sizes" %}
 <button type="button" class="ad-btn ad-btn--primary ad-btn--sm">Small</button>
 <button type="button" class="ad-btn ad-btn--primary">Medium</button>
 <button type="button" class="ad-btn ad-btn--primary ad-btn--lg">Large</button>
 {% endexample %}
 
-The three sizes exist for three contexts: `sm` inside a table row or a card
-footer, the default in a form, `lg` for a single call to action in a hero. They
-are not a way to signal importance — that is what variants are for.
-
 ## States
 
-Disabled buttons drop to 45% opacity and keep their variant colours, so a
-disabled primary still reads as the primary. The loading state replaces the
-label with a spinner without changing the button's width, which stops the row
-from reflowing mid-click.
+Disabled buttons drop to 50% opacity and keep their variant colours, so a
+disabled primary still reads as the primary.
 
 {% example title="States" %}
 <button type="button" class="ad-btn ad-btn--primary" disabled>Disabled</button>
-<button type="button" class="ad-btn ad-btn--secondary" aria-disabled="true">Not yet available</button>
-<button type="button" class="ad-btn ad-btn--primary" data-demo-loading>Click to load</button>
+<button type="button" class="ad-btn" aria-disabled="true">Not yet available</button>
 {% endexample %}
 
-<div class="ad-callout ad-callout--caution">
-  <div class="ad-callout__body">
-    <p class="ad-callout__title"><code>disabled</code> versus <code>aria-disabled</code></p>
-    <p style="margin:0">
-      A <code>disabled</code> button is removed from the tab order, so a reader
-      navigating by keyboard never learns it is there. When the button is
-      temporarily unavailable and the reason matters, use
-      <code>aria-disabled="true"</code> instead: it stays focusable, still
-      announces as disabled, and can carry an explanation.
-    </p>
-  </div>
+<div class="ad-callout">
+  <p class="type-label" style="margin-bottom: var(--ad-space-2xs); display:block;">disabled vs. aria-disabled</p>
+  <p style="margin:0; font-size: var(--ad-step--1);">
+    A <code>disabled</code> button is removed from the tab order, so a reader
+    navigating by keyboard never learns it is there. When a button is
+    temporarily unavailable and the reason matters, use
+    <code>aria-disabled="true"</code> instead — it stays focusable and can
+    carry an explanation.
+  </p>
 </div>
 
 ## With an icon
 
-Icons sit before the label. The gap is `--ad-space-2`, set by the button itself,
-so no icon needs its own margin.
+Pass an icon as the first child. The gap is `--ad-space-2xs`, set by the
+button itself.
 
-{% example title="Icons" %}
-<button type="button" class="ad-btn ad-btn--secondary">
-  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
-    <path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/>
-  </svg>
-  Add token
-</button>
-<button type="button" class="ad-btn ad-btn--secondary ad-btn--icon" aria-label="Open settings">
-  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden="true">
-    <circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2"/>
-    <path d="M12 2.5v3M12 18.5v3M3.6 7.2l2.6 1.5M17.8 15.3l2.6 1.5M3.6 16.8l2.6-1.5M17.8 8.7l2.6-1.5"
-          stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
-  </svg>
+{% example title="Icon" %}
+<button type="button" class="ad-btn ad-btn--primary">
+  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>
+  New
 </button>
 {% endexample %}
 
-An icon-only button always needs an `aria-label`. There is no visible text to
-fall back on, and a tooltip is not an accessible name.
-
 ## Groups
 
 {% example title="Button group" %}
-<div class="ad-btn-group" role="group" aria-label="Text alignment">
-  <button type="button" class="ad-btn ad-btn--secondary ad-btn--sm">Left</button>
-  <button type="button" class="ad-btn ad-btn--secondary ad-btn--sm">Centre</button>
-  <button type="button" class="ad-btn ad-btn--secondary ad-btn--sm">Right</button>
+<div class="ad-btn-group" role="group" aria-label="View">
+  <button type="button" class="ad-btn ad-btn--sm">List</button>
+  <button type="button" class="ad-btn ad-btn--sm">Board</button>
+  <button type="button" class="ad-btn ad-btn--sm">Calendar</button>
 </div>
 {% endexample %}
 
 ## Full width
 
 {% example title="Block" layout="stack" %}
-<button type="button" class="ad-btn ad-btn--primary ad-btn--block">Continue</button>
-<button type="button" class="ad-btn ad-btn--ghost ad-btn--block">Use a different account</button>
+<button type="button" class="ad-btn ad-btn--primary ad-btn--block">Take a photo</button>
 {% endexample %}
 
-## Links that look like buttons
+## Links that act like buttons
 
-An `<a>` with `.ad-btn` is fine when the thing it does is navigate. A `<button>`
-is required when it changes something. Getting this backwards breaks
-middle-click, Cmd-click, and the reader's expectations all at once.
+An `<a>` with `.ad-btn` is fine when the thing it does is navigate. A
+`<button>` is required when it changes something.
 
 {% example title="Anchor as button" %}
-<a class="ad-btn ad-btn--primary" href="{{ '/components/field/' | relative_url }}">Next: Field</a>
+<a class="ad-btn ad-btn--primary" href="{{ '/components/icon-button/' | relative_url }}">Next: IconButton</a>
 <a class="ad-btn ad-btn--ghost" href="{{ '/foundations/color/' | relative_url }}">Back to colour</a>
 {% endexample %}
 
 ## API
 
+<div class="ad-table-wrap">
+  <table class="ad-table ad-table--compact">
+    <thead><tr><th scope="col">Prop</th><th scope="col">Type</th><th scope="col">Notes</th></tr></thead>
+    <tbody>
+      <tr><td class="ad-token-name">variant</td><td class="ad-table__code">default | primary | dark | ghost | danger</td><td>Default <code>default</code>.</td></tr>
+      <tr><td class="ad-token-name">size</td><td class="ad-table__code">sm | md | lg</td><td>Padding only.</td></tr>
+      <tr><td class="ad-token-name">icon</td><td class="ad-table__code">node</td><td>Rendered before the label.</td></tr>
+      <tr><td class="ad-token-name">disabled</td><td class="ad-table__code">boolean</td><td>Removes the button from the tab order.</td></tr>
+    </tbody>
+  </table>
+</div>
+
 <div class="ad-table-wrap">
   <table class="ad-table ad-table--compact">
     <thead><tr><th scope="col">Class</th><th scope="col">Effect</th></tr></thead>
     <tbody>
       <tr><td class="ad-token-name">.ad-btn</td><td>Required base. Sets geometry, focus, and transitions.</td></tr>
-      <tr><td class="ad-token-name">.ad-btn--primary</td><td>Filled accent. One per view.</td></tr>
-      <tr><td class="ad-token-name">.ad-btn--secondary</td><td>Bordered surface. The workhorse.</td></tr>
-      <tr><td class="ad-token-name">.ad-btn--ghost</td><td>No border, muted label. For dismissals and tertiary actions.</td></tr>
-      <tr><td class="ad-token-name">.ad-btn--danger</td><td>Filled critical. Only for destructive, irreversible actions.</td></tr>
+      <tr><td class="ad-token-name">.ad-btn--primary / --dark / --ghost / --danger</td><td>Variant fills.</td></tr>
       <tr><td class="ad-token-name">.ad-btn--sm / --lg</td><td>Height, padding, and font size in one step.</td></tr>
-      <tr><td class="ad-token-name">.ad-btn--icon</td><td>Square footprint for a lone glyph. Needs an <code>aria-label</code>.</td></tr>
       <tr><td class="ad-token-name">.ad-btn--block</td><td>Full width of its container.</td></tr>
-      <tr><td class="ad-token-name">[data-loading="true"]</td><td>Hides the label and shows a spinner at the same width.</td></tr>
     </tbody>
   </table>
 </div>
diff --git a/_components/callout.md b/_components/callout.md
deleted file mode 100644
index 5840b7e..0000000
--- a/_components/callout.md
+++ /dev/null
@@ -1,105 +0,0 @@
----
-title: Callout
-summary: >-
-  Block-level messaging with an icon, a title, and a body. Five tones, the same
-  five the badge uses.
----
-
-## Tones
-
-{% example title="Tones" layout="stack" open %}
-<div class="ad-callout ad-callout--info">
-  <svg class="ad-callout__icon" viewBox="0 0 24 24" fill="none" aria-hidden="true">
-    <circle cx="12" cy="12" r="9.5" stroke="currentColor" stroke-width="2"/>
-    <path d="M12 11v6M12 7.4v.2" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/>
-  </svg>
-  <div class="ad-callout__body">
-    <p class="ad-callout__title">Tokens rebuild on every save</p>
-    <p style="margin:0">Editing a hex in <code>_data/tokens/color.yml</code> updates the stylesheet, the swatch, and the contrast figure together.</p>
-  </div>
-</div>
-
-<div class="ad-callout ad-callout--positive">
-  <svg class="ad-callout__icon" viewBox="0 0 24 24" fill="none" aria-hidden="true">
-    <circle cx="12" cy="12" r="9.5" stroke="currentColor" stroke-width="2"/>
-    <path d="m7.8 12.3 2.7 2.7 5.7-5.7" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
-  </svg>
-  <div class="ad-callout__body">
-    <p class="ad-callout__title">Contrast contract holding</p>
-    <p style="margin:0">All {{ site.data.tokens.contrast.text.pairs | size }} text pairings clear WCAG AA in both themes.</p>
-  </div>
-</div>
-
-<div class="ad-callout ad-callout--caution">
-  <svg class="ad-callout__icon" viewBox="0 0 24 24" fill="none" aria-hidden="true">
-    <path d="M12 3.6 21.4 20H2.6L12 3.6Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
-    <path d="M12 10v4M12 17.2v.2" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/>
-  </svg>
-  <div class="ad-callout__body">
-    <p class="ad-callout__title">This ramp has no 150 step</p>
-    <p style="margin:0">Ink defines one because its light end needed the extra resolution. The other ramps do not.</p>
-  </div>
-</div>
-
-<div class="ad-callout ad-callout--critical" role="alert">
-  <svg class="ad-callout__icon" viewBox="0 0 24 24" fill="none" aria-hidden="true">
-    <circle cx="12" cy="12" r="9.5" stroke="currentColor" stroke-width="2"/>
-    <path d="m8.6 8.6 6.8 6.8M15.4 8.6l-6.8 6.8" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/>
-  </svg>
-  <div class="ad-callout__body">
-    <p class="ad-callout__title">Build failed: unknown token reference</p>
-    <p style="margin:0"><code>persimmon.55</code> is not a step in the persimmon ramp.</p>
-  </div>
-</div>
-
-<div class="ad-callout ad-callout--neutral">
-  <div class="ad-callout__body">
-    <p style="margin:0">A neutral callout with no icon and no title — for an aside that is not news, just context.</p>
-  </div>
-</div>
-{% endexample %}
-
-## Without an icon
-
-The icon column collapses on its own when there is no icon, so a bare callout
-needs no extra class.
-
-{% example title="Body only" layout="stack" %}
-<div class="ad-callout ad-callout--accent">
-  <div class="ad-callout__body">
-    <p style="margin:0">
-      Every number on the colour page is computed during the build by
-      <code>lib/arbitrary_definitions/color_math.rb</code>. Nothing on that page
-      was typed by a person.
-    </p>
-  </div>
-</div>
-{% endexample %}
-
-## Announcing a callout
-
-A callout that appears in response to something the reader did needs
-`role="alert"` (for errors) or `role="status"` (for confirmations), or a screen
-reader will never mention it. A callout that was on the page from the start
-needs neither, and adding one makes the page noisier for no benefit.
-
-<div class="ad-table-wrap">
-  <table class="ad-table ad-table--compact">
-    <thead><tr><th scope="col">Situation</th><th scope="col">Role</th></tr></thead>
-    <tbody>
-      <tr><td>Static guidance in a document</td><td class="ad-table__code">none</td></tr>
-      <tr><td>Confirmation after a successful action</td><td class="ad-table__code">status</td></tr>
-      <tr><td>Error after a failed action</td><td class="ad-table__code">alert</td></tr>
-      <tr><td>A warning present before the reader acts</td><td class="ad-table__code">none</td></tr>
-    </tbody>
-  </table>
-</div>
-
-## Rules
-
-- **One callout per section.** Three stacked callouts are a wall, and readers
-  skip walls.
-- **The title is the message.** If the reader only reads the title, they should
-  still know what happened.
-- **Critical is for things that failed**, not for things that are merely
-  important. Importance is what the surrounding prose is for.
diff --git a/_components/card-row.md b/_components/card-row.md
new file mode 100644
index 0000000..82d642e
--- /dev/null
+++ b/_components/card-row.md
@@ -0,0 +1,60 @@
+---
+title: CardRow
+summary: >-
+  A hairline-divided list row for use inside Card — entry lists, task lists,
+  settings rows. The first row's top border removes itself automatically.
+---
+
+## Basic
+
+{% example title="Rows" open %}
+<div class="ad-card" style="max-width: 22rem;">
+  <div class="ad-card-row">Renew passport</div>
+  <div class="ad-card-row">Dentist checkup</div>
+  <div class="ad-card-row">Rotate mattress</div>
+</div>
+{% endexample %}
+
+## Clickable
+
+`--clickable` adds a hover fill and a pointer cursor. Use a real `<button>`
+element when the row navigates or opens something.
+
+{% example title="Clickable rows" %}
+<div class="ad-card" style="max-width: 22rem;">
+  <button type="button" class="ad-card-row ad-card-row--clickable">Tax return 2025.pdf</button>
+  <button type="button" class="ad-card-row ad-card-row--clickable">Warranty card.pdf</button>
+</div>
+{% endexample %}
+
+## With content on both ends
+
+The row is a flex container — a leading icon, a two-line middle column, and a
+trailing mono value is the most common shape (see the Loop and Archive
+patterns for it in context).
+
+{% example title="Leading icon, trailing meta" %}
+<div class="ad-card" style="max-width: 24rem;">
+  <div class="ad-card-row">
+    <span class="ad-avatar ad-avatar--square">
+      <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 8 12 3 3 8v8l9 5 9-5V8Z"/><path d="M3 8l9 5 9-5M12 13v8"/></svg>
+    </span>
+    <div style="flex:1; min-width:0;">
+      <div style="font-weight: 550;">Dishwasher manual</div>
+      <div class="type-label" style="margin-top: 2px;">PDF · 2.1MB</div>
+    </div>
+    <span class="type-mono" style="color: var(--ad-color-text-tertiary);">2h</span>
+  </div>
+</div>
+{% endexample %}
+
+## API
+
+<div class="ad-table-wrap">
+  <table class="ad-table ad-table--compact">
+    <thead><tr><th scope="col">Prop</th><th scope="col">Type</th><th scope="col">Notes</th></tr></thead>
+    <tbody>
+      <tr><td class="ad-token-name">onClick</td><td class="ad-table__code">function</td><td>Reach for <code>.ad-card-row--clickable</code> and a <code>&lt;button&gt;</code>.</td></tr>
+    </tbody>
+  </table>
+</div>
diff --git a/_components/card.md b/_components/card.md
index d7697da..7714ac3 100644
--- a/_components/card.md
+++ b/_components/card.md
@@ -1,103 +1,57 @@
 ---
 title: Card
 summary: >-
-  A bordered sheet with an optional media area, a body, and a footer. Elevation
-  is opt-in.
+  Flat surface, hairline border, radius-lg. Elevation is opt-in, reserved for
+  the one focused card in a view — a dialog, the primary capture card.
 ---
 
-## Anatomy
+## Flat vs. elevated
 
-{% example title="Card" layout="stack" open %}
-<article class="ad-card" style="max-width: 22rem;">
-  <div class="ad-card__body">
-    <h3 class="ad-card__title">Persimmon 700</h3>
-    <p class="ad-card__text">
-      The light theme's accent. Clears AA against both the canvas and white,
-      which the 600 step did not.
-    </p>
-  </div>
-  <div class="ad-card__footer">
-    <span class="ad-badge ad-badge--accent">Accent</span>
-    <span class="ad-subtle" style="font-size: var(--ad-text-xs);">#c2260c</span>
-  </div>
-</article>
-{% endexample %}
-
-## Grids
-
-`.ad-card-grid` is `repeat(auto-fill, minmax(15rem, 1fr))`. Cards in it stretch
-to equal height, and `margin-top: auto` on the footer pins footers to the bottom
-regardless of how much body copy each card has.
+Flat is the default everywhere. `--elevated` is for the single most-elevated
+element on screen — never a whole grid of cards at once.
 
-{% example title="A grid of uneven cards" layout="stack" surface="sunken" %}
-<div class="ad-card-grid">
-  <article class="ad-card">
-    <div class="ad-card__body">
-      <h3 class="ad-card__title">Short</h3>
-      <p class="ad-card__text">One line.</p>
-    </div>
-    <div class="ad-card__footer"><button type="button" class="ad-btn ad-btn--ghost ad-btn--sm">Open</button></div>
-  </article>
-  <article class="ad-card">
-    <div class="ad-card__body">
-      <h3 class="ad-card__title">Longer</h3>
-      <p class="ad-card__text">
-        Enough copy here to push this card taller than its neighbour, which is
-        exactly the case that breaks a grid built on floats.
-      </p>
-    </div>
-    <div class="ad-card__footer"><button type="button" class="ad-btn ad-btn--ghost ad-btn--sm">Open</button></div>
-  </article>
-  <article class="ad-card">
-    <div class="ad-card__body">
-      <h3 class="ad-card__title">Middling</h3>
-      <p class="ad-card__text">Two lines of copy, which lands between the other two.</p>
-    </div>
-    <div class="ad-card__footer"><button type="button" class="ad-btn ad-btn--ghost ad-btn--sm">Open</button></div>
-  </article>
-</div>
+{% example title="Flat and elevated" surface="sunken" %}
+<div class="ad-card" style="width: 14rem; padding: var(--ad-space-s);">Flat — the default.</div>
+<div class="ad-card ad-card--elevated" style="width: 14rem; padding: var(--ad-space-s);">Elevated — one per view.</div>
 {% endexample %}
 
-## Clickable cards
-
-Do not wrap a whole card in an `<a>`. It makes the entire block one enormous
-link target, which is miserable to read with a screen reader and impossible to
-select text inside. Put the link on the heading and let `.ad-card--link` handle
-the hover and focus affordance for the card around it.
+## As a content tile
 
-{% example title="Linked card" layout="stack" %}
-<article class="ad-card ad-card--link" style="max-width: 22rem;">
+{% example title="Content tile" %}
+<article class="ad-card" style="max-width: 18rem;">
   <div class="ad-card__body">
-    <h3 class="ad-card__title">
-      <a href="{{ '/foundations/elevation/' | relative_url }}">Elevation</a>
-    </h3>
-    <p class="ad-card__text">
-      The heading is the link. The card lights up on hover and shows a focus ring
-      when the link inside it is focused, via <code>:focus-within</code>.
-    </p>
+    <p class="type-label">Recommended</p>
+    <h3 class="ad-card__title">Rename the caution colour</h3>
+    <p class="ad-card__text">Three people have called it "yellow" in review this month.</p>
+  </div>
+  <div class="ad-card__footer">
+    <button type="button" class="ad-btn ad-btn--primary ad-btn--sm">Approve</button>
+    <button type="button" class="ad-btn ad-btn--ghost ad-btn--sm">Dismiss</button>
   </div>
 </article>
 {% endexample %}
 
-## Avatars
+## As a list — with CardRow
 
-{% example title="Avatars" %}
-<span class="ad-avatar ad-avatar--sm">ad</span>
-<span class="ad-avatar">ad</span>
-<span class="ad-avatar ad-avatar--lg">ad</span>
-<span class="ad-avatar-stack">
-  <span class="ad-avatar ad-avatar--sm">ka</span>
-  <span class="ad-avatar ad-avatar--sm">mr</span>
-  <span class="ad-avatar ad-avatar--sm">tp</span>
-</span>
-{% endexample %}
+Card's most common job in this system is holding a list of
+[CardRow]({{ '/components/card-row/' | relative_url }})s: entry lists, task
+lists, settings rows.
 
-Stacked avatars overlap by half a step and carry a 2px ring in the surface
-colour, so the stack still reads on any background the card can sit on.
+{% example title="Card of rows" %}
+<div class="ad-card" style="max-width: 22rem;">
+  <div class="ad-card-row">Dishwasher manual</div>
+  <div class="ad-card-row">Sofa receipt</div>
+  <div class="ad-card-row">Passport scan</div>
+</div>
+{% endexample %}
 
-## Rules
+## API
 
-- **A card is a container, not a component.** It has no opinion about what goes
-  inside it beyond spacing.
-- **Border or shadow, not both** — see [Elevation]({{ '/foundations/elevation/' | relative_url }}).
-- **The heading is the link.** Never the whole card.
+<div class="ad-table-wrap">
+  <table class="ad-table ad-table--compact">
+    <thead><tr><th scope="col">Prop</th><th scope="col">Type</th><th scope="col">Notes</th></tr></thead>
+    <tbody>
+      <tr><td class="ad-token-name">elevated</td><td class="ad-table__code">boolean</td><td>Applies <code>--ad-shadow-float</code>. One per view.</td></tr>
+    </tbody>
+  </table>
+</div>
diff --git a/_components/checkbox.md b/_components/checkbox.md
new file mode 100644
index 0000000..73da0ab
--- /dev/null
+++ b/_components/checkbox.md
@@ -0,0 +1,62 @@
+---
+title: Checkbox
+summary: Flat checkbox, fills with the accent when checked. No bounce.
+---
+
+## Basic
+
+{% example title="Checkbox" open %}
+<label class="ad-choice ad-choice--checkbox">
+  <input type="checkbox">
+  <span class="ad-choice__box"></span>
+  Notify me
+</label>
+<label class="ad-choice ad-choice--checkbox">
+  <input type="checkbox" checked>
+  <span class="ad-choice__box">
+    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>
+  </span>
+  Ship on merge
+</label>
+{% endexample %}
+
+## Disabled
+
+{% example title="Disabled" %}
+<label class="ad-choice ad-choice--checkbox" aria-disabled="true">
+  <input type="checkbox" disabled>
+  <span class="ad-choice__box"></span>
+  Not available on this plan
+</label>
+{% endexample %}
+
+## In a fieldset
+
+{% example title="Fieldset" %}
+<fieldset class="ad-fieldset">
+  <legend>Notify me about</legend>
+  <label class="ad-choice ad-choice--checkbox">
+    <input type="checkbox" checked>
+    <span class="ad-choice__box"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg></span>
+    Overdue tasks
+  </label>
+  <label class="ad-choice ad-choice--checkbox">
+    <input type="checkbox">
+    <span class="ad-choice__box"></span>
+    Weekly digest
+  </label>
+</fieldset>
+{% endexample %}
+
+## API
+
+<div class="ad-table-wrap">
+  <table class="ad-table ad-table--compact">
+    <thead><tr><th scope="col">Prop</th><th scope="col">Type</th><th scope="col">Notes</th></tr></thead>
+    <tbody>
+      <tr><td class="ad-token-name">checked</td><td class="ad-table__code">boolean</td><td>Required.</td></tr>
+      <tr><td class="ad-token-name">label</td><td class="ad-table__code">node</td><td></td></tr>
+      <tr><td class="ad-token-name">disabled</td><td class="ad-table__code">boolean</td><td></td></tr>
+    </tbody>
+  </table>
+</div>
diff --git a/_components/dialog.md b/_components/dialog.md
new file mode 100644
index 0000000..be9b63a
--- /dev/null
+++ b/_components/dialog.md
@@ -0,0 +1,56 @@
+---
+title: Dialog
+summary: >-
+  A centered modal. Flat surface, no coloured header, hairline dividers
+  between title, body, and footer.
+---
+
+The example below is shown open, scoped to its own preview box rather than
+the real viewport-covering overlay, so it can sit safely inside a
+documentation page. Wire real open/close state — and a `position: fixed`
+overlay — in the consuming app; this is a pixel recreation for documentation,
+not a functioning modal.
+
+{% example title="Dialog" open %}
+<div class="ad-dialog-demo">
+  <div class="ad-dialog__overlay">
+    <div class="ad-dialog">
+      <div class="ad-dialog__title">Delete series?</div>
+      <div class="ad-dialog__body">This removes all of its task history. This cannot be undone.</div>
+      <div class="ad-dialog__footer">
+        <button type="button" class="ad-btn">Cancel</button>
+        <button type="button" class="ad-btn ad-btn--danger">Delete</button>
+      </div>
+    </div>
+  </div>
+</div>
+{% endexample %}
+
+## Without a footer
+
+A body-only dialog for a notice rather than a decision.
+
+{% example title="No footer" %}
+<div class="ad-dialog-demo" style="min-height: 14rem;">
+  <div class="ad-dialog__overlay">
+    <div class="ad-dialog" style="width: 20rem;">
+      <div class="ad-dialog__title">Backup complete</div>
+      <div class="ad-dialog__body">412 files, 1.2GB, archived to the household drive.</div>
+    </div>
+  </div>
+</div>
+{% endexample %}
+
+## API
+
+<div class="ad-table-wrap">
+  <table class="ad-table ad-table--compact">
+    <thead><tr><th scope="col">Prop</th><th scope="col">Type</th><th scope="col">Notes</th></tr></thead>
+    <tbody>
+      <tr><td class="ad-token-name">open</td><td class="ad-table__code">boolean</td><td>Required.</td></tr>
+      <tr><td class="ad-token-name">title</td><td class="ad-table__code">string</td><td>Optional — a body-only dialog omits the title bar.</td></tr>
+      <tr><td class="ad-token-name">onClose</td><td class="ad-table__code">function</td><td>Fired on overlay click.</td></tr>
+      <tr><td class="ad-token-name">footer</td><td class="ad-table__code">node</td><td>Usually a pair of Buttons — the least committing action first.</td></tr>
+    </tbody>
+  </table>
+</div>
diff --git a/_components/field.md b/_components/field.md
index b29b684..722f1af 100644
--- a/_components/field.md
+++ b/_components/field.md
@@ -1,166 +1,65 @@
 ---
 title: Field
 summary: >-
-  Label, control, hint, error. The wrapper owns the spacing so that a text input,
-  a select, and a group of checkboxes all line up in the same form.
+  The labeled field wrapper. Mono, uppercase, tracked label — the same
+  catalog-label convention as Badge and type-label — with an optional hint.
 ---
 
-## Anatomy
+## Basic
 
-Every field is a `<label>` (or a `<fieldset>` with a `<legend>`), a control, and
-optionally a hint and an error. The wrapper is a flex column with
-`--ad-space-2` between the parts, which is why controls of different heights
-still produce an even form.
-
-{% example title="A field with a hint" layout="stack" open %}
-<div class="ad-field" style="max-width: 22rem;">
-  <label class="ad-field__label" for="token-name">
-    Token name <span class="ad-field__required" aria-hidden="true">*</span>
-  </label>
-  <input class="ad-input" id="token-name" name="token-name" type="text"
-         placeholder="color-surface-sunken" aria-describedby="token-name-hint" required>
-  <p class="ad-field__hint" id="token-name-hint">
-    Lowercase, hyphenated, no prefix — the build adds <code>--ad-</code>.
-  </p>
-</div>
-{% endexample %}
-
-## Controls
-
-{% example title="Text, textarea, select" layout="stack" %}
-<div class="ad-form">
-  <div class="ad-field">
-    <label class="ad-field__label" for="demo-text">Display name</label>
-    <input class="ad-input" id="demo-text" type="text" value="Persimmon">
-  </div>
-  <div class="ad-field">
-    <label class="ad-field__label" for="demo-select">Ramp</label>
-    <select class="ad-select" id="demo-select">
-      <option>ink</option>
-      <option selected>persimmon</option>
-      <option>verdant</option>
-      <option>amber</option>
-    </select>
-  </div>
-  <div class="ad-field">
-    <label class="ad-field__label" for="demo-textarea">Rationale</label>
-    <textarea class="ad-textarea" id="demo-textarea"
-              placeholder="Why this colour and not the one next to it?"></textarea>
-    <p class="ad-field__hint">Optional, but the next person will thank you.</p>
-  </div>
-</div>
-{% endexample %}
-
-## Validation
-
-Invalid state lives on the control via `aria-invalid`, not on the wrapper. That
-way it works with native constraint validation and it is announced, not merely
-coloured. The error message is wired up with `aria-describedby`.
-
-{% example title="Invalid" layout="stack" %}
-<div class="ad-field" style="max-width: 22rem;">
-  <label class="ad-field__label" for="demo-invalid">Hex value</label>
-  <input class="ad-input" id="demo-invalid" type="text" value="persimmon-ish"
-         aria-invalid="true" aria-describedby="demo-invalid-error">
-  <p class="ad-field__error" id="demo-invalid-error">
-    Enter a six-digit hex, like <code>#ea360c</code>.
-  </p>
+{% example title="Field" open %}
+<div class="ad-field" style="max-width: 20rem;">
+  <label class="ad-field__label" for="field-name">Name</label>
+  <input class="ad-input" id="field-name" type="text" placeholder="Call Mom">
+  <span class="ad-field__hint">First line becomes the display name.</span>
 </div>
 {% endexample %}
 
-## Disabled
-
-{% example title="Disabled" layout="stack" %}
-<div class="ad-form">
-  <div class="ad-field">
-    <label class="ad-field__label" for="demo-disabled">Inherited from the parent theme</label>
-    <input class="ad-input" id="demo-disabled" type="text" value="--ad-color-canvas" disabled>
-  </div>
-  <label class="ad-choice">
-    <input type="checkbox" disabled checked>
-    <span class="ad-choice__text">Locked by the theme<span class="ad-choice__hint">Unlock the theme to edit this.</span></span>
-  </label>
+## Holding any control
+
+Field wraps [Input]({{ '/components/input/' | relative_url }}),
+[Select]({{ '/components/select/' | relative_url }}), or anything else — it
+only owns the label and the hint line, never the control's own styling.
+
+{% example title="Field with a select" %}
+<div class="ad-field" style="max-width: 20rem;">
+  <label class="ad-field__label" for="field-unit">Unit</label>
+  <select class="ad-select" id="field-unit">
+    <option>Day</option>
+    <option selected>Week</option>
+    <option>Month</option>
+    <option>Year</option>
+  </select>
 </div>
 {% endexample %}
 
-## Choices
-
-Checkboxes and radios share `.ad-choice`, which puts the box on the first line
-of a multi-line label instead of centring it against the whole block. Grouped
-choices go in a `<fieldset>` so the group has a name.
-
-{% example title="Checkbox and radio groups" layout="stack" %}
-<fieldset class="ad-fieldset">
-  <legend>Emit</legend>
-  <label class="ad-choice">
-    <input type="checkbox" checked>
-    <span class="ad-choice__text">CSS custom properties<span class="ad-choice__hint">One <code>:root</code> block plus both dark selectors.</span></span>
-  </label>
-  <label class="ad-choice">
-    <input type="checkbox">
-    <span class="ad-choice__text">JSON<span class="ad-choice__hint">For tooling that cannot read CSS.</span></span>
-  </label>
-</fieldset>
-
-<fieldset class="ad-fieldset">
-  <legend>Default theme</legend>
-  <label class="ad-choice"><input type="radio" name="demo-theme" checked> <span class="ad-choice__text">Follow the system</span></label>
-  <label class="ad-choice"><input type="radio" name="demo-theme"> <span class="ad-choice__text">Always light</span></label>
-  <label class="ad-choice"><input type="radio" name="demo-theme"> <span class="ad-choice__text">Always dark</span></label>
-</fieldset>
-{% endexample %}
-
-## Switch
+## In a row
 
-A switch takes effect immediately. A checkbox waits for a submit. If your switch
-needs a Save button, it should have been a checkbox.
-
-{% example title="Switch" layout="stack" %}
-<label class="ad-switch">
-  <input type="checkbox" checked>
-  <span class="ad-switch__track"></span>
-  <span>Rebuild tokens on save</span>
-</label>
-<label class="ad-switch">
-  <input type="checkbox">
-  <span class="ad-switch__track"></span>
-  <span>Warn on contrast regressions</span>
-</label>
-<label class="ad-switch">
-  <input type="checkbox" disabled>
-  <span class="ad-switch__track"></span>
-  <span class="ad-subtle">Publish to npm (not configured)</span>
-</label>
-{% endexample %}
-
-## Rows
-
-`.ad-form__row` is a `repeat(auto-fit, minmax(11rem, 1fr))` grid, so fields sit
-side by side when there is room and stack when there is not. No breakpoint
-needed.
-
-{% example title="Two up" layout="stack" %}
+{% example title="Two fields in a row" %}
 <div class="ad-form" style="max-width: none;">
   <div class="ad-form__row">
     <div class="ad-field">
-      <label class="ad-field__label" for="demo-from">Ramp</label>
-      <input class="ad-input" id="demo-from" type="text" value="persimmon">
+      <label class="ad-field__label" for="field-every">Every</label>
+      <input class="ad-input" id="field-every" type="number" placeholder="2">
     </div>
     <div class="ad-field">
-      <label class="ad-field__label" for="demo-to">Step</label>
-      <input class="ad-input" id="demo-to" type="text" value="600">
+      <label class="ad-field__label" for="field-unit-2">Unit</label>
+      <select class="ad-select" id="field-unit-2">
+        <option>Day</option><option selected>Week</option><option>Month</option>
+      </select>
     </div>
   </div>
 </div>
 {% endexample %}
 
-## Rules
+## API
 
-- **Every control has a real `<label>`.** A placeholder is not a label; it
-  disappears the moment someone types.
-- **Hints go in `aria-describedby`.** Otherwise a screen reader announces the
-  label and nothing else.
-- **Never mark a field invalid before the reader has finished with it.** Validate
-  on blur or on submit.
-- **The required marker is decorative.** `aria-hidden` on the asterisk, `required`
-  on the input.
+<div class="ad-table-wrap">
+  <table class="ad-table ad-table--compact">
+    <thead><tr><th scope="col">Prop</th><th scope="col">Type</th><th scope="col">Notes</th></tr></thead>
+    <tbody>
+      <tr><td class="ad-token-name">label</td><td class="ad-table__code">string</td><td>Mono, uppercase, tracked.</td></tr>
+      <tr><td class="ad-token-name">hint</td><td class="ad-table__code">string</td><td>Renders below the control.</td></tr>
+    </tbody>
+  </table>
+</div>
diff --git a/_components/icon-button.md b/_components/icon-button.md
new file mode 100644
index 0000000..ab81a4f
--- /dev/null
+++ b/_components/icon-button.md
@@ -0,0 +1,60 @@
+---
+title: IconButton
+summary: >-
+  A square icon-only button. Always pass a label — it becomes the accessible
+  name and the native tooltip, since there is no visible text to fall back on.
+---
+
+## Variants
+
+`ghost` is the default and transparent. `outline` is a bordered surface, for
+an icon button sitting alone rather than in a toolbar. `accent` is a soft
+accent tint, for a toggled or active state — the segmented-tabs pattern this
+system does not otherwise reach for.
+
+{% example title="Variants" open %}
+<button type="button" class="ad-icon-btn" aria-label="Delete">
+  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13"/></svg>
+</button>
+<button type="button" class="ad-icon-btn ad-icon-btn--outline" aria-label="Settings">
+  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M12 2.5v3M12 18.5v3M3.6 7.2l2.6 1.5M17.8 15.3l2.6 1.5M3.6 16.8l2.6-1.5M17.8 8.7l2.6-1.5"/></svg>
+</button>
+<button type="button" class="ad-icon-btn ad-icon-btn--accent" aria-label="Filter (active)">
+  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 5h16l-6 8v5l-4 2v-7z"/></svg>
+</button>
+{% endexample %}
+
+## Sizes
+
+{% example title="Sizes" %}
+<button type="button" class="ad-icon-btn ad-icon-btn--outline ad-icon-btn--sm" aria-label="Back"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 18l-6-6 6-6"/></svg></button>
+<button type="button" class="ad-icon-btn ad-icon-btn--outline" aria-label="Back"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 18l-6-6 6-6"/></svg></button>
+<button type="button" class="ad-icon-btn ad-icon-btn--outline ad-icon-btn--lg" aria-label="Back"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 18l-6-6 6-6"/></svg></button>
+{% endexample %}
+
+## Paired with Tooltip
+
+An icon-only control almost always wants a [Tooltip]({{ '/components/tooltip/' | relative_url }}) too — the `aria-label` covers accessibility, the tooltip covers a sighted mouse user glancing for a hint.
+
+{% example title="With a tooltip" %}
+<span class="ad-tooltip">
+  <button type="button" class="ad-icon-btn" aria-label="Delete">
+    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13"/></svg>
+  </button>
+  <span class="ad-tooltip__bubble">Delete</span>
+</span>
+{% endexample %}
+
+## API
+
+<div class="ad-table-wrap">
+  <table class="ad-table ad-table--compact">
+    <thead><tr><th scope="col">Prop</th><th scope="col">Type</th><th scope="col">Notes</th></tr></thead>
+    <tbody>
+      <tr><td class="ad-token-name">icon</td><td class="ad-table__code">node</td><td>Required.</td></tr>
+      <tr><td class="ad-token-name">label</td><td class="ad-table__code">string</td><td>Required — becomes <code>aria-label</code> and <code>title</code>.</td></tr>
+      <tr><td class="ad-token-name">size</td><td class="ad-table__code">sm | md | lg</td><td>28 / 34 / 40px square.</td></tr>
+      <tr><td class="ad-token-name">variant</td><td class="ad-table__code">ghost | outline | accent</td><td>Default <code>ghost</code>.</td></tr>
+    </tbody>
+  </table>
+</div>
diff --git a/_components/input.md b/_components/input.md
new file mode 100644
index 0000000..1049483
--- /dev/null
+++ b/_components/input.md
@@ -0,0 +1,46 @@
+---
+title: Input
+summary: >-
+  A bordered text input, styled to match Select exactly. Usually wrapped in
+  Field for its label and hint.
+---
+
+## Basic
+
+{% example title="Input" open %}
+<input class="ad-input" type="text" placeholder="Dishwasher manual" style="max-width: 20rem;">
+{% endexample %}
+
+## Focus and disabled
+
+{% example title="States" layout="stack" %}
+<input class="ad-input" type="text" value="Focused" style="max-width: 20rem; border-color: var(--ad-color-accent); box-shadow: 0 0 0 2px var(--ad-color-accent-soft);">
+<input class="ad-input" type="text" value="Disabled" disabled style="max-width: 20rem;">
+{% endexample %}
+
+## Invalid
+
+Invalid state is expressed on the control itself, so it works with native
+form validation, plus an error line below.
+
+{% example title="Invalid" %}
+<div class="ad-field" style="max-width: 20rem;">
+  <label class="ad-field__label" for="input-accent">Accent hex</label>
+  <input class="ad-input" id="input-accent" type="text" value="not-a-colour" aria-invalid="true" aria-describedby="input-accent-error">
+  <p class="ad-field__error" id="input-accent-error">Not a valid hex colour.</p>
+</div>
+{% endexample %}
+
+## API
+
+<div class="ad-table-wrap">
+  <table class="ad-table ad-table--compact">
+    <thead><tr><th scope="col">Prop</th><th scope="col">Type</th><th scope="col">Notes</th></tr></thead>
+    <tbody>
+      <tr><td class="ad-token-name">type</td><td class="ad-table__code">string</td><td>Any native input type.</td></tr>
+      <tr><td class="ad-token-name">placeholder</td><td class="ad-table__code">string</td><td></td></tr>
+      <tr><td class="ad-token-name">disabled</td><td class="ad-table__code">boolean</td><td></td></tr>
+      <tr><td class="ad-token-name">aria-invalid</td><td class="ad-table__code">"true"</td><td>Reddens the border and fill.</td></tr>
+    </tbody>
+  </table>
+</div>
diff --git a/_components/progress.md b/_components/progress.md
deleted file mode 100644
index b1bbe86..0000000
--- a/_components/progress.md
+++ /dev/null
@@ -1,78 +0,0 @@
----
-title: Progress
-summary: >-
-  A determinate bar for work that will finish, and a shuttle for work whose
-  finish you cannot predict. Never a fake percentage.
----
-
-## Determinate
-
-{% example title="Progress" layout="stack" open %}
-<div class="ad-progress" style="max-width: 24rem;">
-  <div class="ad-progress__label">
-    <span id="demo-progress-label">Compiling tokens</span>
-    <span class="ad-progress__value">68%</span>
-  </div>
-  <div class="ad-progress__track" role="progressbar" aria-labelledby="demo-progress-label"
-       aria-valuenow="68" aria-valuemin="0" aria-valuemax="100">
-    <div class="ad-progress__fill" style="width: 68%;"></div>
-  </div>
-</div>
-{% endexample %}
-
-The `role="progressbar"` and its three `aria-value*` attributes go on the track,
-not the fill. The fill is a decoration; the track is the control.
-
-## Tones
-
-{% example title="Tones" layout="stack" %}
-<div class="ad-progress ad-progress--positive" style="max-width: 24rem;">
-  <div class="ad-progress__label"><span id="p-1">Contrast checks</span><span class="ad-progress__value">10 / 10</span></div>
-  <div class="ad-progress__track" role="progressbar" aria-labelledby="p-1" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100">
-    <div class="ad-progress__fill" style="width: 100%;"></div>
-  </div>
-</div>
-<div class="ad-progress ad-progress--caution" style="max-width: 24rem;">
-  <div class="ad-progress__label"><span id="p-2">Storage used</span><span class="ad-progress__value">81%</span></div>
-  <div class="ad-progress__track" role="progressbar" aria-labelledby="p-2" aria-valuenow="81" aria-valuemin="0" aria-valuemax="100">
-    <div class="ad-progress__fill" style="width: 81%;"></div>
-  </div>
-</div>
-<div class="ad-progress ad-progress--critical" style="max-width: 24rem;">
-  <div class="ad-progress__label"><span id="p-3">Budget remaining</span><span class="ad-progress__value">4%</span></div>
-  <div class="ad-progress__track" role="progressbar" aria-labelledby="p-3" aria-valuenow="4" aria-valuemin="0" aria-valuemax="100">
-    <div class="ad-progress__fill" style="width: 4%;"></div>
-  </div>
-</div>
-{% endexample %}
-
-Tone is about the meaning of the number, not the size of it. A bar at 100% is
-positive when it is a task completing and critical when it is a quota filling.
-
-## Indeterminate
-
-{% example title="Indeterminate" layout="stack" %}
-<div class="ad-progress ad-progress--indeterminate" style="max-width: 24rem;">
-  <div class="ad-progress__label"><span id="p-4">Waiting for the build</span></div>
-  <div class="ad-progress__track" role="progressbar" aria-labelledby="p-4">
-    <div class="ad-progress__fill"></div>
-  </div>
-</div>
-{% endexample %}
-
-No `aria-valuenow` — its absence is what tells assistive technology the value is
-unknown. Inventing a number here would be a lie told twice: once to the reader
-and once to the screen reader.
-
-Under `prefers-reduced-motion` the shuttle stops moving. The bar stays, the label
-stays, and the reader still knows something is happening because the label says so.
-
-## Rules
-
-- **`role="progressbar"` on the track**, with `aria-labelledby` pointing at a
-  visible label.
-- **Omit `aria-valuenow` when you do not know it.** Do not pass `0`.
-- **Show the number in text as well as in the bar.** A bar alone is unreadable
-  at a glance and unusable in greyscale.
-- **Under about two seconds, show nothing.** A progress bar that flashes is worse
-  than no feedback at all.
diff --git a/_components/radio.md b/_components/radio.md
new file mode 100644
index 0000000..9322632
--- /dev/null
+++ b/_components/radio.md
@@ -0,0 +1,55 @@
+---
+title: Radio
+summary: Flat radio button, the same visual language as Checkbox.
+---
+
+## Basic
+
+{% example title="Radio" open %}
+<label class="ad-choice ad-choice--radio">
+  <input type="radio" name="demo-radio-1" checked>
+  <span class="ad-choice__box"></span>
+  Weekly
+</label>
+<label class="ad-choice ad-choice--radio">
+  <input type="radio" name="demo-radio-1">
+  <span class="ad-choice__box"></span>
+  Monthly
+</label>
+{% endexample %}
+
+## As a group
+
+{% example title="Interval" %}
+<fieldset class="ad-fieldset">
+  <legend>Repeat</legend>
+  <label class="ad-choice ad-choice--radio">
+    <input type="radio" name="demo-radio-2" checked>
+    <span class="ad-choice__box"></span>
+    Every week
+  </label>
+  <label class="ad-choice ad-choice--radio">
+    <input type="radio" name="demo-radio-2">
+    <span class="ad-choice__box"></span>
+    Every month
+  </label>
+  <label class="ad-choice ad-choice--radio">
+    <input type="radio" name="demo-radio-2">
+    <span class="ad-choice__box"></span>
+    Every year
+  </label>
+</fieldset>
+{% endexample %}
+
+## API
+
+<div class="ad-table-wrap">
+  <table class="ad-table ad-table--compact">
+    <thead><tr><th scope="col">Prop</th><th scope="col">Type</th><th scope="col">Notes</th></tr></thead>
+    <tbody>
+      <tr><td class="ad-token-name">checked</td><td class="ad-table__code">boolean</td><td>Required.</td></tr>
+      <tr><td class="ad-token-name">name</td><td class="ad-table__code">string</td><td>Groups mutually exclusive radios.</td></tr>
+      <tr><td class="ad-token-name">label</td><td class="ad-table__code">node</td><td></td></tr>
+    </tbody>
+  </table>
+</div>
diff --git a/_components/select.md b/_components/select.md
new file mode 100644
index 0000000..d82dc21
--- /dev/null
+++ b/_components/select.md
@@ -0,0 +1,42 @@
+---
+title: Select
+summary: Native select, styled to match Input exactly.
+---
+
+## Basic
+
+{% example title="Select" open %}
+<select class="ad-select" style="max-width: 20rem;">
+  <option>Day</option>
+  <option selected>Week</option>
+  <option>Month</option>
+  <option>Year</option>
+</select>
+{% endexample %}
+
+## Disabled
+
+{% example title="Disabled" %}
+<select class="ad-select" disabled style="max-width: 20rem;"><option>Week</option></select>
+{% endexample %}
+
+<div class="ad-callout">
+  <p style="margin:0; font-size: var(--ad-step--1);">
+    The chevron is the one place in this component set that cannot reach a
+    <code>var(--ad-*)</code> token — a data-URI <code>background-image</code>
+    cannot reference a custom property, so its stroke is a literal hex, kept
+    close to <code>--ad-color-text-secondary</code> and commented in the source.
+  </p>
+</div>
+
+## API
+
+<div class="ad-table-wrap">
+  <table class="ad-table ad-table--compact">
+    <thead><tr><th scope="col">Prop</th><th scope="col">Type</th><th scope="col">Notes</th></tr></thead>
+    <tbody>
+      <tr><td class="ad-token-name">options</td><td class="ad-table__code">{value, label}[]</td><td></td></tr>
+      <tr><td class="ad-token-name">disabled</td><td class="ad-table__code">boolean</td><td></td></tr>
+    </tbody>
+  </table>
+</div>
diff --git a/_components/switch.md b/_components/switch.md
new file mode 100644
index 0000000..494586e
--- /dev/null
+++ b/_components/switch.md
@@ -0,0 +1,44 @@
+---
+title: Switch
+summary: >-
+  Toggle switch for on/off settings. The one moving part in this system —
+  everything else transitions colour or border, never position.
+---
+
+## Basic
+
+{% example title="Switch" open %}
+<label class="ad-switch">
+  <input type="checkbox" checked>
+  <span class="ad-switch__track"></span>
+  Dark mode
+</label>
+<label class="ad-switch">
+  <input type="checkbox">
+  <span class="ad-switch__track"></span>
+  Weekly digest
+</label>
+{% endexample %}
+
+## Disabled
+
+{% example title="Disabled" %}
+<label class="ad-switch">
+  <input type="checkbox" checked disabled>
+  <span class="ad-switch__track"></span>
+  Always on for this plan
+</label>
+{% endexample %}
+
+## API
+
+<div class="ad-table-wrap">
+  <table class="ad-table ad-table--compact">
+    <thead><tr><th scope="col">Prop</th><th scope="col">Type</th><th scope="col">Notes</th></tr></thead>
+    <tbody>
+      <tr><td class="ad-token-name">checked</td><td class="ad-table__code">boolean</td><td>Required.</td></tr>
+      <tr><td class="ad-token-name">label</td><td class="ad-table__code">node</td><td></td></tr>
+      <tr><td class="ad-token-name">disabled</td><td class="ad-table__code">boolean</td><td></td></tr>
+    </tbody>
+  </table>
+</div>
diff --git a/_components/table.md b/_components/table.md
deleted file mode 100644
index 33a493f..0000000
--- a/_components/table.md
+++ /dev/null
@@ -1,82 +0,0 @@
----
-title: Table
-summary: >-
-  Left-aligned by default, numeric columns opt into tabular figures, and the
-  whole thing scrolls inside its own wrapper instead of pushing the page sideways.
----
-
-## Default
-
-{% example title="Table" layout="stack" open %}
-<div class="ad-table-wrap">
-  <table class="ad-table">
-    <caption>Ramps and their step counts.</caption>
-    <thead>
-      <tr>
-        <th scope="col">Ramp</th>
-        <th scope="col">Role</th>
-        <th scope="col" class="ad-table__num">Steps</th>
-      </tr>
-    </thead>
-    <tbody>
-      {%- for ramp in site.data.tokens.color.ramps %}
-      <tr>
-        <td class="ad-table__code">{{ ramp.name }}</td>
-        <td>{{ ramp.description | truncate: 64 }}</td>
-        <td class="ad-table__num">{{ ramp.steps | size }}</td>
-      </tr>
-      {%- endfor %}
-    </tbody>
-  </table>
-</div>
-{% endexample %}
-
-That table is not hand-written. It loops over the same YAML the stylesheet is
-compiled from, so adding a ramp adds a row.
-
-## Density and emphasis
-
-{% example title="Compact, striped, hoverable" layout="stack" %}
-<div class="ad-table-wrap">
-  <table class="ad-table ad-table--compact ad-table--striped ad-table--hover">
-    <thead>
-      <tr>
-        <th scope="col">Token</th>
-        <th scope="col" class="ad-table__num">Light</th>
-        <th scope="col" class="ad-table__num">Dark</th>
-      </tr>
-    </thead>
-    <tbody>
-      {%- assign shown = site.data.tokens.scale.space.scale | slice: 1, 6 %}
-      {%- for step in shown %}
-      <tr>
-        <td class="ad-table__code">{{ step.token }}</td>
-        <td class="ad-table__num">{{ step.px }}px</td>
-        <td class="ad-table__num">{{ step.px }}px</td>
-      </tr>
-      {%- endfor %}
-    </tbody>
-  </table>
-</div>
-{% endexample %}
-
-Stripes and hover are alternatives, not partners. Stripes help the eye track a
-long row across many columns; hover helps when rows are clickable. Together they
-fight.
-
-## Overflow
-
-`.ad-table-wrap` owns `overflow-x: auto`, so a wide table scrolls inside its own
-box and the page body never scrolls horizontally. This is the single most
-common way a documentation site breaks on a phone.
-
-## Rules
-
-- **Every header cell gets `scope`.** `scope="col"` on column headers,
-  `scope="row"` on the first cell of a row when it identifies the row.
-- **Numbers right, text left, and `tabular-nums` on the numbers** so digits
-  stack into columns.
-- **A `<caption>` beats a heading above the table.** It is announced as part of
-  the table.
-- **Never nest a table in a table.** If the data is that shaped, it is two
-  tables.
diff --git a/_components/tabs.md b/_components/tabs.md
index 93dc6cd..4a99577 100644
--- a/_components/tabs.md
+++ b/_components/tabs.md
@@ -1,79 +1,63 @@
 ---
 title: Tabs
 summary: >-
-  The WAI-ARIA tabs pattern, wired up in about forty lines of vanilla JavaScript.
-  Arrows move between tabs, Home and End jump to the ends.
+  Two real variants: underline for primary section navigation, segmented for
+  filters. The WAI-ARIA tabs pattern, wired up in vanilla JavaScript — arrows
+  move, Home/End jump to the ends.
 ---
 
-## Tabs
+## Underline
 
-{% example title="Tabs" layout="stack" open %}
-<div class="ad-tabs" data-tabs>
-  <div class="ad-tabs__list" role="tablist" aria-label="Token formats">
-    <button class="ad-tabs__tab" role="tab" id="tab-css" aria-controls="panel-css" aria-selected="true" type="button">CSS</button>
-    <button class="ad-tabs__tab" role="tab" id="tab-yaml" aria-controls="panel-yaml" aria-selected="false" tabindex="-1" type="button">YAML</button>
-    <button class="ad-tabs__tab" role="tab" id="tab-ruby" aria-controls="panel-ruby" aria-selected="false" tabindex="-1" type="button">Ruby</button>
-  </div>
+Mono, with an accent underline. For primary section navigation — a project's
+own tabs, never a filter.
 
-  <div class="ad-tabs__panel" role="tabpanel" id="panel-css" aria-labelledby="tab-css" tabindex="0">
-    <p style="margin:0">What the build emits — one flat block of custom properties.</p>
-    <pre style="margin: var(--ad-space-3) 0 0; padding: var(--ad-space-3); background: var(--ad-color-surface-sunken); border-radius: var(--ad-radius-sm); font-size: var(--ad-text-xs); overflow-x: auto;"><code>:root {
-  --ad-color-accent: #c2260c;
-  --ad-color-accent-hover: #9a2012;
-}</code></pre>
+{% example title="Underline" layout="stack" open %}
+<div class="ad-tabs ad-tabs--underline" data-tabs>
+  <div class="ad-tabs__list" role="tablist" aria-label="Repository">
+    <button class="ad-tabs__tab" role="tab" id="tabs-code-tab" aria-controls="tabs-code-panel" aria-selected="true" tabindex="0">Code</button>
+    <button class="ad-tabs__tab" role="tab" id="tabs-ci-tab" aria-controls="tabs-ci-panel" aria-selected="false" tabindex="-1">CI</button>
+    <button class="ad-tabs__tab" role="tab" id="tabs-issues-tab" aria-controls="tabs-issues-panel" aria-selected="false" tabindex="-1">Issues</button>
   </div>
+  <div class="ad-tabs__panel" role="tabpanel" id="tabs-code-panel" aria-labelledby="tabs-code-tab" tabindex="0">Repository contents.</div>
+  <div class="ad-tabs__panel" role="tabpanel" id="tabs-ci-panel" aria-labelledby="tabs-ci-tab" tabindex="0" hidden>Build history.</div>
+  <div class="ad-tabs__panel" role="tabpanel" id="tabs-issues-panel" aria-labelledby="tabs-issues-tab" tabindex="0" hidden>Open issues.</div>
+</div>
+{% endexample %}
 
-  <div class="ad-tabs__panel" role="tabpanel" id="panel-yaml" aria-labelledby="tab-yaml" tabindex="0" hidden>
-    <p style="margin:0">What a person edits — a reference into a ramp, never a hex.</p>
-    <pre style="margin: var(--ad-space-3) 0 0; padding: var(--ad-space-3); background: var(--ad-color-surface-sunken); border-radius: var(--ad-radius-sm); font-size: var(--ad-text-xs); overflow-x: auto;"><code>- token: color-accent
-  light: persimmon.700
-  dark: persimmon.400</code></pre>
-  </div>
+## Segmented
 
-  <div class="ad-tabs__panel" role="tabpanel" id="panel-ruby" aria-labelledby="tab-ruby" tabindex="0" hidden>
-    <p style="margin:0">What connects them — a generator that runs before Jekyll renders a page.</p>
-    <pre style="margin: var(--ad-space-3) 0 0; padding: var(--ad-space-3); background: var(--ad-color-surface-sunken); border-radius: var(--ad-radius-sm); font-size: var(--ad-text-xs); overflow-x: auto;"><code>site.pages &lt;&lt; stylesheet(site, token_set)</code></pre>
+A pill track; the active segment lifts to the surface. For filters — *All ·
+Overdue · Upcoming* — never primary navigation.
+
+{% example title="Segmented" layout="stack" %}
+<div class="ad-tabs ad-tabs--segmented" data-tabs>
+  <div class="ad-tabs__list" role="tablist" aria-label="Filter">
+    <button class="ad-tabs__tab" role="tab" id="tabs-all-tab" aria-controls="tabs-all-panel" aria-selected="true" tabindex="0">All</button>
+    <button class="ad-tabs__tab" role="tab" id="tabs-overdue-tab" aria-controls="tabs-overdue-panel" aria-selected="false" tabindex="-1">Overdue</button>
+    <button class="ad-tabs__tab" role="tab" id="tabs-upcoming-tab" aria-controls="tabs-upcoming-panel" aria-selected="false" tabindex="-1">Upcoming</button>
   </div>
+  <div class="ad-tabs__panel" role="tabpanel" id="tabs-all-panel" aria-labelledby="tabs-all-tab" tabindex="0">7 series.</div>
+  <div class="ad-tabs__panel" role="tabpanel" id="tabs-overdue-panel" aria-labelledby="tabs-overdue-tab" tabindex="0" hidden>2 series.</div>
+  <div class="ad-tabs__panel" role="tabpanel" id="tabs-upcoming-panel" aria-labelledby="tabs-upcoming-tab" tabindex="0" hidden>5 series.</div>
 </div>
 {% endexample %}
 
-## The contract
+## Rules
+
+- **Never both variants in one view.** Underline is for the page's own
+  sections; segmented is for filtering what is already on screen.
+- **Segmented is not primary navigation.** If it decides which screen the
+  reader is on, it should be underline.
 
-The JavaScript looks for `[data-tabs]` and takes over from there, but it will
-only do the right thing if the markup is right:
+## API
 
 <div class="ad-table-wrap">
   <table class="ad-table ad-table--compact">
-    <thead><tr><th scope="col">Element</th><th scope="col">Needs</th></tr></thead>
+    <thead><tr><th scope="col">Prop</th><th scope="col">Type</th><th scope="col">Notes</th></tr></thead>
     <tbody>
-      <tr><td>Wrapper</td><td class="ad-table__code">data-tabs</td></tr>
-      <tr><td>List</td><td class="ad-table__code">role="tablist"</td></tr>
-      <tr><td>Tab</td><td class="ad-table__code">role="tab", id, aria-controls, aria-selected</td></tr>
-      <tr><td>Panel</td><td class="ad-table__code">role="tabpanel", id, aria-labelledby, tabindex="0"</td></tr>
+      <tr><td class="ad-token-name">items</td><td class="ad-table__code">{value, label}[]</td><td>Required.</td></tr>
+      <tr><td class="ad-token-name">value</td><td class="ad-table__code">string</td><td>The selected item's value.</td></tr>
+      <tr><td class="ad-token-name">variant</td><td class="ad-table__code">underline | segmented</td><td>Default <code>underline</code>.</td></tr>
     </tbody>
   </table>
 </div>
-
-Only the selected tab stays in the tab order; the rest get `tabindex="-1"` and
-are reached with the arrow keys. That is one Tab stop for the whole tablist,
-which is the point of the pattern.
-
-<div class="ad-callout ad-callout--caution">
-  <div class="ad-callout__body">
-    <p class="ad-callout__title">Tabs are not navigation</p>
-    <p style="margin:0">
-      If each panel deserves its own URL — if a reader might want to link to one,
-      or hit Back to leave it — it is a set of pages with a nav, not a tablist.
-      This site's sidebar is nav; the block above is tabs.
-    </p>
-  </div>
-</div>
-
-## Rules
-
-- **The selected panel is the only one not `hidden`.** Do not hide panels with
-  CSS alone; `hidden` keeps them out of the accessibility tree.
-- **Never more than about six tabs.** Past that the labels stop fitting and the
-  arrow keys stop being faster than reading.
-- **Keep panel heights close.** A tablist that resizes the page on every switch
-  makes the reader lose their place.
diff --git a/_components/tag.md b/_components/tag.md
new file mode 100644
index 0000000..cc48e11
--- /dev/null
+++ b/_components/tag.md
@@ -0,0 +1,38 @@
+---
+title: Tag
+summary: >-
+  A removable mono chip for freeform tags — locations, categories. `--dashed`
+  renders an "add tag" affordance instead of a value.
+---
+
+## Default and removable
+
+{% example title="Tags" open %}
+<span class="ad-tag">location:garage</span>
+<span class="ad-tag">category:appliance</span>
+<span class="ad-tag">
+  category:receipt
+  <button type="button" class="ad-tag__remove" aria-label="Remove tag">×</button>
+</span>
+{% endexample %}
+
+## Dashed — the "add tag" affordance
+
+{% example title="Add tag" %}
+<div class="ad-tag-set">
+  <span class="ad-tag">location:garage</span>
+  <span class="ad-tag ad-tag--dashed">+ add tag</span>
+</div>
+{% endexample %}
+
+## API
+
+<div class="ad-table-wrap">
+  <table class="ad-table ad-table--compact">
+    <thead><tr><th scope="col">Prop</th><th scope="col">Type</th><th scope="col">Notes</th></tr></thead>
+    <tbody>
+      <tr><td class="ad-token-name">onRemove</td><td class="ad-table__code">function</td><td>Renders the × button when present.</td></tr>
+      <tr><td class="ad-token-name">dashed</td><td class="ad-table__code">boolean</td><td>Renders an "add tag" affordance instead of a value.</td></tr>
+    </tbody>
+  </table>
+</div>
diff --git a/_components/toast.md b/_components/toast.md
new file mode 100644
index 0000000..513e45e
--- /dev/null
+++ b/_components/toast.md
@@ -0,0 +1,42 @@
+---
+title: Toast
+summary: >-
+  A fixed bottom-right flash bar — the real pattern used for delete
+  confirmations. Auto-dismisses after roughly 8 seconds in practice; wire
+  that timer into the consuming app.
+---
+
+## Basic
+
+The example below stays in document flow so it does not cover the page;
+`.ad-toast--fixed` is what actually pins it bottom-right in a real app.
+
+{% example title="Toast" open %}
+<div class="ad-toast">
+  <span class="ad-toast__message">Deleted "Call Mom"</span>
+  <button type="button" class="ad-toast__action">Undo</button>
+  <button type="button" class="ad-toast__dismiss" aria-label="Dismiss">×</button>
+</div>
+{% endexample %}
+
+## Without an action
+
+{% example title="No action" %}
+<div class="ad-toast">
+  <span class="ad-toast__message">Series created</span>
+  <button type="button" class="ad-toast__dismiss" aria-label="Dismiss">×</button>
+</div>
+{% endexample %}
+
+## API
+
+<div class="ad-table-wrap">
+  <table class="ad-table ad-table--compact">
+    <thead><tr><th scope="col">Prop</th><th scope="col">Type</th><th scope="col">Notes</th></tr></thead>
+    <tbody>
+      <tr><td class="ad-token-name">message</td><td class="ad-table__code">string</td><td>Required.</td></tr>
+      <tr><td class="ad-token-name">actionLabel</td><td class="ad-table__code">string</td><td>Renders a destructive-coloured action, typically "Undo".</td></tr>
+      <tr><td class="ad-token-name">onDismiss</td><td class="ad-table__code">function</td><td></td></tr>
+    </tbody>
+  </table>
+</div>
diff --git a/_components/tooltip.md b/_components/tooltip.md
new file mode 100644
index 0000000..b74cc94
--- /dev/null
+++ b/_components/tooltip.md
@@ -0,0 +1,42 @@
+---
+title: Tooltip
+summary: >-
+  Dark mono tooltip on hover, positioned above the child. Pure CSS — shown on
+  hover and keyboard focus, no JavaScript required.
+---
+
+## Basic
+
+Hover or tab to the button below.
+
+{% example title="Tooltip" open %}
+<span class="ad-tooltip">
+  <button type="button" class="ad-icon-btn" aria-label="Delete">
+    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13"/></svg>
+  </button>
+  <span class="ad-tooltip__bubble">Delete</span>
+</span>
+{% endexample %}
+
+## On any element
+
+A Tooltip wraps whatever it labels — most often an [IconButton]({{ '/components/icon-button/' | relative_url }}), since that is the control with no visible text of its own.
+
+{% example title="On a badge" %}
+<span class="ad-tooltip">
+  <span class="ad-badge ad-badge--danger" tabindex="0">3d overdue</span>
+  <span class="ad-tooltip__bubble">Due Tuesday, was Saturday</span>
+</span>
+{% endexample %}
+
+## API
+
+<div class="ad-table-wrap">
+  <table class="ad-table ad-table--compact">
+    <thead><tr><th scope="col">Prop</th><th scope="col">Type</th><th scope="col">Notes</th></tr></thead>
+    <tbody>
+      <tr><td class="ad-token-name">label</td><td class="ad-table__code">string</td><td>Required.</td></tr>
+      <tr><td class="ad-token-name">children</td><td class="ad-table__code">node</td><td>The trigger element. Give it a real accessible name of its own — a tooltip is not one.</td></tr>
+    </tbody>
+  </table>
+</div>
diff --git a/_config.yml b/_config.yml
index 496aa4f..547bb15 100644
--- a/_config.yml
+++ b/_config.yml
@@ -1,9 +1,10 @@
 title: Arbitrary Definitions
-tagline: A small design system that admits what it is
+tagline: A personal umbrella, given a shared visual spine
 description: >-
-  Arbitrary Definitions is a documented design system — colour, type, space,
-  motion, and a dozen components — rendered from a single set of YAML token
-  files by a Jekyll site running on Ruby 4.0.
+  Arbitrary Definitions is a personal design system for self-hosted,
+  single-user tools — synthesized from three real running projects, not
+  written from a spec. Sixteen components and five foundations, rendered from
+  a single set of YAML token files by a Jekyll site running on Ruby 4.0.
 
 url: https://kejadlen.github.io
 baseurl: /arbitrary-definitions-design-system
diff --git a/_data/meta.yml b/_data/meta.yml
index 71e3fd1..78e3791 100644
--- a/_data/meta.yml
+++ b/_data/meta.yml
@@ -1 +1 @@
-version: "1.0.0"
+version: "2.0.0"
diff --git a/_data/nav.yml b/_data/nav.yml
index f60ad73..cd726cd 100644
--- a/_data/nav.yml
+++ b/_data/nav.yml
@@ -10,17 +10,31 @@
     - { text: Space,      url: /foundations/space/ }
     - { text: Elevation,  url: /foundations/elevation/ }
     - { text: Motion,     url: /foundations/motion/ }
-- title: Components
+- title: Core
   links:
     - { text: Button,     url: /components/button/ }
-    - { text: Field,      url: /components/field/ }
+    - { text: IconButton, url: /components/icon-button/ }
     - { text: Badge,      url: /components/badge/ }
-    - { text: Callout,    url: /components/callout/ }
+    - { text: Tag,        url: /components/tag/ }
     - { text: Card,       url: /components/card/ }
-    - { text: Table,      url: /components/table/ }
+    - { text: CardRow,    url: /components/card-row/ }
+- title: Forms
+  links:
+    - { text: Field,      url: /components/field/ }
+    - { text: Input,      url: /components/input/ }
+    - { text: Select,     url: /components/select/ }
+    - { text: Checkbox,   url: /components/checkbox/ }
+    - { text: Radio,      url: /components/radio/ }
+    - { text: Switch,     url: /components/switch/ }
+- title: Feedback
+  links:
+    - { text: Tooltip,    url: /components/tooltip/ }
+    - { text: Toast,      url: /components/toast/ }
+    - { text: Dialog,     url: /components/dialog/ }
+- title: Navigation
+  links:
     - { text: Tabs,       url: /components/tabs/ }
-    - { text: Progress,   url: /components/progress/ }
 - title: Patterns
   links:
-    - { text: Settings form, url: /patterns/settings-form/ }
-    - { text: Kitchen sink,  url: /patterns/kitchen-sink/ }
+    - { text: "Loop — task dashboard",  url: /patterns/loop/ }
+    - { text: "Archive — capture app",  url: /patterns/archive/ }
diff --git a/_data/tokens/color.yml b/_data/tokens/color.yml
index fb1f617..3a0e851 100644
--- a/_data/tokens/color.yml
+++ b/_data/tokens/color.yml
@@ -1,111 +1,73 @@
-# Raw palette ramps. These are the arbitrary part: hand-picked hexes that,
+# Raw palette ramps. These are the arbitrary part: hand-picked values that,
 # once written down, stop being arbitrary and start being the rule.
 #
 # Nothing in the site references a ramp step directly. Components only ever
 # reach for the semantic aliases defined in `semantic.yml`, which is what
-# makes retheming a one-file change.
+# makes retheming — or flipping to dark — a one-file change.
+#
+# Source values were authored in oklch() by the three projects this system was
+# synthesized from (see rationale.md). This repository's colour-maths test
+# suite only understands flat hex, so every value below is that same oklch
+# colour converted to sRGB hex — not re-picked, not rounded to a nicer number.
+#
+# `order` is a display index (0..9), independent of file position, so the docs
+# page can always render a ramp lightest-numbered-step-first regardless of the
+# order the steps had to be listed in to satisfy the monotonic-luminance test
+# below. `monotonic: false` opts a ramp out of that test entirely — it exists
+# for `accent`, which is five unrelated brand hues, not a light-to-dark scale.
 ramps:
-  - name: ink
-    label: Ink
+  - name: neutral
+    label: Neutral
     description: >-
-      A warm neutral ramp with a faint olive cast. Used for text, borders,
-      and every surface that is not trying to say anything.
+      Low-chroma warm-gray — the intersection of ketchup's plain grays,
+      quire's warm cream/dark palette, and domus's warm paper. Not stark
+      white or black, and not any one project's specific warmth. Ten steps,
+      not twelve: the source defines exactly 0/50/100/150/200/300/500/600/800/900
+      — steps 400 and 700 do not exist.
+    monotonic: true
     steps:
-      - { step: "50",  value: "#faf9f7" }
-      - { step: "100", value: "#f2f0ec" }
-      - { step: "150", value: "#e8e5df" }
-      - { step: "200", value: "#dcd8d0" }
-      - { step: "300", value: "#c3bdb2" }
-      - { step: "400", value: "#a29a8c" }
-      - { step: "500", value: "#78705f" }
-      - { step: "600", value: "#625b50" }
-      - { step: "700", value: "#4b463d" }
-      - { step: "800", value: "#35312b" }
-      - { step: "900", value: "#26231f" }
-      - { step: "950", value: "#171512" }
+      - { step: "50",  order: 1, value: "#fdfdfb", usage: "Card / surface stock — deliberately a hair lighter than the page it sits on." }
+      - { step: "0",   order: 0, value: "#faf9f7", usage: "Page background." }
+      - { step: "100", order: 2, value: "#f1f0ed", usage: "Recessed fill." }
+      - { step: "150", order: 3, value: "#e8e6e2", usage: "Recessed fill, deeper." }
+      - { step: "200", order: 4, value: "#dcdad7", usage: "Hairline border." }
+      - { step: "300", order: 5, value: "#c0bdb9", usage: "Rule / default border." }
+      - { step: "500", order: 6, value: "#888681", usage: "Muted-faint text. Intentionally sub-AA — see the Colour page." }
+      - { step: "600", order: 7, value: "#5f5d59", usage: "Muted / secondary text." }
+      - { step: "800", order: 8, value: "#2a2926", usage: "Strong text." }
+      - { step: "900", order: 9, value: "#100f0d", usage: "Ink — primary text, near-black." }
 
-  - name: persimmon
-    label: Persimmon
+  - name: neutral_dark
+    label: Neutral — dark
     description: >-
-      The one loud hue in the system. Reserved for primary actions, the
-      current page marker, and links. If everything is persimmon, nothing is.
+      The same low-chroma warm-gray scale, inverted, matching quire's
+      OS-preference dark palette. Same ten step numbers as `neutral`; `0` is
+      the darkest step here (the dark-theme page background) rather than the
+      lightest.
+    monotonic: true
     steps:
-      - { step: "50",  value: "#fff4ed" }
-      - { step: "100", value: "#ffe6d5" }
-      - { step: "200", value: "#fec9aa" }
-      - { step: "300", value: "#fda474" }
-      - { step: "400", value: "#fb743c" }
-      - { step: "500", value: "#f95116" }
-      - { step: "600", value: "#ea360c" }
-      - { step: "700", value: "#c2260c" }
-      - { step: "800", value: "#9a2012" }
-      - { step: "900", value: "#7c1d12" }
-      - { step: "950", value: "#430b07" }
+      - { step: "900", order: 9, value: "#e9e8e6", usage: "Primary text in dark mode, near-white." }
+      - { step: "800", order: 8, value: "#d9d7d4", usage: "Strong text in dark mode." }
+      - { step: "600", order: 7, value: "#a09e9a", usage: "Muted / secondary text in dark mode." }
+      - { step: "500", order: 6, value: "#7c7a76", usage: "Muted-faint text in dark mode." }
+      - { step: "300", order: 5, value: "#44423f", usage: "Rule / default border in dark mode." }
+      - { step: "200", order: 4, value: "#2c2b29", usage: "Hairline border in dark mode." }
+      - { step: "150", order: 3, value: "#252421", usage: "Recessed fill, deeper, in dark mode." }
+      - { step: "100", order: 2, value: "#1e1d1a", usage: "Recessed fill in dark mode." }
+      - { step: "50",  order: 1, value: "#151412", usage: "Card / surface stock in dark mode." }
+      - { step: "0",   order: 0, value: "#0e0d0b", usage: "Page background in dark mode." }
 
-  - name: verdant
-    label: Verdant
-    description: A mossy green, desaturated enough to sit next to persimmon without arguing.
-    steps:
-      - { step: "50",  value: "#f1faf2" }
-      - { step: "100", value: "#ddf2e0" }
-      - { step: "150", value: "#cdebd2" }
-      - { step: "200", value: "#bce4c3" }
-      - { step: "300", value: "#8ecf9c" }
-      - { step: "400", value: "#5bb16f" }
-      - { step: "500", value: "#389350" }
-      - { step: "600", value: "#27763f" }
-      - { step: "700", value: "#205e34" }
-      - { step: "800", value: "#1c4b2c" }
-      - { step: "900", value: "#183e26" }
-      - { step: "950", value: "#0a2313" }
-
-  - name: amber
-    label: Amber
-    description: Caution. Warm enough to read as a warning, dark enough to hold white text from 600 up.
-    steps:
-      - { step: "50",  value: "#fef8ec" }
-      - { step: "100", value: "#fbecc9" }
-      - { step: "150", value: "#f9e2ac" }
-      - { step: "200", value: "#f7d78e" }
-      - { step: "300", value: "#f2bc52" }
-      - { step: "400", value: "#eea32b" }
-      - { step: "500", value: "#e08211" }
-      - { step: "600", value: "#c45f0c" }
-      - { step: "700", value: "#a3410e" }
-      - { step: "800", value: "#853312" }
-      - { step: "900", value: "#6f2b12" }
-      - { step: "950", value: "#401405" }
-
-  - name: crimson
-    label: Crimson
-    description: Destructive and error states. Deliberately cooler than persimmon so the two never blur.
-    steps:
-      - { step: "50",  value: "#fef2f3" }
-      - { step: "100", value: "#fde3e5" }
-      - { step: "150", value: "#fcd7db" }
-      - { step: "200", value: "#fbccd1" }
-      - { step: "300", value: "#f7a5ae" }
-      - { step: "400", value: "#f17385" }
-      - { step: "500", value: "#e6455f" }
-      - { step: "600", value: "#d22548" }
-      - { step: "700", value: "#b01a3c" }
-      - { step: "800", value: "#931939" }
-      - { step: "900", value: "#7e1936" }
-      - { step: "950", value: "#470819" }
-
-  - name: azure
-    label: Azure
-    description: Informational states and the focus ring. The only blue in the building.
+  - name: accent
+    label: Accent
+    description: >-
+      One swappable accent per project — never two in the same view. Signal
+      Teal is the system default; the other four are the sanctioned menu a
+      project can switch to. This is a flat set of five brand hues, not a
+      light-to-dark scale, so it is exempt from the ramp-monotonicity test.
+    monotonic: false
     steps:
-      - { step: "50",  value: "#eff7ff" }
-      - { step: "100", value: "#dbecfe" }
-      - { step: "150", value: "#cde5fe" }
-      - { step: "200", value: "#bfdffe" }
-      - { step: "300", value: "#93cbfd" }
-      - { step: "400", value: "#60affa" }
-      - { step: "500", value: "#3b8ef6" }
-      - { step: "600", value: "#2571eb" }
-      - { step: "700", value: "#1d5bd8" }
-      - { step: "800", value: "#1e4baf" }
-      - { step: "900", value: "#1e428a" }
-      - { step: "950", value: "#172a54" }
+      - { step: "teal",     order: 0, value: "#3a6a63", usage: "Default. Neutral, technical." }
+      - { step: "clay",     order: 1, value: "#9a5a3c", usage: "Warm, archival — domus's flavour." }
+      - { step: "ink-blue", order: 2, value: "#2b4a78", usage: "Sober, utilitarian." }
+      - { step: "ochre",    order: 3, value: "#93701f", usage: "Dry, papery." }
+      - { step: "plum",     order: 4, value: "#6a3a5c", usage: "Quiet, personal." }
diff --git a/_data/tokens/contrast.yml b/_data/tokens/contrast.yml
index 93551c6..381a4ae 100644
--- a/_data/tokens/contrast.yml
+++ b/_data/tokens/contrast.yml
@@ -1,36 +1,36 @@
 # The contrast contract.
 #
-# Colour is the one part of a design system that is not arbitrary: a ratio is a
-# fact about two colours and a pair of eyes. So these pairings are asserted by
-# test/test_token_set.rb in both themes, and the same list renders the contract
-# table on the Colour page. Adding a pair here adds it to the deploy gate.
+# Colour is the one part of a design system that is not arbitrary: a ratio is
+# a fact about two colours and a pair of eyes. So these pairings are asserted
+# by test/test_token_set.rb in both themes, and the same list renders the
+# contract table on the Colour page. Adding a pair here adds it to the deploy
+# gate.
+#
+# Two things this system deliberately does NOT assert, both explained on the
+# Colour page rather than forced to pass:
+#   - `color-text-tertiary` (neutral.500) is 3.46:1 on the page in light mode —
+#     under the 4.5:1 floor. The source documents that step as "muted-faint
+#     text", reserved for placeholders and metadata, never body copy. That is
+#     a real, intentional property of the source palette, not a bug to fix by
+#     darkening the step.
+#   - Border contrast. `color-border-hairline` / `color-border-default` sit
+#     well under the 3:1 WCAG 1.4.11 asks of a meaningful boundary — the
+#     source's own words are "hairline (1px) borders and dividers do most of
+#     the elevation work", i.e. borders are deliberately decorative here. The
+#     source never claims a 3:1 bar for them, so this contract does not
+#     invent one.
+#
+# `color-accent-ink`, `color-accent-soft`, `color-success-bg`, and
+# `color-danger-bg` are `color-mix()` expressions, not flat hex, so
+# `lib/arbitrary_definitions/color_math.rb` cannot measure them — none of the
+# four appears on either side of a pair below.
 text:
   label: Text
   minimum: 4.5
   standard: WCAG 2.1 SC 1.4.3 (normal-size text)
   pairs:
-    - { foreground: color-text, background: color-canvas, note: Body copy on the page. }
-    - { foreground: color-text, background: color-surface, note: Body copy on a card or table. }
-    - { foreground: color-text-muted, background: color-canvas, note: Helper text and captions. }
-    - { foreground: color-text-subtle, background: color-canvas, note: Placeholders and metadata. }
-    - { foreground: color-accent, background: color-canvas, note: Links in running text. }
-    - { foreground: color-on-accent, background: color-accent, note: The label on a primary button. }
-    - { foreground: color-positive, background: color-positive-subtle, note: Positive callouts and badges. }
-    - { foreground: color-caution, background: color-caution-subtle, note: Caution callouts and badges. }
-    - { foreground: color-critical, background: color-critical-subtle, note: Critical callouts and badges. }
-    - { foreground: color-info, background: color-info-subtle, note: Informational callouts and badges. }
-    - { foreground: color-on-critical, background: color-critical-solid, note: The label on a destructive button. }
-    - { foreground: color-text-inverse, background: color-accent, note: A solid accent badge. }
-    - { foreground: color-text-inverse, background: color-positive, note: A solid positive badge. }
-    - { foreground: color-text-inverse, background: color-caution, note: A solid caution badge. }
-    - { foreground: color-text-inverse, background: color-critical, note: A solid critical badge. }
-    - { foreground: color-text-inverse, background: color-info, note: A solid info badge. }
-    - { foreground: color-text-inverse, background: color-text-muted, note: A solid neutral badge. }
-
-non_text:
-  label: Non-text
-  minimum: 3.0
-  standard: WCAG 2.1 SC 1.4.11 (user interface components)
-  pairs:
-    - { foreground: color-border-strong, background: color-surface, note: The edge of a control a reader has to find. }
-    - { foreground: color-focus, background: color-canvas, note: The focus ring against the page. }
+    - { foreground: color-text-primary, background: color-surface-page, note: Body copy, headings, and control labels on the page. }
+    - { foreground: color-text-secondary, background: color-surface-page, note: Supporting copy, hints, and mono meta values. }
+    - { foreground: color-accent-fg, background: color-accent, note: The label on a primary or dark-variant Button. }
+    - { foreground: color-success, background: color-accent-fg, note: Success text on a light backdrop — proxied by color-accent-fg, the system's only literal-white token, since color-success-bg is an unmeasurable color-mix() tint. }
+    - { foreground: color-danger, background: color-accent-fg, note: Overdue/destructive text on a light backdrop, proxied the same way as success above. }
diff --git a/_data/tokens/scale.yml b/_data/tokens/scale.yml
index 51350fa..6620585 100644
--- a/_data/tokens/scale.yml
+++ b/_data/tokens/scale.yml
@@ -1,121 +1,99 @@
-# Non-colour tokens. Same deal: `_plugins/token_generator.rb` turns each
-# group into a block of CSS custom properties under `:root`.
-
+# Non-colour tokens. `_plugins/token_generator.rb` turns most groups below
+# into a block of CSS custom properties under `:root`; `typography.styles` is
+# the one exception — those eight are compound rules (family + size + weight +
+# line-height + tracking in one declaration, exactly as the source ships
+# them), so they are written directly into `_sass/base/_typography.scss`
+# rather than compiled from here. This file is still their source of record:
+# the typography foundation page renders its specimen table from `styles`.
 typography:
   label: Typography
+  description: >-
+    Three families and eight fluid Utopia steps (320→1240px viewport, 18→20px
+    body). Only one webfont is loaded — IBM Plex Mono; sans is a web-safe
+    system stack and serif is an opt-in reading variant, not loaded by default.
+  webfont_import: "@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&display=swap');"
   families:
-    - token: font-display
-      value: "\"Fraunces\", \"Iowan Old Style\", Georgia, serif"
-      usage: Wordmark and page titles. Optical sizing does the heavy lifting.
     - token: font-sans
-      value: "\"Inter\", -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif"
-      usage: Everything else. Body copy, UI labels, tables.
+      value: '-apple-system, "Helvetica Neue", Helvetica, Arial, system-ui, sans-serif'
+      usage: Interface text — every component, every screen. Web-safe; nothing is loaded for it.
     - token: font-mono
-      value: "\"JetBrains Mono\", ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, monospace"
-      usage: Code, token names, and any value the reader might need to copy.
-  sizes:
-    - { token: text-2xs, value: "0.6875rem", px: 11, usage: Overlines and table micro-labels. Uppercase only. }
-    - { token: text-xs,  value: "0.75rem",   px: 12, usage: Badges, captions, keyboard hints. }
-    - { token: text-sm,  value: "0.875rem",  px: 14, usage: Dense UI — small buttons, helper text, table cells. }
-    - { token: text-md,  value: "1rem",      px: 16, usage: The default. Body copy and standard controls. }
-    - { token: text-lg,  value: "1.125rem",  px: 18, usage: Lead paragraphs and large controls. }
-    - { token: text-xl,  value: "1.375rem",  px: 22, usage: Section headings (h3). }
-    - { token: text-2xl, value: "1.75rem",   px: 28, usage: Subsection headings (h2). }
-    - { token: text-3xl, value: "2.25rem",   px: 36, usage: Page titles (h1). }
-    - { token: text-4xl, value: "3rem",      px: 48, usage: Hero headline. }
-    - { token: text-5xl, value: "3.75rem",   px: 60, usage: One per site, at most. }
-  line_heights:
-    - { token: leading-tight,   value: "1.15", usage: Display sizes, 28px and up. }
-    - { token: leading-snug,    value: "1.3",  usage: Headings and multi-line labels. }
-    - { token: leading-normal,  value: "1.55", usage: Body copy. }
-    - { token: leading-relaxed, value: "1.75", usage: Long-form prose columns. }
-  weights:
-    - { token: weight-regular,  value: "400", usage: Body copy. }
-    - { token: weight-medium,   value: "500", usage: UI labels and table headers. }
-    - { token: weight-semibold, value: "600", usage: Buttons and headings. }
-    - { token: weight-bold,     value: "700", usage: Emphasis inside running text. }
-  tracking:
-    - { token: tracking-tight,  value: "-0.02em", usage: Sizes 28px and up, where default spacing looks loose. }
-    - { token: tracking-normal, value: "0",       usage: Everything from 12px to 22px. }
-    - { token: tracking-wide,   value: "0.08em",  usage: Uppercase overlines only. }
+      value: '"IBM Plex Mono", ui-monospace, "SF Mono", Menlo, Consolas, "Courier New", monospace'
+      usage: Every label, meta value, technical/catalog value, and code sample. The system's one loaded webfont.
+    - token: font-serif
+      value: 'Georgia, "Times New Roman", "Iowan Old Style", serif'
+      usage: Opt-in reading variant for a project that wants an editorial, archival feel (domus-style). Not loaded by default; web-safe only.
+  steps:
+    - { token: step--2, value: "clamp(0.7813rem, 0.7736rem + 0.0341vw, 0.8rem)",        px: "12.5 → 12.8", usage: "type-label — eyebrows, catalog labels, meta." }
+    - { token: step--1, value: "clamp(0.9375rem, 0.9119rem + 0.1136vw, 1rem)",          px: "15 → 16",     usage: "type-body-sm and type-mono — secondary copy, technical values." }
+    - { token: step-0,  value: "clamp(1.125rem, 1.0739rem + 0.2273vw, 1.25rem)",        px: "18 → 20",     usage: "type-body — the base size, and the body element's own size." }
+    - { token: step-1,  value: "clamp(1.35rem, 1.2631rem + 0.3864vw, 1.5625rem)",       px: "22 → 25",     usage: "type-h3 — card and subsection headings." }
+    - { token: step-2,  value: "clamp(1.62rem, 1.4837rem + 0.6057vw, 1.9531rem)",       px: "26 → 31",     usage: "type-h2 — section headings." }
+    - { token: step-3,  value: "clamp(1.944rem, 1.7405rem + 0.9044vw, 2.4414rem)",      px: "31 → 39",     usage: "type-h1 — screen titles." }
+    - { token: step-4,  value: "clamp(2.3328rem, 2.0387rem + 1.3072vw, 3.0518rem)",     px: "37 → 49",     usage: "type-display — one hero title per screen, at most." }
+    - { token: step-5,  value: "clamp(2.7994rem, 2.384rem + 1.8461vw, 3.8147rem)",      px: "45 → 61",     usage: "Reserved headroom. No compound style claims it yet." }
+  styles:
+    - { class: type-display, family: font-sans, weight: 600, size: step-4,  line_height: "1.08", tracking: "-0.015em", transform: none,    usage: "Page-level hero title, once per screen." }
+    - { class: type-h1,      family: font-sans, weight: 600, size: step-3,  line_height: "1.12", tracking: "-0.012em", transform: none,    usage: "Screen title." }
+    - { class: type-h2,      family: font-sans, weight: 600, size: step-2,  line_height: "1.16", tracking: "-0.01em",  transform: none,    usage: "Section heading." }
+    - { class: type-h3,      family: font-sans, weight: 600, size: step-1,  line_height: "1.25", tracking: "normal",   transform: none,    usage: "Card / subsection heading." }
+    - { class: type-body,    family: font-sans, weight: 400, size: step-0,  line_height: "1.55", tracking: "normal",   transform: none,    usage: "Body copy." }
+    - { class: type-body-sm, family: font-sans, weight: 400, size: step--1, line_height: "1.5",  tracking: "normal",   transform: none,    usage: "Secondary copy, hints." }
+    - { class: type-label,   family: font-mono, weight: 500, size: step--2, line_height: "1",    tracking: "0.08em",   transform: uppercase, usage: "Eyebrows, catalog labels, meta — SAVED, RECOMMENDED, OVERDUE." }
+    - { class: type-mono,    family: font-mono, weight: 400, size: step--1, line_height: "1.5",  tracking: "normal",   transform: none,    usage: "Technical values, IDs, dates, code." }
 
 space:
   label: Space
   description: >-
-    A 4px base with a ramp that thins out as it grows. Small increments matter
-    inside a control; nobody can see the difference between 76px and 80px.
+    Nine fluid Utopia steps (320→1240px viewport), the same shared config as
+    type. 3xs through 3xl — no numbered scale, because there is no fixed base
+    unit here to count multiples of.
   scale:
-    - { token: space-0,  value: "0",       px: 0 }
-    - { token: space-1,  value: "0.25rem", px: 4 }
-    - { token: space-2,  value: "0.5rem",  px: 8 }
-    - { token: space-3,  value: "0.75rem", px: 12 }
-    - { token: space-4,  value: "1rem",    px: 16 }
-    - { token: space-5,  value: "1.5rem",  px: 24 }
-    - { token: space-6,  value: "2rem",    px: 32 }
-    - { token: space-7,  value: "2.5rem",  px: 40 }
-    - { token: space-8,  value: "3rem",    px: 48 }
-    - { token: space-9,  value: "4rem",    px: 64 }
-    - { token: space-10, value: "6rem",    px: 96 }
-    - { token: space-11, value: "8rem",    px: 128 }
+    - { token: space-3xs, value: "clamp(0.3125rem, 0.3125rem + 0vw, 0.3125rem)",       px: "5",       usage: "The tightest gap — an icon to its label inside a small control." }
+    - { token: space-2xs, value: "clamp(0.5625rem, 0.5369rem + 0.1136vw, 0.625rem)",   px: "9 → 10",  usage: "Field-internal gaps, badge and tag padding." }
+    - { token: space-xs,  value: "clamp(0.875rem, 0.8494rem + 0.1136vw, 0.9375rem)",   px: "14 → 15", usage: "Control padding-inline, row-internal gaps, CardRow padding." }
+    - { token: space-s,   value: "clamp(1.125rem, 1.0739rem + 0.2273vw, 1.25rem)",     px: "18 → 20", usage: "Field-to-field spacing, Dialog title/footer padding." }
+    - { token: space-m,   value: "clamp(1.6875rem, 1.6108rem + 0.3409vw, 1.875rem)",   px: "27 → 30", usage: "Card body padding, Dialog body padding — the system's default inset." }
+    - { token: space-l,   value: "clamp(2.25rem, 2.1477rem + 0.4545vw, 2.5rem)",       px: "36 → 40", usage: "Page gutters, section-to-section gaps." }
+    - { token: space-xl,  value: "clamp(3.375rem, 3.2216rem + 0.6818vw, 3.75rem)",     px: "54 → 60", usage: "Major section spacing — the gap above a dashboard's task list." }
+    - { token: space-2xl, value: "clamp(4.5rem, 4.2955rem + 0.9091vw, 5rem)",          px: "72 → 80", usage: "Page-level rhythm." }
+    - { token: space-3xl, value: "clamp(6.75rem, 6.4432rem + 1.3636vw, 7.5rem)",       px: "108 → 120", usage: "Hero and empty-state breathing room." }
 
 radius:
   label: Radius
-  description: Corner rounding. The system tops out at 16px; anything softer reads as a toy.
+  description: >-
+    Small and quiet: a 4/6/10px scale, kept modest so it reads as a neutral
+    default rather than any one source project's specific softness. Nothing
+    pill-shaped except true pills.
   scale:
-    - { token: radius-none, value: "0",      usage: Tables, full-bleed bands. }
-    - { token: radius-xs,   value: "3px",    usage: Badges and inline code. }
-    - { token: radius-sm,   value: "5px",    usage: Inputs, buttons, checkboxes. }
-    - { token: radius-md,   value: "8px",    usage: Cards and callouts. }
-    - { token: radius-lg,   value: "12px",   usage: Panels and modals. }
-    - { token: radius-xl,   value: "16px",   usage: Hero surfaces. }
-    - { token: radius-full, value: "999px",  usage: Pills, avatars, switches. }
+    - { token: radius-sm,   value: "4px",   usage: "Buttons, inputs, badges, tags." }
+    - { token: radius-md,   value: "6px",   usage: "IconButton, the segmented-tabs track, avatar chips." }
+    - { token: radius-lg,   value: "10px",  usage: "Cards, dialogs, toasts, photo slots — the system's \"surface\" radius." }
+    - { token: radius-full, value: "999px", usage: "True pills only — Badge, Tag, Switch, round avatars." }
+    - { token: border-w,    value: "1px",   usage: "The one hairline weight in the system." }
 
 elevation:
   label: Elevation
   description: >-
-    Five levels, each a two-shadow stack: a tight contact shadow plus a wider
-    ambient one. Levels are about hierarchy, not distance.
+    Two levels only, as every source project uses: flat — no shadow, a
+    hairline border does the work, and the default everywhere — and one
+    `shadow-float` for the single most-elevated element in a view: a Dialog,
+    a Toast, a floating menu.
   scale:
-    - token: elevation-0
-      value: "none"
-      usage: Flush with the canvas. Bordered cards live here.
-    - token: elevation-1
-      value: "0 1px 2px rgba(23, 21, 18, 0.06), 0 1px 3px rgba(23, 21, 18, 0.08)"
-      usage: Resting cards that need to separate from a busy background.
-    - token: elevation-2
-      value: "0 2px 4px rgba(23, 21, 18, 0.06), 0 4px 12px rgba(23, 21, 18, 0.08)"
-      usage: Dropdowns, popovers, sticky headers once they detach.
-    - token: elevation-3
-      value: "0 4px 8px rgba(23, 21, 18, 0.07), 0 12px 28px rgba(23, 21, 18, 0.12)"
-      usage: Dialogs and side sheets.
-    - token: elevation-4
-      value: "0 8px 16px rgba(23, 21, 18, 0.08), 0 24px 56px rgba(23, 21, 18, 0.16)"
-      usage: Full-screen overlays. One per view.
+    - token: shadow-sm
+      value: "0 1px 2px rgba(20, 18, 14, 0.05)"
+      usage: Barely-there separation — a Switch thumb, a focused Input.
+    - token: shadow-float
+      value: "0 2px 6px rgba(20, 18, 14, 0.06), 0 10px 24px -12px rgba(20, 18, 14, 0.18)"
+      usage: The one floating layer in a view. Apply it to at most one element on screen at a time.
 
 motion:
   label: Motion
   description: >-
-    Durations are short because this is an interface, not a title sequence.
-    Every one of these is muted to 0ms under `prefers-reduced-motion`.
+    Two durations and one easing curve — this is an interface, not a title
+    sequence. Short ease transitions on hover/focus only; no bounce, no
+    spring, no page-transition choreography.
   durations:
-    - { token: duration-instant, value: "0ms",   usage: State changes that must feel like they already happened. }
-    - { token: duration-fast,    value: "120ms", usage: Hover, focus, and colour transitions. }
-    - { token: duration-base,    value: "200ms", usage: Expanding, revealing, switching. }
-    - { token: duration-slow,    value: "320ms", usage: Anything that crosses more than half the viewport. }
-    - { token: duration-spin,    value: "700ms", usage: One rotation of the button spinner. }
-    - { token: duration-loop,    value: "1400ms", usage: One pass of an indeterminate indicator. }
+    - { token: dur-fast, value: "0.12s", usage: "Hover and focus colour/border transitions — the default." }
+    - { token: dur-base, value: "0.16s", usage: "Switch track and thumb — the one moving part in the system." }
   easings:
-    - { token: ease-standard, value: "cubic-bezier(0.2, 0, 0, 1)",    usage: The default. Fast out, settles gently. }
-    - { token: ease-entrance, value: "cubic-bezier(0, 0, 0.2, 1)",    usage: Elements arriving on screen. }
-    - { token: ease-exit,     value: "cubic-bezier(0.4, 0, 1, 1)",    usage: Elements leaving. Quicker than arriving. }
-    - { token: ease-spring,   value: "cubic-bezier(0.34, 1.4, 0.64, 1)", usage: Toggles and switches. The only overshoot in the system. }
-
-layout:
-  label: Layout
-  description: Container widths and the one breakpoint that actually changes the page.
-  scale:
-    - { token: measure-prose,  value: "68ch",   usage: Max width for running text. Roughly 75 characters. }
-    - { token: width-content,  value: "50rem",  usage: Documentation column. }
-    - { token: width-wide,     value: "72rem",  usage: Full site shell including the sidebar. }
-    - { token: sidebar-width,  value: "16rem",  usage: Persistent navigation rail. }
-    - { token: header-height,  value: "3.5rem", usage: Sticky masthead. }
+    - { token: ease, value: "cubic-bezier(0.2, 0, 0, 1)", usage: "The only easing curve in the system. Fast out, settles gently, applied to every transition." }
diff --git a/_data/tokens/semantic.yml b/_data/tokens/semantic.yml
index 542f9cc..8d4d905 100644
--- a/_data/tokens/semantic.yml
+++ b/_data/tokens/semantic.yml
@@ -1,176 +1,113 @@
 # Semantic colour aliases. Every component reads from this file and never
-# from a raw ramp. Values are either a `ramp.step` reference or a literal
-# CSS colour; `_plugins/token_generator.rb` resolves references at build time.
+# from a raw ramp. Values are either a `ramp.step` reference or a literal CSS
+# colour; `_plugins/token_generator.rb` resolves references at build time.
 #
 # `light` and `dark` are emitted as `:root` and `[data-theme="dark"]` blocks.
+# Two groups below — Accent's `-ink`/`-soft` pair and Status's `-bg` pair —
+# are literal `color-mix()` expressions rather than ramp references, exactly
+# mirroring the source's own `tokens/colors.css`. A `color-mix()` string is
+# not a flat hex, so `lib/arbitrary_definitions/color_math.rb` declines to
+# measure it (by design — see `HEX_PATTERN` / `UNMEASURABLE`); none of those
+# four tokens appears in `contrast.yml` for that reason.
 groups:
   - name: Surfaces
     id: surface
-    description: Backgrounds, from the page itself up to the topmost floating layer.
+    description: Backgrounds, from the page itself up to a card sitting on it.
     tokens:
-      - token: color-canvas
-        light: ink.50
-        dark: ink.950
-        usage: The page. Nothing sits behind it.
-      - token: color-surface
-        light: "#ffffff"
-        dark: ink.900
-        usage: Cards, panels, table bodies — anything that reads as a sheet on the canvas.
-      - token: color-surface-sunken
-        light: ink.100
-        dark: "#100e0c"
-        usage: Wells, code blocks, inset examples. Recessed relative to the canvas.
-      - token: color-surface-raised
-        light: "#ffffff"
-        dark: ink.800
-        usage: Menus, popovers, tooltips. Pairs with elevation 2 or higher.
-      - token: color-surface-hover
-        light: ink.100
-        dark: ink.800
-        usage: The wash under a hovered row or list item.
+      - token: color-surface-page
+        light: neutral.0
+        dark: neutral_dark.0
+        usage: The page itself. Nothing sits behind it.
+      - token: color-surface-card
+        light: neutral.50
+        dark: neutral_dark.50
+        usage: Cards, dialogs, toasts — anything that reads as a sheet on the page.
+      - token: color-surface-fill
+        light: neutral.100
+        dark: neutral_dark.100
+        usage: Recessed fill — a hovered row, a segmented-tab track, a photo placeholder.
+      - token: color-surface-fill-2
+        light: neutral.150
+        dark: neutral_dark.150
+        usage: A second, deeper fill step — the diagonal-grain stripe inside an empty photo slot.
 
   - name: Borders
     id: border
-    description: Three weights, because two was never enough and four was too many.
+    description: Two weights. Hairlines do most of the elevation work in this system, so there was never a reason for a third.
     tokens:
-      - token: color-border-subtle
-        light: ink.150
-        dark: ink.800
-        usage: Hairlines between rows and inside dense tables.
-      - token: color-border
-        light: ink.200
-        dark: ink.700
-        usage: The default. Card edges, input outlines, dividers.
-      - token: color-border-strong
-        light: ink.500
-        dark: ink.400
-        usage: Hovered inputs and anything that needs to look grabbable.
+      - token: color-border-hairline
+        light: neutral.200
+        dark: neutral_dark.200
+        usage: Card edges, row dividers, dialog dividers. The default edge everywhere.
+      - token: color-border-default
+        light: neutral.300
+        dark: neutral_dark.300
+        usage: Input and button borders — anything a reader has to recognise as a control.
 
   - name: Text
     id: text
-    description: Four levels of emphasis. Past four, readers stop perceiving a hierarchy.
+    description: Three levels of emphasis, deliberately not four. Past three, a reader stops perceiving a hierarchy and starts perceiving noise.
     tokens:
-      - token: color-text
-        light: ink.900
-        dark: ink.100
-        usage: Body copy and headings.
-      - token: color-text-muted
-        light: ink.600
-        dark: ink.300
-        usage: Supporting copy, table captions, helper text.
-      - token: color-text-subtle
-        light: ink.500
-        dark: ink.400
-        usage: Placeholders, disabled labels, metadata nobody reads on purpose.
-      - token: color-text-inverse
-        light: ink.50
-        dark: ink.950
-        usage: Text on a filled accent or inverted surface.
+      - token: color-text-primary
+        light: neutral.900
+        dark: neutral_dark.900
+        usage: Body copy, headings, and every control label.
+      - token: color-text-secondary
+        light: neutral.600
+        dark: neutral_dark.600
+        usage: Supporting copy, hints, mono meta values, type-label's colour.
+      - token: color-text-tertiary
+        light: neutral.500
+        dark: neutral_dark.500
+        usage: >-
+          Placeholders and de-emphasised metadata only — never body text. This
+          step is intentionally sub-AA against the page; see the caveat on the
+          Colour page.
 
   - name: Accent
     id: accent
-    description: Persimmon, rationed.
+    description: >-
+      One swappable accent per project, held constant across a whole view.
+      `-ink` and `-soft` re-derive from `--ad-color-accent` via `color-mix()`
+      exactly as the source computes them, so retheming the accent retones
+      both automatically.
     tokens:
       - token: color-accent
-        light: persimmon.700
-        dark: persimmon.400
-        usage: Primary buttons, links, the active nav marker.
-      - token: color-accent-hover
-        light: persimmon.800
-        dark: persimmon.300
-        usage: Hover and active states for accent surfaces.
-      - token: color-accent-subtle
-        light: persimmon.50
-        dark: persimmon.950
-        usage: Tinted backgrounds — selected rows, accent callouts.
-      - token: color-accent-border
-        light: persimmon.200
-        dark: persimmon.800
-        usage: Edges on accent-tinted surfaces.
-      - token: color-on-accent
+        light: accent.teal
+        dark: accent.teal
+        usage: Primary buttons, links, focus affordance, the active tab underline. Never two accents in one view.
+      - token: color-accent-fg
         light: "#ffffff"
-        dark: persimmon.950
-        usage: Text and icons sitting on a filled accent.
+        dark: "#ffffff"
+        usage: Label and icon colour on a filled accent surface. Fixed white, not a ramp reference.
+      - token: color-accent-ink
+        light: "color-mix(in oklch, var(--ad-color-accent) 78%, black)"
+        dark: "color-mix(in oklch, var(--ad-color-accent) 78%, black)"
+        usage: Text sitting on an `-soft` accent tint — an IconButton's active state, an accent avatar's initials.
+      - token: color-accent-soft
+        light: "color-mix(in oklch, var(--ad-color-accent) 12%, white)"
+        dark: "color-mix(in oklch, var(--ad-color-accent) 12%, white)"
+        usage: The tint behind an accent Badge, a toggled IconButton, an accent avatar chip.
 
   - name: Status
     id: status
     description: >-
-      Four states, each with a foreground, a tint, and an edge. Components take
-      the whole triple so a callout and a badge can never drift apart.
-    tokens:
-      - token: color-positive
-        light: verdant.700
-        dark: verdant.300
-        usage: Success messaging and healthy indicators.
-      - token: color-positive-subtle
-        light: verdant.50
-        dark: verdant.950
-        usage: Tint behind positive content.
-      - token: color-positive-border
-        light: verdant.200
-        dark: verdant.800
-        usage: Edge on positive surfaces.
-      - token: color-caution
-        light: amber.700
-        dark: amber.300
-        usage: Warnings that do not block the user.
-      - token: color-caution-subtle
-        light: amber.50
-        dark: amber.950
-        usage: Tint behind caution content.
-      - token: color-caution-border
-        light: amber.200
-        dark: amber.800
-        usage: Edge on caution surfaces.
-      - token: color-critical
-        light: crimson.700
-        dark: crimson.300
-        usage: Errors, validation failures, destructive actions.
-      - token: color-critical-subtle
-        light: crimson.50
-        dark: crimson.950
-        usage: Tint behind critical content.
-      - token: color-critical-border
-        light: crimson.200
-        dark: crimson.800
-        usage: Edge on critical surfaces.
-      - token: color-critical-solid
-        light: crimson.700
-        dark: crimson.600
-        usage: >-
-          Fill for a destructive button. Distinct from `color-critical`, which is
-          a text colour and goes light in the dark theme — filling a button with
-          it would put white on pink.
-      - token: color-on-critical
-        light: "#ffffff"
-        dark: "#ffffff"
-        usage: Label on a filled destructive button.
-      - token: color-info
-        light: azure.700
-        dark: azure.300
-        usage: Neutral notices and tips.
-      - token: color-info-subtle
-        light: azure.50
-        dark: azure.950
-        usage: Tint behind informational content.
-      - token: color-info-border
-        light: azure.200
-        dark: azure.800
-        usage: Edge on informational surfaces.
-
-  - name: Interaction
-    id: interaction
-    description: The bits that only exist while a finger or a caret is involved.
+      Exactly two states, both fixed — they read the same in every project and
+      are never swapped the way the accent is.
     tokens:
-      - token: color-focus
-        light: azure.500
-        dark: azure.400
-        usage: The focus ring. One colour, system-wide, never restyled per component.
-      - token: color-selection
-        light: persimmon.100
-        dark: persimmon.900
-        usage: Text selection highlight.
-      - token: color-overlay
-        light: "rgba(38, 35, 31, 0.5)"
-        dark: "rgba(0, 0, 0, 0.65)"
-        usage: Scrim behind modal surfaces.
+      - token: color-success
+        light: "#3f7a4a"
+        dark: "#3f7a4a"
+        usage: Success text and icons — a completed task's check, a saved-state badge.
+      - token: color-success-bg
+        light: "color-mix(in oklch, var(--ad-color-success) 12%, white)"
+        dark: "color-mix(in oklch, var(--ad-color-success) 12%, white)"
+        usage: Tint behind a success Badge. Mixed against literal white, so it stays constant across themes.
+      - token: color-danger
+        light: "#ad3a2c"
+        dark: "#ad3a2c"
+        usage: Overdue/destructive text, the Toast undo action, a danger Button's fill.
+      - token: color-danger-bg
+        light: "color-mix(in oklch, var(--ad-color-danger) 10%, white)"
+        dark: "color-mix(in oklch, var(--ad-color-danger) 10%, white)"
+        usage: Tint behind a danger Badge. Mixed against literal white, so it stays constant across themes.
diff --git a/_foundations/color.md b/_foundations/color.md
index 8e1de9c..6414a60 100644
--- a/_foundations/color.md
+++ b/_foundations/color.md
@@ -1,46 +1,49 @@
 ---
 title: Colour
 summary: >-
-  Six ramps and thirty-two semantic aliases. Components never touch a ramp
-  directly, which is the only reason the dark theme is one file and not a rewrite.
+  Two neutral ramps, five accents, two fixed semantic colours, seventeen
+  semantic aliases. Low-chroma warm-gray, never stark white or black.
 ---
 
 ## The two layers
 
 The system separates *what a colour is* from *what a colour is for*.
 
-The **ramps** below are raw material: twelve steps of ink, eleven of persimmon,
-and four supporting hues. They exist so that the next decision has something to
-choose from. No component references them.
-
-The **aliases** underneath are the working vocabulary — `--ad-color-text`,
-`--ad-color-critical-subtle`, `--ad-color-focus`. Every rule in every component
-stylesheet reads from these. Swapping a theme means repointing aliases at
-different ramp steps, which is exactly what the dark theme does.
-
-<div class="ad-callout ad-callout--neutral">
-  <div class="ad-callout__body">
-    <p class="ad-callout__title">Contrast figures on this page are computed, not typed</p>
-    <p style="margin:0">
-      Each ratio comes from <code>lib/arbitrary_definitions/color_math.rb</code>, run
-      against the token value during the Jekyll build. A hex cannot change without its
-      contrast number changing with it.
-    </p>
-  </div>
+The **ramps** below are raw material: ten steps of neutral (light), the same
+ten inverted for dark mode, and five accent hues. No component references
+them directly.
+
+The **aliases** underneath are the working vocabulary — `--ad-color-text-primary`,
+`--ad-color-surface-card`, `--ad-color-accent`. Every component stylesheet
+reads from these. Retheming, or switching to dark, means repointing aliases
+at different values — which is exactly what the dark block does.
+
+<div class="ad-callout">
+  <p style="margin:0; font-size: var(--ad-step--1);">
+    The neutral ramps were authored in <code>oklch()</code> by the three
+    projects this system was synthesized from (see
+    <a href="{{ '/rationale/' | relative_url }}">Why arbitrary</a>). This
+    site's contrast-testing suite only understands flat hex, so every value
+    below is that same colour converted to sRGB — not re-picked, not rounded.
+    Contrast figures on this page are computed from those hex values during
+    the build by <code>lib/arbitrary_definitions/color_math.rb</code>.
+  </p>
 </div>
 
-## Ramps
+## Neutral
 
-{% for ramp in site.data.tokens.color.ramps %}
-### {{ ramp.label }}
-
-<p class="ad-muted">{{ ramp.description }}</p>
+Low-chroma warm-gray — the intersection of ketchup's plain grays, quire's
+warm cream/dark palette, and domus's warm paper. Ten steps, not twelve or
+sixteen: the source defines exactly these.
 
+{% assign neutral = site.data.tokens.color.ramps | where: "name", "neutral" | first %}
+<p class="ad-muted">{{ neutral.description }}</p>
 <ul class="ad-swatch-grid">
-  {%- for step in ramp.steps %}
+  {%- assign steps = neutral.steps | sort: "order" %}
+  {%- for step in steps %}
   {%- assign on = step.value | readable_on %}
   <li class="ad-swatch">
-    <div class="ad-swatch__chip" style="background: {{ step.value }}; color: {{ on }};">{{ ramp.name }}&nbsp;{{ step.step }}</div>
+    <div class="ad-swatch__chip" style="background: {{ step.value }}; color: {{ on }};">n-{{ step.step }}</div>
     <div class="ad-swatch__meta">
       <span class="ad-swatch__hex">{{ step.value }}</span>
       <span class="ad-swatch__ratio">{{ step.value | contrast_with: '#ffffff' }} on white</span>
@@ -48,13 +51,55 @@ different ramp steps, which is exactly what the dark theme does.
   </li>
   {%- endfor %}
 </ul>
-{% endfor %}
+
+{% assign neutral_dark = site.data.tokens.color.ramps | where: "name", "neutral_dark" | first %}
+<p class="ad-muted">{{ neutral_dark.description }}</p>
+<ul class="ad-swatch-grid">
+  {%- assign dsteps = neutral_dark.steps | sort: "order" %}
+  {%- for step in dsteps %}
+  {%- assign on = step.value | readable_on %}
+  <li class="ad-swatch">
+    <div class="ad-swatch__chip" style="background: {{ step.value }}; color: {{ on }};">nd-{{ step.step }}</div>
+    <div class="ad-swatch__meta">
+      <span class="ad-swatch__hex">{{ step.value }}</span>
+      <span class="ad-swatch__ratio">{{ step.value | contrast_with: '#000000' }} on black</span>
+    </div>
+  </li>
+  {%- endfor %}
+</ul>
+
+## Accent
+
+One swappable accent per project — never two in the same view. Signal Teal is
+the default; the other four are the sanctioned menu a project can switch to.
+
+{% assign accent = site.data.tokens.color.ramps | where: "name", "accent" | first %}
+<ul class="ad-accent-grid">
+  {%- for step in accent.steps %}
+  <li>
+    <div class="ad-accent-demo" style="background: {{ step.value }};">{{ step.step }}{% if step.step == "teal" %} (default){% endif %}</div>
+    <div class="ad-specimen__meta"><span class="ad-token-name">{{ step.value }}</span><span>{{ step.value | contrast_with: '#ffffff' }}:1 on white</span></div>
+    <p class="ad-muted" style="font-size: var(--ad-step--2); margin: var(--ad-space-3xs) 0 0;">{{ step.usage }}</p>
+  </li>
+  {%- endfor %}
+</ul>
+
+<div class="ad-callout">
+  <p style="margin:0; font-size: var(--ad-step--1);">
+    All five clear AA (4.5:1) for white text at their own value, but Ochre is
+    the tightest of the five at roughly 4.6:1 — closer to the floor than the
+    others (Ink Blue and Plum both clear 8.5:1). That is a real property of a
+    dry, papery ochre and is not being rounded away; a project choosing Ochre
+    as its accent should re-check any place it sets small white text directly
+    on the accent fill, rather than through <code>--ad-color-accent-fg</code>
+    on a full-size Button.
+  </p>
+</div>
 
 ## Semantic aliases
 
-Both themes, side by side. The left half of each chip is the light value and the
-right half is the dark one — a token whose halves look identical across a whole
-group usually means somebody forgot to think about dark mode.
+Both themes, side by side. The left half of each chip is the light value and
+the right half is the dark one.
 
 {% assign groups = site.data.tokens.semantic.groups %}
 {% for group in groups %}
@@ -78,15 +123,19 @@ group usually means somebody forgot to think about dark mode.
       <tr>
         <td>
           <div class="ad-alias-row">
+            {%- if flat.light contains "color-mix" %}
+            <span class="ad-alias-chip" aria-hidden="true" style="background: repeating-linear-gradient(45deg, var(--ad-color-surface-fill) 0 4px, var(--ad-color-surface-card) 4px 8px);"></span>
+            {%- else %}
             <span class="ad-alias-chip ad-alias-chip--pair" aria-hidden="true">
               <span style="background: {{ flat.light }}"></span>
               <span style="background: {{ flat.dark }}"></span>
             </span>
+            {%- endif %}
             <span class="ad-token-name">{{ flat.name }}</span>
           </div>
         </td>
-        <td class="ad-table__code">{{ token.light }}<br><span class="ad-subtle">{{ flat.light }}</span></td>
-        <td class="ad-table__code">{{ token.dark }}<br><span class="ad-subtle">{{ flat.dark }}</span></td>
+        <td class="ad-table__code">{{ token.light }}</td>
+        <td class="ad-table__code">{{ token.dark }}</td>
         <td>{{ token.usage }}</td>
       </tr>
       {%- endfor %}
@@ -97,12 +146,11 @@ group usually means somebody forgot to think about dark mode.
 
 ## Contrast that has to hold
 
-Colour is the one part of this system that is not arbitrary: a ratio is a fact
-about two colours and a pair of eyes, and it does not care what the palette
-wants. So the pairings below are a contract. They live in
-`_data/tokens/contrast.yml`, `test/test_token_set.rb` asserts every one of them
-in both themes, and the table is rendered from that same file — the deploy
-fails before a regression can reach this page.
+Colour is the one part of this system that is not arbitrary: a ratio is a
+fact about two colours and a pair of eyes, and it does not care what the
+palette wants. The pairings below live in `_data/tokens/contrast.yml`,
+`test/test_token_set.rb` asserts every one of them in both themes, and this
+table is rendered from that same file.
 
 {% assign contract = site.data.tokens.contrast %}
 {% assign flat = site.data.semantic_flat %}
@@ -147,31 +195,32 @@ fails before a regression can reach this page.
 </div>
 {% endfor %}
 
-Every figure in those two tables was computed during this build by
-`lib/arbitrary_definitions/color_math.rb`, from the token value itself. None of
-them was typed by a person, which is the only way a number in documentation
-stays true.
-
-<div class="ad-callout ad-callout--caution">
-  <div class="ad-callout__body">
-    <p class="ad-callout__title">Where the system knowingly stops short</p>
-    <p style="margin:0">
-      <code>--ad-color-border</code> sits around 1.3:1 against the surfaces it divides.
-      That is well under the 3:1 that WCAG 1.4.11 asks of a boundary a user has to
-      find. It is deliberate — a table gridded in 3:1 lines is unreadable — but it
-      means the default border is decorative. Any edge that is the <em>only</em> way
-      to locate a control uses <code>--ad-color-border-strong</code>, which is tested
-      at 3:1 in both themes.
-    </p>
-  </div>
+Every figure above was computed during this build from the token value
+itself — none of it was typed by a person.
+
+<div class="ad-callout">
+  <p style="margin:0; font-size: var(--ad-step--1);">
+    Two things this contract deliberately does not assert.
+    <strong>Text tertiary</strong> — <code>--ad-color-text-tertiary</code>
+    (neutral.500) is roughly 3.46:1 against the page in light mode, under the
+    4.5:1 floor. The source documents that step as muted-faint text —
+    placeholders and de-emphasised metadata, never body copy — so this is a
+    real, intentional property of the palette, not a bug to patch by
+    darkening the step.
+    <strong>Borders</strong> — <code>--ad-color-border-hairline</code> and
+    <code>--ad-color-border-default</code> sit well under the 3:1 that WCAG
+    1.4.11 asks of a meaningful boundary. The source's own words are
+    "hairline (1px) borders and dividers do most of the elevation work" —
+    borders here are deliberately quiet, and the source never claims a 3:1
+    bar for them, so this contract does not invent one.
+  </p>
 </div>
 
 ## Rules
 
-- **Never write a hex outside `_data/tokens/color.yml`.** If a component needs a
-  colour that does not exist yet, the colour does not exist yet. Add the alias.
-- **Status colour is never the only signal.** Every status component carries an
-  icon, a label, or a dot alongside the hue.
-- **One focus colour.** `--ad-color-focus` is azure everywhere, including on
-  persimmon surfaces, because a focus ring that changes per component is a focus
-  ring nobody learns.
+- **Never write a hex outside `_data/tokens/color.yml`.** If a component
+  needs a colour that does not exist yet, add the alias.
+- **One accent per project, never two in a view.** Swapping `--ad-color-accent`
+  re-derives `-ink` and `-soft` automatically via `color-mix()`.
+- **`--ad-color-success` and `--ad-color-danger` are fixed.** They read the
+  same in every project and are never swapped the way the accent is.
diff --git a/_foundations/elevation.md b/_foundations/elevation.md
index faf6f53..b267769 100644
--- a/_foundations/elevation.md
+++ b/_foundations/elevation.md
@@ -1,76 +1,55 @@
 ---
 title: Elevation
 summary: >-
-  Five levels, each a two-shadow stack. Elevation is a claim about hierarchy,
-  not about distance — level 3 does not mean "12 pixels above the page".
+  Two shadow levels only. Hairline (1px) borders do most of the elevation
+  work in this system; shadow is reserved for the one floating layer in a view.
 ---
 
-## The levels
+## Borders do the work
 
-Every level pairs a tight contact shadow with a wider ambient one. That is what
-keeps a shadow from reading as a grey smudge: the near shadow anchors the edge,
-the far shadow suggests the light.
+The source's own rule: "hairline (1px) borders and dividers do most of the
+elevation work". A flat, bordered [Card]({{ '/components/card/' | relative_url }})
+is the default everywhere. Shadow is the exception, not the baseline.
+
+## The two levels
 
 <ul class="ad-elevation-grid">
   {%- for step in site.data.tokens.scale.elevation.scale %}
   <li>
     <div class="ad-elevation-demo" style="box-shadow: {{ step.value }};"></div>
     <div class="ad-specimen__meta"><span class="ad-token-name">{{ step.token }}</span></div>
-    <p class="ad-muted" style="font-size: var(--ad-text-xs); margin: var(--ad-space-1) 0 0;">{{ step.usage }}</p>
+    <p class="ad-muted" style="font-size: var(--ad-step--2); margin: var(--ad-space-3xs) 0 0;">{{ step.usage }}</p>
   </li>
   {%- endfor %}
 </ul>
 
-## Border or shadow, not both
-
-A card can separate itself from the canvas with an edge or with a shadow. Doing
-both makes it look like a sticker. The system's default card is bordered and
-flat; `--ad-card--raised` swaps in elevation 1 and `--ad-card--floating` drops
-the border entirely.
-
-{% example title="Three ways to lift a card" layout="grid" surface="sunken" %}
+{% example title="Flat vs. float" layout="grid" surface="sunken" %}
 <article class="ad-card">
   <div class="ad-card__body">
-    <h3 class="ad-card__title" style="font-size: var(--ad-text-md);">Bordered</h3>
-    <p class="ad-card__text">The default. Works on any background.</p>
-  </div>
-</article>
-<article class="ad-card ad-card--raised">
-  <div class="ad-card__body">
-    <h3 class="ad-card__title" style="font-size: var(--ad-text-md);">Raised</h3>
-    <p class="ad-card__text">Border plus elevation 1, for a busy canvas.</p>
+    <h3 class="ad-card__title" style="font-size: var(--ad-step--1);">Flat</h3>
+    <p class="ad-card__text">The default. A hairline border, no shadow.</p>
   </div>
 </article>
-<article class="ad-card ad-card--floating">
+<article class="ad-card ad-card--elevated">
   <div class="ad-card__body">
-    <h3 class="ad-card__title" style="font-size: var(--ad-text-md);">Floating</h3>
-    <p class="ad-card__text">Elevation 2, no border. For things that overlay.</p>
+    <h3 class="ad-card__title" style="font-size: var(--ad-step--1);">Elevated</h3>
+    <p class="ad-card__text">shadow-float. Reserved for one element per view — a Dialog, a Toast.</p>
   </div>
 </article>
 {% endexample %}
 
 ## Elevation in the dark theme
 
-Shadows barely register on a near-black canvas. The dark theme leans on surface
-lightness instead: `--ad-color-surface` is one ink step lighter than
-`--ad-color-canvas`, and `--ad-color-surface-raised` is lighter again. The
-shadow tokens still apply — they just stop being the primary signal.
-
-<div class="ad-callout ad-callout--neutral">
-  <div class="ad-callout__body">
-    <p style="margin:0">
-      Switch themes and watch the row above. In light, the three cards separate
-      by shadow. In dark, they separate by surface. Same markup either way.
-    </p>
-  </div>
-</div>
+Shadows barely register on a near-black surface. The dark theme leans on
+surface lightness instead — `--ad-color-surface-card` is one neutral step
+lighter than `--ad-color-surface-page`, same as in light mode. The shadow
+tokens still apply; they simply stop being the primary signal.
 
 ## Rules
 
-- **One level per layer, and no skipping.** A dropdown inside a dialog is not
-  elevation 4; the dialog owns the elevation and the dropdown inherits the
-  context.
-- **At most one level 4 on screen.** If two things both claim to be the topmost
-  layer, neither is.
-- **Never animate a shadow's blur.** Animate `opacity` on a pseudo-element, or
-  do not animate it at all.
+- **At most one `--ad-shadow-float` on screen.** If two things both claim to
+  be the topmost layer, neither is.
+- **Border or shadow, not both, for a given surface's edge.** A card that
+  both borders and floats looks like a sticker.
+- **Never animate a shadow's blur.** If a floated element needs to arrive,
+  animate its `opacity`, or do not animate it at all.
diff --git a/_foundations/motion.md b/_foundations/motion.md
index e96e06c..f1d4804 100644
--- a/_foundations/motion.md
+++ b/_foundations/motion.md
@@ -1,14 +1,15 @@
 ---
 title: Motion
 summary: >-
-  Four durations, four easings, and one rule that overrides all of them: if the
-  reader asked for less motion, they get none.
+  Two durations, one easing curve, and one rule that overrides both: if the
+  reader asked for less motion, they get none. This is an interface, not a
+  title sequence.
 ---
 
 ## Durations
 
-Interface motion is short. Anything over a third of a second stops feeling like
-a response and starts feeling like a wait.
+Short ease transitions on hover and focus only — no bounce, no springy
+easing, no page-transition choreography.
 
 <div class="ad-table-wrap">
   <table class="ad-table ad-table--compact">
@@ -25,55 +26,51 @@ a response and starts feeling like a wait.
   </table>
 </div>
 
-## Easings
+## One easing
 
-Hover or focus a tile to run its curve. All four move the same distance over the
-same 320ms, so the only difference you are seeing is the shape.
+There is exactly one curve in the system. Hover the tile below to run it.
 
 <ul class="ad-motion-grid">
   {%- for row in site.data.tokens.scale.motion.easings %}
   <li>
-    <div class="ad-motion-demo" tabindex="0"
-         style="--_easing: {{ row.value }}; --_duration: var(--ad-duration-slow);">
+    <div class="ad-motion-demo" tabindex="0">
       <span class="ad-motion-demo__dot"></span>
     </div>
-    <div class="ad-specimen__meta"><span class="ad-token-name">{{ row.token }}</span></div>
-    <p class="ad-muted" style="font-size: var(--ad-text-xs); margin: var(--ad-space-1) 0 0;">{{ row.usage }}</p>
+    <div class="ad-specimen__meta"><span class="ad-token-name">{{ row.token | custom_property }}</span></div>
+    <p class="ad-muted" style="font-size: var(--ad-step--2); margin: var(--ad-space-3xs) 0 0;">{{ row.usage }}</p>
   </li>
   {%- endfor %}
 </ul>
 
 ## Reduced motion is not a downgrade
 
-`_plugins/token_generator.rb` emits a `prefers-reduced-motion: reduce` block that
-sets every duration token to `0ms`, and the base stylesheet clamps any remaining
-transition or animation to a hundredth of a millisecond. Components do not have
-to opt in, and cannot opt out.
+`_plugins/token_generator.rb` emits a `prefers-reduced-motion: reduce` block
+that sets every duration token to `0ms`, and the base stylesheet clamps any
+remaining transition or animation to a hundredth of a millisecond. Components
+do not have to opt in, and cannot opt out.
 
 ```css
 @media (prefers-reduced-motion: reduce) {
   :root {
-    --ad-duration-instant: 0ms;
-    --ad-duration-fast: 0ms;
-    --ad-duration-base: 0ms;
-    --ad-duration-slow: 0ms;
+    --ad-dur-fast: 0ms;
+    --ad-dur-base: 0ms;
   }
 }
 ```
 
-The state change still happens. The switch still moves. It simply arrives
+The state change still happens. The Switch still moves. It simply arrives
 instead of travelling.
 
-{% example title="The one component allowed to overshoot" %}
+{% example title="Motion in practice" %}
 <label class="ad-switch">
   <input type="checkbox" checked>
   <span class="ad-switch__track"></span>
-  <span>Ship on merge</span>
+  <span>Dark mode</span>
 </label>
 <label class="ad-switch">
   <input type="checkbox">
   <span class="ad-switch__track"></span>
-  <span>Notify the channel</span>
+  <span>Weekly digest</span>
 </label>
 {% endexample %}
 
@@ -81,9 +78,8 @@ instead of travelling.
 
 - **Transition named properties, never `all`.** `transition: all` animates
   things you did not think about, including layout.
-- **Prefer `opacity` and `transform`.** They are the two properties the
-  compositor can animate without a layout pass.
-- **Exit faster than you enter.** `--ad-ease-exit` is steeper than
-  `--ad-ease-entrance` for exactly this reason.
-- **`--ad-ease-spring` is for toggles only.** Overshoot on a panel or a dialog
-  reads as a bug.
+- **Hover states shift a background one step deeper, never a colour or
+  opacity trick.** Surface → fill is the whole vocabulary.
+- **Press is not animated.** Rely on the hover-darken plus native `:active`,
+  if anything at all.
+- **Motion is for hover and focus only.** No page-transition choreography.
diff --git a/_foundations/space.md b/_foundations/space.md
index 8010229..6522e78 100644
--- a/_foundations/space.md
+++ b/_foundations/space.md
@@ -1,15 +1,11 @@
 ---
 title: Space
 summary: >-
-  A 4px base that thins out as it grows, because nobody can see the difference
-  between 76px and 80px but everybody sees the difference between 4px and 8px.
+  Nine fluid Utopia steps, 3xs through 3xl — the same clamp() config as type,
+  because there is no fixed base unit here to count multiples of.
 ---
 
-## The ramp
-
-Twelve steps. The gaps are tight and linear at the small end, where spacing
-inside a control is doing real work, and geometric at the large end, where it is
-just separating sections.
+## The scale
 
 <div class="ad-ruler">
   {%- for step in site.data.tokens.scale.space.scale %}
@@ -25,72 +21,62 @@ just separating sections.
 
 | Distance between | Reach for |
 | --- | --- |
-| A glyph and its own border | `--ad-space-2` / `--ad-space-3` |
-| Two controls in a row | `--ad-space-3` |
-| A label and its input | `--ad-space-2` |
-| Two fields in a form | `--ad-space-5` |
-| Two sections of a page | `--ad-space-8` |
-| A page title and its first section | `--ad-space-6` |
+| An icon and its own label | `--ad-space-3xs` / `--ad-space-2xs` |
+| A field's label and its control | `--ad-space-2xs` |
+| Two controls in a row, CardRow padding | `--ad-space-xs` |
+| Two fields in a form, a Dialog's title/footer padding | `--ad-space-s` |
+| Card body padding, a Dialog's body padding | `--ad-space-m` |
+| Two sections of a page | `--ad-space-l` |
+| Major section spacing | `--ad-space-xl` / `--ad-space-2xl` |
+| Hero and empty-state breathing room | `--ad-space-3xl` |
 
 {% example title="The same card at three densities" layout="grid" %}
 <article class="ad-card">
-  <div class="ad-card__body" style="gap: var(--ad-space-1); padding: var(--ad-space-3);">
-    <h3 class="ad-card__title" style="font-size: var(--ad-text-md);">Compact</h3>
-    <p class="ad-card__text">space-3 padding, space-1 gap. For dense tables and lists.</p>
+  <div class="ad-card__body" style="gap: var(--ad-space-3xs); padding: var(--ad-space-xs);">
+    <h3 class="ad-card__title" style="font-size: var(--ad-step--1);">Compact</h3>
+    <p class="ad-card__text">space-xs padding, space-3xs gap. For dense lists.</p>
   </div>
 </article>
 <article class="ad-card">
   <div class="ad-card__body">
-    <h3 class="ad-card__title" style="font-size: var(--ad-text-md);">Default</h3>
-    <p class="ad-card__text">space-4 padding, space-2 gap. What the component ships with.</p>
+    <h3 class="ad-card__title" style="font-size: var(--ad-step--1);">Default</h3>
+    <p class="ad-card__text">space-m padding, space-2xs gap. What Card ships with.</p>
   </div>
 </article>
 <article class="ad-card">
-  <div class="ad-card__body" style="gap: var(--ad-space-4); padding: var(--ad-space-6);">
-    <h3 class="ad-card__title" style="font-size: var(--ad-text-md);">Roomy</h3>
-    <p class="ad-card__text">space-6 padding, space-4 gap. For a card that is the whole page.</p>
+  <div class="ad-card__body" style="gap: var(--ad-space-s); padding: var(--ad-space-l);">
+    <h3 class="ad-card__title" style="font-size: var(--ad-step--1);">Roomy</h3>
+    <p class="ad-card__text">space-l padding, space-s gap. For a card that is the whole screen.</p>
   </div>
 </article>
 {% endexample %}
 
 ## Radius
 
+4/6/10px — small and quiet, kept modest so it reads as a neutral default
+rather than any one source project's specific softness. Nothing pill-shaped
+except true pills.
+
 <ul class="ad-radius-grid">
   {%- for step in site.data.tokens.scale.radius.scale %}
+  {%- unless step.token == "border-w" %}
   <li>
     <div class="ad-radius-demo" style="border-radius: {{ step.value }};"></div>
     <div class="ad-specimen__meta">
       <span class="ad-token-name">{{ step.token }}</span>
       <span>{{ step.value }}</span>
     </div>
-    <p class="ad-muted" style="font-size: var(--ad-text-xs); margin: var(--ad-space-1) 0 0;">{{ step.usage }}</p>
+    <p class="ad-muted" style="font-size: var(--ad-step--2); margin: var(--ad-space-3xs) 0 0;">{{ step.usage }}</p>
   </li>
+  {%- endunless %}
   {%- endfor %}
 </ul>
 
-## Layout
-
-<div class="ad-table-wrap">
-  <table class="ad-table ad-table--compact">
-    <thead><tr><th scope="col">Token</th><th scope="col">Value</th><th scope="col">Use for</th></tr></thead>
-    <tbody>
-      {%- for row in site.data.tokens.scale.layout.scale %}
-      <tr>
-        <td class="ad-token-name">{{ row.token | custom_property }}</td>
-        <td class="ad-table__code">{{ row.value }}</td>
-        <td>{{ row.usage }}</td>
-      </tr>
-      {%- endfor %}
-    </tbody>
-  </table>
-</div>
-
 ## Rules
 
-- **Space comes from the parent.** A component sets its own internal padding and
-  never its external margin; the layout that contains it owns the gap. This is
-  what stops margins from collapsing into arguments.
+- **Space comes from the parent.** A component sets its own internal padding
+  and never its external margin; the layout that contains it owns the gap.
 - **Use `gap`.** Flex and grid gaps do not collapse and do not need a
   `:last-child` reset.
-- **A value not on the ramp is a bug**, not a nudge. If 14px is truly right, the
-  ramp is wrong and should be changed for everybody.
+- **A value not on the scale is a bug**, not a nudge. If a gap looks wrong,
+  the scale is wrong and should change for everybody.
diff --git a/_foundations/typography.md b/_foundations/typography.md
index 2ec4fd5..2284330 100644
--- a/_foundations/typography.md
+++ b/_foundations/typography.md
@@ -1,95 +1,94 @@
 ---
 title: Typography
 summary: >-
-  Two families, ten sizes, four weights. The specimens below are rendered with
-  the same custom properties the components use, at the sizes the tokens define.
+  Three families, eight fluid steps, eight compound styles. Sans for
+  interface, mono for every label and technical value, serif as an opt-in
+  reading variant.
 ---
 
 ## Families
 
-Fraunces carries the wordmark and page titles; its optical sizing axis keeps
-large settings from looking spindly. Inter does everything else. Code is set in
-JetBrains Mono, with a system monospace fallback that will not shift the layout
-if the webfont never arrives.
-
 {% for family in site.data.tokens.scale.typography.families %}
 <div class="ad-specimen">
   <div class="ad-specimen__meta">
     <span class="ad-token-name">{{ family.token | custom_property }}</span>
     <span>{{ family.usage }}</span>
   </div>
-  <div class="ad-specimen__sample" style="font-family: var({{ family.token | custom_property }}); font-size: 1.75rem;">
+  <div class="ad-specimen__sample" style="font-family: var({{ family.token | custom_property }}); font-size: 1.5rem;">
     Arbitrary Definitions — 0123456789
   </div>
 </div>
 {% endfor %}
 
-## Scale
+Only one webfont is loaded — IBM Plex Mono, at 400/500/600:
+
+```css
+{{ site.data.tokens.scale.typography.webfont_import }}
+```
+
+Sans is a web-safe system stack; serif is Georgia-based and opt-in, for a
+project that wants an editorial or archival feel. Neither is fetched.
 
-A minor-third-ish ramp, hand-adjusted at the small end where a strict ratio
-produces sizes nobody can tell apart. Sizes are declared in `rem` so a reader who
-has changed their browser's base font size gets a system that scales with them.
+## Fluid scale
 
-{% for size in site.data.tokens.scale.typography.sizes %}
+Eight steps, `clamp()`-based (Utopia, 320→1240px viewport). The body sits at
+`--ad-step-0` — 18px on a phone, 20px on a wide screen — with no fixed
+breakpoint in between.
+
+{% for step in site.data.tokens.scale.typography.steps %}
 <div class="ad-specimen">
   <div class="ad-specimen__meta">
-    <span class="ad-token-name">{{ size.token | custom_property }}</span>
-    <span>{{ size.value }} · {{ size.px }}px</span>
-    <span>{{ size.usage }}</span>
+    <span class="ad-token-name">{{ step.token | custom_property }}</span>
+    <span>{{ step.px }}px</span>
+    <span>{{ step.usage }}</span>
   </div>
-  <div class="ad-specimen__sample" style="font-size: var({{ size.token | custom_property }});">
+  <div class="ad-specimen__sample" style="font-size: var({{ step.token | custom_property }});">
     The quick brown fox jumps over the lazy dog
   </div>
 </div>
 {% endfor %}
 
-## Line height, weight, tracking
+## The eight compound styles
+
+Real, specific numbers from the source — not a generic ramp compressed to
+fit. Each class sets family, size, weight, line-height, and (where the
+source specifies one) letter-spacing in a single declaration.
 
 <div class="ad-table-wrap">
   <table class="ad-table ad-table--compact">
-    <thead>
-      <tr><th scope="col">Token</th><th scope="col">Value</th><th scope="col">Use for</th></tr>
-    </thead>
+    <thead><tr><th scope="col">Class</th><th scope="col">Weight</th><th scope="col">Size</th><th scope="col">Line-height</th><th scope="col">Tracking</th><th scope="col">Use for</th></tr></thead>
     <tbody>
-      {%- assign type = site.data.tokens.scale.typography %}
-      {%- for row in type.line_heights %}
-      <tr><td class="ad-token-name">{{ row.token | custom_property }}</td><td class="ad-table__code">{{ row.value }}</td><td>{{ row.usage }}</td></tr>
-      {%- endfor %}
-      {%- for row in type.weights %}
-      <tr><td class="ad-token-name">{{ row.token | custom_property }}</td><td class="ad-table__code">{{ row.value }}</td><td>{{ row.usage }}</td></tr>
-      {%- endfor %}
-      {%- for row in type.tracking %}
-      <tr><td class="ad-token-name">{{ row.token | custom_property }}</td><td class="ad-table__code">{{ row.value }}</td><td>{{ row.usage }}</td></tr>
+      {%- for style in site.data.tokens.scale.typography.styles %}
+      <tr>
+        <td class="ad-token-name">.{{ style.class }}</td>
+        <td>{{ style.weight }}</td>
+        <td class="ad-table__code">{{ style.size | custom_property }}</td>
+        <td>{{ style.line_height }}</td>
+        <td>{{ style.tracking }}</td>
+        <td>{{ style.usage }}</td>
+      </tr>
       {%- endfor %}
     </tbody>
   </table>
 </div>
 
-## In practice
-
-{% example title="A heading, a lead, and body copy" layout="stack" %}
-<div style="max-width: 34rem;">
-  <p class="ad-overline">Foundations</p>
-  <h2 style="margin: var(--ad-space-2) 0 var(--ad-space-3);">Type is the cheapest hierarchy you have</h2>
-  <p class="ad-lead">
-    Before reaching for a border, a background, or a badge, try moving one step
-    up the size ramp and one step down the colour ramp.
-  </p>
-  <p>
-    Body copy sits at <code>--ad-text-md</code> with <code>--ad-leading-normal</code>,
-    capped at <code>--ad-measure-prose</code> so a line never runs past about
-    seventy-five characters. Long measures are the single most common readability
-    failure in documentation, and the fix costs one declaration.
-  </p>
-</div>
+{% example title="All eight, in order" layout="stack" %}
+<div class="type-display">Display</div>
+<h1 class="type-h1">Heading one</h1>
+<h2 class="type-h2">Heading two</h2>
+<h3 class="type-h3">Heading three</h3>
+<p class="type-body">Body copy at the base size, 1.55 line-height.</p>
+<p class="type-body-sm">Secondary copy, one step down.</p>
+<span class="type-label">Catalog label</span>
+<span class="type-mono">technical.value / 2026-08-21 / #3a6a63</span>
 {% endexample %}
 
 ## Rules
 
-- **Two families, and no third.** A new typeface needs a reason that a weight or
-  a size cannot supply.
-- **Uppercase only at `--ad-text-2xs` with `--ad-tracking-wide`.** Uppercase at
-  body size is shouting and it is also slower to read.
-- **Never set a size in `px`.** The ramp is in `rem` on purpose.
-- **`text-wrap: balance` on headings, `pretty` on lead paragraphs.** Both are in
-  the base stylesheet; components do not need to repeat them.
+- **Three families, and no fourth.** A new typeface needs a reason a weight
+  or a size cannot supply.
+- **Mono is not decoration.** Every label, meta value, ID, date, and code
+  sample is mono — it is the one thread present in all three source projects.
+- **`type-label` is always uppercase, mono, and tracked.** It is the one
+  place uppercase belongs in this system.
+- **Never set a size in `px`.** The eight steps are fluid on purpose.
diff --git a/_includes/head.html b/_includes/head.html
index aedd0f0..796de97 100644
--- a/_includes/head.html
+++ b/_includes/head.html
@@ -1,13 +1,16 @@
 <meta charset="utf-8">
 <meta name="viewport" content="width=device-width, initial-scale=1">
 <meta name="theme-color" content="#faf9f7" media="(prefers-color-scheme: light)">
-<meta name="theme-color" content="#171512" media="(prefers-color-scheme: dark)">
+<meta name="theme-color" content="#0e0d0b" media="(prefers-color-scheme: dark)">
 
 {% seo %}
 
+<!-- The system's one loaded webfont — see _data/tokens/scale.yml's
+     typography.webfont_import. Sans is a web-safe system stack; nothing is
+     fetched for it. -->
 <link rel="preconnect" href="https://fonts.googleapis.com">
 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
-<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,600&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap">
+<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&display=swap">
 
 <!-- Generated from _data/tokens/*.yml at build time. -->
 <link rel="stylesheet" href="{{ '/assets/css/tokens.css' | relative_url }}">
diff --git a/_patterns/archive.md b/_patterns/archive.md
new file mode 100644
index 0000000..9bc576b
--- /dev/null
+++ b/_patterns/archive.md
@@ -0,0 +1,114 @@
+---
+title: "Archive — capture app"
+summary: >-
+  A capture/document mobile app. Card list, bottom capture dock, photo slots
+  with the diagonal-grain empty state. A generic demonstration of the system,
+  not a recreation of any one source project's actual screens.
+---
+
+Archive is not a real product — it is the system's own reference layout for a
+mobile capture flow: the shape `kejadlen/domus` (a household inventory and
+document archive) would take if it were rebuilt on this token set, at its
+390×760 frame. Three screens, abbreviated to what each one adds.
+
+{% example title="Home — recent captures" layout="row" no-source %}
+<div style="width: 300px; height: 560px; background: var(--ad-color-surface-page); border-radius: var(--ad-radius-lg); box-shadow: var(--ad-shadow-float); overflow:hidden; display:flex; flex-direction:column; position:relative;">
+  <header style="padding: 16px var(--ad-space-m) var(--ad-space-2xs);">
+    <span style="font-weight:600; font-size: var(--ad-step-1); letter-spacing:-0.01em;">Archive</span>
+  </header>
+  <div style="flex:1; overflow:hidden; padding: 0 var(--ad-space-m);">
+    <div style="display:flex; align-items:baseline; gap:8px; margin: var(--ad-space-s) 0 var(--ad-space-xs);">
+      <span style="font-weight:700; font-size: var(--ad-step-0);">Recent</span>
+      <span class="type-label" style="margin-left:auto;">3 tracked</span>
+    </div>
+    <div class="ad-card">
+      <div class="ad-card-row">
+        <span class="ad-avatar ad-avatar--square"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 8 12 3 3 8v8l9 5 9-5V8Z"/><path d="M3 8l9 5 9-5M12 13v8"/></svg></span>
+        <div style="flex:1; min-width:0;">
+          <div style="font-weight:550; font-size: var(--ad-step--1); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">Dishwasher manual</div>
+          <div class="type-label" style="margin-top:2px;">PDF · 2.1MB</div>
+        </div>
+        <span class="type-mono" style="color: var(--ad-color-text-tertiary); font-size: var(--ad-step--2);">2h</span>
+      </div>
+      <div class="ad-card-row">
+        <span class="ad-avatar ad-avatar--square"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 8 12 3 3 8v8l9 5 9-5V8Z"/><path d="M3 8l9 5 9-5M12 13v8"/></svg></span>
+        <div style="flex:1; min-width:0;">
+          <div style="font-weight:550; font-size: var(--ad-step--1);">Sofa receipt</div>
+          <div class="type-label" style="margin-top:2px;">JPG · 640KB</div>
+        </div>
+        <span class="type-mono" style="color: var(--ad-color-text-tertiary); font-size: var(--ad-step--2);">1d</span>
+      </div>
+      <div class="ad-card-row">
+        <span class="ad-avatar ad-avatar--square"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 8 12 3 3 8v8l9 5 9-5V8Z"/><path d="M3 8l9 5 9-5M12 13v8"/></svg></span>
+        <div style="flex:1; min-width:0;">
+          <div style="font-weight:550; font-size: var(--ad-step--1);">Passport scan</div>
+          <div class="type-label" style="margin-top:2px;">PDF · 890KB</div>
+        </div>
+        <span class="type-mono" style="color: var(--ad-color-text-tertiary); font-size: var(--ad-step--2);">4d</span>
+      </div>
+    </div>
+  </div>
+  <div style="position:absolute; left:0; right:0; bottom:0; display:flex; justify-content:center; padding: 14px 20px 22px; background: linear-gradient(to top, var(--ad-color-surface-page) 65%, transparent);">
+    <button type="button" class="ad-btn ad-btn--primary ad-btn--lg ad-btn--block">
+      <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 8a2 2 0 0 1 2-2h1.5l1-1.5h9l1 1.5H20a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8Z"/><circle cx="12" cy="13" r="3.5"/></svg>
+      Take a photo
+    </button>
+  </div>
+</div>
+
+<div style="width: 300px; height: 560px; background: var(--ad-color-surface-page); border-radius: var(--ad-radius-lg); box-shadow: var(--ad-shadow-float); overflow:hidden; display:flex; flex-direction:column;">
+  <header style="display:flex; align-items:center; gap:8px; padding: 16px var(--ad-space-m) var(--ad-space-2xs);">
+    <button type="button" class="ad-icon-btn ad-icon-btn--sm" aria-label="Back"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 18l-6-6 6-6"/></svg></button>
+    <span style="font-weight:600; font-size: var(--ad-step-0);">Save</span>
+  </header>
+  <div style="flex:1; padding: 0 var(--ad-space-m);">
+    <div style="height: 180px; border-radius: var(--ad-radius-lg); border: 1px solid var(--ad-color-border-default); background: repeating-linear-gradient(45deg, transparent 0 8px, var(--ad-color-surface-fill-2) 8px 9px), var(--ad-color-surface-fill); display:flex; flex-direction:column; align-items:center; justify-content:center; gap:6px; margin-top: var(--ad-space-s);">
+      <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 8 12 3 3 8v8l9 5 9-5V8Z"/><path d="M3 8l9 5 9-5M12 13v8"/></svg>
+      <span class="type-label">Photo preview</span>
+    </div>
+    <div style="margin-top: var(--ad-space-m); display:flex; flex-direction:column; gap: var(--ad-space-2xs);">
+      <span class="type-label">Name</span>
+      <input class="ad-input" placeholder="Dishwasher manual">
+    </div>
+  </div>
+  <div style="padding: var(--ad-space-m); display:flex; gap: var(--ad-space-2xs);">
+    <button type="button" class="ad-btn" style="flex:1;">Retake</button>
+    <button type="button" class="ad-btn ad-btn--primary" style="flex:1;">Save</button>
+  </div>
+</div>
+
+<div style="width: 300px; height: 560px; background: var(--ad-color-surface-page); border-radius: var(--ad-radius-lg); box-shadow: var(--ad-shadow-float); overflow:hidden; display:flex; flex-direction:column;">
+  <header style="display:flex; align-items:center; gap:8px; padding: 16px var(--ad-space-m) var(--ad-space-2xs);">
+    <button type="button" class="ad-icon-btn ad-icon-btn--sm" aria-label="Back"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 18l-6-6 6-6"/></svg></button>
+    <span class="type-label">Archive</span>
+  </header>
+  <div style="flex:1; overflow:hidden; padding: 0 var(--ad-space-m) var(--ad-space-l);">
+    <h1 style="font-weight:700; font-size: var(--ad-step-2); letter-spacing:-0.01em; margin: var(--ad-space-xs) 0 var(--ad-space-2xs);">Dishwasher manual</h1>
+    <div style="display:flex; gap:6px; flex-wrap:wrap; margin-bottom: var(--ad-space-m);">
+      <span class="ad-tag">location:kitchen</span>
+      <span class="ad-tag ad-tag--dashed">+ add tag</span>
+    </div>
+    <div class="type-label" style="margin-bottom: var(--ad-space-2xs);">File</div>
+    <div class="ad-card">
+      <div class="ad-card-row">
+        <span class="ad-avatar ad-avatar--square"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 8 12 3 3 8v8l9 5 9-5V8Z"/><path d="M3 8l9 5 9-5M12 13v8"/></svg></span>
+        <div style="flex:1; font-size: var(--ad-step--1);">PDF · 2.1MB</div>
+      </div>
+    </div>
+    <div class="type-label" style="margin: var(--ad-space-m) 0 var(--ad-space-2xs);">Notes</div>
+    <p style="font-size: var(--ad-step--1); color: var(--ad-color-text-secondary); line-height:1.55; margin:0;">No notes yet.</p>
+  </div>
+</div>
+{% endexample %}
+
+## What this screen is testing
+
+- **The diagonal-grain empty state.** The only patterned background this
+  system permits, used only inside an empty photo slot.
+- **CardRow as a file row, twice.** The same component holds a scannable
+  list on Home and a single, focused row on Detail — no special "detail row"
+  variant needed.
+- **Tag alongside its own dashed "add" affordance.** The two live in one row,
+  reading as one continuous, extendable list.
+- **`type-label` as the workhorse.** File size, tag count, and the back-button
+  screen name all reach for the same mono catalog-label convention as Badge.
diff --git a/_patterns/kitchen-sink.md b/_patterns/kitchen-sink.md
deleted file mode 100644
index 49116b3..0000000
--- a/_patterns/kitchen-sink.md
+++ /dev/null
@@ -1,131 +0,0 @@
----
-title: Kitchen sink
-summary: >-
-  Every component the system ships, on one page, in both themes. This is the page
-  to open after changing a token.
----
-
-A kitchen sink is not documentation — nobody learns a system from this page. It
-is a regression test you can look at. Change a token, open this, toggle the
-theme, and see what broke.
-
-## Buttons
-
-{% example no-source %}
-<button type="button" class="ad-btn ad-btn--primary">Primary</button>
-<button type="button" class="ad-btn ad-btn--secondary">Secondary</button>
-<button type="button" class="ad-btn ad-btn--ghost">Ghost</button>
-<button type="button" class="ad-btn ad-btn--danger">Danger</button>
-<button type="button" class="ad-btn ad-btn--primary" disabled>Disabled</button>
-<button type="button" class="ad-btn ad-btn--primary ad-btn--sm">Small</button>
-<button type="button" class="ad-btn ad-btn--primary ad-btn--lg">Large</button>
-<div class="ad-btn-group" role="group" aria-label="Alignment">
-  <button type="button" class="ad-btn ad-btn--secondary ad-btn--sm">One</button>
-  <button type="button" class="ad-btn ad-btn--secondary ad-btn--sm">Two</button>
-  <button type="button" class="ad-btn ad-btn--secondary ad-btn--sm">Three</button>
-</div>
-{% endexample %}
-
-## Badges
-
-{% example no-source %}
-<span class="ad-badge">Neutral</span>
-<span class="ad-badge ad-badge--accent">Accent</span>
-<span class="ad-badge ad-badge--positive"><span class="ad-badge__dot"></span>Positive</span>
-<span class="ad-badge ad-badge--caution"><span class="ad-badge__dot"></span>Caution</span>
-<span class="ad-badge ad-badge--critical"><span class="ad-badge__dot"></span>Critical</span>
-<span class="ad-badge ad-badge--info">Info</span>
-<span class="ad-badge ad-badge--pill ad-badge--solid ad-badge--positive">Solid pill</span>
-<span class="ad-avatar ad-avatar--sm">ad</span>
-<span class="ad-avatar">ad</span>
-{% endexample %}
-
-## Controls
-
-{% example no-source layout="stack" %}
-<div class="ad-form" style="max-width: none;">
-  <div class="ad-form__row">
-    <div class="ad-field">
-      <label class="ad-field__label" for="ks-text">Text</label>
-      <input class="ad-input" id="ks-text" type="text" placeholder="Placeholder">
-    </div>
-    <div class="ad-field">
-      <label class="ad-field__label" for="ks-select">Select</label>
-      <select class="ad-select" id="ks-select"><option>One</option><option>Two</option></select>
-    </div>
-    <div class="ad-field">
-      <label class="ad-field__label" for="ks-invalid">Invalid</label>
-      <input class="ad-input" id="ks-invalid" type="text" value="nope" aria-invalid="true" aria-describedby="ks-invalid-error">
-      <p class="ad-field__error" id="ks-invalid-error">Something is wrong.</p>
-    </div>
-  </div>
-  <div class="ad-form__row">
-    <label class="ad-choice"><input type="checkbox" checked> <span class="ad-choice__text">Checkbox</span></label>
-    <label class="ad-choice"><input type="radio" name="ks-radio" checked> <span class="ad-choice__text">Radio</span></label>
-    <label class="ad-switch"><input type="checkbox" checked><span class="ad-switch__track"></span><span>Switch</span></label>
-  </div>
-  <div class="ad-field">
-    <label class="ad-field__label" for="ks-textarea">Textarea</label>
-    <textarea class="ad-textarea" id="ks-textarea">Multi-line value.</textarea>
-  </div>
-</div>
-{% endexample %}
-
-## Callouts
-
-{% example no-source layout="stack" %}
-<div class="ad-callout ad-callout--info" style="margin:0"><div class="ad-callout__body"><p class="ad-callout__title">Info</p><p style="margin:0">Neutral notice.</p></div></div>
-<div class="ad-callout ad-callout--positive" style="margin:0"><div class="ad-callout__body"><p class="ad-callout__title">Positive</p><p style="margin:0">It worked.</p></div></div>
-<div class="ad-callout ad-callout--caution" style="margin:0"><div class="ad-callout__body"><p class="ad-callout__title">Caution</p><p style="margin:0">It might not work.</p></div></div>
-<div class="ad-callout ad-callout--critical" style="margin:0"><div class="ad-callout__body"><p class="ad-callout__title">Critical</p><p style="margin:0">It did not work.</p></div></div>
-<div class="ad-callout ad-callout--accent" style="margin:0"><div class="ad-callout__body"><p class="ad-callout__title">Accent</p><p style="margin:0">Read this one.</p></div></div>
-<div class="ad-callout ad-callout--neutral" style="margin:0"><div class="ad-callout__body"><p style="margin:0">Neutral aside with no title.</p></div></div>
-{% endexample %}
-
-## Cards, table, progress
-
-{% example no-source layout="stack" surface="sunken" %}
-<div class="ad-card-grid">
-  <article class="ad-card"><div class="ad-card__body"><h3 class="ad-card__title">Bordered</h3><p class="ad-card__text">Flat.</p></div></article>
-  <article class="ad-card ad-card--raised"><div class="ad-card__body"><h3 class="ad-card__title">Raised</h3><p class="ad-card__text">Elevation 1.</p></div></article>
-  <article class="ad-card ad-card--floating"><div class="ad-card__body"><h3 class="ad-card__title">Floating</h3><p class="ad-card__text">Elevation 2.</p></div></article>
-</div>
-
-<div class="ad-table-wrap">
-  <table class="ad-table ad-table--hover ad-table--compact">
-    <thead><tr><th scope="col">Token</th><th scope="col">Tone</th><th scope="col" class="ad-table__num">Steps</th></tr></thead>
-    <tbody>
-      <tr><td class="ad-table__code">--ad-color-accent</td><td><span class="ad-badge ad-badge--accent">accent</span></td><td class="ad-table__num">11</td></tr>
-      <tr><td class="ad-table__code">--ad-color-positive</td><td><span class="ad-badge ad-badge--positive">positive</span></td><td class="ad-table__num">11</td></tr>
-      <tr><td class="ad-table__code">--ad-color-critical</td><td><span class="ad-badge ad-badge--critical">critical</span></td><td class="ad-table__num">11</td></tr>
-    </tbody>
-  </table>
-</div>
-
-<div class="ad-progress" style="max-width: none;">
-  <div class="ad-progress__label"><span id="ks-progress">Determinate</span><span class="ad-progress__value">42%</span></div>
-  <div class="ad-progress__track" role="progressbar" aria-labelledby="ks-progress" aria-valuenow="42" aria-valuemin="0" aria-valuemax="100">
-    <div class="ad-progress__fill" style="width: 42%;"></div>
-  </div>
-</div>
-<div class="ad-progress ad-progress--indeterminate" style="max-width: none;">
-  <div class="ad-progress__label"><span id="ks-progress-2">Indeterminate</span></div>
-  <div class="ad-progress__track" role="progressbar" aria-labelledby="ks-progress-2"><div class="ad-progress__fill"></div></div>
-</div>
-{% endexample %}
-
-## Typography
-
-{% example no-source layout="stack" %}
-<div>
-  <p class="ad-overline">Overline</p>
-  <h1 style="font-size: var(--ad-text-3xl);">Heading one</h1>
-  <h2 style="border:0; padding:0; margin: var(--ad-space-3) 0 0;">Heading two</h2>
-  <h3 style="margin: var(--ad-space-3) 0 0;">Heading three</h3>
-  <p class="ad-lead" style="margin-top: var(--ad-space-3);">A lead paragraph, one step up the ramp and one step down the colour scale.</p>
-  <p>Body copy with <a href="#main">a link</a>, <strong>bold text</strong>, <code>inline code</code>, and a <kbd>⌘K</kbd> key.</p>
-  <blockquote>A blockquote, marked by an accent rule rather than an indent.</blockquote>
-  <p class="ad-muted">Muted text.</p>
-  <p class="ad-subtle">Subtle text.</p>
-</div>
-{% endexample %}
diff --git a/_patterns/loop.md b/_patterns/loop.md
new file mode 100644
index 0000000..2d0804d
--- /dev/null
+++ b/_patterns/loop.md
@@ -0,0 +1,161 @@
+---
+title: "Loop — task dashboard"
+summary: >-
+  A recurring-task web dashboard. Dense rows, mono meta columns, overdue in
+  danger red, one primary action. A generic demonstration of the system, not
+  a recreation of any one source project's actual screens.
+---
+
+Loop is not a real product — it is the system's own reference layout for a
+dense, table-ish list screen: the shape `kejadlen/ketchup` (a recurring-task
+tracker) would take if it were rebuilt on this token set. Every visible value
+below is a component from this system; nothing here is page-specific CSS
+beyond a couple of inline widths.
+
+{% example title="Loop" layout="stack" no-source %}
+<div style="border: 1px solid var(--ad-color-border-hairline); border-radius: var(--ad-radius-lg); overflow: hidden; background: var(--ad-color-surface-page);">
+
+  <header style="display:flex; align-items:center; gap: var(--ad-space-s); padding: var(--ad-space-s) var(--ad-space-l); border-bottom: 1px solid var(--ad-color-border-hairline);">
+    <span style="font-family: var(--ad-font-mono); font-weight:600; letter-spacing:0.02em; font-size: var(--ad-step-0);">Loop</span>
+    <div style="flex:1;"></div>
+    <button type="button" class="ad-btn ad-btn--primary ad-btn--sm">
+      <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>
+      New
+    </button>
+    <span class="ad-avatar">km</span>
+  </header>
+
+  <div style="display:flex;">
+    <div style="flex:1; min-width:0; max-width: 34rem; padding: var(--ad-space-l);">
+
+      <section style="margin-bottom: var(--ad-space-l);">
+        <div style="display:flex; align-items:baseline; gap: var(--ad-space-2xs); margin-bottom: var(--ad-space-s);">
+          <span class="type-label" style="color: var(--ad-color-danger);">Overdue</span>
+          <span class="ad-badge ad-badge--danger">2</span>
+        </div>
+        <div class="ad-card-row">
+          <button type="button" aria-label="Complete" style="all:unset; width:18px; height:18px; border-radius:4px; flex-shrink:0; cursor:pointer; border: 1.5px solid var(--ad-color-danger);"></button>
+          <div style="flex:1; min-width:0;">
+            <div style="font-weight:500; color: var(--ad-color-danger);">Call Mom</div>
+            <div class="type-mono" style="color: var(--ad-color-text-tertiary); font-size: var(--ad-step--2);">every 2 weeks</div>
+          </div>
+          <span class="type-mono" style="color: var(--ad-color-danger); font-size: var(--ad-step--2); white-space:nowrap;">3d overdue</span>
+        </div>
+        <div class="ad-card-row">
+          <button type="button" aria-label="Complete" style="all:unset; width:18px; height:18px; border-radius:4px; flex-shrink:0; cursor:pointer; border: 1.5px solid var(--ad-color-danger);"></button>
+          <div style="flex:1; min-width:0;">
+            <div style="font-weight:500; color: var(--ad-color-danger);">Water the ferns</div>
+            <div class="type-mono" style="color: var(--ad-color-text-tertiary); font-size: var(--ad-step--2);">every 5 days</div>
+          </div>
+          <span class="type-mono" style="color: var(--ad-color-danger); font-size: var(--ad-step--2); white-space:nowrap;">1d overdue</span>
+        </div>
+      </section>
+
+      <section>
+        <div class="type-label" style="margin-bottom: var(--ad-space-s);">Upcoming</div>
+        <div class="ad-card-row">
+          <button type="button" aria-label="Complete" style="all:unset; width:18px; height:18px; border-radius:4px; flex-shrink:0; cursor:pointer; border: 1.5px solid var(--ad-color-text-secondary);"></button>
+          <div style="flex:1; min-width:0;">
+            <div style="font-weight:500;">Renew passport</div>
+            <div class="type-mono" style="color: var(--ad-color-text-tertiary); font-size: var(--ad-step--2);">every 10 years</div>
+          </div>
+          <span class="type-mono" style="color: var(--ad-color-text-secondary); font-size: var(--ad-step--2); white-space:nowrap;">in 3 months</span>
+        </div>
+        <div class="ad-card-row">
+          <button type="button" aria-label="Complete" style="all:unset; width:18px; height:18px; border-radius:4px; flex-shrink:0; cursor:pointer; border: 1.5px solid var(--ad-color-text-secondary);"></button>
+          <div style="flex:1; min-width:0;">
+            <div style="font-weight:500;">Dentist checkup</div>
+            <div class="type-mono" style="color: var(--ad-color-text-tertiary); font-size: var(--ad-step--2);">every 6 months</div>
+          </div>
+          <span class="type-mono" style="color: var(--ad-color-text-secondary); font-size: var(--ad-step--2); white-space:nowrap;">in 2 weeks</span>
+        </div>
+        <div class="ad-card-row">
+          <button type="button" aria-label="Complete" style="all:unset; width:18px; height:18px; border-radius:4px; flex-shrink:0; cursor:pointer; border: 1.5px solid var(--ad-color-text-secondary);"></button>
+          <div style="flex:1; min-width:0;">
+            <div style="font-weight:500;">Rotate mattress</div>
+            <div class="type-mono" style="color: var(--ad-color-text-tertiary); font-size: var(--ad-step--2);">every 6 months</div>
+          </div>
+          <span class="type-mono" style="color: var(--ad-color-text-secondary); font-size: var(--ad-step--2); white-space:nowrap;">in 5 weeks</span>
+        </div>
+      </section>
+    </div>
+
+    <aside style="width: 15rem; flex-shrink:0; background: var(--ad-color-surface-fill); border-left: 1px solid var(--ad-color-border-hairline); padding: var(--ad-space-l);">
+      <div class="type-label" style="margin-bottom: var(--ad-space-s);">This week</div>
+      <div style="display:flex; background: var(--ad-color-surface-card); border: 1px solid var(--ad-color-border-hairline); border-radius: var(--ad-radius-md); overflow:hidden; margin-bottom: var(--ad-space-l);">
+        {%- assign week = "S,M,T,W,T,F,S" | split: "," %}
+        {%- assign nums = "0,1,0,2,0,1,0" | split: "," %}
+        {%- for d in week %}
+        <div style="flex:1; display:flex; flex-direction:column; align-items:center; gap:4px; padding: 8px 0; {% if forloop.index0 == 3 %}background: var(--ad-color-accent-soft);{% endif %} {% unless forloop.last %}border-right: 1px solid var(--ad-color-border-hairline);{% endunless %}">
+          <span class="type-mono" style="font-size:9px; color: var(--ad-color-text-tertiary); text-transform:uppercase;">{{ d }}</span>
+          <span style="font-size:13px; font-weight:600; color: {% if forloop.index0 == 3 %}var(--ad-color-accent-ink){% else %}var(--ad-color-text-primary){% endif %};">{{ nums[forloop.index0] }}</span>
+        </div>
+        {%- endfor %}
+      </div>
+      <div class="type-label" style="margin-bottom: var(--ad-space-2xs);">Series</div>
+      <div>
+        <div style="padding: var(--ad-space-2xs) 0; font-size: var(--ad-step--1); color: var(--ad-color-text-secondary);">Call Mom</div>
+        <div style="padding: var(--ad-space-2xs) 0; border-top: 1px solid var(--ad-color-border-hairline); font-size: var(--ad-step--1); color: var(--ad-color-text-secondary);">Water the ferns</div>
+        <div style="padding: var(--ad-space-2xs) 0; border-top: 1px solid var(--ad-color-border-hairline); font-size: var(--ad-step--1); color: var(--ad-color-text-secondary);">Renew passport</div>
+      </div>
+    </aside>
+  </div>
+</div>
+{% endexample %}
+
+## The New series dialog
+
+{% example title="New series" no-source %}
+<div class="ad-dialog-demo" style="min-height: 18rem;">
+  <div class="ad-dialog__overlay">
+    <div class="ad-dialog" style="width: 22rem;">
+      <div class="ad-dialog__title">New series</div>
+      <div class="ad-dialog__body">
+        <div style="display:flex; flex-direction:column; gap: var(--ad-space-s);">
+          <div class="ad-field">
+            <label class="ad-field__label" for="loop-note">Note</label>
+            <input class="ad-input" id="loop-note" placeholder="Call Mom">
+            <span class="ad-field__hint">First line becomes the display name.</span>
+          </div>
+          <div class="ad-form__row">
+            <div class="ad-field">
+              <label class="ad-field__label" for="loop-every">Every</label>
+              <input class="ad-input" id="loop-every" type="number" placeholder="2">
+            </div>
+            <div class="ad-field">
+              <label class="ad-field__label" for="loop-unit">Unit</label>
+              <select class="ad-select" id="loop-unit"><option>Day</option><option selected>Week</option><option>Month</option><option>Year</option></select>
+            </div>
+          </div>
+        </div>
+      </div>
+      <div class="ad-dialog__footer">
+        <button type="button" class="ad-btn">Cancel</button>
+        <button type="button" class="ad-btn ad-btn--primary">Create</button>
+      </div>
+    </div>
+  </div>
+</div>
+{% endexample %}
+
+## After completing a task
+
+{% example title="Toast" no-source %}
+<div class="ad-toast">
+  <span class="ad-toast__message">Completed "Call Mom"</span>
+  <button type="button" class="ad-toast__dismiss" aria-label="Dismiss">×</button>
+</div>
+{% endexample %}
+
+## What this screen is testing
+
+- **Dense rows next to a mono meta column.** A recurring-task list is
+  fundamentally tabular even though it never touches an actual `<table>`.
+- **Overdue in `--ad-color-danger`, everywhere at once.** Row text, the
+  complete-button border, and the trailing meta all pick it up from the same
+  `overdue` state — colour never drifts row to row.
+- **One primary action.** `New` is the only accent-filled control on screen;
+  everything else is neutral or bordered.
+- **Accent used exactly once, decoratively.** The "km" avatar and the
+  highlighted day both use `--ad-color-accent-soft` / `-ink` — never the raw
+  accent fill outside of `New`.
diff --git a/_patterns/settings-form.md b/_patterns/settings-form.md
deleted file mode 100644
index 5bea81f..0000000
--- a/_patterns/settings-form.md
+++ /dev/null
@@ -1,116 +0,0 @@
----
-title: Settings form
-summary: >-
-  Ten components in one realistic screen. Everything below is system markup with
-  no page-specific CSS beyond a couple of inline widths.
----
-
-A pattern is what a design system is actually judged on. Individual components
-looking tidy on a white background proves nothing; the question is whether they
-hold together when a real screen puts nine of them side by side.
-
-{% example title="Theme settings" layout="stack" surface="sunken" %}
-<form class="ad-card ad-card--raised" style="max-width: 40rem;" onsubmit="return false;">
-  <div class="ad-card__body" style="gap: var(--ad-space-5); padding: var(--ad-space-6);">
-
-    <div>
-      <p class="ad-overline">Workspace</p>
-      <h2 style="font-size: var(--ad-text-xl); margin: var(--ad-space-1) 0 var(--ad-space-1);">Theme settings</h2>
-      <p class="ad-card__text">How this workspace resolves design tokens at build time.</p>
-    </div>
-
-    <div class="ad-callout ad-callout--info" style="margin: 0;">
-      <svg class="ad-callout__icon" viewBox="0 0 24 24" fill="none" aria-hidden="true">
-        <circle cx="12" cy="12" r="9.5" stroke="currentColor" stroke-width="2"/>
-        <path d="M12 11v6M12 7.4v.2" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/>
-      </svg>
-      <div class="ad-callout__body">
-        <p style="margin:0">Changes here rebuild <code>tokens.css</code> for every project in the workspace.</p>
-      </div>
-    </div>
-
-    <div class="ad-form" style="max-width: none;">
-      <div class="ad-form__row">
-        <div class="ad-field">
-          <label class="ad-field__label" for="pattern-prefix">Custom property prefix</label>
-          <input class="ad-input" id="pattern-prefix" type="text" value="ad" aria-describedby="pattern-prefix-hint">
-          <p class="ad-field__hint" id="pattern-prefix-hint">Emitted as <code>--ad-*</code>.</p>
-        </div>
-        <div class="ad-field">
-          <label class="ad-field__label" for="pattern-base">Base unit</label>
-          <select class="ad-select" id="pattern-base">
-            <option>2px</option>
-            <option selected>4px</option>
-            <option>8px</option>
-          </select>
-          <p class="ad-field__hint">Changing this rewrites the whole space ramp.</p>
-        </div>
-      </div>
-
-      <div class="ad-field">
-        <label class="ad-field__label" for="pattern-accent">Accent ramp</label>
-        <input class="ad-input" id="pattern-accent" type="text" value="persimmon" aria-invalid="true" aria-describedby="pattern-accent-error">
-        <p class="ad-field__error" id="pattern-accent-error">
-          Ramp <code>persimmon</code> has no step that clears 4.5:1 on white below 700. Pick a step explicitly.
-        </p>
-      </div>
-
-      <fieldset class="ad-fieldset">
-        <legend>Emit</legend>
-        <label class="ad-choice">
-          <input type="checkbox" checked>
-          <span class="ad-choice__text">CSS custom properties<span class="ad-choice__hint">One <code>:root</code> block plus both dark selectors.</span></span>
-        </label>
-        <label class="ad-choice">
-          <input type="checkbox" checked>
-          <span class="ad-choice__text">Reduced-motion overrides<span class="ad-choice__hint">Zeroes every duration token.</span></span>
-        </label>
-        <label class="ad-choice">
-          <input type="checkbox">
-          <span class="ad-choice__text">JSON<span class="ad-choice__hint">For tooling that cannot read CSS.</span></span>
-        </label>
-      </fieldset>
-
-      <label class="ad-switch">
-        <input type="checkbox" checked>
-        <span class="ad-switch__track"></span>
-        <span>Fail the build on a contrast regression</span>
-      </label>
-
-      <div class="ad-progress" style="max-width: none;">
-        <div class="ad-progress__label">
-          <span id="pattern-progress">Token coverage</span>
-          <span class="ad-progress__value">{{ site.data.build.aliases }} / {{ site.data.build.aliases }} aliases documented</span>
-        </div>
-        <div class="ad-progress__track" role="progressbar" aria-labelledby="pattern-progress"
-             aria-valuenow="100" aria-valuemin="0" aria-valuemax="100">
-          <div class="ad-progress__fill" style="width: 100%;"></div>
-        </div>
-      </div>
-    </div>
-  </div>
-
-  <div class="ad-card__footer" style="justify-content: space-between; padding: var(--ad-space-4) var(--ad-space-6);">
-    <span class="ad-badge ad-badge--caution"><span class="ad-badge__dot"></span>Unsaved changes</span>
-    <span style="display: flex; gap: var(--ad-space-3);">
-      <button type="button" class="ad-btn ad-btn--ghost">Discard</button>
-      <button type="submit" class="ad-btn ad-btn--primary">Save and rebuild</button>
-    </span>
-  </div>
-</form>
-{% endexample %}
-
-## What this screen is testing
-
-- **Vertical rhythm across mixed control heights.** An input, a select, a
-  fieldset of checkboxes, a switch, and a progress bar in one column, all spaced
-  by `--ad-space-5` from the form and never by margins of their own.
-- **The error state in a real form.** The invalid field sits between two valid
-  ones. It has to be findable without being the loudest thing on screen.
-- **A footer that carries both status and actions.** Status left, actions right,
-  and the destructive-ish action (`Discard`) as a ghost so it does not compete.
-- **Two tones at once.** A caution badge and an info callout coexist without the
-  page turning into a traffic light, because both draw from the same restrained
-  status tints.
-- **The dark theme.** Nothing in this pattern has a colour of its own — every
-  value is an alias, and the dark block repoints all of them at once.
diff --git a/_sass/base/_reset.scss b/_sass/base/_reset.scss
index a61a830..7d4be35 100644
--- a/_sass/base/_reset.scss
+++ b/_sass/base/_reset.scss
@@ -1,5 +1,17 @@
 /* A short reset. Everything it sets is something the system would otherwise
-   have to override in every component. */
+   have to override in every component.
+
+   The four rem constants below (measure/content-width/sidebar-width/header-
+   height) belong to *this documentation site's chrome*, not to the design
+   system it documents — the source design system defines no container-width
+   tokens at all, so there is nothing under `--ad-*` to reach for here. See
+   the header comment in site/_shell.scss. */
+
+$doc-measure: 68ch;
+$doc-content-width: 50rem;
+$doc-wide-width: 72rem;
+$doc-sidebar-width: 16rem;
+$doc-header-height: 3.5rem;
 
 *,
 *::before,
@@ -8,7 +20,7 @@
 html {
   -webkit-text-size-adjust: 100%;
   scroll-behavior: smooth;
-  scroll-padding-top: calc(var(--ad-header-height) + var(--ad-space-5));
+  scroll-padding-top: calc(#{$doc-header-height} + var(--ad-space-s));
 }
 
 @media (prefers-reduced-motion: reduce) {
@@ -24,19 +36,19 @@ html {
 
 body {
   margin: 0;
-  background: var(--ad-color-canvas);
-  color: var(--ad-color-text);
+  background: var(--ad-color-surface-page);
+  color: var(--ad-color-text-primary);
   font-family: var(--ad-font-sans);
-  font-size: var(--ad-text-md);
-  line-height: var(--ad-leading-normal);
+  font-size: var(--ad-step-0);
+  line-height: 1.55;
   font-synthesis-weight: none;
   -webkit-font-smoothing: antialiased;
   text-rendering: optimizeLegibility;
 }
 
 ::selection {
-  background: var(--ad-color-selection);
-  color: var(--ad-color-text);
+  background: var(--ad-color-accent-soft);
+  color: var(--ad-color-accent-ink);
 }
 
 h1, h2, h3, h4, h5, h6, p, figure, blockquote, dl, dd { margin: 0; }
@@ -51,16 +63,16 @@ button, input, select, textarea {
 
 hr {
   height: 1px;
-  margin: var(--ad-space-6) 0;
+  margin: var(--ad-space-l) 0;
   border: 0;
-  background: var(--ad-color-border-subtle);
+  background: var(--ad-color-border-hairline);
 }
 
 /* One focus ring, defined once. No component is allowed its own. */
 :focus-visible {
-  outline: 2px solid var(--ad-color-focus);
+  outline: 2px solid var(--ad-color-accent);
   outline-offset: 2px;
-  border-radius: var(--ad-radius-xs);
+  border-radius: var(--ad-radius-sm);
 }
 :focus:not(:focus-visible) { outline: none; }
 
diff --git a/_sass/base/_typography.scss b/_sass/base/_typography.scss
index 8b1419f..7fb5b3f 100644
--- a/_sass/base/_typography.scss
+++ b/_sass/base/_typography.scss
@@ -1,67 +1,62 @@
-/* Every rule here reads from a type token. If a size appears in this file as
-   a raw value, it is a bug. */
+/* Every rule here reads a type token for family and size. Weight, line-height,
+   and letter-spacing are hardcoded to the exact numbers the source ships —
+   there is no `--ad-weight-*` / `--ad-leading-*` / `--ad-tracking-*` scale in
+   the real system, only eight fixed compound styles (see scale.yml's
+   `typography.styles`, which this file implements verbatim). */
 
-h1, h2, h3, h4 {
-  font-weight: var(--ad-weight-semibold);
-  line-height: var(--ad-leading-snug);
-  text-wrap: balance;
-}
-
-h1 {
-  font-family: var(--ad-font-display);
-  font-size: var(--ad-text-3xl);
-  font-weight: var(--ad-weight-semibold);
-  line-height: var(--ad-leading-tight);
-  letter-spacing: var(--ad-tracking-tight);
-  font-optical-sizing: auto;
+.type-display { font: 600 var(--ad-step-4)/1.08 var(--ad-font-sans); letter-spacing: -0.015em; }
+.type-h1      { font: 600 var(--ad-step-3)/1.12 var(--ad-font-sans); letter-spacing: -0.012em; }
+.type-h2      { font: 600 var(--ad-step-2)/1.16 var(--ad-font-sans); letter-spacing: -0.01em; }
+.type-h3      { font: 600 var(--ad-step-1)/1.25 var(--ad-font-sans); }
+.type-body    { font: 400 var(--ad-step-0)/1.55 var(--ad-font-sans); }
+.type-body-sm { font: 400 var(--ad-step--1)/1.5 var(--ad-font-sans); }
+.type-label {
+  font: 500 var(--ad-step--2)/1 var(--ad-font-mono);
+  text-transform: uppercase;
+  letter-spacing: 0.08em;
+  color: var(--ad-color-text-secondary);
 }
+.type-mono { font: 400 var(--ad-step--1)/1.5 var(--ad-font-mono); }
 
-h2 {
-  font-size: var(--ad-text-2xl);
-  letter-spacing: var(--ad-tracking-tight);
-}
+/* Prose defaults for the documentation body copy. These read the same
+   tokens the compound classes above do; they are not a second scale. */
+h1, h2, h3, h4 { font-weight: 600; text-wrap: balance; }
 
-h3 { font-size: var(--ad-text-xl); }
-h4 { font-size: var(--ad-text-lg); }
+h1 { @extend .type-h1; }
+h2 { @extend .type-h2; }
+h3 { @extend .type-h3; }
+h4 { font: 600 var(--ad-step-0)/1.3 var(--ad-font-sans); }
 
-p, ul, ol { margin: 0 0 var(--ad-space-4); }
-li + li { margin-top: var(--ad-space-1); }
+p, ul, ol { margin: 0 0 var(--ad-space-s); }
+li + li { margin-top: var(--ad-space-3xs); }
 
 a {
   color: var(--ad-color-accent);
   text-decoration-color: color-mix(in srgb, currentColor 35%, transparent);
   text-underline-offset: 0.18em;
   text-decoration-thickness: 1px;
-  transition: color var(--ad-duration-fast) var(--ad-ease-standard),
-              text-decoration-color var(--ad-duration-fast) var(--ad-ease-standard);
+  transition: color var(--ad-dur-fast) var(--ad-ease),
+              text-decoration-color var(--ad-dur-fast) var(--ad-ease);
 }
 a:hover {
-  color: var(--ad-color-accent-hover);
+  color: var(--ad-color-accent-ink);
   text-decoration-color: currentColor;
 }
 
-strong, b { font-weight: var(--ad-weight-semibold); }
+strong, b { font-weight: 600; }
 
-small { font-size: var(--ad-text-sm); }
-
-.ad-overline {
-  font-size: var(--ad-text-2xs);
-  font-weight: var(--ad-weight-semibold);
-  letter-spacing: var(--ad-tracking-wide);
-  text-transform: uppercase;
-  color: var(--ad-color-text-muted);
-}
+small { font-size: var(--ad-step--1); }
 
 .ad-lead {
-  font-size: var(--ad-text-lg);
-  line-height: var(--ad-leading-relaxed);
-  color: var(--ad-color-text-muted);
-  max-width: var(--ad-measure-prose);
+  font-size: var(--ad-step-1);
+  line-height: 1.5;
+  color: var(--ad-color-text-secondary);
+  max-width: 68ch;
   text-wrap: pretty;
 }
 
-.ad-muted { color: var(--ad-color-text-muted); }
-.ad-subtle { color: var(--ad-color-text-subtle); }
+.ad-muted { color: var(--ad-color-text-secondary); }
+.ad-subtle { color: var(--ad-color-text-tertiary); }
 
 code, kbd, samp, pre {
   font-family: var(--ad-font-mono);
@@ -71,27 +66,27 @@ code, kbd, samp, pre {
 :not(pre) > code {
   padding: 0.15em 0.4em;
   font-size: 0.875em;
-  background: var(--ad-color-surface-sunken);
-  border: 1px solid var(--ad-color-border-subtle);
-  border-radius: var(--ad-radius-xs);
+  background: var(--ad-color-surface-fill);
+  border: 1px solid var(--ad-color-border-hairline);
+  border-radius: var(--ad-radius-sm);
   white-space: nowrap;
 }
 
 kbd {
   display: inline-block;
   padding: 0.1em 0.45em;
-  font-size: var(--ad-text-xs);
+  font-size: var(--ad-step--2);
   line-height: 1.6;
-  background: var(--ad-color-surface);
-  border: 1px solid var(--ad-color-border);
+  background: var(--ad-color-surface-card);
+  border: 1px solid var(--ad-color-border-default);
   border-bottom-width: 2px;
-  border-radius: var(--ad-radius-xs);
-  color: var(--ad-color-text-muted);
+  border-radius: var(--ad-radius-sm);
+  color: var(--ad-color-text-secondary);
 }
 
 blockquote {
-  margin-block: var(--ad-space-4);
-  padding-left: var(--ad-space-4);
-  border-left: 2px solid var(--ad-color-accent-border);
-  color: var(--ad-color-text-muted);
+  margin-block: var(--ad-space-s);
+  padding-left: var(--ad-space-s);
+  border-left: 2px solid var(--ad-color-accent);
+  color: var(--ad-color-text-secondary);
 }
diff --git a/_sass/components/_badge.scss b/_sass/components/_badge.scss
index 6eb0aa7..b48309d 100644
--- a/_sass/components/_badge.scss
+++ b/_sass/components/_badge.scss
@@ -1,49 +1,74 @@
 /* Badge
-   A label, not a button. Never put a click handler on one. */
+   Mono, uppercase, tracked — the catalog-label convention for status and
+   meta values (SAVED, OVERDUE, RECOMMENDED). A label, never a button. */
 
 .ad-badge {
-  /* --_tone is the badge's hue; --_fg is where that hue lands. Keeping them
-     apart is what lets --solid swap background and foreground without a
-     custom property referring to itself, which resolves to the wrong value. */
-  --_tone: var(--ad-color-text-muted);
-  --_bg: var(--ad-color-surface-sunken);
-  --_fg: var(--_tone);
-  --_border: var(--ad-color-border);
+  --_bg: var(--ad-color-surface-fill);
+  --_fg: var(--ad-color-text-secondary);
+  --_border: var(--ad-color-border-hairline);
 
   display: inline-flex;
   align-items: center;
-  gap: var(--ad-space-1);
-  padding: 0.125rem var(--ad-space-2);
+  gap: 0.25rem;
+  padding: 0.1875rem 0.5rem;
   border: 1px solid var(--_border);
-  border-radius: var(--ad-radius-xs);
+  border-radius: var(--ad-radius-sm);
   background: var(--_bg);
   color: var(--_fg);
-  font-size: var(--ad-text-xs);
-  font-weight: var(--ad-weight-medium);
-  line-height: 1.5;
+  font-family: var(--ad-font-mono);
+  font-size: var(--ad-step--2);
+  text-transform: uppercase;
+  letter-spacing: 0.06em;
+  line-height: 1;
   white-space: nowrap;
 }
 
-.ad-badge--accent   { --_bg: var(--ad-color-accent-subtle);   --_tone: var(--ad-color-accent);   --_border: var(--ad-color-accent-border); }
-.ad-badge--positive { --_bg: var(--ad-color-positive-subtle); --_tone: var(--ad-color-positive); --_border: var(--ad-color-positive-border); }
-.ad-badge--caution  { --_bg: var(--ad-color-caution-subtle);  --_tone: var(--ad-color-caution);  --_border: var(--ad-color-caution-border); }
-.ad-badge--critical { --_bg: var(--ad-color-critical-subtle); --_tone: var(--ad-color-critical); --_border: var(--ad-color-critical-border); }
-.ad-badge--info     { --_bg: var(--ad-color-info-subtle);     --_tone: var(--ad-color-info);     --_border: var(--ad-color-info-border); }
-
-.ad-badge--pill { border-radius: var(--ad-radius-full); padding-inline: var(--ad-space-3); }
-.ad-badge--solid { --_bg: var(--_tone); --_fg: var(--ad-color-text-inverse); --_border: transparent; }
-
-/* A dot lets a badge carry status without relying on colour alone… */
-.ad-badge__dot {
-  width: 0.4rem;
-  height: 0.4rem;
-  border-radius: var(--ad-radius-full);
-  background: currentColor;
-  flex: none;
-}
+.ad-badge--accent  { --_bg: var(--ad-color-accent-soft);  --_fg: var(--ad-color-accent-ink);  --_border: transparent; }
+.ad-badge--success { --_bg: var(--ad-color-success-bg);   --_fg: var(--ad-color-success);      --_border: transparent; }
+.ad-badge--danger  { --_bg: var(--ad-color-danger-bg);    --_fg: var(--ad-color-danger);       --_border: transparent; }
 
 .ad-badge-set {
   display: flex;
   flex-wrap: wrap;
-  gap: var(--ad-space-2);
+  gap: var(--ad-space-2xs);
+}
+
+/* Tag
+   A removable mono chip for freeform tags — locations, categories.
+   `--dashed` renders an "add tag" affordance instead of a value. */
+
+.ad-tag {
+  display: inline-flex;
+  align-items: center;
+  gap: 0.375rem;
+  padding: 0.25rem 0.625rem;
+  border-radius: var(--ad-radius-sm);
+  background: var(--ad-color-surface-fill);
+  border: 1px solid var(--ad-color-border-default);
+  color: var(--ad-color-text-secondary);
+  font-family: var(--ad-font-mono);
+  font-size: var(--ad-step--2);
+  line-height: 1;
+}
+
+.ad-tag--dashed {
+  background: transparent;
+  border-style: dashed;
+  color: var(--ad-color-text-tertiary);
+  cursor: pointer;
+}
+
+.ad-tag__remove {
+  all: unset;
+  cursor: pointer;
+  color: var(--ad-color-text-tertiary);
+  line-height: 1;
+  font-size: 0.75rem;
+}
+.ad-tag__remove:hover { color: var(--ad-color-text-primary); }
+
+.ad-tag-set {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 0.375rem;
 }
diff --git a/_sass/components/_button.scss b/_sass/components/_button.scss
index 56faf22..57f8ec2 100644
--- a/_sass/components/_button.scss
+++ b/_sass/components/_button.scss
@@ -1,111 +1,119 @@
 /* Button
-   Variants: primary, secondary, ghost, danger.
-   Sizes: sm, md (default), lg.
-   Every variant shares one geometry so a row of mixed buttons still lines up. */
+   A flat, quiet button that recedes rather than performs — no gradients, no
+   shine. Variants: default, primary, dark, ghost, danger. Sizes: sm, md
+   (default), lg — padding and font step only, weight and radius stay put. */
 
 .ad-btn {
-  --_bg: transparent;
-  --_fg: var(--ad-color-text);
-  --_border: transparent;
-  --_bg-hover: var(--ad-color-surface-hover);
-  --_height: 2.25rem;
-  --_padding: var(--ad-space-4);
-  --_font: var(--ad-text-sm);
+  --_bg: var(--ad-color-surface-card);
+  --_fg: var(--ad-color-text-primary);
+  --_border: var(--ad-color-border-default);
+  --_pad: var(--ad-space-xs) var(--ad-space-s);
+  --_font: var(--ad-step--1);
 
   display: inline-flex;
   align-items: center;
   justify-content: center;
-  gap: var(--ad-space-2);
-  min-height: var(--_height);
-  padding-inline: var(--_padding);
+  gap: var(--ad-space-2xs);
+  padding: var(--_pad);
   border: 1px solid var(--_border);
-  border-radius: var(--ad-radius-sm);
+  border-radius: var(--ad-radius-md);
   background: var(--_bg);
   color: var(--_fg);
+  font-family: var(--ad-font-sans);
   font-size: var(--_font);
-  font-weight: var(--ad-weight-semibold);
-  line-height: 1;
+  font-weight: 550;
+  letter-spacing: -0.01em;
+  line-height: 1.2;
   text-align: center;
   text-decoration: none;
   white-space: nowrap;
   cursor: pointer;
-  transition: background-color var(--ad-duration-fast) var(--ad-ease-standard),
-              border-color var(--ad-duration-fast) var(--ad-ease-standard),
-              color var(--ad-duration-fast) var(--ad-ease-standard);
+  transition: filter var(--ad-dur-fast) var(--ad-ease),
+              border-color var(--ad-dur-fast) var(--ad-ease);
 }
 
-.ad-btn:hover:not(:disabled):not([aria-disabled="true"]) { background: var(--_bg-hover); }
-.ad-btn:active:not(:disabled):not([aria-disabled="true"]) { transform: translateY(0.5px); }
+.ad-btn:hover:not(:disabled):not([aria-disabled="true"]) { filter: brightness(0.96); }
+.ad-btn:active:not(:disabled):not([aria-disabled="true"]) { filter: brightness(0.92); }
 
 .ad-btn:disabled,
 .ad-btn[aria-disabled="true"] {
-  opacity: 0.45;
-  cursor: not-allowed;
-  transform: none;
+  opacity: 0.5;
+  cursor: default;
 }
 
 .ad-btn--primary {
   --_bg: var(--ad-color-accent);
-  --_fg: var(--ad-color-on-accent);
+  --_fg: var(--ad-color-accent-fg);
   --_border: var(--ad-color-accent);
-  --_bg-hover: var(--ad-color-accent-hover);
 }
-.ad-btn--primary:hover:not(:disabled) { border-color: var(--ad-color-accent-hover); }
 
-.ad-btn--secondary {
-  --_bg: var(--ad-color-surface);
-  --_fg: var(--ad-color-text);
-  --_border: var(--ad-color-border);
-  --_bg-hover: var(--ad-color-surface-hover);
+/* Reserved for the single most-committing action in a view — a final Save,
+   never a whole row of them. */
+.ad-btn--dark {
+  --_bg: var(--ad-color-text-primary);
+  --_fg: var(--ad-color-surface-page);
+  --_border: var(--ad-color-text-primary);
 }
-.ad-btn--secondary:hover:not(:disabled) { border-color: var(--ad-color-border-strong); }
 
 .ad-btn--ghost {
-  --_fg: var(--ad-color-text-muted);
+  --_bg: transparent;
+  --_fg: var(--ad-color-text-primary);
+  --_border: transparent;
 }
-.ad-btn--ghost:hover:not(:disabled) { --_fg: var(--ad-color-text); }
 
-/* Filled danger reads from the solid critical fill, not from
-   --ad-color-critical: that one is a text colour and goes light in the dark
-   theme, which would leave a white label on a pink button. */
+/* The source hardcodes this label white rather than deriving it — danger
+   is not "on a filled accent surface", so --ad-color-accent-fg is reused
+   here only because its resolved value is the same literal white, not
+   because the two roles are the same thing. */
 .ad-btn--danger {
-  --_bg: var(--ad-color-critical-solid);
-  --_fg: var(--ad-color-on-critical);
-  --_border: var(--ad-color-critical-solid);
-  --_bg-hover: color-mix(in srgb, var(--ad-color-critical-solid) 82%, black);
+  --_bg: var(--ad-color-danger);
+  --_fg: var(--ad-color-accent-fg);
+  --_border: var(--ad-color-danger);
 }
 
-.ad-btn--sm { --_height: 1.75rem; --_padding: var(--ad-space-3); --_font: var(--ad-text-xs); }
-.ad-btn--lg { --_height: 2.75rem; --_padding: var(--ad-space-5); --_font: var(--ad-text-md); }
-
-/* Square footprint for a lone glyph. */
-.ad-btn--icon { --_padding: 0; width: var(--_height); }
+.ad-btn--sm { --_pad: 0.375rem 0.75rem; --_font: 0.8125rem; }
+.ad-btn--lg { --_pad: 0.75rem 1.375rem; --_font: var(--ad-step-0); }
 
 .ad-btn--block { width: 100%; }
 
-/* Grouped buttons collapse into one control with shared edges. */
 .ad-btn-group {
   display: inline-flex;
   isolation: isolate;
 }
 .ad-btn-group > .ad-btn { border-radius: 0; }
-.ad-btn-group > .ad-btn:first-child { border-start-start-radius: var(--ad-radius-sm); border-end-start-radius: var(--ad-radius-sm); }
-.ad-btn-group > .ad-btn:last-child { border-start-end-radius: var(--ad-radius-sm); border-end-end-radius: var(--ad-radius-sm); }
+.ad-btn-group > .ad-btn:first-child { border-start-start-radius: var(--ad-radius-md); border-end-start-radius: var(--ad-radius-md); }
+.ad-btn-group > .ad-btn:last-child { border-start-end-radius: var(--ad-radius-md); border-end-end-radius: var(--ad-radius-md); }
 .ad-btn-group > .ad-btn + .ad-btn { margin-inline-start: -1px; }
 .ad-btn-group > .ad-btn:hover,
 .ad-btn-group > .ad-btn:focus-visible { z-index: 1; }
 
-/* A spinner that replaces the label without changing the button's width. */
-.ad-btn[data-loading="true"] { color: transparent; pointer-events: none; position: relative; }
-.ad-btn[data-loading="true"]::after {
-  content: "";
-  position: absolute;
-  width: 0.875em;
-  height: 0.875em;
-  border: 2px solid color-mix(in srgb, var(--_fg) 30%, transparent);
-  border-top-color: var(--_fg);
-  border-radius: var(--ad-radius-full);
-  animation: ad-spin var(--ad-duration-spin) linear infinite;
+/* IconButton — a square icon-only button. Always needs an aria-label; it
+   becomes both the accessible name and the native tooltip. */
+.ad-icon-btn {
+  --_size: 2.125rem;
+  --_bg: transparent;
+  --_border: transparent;
+  --_fg: var(--ad-color-text-secondary);
+
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: var(--_size);
+  height: var(--_size);
+  flex: none;
+  border: 1px solid var(--_border);
+  border-radius: var(--ad-radius-md);
+  background: var(--_bg);
+  color: var(--_fg);
+  cursor: pointer;
+  transition: background-color var(--ad-dur-fast) var(--ad-ease),
+              color var(--ad-dur-fast) var(--ad-ease);
 }
+.ad-icon-btn:hover:not(:disabled) { background: var(--ad-color-surface-fill-2); }
+.ad-icon-btn:disabled { opacity: 0.5; cursor: default; }
+
+.ad-icon-btn--sm { --_size: 1.75rem; }
+.ad-icon-btn--lg { --_size: 2.5rem; }
 
-@keyframes ad-spin { to { transform: rotate(360deg); } }
+.ad-icon-btn--outline { --_bg: var(--ad-color-surface-card); --_border: var(--ad-color-border-default); --_fg: var(--ad-color-text-primary); }
+.ad-icon-btn--accent  { --_bg: var(--ad-color-accent-soft); --_fg: var(--ad-color-accent-ink); }
diff --git a/_sass/components/_callout.scss b/_sass/components/_callout.scss
deleted file mode 100644
index ad86cf8..0000000
--- a/_sass/components/_callout.scss
+++ /dev/null
@@ -1,42 +0,0 @@
-/* Callout
-   Block-level messaging. Takes the full status triple — foreground, tint,
-   edge — so it can never half-match a badge of the same status. */
-
-.ad-callout {
-  --_fg: var(--ad-color-info);
-  --_bg: var(--ad-color-info-subtle);
-  --_border: var(--ad-color-info-border);
-
-  display: grid;
-  grid-template-columns: auto 1fr;
-  gap: var(--ad-space-3);
-  padding: var(--ad-space-4);
-  margin-block: var(--ad-space-5);
-  background: var(--_bg);
-  border: 1px solid var(--_border);
-  border-inline-start: 3px solid var(--_fg);
-  border-radius: var(--ad-radius-md);
-  color: var(--ad-color-text);
-  font-size: var(--ad-text-sm);
-  line-height: var(--ad-leading-normal);
-}
-
-.ad-callout--positive { --_fg: var(--ad-color-positive); --_bg: var(--ad-color-positive-subtle); --_border: var(--ad-color-positive-border); }
-.ad-callout--caution  { --_fg: var(--ad-color-caution);  --_bg: var(--ad-color-caution-subtle);  --_border: var(--ad-color-caution-border); }
-.ad-callout--critical { --_fg: var(--ad-color-critical); --_bg: var(--ad-color-critical-subtle); --_border: var(--ad-color-critical-border); }
-.ad-callout--accent   { --_fg: var(--ad-color-accent);   --_bg: var(--ad-color-accent-subtle);   --_border: var(--ad-color-accent-border); }
-.ad-callout--neutral  { --_fg: var(--ad-color-border-strong); --_bg: var(--ad-color-surface-sunken); --_border: var(--ad-color-border); }
-
-.ad-callout__icon {
-  flex: none;
-  width: 1.25rem;
-  height: 1.25rem;
-  color: var(--_fg);
-}
-.ad-callout__body > :last-child { margin-bottom: 0; }
-.ad-callout__title {
-  margin: 0 0 var(--ad-space-1);
-  font-size: var(--ad-text-sm);
-  font-weight: var(--ad-weight-semibold);
-  color: var(--_fg);
-}
diff --git a/_sass/components/_card.scss b/_sass/components/_card.scss
index be2f8c5..f2b4c32 100644
--- a/_sass/components/_card.scss
+++ b/_sass/components/_card.scss
@@ -1,80 +1,101 @@
 /* Card
-   A bordered sheet by default. Elevation is opt-in, because a page of
-   floating cards has no hierarchy at all. */
+   Flat surface, hairline border, radius-lg. `--elevated` is opt-in and
+   reserved for the one focused/primary card in a view — a dialog, the
+   primary capture card — flat is the default everywhere else. */
 
 .ad-card {
   display: flex;
   flex-direction: column;
-  background: var(--ad-color-surface);
-  border: 1px solid var(--ad-color-border);
-  border-radius: var(--ad-radius-md);
+  background: var(--ad-color-surface-card);
+  border: 1px solid var(--ad-color-border-hairline);
+  border-radius: var(--ad-radius-lg);
+  box-shadow: none;
   overflow: hidden;
 }
 
-.ad-card--raised { box-shadow: var(--ad-elevation-1); }
-.ad-card--floating { box-shadow: var(--ad-elevation-2); border-color: transparent; }
+.ad-card--elevated { box-shadow: var(--ad-shadow-float); }
 
-.ad-card__media { background: var(--ad-color-surface-sunken); aspect-ratio: 16 / 9; }
+/* CardRow — a hairline-divided list row for use inside Card: entry lists,
+   task lists, settings rows. The first row's top border is removed
+   automatically so callers never have to override it by hand. */
+.ad-card-row {
+  display: flex;
+  align-items: center;
+  gap: var(--ad-space-xs);
+  padding: var(--ad-space-xs) var(--ad-space-s);
+  border-top: 1px solid var(--ad-color-border-hairline);
+  color: var(--ad-color-text-primary);
+  font-family: var(--ad-font-sans);
+  font-size: var(--ad-step--1);
+  transition: background-color var(--ad-dur-fast) var(--ad-ease);
+}
+.ad-card > .ad-card-row:first-child { border-top: none; }
+
+.ad-card-row--clickable {
+  cursor: pointer;
+  text-align: left;
+  width: 100%;
+  border-left: 0;
+  border-right: 0;
+  background: none;
+}
+.ad-card-row--clickable:hover { background: var(--ad-color-surface-fill); }
 
+/* Documentation-only extras: a body/title/text/footer quartet and a
+   responsive grid, used by the prose pages (rationale.md, index.md) to show
+   a Card as a plain content tile rather than a data row. */
 .ad-card__body {
   display: flex;
   flex-direction: column;
-  gap: var(--ad-space-2);
-  padding: var(--ad-space-4);
+  gap: var(--ad-space-2xs);
+  padding: var(--ad-space-m);
 }
-
 .ad-card__title {
-  font-size: var(--ad-text-lg);
-  font-weight: var(--ad-weight-semibold);
-  line-height: var(--ad-leading-snug);
+  font: 600 var(--ad-step-0)/1.3 var(--ad-font-sans);
 }
 .ad-card__text {
   margin: 0;
-  color: var(--ad-color-text-muted);
-  font-size: var(--ad-text-sm);
+  color: var(--ad-color-text-secondary);
+  font-size: var(--ad-step--1);
 }
-
 .ad-card__footer {
   display: flex;
   flex-wrap: wrap;
   align-items: center;
-  gap: var(--ad-space-3);
+  gap: var(--ad-space-xs);
   margin-top: auto;
-  padding: var(--ad-space-3) var(--ad-space-4);
-  border-top: 1px solid var(--ad-color-border-subtle);
-  background: var(--ad-color-surface-sunken);
+  padding: var(--ad-space-xs) var(--ad-space-m);
+  border-top: 1px solid var(--ad-color-border-hairline);
+  background: var(--ad-color-surface-fill);
 }
 
-/* A whole card that behaves as one link target. */
-.ad-card--link { transition: border-color var(--ad-duration-fast) var(--ad-ease-standard), box-shadow var(--ad-duration-fast) var(--ad-ease-standard); }
-.ad-card--link:hover { border-color: var(--ad-color-border-strong); box-shadow: var(--ad-elevation-1); }
-.ad-card--link:focus-within { outline: 2px solid var(--ad-color-focus); outline-offset: 2px; }
+.ad-card--link { transition: border-color var(--ad-dur-fast) var(--ad-ease), box-shadow var(--ad-dur-fast) var(--ad-ease); }
+.ad-card--link:hover { border-color: var(--ad-color-border-default); box-shadow: var(--ad-shadow-sm); }
+.ad-card--link:focus-within { outline: 2px solid var(--ad-color-accent); outline-offset: 2px; }
 
 .ad-card-grid {
   display: grid;
   grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
-  gap: var(--ad-space-4);
+  gap: var(--ad-space-s);
 }
 
-/* Avatar lives here because it almost never appears outside a card or a row. */
+/* Avatar — lives here because it almost never appears outside a card or a
+   row (the Loop dashboard's initials chip, Archive's file-type chip). */
 .ad-avatar {
   display: inline-flex;
   align-items: center;
   justify-content: center;
-  width: 2rem;
-  height: 2rem;
+  width: 1.875rem;
+  height: 1.875rem;
   flex: none;
   border-radius: var(--ad-radius-full);
-  background: var(--ad-color-accent-subtle);
-  color: var(--ad-color-accent);
-  font-size: var(--ad-text-xs);
-  font-weight: var(--ad-weight-semibold);
-  letter-spacing: var(--ad-tracking-wide);
-  text-transform: uppercase;
+  background: var(--ad-color-accent-soft);
+  color: var(--ad-color-accent-ink);
+  font-family: var(--ad-font-mono);
+  font-size: var(--ad-step--2);
+  font-weight: 600;
   overflow: hidden;
 }
-.ad-avatar--sm { width: 1.5rem; height: 1.5rem; font-size: var(--ad-text-2xs); }
-.ad-avatar--lg { width: 3rem; height: 3rem; font-size: var(--ad-text-md); }
-
-.ad-avatar-stack { display: flex; }
-.ad-avatar-stack > .ad-avatar + .ad-avatar { margin-inline-start: -0.5rem; box-shadow: 0 0 0 2px var(--ad-color-surface); }
+.ad-avatar--square { border-radius: var(--ad-radius-md); background: var(--ad-color-surface-fill); color: var(--ad-color-text-secondary); border: 1px solid var(--ad-color-border-hairline); }
+.ad-avatar--sm { width: 1.5rem; height: 1.5rem; }
+.ad-avatar--lg { width: 2.25rem; height: 2.25rem; }
diff --git a/_sass/components/_feedback.scss b/_sass/components/_feedback.scss
new file mode 100644
index 0000000..6d0793b
--- /dev/null
+++ b/_sass/components/_feedback.scss
@@ -0,0 +1,129 @@
+/* Tooltip — a dark mono label on hover, positioned above the child. Pure CSS:
+   the trigger and bubble are siblings inside one relatively-positioned span,
+   shown on :hover/:focus-within so it works from the keyboard too. */
+.ad-tooltip {
+  position: relative;
+  display: inline-flex;
+}
+
+.ad-tooltip__bubble {
+  position: absolute;
+  bottom: 100%;
+  left: 50%;
+  transform: translateX(-50%);
+  margin-bottom: 0.375rem;
+  padding: 0.25rem 0.5rem;
+  border-radius: var(--ad-radius-sm);
+  background: var(--ad-color-text-primary);
+  color: var(--ad-color-surface-page);
+  font-family: var(--ad-font-mono);
+  /* 11px is the source's own literal choice — smaller than --ad-step--2, the
+     smallest step on the scale, because a tooltip is meant to read as a
+     glance-only aside rather than body text. */
+  font-size: 11px;
+  white-space: nowrap;
+  line-height: 1.4;
+  z-index: 20;
+  opacity: 0;
+  visibility: hidden;
+  pointer-events: none;
+  transition: opacity var(--ad-dur-fast) var(--ad-ease);
+}
+.ad-tooltip:hover .ad-tooltip__bubble,
+.ad-tooltip:focus-within .ad-tooltip__bubble {
+  opacity: 1;
+  visibility: visible;
+}
+
+/* Toast — the real pattern used for delete confirmations: a flat bar with an
+   optional destructive action and a dismiss. `--fixed` pins it bottom-right
+   for real use; the bare class stays in document flow for documentation. */
+.ad-toast {
+  display: flex;
+  align-items: center;
+  gap: var(--ad-space-xs);
+  min-width: 20rem;
+  padding: var(--ad-space-xs) var(--ad-space-s);
+  background: var(--ad-color-surface-card);
+  border: 1px solid var(--ad-color-border-default);
+  border-radius: var(--ad-radius-md);
+  box-shadow: var(--ad-shadow-float);
+  color: var(--ad-color-text-primary);
+  font-family: var(--ad-font-sans);
+  font-size: var(--ad-step--2);
+}
+.ad-toast--fixed {
+  position: fixed;
+  inset-block-end: var(--ad-space-s);
+  inset-inline-end: var(--ad-space-s);
+  z-index: 60;
+}
+.ad-toast__message { flex: 1; }
+.ad-toast__action {
+  all: unset;
+  cursor: pointer;
+  color: var(--ad-color-danger);
+  font-weight: 600;
+}
+.ad-toast__dismiss {
+  all: unset;
+  cursor: pointer;
+  color: var(--ad-color-text-tertiary);
+  font-size: 1rem;
+  line-height: 1;
+}
+.ad-toast__dismiss:hover { color: var(--ad-color-text-primary); }
+
+/* Dialog — a centered modal: flat surface, no coloured header, hairline
+   dividers between title/body/footer. `.ad-dialog-demo` scopes the overlay
+   to its own preview container instead of the viewport, so a static,
+   always-open example can sit safely inside a documentation page; wire real
+   open/close state (and a viewport-fixed overlay) in the consuming app. */
+.ad-dialog-demo {
+  position: relative;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  min-height: 20rem;
+  background: var(--ad-color-surface-fill);
+  border-radius: var(--ad-radius-lg);
+}
+.ad-dialog__overlay {
+  position: absolute;
+  inset: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: var(--ad-space-s);
+  background: rgba(20, 18, 14, 0.35);
+  border-radius: inherit;
+}
+.ad-dialog {
+  width: 26rem;
+  max-width: 100%;
+  background: var(--ad-color-surface-card);
+  border: 1px solid var(--ad-color-border-hairline);
+  border-radius: var(--ad-radius-lg);
+  box-shadow: var(--ad-shadow-float);
+  overflow: hidden;
+}
+.ad-dialog__title {
+  padding: var(--ad-space-s) var(--ad-space-m);
+  border-bottom: 1px solid var(--ad-color-border-hairline);
+  font-family: var(--ad-font-sans);
+  font-weight: 600;
+  font-size: var(--ad-step-0);
+  color: var(--ad-color-text-primary);
+}
+.ad-dialog__body {
+  padding: var(--ad-space-m);
+  color: var(--ad-color-text-primary);
+  font-size: var(--ad-step--1);
+}
+.ad-dialog__footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: var(--ad-space-2xs);
+  padding: var(--ad-space-s) var(--ad-space-m);
+  border-top: 1px solid var(--ad-color-border-hairline);
+}
diff --git a/_sass/components/_field.scss b/_sass/components/_field.scss
index 00a71a3..debfc82 100644
--- a/_sass/components/_field.scss
+++ b/_sass/components/_field.scss
@@ -1,131 +1,150 @@
 /* Field
-   The label / control / description / error quartet. The wrapper owns the
-   spacing so that every control type lines up in a form regardless of height. */
+   The label / control / hint trio. The label is always mono, uppercase, and
+   tracked — the same catalog-label convention as Badge and type-label. */
 
 .ad-field {
   display: flex;
   flex-direction: column;
-  gap: var(--ad-space-2);
+  gap: var(--ad-space-2xs);
   min-width: 0;
 }
 
 .ad-field__label {
-  font-size: var(--ad-text-sm);
-  font-weight: var(--ad-weight-medium);
-  color: var(--ad-color-text);
+  font-family: var(--ad-font-mono);
+  font-size: var(--ad-step--2);
+  font-weight: 500;
+  text-transform: uppercase;
+  letter-spacing: 0.06em;
+  color: var(--ad-color-text-secondary);
 }
 
-.ad-field__required {
-  color: var(--ad-color-critical);
-  margin-inline-start: 0.15em;
+.ad-field__hint {
+  font-size: var(--ad-step--2);
+  color: var(--ad-color-text-tertiary);
 }
 
-.ad-field__hint,
-.ad-field__error {
-  font-size: var(--ad-text-sm);
-  line-height: var(--ad-leading-snug);
-}
-.ad-field__hint { color: var(--ad-color-text-muted); }
-.ad-field__error { color: var(--ad-color-critical); font-weight: var(--ad-weight-medium); }
-
+/* Input / Select — bordered, matched to each other exactly. */
 .ad-input,
-.ad-textarea,
 .ad-select {
   width: 100%;
-  min-height: 2.25rem;
-  padding: var(--ad-space-2) var(--ad-space-3);
-  background: var(--ad-color-surface);
-  border: 1px solid var(--ad-color-border);
+  padding: var(--ad-space-xs) var(--ad-space-s);
+  background: var(--ad-color-surface-card);
+  border: 1px solid var(--ad-color-border-default);
   border-radius: var(--ad-radius-sm);
-  color: var(--ad-color-text);
-  font-size: var(--ad-text-sm);
-  transition: border-color var(--ad-duration-fast) var(--ad-ease-standard),
-              background-color var(--ad-duration-fast) var(--ad-ease-standard);
-}
-
-.ad-input::placeholder,
-.ad-textarea::placeholder { color: var(--ad-color-text-subtle); }
+  color: var(--ad-color-text-primary);
+  font-family: var(--ad-font-sans);
+  font-size: var(--ad-step--1);
+  outline: none;
+  transition: border-color var(--ad-dur-fast) var(--ad-ease),
+              box-shadow var(--ad-dur-fast) var(--ad-ease);
+}
+.ad-input::placeholder { color: var(--ad-color-text-tertiary); }
 
-.ad-input:hover:not(:disabled),
-.ad-textarea:hover:not(:disabled),
-.ad-select:hover:not(:disabled) { border-color: var(--ad-color-border-strong); }
+.ad-input:focus,
+.ad-select:focus {
+  border-color: var(--ad-color-accent);
+  box-shadow: 0 0 0 2px var(--ad-color-accent-soft);
+}
 
 .ad-input:disabled,
-.ad-textarea:disabled,
 .ad-select:disabled {
-  background: var(--ad-color-surface-sunken);
-  color: var(--ad-color-text-subtle);
-  cursor: not-allowed;
+  opacity: 0.5;
+  cursor: default;
 }
 
-.ad-textarea { min-height: 5.5rem; resize: vertical; line-height: var(--ad-leading-normal); }
-
+/* The chevron is the one flagged colour exception in this component set:
+   CSS cannot point a data-URI background-image at a custom property, so its
+   stroke is a literal hex rather than a var(--ad-*) reference. It is picked
+   to match --ad-color-text-secondary and commented here so it is not
+   mistaken for a stray colour. */
 .ad-select {
   appearance: none;
-  padding-inline-end: var(--ad-space-7);
-  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5 6 6.5l5-5' fill='none' stroke='%2378705f' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
+  padding-inline-end: var(--ad-space-l);
+  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5 6 6.5l5-5' fill='none' stroke='%235f5d59' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
   background-repeat: no-repeat;
-  background-position: right var(--ad-space-3) center;
+  background-position: right var(--ad-space-xs) center;
 }
 
-/* Invalid is expressed on the control, so it works with native validation. */
 .ad-input[aria-invalid="true"],
-.ad-textarea[aria-invalid="true"],
 .ad-select[aria-invalid="true"] {
-  border-color: var(--ad-color-critical);
-  background: var(--ad-color-critical-subtle);
+  border-color: var(--ad-color-danger);
+  background-color: var(--ad-color-danger-bg);
+}
+.ad-field__error {
+  font-size: var(--ad-step--2);
+  color: var(--ad-color-danger);
 }
 
-/* Checkbox and radio share a box; only the radius differs. */
+/* Checkbox / Radio — one shared visual language; only the radius differs. */
 .ad-choice {
-  display: flex;
-  align-items: flex-start;
-  gap: var(--ad-space-2);
-  font-size: var(--ad-text-sm);
+  display: inline-flex;
+  align-items: center;
+  gap: var(--ad-space-2xs);
   cursor: pointer;
+  font-family: var(--ad-font-sans);
+  font-size: var(--ad-step--1);
+  color: var(--ad-color-text-primary);
 }
-.ad-choice input[type="checkbox"],
-.ad-choice input[type="radio"] {
+.ad-choice input { position: absolute; opacity: 0; width: 1.125rem; height: 1.125rem; margin: 0; }
+
+.ad-choice__box {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 1.125rem;
+  height: 1.125rem;
   flex: none;
-  width: 1rem;
-  height: 1rem;
-  margin: 0.2rem 0 0;
-  accent-color: var(--ad-color-accent);
-  cursor: pointer;
+  border: 1.5px solid var(--ad-color-border-default);
+  background: var(--ad-color-surface-card);
+  color: transparent;
+  transition: background-color var(--ad-dur-fast) var(--ad-ease),
+              border-color var(--ad-dur-fast) var(--ad-ease);
+}
+.ad-choice--checkbox .ad-choice__box { border-radius: var(--ad-radius-sm); }
+.ad-choice--radio .ad-choice__box { border-radius: var(--ad-radius-full); }
+
+.ad-choice input:checked + .ad-choice__box {
+  border-color: var(--ad-color-accent);
 }
-.ad-choice__text { display: block; }
-.ad-choice__hint {
-  display: block;
-  color: var(--ad-color-text-muted);
-  font-size: var(--ad-text-xs);
+.ad-choice--checkbox input:checked + .ad-choice__box {
+  background: var(--ad-color-accent);
+  color: var(--ad-color-accent-fg);
 }
-.ad-choice input:disabled,
-.ad-choice:has(input:disabled) { cursor: not-allowed; color: var(--ad-color-text-subtle); }
+.ad-choice--radio input:checked + .ad-choice__box::after {
+  content: "";
+  width: 0.5625rem;
+  height: 0.5625rem;
+  border-radius: var(--ad-radius-full);
+  background: var(--ad-color-accent);
+}
+.ad-choice__box svg { width: 0.6875rem; height: 0.6875rem; }
 
-/* Switch — the one place in the system that overshoots on the way out. */
+.ad-choice input:focus-visible + .ad-choice__box {
+  outline: 2px solid var(--ad-color-accent);
+  outline-offset: 2px;
+}
+.ad-choice:has(input:disabled) { cursor: default; opacity: 0.5; }
+
+/* Switch — for on/off settings only, never a substitute for Checkbox. */
 .ad-switch {
   display: inline-flex;
   align-items: center;
-  gap: var(--ad-space-3);
-  cursor: pointer;
-  font-size: var(--ad-text-sm);
-}
-.ad-switch input {
-  position: absolute;
-  opacity: 0;
-  width: 2.25rem;
-  height: 1.25rem;
-  margin: 0;
+  gap: var(--ad-space-2xs);
   cursor: pointer;
+  font-family: var(--ad-font-sans);
+  font-size: var(--ad-step--1);
+  color: var(--ad-color-text-primary);
 }
+.ad-switch input { position: absolute; opacity: 0; width: 2.25rem; height: 1.25rem; margin: 0; }
+
 .ad-switch__track {
   position: relative;
   flex: none;
   width: 2.25rem;
   height: 1.25rem;
-  background: var(--ad-color-border-strong);
   border-radius: var(--ad-radius-full);
-  transition: background-color var(--ad-duration-base) var(--ad-ease-standard);
+  background: var(--ad-color-border-default);
+  transition: background-color var(--ad-dur-base) var(--ad-ease);
 }
 .ad-switch__track::after {
   content: "";
@@ -134,47 +153,42 @@
   inset-inline-start: 2px;
   width: 1rem;
   height: 1rem;
-  background: var(--ad-color-surface);
   border-radius: var(--ad-radius-full);
-  box-shadow: var(--ad-elevation-1);
-  transition: translate var(--ad-duration-base) var(--ad-ease-spring);
+  background: var(--ad-color-accent-fg);
+  box-shadow: var(--ad-shadow-sm);
+  transition: translate var(--ad-dur-base) var(--ad-ease);
 }
 .ad-switch input:checked + .ad-switch__track { background: var(--ad-color-accent); }
 .ad-switch input:checked + .ad-switch__track::after { translate: 1rem 0; }
-.ad-switch input:focus-visible + .ad-switch__track {
-  outline: 2px solid var(--ad-color-focus);
-  outline-offset: 2px;
-}
-.ad-switch input:disabled + .ad-switch__track { opacity: 0.45; }
+.ad-switch input:focus-visible + .ad-switch__track { outline: 2px solid var(--ad-color-accent); outline-offset: 2px; }
+.ad-switch:has(input:disabled) { cursor: default; opacity: 0.5; }
 
-/* Rows of fields. Collapses to one column on narrow viewports. */
+/* Rows of fields — used by the settings-form pattern. */
 .ad-form {
   display: flex;
   flex-direction: column;
-  gap: var(--ad-space-5);
-  max-width: 32rem;
+  gap: var(--ad-space-s);
+  max-width: 28rem;
 }
 .ad-form__row {
   display: grid;
-  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
-  gap: var(--ad-space-4);
-}
-.ad-form__actions {
-  display: flex;
-  flex-wrap: wrap;
-  gap: var(--ad-space-3);
-  padding-top: var(--ad-space-2);
+  grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
+  gap: var(--ad-space-xs);
 }
 .ad-fieldset {
   display: flex;
   flex-direction: column;
-  gap: var(--ad-space-3);
+  gap: var(--ad-space-2xs);
   margin: 0;
   padding: 0;
   border: 0;
 }
 .ad-fieldset > legend {
-  padding: 0 0 var(--ad-space-2);
-  font-size: var(--ad-text-sm);
-  font-weight: var(--ad-weight-medium);
+  padding: 0 0 var(--ad-space-2xs);
+  font-family: var(--ad-font-mono);
+  font-size: var(--ad-step--2);
+  font-weight: 500;
+  text-transform: uppercase;
+  letter-spacing: 0.06em;
+  color: var(--ad-color-text-secondary);
 }
diff --git a/_sass/components/_progress.scss b/_sass/components/_progress.scss
deleted file mode 100644
index a19bb56..0000000
--- a/_sass/components/_progress.scss
+++ /dev/null
@@ -1,45 +0,0 @@
-/* Progress and meter
-   `progress` is for a task that will finish; `meter` is for a level that
-   simply is what it is. The system keeps them visually distinct on purpose. */
-
-.ad-progress {
-  display: flex;
-  flex-direction: column;
-  gap: var(--ad-space-2);
-  min-width: 12rem;
-}
-.ad-progress__label {
-  display: flex;
-  justify-content: space-between;
-  gap: var(--ad-space-3);
-  font-size: var(--ad-text-sm);
-  color: var(--ad-color-text-muted);
-}
-.ad-progress__value { font-variant-numeric: tabular-nums; color: var(--ad-color-text); }
-
-.ad-progress__track {
-  height: 0.5rem;
-  background: var(--ad-color-surface-sunken);
-  border: 1px solid var(--ad-color-border-subtle);
-  border-radius: var(--ad-radius-full);
-  overflow: hidden;
-}
-.ad-progress__fill {
-  height: 100%;
-  background: var(--ad-color-accent);
-  border-radius: inherit;
-  transition: width var(--ad-duration-slow) var(--ad-ease-standard);
-}
-.ad-progress--positive .ad-progress__fill { background: var(--ad-color-positive); }
-.ad-progress--caution  .ad-progress__fill { background: var(--ad-color-caution); }
-.ad-progress--critical .ad-progress__fill { background: var(--ad-color-critical); }
-
-/* Indeterminate: a shuttle rather than a fill, because a fake percentage is a lie. */
-.ad-progress--indeterminate .ad-progress__fill {
-  width: 35%;
-  animation: ad-shuttle var(--ad-duration-loop) var(--ad-ease-standard) infinite;
-}
-@keyframes ad-shuttle {
-  0%   { margin-inline-start: -35%; }
-  100% { margin-inline-start: 100%; }
-}
diff --git a/_sass/components/_table.scss b/_sass/components/_table.scss
deleted file mode 100644
index 2ac115b..0000000
--- a/_sass/components/_table.scss
+++ /dev/null
@@ -1,62 +0,0 @@
-/* Table
-   Left-aligned by default; numeric columns opt into tabular figures and
-   right alignment so digits stack. */
-
-.ad-table-wrap {
-  overflow-x: auto;
-  border: 1px solid var(--ad-color-border);
-  border-radius: var(--ad-radius-md);
-  background: var(--ad-color-surface);
-}
-
-.ad-table {
-  width: 100%;
-  border-collapse: collapse;
-  font-size: var(--ad-text-sm);
-  text-align: left;
-}
-
-.ad-table caption {
-  padding: var(--ad-space-3) var(--ad-space-4);
-  color: var(--ad-color-text-muted);
-  font-size: var(--ad-text-xs);
-  text-align: left;
-  border-bottom: 1px solid var(--ad-color-border-subtle);
-}
-
-.ad-table th {
-  padding: var(--ad-space-3) var(--ad-space-4);
-  background: var(--ad-color-surface-sunken);
-  border-bottom: 1px solid var(--ad-color-border);
-  font-size: var(--ad-text-2xs);
-  font-weight: var(--ad-weight-semibold);
-  letter-spacing: var(--ad-tracking-wide);
-  text-transform: uppercase;
-  color: var(--ad-color-text-muted);
-  white-space: nowrap;
-}
-
-.ad-table td {
-  padding: var(--ad-space-3) var(--ad-space-4);
-  border-bottom: 1px solid var(--ad-color-border-subtle);
-  vertical-align: baseline;
-}
-
-.ad-table tbody tr:last-child td { border-bottom: 0; }
-.ad-table--hover tbody tr { transition: background-color var(--ad-duration-fast) var(--ad-ease-standard); }
-.ad-table--hover tbody tr:hover { background: var(--ad-color-surface-hover); }
-.ad-table--striped tbody tr:nth-child(even) { background: color-mix(in srgb, var(--ad-color-surface-sunken) 50%, transparent); }
-.ad-table--compact th,
-.ad-table--compact td { padding: var(--ad-space-2) var(--ad-space-3); }
-
-.ad-table__num {
-  text-align: right;
-  font-variant-numeric: tabular-nums;
-  font-feature-settings: "tnum";
-}
-.ad-table__code {
-  font-family: var(--ad-font-mono);
-  font-size: var(--ad-text-xs);
-  color: var(--ad-color-text-muted);
-  white-space: nowrap;
-}
diff --git a/_sass/components/_tabs.scss b/_sass/components/_tabs.scss
index 4655a46..7fd39c7 100644
--- a/_sass/components/_tabs.scss
+++ b/_sass/components/_tabs.scss
@@ -1,37 +1,61 @@
 /* Tabs
    Markup follows the WAI-ARIA tabs pattern: a `tablist` of `tab` buttons
-   controlling `tabpanel`s. The keyboard wiring lives in assets/js/system.js. */
+   controlling `tabpanel`s. The keyboard wiring lives in assets/js/system.js
+   and is variant-agnostic — it only cares about `[data-tabs]` / `[role=tab]`.
 
-.ad-tabs { display: flex; flex-direction: column; gap: var(--ad-space-4); }
+   Two real variants: `underline` (mono, accent underline — primary section
+   nav) and `segmented` (pill track, active segment lifts to the surface —
+   filters like All · Overdue · Upcoming, never primary navigation). */
 
-.ad-tabs__list {
-  display: flex;
-  gap: var(--ad-space-1);
-  overflow-x: auto;
-  border-bottom: 1px solid var(--ad-color-border);
-  scrollbar-width: none;
-}
-.ad-tabs__list::-webkit-scrollbar { display: none; }
+.ad-tabs__list { display: flex; }
 
-.ad-tabs__tab {
-  position: relative;
-  padding: var(--ad-space-2) var(--ad-space-3);
+.ad-tabs--underline .ad-tabs__list {
+  gap: var(--ad-space-m);
+  border-bottom: 1px solid var(--ad-color-border-hairline);
+}
+.ad-tabs--underline .ad-tabs__tab {
+  padding: 0.5rem 0;
   margin-bottom: -1px;
   background: none;
   border: 0;
   border-bottom: 2px solid transparent;
-  color: var(--ad-color-text-muted);
-  font-size: var(--ad-text-sm);
-  font-weight: var(--ad-weight-medium);
-  white-space: nowrap;
+  color: var(--ad-color-text-secondary);
+  font-family: var(--ad-font-mono);
+  font-size: var(--ad-step--1);
+  letter-spacing: 0.02em;
   cursor: pointer;
-  transition: color var(--ad-duration-fast) var(--ad-ease-standard),
-              border-color var(--ad-duration-fast) var(--ad-ease-standard);
+  transition: color var(--ad-dur-fast) var(--ad-ease), border-color var(--ad-dur-fast) var(--ad-ease);
 }
-.ad-tabs__tab:hover { color: var(--ad-color-text); }
-.ad-tabs__tab[aria-selected="true"] {
-  color: var(--ad-color-accent);
+.ad-tabs--underline .ad-tabs__tab:hover { color: var(--ad-color-text-primary); }
+.ad-tabs--underline .ad-tabs__tab[aria-selected="true"] {
+  color: var(--ad-color-text-primary);
   border-bottom-color: var(--ad-color-accent);
 }
+
+.ad-tabs--segmented .ad-tabs__list {
+  display: inline-flex;
+  gap: 0.125rem;
+  padding: 0.1875rem;
+  background: var(--ad-color-surface-fill);
+  border-radius: var(--ad-radius-md);
+}
+.ad-tabs--segmented .ad-tabs__tab {
+  all: unset;
+  cursor: pointer;
+  padding: 0.375rem 0.875rem;
+  border-radius: var(--ad-radius-sm);
+  font-family: var(--ad-font-sans);
+  font-size: var(--ad-step--1);
+  font-weight: 550;
+  color: var(--ad-color-text-secondary);
+  transition: background-color var(--ad-dur-fast) var(--ad-ease), color var(--ad-dur-fast) var(--ad-ease);
+}
+.ad-tabs--segmented .ad-tabs__tab[aria-selected="true"] {
+  color: var(--ad-color-text-primary);
+  background: var(--ad-color-surface-card);
+  box-shadow: var(--ad-shadow-sm);
+}
+
+.ad-tabs__panel { padding-top: var(--ad-space-s); }
 .ad-tabs__panel[hidden] { display: none; }
 .ad-tabs__panel:focus-visible { outline-offset: 4px; }
diff --git a/_sass/site/_example.scss b/_sass/site/_example.scss
index ab07cf2..34d6a20 100644
--- a/_sass/site/_example.scss
+++ b/_sass/site/_example.scss
@@ -1,30 +1,31 @@
 /* The live-example figure emitted by _plugins/example_tag.rb. */
 
 .ad-example {
-  margin: var(--ad-space-5) 0 var(--ad-space-6);
-  border: 1px solid var(--ad-color-border);
-  border-radius: var(--ad-radius-md);
-  background: var(--ad-color-surface);
+  margin: var(--ad-space-s) 0 var(--ad-space-l);
+  border: 1px solid var(--ad-color-border-default);
+  border-radius: var(--ad-radius-lg);
+  background: var(--ad-color-surface-card);
   overflow: hidden;
 }
 
 .ad-example__title {
-  padding: var(--ad-space-2) var(--ad-space-4);
-  border-bottom: 1px solid var(--ad-color-border-subtle);
-  background: var(--ad-color-surface-sunken);
-  font-size: var(--ad-text-2xs);
-  font-weight: var(--ad-weight-semibold);
-  letter-spacing: var(--ad-tracking-wide);
+  padding: var(--ad-space-2xs) var(--ad-space-xs);
+  border-bottom: 1px solid var(--ad-color-border-hairline);
+  background: var(--ad-color-surface-fill);
+  font-family: var(--ad-font-mono);
+  font-size: 11px;
+  font-weight: 600;
+  letter-spacing: 0.06em;
   text-transform: uppercase;
-  color: var(--ad-color-text-muted);
+  color: var(--ad-color-text-secondary);
 }
 
 .ad-example__preview {
   display: flex;
   flex-wrap: wrap;
   align-items: center;
-  gap: var(--ad-space-3);
-  padding: var(--ad-space-5);
+  gap: var(--ad-space-xs);
+  padding: var(--ad-space-s);
 }
 .ad-example__preview--center { justify-content: center; }
 .ad-example__preview--end { justify-content: flex-end; }
@@ -38,25 +39,25 @@
   grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
   align-items: start;
 }
-.ad-example__preview--sunken { background: var(--ad-color-surface-sunken); }
+.ad-example__preview--sunken { background: var(--ad-color-surface-fill); }
 .ad-example__preview--plain { padding: 0; }
 .ad-example__preview > * { min-width: 0; }
 
 .ad-example__source {
-  border-top: 1px solid var(--ad-color-border-subtle);
-  background: var(--ad-color-surface-sunken);
+  border-top: 1px solid var(--ad-color-border-hairline);
+  background: var(--ad-color-surface-fill);
 }
 .ad-example__summary {
-  padding: var(--ad-space-2) var(--ad-space-4);
-  font-size: var(--ad-text-xs);
-  font-weight: var(--ad-weight-medium);
-  color: var(--ad-color-text-muted);
+  padding: var(--ad-space-2xs) var(--ad-space-xs);
+  font-size: var(--ad-step--2);
+  font-weight: 500;
+  color: var(--ad-color-text-secondary);
   cursor: pointer;
   list-style: none;
   user-select: none;
   display: flex;
   align-items: center;
-  gap: var(--ad-space-2);
+  gap: var(--ad-space-2xs);
 }
 .ad-example__summary::-webkit-details-marker { display: none; }
 .ad-example__summary::before {
@@ -66,39 +67,40 @@
   border-inline-end: 1.5px solid currentColor;
   border-block-end: 1.5px solid currentColor;
   rotate: -45deg;
-  transition: rotate var(--ad-duration-fast) var(--ad-ease-standard);
+  transition: rotate var(--ad-dur-fast) var(--ad-ease);
 }
 .ad-example__source[open] > .ad-example__summary::before { rotate: 45deg; }
-.ad-example__summary:hover { color: var(--ad-color-text); }
+.ad-example__summary:hover { color: var(--ad-color-text-primary); }
 
 .ad-example__code {
   position: relative;
-  border-top: 1px solid var(--ad-color-border-subtle);
+  border-top: 1px solid var(--ad-color-border-hairline);
 }
 .ad-example__copy {
   position: absolute;
-  inset-block-start: var(--ad-space-2);
-  inset-inline-end: var(--ad-space-2);
-  padding: 0.125rem var(--ad-space-2);
-  background: var(--ad-color-surface);
-  border: 1px solid var(--ad-color-border);
-  border-radius: var(--ad-radius-xs);
-  color: var(--ad-color-text-muted);
-  font-size: var(--ad-text-2xs);
+  inset-block-start: var(--ad-space-2xs);
+  inset-inline-end: var(--ad-space-2xs);
+  padding: 0.125rem var(--ad-space-2xs);
+  background: var(--ad-color-surface-card);
+  border: 1px solid var(--ad-color-border-default);
+  border-radius: var(--ad-radius-sm);
+  color: var(--ad-color-text-secondary);
+  font-family: var(--ad-font-mono);
+  font-size: 11px;
   cursor: pointer;
   opacity: 0;
-  transition: opacity var(--ad-duration-fast) var(--ad-ease-standard);
+  transition: opacity var(--ad-dur-fast) var(--ad-ease);
 }
 .ad-example__code:hover .ad-example__copy,
 .ad-example__copy:focus-visible { opacity: 1; }
-.ad-example__copy[data-copied="true"] { color: var(--ad-color-positive); border-color: var(--ad-color-positive-border); }
+.ad-example__copy[data-copied="true"] { color: var(--ad-color-success); border-color: var(--ad-color-success); }
 
 .ad-example pre {
   margin: 0;
-  padding: var(--ad-space-4);
+  padding: var(--ad-space-xs);
   overflow-x: auto;
-  font-size: var(--ad-text-xs);
-  line-height: var(--ad-leading-normal);
+  font-size: var(--ad-step--2);
+  line-height: 1.5;
   tab-size: 2;
 }
 .ad-example pre code { background: none; border: 0; padding: 0; white-space: pre; }
@@ -106,28 +108,30 @@
 /* Standalone fenced code blocks in the prose. */
 .ad-doc-main > .highlighter-rouge,
 .ad-doc-main > .highlight {
-  margin-block: var(--ad-space-4);
-  border: 1px solid var(--ad-color-border);
+  margin-block: var(--ad-space-xs);
+  border: 1px solid var(--ad-color-border-default);
   border-radius: var(--ad-radius-md);
-  background: var(--ad-color-surface-sunken);
+  background: var(--ad-color-surface-fill);
   overflow: hidden;
 }
 .ad-doc-main > .highlighter-rouge pre {
   margin: 0;
-  padding: var(--ad-space-4);
+  padding: var(--ad-space-xs);
   overflow-x: auto;
-  font-size: var(--ad-text-xs);
-  line-height: var(--ad-leading-normal);
+  font-size: var(--ad-step--2);
+  line-height: 1.5;
 }
 .ad-doc-main > .highlighter-rouge pre code { background: none; border: 0; padding: 0; white-space: pre; }
 
-/* Rouge tokens, mapped onto system colours so highlighting themes with the page. */
-.highlight { color: var(--ad-color-text); }
-.highlight .c, .highlight .c1, .highlight .cm, .highlight .cd { color: var(--ad-color-text-subtle); font-style: italic; }
+/* Rouge tokens, mapped onto system colours so highlighting themes with the
+   page. The source's palette has no caution/info hue, so both collapse onto
+   the nearest thing this system does have. */
+.highlight { color: var(--ad-color-text-primary); }
+.highlight .c, .highlight .c1, .highlight .cm, .highlight .cd { color: var(--ad-color-text-tertiary); font-style: italic; }
 .highlight .nt { color: var(--ad-color-accent); }
-.highlight .na { color: var(--ad-color-info); }
-.highlight .s, .highlight .s1, .highlight .s2, .highlight .dl { color: var(--ad-color-positive); }
-.highlight .k, .highlight .kd, .highlight .kn, .highlight .kr { color: var(--ad-color-critical); }
-.highlight .p, .highlight .o { color: var(--ad-color-text-muted); }
-.highlight .m, .highlight .mi, .highlight .mf { color: var(--ad-color-caution); }
-.highlight .nc, .highlight .nf, .highlight .nn { color: var(--ad-color-info); }
+.highlight .na { color: var(--ad-color-accent-ink); }
+.highlight .s, .highlight .s1, .highlight .s2, .highlight .dl { color: var(--ad-color-success); }
+.highlight .k, .highlight .kd, .highlight .kn, .highlight .kr { color: var(--ad-color-danger); }
+.highlight .p, .highlight .o { color: var(--ad-color-text-secondary); }
+.highlight .m, .highlight .mi, .highlight .mf { color: var(--ad-color-accent-ink); }
+.highlight .nc, .highlight .nf, .highlight .nn { color: var(--ad-color-accent-ink); }
diff --git a/_sass/site/_shell.scss b/_sass/site/_shell.scss
index 1050e91..c26051b 100644
--- a/_sass/site/_shell.scss
+++ b/_sass/site/_shell.scss
@@ -1,161 +1,168 @@
 /* Documentation site chrome. Everything below the `.ad-doc-` prefix belongs
-   to this site rather than to the design system it documents. */
+   to this site rather than to the design system it documents — including the
+   four width/height constants it reads from base/_reset.scss. The source
+   design system defines no container-width tokens at all, so there is
+   nothing under `--ad-*` for a doc shell to reach for; scale.yml's own
+   comment says to omit that group entirely rather than invent one. */
+
+@use "../base/reset" as reset;
 
 .ad-doc-skip {
   position: absolute;
-  inset-inline-start: var(--ad-space-4);
+  inset-inline-start: var(--ad-space-xs);
   inset-block-start: -4rem;
   z-index: 40;
-  padding: var(--ad-space-2) var(--ad-space-4);
-  background: var(--ad-color-surface-raised);
-  border: 1px solid var(--ad-color-border);
+  padding: var(--ad-space-2xs) var(--ad-space-xs);
+  background: var(--ad-color-surface-card);
+  border: 1px solid var(--ad-color-border-default);
   border-radius: var(--ad-radius-sm);
-  box-shadow: var(--ad-elevation-2);
-  transition: inset-block-start var(--ad-duration-fast) var(--ad-ease-standard);
+  box-shadow: var(--ad-shadow-float);
+  transition: inset-block-start var(--ad-dur-fast) var(--ad-ease);
 }
-.ad-doc-skip:focus { inset-block-start: var(--ad-space-3); }
+.ad-doc-skip:focus { inset-block-start: var(--ad-space-2xs); }
 
 .ad-doc-header {
   position: sticky;
   inset-block-start: 0;
   z-index: 30;
-  height: var(--ad-header-height);
-  background: color-mix(in srgb, var(--ad-color-canvas) 88%, transparent);
-  backdrop-filter: saturate(1.6) blur(10px);
-  border-bottom: 1px solid var(--ad-color-border-subtle);
+  height: reset.$doc-header-height;
+  background: color-mix(in srgb, var(--ad-color-surface-page) 92%, transparent);
+  border-bottom: 1px solid var(--ad-color-border-hairline);
 }
 .ad-doc-header__inner {
   display: flex;
   align-items: center;
-  gap: var(--ad-space-4);
+  gap: var(--ad-space-xs);
   height: 100%;
-  max-width: var(--ad-width-wide);
+  max-width: reset.$doc-wide-width;
   margin-inline: auto;
-  padding-inline: var(--ad-space-4);
+  padding-inline: var(--ad-space-xs);
 }
 
 .ad-doc-wordmark {
   display: flex;
   align-items: baseline;
-  gap: var(--ad-space-2);
-  color: var(--ad-color-text);
+  gap: var(--ad-space-2xs);
+  color: var(--ad-color-text-primary);
   text-decoration: none;
-  font-family: var(--ad-font-display);
-  font-size: var(--ad-text-lg);
-  font-weight: var(--ad-weight-semibold);
-  letter-spacing: var(--ad-tracking-tight);
+  font-family: var(--ad-font-sans);
+  font-size: var(--ad-step-0);
+  font-weight: 600;
+  letter-spacing: -0.01em;
   white-space: nowrap;
 }
-.ad-doc-wordmark:hover { color: var(--ad-color-text); }
+.ad-doc-wordmark:hover { color: var(--ad-color-text-primary); }
 .ad-doc-wordmark__mark {
-  width: 1.5rem;
-  height: 1.5rem;
+  width: 1.375rem;
+  height: 1.375rem;
   flex: none;
   align-self: center;
   color: var(--ad-color-accent);
 }
 .ad-doc-wordmark__version {
   font-family: var(--ad-font-mono);
-  font-size: var(--ad-text-2xs);
-  font-weight: var(--ad-weight-regular);
-  color: var(--ad-color-text-subtle);
+  font-size: 11px;
+  font-weight: 400;
+  color: var(--ad-color-text-tertiary);
 }
 
-.ad-doc-header__actions { margin-inline-start: auto; display: flex; align-items: center; gap: var(--ad-space-2); }
+.ad-doc-header__actions { margin-inline-start: auto; display: flex; align-items: center; gap: var(--ad-space-2xs); }
 
 .ad-doc-layout {
   display: grid;
-  grid-template-columns: var(--ad-sidebar-width) minmax(0, 1fr);
-  gap: var(--ad-space-8);
-  max-width: var(--ad-width-wide);
+  grid-template-columns: reset.$doc-sidebar-width minmax(0, 1fr);
+  gap: var(--ad-space-l);
+  max-width: reset.$doc-wide-width;
   margin-inline: auto;
-  padding: var(--ad-space-6) var(--ad-space-4) var(--ad-space-10);
+  padding: var(--ad-space-m) var(--ad-space-xs) var(--ad-space-2xl);
 }
 
 .ad-doc-sidebar {
   position: sticky;
-  inset-block-start: calc(var(--ad-header-height) + var(--ad-space-6));
+  inset-block-start: calc(#{reset.$doc-header-height} + var(--ad-space-m));
   align-self: start;
-  max-height: calc(100vh - var(--ad-header-height) - var(--ad-space-8));
+  max-height: calc(100vh - #{reset.$doc-header-height} - var(--ad-space-l));
   overflow-y: auto;
-  padding-inline-end: var(--ad-space-2);
+  padding-inline-end: var(--ad-space-2xs);
 }
-.ad-doc-nav__group + .ad-doc-nav__group { margin-top: var(--ad-space-5); }
+.ad-doc-nav__group + .ad-doc-nav__group { margin-top: var(--ad-space-s); }
 .ad-doc-nav__heading {
-  margin-bottom: var(--ad-space-2);
-  font-size: var(--ad-text-2xs);
-  font-weight: var(--ad-weight-semibold);
-  letter-spacing: var(--ad-tracking-wide);
+  margin-bottom: var(--ad-space-2xs);
+  font-family: var(--ad-font-mono);
+  font-size: 11px;
+  font-weight: 600;
+  letter-spacing: 0.08em;
   text-transform: uppercase;
-  color: var(--ad-color-text-subtle);
+  color: var(--ad-color-text-tertiary);
 }
 .ad-doc-nav__link {
   display: block;
-  padding: var(--ad-space-1) var(--ad-space-3);
-  margin-inline-start: calc(var(--ad-space-3) * -1);
+  padding: var(--ad-space-3xs) var(--ad-space-xs);
+  margin-inline-start: calc(var(--ad-space-xs) * -1);
   border-inline-start: 2px solid transparent;
-  border-radius: 0 var(--ad-radius-xs) var(--ad-radius-xs) 0;
-  color: var(--ad-color-text-muted);
-  font-size: var(--ad-text-sm);
+  border-radius: 0 var(--ad-radius-sm) var(--ad-radius-sm) 0;
+  color: var(--ad-color-text-secondary);
+  font-size: var(--ad-step--1);
   text-decoration: none;
-  transition: color var(--ad-duration-fast) var(--ad-ease-standard),
-              background-color var(--ad-duration-fast) var(--ad-ease-standard);
+  transition: color var(--ad-dur-fast) var(--ad-ease),
+              background-color var(--ad-dur-fast) var(--ad-ease);
 }
-.ad-doc-nav__link:hover { color: var(--ad-color-text); background: var(--ad-color-surface-hover); }
+.ad-doc-nav__link:hover { color: var(--ad-color-text-primary); background: var(--ad-color-surface-fill); }
 .ad-doc-nav__link[aria-current="page"] {
-  color: var(--ad-color-accent);
+  color: var(--ad-color-accent-ink);
   border-inline-start-color: var(--ad-color-accent);
-  background: var(--ad-color-accent-subtle);
-  font-weight: var(--ad-weight-medium);
+  background: var(--ad-color-accent-soft);
+  font-weight: 500;
 }
 
 .ad-doc-main { min-width: 0; }
-.ad-doc-main > * { max-width: var(--ad-width-content); }
+.ad-doc-main > * { max-width: reset.$doc-content-width; }
 
-.ad-doc-title { margin-bottom: var(--ad-space-3); }
+.ad-doc-title { margin-bottom: var(--ad-space-xs); }
 .ad-doc-title__section {
   display: block;
-  margin-bottom: var(--ad-space-2);
-  font-size: var(--ad-text-2xs);
-  font-weight: var(--ad-weight-semibold);
-  letter-spacing: var(--ad-tracking-wide);
+  margin-bottom: var(--ad-space-2xs);
+  font-family: var(--ad-font-mono);
+  font-size: 11px;
+  font-weight: 600;
+  letter-spacing: 0.08em;
   text-transform: uppercase;
   color: var(--ad-color-accent);
 }
 
 .ad-doc-main h2 {
-  margin-block: var(--ad-space-8) var(--ad-space-3);
-  padding-bottom: var(--ad-space-2);
-  border-bottom: 1px solid var(--ad-color-border-subtle);
+  margin-block: var(--ad-space-2xl) var(--ad-space-xs);
+  padding-bottom: var(--ad-space-2xs);
+  border-bottom: 1px solid var(--ad-color-border-hairline);
 }
-.ad-doc-main h3 { margin-block: var(--ad-space-6) var(--ad-space-2); }
-.ad-doc-main h4 { margin-block: var(--ad-space-5) var(--ad-space-2); }
+.ad-doc-main h3 { margin-block: var(--ad-space-l) var(--ad-space-2xs); }
+.ad-doc-main h4 { margin-block: var(--ad-space-s) var(--ad-space-2xs); }
 .ad-doc-main p,
 .ad-doc-main ul,
-.ad-doc-main ol { max-width: var(--ad-measure-prose); }
+.ad-doc-main ol { max-width: reset.$doc-measure; }
 .ad-doc-main ul:not([class]),
-.ad-doc-main ol:not([class]) { padding-inline-start: var(--ad-space-5); }
+.ad-doc-main ol:not([class]) { padding-inline-start: var(--ad-space-s); }
 
 .ad-doc-footer {
-  max-width: var(--ad-width-wide);
+  max-width: reset.$doc-wide-width;
   margin-inline: auto;
-  padding: var(--ad-space-5) var(--ad-space-4) var(--ad-space-8);
-  border-top: 1px solid var(--ad-color-border-subtle);
-  color: var(--ad-color-text-subtle);
-  font-size: var(--ad-text-sm);
+  padding: var(--ad-space-s) var(--ad-space-xs) var(--ad-space-l);
+  border-top: 1px solid var(--ad-color-border-hairline);
+  color: var(--ad-color-text-tertiary);
+  font-size: var(--ad-step--1);
 }
-.ad-doc-footer__inner { display: flex; flex-wrap: wrap; gap: var(--ad-space-4); justify-content: space-between; }
+.ad-doc-footer__inner { display: flex; flex-wrap: wrap; gap: var(--ad-space-xs); justify-content: space-between; }
 
 .ad-doc-nav-toggle { display: none; }
 
 @media (max-width: 60rem) {
-  .ad-doc-layout { grid-template-columns: minmax(0, 1fr); gap: var(--ad-space-5); }
+  .ad-doc-layout { grid-template-columns: minmax(0, 1fr); gap: var(--ad-space-s); }
   .ad-doc-sidebar {
     position: static;
     max-height: none;
-    padding: var(--ad-space-4);
-    background: var(--ad-color-surface);
-    border: 1px solid var(--ad-color-border);
+    padding: var(--ad-space-xs);
+    background: var(--ad-color-surface-card);
+    border: 1px solid var(--ad-color-border-default);
     border-radius: var(--ad-radius-md);
   }
   .ad-doc-sidebar[data-collapsed="true"] > .ad-doc-nav { display: none; }
@@ -164,10 +171,10 @@
     width: 100%;
     justify-content: space-between;
   }
-  .ad-doc-nav-toggle + .ad-doc-nav { margin-top: var(--ad-space-4); }
+  .ad-doc-nav-toggle + .ad-doc-nav { margin-top: var(--ad-space-xs); }
 }
 
 @media (max-width: 34rem) {
   .ad-doc-wordmark__version { display: none; }
-  .ad-doc-layout { padding-inline: var(--ad-space-3); }
+  .ad-doc-layout { padding-inline: var(--ad-space-2xs); }
 }
diff --git a/_sass/site/_tokens-docs.scss b/_sass/site/_tokens-docs.scss
index e5fcdc9..9b8aad5 100644
--- a/_sass/site/_tokens-docs.scss
+++ b/_sass/site/_tokens-docs.scss
@@ -1,117 +1,120 @@
-/* Presentation for the token reference tables and swatch grids. */
+/* Presentation for the token reference tables and swatch grids, plus a
+   generic data-table utility. All of this is documentation chrome — the
+   design system itself has no Table component (see the note on the removed
+   `table`/`progress`/`callout` pages in git history). */
 
 .ad-swatch-grid {
   display: grid;
   grid-template-columns: repeat(auto-fill, minmax(7.5rem, 1fr));
-  gap: var(--ad-space-3);
-  margin-block: var(--ad-space-4) var(--ad-space-6);
+  gap: var(--ad-space-xs);
+  margin-block: var(--ad-space-xs) var(--ad-space-s);
   padding: 0;
   list-style: none;
 }
 .ad-swatch {
   display: flex;
   flex-direction: column;
-  border: 1px solid var(--ad-color-border);
+  border: 1px solid var(--ad-color-border-default);
   border-radius: var(--ad-radius-sm);
   overflow: hidden;
-  background: var(--ad-color-surface);
+  background: var(--ad-color-surface-card);
 }
-.ad-swatch + .ad-swatch { margin: 0; }
 .ad-swatch__chip {
   display: flex;
   align-items: flex-end;
   justify-content: flex-end;
   height: 4rem;
-  padding: var(--ad-space-2);
-  font-size: var(--ad-text-2xs);
-  font-weight: var(--ad-weight-semibold);
+  padding: var(--ad-space-2xs);
+  font-family: var(--ad-font-mono);
+  font-size: 11px;
+  font-weight: 600;
 }
 .ad-swatch__meta {
   display: flex;
   flex-direction: column;
   gap: 0.1rem;
-  padding: var(--ad-space-2);
-  border-top: 1px solid var(--ad-color-border-subtle);
+  padding: var(--ad-space-2xs);
+  border-top: 1px solid var(--ad-color-border-hairline);
   font-family: var(--ad-font-mono);
-  font-size: var(--ad-text-2xs);
+  font-size: 11px;
 }
-.ad-swatch__hex { color: var(--ad-color-text); text-transform: uppercase; }
-.ad-swatch__ratio { color: var(--ad-color-text-subtle); }
+.ad-swatch__hex { color: var(--ad-color-text-primary); text-transform: uppercase; }
+.ad-swatch__ratio { color: var(--ad-color-text-tertiary); }
 
-.ad-alias-row { display: flex; align-items: center; gap: var(--ad-space-2); }
+.ad-alias-row { display: flex; align-items: center; gap: var(--ad-space-2xs); }
 .ad-alias-chip {
   width: 1.5rem;
   height: 1.5rem;
   flex: none;
-  border: 1px solid var(--ad-color-border);
-  border-radius: var(--ad-radius-xs);
+  border: 1px solid var(--ad-color-border-default);
+  border-radius: var(--ad-radius-sm);
 }
 .ad-alias-chip--pair { display: flex; }
 .ad-alias-chip--pair > span { flex: 1; }
 
-.ad-grade { font-weight: var(--ad-weight-semibold); }
-.ad-grade--AAA, .ad-grade--AA { color: var(--ad-color-positive); }
-.ad-grade--AA-Large { color: var(--ad-color-caution); }
-.ad-grade--Fail { color: var(--ad-color-critical); }
+.ad-grade { font-weight: 600; }
+.ad-grade--AAA, .ad-grade--AA { color: var(--ad-color-success); }
+.ad-grade--AA-Large { color: var(--ad-color-text-secondary); }
+.ad-grade--Fail { color: var(--ad-color-danger); }
 
 /* Type specimen rows. */
 .ad-specimen {
   display: grid;
   grid-template-columns: minmax(0, 1fr);
-  gap: var(--ad-space-1);
-  padding-block: var(--ad-space-3);
-  border-bottom: 1px solid var(--ad-color-border-subtle);
+  gap: var(--ad-space-3xs);
+  padding-block: var(--ad-space-xs);
+  border-bottom: 1px solid var(--ad-color-border-hairline);
 }
 .ad-specimen__meta {
   display: flex;
   flex-wrap: wrap;
-  gap: var(--ad-space-3);
+  gap: var(--ad-space-xs);
   font-family: var(--ad-font-mono);
-  font-size: var(--ad-text-2xs);
-  color: var(--ad-color-text-subtle);
+  font-size: 11px;
+  color: var(--ad-color-text-tertiary);
 }
 .ad-specimen__sample {
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
-  color: var(--ad-color-text);
+  color: var(--ad-color-text-primary);
 }
 
 /* Space and radius rulers. */
-.ad-ruler { display: flex; flex-direction: column; gap: var(--ad-space-2); }
-.ad-ruler__row { display: grid; grid-template-columns: 5.5rem 3rem 1fr; align-items: center; gap: var(--ad-space-3); font-family: var(--ad-font-mono); font-size: var(--ad-text-2xs); }
-.ad-ruler__bar { height: 0.75rem; background: var(--ad-color-accent); border-radius: var(--ad-radius-xs); }
-.ad-ruler__num { color: var(--ad-color-text-subtle); text-align: right; }
+.ad-ruler { display: flex; flex-direction: column; gap: var(--ad-space-2xs); }
+.ad-ruler__row { display: grid; grid-template-columns: 6.5rem 4.5rem 1fr; align-items: center; gap: var(--ad-space-xs); font-family: var(--ad-font-mono); font-size: 11px; }
+.ad-ruler__bar { height: 0.75rem; background: var(--ad-color-accent); border-radius: var(--ad-radius-sm); }
+.ad-ruler__num { color: var(--ad-color-text-tertiary); text-align: right; }
 
-.ad-radius-grid, .ad-elevation-grid, .ad-motion-grid {
+.ad-radius-grid, .ad-elevation-grid, .ad-motion-grid, .ad-accent-grid {
   display: grid;
   grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
-  gap: var(--ad-space-4);
-  margin-block: var(--ad-space-4);
+  gap: var(--ad-space-xs);
+  margin-block: var(--ad-space-xs);
   padding: 0;
   list-style: none;
 }
 .ad-radius-demo {
   height: 4.5rem;
-  background: var(--ad-color-accent-subtle);
-  border: 1px solid var(--ad-color-accent-border);
-  margin-bottom: var(--ad-space-2);
+  background: var(--ad-color-accent-soft);
+  border: 1px solid var(--ad-color-accent);
+  margin-bottom: var(--ad-space-2xs);
 }
 .ad-elevation-demo {
   height: 4.5rem;
-  background: var(--ad-color-surface);
-  border: 1px solid var(--ad-color-border-subtle);
-  border-radius: var(--ad-radius-md);
-  margin-bottom: var(--ad-space-2);
+  background: var(--ad-color-surface-card);
+  border: 1px solid var(--ad-color-border-hairline);
+  border-radius: var(--ad-radius-lg);
+  margin-bottom: var(--ad-space-2xs);
 }
 .ad-motion-demo {
   height: 4.5rem;
   display: grid;
   place-items: center;
-  background: var(--ad-color-surface-sunken);
-  border: 1px solid var(--ad-color-border-subtle);
+  background: var(--ad-color-surface-fill);
+  border: 1px solid var(--ad-color-border-hairline);
   border-radius: var(--ad-radius-md);
-  margin-bottom: var(--ad-space-2);
+  margin-bottom: var(--ad-space-2xs);
   cursor: pointer;
   overflow: hidden;
 }
@@ -122,47 +125,89 @@
   background: var(--ad-color-accent);
   translate: -3rem 0;
   transition-property: translate;
-  transition-duration: var(--_duration, 200ms);
-  transition-timing-function: var(--_easing, ease);
+  transition-duration: var(--ad-dur-base);
+  transition-timing-function: var(--ad-ease);
 }
 .ad-motion-demo:hover .ad-motion-demo__dot,
 .ad-motion-demo:focus-visible .ad-motion-demo__dot { translate: 3rem 0; }
 
+.ad-accent-demo {
+  height: 4.5rem;
+  display: flex;
+  align-items: flex-end;
+  padding: var(--ad-space-2xs);
+  border-radius: var(--ad-radius-md);
+  color: #fff;
+  font-family: var(--ad-font-mono);
+  font-size: 11px;
+  margin-bottom: var(--ad-space-2xs);
+}
+
 .ad-token-name {
   font-family: var(--ad-font-mono);
-  font-size: var(--ad-text-xs);
-  color: var(--ad-color-text);
+  font-size: var(--ad-step--2);
+  color: var(--ad-color-text-primary);
   white-space: nowrap;
 }
 
+/* A documentation aside. Like `.ad-table`, this is site chrome for the prose
+   itself, not a system component — the real system has no Callout. */
+.ad-callout {
+  margin-block: var(--ad-space-s);
+  padding: var(--ad-space-xs) var(--ad-space-s);
+  border: 1px solid var(--ad-color-border-hairline);
+  border-left: 3px solid var(--ad-color-accent);
+  border-radius: var(--ad-radius-md);
+  background: var(--ad-color-surface-fill);
+}
+.ad-callout--danger { border-left-color: var(--ad-color-danger); }
+.ad-callout--success { border-left-color: var(--ad-color-success); }
+
+/* Generic data-table utility for reference tables (component APIs, alias
+   tables, the contrast contract). Site chrome, not a system component. */
+.ad-table-wrap { overflow-x: auto; margin-block: var(--ad-space-xs); }
+.ad-table { width: 100%; border-collapse: collapse; font-size: var(--ad-step--1); }
+.ad-table th, .ad-table td {
+  padding: var(--ad-space-2xs) var(--ad-space-xs);
+  border-bottom: 1px solid var(--ad-color-border-hairline);
+  text-align: left;
+  vertical-align: top;
+}
+.ad-table th { font-family: var(--ad-font-mono); font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--ad-color-text-secondary); font-weight: 500; }
+.ad-table--compact th, .ad-table--compact td { padding: var(--ad-space-3xs) var(--ad-space-xs); }
+.ad-table--hover tbody tr:hover { background: var(--ad-color-surface-fill); }
+.ad-table__code { font-family: var(--ad-font-mono); font-size: var(--ad-step--2); white-space: nowrap; }
+.ad-table__num { text-align: right; font-variant-numeric: tabular-nums; }
+
 /* Home page. */
-.ad-hero { padding-block: var(--ad-space-6) var(--ad-space-7); }
+.ad-hero { padding-block: var(--ad-space-m) var(--ad-space-l); }
 .ad-hero__title {
-  font-size: var(--ad-text-4xl);
-  margin-bottom: var(--ad-space-4);
+  font-size: var(--ad-step-4);
+  margin-bottom: var(--ad-space-xs);
 }
-.ad-hero__actions { display: flex; flex-wrap: wrap; gap: var(--ad-space-3); margin-top: var(--ad-space-5); }
+.ad-hero__actions { display: flex; flex-wrap: wrap; gap: var(--ad-space-xs); margin-top: var(--ad-space-s); }
 .ad-stat-row {
   display: grid;
   grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
-  gap: var(--ad-space-4);
-  margin-block: var(--ad-space-6);
-  padding: var(--ad-space-4);
-  background: var(--ad-color-surface);
-  border: 1px solid var(--ad-color-border);
-  border-radius: var(--ad-radius-md);
+  gap: var(--ad-space-xs);
+  margin-block: var(--ad-space-l);
+  padding: var(--ad-space-xs);
+  background: var(--ad-color-surface-card);
+  border: 1px solid var(--ad-color-border-default);
+  border-radius: var(--ad-radius-lg);
 }
 .ad-stat__value {
   display: block;
-  font-family: var(--ad-font-display);
-  font-size: var(--ad-text-2xl);
-  font-weight: var(--ad-weight-semibold);
-  line-height: var(--ad-leading-tight);
+  font-family: var(--ad-font-sans);
+  font-size: var(--ad-step-2);
+  font-weight: 600;
+  line-height: 1.16;
   font-variant-numeric: tabular-nums;
 }
 .ad-stat__label {
-  font-size: var(--ad-text-2xs);
-  letter-spacing: var(--ad-tracking-wide);
+  font-family: var(--ad-font-mono);
+  font-size: 11px;
+  letter-spacing: 0.06em;
   text-transform: uppercase;
-  color: var(--ad-color-text-subtle);
+  color: var(--ad-color-text-tertiary);
 }
diff --git a/assets/css/style.scss b/assets/css/style.scss
index ed2f5b9..dfface1 100644
--- a/assets/css/style.scss
+++ b/assets/css/style.scss
@@ -10,11 +10,9 @@
 @use "components/button";
 @use "components/field";
 @use "components/badge";
-@use "components/callout";
 @use "components/card";
-@use "components/table";
+@use "components/feedback";
 @use "components/tabs";
-@use "components/progress";
 
 @use "site/shell";
 @use "site/example";
diff --git a/index.md b/index.md
index fb72785..209d08b 100644
--- a/index.md
+++ b/index.md
@@ -1,20 +1,24 @@
 ---
 layout: default
 title: Overview
-description: A small, documented design system rendered from YAML tokens by Jekyll on Ruby 4.0.
+description: >-
+  A personal design system for self-hosted, single-user tools — synthesized
+  from three real running projects and rendered from YAML tokens by Jekyll on
+  Ruby 4.0.
 ---
 
 <div class="ad-hero">
   <p class="ad-overline">Design system</p>
-  <h1 class="ad-hero__title">Every design system is a pile of arbitrary definitions. This one says so out loud.</h1>
+  <h1 class="ad-hero__title">A personal umbrella for side projects, given a shared visual spine.</h1>
   <p class="ad-lead">
-    Somebody decided that the gap is 16px, that the accent is this orange and not that one,
-    and that a warning is amber. None of it was inevitable. What makes it a system is that
-    the decisions got written down in one place, given names, and then actually used.
+    Arbitrary Definitions is not a company's design system — it is one person's,
+    synthesized by reading three real, running personal projects rather than
+    written from a spec. Each project keeps its own accent colour and
+    typographic mood; this system is what they all share underneath.
   </p>
   <div class="ad-hero__actions">
     <a class="ad-btn ad-btn--primary ad-btn--lg" href="{{ '/foundations/color/' | relative_url }}">Start with colour</a>
-    <a class="ad-btn ad-btn--secondary ad-btn--lg" href="{{ '/components/button/' | relative_url }}">Jump to components</a>
+    <a class="ad-btn ad-btn--lg" href="{{ '/components/button/' | relative_url }}">Jump to components</a>
   </div>
 </div>
 
@@ -32,30 +36,45 @@ description: A small, documented design system rendered from YAML tokens by Jeky
     <span class="ad-stat__label">Components</span>
   </div>
   <div class="ad-stat">
-    <span class="ad-stat__value">{{ site.data.build.ruby }}</span>
-    <span class="ad-stat__label">Ruby</span>
+    <span class="ad-stat__value">3</span>
+    <span class="ad-stat__label">Source projects</span>
   </div>
 </div>
 
+## Where it came from
+
+Synthesized by reading three real projects, not copied from any one of them:
+[domus](https://github.com/kejadlen/domus) (a household inventory and
+document archive — warm paper surfaces, a swappable "Clay" accent, a
+capture dock), [ketchup](https://github.com/kejadlen/ketchup) (a
+recurring-task tracker — all-mono, plain grays, a flash-bar toast), and
+[quire](https://github.com/kejadlen/quire) (a personal git forge — humanist
+sans and mono, a full dark palette, underline tabs). The one literal constant
+across all three is an identical Utopia fluid type/space scale — adopted
+verbatim as this system's spine. Everything else is a synthesized middle
+ground. See [Why arbitrary]({{ '/rationale/' | relative_url }}) for the full
+story, including where this system knowingly guesses.
+
 ## How it is put together
 
-Three YAML files under `_data/tokens/` hold every value in the system. A Jekyll
-generator written in Ruby reads them at build time and emits
+Three YAML files under `_data/tokens/` hold every value in the system. A
+Jekyll generator written in Ruby reads them at build time and emits
 `assets/css/tokens.css` — one flat block of custom properties, plus the dark
 theme and a `prefers-reduced-motion` override. The component stylesheets read
 `var(--ad-*)` and nothing else.
 
-The same YAML feeds the tables and swatches on these pages, so the documentation
-cannot drift from the stylesheet. If a hex changes in the YAML, the swatch, the
-contrast figure, and the button all change together or none of them do.
+The same YAML feeds the tables and swatches on these pages, so the
+documentation cannot drift from the stylesheet. If a hex changes in the YAML,
+the swatch, the contrast figure, and the button all change together or none
+of them do.
 
 <div class="ad-card-grid">
   <article class="ad-card ad-card--link">
     <div class="ad-card__body">
       <h3 class="ad-card__title"><a href="{{ '/foundations/color/' | relative_url }}">Foundations</a></h3>
       <p class="ad-card__text">
-        Colour, type, space, elevation, and motion — with contrast ratios computed
-        from the tokens during the build rather than typed in by hand.
+        Colour, type, space, elevation, and motion — with contrast ratios
+        computed from the tokens during the build rather than typed by hand.
       </p>
     </div>
   </article>
@@ -63,17 +82,17 @@ contrast figure, and the button all change together or none of them do.
     <div class="ad-card__body">
       <h3 class="ad-card__title"><a href="{{ '/components/button/' | relative_url }}">Components</a></h3>
       <p class="ad-card__text">
-        Eight components, each rendered live next to the exact markup that produced
-        it. The demo and the snippet are the same string.
+        Sixteen components across core, forms, feedback, and navigation, each
+        rendered live next to the exact markup that produced it.
       </p>
     </div>
   </article>
   <article class="ad-card ad-card--link">
     <div class="ad-card__body">
-      <h3 class="ad-card__title"><a href="{{ '/rationale/' | relative_url }}">Why arbitrary</a></h3>
+      <h3 class="ad-card__title"><a href="{{ '/patterns/loop/' | relative_url }}">Patterns</a></h3>
       <p class="ad-card__text">
-        The argument for naming a decision you cannot justify, and the four rules
-        this system actually enforces.
+        Loop and Archive — a task dashboard and a capture app — put every
+        component family in one screen at once.
       </p>
     </div>
   </article>
@@ -82,16 +101,17 @@ contrast figure, and the button all change together or none of them do.
 ## A taste of it
 
 {% example title="Everything on one card" layout="stack" %}
-<article class="ad-card ad-card--raised" style="max-width: 26rem;">
+<article class="ad-card ad-card--elevated" style="max-width: 26rem;">
   <div class="ad-card__body">
     <div class="ad-badge-set">
-      <span class="ad-badge ad-badge--accent">Draft</span>
-      <span class="ad-badge"><span class="ad-badge__dot"></span>Unassigned</span>
+      <span class="ad-badge ad-badge--accent">Recommended</span>
+      <span class="ad-badge">Unassigned</span>
     </div>
     <h3 class="ad-card__title">Rename the caution colour</h3>
     <p class="ad-card__text">
-      Three people have called it “yellow” in review this month. The token is
-      <code>--ad-color-caution</code>; the ramp is amber.
+      Three people have called it "yellow" in review this month. There is no
+      caution token in this system — only <code>--ad-color-success</code> and
+      <code>--ad-color-danger</code>, fixed across every project.
     </p>
   </div>
   <div class="ad-card__footer">
@@ -101,15 +121,23 @@ contrast figure, and the button all change together or none of them do.
 </article>
 {% endexample %}
 
+## Voice
+
+Plain, unhurried, never marketing copy. "Take a photo or pick a file to
+keep." — not "Effortlessly supercharge your workflow!" These are tools for
+one person or household; copy never addresses "you" as a customer, and an
+empty state just states the fact: "Nothing tracked yet."
+
 ## Using it elsewhere
 
-Everything the system needs is two stylesheets and no JavaScript. Copy them into
-any project and the class names work as documented.
+Everything the system needs is two stylesheets and, for most components, no
+JavaScript at all.
 
 ```html
 <link rel="stylesheet" href="tokens.css">
 <link rel="stylesheet" href="style.css">
 ```
 
-See [Using the CSS]({{ '/install/' | relative_url }}) for the details, including
-how to retheme the whole thing by overriding a dozen custom properties.
+See [Using the CSS]({{ '/install/' | relative_url }}) for the details,
+including how to retheme the whole thing by overriding a dozen custom
+properties.
diff --git a/install.md b/install.md
index 6134376..508a834 100644
--- a/install.md
+++ b/install.md
@@ -3,15 +3,15 @@ title: Using the CSS
 permalink: /install/
 section: Start here
 summary: >-
-  Two stylesheets, no JavaScript, no build step. Then a dozen custom properties
-  if you want it to stop looking like this site.
+  Two stylesheets, almost no JavaScript, no build step. Then a dozen custom
+  properties if you want it to stop looking like this site.
 ---
 
 ## The two files
 
-The system compiles to exactly two stylesheets. `tokens.css` is generated from
-YAML by a Jekyll plugin; `style.css` is the components. The order matters —
-the components read the custom properties the tokens define.
+The system compiles to exactly two stylesheets. `tokens.css` is generated
+from YAML by a Jekyll plugin; `style.css` is the components. The order
+matters — the components read the custom properties the tokens define.
 
 ```html
 <link rel="stylesheet" href="tokens.css">
@@ -20,69 +20,73 @@ the components read the custom properties the tokens define.
 
 Both are on this site at
 [`/assets/css/tokens.css`]({{ '/assets/css/tokens.css' | relative_url }}) and
-[`/assets/css/style.css`]({{ '/assets/css/style.css' | relative_url }}).
-The generated token file is {{ site.data.tokens_css_size }} bytes uncompressed.
+[`/assets/css/style.css`]({{ '/assets/css/style.css' | relative_url }}). The
+generated token file is {{ site.data.tokens_css_size }} bytes uncompressed.
 
-Nothing in the components needs JavaScript. `assets/js/system.js` adds keyboard
-handling for [Tabs]({{ '/components/tabs/' | relative_url }}), the theme toggle,
-and the copy buttons on this site — that is all it is for.
+Load IBM Plex Mono separately — it is the system's one webfont, and nothing
+else is fetched:
+
+```css
+{{ site.data.tokens.scale.typography.webfont_import }}
+```
+
+Almost nothing here needs JavaScript. `assets/js/system.js` adds keyboard
+handling for [Tabs]({{ '/components/tabs/' | relative_url }}), the theme
+toggle, and the copy buttons on this site — Tooltip, Checkbox, Radio, and
+Switch are pure CSS, and Dialog/Toast are shown as static states for
+documentation rather than wired up as production widgets.
 
 ## Dark theme
 
-The token file emits the dark palette twice: once under
-`@media (prefers-color-scheme: dark)`, so a reader who has never touched your UI
-still gets the right theme, and once under `:root[data-theme="dark"]`, so an
-explicit choice beats the media query in both directions.
+The source's own convention is an opt-in `.theme-dark` class, applied
+explicitly rather than inherited from the OS. This site instead keeps this
+repository's own mechanism — a `data-theme="dark"` attribute plus
+`prefers-color-scheme` — because the toggle UI on every page already depends
+on it and the two are functionally equivalent: both repoint the same
+semantic aliases at the inverted neutral ramp.
 
 ```css
-:root { --ad-color-canvas: #faf9f7; }
+:root { --ad-color-surface-page: #faf9f7; }
 
 @media (prefers-color-scheme: dark) {
-  :root:not([data-theme="light"]) { --ad-color-canvas: #171512; }
+  :root:not([data-theme="light"]) { --ad-color-surface-page: #0e0d0b; }
 }
 
-:root[data-theme="dark"] { --ad-color-canvas: #171512; }
+:root[data-theme="dark"] { --ad-color-surface-page: #0e0d0b; }
 ```
 
-To wire up a toggle, set or clear `data-theme` on `<html>` and persist the
-choice. Apply it in a blocking inline script in `<head>` or the page will flash
-the wrong theme before your bundle runs.
+To wire up your own toggle, set or clear `data-theme` on `<html>` and persist
+the choice. Apply it in a blocking inline script in `<head>` or the page will
+flash the wrong theme before your bundle runs.
 
 ## Retheming
 
-Because every component reads an alias, retheming is a matter of repointing a
-dozen custom properties after the token file loads. No recompile, no fork.
+Because every component reads an alias, retheming is a matter of repointing
+a handful of custom properties after the token file loads — most of all,
+`--ad-color-accent`, which `-ink` and `-soft` re-derive from automatically via
+`color-mix()`.
 
 ```css
 :root {
-  --ad-color-accent: #1d5bd8;
-  --ad-color-accent-hover: #1e4baf;
-  --ad-color-accent-subtle: #eff7ff;
-  --ad-color-accent-border: #bfdffe;
-  --ad-color-on-accent: #ffffff;
-
-  --ad-radius-sm: 2px;
-  --ad-radius-md: 3px;
-  --ad-font-sans: "Söhne", system-ui, sans-serif;
+  --ad-color-accent: #9a5a3c; /* Clay */
 }
 ```
 
-<div class="ad-callout ad-callout--caution">
-  <div class="ad-callout__body">
-    <p class="ad-callout__title">Re-check contrast after retheming</p>
-    <p style="margin:0">
-      The system's contrast guarantees are about the system's own values. Swap the
-      accent and you own the result. The checker is a hundred lines of dependency-free
-      Ruby in <code>lib/arbitrary_definitions/color_math.rb</code> if you want to
-      run the same assertions against your palette.
-    </p>
-  </div>
+<div class="ad-callout">
+  <p style="margin:0; font-size: var(--ad-step--1);">
+    Re-check contrast after retheming. The system's contrast guarantees are
+    about the system's own values — swap the accent and you own the result.
+    The checker is dependency-free Ruby in
+    <code>lib/arbitrary_definitions/color_math.rb</code> if you want to run
+    the same assertions against your palette.
+  </p>
 </div>
 
 ## Building this site
 
-The plugins in `_plugins/` mean the GitHub-hosted Jekyll build will not run this
-site, so it builds on GitHub Actions and uploads the result to Pages. Locally:
+The plugins in `_plugins/` mean the GitHub-hosted Jekyll build will not run
+this site, so it builds on GitHub Actions and uploads the result to Pages.
+Locally:
 
 ```console
 $ rbenv install 4.0.6      # or however you get Ruby 4.0
@@ -97,10 +101,10 @@ $ bundle exec rake serve   # http://127.0.0.1:4000
   <table class="ad-table ad-table--compact">
     <thead><tr><th scope="col">Path</th><th scope="col">What it is</th></tr></thead>
     <tbody>
-      <tr><td class="ad-table__code">_data/tokens/</td><td>The three YAML files. Every value in the system.</td></tr>
+      <tr><td class="ad-table__code">_data/tokens/</td><td>The four YAML files. Every value in the system.</td></tr>
       <tr><td class="ad-table__code">lib/arbitrary_definitions/</td><td>Token compiler and WCAG colour maths. No Jekyll dependency, so it is unit tested on its own.</td></tr>
       <tr><td class="ad-table__code">_plugins/</td><td>The Jekyll wiring: a generator, Liquid filters, and the <code>{% raw %}{% example %}{% endraw %}</code> block.</td></tr>
-      <tr><td class="ad-table__code">_sass/components/</td><td>One stylesheet per component. No hexes, no pixels.</td></tr>
+      <tr><td class="ad-table__code">_sass/components/</td><td>One stylesheet per component group. No hexes, no pixels, no durations.</td></tr>
       <tr><td class="ad-table__code">test/</td><td>Minitest. Contrast floors, ramp monotonicity, CSS well-formedness.</td></tr>
     </tbody>
   </table>
@@ -108,14 +112,15 @@ $ bundle exec rake serve   # http://127.0.0.1:4000
 
 ## How the example blocks work
 
-Every demo on this site is one Liquid block. The plugin renders its body twice —
-live into the page, and Rouge-highlighted into the source panel underneath.
+Every demo on this site is one Liquid block. The plugin renders its body
+twice — live into the page, and Rouge-highlighted into the source panel
+underneath.
 
 {% raw %}
 ```liquid
 {% example title="Variants" open %}
-<button type="button" class="ad-btn ad-btn--primary">Publish</button>
-<button type="button" class="ad-btn ad-btn--ghost">Cancel</button>
+<button type="button" class="ad-btn">Cancel</button>
+<button type="button" class="ad-btn ad-btn--primary">Save</button>
 {% endexample %}
 ```
 {% endraw %}
diff --git a/lib/arbitrary_definitions/token_set.rb b/lib/arbitrary_definitions/token_set.rb
index 759187b..fd1f3b8 100644
--- a/lib/arbitrary_definitions/token_set.rb
+++ b/lib/arbitrary_definitions/token_set.rb
@@ -14,7 +14,7 @@ module ArbitraryDefinitions
   class TokenSet
     class UnknownReference < StandardError; end
 
-    # A `ramp.step` reference, e.g. `persimmon.600`.
+    # A `ramp.step` reference, e.g. `accent.teal`.
     REFERENCE = /\A([a-z][a-z0-9_]*)\.([a-z0-9]+)\z/
 
     attr_reader :prefix, :ramps, :semantic, :scale, :contrast
@@ -184,16 +184,12 @@ module ArbitraryDefinitions
       groups = []
       typography = scale.fetch("typography")
       groups << ["Type families", typography.fetch("families")]
-      groups << ["Type sizes", typography.fetch("sizes")]
-      groups << ["Line heights", typography.fetch("line_heights")]
-      groups << ["Weights", typography.fetch("weights")]
-      groups << ["Tracking", typography.fetch("tracking")]
+      groups << ["Type steps", typography.fetch("steps")]
       groups << ["Space", scale.dig("space", "scale")]
       groups << ["Radius", scale.dig("radius", "scale")]
       groups << ["Elevation", scale.dig("elevation", "scale")]
       groups << ["Durations", scale.dig("motion", "durations")]
       groups << ["Easings", scale.dig("motion", "easings")]
-      groups << ["Layout", scale.dig("layout", "scale")]
       groups.reject { |(_, entries)| entries.nil? || entries.empty? }
     end
 
diff --git a/rationale.md b/rationale.md
index 613b508..a430122 100644
--- a/rationale.md
+++ b/rationale.md
@@ -3,24 +3,52 @@ title: Why arbitrary
 permalink: /rationale/
 section: Start here
 summary: >-
-  The case for writing down a decision you cannot fully justify, and the four
-  rules this system actually enforces.
+  Why this system was synthesized from three real projects instead of
+  written from a spec, and the contract its build actually enforces.
 ---
 
-## The uncomfortable part
+## Where the name comes from
 
-Almost nothing in a design system is derived. The spacing base is 4px because
-4 divides nicely and screens used to be 1x, not because 4 is correct. The accent
-is persimmon because somebody liked it. The type ramp is a minor third until it
-isn't, at the small end, where a strict ratio produces 13.3px and 11.1px and
-nobody can tell those apart anyway.
+Arbitrary Definitions is a personal umbrella for side projects — self-hosted,
+single-user tools built for one household or one person, never a SaaS
+product. Almost nothing in a design system is derived: the accent is teal
+because someone liked it, the type scale ratio is 1.20 at the small end and
+1.25 at the large end because that is what looked right, not because either
+number is correct. What makes it a *system* is not that the choices are
+correct but that they were written down once, given names, actually used, and
+are therefore changeable.
 
-Design systems that pretend otherwise end up in an unwinnable argument. Somebody
-asks *why* the gap is 16px and not 14px, and the honest answer — "it isn't
-16 because it's right, it's right because it's 16" — sounds like a dodge. So the
-team invents a justification, and then defends the justification instead of the
-decision, and a year later nobody can change the number because the reasoning
-has calcified around it.
+## Synthesized, not specified
+
+This system was not designed from a blank page. It was built by reading three
+real, running personal projects and finding what they actually share:
+
+<div class="ad-card-grid">
+  <article class="ad-card">
+    <div class="ad-card__body">
+      <h3 class="ad-card__title"><a href="https://github.com/kejadlen/domus">domus</a></h3>
+      <p class="ad-card__text">Household inventory & document archive. Warm paper surfaces, a Georgia/Helvetica/mono mix, a swappable "Clay" accent, a bottom capture dock.</p>
+    </div>
+  </article>
+  <article class="ad-card">
+    <div class="ad-card__body">
+      <h3 class="ad-card__title"><a href="https://github.com/kejadlen/ketchup">ketchup</a></h3>
+      <p class="ad-card__text">Recurring-task tracker. All-mono interface, plain grays, red/green semantics, a flash-bar toast.</p>
+    </div>
+  </article>
+  <article class="ad-card">
+    <div class="ad-card__body">
+      <h3 class="ad-card__title"><a href="https://github.com/kejadlen/quire">quire</a></h3>
+      <p class="ad-card__text">Personal git forge / CI server. Humanist sans + mono, a warm cream surface, a full dark palette, underline tabs.</p>
+    </div>
+  </article>
+</div>
+
+The one literal constant across all three is an identical
+[Utopia](https://utopia.fyi) fluid type/space scale — 320→1240px viewport,
+18→20px body, a 1.20→1.25 ratio — adopted verbatim as this system's spine.
+Everything else (palette, radius, component set) is a synthesized middle
+ground, deliberately not lifted wholesale from any one of the three.
 
 ## What actually makes it a system
 
@@ -31,7 +59,7 @@ Not the quality of the individual choices. The properties below:
     <div class="ad-card__body">
       <h3 class="ad-card__title">It is written down once</h3>
       <p class="ad-card__text">
-        Three YAML files. Not a Figma library that disagrees with the CSS, and
+        Four YAML files. Not a Figma library that disagrees with the CSS, and
         not a spreadsheet somebody exports by hand.
       </p>
     </div>
@@ -40,8 +68,8 @@ Not the quality of the individual choices. The properties below:
     <div class="ad-card__body">
       <h3 class="ad-card__title">It has names</h3>
       <p class="ad-card__text">
-        <code>--ad-color-critical</code> survives a change of mind about which
-        red. <code>#b01a3c</code> does not.
+        <code>--ad-color-danger</code> survives a change of mind about which
+        red. <code>#ad3a2c</code> does not.
       </p>
     </div>
   </article>
@@ -49,8 +77,8 @@ Not the quality of the individual choices. The properties below:
     <div class="ad-card__body">
       <h3 class="ad-card__title">It is actually used</h3>
       <p class="ad-card__text">
-        Every colour, type, spacing, radius, elevation, and motion value in the
-        components is a <code>var(--ad-*)</code>. Not one is a literal.
+        Every colour, type, spacing, radius, elevation, and motion value in
+        the components is a <code>var(--ad-*)</code>. Not one is a literal.
       </p>
     </div>
   </article>
@@ -58,8 +86,8 @@ Not the quality of the individual choices. The properties below:
     <div class="ad-card__body">
       <h3 class="ad-card__title">It can be changed</h3>
       <p class="ad-card__text">
-        Because of the three above, moving the accent one ramp step is a
-        one-line diff — which is exactly what happened while this site was built.
+        Because of the three above, swapping the accent for Clay, Ochre, Ink
+        Blue, or Plum is a one-line diff.
       </p>
     </div>
   </article>
@@ -67,92 +95,101 @@ Not the quality of the individual choices. The properties below:
 
 ## The one thing that is not arbitrary
 
-Contrast. A ratio is a physical fact about two colours and a pair of eyes, and
-it does not care what the brand guidelines say.
+Contrast. A ratio is a physical fact about two colours and a pair of eyes,
+and it does not care what the brand guidelines say.
 
 So it is the one thing the build refuses to take on trust.
 `_data/tokens/contrast.yml` names
-{{ site.data.tokens.contrast.text.pairs | size }} text pairings and
-{{ site.data.tokens.contrast.non_text.pairs | size }} non-text pairings that
-have to hold; `lib/arbitrary_definitions/color_math.rb` computes WCAG relative
-luminance from the token values, and `test/test_token_set.rb` asserts every one
-of them in both themes. The [contract table]({{ '/foundations/color/' | relative_url }}#contrast-that-has-to-hold)
-is rendered from that same file. If a token change drops any pairing below its
-floor, `rake test` fails and the site does not deploy.
-
-That is not a formality. The accent in this system started at persimmon 600 —
-a nicer orange — and the test caught it at 4.17:1, under the 4.5:1 floor for
-normal text. It moved to 700. The ink ramp got darker at steps 500 through 800
-for the same reason.
-
-The dark theme caught a second class of mistake that no single ratio would have
-found: the destructive button was filled with `--ad-color-critical`, which is a
-*text* colour and therefore goes pale in dark mode. White label, pink button.
-The fix was a separate `--ad-color-critical-solid` token — and, more usefully,
-a rule in the suite that no `color-text-*` or `color-on-*` token may sit outside
-the contract at all, so the next filled surface cannot be forgotten.
+{{ site.data.tokens.contrast.text.pairs | size }} pairings that have to hold;
+`lib/arbitrary_definitions/color_math.rb` computes WCAG relative luminance
+from the token values, and `test/test_token_set.rb` asserts every one of them
+in both themes. The [contract table]({{ '/foundations/color/' | relative_url }}#contrast-that-has-to-hold)
+is rendered from that same file. If a token change drops any pairing below
+its floor, `rake test` fails and the site does not deploy.
 
 ```console
 $ bundle exec rake test
 0 failures, 0 errors, 0 skips
 ```
 
+That contract is deliberately narrower than "every colour in the system".
+`--ad-color-text-tertiary` — the source's own "muted-faint text" step — is
+about 3.46:1 against the page in light mode, under the 4.5:1 floor for normal
+text. That is not a bug the build should paper over by darkening a real brand
+value; it is a documented, intentional property of the palette (see the
+[Colour page]({{ '/foundations/color/' | relative_url }})), so the contract
+excludes it rather than silently forcing it to pass. Borders get the same
+treatment for the same reason: the source's own rule is that hairline borders
+do the elevation work, not high contrast, so this system does not invent a
+3:1 border assertion the source never claimed.
+
 ## The four rules
 
-<div class="ad-callout ad-callout--accent">
-  <div class="ad-callout__body">
-    <p class="ad-callout__title">1. No colour and no motion outside the token files</p>
-    <p style="margin:0">
-      A hex or a duration in a component stylesheet is a decision made in the
-      dark, and it is the kind that breaks the moment somebody adds a theme. No
-      component stylesheet in this repository contains either; the single
-      exception is the chevron inside the select's data-URI, which CSS cannot
-      reach with a custom property, and it is commented as such. Geometry is
-      looser: a 1px hairline or a control's own height is the component's
-      business. Its <em>colour</em> never is.
-    </p>
-  </div>
+<div class="ad-callout">
+  <p class="type-label" style="margin-bottom: var(--ad-space-2xs); display:block;">1. No colour and no duration outside the token files</p>
+  <p style="margin:0; font-size: var(--ad-step--1);">
+    A hex or a duration in a component stylesheet is a decision made in the
+    dark, and it is the kind that breaks the moment somebody adds a theme. No
+    component stylesheet in this repository contains either; the one flagged
+    exception is Select's chevron, which CSS cannot point at a custom
+    property inside a data-URI, and it is commented as such.
+  </p>
 </div>
 
-<div class="ad-callout ad-callout--accent">
-  <div class="ad-callout__body">
-    <p class="ad-callout__title">2. Components read aliases, never ramps</p>
-    <p style="margin:0">
-      <code>--ad-color-text</code>, not <code>--ad-ink-900</code>. The ramp is
-      raw material; the alias is the decision. This is the whole reason the dark
-      theme is a block of repointed aliases rather than a second stylesheet.
-    </p>
-  </div>
+<div class="ad-callout">
+  <p class="type-label" style="margin-bottom: var(--ad-space-2xs); display:block;">2. Components read aliases, never ramps</p>
+  <p style="margin:0; font-size: var(--ad-step--1);">
+    <code>--ad-color-text-primary</code>, not <code>--ad-neutral-900</code>.
+    The ramp is raw material; the alias is the decision. This is the whole
+    reason the dark theme is a block of repointed aliases rather than a
+    second stylesheet.
+  </p>
 </div>
 
-<div class="ad-callout ad-callout--accent">
-  <div class="ad-callout__body">
-    <p class="ad-callout__title">3. Every token says what it is for</p>
-    <p style="margin:0">
-      A <code>usage</code> line on every semantic token, enforced by a test. A
-      token nobody can describe is a token nobody will use correctly.
-    </p>
-  </div>
+<div class="ad-callout">
+  <p class="type-label" style="margin-bottom: var(--ad-space-2xs); display:block;">3. Every token says what it is for</p>
+  <p style="margin:0; font-size: var(--ad-step--1);">
+    A <code>usage</code> line on every semantic token, enforced by a test. A
+    token nobody can describe is a token nobody will use correctly.
+  </p>
 </div>
 
-<div class="ad-callout ad-callout--accent">
-  <div class="ad-callout__body">
-    <p class="ad-callout__title">4. The docs are generated from the tokens</p>
-    <p style="margin:0">
-      Every swatch, every ratio, and every table on this site is rendered from
-      the same YAML the stylesheet is compiled from. Documentation that is
-      re-typed is documentation that will be wrong by Thursday.
-    </p>
-  </div>
+<div class="ad-callout">
+  <p class="type-label" style="margin-bottom: var(--ad-space-2xs); display:block;">4. One accent, one dark-mode convention</p>
+  <p style="margin:0; font-size: var(--ad-step--1);">
+    Never two accents in one view — the source is explicit about this. Dark
+    mode is opt-in in the source (a <code>.theme-dark</code> class); this
+    site adapts that to its own <code>data-theme</code> toggle plus
+    <code>prefers-color-scheme</code>, which is functionally equivalent and
+    keeps the site's existing toggle working.
+  </p>
 </div>
 
 ## What this system does not have
 
-Being honest about the arbitrary parts means being honest about the gaps, too.
-This is a small system built to be read, not a product design system. It has no
-modal, no menu, no toast, no date picker, no icon set, no RTL audit beyond using
-logical properties, and no component tests. The border colour knowingly sits
-under the 3:1 that WCAG asks of a meaningful boundary — see the note on
-[Colour]({{ '/foundations/color/' | relative_url }}).
-
-Those are all real omissions. Naming them is cheaper than being caught by them.
+Being honest about the synthesized parts means being honest about the gaps.
+
+- **No shared component library or Figma exists across the three source
+  projects** — they are hand-rolled app CSS, not a design system. The
+  sixteen components documented here are a from-scratch, best-guess
+  personal-tool kit, grounded in real patterns found in the three apps
+  (ketchup's flash-bar → Toast; domus's drop-up menu → Dialog's flat
+  treatment; domus's segmented control and quire's underline tabs → Tabs'
+  two variants) — not a literal recreation of any one app's screens.
+- **No logo or brand mark.** The source is explicit: render "Arbitrary
+  Definitions" or the project's own name in type wherever a mark would go,
+  and do not invent one.
+- **The icon set (Lucide) is a flagged substitution.** No source project
+  ships a shared icon library; domus hand-draws its own line icons, and
+  ketchup/quire mostly avoid icons.
+- **Signal Teal, the default accent, is a synthesized pick**, not lifted from
+  any one project.
+- **No Table, Callout, or Progress component.** An earlier pass at this site
+  invented all three; none exists in the source system, so they were removed
+  rather than re-skinned. (This documentation site still uses a plain
+  `<table>` and an aside box for its own reference material — those are site
+  chrome, not system components, and are not documented as one.)
+- **Loop and Archive are generic demonstrations**, not recreations of
+  ketchup or domus specifically.
+
+Naming these is cheaper than being caught by them.
diff --git a/script/build_design_bundle.rb b/script/build_design_bundle.rb
index b8e802d..797bc19 100644
--- a/script/build_design_bundle.rb
+++ b/script/build_design_bundle.rb
@@ -33,16 +33,24 @@ CARDS = [
   ["foundations/space",       "foundations/space.html",       "Foundations", "Space"],
   ["foundations/elevation",   "foundations/elevation.html",   "Foundations", "Elevation"],
   ["foundations/motion",      "foundations/motion.html",      "Foundations", "Motion"],
-  ["components/button",       "components/button.html",       "Components",  "Button"],
-  ["components/field",        "components/field.html",        "Components",  "Field"],
-  ["components/badge",        "components/badge.html",        "Components",  "Badge"],
-  ["components/callout",      "components/callout.html",      "Components",  "Callout"],
-  ["components/card",         "components/card.html",         "Components",  "Card"],
-  ["components/table",        "components/table.html",        "Components",  "Table"],
-  ["components/tabs",         "components/tabs.html",         "Components",  "Tabs"],
-  ["components/progress",     "components/progress.html",     "Components",  "Progress"],
-  ["patterns/settings-form",  "patterns/settings-form.html",  "Patterns",    "Settings form"],
-  ["patterns/kitchen-sink",   "patterns/kitchen-sink.html",   "Patterns",    "Kitchen sink"],
+  ["components/button",       "components/button.html",       "Core",        "Button"],
+  ["components/icon-button",  "components/icon-button.html",  "Core",        "IconButton"],
+  ["components/badge",        "components/badge.html",        "Core",        "Badge"],
+  ["components/tag",          "components/tag.html",          "Core",        "Tag"],
+  ["components/card",         "components/card.html",         "Core",        "Card"],
+  ["components/card-row",     "components/card-row.html",     "Core",        "CardRow"],
+  ["components/field",        "components/field.html",        "Forms",       "Field"],
+  ["components/input",        "components/input.html",        "Forms",       "Input"],
+  ["components/select",       "components/select.html",       "Forms",       "Select"],
+  ["components/checkbox",     "components/checkbox.html",     "Forms",       "Checkbox"],
+  ["components/radio",        "components/radio.html",        "Forms",       "Radio"],
+  ["components/switch",       "components/switch.html",       "Forms",       "Switch"],
+  ["components/tooltip",      "components/tooltip.html",      "Feedback",    "Tooltip"],
+  ["components/toast",        "components/toast.html",        "Feedback",    "Toast"],
+  ["components/dialog",       "components/dialog.html",       "Feedback",    "Dialog"],
+  ["components/tabs",         "components/tabs.html",         "Navigation",  "Tabs"],
+  ["patterns/loop",           "patterns/loop.html",           "Patterns",    "Loop"],
+  ["patterns/archive",        "patterns/archive.html",        "Patterns",    "Archive"],
 ].freeze
 
 def read_site(path)
@@ -76,13 +84,13 @@ def card_html(group:, title:, body:, tokens:, styles:, script:)
     <title>#{title}</title>
     <link rel="preconnect" href="https://fonts.googleapis.com">
     <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
-    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,600&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap">
+    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&display=swap">
     <style>
     #{tokens}
     #{styles}
     /* Card chrome. The pane supplies the frame; this just insets the page. */
-    body { padding: var(--ad-space-6) var(--ad-space-5); }
-    .ad-doc-main { max-width: var(--ad-width-content); margin-inline: auto; }
+    body { padding: var(--ad-space-l) var(--ad-space-m); }
+    .ad-doc-main { max-width: 50rem; margin-inline: auto; }
     .ad-doc-main > * { max-width: none; }
     </style>
     </head>
diff --git a/test/test_color_math.rb b/test/test_color_math.rb
index 49fbbca..6294420 100644
--- a/test/test_color_math.rb
+++ b/test/test_color_math.rb
@@ -18,7 +18,7 @@ class TestColorMath < Minitest::Test
   def test_declines_values_that_have_no_single_luminance
     assert_nil CM.to_rgb("rgba(38, 35, 31, 0.5)")
     assert_nil CM.to_rgb("none")
-    assert_nil CM.relative_luminance("var(--ad-color-canvas)")
+    assert_nil CM.relative_luminance("var(--ad-color-surface-page)")
     assert_nil CM.contrast_ratio("#ffffff", "none")
   end
 
diff --git a/test/test_token_set.rb b/test/test_token_set.rb
index 3745d48..75dbe98 100644
--- a/test/test_token_set.rb
+++ b/test/test_token_set.rb
@@ -12,18 +12,19 @@ class TestTokenSet < Minitest::Test
   end
 
   def test_resolves_a_ramp_reference_to_its_hex
-    assert_equal "#f95116", @tokens.resolve("persimmon.500")
-    assert_equal "#26231f", @tokens.resolve("ink.900")
+    assert_equal "#3a6a63", @tokens.resolve("accent.teal")
+    assert_equal "#100f0d", @tokens.resolve("neutral.900")
   end
 
   def test_passes_literal_values_through_untouched
     assert_equal "#ffffff", @tokens.resolve("#ffffff")
-    assert_equal "rgba(0, 0, 0, 0.65)", @tokens.resolve("rgba(0, 0, 0, 0.65)")
+    assert_equal "color-mix(in oklch, var(--ad-color-accent) 12%, white)",
+                 @tokens.resolve("color-mix(in oklch, var(--ad-color-accent) 12%, white)")
   end
 
   def test_raises_on_a_reference_that_does_not_exist
     assert_raises(ArbitraryDefinitions::TokenSet::UnknownReference) do
-      @tokens.resolve("persimmon.55")
+      @tokens.resolve("neutral.55")
     end
   end
 
@@ -49,8 +50,15 @@ class TestTokenSet < Minitest::Test
     assert_equal names.uniq, names, "duplicate semantic token names"
   end
 
+  # `accent` is a flat set of five unrelated brand hues, not a light-to-dark
+  # scale, so it opts out via `monotonic: false` in color.yml. `neutral` and
+  # `neutral-dark` stay covered: each is listed in the YAML in the order that
+  # actually darkens (or, for neutral-dark, lightens) step to step, which can
+  # differ from ascending step-number order — see the comment in color.yml.
   def test_ramps_run_light_to_dark_without_reversing
     @tokens.ramps.each do |ramp|
+      next if ramp["monotonic"] == false
+
       luminances = ramp.fetch("steps").map do |step|
         ArbitraryDefinitions::ColorMath.relative_luminance(step.fetch("value"))
       end
@@ -91,15 +99,20 @@ class TestTokenSet < Minitest::Test
     assert_empty named.uniq - known, "the contrast contract names tokens the system does not define"
   end
 
+  # `color-text-tertiary` is the one deliberate exception: the source
+  # documents that step as muted-faint metadata/placeholder text, not body
+  # copy, and it is sub-AA on purpose (see contrast.yml's header comment and
+  # the Colour foundation page). Holding it to the same floor as
+  # `-primary`/`-secondary` would mean either lying about the contract or
+  # darkening a real brand value just to make the suite green — this system
+  # does neither.
   def test_the_contract_covers_the_pairings_that_matter
     covered = @tokens.contrast_pairs.map { |pair| pair.fetch("foreground") }
 
-    # Every text colour and every "on-" colour is something a reader decodes,
-    # so none of them may sit outside the contract.
     required = @tokens.semantic_tokens
                       .map { |token| token["token"] }
                       .select { |name| name.start_with?("color-text", "color-on-") }
-                      .reject { |name| name == "color-text-inverse" }
+                      .reject { |name| %w[color-text-inverse color-text-tertiary].include?(name) }
 
     assert_empty required - covered, "a text token is not covered by the contrast contract"
   end