v3 Design Tokens — Foundation 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: Establish the design-token foundation — capture a visual baseline, create the canonical tokens.css with every token scale, link it site-wide, and consolidate the duplicated :root blocks — with verified zero visual change.

Architecture: A new www/v3/css/tokens.css holds one canonical :root with every token scale (type, weight, color, icon, spacing, radius). It is linked first on all 30 pages, before v3.css. The duplicated :root blocks in v3.css and product.css are removed. The screenshot.mjs harness gains baseline + diff modes for visual-regression checking. This plan only sets up the system — it must not change rendering; the per-value migration to var(--token) is in follow-up plans.

Tech Stack: Static HTML/CSS, vanilla JS. Playwright (Chromium) + pixelmatch/pngjs for visual-regression.


Scope

This is Plan 1 of sub-project B (spec: docs/superpowers/specs/2026-05-14-v3-design-tokens-design.md). It delivers the token foundation — the scales are defined and codified, the baseline is captured, the verification tooling exists. Follow-up plans, written once tokens.css and the baseline exist:

This plan changes no rendering. Adding tokens.css (whose color values are identical to the current :roots) and removing the now-redundant :root blocks is rendering-neutral; Task 6's diff proves it.

Token naming convention

Numeric scales use value-suffixed names (--text-13, --space-16, --radius-8) — this is a deliberate refinement of the spec's --text-*/--space-*/--radius-* shorthand: value-suffixed names make the data-driven snap migration an unambiguous mechanical lookup (font-size: 13pxvar(--text-13); an off-scale 13.5px → its nearest, var(--text-14)). Semantic tokens that are already semantic (colors, weights, icon sizes) keep semantic names.

Prerequisites


File Structure

File Status Responsibility
www/v3/css/tokens.css create The canonical :root — all color tokens (consolidated from the two existing :roots) + the new --text-* / --weight-* / --icon-* / --space-* / --radius-* scales + --cat-1..5.
www/v3/css/v3.css modify Remove its :root block (now in tokens.css).
www/v3/css/product.css modify Remove its :root block (now in tokens.css).
all 30 page .html files modify Add <link rel="stylesheet" href="…/tokens.css?v=checklist"> as the first stylesheet.
www/v3/dev/screenshot.mjs modify Add baseline and diff modes (pixelmatch-based visual regression).
www/v3/dev/package.json modify Add pixelmatch + pngjs deps.
www/v3/dev/baseline/ create (gitignored) The pre-migration screenshot baseline.

Task 1: Add visual-regression modes to the harness

