Files
mokosh/.planning/debug/04-06-dark-mode-mark-decouple.md
Mark a8bcc17822 fix(debug 04-06): decouple welcome-hero mark stroke via --mks-mark-stroke
Operator-empirical Task 4 checkpoint flagged the dark-mode mark stroke
as muddy ink-on-madder. Root cause: .welcome-hero__mark used
`color: var(--mks-fg-inverse)`, which is a SEMANTIC text-foreground-on-
inverse-surface token that flips to ink-900 in the dark theme
(tokens.css line 244). The mark sits on a theme-independent madder-600
circle, so the stroke must be theme-independent too.

Fix: introduce a dedicated BRAND-COMPONENT token --mks-mark-stroke =
var(--mks-linen-50) in the universal :root block. CRUCIALLY NOT
overridden in the .dark/[data-theme="dark"] block — stays linen-50 on
every surface. Rewire .welcome-hero__mark to point at the new token.

SVG (mokosh-mark.svg) unchanged — `stroke="currentColor"` cascade
plumbing identical; only the wrapper's color source changed.

A35 strengthened: extracted live-DOM probe into a helper, now probes
BOTH light + dark themes (data-theme="dark" toggle on documentElement),
and added A35.5 — the decouple proof that light.computedStroke ===
dark.computedStroke === "rgb(250, 247, 241)" (linen-50). No new
__MOKOSH_UAT__ symbol; FORBIDDEN_HOOK_STRINGS stays at 12.

Scope expansion note: src/welcome/welcome.css was not in Plan 04-06
re-plan iter-2 files_modified. The edit is authorized by the operator's
TWEAK verdict on Task 4 checkpoint.

Verification:
- /tmp/04-06-welcome-hero-{light,dark}.png re-shot — both show identical
  crisp linen-on-madder grid icon.
- A35.5 LIVE-DOM probe (UAT): light="rgb(250, 247, 241)", dark=same.
- UAT 36/36 GREEN; vitest 187 + 1 tolerated webm-remux flake.
- 6/6 pre-checkpoint bundle gates PASS; FORBIDDEN_HOOK_STRINGS = 12.

Debug session: .planning/debug/04-06-dark-mode-mark-decouple.md

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 12:54:54 +02:00

116 lines
11 KiB
Markdown

---
status: awaiting_human_verify
trigger: "Operator-empirical Task 4 checkpoint on Plan 04-06 returned TWEAK NEEDED — decouple welcome-hero mark stroke from --mks-fg-inverse via a dedicated --mks-mark-stroke token that stays linen-50 in BOTH light and dark themes"
created: 2026-05-26T00:00:00Z
updated: 2026-05-26T10:30:00Z
---
## Current Focus
reasoning_checkpoint:
hypothesis: ".welcome-hero__mark { color: var(--mks-fg-inverse) } couples the mark stroke to the theme-conditional semantic foreground-inverse token (linen-50 in light, ink-900 in dark). The mark sits on a theme-independent madder-600 circle, so a theme-coupled stroke is the wrong abstraction. Adding a theme-INDEPENDENT --mks-mark-stroke = var(--mks-linen-50) in :root (NOT inside .dark/[data-theme=dark] override) and re-pointing the wrapper resolves the stroke to linen-50 in BOTH themes."
confirming_evidence:
- "tokens.css line 128 (universal :root): --mks-fg-inverse: var(--mks-linen-50)"
- "tokens.css line 244 (.dark override): --mks-fg-inverse: var(--mks-ink-900) — the theme flip confirmed"
- "welcome.css line 72: .welcome-hero__mark { color: var(--mks-fg-inverse) } — the exact cascade source"
- "mokosh-mark.svg line 2: stroke=currentColor — confirms the cascade plumbing target"
- "--mks-linen-50 = #faf7f1 defined at tokens.css line 90 (universal :root)"
- "popup/style.css:39 also reads --mks-fg-inverse but is unrelated to welcome mark — leave untouched"
falsification_test: "After the fix, re-run scripts/04-06-welcome-hero-screenshots.mjs. If `computedStroke` for the LIGHT screenshot differs from `computedStroke` for the DARK screenshot, the decoupling failed. If they match (expected rgb(250, 247, 241)), the fix is confirmed. A35 will also assert this equality programmatically."
fix_rationale: "Root cause is the wrong token abstraction — --mks-fg-inverse is a semantic token meant for text foreground on inverse surfaces (where the surface flips with theme), but the welcome-hero mark sits on a NON-flipping surface (madder). The fix introduces a dedicated brand-component token --mks-mark-stroke that is purpose-built for this case: theme-independent linen-50 stroke for the madder-circle mark. It addresses the abstraction error directly, not the symptom."
blind_spots: "1) A17.8 source-bundling grep doesn't check welcome.css text — safe. 2) inline-svg.test.ts only pins SVG + welcome.ts + globals.d.ts text — safe. 3) jsdom-style assertions in unit tests don't load welcome.css — safe. 4) Risk: if any other rule in welcome.css cascades from the wrapper's `color` (text nodes inside .welcome-hero__mark), they'd flip too — but the placeholder span is now replaced by the SVG at populateMark, so no visible text consumes the wrapper color. 5) The popup/style.css unrelated --mks-fg-inverse consumer is untouched (separate surface, separate decision)."
hypothesis: ".welcome-hero__mark { color: var(--mks-fg-inverse) } is theme-coupled; --mks-mark-stroke = var(--mks-linen-50) in :root decouples it."
test: Apply token addition + welcome.css edit; strengthen A35; re-screenshot.
expecting: Light and dark `computedStroke` both resolve to rgb(250, 247, 241) (linen-50).
next_action: Add --mks-mark-stroke to tokens.css :root, update welcome.css line 72, strengthen A35.3, run build + screenshots + UAT + vitest.
## Symptoms
expected: Welcome-hero grid icon (mokosh-mark.svg) renders with linen-50 (off-white) stroke on madder-600 circle in BOTH light and dark themes — high-contrast crisp aesthetic per the Plan 01-10 baseline.
actual: In LIGHT theme, stroke is linen-50 (`--mks-fg-inverse` = linen-50 there) — crisp linen-on-madder. In DARK theme, stroke is ink-900 (`--mks-fg-inverse` = ink-900 there) — indigo-on-madder, lower contrast.
errors: None — this is an aesthetic/design defect, not a runtime error.
reproduction: Open `dist-test/src/welcome/welcome.html` with `<html data-theme="dark">` vs without. Inspect `.welcome-hero__mark svg` `getComputedStyle().stroke`. Light = `rgb(250, 247, 241)` (linen-50). Dark = `rgb(24, 27, 42)` (ink-900). Visual: light is crisp, dark is muddy.
started: Plan 04-06 Task 1-3 landed; operator-empirical Task 4 checkpoint flagged the cascade-coupling defect.
## Evidence
- timestamp: 2026-05-26T00:00:00Z
checked: src/shared/tokens.css :root block (lines 82-229)
found: `--mks-fg-inverse: var(--mks-linen-50)` at line 128 (universal/light); `--mks-fg-inverse: var(--mks-ink-900)` at line 244 (.dark theme override).
implication: Confirms operator's diagnosis: --mks-fg-inverse is theme-coupled and unsuitable as a stroke anchor for a mark that must be crisp on madder in both themes.
- timestamp: 2026-05-26T00:00:00Z
checked: src/shared/tokens.css for --mks-linen-50 token
found: `--mks-linen-50: #faf7f1;` defined at line 90 in the universal :root block.
implication: `var(--mks-linen-50)` is the correct hex source; no need to use a literal hex.
- timestamp: 2026-05-26T00:00:00Z
checked: src/welcome/welcome.css .welcome-hero__mark (lines 64-73)
found: `color: var(--mks-fg-inverse);` at line 72 — the exact line that needs to flip to `var(--mks-mark-stroke)`.
implication: One-line CSS change at the wrapper. SVG remains untouched.
- timestamp: 2026-05-26T00:00:00Z
checked: src/shared/brand/mokosh-mark.svg
found: `stroke="currentColor"` at line 2 — unchanged across the fix.
implication: Cascade plumbing remains: SVG inherits from wrapper's `color`, which now points to the new theme-independent token.
- timestamp: 2026-05-26T10:15:00Z
checked: Grep for other consumers of --mks-fg-inverse across src/ + tests/
found: Two consumers — src/welcome/welcome.css:72 (target; rewired to --mks-mark-stroke) and src/popup/style.css:39 (separate popup-surface concern; LEFT UNTOUCHED). No tests grep for the literal `var(--mks-fg-inverse)` in welcome.css.
implication: Scope is minimal; only the welcome-hero mark wrapper changed. Popup style.css continues using --mks-fg-inverse as before.
- timestamp: 2026-05-26T10:20:00Z
checked: Built bundle dist/assets/welcome-BAisfyci.css after `npm run build`
found: ":root{...--mks-fg-inverse: var(--mks-linen-50);--mks-mark-stroke: var(--mks-linen-50);...}" + ".dark,[data-theme=dark]{...--mks-fg-inverse: var(--mks-ink-900);...}" — confirmed --mks-mark-stroke is in :root and NOT overridden in the dark block. ".welcome-hero__mark{...color:var(--mks-mark-stroke)}" — confirmed wrapper points at the new token.
implication: Build plumbing is correct.
- timestamp: 2026-05-26T10:22:00Z
checked: scripts/04-06-welcome-hero-screenshots.mjs re-run on dist/
found: "LIGHT screenshot: /tmp/04-06-welcome-hero-light.png (computed stroke: rgb(250, 247, 241))" and "DARK screenshot: /tmp/04-06-welcome-hero-dark.png (computed stroke: rgb(250, 247, 241))" — same RGB in both themes; rgb(250, 247, 241) is linen-50 (#faf7f1).
implication: Falsification test PASSED — the decoupling works at the live-DOM cascade level. Visual inspection of the dark screenshot shows the crisp linen-on-madder grid icon expected by the operator.
- timestamp: 2026-05-26T10:24:00Z
checked: vitest full suite (npx vitest run)
found: 187/188 passed, 1 tolerated flake (tests/background/webm-remux.test.ts > ffprobe-count_frames 905-912 timeout — documented 04-CONTEXT #9/#10; PASSES in isolation). Baseline 184 + 04-06 deltas +4 = 188 contract held.
implication: No vitest regression from the decouple changes.
- timestamp: 2026-05-26T10:25:00Z
checked: Pre-checkpoint bundle gates 6/6 (sw-bundle-import, no-test-hooks-in-prod-bundle, no-remote-fonts, no-new-function-in-sw-chunk, manifest-i18n, welcome inline-svg)
found: 6/6 PASS (37 tests green); FORBIDDEN_HOOK_STRINGS = 12 (unchanged).
implication: No CSP / globals / hook-leak regression.
- timestamp: 2026-05-26T10:28:00Z
checked: UAT skip-mode (`npm run test:uat`)
found: 36/36 PASS; A35.5 (new decouple sub-check) PASSES with: light="rgb(250, 247, 241)", dark="rgb(250, 247, 241)". A35 diagnostics show populateMark injected the inline <svg> in both themes and the cascade resolves to linen-50 in both.
implication: Live-DOM proof of theme decoupling is in the automated suite (not just the screenshot artifact).
## Resolution
root_cause: `.welcome-hero__mark` used `color: var(--mks-fg-inverse)`. The semantic token `--mks-fg-inverse` is theme-conditional — it resolves to linen-50 in light and ink-900 in dark (`tokens.css :root` line 128 vs `.dark, [data-theme="dark"]` line 244). The SVG inherits via `stroke="currentColor"`, so the stroke flipped with theme. But the mark sits on a theme-INDEPENDENT madder-600 circle, so a theme-coupled stroke produced muddy ink-on-madder in dark mode. The abstraction was wrong: --mks-fg-inverse is a SEMANTIC text-foreground-on-inverse-surface token, not a BRAND-COMPONENT stroke anchor for the welcome mark.
fix: Two-file change.
1. src/shared/tokens.css :root block — added a new brand-component token `--mks-mark-stroke: var(--mks-linen-50)` adjacent to --mks-fg-inverse. CRUCIALLY, this token is NOT overridden in the `.dark, [data-theme="dark"]` block below — it stays linen-50 on every surface. Comment in source explains the design intent + reference to this debug note.
2. src/welcome/welcome.css `.welcome-hero__mark` — flipped `color: var(--mks-fg-inverse)``color: var(--mks-mark-stroke)`. Comment in source explains the cascade chain.
Plus comment-only updates in src/welcome/welcome.ts + tests/welcome/inline-svg.test.ts + tests/uat/lib/harness-page-driver.ts + tests/uat/harness.test.ts to reflect the new wiring (no behavior change in those files beyond comments and A35 strengthening).
Plus A35 STRENGTHENING in tests/uat/lib/harness-page-driver.ts: extracted the live-DOM probe into a `a35Probe(welcomePage, dark)` helper, probed BOTH light + dark themes (data-theme="dark" toggle), and ADDED A35.5 — the decouple proof: assert light.computedStroke === dark.computedStroke === "rgb(250, 247, 241)" (linen-50). FORBIDDEN_HOOK_STRINGS unchanged (no new __MOKOSH_UAT__ symbol).
SVG `src/shared/brand/mokosh-mark.svg` UNCHANGED.
verification:
- LIGHT screenshot /tmp/04-06-welcome-hero-light.png: linen-50 stroke on madder-600 circle on linen-100 card on linen-50 page bg — baseline aesthetic preserved.
- DARK screenshot /tmp/04-06-welcome-hero-dark.png: linen-50 stroke on madder-600 circle on ink-800 card on ink-900 page bg — crisp linen-on-madder mark in dark mode (operator's spec). Visual contrast on the mark itself is IDENTICAL to light mode.
- A35.5 live-DOM probe: light + dark computedStroke both rgb(250, 247, 241). Programmatic proof of decoupling.
- vitest 187/188 + 1 tolerated flake (baseline contract held).
- UAT 36/36 (A35 now has 5 sub-checks all GREEN).
- 6/6 bundle gates PASS; FORBIDDEN_HOOK_STRINGS = 12 (unchanged).
- SCOPE EXPANSION NOTE: src/welcome/welcome.css was NOT in Plan 04-06 re-plan iter-2 files_modified. Adding this edit is a scope expansion authorized by the operator's empirical TWEAK verdict on Task 4 checkpoint. Surfaced explicitly here for the orchestrator's re-checkpoint payload.
files_changed:
- src/shared/tokens.css (added --mks-mark-stroke token in :root)
- src/welcome/welcome.css (rewired .welcome-hero__mark to new token)
- src/welcome/welcome.ts (comment-only updates)
- tests/welcome/inline-svg.test.ts (comment-only update)
- tests/uat/lib/harness-page-driver.ts (A35 strengthening + new A35.5 + helper extraction)
- tests/uat/harness.test.ts (comment-only update)
- .planning/debug/04-06-dark-mode-mark-decouple.md (this debug note)