diff --git a/docs/superpowers/specs/2026-09-02-design-foundation-design.md b/docs/superpowers/specs/2026-09-02-design-foundation-design.md
new file mode 100644
index 0000000..5cb6f91
--- /dev/null
+++ b/docs/superpowers/specs/2026-09-02-design-foundation-design.md
@@ -0,0 +1,152 @@
+# Design foundation
+
+Sub-project 1 of the visual redesign. Owner's brief: *"right now it looks a bit
+boring, simple and ugly. We want sleek, modern, and nice to use."* Owner's
+answers to the four scoping questions: both themes with a toggle, a full
+rethink of every screen, purchases compact and grouped by date, and a
+self-hosted webfont. Owner then delegated the specifics: *"Override everything
+you want for this step. This needs to look good."*
+
+## The diagnosis that shaped everything else
+
+A design system already existed and was never wired up. `src/index.css`
+defined `--paper` (annotated "cool statement-paper ground, NOT cream"),
+`--ink`, a muted teal, and surplus/overspend colours, with a design-contract
+document behind them. The app painted itself with shadcn's defaults —
+`bg-background text-foreground`, i.e. white and near-black. Those tokens
+reached the screen in exactly two places: the carry meter, and the sign
+colours on figures.
+
+So "boring, simple and ugly" was not a missing design. It was a design that was
+specified and then bypassed. That is why the foundation comes first in the
+decomposition, and why it re-themes shadcn rather than styling screens: doing
+it screen by screen is how the bypass happened.
+
+## Decomposition
+
+The redesign is four sub-projects, each its own spec → plan → implementation:
+
+1. **Design foundation** (this document) — tokens, themes, type, shared furniture.
+2. **Month view** — income and totals, post rows and carry meter, purchases grouped by date.
+3. **Year, Summary, Post detail** — the reporting screens, including whether the 12-month matrix stays a scrolling table.
+4. **Dialogs and fast entry** — purchase dialog, split editor, plan editor, bulk entry.
+
+A single spec covering all four would have meant dozens of layout decisions
+made on the owner's behalf under one approval.
+
+## Token architecture
+
+Two layers, deliberately separate.
+
+**Layer 1, `--budget-*`**: the app's own vocabulary — paper, raised, ink,
+ink-muted, rule, rule-strong, accent, accent-ink, tint — plus `--surplus` and
+`--overspend`, which shadcn has no concept of. Defined once per theme. The only
+colours app components may name.
+
+**Layer 2, shadcn**: its entire variable set points at layer 1. Every primitive
+(dialog, select, badge, button) inherits both themes for free.
+
+### The brand teal is `--primary`, never `--accent`
+
+CLAUDE.md's hardest-won rule is "never define a bare `--accent`", paid for with
+an app-wide contrast bug. The mechanism matters: shadcn uses `--accent` as a
+hover **background** with `--accent-foreground` as the text on it. Setting one
+without the other repaints the background and leaves the text behind.
+
+Two conclusions follow, and both are enforced rather than remembered:
+
+- Every shadcn pair is redefined **together**, in both themes.
+ `src/cssPairs.test.ts` fails the build if either half of a pair appears
+ without the other, in any block.
+- The saturated teal belongs on `--primary`. Putting a brand colour on
+ `--accent` — which is what "override `--accent`" invites — turns every hover
+ into a coloured slab with dark text on it. `--accent` stays a tint of the
+ surface.
+
+### Contrast was computed, not chosen
+
+Weakest text pair: 5.50:1 (muted on paper, light) against a 4.5:1 requirement.
+Field borders were iterated until they cleared the 3:1 non-text requirement on
+**both** backgrounds they appear against — the first two candidates measured
+2.17 and 2.55 and were rejected. The final palette was then read back out of
+the live browser in both themes, so the check is on what actually rendered
+rather than on the arithmetic.
+
+## Typography
+
+Inter for text, JetBrains Mono for figures, both variable, both self-hosted,
+latin + latin-ext only. SIL OFL 1.1; licences committed beside the files.
+Self-hosted rather than CDN-linked because PRODUCT.md's position is that the
+data never leaves the browser and the app needs no third party — a font request
+to another origin on every cold load quietly contradicts that.
+
+`.font-money` carries tabular figures and a slashed zero, so columns align on
+the decimal and `0` cannot be misread as `O`.
+
+### Rejected: @font-face in the bundled stylesheet
+
+The obvious home. Bun's CSS bundler resolves every `url()` from disk and inlines
+woff2 as base64 — **relative or absolute**, and `loader: { ".woff2": "file" }`
+does not change it. Measured cost: `index.css` went from 70 KB to 303 KB of
+render-blocking bytes, and the `unicode-range` subsetting became pointless,
+since a subset embedded in the stylesheet downloads whether or not one
+character needs it.
+
+Also rejected: a ``. It works in a
+production build (`external: ["*fonts.css"]`), but the dev server's HTML
+bundler resolves every `` from disk and has no `external` option, so
+dev would serve a placeholder and screenshots would lie about typography.
+Making the manifest and icon hrefs root-absolute for the same reason 500'd the
+dev server outright.
+
+**Chosen**: `src/fontCss.ts` is the one definition, injected as a `