## What was red
Both failures in the dashboard `app:components-b` lane. **Neither is a
style regression** — the CSS is byte-unchanged and correct in both
cases.
## Root cause: a jsdom upgrade, not a CSS change
jsdom does not substitute `var()`. What it does *instead* changed under
us in **4819c2634 (jsdom 27.4.0 → 29.1.1)**:
| | jsdom 27 | jsdom 29 |
|---|---|---|
| unresolvable shorthand (`padding`) | echoes raw text `var(--space-xs)
var(--space-sm)` | computes to `"0"` |
| single-value longhand (`gap`) | echoes | still echoes |
Tests asserting the **echoed string** were pinning a jsdom
implementation detail. The bump turned them red with nothing changed in
the product.
### 1. `renders a promote action when onPromote is provided`
`expected 'var(--space-xs) var(--space-sm)', received '0'`.
`.card-promote-action` still declares exactly that padding
(`TaskCard.css:1129`).
### 2. `FN-4511 keeps GitHub badge and timer chip geometry in parity`
`expected '1px' to be 'medium'`. The chips **are** in parity:
- badge: `border: 1px solid transparent`
- timer chip: `border: var(--btn-border-width) solid transparent`
- `--btn-border-width: 1px` (`styles.css:183`)
Here jsdom **discards** the unresolvable width rather than echoing it,
so `borderTopWidth` falls back to the initial value `medium`. The
existing `|| "1px"` fallbacks could not save it — `medium` is a
non-empty string, so it was the fallback that never ran, not the value
that was missing.
## The fix
Both assertions now read the **declared** value from the mounted
stylesheet's CSSOM and resolve a single `var()` against `:root`. That is
stable across jsdom versions and is what the assertions always meant.
Via the CSSOM rather than a regex over the CSS text **on purpose**: a
hand-rolled matcher over grouped selectors silently matches the wrong
rule and still reports success. Everything jsdom *can* resolve
(font-size, line-height, gap, padding parity) stays asserted against
computed style.
## Evidence
`components-b`: **1688/1688** (was 2 failed). Mutation-proved — both
fail as they should:
| mutation | result |
|---|---|
| timer chip border `1px → 2px` | `expected '2px' to be '1px'` |
| promote padding tokens changed | `expected 'var(--space-sm)
var(--space-lg)' to be 'var(--space-xs) var(--space-sm)'` |
**My first border mutation passed**, which would have read as a vacuous
guard. It had patched the wrong one of five identical `border:
var(--btn-border-width)` lines in the file. Re-run against
`TaskCard.css:1094` it fails correctly. Recorded because the mutation,
not the guard, was the thing that was wrong — a passing mutation is a
claim that needs checking too.
`pnpm lint` clean. Test-only — no production file or CSS touched
(mutations reverted; `git diff` clean).
## Ownership
`packages/dashboard/app` belongs to the **batch-dashboard-app** owner
(u12) under the mega-batch split. This is fix-forward on a red rather
than a conversion, confined to one test file, and touches no production
code — it should not conflict with the batch.
## Not fixed here
The `app:app` lane has **10 pre-existing failures** in `App.test.tsx`
(deep-link handling, board branch filters, FN-5817 mobile shell). They
were masked by this lane failing first — the runner skips remaining
lanes after the first failure, so they only became visible once
components-b went green. Separate change.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>