Plan correction (during execution, 2026-05-14): the harness as first written below was non-deterministic — looping CSS animations and JS-driven content made every diff non-reproducible (Task 6's first sweep). Two fixes were applied to screenshot.mjs and committed: (1) before each screenshot, freeze CSS animations/transitions (page.addStyleTag with animation-duration:0s etc. + reducedMotion: "reduce" + a 200ms settle) — this cleared 28/30 pages to exact zero diff; (2) a NOISY set (de-agenten, en-agenten) whose diffs are reported but excluded from pass/fail — those pages run a JS-driven live agent console that mutates the DOM on timers and cannot be pixel-diffed deterministically; they are verified by manual spot-check. The committed screenshot.mjs is the source of truth.

Files: Modify www/v3/dev/package.json, www/v3/dev/screenshot.mjs, www/v3/dev/.gitignore

In www/v3/dev/package.json, change the devDependencies block to:

  "devDependencies": {
    "playwright": "^1.48.0",
    "pixelmatch": "^6.0.0",
    "pngjs": "^7.0.0"
  }

Then run: cd /Users/maximilianbrandstaetter/Orcha/.claude/worktrees/website-v3-mobile-responsive/www/v3/dev && npm install Expected: pixelmatch and pngjs install with no errors.

www/v3/dev/.gitignore currently contains node_modules/ and shots/. Add a third line so it reads:

node_modules/
shots/
baseline/

Replace the entire file with:

// Responsive + visual-regression harness for the v3 website.
// Modes:
//   node screenshot.mjs               → overflow check, screenshots to ./shots
//   node screenshot.mjs baseline      → screenshots to ./baseline (the pre-migration ground truth)
//   node screenshot.mjs diff          → screenshots to ./shots, pixel-diff each vs ./baseline
//   node screenshot.mjs <filter>      → as default, comma-list slug-substring filter
//   node screenshot.mjs diff <filter> → as diff, filtered
import { chromium } from "playwright";
import { mkdirSync, existsSync, readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { PNG } from "pngjs";
import pixelmatch from "pixelmatch";

const BASE = process.env.BASE || "http://127.0.0.1:8123";
const WIDTHS = [320, 375, 768, 1024, 1280];
const HERE = fileURLToPath(new URL("./", import.meta.url));
const argv = process.argv.slice(2);
const MODE = (argv[0] === "baseline" || argv[0] === "diff") ? argv[0] : "check";
const FILTER = ((MODE === "check" ? argv[0] : argv[1]) || "").split(",").filter(Boolean);
const OUT = MODE === "baseline" ? `${HERE}baseline/` : `${HERE}shots/`;
const BASELINE = `${HERE}baseline/`;

const PAGES = [
  "/v3/de/", "/v3/en/",
  "/v3/de/produkt/abschluss.html", "/v3/de/produkt/debitoren.html", "/v3/de/produkt/dokumente.html",
  "/v3/de/produkt/fpa.html", "/v3/de/produkt/freigaben.html", "/v3/de/produkt/kreditoren.html",
  "/v3/de/produkt/spesen.html", "/v3/de/produkt/varianz.html", "/v3/de/produkt/vertraege.html",
  "/v3/en/produkt/abschluss.html", "/v3/en/produkt/debitoren.html", "/v3/en/produkt/dokumente.html",
  "/v3/en/produkt/fpa.html", "/v3/en/produkt/freigaben.html", "/v3/en/produkt/kreditoren.html",
  "/v3/en/produkt/spesen.html", "/v3/en/produkt/varianz.html", "/v3/en/produkt/vertraege.html",
  "/v3/de/agenten/", "/v3/de/implementierung/", "/v3/de/roi/", "/v3/de/steuerberater/", "/v3/de/trial/",
  "/v3/en/agenten/", "/v3/en/implementierung/", "/v3/en/roi/", "/v3/en/steuerberater/", "/v3/en/trial/",
];

const slug = (p) => {
  if (p === "/v3/de/") return "de-home";
  if (p === "/v3/en/") return "en-home";
  return p.replace(/^\/v3\//, "").replace(/index\.html$/, "").replace(/\.html$/, "")
          .replace(/\/$/, "").replace(/\//g, "-");
};

mkdirSync(OUT, { recursive: true });
const pages = FILTER.length ? PAGES.filter((p) => FILTER.some((f) => slug(p).includes(f))) : PAGES;

const browser = await chromium.launch();
const overflows = [];
const diffs = [];

try {
  for (const path of pages) {
    const s = slug(path);
    for (const width of WIDTHS) {
      const page = await browser.newPage({ viewport: { width, height: 900 } });
      try {
        await page.goto(BASE + path, { waitUntil: "networkidle" });
        const overflow = await page.evaluate(() => {
          const de = document.documentElement;
          return de.scrollWidth - de.clientWidth;
        });
        const file = `${OUT}${s}-${width}.png`;
        await page.screenshot({ path: file, fullPage: true });
        if (overflow > 1) overflows.push(`${s} @ ${width}px (+${overflow}px)`);

        if (MODE === "diff") {
          const basePath = `${BASELINE}${s}-${width}.png`;
          if (!existsSync(basePath)) {
            diffs.push(`${s} @ ${width}px — NO BASELINE`);
          } else {
            const a = PNG.sync.read(readFileSync(basePath));
            const b = PNG.sync.read(readFileSync(file));
            if (a.width !== b.width || a.height !== b.height) {
              diffs.push(`${s} @ ${width}px — SIZE CHANGED ${a.width}x${a.height}${b.width}x${b.height}`);
            } 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) diffs.push(`${s} @ ${width}px — ${changed}px changed (${pct.toFixed(3)}%)`);
            }
          }
        }
      } finally {
        await page.close();
      }
    }
  }
} finally {
  await browser.close();
}

console.log(`\nmode=${MODE}  ${pages.length} pages x ${WIDTHS.length} widths = ${pages.length * WIDTHS.length} screenshots`);
if (MODE === "baseline") {
  console.log(`baseline written to ./baseline/`);
  process.exit(0);
}
let fail = false;
if (overflows.length) {
  fail = true;
  console.log(`\n${overflows.length} OVERFLOW FAILURES:`);
  for (const o of overflows) console.log(`  ${o}`);
} else {
  console.log(`\nno horizontal overflow on any page at any width`);
}
if (MODE === "diff") {
  if (diffs.length) {
    fail = true;
    console.log(`\n${diffs.length} VISUAL DIFFS vs baseline:`);
    for (const d of diffs) console.log(`  ${d}`);
  } else {
    console.log(`zero visual diff vs baseline — rendering unchanged`);
  }
}
process.exit(fail ? 1 : 0);

Ensure the server is running, then:

cd /Users/maximilianbrandstaetter/Orcha/.claude/worktrees/website-v3-mobile-responsive/www/v3/dev && node screenshot.mjs de-home

Expected: runs de-home × 5 widths, prints mode=check, all ok (the homepage is responsive), exit 0.

git add www/v3/dev/screenshot.mjs www/v3/dev/package.json www/v3/dev/package-lock.json www/v3/dev/.gitignore
git commit -m "test(v3): add baseline + visual-diff modes to the harness"

Task 2: Capture the pre-migration baseline

Files: none committed (baseline/ is gitignored) — this task produces the ground-truth artifact.

With the server running and no token changes yet made (the working tree must be clean except the Task 1 commit):

cd /Users/maximilianbrandstaetter/Orcha/.claude/worktrees/website-v3-mobile-responsive/www/v3/dev && node screenshot.mjs baseline

Expected: mode=baseline, 30 pages × 5 widths = 150 screenshots written to www/v3/dev/baseline/, exit 0. This is the canonical "site as it looks today" — every later migration stage diffs against it.

ls www/v3/dev/baseline/*.png | wc -l

Expected: 150.

cd /Users/maximilianbrandstaetter/Orcha/.claude/worktrees/website-v3-mobile-responsive/www/v3/dev && node screenshot.mjs diff de-home

Expected: mode=diff, de-home 5 widths, zero visual diff vs baseline (diffing the baseline against an immediate re-render of the unchanged site must be zero), exit 0. If it reports diffs, the harness or rendering is non-deterministic — stop and investigate before proceeding.

(No commit — baseline/ is gitignored.)


Task 3: Create tokens.css

Files: Create www/v3/css/tokens.css

/* ═══════════════════════════════════════════════════════════════
   tokens.css — the v3 design-token system (sub-project B, 2026-05-14)
   The single canonical :root. Linked FIRST on every v3 page, before
   v3.css / product.css. Scales are data-driven (kept the well-used
   values from the audited distribution); migration snaps every raw
   value to its nearest token. See docs/superpowers/specs/.
═══════════════════════════════════════════════════════════════ */
:root {
  /* ── Color — semantic (consolidated from the former v3.css + product.css :roots) ── */
  --bg: #ffffff;
  --bg-2: #f5f3ee;
  --bg-3: #f5f3ee;          /* aliased to --bg-2 */
  --ink: #111111;
  --ink-2: #2b2b2b;
  --ink-3: #5a5a56;
  --muted: #8a8680;
  --line: #dcd7cb;
  --line-2: #c9c3b4;
  --accent: #006EC7;
  --accent-ink: #ffffff;
  --accent-soft: #e6f0fb;
  --good: #2e5d3b;
  --warn: #8a5a1a;
  --red: #8a2a1a;

  /* ── Color — categorical palette (was raw hex #D49B70 etc.) ── */
  --cat-1: #D49B70;
  --cat-2: #BC7B95;
  --cat-3: #A185BB;
  --cat-4: #7A9BC0;
  --cat-5: #6DAF9F;

  /* ── Type — font families ── */
  --sans: "Geist", "Helvetica Neue", Helvetica, Arial, sans-serif;
  --mono: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
  --serif: "Geist", "Helvetica Neue", Helvetica, Arial, sans-serif;  /* legacy alias of --sans */

  /* ── Type — font weights ── */
  --weight-regular: 400;
  --weight-medium: 500;
  --weight-semibold: 600;

  /* ── Type — discrete font-size scale (px). Display headings keep their
     own clamp() expressions and are not tokenized here. ── */
  --text-9: 9px;
  --text-10: 10px;
  --text-11: 11px;
  --text-12: 12px;
  --text-13: 13px;
  --text-14: 14px;
  --text-15: 15px;
  --text-16: 16px;
  --text-19: 19px;
  --text-24: 24px;
  --text-32: 32px;

  /* ── Icons ── */
  --icon-sm: 14px;
  --icon-md: 16px;
  --icon-lg: 18px;
  --icon-stroke: 1.6;
  --icon-stroke-bold: 1.8;

  /* ── Spacing scale (px) ── */
  --space-2: 2px;
  --space-4: 4px;
  --space-6: 6px;
  --space-8: 8px;
  --space-10: 10px;
  --space-12: 12px;
  --space-16: 16px;
  --space-20: 20px;
  --space-24: 24px;
  --space-32: 32px;
  --space-40: 40px;
  --space-48: 48px;
  --space-64: 64px;
  --space-80: 80px;
  --space-120: 120px;

  /* ── Border radius ── */
  --radius-2: 2px;
  --radius-4: 4px;
  --radius-8: 8px;
  --radius-12: 12px;
  --radius-16: 16px;
  --radius-pill: 999px;
  --radius: 10px;          /* legacy token, still referenced as var(--radius); migrated later */
}
node -e "const c=require('fs').readFileSync('www/v3/css/tokens.css','utf8'); const o=(c.match(/{/g)||[]).length,cl=(c.match(/}/g)||[]).length; console.log('braces',o,cl,o===cl?'OK':'MISMATCH')"

Expected: braces 1 1 OK.

git add www/v3/css/tokens.css
git commit -m "feat(v3): add tokens.css — the canonical design-token system"

Files: Modify all 30 page .html files.

Every page has, in <head>, a line linking v3.css. Only the two homepages (de/index.html, en/index.html) are one directory deep and use href="../css/v3.css?v=checklist"; all other pages — product pages and section pages (de/roi/index.html, de/agenten/index.html, …) — are two directories deep and use href="../../css/v3.css?v=checklist". For each of the 30 pages, insert a tokens.css link immediately before its v3.css link, with the matching relative path.

Apply with this script (it inserts the correct relative path per page depth and is idempotent — it skips any page that already links tokens.css):

cd /Users/maximilianbrandstaetter/Orcha/.claude/worktrees/website-v3-mobile-responsive/www/v3
for f in de/index.html en/index.html \
  de/produkt/abschluss.html de/produkt/debitoren.html de/produkt/dokumente.html de/produkt/fpa.html \
  de/produkt/freigaben.html de/produkt/kreditoren.html de/produkt/spesen.html de/produkt/varianz.html de/produkt/vertraege.html \
  en/produkt/abschluss.html en/produkt/debitoren.html en/produkt/dokumente.html en/produkt/fpa.html \
  en/produkt/freigaben.html en/produkt/kreditoren.html en/produkt/spesen.html en/produkt/varianz.html en/produkt/vertraege.html \
  de/agenten/index.html de/implementierung/index.html de/roi/index.html de/steuerberater/index.html de/trial/index.html \
  en/agenten/index.html en/implementierung/index.html en/roi/index.html en/steuerberater/index.html en/trial/index.html; do
  if grep -q 'css/tokens.css' "$f"; then echo "skip (already linked): $f"; continue; fi
  case "$f" in
    de/index.html|en/index.html) rel="../css" ;;   # the two homepages are 1 level deep
    *) rel="../../css" ;;                          # product + section pages are 2 levels deep
  esac
  perl -0pi -e "s{(\\s*)(<link rel=\"stylesheet\" href=\"\\Q$rel\\E/v3\\.css\\?v=checklist\">)}{\$1<link rel=\"stylesheet\" href=\"$rel/tokens.css?v=checklist\">\$1\$2}" "$f"
  echo "linked: $f"
