a11y(styles): lighten the vsl link accent to meet 4.5:1 - #251
Conversation
--color-vsl (#ad7639) is the only one of the vs/vsl/vsd brand ramp ever used as text: text-vsl links plus two status icons. It always renders through the same translucent zinc-950 scrim stack over the player's background image that #236 measured everything else against, never on a real white surface, so the worst case is that scrim over a white image (~#353537), where it read 3.15:1. The fix lightens the token to #d49754 (~4.85:1 there), not darker, since darkening would fail the black-image extreme instead. --color-vs and --color-vsd, which style the active menu marker and the enabled toggle, are untouched. Extends tests/text-contrast.test.ts with the same worst-case compositing #236 used, covering all eight text-vsl link call sites and the two status icons, plus a coherence check that the vs/vsl/vsd ramp stays ordered and that the two decorative border-vsl call sites still track the token.
Pixnop
left a comment
There was a problem hiding this comment.
I recomputed the compositing from scratch instead of reading numbers off the new test, and the headline result holds. Blending zinc-950 at 70 percent (the App.tsx shell) then at 40 percent (DropdownSection or ListWrapper) over pure white gives #353537, and #ad7639 against that reads 3.151:1, which matches the 3.15 figure in #248 to two decimals. #d49754 on the same stack reads 4.851:1. Every other stack these links ship on is darker and so reads better: 6.536 on the version table, 6.810 on the installations menu card. The two status icons land at 5.970 for the toast and 5.557 for the pending task, both clear of the 3:1 non-text bar. The black extreme is never the binding one for a light accent, so the lighter-not-darker argument in the description is correct.
I checked the two premises you push back on, since both matter. --color-vs is what MainMenu.tsx:195 uses for the active marker (border-vs bg-vs/15) and what FormInputs.tsx:166 uses for the enabled toggle (data-checked:bg-vs). Neither goes near --color-vsl. The issue was wrong and you are right. I also traced all ten text-vsl sites to whatever actually paints under them: the Add and Edit start-params links land in a FormFieldDescription inside a FormGroupWrapper, which is a DropdownSection; the mods section header and the empty-mods notice sit in a ListWrapper where ListGroup adds no fill of its own; the version picker link is a direct child of TableBody rather than a row, so leaving the row tint out is the conservative reading. Every classification in the test is right.
Gates on 3c5c132 are green. typecheck clean, lint:ci 0 errors with the same 15 pre-existing warnings as dev, format:check clean, test:coverage 128 files and 1506 passed with 2 skipped, coverage 92.4/89.35/91.3/93.97 against floors of 87/85/85/89. The new tests bite, too. Reverting the token to #ad7639 fails the link test with exactly "3.15:1 on the worse extreme". Renaming one link's class fails the anchor guard by file and by pattern. Setting vsl to #2a2a2c fails three of them including the icon one, so none are vacuous.
Here is why I am not approving.
The accent no longer separates from the prose it sits inside. All eight of these are inline <Trans> links inside a paragraph, and neither NormalButton nor LinkButton adds an underline or a hover style. Colour is the only thing marking them as links. #d49754 has a relative luminance of 0.368. zinc-400 (#a1a1aa), which wraps six of the eight, is 0.360. That is 1.02:1 between the link and the sentence around it. The old value gave 1.51:1, already bad. The other two sit in zinc-200 prose, where the old accent read 3.05:1 and just cleared the 3:1 bar that 1.4.1 asks for when colour is the only cue; the new one reads 1.98:1 and does not. So this fixes 1.4.3 and breaks 1.4.1 on the same eight elements, and anyone with meaningful loss of colour discrimination now gets a link at the exact lightness of the text containing it.
No single hex can satisfy both. Clearing 4.5:1 on the #353537 backdrop needs luminance of at least 0.336. Staying 3:1 away from zinc-200 needs 0.22 or below, and away from zinc-400 needs 0.087 or below. There is no overlap, which is the useful part: no amount of palette tuning gets you out of this. It needs a cue that is not colour. Adding underline at the eight call sites, or better in the shared className in Buttons.tsx so the next call site inherits it, is a small diff and closes it.
The Settings background tile border moves the wrong way. ConfigPage.tsx:310 paints border-vsl at full opacity straight against the catalog thumbnail. Against a light thumbnail the old value read 3.87:1 and this one reads 2.51:1. Against a dark one it improves, 5.43 to 8.35. Your Limitations section says this was already true before the change, and in the trivial sense that no colour survives a thumbnail of its own colour, fine. But at the white and black extremes the old value cleared 3:1 in both directions and this one does not, and the selected state of that tile rides on the border alone. That is a trade this PR makes rather than one it inherits. Either say so plainly or thicken the border.
Two smaller notes, neither blocking. The Grid selected card border reads 1.53:1 worst case at 25 percent alpha over bg-vsd/50, up from 1.34, and the test declines to assert on it. I agree with not asserting and the comment explaining why is honest, but the selected state there is carried by fill and border colour with nothing else, so it is a 1.4.11 gap that predates you and deserves its own issue rather than a comment in a test. And TASKS_ROW leaves out the MainMenu scrim that TasksMenu actually renders inside. It errs toward a lighter backdrop, so the assertion is stricter than reality and nothing breaks, but the rest of that file is scrupulous about painting the real stack and this one line is not.
On the visual check: I did not get a usable screenshot of the running app, so I have not eyeballed it either. The link-versus-prose numbers above are the closest thing I have to answering "do they still read as links", and the answer they give is worse than before.
Responds to Pixnop's review on PR #251. Lightening --color-vsl to clear 4.5:1 against the shell scrim brought it within 1.02:1 of zinc-400 (the prose six of the eight text-vsl links sit inside) and to 1.98:1 against zinc-200 (the other two), both below the 3:1 WCAG 1.4.1 needs when colour is the only cue marking an element as a link. No single hex clears 4.5:1 on the scrim backdrop and stays 3:1 away from both greys at once, so the fix is a non-colour cue: all eight call sites now add `underline` alongside `text-vsl`. The two text-vsl status icons (the info toast, the pending task icon) are untouched, since they aren't links and underlining an icon glyph does nothing. ConfigPage.tsx's selected background-tile border also regressed: `border-vsl` against a light catalog thumbnail read 3.87:1 before this lightening and 2.51:1 after, below the 3:1 non-text floor that border is the sole indicator of. It now renders at `border-2` when selected instead of the shared `border`, which is what keeps the selected state visible against a light thumbnail; the unselected state keeps its original 1px width. tests/text-contrast.test.ts: the 8 link anchors and the ConfigPage border anchor are updated to match the new class strings. TASKS_ROW also gets a fix unrelated to the token, but caught by the same review: it left out the MainMenu header scrim that TasksMenu actually renders inside, testing a lighter, unrealistic backdrop; it now includes that scrim. Verified with a mutation check: reverting either the border width or the underline on a call site turns the matching anchor test red, then reverted both. The Grid.tsx selected-card border gap (25% alpha border-vsl/25 next to a bg-vsd/50 fill, worst case 1.53:1 with no other indicator of the selected state) is a separate, pre-existing issue per the review and is not touched here; filed as its own issue instead.
|
Both points are addressed in Your math on the 1.4.1 gap holds and there is no hex that closes it: clearing 4.5:1 against the The
The Grid.tsx selected-card border gap you flagged is filed as its own issue, #258, rather than a comment in the test, per your note that it predates this PR and deserves that. Verified with a mutation check before pushing: reverting either the underline addition or the border-width change on one call site turns its matching anchor test in Local gates on the new head: typecheck, lint (0 errors, same 15 pre-existing warnings), format, and build:unpack all pass. |
Pixnop
left a comment
There was a problem hiding this comment.
Re-reviewed the two new commits. I recomputed every ratio below from scratch with my own compositing and luminance implementation, against the scrim values actually in the source rather than the ones in the description (bg-zinc-950/70 on the shell, /40 on the section and list panels, /50 on the menu, card and table fills). Everything you published reproduces to the second decimal, including the two you had to work out during the last round.
Finding 1, the accent no longer separating from prose: resolved
underline is on all eight sites, and they are the same eight I listed in #236. Recomputed on the stack each one actually ships on:
| Site | Stack | #d49754 worst case |
|---|---|---|
AddInstallation.tsx:179 |
shell + section | 4.85:1 |
EditInstallation.tsx:190 |
shell + section | 4.85:1 |
InfoAndHelpPage.tsx:60 |
shell + section | 4.85:1 |
NoInstalledModsNotice.tsx:26 |
shell + list panel | 4.85:1 |
InstalledModsSectionHeader.tsx:36 |
shell + list panel | 4.85:1 |
InstalledModsSectionHeader.tsx:48 |
shell + list panel | 4.85:1 |
GameVersionPicker.tsx:55 |
shell + section + table fill | 6.54:1 |
InstallationsDropdownMenu.tsx:32 |
shell + menu + card | 6.81:1 |
The binding case is the section and list panel stack, which composites to #353537 over a white image. 4.85:1 there is the number to hold onto: it clears 4.5 but not by a lot, so anything that later removes a scrim between the shell and one of these links will move it. The test is the right place for that to get caught.
I also checked the underline survives the wrappers rather than just sitting in a class string. NormalButton and LinkButton in Buttons.tsx set no text-decoration, no no-underline, and no hover rules at all. The edit page link goes straight to Headless UI's Button, which brings nothing of its own. styles.css adds no reset. So it renders at rest and is unchanged on hover across all eight. These buttons have no hover treatment whatsoever, so hover was never carrying any part of the signal to begin with.
The separation numbers that made a colour-only fix impossible also reproduce: #d49754 sits 1.02:1 from zinc-400 and 1.98:1 from zinc-200, where the old #ad7639 was 1.51:1 and 3.05:1. The argument that no single hex satisfies both constraints holds.
Finding 2, the ConfigPage tile border: resolved, but the comment argues it wrong
border-2 border-vsl ships, and the regression I reported is real. I get 3.87:1 before and 2.51:1 after against a white thumbnail, 5.43:1 rising to 8.35:1 against a black one. Same figures you had.
The outcome is fine. The justification in the code comment is not. It reads as though extra width buys you past a 3:1 shortfall, and 1.4.11 has no width provision anywhere in it. A thicker border at 2.51:1 is still 2.51:1. What width actually rescues is 1.4.1, because the selected state stops being marked by hue alone.
The argument that does save the 1.4.11 case is one the PR never makes. That border has two adjacent colours, not one. Inside is the arbitrary thumbnail, but outside is the section panel over the shell, the same fixed #353537 stack the links sit on, and the accent reads 4.85:1 against it no matter which image the player picked. A boundary that is unmistakable along one of its edges is perceivable. That belongs in the comment, because the wording there now will read to the next person as licence to fix a contrast shortfall by drawing a fatter line.
Not blocking. Comment only.
Finding 3, the test rows: one of the eight anchors cannot fail
This is the one thing I want changed.
The description says reverting underline on a call site turns its matching anchor red. True for seven of the eight. Not true for mods section issues link:
/openExternalLink\(ISSUES_URL\)[\s\S]*?className="text-vsl underline"/
InstalledModsSectionHeader.tsx holds two link sites and the Issues one comes first. Strip underline from line 36 and the lazy [\s\S]*? simply keeps scanning, reaches the Discord button's className="text-vsl underline" a dozen lines further down, and matches there instead. I ran it: 13 passed, 0 failed, with the Issues link shipping bare.
The other two anchors built the same way are sound, because nothing sits after them to slide onto. Removing underline from the Discord link fails its anchor, and so does removing it from AddInstallation.tsx. The hole is specific to being the first of a pair in one file.
Small fix and I do not much mind which way. A bridge that cannot cross a component boundary works:
/openExternalLink\(ISSUES_URL\)(?:(?!NormalButton)[\s\S])*?className="text-vsl underline"/
Or drop the regex gymnastics and assert that file carries text-vsl underline exactly twice. That reads better than a negative lookahead and it fails for either site.
The rest of what I checked
The TASKS_ROW correction is right, and it tightens something real rather than being cosmetic. Without the MainMenu header scrim the stack composites to #2c2c2e over a white image and the pending icon reads 5.56:1. With it, #1f1f21 and 6.55:1. Both clear the 3:1 non-text floor, so nothing was ever broken, but the stack in the file is now the one that ships. The info toast icon reads 5.97:1 on its own stack.
I grepped every consumer of the token on this branch: eight links, two status icons, two borders, and nothing else outside the tests. So lightening it repaints exactly what you say it repaints, and the diff confirms --color-vs and --color-vsd are untouched.
Mutation checks I ran myself, independent of yours. Token back to #ad7639 fails with add installation start-params link reads 3.15:1 on the worse extreme, below 4.5:1, which is your original measurement coming back out of the suite. border-2 back to border fails the ConfigPage anchor. underline off NoInstalledModsNotice.tsx fails its anchor. Three bite. The fourth is finding 3.
Gates
Run on 53fb7de after a clean npm ci.
npm run typecheck: passes, all three projects.npm run lint:ci: 0 errors, 15 warnings. I ran the same command ondevand got 15 there too, so this branch adds none.npm run format:check: clean.npm run test:coverage: 128 files, 1506 passed, 2 skipped. Statements 92.4, branches 89.35, functions 91.3, lines 93.97, against floors of 87, 85, 85 and 89. Every figure matches what you posted.
Requesting changes on finding 3 alone. The shipped code is correct on all eight links. It is the proof that has a gap on one of them, and it is a one-line repair.
The "mods section issues link" anchor could not fail. InstalledModsSectionHeader.tsx holds two text-vsl links and the Issues one comes first, so the lazy [\s\S]*? bridge scanned past a bare Issues link and matched the Discord button's className twelve lines down, passing on a site it was not checking. Raised by Pixnop re-reviewing PR #251. Every bridging anchor now spans whitespace only, so none can cross the markup between two call sites. The Issues and Discord anchors require the handler's closing braces and the class string with nothing but whitespace between them, and AddInstallation.tsx's anchor drops its [\s\S]*? bridge for the same reason: those two were sound only because nothing sits after them in their file. Verified one mutation at a time. Removing underline from InstalledModsSectionHeader.tsx line 36 fails the Issues anchor, line 48 fails the Discord anchor, and AddInstallation.tsx line 179 fails its own.
The comment added with the border-2 change argued that extra width carries the border past a 3:1 shortfall. WCAG 1.4.11 has no width provision, so a thicker border at 2.51:1 is still 2.51:1, and the wording read as licence to answer a contrast gap by drawing a fatter line. Raised by Pixnop re-reviewing PR #251. The border has two adjacent colours, not one. Inside is the player's thumbnail, where the accent can fall to 2.51:1 against a light image. Outside is the section panel over the shell, the same fixed stack the eight accent links sit on, where it reads 4.85:1 whatever image was picked. A boundary unmistakable along one of its edges is perceivable, and that is what holds 1.4.11 here. The width buys 1.4.1 instead: the selected state stops being marked by hue alone. Both comments say that now, and the test comment adds why no single ratio is asserted at that call site. No shipped code changes.
|
Both fixed, in Finding 3. You are right that the anchor cannot fail, and it reproduces exactly as you describe: strip I took neither suggestion literally. The anchor now bridges whitespace only:
Mutation checks, one at a time against the fixed tree: Finding 2. Rewrote both comments, the one in No shipped code changed for either finding. Gates, local, Node 24.15.0:
CI on One knock-on worth flagging: PR #260 stacks on this branch and its description drew the same contrast you just corrected, calling the ConfigPage border one with no fixed backdrop to measure against. Rebased it onto this head, resolved the one conflict (the ConfigPage comment block, which #260 also edits), and fixed that wording in its description and commit message so the two do not argue opposite things about the same border. |
Fixes #258. The selected-card border in Grid.tsx (bg-vsd/50 border-vsl/25) sat at 25% alpha next to its own dark fill, reading about 1.53:1 worst case, below the 3:1 WCAG 1.4.11 floor for a UI boundary that is the sole indicator of a state (ModListCard.tsx's selected={installed}, the only consumer). Raised during review of PR #251, filed separately since the gap predates it. Unlike ConfigPage's selected background-tile border, whose inner edge paints over an arbitrary user-chosen thumbnail so no single ratio can be asserted for it (its outer edge holds 1.4.11 against the fixed section panel at 4.85:1, and the border-2 there answers 1.4.1 instead), the Grid card's backdrop is fully known on both sides: the card's own bg-vsd/50 fill composited over the grid panel's fixed scrim stack. That makes the ratio actually computable, so the fix is opacity, not width: the border drops its /25 and reads at full strength, which the same compositing math the rest of text-contrast.test.ts already uses puts at about 4.82:1, comfortably clear of the floor. tests/text-contrast.test.ts gets a real assertion where there was previously only a comment explaining why the border went unmeasured. Verified it actually pins the fix: reverting the border to border-vsl/25 makes the test's anchor regex fail to match, since it now requires the bare border-vsl string with no alpha suffix.
Pixnop
left a comment
There was a problem hiding this comment.
The anchor bites now.
I re-ran the exact kill test that exposed the problem last time. Stripping underline off the Issues link at InstalledModsSectionHeader.tsx:36 takes the suite to 1 failed, 12 passed, and the failure names the Issues anchor specifically:
AssertionError: nothing matching /openExternalLink\(ISSUES_URL\)\s+\}\}\s+className="text-vsl underline"/ in features/mods/components/InstalledModsSectionHeader.tsx, the class this test pins has moved or gone
That same mutation used to leave all 13 tests green, which was the whole complaint.
Since the loop aborts on the first failed anchor, I also probed both regexes directly against the mutated file rather than relying on the suite alone. With only line 36 stripped, the Issues anchor no longer matches and the Discord anchor still does, so the two are pinned independently instead of one quietly standing in for the other. For the record the old lazy pattern still matches on that same mutated file and runs to offset line 48, which is precisely the Discord className it had been landing on.
I then ran the mutation across all eight call sites, one at a time. Every one of them kills the suite, and in each case the failing anchor is the one belonging to that site. Trading [\s\S]*? for \s+ is the right shape for this rather than a patch over the one bad case: whitespace cannot span intervening markup, so no anchor can physically reach a neighbouring call site, and dropping the bridge on AddInstallation.tsx for the same reason is the correct instinct even though nothing follows it today.
The docs commit is a real improvement too. Losing the "extra width carries the border past a shortfall" wording matters, because 1.4.11 has no width provision and the old phrasing read as licence to answer a contrast gap by drawing a fatter line. Splitting the border into its two edges is the accurate account: the outer edge against the fixed section panel is what holds 1.4.11 at 4.85:1, and the width is doing 1.4.1 work by making the selected state something other than hue alone. Comment text only, no shipped code moved.
Gates are green locally on 111cd37: typecheck clean, lint at 0 errors (the 15 warnings are pre-existing and in untouched files), format:check clean, and test:coverage at 128 files with 1506 passed and 2 skipped, statements 92.4% and branches 89.35%, floors holding.
Approving.
Fixes #258. The selected-card border in Grid.tsx (bg-vsd/50 border-vsl/25) sat at 25% alpha next to its own dark fill, reading about 1.53:1 worst case, below the 3:1 WCAG 1.4.11 floor for a UI boundary that is the sole indicator of a state (ModListCard.tsx's selected={installed}, the only consumer). Raised during review of PR #251, filed separately since the gap predates it. Unlike ConfigPage's selected background-tile border, whose inner edge paints over an arbitrary user-chosen thumbnail so no single ratio can be asserted for it (its outer edge holds 1.4.11 against the fixed section panel at 4.85:1, and the border-2 there answers 1.4.1 instead), the Grid card's backdrop is fully known on both sides: the card's own bg-vsd/50 fill composited over the grid panel's fixed scrim stack. That makes the ratio actually computable, so the fix is opacity, not width: the border drops its /25 and reads at full strength, which the same compositing math the rest of text-contrast.test.ts already uses puts at about 4.82:1, comfortably clear of the floor. tests/text-contrast.test.ts gets a real assertion where there was previously only a comment explaining why the border went unmeasured. Verified it actually pins the fix: reverting the border to border-vsl/25 makes the test's anchor regex fail to match, since it now requires the bare border-vsl string with no alpha suffix.
Fixes #258. The selected-card border in Grid.tsx (bg-vsd/50 border-vsl/25) sat at 25% alpha next to its own dark fill, reading about 1.53:1 worst case, below the 3:1 WCAG 1.4.11 floor for a UI boundary that is the sole indicator of a state (ModListCard.tsx's selected={installed}, the only consumer). Raised during review of PR #251, filed separately since the gap predates it. Unlike ConfigPage's selected background-tile border, whose inner edge paints over an arbitrary user-chosen thumbnail so no single ratio can be asserted for it (its outer edge holds 1.4.11 against the fixed section panel at 4.85:1, and the border-2 there answers 1.4.1 instead), the Grid card's backdrop is fully known on both sides: the card's own bg-vsd/50 fill composited over the grid panel's fixed scrim stack. That makes the ratio actually computable, so the fix is opacity, not width: the border drops its /25 and reads at full strength, which the same compositing math the rest of text-contrast.test.ts already uses puts at about 4.82:1, comfortably clear of the floor. tests/text-contrast.test.ts gets a real assertion where there was previously only a comment explaining why the border went unmeasured. Verified it actually pins the fix: reverting the border to border-vsl/25 makes the test's anchor regex fail to match, since it now requires the bare border-vsl string with no alpha suffix.
Summary
--color-vsl(#ad7639), the accent behind the eighttext-vsllinks plus two status icons, reads 3.15:1 on its binding stack, below the 4.5:1 target #236 set for every other piece of text over the player's background image. The fix lightens the token to#d49754, which clears 4.5:1 on the same worst case, addsunderlineto the eight links so colour is not the only thing marking them as links, thickens the selected background-tile border where the lighter token dropped below its own 3:1 floor, and adds a regression test using the exact compositing method #236 introduced.Two premises in the issue don't hold once you check the source. The active menu marker and the enabled toggle use
--color-vs(#7e501e), a separate token, not--color-vsl, so this change never touches them. And the fix direction is lighter, not darker: everytext-vslsite renders through a translucentzinc-950scrim stack over whatever background image the player picked, never on a real white surface, so the worst case is that stack over a white image (composites to about#353537), where the old value read 3.15:1. Darkening it toward#ad7639's black-extreme reading of 5.19:1 would have made the white extreme worse, not better.Root cause
vslsits in a three-token brand ramp (vs#7e501e,vsl#ad7639,vsd#4f3110). It's the only one of the three ever used as text, and #236's audit of 557 text elements deliberately left it out of scope, which is why #248 was filed.#ad7639has a relative luminance of 0.2214; the binding stack (the shell scrim at 70% plus a section or list panel at 40%, composited over white) needs at least 0.3375 to clear 4.5:1, so the accent needed about 52% more luminance.Fix
src/renderer/src/styles.css:--color-vslmoves from#ad7639to#d49754, keeping the same hue (about 31.5°) and raising HSL lightness from 0.451 to 0.580. A comment above the ramp records whyvslmoves whilevsandvsdstay put: those two are fills that carry light text on top, not text colors themselves.No call site changes to
--color-vs,--color-vsd, or any surface that uses them (the active menu marker, the enabled toggle, selected table rows, progress bars).Update: response to review
Pixnop requested changes, having independently confirmed the token math and the two corrected premises above, then found two real problems the fix introduced.
The lighter accent no longer separates from the prose it sits inside. All eight
text-vsllinks are inline<Trans>links inside a paragraph with no underline or hover style of their own, so colour was the only cue marking them as links.#d49754has a relative luminance of 0.368, which is 1.02:1 againstzinc-400(0.360, the wrap color for six of the eight) and 1.98:1 againstzinc-200(the other two), both below the 3:1 WCAG 1.4.1 needs when colour alone marks a link. The old value already read a marginal 1.51:1 againstzinc-400, but cleared 3:1 againstzinc-200. There is no single hex that clears 4.5:1 against the#353537scrim backdrop and stays 3:1 away from bothzinc-400andzinc-200: the review worked out that clearing the backdrop needs a luminance of at least 0.336, while staying 3:1 fromzinc-200needs 0.22 or below and fromzinc-400needs 0.087 or below, and those ranges don't overlap. No amount of palette tuning gets out of this; it needs a cue that isn't colour. All eight call sites now addunderlinealongsidetext-vsl. The two status icons that also usetext-vsl(the info toast, the pending task icon) are untouched, since they aren't links and underlining an icon glyph does nothing.The Settings background-tile selected border moved the wrong way.
ConfigPage.tsx'sborder-vslon the selected catalog thumbnail read 3.87:1 against a light thumbnail before this change and 2.51:1 after, below the 3:1 floor that border is the sole indicator of (it improved from 5.43 to 8.35 against a dark thumbnail, so this is worse only at one extreme, but the selected state has to hold at both). The border now renders atborder-2when selected instead of the shared 1pxborder; the unselected state keeps its original width. A thicker indicator is the accepted alternative here since the thumbnail itself has no fixed backdrop to guarantee a contrast ratio against.Two non-blocking items from the same review, handled outside this PR: the Grid.tsx selected-card border (25% alpha
border-vsl/25next to abg-vsd/50fill, worst case 1.53:1 with nothing else marking the selected state) is a separate, pre-existing gap the review said deserves its own issue rather than a passing comment in the test, filed as #258.TASKS_ROWin the test file left out theMainMenuheader scrim thatTasksMenuactually renders inside (<TasksMenu />inMainMenu.tsx, under its ownbg-zinc-950/50header), testing a lighter, unrealistic backdrop; nothing broke since the stricter backdrop still clears 3:1, but it's fixed here alongside the rest of the file's edits since it was a one-line, unambiguous correction.Verified with a mutation check before pushing: reverting either the
underlineaddition or the border-width change on one call site turns its matching anchor assertion intests/text-contrast.test.tsred, then reverted both.Regression proof
tests/text-contrast.test.tsgets a newdescribeblock, reusing #236's existing helpers (luminance,contrast,over,worstCase,assertReadable) rather than duplicating them:text-vsllink call sites, classifies each into the real paint stack it ships on, and asserts 4.5:1 on the worse of the white and black image extremes for every one of them, with each anchor now also requiringunderlineon the class string, since that is the part of each site actually carrying the "this is a link" signal.text-vslagainst the 3:1 non-text floor, on a backdrop stack that now correctly includes every scrim between the shell and the icon.vs<vslin luminance in that order, and that the two decorativeborder-vslsites (the selected Grid card, the selected background tile in Settings) still reference the token; the Settings tile assertion now also pins the border atborder-2when selected.Testing
npm run typecheck: passes.npm run lint:ci: 0 errors, 15 pre-existing warnings (unchanged from before this branch).npm run format:check: passes.npm run test:coverage: 128 files, 1,506 passed, 2 skipped. Coverage 92.4% statements, 89.35% branches, 91.3% functions, 93.97% lines, all at or above thevitest.config.tsfloor.npm run build:unpack: passes on Linux x64.git diff --check: passes.I did not verify this visually in the running app, for the same reason as the original PR: no safe, non-disruptive way to drive it through the affected screens on this machine. This rests on the same compositing math #236 and this review both used, not an eyeballed comparison.
Related issues
Fixes #248. Follow-up for the Grid.tsx selected-card border filed as #258.