Skip to content

a11y(styles): lighten the vsl link accent to meet 4.5:1 - #251

Merged
Pixnop merged 4 commits into
devfrom
fix/issue-248-vsl-link-palette
Aug 28, 2026
Merged

a11y(styles): lighten the vsl link accent to meet 4.5:1#251
Pixnop merged 4 commits into
devfrom
fix/issue-248-vsl-link-palette

Conversation

@Zaldaryon

@Zaldaryon Zaldaryon commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

--color-vsl (#ad7639), the accent behind the eight text-vsl links 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, adds underline to 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: every text-vsl site renders through a translucent zinc-950 scrim 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

vsl sits 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. #ad7639 has 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-vsl moves from #ad7639 to #d49754, keeping the same hue (about 31.5°) and raising HSL lightness from 0.451 to 0.580. A comment above the ramp records why vsl moves while vs and vsd stay 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-vsl links 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. #d49754 has a relative luminance of 0.368, which is 1.02:1 against zinc-400 (0.360, the wrap color for six of the eight) and 1.98:1 against zinc-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 against zinc-400, but cleared 3:1 against zinc-200. There is no single hex that clears 4.5:1 against the #353537 scrim backdrop and stays 3:1 away from both zinc-400 and zinc-200: the review worked out that clearing the backdrop needs a luminance of at least 0.336, while staying 3:1 from zinc-200 needs 0.22 or below and from zinc-400 needs 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 add underline alongside text-vsl. The two status icons that also use text-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's border-vsl on 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 at border-2 when selected instead of the shared 1px border; 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/25 next to a bg-vsd/50 fill, 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_ROW in the test file left out the MainMenu header scrim that TasksMenu actually renders inside (<TasksMenu /> in MainMenu.tsx, under its own bg-zinc-950/50 header), 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 underline addition or the border-width change on one call site turns its matching anchor assertion in tests/text-contrast.test.ts red, then reverted both.

Regression proof

tests/text-contrast.test.ts gets a new describe block, reusing #236's existing helpers (luminance, contrast, over, worstCase, assertReadable) rather than duplicating them:

  • One test walks all eight text-vsl link 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 requiring underline on the class string, since that is the part of each site actually carrying the "this is a link" signal.
  • A second test covers the two status icons that reuse text-vsl against the 3:1 non-text floor, on a backdrop stack that now correctly includes every scrim between the shell and the icon.
  • A third test pins that vs < vsl in luminance in that order, and that the two decorative border-vsl sites (the selected Grid card, the selected background tile in Settings) still reference the token; the Settings tile assertion now also pins the border at border-2 when 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 the vitest.config.ts floor.
  • 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.

--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.
@Zaldaryon
Zaldaryon marked this pull request as ready for review August 26, 2026 11:54
@Zaldaryon
Zaldaryon requested a review from Pixnop August 26, 2026 11:54

@Pixnop Pixnop left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@Zaldaryon

Copy link
Copy Markdown
Collaborator Author

Both points are addressed in 53fb7de, pushed to this branch. Details in the updated PR description above; short version here.

Your math on the 1.4.1 gap holds and there is no hex that closes it: clearing 4.5:1 against the #353537 scrim needs luminance at least 0.336, staying 3:1 from zinc-200 needs 0.22 or below, and from zinc-400 needs 0.087 or below, and none of those ranges overlap. All eight text-vsl links now add underline alongside the color, so the link cue no longer depends on color alone. The two status icons that also use text-vsl (the info toast, the pending task icon) are untouched, since underlining an icon does nothing and they were never the problem.

The ConfigPage.tsx selected-tile border now renders at border-2 instead of the shared 1px border when selected. That is the case where the thumbnail has no fixed backdrop to guarantee a ratio against, so width is the lever left; the unselected state keeps its original 1px.

TASKS_ROW now includes the MainMenu header scrim TasksMenu actually renders inside, matching the rest of the file's care about painting the real stack.

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 tests/text-contrast.test.ts red, confirming both assertions actually pin the fix, then reverted both.

Local gates on the new head: typecheck, lint (0 errors, same 15 pre-existing warnings), format, and build:unpack all pass. npm run test:coverage is 128 files, 1,506 passed, 2 skipped, 0 failed, coverage 92.4 statements, 89.35 branches, 91.3 functions, 93.97 lines against floors of 87, 85, 85, 89. CI on this head is green across typecheck, lint, test, SonarCloud, and both Ubuntu and Windows builds.

@Pixnop Pixnop left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 on dev and 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.
@Zaldaryon

Copy link
Copy Markdown
Collaborator Author

Both fixed, in 97ad8f1 and 111cd37.

Finding 3. You are right that the anchor cannot fail, and it reproduces exactly as you describe: strip underline from line 36 and the lazy bridge keeps scanning to the Discord button's class string twelve lines down, so the assertion passes on a site it was never checking.

I took neither suggestion literally. The anchor now bridges whitespace only:

/openExternalLink\(ISSUES_URL\)\s+\}\}\s+className="text-vsl underline"/