done
cd /Users/maximilianbrandstaetter/Orcha/.claude/worktrees/website-v3-mobile-responsive/www/v3
grep -lc 'css/tokens.css' de/index.html en/index.html de/produkt/*.html en/produkt/*.html de/*/index.html en/*/index.html 2>/dev/null | grep -c ':1' ; echo "↑ pages linking tokens.css (expect 30)"
# confirm order on a sample of each depth: tokens.css must appear on an earlier line than v3.css
for f in de/index.html de/produkt/kreditoren.html de/roi/index.html en/produkt/abschluss.html; do
  t=$(grep -n 'css/tokens.css' "$f" | cut -d: -f1); v=$(grep -n 'css/v3.css' "$f" | cut -d: -f1)
  echo "$f  tokens@$t v3@$v  $([ "$t" -lt "$v" ] && echo OK || echo BAD)"
done

Expected: 30 pages link tokens.css; all four samples report OK.

cd /Users/maximilianbrandstaetter/Orcha/.claude/worktrees/website-v3-mobile-responsive
git add www/v3/de www/v3/en
git commit -m "feat(v3): link tokens.css first on all 30 pages"

Task 5: Remove the duplicate :root blocks from v3.css and product.css

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

In www/v3/css/product.css, delete this exact block (it is near the top of the file):

:root {
  --bg: #ffffff;
  --bg-2: #f5f3ee;
  /* --bg-3 aliased to --bg-2; previously a darker third sand tone, dropped to reduce beige intensity */
  --bg-3: #f5f3ee;
  --ink: #111111;
  --ink-2: #2b2b2b;
  --ink-3: #5a5a56;
  --muted: #8a8680;
  --line: #dcd7cb;
  --line-2: #c9c3b4;
  --accent: #006EC7;
  --accent-ink: #ffffff;
  --accent-soft: #e6f0fb;
  --good: #2e5d3b;
  --warn: #8a5a1a;
  --red: #8a2a1a;
  --serif: "Geist", "Helvetica Neue", Helvetica, Arial, sans-serif;
  --sans: "Geist", "Helvetica Neue", Helvetica, Arial, sans-serif;
  --mono: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
}

