Stand the felt up on a phone, and check which rule actually wins

A portrait phone got a square felt, so the vertical room the layout had
already measured was published and then dropped: 522px of room under a
366px table on an iPhone 14, 638 under 406 on a Pro Max. The cost was
seat names — 44px pucks with five of twelve clipped.

--feltAspect: 1.5 spends it. Twelve seats at 390x844 go from a 44px puck
with five names clipped to 66px with one; five seats go from three
clipped to none; the Pixel 7's twelve-seat ring goes to zero. 1.6 and
1.7 measure byte-identical, so 1.5 is where the sweep stops paying. The
three smaller phones are room-bound at any cap and measured identical
from 1.15 to 1.7.

The first version of this was a no-op. The rule went into
@media (max-width: 700px), and @media (max-width: 900px) below it set
the same property back to 1 — both match a phone, same specificity,
later one wins. It measured as 1 on every phone while reading perfectly.
The 900px rule is now a 701-900px range, and feltShape.test.js resolves
--feltAspect through the media queries the way the cascade does so the
next one of these is caught.

Also tried and removed: a floor under the max-height, so a tall bar
couldn't press the table flat. Twelve all in with nine side pots takes
the bar to 341px, leaving 142px of room at 360x640; floored to 242 the
felt keeps its shape by putting five of twelve seats behind the bar,
unfloored it is flat with all twelve visible. The slit it defended
against came from forcing the custom property by hand. A test asserts
its absence with the numbers.

Audit: 4 failing configurations and 5 offenders down to 2 and 3, both
remaining ones pre-existing. 776 unit 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-05 21:02:34 -06:00
co-authored by Claude Opus 5
parent 7bd205a91a
commit 13d704301d
3 changed files with 255 additions and 7 deletions
+29
View File
@@ -23,6 +23,7 @@ other in the page, in the load order from `TexasHoldem.cshtml`.
| `handRank.test.js` | What beats what: categories, the wheel, kickers, best five of seven |
| `readingTheShowdown.test.js` | The board, the picker that fills it, and who wins a side pot |
| `theRail.test.js` | A phone on its side: which shape the bar is in, what it leaves under the felt, how a name is shortened, how the ring is pulled back onto the table |
| `feltShape.test.js` | What shape the felt is at each phone size, resolved through the media queries in source order (see below) |
| `invariants.test.js` | 1000 randomised hands per property (see below) |
## The layout audit
@@ -155,6 +156,34 @@ something changed, and each time they only covered whatever had just been touche
That is how the standings table shipped with a player-name column zero pixels wide
and kept it for two rounds.
## Which rule wins
`feltShape.test.js` is here because of a change that measured as correct and was a
no-op. The felt on a phone was made an upright oval by setting `--feltAspect` inside
`@media (max-width: 700px)`, and forty lines below it `@media (max-width: 900px)` set
the same property back to 1. Both queries match a phone, the declarations have the same
specificity, and the later one wins — so the property was 1 on every phone on the site
while the rule that was supposed to change it sat there reading perfectly.
Nothing in the suite could have caught it and nothing in review did. A stylesheet is not
a list of rules for viewports; it is a list of rules in an order, and the question "what
does a 390px phone get?" has an answer that no single rule knows. So the test walks the
media queries the way the cascade does — every declaration of a property on a selector,
in source order, keeping the last one whose query matches — and asks what each of five
real phone sizes actually ends up with. It carries just enough of a media-query engine
for the forms this stylesheet uses, and refuses loudly rather than guessing when it
meets one it doesn't know.
The same file records a second thing, which is a floor that was added and removed. The
`max-height` that keeps the felt above the action bar has no lower bound, so a tall
enough bar presses the table flat; flooring it at the desktop oval looked obviously
right. Measured, it was worse: twelve players all in with nine side pots takes the bar
to 341px, which at 360x640 leaves 142px of room, and a felt floored to 242px keeps its
shape by putting five of the twelve seats behind the bar. Unfloored it is flat and all
twelve are visible. The slit the floor defended against turned out to be unreachable —
it came from forcing the custom property by hand. A test now asserts the floor is
*absent*, with the numbers, so it doesn't get re-added by someone reasoning the same way.
## Why the hand evaluator is tested the way it is
`handRank.js` is the only module here that can pay the wrong person without anybody