v3 Design Tokens — Shared-CSS Migration Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Migrate every raw value in the four shared v3 stylesheets (v3.css, product.css, responsive.css, trial.css) to var(--token) references from tokens.css, snapping off-scale values to their nearest token — with no meaningful visual change.

Architecture: This is Plan 2 of Sub-project B. Plan 1 (token foundation) is complete: tokens.css exists as the canonical :root, is linked first on all 30 pages, the duplicate :root blocks are gone, and a pre-migration visual baseline is captured in www/v3/dev/baseline/. This plan does the data-driven snap migration of the shared CSS only — the 30 pages' inline styles are Plan 3+. Migration proceeds one value class at a time (color → type/icon → radius → spacing) across all four files, so each commit is one concept and any visual regression is trivially bisectable. Every task is verified against the baseline with the diff harness.

Tech Stack: Static CSS (no preprocessor); tokens.css custom properties; Playwright + pixelmatch visual-regression harness (www/v3/dev/screenshot.mjs).


Shared context (read before starting)

Worktree: /Users/maximilianbrandstaetter/Orcha/.claude/worktrees/website-v3-mobile-responsive, branch website-v3-mobile-responsive. All paths below are relative to the worktree root.

The four files and their cascade order: tokens.cssv3.cssproduct.css → (responsive.css on the two homepages only / trial.css on the two trial pages only). tokens.css is the base layer and is not edited for value migration (only Task 1 finalizes its scales).

The token scales (defined in www/v3/css/tokens.css): semantic colors (--bg, --bg-2, --ink, --ink-2, --ink-3, --muted, --line, --line-2, --accent, --accent-ink, --accent-soft, --good, --warn, --red), categorical palette (--cat-1--cat-5), families (--sans, --mono), weights (--weight-regular/medium/semibold = 400/500/600), font sizes (--text-9/10/11/12/13/14/15/16/19/24/32 + the display sizes added in Task 1), icons (--icon-sm/md/lg, --icon-stroke = 1.6, --icon-stroke-bold = 1.8), spacing (--space-2/4/6/8/10/12/16/20/24/32/40/48/64/80/120), radius (--radius-2/4/8/12/16, --radius-pill = 999px, --radius = 10px legacy).

Snap philosophy: every raw value is replaced by its nearest token. Because the scales kept the dominant values, most usages don't move; outliers shift 1–4px at most. Snap tables in each task are exhaustive for the values that actually occur in these four files (audited 2026-05-14) — apply them mechanically.

What is NOT migrated (intentionally out of scope for shared CSS):

Local server (required for the harness): serves www/ at http://127.0.0.1:8123. Verify/restart if down:

curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8123/v3/de/ || \
  (cd /Users/maximilianbrandstaetter/Orcha/.claude/worktrees/website-v3-mobile-responsive/www && \
   python3 -m http.server 8123 --bind 127.0.0.1 >/tmp/orcha-v3-www.log 2>&1 &)

The verification harness: cd www/v3/dev && node screenshot.mjs diff re-renders all 30 pages × 5 widths and pixel-compares against baseline/. After Task 1 it also writes a shots/<slug>-<width>.diff.png highlight image for every page/width that changed. The de-agenten/en-agenten pages are NOISY (JS-driven live console) — their diffs are reported separately and are never pass/fail; verify those two by eye if needed. shots/ and baseline/ are gitignored.

Reading a diff result:

Per-task verification is also gated by the responsive harness: node screenshot.mjs (no args) must still report no horizontal overflow on any page at any width — snapping must never reintroduce overflow.


Task 1: Finalize tokens.css and the diff harness

Files:

The discrete --text-* scale tops out at --text-32, but the shared CSS has four fixed display sizes above it (44px ×3, 48px ×2, 80px ×1, 96px ×1) that cannot snap to --text-32 without a large, visible change. Give them 1:1 tokens (value-suffixed, consistent with the rest of the file). They are not merged with each other — merging big display numbers risks a visible change, and the variation reduction comes from collapsing the half-pixel/one-off body sizes, not these.

