Measure the text the way an eye reads it, not the way a ratio scores it

The sweep has checked WCAG AA contrast for a long time and the page passed. That
result was worth less than it looked.

WCAG 2's ratio is a luminance gap, and a luminance gap overstates readability once
one colour is near black — which is every reading on a page whose ground is #141414.
Tuning a colour until it clears 4.5:1 there lands on text that passes the audit and
still reads as a smudge across a table in a dim room. Several rules in this
stylesheet said as much in their own comments, one of them "measured 4.5:1 at
#a5a5a5 exactly, so it sits above that": the right instinct through the wrong lens.

The check also could not see half the backgrounds it scored against. It reads
background-color and takes the best contrast of any candidate ancestor, so text on
the felt was scored against the dark page behind the felt. Two plain AA failures sat
in a clean report for as long as the check has existed — the chair number on the
brown rim at 2.65:1, and the pot's own label, gold on green, at 2.66:1.

runContrastAudit.js scores with APCA and gets the background by screenshotting the
page with its text made transparent and reading the pixels, which no gradient or
shadow can fool. The first version walked the DOM and called the gold dealer button
black-on-black at Lc 0.4; that false positive is why it works this way.

Found: 65 of 92 pieces of text below their level, 54 passing AA at the same moment,
worst Lc 28. Now nothing is below Lc 60, nothing fails AA, and the worst is Lc 60.4.

Three fixes worth naming, none of them "make the grey lighter":

- A player's stack was secondary text at Lc 44, under a name in --text at a heavier
  weight. That says a dealer needs the name more than the number; during a hand the
  opposite is true. Hierarchy comes from size and weight now, which is free, rather
  than from fading, which on a dark ground is the most expensive way to say it.
- An empty seat had no background of its own, so the felt decided how readable its
  number was: Lc 28 on the rim, 30 on the green, 33 further in. It has a ground now,
  at 0.62 and not the 0.82 first written — contrast is nearly flat across that range
  and the heavier wash put four dark rectangles on the felt like holes.
- The pot's label sat on the one green in the design. The disc is deeper rather than
  the label bleached, because gold on dark green is the pairing this page is built
  on. Deepening only its outer stop moved it two points: the highlight is at 32%/28%
  and that is exactly where the label sits.

apca.js is checked against the eight published reference pairs, png.js against
round-tripped fixtures for every filter. Four fault injections caught; a fifth
turned out to be a no-op — the Paeth predictor is symmetric in its first two
arguments over all 16,777,216 byte triples, so that swap is untestable, which is
written down rather than papered over with a fixture that appears to catch it.

Still below the strict APCA level: 55, all of them 10-12px labels that would need
white type or a bigger seat puck. Reported, not chased, and said out loud in the
sweep's own notes.

Layout sweep unchanged: 5 FAILs, 14 offender lines, no regressions. 950 tests pass.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
This commit is contained in:
2026-09-09 06:40:28 -06:00
co-authored by Claude Opus 5
parent 295d9eff4a
commit 853fcedea0
8 changed files with 988 additions and 23 deletions
+69
View File
@@ -837,6 +837,75 @@ the bet chips at extreme crowding the way the tight tier already drops the hole
capping the seat count by viewport, or letting the felt scroll. Each trades something a
dealer can see, so none of them should be picked by whoever is next holding a ruler.
## The contrast check was passing text nobody could read
The layout sweep has checked WCAG AA contrast for a long time and the page passed it.
That result was worth less than it looked, for two separate reasons.
The first is the instrument. WCAG 2's contrast ratio is a luminance gap, and a luminance
gap overstates readability once one of the two colours is near black — which is every
reading taken on this page, whose ground is `#141414` and whose panels are darker than
its rows. This is not a quibble about the spec; it is the spec's own working group's
position, and it is why APCA exists. Tuning a colour until it clears 4.5:1 on a
near-black ground lands on text that passes the audit and still reads as a smudge across
a table in a dim room. Several rules in the stylesheet said so in their own comments —
one of them "measured 4.5:1 at `#a5a5a5` exactly, so it sits above that" — which is the
right instinct aimed through the wrong lens.
The second is that the check could not see half the backgrounds it was scoring against.
It reads `background-color` and walks up the ancestors, so a background painted by a
gradient is invisible to it; and where several ancestors are candidates it takes *the
best* contrast of any of them. Put together, text on the felt was being scored against
the dark page behind the felt rather than the felt. Two plain AA failures had been
sitting in a clean report for as long as the check has existed: the chair number on an
empty seat, on the table's brown rim, at 2.65:1, and the pot's own label, gold on green,
at 2.66:1.
`runContrastAudit.js` is the answer to both. It scores with APCA, and it gets the
background by screenshotting the page with every piece of text made transparent and
reading the pixels underneath — which cannot be fooled by a gradient, a shadow or a
border, because it is looking at what was painted. The first version of it *did* walk the
DOM, and reported the gold dealer button as black-on-black at Lc 0.4; an element anyone
can see is legible. That false positive is why the tool works the way it does.
What it found, on 92 distinct pieces of text: 65 below the level APCA sets for the size
and weight they are drawn at, **54 of them passing WCAG AA at the same moment**. The
worst was Lc 28. After this round nothing on the page is below Lc 60, nothing fails AA,
and the worst reading is Lc 60.4.
Three of the fixes are worth naming, because none of them is "make the grey lighter":
- **A player's stack was styled as secondary text.** Muted grey at Lc 44, under a name
set in `--text` at a heavier weight. Read back the other way that says a dealer needs
the name more than the number, and during a hand the opposite is true — the name is
looked up once to find the chair, the stack is read on every action because it decides
whether a player can cover the bet. Hierarchy now comes from size and weight, which is
free, instead of from fading, which on a dark ground is the most expensive way to say
"secondary".
- **An empty seat had no background of its own**, just a 2%-white wash at 0.7 opacity, so
whatever the felt was doing behind it decided how readable its number was: Lc 28 on the
rim, 30 on the dark green, 33 further in. Contrast that depends on where a seat happens
to sit cannot be reasoned about. It has a ground now. The wash is 0.62 and not the 0.82
it was first written at, because contrast turns out to be nearly flat across that range
— two points of Lc — and the heavier one put four dark rectangles on the felt like
holes.
- **The pot's label sat on the one green in the design.** `--gold` is tuned for near-black
and measures Lc 70-odd everywhere else it is used; on the pot disc it was Lc 38. The
disc is deeper now rather than the label bleached, because gold on dark green is the
pairing the whole page is built on. Deepening only the disc's outer stop moved it two
points and no further — the highlight is at 32%/28% and that is exactly where the label
sits, so the reading was being taken on the brightest part of the disc.
### What is deliberately not fixed
APCA asks Lc 90 of anything at 12px/400 or below, and this page is full of 10px and 11px
labels that live inside a 58px seat puck. Fifty-five pieces of text are still below that
bar. Reaching it means white micro-labels and a flatter, shoutier page; the honest way to
clear it is bigger type, and bigger type is a layout change that would relitigate the
seat-crowding work above. So the sweep reports two numbers — the strict per-size level,
and the Lc 60 / Lc 75 floors this page actually holds itself to — and only the floors are
treated as pass/fail.
## The failures that only happen at the table
Everything above this line is about a tool being easy to read and quick to press. This