(All of those values are now provided by tokens.css, which loads first.)

In www/v3/css/v3.css, find its :root block (indented two spaces) and delete it. It is structurally:

  :root {
    --bg: #ffffff;
    ... (the same tokens, plus --radius: 10px) ...
  }

Read the file to get the block's exact text (it spans from the :root { line to its matching }), then delete that exact block. Do not delete anything else. Every token it defined — including --radius: 10px — is provided by tokens.css.

cd /Users/maximilianbrandstaetter/Orcha/.claude/worktrees/website-v3-mobile-responsive
grep -c ':root' www/v3/css/v3.css www/v3/css/product.css   # expect 0 for both
grep -c ':root' www/v3/css/tokens.css                       # expect 1
node -e "for(const f of ['www/v3/css/v3.css','www/v3/css/product.css','www/v3/css/tokens.css']){const c=require('fs').readFileSync(f,'utf8');const o=(c.match(/{/g)||[]).length,cl=(c.match(/}/g)||[]).length;console.log(f,o===cl?'OK':'MISMATCH '+o+'/'+cl)}"

Expected: v3.css and product.css0 :root; tokens.css1; all three brace-balanced OK.

git add www/v3/css/v3.css www/v3/css/product.css
git commit -m "refactor(v3): remove duplicate :root blocks — tokens.css is now canonical"