In www/v3/css/tokens.css, immediately after the --text-32: 32px; line (currently line 55) and before the /* ── Icons ── */ comment, add:


  /* ── Type — fixed display sizes (clamp()-based display headings stay inline) ── */
  --text-44: 44px;
  --text-48: 48px;
  --text-80: 80px;
  --text-96: 96px;

v3.css:10-20 holds a theme override that redefines the semantic color tokens for body[data-mode="systems"]. These are token definitions (a theme variant), not raw-hex usages — they belong with the other definitions in tokens.css so it is the single source of truth. The hex values stay literal (they are the definitions). Moving the rule earlier in the cascade is safe: nothing else redefines these custom properties at body[...] specificity, and tokens.css loads before v3.css/product.css.

Delete these lines from www/v3/css/v3.css (lines 10-20, including the surrounding blank line so the file flows from ::selection straight to /* Layout helpers */):


  /* Mode-specific overrides */
  body[data-mode="systems"] {
    --bg: #f2efe6;
    --bg-2: #ebe7d9;
    --bg-3: #dfdac6;
    --ink: #0f0f0e;
    --line: #d6cfb9;
    --line-2: #bdb499;
    --accent: #d64b1a;
    --accent-ink: #fff8ef;
  }

Then append to the end of www/v3/css/tokens.css (after the closing } of :root):


/* ── Theme override — "systems" mode. Token *definitions* (a theme variant),
   so the hex values are intentionally literal here. ── */
body[data-mode="systems"] {
  --bg: #f2efe6;
  --bg-2: #ebe7d9;
  --bg-3: #dfdac6;
  --ink: #0f0f0e;
  --line: #d6cfb9;
  --line-2: #bdb499;
  --accent: #d64b1a;
  --accent-ink: #fff8ef;
}

So intended snap-drift can be reviewed (especially for the spacing task), make diff mode write a pixelmatch highlight PNG for every changed page/width.

In www/v3/dev/screenshot.mjs, change the node:fs import (line 9) from:

import { mkdirSync, existsSync, readFileSync } from "node:fs";

to:

import { mkdirSync, existsSync, readFileSync, writeFileSync } from "node:fs";

Then replace the else branch inside the diff block (currently lines 88-92):

            } else {
              const changed = pixelmatch(a.data, b.data, null, a.width, a.height, { threshold: 0.1 });
              const pct = (changed / (a.width * a.height)) * 100;
              if (changed > 0) sink.push(`${s} @ ${width}px — ${changed}px changed (${pct.toFixed(3)}%)`);
            }

with:

            } else {
              const diff = new PNG({ width: a.width, height: a.height });
              const changed = pixelmatch(a.data, b.data, diff.data, a.width, a.height, { threshold: 0.1 });
              const pct = (changed / (a.width * a.height)) * 100;
              if (changed > 0) {
                writeFileSync(`${OUT}${s}-${width}.diff.png`, PNG.sync.write(diff));
                sink.push(`${s} @ ${width}px — ${changed}px changed (${pct.toFixed(3)}%) → ${s}-${width}.diff.png`);
              }
            }

Ensure the server is up (see Shared context). Then:

cd www/v3/dev && node screenshot.mjs diff

Expected: no horizontal overflow on any page at any width; the 10 de-agenten/en-agenten entries under "known JS-dynamic pages (NOT pass/fail)"; and zero visual diff vs baseline (excluding known JS-dynamic pages) — rendering unchanged, exit 0.

This proves Step 1 (adding unused tokens — inert) and Step 2 (moving the theme block — same rule, same cascade outcome) changed nothing. If a non-noisy page diffs, the body[data-mode="systems"] move changed the cascade — recheck that the block was moved verbatim and that nothing in v3.css/product.css redefines those properties.

git add www/v3/css/tokens.css www/v3/css/v3.css www/v3/dev/screenshot.mjs
git commit -m "$(cat <<'EOF'
feat(v3): finalize token scales + diff-image output

