Compare commits

...
Author SHA1 Message Date
jheapsandClaude Opus 5 d297c1a2c9 Close two blind spots in the contrast check
Last change found a folded player's stack sitting at 2.22:1, hidden for twenty rounds
behind an exemption. So I went looking for the other exemptions rather than waiting for
the next one to surface. There were two, and neither is hiding a live fault today --
which is worth saying plainly, because it means this is a fix to the instrument and not
to the tool.

Leaf elements only

    if (el.children.length > 0) return;   // leaf text only

An element's own text stopped being measured the moment a child was added to it. The
undo control went that way two changes ago, the day a <kbd> was appended to it: its
label became invisible to this check with no sign that anything had changed. Now it
measures an element's own text nodes and leaves the descendants to their own turn,
against their own colours.

aria-hidden as a contrast exemption

The bets layer is aria-hidden because the amounts on the chips are already announced by
the seat and the action bar, and repeating them would be noise. That is a good reason
to hide them from a screen reader and no reason at all to stop checking whether a
sighted dealer can read them -- and the chips are exactly what the felt is read for.
Two different questions were being answered by one attribute.

Nothing is exempt now. If something genuinely decorative turns up, it should say so in
its own words rather than borrowing an attribute that means something else. The
watermark that the exemption was written for is gone as of the change before last.

What the widened check finds

Nothing. Zero offenders at six viewports across touch and pointer contexts, and with
the sidebar panels and the rulings sheet open. Measured from rendered pixels, the two
things that had been invisible are comfortable: the bet chip amounts read 12.65 to
13.74:1 and the undo label 8.06:1.

So the fix is proved by injection instead. Colouring the bet chip amounts #3a3a3a now
reports three offenders at 1.80:1, and colouring the undo control #2f2f2f reports it at
1.13:1. Neither was reported before.

269 tests still passing, no console errors.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
2026-09-01 19:21:06 -06:00
jheapsandClaude Opus 5 f0cfd24647 Make the seats that are out of the hand readable again
A folded player's stack measured 2.22:1.

The contrast audit has been reporting clean for twenty rounds, and it never looked at
these seats: the check skipped anything with opacity below 0.99 as "faded on purpose",
and folded and sat-out seats faded their whole box -- 0.5 and 0.58. Opacity on a box
fades the text you still have to read along with the box you wanted to push back.

Measured off rendered pixels, 11.5px text, where AA asks 4.5:1:

                  name    stack
  live            13.28   4.75
  folded           4.75   2.22
  sitting out      7.00   3.22

A folded player's stack is not decoration. It is what they will have next hand and the
answer to "how much has Ben got?", and at 2.22 it was barely there.

So the box recedes and the text does not: a flatter, darker background and a muted
border, with name and stack colours that measure.

                  name    stack
  live            13.28   4.75   (unchanged)
  folded           7.60   4.55
  sitting out      7.60   4.71

Taking the fade away took the signal with it, which the screenshot showed and the
numbers did not: a sat-out seat became almost indistinguishable from a live one. A
folded seat still says so twice, with a red border and a line through the name, but
sitting out had only faintness. It wears a dashed edge now -- the same thing an empty
chair wears, which is right, because neither is in this hand, and the name and stack
tell the two apart.

The same fault, found once the audit stopped hiding it

The key hints on the action bar are 9.6px and were measuring 2.97 to 3.33 against
their buttons -- a fade chosen for looks pushing text a dealer is meant to read under
the threshold, exactly like the seats. They are 4.93 to 7.63 now, still quieter than
the labels they sit beside.

And the audit no longer passes over faded text in silence. Something invisible is still
exempt, since there is nothing to read at zero opacity, but anything in between is
reported as unmeasured -- a number nobody checked is not the same as a number that is
fine. It now lists five: the four key caps and the disabled Export CSV button, which is
disabled and therefore genuinely exempt.

269 tests still passing. Contrast offenders zero, layout audit clean, no console
errors; the folded seat's red line-through and the sat-out seat's dashed edge both
confirmed in cropped screenshots rather than assumed.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
2026-09-01 19:10:06 -06:00
2 changed files with 98 additions and 12 deletions
+49 -4
View File
@@ -510,13 +510,53 @@ button,
/* Sitting out, not gone: still a person the dealer may need to deal back in, so
the name has to stay readable rather than fading almost to the felt. */
/* Out of the hand, still readable.
Both of these used to fade the whole seat — opacity 0.5 folded, 0.58 sitting out —
and opacity on a box fades the text you still have to read along with the box you
wanted to push back. Measured off the rendered pixels at 11.5px text, where AA asks
for 4.5:1:
live name 13.28 stack 4.75
folded name 4.75 stack 2.22
sat out name 7.00 stack 3.22
A folded player's stack is not decoration. It's what they'll have next hand and the
answer to "how much has Ben got?", and at 2.22:1 it was barely there.
So the box recedes and the text doesn't: a flatter, darker background and a muted
border say "not in this hand", while the name and the stack keep colours that
measure. The folded seat keeps its red border and the line through the name, which
is the part that actually names the state. */
.seat.seatInactive,
.seat.seatFolded {
background: linear-gradient(160deg, #232323, #171717);
border-color: rgba(255, 255, 255, 0.06);
}
/* A folded seat already says so twice — a red border and a line through the name — but
a seat that's sitting out said it only by being faint, and taking the faintness away
took the whole signal with it. A dashed edge is the same thing an empty chair wears,
which is right: neither is in this hand. The name and the stack tell them apart. */
.seat.seatInactive {
opacity: 0.58;
border-style: dashed;
border-color: rgba(255, 255, 255, 0.22);
}
.seat.seatInactive .seatPuckName,
.seat.seatFolded .seatPuckName {
color: #b0b0b0;
}
.seat.seatInactive .seatPuckStack,
.seat.seatFolded .seatPuckStack,
.seat.seatInactive .seatPuckTotal,
.seat.seatFolded .seatPuckTotal {
color: #8f8f8f;
}
.seat.seatFolded {
border-color: rgba(224, 123, 123, 0.4);
opacity: 0.5;
}
.seat.seatFolded .seatPuckName {
@@ -2169,11 +2209,12 @@ body.hasActionBar #holdemSidebar {
/* The undo pill's key, inline rather than pinned to a corner — see actionBar.js. */
.actionUndoKey {
/* See .actionKey below for why these are not fainter. */
margin-left: 0.4rem;
padding: 0 0.25rem;
border-radius: 4px;
border: 1px solid currentColor;
opacity: 0.55;
opacity: 0.75;
font-family: inherit;
font-size: 0.6rem;
line-height: 1.4;
@@ -2181,6 +2222,10 @@ body.hasActionBar #holdemSidebar {
pointer-events: none;
}
/* A key hint is small — 9.6px — so AA asks 4.5:1 of it, and at the opacity these used
to carry they measured 2.97 to 3.33 against their buttons. That's the same fault as
the folded seats above: a fade chosen for looks pushing text a dealer is meant to
read under the threshold. They're still quieter than the label they sit beside. */
.actionKey {
position: absolute;
top: 5px;
@@ -2188,7 +2233,7 @@ body.hasActionBar #holdemSidebar {
padding: 0 0.25rem;
border-radius: 4px;
border: 1px solid currentColor;
opacity: 0.4;
opacity: 0.75;
font-family: inherit;
font-size: 0.6rem;
line-height: 1.4;
+49 -8
View File
@@ -349,6 +349,16 @@ const LayoutAudit = {
});
},
// Every element between this one and the document, so an opacity set on a
// container can be seen from the text inside it.
*ancestors(el) {
let node = el.parentElement;
while (node) {
yield node;
node = node.parentElement;
}
},
contrast({ minimum = 4.5, largeMinimum = 3, selector = null } = {}) {
const offenders = [];
const unmeasured = [];
@@ -358,19 +368,50 @@ const LayoutAudit = {
: [...document.querySelectorAll("*")];
nodes.forEach(el => {
if (el.children.length > 0) return; // leaf text only
const text = (el.textContent || "").trim();
// An element's *own* text, not its descendants' — they get measured on
// their own turn, against their own colour.
//
// This used to be "leaf elements only", which quietly stopped measuring
// anything the moment a child was added to it. The undo control went that
// way the day a <kbd> was appended to it: its label became invisible to
// this check, with no sign that anything had changed.
const text = [...el.childNodes]
.filter(node => node.nodeType === 3)
.map(node => node.textContent)
.join(" ")
.trim();
if (!text) return;
const rect = el.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) return;
const style = getComputedStyle(el);
if (style.visibility === "hidden" || style.display === "none") return;
if (parseFloat(style.opacity) < 0.99) return; // faded on purpose
// Decoration is exempt by definition: the watermark on the felt is meant
// to be barely there, and holding it to a reading threshold would mean
// making it not a watermark. Marked aria-hidden so screen readers skip it
// too, which is the same claim expressed where it also does some good.
if (el.closest("[aria-hidden='true']")) return;
// Faded text used to be skipped outright as "faded on purpose", and that
// exemption hid a real fault: the folded and sat-out seats dimmed their
// whole box with opacity, so a folded player's stack measured 2.22:1 at
// 11.5px and this check never looked at it. Something invisible is still
// exempt — there's nothing to read at zero — but anything in between is
// reported as unmeasured rather than passed over in silence, because a
// number nobody checked is not the same as a number that's fine.
const opacity = parseFloat(style.opacity);
if (opacity <= 0.02) return;
const faded = opacity < 0.99 ||
[...this.ancestors(el)].some(a => parseFloat(getComputedStyle(a).opacity) < 0.99);
if (faded) {
unmeasured.push(`${this.describe(el)} :: faded (opacity ${opacity}), contrast not checked`);
return;
}
// aria-hidden used to exempt text here, on the grounds that decoration
// marked for screen readers was decoration for everyone. Those are two
// different questions and the answer differs: the bet chips on the felt are
// aria-hidden because their amounts are already announced by the seat and
// the action bar, and repeating them would be noise — but they are exactly
// what a sighted dealer reads the felt for, and this check had stopped
// looking at them. Hiding something from one audience is not a claim about
// whether the other audience can read it.
//
// Nothing is exempt now. If something genuinely decorative turns up, it
// should say so in its own words rather than borrowing an attribute that
// means something else.
const fgRaw = this.parseColour(style.color);
if (!fgRaw) return;