\s+ cannot cross the >, the label, or the closing </NormalButton> between the two sites, so there is nothing for it to slide onto. I preferred this to the count assertion because it keeps the per-link tuple shape the rest of the array uses, and because "this file carries text-vsl underline twice" does not actually say the Issues link is one of the two. I applied the same tightening to the Discord anchor and to the AddInstallation.tsx one. Both are sound today only because nothing sits after them in their file, which is one added link away from being false.

Mutation checks, one at a time against the fixed tree: underline off InstalledModsSectionHeader.tsx:36 fails the link test naming that file and the ISSUES_URL regex, off line 48 it names the DISCORD_URL regex, and off AddInstallation.tsx:179 it names that file. That is your fourth check biting.

Finding 2. Rewrote both comments, the one in ConfigPage.tsx and the one in the test. You are right that width buys nothing under 1.4.11 and that the old wording read as licence to answer a contrast shortfall with a fatter line. Both edges are in the comment now: inside is the thumbnail at 2.51:1 worst case, outside is the section panel over the shell where the accent reads 4.85:1 whatever image the player picked, and a boundary unmistakable along one edge is perceivable. The width is attributed to 1.4.1, where it belongs. The test comment adds why no single ratio is asserted at that call site: the two edges do not share a backdrop, and the outer one is FORM_SECTION, already pinned by the link assertions above against 4.5:1, stricter than a border needs. I confirmed the tile really does sit in a DropdownSection, through FormGroupWrapper, with no fill in between, before writing that.

No shipped code changed for either finding. border-2 border-vsl stays.

Gates, local, Node 24.15.0:

  • npm run typecheck: passes, all three projects.
  • npm run lint:ci: 0 errors, the same 15 pre-existing warnings.
  • npm run format:check: clean.
  • npm run test:coverage: 128 files, 1,506 passed, 2 skipped, 0 failed. Coverage 92.38% statements, 89.32% branches, 91.3% functions, 93.97% lines.
  • npm run build:unpack: passes, Linux x64.

CI on 111cd37: green across all six checks.

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.

@Zaldaryon
Zaldaryon requested a review from Pixnop August 27, 2026 23:27
Zaldaryon added a commit that referenced this pull request Aug 27, 2026
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 Pixnop left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@Pixnop
Pixnop merged commit 4432f22 into dev Aug 28, 2026
7 checks passed
@Pixnop
Pixnop deleted the fix/issue-248-vsl-link-palette branch August 28, 2026 11:59
@Pixnop
Pixnop restored the fix/issue-248-vsl-link-palette branch August 28, 2026 12:00
Pixnop pushed a commit that referenced this pull request Aug 28, 2026
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
Pixnop deleted the fix/issue-248-vsl-link-palette branch August 28, 2026 12:05
Pixnop pushed a commit that referenced this pull request Aug 28, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants