Modal answers one question for all six overlays — where focus goes and how Tab is held — and this round it was measured as wrong in three directions. Its list of Tab stops could not see a <summary>. The rulings sheet is twelve of them and one Done button, so the trap decided Done was both ends of the ring: eighty Tab presses, one element ever reached, twelve rulings that could not be opened without a mouse. Adding `details > summary` gives the sheet its thirteen stops back. `:not([tabindex="-1"])` moves onto every line while it is open, because a control taken out of the Tab order still matches `button`. "The first control" was the card picker's Clear: the sheet a dealer opens to write a card down opened on the button that rubs one out. A sheet names its own landing spot now, and on five of six it is the heading — which says which seat, which card, which pot. The confirm dialog keeps a control, because a short question with one irreversible answer should land on the answer that leaves the dealer no worse off. And a sheet that redraws itself was deleting the control that held focus. Choosing a buy-in, picking a card, switching slots, confirming a side pot — all four left focus on the document body. Rebuilt controls carry a name now and are handed back to whoever had them; where there is no coming back, the caller says where to go instead. The card grid was fifty-two Tab stops of the fifty-six in that sheet. It is one now, with the arrows moving inside it, stepping over cards already dealt and stopping at the edges rather than wrapping onto a different rank. 1136 tests, 418 audits clean.
6143 lines
219 KiB
CSS
6143 lines
219 KiB
CSS
/* ---------- Design tokens ----------
|
||
Before this block the stylesheet used 42 different spacing values, 31 font sizes,
|
||
14 border-radii and 139 colours, and defined three custom properties. Every number
|
||
had been picked by hand at the moment it was needed, which is why the tool read as
|
||
crowded even on the screens where nothing was actually overlapping: a gap of 0.42rem
|
||
next to one of 0.45rem next to one of 0.5rem gives the eye no rhythm to lock onto,
|
||
so it reads the whole surface as noise.
|
||
|
||
The fix is not "less" — it is "fewer choices". Constrained scales are the point:
|
||
pick from these, never from thin air, and the layout falls into a rhythm without
|
||
anybody art-directing it.
|
||
|
||
Nothing here changes what the tool does. It is the vocabulary the rest of the file
|
||
is written in. */
|
||
:root {
|
||
/* Spacing. An 8px rhythm, because 8 divides cleanly into every device pixel ratio
|
||
and is what Material and HIG already tuned their components against. --sp-0 is
|
||
the sub-step for badges and chips where 4px is already too much. */
|
||
--sp-0: 0.125rem; /* 2px */
|
||
--sp-1: 0.25rem; /* 4px */
|
||
--sp-2: 0.5rem; /* 8px */
|
||
--sp-3: 0.75rem; /* 12px */
|
||
--sp-4: 1rem; /* 16px */
|
||
--sp-5: 1.5rem; /* 24px */
|
||
--sp-6: 2rem; /* 32px */
|
||
|
||
/* Type. Eight steps, and the two smallest exist only for the seat pucks, where the
|
||
label has to fit inside a 58px circle. Hierarchy is built with weight and colour
|
||
below, not by inventing another size. */
|
||
--fs-2xs: 0.625rem; /* 10px — puck micro-labels */
|
||
--fs-xs: 0.6875rem; /* 11px */
|
||
--fs-sm: 0.75rem; /* 12px */
|
||
--fs-base: 0.8125rem; /* 13px — body copy */
|
||
--fs-md: 0.9375rem; /* 15px */
|
||
--fs-lg: 1.125rem; /* 18px */
|
||
--fs-xl: 1.375rem; /* 22px */
|
||
--fs-2xl: 1.75rem; /* 28px — page title */
|
||
|
||
/* Weights. Two, plus the body default. */
|
||
--fw-medium: 600;
|
||
--fw-bold: 700;
|
||
|
||
/* Corners. */
|
||
--r-sm: 6px;
|
||
--r-md: 10px;
|
||
--r-lg: 16px;
|
||
--r-pill: 999px;
|
||
|
||
/* Surfaces. Dark grey rather than black: shadows are invisible on true black, so
|
||
depth has to come from a lighter surface, and #000 against light text is the
|
||
contrast pairing that smears on OLED. Four steps is enough for a felt, a panel,
|
||
a row and a control. */
|
||
--surface-0: #141414; /* page behind everything */
|
||
--surface-1: #1a1a1a; /* panels */
|
||
--surface-2: #232323; /* rows, fields, resting controls */
|
||
--surface-3: #2c2c2c; /* raised: hover, the seat puck */
|
||
--line: #3a3a3a;
|
||
--line-strong: #4a4a4a;
|
||
|
||
/* Text.
|
||
These two were picked to clear WCAG AA — several rules in this file still say so
|
||
in their own comments, one of them "measured 4.5:1 at #a5a5a5 exactly, so it sits
|
||
above that". That was the right instinct with the wrong instrument. WCAG 2's
|
||
ratio is a luminance gap, and a luminance gap overstates readability once one of
|
||
the colours is near black, which every reading on this page is: the page is
|
||
#141414 and the panels are darker than the rows. Tuning to 4.5:1 against a
|
||
near-black ground lands on text that passes the audit and still reads as a smudge
|
||
across a table in a dim room.
|
||
|
||
Re-measured with APCA, which scores the pair for the size and weight it is
|
||
actually drawn at: #9a9a9a on a panel came out at Lc 46 and #8a8a8a at Lc 38,
|
||
against a floor of Lc 60 for a label and Lc 75 for anything read to make a
|
||
decision. Fifty-four separate pieces of text were below their level while passing
|
||
AA. These are the values that clear it — muted at Lc ~71, dim at Lc ~59.
|
||
|
||
They are lighter than they were, and the hierarchy did not come out of it: --text
|
||
is still Lc 95, so the three tiers are 95 / 71 / 59, which is a wider spread than
|
||
the 95 / 46 / 38 it replaces. Fading is not the only way to say "secondary", and
|
||
on a dark ground it is the most expensive one — size and weight say it without
|
||
spending contrast, which is what the seat puck does now. */
|
||
--text-bright: #f5f5f5;
|
||
--text: #eee;
|
||
--text-muted: #c8c8c8;
|
||
--text-dim: #b4b4b4;
|
||
|
||
/* The one accent. Desaturated against the dark ground — a fully saturated gold
|
||
vibrates on a near-black surface and stops reading as a highlight. */
|
||
--gold: #d8b46a;
|
||
--gold-bright: #f0d9a8;
|
||
--gold-deep: #a9782f;
|
||
--gold-tint: #3a2f18;
|
||
--gold-shade: #241d0f;
|
||
|
||
/* The page is #141414 and never said so.
|
||
|
||
Everything above is a colour this stylesheet paints. `color-scheme` is the half
|
||
the stylesheet cannot paint: the browser draws the checkboxes, the dropdown a
|
||
<select> opens, the scrollbars, the caret, the spellcheck underline and the
|
||
autofill wash itself, and with no scheme declared it draws all of them for a
|
||
light page. Measured before this line: `color-scheme: normal`, three checkboxes
|
||
rendering as bright white squares in the Table Setup panel — the loudest objects
|
||
on a near-black screen, brighter than the gold — and a <select> whose menu opens
|
||
white over the felt.
|
||
|
||
One declaration moves every one of them, and it moves them *natively*, which is
|
||
the part worth having: a control the browser draws still answers Windows High
|
||
Contrast Mode, still matches the platform a dealer already knows, and cannot
|
||
lose its tick to a stylesheet. See .potWizardChoice input for what painting one
|
||
by hand costs, and what it has to pay back.
|
||
|
||
--accent-on is the checked colour, and it is the green this tool already uses for
|
||
yes: the same #1e8449 the pot wizard's hand-drawn box fills with, so the one
|
||
painted checkbox and the six native ones agree. Chrome picks the tick's own
|
||
colour from it, and against a green this dark that is white. */
|
||
--accent-on: #1e8449;
|
||
|
||
color-scheme: dark;
|
||
accent-color: var(--accent-on);
|
||
}
|
||
|
||
/* Every number that changes while the dealer is looking at it — the pot, the stacks,
|
||
the bets on the felt, the clock — is set in tabular figures. In a proportional face
|
||
a "1" is narrower than an "8", so a pot ticking $18 -> $21 shuffles the digits
|
||
sideways and the eye reads movement where there is only arithmetic. */
|
||
#potBadgeAmount,
|
||
.seatPuckStack,
|
||
.betChipAmount,
|
||
.actionPot,
|
||
.actionClockBtn,
|
||
.standingsRow,
|
||
.bankReconcileRow,
|
||
.potLayerAmount,
|
||
.handHistoryRow,
|
||
.handHistoryPot,
|
||
input[type="number"] {
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
/* Read aloud, never drawn. `display: none` and `visibility: hidden` both take an
|
||
element out of the accessibility tree along with the layout, which would silence the
|
||
live region entirely — so it stays in the tree at 1px, clipped. */
|
||
.visuallyHidden {
|
||
position: absolute;
|
||
width: 1px;
|
||
height: 1px;
|
||
margin: -1px;
|
||
padding: 0;
|
||
overflow: hidden;
|
||
clip-path: inset(50%);
|
||
white-space: nowrap;
|
||
border: 0;
|
||
}
|
||
|
||
/* Hold the scrollbar's width whether or not there is one to show. A panel opening far
|
||
enough to make the page scrollable used to narrow the viewport by the scrollbar's
|
||
width, which re-centres the felt and nudges every line of text left. No effect on
|
||
phones, where scrollbars float over the content — this is for the desktop the tool
|
||
also gets run on. */
|
||
html {
|
||
scrollbar-gutter: stable;
|
||
}
|
||
|
||
body {
|
||
background: radial-gradient(ellipse at 50% -10%, var(--surface-1) 0%, var(--surface-0) 55%, #0a0a0a 100%);
|
||
min-height: 100vh;
|
||
margin: 0;
|
||
color: var(--text);
|
||
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. */
|
||
|
||
/* Nothing on this page moves under the pointer for anyone who asked it not to.
|
||
`:hover` and `:active` together, because they are the same 1px of travel and the
|
||
setting does not distinguish them.
|
||
|
||
This list used to hold nine `:active` selectors and its membership looked arbitrary,
|
||
because it was: it was written to stop a *stuck press state* when a tap turns into a
|
||
scroll, and it reached for the reduced-motion query as the nearest available proxy.
|
||
Solving one problem with another's tool is how it came to cover `.dealerChip` and
|
||
`.raiseQuickBtn` while missing `.actionBtn` — the class on Fold, Call, Raise and Post
|
||
blinds & deal, which is to say the most-tapped control in the tool — and every
|
||
`:hover` in the file.
|
||
|
||
Read as its own rule rather than as a side effect of that one, the membership is not
|
||
a judgement call at all: every `:hover` and `:active` transform in the stylesheet
|
||
belongs here. reducedMotion.test.js scans for them and fails on any that is missing,
|
||
so a new one cannot be added without this being updated. */
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.holdemBtn:hover,
|
||
.holdemBtn:active:not(:disabled),
|
||
.linkBtn:active:not(:disabled),
|
||
.seatMenuBtn:active:not(:disabled),
|
||
.seatSheetClose:active:not(:disabled),
|
||
.cardSlot:hover,
|
||
.cardSlot:active,
|
||
.pickerCard:hover:not(:disabled),
|
||
.pickerCard:active:not(:disabled),
|
||
.dealerChip:active,
|
||
.raiseQuickBtn:active:not(:disabled),
|
||
.actionBtn:active,
|
||
.actionUndo:active:not(:disabled),
|
||
.toastAction:active,
|
||
.toastClose:active,
|
||
.potWizardChoice:active {
|
||
transform: none;
|
||
}
|
||
|
||
/* The seat sheet's disclosure arrows keep their two positions and lose the swing
|
||
between them. Not in the list above — nothing here is under a pointer, and the
|
||
end state is meaningful: an arrow that pointed the same way open and shut would
|
||
have stopped saying anything.
|
||
|
||
The scrim loses its fade with it, and the delay goes with the fade: with nothing
|
||
to cover, a quarter second of visibility held open is a scrim still painted over
|
||
a sheet that has already gone. `none` flips it at once in both directions, which
|
||
is what the sheet's own reduced-motion rule concluded for the same reason. */
|
||
.seatSubPanel > summary::before,
|
||
#seatSheetScrim {
|
||
transition: none;
|
||
}
|
||
}
|
||
|
||
/* ---------- Border-box, everywhere ----------
|
||
A width is the width the thing is drawn at. Under the default `content-box` it isn't:
|
||
padding and border are added on afterwards, so `width: 192px` on a padded panel draws
|
||
217, and every other rule that reserved room for it was 25px short in the same
|
||
direction.
|
||
|
||
That is not hypothetical here. Nine elements in this file already opt in one at a
|
||
time — the seat puck's comment explains that it had to, because the search that sizes
|
||
it compares the number against distances on the felt — and the two that hadn't both
|
||
went wrong in the same afternoon: the action rail overran the page's padding by 25px
|
||
and put Resolve Pot underneath itself, and the raise pad's amount field at `100%`
|
||
plus 16px of padding pushed a horizontal scrollbar across a 640px phone.
|
||
|
||
Opting in one element at a time leaves the trap set for the next one. This is the
|
||
inherited form, so a new element is border-box unless it says otherwise, and the
|
||
`inherit` means anything that does say otherwise still governs its own children. */
|
||
*,
|
||
*::before,
|
||
*::after {
|
||
box-sizing: inherit;
|
||
}
|
||
|
||
#holdemApp,
|
||
#actionBar,
|
||
#toastHost,
|
||
[id$="Overlay"] {
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
:root {
|
||
/* The sidebar's width, named because three rules need to agree about it: the grid
|
||
that draws the column, the action bar that has to stay out of it, and the seat
|
||
sheet's right edge. */
|
||
--sidebarW: 320px;
|
||
}
|
||
|
||
#holdemApp {
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
padding: var(--sp-6);
|
||
}
|
||
|
||
/* ---------- Header ---------- */
|
||
|
||
#holdemHeader {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
gap: var(--sp-4);
|
||
margin-bottom: var(--sp-5);
|
||
}
|
||
|
||
#holdemHeader h1 {
|
||
margin: 0;
|
||
font-size: var(--fs-2xl);
|
||
font-weight: 300;
|
||
letter-spacing: 0.04em;
|
||
color: var(--text-bright);
|
||
position: relative;
|
||
display: inline-block;
|
||
padding-bottom: var(--sp-2);
|
||
}
|
||
|
||
#holdemHeader h1::after {
|
||
content: "";
|
||
position: absolute;
|
||
left: 0;
|
||
bottom: 0;
|
||
width: 42px;
|
||
height: 3px;
|
||
border-radius: 2px;
|
||
background: linear-gradient(90deg, var(--gold), var(--gold-deep));
|
||
}
|
||
|
||
/* The way back to the site. It used to be a button in the action row, the same size
|
||
and weight as Resolve Pot; navigation is not an action, and putting it in that row
|
||
cost a sixth of the row's width to say "← Casino". Up here it is where a back link
|
||
goes, and it reads as one. */
|
||
.holdemBackLink {
|
||
/* Reads as a quiet link, hits like a control. As bare text it drew a 56x14 target
|
||
— the audit caught it at every viewport — and a back link a thumb misses is
|
||
worse than the button it replaced. The padding is the hit area; the negative
|
||
margins give most of it back to the layout, so 44px of reachable link costs the
|
||
header about eight. */
|
||
display: inline-flex;
|
||
align-items: center;
|
||
min-height: 44px;
|
||
padding: 0 var(--sp-2);
|
||
margin: calc(var(--sp-3) * -1) calc(var(--sp-2) * -1) calc(var(--sp-2) * -1);
|
||
color: var(--text-dim);
|
||
font-size: var(--fs-sm);
|
||
text-decoration: none;
|
||
letter-spacing: 0.03em;
|
||
}
|
||
|
||
@media (hover: hover) {
|
||
.holdemBackLink:hover {
|
||
color: var(--gold);
|
||
}
|
||
}
|
||
|
||
/* The word beside the arrow, which a phone drops. Below 700px the link is the arrow
|
||
and a 44px box around it; the name a screen reader gets is on the <a> either way. */
|
||
.backLinkWord {
|
||
white-space: nowrap;
|
||
}
|
||
|
||
#holdemHeaderActions {
|
||
display: flex;
|
||
gap: var(--sp-2);
|
||
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, var(--surface-3), var(--surface-2));
|
||
color: var(--text);
|
||
border: 1px solid var(--line);
|
||
border-radius: var(--r-sm);
|
||
padding: var(--sp-2) var(--sp-4);
|
||
font-size: var(--fs-base);
|
||
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: var(--sp-1);
|
||
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, var(--line), #2a2a2a);
|
||
border-color: var(--line-strong);
|
||
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: var(--sp-1) var(--sp-2);
|
||
font-size: var(--fs-sm);
|
||
}
|
||
|
||
.holdemBtn:disabled {
|
||
opacity: 0.4;
|
||
cursor: not-allowed;
|
||
transform: none;
|
||
}
|
||
|
||
.handHistoryActions {
|
||
display: flex;
|
||
gap: var(--sp-2);
|
||
margin-top: var(--sp-2);
|
||
}
|
||
|
||
/* ---------- Layout ---------- */
|
||
|
||
#holdemLayout {
|
||
display: grid;
|
||
grid-template-columns: 1fr var(--sidebarW);
|
||
gap: var(--sp-5);
|
||
align-items: start;
|
||
}
|
||
|
||
#tableSection {
|
||
min-width: 0;
|
||
}
|
||
|
||
.tableHint {
|
||
color: var(--text-muted);
|
||
font-size: var(--fs-base);
|
||
text-align: center;
|
||
margin-top: var(--sp-4);
|
||
}
|
||
|
||
/* `hidden` means hidden.
|
||
|
||
The attribute only carries `display: none` from the browser's own stylesheet, which
|
||
is the weakest rule there is: any class that sets a display — and every button here
|
||
sets `inline-flex` — beats it. That is not a hypothetical. Seven overlays in this
|
||
file already carry their own `[hidden] { display: none }` because each one was found
|
||
the hard way, and the eighth was a button that shipped as a 21x44 empty box under the
|
||
bank check on every table that didn't need it.
|
||
|
||
One rule, for everything, so there is no ninth. */
|
||
[hidden] {
|
||
display: none !important;
|
||
}
|
||
|
||
/* ---------- 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. */
|
||
|
||
/* The middle of the felt: the board above, the pot below it. One box rather than two
|
||
loose ones, because the bet chips have to keep clear of whatever is in the middle and
|
||
that is a single question about a single rectangle — see feltCentreBox() in render.js. */
|
||
#tableCentre {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: var(--sp-2);
|
||
min-width: 0;
|
||
/* The real ceiling is set in pixels by sizeTableCentre(), from the room the seat
|
||
ring actually leaves. This is the answer before the first measurement and the
|
||
backstop if there is nothing to measure. */
|
||
max-width: 86%;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* The community cards, in the order they were turned. Sized off the felt rather than
|
||
the viewport: the same five cards have to sit on a 248px phone table and an 839px
|
||
desktop one, and a fixed width is wrong at one end or the other. */
|
||
/* Where the board goes when the middle of the felt cannot hold it — see placeBoard()
|
||
in render.js. Its own strip above the table, at the same size, with nothing over it. */
|
||
#boardStrip {
|
||
display: flex;
|
||
justify-content: center;
|
||
margin-bottom: var(--sp-2);
|
||
}
|
||
|
||
#boardStrip[hidden] {
|
||
display: none;
|
||
}
|
||
|
||
#boardStrip #boardRow {
|
||
width: auto;
|
||
max-width: 100%;
|
||
}
|
||
|
||
#boardStrip .boardCard {
|
||
flex: 0 0 auto;
|
||
min-width: 44px;
|
||
min-height: 48px;
|
||
}
|
||
|
||
#boardRow {
|
||
display: flex;
|
||
gap: var(--sp-1);
|
||
justify-content: center;
|
||
width: 100%;
|
||
max-width: 100%;
|
||
}
|
||
|
||
#boardRow[hidden] {
|
||
display: none;
|
||
}
|
||
|
||
.boardCard {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
/* They share whatever the middle of the felt turned out to be, down to a floor a
|
||
rank and a suit still fit in. Every one of these is a control that opens the
|
||
picker, so the floor is a thumb's worth of card rather than a legible one. */
|
||
flex: 1 1 0;
|
||
min-width: 24px;
|
||
max-width: 44px;
|
||
min-height: 44px;
|
||
padding: var(--sp-0) var(--sp-1);
|
||
border-radius: var(--r-sm);
|
||
border: 1px solid var(--line-strong);
|
||
background: linear-gradient(180deg, #f4f1e8, #ded9cb);
|
||
color: #16181c;
|
||
font-family: inherit;
|
||
font-size: var(--fs-md);
|
||
font-weight: 700;
|
||
line-height: 1.05;
|
||
cursor: pointer;
|
||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.45);
|
||
}
|
||
|
||
.boardCard .boardCardSuit {
|
||
font-size: var(--fs-sm);
|
||
font-weight: 400;
|
||
}
|
||
|
||
/* A playing card is a narrow thing and a control is a 44px thing, and on a felt with
|
||
150px of middle to share between five of them the two cannot both be true of the
|
||
drawn card. Same answer as the toast's dismiss button and the dealer puck: the hit
|
||
area grows past the paint. Five cards in a row means the overlap can only be
|
||
sideways, so it reaches out rather than all round, and the gap between two cards
|
||
belongs half to each. */
|
||
.boardCard {
|
||
position: relative;
|
||
}
|
||
|
||
.boardCard::after {
|
||
content: "";
|
||
position: absolute;
|
||
top: -2px;
|
||
bottom: -2px;
|
||
left: -10px;
|
||
right: -10px;
|
||
}
|
||
|
||
/* Except on the strip, where the board has the whole width of the page and can simply
|
||
be big enough. */
|
||
#boardStrip .boardCard::after {
|
||
content: none;
|
||
}
|
||
|
||
.boardCard.cardRed {
|
||
color: #b3202b;
|
||
}
|
||
|
||
/* A card not yet turned. Dashed and empty on the felt itself rather than a white
|
||
rectangle, so a board half dealt reads as three cards and two gaps at a glance —
|
||
which is the same language an empty chair already speaks on this table. */
|
||
.boardCard.boardCardEmpty {
|
||
background: rgba(0, 0, 0, 0.28);
|
||
border-style: dashed;
|
||
border-color: rgba(255, 255, 255, 0.32);
|
||
color: rgba(255, 255, 255, 0.5);
|
||
box-shadow: none;
|
||
font-weight: 400;
|
||
}
|
||
|
||
@media (hover: hover) {
|
||
.boardCard:hover {
|
||
border-color: var(--gold);
|
||
}
|
||
}
|
||
|
||
/* The disc is deeper than it was. Its bright stop left the label sitting on a mid green
|
||
that measured Lc 38 and — the reading that settles it — 3.79:1, which is a plain AA
|
||
failure the audit had never reported because it reads background-color and this is a
|
||
gradient over the felt.
|
||
Deepening the disc rather than bleaching the label is what keeps the accent: gold on
|
||
a dark green is the pairing this whole page is built on, and gold on a mid green is
|
||
the one place it wasn't. The pot is also the number read from furthest away, so a
|
||
disc that separates harder from the felt is the right answer twice over.
|
||
|
||
Both stops, not just the far one. Deepening only the outer stop moved the label by
|
||
two points of Lc and no more, because the highlight is at 32%/28% and that is exactly
|
||
where the label sits — the reading was being taken on the brightest part of the disc.
|
||
The highlight is still there, it is just a lift within a dark disc now rather than a
|
||
wash of white over a mid green. */
|
||
#potBadge {
|
||
position: relative;
|
||
background: radial-gradient(circle at 32% 28%, rgba(0, 0, 0, 0.34), rgba(0, 0, 0, 0.78));
|
||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||
border-radius: var(--r-pill);
|
||
padding: var(--sp-3) var(--sp-5);
|
||
text-align: center;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--sp-0);
|
||
box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.6), 0 4px 14px rgba(0, 0, 0, 0.45);
|
||
}
|
||
|
||
/* --gold is tuned to sit on the near-black of the panels and the bar. The pot badge is
|
||
the one place it lands on green instead, and green is the worst possible ground for
|
||
it: measured Lc 38 there against Lc 70-odd everywhere else the same token is used.
|
||
The brighter step of the same accent clears it without the badge losing the accent —
|
||
white would read as a second amount next to the one underneath it. */
|
||
#potBadgeLabel {
|
||
font-size: var(--fs-xs);
|
||
letter-spacing: 0.14em;
|
||
text-transform: uppercase;
|
||
color: var(--gold-bright);
|
||
}
|
||
|
||
#potBadgeAmount {
|
||
font-size: var(--fs-xl);
|
||
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: var(--sp-1);
|
||
padding: var(--sp-0) var(--sp-2) var(--sp-0) var(--sp-1);
|
||
background: rgba(0, 0, 0, 0.55);
|
||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||
border-radius: var(--r-pill);
|
||
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: var(--fs-2xs);
|
||
font-weight: 700;
|
||
color: var(--text-bright);
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
/* Clinging to a puck rather than sitting on its own ring — the phone layout.
|
||
|
||
It has to fit the puck it is clinging to, which it did not: measured on a Galaxy S8,
|
||
the puck was 48px across and every one of the five chips on the felt was 50, so each
|
||
one hung over both edges of its own seat and three of them reached onto the seat next
|
||
door. The same fault the small blind badge had, on the other thing that hangs off a
|
||
puck — and the one that carries a number rather than two letters.
|
||
|
||
The disc is what gives way. It is 13px of decoration plus a 4px gap next to a figure
|
||
the dealer has to read, and dropping it takes the chip from 50px to 33 — inside the
|
||
narrowest puck the layout will ever draw. Its colour moves to the chip's own rim, so
|
||
the denomination the legend assigns is still there at no width at all. Same trade the
|
||
tight puck already makes with the hole cards. */
|
||
.betChipAnchored {
|
||
left: 50%;
|
||
z-index: 3;
|
||
max-width: 100%;
|
||
padding: var(--sp-0) var(--sp-1);
|
||
border-color: var(--betChipColour, rgba(255, 255, 255, 0.14));
|
||
}
|
||
|
||
.betChipAnchored .betChipDisc {
|
||
display: none;
|
||
}
|
||
|
||
/* Where along the puck's edge it lands is set by render.js, which walks the seat's own
|
||
spoke out to just past the puck — see anchoredChipOffset(). The `top` here is only what
|
||
it wears before that lands, and on the one path where there is nothing to measure. */
|
||
.betChipAnchored {
|
||
top: -11px;
|
||
transform: translate(-50%, -50%);
|
||
}
|
||
|
||
/* ---------- Seats (compact pucks on the table) ---------- */
|
||
|
||
.seat {
|
||
position: absolute;
|
||
transform: translate(-50%, -50%);
|
||
/* Set by render.js, which measures the widest puck that lets every seat keep its
|
||
own box on this particular felt. The 96px is what a table gets before the first
|
||
measurement lands, and what it keeps whenever there is room for it.
|
||
|
||
border-box so the number means the width the puck is drawn at. As content-box it
|
||
did not: a 64px puck measured 74 across once its padding and border were counted,
|
||
and the search that sets this compares it against distances on the felt. */
|
||
box-sizing: border-box;
|
||
width: var(--seatPuckW, 96px);
|
||
/* Every min-height on a seat is a drawn height now, border and padding included,
|
||
because of the box-sizing above. They were content heights before, so each one
|
||
here and in the three blocks that override it has gained the 8px of padding and
|
||
2px of border it used to be quoted without — the puck is the size it always was.
|
||
Caught by the audit rather than by reading: an empty chair went from a 52px tap
|
||
target to a 42px one on every phone in the sweep, which is under the floor. */
|
||
min-height: 64px;
|
||
background: linear-gradient(160deg, var(--surface-3), #1b1b1b);
|
||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||
border-radius: var(--r-lg);
|
||
padding: var(--sp-1) var(--sp-2);
|
||
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 var(--gold);
|
||
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, var(--surface-2), #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);
|
||
}
|
||
|
||
/* An empty seat used to be a 2%-white wash at 0.7 opacity, which is another way of
|
||
saying it had no background of its own and whatever the felt was doing behind it
|
||
decided how readable its label was. The felt is a gradient with a brown rim, so that
|
||
varied by chair: the same #aeaeae chair number measured Lc 28 on the rim, Lc 30 on
|
||
the dark green and Lc 33 further in. Text whose contrast depends on where the seat
|
||
happens to sit cannot be reasoned about, and the fade was making every part of it
|
||
worse at once.
|
||
|
||
A ground of its own fixes both. The dashed border and the EMPTY label are what say
|
||
"open" — they always were — so the fade was not carrying the meaning, only costing
|
||
contrast. It stays louder than seatInactive, which is what the old comment here was
|
||
protecting and is still true.
|
||
|
||
0.62 and not the 0.82 this was first written at. Contrast turns out to be almost flat
|
||
across that range — Lc 69.6 against the rim at 0.62 versus 71.7 at 0.82, because the
|
||
label is light and every ground in that range is dark — so the heavier wash bought
|
||
about two points of Lc and cost four dark rectangles sitting on the felt like holes.
|
||
Taking the lighter one is free. */
|
||
.seat.seatEmpty {
|
||
background: rgba(0, 0, 0, 0.28);
|
||
border-style: dashed;
|
||
border-color: rgba(255, 255, 255, 0.2);
|
||
box-shadow: none;
|
||
}
|
||
|
||
/* The ground moved off the chair and under the one thing on it.
|
||
|
||
An open chair was drawn as a whole puck of near-black on the green — the same box a
|
||
seated player gets, at the same weight, saying nothing. Five players nine-handed is
|
||
four of them, and on the felt they read as holes punched in the table rather than as
|
||
chairs nobody is in: the eye has to sort the loud empty boxes from the loud full ones
|
||
before it can find who is in the hand.
|
||
|
||
The wash is off the chair now and under its number, which is the only thing an open
|
||
chair has to say. That keeps what the wash was actually for — the number's contrast
|
||
cannot depend on whether its chair happens to sit over the rim, the dark green or the
|
||
light, which is a difference of about Lc 5 across one ring — while giving the felt
|
||
back four boxes' worth of quiet. The dashes and the outline still say "open", which
|
||
they always did, and the box a thumb lands on has not moved a pixel.
|
||
|
||
Centred rather than in the corner, because on an empty chair the number is the label
|
||
and not a reference mark on something else. */
|
||
.seatEmpty .seatChairNumber {
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
min-width: 18px;
|
||
padding: 0 var(--sp-1);
|
||
border-radius: var(--r-pill);
|
||
background: rgba(18, 18, 18, 0.72);
|
||
text-align: center;
|
||
}
|
||
|
||
.seatEmptyLabel {
|
||
color: var(--text-dim);
|
||
font-size: var(--fs-2xs);
|
||
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);
|
||
}
|
||
|
||
/* The chair number answers "which seat is seat 6?", and it is the smallest text on the
|
||
felt. It was set to a hand-picked #aeaeae chosen for being a hair over 4.5:1 — see
|
||
the token block for why that bar was the wrong one, and the rule above for why the
|
||
fade behind it made the reading meaningless anyway. On the token now, so it moves
|
||
with the rest of the page instead of being pinned to a number from one measurement. */
|
||
.seatChairNumber {
|
||
position: absolute;
|
||
top: 2px;
|
||
left: 6px;
|
||
font-size: var(--fs-2xs);
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
/* ---- The puck is a stack of rows, and every row is the height of a row ----
|
||
|
||
Nine chairs on a ring, and until this block no two of them were the same size. A puck
|
||
was as tall as whatever happened to be written in it: measured at 1280x900 with five
|
||
players seated, 64px for Bob, 70 for Christopher because his name wrapped, 71 for
|
||
Alexandra because she had chips out, 89 for "Mike from work" who had both. Twenty-five
|
||
pixels of difference around one ring, and the ragged edge was the smaller half of it.
|
||
|
||
The larger half is that the sizes *moved*. A puck grew 17px the moment its player put
|
||
money in and shrank again when the street closed, so the felt reflowed twice a hand
|
||
under a dealer's thumb — and the ring is sized against the tallest puck on it, which
|
||
meant every one of those changes could resize the whole table.
|
||
|
||
Three rows are held open instead: the name, the amount, and the stack. Each is a token
|
||
rather than an em, so an amount that steps its type down to fit (see .puckAmountLong)
|
||
shortens its digits and not its row. What the puck says changes all night; how big it
|
||
is does not.
|
||
|
||
Sized so it costs nothing. The ring already reserved room for the tallest puck at every
|
||
seat — measuredSeatBox() takes the maximum, on purpose — so drawing the short ones at
|
||
that height spends room that was already spent. What pays for the third row is the
|
||
second line of name going away above: uniform-with-two-lines was 88px, which an earlier
|
||
round measured and rightly refused. Uniform-with-one is 71. */
|
||
/* The two sizes a puck is written at, named so that the steps below them are steps *of*
|
||
them rather than separate sizes to be kept in agreement — and capped, which is the one
|
||
place in this stylesheet where type does not follow the browser's font size.
|
||
|
||
Why it has to be. Every row above is a `px` token so that an amount stepping its type
|
||
down shortens its digits and not its row — but a token is a floor, not a ceiling, and
|
||
text bigger than its row simply makes the row taller. Turn a phone's text up to the
|
||
125% its slider offers and --fs-2xs goes from 10px to 12.5, the two money rows go from
|
||
14px each to 17.5, and the puck grows with them. At 150% the figures were being cut
|
||
off by the puck's own edge; on the tightest ring, where twelve pucks are already at
|
||
the 44px a thumb needs and there is nowhere narrower to go, they came down on each
|
||
other instead — the sweep measured seat 3's "$1,495" five-sevenths under seat 4's BB
|
||
badge at every phone in the list.
|
||
|
||
`min(…, Npx)` is exactly the size each of these has always been drawn at, so nothing
|
||
changes for anybody at an ordinary text size: the cap only ever engages where the
|
||
alternative is a figure clipped in half or two seats printed on top of each other.
|
||
And what it holds is the one thing on the page that is also somewhere that does
|
||
scale — the Standings panel lists every stack in full, at whatever size its owner
|
||
asked for. The felt is a diagram; its cells are sized by the table, and their writing
|
||
has to fit inside them. That rule already governed a puck's width. This is the same
|
||
rule for its height.
|
||
|
||
.seatsRoomy is the exception, below: a puck past the ordinary ceiling has the room. */
|
||
#seatsLayer {
|
||
--seatRowName: 15px; /* 12px of name at 1.2 */
|
||
--seatRowMoney: 14px; /* 10px of figures at 1.4 */
|
||
--seatNameSize: min(var(--fs-sm), 12px);
|
||
--seatAmountSize: min(var(--fs-2xs), 10px);
|
||
}
|
||
|
||
/* One line, and the ellipsis is not what saves it.
|
||
A 96px puck fits about nine characters, so "Christopher" came out as "Chri…" and
|
||
"Mike from work" as "Mike fro…" — and "from work" is the half a dealer typed to tell
|
||
two Mikes apart, so the ellipsis was eating exactly the part that carried the
|
||
information. The second line was the first answer to that. fitSeatNames() is the
|
||
better one: it drops whole trailing words, so the same name becomes "Mike", which is
|
||
what the table calls him and reads at a glance rather than being looked up.
|
||
|
||
The second line is what made the pucks different sizes from each other, and its cost
|
||
was never only the pixels. See the row heights below. */
|
||
.seatPuckName {
|
||
/* The size a name is read at. Dropping a word is what deals with length first,
|
||
because a word is a unit a dealer recognises and half a step of type is not; the
|
||
type only moves for the name with no word left to drop — see .puckNameTight. */
|
||
font-size: var(--seatNameSize);
|
||
font-weight: 600;
|
||
color: var(--text-bright);
|
||
/* One line because it cannot wrap, not because a second line is hidden afterwards.
|
||
|
||
A `-webkit-box` clamped to one line did the job for as long as the row was exactly
|
||
one line tall, and it stopped the moment the type inside it moved. The row is a
|
||
`px` token — 15px, "12px of name at 1.2" — while .puckNameTight steps the name down
|
||
to 10.2px, whose line box is 12.24. The clamp hides the lines past the first; it
|
||
does not hide the 2.76px of the second one that the row's own min-height leaves
|
||
uncovered. Measured on a 390px phone: "Christopher" on the button seat drew as
|
||
"Christ…" with the ascenders of "opher" showing underneath it as a stray mark — a
|
||
name that read as a rendering fault on the one seat a dealer looks at first.
|
||
|
||
`nowrap` has no second line to leak. It also makes the box measure honestly, which
|
||
the clamp did not: a name that wrapped reported scrollWidth == clientWidth, so
|
||
fitSeatNames() was detecting overflow by height and never by width — right answer,
|
||
wrong question, and only ever right while the row and the line agreed. */
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
line-height: 1.2;
|
||
/* The row is as tall as the row, whether or not there is a name in it — see the
|
||
token block above .seatPuckStack. An open chair's blank label held no height at
|
||
all, so the one puck on the ring with nobody in it was the one puck a different
|
||
size from the rest. */
|
||
min-height: var(--seatRowName);
|
||
margin-top: var(--sp-2);
|
||
}
|
||
|
||
/* The name has no gutter reserved in it any more, on any seat.
|
||
|
||
It used to have one on the button seat: `padding-right: 25px`, the dealer chip's 22px
|
||
plus the 3px it was inset by, taken off the name's line because the chip sat inside the
|
||
puck's top-right corner on a layer above the text. Three attempts lived in this spot —
|
||
a float, then a float with an indent, then the padding — and all three were answers to
|
||
the same question, which was the wrong question. The chip has moved to the puck's rim
|
||
instead; see .dealerChip.
|
||
|
||
What the gutter cost, measured across the sweep's tables with the twelve packed names:
|
||
|
||
Galaxy S8 9-handed name box 56px 11 of 12 names fit — 7 on the button seat
|
||
Galaxy S8 12-handed name box 34px 7 of 12 names fit — 1 on the button seat
|
||
iPhone 14 9-handed name box 68px 11 of 12 names fit — 7 on the button seat
|
||
desktop 9-handed name box 105px 12 of 12 names fit — 11 on the button seat
|
||
|
||
Four names in twelve, on a phone, cut on one seat and whole on the other eight. And the
|
||
button is the one marker on the felt that moves every hand, so that is not four unlucky
|
||
players — it is every player in turn, once a round, all night. */
|
||
|
||
/* The name with no word left to drop.
|
||
|
||
fitSeatNames() shortens "Mike from work" to "Mike", which is what the table calls him.
|
||
It can do nothing at all with "Christopher", and what was left for that name was the
|
||
ellipsis — "Christop…" — on a felt that may well also have a Christine at it.
|
||
|
||
This is the rule the amounts have had all along, applied to the other thing a dealer
|
||
reads across a table: shrink rather than abbreviate, because a name read wrong is a
|
||
worse failure than a name read small. One step only, and floored, because past about
|
||
9px it stops being legible from across a table and an honest ellipsis is better than
|
||
an unreadable word. The row it sits in does not move — see --seatRowName. */
|
||
.seatPuckName.puckNameTight {
|
||
font-size: max(9px, calc(var(--seatNameSize) * 0.85));
|
||
}
|
||
|
||
.seatPuckCards {
|
||
font-size: var(--fs-xs);
|
||
font-weight: 700;
|
||
font-family: var(--font-mono, monospace);
|
||
color: var(--text);
|
||
min-height: 0.95em;
|
||
letter-spacing: 0.05em;
|
||
}
|
||
|
||
.seatPuckCards .cardRed {
|
||
color: #ff6b6b;
|
||
}
|
||
|
||
.seatPuckTotal {
|
||
display: inline-block;
|
||
margin-top: var(--sp-0);
|
||
font-size: var(--seatAmountSize);
|
||
font-weight: 700;
|
||
color: #a9f5b8;
|
||
background: rgba(9, 255, 0, 0.1);
|
||
border-radius: var(--r-pill);
|
||
padding: 0 var(--sp-1);
|
||
/* The token, not an em: .puckAmountLong steps this pill's type down so a tournament
|
||
stack keeps every digit, and an em would have taken the row down with it. */
|
||
min-height: var(--seatRowMoney);
|
||
line-height: 1.4;
|
||
/* An inline-block sits on its own last line's baseline, and an empty one has no
|
||
last line — so it sits on its bottom edge instead and the line box comes out 2px
|
||
shorter than the same pill with a figure in it. Which put the two seats that had
|
||
money out 2px below everyone else: the one difference this row was reserved to
|
||
remove, reappearing as its own mirror image. Aligned to the top, the pill is the
|
||
same box whether or not there is anything written in it. */
|
||
vertical-align: top;
|
||
}
|
||
|
||
/* Neither row has anything in it most of the time, and both were holding their space
|
||
anyway — the cards line on every seat of every table that isn't tracking hole cards,
|
||
and the amount pill on everyone who hasn't put chips in yet.
|
||
|
||
The pill was worse than empty space. It has a background, a pill radius and side
|
||
padding, so with no text it painted an 8x11px green lozenge in the middle of the puck
|
||
— measured on seven of nine seats at an ordinary table, a coloured mark on almost
|
||
every seat that stood for nothing. Nine of those is not a table, it is a rash.
|
||
|
||
The space they held was not free either. The ring is sized against the tallest puck
|
||
on it, so two rows nobody had filled were setting how far apart twelve seats had to
|
||
sit: on an iPhone 14 nine-handed the puck measured 78x88 with 25px of it reserved for
|
||
nothing. Collapsed, the search settles on 78x63 — same width for the name, a quarter
|
||
less height to fit on the ring.
|
||
|
||
`:empty` is exact here: both rows are written by updatePuckContent, which sets an
|
||
empty string rather than a space when there is nothing to say.
|
||
|
||
The cards row still collapses, and the amount pill no longer does. They are not the
|
||
same case, and the difference is how often each one changes.
|
||
|
||
Hole cards are a whole-table decision — a dealer either tracks them or doesn't, and
|
||
for most tables the row is empty from the first hand to the last. Holding 15px open on
|
||
nine seats all night for a feature the table isn't using is the crowding that comment
|
||
above is about.
|
||
|
||
Chips are the opposite. Somebody has money out for most of every hand, and *which*
|
||
seats changes on every action. Collapsing that row is what made a puck grow 17px when
|
||
its player called and shrink again when the street closed, under the thumb of someone
|
||
trying to tap it — and because the ring is sized against the tallest puck, the whole
|
||
table could resize with it. The pill keeps its row and hides its paint instead, so the
|
||
lozenge that comment is about never appears and the row underneath never moves.
|
||
|
||
Visibility rather than display, because that is the difference between a box that
|
||
holds its place and a box that isn't there. It leaves the accessibility tree either
|
||
way, which is right: there is nothing to announce. */
|
||
.seatPuckCards:empty {
|
||
display: none;
|
||
}
|
||
|
||
.seatPuckTotal:empty {
|
||
visibility: hidden;
|
||
}
|
||
|
||
/* On the rim, in the slot the blind badges keep — which is what it is.
|
||
|
||
There are four things a puck can say about where a seat sits in the hand: small blind,
|
||
big blind, straddle, button. Three of them were pills hanging off the top-left edge and
|
||
the fourth was a 22px disc inside the top-right corner, in the middle of the writing.
|
||
So the dealer's eye had two places to look for one class of fact, and the corner one
|
||
was charging the name a quarter of its width to be there — see .seatPuckName above for
|
||
what that cost in names.
|
||
|
||
It hangs off the edge exactly as far as the badges do and no further, because that
|
||
distance is not free: render.js's SEAT_BADGE_OVERHANG_PX charges every chair on the
|
||
ring for it, once, and seatFootprint.test.js holds the three numbers together. Same
|
||
5px, so the ring does not move at all — this chip now fits in room the felt had already
|
||
reserved for the badge on the other corner.
|
||
|
||
Which is what sets its size. 12px, the badges' height, is what fits between -5px and
|
||
the top of the name: on the tight ring the name starts 8px down (1px border, 3px
|
||
padding, 4px of margin), and the chip reaches 7.
|
||
|
||
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 then on a nine-seat
|
||
table: tapping the centre of seat 1 gave the button to seat 9. That is why this stays
|
||
inside the puck's own width, at the badges' `2px`, and only ever hangs vertically.
|
||
|
||
12px is smaller than the 22 it was, and the block below is why that costs nothing: a
|
||
finger never reaches this chip, so the only pointer it has to answer is a mouse on a
|
||
ghost that appears under it.
|
||
|
||
Round, where the badges are pills, and on the right, where they are on the left. Both
|
||
matter more now that the button's gold and the big blind's are on the same edge: what
|
||
tells them apart is the shape and the side, not the colour. */
|
||
.dealerChip {
|
||
position: absolute;
|
||
top: -5px;
|
||
right: 2px;
|
||
width: 12px;
|
||
height: 12px;
|
||
padding: 0;
|
||
border-radius: 50%;
|
||
background: radial-gradient(circle at 35% 30%, #fff, #d8d8d8 55%, #b8b8b8 100%);
|
||
color: var(--surface-2);
|
||
border: 1.5px solid var(--text-muted);
|
||
font-weight: 700;
|
||
font-size: var(--fs-2xs);
|
||
line-height: 1;
|
||
cursor: pointer;
|
||
/* The badges' layer, not the seat's. It shares their edge now, so it has to outrank
|
||
the bet chips that come down onto that edge for the same reason they do. */
|
||
z-index: 4;
|
||
transition: opacity 0.15s ease;
|
||
}
|
||
|
||
/* And it follows them round the ring. Seats on the bottom arc wear their markers
|
||
underneath, because bets sit between a seat and the pot — on that arc the bet chip
|
||
lands on the seat's top edge, which is where this was. See .blindBadge. */
|
||
.seat.seatBelowCentre .dealerChip {
|
||
top: auto;
|
||
bottom: -5px;
|
||
}
|
||
|
||
/* There is no invisible pad around this chip any more, and that is the point.
|
||
|
||
It was here to make a 22px disc meet the 44px a fingertip needs. It was moved twice
|
||
trying to find somewhere to grow that wasn't already spoken for — outward first,
|
||
where it took taps from the seat next door; then inward, over "its own seat, which
|
||
is a large target that can spare the corner".
|
||
|
||
The seat could not spare it. Measured on a 390px phone, a 72x46 puck: the pad ran
|
||
from the puck's own centre to its right edge and from its top to 3px off its bottom,
|
||
and the chip's click handler calls stopPropagation. The seat holding the button kept
|
||
52% of its face — 130 of 276 sampled points went to the chip, its exact middle among
|
||
them. Tapping the button seat to open its editor did nothing at all, and on a fresh
|
||
table the button is on seat 1, so the first thing a dealer touches is the one dead
|
||
puck on the felt.
|
||
|
||
The pad had no user left to serve. A finger never reaches this chip: on touch the
|
||
ghost chips are pointer-events: none (below), and the one live chip is the seat that
|
||
already has the button — see .dealerChip.active. Giving the button on touch has
|
||
always been the seat's long-press menu. What remains is a mouse on a revealed ghost,
|
||
and 22px is a mouse target with room to spare. */
|
||
|
||
/* 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;
|
||
}
|
||
|
||
/* The one that has the button is a marker, not a control.
|
||
|
||
Every other chip on the felt answers "give the button to this seat". This one would
|
||
answer "give the button to the seat that has it" — which is nothing, except mid-hand,
|
||
where makeDealer takes the blinds back and re-posts them. So it was a puck that either
|
||
did nothing or did something unasked-for, sitting on top of the seat a dealer was
|
||
reaching for. The seat menu already knew this and hides "Give the button" on the seat
|
||
that holds it; the chip now agrees.
|
||
|
||
Inert to the pointer, still there to read. Its label says "<name> has the dealer
|
||
button" — a statement, and the only place on the page that says it — so it keeps its
|
||
place in the tree and under the keyboard. */
|
||
.dealerChip.active {
|
||
background: radial-gradient(circle at 35% 30%, #ffe9a8, #f5d547 55%, #cba428 100%);
|
||
border-color: var(--gold-deep);
|
||
color: #3a2e00;
|
||
opacity: 1;
|
||
pointer-events: none;
|
||
cursor: default;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
/* The badge hangs off the puck's edge, and how far is not a taste question — it is
|
||
two measurements pulling in opposite directions.
|
||
|
||
Downward, it has to stay off its own player's name. On a tight puck the name starts
|
||
8px in, so a badge 15px tall had to hang 9px off to clear it, and 9px is what the
|
||
seat search has to charge every chair in the ring for (see seatFootprint in
|
||
render.js). Measured at 640x360, that charge is the difference between a 66px puck
|
||
and a 48px one — which is "Christopher, Mike, Alexandra" against "Christo…, Mike…,
|
||
Alexan…" on nine chairs.
|
||
|
||
Upward, it has to stay off the neighbour. The badge is the thing painted on top of
|
||
everything else on the felt, so where it reaches past its own puck onto the next
|
||
one, what the dealer reads there is somebody else's stack.
|
||
|
||
So the badge gets shorter instead of hanging further. Tight leading and no vertical
|
||
padding take it from 15px to 12, which clears the name at 5px of overhang instead of
|
||
9 — and four pixels off what every seat is charged is a whole size step of puck back.
|
||
The label is the same 10px bold it always was; what went was the air around it. */
|
||
/* How far it hangs off the edge, and the number matters in two places at once: this is
|
||
what render.js's SEAT_BADGE_OVERHANG_PX reserves between two seats on the ring, so the
|
||
two have to keep agreeing. seatFootprint.test.js holds them to it.
|
||
|
||
Hanging it further out was tried, to stop it reaching into the writing on its own puck,
|
||
and it is the wrong direction: on the crowded rings the badge that lands on a stack
|
||
figure is the one belonging to the seat *next door*, so every pixel further out is a
|
||
pixel further onto a neighbour. Measured on a Galaxy S8 twelve-handed with everyone
|
||
all-in, -8px took the worst case from four of seven probes across an "ALL IN" to five.
|
||
The room a crowded puck needs comes out of its own leading instead — see
|
||
--seatRowMoney on .seatsTight. */
|
||
.blindBadge {
|
||
display: none;
|
||
position: absolute;
|
||
top: -5px;
|
||
font-size: var(--fs-2xs);
|
||
font-weight: 700;
|
||
line-height: 1.1;
|
||
padding: 0 var(--sp-1);
|
||
border-radius: var(--r-pill);
|
||
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: -5px;
|
||
}
|
||
|
||
/* 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. */
|
||
/* All three sit in the same place, and inside the puck's own width.
|
||
|
||
They used to be spread along the top edge — the small blind at -6px, so hanging off
|
||
the left corner, the big blind at 22px — and that cost twice over. The overhang put
|
||
the badge onto whatever was to the left of the seat: measured on an iPhone Pro Max at
|
||
six seats, where the pucks were not touching at all, "SB" was painted across the next
|
||
player's $1,137. And two badges in two different places means the dealer's eye has to
|
||
find which of two spots this seat is using before it can read which blind this is,
|
||
forty times a night. A seat only ever wears one of these — the small and the big
|
||
blind are different players by construction, and the straddle chain starts left of
|
||
the big blind and stops short of the small one — so there was never anything for the
|
||
second position to avoid.
|
||
|
||
Left rather than centred because the button's chip has the other end of the same edge,
|
||
and the two are read together: who has the button, and who is posting. One line of
|
||
markers along the puck's rim, blinds at one end and the button at the other. */
|
||
.sbBadge {
|
||
left: 2px;
|
||
background: linear-gradient(180deg, #2f6cb5, #22508c);
|
||
color: #eaf3ff;
|
||
}
|
||
|
||
/* Lightened a step. Dark-on-gold was the right call and stays; the gold underneath it
|
||
was just too deep to carry 9px bold at its bottom stop, which measured Lc 46 while
|
||
the small blind's blue was already at Lc 84. Raising the gold is what closes the gap,
|
||
and it costs nothing — the badge reads as the same gold chip, only brighter. */
|
||
.bbBadge {
|
||
left: 2px;
|
||
background: linear-gradient(180deg, #f0c069, #e2a845);
|
||
color: #241a05;
|
||
}
|
||
|
||
/* The straddle. It takes the big blind's slot on the puck, which is free by
|
||
construction: the chain starts left of the big blind and stops short of the small
|
||
one, so no seat ever wears two of these.
|
||
|
||
Violet rather than a third shade of gold or blue, because the badge a dealer is
|
||
looking for mid-hand is the one telling them where the action comes back to.
|
||
Dark text on the light end of the ramp, measured 5.76:1 against the darker stop. */
|
||
.strBadge {
|
||
left: 2px;
|
||
background: linear-gradient(180deg, #cbaaf0, #a97fd6);
|
||
color: #1b1030;
|
||
}
|
||
|
||
/* 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 var(--text-bright);
|
||
outline-offset: 1px;
|
||
}
|
||
|
||
/* Seats close enough together that a puck this narrow can't carry the full layout. The
|
||
class is set from a measurement render.js takes off the ring itself, because no media
|
||
query can see it: the same 390px phone is roomy at six seats and crowded at nine, and
|
||
a wide landscape felt crowds at its ends while its rim says there is space. Not scoped
|
||
to a screen size for the same reason.
|
||
|
||
It used to carry the width too — 58px, against the roomy layout's 96 — and those two
|
||
numbers were the whole vocabulary the felt had. A table that needed 68 got 96 and
|
||
overlapped, or got 58 and threw away ten pixels of name it could have kept. The width
|
||
is now searched for and set as a custom property; what's left here is the typography,
|
||
which does still want to change in one step rather than slide. */
|
||
.seatsTight .seat {
|
||
/* 38px of content plus 6 of padding and 2 of border, which is what this drew before
|
||
the box became a border-box — and 46 clears the 44px a chair has to answer a thumb
|
||
with. A seat is a control: tapping it opens that player's form. */
|
||
min-height: 46px;
|
||
padding: 3px 4px;
|
||
}
|
||
|
||
/* One line again once the ring is crowded. A second line is 14px the tight layout
|
||
hasn't got — it already gives up the hole cards to fit — and a puck that grew taller
|
||
here would push its neighbours into each other, which is the one thing the tight
|
||
layout exists to stop. */
|
||
/* The clamp used to be here, because this was the only ring that gave the name one line.
|
||
Every ring does now, so what is left is the type and the tighter row it sits in. */
|
||
/* Tighter leading on both money rows, which is where a crowded ring pays for holding
|
||
the amount row open. 1.4 is a comfortable line for a figure with room around it and
|
||
this ring has none: a 10px figure is 12px of ink, and the extra 2px was blank.
|
||
|
||
Four pixels off the puck, and it is the four that keep this ring where it was. The
|
||
Galaxy S8 twelve-handed is the tightest table in the sweep — the pucks are already at
|
||
the 44px floor there — and at 58px tall the neighbouring seats' badges came down onto
|
||
each other's stack figures. At 54 they don't, which is the height that table's pucks
|
||
measured before the amount row was held open at all. */
|
||
#seatsLayer.seatsTight {
|
||
--seatRowName: 12px; /* 10px of name at 1.2 */
|
||
--seatRowMoney: 12px; /* and 10px of figures at 1.2, not 1.4 */
|
||
--seatNameSize: min(var(--fs-2xs), 10px);
|
||
--seatAmountSize: min(var(--fs-2xs), 10px);
|
||
}
|
||
|
||
.seatsTight .seatPuckTotal,
|
||
.seatsTight .seatPuckStack {
|
||
line-height: 1.2;
|
||
}
|
||
|
||
/* The ellipsis that used to be repeated here did nothing: the base rule was a
|
||
`-webkit-box`, which `text-overflow` has no effect on. It is in the base rule properly
|
||
now, for every ring. */
|
||
.seatsTight .seatPuckName {
|
||
margin-top: var(--sp-1);
|
||
}
|
||
|
||
/* Not wrapping is only half an answer: a line that cannot wrap and still does not fit
|
||
paints over the seat next door instead. So on the tightest pucks the mark gives up
|
||
most of its letter-spacing, which is 3.6px of the 36 it wanted.
|
||
|
||
The spacing is there to stop "ALL IN" reading as money at a glance, and it keeps the
|
||
two signals that actually do that job — the warm colour and the bold weight. What it
|
||
gives up is the third one, on the only pucks too narrow to afford it. */
|
||
.seatsTight .seatPuckStack.seatPuckAllIn {
|
||
letter-spacing: 0.01em;
|
||
}
|
||
|
||
/* The other end of the same idea. A puck past the ordinary 96px ceiling has room the
|
||
type was never given: the felt grows with the window and the text did not, so a
|
||
dealer on a 2560px monitor read their stacks at the same 10px as one on a 360px
|
||
phone, on a table where the seats had dropped to 8% of the felt.
|
||
|
||
Two steps up the scale for the two numbers a dealer reads across the table, one for
|
||
the chair number in the corner, which is a reference rather than a reading. Not
|
||
proportional to the width — 96 to 144 is 1.5x and this is 1.25x — because a name is
|
||
a label on a puck, not a headline, and the puck also has to hold it: 15px is the
|
||
largest size at which "Mike from work", the longest name the sweep uses, still comes
|
||
out on one line at 144px.
|
||
|
||
Steps on the scale rather than a clamp(), for the same reason the tight set is: the
|
||
width is a continuum and the type is a set of deliberate sizes. */
|
||
#seatsLayer.seatsRoomy {
|
||
--seatRowName: 18px; /* 15px of name at 1.2 */
|
||
--seatRowMoney: 17px; /* 12px of figures at 1.4 */
|
||
--seatNameSize: var(--fs-md);
|
||
--seatAmountSize: var(--fs-sm);
|
||
}
|
||
|
||
.seatsRoomy .seatChairNumber {
|
||
font-size: var(--fs-xs);
|
||
}
|
||
|
||
/* The hole cards give up their line here. A puck this close to its neighbours is 64px
|
||
tall and the seats at the ends of a landscape felt are 55px apart, so the line has to
|
||
come from somewhere — and of the four things on a puck, the cards are the one the
|
||
dealer is holding in their other hand. Name, bet and stack stay. The seat editor
|
||
still shows the cards in full. */
|
||
.seatsTight .seatPuckCards {
|
||
display: none;
|
||
}
|
||
|
||
/* ---------- Seat editor panel (sidebar form for the selected seat) ---------- */
|
||
|
||
.seatEditorPanel {
|
||
padding: var(--sp-4);
|
||
}
|
||
|
||
.seatEditorHeader {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--sp-2);
|
||
}
|
||
|
||
.seatEditorHeader h2 {
|
||
flex: 1;
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* Grip, Done button and scrim are the sheet's furniture — no reason for any of them
|
||
on a desktop sidebar, where the panel is just a panel. */
|
||
.seatSheetGrip,
|
||
.seatSheetClose,
|
||
#seatSheetScrim {
|
||
display: none;
|
||
}
|
||
|
||
.seatEditorPanel h2 {
|
||
margin: 0 0 var(--sp-2);
|
||
padding-bottom: var(--sp-2);
|
||
font-size: var(--fs-md);
|
||
font-weight: 500;
|
||
color: var(--text-bright);
|
||
border-bottom: 1px solid rgba(216, 180, 106, 0.35);
|
||
}
|
||
|
||
.seatEditorForm {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--sp-2);
|
||
}
|
||
|
||
/* A label and a field, and the field takes what is left.
|
||
`.fieldRow input` caps at 140px, which is the right width in a 320px sidebar of
|
||
settings and the wrong one in a sheet that has the whole phone: "Name" sat against
|
||
the left edge with 203px of nothing between it and a field a third of the row wide.
|
||
The row's height does not change — this is the same 44px control, given the width
|
||
the sheet already had. */
|
||
.seatEditorForm .fieldRow input:not([type="checkbox"]) {
|
||
flex: 1;
|
||
min-width: 0;
|
||
max-width: none;
|
||
}
|
||
|
||
/* The three folded sections. `.subPanel` already draws the rule above and the muted
|
||
summary; what these add is the room to hit one with a thumb and the fact on the
|
||
right that says whether opening it is worth the tap. */
|
||
.seatSubPanel > summary {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: var(--sp-2);
|
||
padding: var(--sp-1) 0;
|
||
list-style: none;
|
||
}
|
||
|
||
/* A flex <summary> loses its disclosure triangle in Safari and Chrome alike, so the
|
||
marker is drawn here instead — and it is the one piece of this sheet that says
|
||
"there is more underneath" before anything is tapped. */
|
||
.seatSubPanel > summary::before {
|
||
content: "▸";
|
||
flex: none;
|
||
font-size: var(--fs-xs);
|
||
color: var(--text-dim);
|
||
transition: transform 0.15s ease;
|
||
}
|
||
|
||
.seatSubPanel[open] > summary::before {
|
||
transform: rotate(90deg);
|
||
}
|
||
|
||
.seatSubPanel > summary::-webkit-details-marker {
|
||
display: none;
|
||
}
|
||
|
||
.seatSubPanelName {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
/* The answer, on the shut row. Sized and coloured like the sidebar's panel glances,
|
||
because it is the same idea one level down: the one fact worth opening for. */
|
||
.seatGlance {
|
||
flex: none;
|
||
max-width: 55%;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
color: var(--text-muted);
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
.seatCardToggles {
|
||
display: flex;
|
||
gap: var(--sp-2);
|
||
font-size: var(--fs-base);
|
||
color: #aaa;
|
||
}
|
||
|
||
.inHandToggle,
|
||
.foldToggle {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--sp-1);
|
||
cursor: pointer;
|
||
}
|
||
|
||
.seatNameInput {
|
||
flex: 1;
|
||
min-width: 0;
|
||
background: var(--surface-1);
|
||
border: 1px solid var(--line);
|
||
border-radius: var(--r-sm);
|
||
color: var(--text);
|
||
padding: var(--sp-1) var(--sp-2);
|
||
font-size: var(--fs-base);
|
||
}
|
||
|
||
.seatCards {
|
||
display: flex;
|
||
gap: var(--sp-1);
|
||
margin-bottom: var(--sp-2);
|
||
}
|
||
|
||
/* 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 var(--line);
|
||
border-radius: var(--r-sm);
|
||
padding: var(--sp-1) 0;
|
||
font-size: var(--fs-lg);
|
||
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: var(--surface-1);
|
||
color: var(--text-dim);
|
||
border-style: dashed;
|
||
border-color: #3f3f3f;
|
||
box-shadow: none;
|
||
font-weight: 400;
|
||
}
|
||
|
||
/* ---------- Add chips ---------- */
|
||
|
||
.seatBuyIns {
|
||
margin-top: -var(--sp-1);
|
||
color: var(--text-muted);
|
||
font-size: var(--fs-sm);
|
||
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, var(--surface-2), var(--surface-1));
|
||
border: 1px solid var(--line);
|
||
border-bottom: none;
|
||
border-radius: var(--r-lg) var(--r-lg) 0 0;
|
||
padding: var(--sp-1) var(--sp-4) calc(var(--sp-5) + env(safe-area-inset-bottom, 0px));
|
||
box-shadow: 0 -14px 40px rgba(0, 0, 0, 0.65);
|
||
}
|
||
|
||
.addChipsHeader {
|
||
margin-bottom: var(--sp-3);
|
||
}
|
||
|
||
.addChipsHeader h2 {
|
||
margin: 0;
|
||
font-size: var(--fs-md);
|
||
font-weight: 500;
|
||
color: var(--text-bright);
|
||
}
|
||
|
||
#addChipsSubtitle {
|
||
color: var(--text-muted);
|
||
font-size: var(--fs-sm);
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
#addChipsQuick {
|
||
display: flex;
|
||
gap: var(--sp-2);
|
||
margin-bottom: var(--sp-3);
|
||
}
|
||
|
||
.addChipsQuickBtn {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: var(--sp-0);
|
||
min-height: 52px;
|
||
border-radius: var(--r-md);
|
||
border: 1px solid var(--line);
|
||
background: linear-gradient(180deg, var(--surface-3), var(--surface-2));
|
||
color: var(--text);
|
||
font-family: inherit;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.addChipsQuickBtn.selected {
|
||
border-color: var(--gold);
|
||
background: linear-gradient(180deg, var(--gold-tint), var(--gold-shade));
|
||
color: var(--gold-bright);
|
||
}
|
||
|
||
.addChipsQuickLabel {
|
||
font-size: var(--fs-sm);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.addChipsQuickAmount {
|
||
font-size: var(--fs-xs);
|
||
color: #a8a8a8;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
.addChipsQuickBtn.selected .addChipsQuickAmount {
|
||
color: var(--gold);
|
||
}
|
||
|
||
.addChipsEntry {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--sp-2);
|
||
background: var(--surface-0);
|
||
border: 1px solid var(--line);
|
||
border-radius: var(--r-md);
|
||
padding: var(--sp-1) var(--sp-3);
|
||
margin-bottom: var(--sp-3);
|
||
}
|
||
|
||
.addChipsEntry:focus-within {
|
||
border-color: var(--gold);
|
||
}
|
||
|
||
.addChipsCurrency {
|
||
color: var(--text-dim);
|
||
font-size: var(--fs-xl);
|
||
}
|
||
|
||
#addChipsInput {
|
||
flex: 1;
|
||
min-width: 0;
|
||
background: none;
|
||
border: none;
|
||
outline: none;
|
||
color: var(--text-bright);
|
||
font-family: inherit;
|
||
font-size: var(--fs-xl);
|
||
font-weight: 500;
|
||
font-variant-numeric: tabular-nums;
|
||
padding: var(--sp-2) 0;
|
||
/* Stated, not arrived at. This field used to clear 44px by way of its font size
|
||
plus its padding, so moving either on to the scales dropped it to 42 — a touch
|
||
target is a promise about the size of the thing, and it should not be an
|
||
accident of two other numbers. */
|
||
box-sizing: border-box;
|
||
min-height: 44px;
|
||
}
|
||
|
||
.addChipsActions {
|
||
display: flex;
|
||
flex-direction: column-reverse;
|
||
gap: var(--sp-2);
|
||
}
|
||
|
||
.addChipsActions .holdemBtn {
|
||
width: 100%;
|
||
justify-content: center;
|
||
min-height: 48px;
|
||
}
|
||
|
||
/* ---------- Rulings reference ---------- */
|
||
|
||
.tableHintActions {
|
||
margin: var(--sp-1) 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.
|
||
|
||
This said `#dedicatedDealerToggle` and fixed one box of six. The other five were
|
||
still 13x13 — measured — including the two in the seat sheet that a dealer uses far
|
||
more often than any setting. Said about checkboxes instead, since that is what the
|
||
sentence above is about. */
|
||
input[type="checkbox"] {
|
||
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: var(--gold);
|
||
font-family: inherit;
|
||
font-size: var(--fs-sm);
|
||
text-decoration: underline;
|
||
text-underline-offset: 2px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
@media (hover: hover) {
|
||
.linkBtn:hover {
|
||
color: var(--gold);
|
||
}
|
||
}
|
||
|
||
#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, var(--surface-2), var(--surface-1));
|
||
border: 1px solid var(--line);
|
||
border-bottom: none;
|
||
border-radius: var(--r-lg) var(--r-lg) 0 0;
|
||
padding: var(--sp-1) var(--sp-4) calc(var(--sp-5) + env(safe-area-inset-bottom, 0px));
|
||
box-shadow: 0 -14px 40px rgba(0, 0, 0, 0.65);
|
||
}
|
||
|
||
.rulingsHeader {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--sp-2);
|
||
margin-bottom: var(--sp-2);
|
||
}
|
||
|
||
.rulingsHeader h2 {
|
||
flex: 1;
|
||
margin: 0;
|
||
font-size: var(--fs-lg);
|
||
font-weight: 500;
|
||
color: var(--text-bright);
|
||
}
|
||
|
||
.rulingsIntro {
|
||
margin: 0 0 var(--sp-4);
|
||
color: var(--text-muted);
|
||
font-size: var(--fs-base);
|
||
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: var(--sp-2) var(--sp-2);
|
||
border: 1px solid rgba(240, 180, 106, 0.45);
|
||
border-radius: var(--r-sm);
|
||
background: rgba(240, 180, 106, 0.08);
|
||
color: #f0b46a;
|
||
font-size: var(--fs-sm);
|
||
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: var(--sp-1) 0 0;
|
||
}
|
||
|
||
.colourUpWho {
|
||
color: var(--text);
|
||
font-size: var(--fs-sm);
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
.colourUpChips {
|
||
margin: 0 0 var(--sp-2);
|
||
color: #9fb8a6;
|
||
font-size: var(--fs-sm);
|
||
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: var(--sp-2) var(--sp-2);
|
||
margin: 0;
|
||
align-items: baseline;
|
||
}
|
||
|
||
.shortcutLegend dt {
|
||
display: flex;
|
||
gap: var(--sp-1);
|
||
}
|
||
|
||
.shortcutLegend dd {
|
||
margin: 0;
|
||
color: #b8b8b8;
|
||
font-size: var(--fs-sm);
|
||
line-height: 1.35;
|
||
}
|
||
|
||
.shortcutKey {
|
||
background: var(--surface-2);
|
||
border: 1px solid #3f3f3f;
|
||
border-bottom-width: 2px;
|
||
border-radius: var(--r-sm);
|
||
padding: var(--sp-0) var(--sp-1);
|
||
font-family: inherit;
|
||
font-size: var(--fs-xs);
|
||
color: var(--gold);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* A shut ruling is a row you can read; an open one is a passage you can read. The gold
|
||
left bar used to run down all twelve at once, which on a list of headings is twelve
|
||
vertical rules and no hierarchy. It belongs to the one that is open. */
|
||
.ruling {
|
||
border-bottom: 1px solid var(--line);
|
||
}
|
||
|
||
.ruling[open] {
|
||
margin-bottom: var(--sp-2);
|
||
padding-left: var(--sp-3);
|
||
border-left: 2px solid rgba(216, 180, 106, 0.4);
|
||
border-bottom-color: transparent;
|
||
}
|
||
|
||
/* Padding rather than a flex box with min-height, because a <summary> only draws its
|
||
disclosure triangle while it is `display: list-item` — and content-box means a
|
||
min-height would be *added* to the padding. Both traps are the sidebar's, one
|
||
stylesheet section over, and they are the same traps here. */
|
||
.ruling > summary {
|
||
box-sizing: border-box;
|
||
min-height: 44px;
|
||
padding: var(--sp-2) 0;
|
||
color: var(--text);
|
||
font-size: var(--fs-base);
|
||
font-weight: 600;
|
||
letter-spacing: 0.03em;
|
||
line-height: 1.35;
|
||
cursor: pointer;
|
||
}
|
||
|
||
/* The gold arrives when a ruling is opened, and not before. These were all gold while
|
||
they were headings with prose between them; twelve of them in a column is the accent
|
||
doing the job of body text, which leaves it nothing to say when one of them is the
|
||
one the dealer is reading. The sidebar's eight panel names are `--text` for the same
|
||
reason — this is the same component doing the same job. */
|
||
.ruling[open] > summary {
|
||
margin-bottom: var(--sp-1);
|
||
color: var(--gold);
|
||
}
|
||
|
||
/* A <summary> is announced as a button, not as a heading, so the twelve names — which
|
||
are now the whole of this sheet's navigation — would be invisible to anybody moving
|
||
through it by heading. Same fix as the sidebar's panel names: keep the heading and
|
||
make it look like nothing happened. Inline, because a block child of a summary
|
||
pushes the disclosure triangle onto its own line. */
|
||
.ruling > summary > h3 {
|
||
display: inline;
|
||
margin: 0;
|
||
font: inherit;
|
||
color: inherit;
|
||
letter-spacing: inherit;
|
||
}
|
||
|
||
@media (hover: hover) {
|
||
.ruling > summary:hover {
|
||
color: var(--gold-bright);
|
||
}
|
||
}
|
||
|
||
.ruling p,
|
||
.ruling li {
|
||
color: #c4c4c4;
|
||
font-size: var(--fs-base);
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.ruling p {
|
||
margin: 0;
|
||
}
|
||
|
||
/* Five of the twelve are two paragraphs, and they ran into each other: the sections
|
||
were spaced apart from one another but nothing separated the halves of one ruling,
|
||
so "the most that chip allows." and "Facing no bet," read as one paragraph. It did
|
||
not show while there was a gold heading every few lines to break the column up. */
|
||
.ruling p + p {
|
||
margin-top: var(--sp-2);
|
||
}
|
||
|
||
.ruling ul {
|
||
margin: 0;
|
||
padding-left: var(--sp-4);
|
||
}
|
||
|
||
.ruling li + li {
|
||
margin-top: var(--sp-1);
|
||
}
|
||
|
||
.rulingsSource {
|
||
margin: 0;
|
||
padding-top: var(--sp-2);
|
||
border-top: 1px solid var(--surface-3);
|
||
color: var(--text-dim);
|
||
font-size: var(--fs-sm);
|
||
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, var(--surface-2), var(--surface-1));
|
||
border: 1px solid var(--line);
|
||
border-bottom: none;
|
||
border-radius: var(--r-lg) var(--r-lg) 0 0;
|
||
/* No padding at the top: the header owns it, because the header is sticky and a
|
||
gap above a sticky thing is a gap the list scrolls through. */
|
||
padding: 0 var(--sp-4) calc(var(--sp-4) + 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;
|
||
}
|
||
|
||
/* Stuck to the top of the sheet's own scrollport. Two reasons, and the second is the
|
||
one that made it necessary: whose menu this is should not scroll away above a list
|
||
of eleven verbs, and the way out should not either. Measured before this, on the
|
||
fullest menu the tool can build, Cancel was off the bottom of the screen on all four
|
||
viewports in the sweep — 79px past it on an iPhone, 252px on a 360px phone, 448px in
|
||
landscape. Now the close button is in the part that cannot leave. */
|
||
.seatMenuHeader {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 1;
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) auto;
|
||
align-items: center;
|
||
column-gap: var(--sp-2);
|
||
/* Out to the sheet's edges and back again, so nothing shows through either side
|
||
of a header sitting over a scrolling list. */
|
||
margin: 0 calc(var(--sp-4) * -1) var(--sp-3);
|
||
padding: var(--sp-1) var(--sp-4) var(--sp-2);
|
||
background: var(--surface-2);
|
||
border-bottom: 1px solid var(--line);
|
||
}
|
||
|
||
.seatMenuHeader .seatSheetGrip {
|
||
grid-column: 1 / -1;
|
||
}
|
||
|
||
.seatMenuHeader h2 {
|
||
margin: 0;
|
||
font-size: var(--fs-lg);
|
||
font-weight: 500;
|
||
color: var(--text-bright);
|
||
}
|
||
|
||
#seatMenuSubtitle {
|
||
color: var(--text-muted);
|
||
font-size: var(--fs-base);
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
.seatMenuClose {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-width: 44px;
|
||
min-height: 44px;
|
||
padding: 0;
|
||
border: 1px solid var(--line);
|
||
border-radius: var(--r-md);
|
||
background: var(--surface-1);
|
||
color: var(--text-muted);
|
||
font-family: inherit;
|
||
font-size: var(--fs-lg);
|
||
line-height: 1;
|
||
cursor: pointer;
|
||
}
|
||
|
||
@media (hover: hover) {
|
||
.seatMenuClose:hover {
|
||
border-color: var(--line-strong);
|
||
color: var(--text-bright);
|
||
}
|
||
}
|
||
|
||
#seatMenuActions {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--sp-2);
|
||
margin-bottom: var(--sp-2);
|
||
}
|
||
|
||
/* What the group is for, said once, so the tiles under it don't each have to say it.
|
||
Small and unsaturated on purpose: a caption that competes with the actions has made
|
||
the sheet busier, which is the thing it is here to undo. */
|
||
.seatMenuCaption {
|
||
margin: var(--sp-2) 0 0;
|
||
font-size: var(--fs-xs);
|
||
font-weight: var(--fw-medium);
|
||
letter-spacing: 0.08em;
|
||
text-transform: uppercase;
|
||
color: var(--text-dim);
|
||
}
|
||
|
||
.seatMenuGrid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: var(--sp-2);
|
||
}
|
||
|
||
.seatMenuBtn {
|
||
width: 100%;
|
||
min-height: 52px;
|
||
border-radius: var(--r-md);
|
||
border: 1px solid var(--line);
|
||
background: linear-gradient(180deg, var(--surface-3), var(--surface-2));
|
||
color: var(--text);
|
||
font-family: inherit;
|
||
font-size: var(--fs-md);
|
||
cursor: pointer;
|
||
transition: all 0.12s ease;
|
||
}
|
||
|
||
/* The housekeeping half. Same control, quieter and half the width: flat instead of
|
||
raised, a step down in type, and the 44px the touch guidance asks for rather than
|
||
the 52 a bar gets — because these are the ones you tap while looking, between
|
||
hands, and the ones above are the ones you tap without. */
|
||
.seatMenuBtn.menuTile {
|
||
min-height: 44px;
|
||
padding: var(--sp-1) var(--sp-2);
|
||
background: var(--surface-2);
|
||
font-size: var(--fs-base);
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
/* The one action here that isn't about this seat — it puts the whole table's blinds
|
||
back on automatic. It spans the grid so it doesn't read as a fourth position to
|
||
give, and it is drawn lighter than its neighbours so it doesn't read as the Cancel
|
||
directly under it either: two full-width rows the same weight, one of which changes
|
||
the table and one of which changes nothing, is a mistake waiting at the end of a
|
||
scroll. Lighter, not smaller: it keeps the 44px every other tile has, because a
|
||
thumb does not care how rare an action is. The dashed edge is the same word the
|
||
felt already uses for an empty chair — nothing set by hand here. */
|
||
.seatMenuBtn.menuTileWide {
|
||
grid-column: 1 / -1;
|
||
background: none;
|
||
border-style: dashed;
|
||
font-size: var(--fs-sm);
|
||
color: var(--text-dim);
|
||
}
|
||
|
||
@media (hover: hover) {
|
||
.seatMenuBtn:hover {
|
||
border-color: var(--line-strong);
|
||
background: linear-gradient(180deg, var(--line), #2a2a2a);
|
||
}
|
||
|
||
.seatMenuBtn.menuTile:hover {
|
||
background: var(--surface-3);
|
||
color: var(--text-bright);
|
||
}
|
||
|
||
/* After .menuTile, not before it: the two selectors weigh the same, so the one
|
||
that keeps the dashed row flat has to be the one the cascade reads last. */
|
||
.seatMenuBtn.menuTileWide:hover {
|
||
background: none;
|
||
color: var(--text);
|
||
}
|
||
}
|
||
|
||
.seatMenuBtn.menuDanger {
|
||
background: linear-gradient(180deg, #3d1f1f, #2c1515);
|
||
border-color: #5b2626;
|
||
color: #ffd6d6;
|
||
}
|
||
|
||
.seatMenuBtn.menuAccent {
|
||
background: linear-gradient(180deg, var(--gold-tint), var(--gold-shade));
|
||
border-color: var(--gold-deep);
|
||
color: var(--gold-bright);
|
||
}
|
||
|
||
#seatMenuCancelBtn {
|
||
width: 100%;
|
||
justify-content: center;
|
||
min-height: 48px;
|
||
/* A gap the eye reads as "the sheet ends here", so the last action and the way out
|
||
are not two rows of the same size touching. */
|
||
margin-top: var(--sp-3);
|
||
}
|
||
|
||
/* ---------- 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, var(--surface-2), var(--surface-1));
|
||
border: 1px solid var(--line);
|
||
border-bottom: none;
|
||
border-radius: var(--r-lg) var(--r-lg) 0 0;
|
||
padding: var(--sp-1) var(--sp-4) calc(var(--sp-4) + env(safe-area-inset-bottom, 0px));
|
||
box-shadow: 0 -14px 40px rgba(0, 0, 0, 0.65);
|
||
}
|
||
|
||
.cardPickerHeader {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--sp-2);
|
||
margin-bottom: var(--sp-2);
|
||
}
|
||
|
||
.cardPickerHeader h2 {
|
||
flex: 1;
|
||
min-width: 0;
|
||
margin: 0;
|
||
font-size: var(--fs-md);
|
||
font-weight: 500;
|
||
color: var(--text-bright);
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.cardPickerHeader .holdemBtn {
|
||
min-height: 40px;
|
||
}
|
||
|
||
#cardPickerSlots {
|
||
display: flex;
|
||
gap: var(--sp-2);
|
||
margin-bottom: var(--sp-3);
|
||
}
|
||
|
||
.pickerSlot {
|
||
flex: 1;
|
||
min-height: 46px;
|
||
border-radius: var(--r-md);
|
||
border: 1px solid var(--line);
|
||
background: var(--surface-1);
|
||
color: var(--text-dim);
|
||
font-family: var(--font-mono, monospace);
|
||
font-size: var(--fs-lg);
|
||
font-weight: 700;
|
||
cursor: pointer;
|
||
}
|
||
|
||
/* The slot the next tap will fill. */
|
||
.pickerSlot.active {
|
||
border-color: var(--gold);
|
||
background: var(--gold-shade);
|
||
color: var(--gold-bright);
|
||
}
|
||
|
||
.pickerSlot.cardRed {
|
||
color: #e07b7b;
|
||
}
|
||
|
||
#cardPickerGrid {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: var(--sp-1);
|
||
}
|
||
|
||
.pickerCard {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: var(--sp-0);
|
||
min-height: 44px;
|
||
border-radius: var(--r-sm);
|
||
border: 1px solid #d5d5d5;
|
||
background: #fdfdfd;
|
||
color: #111;
|
||
font-family: var(--font-mono, monospace);
|
||
font-size: var(--fs-md);
|
||
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 var(--gold);
|
||
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: var(--line-strong);
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.pickerSuit {
|
||
font-size: 1.05em;
|
||
line-height: 1;
|
||
}
|
||
|
||
/* The rule above it is the disclosure's own now, so this no longer draws one of its
|
||
own — two lines a few pixels apart was the look of a section that didn't know where
|
||
it started. */
|
||
.seatContribution {
|
||
margin-bottom: var(--sp-1);
|
||
}
|
||
|
||
/* A grid rather than a wrapping flex row. Five chip colours in a flex row came out
|
||
four-and-one, with the fifth stranded on a line of its own and the four above it
|
||
ending wherever their widths happened to land — a ragged block for a set of things
|
||
that are all the same shape. Columns are as wide as they need to be for a value and
|
||
a count, and every cell is the same. */
|
||
.chipCounts {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(7.5rem, 1fr));
|
||
gap: var(--sp-1);
|
||
margin-bottom: var(--sp-1);
|
||
}
|
||
|
||
.chipCountRow {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--sp-1);
|
||
min-width: 0;
|
||
}
|
||
|
||
.chipSwatch {
|
||
width: 12px;
|
||
height: 12px;
|
||
flex: none;
|
||
border-radius: 50%;
|
||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||
display: inline-block;
|
||
}
|
||
|
||
/* What the colour is worth, in the row. Fixed-width and tabular so five of these
|
||
stack into a column the eye can run down, rather than five labels each starting
|
||
somewhere else. */
|
||
.chipCountValue {
|
||
flex: none;
|
||
min-width: 2.75rem;
|
||
color: var(--text-muted);
|
||
font-size: var(--fs-sm);
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
.chipCountInput {
|
||
flex: 1;
|
||
min-width: 0;
|
||
width: 34px;
|
||
background: var(--surface-1);
|
||
border: 1px solid var(--line);
|
||
border-radius: var(--r-sm);
|
||
color: var(--text);
|
||
font-size: var(--fs-sm);
|
||
padding: var(--sp-0) var(--sp-1);
|
||
}
|
||
|
||
.amountOverrideRow {
|
||
justify-content: space-between;
|
||
font-size: var(--fs-sm);
|
||
color: #aaa;
|
||
margin-bottom: var(--sp-1);
|
||
}
|
||
|
||
.amountOverrideInput {
|
||
width: 70px;
|
||
background: var(--surface-1);
|
||
border: 1px solid var(--line);
|
||
border-radius: var(--r-sm);
|
||
color: var(--text);
|
||
padding: var(--sp-1) var(--sp-1);
|
||
}
|
||
|
||
.seatTotal {
|
||
font-size: var(--fs-base);
|
||
color: #d6ffe0;
|
||
font-weight: 600;
|
||
text-align: right;
|
||
}
|
||
|
||
.customFields {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--sp-1);
|
||
margin-bottom: var(--sp-1);
|
||
}
|
||
|
||
.customFieldRow {
|
||
display: flex;
|
||
gap: var(--sp-1);
|
||
}
|
||
|
||
.customFieldLabel,
|
||
.customFieldValue {
|
||
min-width: 0;
|
||
flex: 1;
|
||
background: var(--surface-1);
|
||
border: 1px solid var(--line);
|
||
border-radius: var(--r-sm);
|
||
color: var(--text);
|
||
font-size: var(--fs-xs);
|
||
padding: var(--sp-1) var(--sp-1);
|
||
}
|
||
|
||
.removeFieldBtn,
|
||
.removeChipColorBtn {
|
||
line-height: 1;
|
||
}
|
||
|
||
/* ---------- Sidebar ---------- */
|
||
|
||
#holdemSidebar {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--sp-3);
|
||
/* 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;
|
||
}
|
||
|
||
/* No border. The panel already has a lighter background than the page behind it and a
|
||
shadow lifting it off — three ways of drawing the same edge, where the background
|
||
alone does the job. Putting a line round every box is what makes an interface read
|
||
like a spreadsheet: the eye spends itself on the lines rather than the contents. */
|
||
.sidebarPanel {
|
||
background: linear-gradient(160deg, #212121, var(--surface-1));
|
||
border-radius: var(--r-md);
|
||
padding: var(--sp-2) var(--sp-4);
|
||
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: var(--sp-3) 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: var(--fs-md);
|
||
color: var(--text);
|
||
letter-spacing: 0.03em;
|
||
}
|
||
|
||
/* The panel's name is a heading as well as a control.
|
||
|
||
A <summary> is announced as a button and not as a heading, so the sidebar's names —
|
||
which are the whole of its navigation — were invisible to anyone moving through the
|
||
page by heading. The four that were headings were the subheads buried inside the old
|
||
Session panel, and splitting it took those away, so without this the page would have
|
||
lost the outline rather than gained one. The spec allows heading content in a summary
|
||
and both VoiceOver and NVDA expose it.
|
||
|
||
It has to look like nothing happened, so the heading gives up everything a heading
|
||
brings with it. Inline, because a block child of a summary pushes the disclosure
|
||
triangle onto its own line. */
|
||
.sidebarPanel > summary > h2 {
|
||
display: inline;
|
||
margin: 0;
|
||
font: inherit;
|
||
color: inherit;
|
||
letter-spacing: inherit;
|
||
}
|
||
|
||
/* The one fact a shut panel is worth opening for, said on the row itself.
|
||
See Render.panelGlances for what each one says and why.
|
||
|
||
Taken out of flow rather than laid out beside the label, because a <summary> only
|
||
draws its disclosure triangle while it is `display: list-item` — the same constraint
|
||
that put padding rather than a flex box on the row above.
|
||
|
||
Out of flow is also the guarantee that matters here: nothing about the glance may
|
||
make a sidebar row taller, and an absolutely positioned box cannot. Floating it did
|
||
keep the row at 60px, but it was aligned to the label's line box rather than to the
|
||
row, and a 12px string in a box sized for 15px text rode 2.5px low on all six panels.
|
||
Centring against the row is both the right alignment and the smaller promise.
|
||
|
||
It lands 1.5px below the label, measured, and that is the label's offset rather than
|
||
this one's: the row's 44px is a min-height, so the three pixels it adds to a 41px
|
||
header all go on the bottom and leave the label sitting high in its own row. The
|
||
glance is on the row's centre, which is the stable thing to be on. */
|
||
.sidebarPanel > summary {
|
||
position: relative;
|
||
}
|
||
|
||
.panelGlance {
|
||
position: absolute;
|
||
right: 0;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
/* The labels are short and fixed — "Waiting list" is the longest at under a third
|
||
of the column — so this is room to spare, not a division of the row. */
|
||
max-width: 55%;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
font-size: var(--fs-sm);
|
||
letter-spacing: 0;
|
||
color: var(--text-muted);
|
||
/* The amounts change while a hand is being dealt, and a proportional font shifts
|
||
the whole string sideways every time a digit does. */
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
/* The same two colours the bank check's own sentence uses, because this is that
|
||
sentence with the explanation taken off — a glance that agreed with the panel in
|
||
words and disagreed with it in colour would be worse than no glance. */
|
||
.panelGlanceOk {
|
||
color: #7fc99a;
|
||
}
|
||
|
||
.panelGlanceWarn {
|
||
color: #f0b46a;
|
||
}
|
||
|
||
.panelBody {
|
||
padding-bottom: var(--sp-4);
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--sp-3);
|
||
}
|
||
|
||
/* Deliberately not animated. The obvious move here is the grid 0fr -> 1fr trick, and
|
||
it does not work on a <details>: the browser doesn't render the body at all while
|
||
the panel is shut, so there is no "from" for a transition to start at. The real
|
||
answer is ::details-content with interpolate-size, which Chrome has and the others
|
||
don't — and a panel that glides open on one phone and snaps on another is worse
|
||
than one that snaps on both. Measured either way, opening a panel moves nothing
|
||
else on the page, so the snap is contained. Left alone until support catches up. */
|
||
|
||
/* 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: var(--sp-1) 0 0;
|
||
color: var(--gold);
|
||
font-size: var(--fs-xs);
|
||
font-weight: 600;
|
||
letter-spacing: 0.09em;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.panelSubhead + * {
|
||
margin-top: -var(--sp-1);
|
||
}
|
||
|
||
/* 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 var(--surface-3);
|
||
padding-top: var(--sp-2);
|
||
}
|
||
|
||
.subPanel summary {
|
||
cursor: pointer;
|
||
color: var(--text-muted);
|
||
font-size: var(--fs-sm);
|
||
}
|
||
|
||
@media (hover: hover) {
|
||
.subPanel summary:hover {
|
||
color: #c5c5c5;
|
||
}
|
||
}
|
||
|
||
.subPanelBody {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--sp-2);
|
||
padding-top: var(--sp-2);
|
||
}
|
||
|
||
.fieldRow {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: var(--sp-2);
|
||
font-size: var(--fs-base);
|
||
color: #ccc;
|
||
}
|
||
|
||
.fieldRow input,
|
||
.fieldRow select {
|
||
background: var(--surface-0);
|
||
border: 1px solid var(--line);
|
||
border-radius: var(--r-sm);
|
||
color: var(--text);
|
||
padding: var(--sp-1) var(--sp-2);
|
||
max-width: 140px;
|
||
}
|
||
|
||
.fieldHint {
|
||
font-size: var(--fs-sm);
|
||
color: var(--text-muted);
|
||
line-height: 1.4;
|
||
margin: 0;
|
||
}
|
||
|
||
#chipLegendList {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--sp-2);
|
||
}
|
||
|
||
.chipLegendRow {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--sp-2);
|
||
}
|
||
|
||
/* `flex: none` here for the same reason the coarse-pointer block already says it: a
|
||
stated width is only a suggestion to a flex item, and this one sits in a crowded row.
|
||
It was declared 32px wide and measured 22 with a mouse — under the 24px floor the
|
||
checkbox rule near the top of this file is written to clear, and squeezed by exactly
|
||
the mechanism that block had already been bitten by once. */
|
||
.chipColorInput {
|
||
flex: none;
|
||
width: 32px;
|
||
height: 28px;
|
||
background: none;
|
||
border: 1px solid var(--line);
|
||
border-radius: var(--r-sm);
|
||
padding: 0;
|
||
}
|
||
|
||
.chipValueInput {
|
||
flex: 1;
|
||
background: var(--surface-0);
|
||
border: 1px solid var(--line);
|
||
border-radius: var(--r-sm);
|
||
color: var(--text);
|
||
padding: var(--sp-1) var(--sp-2);
|
||
}
|
||
|
||
#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;
|
||
}
|
||
|
||
/* A rule under every row turned the standings into ruled paper — seven lines on a
|
||
six-player table, none of which said anything the row spacing wasn't already saying.
|
||
The rows are separated by the space between them; the header keeps its rule, because
|
||
that one marks a real change of kind. */
|
||
.standingsRow {
|
||
display: grid;
|
||
grid-template-columns: minmax(4rem, 1fr) auto auto;
|
||
gap: var(--sp-2);
|
||
align-items: baseline;
|
||
padding: var(--sp-2) var(--sp-0);
|
||
font-size: var(--fs-sm);
|
||
}
|
||
|
||
.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: var(--text);
|
||
}
|
||
|
||
.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: var(--sp-2);
|
||
padding: var(--sp-2) var(--sp-2);
|
||
border: 1px solid #2f2f2f;
|
||
border-radius: var(--r-md);
|
||
background: #151515;
|
||
}
|
||
|
||
.bankReconcileRow {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: var(--sp-3);
|
||
font-size: var(--fs-sm);
|
||
color: #b8b8b8;
|
||
font-variant-numeric: tabular-nums;
|
||
padding: var(--sp-0) 0;
|
||
}
|
||
|
||
.bankReconcileVerdict {
|
||
margin: var(--sp-2) 0 0;
|
||
font-size: var(--fs-sm);
|
||
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: var(--sp-1) 0 0;
|
||
padding: 0;
|
||
}
|
||
|
||
.settleUpRow {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: baseline;
|
||
gap: var(--sp-3);
|
||
font-size: var(--fs-base);
|
||
padding: var(--sp-0) 0;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||
}
|
||
|
||
.settleUpRow:last-child {
|
||
border-bottom: 0;
|
||
}
|
||
|
||
.settleUpWho {
|
||
color: var(--text);
|
||
}
|
||
|
||
.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: var(--text-dim);
|
||
font-size: var(--fs-xs);
|
||
letter-spacing: 0.04em;
|
||
text-transform: uppercase;
|
||
/* The one rule kept in this table: it separates the column names from the players,
|
||
which is a change of kind rather than just the next row down. Stated in full
|
||
now the rows themselves have none to inherit a colour from. */
|
||
border-bottom: 1px solid var(--line);
|
||
position: sticky;
|
||
top: 0;
|
||
background: #1b1b1b;
|
||
}
|
||
|
||
.standingsName {
|
||
color: var(--text);
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.standingsIn,
|
||
.standingsWon {
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.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 var(--sp-2);
|
||
text-align: center;
|
||
font-size: var(--fs-base);
|
||
color: #f0d98c;
|
||
}
|
||
|
||
.handHistoryFilterRow {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--sp-2);
|
||
margin-bottom: var(--sp-2);
|
||
}
|
||
|
||
.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: var(--sp-1) var(--sp-2);
|
||
background: var(--surface-0);
|
||
border: 1px solid var(--surface-3);
|
||
border-radius: var(--r-sm);
|
||
color: var(--text);
|
||
}
|
||
|
||
.handHistoryFilter::placeholder {
|
||
color: var(--text-dim);
|
||
}
|
||
|
||
.handHistoryFilter:focus-visible {
|
||
outline: 2px solid var(--gold);
|
||
outline-offset: 1px;
|
||
}
|
||
|
||
.handHistoryCount {
|
||
flex: 0 0 auto;
|
||
font-size: var(--fs-sm);
|
||
color: var(--text-dim);
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
#handHistoryList {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--sp-2);
|
||
/* 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: var(--surface-0);
|
||
border: 1px solid var(--surface-3);
|
||
border-radius: var(--r-sm);
|
||
padding: var(--sp-2) var(--sp-2);
|
||
}
|
||
|
||
.handHistoryRowHeader {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: var(--sp-2);
|
||
font-size: var(--fs-base);
|
||
}
|
||
|
||
.handHistoryHandNumber {
|
||
color: var(--gold);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.handHistoryPot {
|
||
color: var(--text);
|
||
}
|
||
|
||
.handHistoryTime {
|
||
margin-left: auto;
|
||
color: var(--text-dim);
|
||
font-size: var(--fs-sm);
|
||
}
|
||
|
||
.handHistoryPayouts {
|
||
margin-top: var(--sp-0);
|
||
color: #aaa;
|
||
font-size: var(--fs-sm);
|
||
line-height: 1.4;
|
||
}
|
||
|
||
#tableNotes {
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
background: var(--surface-0);
|
||
border: 1px solid var(--line);
|
||
border-radius: var(--r-sm);
|
||
color: var(--text);
|
||
padding: var(--sp-2);
|
||
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 {
|
||
/* Which of the two shapes this bar is in, declared for actionBar.js to read back —
|
||
see isRail(). The layout is chosen by a media query, and the one thing that must
|
||
not happen is the same breakpoint written out a second time in JavaScript. */
|
||
--actionBarLayout: bar;
|
||
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, var(--surface-2), var(--surface-1));
|
||
border: 1px solid var(--line);
|
||
border-bottom: none;
|
||
border-radius: var(--r-lg) var(--r-lg) 0 0;
|
||
padding: var(--sp-2) var(--sp-3) calc(var(--sp-3) + 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 page needs room to scroll past it.
|
||
|
||
This was 9rem on the sidebar alone, which gave the column of panels somewhere to go
|
||
and left the table's column with nothing. That is most of the page on a desktop, and
|
||
it is where the two links under the felt live: measured at 1280x900, the page had 36px
|
||
of scroll in it against the 94px needed to lift Common rulings out from under a toast,
|
||
so the focus handler asked for a scroll the page could not make.
|
||
|
||
On the whole app rather than one column, and taken from the bar's published height
|
||
rather than guessed, so it is right in both layouts — 0 when the bar is a rail down
|
||
the side, which takes no height at all. The fallback keeps the old number for the
|
||
moment before the first measurement lands. */
|
||
body.hasActionBar #holdemApp {
|
||
padding-bottom: calc(var(--actionBarHeight, 9rem) + var(--sp-4));
|
||
}
|
||
|
||
/* ---------- The bar lines up with the table ----------
|
||
It was centred on the window. The table is centred on its own column, and the sidebar
|
||
takes 320px out of the right of that — so the two things a dealer is actually looking
|
||
at were 172px apart, at 960px wide and at 1600px wide and at every width in between,
|
||
because the offset is half the sidebar and the sidebar is a constant. The bar also
|
||
finished 80px inside the sidebar's column at 1280, painting over whichever panel
|
||
happened to be at that height.
|
||
|
||
Centred between the page's left padding and the table column's right edge instead.
|
||
Worth doing in offsets rather than a width: the auto margins still centre the bar, so
|
||
it stays 720px at most and centred inside whatever room this leaves it.
|
||
|
||
The arithmetic comes out exact at every width, above and below the 1400px cap, which
|
||
is worth showing because it looks like it shouldn't:
|
||
|
||
bar centre = (--sp-6 + 100vw - (--sidebarW + --sp-5 + --sp-6)) / 2
|
||
= (100vw - 344) / 2
|
||
table centre = appLeft + --sp-6 + (contentWidth - (--sidebarW + --sp-5)) / 2
|
||
|
||
and under the cap appLeft is 0 with content = 100vw - 64, while over it appLeft is
|
||
(100vw - 1400)/2 with content fixed at 1336. Both reduce to (100vw - 344) / 2.
|
||
|
||
No 100vw anywhere, which is the point — on a page with a scrollbar 100vw is wider
|
||
than the viewport, and that is exactly the trap the bar's own centring comment
|
||
warns about. */
|
||
@media (min-width: 901px) {
|
||
#actionBar {
|
||
left: var(--sp-6);
|
||
right: calc(var(--sidebarW) + var(--sp-5) + var(--sp-6));
|
||
}
|
||
}
|
||
|
||
.actionStatus {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: baseline;
|
||
gap: var(--sp-2) var(--sp-2);
|
||
margin-bottom: var(--sp-2);
|
||
font-size: var(--fs-sm);
|
||
}
|
||
|
||
/* The bar is fixed to the bottom, so anything that changes its height moves its top
|
||
edge — the one part of it the dealer is actually looking at. This row is where that
|
||
comes from: it wraps, and "PRE-FLOP · Pot $0 · 6 players" and "FLOP · Pot $128 ·
|
||
5 players" don't take the same number of lines at every width.
|
||
So while there is a hand running — which is when the undo pill is present, and the
|
||
only time the bar is tapped repeatedly — the row holds two lines' worth of room
|
||
whether it needs them or not. Reserving space is cheaper than moving. */
|
||
.actionStatus:has(.actionUndo) {
|
||
/* The undo pill's own height, the gap under it, and one line of status text. */
|
||
min-height: calc(30px + var(--sp-2) + 1.75rem);
|
||
}
|
||
|
||
/* Except on a phone held sideways, where the row is wide enough that it never wraps in
|
||
the first place — there is nothing to reserve against, and reserving anyway made the
|
||
bar 36px taller than it needed to be. That came straight off --feltRoomPx, and a
|
||
felt squeezed to 117px pushed the top seat's dealer button up over the header, where
|
||
it covered a corner of Next Hand. Height is scarce in landscape; this buys nothing
|
||
there. */
|
||
@media (orientation: landscape) and (max-height: 560px) {
|
||
.actionStatus:has(.actionUndo) {
|
||
min-height: 0;
|
||
}
|
||
}
|
||
|
||
/* Always there while there's something to take back, rather than a toast per tap.
|
||
|
||
Fixed width, not content width. The label names what would be taken back, so it
|
||
changes on every single action — "↶ dealing the hand" is 148px, "↶ Fay's call" is
|
||
108 — and a control that resizes under the dealer's thumb after every tap reads as
|
||
the interface twitching rather than responding. Measured across one hand: 148, 110,
|
||
109, 108, 109, 110, 120. Now it is 9rem whatever it says, and the long labels
|
||
ellipsize into it, which they were already set up to do. */
|
||
.actionUndo {
|
||
flex: none;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: var(--sp-1);
|
||
min-height: 30px;
|
||
width: 10rem;
|
||
max-width: 100%;
|
||
padding: var(--sp-0) var(--sp-2);
|
||
border-radius: var(--r-pill);
|
||
border: 1px solid #414141;
|
||
background: #262626;
|
||
color: var(--text-muted);
|
||
font-family: inherit;
|
||
font-size: var(--fs-sm);
|
||
cursor: pointer;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
@media (hover: hover) {
|
||
.actionUndo:hover {
|
||
background: var(--surface-3);
|
||
border-color: #4f4f4f;
|
||
color: var(--text);
|
||
}
|
||
}
|
||
|
||
/* Which hand this is, at the head of the row that says what hand you are in. Quieter
|
||
than the street beside it — the street is read every deal and this is read when
|
||
somebody asks — and tabular, because it is a number that ticks over while the dealer
|
||
is looking at it. */
|
||
.actionHand {
|
||
color: var(--text-dim);
|
||
font-variant-numeric: tabular-nums;
|
||
font-size: var(--fs-xs);
|
||
}
|
||
|
||
.actionStreet {
|
||
color: var(--gold);
|
||
font-weight: 600;
|
||
letter-spacing: 0.06em;
|
||
text-transform: uppercase;
|
||
font-size: var(--fs-xs);
|
||
}
|
||
|
||
.actionPot {
|
||
color: #e9e9e9;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
.actionPlayers {
|
||
color: var(--text-muted);
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
.actionRunout {
|
||
margin-left: auto;
|
||
color: var(--gold);
|
||
font-size: var(--fs-sm);
|
||
}
|
||
|
||
/* Who is being waited on, and what they have behind.
|
||
|
||
It already said `text-overflow: ellipsis`, and the ellipsis never once appeared. A
|
||
flex item's `min-width` is `auto`, which means "never smaller than your own content",
|
||
so instead of giving way this pushed the clock and the undo pill onto a third line —
|
||
and the row is in a bar fixed to the bottom, so a third line comes straight off the
|
||
felt. Measured at 360x640 mid-street: "Dee · $600 behind" left the bar 151px tall,
|
||
"Christopher · $1,200 behind" made it 173, and the table resized between one player
|
||
and the next. Whose turn it was decided how big the felt was.
|
||
|
||
`min-width: 0` is what lets the ellipsis do the job it was already written for. What
|
||
gives way is the name — see .actionWhoName. */
|
||
/* The second line of the status block, and it is a line because it is written as one.
|
||
|
||
`flex: 1 1 100%` is what makes it take a line of its own inside a wrapping parent —
|
||
which is the whole point. Flex settles its line breaks before it decides what to
|
||
shrink, so a long name in the wrapping row did not give way, it pushed the clock and
|
||
the undo pill onto a third line. Here the three of them are on a row that can shrink
|
||
instead of a row that can wrap. */
|
||
.actionActing {
|
||
/* Only what it holds, when all it holds is the undo pill. Claiming a line
|
||
unconditionally cost one in the state that can least afford it: while a board is
|
||
running out there is nobody to act, so this row is the pill alone — it used to sit
|
||
up beside "All-in — running out the board" and now it wanted a line of its own.
|
||
That is 22px off the felt on the twelve-handed 360x640 table where the pucks are
|
||
already at their 44px floor, and the sweep caught it as a seat's "ALL IN" going
|
||
back under the blind badge next door. */
|
||
flex: 0 1 auto;
|
||
display: flex;
|
||
/* The one that matters. Left wrapping, this row reproduced the fault it was made to
|
||
fix one level down: the undo pill is a fixed 9rem, so a long name pushed it onto a
|
||
line of its own and the block went back to being 84px for some players and 66 for
|
||
others. Not wrapping is what forces the shrink, and the shrink is what the name
|
||
is set up to absorb. */
|
||
flex-wrap: nowrap;
|
||
align-items: baseline;
|
||
gap: var(--sp-1) var(--sp-2);
|
||
min-width: 0;
|
||
}
|
||
|
||
/* And a line of its own the moment there is somebody being waited on, which is the case
|
||
this row exists for — the name, what they have behind, the clock, and the way back.
|
||
`flex: 1 1 100%` is what takes the line; nowrap above is what makes the name give way
|
||
inside it instead of pushing the pill onto another one. */
|
||
.actionActing:has(.actionWho) {
|
||
flex: 1 1 100%;
|
||
}
|
||
|
||
/* On a phone the pill gives some of its width back to the name beside it.
|
||
10rem is 160px, and at 360px the acting row has 319 to spend: the pill took half, the
|
||
clock took 50, and what was left over went to the player being waited on — measured,
|
||
"Christopher" was cut to 23px of its 75, which reads "Ch…". A third of the row is
|
||
enough for a safety net when the other thing on the line is the question the dealer is
|
||
actually answering.
|
||
|
||
Still one width at a time, which is the property that matters: the pill names the last
|
||
action, so its label changes on every tap, and it must not resize under a thumb.
|
||
A media query keeps that true. `min(10rem, 34%)` does not — the percentage resolves
|
||
against this row, the row is sized to its contents, and the two chase each other: the
|
||
sweep caught the pill drawn 38px wide at ten viewports. */
|
||
@media (max-width: 480px) {
|
||
.actionUndo {
|
||
width: 7rem;
|
||
}
|
||
}
|
||
|
||
/* Pushed to the far end of its own line, where it was before — the difference is that
|
||
it is now the end of a line this block chose rather than wherever the text ran out. */
|
||
.actionActing .actionUndo {
|
||
margin-left: auto;
|
||
}
|
||
|
||
/* The gap is the space before the separator, and it is here rather than in the string
|
||
because it cannot be in the string: these are two flex items, and a flex item's own
|
||
leading whitespace is collapsed at its edge. The bar has been reading "Alexandra·
|
||
$1,500 behind" ever since the name and the stack were split into two spans to decide
|
||
which of them gives way. */
|
||
.actionWho {
|
||
color: var(--text-muted);
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: var(--sp-1);
|
||
min-width: 0;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* The half that shrinks. */
|
||
.actionWhoName {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
min-width: 0;
|
||
}
|
||
|
||
/* And the half that doesn't. The same rule the seat pucks keep: a number read wrong is
|
||
a worse failure than a name read short, so the stack keeps every digit and the name
|
||
is what loses characters.
|
||
|
||
With a floor under the name, which is what this was missing: shrinking the only
|
||
shrinkable item until the line fits took "Christopher" to 0px on two of the three
|
||
phones in the sweep, and what the bar then read was "· $1,200 behind". So when the
|
||
name is being cut, this comes off the line instead — see ActionBar.fitActingLine.
|
||
The figure is on that player's seat puck either way; the name is the thing only the
|
||
bar is saying. */
|
||
.actionWhoStack {
|
||
flex: none;
|
||
}
|
||
|
||
.actionWhoStack[hidden] {
|
||
display: none;
|
||
}
|
||
|
||
/* Stopwatch, not a shot clock: it colours up as a prompt to the dealer, and does
|
||
nothing else — and a prompt that is on the line from the first second of every
|
||
decision is not a prompt. It arrives when the button beside it does, half a minute
|
||
in, which is also 50px of a 349px line handed back to the name. */
|
||
.actionTank {
|
||
flex: none;
|
||
min-width: 3.1rem;
|
||
text-align: right;
|
||
color: var(--text-muted);
|
||
font-variant-numeric: tabular-nums;
|
||
transition: color 0.3s ease;
|
||
}
|
||
|
||
.actionTank[hidden] {
|
||
display: none;
|
||
}
|
||
|
||
.actionTank.tankNudge {
|
||
color: var(--gold);
|
||
}
|
||
|
||
.actionTank.tankSlow {
|
||
color: #e07b7b;
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* Starting and stopping a clock. Quiet, because it only appears once somebody has
|
||
been thinking long enough for the table to ask.
|
||
|
||
44px like everything else a thumb has to find. It was drawn at 36 to keep the status
|
||
line short, and the audit called it at every touch viewport the moment it ran — which
|
||
is the rule doing its job: this control kills a hand, and the argument for shaving it
|
||
was that the row it sits in is made of text. */
|
||
.actionClockBtn {
|
||
flex: none;
|
||
min-height: 44px;
|
||
padding: var(--sp-1) var(--sp-2);
|
||
border-radius: var(--r-md);
|
||
border: 1px solid #5b4a2a;
|
||
background: rgba(216, 180, 106, 0.12);
|
||
color: #e8cf9a;
|
||
font-family: inherit;
|
||
font-size: var(--fs-sm);
|
||
cursor: pointer;
|
||
}
|
||
|
||
/* The count has run out. Said in the dealer's own words, above the button that
|
||
carries it out — and coloured like the thing it is, which is a hand about to be
|
||
killed for slow play. */
|
||
.clockExpired {
|
||
margin: 0 0 var(--sp-2);
|
||
padding: var(--sp-2) var(--sp-2);
|
||
border-radius: var(--r-md);
|
||
background: rgba(224, 123, 123, 0.12);
|
||
border: 1px solid rgba(224, 123, 123, 0.45);
|
||
color: #f3c9c9;
|
||
font-size: var(--fs-base);
|
||
}
|
||
|
||
.actionBtn.actionEnforceClock {
|
||
background: linear-gradient(180deg, #6b2b2b, #4a1d1d);
|
||
border-color: #8c3a3a;
|
||
color: #ffe2e2;
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* A player who has taken their chips and gone. Still on the list, because the box took
|
||
their cash and the night's record has to say so, but set apart so nobody counts money
|
||
out to them twice at 2am.
|
||
|
||
Set apart by a stripe rather than by fading the row, which is what this first did:
|
||
at 0.72 the contrast check measured the In and Won cells at 3.83:1, under AA, on the
|
||
list a dealer settles the night from. The row says "paid out" in words next to the
|
||
name — it doesn't need to be hard to read as well. */
|
||
.standingsRow.standingsGone {
|
||
background: rgba(255, 255, 255, 0.04);
|
||
border-left: 2px solid var(--line-strong);
|
||
padding-left: var(--sp-1);
|
||
}
|
||
|
||
.standingsRow.standingsGone .standingsName {
|
||
font-style: italic;
|
||
}
|
||
|
||
/* The people standing behind the chairs. A short list in a panel that already has a
|
||
lot in it, so it is built from rows rather than a table: a name, the seat it can go
|
||
in, and a way off the list. */
|
||
.waitingAdd {
|
||
display: flex;
|
||
gap: var(--sp-2);
|
||
margin-bottom: var(--sp-2);
|
||
}
|
||
|
||
.waitingAdd input {
|
||
flex: 1;
|
||
min-width: 0;
|
||
min-height: 44px;
|
||
background: var(--surface-0);
|
||
border: 1px solid var(--line);
|
||
border-radius: var(--r-md);
|
||
color: var(--text);
|
||
font-family: inherit;
|
||
/* 16px, or iOS zooms the whole page in when it takes focus. */
|
||
font-size: 16px;
|
||
padding: var(--sp-1) var(--sp-2);
|
||
}
|
||
|
||
.waitingAdd .holdemBtn {
|
||
flex: none;
|
||
min-height: 44px;
|
||
/* "Add" is three letters, which came out 43px wide — one pixel under the rule, and
|
||
the audit called it. A thumb doesn't know the word is short. */
|
||
min-width: 56px;
|
||
}
|
||
|
||
.waitingList {
|
||
list-style: none;
|
||
counter-reset: waiting;
|
||
margin: 0 0 var(--sp-2);
|
||
padding: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--sp-1);
|
||
}
|
||
|
||
.waitingRow {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--sp-2);
|
||
counter-increment: waiting;
|
||
}
|
||
|
||
/* The position in the queue, which is the whole point of the list. */
|
||
.waitingRow::before {
|
||
content: counter(waiting) ".";
|
||
flex: none;
|
||
min-width: 1.2rem;
|
||
color: var(--text-muted);
|
||
font-size: var(--fs-sm);
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
.waitingName {
|
||
flex: 1;
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
color: var(--text);
|
||
font-size: var(--fs-base);
|
||
}
|
||
|
||
.waitingNote {
|
||
flex: none;
|
||
color: var(--text-muted);
|
||
font-size: var(--fs-sm);
|
||
}
|
||
|
||
.waitingRow .holdemBtn {
|
||
flex: none;
|
||
min-height: 44px;
|
||
}
|
||
|
||
.waitingDropBtn {
|
||
min-width: 44px;
|
||
}
|
||
|
||
/* ---------- Side-pot breakdown ---------- */
|
||
|
||
.potBreakdown {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--sp-1);
|
||
margin-bottom: var(--sp-2);
|
||
padding: var(--sp-2) var(--sp-2);
|
||
background: rgba(216, 180, 106, 0.07);
|
||
border: 1px solid rgba(216, 180, 106, 0.25);
|
||
border-radius: var(--r-md);
|
||
}
|
||
|
||
.potBreakdownHeading {
|
||
margin: 0 0 var(--sp-1);
|
||
color: var(--gold);
|
||
font-size: var(--fs-xs);
|
||
letter-spacing: 0.08em;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.potLayer {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: var(--sp-2);
|
||
font-size: var(--fs-sm);
|
||
}
|
||
|
||
.potLayerLabel {
|
||
color: var(--gold);
|
||
font-weight: 600;
|
||
flex: none;
|
||
}
|
||
|
||
.potLayerAmount {
|
||
color: var(--text-bright);
|
||
font-variant-numeric: tabular-nums;
|
||
font-weight: 600;
|
||
flex: none;
|
||
}
|
||
|
||
.potLayerWho {
|
||
color: var(--text-muted);
|
||
flex: 1;
|
||
min-width: 0;
|
||
text-align: right;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.potBreakdownWizard {
|
||
margin-bottom: var(--sp-4);
|
||
}
|
||
|
||
/* The action bar is a fixed strip and its rows have to stay one line each; the wizard
|
||
is a dialog that scrolls, and this is the announcement a dealer reads out loud before
|
||
anybody shows a hand. So here the names are allowed the room to be read, and nothing
|
||
in the announcement is ever cut off. */
|
||
.potBreakdownWizard .potLayerWho {
|
||
white-space: normal;
|
||
overflow: visible;
|
||
}
|
||
|
||
.potLayerReturn .potLayerLabel,
|
||
.potLayerReturn .potLayerAmount {
|
||
color: var(--text-muted);
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* The layers between the main pot and the newest side pot, collapsed onto one line so
|
||
the bar's height stops depending on how many people are all-in. Set back rather than
|
||
picked out: it is the one row in the list that is not a pot to announce, and reading
|
||
as a pot is exactly the mistake it could cause. */
|
||
.potLayerMore .potLayerLabel,
|
||
.potLayerMore .potLayerAmount {
|
||
color: var(--text-muted);
|
||
font-weight: 500;
|
||
font-style: italic;
|
||
}
|
||
|
||
.actionButtons {
|
||
display: flex;
|
||
gap: var(--sp-2);
|
||
}
|
||
|
||
.actionBtn {
|
||
position: relative;
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: var(--sp-0);
|
||
min-height: 56px;
|
||
padding: var(--sp-2) var(--sp-2);
|
||
border-radius: var(--r-md);
|
||
border: 1px solid var(--line);
|
||
background: linear-gradient(180deg, var(--surface-3), var(--surface-2));
|
||
color: var(--text);
|
||
font-family: inherit;
|
||
font-size: var(--fs-md);
|
||
cursor: pointer;
|
||
transition: all 0.12s ease;
|
||
}
|
||
|
||
@media (hover: hover) {
|
||
.actionBtn:hover {
|
||
border-color: var(--line-strong);
|
||
background: linear-gradient(180deg, var(--line), #2a2a2a);
|
||
}
|
||
}
|
||
|
||
.actionBtn:active {
|
||
transform: translateY(1px);
|
||
}
|
||
|
||
.actionBtnLabel {
|
||
font-weight: 600;
|
||
}
|
||
|
||
.actionBtnSub {
|
||
font-size: var(--fs-sm);
|
||
color: #a8a8a8;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
/* The undo pill's key, inline rather than pinned to a corner — see actionBar.js. */
|
||
/* The half of the pill that gives: the label shortens, the keycap never does.
|
||
|
||
And which part of the label shortens is decided too. It is three items rather than one
|
||
string — the arrow, the actor, the act — so that a pill too narrow for all of it drops
|
||
the actor whole instead of cutting the sentence off wherever the ellipsis lands. On a
|
||
phone this label has 73px and "↶ Mike from work's fold" wants 128; cut, that reads
|
||
"↶ Mike from wor…", which names everything except the thing you would be taking back.
|
||
See ActionBar.fitUndoLabel. */
|
||
.actionUndoLabel {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: var(--sp-1);
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.actionUndoGlyph,
|
||
.actionUndoLead {
|
||
flex: none;
|
||
}
|
||
|
||
.actionUndoLead[hidden] {
|
||
display: none;
|
||
}
|
||
|
||
/* The half that stays. It can still ellipsize — "raise to $1,200" is 69px of the 59 the
|
||
label has on the narrowest phone once the arrow is drawn — and it is the only act that
|
||
ever does. */
|
||
.actionUndoAct {
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.actionUndoKey {
|
||
/* See .actionKey below for why these are not fainter. */
|
||
flex: none;
|
||
/* Pinned to the end of the pill rather than trailing the label, so it sits in the
|
||
same place whatever the label says. */
|
||
margin-left: auto;
|
||
padding: 0 var(--sp-1);
|
||
border-radius: var(--r-sm);
|
||
border: 1px solid currentColor;
|
||
opacity: 0.9;
|
||
font-family: inherit;
|
||
font-size: var(--fs-2xs);
|
||
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.
|
||
|
||
Raised again from 0.75 to 0.9 for the same reason the text tokens moved: clearing
|
||
4.5:1 was never the same thing as being readable on this ground, and a fade is the
|
||
most expensive way to say "quieter" when the ground is already near black. The
|
||
keycap's border and its size still say it, and both are free. */
|
||
.actionKey {
|
||
position: absolute;
|
||
top: 5px;
|
||
right: 7px;
|
||
padding: 0 var(--sp-1);
|
||
border-radius: var(--r-sm);
|
||
border: 1px solid currentColor;
|
||
opacity: 0.9;
|
||
font-family: inherit;
|
||
font-size: var(--fs-2xs);
|
||
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.
|
||
|
||
The undo pill's cap is a different class (see .actionUndoKey: this one pins itself to
|
||
a corner and that one sits inline) and had been left out of this rule since the day it
|
||
was written, so the one control on the bar too narrow to spare the room was the only
|
||
one still printing a shortcut nothing could press. It is 18px and a 4px gap of a 73px
|
||
label — enough, on its own, for "raise to $1,200" to keep its last three digits. The
|
||
Keyboard panel still lists the shortcut for anyone who has a keyboard to use it. */
|
||
@media (hover: none) {
|
||
.actionKey,
|
||
.actionUndoKey {
|
||
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, var(--gold-tint), var(--gold-shade));
|
||
border-color: var(--gold-deep);
|
||
color: var(--gold-bright);
|
||
}
|
||
|
||
/* Deal Flop, Deal Turn, Deal River, "Showdown — resolve pot", "Award pot to Dee".
|
||
Every one of them is the only thing the bar is offering at the moment it appears, and
|
||
every one of them was the quietest control on the screen.
|
||
|
||
Measured over a whole hand, seventeen steps: the accent green was on two controls at
|
||
thirteen of them — the bar's Call and the header's Resolve Pot, painted the identical
|
||
gradient at opposite ends of a phone — and at the other four it was on the header
|
||
alone while the bar held the only button there was to press. At showdown that is as
|
||
wrong as it gets: "Showdown — resolve pot" is the most consequential tap in the hand
|
||
and it was blue-grey, while a shortcut in the corner that had looked exactly the same
|
||
for the previous five minutes was green.
|
||
|
||
So the accent goes where the next step is. The same green as Call, deliberately,
|
||
because the rule worth learning is "green is the thing to do next" and not "green is
|
||
a player's action" — and the two can never be confused, because they never appear
|
||
together: renderStreetDone and renderHandOver each build their own row and neither
|
||
ever renders alongside the fold/call/raise triad. Checked over a full hand, and
|
||
checked again by the audit on every viewport. The fold red and the raise gold are
|
||
untouched, and the labels and the shortcut glyph still tell them apart. */
|
||
.actionAdvance {
|
||
background: linear-gradient(180deg, #16783a, #0d5225);
|
||
border-color: #1e8449;
|
||
color: #ecffef;
|
||
}
|
||
|
||
/* ---------- Raise pad ---------- */
|
||
|
||
.raiseQuick {
|
||
display: flex;
|
||
gap: var(--sp-2);
|
||
margin-bottom: var(--sp-2);
|
||
}
|
||
|
||
.raiseQuickBtn {
|
||
min-height: 48px;
|
||
font-size: var(--fs-base);
|
||
}
|
||
|
||
.raiseQuickBtn.selected {
|
||
border-color: var(--gold);
|
||
background: linear-gradient(180deg, var(--gold-tint), var(--gold-shade));
|
||
color: var(--gold-bright);
|
||
}
|
||
|
||
.raiseEntry {
|
||
display: flex;
|
||
gap: var(--sp-2);
|
||
}
|
||
|
||
.raiseInput {
|
||
flex: 1;
|
||
min-width: 0;
|
||
background: var(--surface-0);
|
||
border: 1px solid var(--line);
|
||
border-radius: var(--r-md);
|
||
color: var(--text-bright);
|
||
font-family: inherit;
|
||
font-size: 16px;
|
||
font-variant-numeric: tabular-nums;
|
||
text-align: center;
|
||
padding: var(--sp-2);
|
||
}
|
||
|
||
.raiseInput:focus {
|
||
outline: none;
|
||
border-color: var(--gold);
|
||
}
|
||
|
||
.raiseCancel {
|
||
flex: 0 0 auto;
|
||
min-width: 84px;
|
||
}
|
||
|
||
.raiseConfirm {
|
||
flex: 0 0 auto;
|
||
min-width: 116px;
|
||
background: linear-gradient(180deg, var(--gold-tint), var(--gold-shade));
|
||
border-color: var(--gold-deep);
|
||
color: var(--gold-bright);
|
||
}
|
||
|
||
/* 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);
|
||
}
|
||
}
|
||
}
|
||
|
||
/* One line, always — the same rule every design system gives a badge, and the one
|
||
thing on this puck that was never told it.
|
||
|
||
A stack figure has no spaces in it, so it could never wrap; it overflows instead, and
|
||
the puckAmountLong/Huge sizes below exist to stop it. "ALL IN" has a space, and that
|
||
space cost the two smallest phones their seat layout.
|
||
|
||
Measured at 360x640 with twelve players all-in: "ALL IN" wanted 36px and had 34. It
|
||
broke onto a second line, an 11px line became 22, and the puck went from 55px tall to
|
||
66. Height is exactly what is scarce at the ends of the ring — the seats there are
|
||
separated vertically, so a puck 11px taller is a puck 11px further into its
|
||
neighbour. The layout search then walked its candidate widths, found none that
|
||
cleared, and settled for the narrowest, which is also the one that wraps hardest.
|
||
|
||
Two pixels, and every seat on the felt paid for them. */
|
||
/* The stack is not secondary text, and it had been styled as some for as long as this
|
||
file has existed: muted grey under a name set in --text at a heavier weight. Read
|
||
back the other way, that says a dealer needs the name more than the number — and
|
||
during a hand the opposite is true. The name is looked up once, to find the chair;
|
||
the stack is read on every single action, because it is what decides whether a player
|
||
can cover the bet. It measured Lc 44 on the puck, the dimmest thing on the felt that
|
||
anyone has to read.
|
||
|
||
Set in --text now. The name is still the louder of the two — it is bigger and it is
|
||
600 — so the hierarchy is intact; it is just carried by size and weight instead of by
|
||
fading the number underneath it. */
|
||
.seatPuckStack {
|
||
font-size: var(--seatAmountSize);
|
||
color: var(--text);
|
||
font-variant-numeric: tabular-nums;
|
||
margin-top: var(--sp-0);
|
||
white-space: nowrap;
|
||
/* Held open on the empty chairs too, so an open seat is the same puck as a taken
|
||
one with nothing written in it — which is what an open seat is. */
|
||
min-height: var(--seatRowMoney);
|
||
}
|
||
|
||
/* "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);
|
||
}
|
||
|
||
/* The two steps an amount comes down when it is too wide for its puck. A number has no
|
||
spaces to break on, so an amount too wide neither wraps nor ellipsises — it draws
|
||
straight out over the seat next to it. 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.
|
||
|
||
Both are steps *of* --seatAmountSize rather than sizes of their own, so they follow
|
||
the tight ring's cap down instead of stepping back over it — and so the ladder is one
|
||
number with two fractions under it rather than three sizes to keep in agreement.
|
||
|
||
Which step a figure gets is measured, not counted. See Render.fitAmounts: the old
|
||
rule was eight characters for the first and ten for the second, which is a guess at
|
||
how wide a figure is, and it stopped being true the moment anybody turned their
|
||
phone's text up. */
|
||
.seatPuckStack.puckAmountLong,
|
||
.seatPuckTotal.puckAmountLong {
|
||
font-size: calc(var(--seatAmountSize) * 0.85);
|
||
letter-spacing: -0.01em;
|
||
}
|
||
|
||
.seatPuckStack.puckAmountHuge,
|
||
.seatPuckTotal.puckAmountHuge {
|
||
font-size: calc(var(--seatAmountSize) * 0.7);
|
||
letter-spacing: -0.02em;
|
||
}
|
||
|
||
.blindsRow {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: var(--sp-2);
|
||
}
|
||
|
||
.blindsRow .fieldRow {
|
||
flex: 1 1 120px;
|
||
min-width: 0;
|
||
gap: var(--sp-2);
|
||
}
|
||
|
||
/* "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: var(--sp-5);
|
||
margin: 0 auto;
|
||
z-index: 80;
|
||
max-width: 560px;
|
||
padding: 0 var(--sp-3);
|
||
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: var(--sp-3);
|
||
background: linear-gradient(180deg, var(--surface-3), #202020);
|
||
border: 1px solid #414141;
|
||
border-left: 3px solid var(--gold);
|
||
border-radius: var(--r-md);
|
||
padding: var(--sp-3) var(--sp-3) var(--sp-3) var(--sp-4);
|
||
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);
|
||
}
|
||
|
||
/* Reduced motion keeps the fade and loses the travel.
|
||
|
||
This file already honoured the setting in three places, and all three were the wrong
|
||
size of thing: a 2px press-down on a button, a checkbox's tick, and the acting seat's
|
||
pulse. What it never covered was the motion that actually moves something across the
|
||
screen — and for anyone who gets motion sick, a 12px slide is the part that matters,
|
||
not a button dipping under a thumb.
|
||
|
||
Deleting the animation outright is the wrong answer too. A toast that simply appears,
|
||
already in place, reads as a rendering glitch rather than as news, and the same
|
||
guidance that says to cut the movement says to keep something: fade it instead, so
|
||
the interface still feels like it responded. */
|
||
@media (prefers-reduced-motion: reduce) {
|
||
#toastHost {
|
||
transition: none;
|
||
}
|
||
|
||
.toast {
|
||
transform: none;
|
||
transition: opacity 0.18s ease;
|
||
}
|
||
|
||
.toast.toastVisible {
|
||
transform: none;
|
||
}
|
||
}
|
||
|
||
.toastText {
|
||
flex: 1;
|
||
min-width: 0;
|
||
color: #e9e9e9;
|
||
font-size: var(--fs-base);
|
||
line-height: 1.35;
|
||
}
|
||
|
||
.toastAction {
|
||
flex: none;
|
||
min-height: 38px;
|
||
padding: var(--sp-1) var(--sp-4);
|
||
border-radius: var(--r-md);
|
||
border: 1px solid var(--gold);
|
||
background: rgba(216, 180, 106, 0.12);
|
||
color: var(--gold-bright);
|
||
font-size: var(--fs-base);
|
||
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: var(--text-muted);
|
||
font-size: var(--fs-base);
|
||
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: var(--surface-3);
|
||
color: var(--text);
|
||
}
|
||
}
|
||
|
||
/* ---------- 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: var(--sp-5);
|
||
}
|
||
|
||
#confirmOverlay[hidden] {
|
||
display: none;
|
||
}
|
||
|
||
#confirmModal {
|
||
background: var(--surface-1);
|
||
border: 1px solid var(--line);
|
||
border-radius: var(--r-md);
|
||
width: 100%;
|
||
max-width: 420px;
|
||
padding: var(--sp-5) var(--sp-5);
|
||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
|
||
outline: none;
|
||
}
|
||
|
||
#confirmTitle {
|
||
margin: 0 0 var(--sp-2);
|
||
font-size: var(--fs-lg);
|
||
font-weight: 500;
|
||
color: var(--text-bright);
|
||
}
|
||
|
||
#confirmMessage {
|
||
margin: 0 0 var(--sp-5);
|
||
color: #a5a5a5;
|
||
font-size: var(--fs-base);
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.confirmActions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: var(--sp-2);
|
||
}
|
||
|
||
.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: var(--sp-5);
|
||
}
|
||
|
||
#potWizardOverlay[hidden] {
|
||
display: none;
|
||
}
|
||
|
||
#potWizardModal {
|
||
background: var(--surface-1);
|
||
border: 1px solid var(--line);
|
||
border-radius: var(--r-md);
|
||
/* So the fade over the bottom of the list has something to hang off. */
|
||
position: relative;
|
||
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.
|
||
That was true while this overlay sat at z-index 50, below the bar's 55. It sits
|
||
at 95 now and the bar cannot draw on it, so the subtraction buys nothing and
|
||
costs 110px of a 640px screen — which on a Galaxy S8 is the difference between
|
||
a three-way pot showing all three names and showing two of them. */
|
||
max-height: 85vh;
|
||
/* A column of three: the title, the part that scrolls, and the buttons. The modal
|
||
itself no longer scrolls — only its middle does — which is what keeps Confirm on
|
||
screen without asking a sticky footer to do something it cannot. */
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
padding: var(--sp-5) var(--sp-5) var(--sp-5);
|
||
}
|
||
|
||
#potWizardBody {
|
||
overflow-y: auto;
|
||
overscroll-behavior: contain;
|
||
/* Without this a flex child refuses to be shorter than its content, and the middle
|
||
would push the buttons back off the bottom of the dialog. */
|
||
min-height: 0;
|
||
/* Room for a focus ring on the first and last rows, which a flush edge clips. */
|
||
padding: 2px;
|
||
margin: -2px;
|
||
scroll-padding: var(--sp-4);
|
||
}
|
||
|
||
/* The footer is itself the row of buttons, so a step that appends a single button gets
|
||
the same full-width treatment as one that appends a pair — the winner step used to
|
||
put a lone Confirm in the left half of the dialog, looking like one option out of
|
||
several that hadn't drawn yet. */
|
||
/* There is more list above the buttons than the dialog can show.
|
||
|
||
Measured on a Galaxy S8: three players are in the pot, the list holds three rows, and
|
||
two of them fit. The third was cut off exactly on a row boundary, immediately under a
|
||
hairline that reads as the end of the list — so the dialog that pays the table looked
|
||
finished with a name missing from it, and a dealer ticking the two they could see
|
||
would have paid two thirds of a three-way pot. The list does scroll; nothing said so.
|
||
Now the cut runs through a row rather than between them, under a fade, which is what
|
||
a list that continues looks like. */
|
||
#potWizardModal.potWizardMoreBelow #potWizardFooter::before {
|
||
content: "";
|
||
position: absolute;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 100%;
|
||
height: 2.25rem;
|
||
background: linear-gradient(rgba(30, 30, 30, 0), var(--surface-1) 85%);
|
||
pointer-events: none;
|
||
}
|
||
|
||
#potWizardFooter {
|
||
flex: none;
|
||
position: relative;
|
||
display: flex;
|
||
gap: var(--sp-2);
|
||
margin-top: var(--sp-4);
|
||
/* A hairline, so the buttons read as sitting below the list rather than as its
|
||
last row — the body scrolls under them and the edge is what says so. */
|
||
padding-top: var(--sp-3);
|
||
border-top: 1px solid var(--line);
|
||
}
|
||
|
||
#potWizardFooter > .holdemBtn,
|
||
#potWizardFooter > .potWizardActions {
|
||
flex: 1;
|
||
}
|
||
|
||
#potWizardFooter:empty {
|
||
display: none;
|
||
}
|
||
|
||
/* No longer sticky, and doesn't need to be: the modal stopped scrolling when its body
|
||
became the only thing that does, so the title and the ✕ stay put on their own. */
|
||
.potWizardHeader {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
flex: none;
|
||
margin-bottom: var(--sp-3);
|
||
}
|
||
|
||
.potWizardHeader h2 {
|
||
margin: 0;
|
||
font-size: var(--fs-lg);
|
||
font-weight: 400;
|
||
color: var(--text-bright);
|
||
}
|
||
|
||
.potWizardLog {
|
||
margin-bottom: var(--sp-3);
|
||
}
|
||
|
||
.potWizardLogLine {
|
||
font-size: var(--fs-base);
|
||
color: var(--text-muted);
|
||
margin: var(--sp-1) 0;
|
||
}
|
||
|
||
.potWizardPrompt {
|
||
font-size: var(--fs-md);
|
||
color: var(--text);
|
||
margin-bottom: var(--sp-3);
|
||
}
|
||
|
||
/* This is the one screen in the tool that divides money, and its Confirm button sat
|
||
below a scroll at every size — measured with six players and a side pot: 244px of it
|
||
off the bottom on an iPhone 14, 417 on a Galaxy S8, 583 on a phone held sideways,
|
||
172 on a desktop. A dealer with the table waiting had to scroll a list of names to
|
||
find the only control that finishes the hand, and nothing on screen said there was
|
||
anything below it.
|
||
|
||
Capping the list rather than pinning the button, because a sticky footer cannot work
|
||
here: the actions are the last child of the scrolling content, so their containing
|
||
block ends exactly where they do and there is no room for `bottom` to lift them. The
|
||
list is the only part that grows without limit anyway — the pot breakdown, the
|
||
prompt and the buttons are all a fixed size — so bounding it keeps the whole dialog
|
||
inside the screen and the button where the dealer's thumb already is. */
|
||
/* What the tool read off the felt, above the list of names. Two of them: the one that
|
||
settles the pot, and the one that says why it could not. */
|
||
.potWizardVerdict {
|
||
margin: 0 0 var(--sp-3);
|
||
padding: var(--sp-2) var(--sp-3);
|
||
border-radius: var(--r-md);
|
||
border-left: 3px solid var(--line-strong);
|
||
background: var(--surface-0);
|
||
font-size: var(--fs-base);
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.potWizardVerdictSure {
|
||
border-left-color: var(--gold);
|
||
color: var(--text-bright);
|
||
}
|
||
|
||
/* Not a warning — the dealer picking the winner themselves is the tool's own default
|
||
and always has been. This says only that the reading is theirs to make this time. */
|
||
.potWizardVerdictUnsure {
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
/* The hand each player is holding, under their name. Muted, because the row's job is
|
||
still to be a name and a target — this is the working, shown so the answer above can
|
||
be checked rather than taken. */
|
||
.potWizardChoiceHand {
|
||
color: var(--text-muted);
|
||
font-size: var(--fs-xs);
|
||
line-height: 1.3;
|
||
}
|
||
|
||
/* What this player can win, out beside the name rather than under it.
|
||
It used to sit in the same column as the name, and the read hand now has that line —
|
||
three stacked lines makes a 100px row, and a nine-handed showdown is nine of them.
|
||
Beside is also where the phone layout had already moved it, for the same reason. */
|
||
.potWizardChoiceUpTo {
|
||
flex: none;
|
||
align-self: center;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.potWizardChoices {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--sp-2);
|
||
margin-bottom: var(--sp-2);
|
||
}
|
||
|
||
/* 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: var(--sp-3);
|
||
min-height: 56px;
|
||
background: var(--surface-0);
|
||
border: 1px solid var(--surface-3);
|
||
border-radius: var(--r-md);
|
||
padding: var(--sp-2) var(--sp-3);
|
||
cursor: pointer;
|
||
font-size: var(--fs-md);
|
||
transition: border-color 0.12s ease, background 0.12s ease;
|
||
}
|
||
|
||
@media (hover: hover) {
|
||
.potWizardChoice:hover {
|
||
border-color: #555;
|
||
background: var(--surface-1);
|
||
}
|
||
}
|
||
|
||
/* The one checkbox in the tool that is drawn rather than asked for.
|
||
The comment here called it "the last raw browser control", which was not true when
|
||
it was written — six more were still the platform's, and they were white because the
|
||
page had never declared `color-scheme`. That is fixed at the top of this file, and
|
||
fixing it there is what makes this one the exception rather than the rule.
|
||
|
||
It stays painted because it is not a setting: it is the control a dealer ticks to
|
||
move money, with players watching, and it wants a fill and a tick that read across a
|
||
table rather than the platform's 24px square. What painting costs is the browser's
|
||
own answer to forced-colors mode, which is bought back below.
|
||
|
||
It takes its size from the checkbox rule near the top rather than restating it, so
|
||
there is one number for how big a checkbox is here. */
|
||
.potWizardChoice input {
|
||
flex: none;
|
||
appearance: none;
|
||
-webkit-appearance: none;
|
||
border: 2px solid var(--line-strong);
|
||
border-radius: var(--r-sm);
|
||
background: var(--surface-2);
|
||
display: grid;
|
||
place-content: center;
|
||
cursor: pointer;
|
||
transition: background 0.12s ease, border-color 0.12s ease;
|
||
}
|
||
|
||
/* The tick is drawn rather than typed, so it can't pick up a font the device happens
|
||
to substitute — and it scales with the box instead of with the text around it. */
|
||
.potWizardChoice input::before {
|
||
content: "";
|
||
width: 12px;
|
||
height: 12px;
|
||
transform: scale(0);
|
||
transition: transform 0.12s ease;
|
||
background: var(--surface-0);
|
||
clip-path: polygon(14% 46%, 0 60%, 40% 100%, 100% 22%, 86% 8%, 38% 72%);
|
||
}
|
||
|
||
.potWizardChoice input:checked {
|
||
background: #1e8449;
|
||
border-color: #2fa563;
|
||
}
|
||
|
||
.potWizardChoice input:checked::before {
|
||
transform: scale(1);
|
||
}
|
||
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.potWizardChoice input,
|
||
.potWizardChoice input::before {
|
||
transition: none;
|
||
}
|
||
}
|
||
|
||
/* What the paint job owes back.
|
||
In forced-colors mode the OS replaces every colour on the page with its own palette,
|
||
and a tick drawn as a `background` on a pseudo-element is not a colour it knows how
|
||
to replace — it is simply gone. So this box, alone among the tool's checkboxes, had
|
||
no visible checked state at all in Windows High Contrast Mode, on the dialog that
|
||
pays the table. The system keywords are the fix: they are the only colours
|
||
guaranteed to survive, because they *are* the palette. */
|
||
@media (forced-colors: active) {
|
||
.potWizardChoice input {
|
||
border-color: ButtonBorder;
|
||
background: Canvas;
|
||
}
|
||
|
||
.potWizardChoice input::before {
|
||
background: CanvasText;
|
||
}
|
||
|
||
.potWizardChoice input:checked {
|
||
border-color: Highlight;
|
||
background: Canvas;
|
||
}
|
||
|
||
/* The row's own chosen state is a background gradient, which goes the same way. */
|
||
.potWizardChoice.chosen {
|
||
outline: 2px solid Highlight;
|
||
}
|
||
}
|
||
|
||
/* 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: var(--sp-0);
|
||
}
|
||
|
||
.potWizardChoiceName {
|
||
color: var(--text-bright);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.potWizardChoiceUpTo {
|
||
color: var(--text-muted);
|
||
font-size: var(--fs-sm);
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
.potWizardChoiceCards {
|
||
flex: none;
|
||
font-family: var(--font-mono, monospace);
|
||
font-size: var(--fs-md);
|
||
font-weight: 700;
|
||
color: var(--text);
|
||
}
|
||
|
||
.potWizardChoiceCards .cardRed {
|
||
color: #ff6b6b;
|
||
}
|
||
|
||
/* What the house takes, above the payouts it comes out of. Stated in the summary
|
||
rather than only in the log afterwards: this is the dealer's last chance to
|
||
disagree with the arithmetic before the chips move, and money taken quietly is the
|
||
one thing a tool like this must never do. */
|
||
.potWizardRake {
|
||
margin: 0 0 var(--sp-3);
|
||
padding: var(--sp-2) var(--sp-2);
|
||
border-radius: var(--r-md);
|
||
background: rgba(216, 180, 106, 0.1);
|
||
border: 1px solid rgba(216, 180, 106, 0.35);
|
||
color: #e8cf9a;
|
||
font-size: var(--fs-base);
|
||
}
|
||
|
||
.potWizardSummary ul {
|
||
list-style: none;
|
||
padding: 0;
|
||
margin: 0 0 var(--sp-4);
|
||
}
|
||
|
||
.potWizardSummary li {
|
||
padding: var(--sp-2) 0;
|
||
border-bottom: 1px dashed var(--surface-3);
|
||
font-size: var(--fs-md);
|
||
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: var(--sp-0);
|
||
font-size: var(--fs-sm);
|
||
color: #9fb8a6;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
.potWizardActions {
|
||
display: flex;
|
||
gap: var(--sp-2);
|
||
}
|
||
|
||
/* 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 cap lives here now, and the reason it didn't is worth keeping.
|
||
|
||
It was measured and rejected once: 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 narrow layout
|
||
fell back on reserving the bar's height as scroll room, on the grounds that a
|
||
phone is a thing you scroll.
|
||
|
||
That trade was real then and isn't now: seats are spaced by distance around the
|
||
rim rather than by angle, so a shorter felt no longer piles them up. Re-measured
|
||
at 360x640 with the bar grown by a side pot, where the bottom seat sat 38px
|
||
inside it: the ring now clears the bar by 50. Scroll room is not an answer for a
|
||
seat that is covered rather than merely below the fold.
|
||
|
||
The room is spent on height, not on both. Capping the *width* by it — the wide
|
||
layout's way, because there the felt is sized from its width — kept the table
|
||
square and threw away 96px of a 360px screen: 240x240 inside a 336px column. A
|
||
max-height leaves the width alone and lets the aspect ratio give way, so a short
|
||
screen gets a wide oval, which is the shape a poker table is anyway. Measured at
|
||
360x640: 336x240 rather than 240x240, same clearance, and the twelve-seat ring
|
||
that was crowded at 240 comes out clean.
|
||
|
||
"Where the room isn't the binding constraint — 390x844 and up — nothing changes
|
||
and the table stays round" is what this used to say, and it was true and it was
|
||
the bug. A square felt on a portrait phone can never be taller than it is wide,
|
||
so on every phone with room to spare the surplus was measured, published, and
|
||
dropped: 522px of room under a 366px felt on an iPhone 14, 638 under 406 on a Pro
|
||
Max. Nothing was wrong with the arithmetic. The shape simply refused the answer.
|
||
|
||
So the preferred shape is an oval standing up, and the cap above stays to stop it.
|
||
A phone is a portrait screen and this is a table seen from above — the wide layout
|
||
has been an oval lying down (0.72) all along, and the two are the same decision
|
||
made twice. What it buys is rim: twelve seats at 390x844 went from a 44px puck
|
||
with five names clipped to 66px with one, and five seats went from three clipped
|
||
to none.
|
||
|
||
1.5 is where the sweep stops paying. 1.6 and 1.7 measured byte-identical to each
|
||
other on every phone, because by then the room is the binding constraint again;
|
||
between 1.35 and 1.5 the twelve-seat ring on a Pixel 7 goes from one clipped name
|
||
to none. The three smaller phones are room-bound at any cap at all and measured
|
||
identical from 1.15 to 1.7, so this cannot regress them.
|
||
|
||
It was tempting to pick a lower number and claim it bought stillness — a felt
|
||
whose height *is* --feltRoomPx moves every time the bar does. That claim was
|
||
written here and it was wrong: the bar's growth was measured at 125px resting,
|
||
233px with six players all in, and 341px with twelve and their nine side pots
|
||
listed. 216px of growth is more slack than any phone has at any aspect ratio, so
|
||
there is no number here that buys stillness. renderSeats() is
|
||
already told when the room changes, which is the thing that actually holds the
|
||
chips on their seats, and this rule stops trying to.
|
||
|
||
A floor under the max-height was tried here and taken out again, which is worth
|
||
writing down because it looks obviously right. The worry was that max-height has
|
||
no lower bound, so a tall enough bar presses the felt flat; the fix was to floor
|
||
it at 72cqw, the wide layout's own oval, so the worst a phone could do was arrive
|
||
at the shape the desktop already has.
|
||
|
||
It made things worse. Twelve players all in with nine side pots is the tallest the
|
||
bar gets — 341px — and at 360x640 that leaves 142px of room. Floored to 242 the
|
||
felt keeps its shape and puts five of the twelve seats behind the bar; unfloored
|
||
at 142 it is a flat table with all twelve of them visible. Same on an iPhone SE,
|
||
three seats against none. A covered seat is the fault this whole measurement
|
||
exists to prevent, and a flat table is only ugly.
|
||
|
||
The slit the floor was defending against was never reachable either. It came from
|
||
forcing --feltRoomPx to 10px by hand; the real floor is FELT_FLOOR_PX at 96, and
|
||
the worst any of these phones actually publishes is 142, which lays out fine.
|
||
|
||
Landscape overrides all of this outright, further down the file. */
|
||
#pokerTableWrap {
|
||
--feltAspect: 1.5;
|
||
max-width: none;
|
||
max-height: var(--feltRoomPx, 100vh);
|
||
}
|
||
|
||
/* 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;
|
||
}
|
||
|
||
/* No width here any more. This was 78px, and two blocks below it were 64 and 62 —
|
||
three guesses, at three breakpoints, at the one number that depends on something
|
||
a media query cannot see: how many people are sitting down. The same 390px phone
|
||
has room for a 90px puck at four seats and 61px at nine. render.js measures it. */
|
||
.seat {
|
||
min-height: 54px;
|
||
}
|
||
}
|
||
|
||
/* One column, and still wide enough that a round table fits. Below 701px the block
|
||
further up stands the oval up instead, which is why this has to be written as a
|
||
range rather than as another max-width: both queries match a phone, they set the
|
||
same custom property at the same specificity, and the one lower in the file wins.
|
||
It did win, for a while, and the upright oval above was dead text. */
|
||
@media (min-width: 701px) and (max-width: 900px) {
|
||
#pokerTableWrap {
|
||
--feltAspect: 1;
|
||
}
|
||
}
|
||
|
||
/* ---------- 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: var(--sp-4) var(--sp-3) calc(var(--actionBarHeight, 0px) + 5rem + env(safe-area-inset-bottom, 0px));
|
||
}
|
||
|
||
/* ---- One row, the way a phone on its side already had it ----
|
||
|
||
The header stood 120px tall on a 390px phone and only 46 of that was a control.
|
||
The other 74 were a 22px title with a gold rule under it, a back link and a gold
|
||
"Hand #1" pill — read once, at the top of a screen whose subject is the green
|
||
table underneath. Sixteen percent of the window, before the felt started.
|
||
|
||
A phone held sideways has solved this since the landscape block went in: one row,
|
||
title clipped, arrow and buttons. Upright never got the same answer, and upright
|
||
is how this tool is held. It is 44px now, and the felt is 76px taller — 496 to
|
||
572, about fifteen percent more table on the same screen.
|
||
|
||
Nothing left the page. The title is still the document's <h1>, clipped rather
|
||
than removed, so it is still the heading a screen reader lands on and still what
|
||
the browser tab and the installed app are called. The hand number moved into the
|
||
action bar's status row, where the street, the pot and the player count already
|
||
say what hand you are in. And the arrow keeps its 44px box; only the word "Casino"
|
||
is gone, which is what every phone does with a back link. */
|
||
#holdemHeader {
|
||
flex-wrap: nowrap;
|
||
align-items: center;
|
||
margin-bottom: var(--sp-4);
|
||
gap: var(--sp-2);
|
||
}
|
||
|
||
/* So the arrow is a flex item of the header itself rather than a block inside a
|
||
block — there is nothing else left in this group once the title is clipped. */
|
||
#holdemHeaderLeft {
|
||
display: contents;
|
||
}
|
||
|
||
/* Clipped, not removed: `display: none` would take the page's only <h1> off the
|
||
accessibility tree, and a page whose heading disappears below 700px is a page
|
||
with no heading on the device it is mostly read on. */
|
||
#holdemHeader h1 {
|
||
position: absolute;
|
||
width: 1px;
|
||
height: 1px;
|
||
margin: -1px;
|
||
padding: 0;
|
||
overflow: hidden;
|
||
clip-path: inset(50%);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.backLinkWord {
|
||
display: none;
|
||
}
|
||
|
||
/* 44 square, not 32. The word used to carry the width and the padding only had to
|
||
make up the height; with the word gone the arrow is the whole target, and the
|
||
layout sweep caught it at 31x43 on every touch viewport there is. It costs
|
||
nothing — the arrow and the three buttons come to 323px of the 336 a 360px phone
|
||
has, which is the narrowest screen in the sweep. */
|
||
.holdemBackLink {
|
||
flex: none;
|
||
justify-content: center;
|
||
min-width: 44px;
|
||
/* The negative margins that gave the hit area back to a two-row header would
|
||
now pull the arrow out of a row it is the first item of. */
|
||
margin: 0;
|
||
font-size: var(--fs-md);
|
||
}
|
||
|
||
/* Takes the row rather than shrink-wrapping to the right of it. Sized to their
|
||
labels the three buttons left 78px of nothing where the title used to be, and
|
||
that gap buys nothing — spent on the buttons instead it is about 26px of extra
|
||
target each, on the controls a dealer presses once a hand with a thumb.
|
||
|
||
Wrapping, though, and this used to say `nowrap`. Three labels that will not break
|
||
and will not shrink — `white-space: nowrap` and a `min-width: auto` floor apiece —
|
||
have to go somewhere when they stop fitting, and with nowrap that somewhere was
|
||
off the side of the screen. Measured with the text turned up, which every phone
|
||
lets you do: at 20px of root text (an Android slider at 125%) "Resolve Pot" ended
|
||
23px past the right edge of a 360px window and put a horizontal scrollbar under
|
||
the whole page; at 24px it was 60px past the edge of a 390px one. Nothing is lost
|
||
by letting them take a second row on the day they need one — at any ordinary text
|
||
size they still fit on the first and still grow into it. */
|
||
#holdemHeaderActions {
|
||
flex: 1 1 auto;
|
||
flex-wrap: wrap;
|
||
min-width: 0;
|
||
gap: var(--sp-1);
|
||
}
|
||
|
||
/* Grow into that spare width, never shrink below the label. `flex: 1 1 0` would
|
||
divide the row into equal thirds and squeeze "Next Hand ⟳" — 105px of text — into
|
||
99, and these are `white-space: nowrap`, so what gives is the text. An `auto`
|
||
basis makes the label the floor and the spare width the bonus. */
|
||
#holdemHeaderActions .holdemBtn {
|
||
flex: 1 1 auto;
|
||
justify-content: center;
|
||
min-height: 46px;
|
||
padding: var(--sp-2) var(--sp-2);
|
||
font-size: var(--fs-sm);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* 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.
|
||
|
||
Half a puck is the right answer when the felt is tall enough to spare it. On a
|
||
phone held sideways it isn't: the felt is 96px tall there once the action bar has
|
||
grown, and 26px off the top and bottom left the ring 44px of height to work with
|
||
— less than half the table. That is what kept the seats at the ends stacked no
|
||
matter what shape the rim was.
|
||
|
||
Above about 216px of felt the flat 26px still wins, so nothing upright changes. */
|
||
#seatsLayer,
|
||
#betsLayer {
|
||
inset: min(26px, 12%) 44px;
|
||
}
|
||
|
||
/* The puck is only a summary — the real form is the sheet — so a phone gives it
|
||
tighter padding and a shorter floor. How *wide* it gets is measured, not declared
|
||
here: see the note on the 900px block above. */
|
||
.seat {
|
||
min-height: 52px;
|
||
padding: var(--sp-1) var(--sp-1);
|
||
}
|
||
|
||
#potBadge {
|
||
padding: var(--sp-2) var(--sp-4);
|
||
}
|
||
|
||
#potBadgeAmount {
|
||
font-size: var(--fs-lg);
|
||
}
|
||
|
||
.tableHint {
|
||
margin-top: var(--sp-3);
|
||
font-size: var(--fs-sm);
|
||
}
|
||
|
||
/* ---- Seat editor as a bottom sheet ---- */
|
||
|
||
/* What the sheet is in front of.
|
||
Lighter than the 0.72 the other six overlays use, on purpose: those replace the
|
||
table, and this one is a form *about* something on it. The chair being edited
|
||
should still read through the dim, and at 0.55 it does — the felt stays a felt
|
||
and the header behind it stops looking like somewhere to put a thumb. */
|
||
/* Put away with visibility, not with opacity — the same lesson the sheet above it
|
||
already learned about transforms, and it was caught the same way. A fixed,
|
||
full-window box at `opacity: 0` is invisible and still entirely there: the layout
|
||
sweep read it as furniture standing 640px tall at the bottom of a 640px phone, on
|
||
every screen and in every state, and asked scroll-padding to reserve the whole
|
||
window for it. Hidden is hidden — off the accessibility tree, out of hit testing,
|
||
and out of that measurement.
|
||
|
||
The delay is what keeps the fade-out on screen for its own quarter second:
|
||
visibility flips at the end of the 0.25s going out, and immediately coming in. */
|
||
#seatSheetScrim {
|
||
display: block;
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 59;
|
||
background: rgba(0, 0, 0, 0.55);
|
||
opacity: 0;
|
||
visibility: hidden;
|
||
transition: opacity 0.25s ease, visibility 0s linear 0.25s;
|
||
}
|
||
|
||
body.seatSheetOpen #seatSheetScrim {
|
||
opacity: 1;
|
||
visibility: visible;
|
||
transition: opacity 0.25s ease, visibility 0s;
|
||
}
|
||
|
||
/* And the toast steps over the sheet, the way it already steps over the action
|
||
bar. Both are fixed to the bottom edge, and the sheet is the taller of the two
|
||
while it is open — a toast left at the bar's height lands in the middle of the
|
||
seat form, with a live Undo and a live × sitting on top of the fields. Written
|
||
after the `body.hasActionBar` rule it overrides: equal specificity, later wins.
|
||
|
||
The height is measured and published by Render.watchSeatSheetHeight, so it
|
||
follows the sheet when a section is unfolded rather than assuming a size. */
|
||
body.seatSheetOpen #toastHost {
|
||
bottom: calc(var(--seatSheetHeight, 0px) + 0.75rem + env(safe-area-inset-bottom, 0px));
|
||
}
|
||
|
||
#seatEditorPanel {
|
||
position: fixed;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
z-index: 60;
|
||
margin: 0;
|
||
max-height: 74vh;
|
||
overflow-y: auto;
|
||
border-radius: var(--r-lg) var(--r-lg) 0 0;
|
||
border-bottom: none;
|
||
padding: var(--sp-1) var(--sp-4) var(--sp-5);
|
||
background: linear-gradient(180deg, var(--surface-2), var(--surface-1));
|
||
box-shadow: 0 -14px 40px rgba(0, 0, 0, 0.65);
|
||
transform: translateY(105%);
|
||
/* A transform moves a thing; it does not put it away. Parked off the bottom of
|
||
the window the sheet was still in the tab order and still in the accessibility
|
||
tree, so tabbing through a phone reached its Done button at y=850 on an 844px
|
||
screen — focused, announced, and impossible to see or scroll to, because a
|
||
fixed element does not move when the page does.
|
||
|
||
visibility does put it away: hidden takes the subtree out of the focus order
|
||
and off the tree, and unlike display it can be transitioned. The delay is what
|
||
keeps the slide-out visible — visibility flips at the end of the 0.25s on the
|
||
way out, and immediately on the way in. */
|
||
visibility: hidden;
|
||
transition: transform 0.25s ease, visibility 0s linear 0.25s;
|
||
-webkit-overflow-scrolling: touch;
|
||
}
|
||
|
||
body.seatSheetOpen #seatEditorPanel {
|
||
transform: translateY(0);
|
||
visibility: visible;
|
||
transition: transform 0.25s ease, visibility 0s;
|
||
}
|
||
|
||
.seatSheetGrip {
|
||
display: block;
|
||
width: 38px;
|
||
height: 4px;
|
||
margin: var(--sp-1) auto var(--sp-2);
|
||
border-radius: var(--r-pill);
|
||
background: var(--line-strong);
|
||
}
|
||
|
||
.seatSheetClose {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
flex: none;
|
||
min-height: 44px;
|
||
padding: var(--sp-1) var(--sp-4);
|
||
border-radius: var(--r-md);
|
||
border: 1px solid var(--line);
|
||
background: linear-gradient(180deg, var(--surface-3), var(--surface-2));
|
||
color: var(--text);
|
||
font-size: var(--fs-base);
|
||
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: var(--sp-2) var(--sp-2);
|
||
}
|
||
|
||
.seatEditorForm .chipCountInput {
|
||
width: 56px;
|
||
text-align: center;
|
||
}
|
||
|
||
/* A section's name is a control, and a thumb has to be able to land on it without
|
||
aiming. Same 44px floor as every field below it, and the same 15px the toggles
|
||
and the total are set in — a summary at 12px reads as a caption for the row
|
||
above rather than as the thing you press. */
|
||
.seatEditorForm .seatSubPanel > summary {
|
||
min-height: 44px;
|
||
font-size: var(--fs-md);
|
||
color: var(--text);
|
||
}
|
||
|
||
.seatEditorForm .seatGlance {
|
||
font-size: var(--fs-base);
|
||
}
|
||
|
||
.seatEditorForm .chipCounts {
|
||
gap: var(--sp-2);
|
||
}
|
||
|
||
.seatEditorForm .chipSwatch {
|
||
width: 16px;
|
||
height: 16px;
|
||
}
|
||
|
||
.seatEditorForm .amountOverrideInput {
|
||
width: 110px;
|
||
}
|
||
|
||
.seatEditorForm .cardSlot {
|
||
font-size: var(--fs-xl);
|
||
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: var(--fs-lg);
|
||
}
|
||
|
||
.seatEditorForm .seatCardToggles {
|
||
gap: var(--sp-4);
|
||
}
|
||
|
||
.seatEditorForm .inHandToggle,
|
||
.seatEditorForm .foldToggle {
|
||
min-height: 44px;
|
||
font-size: var(--fs-md);
|
||
}
|
||
|
||
.seatEditorForm .inHandToggle input,
|
||
.seatEditorForm .foldToggle input {
|
||
width: 22px;
|
||
height: 22px;
|
||
}
|
||
|
||
.seatEditorForm .amountOverrideRow,
|
||
.seatEditorForm .seatTotal {
|
||
font-size: var(--fs-md);
|
||
}
|
||
|
||
.seatEditorForm .addFieldBtn {
|
||
width: 100%;
|
||
justify-content: center;
|
||
min-height: 44px;
|
||
margin-top: var(--sp-2);
|
||
}
|
||
|
||
/* 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: var(--sp-2) var(--sp-2);
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
/* 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 var(--sp-2);
|
||
}
|
||
|
||
/* The dealer chip's 44px tap pad used to be redeclared here, centred on the disc.
|
||
On a 72x46 puck that square started at the puck's own middle, and the seat that
|
||
had the button could not be tapped where a thumb lands. It is gone; see
|
||
.dealerChip near the top of this file for why nothing needs to replace it. */
|
||
|
||
.toastAction {
|
||
min-height: 44px;
|
||
}
|
||
|
||
.confirmActions {
|
||
flex-direction: column-reverse;
|
||
}
|
||
|
||
.confirmActions .holdemBtn {
|
||
width: 100%;
|
||
justify-content: center;
|
||
}
|
||
}
|
||
|
||
/* The seat sheet, for anyone who asked not to be moved.
|
||
|
||
This is the largest movement the tool makes: on a phone the seat editor is a bottom
|
||
sheet that travels the full height of itself, up from off-screen, every time a dealer
|
||
taps a chair — which on a busy table is most of the taps they make all night.
|
||
|
||
Placed after the 700px block rather than inside it because the rule it overrides is an
|
||
ID selector: at equal specificity the later one wins, and a copy sitting earlier in
|
||
the file would have looked right and done nothing.
|
||
|
||
The visibility delay has to go with the transform, and that is the part worth being
|
||
careful about. It exists so the slide-out stays on screen for its own 0.25s; with the
|
||
slide gone, the same delay leaves a sheet that has already jumped away still painted
|
||
for a quarter of a second. So it flips at once here, in both directions. */
|
||
@media (max-width: 700px) and (prefers-reduced-motion: reduce) {
|
||
#seatEditorPanel {
|
||
transition: visibility 0s linear 0s;
|
||
}
|
||
|
||
body.seatSheetOpen #seatEditorPanel {
|
||
transition: visibility 0s;
|
||
}
|
||
}
|
||
|
||
/* ---------- 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.
|
||
|
||
The height comes from --feltRoomPx, the same measured value the wide layout
|
||
caps its width by: the bar's real height, the wrap's real offset and the
|
||
bottom seat's overhang, all taken off the window. This branch used to work
|
||
it out itself as `100vh - bar - 7rem`, guessing that everything above the
|
||
felt came to 7rem. It comes to 108px at 640x360, which the guess covers, and
|
||
119px at 844x390, which it doesn't — so on an iPhone on its side the bottom
|
||
three seats sat up to 26px inside the action bar. Nothing here needs to
|
||
guess; the measurement was already being published. */
|
||
width: 100%;
|
||
height: var(--feltRoomPx, calc(100vh - var(--actionBarHeight, 0px) - 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;
|
||
/* This branch already spends the measured room on its own height. The phone
|
||
block's max-height is the same number arriving twice, and applying it here
|
||
took the felt down to the floor value and crowded the ring — measured at
|
||
640x360, two seats lost their centres to their neighbours. */
|
||
max-height: none;
|
||
}
|
||
|
||
/* The page's own top padding is 32px of nothing, on the screen that has the least
|
||
to spare. */
|
||
#holdemApp {
|
||
padding-top: var(--sp-2);
|
||
}
|
||
|
||
/* 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.
|
||
|
||
It still wrapped to two rows, and that second row is 44px of the 390 — the title
|
||
block and the five buttons came to 829px inside a 765px header at 844x390, so
|
||
they missed sharing one line by 64px, and at 640x360 by rather more. What goes is
|
||
the page title: the one thing on this screen a dealer already knows. It is hidden
|
||
from the eye rather than removed, so the page keeps its heading for a screen
|
||
reader and for the browser's own outline.
|
||
|
||
The upright block above now does all of this too, and the hand pill it used to
|
||
make room for has gone to the action bar's status row for both of them. What is
|
||
left here is what upright does not need: this query reaches up to 900px wide,
|
||
where the 700px block does not, so a phone held sideways still needs to be told. */
|
||
#holdemHeader {
|
||
margin-bottom: var(--sp-2);
|
||
gap: var(--sp-1);
|
||
flex-wrap: nowrap;
|
||
align-items: center;
|
||
}
|
||
|
||
#holdemHeaderLeft {
|
||
display: contents;
|
||
}
|
||
|
||
#holdemHeader h1 {
|
||
position: absolute;
|
||
width: 1px;
|
||
height: 1px;
|
||
margin: -1px;
|
||
padding: 0;
|
||
overflow: hidden;
|
||
clip-path: inset(50%);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.backLinkWord {
|
||
display: none;
|
||
}
|
||
|
||
.holdemBackLink {
|
||
flex: none;
|
||
justify-content: center;
|
||
min-width: 44px;
|
||
margin: 0;
|
||
font-size: var(--fs-md);
|
||
}
|
||
|
||
/* Tightened across, never down: the buttons keep the 44px a thumb needs. */
|
||
#holdemHeaderActions {
|
||
flex-wrap: nowrap;
|
||
margin-left: auto;
|
||
min-width: 0;
|
||
gap: var(--sp-1);
|
||
}
|
||
|
||
#holdemHeaderActions .holdemBtn {
|
||
flex: 0 1 auto;
|
||
padding-left: var(--sp-2);
|
||
padding-right: var(--sp-2);
|
||
font-size: var(--fs-sm);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* 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));
|
||
}
|
||
|
||
/* The clock's ruling, said in a sentence, is a line this screen cannot pay for:
|
||
measured at 640x360, the bar goes from 152px to 269px when the count runs out and
|
||
the bottom seats end up behind it. The button carries the same ruling in three
|
||
words — "Kill Dee's hand" — so the sentence is what goes, not the call. */
|
||
.clockExpired {
|
||
display: none;
|
||
}
|
||
|
||
/* The same trade, for the pot total. Measured on a phone held sideways, the badge is
|
||
86x66 on a felt that is 692x113 — it takes 58% of the table's height for a number
|
||
the action bar is already printing three inches below it, in the line that starts
|
||
"PRE-FLOP Pot $65". Two seat pucks are 55px tall and the felt has to hold two rows
|
||
of them, so the badge is not overlapping some empty middle: it is standing in the
|
||
only band a player's bet chip has to go in, and the bottom-middle seat's chip had
|
||
nowhere left to be put but on top of its own player's name.
|
||
|
||
Upright it stays. There the felt is 248x280, the badge is a fifth of it, and it is
|
||
the only thing in the middle of the table. */
|
||
#potBadge {
|
||
display: none;
|
||
}
|
||
|
||
/* The board stays — it is the one thing in the middle of the felt that is not
|
||
written down anywhere else. It gets the room the pot badge gave up. */
|
||
.boardCard {
|
||
min-width: 26px;
|
||
min-height: 34px;
|
||
font-size: var(--fs-sm);
|
||
padding: 0 2px;
|
||
}
|
||
|
||
.boardCard .boardCardSuit {
|
||
font-size: var(--fs-xs);
|
||
}
|
||
|
||
/* Seats around a table this small overlap each other, so they shrink to suit — by
|
||
measurement now rather than by this block's old guess of 62px. */
|
||
.seat {
|
||
min-height: 50px;
|
||
}
|
||
|
||
/* The same inset the phone layout uses, which this branch never had because it is
|
||
matched on height rather than width — so an 844px-wide phone on its side drew its
|
||
ring against the felt's full box. With the rim a stadium, the top rail is flat,
|
||
which put the button seat's dealer chip over the Resolve Pot button in the header
|
||
above. Percentages of a short felt, so the ring keeps the height it needs.
|
||
|
||
The top edge is a floor rather than a ceiling, because it is the only edge with
|
||
something above it to hit. A seat puck hangs 29px above the ring path it sits on
|
||
and its dealer button another 25px, while landscape leaves 8px between the header
|
||
and the felt — so at 640x360, 12% of a 124px felt is 14.9px of inset and the
|
||
button lands 2px inside the header. Measured, not guessed: 24px puts it back to
|
||
the 7px of daylight this layout had before, and on any felt tall enough for 12%
|
||
to be the larger number, 12% still wins. The bottom edge keeps the ceiling: there
|
||
is nothing under the felt but the gap to the bar. */
|
||
#seatsLayer,
|
||
#betsLayer {
|
||
inset: max(24px, 12%) 44px min(26px, 12%);
|
||
}
|
||
|
||
}
|
||
|
||
/* ---------- 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: var(--sp-2) var(--sp-3);
|
||
font-size: var(--fs-sm);
|
||
}
|
||
}
|
||
|
||
@media (max-width: 700px) and (max-height: 700px) {
|
||
#holdemHeader {
|
||
margin-bottom: var(--sp-2);
|
||
gap: var(--sp-2);
|
||
}
|
||
|
||
#holdemHeader h1 {
|
||
font-size: var(--fs-lg);
|
||
padding-bottom: var(--sp-1);
|
||
}
|
||
}
|
||
|
||
/* ---------- A phone on its side: the bar goes beside the table ----------
|
||
|
||
Everything above spends its effort dividing one column of height between a table and
|
||
a bar, on the screen that has the least height in the world. Measured at 640x360
|
||
mid-hand, the split comes out: 70px of header, 85px of felt, 205px of bar. The felt
|
||
is 529x85. Two rows of seat pucks 55px tall have to fit in 85px of it, which they
|
||
cannot, so seven of the nine finish past the rail — up to 17px of puck floating on
|
||
the page. That is 96 of the 98 offenders the layout audit finds anywhere.
|
||
|
||
No amount of shrinking fixes it, because the shortage is not in the table. A phone
|
||
on its side is 640x360: short of height and swimming in width. The bar is 205px tall
|
||
and 529 wide to hold three buttons and a status line, and it is taking that height
|
||
from the one element on the page that needs height.
|
||
|
||
So it moves. In landscape the bar becomes a right-hand rail, the table takes the
|
||
whole column beside it, and the felt goes from 529x85 to roughly 420x270 — an actual
|
||
oval, with room for two rows of pucks and a gap between them.
|
||
|
||
The rail is also where the hand already is. This is the layout every mapping and
|
||
drawing app uses on a rotated phone for the same reason, and it puts Fold/Call/Raise
|
||
under the right thumb rather than across the bottom edge where a landscape grip has
|
||
the palm.
|
||
|
||
Scoped to phones. Above 900px the sidebar is a right-hand column already (see
|
||
#holdemLayout) and a rail would land on top of it; short desktop windows have height
|
||
to spare and don't have the problem. */
|
||
@media (orientation: landscape) and (max-height: 560px) and (max-width: 900px) {
|
||
:root {
|
||
/* Wide enough for "Raise to" and a four-figure amount side by side, and for a
|
||
48px control to keep its own padding; never more than a third of the screen,
|
||
because the rest of it is the table. */
|
||
--actionRailW: clamp(11.5rem, 30vw, 15rem);
|
||
}
|
||
|
||
#actionBar {
|
||
--actionBarLayout: rail;
|
||
top: 0;
|
||
bottom: 0;
|
||
left: auto;
|
||
right: 0;
|
||
/* Border-box, so --actionRailW is the whole rail. Content-box made it 217px of
|
||
a 192px token, and everything that reserves room for it — the page's padding,
|
||
the seat sheet's right edge, the toast — was 25px short in the same direction:
|
||
Resolve Pot finished 13px underneath it. One number, one meaning. */
|
||
box-sizing: border-box;
|
||
width: var(--actionRailW);
|
||
max-width: none;
|
||
margin: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--sp-2);
|
||
/* The raise pad is the tallest thing that appears here. On the shortest screen
|
||
in the list it can outgrow the rail, and a control that has fallen off the
|
||
end of a fixed panel is unreachable rather than merely awkward. */
|
||
overflow-y: auto;
|
||
overscroll-behavior: contain;
|
||
border-radius: var(--r-lg) 0 0 var(--r-lg);
|
||
border: 1px solid var(--line);
|
||
border-right: none;
|
||
padding: var(--sp-3) calc(var(--sp-3) + env(safe-area-inset-right, 0px)) var(--sp-3) var(--sp-3);
|
||
box-shadow: -12px 0 34px rgba(0, 0, 0, 0.6);
|
||
}
|
||
|
||
/* Anchored low: the stack reads top-to-bottom in the order it always has — status,
|
||
then the buttons — and sitting at the bottom of the rail puts Fold, Call and
|
||
Raise where the thumb rests rather than where the hand has to reach up.
|
||
|
||
Done with a margin on the first child rather than `justify-content: flex-end`,
|
||
which is the same picture until the content outgrows the rail. Then flex-end
|
||
overflows the *start* edge, and a scroll container cannot scroll back past its
|
||
own start — the status line would be painted off the top and unreachable. A
|
||
margin pushes the same distance and collapses to nothing when there is none to
|
||
give, so the overflow lands at the end, where scrolling can reach it. Measured at
|
||
640x360 the rail is 358px of content in 360px: it is one side pot away from
|
||
needing this. */
|
||
#actionBar > :first-child {
|
||
margin-top: auto;
|
||
}
|
||
|
||
/* And nothing in the rail gives up its own height to make room. A flex item shrinks
|
||
by default, so once the raise pad and the status line together outgrew the rail —
|
||
which they do at 640x360 the moment the amount field is thumb-sized — the status
|
||
line was squeezed under the pad and the undo pill came out two-fifths covered by
|
||
the Min button. Overflow is the honest outcome, and the scroll above can reach
|
||
it; a control drawn on top of another one cannot be reached at all. */
|
||
#actionBar > * {
|
||
flex: none;
|
||
}
|
||
|
||
/* The page gives up the width instead of the height. The bottom padding that was
|
||
clearing the bar has nothing left to clear — and 5rem of it on a 360px-tall
|
||
screen is a seventh of the window held empty for a bar that isn't there. */
|
||
#holdemApp {
|
||
padding: var(--sp-2) calc(var(--actionRailW) + var(--sp-3)) var(--sp-2) var(--sp-3);
|
||
}
|
||
|
||
/* Everything in the rail is a column now: three buttons stacked, and a status line
|
||
that was written to sit on one row of a 529px bar. */
|
||
.actionButtons,
|
||
.dealPromptButtons {
|
||
flex-direction: column;
|
||
}
|
||
|
||
/* This row already wraps, which is the right behaviour in a narrow column — it just
|
||
has to stop reserving two lines it no longer needs. That reservation exists so a
|
||
bar fixed to the bottom edge doesn't move its top edge under the dealer's thumb
|
||
as the text rewraps; a rail is anchored at the bottom the same way, but it is
|
||
190px wide, where every one of these readings is on its own line regardless. */
|
||
.actionStatus,
|
||
.actionStatus:has(.actionUndo) {
|
||
gap: var(--sp-1) var(--sp-2);
|
||
margin-bottom: 0;
|
||
min-height: 0;
|
||
}
|
||
|
||
/* The rail is 190px wide, so its acting row is a stack rather than a line — every
|
||
one of these readings is on its own line here regardless, and squeezing a name
|
||
into a fifth of the column would be shrinking to fit a row that isn't there. */
|
||
.actionActing {
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.actionUndo {
|
||
width: 100%;
|
||
}
|
||
|
||
/* Four quick-raise buttons across 190px is 40px each. Two rows of two is 90px each,
|
||
which is a control rather than a target to aim at. */
|
||
.raiseQuick {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
}
|
||
|
||
.raiseEntry {
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.raiseInput {
|
||
flex: 1 0 100%;
|
||
}
|
||
|
||
.raiseCancel,
|
||
.raiseConfirm {
|
||
flex: 1 1 0;
|
||
min-width: 0;
|
||
}
|
||
|
||
/* Back to the middle of the screen. The old landscape rule pushed the toast to the
|
||
right edge to keep it off a full-width felt; that edge is the rail now, and the
|
||
felt no longer runs under it. */
|
||
body.hasActionBar #toastHost,
|
||
#toastHost {
|
||
left: var(--sp-3);
|
||
right: calc(var(--actionRailW) + var(--sp-3));
|
||
max-width: none;
|
||
bottom: var(--sp-3);
|
||
}
|
||
|
||
/* The pot total comes back. It was hidden here because the badge took 58% of an
|
||
85px felt and stood in the only band a bet chip could go in — on a felt three
|
||
times as tall it is the middle of the table again, which is where a dealer looks
|
||
for the pot. */
|
||
#potBadge {
|
||
display: flex;
|
||
}
|
||
|
||
/* And the ring goes back to being measured rather than held off the edges by hand.
|
||
The insets above exist to stop a flat felt's top seat putting its dealer button
|
||
through the header; with the felt this tall the ring has its own room, and the
|
||
44px side inset was reserving a fifth of the width of a felt that is now the
|
||
narrower dimension. */
|
||
#seatsLayer,
|
||
#betsLayer {
|
||
inset: 0;
|
||
}
|
||
|
||
/* And the felt fills the box it is given. The 6% inset everywhere else is the strip
|
||
a seat puck straddles — half on the rail, half off it, the way a player sits at a
|
||
table. In this column that strip is 25px of a 409px width taken off both ends of
|
||
the widest dimension the table has left, and the ring does not want it: the seats
|
||
are held inside the rail here by measurement, so nothing is ever painted in it.
|
||
Measured, giving it back is 49px of felt, which is a puck size for every chair. */
|
||
#pokerTable {
|
||
inset: 0;
|
||
}
|
||
|
||
/* The links under the table are the last thing in the column, and on the old layout
|
||
they were simply behind the bar. There is room for them now, but not for the
|
||
margins of a desktop. */
|
||
.tableHintActions {
|
||
margin: var(--sp-1) 0 0;
|
||
}
|
||
|
||
/* The phone layout gives this row the full width, which puts it on a line of its
|
||
own — 36px of a 360px-tall screen for three buttons that need 217. Beside the
|
||
title instead, which is where they sit at every other size and which the header
|
||
has room for now that it is a column rather than the whole window. The wrap stays
|
||
as the backstop for a narrower screen than any in the list. */
|
||
#holdemHeader {
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
#holdemHeaderActions {
|
||
width: auto;
|
||
margin-left: auto;
|
||
}
|
||
|
||
/* The seat editor comes up as a sheet from the bottom on a phone, full width. Full
|
||
width now runs under the rail, and it sits a layer above it, so it was covering
|
||
Fold, Call and Raise with a form. It stops at the rail instead — which also means
|
||
the dealer can still see the hand they are in while editing a chair. */
|
||
#seatEditorPanel {
|
||
right: var(--actionRailW);
|
||
border-top-right-radius: var(--r-lg);
|
||
}
|
||
|
||
/* The scrim stops where the sheet stops. Fold, Call and Raise live in the rail
|
||
down the right-hand side when a phone is on its side, and a scrim that reached
|
||
across it would be dimming — and swallowing the taps on — the one part of the
|
||
screen the sheet was deliberately kept clear of. */
|
||
#seatSheetScrim {
|
||
right: var(--actionRailW);
|
||
}
|
||
}
|
||
|
||
/* ---------- Focus is brought to rest where it can be seen ----------
|
||
|
||
The ring below was the easy half. The hard half is that the bar is fixed over the
|
||
bottom of the page, so it is not in the flow and the browser does not know it is
|
||
there: Tab to a control near the bottom and the browser scrolls it flush against the
|
||
edge of the window — under the bar — and considers the job done. Measured by tabbing
|
||
the whole page on a 390x844 phone: 16 stops came to rest completely covered and 27
|
||
more partly. That is WCAG 2.2 SC 2.4.11, which asks that a focused control not be
|
||
entirely hidden by something the page has put on top of it.
|
||
|
||
scroll-padding is the fix, and it goes on the scroll container rather than on the
|
||
controls: it tells the browser to treat the bottom of the viewport as not there when
|
||
it scrolls something into view. The number comes from the bar, which already
|
||
publishes its own height for the toast and the felt, so it cannot drift from the
|
||
bar's real size the way a guess would — and it is already 0 when the bar is hidden or
|
||
has become a rail down the side, which is the case scroll-padding-right answers.
|
||
|
||
The toast publishes its height into the same sum, because when the browser does
|
||
scroll it has to clear both.
|
||
|
||
This is only half of it, though, and the half that is easy to mistake for all of it.
|
||
scroll-padding only applies when the browser scrolls at all, and a control already
|
||
inside the viewport is one it sees no reason to move — a fixed overlay on top of it
|
||
does not enter into the decision. Measured on a 1280x900 desktop: tabbing to Common
|
||
rulings while a toast was up left it completely covered, and this rule did nothing
|
||
about it, because nothing scrolled. The other half is the focusin handler in main.js,
|
||
which measures the overlap and scrolls by it. */
|
||
html {
|
||
scroll-padding-bottom: calc(
|
||
var(--actionBarHeight, 0px) + var(--toastHeight, 0px) +
|
||
var(--sp-4) + env(safe-area-inset-bottom, 0px));
|
||
scroll-padding-right: var(--actionRailW, 0px);
|
||
}
|
||
|
||
/* ---------- 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,
|
||
/* Every summary in the tool, not only the ones nested inside Table Setup. The rulings
|
||
are twelve of them and the sidebar's panel names are eight more, and until the Tab
|
||
order could reach a summary at all there was no way to notice they had no ring. */
|
||
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;
|
||
}
|
||
|
||
/* The heading a sheet lands on. It is only ever focused by script, so `:focus` would
|
||
put a ring around the title of every sheet every time one opened, on a phone that has
|
||
no keyboard to show a ring for. `:focus-visible` asks the browser the question that
|
||
actually matters — did this dealer arrive by keyboard — and answers no for a tap.
|
||
Offset inward: these headings run to the edge of their sheet and an outward ring
|
||
would be clipped by it.
|
||
|
||
Scoped to the headings on purpose: the confirm dialog names a button as its landing
|
||
spot, and a button already has a ring above, drawn outward the way every other
|
||
control in the tool draws one. */
|
||
h2[data-modal-focus]:focus-visible {
|
||
outline: 2px solid #7fc4ff;
|
||
outline-offset: -2px;
|
||
border-radius: var(--r-sm);
|
||
}
|
||
|
||
/* ---------- 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,
|
||
/* The raise amount, which this rule reached for the settings fields and missed —
|
||
it is in the action bar rather than in a panel or the seat form, so it is the
|
||
one field the list didn't name. Measured on a phone: 36px, on the one surface in
|
||
the tool that exists to be hit with a thumb, holding the one number that is the
|
||
size of somebody's bet. The audit never caught it either, because the sweep had
|
||
no step that opened the raise pad; it has one now. */
|
||
.raiseInput {
|
||
min-height: 44px;
|
||
padding: var(--sp-2) var(--sp-2);
|
||
}
|
||
}
|
||
|
||
.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: var(--sp-2) 0 0;
|
||
font-size: var(--fs-sm);
|
||
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) {
|
||
/* The same economy for the pot dialog's list of winners, which is the other place
|
||
on a short screen where a line costs a name: two lines become one, a row goes
|
||
from 78px to 56, and a three-way pot fits without scrolling on a 640px phone.
|
||
The row is still 56px, so it is still a target a thumb can hit — this is where
|
||
the money is decided and that floor does not move. */
|
||
.potWizardChoice {
|
||
font-size: var(--fs-base);
|
||
}
|
||
|
||
.potWizardChoiceHand {
|
||
font-size: var(--fs-2xs);
|
||
}
|
||
|
||
.raiseNote {
|
||
margin-top: var(--sp-1);
|
||
font-size: var(--fs-xs);
|
||
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: var(--sp-2);
|
||
}
|
||
|
||
.dealPromptLine {
|
||
margin: 0;
|
||
text-align: center;
|
||
font-size: var(--fs-base);
|
||
color: #c8c8c8;
|
||
line-height: 1.35;
|
||
}
|
||
|
||
.dealPromptLine.dealPromptBlocked {
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.actionBtn.actionDeal {
|
||
background: linear-gradient(180deg, #16783a, #0d5225);
|
||
border-color: #1e8449;
|
||
color: #ecffef;
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* The straddle rides on the deal row, so it must not take half of it: .actionBtn is
|
||
flex: 1 and would have split the row evenly with the button the dealer presses every
|
||
hand. It sizes to its own words instead, and stays quiet next to the green one —
|
||
choosing a straddle is the exception, dealing is the rule. */
|
||
.actionBtn.actionStraddle {
|
||
flex: 0 1 auto;
|
||
background: linear-gradient(180deg, #33304a, #24223a);
|
||
border-color: #4b4770;
|
||
color: #e6e1ff;
|
||
font-size: var(--fs-base);
|
||
padding-left: var(--sp-3);
|
||
padding-right: var(--sp-3);
|
||
}
|
||
|
||
.actionBtn.actionStraddleOff {
|
||
background: linear-gradient(180deg, var(--surface-3), var(--surface-2));
|
||
border-color: var(--line-strong);
|
||
color: #dcdcdc;
|
||
}
|
||
|
||
/* Three buttons across a 360px phone is a squeeze, and the two ways out are both
|
||
worse than this one. Letting the row wrap put the deal button on its own line and
|
||
made the bar 64px taller — measured at 360x640, that is the bottom seat three fifths
|
||
behind the bar, and this layout deliberately doesn't shrink the felt to suit (see the
|
||
max-width: none override, where capping it was tried and rejected). Letting flex
|
||
squash freely took the deal button to 84px with its label over three lines.
|
||
A floor of 7rem keeps the label to two lines and the row to one, so arming a straddle
|
||
costs no felt at all. */
|
||
.dealPromptButtons .actionDeal {
|
||
min-width: 7rem;
|
||
}
|
||
|
||
/* A prompt with one thing to do gives that thing the whole row. The empty-table
|
||
prompt is the first control a dealer ever meets here, and a button sitting in the
|
||
left third of the bar reads as one option among several that haven't loaded yet. */
|
||
.dealPromptButtons:has(> :only-child) .actionDeal {
|
||
flex: 1;
|
||
width: 100%;
|
||
}
|
||
|
||
/* 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: var(--fs-sm);
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
/* 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: var(--sp-2) var(--sp-2);
|
||
}
|
||
|
||
.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: var(--sp-1) var(--sp-3);
|
||
font-size: var(--fs-sm);
|
||
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: var(--sp-2) var(--sp-4);
|
||
}
|
||
}
|
||
|
||
/* ---------- 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: var(--sp-2) var(--sp-2);
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
/* 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 var(--sp-2);
|
||
}
|
||
}
|