Summary
Inline text actions inside FieldConsole are effectively invisible in dark theme. Measured idle contrast is 1.02:1 against --surface-2; WCAG AA requires 4.5:1 for text. Only the underline survives, so the control reads as a bare underscore.
The guard that exists to prevent exactly this (web/src/styles.test.ts, the #834 link-retirement test) passes today, because it asserts against static token defaults and never renders a FieldConsole-scoped DOM.
Root cause
web/src/components/FieldConsole.tsx:96 scopes the --link custom property for its whole subtree:
// Concept B uses brand-coloured row links; other pages retain the shared theme default.
"--link": "var(--brand)",
--brand is a fixed palette hex (#4a154b on the default palette, styles.css:14) that does not vary between light and dark. It therefore shadows the global :root[data-theme="dark"] { --link: var(--ink) } (styles.css:205) that every --link consumer expects.
Confirmed by walking getComputedStyle(el).getPropertyValue('--link') up a live dark-theme render: correct #f2ecf4 at <html>, still correct at MAIN.content, then flipping to #4a154b at the FieldConsole <section data-field-console>.
This is not an emotion cache-order or MUI specificity problem. It is a custom property being shadowed by a non-theme-aware value.
Measured contrast (Chromium, live render, dark theme, default palette)
| State |
Computed color |
vs --surface-2 |
vs --surface |
| Idle |
#4a154b (--brand) |
1.02:1 |
1.18:1 |
| Busy / disabled |
rgba(255,255,255,0.3) |
2.64:1 |
2.71:1 |
If --link were not shadowed |
#f2ecf4 (--ink) |
12.35:1 |
— |
Both states fail. The disabled state is a separate, secondary MUI .Mui-disabled override landing on top of an already-broken idle colour.
In light theme the same brand colour scores 13.78:1, which is why this shipped unnoticed.
Blast radius
FieldConsole is consumed by seven routes on main today: ExpensesPage, FeedPage, HistoryPage, InventoryPage, ReportsPage, StockPage, WaterPage. Affected controls include History's edit/adjust/void, Expenses' manage-categories, Water's correct, Inventory's choose-another-item, and Stock's Audit history.
PR #927 adds SalesPage as an eighth, via its own LINK_ACTION_SX copy — including the save/cancel pair fixed in that PR.
Introduced by #899, not by #927.
Not verified: only the default palette was measured. --brand is redefined per palette (styles.css:142, 160, 178). The other three palettes' dark-mode contrast is unknown and may differ in either direction.
The guard is blind, and that is half the bug
web/src/styles.test.ts (~161-175) asserts --link === --ink and checks contrast >= 4.5:1 against --surface/--surface-2. It reads static token defaults via resolveTokens and never mounts a FieldConsole-scoped DOM, so a runtime sx-level override shadowing the token is invisible to it.
Per AGENTS.md's "Writing a guard" section: "A wrong guard is worse than none because it looks safe." This one tests configuration rather than the live cascade, and it reported green through eight affected screens.
Scope
Verification
- Measured contrast >= 4.5:1 for idle and >= 3:1 for disabled, in dark, on every palette, on all eight screens.
- The repaired guard fails against the current
FieldConsole.tsx:96 and passes after the fix.
- Before/after dark-theme screenshots at 1:1.
Summary
Inline text actions inside
FieldConsoleare effectively invisible in dark theme. Measured idle contrast is 1.02:1 against--surface-2; WCAG AA requires 4.5:1 for text. Only the underline survives, so the control reads as a bare underscore.The guard that exists to prevent exactly this (
web/src/styles.test.ts, the #834 link-retirement test) passes today, because it asserts against static token defaults and never renders aFieldConsole-scoped DOM.Root cause
web/src/components/FieldConsole.tsx:96scopes the--linkcustom property for its whole subtree:--brandis a fixed palette hex (#4a154bon the default palette,styles.css:14) that does not vary between light and dark. It therefore shadows the global:root[data-theme="dark"] { --link: var(--ink) }(styles.css:205) that every--linkconsumer expects.Confirmed by walking
getComputedStyle(el).getPropertyValue('--link')up a live dark-theme render: correct#f2ecf4at<html>, still correct atMAIN.content, then flipping to#4a154bat theFieldConsole<section data-field-console>.This is not an emotion cache-order or MUI specificity problem. It is a custom property being shadowed by a non-theme-aware value.
Measured contrast (Chromium, live render, dark theme, default palette)
color--surface-2--surface#4a154b(--brand)rgba(255,255,255,0.3)--linkwere not shadowed#f2ecf4(--ink)Both states fail. The disabled state is a separate, secondary MUI
.Mui-disabledoverride landing on top of an already-broken idle colour.In light theme the same brand colour scores 13.78:1, which is why this shipped unnoticed.
Blast radius
FieldConsoleis consumed by seven routes onmaintoday:ExpensesPage,FeedPage,HistoryPage,InventoryPage,ReportsPage,StockPage,WaterPage. Affected controls include History's edit/adjust/void, Expenses' manage-categories, Water's correct, Inventory's choose-another-item, and Stock'sAudit history.PR #927 adds
SalesPageas an eighth, via its ownLINK_ACTION_SXcopy — including thesave/cancelpair fixed in that PR.Introduced by #899, not by #927.
Not verified: only the default palette was measured.
--brandis redefined per palette (styles.css:142,160,178). The other three palettes' dark-mode contrast is unknown and may differ in either direction.The guard is blind, and that is half the bug
web/src/styles.test.ts(~161-175) asserts--link === --inkand checks contrast >= 4.5:1 against--surface/--surface-2. It reads static token defaults viaresolveTokensand never mounts aFieldConsole-scoped DOM, so a runtimesx-level override shadowing the token is invisible to it.Per AGENTS.md's "Writing a guard" section: "A wrong guard is worse than none because it looks safe." This one tests configuration rather than the live cascade, and it reported green through eight affected screens.
Scope
FieldConsolelink colour theme-aware, or drop the--linkoverride and let the theme default apply. Decide whether brand-coloured row links are still wanted in light mode, and record the decision.FieldConsole-scoped override. Mutate it red before claiming it catches this, per the repo's mutation-first rule.saveon the Sales Draft line item specifically, which routes through the same path as of feat(web): redesign Sales with a MUI order desk and settlement rail #927.Verification
FieldConsole.tsx:96and passes after the fix.