Opened the pot wizard — the one dialog in this tool that divides money — and its Confirm button was below a scroll. Not on a tall table, not on one screen: at every size I measured, with six players and a side pot. iPhone 14 390x844 244px of the dialog below the fold Galaxy S8 360x640 417px 844x390 landscape 583px desktop 1280x900 172px Nothing on screen said there was anything under the list of names, so a dealer with the table waiting had to scroll a list of players to find the only control that finishes the hand. Two goes at this, and the first was wrong. A sticky footer is the obvious answer and it cannot work here: the actions were the last child of the scrolling content, so their containing block ended exactly where they did and `bottom` had no room to lift them. Sticky on the *header* did work, for the mirror-image reason — its container extends below it — which is what made the failure confusing to read. Capping the player list wasn't enough either; the pot breakdown and the log still overflowed on a short screen. So the dialog is a proper column now: a title that doesn't move, a body that scrolls, and a footer outside the scroll. potWizard.js appends its buttons to the footer instead of into the body, and clears both together so a step can't inherit the previous step's buttons. Measured after: Confirm on screen and hit-testable at all six configurations, twelve-handed on a Galaxy S8 included, and the dialog itself no longer scrolls at all. The checkboxes are painted now, too. `accent-color` only ever coloured the tick — the box around it stayed the platform's own bright white square, which on a near-black dialog was the loudest thing on screen and the last raw control in a tool where everything else had been drawn. Same surface ramp, same corner radius, and a tick drawn with clip-path so it can't pick up a substituted font. uiAudit gains "an open dialog's buttons are on screen", which hit-tests rather than trusting geometry — a button can be inside the viewport and still be under something. Fault-injected: putting the footer back inside the scrolling body reports Confirm 105px below the fold. Two things checked and left alone, because they turned out to be right: the standings table hides three columns on narrow screens through a container query keyed to the sidebar rather than the viewport, which is why a desktop shows fewer columns than an iPad — the sidebar is the narrower box there. And In/Won/ Net read $0 for hands settled outside the wizard, which the comment above them already explains. Caught by my own guard on the way past: the token test read `scroll-padding` as `padding` and failed the spacing scale. Correct by its rules, wrong in substance — those values are a sticky bar's measured height, not a step in the rhythm — so the lookbehind now rejects a hyphen as well as a word character. Re-fault- injected to confirm it still catches a real ad-hoc gap. 619 tests, 0 failures. 65 layout audits across 13 viewports x 5 table sizes and 35 across the five overlays. The whole wizard walked end to end: side pot resolved, hand logged, $1,560 on the table before and after. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
4278 lines
126 KiB
CSS
4278 lines
126 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. */
|
|
--text-bright: #f5f5f5;
|
|
--text: #eee;
|
|
--text-muted: #9a9a9a;
|
|
--text-dim: #8a8a8a;
|
|
|
|
/* 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;
|
|
}
|
|
|
|
/* 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. */
|
|
|
|
/* Nobody should get a stuck press state from a tap that turned into a scroll. */
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.holdemBtn:active:not(:disabled),
|
|
.linkBtn:active:not(:disabled),
|
|
.seatMenuBtn:active:not(:disabled),
|
|
.seatSheetClose:active:not(:disabled),
|
|
.cardSlot:active,
|
|
.pickerCard:active:not(:disabled),
|
|
.dealerChip:active,
|
|
.raiseQuickBtn:active:not(:disabled),
|
|
.actionUndo:active:not(:disabled) {
|
|
transform: none;
|
|
}
|
|
}
|
|
|
|
#holdemApp {
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
padding: 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);
|
|
}
|
|
}
|
|
|
|
.handCounterBadge {
|
|
display: inline-block;
|
|
margin-top: var(--sp-2);
|
|
padding: var(--sp-1) var(--sp-3);
|
|
border-radius: var(--r-pill);
|
|
background: rgba(216, 180, 106, 0.12);
|
|
border: 1px solid rgba(216, 180, 106, 0.4);
|
|
color: var(--gold);
|
|
font-size: var(--fs-sm);
|
|
letter-spacing: 0.04em;
|
|
}
|
|
|
|
#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 320px;
|
|
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. */
|
|
|
|
#potBadge {
|
|
position: relative;
|
|
background: radial-gradient(circle at 32% 28%, rgba(255, 255, 255, 0.08), rgba(0, 0, 0, 0.55));
|
|
border: 1px solid rgba(255, 255, 255, 0.14);
|
|
border-radius: 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);
|
|
}
|
|
|
|
#potBadgeLabel {
|
|
font-size: var(--fs-xs);
|
|
letter-spacing: 0.14em;
|
|
text-transform: uppercase;
|
|
color: var(--gold);
|
|
}
|
|
|
|
#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. */
|
|
.betChipAnchored {
|
|
left: 50%;
|
|
z-index: 3;
|
|
}
|
|
|
|
.betChipAbove {
|
|
top: -11px;
|
|
transform: translate(-50%, -50%);
|
|
}
|
|
|
|
.betChipBelow {
|
|
top: auto;
|
|
bottom: -11px;
|
|
transform: translate(-50%, 50%);
|
|
}
|
|
|
|
/* ---------- Seats (compact pucks on the table) ---------- */
|
|
|
|
.seat {
|
|
position: absolute;
|
|
transform: translate(-50%, -50%);
|
|
width: 96px;
|
|
min-height: 54px;
|
|
background: linear-gradient(160deg, 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);
|
|
}
|
|
|
|
.seat.seatEmpty {
|
|
background: rgba(255, 255, 255, 0.02);
|
|
border-style: dashed;
|
|
border-color: rgba(255, 255, 255, 0.13);
|
|
box-shadow: none;
|
|
/* Deliberately louder than seatInactive: an open seat is information a
|
|
dealer wants, not something to squint at. */
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.seatEmptyLabel {
|
|
color: 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 is 9px, and on an empty seat it is 9px behind a 0.7 fade: var(--text-dim)
|
|
came out at 3.39:1 once the fade was composited, for the label a dealer reads to
|
|
answer "which seat is seat 6?". Lifted until it passes with a little to spare —
|
|
measured 4.5:1 at #a5a5a5 exactly, so it sits above that. */
|
|
.seatChairNumber {
|
|
position: absolute;
|
|
top: 2px;
|
|
left: 6px;
|
|
font-size: var(--fs-2xs);
|
|
color: #aeaeae;
|
|
}
|
|
|
|
/* Two lines, not an ellipsis.
|
|
A 96px puck fits about nine characters on a line, 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. Wrapping keeps both, and a name is the one thing on the
|
|
felt read at a glance rather than looked up. */
|
|
.seatPuckName {
|
|
/* Not shrunk to buy a line. Dropping a step did fit another character and a half,
|
|
and still didn't get "Christopher" onto one line on the button seat, where the
|
|
dealer chip takes 22px of the row — so it bought nothing and cost legibility on
|
|
the one label a dealer reads at a glance across a table. The name stays the size
|
|
it was; the wrap deals with the length. */
|
|
font-size: var(--fs-sm);
|
|
font-weight: 600;
|
|
color: var(--text-bright);
|
|
display: -webkit-box;
|
|
-webkit-box-orient: vertical;
|
|
-webkit-line-clamp: 2;
|
|
line-clamp: 2;
|
|
overflow: hidden;
|
|
/* A single word longer than the puck has nothing to break on. `anywhere` was the
|
|
first try and it gave "Christo / pher", which reads as a rendering fault; with
|
|
`break-word` the browser only breaks a word that cannot fit on a line of its
|
|
own, and `hyphens` puts a hyphen in when it does. */
|
|
overflow-wrap: break-word;
|
|
hyphens: auto;
|
|
line-height: 1.2;
|
|
/* Deliberately no reserved second line. Reserving one made every puck the height of
|
|
the tallest — measured, 69-73px became a uniform 88 — and 15px added to all nine
|
|
seats is fifteen more pixels of felt spent on chrome, which is the thing this
|
|
round is trying to give back. A ragged ring is cheaper than a crowded one, and
|
|
the ring was already ragged by 4px before any of this. */
|
|
margin-top: var(--sp-2);
|
|
}
|
|
|
|
/* The button's chip sits in this puck's top-right corner, on a layer above the text.
|
|
Reserving its width means a long name ends in an ellipsis before it reaches the
|
|
chip, rather than disappearing underneath one — on the one seat at the table a
|
|
dealer looks at most. */
|
|
.seat.seatHasButton .seatPuckName {
|
|
padding-right: 22px;
|
|
}
|
|
|
|
.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(--fs-2xs);
|
|
font-weight: 700;
|
|
color: #a9f5b8;
|
|
background: rgba(9, 255, 0, 0.1);
|
|
border-radius: var(--r-pill);
|
|
padding: 0 var(--sp-1);
|
|
min-height: 1.1em;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.dealerChip {
|
|
position: absolute;
|
|
/* Inside its own seat, not on the rim.
|
|
|
|
It used to sit at -10px/-10px, hanging over the seat's corner — which on an oval
|
|
of overlapping seats means hanging over the seat *next to it*. Measured on a
|
|
nine-seat table: tapping the centre of seat 1 gave the button to seat 9. That
|
|
was always slightly true and the enlarged hit area made it much worse; three of
|
|
nine seats were losing taps, one of them losing its own middle.
|
|
|
|
Tucked inside, the overlap is zero at every size. It reads as a puck in the
|
|
corner of the seat rather than on its edge, which is a small price for tapping
|
|
a player and getting that player. */
|
|
top: 3px;
|
|
right: 3px;
|
|
width: 22px;
|
|
height: 22px;
|
|
border-radius: 50%;
|
|
background: radial-gradient(circle at 35% 30%, #fff, #d8d8d8 55%, #b8b8b8 100%);
|
|
color: var(--surface-2);
|
|
border: 2px solid var(--text-muted);
|
|
font-weight: 700;
|
|
font-size: var(--fs-xs);
|
|
cursor: pointer;
|
|
z-index: 2;
|
|
transition: opacity 0.15s ease;
|
|
/* A dealer puck should look like a puck — small, sitting on the rim of its seat.
|
|
But tapping it hands the button to that seat and re-posts the blinds, and 22px
|
|
is half the 44px a fingertip needs. The hit area grows without the chip doing
|
|
so: the visual stays 22px and an invisible pad around it catches the tap.
|
|
Deliberately not centred on the chip — it's pushed down and left, away from the
|
|
neighbouring seat the chip juts towards, so the bigger target can't start
|
|
stealing taps meant for the seat next door. */
|
|
isolation: isolate;
|
|
}
|
|
|
|
.dealerChip::after {
|
|
content: "";
|
|
position: absolute;
|
|
/* Grows inward, over its own seat, not outward onto the neighbours.
|
|
|
|
The chip already sits outside its seat by design — top:-10px, right:-10px — so
|
|
a pad that also reaches up and right lands on whatever seat is next around the
|
|
oval. It did: three of nine seats had taps taken by a neighbour's chip, seat 1
|
|
losing 10 of 49 sampled points including its own centre, which meant tapping
|
|
the middle of seat 1 gave the button to seat 9.
|
|
|
|
Down and left is its own seat, which is a large target that can spare the
|
|
corner. The two pixels of outward reach are just to cover the chip's border. */
|
|
top: -3px;
|
|
right: -3px;
|
|
bottom: -18px;
|
|
left: -18px;
|
|
}
|
|
|
|
/* A poker table has one dealer button. This one drew nine.
|
|
|
|
Every seat carried a puck — the button's, and eight ghosts at 0.32 opacity that
|
|
were there to be pressed. Measured on a nine-seat table: eight pale discs, all
|
|
eight showing the letter D, one of them real. Finding the button meant comparing
|
|
nine identical shapes for the one that's gold, when it should be the only thing
|
|
of its kind on the felt. The ghosts also sat on top of their neighbours: every
|
|
remaining "something important is covered" offender on a crowded table was a
|
|
seat covered by the seat-next-door's ghost puck.
|
|
|
|
So a seat that doesn't have the button doesn't draw one. The affordance is still
|
|
there — hovering a seat fades its slot in, which is a preview of where the button
|
|
would go — and on touch, where there is no hover, "Give the button" has always
|
|
been in the seat's long-press menu. Keyboard reaches it by focus.
|
|
|
|
pointer-events matters as much as the opacity: an invisible puck that still takes
|
|
taps would be worse than a visible one. */
|
|
.dealerChip:not(.active) {
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
.seat:hover .dealerChip:not(.active) {
|
|
opacity: 0.5;
|
|
pointer-events: auto;
|
|
}
|
|
}
|
|
|
|
.dealerChip:not(.active):focus-visible {
|
|
opacity: 1;
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.dealerChip.active {
|
|
background: radial-gradient(circle at 35% 30%, #ffe9a8, #f5d547 55%, #cba428 100%);
|
|
border-color: var(--gold-deep);
|
|
color: #3a2e00;
|
|
opacity: 1;
|
|
}
|
|
|
|
.dealerChip.hidden {
|
|
display: none;
|
|
}
|
|
|
|
/* The dead button. It's on an empty chair on purpose, and that reads as a mistake —
|
|
a gold puck on nobody looks exactly like a button that has drifted and wants
|
|
dragging back. So it keeps the button's gold, to stay findable as the button, but
|
|
goes hollow and dashed: the same shape, visibly holding a place rather than
|
|
marking a player. The deal prompt says why in words; this is so the felt agrees
|
|
with it at a glance instead of contradicting it. */
|
|
.dealerChip.active.dealerChipDead {
|
|
background: rgba(245, 213, 71, 0.14);
|
|
border-style: dashed;
|
|
color: #f0d98c;
|
|
}
|
|
|
|
.blindBadge {
|
|
display: none;
|
|
position: absolute;
|
|
top: -9px;
|
|
font-size: var(--fs-2xs);
|
|
font-weight: 700;
|
|
padding: var(--sp-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: -9px;
|
|
}
|
|
|
|
/* And the badge outranks the chips regardless, so a layout this doesn't anticipate
|
|
can't hide it again. Which blind a seat is posting is the one thing on that puck
|
|
nothing else on the felt repeats — a bet chip's amount is on the seat's own total
|
|
line as well. Measured across 12 table sizes: no chip loses its amount to this. */
|
|
.blindBadge {
|
|
z-index: 4;
|
|
}
|
|
|
|
/* Both badges failed WCAG AA against their own gradients — measured at the worst
|
|
stop, the small blind was 3.52:1 and the big blind 2.15:1, in 9px bold text a
|
|
dealer is meant to read at a glance across a table.
|
|
|
|
The small blind keeps its blue and just darkens (4.77:1 at its worst stop). The
|
|
big blind flips to dark text on brighter gold (5.84:1), which is how a gold chip
|
|
is marked anyway and has the side benefit of making the two badges far easier to
|
|
tell apart at speed — light-on-blue against dark-on-gold, rather than two pale
|
|
labels differing only in hue. */
|
|
.sbBadge {
|
|
left: -6px;
|
|
background: linear-gradient(180deg, #2f6cb5, #22508c);
|
|
color: #eaf3ff;
|
|
}
|
|
|
|
.bbBadge {
|
|
left: 22px;
|
|
background: linear-gradient(180deg, #e6ad4d, #c98a2a);
|
|
color: #241a05;
|
|
}
|
|
|
|
/* 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: 22px;
|
|
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 full-size pucks would overlap. The class is set
|
|
from a measurement render.js takes off the ring itself — the closest two neighbours,
|
|
straight line — 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. */
|
|
.seatsTight .seat {
|
|
/* 58 was chosen by measurement and 46 was tried against it. On a roomy phone 46
|
|
cleared every overlap at nine seats where 58 left two — but in the state that
|
|
actually matters, a hand with a side pot where the bar has grown and the felt has
|
|
shrunk to suit, 46 overlapped four times and 58 twice. Pucks touching at that
|
|
density is the felt being small, not the puck being wide, and paying for it with
|
|
an ellipsis on every long name buys nothing. What has to hold is that each seat
|
|
keeps its own centre, so a tap lands on the player it was aimed at, and that is
|
|
measured at every size. */
|
|
width: 58px;
|
|
min-height: 38px;
|
|
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. */
|
|
.seatsTight .seatPuckName {
|
|
margin-top: var(--sp-1);
|
|
font-size: var(--fs-2xs);
|
|
-webkit-line-clamp: 1;
|
|
line-clamp: 1;
|
|
min-height: 0;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.seatsTight .seatPuckStack {
|
|
font-size: var(--fs-2xs);
|
|
}
|
|
|
|
/* 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 and Done button are the sheet's furniture — no reason for either on a
|
|
desktop sidebar, where the panel is just a panel. */
|
|
.seatSheetGrip,
|
|
.seatSheetClose {
|
|
display: none;
|
|
}
|
|
|
|
.seatEditorPanel h2 {
|
|
margin: 0 0 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);
|
|
}
|
|
|
|
.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. */
|
|
#dedicatedDealerToggle {
|
|
width: 24px;
|
|
height: 24px;
|
|
}
|
|
|
|
.linkBtn {
|
|
background: none;
|
|
border: none;
|
|
/* An underlined word was a 91x14 target, which misses the 24px minimum with a
|
|
mouse as well as with a thumb. The coarse-pointer block takes it to 44; this is
|
|
the floor everywhere else, and it changes nothing about how it looks. */
|
|
display: inline-flex;
|
|
align-items: center;
|
|
box-sizing: border-box;
|
|
min-height: 24px;
|
|
padding: 0;
|
|
color: 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;
|
|
}
|
|
|
|
/* One chip per ruling, wrapped across the top of the sheet. Names rather than icons:
|
|
a dealer scanning this is looking for the words somebody at the table just said. */
|
|
.rulingsIndex {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: var(--sp-1);
|
|
margin: 0 0 var(--sp-4);
|
|
}
|
|
|
|
.rulingsIndexBtn {
|
|
box-sizing: border-box;
|
|
min-height: 32px;
|
|
padding: var(--sp-1) var(--sp-2);
|
|
border-radius: var(--r-pill);
|
|
border: 1px solid var(--line);
|
|
background: var(--surface-2);
|
|
color: var(--gold);
|
|
font-family: inherit;
|
|
font-size: var(--fs-sm);
|
|
line-height: 1.25;
|
|
text-align: left;
|
|
cursor: pointer;
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
.rulingsIndexBtn:hover {
|
|
background: var(--surface-3);
|
|
border-color: var(--line-strong);
|
|
}
|
|
}
|
|
|
|
/* A thumb needs the same 44px here as everywhere else. */
|
|
@media (pointer: coarse) {
|
|
.rulingsIndexBtn {
|
|
min-height: 44px;
|
|
font-size: var(--fs-sm);
|
|
}
|
|
}
|
|
|
|
/* The heading the index jumps to takes focus, and a focus ring on a heading that the
|
|
dealer just asked for reads as noise rather than as help. */
|
|
.ruling h3:focus {
|
|
outline: none;
|
|
}
|
|
|
|
.ruling {
|
|
margin-bottom: var(--sp-4);
|
|
padding-left: var(--sp-3);
|
|
border-left: 2px solid rgba(216, 180, 106, 0.4);
|
|
}
|
|
|
|
.ruling h3 {
|
|
margin: 0 0 var(--sp-1);
|
|
color: var(--gold);
|
|
font-size: var(--fs-base);
|
|
font-weight: 600;
|
|
letter-spacing: 0.03em;
|
|
}
|
|
|
|
.ruling p,
|
|
.ruling li {
|
|
color: #c4c4c4;
|
|
font-size: var(--fs-base);
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.ruling p {
|
|
margin: 0;
|
|
}
|
|
|
|
.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;
|
|
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);
|
|
/* This sheet had no height limit and didn't scroll, so on a phone held sideways
|
|
its first two actions — Fold and All-in, the two that matter — sat below the
|
|
bottom of the screen with no way to reach them. A dealer in landscape could
|
|
long-press a seat and then not fold it. */
|
|
max-height: 85vh;
|
|
overflow-y: auto;
|
|
overscroll-behavior: contain;
|
|
}
|
|
|
|
.seatMenuHeader {
|
|
margin-bottom: var(--sp-3);
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
#seatMenuActions {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--sp-2);
|
|
margin-bottom: 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;
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
.seatMenuBtn:hover {
|
|
border-color: var(--line-strong);
|
|
background: linear-gradient(180deg, var(--line), #2a2a2a);
|
|
}
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
/* ---------- 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;
|
|
}
|
|
|
|
.seatContribution {
|
|
border-top: 1px dashed var(--line);
|
|
padding-top: var(--sp-2);
|
|
margin-bottom: var(--sp-1);
|
|
}
|
|
|
|
.chipCounts {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: var(--sp-1);
|
|
margin-bottom: var(--sp-1);
|
|
}
|
|
|
|
.chipCountRow {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-1);
|
|
}
|
|
|
|
.chipSwatch {
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 50%;
|
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
display: inline-block;
|
|
}
|
|
|
|
.chipCountInput {
|
|
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;
|
|
}
|
|
|
|
.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);
|
|
}
|
|
|
|
.chipColorInput {
|
|
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 {
|
|
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 sidebar needs room to scroll past it. */
|
|
body.hasActionBar #holdemSidebar {
|
|
padding-bottom: 9rem;
|
|
}
|
|
|
|
.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: #bdbdbd;
|
|
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);
|
|
}
|
|
}
|
|
|
|
.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: #9c9c9c;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.actionRunout {
|
|
margin-left: auto;
|
|
color: var(--gold);
|
|
font-size: var(--fs-sm);
|
|
}
|
|
|
|
.actionWho {
|
|
margin-left: auto;
|
|
color: #9c9c9c;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* Stopwatch, not a shot clock: it colours up as a prompt to the dealer, and does
|
|
nothing else. */
|
|
.actionTank {
|
|
flex: none;
|
|
min-width: 3.1rem;
|
|
text-align: right;
|
|
color: var(--text-muted);
|
|
font-variant-numeric: tabular-nums;
|
|
transition: color 0.3s ease;
|
|
}
|
|
|
|
.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);
|
|
}
|
|
|
|
.potLayerReturn .potLayerLabel,
|
|
.potLayerReturn .potLayerAmount {
|
|
color: var(--text-muted);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.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. */
|
|
.actionUndoLabel {
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.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.75;
|
|
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. */
|
|
.actionKey {
|
|
position: absolute;
|
|
top: 5px;
|
|
right: 7px;
|
|
padding: 0 var(--sp-1);
|
|
border-radius: var(--r-sm);
|
|
border: 1px solid currentColor;
|
|
opacity: 0.75;
|
|
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. */
|
|
@media (hover: none) {
|
|
.actionKey {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
.actionFold {
|
|
background: linear-gradient(180deg, #3d1f1f, #2c1515);
|
|
border-color: #5b2626;
|
|
color: #ffd6d6;
|
|
}
|
|
|
|
.actionCheck,
|
|
.actionCall {
|
|
background: linear-gradient(180deg, #16783a, #0d5225);
|
|
border-color: #1e8449;
|
|
color: #ecffef;
|
|
}
|
|
|
|
.actionCheck .actionBtnSub,
|
|
.actionCall .actionBtnSub {
|
|
color: #bfe8cb;
|
|
}
|
|
|
|
.actionRaise {
|
|
background: linear-gradient(180deg, var(--gold-tint), var(--gold-shade));
|
|
border-color: var(--gold-deep);
|
|
color: var(--gold-bright);
|
|
}
|
|
|
|
.actionAdvance {
|
|
background: linear-gradient(180deg, #2b3f57, #1b2a3a);
|
|
border-color: #3d5f83;
|
|
color: #dceaf8;
|
|
}
|
|
|
|
/* ---------- 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
.seatPuckStack {
|
|
font-size: var(--fs-2xs);
|
|
color: var(--text-muted);
|
|
font-variant-numeric: tabular-nums;
|
|
margin-top: var(--sp-0);
|
|
}
|
|
|
|
/* "ALL IN" takes the stack line's place, so it has to not read as a stack. Wider
|
|
letters and a warm colour separate it from the grey money above it at a glance —
|
|
which is the whole job, because the dealer is scanning the ring for who can still
|
|
be bet at. */
|
|
.seatPuckStack.seatPuckAllIn {
|
|
color: #ffcf6b;
|
|
font-weight: 700;
|
|
letter-spacing: 0.06em;
|
|
}
|
|
|
|
/* And the seat itself carries a hint of it, so the mark survives being read out of
|
|
the corner of an eye. Deliberately quieter than .seatActing — all-in is a standing
|
|
fact about a player, not a request for the dealer's attention. */
|
|
.seat.seatAllIn {
|
|
border-color: rgba(255, 207, 107, 0.45);
|
|
}
|
|
|
|
/* Tournament-sized amounts. A number has no spaces to break on, so an amount too
|
|
wide for the 64px puck neither wraps nor ellipsises — it draws straight out over
|
|
the seat next to it. These step the size down so every digit stays on its own
|
|
puck. Shrinking rather than abbreviating is deliberate: "$1.25M" would fit more
|
|
comfortably, but a dealer reading a rounded stack off the felt is a worse failure
|
|
than a small one, and the exact figure is what the tool is for. */
|
|
.seatPuckStack.puckAmountLong,
|
|
.seatPuckTotal.puckAmountLong {
|
|
font-size: var(--fs-2xs);
|
|
letter-spacing: -0.01em;
|
|
}
|
|
|
|
.seatPuckStack.puckAmountHuge,
|
|
.seatPuckTotal.puckAmountHuge {
|
|
font-size: 0.44rem;
|
|
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);
|
|
}
|
|
|
|
.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);
|
|
width: 100%;
|
|
max-width: 480px;
|
|
/* 85vh is most of a 390px-tall landscape window, and the action bar is fixed over
|
|
the bottom of it — so the last of the wizard sat underneath the bar and its
|
|
controls couldn't be tapped. Leaving the bar's own height out of the budget
|
|
keeps the whole dialog above it. */
|
|
max-height: calc(85vh - var(--actionBarHeight, 0px));
|
|
/* 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. */
|
|
#potWizardFooter {
|
|
flex: none;
|
|
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. */
|
|
.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 last raw browser control in the tool.
|
|
`accent-color` only paints the tick; the box around it stayed the platform's own —
|
|
a bright white square, which on this near-black dialog was the loudest thing on
|
|
screen and read as unfinished next to controls that had all been drawn. Painted to
|
|
match the rest, on the same surface ramp and the same corner radius. */
|
|
.potWizardChoice input {
|
|
flex: none;
|
|
appearance: none;
|
|
-webkit-appearance: none;
|
|
width: 22px;
|
|
height: 22px;
|
|
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;
|
|
}
|
|
}
|
|
|
|
/* 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. */
|
|
#pokerTableWrap {
|
|
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;
|
|
}
|
|
|
|
/* One column, so the table has the full width and can afford to be round. */
|
|
#pokerTableWrap {
|
|
--feltAspect: 1;
|
|
}
|
|
|
|
.seat {
|
|
width: 78px;
|
|
min-height: 44px;
|
|
}
|
|
}
|
|
|
|
/* ---------- Phones ----------
|
|
Two things break on a narrow screen: seat pucks ring the table on a radius
|
|
measured from the wrap, so the left- and right-most ones hang off the edge of
|
|
the viewport; and the seat editor lives far enough down the page that tapping a
|
|
seat looks like it did nothing. Inset the ring by half a puck, and float the
|
|
editor up as a bottom sheet over the table. */
|
|
@media (max-width: 700px) {
|
|
/* The bottom padding has to clear the fixed action bar, plus room for a toast to
|
|
sit above it. Without it the table just carries on underneath the bar and the
|
|
lower seats of the oval are behind the Fold button — measured at 360x640, where
|
|
62px of felt was covered. `--actionBarHeight` is published by actionBar.js and
|
|
is 0 when no hand is running, so the page doesn't hold a gap it doesn't need. */
|
|
#holdemApp {
|
|
padding: var(--sp-4) var(--sp-3) calc(var(--actionBarHeight, 0px) + 5rem + env(safe-area-inset-bottom, 0px));
|
|
}
|
|
|
|
#holdemHeader {
|
|
margin-bottom: var(--sp-4);
|
|
gap: var(--sp-2);
|
|
}
|
|
|
|
#holdemHeader h1 {
|
|
font-size: var(--fs-xl);
|
|
}
|
|
|
|
#holdemHeaderActions {
|
|
width: 100%;
|
|
gap: var(--sp-2);
|
|
}
|
|
|
|
#holdemHeaderActions .holdemBtn {
|
|
flex: 1 1 auto;
|
|
justify-content: center;
|
|
min-height: 46px;
|
|
padding: var(--sp-2) var(--sp-3);
|
|
font-size: var(--fs-base);
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
|
|
/* Narrower than the desktop puck: at a full ten-handed table the ring is
|
|
flattest at the top and bottom, and wide pucks are what made neighbours
|
|
overlap there. The puck is only a summary — the real form is the sheet. */
|
|
.seat {
|
|
width: 64px;
|
|
min-height: 42px;
|
|
padding: 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 ---- */
|
|
|
|
#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%);
|
|
transition: transform 0.25s ease;
|
|
-webkit-overflow-scrolling: touch;
|
|
}
|
|
|
|
body.seatSheetOpen #seatEditorPanel {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
/* A bare checkbox with no label box around it to grow. */
|
|
#dedicatedDealerToggle {
|
|
width: 24px;
|
|
height: 24px;
|
|
}
|
|
|
|
/* An underlined word is a 14px-tall target; give it room without making it look
|
|
like a button. */
|
|
.linkBtn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
min-height: 44px;
|
|
padding: 0 var(--sp-2);
|
|
}
|
|
|
|
/* The dealer button has to stay a 22px disc — it's a chip sitting on a seat, and
|
|
drawing it thumb-sized would swallow the puck. So the disc keeps its size and
|
|
an invisible pad around it takes the tap. It reaches outward, away from the
|
|
puck's own tap target, so pressing the seat still opens the seat. */
|
|
.dealerChip::after {
|
|
content: "";
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
width: 44px;
|
|
height: 44px;
|
|
transform: translate(-50%, -50%);
|
|
}
|
|
|
|
.dealerChip {
|
|
overflow: visible;
|
|
}
|
|
|
|
.toastAction {
|
|
min-height: 44px;
|
|
}
|
|
|
|
.confirmActions {
|
|
flex-direction: column-reverse;
|
|
}
|
|
|
|
.confirmActions .holdemBtn {
|
|
width: 100%;
|
|
justify-content: center;
|
|
}
|
|
}
|
|
|
|
/* ---------- A phone on its side ----------
|
|
Deliberately last in the file. The felt is an aspect-ratio box whose height comes
|
|
from `padding-bottom`, and the `max-width: 900px` block above re-sets that to 100%
|
|
— so a landscape rule placed any earlier is overridden at equal specificity and
|
|
does nothing, which is exactly what happened on the first attempt.
|
|
|
|
Landscape is where this layout falls apart worst: sized from width, the table came
|
|
out 765px tall in a 390px-tall window, putting six of the nine seats below the fold
|
|
and the rest behind the action bar. Sizing it from the height that's actually left
|
|
makes every seat reachable without scrolling. The table ends up small, but a seat
|
|
you have to go hunting for is worse than a seat that's small. */
|
|
@media (orientation: landscape) and (max-height: 560px) {
|
|
/* Set outright rather than via padding-bottom: a percentage padding resolves
|
|
against the containing block's width, not the element's own, so narrowing the
|
|
wrap on its own left it full height regardless. */
|
|
#pokerTableWrap {
|
|
/* A wide ellipse, which is what a real poker table is.
|
|
|
|
Capping the width by the height to hold the portrait aspect ratio was
|
|
throwing away most of the screen: at 640x360 the felt came out 135px wide
|
|
inside a 601px column — 22% of the width — leaving 366px of rim for nine
|
|
seats that need 662px. Adjacent seat centres ended up 13px apart, which is
|
|
why a seat's centre belonged to its neighbour.
|
|
|
|
Landscape has width and no height, so the table takes the width it has and
|
|
is only as tall as the room left over.
|
|
|
|
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 hand number stays — nothing else on
|
|
the screen says which hand this is. */
|
|
#holdemHeader {
|
|
margin-bottom: var(--sp-2);
|
|
gap: var(--sp-1);
|
|
flex-wrap: nowrap;
|
|
align-items: center;
|
|
}
|
|
|
|
#holdemHeader h1 {
|
|
position: absolute;
|
|
width: 1px;
|
|
height: 1px;
|
|
margin: -1px;
|
|
padding: 0;
|
|
overflow: hidden;
|
|
clip-path: inset(50%);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.handCounterBadge {
|
|
margin-top: 0;
|
|
}
|
|
|
|
/* Tightened across, never down: the buttons keep the 44px a thumb needs. */
|
|
#holdemHeaderActions {
|
|
flex-wrap: nowrap;
|
|
gap: var(--sp-1);
|
|
}
|
|
|
|
#holdemHeaderActions .holdemBtn {
|
|
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;
|
|
}
|
|
|
|
/* Nine seats around a table this small overlap each other, so they shrink to suit. */
|
|
.seat {
|
|
width: 62px;
|
|
min-height: 40px;
|
|
}
|
|
|
|
/* 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);
|
|
}
|
|
}
|
|
|
|
/* ---------- Keyboard focus ----------
|
|
Three focus rules existed for the whole tool, so tabbing through it relied on
|
|
whatever ring the browser draws by default — which on these dark, custom-painted
|
|
controls is close to invisible. `:focus-visible` rather than `:focus` keeps it to
|
|
keyboard use: a dealer tapping Fold shouldn't be left with a ring on it. */
|
|
.holdemBtn:focus-visible,
|
|
.linkBtn:focus-visible,
|
|
.seatMenuBtn:focus-visible,
|
|
.seatSheetClose:focus-visible,
|
|
.pickerCard:focus-visible,
|
|
.actionBtn:focus-visible,
|
|
.raiseQuickBtn:focus-visible,
|
|
.actionUndo:focus-visible,
|
|
.toastAction:focus-visible,
|
|
.toastClose:focus-visible,
|
|
.potWizardChoice:focus-visible,
|
|
.cardSlot:focus-visible,
|
|
.subPanel summary:focus-visible,
|
|
.seat:focus-visible,
|
|
button:focus-visible,
|
|
[role="button"]:focus-visible,
|
|
input:focus-visible,
|
|
select:focus-visible,
|
|
textarea:focus-visible {
|
|
outline: 2px solid #7fc4ff;
|
|
outline-offset: 2px;
|
|
/* The ring has to sit above the neighbouring seat that overlaps this one. */
|
|
position: relative;
|
|
z-index: 3;
|
|
}
|
|
|
|
/* A sheet with nothing focusable in it takes focus itself so its title is read out.
|
|
That shouldn't paint a ring around the whole panel. */
|
|
[role="dialog"]:focus,
|
|
[role="alertdialog"]:focus {
|
|
outline: none;
|
|
}
|
|
|
|
/* ---------- Fields on a tablet ----------
|
|
The 16px minimum and the 44px touch targets were both written into the phone
|
|
media query, so every iPad size missed them — measured at 768, 834 and 1024 wide,
|
|
where all ten setup fields sat at 13.3px. iOS zooms the page whenever a field
|
|
under 16px takes focus, so a dealer on an iPad was getting the table zoomed every
|
|
time they touched the blind amount, then pinching back out. The reason for the
|
|
rule was never "narrow screen", it was "iOS", and an iPad is iOS.
|
|
|
|
Font size applies everywhere now — 13.3px was small for a settings field on a
|
|
desktop anyway. The touch sizing keys off `hover: none`, which is what actually
|
|
distinguishes a finger from a mouse; a tablet gets thumb-sized controls and a
|
|
desktop keeps its compact sidebar. */
|
|
.seatEditorForm input:not([type="checkbox"]):not([type="color"]),
|
|
.panelBody input:not([type="checkbox"]):not([type="color"]),
|
|
.subPanelBody input:not([type="checkbox"]):not([type="color"]),
|
|
.panelBody select,
|
|
.panelBody textarea {
|
|
font-size: 16px;
|
|
}
|
|
|
|
@media (hover: none) {
|
|
.seatEditorForm input:not([type="checkbox"]):not([type="color"]),
|
|
.panelBody input:not([type="checkbox"]):not([type="color"]),
|
|
.subPanelBody input:not([type="checkbox"]):not([type="color"]),
|
|
.panelBody select {
|
|
min-height: 44px;
|
|
padding: 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) {
|
|
.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;
|
|
}
|
|
|
|
#dedicatedDealerToggle {
|
|
width: 24px;
|
|
height: 24px;
|
|
}
|
|
|
|
/* An underlined word is a 14px-tall target; give it room without making it look
|
|
like a button. */
|
|
.linkBtn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
min-height: 44px;
|
|
padding: 0 var(--sp-2);
|
|
}
|
|
}
|