Task 6: Foundation verification — prove zero visual change

Files: none (verification only — unless a regression fix is needed).

With the server running:

cd /Users/maximilianbrandstaetter/Orcha/.claude/worktrees/website-v3-mobile-responsive/www/v3/dev && node screenshot.mjs diff

Expected (with the deterministic harness from Task 1): mode=diff, 30 pages × 5 widths, no horizontal overflow on any page at any width, the 10 de-agenten/en-agenten entries listed under "known JS-dynamic pages (NOT pass/fail)", and zero visual diff vs baseline (excluding known JS-dynamic pages) — rendering unchanged, exit 0.

This is the core check: adding tokens.css (identical color values) and removing the redundant :root blocks must not change a single pixel on the 28 deterministic pages. If a non-NOISY page reports a diff, it is a real regression — tokens.css is missing a token that one of the removed :root blocks provided, or the link order is wrong. Fix tokens.css / the link order and re-run. For the NOISY agenten pages, manually open a shots/de-agenten-1280.png and confirm the page renders correctly (header, sections, footer all present) — the foundation change is mechanically neutral, so any agenten diff is JS-content jitter, not a regression.

git add -A
git commit -m "fix(v3): tokens foundation regression fix"

If Step 1 was clean, skip this step.

Append to www/v3/dev/foundation-baseline.md a short "Sub-project B — Plan 1 (Token Foundation) complete" section: tokens.css is the canonical :root, linked first on all 30 pages; the duplicated :root blocks are removed; the visual baseline is captured in www/v3/dev/baseline/; the diff mode reports zero change. Note that Plans 2+ migrate raw values to var(--token). Commit:

git add www/v3/dev/foundation-baseline.md
git commit -m "docs(v3): record token-foundation completion"

Self-Review

Checked against docs/superpowers/specs/2026-05-14-v3-design-tokens-design.md:

Placeholder scan: none — tokens.css is given in full; the harness rewrite is given in full; the :root removal for product.css is given exactly, and for v3.css it is precisely identified ("from :root { to its matching }") because that block carries the same content plus --radius — the executor reads the exact text, which is bounded and unambiguous.

Type/name consistency: token names used consistently (--text-N, --space-N, --radius-N, --icon-{sm,md,lg}, --weight-{regular,medium,semibold}, --cat-N). screenshot.mjs modes (baseline/diff/check), PAGES, slug(), OUT, BASELINE all internally consistent. The PAGES list and slug() match the harness from sub-project A (homepages → de-home/en-home).