Mobile-feel round. Four measured problems, all of which only show up under a thumb. CSS only — stacked on #43.
1. Every tap waited on the browser — 53 of 53 controls
Not one interactive element set touch-action, so a mobile browser held every tap for ~300ms to see whether a second one was coming that would mean "zoom in". That's on the fold button, the call button, every seat, every card slot.
Worse: this tool uses double-tap on a seat as the fold shortcut — which is the browser's zoom gesture. The dealer's fastest shortcut was fighting the page zooming under their thumb. A single seat tap cost ~300ms of browser hesitation before the app's own 220ms double-tap window even started.
touch-action: manipulation keeps panning and pinch-zoom and drops only double-tap-to-zoom — precisely the one that was in the way.
2. Press feedback was the OS grey box — 1 :active rule in 2385 lines
Only .actionBtn had a pressed state. Everything else — seats, header buttons, card slots, picker cards, menu rows, toasts, wizard choices — leaned on the translucent grey rectangle the browser paints on tap.
So the tap highlight couldn't just be switched off; the controls needed real pressed states first. They now press down 1px and brighten in 0.04s, matching the action bar that already did. Then the grey box goes.
3. Hover stuck after every tap — 21 unguarded rules
:hover has no meaning on touch but still applies, and stays applied until you tap elsewhere. Tapping "Next hand" left it lifted and lightened as though still under a cursor — and it would have fought the new pressed states directly. All are behind @media (hover: hover) now. The one rule sharing a block with :focus-visible was split so keyboard focus survives.
4. The notes field zoomed the page (iOS)
#tableNotes was 13.3px. iOS zooms the viewport when a field under 16px takes focus, and the dealer is then pinching back out to see the table. Every other input was already 16px; this was the straggler.
Seats deliberately have no :active rule
Worth flagging because it nearly went in. A seat's transform is what positions it around the felt, so a pressed state setting transform drops the translate(-50%, -50%) and throws every pressed seat ~38px off its spot.
Validation caught it before it shipped: seats already dip via .seatPressing, which pointerdown adds in render.js — earlier than :active resolves, and it restates the translate correctly. The rule is gone and the reason is a comment in the stylesheet.
Validation (Playwright, 390x844)
Check
Result
touch-action: auto
53 → 0, across table, all 4 overlays, Quick Split
Tap-highlighted controls
0 everywhere
Text fields still selectable
12/12 table, 4/4 Quick Split
Buttons made unselectable
all
Hover rules gated
14/14, 0 unguarded (both sheets)
Fold press, measured live
translateY(1px) + brightness(1.3)
Seat press, measured live
keeps its translate; 2px drift from the shrink alone
Plus a hand played through the UI including a double-tap fold, no console errors, and unit tests still 58/58.
Mobile-feel round. Four measured problems, all of which only show up under a thumb. CSS only — stacked on #43.
## 1. Every tap waited on the browser — 53 of 53 controls
Not one interactive element set `touch-action`, so a mobile browser held **every tap for ~300ms** to see whether a second one was coming that would mean "zoom in". That's on the fold button, the call button, every seat, every card slot.
Worse: this tool uses **double-tap on a seat as the fold shortcut** — which *is* the browser's zoom gesture. The dealer's fastest shortcut was fighting the page zooming under their thumb. A single seat tap cost ~300ms of browser hesitation before the app's own 220ms double-tap window even started.
`touch-action: manipulation` keeps panning and pinch-zoom and drops only double-tap-to-zoom — precisely the one that was in the way.
## 2. Press feedback was the OS grey box — 1 `:active` rule in 2385 lines
Only `.actionBtn` had a pressed state. Everything else — seats, header buttons, card slots, picker cards, menu rows, toasts, wizard choices — leaned on the translucent grey rectangle the browser paints on tap.
So the tap highlight couldn't just be switched off; the controls needed real pressed states first. They now press down 1px and brighten in 0.04s, matching the action bar that already did. Then the grey box goes.
## 3. Hover stuck after every tap — 21 unguarded rules
`:hover` has no meaning on touch but still applies, and stays applied until you tap elsewhere. Tapping "Next hand" left it lifted and lightened as though still under a cursor — and it would have fought the new pressed states directly. All are behind `@media (hover: hover)` now. The one rule sharing a block with `:focus-visible` was split so keyboard focus survives.
## 4. The notes field zoomed the page (iOS)
`#tableNotes` was 13.3px. iOS zooms the viewport when a field under 16px takes focus, and the dealer is then pinching back out to see the table. Every other input was already 16px; this was the straggler.
## Seats deliberately have no `:active` rule
Worth flagging because it nearly went in. A seat's `transform` is what positions it around the felt, so a pressed state setting `transform` drops the `translate(-50%, -50%)` and throws every pressed seat **~38px off its spot**.
Validation caught it before it shipped: seats already dip via `.seatPressing`, which `pointerdown` adds in `render.js` — earlier than `:active` resolves, and it restates the translate correctly. The rule is gone and the reason is a comment in the stylesheet.
## Validation (Playwright, 390x844)
| Check | Result |
| --- | --- |
| `touch-action: auto` | 53 → **0**, across table, all 4 overlays, Quick Split |
| Tap-highlighted controls | **0** everywhere |
| Text fields still selectable | 12/12 table, 4/4 Quick Split |
| Buttons made unselectable | all |
| Hover rules gated | **14/14**, 0 unguarded (both sheets) |
| Fold press, measured live | `translateY(1px)` + `brightness(1.3)` |
| Seat press, measured live | keeps its translate; 2px drift from the shrink alone |
Plus a hand played through the UI including a double-tap fold, no console errors, and unit tests still 58/58.
Four measured problems, all of which only show up under a thumb.
Every tap waited on the browser (53 of 53 controls)
Not one interactive element set `touch-action`, so a mobile browser held every
tap for ~300ms to see whether a second one was coming that would mean "zoom in".
That is on the fold button, the call button, every seat, every card slot.
Worse, this tool uses double-tap on a seat as the fold shortcut — which *is* the
browser's zoom gesture. The dealer's fastest shortcut was fighting the page
zooming under their thumb. A single seat tap cost ~300ms of browser hesitation
before the app's own 220ms double-tap window even started.
`touch-action: manipulation` everywhere keeps panning and pinch-zoom and drops
only double-tap-to-zoom, which is precisely the one that was in the way.
Press feedback was the OS grey box (1 `:active` rule in 2385 lines)
Only `.actionBtn` had a pressed state. Everything else — seats, header buttons,
card slots, picker cards, menu rows, toasts, wizard choices — relied on the
translucent grey rectangle the browser paints on tap. So the tap highlight
couldn't just be switched off; the controls needed real pressed states first.
They press down 1px and brighten, in 0.04s, matching the action bar that already
did. Then the grey box goes.
Hover stuck after every tap (21 unguarded rules)
`:hover` has no meaning on touch, but it still applies — and stays applied until
you tap elsewhere. Tapping "Next hand" left it lifted and lightened as though
still under a cursor, and it would have fought the new pressed states directly.
All of them are behind `@media (hover: hover)` now. The one rule that shared a
block with `:focus-visible` was split so keyboard focus survives.
The notes field zoomed the page (iOS)
`#tableNotes` was 13.3px. iOS zooms the viewport when a field under 16px takes
focus, and the dealer is then pinching back out to see the table. Every other
input here was already 16px; this was the straggler.
Seats deliberately have no `:active` rule
Worth writing down because it nearly went in. A seat's `transform` is what
positions it around the felt, so a pressed state setting `transform` drops the
`translate(-50%, -50%)` and throws every pressed seat ~38px off its spot.
Validation caught it: seats already dip via `.seatPressing`, which `pointerdown`
adds in render.js — earlier than `:active` resolves, and it restates the
translate correctly.
Validated with Playwright at 390x844: 53/53 controls off `touch-action: auto` and
0 tap-highlighted across the table, all four overlays and Quick Split; text
fields still selectable and buttons not; 14/14 hover rules gated; presses
measured live via mouse-down (fold reads translateY(1px)/brightness(1.3), a seat
holds its translate with 2px of drift from the shrink alone). Hand played through
the UI including a double-tap fold. No console errors. Unit tests 58/58.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
jheaps
merged commit ee37cf7bd9 into master2026-09-01 09:29:54 -06:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Mobile-feel round. Four measured problems, all of which only show up under a thumb. CSS only — stacked on #43.
1. Every tap waited on the browser — 53 of 53 controls
Not one interactive element set
touch-action, so a mobile browser held every tap for ~300ms to see whether a second one was coming that would mean "zoom in". That's on the fold button, the call button, every seat, every card slot.Worse: this tool uses double-tap on a seat as the fold shortcut — which is the browser's zoom gesture. The dealer's fastest shortcut was fighting the page zooming under their thumb. A single seat tap cost ~300ms of browser hesitation before the app's own 220ms double-tap window even started.
touch-action: manipulationkeeps panning and pinch-zoom and drops only double-tap-to-zoom — precisely the one that was in the way.2. Press feedback was the OS grey box — 1
:activerule in 2385 linesOnly
.actionBtnhad a pressed state. Everything else — seats, header buttons, card slots, picker cards, menu rows, toasts, wizard choices — leaned on the translucent grey rectangle the browser paints on tap.So the tap highlight couldn't just be switched off; the controls needed real pressed states first. They now press down 1px and brighten in 0.04s, matching the action bar that already did. Then the grey box goes.
3. Hover stuck after every tap — 21 unguarded rules
:hoverhas no meaning on touch but still applies, and stays applied until you tap elsewhere. Tapping "Next hand" left it lifted and lightened as though still under a cursor — and it would have fought the new pressed states directly. All are behind@media (hover: hover)now. The one rule sharing a block with:focus-visiblewas split so keyboard focus survives.4. The notes field zoomed the page (iOS)
#tableNoteswas 13.3px. iOS zooms the viewport when a field under 16px takes focus, and the dealer is then pinching back out to see the table. Every other input was already 16px; this was the straggler.Seats deliberately have no
:activeruleWorth flagging because it nearly went in. A seat's
transformis what positions it around the felt, so a pressed state settingtransformdrops thetranslate(-50%, -50%)and throws every pressed seat ~38px off its spot.Validation caught it before it shipped: seats already dip via
.seatPressing, whichpointerdownadds inrender.js— earlier than:activeresolves, and it restates the translate correctly. The rule is gone and the reason is a comment in the stylesheet.Validation (Playwright, 390x844)
touch-action: autotranslateY(1px)+brightness(1.3)Plus a hand played through the UI including a double-tap fold, no console errors, and unit tests still 58/58.