Files
JoshHeaps.Net/JoshHeaps.Net/wwwroot/css/casino/holdem.css
T
jheapsandClaude Opus 5 8a2fc3e540 Give undo and the card picker's buttons a fingertip to land on
The first thing the repaired audit found, run on a real touch context at 44px: three
controls a dealer taps on a phone that are too small to tap.

    .actionUndo             145x30   the "I recorded that wrong" pill
    #cardPickerClearBtn      50x40   wipes a player's cards
    #cardPickerCloseBtn      50x40   closes the picker

Undo is the worst of them. It is the control a dealer reaches for having just entered
the wrong action with five people waiting, and it was fourteen pixels short of a
fingertip. The picker's two are short because the touch rule that fixes every other
small button is scoped to .panelBody, and the picker is a sheet — it never reached
them. One of them is destructive.

All three now sit at 44px on touch. Nothing changes on a pointer device.

I tried an invisible hit-area pad on undo first, to avoid spending bar height, and the
measurement said it did nothing: the pill clips its overflow to keep a long label on
one line, and giving it a positioning context made it clip the pad too. Growing the
box is the honest fix, and it turned out to cost nothing anyway — the bar goes from
138px to 152px and the felt stays at 366px, because the felt had slack against its
220px floor rather than sitting on it as I had assumed.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
2026-09-02 00:55:56 -06:00

3580 lines
92 KiB
CSS