Add fixed display font tokens (--text-44/48/80/96), move the
body[data-mode="systems"] theme block into tokens.css so all token
definitions live in one file, and make the diff harness emit
highlight PNGs for changed page/widths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"

Task 2: Migrate colors

Files: Modify www/v3/css/v3.css, www/v3/css/product.css, www/v3/css/trial.css. (responsive.css has no raw hex.)

Every hex literal in these files either equals an existing semantic token exactly, or is a now-redundant var(--token, #fallback) fallback (the var is always defined by tokens.css, so the fallback never applies — stripping it is zero-change), or is a one-off that snaps to the nearest semantic token. There are no genuinely-new colors here that need new tokens. rgba()/hsl() literals and the body[data-mode] definition hexes (now in tokens.css) are out of scope.

tokens.css always defines these vars, so the hex fallbacks are dead. Replace each of these exact occurrences in www/v3/css/trial.css (the var name stays, only the , #hex is removed):

Line From To
17 var(--line, #e5e5e5) var(--line)
19 var(--bg, #fff) var(--bg)
34 var(--ink-2, #555) var(--ink-2)
42 var(--line, #d4d4d4) var(--line)
45 var(--ink, #111) var(--ink)
52 var(--accent, #0a0a0a) var(--accent)
63 var(--muted, #888) var(--muted)
85 var(--ink, #111) var(--ink)
86 var(--bg, #fff) var(--bg)
125 var(--ink-2, #555) var(--ink-2)

--bg is #ffffff, so this is exact. Four occurrences:

These style runtime form-validation states; they are off-palette brighter variants of the semantic error/success colors. Per the spec, a new color used <2× snaps to the nearest existing token — --red and --good are exactly the semantic intent. (They affect only validation states not present in the baseline screenshots, so the harness diff for this is zero anyway.)

grep -nE '#[0-9a-fA-F]{3,8}\b' www/v3/css/v3.css www/v3/css/product.css www/v3/css/responsive.css www/v3/css/trial.css

Expected: no output. (The body[data-mode] hexes now live in tokens.css, which is intentionally not in this grep.)

Server up, then:

cd www/v3/dev && node screenshot.mjs diff && node screenshot.mjs

Expected: diff reports zero visual diff vs baseline (excluding known JS-dynamic pages) (every color change is an exact-equal token, a dead-fallback strip, or a non-screenshot validation state), exit 0; the plain run reports no horizontal overflow on any page at any width. If any non-noisy page diffs, a #fff was swapped on a non-white element or a fallback strip hit a var that isn't in tokens.css — check the .diff.png.

git add www/v3/css/v3.css www/v3/css/product.css www/v3/css/trial.css
git commit -m "$(cat <<'EOF'
refactor(v3): migrate shared-CSS colors to tokens

Strip redundant var() hex fallbacks in trial.css, replace standalone
#fff with var(--bg), snap trial.css's two off-palette validation
colors to var(--red)/var(--good). No raw hex left in the shared CSS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"

Task 3: Migrate type & icon scalars (font-size, font-weight, stroke-width)

Files: Modify www/v3/css/v3.css, www/v3/css/product.css, www/v3/css/responsive.css, www/v3/css/trial.css.

Plan correction (during execution): responsive.css and trial.css do carry type scalars — on-scale font-sizes (9/11/12/13/14/15/32px) and a font-weight: 500 — that still need tokenizing. The Step 4 verification grep already spans all four files, so all four are in scope; this header originally listed only two.

Apply this exhaustive snap table to every font-size: declaration in v3.css and product.css. clamp(...) expressions and 0.85em are left unchanged.

Raw Token Raw Token
8.5px var(--text-9) 15px var(--text-15)
9px var(--text-9) 16px var(--text-16)
9.5px var(--text-10) 17px var(--text-16)
10px var(--text-10) 18px var(--text-19)
10.5px var(--text-11) 19px var(--text-19)
11px var(--text-11) 21px var(--text-19)
11.5px var(--text-12) 22px var(--text-24)
12px var(--text-12) 24px var(--text-24)
12.5px var(--text-13) 26px var(--text-24)
13px var(--text-13) 28px var(--text-32)
13.5px var(--text-14) 30px var(--text-32)
14px var(--text-14) 32px var(--text-32)
14.5px var(--text-15) 44px var(--text-44)
48px var(--text-48)
80px var(--text-80)
96px var(--text-96)

The off-scale values (8.5/9.5/10.5/11.5/12.5/13.5/14.5/17/18/21/22/26/28/30) and large fixed sizes are at these lines (from the audit) — but apply by value, not by line number, since earlier edits shift lines: product.css lines 162, 343, 352, 365, 400, 414, 417, 451, 455, 492, 502, 519, 596; v3.css lines 175, 271, 598, 651, 719, 750, 760, 775, 784, 798, 807, 842, 846, 892, 897, 976, 1000, 1031, 1074, 1096, 1102, 1134, 1152, 1204, 1219, 1248, 1255, 1293, 1377, 1402, 1445, 1473, 1534, 1585, 1650, 1705, 1784, 1789, 1814, 1827, 1843, 1850, 1920, 1944, 1998, 2040, 2258, 2279, 2295, 2307, 2372, 2597, 2611, 2636, 2644. The on-scale values (9/10/11/12/13/14/15/16/19/24/32px) also migrate to their var(--text-N) token.

Raw Token
400 var(--weight-regular)
500 var(--weight-medium)
600 var(--weight-semibold)
700 var(--weight-semibold)

The lone font-weight: 700 is at v3.css:1957 — snapping it to 600 is a spec-approved consolidation of the one stray weight.

Two occurrences, both in v3.css:

grep -nE 'font-size:[[:space:]]*[0-9.]+px' www/v3/css/v3.css www/v3/css/product.css www/v3/css/responsive.css www/v3/css/trial.css
grep -nE 'font-weight:[[:space:]]*[0-9]+' www/v3/css/v3.css www/v3/css/product.css www/v3/css/responsive.css www/v3/css/trial.css
grep -nE 'stroke-width:[[:space:]]*[0-9.]+' www/v3/css/v3.css www/v3/css/product.css www/v3/css/responsive.css www/v3/css/trial.css

Expected: no output from any of the three (only clamp() font-sizes and var(--token) references remain).

Server up, then:

cd www/v3/dev && node screenshot.mjs diff && node screenshot.mjs

Expected: widespread but cosmetic diffs. The audit's font-size long tail was mostly half-pixel values (10.5/12.5/13.5px etc.), so nearly every text element re-renders at a marginally different integer size. This (a) reflows content, so most pages report SIZE CHANGED (a small height delta — tens of px on multi-thousand-px pages — which pixelmatch cannot compare), and (b) where height happens to match, inflates the pixel-diff % (1–3%) because text covers a large fraction of the page and all text re-rendered.

This is not a regression — verify it by eye, not by exit code: open 2–3 baseline/<slug>-<width>.png vs shots/<slug>-<width>.png pairs (include a homepage and a high-% product page) — the layout, hierarchy and element positions must be visually identical. Open the .diff.png for the highest-% pages — the changed pixels must sit on text glyphs (re-rendered text), never as a displaced section band, overlapping content, or a mis-sized box. no horizontal overflow on any page at any width must still hold. If a .diff.png shows a whole section displaced or broken, a font-size was snapped on the wrong element or a clamp() was touched — fix it.

Harness note: once font-size/spacing snaps reflow content, full-page screenshot heights change and pixelmatch reports SIZE CHANGED instead of a comparable diff. From Task 3 onward the harness is a "nothing catastrophic happened" gate (overflow check + .diff.png review of comparable pages), not a clean exit-0 gate — diff will exit 1. This is expected; the spec's "near-zero meaningful diff" criterion is judged visually.

git add www/v3/css/v3.css www/v3/css/product.css
git commit -m "$(cat <<'EOF'
refactor(v3): migrate shared-CSS type & icon scalars to tokens

Snap every font-size, font-weight and stroke-width to its nearest
token; clamp() display headings preserved verbatim.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"

Task 4: Migrate border-radius

Files: Modify www/v3/css/v3.css, www/v3/css/product.css. (responsive.css/trial.css have no off-token radius — verify in Step 3.)

Apply this exhaustive snap table. border-radius: 50% and the existing var(--radius) are left unchanged. Ties are broken toward the more-used neighbouring step (concentrates mass).

Raw Token Note
1px var(--radius-2) nearest
2px var(--radius-2) exact
3px var(--radius-4) tie 2/4 → toward more-used 4
4px var(--radius-4) exact
6px var(--radius-4) tie 4/8 → toward more-used 4
8px var(--radius-8) exact
9px var(--radius) tie 8/10 → toward more-used 10
10px var(--radius) exact (legacy token = 10px)
12px var(--radius-12) exact
14px var(--radius-12) tie 12/16 → toward more-used 12
16px var(--radius-16) exact
18px var(--radius-16) nearest
999px var(--radius-pill) exact
50% (unchanged) geometric circle

Multi-value border-radius — snap each px component:

grep -nE 'border-radius:[^;]*[0-9]+(px|%)' www/v3/css/v3.css www/v3/css/product.css www/v3/css/responsive.css www/v3/css/trial.css

Expected: only lines whose numeric value is 50% (circles, intentionally kept). Everything else must be var(--radius*).

Server up, then:

cd www/v3/dev && node screenshot.mjs diff && node screenshot.mjs

Expected: small diffs confined to rounded corners — 14px→12px, 6px→4px, 3px→4px, 9px→10px, 18px→16px change corner curvature by 1–4px on the affected elements. Each reported page/width should localize (via .diff.png) to card/button/pill corners. No layout shift, no horizontal overflow still holds.

git add www/v3/css/v3.css www/v3/css/product.css
git commit -m "$(cat <<'EOF'
refactor(v3): migrate shared-CSS border-radius to tokens

Snap every border-radius to its nearest radius token (ties toward
the more-used step); border-radius:50% circles left as-is.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"

Task 5: Migrate spacing (padding / margin / gap)

Files: Modify www/v3/css/v3.css, www/v3/css/product.css, www/v3/css/responsive.css, www/v3/css/trial.css.

This is the largest and riskiest task — re-read the "Reading a diff result → Spacing task" note in Shared context before starting.

Scope: padding, padding-top/right/bottom/left, margin, margin-top/right/bottom/left, gap, row-gap, column-gap. Not width/height/top/left/right/bottom/inset (component-intrinsic, out of scope).

Snap rule: each px component → nearest step on the spacing scale 2,4,6,8,10,12,16,20,24,32,40,48,64,80,120; ties round up (toward the larger step); 0, auto, %, vw/vh, em, calc(...), and existing var(...) are left unchanged; multi-value declarations snap each component independently.

On-scale values (2,4,6,8,10,12,16,20,24,32,40,48,64,80,120px) become their var(--space-N) token directly. Off-scale values present in the four files snap as follows:

Raw Token Raw Token
3px var(--space-4) 26px var(--space-24)
5px var(--space-6) 28px var(--space-32)
7px var(--space-8) 30px var(--space-32)
9px var(--space-10) 34px var(--space-32)
11px var(--space-12) 36px var(--space-40)
13px var(--space-12) 38px var(--space-40)
14px var(--space-16) 44px var(--space-48)
15px var(--space-16) 56px var(--space-64)
17px var(--space-16) 60px var(--space-64)
18px var(--space-20) 72px var(--space-80)
19px var(--space-20) 88px var(--space-80)
21px var(--space-20) 100px var(--space-120)
22px var(--space-24) 140px var(--space-120)
23px var(--space-24) 160px var(--space-120)

Worked examples of multi-value declarations (apply the same per-component logic everywhere):

Work file-by-file. The audited off-scale values cluster around 14, 18, 22, 28, 36, 56, 5, 7214px is the most frequent, so most reflow comes from 14→16.

grep -nE '(padding|margin|gap)[a-z-]*:[^;]*[0-9]+px' www/v3/css/v3.css www/v3/css/product.css www/v3/css/responsive.css www/v3/css/trial.css

Expected: no output — every padding/margin/gap px value is now 0, auto, an em/%/calc/var expression, or a var(--space-N) token.

Server up, then:

cd www/v3/dev && node screenshot.mjs diff && node screenshot.mjs

Expected: no horizontal overflow on any page at any width must still hold — this is the hard gate; snapping must never reintroduce overflow. The diff numeric percentages will be larger than in Tasks 2–4 because spacing snaps reflow content below them — judge by the .diff.png images: every change should read as content shifted uniformly by a small amount (clean horizontal bands), never as overlapping text, a mis-sized element, or a broken layout. Spot-check at least the two homepages and three section pages across widths. If the reflow is pervasive enough to be a genuinely noticeable change to the site's spacing rhythm — STOP and check in with the user about adding 14px (and possibly 18/28) back to the --space-* scale before continuing.

git add www/v3/css/v3.css www/v3/css/product.css www/v3/css/responsive.css www/v3/css/trial.css
git commit -m "$(cat <<'EOF'
refactor(v3): migrate shared-CSS spacing to tokens

Snap every padding/margin/gap px value to its nearest --space-* step
(ties round up); 0/auto/em/%/calc/var left as-is.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"

Task 6: Full verification and record completion

Files: Modify www/v3/dev/foundation-baseline.md.

grep -nE '#[0-9a-fA-F]{3,8}\b' www/v3/css/v3.css www/v3/css/product.css www/v3/css/responsive.css www/v3/css/trial.css
grep -nE 'font-size:[[:space:]]*[0-9.]+px|font-weight:[[:space:]]*[0-9]+|stroke-width:[[:space:]]*[0-9.]+' www/v3/css/v3.css www/v3/css/product.css www/v3/css/responsive.css www/v3/css/trial.css
grep -nE 'border-radius:[^;]*[0-9]+px' www/v3/css/v3.css www/v3/css/product.css www/v3/css/responsive.css www/v3/css/trial.css
grep -nE '(padding|margin|gap)[a-z-]*:[^;]*[0-9]+px' www/v3/css/v3.css www/v3/css/product.css www/v3/css/responsive.css www/v3/css/trial.css

Expected: the first, second and fourth greps produce no output; the third produces only border-radius: ...50%... lines. Anything else is a missed value — go back to the relevant task and fix it.

Server up, then:

cd www/v3/dev && node screenshot.mjs diff && node screenshot.mjs

Expected: no horizontal overflow on any page at any width; diff reports only small, explainable, localized changes (the cumulative spacing reflow from Task 5 dominates the numbers — confirm via .diff.png that it is uniform settle, not breakage); the two agenten pages remain NOISY. This is the spec's "near-zero meaningful diff" success criterion: the site looks the same, with far fewer distinct values.

In www/v3/dev/foundation-baseline.md, under the "Sub-project B" section, add a line after the Plan 1 entry:

- **Plan 2 — Shared-CSS Migration** (`2026-05-14-v3-design-tokens-shared-css.md`) — **complete.**
  - `v3.css`, `product.css`, `responsive.css`, `trial.css` fully migrated to `var(--token)`:
    every color, font-size, font-weight, stroke-width, border-radius and
    padding/margin/gap is now a token reference or a deliberately-preserved
    expression (`clamp()` display headings, `em`-relative values, `rgba()` overlays,
    `border-radius:50%`). `tokens.css` gained `--text-44/48/80/96` and the
    `body[data-mode="systems"]` theme block; the diff harness now emits `.diff.png`
    highlight images.
  - `node screenshot.mjs diff` shows only small localized snap-drift vs. the baseline;
    `node screenshot.mjs` still reports zero overflow.
  - Plan 3+ migrates the 30 pages' inline styles.
git add www/v3/dev/foundation-baseline.md
git commit -m "$(cat <<'EOF'
docs(v3): record shared-CSS token migration completion

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"

Self-review notes