body {
background: radial-gradient(ellipse at 50% -10%, #1c1c1c 0%, #101010 55%, #0a0a0a 100%);
min-height: 100vh;
margin: 0;
color: #e6e6e6;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
/* ---------- Touch ----------
A dealer runs this one-handed on a phone with players waiting, so a tap has to
register the instant their thumb lands.
`touch-action: manipulation` is the whole game here. Without it a mobile browser
holds every tap for ~300ms to see whether a second one is coming that would mean
"zoom in" — on top of which double-tapping a seat, which this tool uses for fold,
*is* the browser's zoom gesture, so the dealer's shortcut fights the page zooming
under their thumb. `manipulation` keeps panning and pinch-zoom and drops only the
double-tap-to-zoom, which is exactly the one we don't want.
The tap highlight goes with it: every control below gets a real pressed state, and
the browser's translucent grey rectangle on top of a designed one just looks like
a rendering artefact. Text selection goes too — a fast tap-and-hold on a seat
should open the seat menu, not raise a text-selection magnifier over the felt. */
.holdemBtn,
.seat,
.dealerChip,
.cardSlot,
.linkBtn,
.seatMenuBtn,
.seatSheetClose,
.pickerCard,
.actionBtn,
.raiseQuickBtn,
.actionUndo,
.toastAction,
.toastClose,
.potWizardChoice,
.subPanel summary,
button,
[role="button"],
input,
select,
textarea,
a {
touch-action: manipulation;
}
/* The grey rectangle goes from every control, fields included — a tapped input
already shows a caret and a focus ring, which say the same thing more quietly. */
.holdemBtn,
.seat,
.dealerChip,
.cardSlot,
.linkBtn,
.seatMenuBtn,
.seatSheetClose,
.pickerCard,
.actionBtn,
.raiseQuickBtn,
.actionUndo,
.toastAction,
.toastClose,
.potWizardChoice,
.subPanel summary,
button,
[role="button"],
input,
select,
textarea,
a {
-webkit-tap-highlight-color: transparent;
}
/* Text selection is switched off only on the things you press, never on the fields
you type into — a dealer editing a player's name still needs to select the text. */
.holdemBtn,
.seat,
.dealerChip,
.cardSlot,
.linkBtn,
.seatMenuBtn,
.seatSheetClose,
.pickerCard,
.actionBtn,
.raiseQuickBtn,
.actionUndo,
.toastAction,
.toastClose,
.potWizardChoice,
.subPanel summary,
button,
[role="button"] {
-webkit-user-select: none;
user-select: none;
}
/* ---------- Pressed states ----------
Touch has no hover, so the press is the only feedback a dealer gets that the tap
landed — and with the grey highlight suppressed above, these have to carry it.
Everything presses the same way it would on a real table: down and slightly
darker, no travel time. */
.holdemBtn:active:not(:disabled),
.linkBtn:active:not(:disabled),
.seatMenuBtn:active:not(:disabled),
.seatSheetClose:active:not(:disabled),
.toastAction:active,
.toastClose:active,
.potWizardChoice:active,
.raiseQuickBtn:active:not(:disabled),
.actionUndo:active:not(:disabled) {
transform: translateY(1px);
filter: brightness(1.25);
transition-duration: 0.04s;
}
.cardSlot:active,
.pickerCard:active:not(:disabled),
.dealerChip:active {
transform: translateY(1px) scale(0.97);
transition-duration: 0.04s;
}
/* The action bar already presses down; the brightness is what it was missing, and
it's the one place a dealer taps without looking. */
.actionBtn:active:not(:disabled) {
filter: brightness(1.3);
transition-duration: 0.04s;
}
/* Seats deliberately have no `:active` rule. They already dip via `.seatPressing`,
which `pointerdown` adds in render.js — that fires earlier than `:active` resolves,
and, more importantly, a seat's `transform` is what positions it around the felt
(`translate(-50%, -50%)` plus its own left/top). A press state setting `transform`
would drop that translate and throw every pressed seat about 38px off its spot. */
/* Nobody should get a stuck press state from a tap that turned into a scroll. */
@media (prefers-reduced-motion: reduce) {
.holdemBtn:active:not(:disabled),
.linkBtn:active:not(:disabled),
.seatMenuBtn:active:not(:disabled),
.seatSheetClose:active:not(:disabled),
.cardSlot:active,
.pickerCard:active:not(:disabled),
.dealerChip:active,
.raiseQuickBtn:active:not(:disabled),
.actionUndo:active:not(:disabled) {
transform: none;
}
}
#holdemApp {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
/* ---------- Header ---------- */
#holdemHeader {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
}
#holdemHeader h1 {
margin: 0;
font-size: 1.8rem;
font-weight: 300;
letter-spacing: 0.04em;
color: #f5f5f5;
position: relative;
display: inline-block;
padding-bottom: 0.4rem;
}
#holdemHeader h1::after {
content: "";
position: absolute;
left: 0;
bottom: 0;
width: 42px;
height: 3px;
border-radius: 2px;
background: linear-gradient(90deg, #d8b46a, #a9782f);
}
.holdemSubtitle {
margin: 0.25rem 0 0;
color: #999;
font-size: 0.9rem;
}
.handCounterBadge {
display: inline-block;
margin-top: 0.5rem;
padding: 0.2rem 0.65rem;
border-radius: 999px;
background: rgba(216, 180, 106, 0.12);
border: 1px solid rgba(216, 180, 106, 0.4);
color: #d8b46a;
font-size: 0.75rem;
letter-spacing: 0.04em;
}
#holdemHeaderActions {
display: flex;
gap: 0.6rem;
flex-wrap: wrap;
}
.holdemBtn {
/* Stated, because this class is worn by both <button> and <a> and the browser
does not give them the same box. Chromium's own stylesheet makes a button
border-box and leaves a link content-box, so `min-height: 46px` on the header
row came out as 46px on the buttons and 64px on the two links — and the row
stretches to its tallest item, so every button became 64px too. A 46px touch
target was quietly costing 64px of a phone screen, twice over, because the
header wraps to two rows. */
box-sizing: border-box;
/* And the touch floor is stated here rather than only under a width breakpoint.
It used to arrive by accident — the content-box links were 64px tall and the
row stretched everything to match — so once the boxes agree, a landscape phone
(wider than the phone breakpoint, still a thumb) would have been left with 42px
buttons. 44px is the floor everywhere; the narrow layout still asks for 46. */
min-height: 44px;
background: linear-gradient(180deg, #2e2e2e, #232323);
color: #eee;
border: 1px solid #3a3a3a;
border-radius: 7px;
padding: 0.6rem 1rem;
font-size: 0.85rem;
cursor: pointer;
/* Named rather than `all`, so the focus ring isn't animated. With `all` the
outline faded in over 150ms, which on keyboard navigation reads as the ring
lagging a step behind the Tab key. Nothing else here changes. */
transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease,
box-shadow 0.15s ease, color 0.15s ease, opacity 0.15s ease, filter 0.15s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 0.3rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
@media (hover: hover) {
.holdemBtn:hover {
background: linear-gradient(180deg, #383838, #2a2a2a);
border-color: #4a4a4a;
transform: translateY(-1px);
}
}
.holdemBtnAccent {
background: linear-gradient(180deg, #16783a, #0d5225);
border-color: #1e8449;
color: #ecffef;
}
@media (hover: hover) {
.holdemBtnAccent:hover {
background: linear-gradient(180deg, #1a8a44, #0f602c);
}
}
.holdemBtnDanger {
background: linear-gradient(180deg, #4a1f1f, #351515);
border-color: #6b2626;
color: #ffd6d6;
}
@media (hover: hover) {
.holdemBtnDanger:hover {
background: linear-gradient(180deg, #572525, #401818);
}
}
.holdemBtnGhost {
background: transparent;
}
.holdemBtnSmall {
padding: 0.3rem 0.6rem;
font-size: 0.75rem;
}
.holdemBtn:disabled {
opacity: 0.4;
cursor: not-allowed;
transform: none;
}
.handHistoryActions {
display: flex;
gap: 0.5rem;
margin-top: 0.5rem;
}
/* ---------- Layout ---------- */
#holdemLayout {
display: grid;
grid-template-columns: 1fr 320px;
gap: 1.5rem;
align-items: start;
}
#tableSection {
min-width: 0;
}
.tableHint {
color: #949494;
font-size: 0.8rem;
text-align: center;
margin-top: 1rem;
}
/* ---------- Poker table ---------- */
#pokerTableWrap {
position: relative;
/* The shape is stated once, and both the height and the cap below are derived from
it. It used to be a `padding-bottom: 72%` box, which has a trap in it: percentage
padding resolves against the *containing block's* width, not the element's own,
so narrowing the wrap left it exactly as tall as before. Capping the width was
therefore no way to shorten the table — measured, after trying it: the felt went
822px wide to 746px and stayed 617px tall, and the bottom seat sat as far into
the action bar as it had. aspect-ratio ties the two together properly. */
--feltAspect: 0.72;
width: 100%;
aspect-ratio: 1 / var(--feltAspect);
margin: 0 auto;
/* Capping the width is how the felt is told to stay above the action bar.
--feltRoomPx is published by actionBar.js with the bar's real height and the
bottom seat's overhang already taken out; the fallback leaves the table its full
size if that ever fails to arrive. */
max-width: calc(var(--feltRoomPx, 100vh) / var(--feltAspect));
}
#pokerTable {
position: absolute;
inset: 6%;
border-radius: 50%;
background: radial-gradient(ellipse at 50% 42%, #227a45 0%, #175f37 45%, #0e4527 75%, #082c19 100%);
border: 14px solid #6b4225;
box-shadow:
inset 0 0 0 3px rgba(216, 180, 106, 0.55),
inset 0 0 80px rgba(0, 0, 0, 0.55),
0 0 0 4px #2c1a0d,
0 20px 50px rgba(0, 0, 0, 0.55);
display: flex;
align-items: center;
justify-content: center;
}
/* The felt used to carry a "TEXAS HOLD'EM" wordmark across its middle. It came out
from behind the pot badge in two pieces — measured at every seat count from two to
ten, the middle 20% of it was covered, so the felt read "TEXAS LD'EM" — and no
placement fixed that everywhere. Thirty per cent down cleared the badge on a desktop
and landed on the seats of a phone's square felt (43-70% covered); a smaller mark at
20% or 78% was clean on desktop and tablet and no better on a phone; the bottom one
fell off the oval entirely at 360px. A square felt with six to nine seats has no
clear band wide enough for a word.
So it's gone. A watermark that looks broken is worse than no watermark, and the felt
has a gradient, a rim, an inner gold line and a shadow to be a table with — plus a
header saying "Texas Hold'em Dealer" a few inches above it. */
#potBadge {
position: relative;
background: radial-gradient(circle at 32% 28%, rgba(255, 255, 255, 0.08), rgba(0, 0, 0, 0.55));
border: 1px solid rgba(255, 255, 255, 0.14);
border-radius: 999px;
padding: 0.7rem 1.7rem;
text-align: center;
display: flex;
flex-direction: column;
gap: 0.15rem;
box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.6), 0 4px 14px rgba(0, 0, 0, 0.45);
}
#potBadgeLabel {
font-size: 0.68rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: #d8b46a;
}
#potBadgeAmount {
font-size: 1.5rem;
font-weight: 700;
color: #fff;
}
#seatsLayer {
position: absolute;
inset: 0;
}
/* ---------- Bets on the felt ---------- */
/* Shares the seat ring's box exactly, so a bet always lands on the spoke between its
player and the pot. When the seat ring gets inset on a phone, this moves with it —
measured the alternative, and a bets ring on its own geometry ends up either behind
the pucks or on top of the pot badge. */
#betsLayer {
position: absolute;
inset: 0;
pointer-events: none;
}
.betChip {
position: absolute;
transform: translate(-50%, -50%);
display: flex;
align-items: center;
gap: 0.28rem;
padding: 0.1rem 0.42rem 0.1rem 0.2rem;
background: rgba(0, 0, 0, 0.55);
border: 1px solid rgba(255, 255, 255, 0.14);
border-radius: 999px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5);
white-space: nowrap;
}
/* Reads as a chip seen from above: a lighter top-left, a rim, and the notches a
casino chip has around its edge. */
.betChipDisc {
width: 13px;
height: 13px;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.45);
box-shadow:
inset 0 1px 1px rgba(255, 255, 255, 0.45),
inset 0 -1px 2px rgba(0, 0, 0, 0.45);
}
.betChipAmount {
font-size: 0.62rem;
font-weight: 700;
color: #f0f0f0;
font-variant-numeric: tabular-nums;
}
/* Clinging to a puck rather than sitting on its own ring — the phone layout. */
.betChipAnchored {
left: 50%;
z-index: 3;
}
.betChipAbove {
top: -11px;
transform: translate(-50%, -50%);
}
.betChipBelow {
top: auto;
bottom: -11px;
transform: translate(-50%, 50%);
}
/* ---------- Seats (compact pucks on the table) ---------- */
.seat {
position: absolute;
transform: translate(-50%, -50%);
width: 96px;
min-height: 54px;
background: linear-gradient(160deg, #2c2c2c, #1b1b1b);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 14px;
padding: 0.3rem 0.4rem;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.05);
cursor: grab;
z-index: 1;
text-align: center;
transition: box-shadow 0.15s ease, border-color 0.15s ease, transform 0.1s ease;
/* A long press on a phone otherwise selects the player's name or raises the
system callout menu instead of ours. */
user-select: none;
-webkit-user-select: none;
-webkit-touch-callout: none;
}
.seat.dragging {
opacity: 0.4;
}
.seat.dropTarget {
outline: 2px dashed #d8b46a;
outline-offset: 2px;
}
/* Four states a dealer needs to tell apart from across the table, without
reading anything: live in this hand, folded, sitting out, and nobody there.
Live gets a green edge, folded gets a red one and a struck-through name,
sitting-out just fades, and an empty chair is the only dashed outline. */
.seat.seatLive {
border-color: rgba(78, 201, 122, 0.45);
box-shadow:
0 0 0 1px rgba(78, 201, 122, 0.18),
0 3px 10px rgba(0, 0, 0, 0.5),
inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
/* 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 {
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);
}
.seat.seatFolded .seatPuckName {
text-decoration: line-through;
text-decoration-color: rgba(224, 123, 123, 0.75);
}
.seat.seatEmpty {
background: rgba(255, 255, 255, 0.02);
border-style: dashed;
border-color: rgba(255, 255, 255, 0.13);
box-shadow: none;
/* Deliberately louder than seatInactive: an open seat is information a
dealer wants, not something to squint at. */
opacity: 0.7;
}
.seatEmptyLabel {
color: #8a8a8a;
font-size: 0.6rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.seat.seatSelected {
border-color: rgba(9, 255, 0, 0.6);
box-shadow: 0 0 0 2px rgba(9, 255, 0, 0.4), 0 4px 14px rgba(0, 0, 0, 0.5);
}
.seatChairNumber {
position: absolute;
top: 2px;
left: 6px;
font-size: 0.58rem;
color: #8a8a8a;
}
.seatPuckName {
font-size: 0.72rem;
font-weight: 600;
color: #f0f0f0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-top: 0.6rem;
}
.seatPuckCards {
font-size: 0.68rem;
font-weight: 700;
font-family: var(--font-mono, monospace);
color: #ddd;
min-height: 0.95em;
letter-spacing: 0.05em;
}
.seatPuckCards .cardRed {
color: #ff6b6b;
}
.seatPuckTotal {
display: inline-block;
margin-top: 0.15rem;
font-size: 0.64rem;
font-weight: 700;
color: #a9f5b8;
background: rgba(9, 255, 0, 0.1);
border-radius: 999px;
padding: 0 0.35rem;
min-height: 1.1em;
line-height: 1.4;
}
.dealerChip {
position: absolute;
/* Inside its own seat, not on the rim.
It used to sit at -10px/-10px, hanging over the seat's corner — which on an oval
of overlapping seats means hanging over the seat *next to it*. Measured on a
nine-seat table: tapping the centre of seat 1 gave the button to seat 9. That
was always slightly true and the enlarged hit area made it much worse; three of
nine seats were losing taps, one of them losing its own middle.
Tucked inside, the overlap is zero at every size. It reads as a puck in the
corner of the seat rather than on its edge, which is a small price for tapping
a player and getting that player. */
top: 3px;
right: 3px;
width: 22px;
height: 22px;
border-radius: 50%;
background: radial-gradient(circle at 35% 30%, #fff, #d8d8d8 55%, #b8b8b8 100%);
color: #222;
border: 2px solid #999;
font-weight: 700;
font-size: 0.68rem;
cursor: pointer;
z-index: 2;
transition: opacity 0.15s ease;
/* A dealer puck should look like a puck — small, sitting on the rim of its seat.
But tapping it hands the button to that seat and re-posts the blinds, and 22px
is half the 44px a fingertip needs. The hit area grows without the chip doing
so: the visual stays 22px and an invisible pad around it catches the tap.
Deliberately not centred on the chip — it's pushed down and left, away from the
neighbouring seat the chip juts towards, so the bigger target can't start
stealing taps meant for the seat next door. */
isolation: isolate;
}
.dealerChip::after {
content: "";
position: absolute;
/* Grows inward, over its own seat, not outward onto the neighbours.
The chip already sits outside its seat by design — top:-10px, right:-10px — so
a pad that also reaches up and right lands on whatever seat is next around the
oval. It did: three of nine seats had taps taken by a neighbour's chip, seat 1
losing 10 of 49 sampled points including its own centre, which meant tapping
the middle of seat 1 gave the button to seat 9.
Down and left is its own seat, which is a large target that can spare the
corner. The two pixels of outward reach are just to cover the chip's border. */
top: -3px;
right: -3px;
bottom: -18px;
left: -18px;
}
/* A poker table has one dealer button. This one drew nine.
Every seat carried a puck — the button's, and eight ghosts at 0.32 opacity that
were there to be pressed. Measured on a nine-seat table: eight pale discs, all
eight showing the letter D, one of them real. Finding the button meant comparing
nine identical shapes for the one that's gold, when it should be the only thing
of its kind on the felt. The ghosts also sat on top of their neighbours: every
remaining "something important is covered" offender on a crowded table was a
seat covered by the seat-next-door's ghost puck.
So a seat that doesn't have the button doesn't draw one. The affordance is still
there — hovering a seat fades its slot in, which is a preview of where the button
would go — and on touch, where there is no hover, "Give the button" has always
been in the seat's long-press menu. Keyboard reaches it by focus.
pointer-events matters as much as the opacity: an invisible puck that still takes
taps would be worse than a visible one. */
.dealerChip:not(.active) {
opacity: 0;
pointer-events: none;
}
@media (hover: hover) {
.seat:hover .dealerChip:not(.active) {
opacity: 0.5;
pointer-events: auto;
}
}
.dealerChip:not(.active):focus-visible {
opacity: 1;
pointer-events: auto;
}
.dealerChip.active {
background: radial-gradient(circle at 35% 30%, #ffe9a8, #f5d547 55%, #cba428 100%);
border-color: #a9782f;
color: #3a2e00;
opacity: 1;
}
.dealerChip.hidden {
display: none;
}
/* The dead button. It's on an empty chair on purpose, and that reads as a mistake —
a gold puck on nobody looks exactly like a button that has drifted and wants
dragging back. So it keeps the button's gold, to stay findable as the button, but
goes hollow and dashed: the same shape, visibly holding a place rather than
marking a player. The deal prompt says why in words; this is so the felt agrees
with it at a glance instead of contradicting it. */
.dealerChip.active.dealerChipDead {
background: rgba(245, 213, 71, 0.14);
border-style: dashed;
color: #f0d98c;
}
.blindBadge {
display: none;
position: absolute;
top: -9px;
font-size: 0.58rem;
font-weight: 700;
padding: 0.05rem 0.34rem;
border-radius: 999px;
z-index: 2;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
}
.blindBadge.visible {
display: inline-block;
}
/* Seats on the bottom of the ring wear their badge underneath, because that's the
edge the pot isn't on. Bets sit between a seat and the pot, so on the bottom arc
the chip comes down onto the seat's top edge — which is exactly where this badge
was, and the chip is a layer above it. See render.js for the measurement. */
.seat.seatBelowCentre .blindBadge {
top: auto;
bottom: -9px;
}
/* And the badge outranks the chips regardless, so a layout this doesn't anticipate
can't hide it again. Which blind a seat is posting is the one thing on that puck
nothing else on the felt repeats — a bet chip's amount is on the seat's own total
line as well. Measured across 12 table sizes: no chip loses its amount to this. */
.blindBadge {
z-index: 4;
}
/* Both badges failed WCAG AA against their own gradients — measured at the worst
stop, the small blind was 3.52:1 and the big blind 2.15:1, in 9px bold text a
dealer is meant to read at a glance across a table.
The small blind keeps its blue and just darkens (4.77:1 at its worst stop). The
big blind flips to dark text on brighter gold (5.84:1), which is how a gold chip
is marked anyway and has the side benefit of making the two badges far easier to
tell apart at speed — light-on-blue against dark-on-gold, rather than two pale
labels differing only in hue. */
.sbBadge {
left: -6px;
background: linear-gradient(180deg, #2f6cb5, #22508c);
color: #eaf3ff;
}
.bbBadge {
left: 22px;
background: linear-gradient(180deg, #e6ad4d, #c98a2a);
color: #241a05;
}
/* A blind the dealer named by hand rather than one the button handed out. Without
the ring the table just looks like it's put the blinds in the wrong place. */
.blindBadge.blindManual {
outline: 1.5px solid #f2f2f2;
outline-offset: 1px;
}
/* ---------- Seat editor panel (sidebar form for the selected seat) ---------- */
.seatEditorPanel {
padding: 0.9rem;
}
.seatEditorHeader {
display: flex;
align-items: center;
gap: 0.6rem;
}
.seatEditorHeader h2 {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Grip and Done button are the sheet's furniture — no reason for either on a
desktop sidebar, where the panel is just a panel. */
.seatSheetGrip,
.seatSheetClose {
display: none;
}
.seatEditorPanel h2 {
margin: 0 0 0.6rem;
padding-bottom: 0.5rem;
font-size: 0.95rem;
font-weight: 500;
color: #f5f5f5;
border-bottom: 1px solid rgba(216, 180, 106, 0.35);
}
.seatEditorForm {
display: flex;
flex-direction: column;
gap: 0.6rem;
}
.seatCardToggles {
display: flex;
gap: 0.6rem;
font-size: 0.8rem;
color: #aaa;
}
.inHandToggle,
.foldToggle {
display: flex;
align-items: center;
gap: 0.3rem;
cursor: pointer;
}
.seatNameInput {
flex: 1;
min-width: 0;
background: #1a1a1a;
border: 1px solid #3a3a3a;
border-radius: 5px;
color: #eee;
padding: 0.3rem 0.45rem;
font-size: 0.8rem;
}
.seatCards {
display: flex;
gap: 0.35rem;
margin-bottom: 0.4rem;
}
/* The two hole-card slots: they look like cards face-up on the felt, and tapping
one opens the picker rather than a keyboard. */
.cardSlot {
width: 100%;
min-height: 52px;
text-align: center;
background: #fdfdfd;
color: #111;
border: 1px solid #3a3a3a;
border-radius: 6px;
padding: 0.35rem 0;
font-size: 1.25rem;
font-weight: 700;
font-family: var(--font-mono, monospace);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
cursor: pointer;
transition: transform 0.12s ease, box-shadow 0.12s ease;
}
@media (hover: hover) {
.cardSlot:hover {
transform: translateY(-1px);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.4);
}
}
.cardSlot.cardRed {
color: #c0392b;
}
.cardSlot.cardSlotEmpty {
background: #191919;
color: #8a8a8a;
border-style: dashed;
border-color: #3f3f3f;
box-shadow: none;
font-weight: 400;
}
/* ---------- Add chips ---------- */
.seatBuyIns {
margin-top: -0.3rem;
color: #9a9a9a;
font-size: 0.75rem;
font-variant-numeric: tabular-nums;
}
#addChipsOverlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.72);
display: flex;
align-items: flex-end;
justify-content: center;
z-index: 98;
}
#addChipsOverlay[hidden] {
display: none;
}
#addChipsSheet {
width: 100%;
max-width: 420px;
background: linear-gradient(180deg, #242424, #1a1a1a);
border: 1px solid #3a3a3a;
border-bottom: none;
border-radius: 16px 16px 0 0;
padding: 0.35rem 1rem calc(1.2rem + env(safe-area-inset-bottom, 0px));
box-shadow: 0 -14px 40px rgba(0, 0, 0, 0.65);
}
.addChipsHeader {
margin-bottom: 0.8rem;
}
.addChipsHeader h2 {
margin: 0;
font-size: 1rem;
font-weight: 500;
color: #f5f5f5;
}
#addChipsSubtitle {
color: #9a9a9a;
font-size: 0.78rem;
font-variant-numeric: tabular-nums;
}
#addChipsQuick {
display: flex;
gap: 0.45rem;
margin-bottom: 0.7rem;
}
.addChipsQuickBtn {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.1rem;
min-height: 52px;
border-radius: 11px;
border: 1px solid #3a3a3a;
background: linear-gradient(180deg, #2e2e2e, #232323);
color: #ddd;
font-family: inherit;
cursor: pointer;
}
.addChipsQuickBtn.selected {
border-color: #d8b46a;
background: linear-gradient(180deg, #3a2f18, #241d0f);
color: #f0d9a8;
}
.addChipsQuickLabel {
font-size: 0.78rem;
font-weight: 600;
}
.addChipsQuickAmount {
font-size: 0.7rem;
color: #a8a8a8;
font-variant-numeric: tabular-nums;
}
.addChipsQuickBtn.selected .addChipsQuickAmount {
color: #d8b46a;
}
.addChipsEntry {
display: flex;
align-items: center;
gap: 0.4rem;
background: #141414;
border: 1px solid #3a3a3a;
border-radius: 11px;
padding: 0.2rem 0.8rem;
margin-bottom: 0.8rem;
}
.addChipsEntry:focus-within {
border-color: #d8b46a;
}
.addChipsCurrency {
color: #8a8a8a;
font-size: 1.3rem;
}
#addChipsInput {
flex: 1;
min-width: 0;
background: none;
border: none;
outline: none;
color: #f2f2f2;
font-family: inherit;
font-size: 1.5rem;
font-weight: 500;
font-variant-numeric: tabular-nums;
padding: 0.4rem 0;
}
.addChipsActions {
display: flex;
flex-direction: column-reverse;
gap: 0.5rem;
}
.addChipsActions .holdemBtn {
width: 100%;
justify-content: center;
min-height: 48px;
}
/* ---------- Rulings reference ---------- */
.tableHintActions {
margin: 0.35rem 0 0;
text-align: center;
}
/* A bare checkbox ships at 13x13, which misses the 24px minimum with a mouse as well
as with a thumb. The row around it is the real target on touch, but the box itself
should be findable on any device. */
#dedicatedDealerToggle {
width: 24px;
height: 24px;
}
.linkBtn {
background: none;
border: none;
/* An underlined word was a 91x14 target, which misses the 24px minimum with a
mouse as well as with a thumb. The coarse-pointer block takes it to 44; this is
the floor everywhere else, and it changes nothing about how it looks. */
display: inline-flex;
align-items: center;
box-sizing: border-box;
min-height: 24px;
padding: 0;
color: #d8b46a;
font-family: inherit;
font-size: 0.78rem;
text-decoration: underline;
text-underline-offset: 2px;
cursor: pointer;
}
@media (hover: hover) {
.linkBtn:hover {
color: #d8b46a;
}
}
#rulingsOverlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.72);
display: flex;
align-items: flex-end;
justify-content: center;
z-index: 97;
}
#rulingsOverlay[hidden] {
display: none;
}
#rulingsSheet {
width: 100%;
max-width: 460px;
max-height: 86vh;
overflow-y: auto;
background: linear-gradient(180deg, #242424, #1a1a1a);
border: 1px solid #3a3a3a;
border-bottom: none;
border-radius: 16px 16px 0 0;
padding: 0.35rem 1.1rem calc(1.2rem + env(safe-area-inset-bottom, 0px));
box-shadow: 0 -14px 40px rgba(0, 0, 0, 0.65);
}
.rulingsHeader {
display: flex;
align-items: center;
gap: 0.6rem;
margin-bottom: 0.5rem;
}
.rulingsHeader h2 {
flex: 1;
margin: 0;
font-size: 1.05rem;
font-weight: 500;
color: #f5f5f5;
}
.rulingsIntro {
margin: 0 0 1rem;
color: #9a9a9a;
font-size: 0.8rem;
line-height: 1.5;
}
/* Amber, like the bank check's disagreement — both are "your record of the night is
wrong and you can still do something about it". */
.storageWarning {
margin: 0;
padding: 0.5rem 0.6rem;
border: 1px solid rgba(240, 180, 106, 0.45);
border-radius: 7px;
background: rgba(240, 180, 106, 0.08);
color: #f0b46a;
font-size: 0.76rem;
line-height: 1.4;
}
/* Each stack and the chips that make it, laid out like the shortcut legend below —
the thing being named on one line, the answer under it, so a dealer with a rack in
one hand reads down rather than across. */
.colourUpList {
margin: 0.2rem 0 0;
}
.colourUpWho {
color: #ddd;
font-size: 0.78rem;
font-variant-numeric: tabular-nums;
}
.colourUpChips {
margin: 0 0 0.45rem;
color: #9fb8a6;
font-size: 0.74rem;
font-variant-numeric: tabular-nums;
}
.colourUpChips:last-child {
margin-bottom: 0;
}
/* The shortcut legend. A touch device has no keys to press, so it doesn't get a panel
telling it about them — the same pointer question the touch-target rules ask. */
@media (pointer: coarse) {
#keyboardPanel {
display: none;
}
}
.shortcutLegend {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.45rem 0.6rem;
margin: 0;
align-items: baseline;
}
.shortcutLegend dt {
display: flex;
gap: 0.2rem;
}
.shortcutLegend dd {
margin: 0;
color: #b8b8b8;
font-size: 0.78rem;
line-height: 1.35;
}
.shortcutKey {
background: #232323;
border: 1px solid #3f3f3f;
border-bottom-width: 2px;
border-radius: 4px;
padding: 0.05rem 0.32rem;
font-family: inherit;
font-size: 0.68rem;
color: #d8b46a;
white-space: nowrap;
}
/* One chip per ruling, wrapped across the top of the sheet. Names rather than icons:
a dealer scanning this is looking for the words somebody at the table just said. */
.rulingsIndex {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
margin: 0 0 1rem;
}
.rulingsIndexBtn {
box-sizing: border-box;
min-height: 32px;
padding: 0.3rem 0.6rem;
border-radius: 999px;
border: 1px solid #3a3a3a;
background: #232323;
color: #d8b46a;
font-family: inherit;
font-size: 0.72rem;
line-height: 1.25;
text-align: left;
cursor: pointer;
}
@media (hover: hover) {
.rulingsIndexBtn:hover {
background: #2e2e2e;
border-color: #4a4a4a;
}
}
/* A thumb needs the same 44px here as everywhere else. */
@media (pointer: coarse) {
.rulingsIndexBtn {
min-height: 44px;
font-size: 0.78rem;
}
}
/* The heading the index jumps to takes focus, and a focus ring on a heading that the
dealer just asked for reads as noise rather than as help. */
.ruling h3:focus {
outline: none;
}
.ruling {
margin-bottom: 1rem;
padding-left: 0.7rem;
border-left: 2px solid rgba(216, 180, 106, 0.4);
}
.ruling h3 {
margin: 0 0 0.35rem;
color: #d8b46a;
font-size: 0.82rem;
font-weight: 600;
letter-spacing: 0.03em;
}
.ruling p,
.ruling li {
color: #c4c4c4;
font-size: 0.82rem;
line-height: 1.5;
}
.ruling p {
margin: 0;
}
.ruling ul {
margin: 0;
padding-left: 1.1rem;
}
.ruling li + li {
margin-top: 0.25rem;
}
.rulingsSource {
margin: 0;
padding-top: 0.6rem;
border-top: 1px solid #2c2c2c;
color: #8a8a8a;
font-size: 0.72rem;
line-height: 1.45;
}
/* ---------- Seat long-press menu ---------- */
/* Held down, before the menu opens — the only feedback that a press is being
counted rather than ignored. */
.seat.seatPressing {
transform: translate(-50%, -50%) scale(0.94);
}
#seatMenuOverlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.72);
display: flex;
align-items: flex-end;
justify-content: center;
z-index: 96;
}
#seatMenuOverlay[hidden] {
display: none;
}
#seatMenuSheet {
width: 100%;
max-width: 420px;
background: linear-gradient(180deg, #242424, #1a1a1a);
border: 1px solid #3a3a3a;
border-bottom: none;
border-radius: 16px 16px 0 0;
padding: 0.35rem 0.9rem calc(1rem + env(safe-area-inset-bottom, 0px));
box-shadow: 0 -14px 40px rgba(0, 0, 0, 0.65);
/* This sheet had no height limit and didn't scroll, so on a phone held sideways
its first two actions — Fold and All-in, the two that matter — sat below the
bottom of the screen with no way to reach them. A dealer in landscape could
long-press a seat and then not fold it. */
max-height: 85vh;
overflow-y: auto;
overscroll-behavior: contain;
}
.seatMenuHeader {
margin-bottom: 0.8rem;
}
.seatMenuHeader h2 {
margin: 0;
font-size: 1.05rem;
font-weight: 500;
color: #f5f5f5;
}
#seatMenuSubtitle {
color: #9a9a9a;
font-size: 0.8rem;
font-variant-numeric: tabular-nums;
}
#seatMenuActions {
display: flex;
flex-direction: column;
gap: 0.45rem;
margin-bottom: 0.6rem;
}
.seatMenuBtn {
width: 100%;
min-height: 52px;
border-radius: 11px;
border: 1px solid #3a3a3a;
background: linear-gradient(180deg, #2e2e2e, #232323);
color: #eee;
font-family: inherit;
font-size: 0.95rem;
cursor: pointer;
transition: all 0.12s ease;
}
@media (hover: hover) {
.seatMenuBtn:hover {
border-color: #4a4a4a;
background: linear-gradient(180deg, #383838, #2a2a2a);
}
}
.seatMenuBtn.menuDanger {
background: linear-gradient(180deg, #3d1f1f, #2c1515);
border-color: #5b2626;
color: #ffd6d6;
}
.seatMenuBtn.menuAccent {
background: linear-gradient(180deg, #3a2f18, #241d0f);
border-color: #a9782f;
color: #f0d9a8;
}
#seatMenuCancelBtn {
width: 100%;
justify-content: center;
min-height: 48px;
}
/* ---------- Card picker ---------- */
#cardPickerOverlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.72);
display: flex;
align-items: flex-end;
justify-content: center;
z-index: 95;
}
#cardPickerOverlay[hidden] {
display: none;
}
#cardPickerSheet {
width: 100%;
max-width: 460px;
max-height: 88vh;
overflow-y: auto;
background: linear-gradient(180deg, #242424, #1a1a1a);
border: 1px solid #3a3a3a;
border-bottom: none;
border-radius: 16px 16px 0 0;
padding: 0.35rem 0.9rem calc(1rem + env(safe-area-inset-bottom, 0px));
box-shadow: 0 -14px 40px rgba(0, 0, 0, 0.65);
}
.cardPickerHeader {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.6rem;
}
.cardPickerHeader h2 {
flex: 1;
min-width: 0;
margin: 0;
font-size: 0.95rem;
font-weight: 500;
color: #f5f5f5;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cardPickerHeader .holdemBtn {
min-height: 40px;
}
#cardPickerSlots {
display: flex;
gap: 0.5rem;
margin-bottom: 0.7rem;
}
.pickerSlot {
flex: 1;
min-height: 46px;
border-radius: 8px;
border: 1px solid #3a3a3a;
background: #191919;
color: #8a8a8a;
font-family: var(--font-mono, monospace);
font-size: 1.1rem;
font-weight: 700;
cursor: pointer;
}
/* The slot the next tap will fill. */
.pickerSlot.active {
border-color: #d8b46a;
background: #241d0f;
color: #f0d9a8;
}
.pickerSlot.cardRed {
color: #e07b7b;
}
#cardPickerGrid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.35rem;
}
.pickerCard {
display: flex;
align-items: center;
justify-content: center;
gap: 0.1rem;
min-height: 44px;
border-radius: 7px;
border: 1px solid #d5d5d5;
background: #fdfdfd;
color: #111;
font-family: var(--font-mono, monospace);
font-size: 1rem;
font-weight: 700;
cursor: pointer;
transition: transform 0.1s ease;
}
@media (hover: hover) {
.pickerCard:hover:not(:disabled) {
transform: translateY(-1px);
}
}
.pickerCard.cardRed {
color: #c0392b;
}
.pickerCard.selected {
outline: 3px solid #d8b46a;
outline-offset: -1px;
}
/* Already dealt somewhere else at the table — visibly out of play rather than
silently accepting a duplicate. */
.pickerCard:disabled {
background: #1d1d1d;
border-color: #303030;
color: #4a4a4a;
cursor: not-allowed;
}
.pickerSuit {
font-size: 1.05em;
line-height: 1;
}
.seatContribution {
border-top: 1px dashed #3a3a3a;
padding-top: 0.4rem;
margin-bottom: 0.3rem;
}
.chipCounts {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
margin-bottom: 0.35rem;
}
.chipCountRow {
display: flex;
align-items: center;
gap: 0.2rem;
}
.chipSwatch {
width: 12px;
height: 12px;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.3);
display: inline-block;
}
.chipCountInput {
width: 34px;
background: #1a1a1a;
border: 1px solid #3a3a3a;
border-radius: 4px;
color: #eee;
font-size: 0.75rem;
padding: 0.1rem 0.2rem;
}
.amountOverrideRow {
justify-content: space-between;
font-size: 0.75rem;
color: #aaa;
margin-bottom: 0.3rem;
}
.amountOverrideInput {
width: 70px;
background: #1a1a1a;
border: 1px solid #3a3a3a;
border-radius: 4px;
color: #eee;
padding: 0.2rem 0.3rem;
}
.seatTotal {
font-size: 0.8rem;
color: #d6ffe0;
font-weight: 600;
text-align: right;
}
.customFields {
display: flex;
flex-direction: column;
gap: 0.25rem;
margin-bottom: 0.3rem;
}
.customFieldRow {
display: flex;
gap: 0.25rem;
}
.customFieldLabel,
.customFieldValue {
min-width: 0;
flex: 1;
background: #1a1a1a;
border: 1px solid #3a3a3a;
border-radius: 4px;
color: #eee;
font-size: 0.7rem;
padding: 0.2rem 0.3rem;
}
.removeFieldBtn,
.removeChipColorBtn {
line-height: 1;
}
/* ---------- Sidebar ---------- */
#holdemSidebar {
display: flex;
flex-direction: column;
gap: 0.8rem;
/* Grid items default to min-width:auto, so anything with a wide min-content —
a side-by-side field row, say — would push this column past the viewport and
drag the table out with it. */
min-width: 0;
}
.sidebarPanel {
background: linear-gradient(160deg, #212121, #1a1a1a);
border: 1px solid #333;
border-radius: 10px;
padding: 0.2rem 0.9rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.sidebarPanel summary {
cursor: pointer;
/* These headers are the sidebar's whole navigation and were 38px tall on every
device, phones included — the touch pass further down never reached them.
Padding rather than a flex box with min-height, because a <summary> only draws
its disclosure triangle while it is display: list-item. */
padding: 0.85rem 0;
/* A <summary> is content-box, so min-height would have been *added* to the padding
— measured at 71px before this line went in. Same trap the header buttons were
in, one file over. */
box-sizing: border-box;
min-height: 44px;
font-size: 0.9rem;
color: #eee;
letter-spacing: 0.03em;
}
.panelBody {
padding-bottom: 0.9rem;
display: flex;
flex-direction: column;
gap: 0.7rem;
}
/* A heading inside a panel, so one panel can hold two related things — the
standings and the hand log are two views of the same session — without needing
two identical-looking accordions to say so. */
.panelSubhead {
margin: 0.2rem 0 0;
color: #d8b46a;
font-size: 0.68rem;
font-weight: 600;
letter-spacing: 0.09em;
text-transform: uppercase;
}
.panelSubhead + * {
margin-top: -0.2rem;
}
/* Nested disclosure for the things a dealer sets once and then forgets, kept out
of the way of the settings they actually touch. */
.subPanel {
border-top: 1px solid #2c2c2c;
padding-top: 0.5rem;
}
.subPanel summary {
cursor: pointer;
color: #9a9a9a;
font-size: 0.78rem;
}
@media (hover: hover) {
.subPanel summary:hover {
color: #c5c5c5;
}
}
.subPanelBody {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding-top: 0.6rem;
}
.fieldRow {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.6rem;
font-size: 0.85rem;
color: #ccc;
}
.fieldRow input,
.fieldRow select {
background: #141414;
border: 1px solid #3a3a3a;
border-radius: 5px;
color: #eee;
padding: 0.35rem 0.5rem;
max-width: 140px;
}
.fieldHint {
font-size: 0.75rem;
color: #949494;
line-height: 1.4;
margin: 0;
}
#chipLegendList {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.chipLegendRow {
display: flex;
align-items: center;
gap: 0.5rem;
}
.chipColorInput {
width: 32px;
height: 28px;
background: none;
border: 1px solid #3a3a3a;
border-radius: 5px;
padding: 0;
}
.chipValueInput {
flex: 1;
background: #141414;
border: 1px solid #3a3a3a;
border-radius: 5px;
color: #eee;
padding: 0.3rem 0.5rem;
}
#standingsTable {
max-height: 260px;
overflow-y: auto;
}
/* The sidebar is about 290px wide, and four numeric columns at a 4.5rem minimum
plus gaps come to 320px — so the name column, being the only flexible one, was
collapsing to *zero* and player names disappeared entirely. Names were already
down to 50px with four columns; adding the cash-out column finished them off.
Measured at 360, 390 and 1280 alike, since the sidebar is narrow at all of them.
So the table earns its columns instead of assuming them. Narrow: the player and
the two numbers a banker settles up from. Wide enough: the per-hand detail as
well. Everything is in the CSV export either way. */
.standingsTableWrap {
container-type: inline-size;
}
.standingsRow {
display: grid;
grid-template-columns: minmax(4rem, 1fr) auto auto;
gap: 0.5rem;
align-items: baseline;
padding: 0.3rem 0.1rem;
font-size: 0.78rem;
border-bottom: 1px solid #232323;
}
.standingsRow > span:not(:first-child) {
text-align: right;
font-variant-numeric: tabular-nums;
min-width: 3.6rem;
}
.standingsRow > .standingsName {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* The per-hand columns are detail: how the night's swings happened. They're the first
to go when there isn't room, and Net goes with them — it only moves when a pot is
resolved through the wizard, so on a table where the dealer settles pots by hand it
reads $0 all night beside two columns that don't.
What survives at every width is the pair the banker actually works from: what the
player is holding, and whether that leaves them up or down. */
.standingsIn,
.standingsWon,
.standingsNet,
.standingsInHead,
.standingsWonHead,
.standingsNetHead {
display: none;
}
@container (min-width: 30rem) {
.standingsRow {
grid-template-columns: minmax(5rem, 1fr) auto auto auto auto auto;
}
.standingsIn,
.standingsWon,
.standingsNet,
.standingsInHead,
.standingsWonHead,
.standingsNetHead {
display: block;
}
}
/* The chips column is the one a banker's hands follow while counting money onto the
table, so it carries the weight. Up/down is read once, at the end. */
.standingsChips {
font-weight: 600;
color: #e6e6e6;
}
.standingsCashOut {
font-weight: 600;
}
/* ---------- Bank reconciliation ----------
Cash taken versus chips on the table. Every chip is a promise to pay cash for it
later, and if the chips outrun the box that promise can't be kept — which a dealer
otherwise discovers when the last two players cash out and there's nothing left. */
.bankReconcile {
margin-top: 0.6rem;
padding: 0.55rem 0.6rem;
border: 1px solid #2f2f2f;
border-radius: 8px;
background: #151515;
}
.bankReconcileRow {
display: flex;
justify-content: space-between;
gap: 0.75rem;
font-size: 0.78rem;
color: #b8b8b8;
font-variant-numeric: tabular-nums;
padding: 0.12rem 0;
}
.bankReconcileVerdict {
margin: 0.4rem 0 0;
font-size: 0.74rem;
line-height: 1.35;
}
.bankReconcileVerdict.bankBalanced {
color: #7fc99a;
}
.bankReconcileVerdict.bankOff {
color: #f0b46a;
}
/* The settle-up list is read out loud, one line per payment, so it's laid out like
the bank rows above it: name on the left, money hard right on tabular figures so a
column of amounts lines up on the decimal point. */
.settleUpList {
list-style: none;
margin: 0.35rem 0 0;
padding: 0;
}
.settleUpRow {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 0.75rem;
font-size: 0.8rem;
padding: 0.18rem 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.settleUpRow:last-child {
border-bottom: 0;
}
.settleUpWho {
color: #ddd;
}
.settleUpAmount {
color: #f2d489;
font-weight: 600;
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
/* Same amber the bank check uses when the columns don't agree — it's the same fault
being reported twice, and it should look like it. */
.fieldHint.settleUpOff {
color: #f0b46a;
}
.standingsHeader {
color: #8a8a8a;
font-size: 0.7rem;
letter-spacing: 0.04em;
text-transform: uppercase;
border-bottom-color: #3a3a3a;
position: sticky;
top: 0;
background: #1b1b1b;
}
.standingsName {
color: #eee;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.standingsIn,
.standingsWon {
color: #999;
}
.standingsNet {
color: #bbb;
font-weight: 600;
}
.standingsNet.netUp {
color: #4ec97a;
}
.standingsNet.netDown {
color: #e07b7b;
}
/* The showdown call, sat above the resolve button. Loud enough to be read across a
table at a glance — this is something the dealer says out loud — but not so loud it
competes with the button underneath it. */
.showdownCall {
margin: 0 0 0.45rem;
text-align: center;
font-size: 0.82rem;
color: #f0d98c;
}
.handHistoryFilterRow {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.45rem;
}
.handHistoryFilter {
flex: 1 1 auto;
min-width: 0;
/* 16px or iOS zooms the whole page on focus, which on a phone at the table throws
the felt off screen mid-hand. Every other field in this panel is held at 16px
for the same reason. */
font-size: 16px;
padding: 0.35rem 0.5rem;
background: #141414;
border: 1px solid #2c2c2c;
border-radius: 6px;
color: #e6e6e6;
}
.handHistoryFilter::placeholder {
color: #8a8a8a;
}
.handHistoryFilter:focus-visible {
outline: 2px solid #d8b46a;
outline-offset: 1px;
}
.handHistoryCount {
flex: 0 0 auto;
font-size: 0.72rem;
color: #8a8a8a;
font-variant-numeric: tabular-nums;
}
#handHistoryList {
display: flex;
flex-direction: column;
gap: 0.4rem;
/* Was a flat 260px, which is about four and a half rows — the same four and a half
whether the dealer is on a phone or a 27" screen with room for twenty. The floor
keeps it honest on a short window; the ceiling stops the panel from running away
on a tall one and pushing everything below it out of reach. */
max-height: clamp(200px, 42vh, 560px);
overflow-y: auto;
}
.handHistoryRow {
background: #141414;
border: 1px solid #2c2c2c;
border-radius: 6px;
padding: 0.45rem 0.6rem;
}
.handHistoryRowHeader {
display: flex;
align-items: baseline;
gap: 0.5rem;
font-size: 0.8rem;
}
.handHistoryHandNumber {
color: #d8b46a;
font-weight: 600;
}
.handHistoryPot {
color: #eee;
}
.handHistoryTime {
margin-left: auto;
color: #8a8a8a;
font-size: 0.72rem;
}
.handHistoryPayouts {
margin-top: 0.15rem;
color: #aaa;
font-size: 0.75rem;
line-height: 1.4;
}
#tableNotes {
width: 100%;
box-sizing: border-box;
background: #141414;
border: 1px solid #3a3a3a;
border-radius: 6px;
color: #eee;
padding: 0.5rem;
resize: vertical;
font-family: inherit;
/* iOS zooms the whole page in when you focus a field under 16px, and the dealer
is then pinching back out to see the table. Every other input here is already
at 16px; this one was the straggler. */
font-size: 16px;
}
/* ---------- Pot wizard ---------- */
/* ---------- Action bar ---------- */
#actionBar {
position: fixed;
/* Centred with auto margins rather than 100vw + translate: on a phone with a
scrollbar, 100vw is wider than the viewport and the overflow drags the whole
table sideways. */
left: 0;
right: 0;
bottom: 0;
margin: 0 auto;
z-index: 55;
max-width: 720px;
background: linear-gradient(180deg, #232323, #191919);
border: 1px solid #383838;
border-bottom: none;
border-radius: 14px 14px 0 0;
padding: 0.6rem 0.75rem calc(0.75rem + env(safe-area-inset-bottom, 0px));
box-shadow: 0 -12px 34px rgba(0, 0, 0, 0.6);
}
#actionBar[hidden] {
display: none;
}
/* The bar is fixed over the page, so the sidebar needs room to scroll past it. */
body.hasActionBar #holdemSidebar {
padding-bottom: 9rem;
}
.actionStatus {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.4rem 0.6rem;
margin-bottom: 0.55rem;
font-size: 0.78rem;
}
/* Always there while there's something to take back, rather than a toast per tap. */
.actionUndo {
flex: none;
min-height: 30px;
max-width: 100%;
padding: 0.15rem 0.6rem;
border-radius: 999px;
border: 1px solid #414141;
background: #262626;
color: #bdbdbd;
font-family: inherit;
font-size: 0.72rem;
cursor: pointer;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@media (hover: hover) {
.actionUndo:hover {
background: #333;
border-color: #4f4f4f;
color: #eee;
}
}
.actionStreet {
color: #d8b46a;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
font-size: 0.7rem;
}
.actionPot {
color: #e9e9e9;
font-variant-numeric: tabular-nums;
}
.actionPlayers {
color: #9c9c9c;
font-variant-numeric: tabular-nums;
}
.actionRunout {
margin-left: auto;
color: #d8b46a;
font-size: 0.74rem;
}
.actionWho {
margin-left: auto;
color: #9c9c9c;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Stopwatch, not a shot clock: it colours up as a prompt to the dealer, and does
nothing else. */
.actionTank {
flex: none;
min-width: 3.1rem;
text-align: right;
color: #949494;
font-variant-numeric: tabular-nums;
transition: color 0.3s ease;
}
.actionTank.tankNudge {
color: #d8b46a;
}
.actionTank.tankSlow {
color: #e07b7b;
font-weight: 600;
}
/* ---------- Side-pot breakdown ---------- */
.potBreakdown {
display: flex;
flex-direction: column;
gap: 0.2rem;
margin-bottom: 0.55rem;
padding: 0.4rem 0.55rem;
background: rgba(216, 180, 106, 0.07);
border: 1px solid rgba(216, 180, 106, 0.25);
border-radius: 8px;
}
.potBreakdownHeading {
margin: 0 0 0.35rem;
color: #d8b46a;
font-size: 0.68rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.potLayer {
display: flex;
align-items: baseline;
gap: 0.5rem;
font-size: 0.76rem;
}
.potLayerLabel {
color: #d8b46a;
font-weight: 600;
flex: none;
}
.potLayerAmount {
color: #f2f2f2;
font-variant-numeric: tabular-nums;
font-weight: 600;
flex: none;
}
.potLayerWho {
color: #9a9a9a;
flex: 1;
min-width: 0;
text-align: right;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.potBreakdownWizard {
margin-bottom: 1rem;
}
.potLayerReturn .potLayerLabel,
.potLayerReturn .potLayerAmount {
color: #9a9a9a;
font-weight: 500;
}
.actionButtons {
display: flex;
gap: 0.5rem;
}
.actionBtn {
position: relative;
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.1rem;
min-height: 56px;
padding: 0.5rem 0.6rem;
border-radius: 11px;
border: 1px solid #3a3a3a;
background: linear-gradient(180deg, #2e2e2e, #232323);
color: #eee;
font-family: inherit;
font-size: 0.95rem;
cursor: pointer;
transition: all 0.12s ease;
}
@media (hover: hover) {
.actionBtn:hover {
border-color: #4a4a4a;
background: linear-gradient(180deg, #383838, #2a2a2a);
}
}
.actionBtn:active {
transform: translateY(1px);
}
.actionBtnLabel {
font-weight: 600;
}
.actionBtnSub {
font-size: 0.72rem;
color: #a8a8a8;
font-variant-numeric: tabular-nums;
}
/* 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.75;
font-family: inherit;
font-size: 0.6rem;
line-height: 1.4;
text-transform: uppercase;
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;
right: 7px;
padding: 0 0.25rem;
border-radius: 4px;
border: 1px solid currentColor;
opacity: 0.75;
font-family: inherit;
font-size: 0.6rem;
line-height: 1.4;
text-transform: uppercase;
pointer-events: none;
}
/* No keyboard, no key badges — on a phone they'd just be clutter on the buttons
the dealer is already tapping. */
@media (hover: none) {
.actionKey {
display: none;
}
}
.actionFold {
background: linear-gradient(180deg, #3d1f1f, #2c1515);
border-color: #5b2626;
color: #ffd6d6;
}
.actionCheck,
.actionCall {
background: linear-gradient(180deg, #16783a, #0d5225);
border-color: #1e8449;
color: #ecffef;
}
.actionCheck .actionBtnSub,
.actionCall .actionBtnSub {
color: #bfe8cb;
}
.actionRaise {
background: linear-gradient(180deg, #3a2f18, #241d0f);
border-color: #a9782f;
color: #f0d9a8;
}
.actionAdvance {
background: linear-gradient(180deg, #2b3f57, #1b2a3a);
border-color: #3d5f83;
color: #dceaf8;
}
/* ---------- Raise pad ---------- */
.raiseQuick {
display: flex;
gap: 0.4rem;
margin-bottom: 0.5rem;
}
.raiseQuickBtn {
min-height: 48px;
font-size: 0.85rem;
}
.raiseQuickBtn.selected {
border-color: #d8b46a;
background: linear-gradient(180deg, #3a2f18, #241d0f);
color: #f0d9a8;
}
.raiseEntry {
display: flex;
gap: 0.4rem;
}
.raiseInput {
flex: 1;
min-width: 0;
background: #141414;
border: 1px solid #3a3a3a;
border-radius: 11px;
color: #f2f2f2;
font-family: inherit;
font-size: 16px;
font-variant-numeric: tabular-nums;
text-align: center;
padding: 0.5rem;
}
.raiseInput:focus {
outline: none;
border-color: #d8b46a;
}
.raiseCancel {
flex: 0 0 auto;
min-width: 84px;
}
.raiseConfirm {
flex: 0 0 auto;
min-width: 116px;
background: linear-gradient(180deg, #3a2f18, #241d0f);
border-color: #a9782f;
color: #f0d9a8;
}
/* Whose turn it is, shown on the table itself as well as in the bar.
Dealers say the biggest single time sink at a home table isn't how fast the cards
come out — it's players not noticing the action is on them. So this is the one
state that has to be unmistakable from across a table at a glance, and it was
distinguished only by a rim colour: a gold border and a 2px ring where a live seat
has a green border and a 1px one. Same fill, same size, nothing moving.
It now reads as lit rather than merely outlined: a solid ring at full strength, a
warm glow that separates it from the felt, and a lift of its own. The seat's
`transform` positions it around the table, so the lift is box-shadow rather than
translate — setting transform here would throw the seat off its spot. */
.seat.seatActing {
border-color: #f0cd85;
background: linear-gradient(160deg, #3a3222, #241f16);
box-shadow:
0 0 0 3px rgba(216, 180, 106, 0.9),
0 0 18px 2px rgba(216, 180, 106, 0.35),
0 6px 20px rgba(0, 0, 0, 0.6);
z-index: 4;
}
/* A slow breath, so a dealer glancing up finds it without hunting. Deliberately
gentle and deliberately optional — the ring above carries the whole job on its own
for anyone who'd rather nothing moved. */
@media (prefers-reduced-motion: no-preference) {
.seat.seatActing {
animation: seatActingPulse 2.4s ease-in-out infinite;
}
@keyframes seatActingPulse {
0%, 100% {
box-shadow:
0 0 0 3px rgba(216, 180, 106, 0.9),
0 0 18px 2px rgba(216, 180, 106, 0.35),
0 6px 20px rgba(0, 0, 0, 0.6);
}
50% {
box-shadow:
0 0 0 3px rgba(216, 180, 106, 0.65),
0 0 26px 5px rgba(216, 180, 106, 0.5),
0 6px 20px rgba(0, 0, 0, 0.6);
}
}
}
.seatPuckStack {
font-size: 0.58rem;
color: #9a9a9a;
font-variant-numeric: tabular-nums;
margin-top: 0.1rem;
}
/* "ALL IN" takes the stack line's place, so it has to not read as a stack. Wider
letters and a warm colour separate it from the grey money above it at a glance —
which is the whole job, because the dealer is scanning the ring for who can still
be bet at. */
.seatPuckStack.seatPuckAllIn {
color: #ffcf6b;
font-weight: 700;
letter-spacing: 0.06em;
}
/* And the seat itself carries a hint of it, so the mark survives being read out of
the corner of an eye. Deliberately quieter than .seatActing — all-in is a standing
fact about a player, not a request for the dealer's attention. */
.seat.seatAllIn {
border-color: rgba(255, 207, 107, 0.45);
}
/* Tournament-sized amounts. A number has no spaces to break on, so an amount too
wide for the 64px puck neither wraps nor ellipsises — it draws straight out over
the seat next to it. These step the size down so every digit stays on its own
puck. Shrinking rather than abbreviating is deliberate: "$1.25M" would fit more
comfortably, but a dealer reading a rounded stack off the felt is a worse failure
than a small one, and the exact figure is what the tool is for. */
.seatPuckStack.puckAmountLong,
.seatPuckTotal.puckAmountLong {
font-size: 0.5rem;
letter-spacing: -0.01em;
}
.seatPuckStack.puckAmountHuge,
.seatPuckTotal.puckAmountHuge {
font-size: 0.44rem;
letter-spacing: -0.02em;
}
.blindsRow {
display: flex;
flex-wrap: wrap;
gap: 0.6rem;
}
.blindsRow .fieldRow {
flex: 1 1 120px;
min-width: 0;
gap: 0.4rem;
}
/* "Small blind" broke onto two lines beside its input, which made the pair look
like a mistake rather than a pair. */
.blindsRow .fieldRow > span {
white-space: nowrap;
}
.blindsRow input {
min-width: 0;
width: 3.4rem;
}
/* ---------- Toasts ---------- */
#toastHost {
position: fixed;
left: 0;
right: 0;
bottom: 1.25rem;
margin: 0 auto;
z-index: 80;
max-width: 560px;
padding: 0 0.75rem;
box-sizing: border-box;
pointer-events: none;
transition: bottom 0.18s ease;
}
/* Sit above the action bar rather than on top of it, so an Undo never covers the
Fold button the dealer is reaching for. */
/* Sit the toast directly on top of the action bar, whatever height it happens to be.
This was a hardcoded 9rem, which is roughly right for the bar at rest and wrong the
moment the raise pad opens — and on a short phone it put the toast, Undo button and
all, straight over the bottom seats of the table, so a tap meant for a player hit
"Undo" instead. */
body.hasActionBar #toastHost {
bottom: calc(var(--actionBarHeight, 9rem) + 0.75rem + env(safe-area-inset-bottom, 0px));
}
.toast {
display: flex;
align-items: center;
gap: 0.75rem;
background: linear-gradient(180deg, #2b2b2b, #202020);
border: 1px solid #414141;
border-left: 3px solid #d8b46a;
border-radius: 10px;
padding: 0.7rem 0.8rem 0.7rem 0.9rem;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.55);
pointer-events: auto;
opacity: 0;
transform: translateY(12px);
transition: opacity 0.18s ease, transform 0.18s ease;
}
.toast.toastVisible {
opacity: 1;
transform: translateY(0);
}
.toastText {
flex: 1;
min-width: 0;
color: #e9e9e9;
font-size: 0.85rem;
line-height: 1.35;
}
.toastAction {
flex: none;
min-height: 38px;
padding: 0.35rem 0.9rem;
border-radius: 8px;
border: 1px solid #d8b46a;
background: rgba(216, 180, 106, 0.12);
color: #f0d9a8;
font-size: 0.85rem;
font-weight: 600;
font-family: inherit;
cursor: pointer;
}
@media (hover: hover) {
.toastAction:hover {
background: rgba(216, 180, 106, 0.22);
}
}
.toastClose {
flex: none;
width: 30px;
height: 30px;
border-radius: 50%;
border: none;
background: transparent;
color: #949494;
font-size: 0.8rem;
font-family: inherit;
cursor: pointer;
/* Same treatment as the dealer puck: a 30px dismiss button in a toast a dealer is
swatting away mid-hand, given a fingertip's worth of hit area without becoming
a bigger piece of furniture. */
position: relative;
}
@media (hover: hover) {
.toastClose:hover {
background: #333;
color: #ddd;
}
}
/* ---------- Confirmation dialog ---------- */
#confirmOverlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.72);
display: flex;
align-items: center;
justify-content: center;
z-index: 90;
padding: 1.5rem;
}
#confirmOverlay[hidden] {
display: none;
}
#confirmModal {
background: #1e1e1e;
border: 1px solid #3a3a3a;
border-radius: 12px;
width: 100%;
max-width: 420px;
padding: 1.3rem 1.4rem 1.2rem;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
outline: none;
}
#confirmTitle {
margin: 0 0 0.5rem;
font-size: 1.05rem;
font-weight: 500;
color: #f5f5f5;
}
#confirmMessage {
margin: 0 0 1.2rem;
color: #a5a5a5;
font-size: 0.85rem;
line-height: 1.5;
}
.confirmActions {
display: flex;
justify-content: flex-end;
gap: 0.6rem;
}
.confirmActions .holdemBtn {
min-height: 44px;
}
#potWizardOverlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
/* Every other sheet in here sits at 90-98; this one was at 50, underneath both
the action bar (55) and the toast (80). So the bar drew on top of the dialog
that pays people — in landscape one of its inputs was completely covered by
the Fold button — and a toast could sit over "Confirm Winner(s)". It's the odd
one out rather than a deliberate choice, and it joins the others. */
z-index: 95;
padding: 1.5rem;
}
#potWizardOverlay[hidden] {
display: none;
}
#potWizardModal {
background: #1e1e1e;
border: 1px solid #3a3a3a;
border-radius: 10px;
width: 100%;
max-width: 480px;
/* 85vh is most of a 390px-tall landscape window, and the action bar is fixed over
the bottom of it — so the last of the wizard sat underneath the bar and its
controls couldn't be tapped. Leaving the bar's own height out of the budget
keeps the whole dialog above it. */
max-height: calc(85vh - var(--actionBarHeight, 0px));
overflow-y: auto;
overscroll-behavior: contain;
padding: 1.2rem 1.4rem 1.6rem;
}
.potWizardHeader {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.8rem;
}
.potWizardHeader h2 {
margin: 0;
font-size: 1.2rem;
font-weight: 400;
color: #f5f5f5;
}
.potWizardLog {
margin-bottom: 0.8rem;
}
.potWizardLogLine {
font-size: 0.8rem;
color: #999;
margin: 0.2rem 0;
}
.potWizardPrompt {
font-size: 0.95rem;
color: #eee;
margin-bottom: 0.8rem;
}
.potWizardChoices {
display: flex;
flex-direction: column;
gap: 0.4rem;
margin-bottom: 0.6rem;
}
/* This is the screen where the money gets decided, so it holds the same 44px+
targets as the rest of the tool rather than the smallest ones in it. Picking the
wrong winner is the one mistake here that costs somebody real money. */
.potWizardChoice {
display: flex;
align-items: center;
gap: 0.7rem;
min-height: 56px;
background: #141414;
border: 1px solid #333;
border-radius: 10px;
padding: 0.6rem 0.8rem;
cursor: pointer;
font-size: 0.95rem;
transition: border-color 0.12s ease, background 0.12s ease;
}
@media (hover: hover) {
.potWizardChoice:hover {
border-color: #555;
background: #191919;
}
}
.potWizardChoice input {
flex: none;
width: 22px;
height: 22px;
accent-color: #1e8449;
}
/* Selected has to be obvious at a glance — a dealer confirms this with players
watching, and a checkbox alone is easy to misread across a table. */
.potWizardChoice.chosen {
border-color: #1e8449;
background: linear-gradient(180deg, #16301f, #12241a);
}
.potWizardChoice.chosen .potWizardChoiceName {
color: #d6ffe0;
}
.potWizardChoiceText {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.1rem;
}
.potWizardChoiceName {
color: #f0f0f0;
font-weight: 600;
}
.potWizardChoiceUpTo {
color: #9a9a9a;
font-size: 0.76rem;
font-variant-numeric: tabular-nums;
}
.potWizardChoiceCards {
flex: none;
font-family: var(--font-mono, monospace);
font-size: 1rem;
font-weight: 700;
color: #ddd;
}
.potWizardChoiceCards .cardRed {
color: #ff6b6b;
}
.potWizardSummary ul {
list-style: none;
padding: 0;
margin: 0 0 1rem;
}
.potWizardSummary li {
padding: 0.4rem 0;
border-bottom: 1px dashed #333;
font-size: 0.9rem;
color: #d6ffe0;
}
/* The chips to push, under the amount they add up to. Quieter than the payout it
belongs to — the dealer reads the name and the money first, then counts. */
.payoutChips {
display: block;
margin-top: 0.15rem;
font-size: 0.76rem;
color: #9fb8a6;
font-variant-numeric: tabular-nums;
}
.potWizardActions {
display: flex;
gap: 0.6rem;
}
/* Every button in this modal is one a dealer presses with players waiting. */
#potWizardBody .holdemBtn {
min-height: 48px;
justify-content: center;
}
/* The confirm is the whole point of the step, so it gets the whole width rather
than being sized to the length of its own label. */
.potWizardRound > .holdemBtn {
width: 100%;
}
.potWizardActions .holdemBtn {
flex: 1;
}
.potWizardSummary li {
font-variant-numeric: tabular-nums;
}
@media (max-width: 700px) {
/* The narrow layout already had an answer for the bar: it reserves the bar's height
as scroll room below the table, and a phone is a thing you scroll. Capping the
felt here as well was measured and rejected — at 360x640 it took the table from
321px to 226px, which fixed the six-seat ring but broke the nine-seat one, two
covered seats traded for two seats whose centre landed on a neighbour. The cap is
for the wide layout, which never consulted the bar's height at all. */
#pokerTableWrap {
max-width: none;
}
/* Stacked, with the action that ends the hand furthest from a stray thumb. */
.potWizardActions {
flex-direction: column-reverse;
}
.potWizardChoice {
min-height: 60px;
}
}
@media (max-width: 900px) {
#holdemLayout {
grid-template-columns: 1fr;
}
/* One column, so the table has the full width and can afford to be round. */
#pokerTableWrap {
--feltAspect: 1;
}
.seat {
width: 78px;
min-height: 44px;
}
}
/* ---------- Phones ----------
Two things break on a narrow screen: seat pucks ring the table on a radius
measured from the wrap, so the left- and right-most ones hang off the edge of
the viewport; and the seat editor lives far enough down the page that tapping a
seat looks like it did nothing. Inset the ring by half a puck, and float the
editor up as a bottom sheet over the table. */
@media (max-width: 700px) {
/* The bottom padding has to clear the fixed action bar, plus room for a toast to
sit above it. Without it the table just carries on underneath the bar and the
lower seats of the oval are behind the Fold button — measured at 360x640, where
62px of felt was covered. `--actionBarHeight` is published by actionBar.js and
is 0 when no hand is running, so the page doesn't hold a gap it doesn't need. */
#holdemApp {
padding: 1rem 0.75rem calc(var(--actionBarHeight, 0px) + 5rem + env(safe-area-inset-bottom, 0px));
}
#holdemHeader {
margin-bottom: 1rem;
gap: 0.6rem;
}
#holdemHeader h1 {
font-size: 1.4rem;
}
.holdemSubtitle {
font-size: 0.8rem;
}
#holdemHeaderActions {
width: 100%;
gap: 0.45rem;
}
#holdemHeaderActions .holdemBtn {
flex: 1 1 auto;
justify-content: center;
min-height: 46px;
padding: 0.5rem 0.7rem;
font-size: 0.82rem;
}
/* Keeps every puck inside the viewport: the ring is drawn within this box,
so half a puck of padding is exactly enough to stop the overhang. */
#seatsLayer,
#betsLayer {
inset: 26px 44px;
}
/* Narrower than the desktop puck: at a full ten-handed table the ring is
flattest at the top and bottom, and wide pucks are what made neighbours
overlap there. The puck is only a summary — the real form is the sheet. */
.seat {
width: 64px;
min-height: 42px;
padding: 0.25rem 0.3rem;
}
#potBadge {
padding: 0.5rem 1.1rem;
}
#potBadgeAmount {
font-size: 1.2rem;
}
.tableHint {
margin-top: 0.75rem;
font-size: 0.75rem;
}
/* ---- Seat editor as a bottom sheet ---- */
#seatEditorPanel {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 60;
margin: 0;
max-height: 74vh;
overflow-y: auto;
border-radius: 16px 16px 0 0;
border-bottom: none;
padding: 0.35rem 1rem 1.25rem;
background: linear-gradient(180deg, #242424, #1a1a1a);
box-shadow: 0 -14px 40px rgba(0, 0, 0, 0.65);
transform: translateY(105%);
transition: transform 0.25s ease;
-webkit-overflow-scrolling: touch;
}
body.seatSheetOpen #seatEditorPanel {
transform: translateY(0);
}
.seatSheetGrip {
display: block;
width: 38px;
height: 4px;
margin: 0.35rem auto 0.5rem;
border-radius: 999px;
background: #4a4a4a;
}
.seatSheetClose {
display: inline-flex;
align-items: center;
flex: none;
min-height: 44px;
padding: 0.35rem 1.1rem;
border-radius: 8px;
border: 1px solid #3a3a3a;
background: linear-gradient(180deg, #2e2e2e, #232323);
color: #ddd;
font-size: 0.8rem;
font-family: inherit;
cursor: pointer;
}
/* The sheet covers the bottom of the page while it's open — give the sidebar
room so nothing is stranded underneath it. */
body.seatSheetOpen #holdemSidebar {
padding-bottom: 40vh;
}
/* The seat form was sized for a 320px desktop sidebar. In the sheet it has a
whole phone width to work with, so everything grows to thumb size — and
every field goes to 16px, below which iOS zooms the page on focus and the
dealer has to pinch back out mid-hand. */
/* Every text or number field in the form, rather than a list of the ones that
existed when this was written. The stack field was added later and sat at 28px
next to its 44px neighbours precisely because it wasn't on that list. */
.seatEditorForm input:not([type="checkbox"]):not([type="color"]) {
font-size: 16px;
min-height: 44px;
padding: 0.4rem 0.6rem;
}
.seatEditorForm .chipCountInput {
width: 56px;
text-align: center;
}
.seatEditorForm .chipCounts {
gap: 0.6rem;
}
.seatEditorForm .chipSwatch {
width: 16px;
height: 16px;
}
.seatEditorForm .amountOverrideInput {
width: 110px;
}
.seatEditorForm .cardSlot {
font-size: 1.5rem;
min-height: 62px;
}
/* Thirteen rows of cards need to stay reachable without the sheet running off
the bottom of a phone. */
.pickerCard {
min-height: 46px;
font-size: 1.05rem;
}
.seatEditorForm .seatCardToggles {
gap: 1rem;
}
.seatEditorForm .inHandToggle,
.seatEditorForm .foldToggle {
min-height: 44px;
font-size: 0.95rem;
}
.seatEditorForm .inHandToggle input,
.seatEditorForm .foldToggle input {
width: 22px;
height: 22px;
}
.seatEditorForm .amountOverrideRow,
.seatEditorForm .seatTotal {
font-size: 0.95rem;
}
.seatEditorForm .addFieldBtn {
width: 100%;
justify-content: center;
min-height: 44px;
margin-top: 0.4rem;
}
/* The sidebar's own controls never got the touch pass the seat form did — these
are settings a dealer changes at the table, on the same phone. Written against
the panel rather than each field, so a setting added later inherits it. */
.panelBody input:not([type="checkbox"]):not([type="color"]),
.subPanelBody input:not([type="checkbox"]):not([type="color"]),
.panelBody select {
font-size: 16px;
min-height: 44px;
padding: 0.4rem 0.6rem;
}
.panelBody .holdemBtnSmall,
.handHistoryActions .holdemBtn,
/* The card picker's Clear and Done sat at 40px: the touch rule was scoped to
.panelBody and the picker is a sheet, so it never reached them. Clear wipes a
player's cards, which is not a thing to make a dealer aim at. */
.cardPickerHeader .holdemBtn,
/* Undo was 30px, and it is the control a dealer reaches for having just recorded
the wrong thing with five people waiting — the worst place to be fourteen pixels
short of a fingertip. Growing the box costs bar height, which comes out of the
felt, but measured at 390x844 the felt is 366px against a 220px floor, so there
is room for it. A hit-area pad was tried first and does not work here: the pill
clips its overflow to keep a long label on one line, which clips the pad too. */
.actionUndo {
min-height: 44px;
}
/* Destructive and tiny is the worst combination: these delete a chip colour or a
custom field, and were 28px wide. */
.removeChipColorBtn,
.removeFieldBtn {
min-width: 44px;
min-height: 44px;
}
/* flex-shrink would take a stated width straight back off again in a row this
narrow, which is why it stayed 32px wide with the height applied. */
.chipColorInput {
flex: none;
width: 44px;
height: 44px;
}
/* A bare checkbox can't grow into a target, but the row it sits in can — the
label is what a tap actually lands on. */
.panelBody .fieldRow {
min-height: 44px;
}
/* A bare checkbox with no label box around it to grow. */
#dedicatedDealerToggle {
width: 24px;
height: 24px;
}
/* An underlined word is a 14px-tall target; give it room without making it look
like a button. */
.linkBtn {
display: inline-flex;
align-items: center;
min-height: 44px;
padding: 0 0.4rem;
}
/* The dealer button has to stay a 22px disc — it's a chip sitting on a seat, and
drawing it thumb-sized would swallow the puck. So the disc keeps its size and
an invisible pad around it takes the tap. It reaches outward, away from the
puck's own tap target, so pressing the seat still opens the seat. */
.dealerChip::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 44px;
height: 44px;
transform: translate(-50%, -50%);
}
.dealerChip {
overflow: visible;
}
.toastAction {
min-height: 44px;
}
.confirmActions {
flex-direction: column-reverse;
}
.confirmActions .holdemBtn {
width: 100%;
justify-content: center;
}
}
/* ---------- A phone on its side ----------
Deliberately last in the file. The felt is an aspect-ratio box whose height comes
from `padding-bottom`, and the `max-width: 900px` block above re-sets that to 100%
— so a landscape rule placed any earlier is overridden at equal specificity and
does nothing, which is exactly what happened on the first attempt.
Landscape is where this layout falls apart worst: sized from width, the table came
out 765px tall in a 390px-tall window, putting six of the nine seats below the fold
and the rest behind the action bar. Sizing it from the height that's actually left
makes every seat reachable without scrolling. The table ends up small, but a seat
you have to go hunting for is worse than a seat that's small. */
@media (orientation: landscape) and (max-height: 560px) {
/* Set outright rather than via padding-bottom: a percentage padding resolves
against the containing block's width, not the element's own, so narrowing the
wrap on its own left it full height regardless. */
#pokerTableWrap {
/* A wide ellipse, which is what a real poker table is.
Capping the width by the height to hold the portrait aspect ratio was
throwing away most of the screen: at 640x360 the felt came out 135px wide
inside a 601px column — 22% of the width — leaving 366px of rim for nine
seats that need 662px. Adjacent seat centres ended up 13px apart, which is
why a seat's centre belonged to its neighbour.
Landscape has width and no height, so the table takes the width it has and
is only as tall as the room left over. */
--feltRoom: calc(100vh - var(--actionBarHeight, 0px) - 7rem);
width: 100%;
height: var(--feltRoom);
min-height: 7rem;
/* This branch sets its own height outright, so neither the aspect ratio nor the
cap derived from it has anything to say here — and the cap would only narrow
a table that is already as short as the screen allows. */
aspect-ratio: auto;
max-width: none;
}
/* The header ate 148px of a 390px-tall window, which was most of the reason the
table started below the fold. Here it only needs to be a title bar. */
#holdemHeader {
margin-bottom: 0.4rem;
gap: 0.35rem;
}
#holdemHeader h1 {
font-size: 1.05rem;
padding-bottom: 0.15rem;
}
.holdemSubtitle {
display: none;
}
/* There's no vertical room left, but plenty either side of a table this narrow,
so the toast moves out of the middle and sits beside the felt instead of on
top of it. Centred, it was covering up to six seats at once — and it carries
an Undo button, so a tap meant for a player was landing on "Undo". */
body.hasActionBar #toastHost,
#toastHost {
left: auto;
right: 0.5rem;
max-width: 17rem;
bottom: calc(var(--actionBarHeight, 0px) + 0.5rem + env(safe-area-inset-bottom, 0px));
}
/* Nine seats around a table this small overlap each other, so they shrink to suit. */
.seat {
width: 62px;
min-height: 40px;
}
}
/* ---------- Short phones, upright ----------
A 640px-tall screen spends ~150px on the header before the felt even starts, which
left the bottom of the table sitting behind the action bar until you scrolled. The
header carries no information a dealer needs mid-hand, so it gives the space up. */
@media (max-height: 700px) {
/* Nowhere on a screen this short is the toast clear of both the table and the
action bar, so it overlaps the one that costs least: it sits over the bar's
status line, which is information a dealer can re-read a second later, rather
than over the seats and buttons, which are things they need to hit. Overlapping
a readout beats covering a control. */
body.hasActionBar #toastHost {
bottom: calc(var(--actionBarHeight, 0px) - 3.1rem);
}
.toast {
padding: 0.5rem 0.7rem;
font-size: 0.78rem;
}
}
@media (max-width: 700px) and (max-height: 700px) {
#holdemHeader {
margin-bottom: 0.5rem;
gap: 0.4rem;
}
#holdemHeader h1 {
font-size: 1.15rem;
padding-bottom: 0.2rem;
}
.holdemSubtitle {
display: none;
}
}
/* ---------- Keyboard focus ----------
Three focus rules existed for the whole tool, so tabbing through it relied on
whatever ring the browser draws by default — which on these dark, custom-painted
controls is close to invisible. `:focus-visible` rather than `:focus` keeps it to
keyboard use: a dealer tapping Fold shouldn't be left with a ring on it. */
.holdemBtn:focus-visible,
.linkBtn:focus-visible,
.seatMenuBtn:focus-visible,
.seatSheetClose:focus-visible,
.pickerCard:focus-visible,
.actionBtn:focus-visible,
.raiseQuickBtn:focus-visible,
.actionUndo:focus-visible,
.toastAction:focus-visible,
.toastClose:focus-visible,
.potWizardChoice:focus-visible,
.cardSlot:focus-visible,
.subPanel summary:focus-visible,
.seat:focus-visible,
button:focus-visible,
[role="button"]:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
outline: 2px solid #7fc4ff;
outline-offset: 2px;
/* The ring has to sit above the neighbouring seat that overlaps this one. */
position: relative;
z-index: 3;
}
/* A sheet with nothing focusable in it takes focus itself so its title is read out.
That shouldn't paint a ring around the whole panel. */
[role="dialog"]:focus,
[role="alertdialog"]:focus {
outline: none;
}
/* ---------- Fields on a tablet ----------
The 16px minimum and the 44px touch targets were both written into the phone
media query, so every iPad size missed them — measured at 768, 834 and 1024 wide,
where all ten setup fields sat at 13.3px. iOS zooms the page whenever a field
under 16px takes focus, so a dealer on an iPad was getting the table zoomed every
time they touched the blind amount, then pinching back out. The reason for the
rule was never "narrow screen", it was "iOS", and an iPad is iOS.
Font size applies everywhere now — 13.3px was small for a settings field on a
desktop anyway. The touch sizing keys off `hover: none`, which is what actually
distinguishes a finger from a mouse; a tablet gets thumb-sized controls and a
desktop keeps its compact sidebar. */
.seatEditorForm input:not([type="checkbox"]):not([type="color"]),
.panelBody input:not([type="checkbox"]):not([type="color"]),
.subPanelBody input:not([type="checkbox"]):not([type="color"]),
.panelBody select,
.panelBody textarea {
font-size: 16px;
}
@media (hover: none) {
.seatEditorForm input:not([type="checkbox"]):not([type="color"]),
.panelBody input:not([type="checkbox"]):not([type="color"]),
.subPanelBody input:not([type="checkbox"]):not([type="color"]),
.panelBody select {
min-height: 44px;
padding: 0.4rem 0.6rem;
}
}
.toastClose::after {
content: "";
position: absolute;
inset: -7px;
}
/* A note under the raise entry when the amount typed isn't a legal raise. It informs
and never blocks: this tool records what the humans at the table actually did, and
a dealer overruling the arithmetic is right more often than the arithmetic is.
Under-minimum raises are the single most common thing that stops a home game, and
the bar used to accept one without a word. */
.raiseNote {
margin: 0.45rem 0 0;
font-size: 0.74rem;
line-height: 1.35;
text-align: center;
}
.raiseNote.raiseNoteWarn {
color: #f0b46a;
}
.raiseNote.raiseNoteInfo {
color: #9fc7e8;
}
/* On a short phone the raise pad already makes the bar tall enough to cover the
lowest seats, and the note must not make that materially worse. Measured at
360x640: bar 122px closed, 178px with the pad open, 217px with a note. Tightened
here so the note costs a line rather than a paragraph — the bar is a transient
input mode with Cancel and Escape, but it shouldn't grow for the sake of it. */
@media (max-height: 700px) {
.raiseNote {
margin-top: 0.3rem;
font-size: 0.68rem;
line-height: 1.25;
}
/* The pot breakdown is reference material — which side pots exist and who can win
them. While an amount is being typed it isn't what the dealer is reading, and
it's the tallest thing competing for the strip. */
body.hasActionBar #actionBar:has(.raisePad) .potBreakdown {
display: none;
}
}
/* ---------- The deal step ----------
Between hands the bar shows who will post and waits. Dealing used to happen the
instant the previous hand ended, which committed blinds while people were still
sitting down — and left the blind badges disagreeing with the money already in. */
.dealPrompt {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.dealPromptLine {
margin: 0;
text-align: center;
font-size: 0.8rem;
color: #c8c8c8;
line-height: 1.35;
}
.dealPromptLine.dealPromptBlocked {
color: #9a9a9a;
}
.actionBtn.actionDeal {
background: linear-gradient(180deg, #16783a, #0d5225);
border-color: #1e8449;
color: #ecffef;
font-weight: 600;
}
/* The quieter half of the deal prompt: things the deal is about to do that a dealer
would otherwise only discover afterwards. */
.dealPromptLine.dealPromptAside {
font-size: 0.72rem;
color: #9a9a9a;
}
/* An aside that has something to press. It wraps rather than squeezing the button:
on a phone the sentence takes the width it needs and the button drops under it,
still at full tap size, instead of being crushed to a sliver at the end of a line. */
.dealPromptLine.dealPromptDebt {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 0.4rem 0.55rem;
}
.dealPromptDebtBtn {
/* Sized down from a full action button — it's an option beside a sentence, not
the thing the dealer came here to press. "Post blinds & deal" stays the button
that looks like the button. */
flex: 0 0 auto;
min-height: 32px;
padding: 0.24rem 0.7rem;
font-size: 0.72rem;
background: rgba(245, 213, 71, 0.1);
border: 1px solid rgba(245, 213, 71, 0.45);
color: #f0d98c;
}
@media (max-width: 700px), (pointer: coarse) {
/* A fingertip needs 44px, and this one moves money. */
.dealPromptDebtBtn {
min-height: 44px;
padding: 0.4rem 0.9rem;
}
}
/* ---------- Touch targets on a wide touch screen ----------
Every touch-target rule in this file sits behind `max-width: 700px`, and width was
never the question. A phone held sideways is 844px wide, so none of that work
applied to it. Measured on an emulated touch device at 844x390, with the sidebar
panels open: twelve controls under 44px, including the rulings link at 91x14 and
the dedicated-dealer checkbox at 13x13.
Restated here against the pointer instead of the viewport. The width-scoped block
keeps its layout changes — one column, stacked dialogs — because those really are
about how much room there is. This is only about what a thumb can hit, so it asks
about the thumb. */
@media (pointer: coarse) {
.panelBody input:not([type="checkbox"]):not([type="color"]),
.subPanelBody input:not([type="checkbox"]):not([type="color"]),
.panelBody select {
font-size: 16px;
min-height: 44px;
padding: 0.4rem 0.6rem;
}
.panelBody .holdemBtnSmall,
.handHistoryActions .holdemBtn,
.toastAction,
.cardPickerHeader .holdemBtn,
.actionUndo {
min-height: 44px;
}
/* Destructive and tiny is the worst combination: these delete a chip colour or a
custom field, and are 28px wide. */
.removeChipColorBtn,
.removeFieldBtn {
min-width: 44px;
min-height: 44px;
}
/* flex-shrink would take a stated width straight back off again. */
.chipColorInput {
flex: none;
width: 44px;
height: 44px;
}
/* A bare checkbox can't grow into a target, but the row it sits in can — the
label is what a tap actually lands on. */
.panelBody .fieldRow {
min-height: 44px;
}
#dedicatedDealerToggle {
width: 24px;
height: 24px;
}
/* An underlined word is a 14px-tall target; give it room without making it look
like a button. */
.linkBtn {
display: inline-flex;
align-items: center;
min-height: 44px;
padding: 0 0.4rem;
}
}