Last round I left a residual and said it wanted its own fix: on a phone held sideways, seats overlapped so badly that a seat's centre belonged to the seat next door. Here's the cause. The landscape rule capped the felt's width by its height to hold the portrait aspect ratio. At 640x360 that produced a table 135px wide inside a 601px column -- 22% of the screen -- with 366px of rim for nine seats needing 662px. Adjacent seat centres ended up 13px apart for seats rendering 74px wide. Landscape has width and no height, so the table now takes the width it has and is only as tall as the room left over. A wide ellipse, which is what a real poker table is anyway. 640x360 felt 135x97 -> 601x121 rim 366 -> 1263 centres 13px -> 33px 844x390 felt 176x127 -> 765x151 rim 480 -> 1605 centres 37px -> 59px 915x412 felt 207x149 -> 492x173 rim 563 -> 1106 centres 44px -> 55px Seat ownership went from 8 seats wrong at 640x360 and 2 at 844x390 to zero at 844x390, zero at 915x412, and two at the very smallest. Nine of eleven sizes clean, portrait and desktop untouched. Two things I tried and threw away Spacing seats by arc length instead of by angle. It sounds obviously right -- even distance around the rim rather than even angle -- and it made things worse: 640x360 went from 2 wrong to 3, closest centres 33px to 21px, and even the desktop table dropped from 180px to 155px. Even arc spacing packs seats through the tight turns at the ends of an ellipse, where a fixed distance along the rim covers much less straight-line distance. Equal angles happen to separate them better. Reverted. Shrinking seats on the tightest landscape phones. Took the rendered seat from 74px to 64px and moved closest centres not at all -- still 33px -- while the covered count at 844x390 went from 3 to 4. A change that measures as nothing but costs CSS and legibility isn't worth keeping. Reverted. So this is one CSS rule, which is all the measurements supported. What's left 640x360 and 667x375 still have two seats whose centre lands on a neighbour. Nine seats on a ring 121px tall is a genuine physical constraint rather than a layout mistake, and both remaining fixes I tried made it worse rather than better. It is reported by the audit rather than hidden, and a dealer on a 360px-tall screen has the seat menu and the action bar for everything that matters. 152 tests passing; contrast clean at every size; dealing still works in landscape. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
3049 lines
72 KiB
CSS
3049 lines
72 KiB
CSS
body {
|
|
background: radial-gradient(ellipse at 50% -10%, #1c1c1c 0%, #101010 55%, #0a0a0a 100%);
|
|
min-height: 100vh;
|
|
margin: 0;
|
|
color: #e6e6e6;
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
}
|
|
|
|
/* ---------- Touch ----------
|
|
A dealer runs this one-handed on a phone with players waiting, so a tap has to
|
|
register the instant their thumb lands.
|
|
|
|
`touch-action: manipulation` is the whole game here. Without it a mobile browser
|
|
holds every tap for ~300ms to see whether a second one is coming that would mean
|
|
"zoom in" — on top of which double-tapping a seat, which this tool uses for fold,
|
|
*is* the browser's zoom gesture, so the dealer's shortcut fights the page zooming
|
|
under their thumb. `manipulation` keeps panning and pinch-zoom and drops only the
|
|
double-tap-to-zoom, which is exactly the one we don't want.
|
|
|
|
The tap highlight goes with it: every control below gets a real pressed state, and
|
|
the browser's translucent grey rectangle on top of a designed one just looks like
|
|
a rendering artefact. Text selection goes too — a fast tap-and-hold on a seat
|
|
should open the seat menu, not raise a text-selection magnifier over the felt. */
|
|
.holdemBtn,
|
|
.seat,
|
|
.dealerChip,
|
|
.cardSlot,
|
|
.linkBtn,
|
|
.seatMenuBtn,
|
|
.seatSheetClose,
|
|
.pickerCard,
|
|
.actionBtn,
|
|
.raiseQuickBtn,
|
|
.actionUndo,
|
|
.toastAction,
|
|
.toastClose,
|
|
.potWizardChoice,
|
|
.subPanel summary,
|
|
button,
|
|
[role="button"],
|
|
input,
|
|
select,
|
|
textarea,
|
|
a {
|
|
touch-action: manipulation;
|
|
}
|
|
|
|
/* The grey rectangle goes from every control, fields included — a tapped input
|
|
already shows a caret and a focus ring, which say the same thing more quietly. */
|
|
.holdemBtn,
|
|
.seat,
|
|
.dealerChip,
|
|
.cardSlot,
|
|
.linkBtn,
|
|
.seatMenuBtn,
|
|
.seatSheetClose,
|
|
.pickerCard,
|
|
.actionBtn,
|
|
.raiseQuickBtn,
|
|
.actionUndo,
|
|
.toastAction,
|
|
.toastClose,
|
|
.potWizardChoice,
|
|
.subPanel summary,
|
|
button,
|
|
[role="button"],
|
|
input,
|
|
select,
|
|
textarea,
|
|
a {
|
|
-webkit-tap-highlight-color: transparent;
|
|
}
|
|
|
|
/* Text selection is switched off only on the things you press, never on the fields
|
|
you type into — a dealer editing a player's name still needs to select the text. */
|
|
.holdemBtn,
|
|
.seat,
|
|
.dealerChip,
|
|
.cardSlot,
|
|
.linkBtn,
|
|
.seatMenuBtn,
|
|
.seatSheetClose,
|
|
.pickerCard,
|
|
.actionBtn,
|
|
.raiseQuickBtn,
|
|
.actionUndo,
|
|
.toastAction,
|
|
.toastClose,
|
|
.potWizardChoice,
|
|
.subPanel summary,
|
|
button,
|
|
[role="button"] {
|
|
-webkit-user-select: none;
|
|
user-select: none;
|
|
}
|
|
|
|
/* ---------- Pressed states ----------
|
|
Touch has no hover, so the press is the only feedback a dealer gets that the tap
|
|
landed — and with the grey highlight suppressed above, these have to carry it.
|
|
Everything presses the same way it would on a real table: down and slightly
|
|
darker, no travel time. */
|
|
.holdemBtn:active:not(:disabled),
|
|
.linkBtn:active:not(:disabled),
|
|
.seatMenuBtn:active:not(:disabled),
|
|
.seatSheetClose:active:not(:disabled),
|
|
.toastAction:active,
|
|
.toastClose:active,
|
|
.potWizardChoice:active,
|
|
.raiseQuickBtn:active:not(:disabled),
|
|
.actionUndo:active:not(:disabled) {
|
|
transform: translateY(1px);
|
|
filter: brightness(1.25);
|
|
transition-duration: 0.04s;
|
|
}
|
|
|
|
.cardSlot:active,
|
|
.pickerCard:active:not(:disabled),
|
|
.dealerChip:active {
|
|
transform: translateY(1px) scale(0.97);
|
|
transition-duration: 0.04s;
|
|
}
|
|
|
|
/* The action bar already presses down; the brightness is what it was missing, and
|
|
it's the one place a dealer taps without looking. */
|
|
.actionBtn:active:not(:disabled) {
|
|
filter: brightness(1.3);
|
|
transition-duration: 0.04s;
|
|
}
|
|
|
|
/* Seats deliberately have no `:active` rule. They already dip via `.seatPressing`,
|
|
which `pointerdown` adds in render.js — that fires earlier than `:active` resolves,
|
|
and, more importantly, a seat's `transform` is what positions it around the felt
|
|
(`translate(-50%, -50%)` plus its own left/top). A press state setting `transform`
|
|
would drop that translate and throw every pressed seat about 38px off its spot. */
|
|
|
|
/* Nobody should get a stuck press state from a tap that turned into a scroll. */
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.holdemBtn:active:not(:disabled),
|
|
.linkBtn:active:not(:disabled),
|
|
.seatMenuBtn:active:not(:disabled),
|
|
.seatSheetClose:active:not(:disabled),
|
|
.cardSlot:active,
|
|
.pickerCard:active:not(:disabled),
|
|
.dealerChip:active,
|
|
.raiseQuickBtn:active:not(:disabled),
|
|
.actionUndo:active:not(:disabled) {
|
|
transform: none;
|
|
}
|
|
}
|
|
|
|
#holdemApp {
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
padding: 2rem;
|
|
}
|
|
|
|
/* ---------- Header ---------- */
|
|
|
|
#holdemHeader {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
#holdemHeader h1 {
|
|
margin: 0;
|
|
font-size: 1.8rem;
|
|
font-weight: 300;
|
|
letter-spacing: 0.04em;
|
|
color: #f5f5f5;
|
|
position: relative;
|
|
display: inline-block;
|
|
padding-bottom: 0.4rem;
|
|
}
|
|
|
|
#holdemHeader h1::after {
|
|
content: "";
|
|
position: absolute;
|
|
left: 0;
|
|
bottom: 0;
|
|
width: 42px;
|
|
height: 3px;
|
|
border-radius: 2px;
|
|
background: linear-gradient(90deg, #d8b46a, #a9782f);
|
|
}
|
|
|
|
.holdemSubtitle {
|
|
margin: 0.25rem 0 0;
|
|
color: #999;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.handCounterBadge {
|
|
display: inline-block;
|
|
margin-top: 0.5rem;
|
|
padding: 0.2rem 0.65rem;
|
|
border-radius: 999px;
|
|
background: rgba(216, 180, 106, 0.12);
|
|
border: 1px solid rgba(216, 180, 106, 0.4);
|
|
color: #d8b46a;
|
|
font-size: 0.75rem;
|
|
letter-spacing: 0.04em;
|
|
}
|
|
|
|
#holdemHeaderActions {
|
|
display: flex;
|
|
gap: 0.6rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.holdemBtn {
|
|
background: linear-gradient(180deg, #2e2e2e, #232323);
|
|
color: #eee;
|
|
border: 1px solid #3a3a3a;
|
|
border-radius: 7px;
|
|
padding: 0.6rem 1rem;
|
|
font-size: 0.85rem;
|
|
cursor: pointer;
|
|
/* Named rather than `all`, so the focus ring isn't animated. With `all` the
|
|
outline faded in over 150ms, which on keyboard navigation reads as the ring
|
|
lagging a step behind the Tab key. Nothing else here changes. */
|
|
transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease,
|
|
box-shadow 0.15s ease, color 0.15s ease, opacity 0.15s ease, filter 0.15s ease;
|
|
text-decoration: none;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.3rem;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
.holdemBtn:hover {
|
|
background: linear-gradient(180deg, #383838, #2a2a2a);
|
|
border-color: #4a4a4a;
|
|
transform: translateY(-1px);
|
|
}
|
|
}
|
|
|
|
.holdemBtnAccent {
|
|
background: linear-gradient(180deg, #16783a, #0d5225);
|
|
border-color: #1e8449;
|
|
color: #ecffef;
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
.holdemBtnAccent:hover {
|
|
background: linear-gradient(180deg, #1a8a44, #0f602c);
|
|
}
|
|
}
|
|
|
|
.holdemBtnDanger {
|
|
background: linear-gradient(180deg, #4a1f1f, #351515);
|
|
border-color: #6b2626;
|
|
color: #ffd6d6;
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
.holdemBtnDanger:hover {
|
|
background: linear-gradient(180deg, #572525, #401818);
|
|
}
|
|
}
|
|
|
|
.holdemBtnGhost {
|
|
background: transparent;
|
|
}
|
|
|
|
.holdemBtnSmall {
|
|
padding: 0.3rem 0.6rem;
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
.holdemBtn:disabled {
|
|
opacity: 0.4;
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
}
|
|
|
|
.handHistoryActions {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
margin-top: 0.5rem;
|
|
}
|
|
|
|
/* ---------- Layout ---------- */
|
|
|
|
#holdemLayout {
|
|
display: grid;
|
|
grid-template-columns: 1fr 320px;
|
|
gap: 1.5rem;
|
|
align-items: start;
|
|
}
|
|
|
|
#tableSection {
|
|
min-width: 0;
|
|
}
|
|
|
|
.tableHint {
|
|
color: #949494;
|
|
font-size: 0.8rem;
|
|
text-align: center;
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
/* ---------- Poker table ---------- */
|
|
|
|
#pokerTableWrap {
|
|
position: relative;
|
|
width: 100%;
|
|
padding-bottom: 72%;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
|
|
#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;
|
|
}
|
|
|
|
#tableWatermark {
|
|
position: absolute;
|
|
inset: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: rgba(255, 255, 255, 0.06);
|
|
font-size: 2.4rem;
|
|
font-weight: 700;
|
|
letter-spacing: 0.15em;
|
|
text-transform: uppercase;
|
|
text-align: center;
|
|
pointer-events: none;
|
|
user-select: none;
|
|
}
|
|
|
|
#potBadge {
|
|
position: relative;
|
|
background: radial-gradient(circle at 32% 28%, rgba(255, 255, 255, 0.08), rgba(0, 0, 0, 0.55));
|
|
border: 1px solid rgba(255, 255, 255, 0.14);
|
|
border-radius: 999px;
|
|
padding: 0.7rem 1.7rem;
|
|
text-align: center;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.15rem;
|
|
box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.6), 0 4px 14px rgba(0, 0, 0, 0.45);
|
|
}
|
|
|
|
#potBadgeLabel {
|
|
font-size: 0.68rem;
|
|
letter-spacing: 0.14em;
|
|
text-transform: uppercase;
|
|
color: #d8b46a;
|
|
}
|
|
|
|
#potBadgeAmount {
|
|
font-size: 1.5rem;
|
|
font-weight: 700;
|
|
color: #fff;
|
|
}
|
|
|
|
#seatsLayer {
|
|
position: absolute;
|
|
inset: 0;
|
|
}
|
|
|
|
/* ---------- Bets on the felt ---------- */
|
|
|
|
/* Shares the seat ring's box exactly, so a bet always lands on the spoke between its
|
|
player and the pot. When the seat ring gets inset on a phone, this moves with it —
|
|
measured the alternative, and a bets ring on its own geometry ends up either behind
|
|
the pucks or on top of the pot badge. */
|
|
#betsLayer {
|
|
position: absolute;
|
|
inset: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.betChip {
|
|
position: absolute;
|
|
transform: translate(-50%, -50%);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.28rem;
|
|
padding: 0.1rem 0.42rem 0.1rem 0.2rem;
|
|
background: rgba(0, 0, 0, 0.55);
|
|
border: 1px solid rgba(255, 255, 255, 0.14);
|
|
border-radius: 999px;
|
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* Reads as a chip seen from above: a lighter top-left, a rim, and the notches a
|
|
casino chip has around its edge. */
|
|
.betChipDisc {
|
|
width: 13px;
|
|
height: 13px;
|
|
border-radius: 50%;
|
|
border: 1px solid rgba(255, 255, 255, 0.45);
|
|
box-shadow:
|
|
inset 0 1px 1px rgba(255, 255, 255, 0.45),
|
|
inset 0 -1px 2px rgba(0, 0, 0, 0.45);
|
|
}
|
|
|
|
.betChipAmount {
|
|
font-size: 0.62rem;
|
|
font-weight: 700;
|
|
color: #f0f0f0;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
/* Clinging to a puck rather than sitting on its own ring — the phone layout. */
|
|
.betChipAnchored {
|
|
left: 50%;
|
|
z-index: 3;
|
|
}
|
|
|
|
.betChipAbove {
|
|
top: -11px;
|
|
transform: translate(-50%, -50%);
|
|
}
|
|
|
|
.betChipBelow {
|
|
top: auto;
|
|
bottom: -11px;
|
|
transform: translate(-50%, 50%);
|
|
}
|
|
|
|
/* ---------- Seats (compact pucks on the table) ---------- */
|
|
|
|
.seat {
|
|
position: absolute;
|
|
transform: translate(-50%, -50%);
|
|
width: 96px;
|
|
min-height: 54px;
|
|
background: linear-gradient(160deg, #2c2c2c, #1b1b1b);
|
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
border-radius: 14px;
|
|
padding: 0.3rem 0.4rem;
|
|
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
|
cursor: grab;
|
|
z-index: 1;
|
|
text-align: center;
|
|
transition: box-shadow 0.15s ease, border-color 0.15s ease, transform 0.1s ease;
|
|
/* A long press on a phone otherwise selects the player's name or raises the
|
|
system callout menu instead of ours. */
|
|
user-select: none;
|
|
-webkit-user-select: none;
|
|
-webkit-touch-callout: none;
|
|
}
|
|
|
|
.seat.dragging {
|
|
opacity: 0.4;
|
|
}
|
|
|
|
.seat.dropTarget {
|
|
outline: 2px dashed #d8b46a;
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
/* Four states a dealer needs to tell apart from across the table, without
|
|
reading anything: live in this hand, folded, sitting out, and nobody there.
|
|
Live gets a green edge, folded gets a red one and a struck-through name,
|
|
sitting-out just fades, and an empty chair is the only dashed outline. */
|
|
|
|
.seat.seatLive {
|
|
border-color: rgba(78, 201, 122, 0.45);
|
|
box-shadow:
|
|
0 0 0 1px rgba(78, 201, 122, 0.18),
|
|
0 3px 10px rgba(0, 0, 0, 0.5),
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
|
}
|
|
|
|
/* Sitting out, not gone: still a person the dealer may need to deal back in, so
|
|
the name has to stay readable rather than fading almost to the felt. */
|
|
.seat.seatInactive {
|
|
opacity: 0.58;
|
|
}
|
|
|
|
.seat.seatFolded {
|
|
border-color: rgba(224, 123, 123, 0.4);
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.seat.seatFolded .seatPuckName {
|
|
text-decoration: line-through;
|
|
text-decoration-color: rgba(224, 123, 123, 0.75);
|
|
}
|
|
|
|
.seat.seatEmpty {
|
|
background: rgba(255, 255, 255, 0.02);
|
|
border-style: dashed;
|
|
border-color: rgba(255, 255, 255, 0.13);
|
|
box-shadow: none;
|
|
/* Deliberately louder than seatInactive: an open seat is information a
|
|
dealer wants, not something to squint at. */
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.seatEmptyLabel {
|
|
color: #8a8a8a;
|
|
font-size: 0.6rem;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.seat.seatSelected {
|
|
border-color: rgba(9, 255, 0, 0.6);
|
|
box-shadow: 0 0 0 2px rgba(9, 255, 0, 0.4), 0 4px 14px rgba(0, 0, 0, 0.5);
|
|
}
|
|
|
|
.seatChairNumber {
|
|
position: absolute;
|
|
top: 2px;
|
|
left: 6px;
|
|
font-size: 0.58rem;
|
|
color: #8a8a8a;
|
|
}
|
|
|
|
.seatPuckName {
|
|
font-size: 0.72rem;
|
|
font-weight: 600;
|
|
color: #f0f0f0;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
margin-top: 0.6rem;
|
|
}
|
|
|
|
.seatPuckCards {
|
|
font-size: 0.68rem;
|
|
font-weight: 700;
|
|
font-family: var(--font-mono, monospace);
|
|
color: #ddd;
|
|
min-height: 0.95em;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
.seatPuckCards .cardRed {
|
|
color: #ff6b6b;
|
|
}
|
|
|
|
.seatPuckTotal {
|
|
display: inline-block;
|
|
margin-top: 0.15rem;
|
|
font-size: 0.64rem;
|
|
font-weight: 700;
|
|
color: #a9f5b8;
|
|
background: rgba(9, 255, 0, 0.1);
|
|
border-radius: 999px;
|
|
padding: 0 0.35rem;
|
|
min-height: 1.1em;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.dealerChip {
|
|
position: absolute;
|
|
/* Inside its own seat, not on the rim.
|
|
|
|
It used to sit at -10px/-10px, hanging over the seat's corner — which on an oval
|
|
of overlapping seats means hanging over the seat *next to it*. Measured on a
|
|
nine-seat table: tapping the centre of seat 1 gave the button to seat 9. That
|
|
was always slightly true and the enlarged hit area made it much worse; three of
|
|
nine seats were losing taps, one of them losing its own middle.
|
|
|
|
Tucked inside, the overlap is zero at every size. It reads as a puck in the
|
|
corner of the seat rather than on its edge, which is a small price for tapping
|
|
a player and getting that player. */
|
|
top: 3px;
|
|
right: 3px;
|
|
width: 22px;
|
|
height: 22px;
|
|
border-radius: 50%;
|
|
background: radial-gradient(circle at 35% 30%, #fff, #d8d8d8 55%, #b8b8b8 100%);
|
|
color: #222;
|
|
border: 2px solid #999;
|
|
font-weight: 700;
|
|
font-size: 0.68rem;
|
|
cursor: pointer;
|
|
z-index: 2;
|
|
opacity: 0.32;
|
|
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;
|
|
}
|
|
|
|
.dealerChip:focus-visible {
|
|
opacity: 1;
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
.seat:hover .dealerChip {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
.dealerChip.active {
|
|
background: radial-gradient(circle at 35% 30%, #ffe9a8, #f5d547 55%, #cba428 100%);
|
|
border-color: #a9782f;
|
|
color: #3a2e00;
|
|
opacity: 1;
|
|
}
|
|
|
|
.dealerChip.hidden {
|
|
display: none;
|
|
}
|
|
|
|
.blindBadge {
|
|
display: none;
|
|
position: absolute;
|
|
top: -9px;
|
|
font-size: 0.58rem;
|
|
font-weight: 700;
|
|
padding: 0.05rem 0.34rem;
|
|
border-radius: 999px;
|
|
z-index: 2;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
|
|
}
|
|
|
|
.blindBadge.visible {
|
|
display: inline-block;
|
|
}
|
|
|
|
/* Both badges failed WCAG AA against their own gradients — measured at the worst
|
|
stop, the small blind was 3.52:1 and the big blind 2.15:1, in 9px bold text a
|
|
dealer is meant to read at a glance across a table.
|
|
|
|
The small blind keeps its blue and just darkens (4.77:1 at its worst stop). The
|
|
big blind flips to dark text on brighter gold (5.84:1), which is how a gold chip
|
|
is marked anyway and has the side benefit of making the two badges far easier to
|
|
tell apart at speed — light-on-blue against dark-on-gold, rather than two pale
|
|
labels differing only in hue. */
|
|
.sbBadge {
|
|
left: -6px;
|
|
background: linear-gradient(180deg, #2f6cb5, #22508c);
|
|
color: #eaf3ff;
|
|
}
|
|
|
|
.bbBadge {
|
|
left: 22px;
|
|
background: linear-gradient(180deg, #e6ad4d, #c98a2a);
|
|
color: #241a05;
|
|
}
|
|
|
|
/* A blind the dealer named by hand rather than one the button handed out. Without
|
|
the ring the table just looks like it's put the blinds in the wrong place. */
|
|
.blindBadge.blindManual {
|
|
outline: 1.5px solid #f2f2f2;
|
|
outline-offset: 1px;
|
|
}
|
|
|
|
/* ---------- Seat editor panel (sidebar form for the selected seat) ---------- */
|
|
|
|
.seatEditorPanel {
|
|
padding: 0.9rem;
|
|
}
|
|
|
|
.seatEditorHeader {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.6rem;
|
|
}
|
|
|
|
.seatEditorHeader h2 {
|
|
flex: 1;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* Grip and Done button are the sheet's furniture — no reason for either on a
|
|
desktop sidebar, where the panel is just a panel. */
|
|
.seatSheetGrip,
|
|
.seatSheetClose {
|
|
display: none;
|
|
}
|
|
|
|
.seatEditorPanel h2 {
|
|
margin: 0 0 0.6rem;
|
|
padding-bottom: 0.5rem;
|
|
font-size: 0.95rem;
|
|
font-weight: 500;
|
|
color: #f5f5f5;
|
|
border-bottom: 1px solid rgba(216, 180, 106, 0.35);
|
|
}
|
|
|
|
.seatEditorForm {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.6rem;
|
|
}
|
|
|
|
.seatCardToggles {
|
|
display: flex;
|
|
gap: 0.6rem;
|
|
font-size: 0.8rem;
|
|
color: #aaa;
|
|
}
|
|
|
|
.inHandToggle,
|
|
.foldToggle {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.3rem;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.seatNameInput {
|
|
flex: 1;
|
|
min-width: 0;
|
|
background: #1a1a1a;
|
|
border: 1px solid #3a3a3a;
|
|
border-radius: 5px;
|
|
color: #eee;
|
|
padding: 0.3rem 0.45rem;
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
.seatCards {
|
|
display: flex;
|
|
gap: 0.35rem;
|
|
margin-bottom: 0.4rem;
|
|
}
|
|
|
|
/* The two hole-card slots: they look like cards face-up on the felt, and tapping
|
|
one opens the picker rather than a keyboard. */
|
|
.cardSlot {
|
|
width: 100%;
|
|
min-height: 52px;
|
|
text-align: center;
|
|
background: #fdfdfd;
|
|
color: #111;
|
|
border: 1px solid #3a3a3a;
|
|
border-radius: 6px;
|
|
padding: 0.35rem 0;
|
|
font-size: 1.25rem;
|
|
font-weight: 700;
|
|
font-family: var(--font-mono, monospace);
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
|
cursor: pointer;
|
|
transition: transform 0.12s ease, box-shadow 0.12s ease;
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
.cardSlot:hover {
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.4);
|
|
}
|
|
}
|
|
|
|
.cardSlot.cardRed {
|
|
color: #c0392b;
|
|
}
|
|
|
|
.cardSlot.cardSlotEmpty {
|
|
background: #191919;
|
|
color: #8a8a8a;
|
|
border-style: dashed;
|
|
border-color: #3f3f3f;
|
|
box-shadow: none;
|
|
font-weight: 400;
|
|
}
|
|
|
|
/* ---------- Add chips ---------- */
|
|
|
|
.seatBuyIns {
|
|
margin-top: -0.3rem;
|
|
color: #9a9a9a;
|
|
font-size: 0.75rem;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
#addChipsOverlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.72);
|
|
display: flex;
|
|
align-items: flex-end;
|
|
justify-content: center;
|
|
z-index: 98;
|
|
}
|
|
|
|
#addChipsOverlay[hidden] {
|
|
display: none;
|
|
}
|
|
|
|
#addChipsSheet {
|
|
width: 100%;
|
|
max-width: 420px;
|
|
background: linear-gradient(180deg, #242424, #1a1a1a);
|
|
border: 1px solid #3a3a3a;
|
|
border-bottom: none;
|
|
border-radius: 16px 16px 0 0;
|
|
padding: 0.35rem 1rem calc(1.2rem + env(safe-area-inset-bottom, 0px));
|
|
box-shadow: 0 -14px 40px rgba(0, 0, 0, 0.65);
|
|
}
|
|
|
|
.addChipsHeader {
|
|
margin-bottom: 0.8rem;
|
|
}
|
|
|
|
.addChipsHeader h2 {
|
|
margin: 0;
|
|
font-size: 1rem;
|
|
font-weight: 500;
|
|
color: #f5f5f5;
|
|
}
|
|
|
|
#addChipsSubtitle {
|
|
color: #9a9a9a;
|
|
font-size: 0.78rem;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
#addChipsQuick {
|
|
display: flex;
|
|
gap: 0.45rem;
|
|
margin-bottom: 0.7rem;
|
|
}
|
|
|
|
.addChipsQuickBtn {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 0.1rem;
|
|
min-height: 52px;
|
|
border-radius: 11px;
|
|
border: 1px solid #3a3a3a;
|
|
background: linear-gradient(180deg, #2e2e2e, #232323);
|
|
color: #ddd;
|
|
font-family: inherit;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.addChipsQuickBtn.selected {
|
|
border-color: #d8b46a;
|
|
background: linear-gradient(180deg, #3a2f18, #241d0f);
|
|
color: #f0d9a8;
|
|
}
|
|
|
|
.addChipsQuickLabel {
|
|
font-size: 0.78rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.addChipsQuickAmount {
|
|
font-size: 0.7rem;
|
|
color: #a8a8a8;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.addChipsQuickBtn.selected .addChipsQuickAmount {
|
|
color: #d8b46a;
|
|
}
|
|
|
|
.addChipsEntry {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.4rem;
|
|
background: #141414;
|
|
border: 1px solid #3a3a3a;
|
|
border-radius: 11px;
|
|
padding: 0.2rem 0.8rem;
|
|
margin-bottom: 0.8rem;
|
|
}
|
|
|
|
.addChipsEntry:focus-within {
|
|
border-color: #d8b46a;
|
|
}
|
|
|
|
.addChipsCurrency {
|
|
color: #8a8a8a;
|
|
font-size: 1.3rem;
|
|
}
|
|
|
|
#addChipsInput {
|
|
flex: 1;
|
|
min-width: 0;
|
|
background: none;
|
|
border: none;
|
|
outline: none;
|
|
color: #f2f2f2;
|
|
font-family: inherit;
|
|
font-size: 1.5rem;
|
|
font-weight: 500;
|
|
font-variant-numeric: tabular-nums;
|
|
padding: 0.4rem 0;
|
|
}
|
|
|
|
.addChipsActions {
|
|
display: flex;
|
|
flex-direction: column-reverse;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.addChipsActions .holdemBtn {
|
|
width: 100%;
|
|
justify-content: center;
|
|
min-height: 48px;
|
|
}
|
|
|
|
/* ---------- Rulings reference ---------- */
|
|
|
|
.tableHintActions {
|
|
margin: 0.35rem 0 0;
|
|
text-align: center;
|
|
}
|
|
|
|
.linkBtn {
|
|
background: none;
|
|
border: none;
|
|
padding: 0;
|
|
color: #d8b46a;
|
|
font-family: inherit;
|
|
font-size: 0.78rem;
|
|
text-decoration: underline;
|
|
text-underline-offset: 2px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
.linkBtn:hover {
|
|
color: #d8b46a;
|
|
}
|
|
}
|
|
|
|
#rulingsOverlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.72);
|
|
display: flex;
|
|
align-items: flex-end;
|
|
justify-content: center;
|
|
z-index: 97;
|
|
}
|
|
|
|
#rulingsOverlay[hidden] {
|
|
display: none;
|
|
}
|
|
|
|
#rulingsSheet {
|
|
width: 100%;
|
|
max-width: 460px;
|
|
max-height: 86vh;
|
|
overflow-y: auto;
|
|
background: linear-gradient(180deg, #242424, #1a1a1a);
|
|
border: 1px solid #3a3a3a;
|
|
border-bottom: none;
|
|
border-radius: 16px 16px 0 0;
|
|
padding: 0.35rem 1.1rem calc(1.2rem + env(safe-area-inset-bottom, 0px));
|
|
box-shadow: 0 -14px 40px rgba(0, 0, 0, 0.65);
|
|
}
|
|
|
|
.rulingsHeader {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.6rem;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.rulingsHeader h2 {
|
|
flex: 1;
|
|
margin: 0;
|
|
font-size: 1.05rem;
|
|
font-weight: 500;
|
|
color: #f5f5f5;
|
|
}
|
|
|
|
.rulingsIntro {
|
|
margin: 0 0 1rem;
|
|
color: #9a9a9a;
|
|
font-size: 0.8rem;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.ruling {
|
|
margin-bottom: 1rem;
|
|
padding-left: 0.7rem;
|
|
border-left: 2px solid rgba(216, 180, 106, 0.4);
|
|
}
|
|
|
|
.ruling h3 {
|
|
margin: 0 0 0.35rem;
|
|
color: #d8b46a;
|
|
font-size: 0.82rem;
|
|
font-weight: 600;
|
|
letter-spacing: 0.03em;
|
|
}
|
|
|
|
.ruling p,
|
|
.ruling li {
|
|
color: #c4c4c4;
|
|
font-size: 0.82rem;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.ruling p {
|
|
margin: 0;
|
|
}
|
|
|
|
.ruling ul {
|
|
margin: 0;
|
|
padding-left: 1.1rem;
|
|
}
|
|
|
|
.ruling li + li {
|
|
margin-top: 0.25rem;
|
|
}
|
|
|
|
.rulingsSource {
|
|
margin: 0;
|
|
padding-top: 0.6rem;
|
|
border-top: 1px solid #2c2c2c;
|
|
color: #8a8a8a;
|
|
font-size: 0.72rem;
|
|
line-height: 1.45;
|
|
}
|
|
|
|
/* ---------- Seat long-press menu ---------- */
|
|
|
|
/* Held down, before the menu opens — the only feedback that a press is being
|
|
counted rather than ignored. */
|
|
.seat.seatPressing {
|
|
transform: translate(-50%, -50%) scale(0.94);
|
|
}
|
|
|
|
#seatMenuOverlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.72);
|
|
display: flex;
|
|
align-items: flex-end;
|
|
justify-content: center;
|
|
z-index: 96;
|
|
}
|
|
|
|
#seatMenuOverlay[hidden] {
|
|
display: none;
|
|
}
|
|
|
|
#seatMenuSheet {
|
|
width: 100%;
|
|
max-width: 420px;
|
|
background: linear-gradient(180deg, #242424, #1a1a1a);
|
|
border: 1px solid #3a3a3a;
|
|
border-bottom: none;
|
|
border-radius: 16px 16px 0 0;
|
|
padding: 0.35rem 0.9rem calc(1rem + env(safe-area-inset-bottom, 0px));
|
|
box-shadow: 0 -14px 40px rgba(0, 0, 0, 0.65);
|
|
/* This sheet had no height limit and didn't scroll, so on a phone held sideways
|
|
its first two actions — Fold and All-in, the two that matter — sat below the
|
|
bottom of the screen with no way to reach them. A dealer in landscape could
|
|
long-press a seat and then not fold it. */
|
|
max-height: 85vh;
|
|
overflow-y: auto;
|
|
overscroll-behavior: contain;
|
|
}
|
|
|
|
.seatMenuHeader {
|
|
margin-bottom: 0.8rem;
|
|
}
|
|
|
|
.seatMenuHeader h2 {
|
|
margin: 0;
|
|
font-size: 1.05rem;
|
|
font-weight: 500;
|
|
color: #f5f5f5;
|
|
}
|
|
|
|
#seatMenuSubtitle {
|
|
color: #9a9a9a;
|
|
font-size: 0.8rem;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
#seatMenuActions {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.45rem;
|
|
margin-bottom: 0.6rem;
|
|
}
|
|
|
|
.seatMenuBtn {
|
|
width: 100%;
|
|
min-height: 52px;
|
|
border-radius: 11px;
|
|
border: 1px solid #3a3a3a;
|
|
background: linear-gradient(180deg, #2e2e2e, #232323);
|
|
color: #eee;
|
|
font-family: inherit;
|
|
font-size: 0.95rem;
|
|
cursor: pointer;
|
|
transition: all 0.12s ease;
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
.seatMenuBtn:hover {
|
|
border-color: #4a4a4a;
|
|
background: linear-gradient(180deg, #383838, #2a2a2a);
|
|
}
|
|
}
|
|
|
|
.seatMenuBtn.menuDanger {
|
|
background: linear-gradient(180deg, #3d1f1f, #2c1515);
|
|
border-color: #5b2626;
|
|
color: #ffd6d6;
|
|
}
|
|
|
|
.seatMenuBtn.menuAccent {
|
|
background: linear-gradient(180deg, #3a2f18, #241d0f);
|
|
border-color: #a9782f;
|
|
color: #f0d9a8;
|
|
}
|
|
|
|
#seatMenuCancelBtn {
|
|
width: 100%;
|
|
justify-content: center;
|
|
min-height: 48px;
|
|
}
|
|
|
|
/* ---------- Card picker ---------- */
|
|
|
|
#cardPickerOverlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.72);
|
|
display: flex;
|
|
align-items: flex-end;
|
|
justify-content: center;
|
|
z-index: 95;
|
|
}
|
|
|
|
#cardPickerOverlay[hidden] {
|
|
display: none;
|
|
}
|
|
|
|
#cardPickerSheet {
|
|
width: 100%;
|
|
max-width: 460px;
|
|
max-height: 88vh;
|
|
overflow-y: auto;
|
|
background: linear-gradient(180deg, #242424, #1a1a1a);
|
|
border: 1px solid #3a3a3a;
|
|
border-bottom: none;
|
|
border-radius: 16px 16px 0 0;
|
|
padding: 0.35rem 0.9rem calc(1rem + env(safe-area-inset-bottom, 0px));
|
|
box-shadow: 0 -14px 40px rgba(0, 0, 0, 0.65);
|
|
}
|
|
|
|
.cardPickerHeader {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
margin-bottom: 0.6rem;
|
|
}
|
|
|
|
.cardPickerHeader h2 {
|
|
flex: 1;
|
|
min-width: 0;
|
|
margin: 0;
|
|
font-size: 0.95rem;
|
|
font-weight: 500;
|
|
color: #f5f5f5;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.cardPickerHeader .holdemBtn {
|
|
min-height: 40px;
|
|
}
|
|
|
|
#cardPickerSlots {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
margin-bottom: 0.7rem;
|
|
}
|
|
|
|
.pickerSlot {
|
|
flex: 1;
|
|
min-height: 46px;
|
|
border-radius: 8px;
|
|
border: 1px solid #3a3a3a;
|
|
background: #191919;
|
|
color: #8a8a8a;
|
|
font-family: var(--font-mono, monospace);
|
|
font-size: 1.1rem;
|
|
font-weight: 700;
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* The slot the next tap will fill. */
|
|
.pickerSlot.active {
|
|
border-color: #d8b46a;
|
|
background: #241d0f;
|
|
color: #f0d9a8;
|
|
}
|
|
|
|
.pickerSlot.cardRed {
|
|
color: #e07b7b;
|
|
}
|
|
|
|
#cardPickerGrid {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
gap: 0.35rem;
|
|
}
|
|
|
|
.pickerCard {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.1rem;
|
|
min-height: 44px;
|
|
border-radius: 7px;
|
|
border: 1px solid #d5d5d5;
|
|
background: #fdfdfd;
|
|
color: #111;
|
|
font-family: var(--font-mono, monospace);
|
|
font-size: 1rem;
|
|
font-weight: 700;
|
|
cursor: pointer;
|
|
transition: transform 0.1s ease;
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
.pickerCard:hover:not(:disabled) {
|
|
transform: translateY(-1px);
|
|
}
|
|
}
|
|
|
|
.pickerCard.cardRed {
|
|
color: #c0392b;
|
|
}
|
|
|
|
.pickerCard.selected {
|
|
outline: 3px solid #d8b46a;
|
|
outline-offset: -1px;
|
|
}
|
|
|
|
/* Already dealt somewhere else at the table — visibly out of play rather than
|
|
silently accepting a duplicate. */
|
|
.pickerCard:disabled {
|
|
background: #1d1d1d;
|
|
border-color: #303030;
|
|
color: #4a4a4a;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.pickerSuit {
|
|
font-size: 1.05em;
|
|
line-height: 1;
|
|
}
|
|
|
|
.seatContribution {
|
|
border-top: 1px dashed #3a3a3a;
|
|
padding-top: 0.4rem;
|
|
margin-bottom: 0.3rem;
|
|
}
|
|
|
|
.chipCounts {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.35rem;
|
|
margin-bottom: 0.35rem;
|
|
}
|
|
|
|
.chipCountRow {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.2rem;
|
|
}
|
|
|
|
.chipSwatch {
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 50%;
|
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
display: inline-block;
|
|
}
|
|
|
|
.chipCountInput {
|
|
width: 34px;
|
|
background: #1a1a1a;
|
|
border: 1px solid #3a3a3a;
|
|
border-radius: 4px;
|
|
color: #eee;
|
|
font-size: 0.75rem;
|
|
padding: 0.1rem 0.2rem;
|
|
}
|
|
|
|
.amountOverrideRow {
|
|
justify-content: space-between;
|
|
font-size: 0.75rem;
|
|
color: #aaa;
|
|
margin-bottom: 0.3rem;
|
|
}
|
|
|
|
.amountOverrideInput {
|
|
width: 70px;
|
|
background: #1a1a1a;
|
|
border: 1px solid #3a3a3a;
|
|
border-radius: 4px;
|
|
color: #eee;
|
|
padding: 0.2rem 0.3rem;
|
|
}
|
|
|
|
.seatTotal {
|
|
font-size: 0.8rem;
|
|
color: #d6ffe0;
|
|
font-weight: 600;
|
|
text-align: right;
|
|
}
|
|
|
|
.customFields {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.25rem;
|
|
margin-bottom: 0.3rem;
|
|
}
|
|
|
|
.customFieldRow {
|
|
display: flex;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.customFieldLabel,
|
|
.customFieldValue {
|
|
min-width: 0;
|
|
flex: 1;
|
|
background: #1a1a1a;
|
|
border: 1px solid #3a3a3a;
|
|
border-radius: 4px;
|
|
color: #eee;
|
|
font-size: 0.7rem;
|
|
padding: 0.2rem 0.3rem;
|
|
}
|
|
|
|
.removeFieldBtn,
|
|
.removeChipColorBtn {
|
|
line-height: 1;
|
|
}
|
|
|
|
/* ---------- Sidebar ---------- */
|
|
|
|
#holdemSidebar {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.8rem;
|
|
/* Grid items default to min-width:auto, so anything with a wide min-content —
|
|
a side-by-side field row, say — would push this column past the viewport and
|
|
drag the table out with it. */
|
|
min-width: 0;
|
|
}
|
|
|
|
.sidebarPanel {
|
|
background: linear-gradient(160deg, #212121, #1a1a1a);
|
|
border: 1px solid #333;
|
|
border-radius: 10px;
|
|
padding: 0.2rem 0.9rem;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
.sidebarPanel summary {
|
|
cursor: pointer;
|
|
padding: 0.7rem 0;
|
|
font-size: 0.9rem;
|
|
color: #eee;
|
|
letter-spacing: 0.03em;
|
|
}
|
|
|
|
.panelBody {
|
|
padding-bottom: 0.9rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.7rem;
|
|
}
|
|
|
|
/* A heading inside a panel, so one panel can hold two related things — the
|
|
standings and the hand log are two views of the same session — without needing
|
|
two identical-looking accordions to say so. */
|
|
.panelSubhead {
|
|
margin: 0.2rem 0 0;
|
|
color: #d8b46a;
|
|
font-size: 0.68rem;
|
|
font-weight: 600;
|
|
letter-spacing: 0.09em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.panelSubhead + * {
|
|
margin-top: -0.2rem;
|
|
}
|
|
|
|
/* Nested disclosure for the things a dealer sets once and then forgets, kept out
|
|
of the way of the settings they actually touch. */
|
|
.subPanel {
|
|
border-top: 1px solid #2c2c2c;
|
|
padding-top: 0.5rem;
|
|
}
|
|
|
|
.subPanel summary {
|
|
cursor: pointer;
|
|
color: #9a9a9a;
|
|
font-size: 0.78rem;
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
.subPanel summary:hover {
|
|
color: #c5c5c5;
|
|
}
|
|
}
|
|
|
|
.subPanelBody {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
padding-top: 0.6rem;
|
|
}
|
|
|
|
.fieldRow {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 0.6rem;
|
|
font-size: 0.85rem;
|
|
color: #ccc;
|
|
}
|
|
|
|
.fieldRow input,
|
|
.fieldRow select {
|
|
background: #141414;
|
|
border: 1px solid #3a3a3a;
|
|
border-radius: 5px;
|
|
color: #eee;
|
|
padding: 0.35rem 0.5rem;
|
|
max-width: 140px;
|
|
}
|
|
|
|
.fieldHint {
|
|
font-size: 0.75rem;
|
|
color: #949494;
|
|
line-height: 1.4;
|
|
margin: 0;
|
|
}
|
|
|
|
#chipLegendList {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.4rem;
|
|
}
|
|
|
|
.chipLegendRow {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.chipColorInput {
|
|
width: 32px;
|
|
height: 28px;
|
|
background: none;
|
|
border: 1px solid #3a3a3a;
|
|
border-radius: 5px;
|
|
padding: 0;
|
|
}
|
|
|
|
.chipValueInput {
|
|
flex: 1;
|
|
background: #141414;
|
|
border: 1px solid #3a3a3a;
|
|
border-radius: 5px;
|
|
color: #eee;
|
|
padding: 0.3rem 0.5rem;
|
|
}
|
|
|
|
#standingsTable {
|
|
max-height: 260px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
/* The sidebar is about 290px wide, and four numeric columns at a 4.5rem minimum
|
|
plus gaps come to 320px — so the name column, being the only flexible one, was
|
|
collapsing to *zero* and player names disappeared entirely. Names were already
|
|
down to 50px with four columns; adding the cash-out column finished them off.
|
|
Measured at 360, 390 and 1280 alike, since the sidebar is narrow at all of them.
|
|
|
|
So the table earns its columns instead of assuming them. Narrow: the player and
|
|
the two numbers a banker settles up from. Wide enough: the per-hand detail as
|
|
well. Everything is in the CSV export either way. */
|
|
.standingsTableWrap {
|
|
container-type: inline-size;
|
|
}
|
|
|
|
.standingsRow {
|
|
display: grid;
|
|
grid-template-columns: minmax(4rem, 1fr) auto auto;
|
|
gap: 0.5rem;
|
|
align-items: baseline;
|
|
padding: 0.3rem 0.1rem;
|
|
font-size: 0.78rem;
|
|
border-bottom: 1px solid #232323;
|
|
}
|
|
|
|
.standingsRow > span:not(:first-child) {
|
|
text-align: right;
|
|
font-variant-numeric: tabular-nums;
|
|
min-width: 3.6rem;
|
|
}
|
|
|
|
.standingsRow > .standingsName {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* The per-hand columns are detail: how the night's swings happened. They're the
|
|
first thing to go when there isn't room, because Net and Cash out are what the
|
|
banker is actually reading. */
|
|
.standingsIn,
|
|
.standingsWon,
|
|
.standingsInHead,
|
|
.standingsWonHead {
|
|
display: none;
|
|
}
|
|
|
|
@container (min-width: 30rem) {
|
|
.standingsRow {
|
|
grid-template-columns: minmax(5rem, 1fr) auto auto auto auto;
|
|
}
|
|
|
|
.standingsIn,
|
|
.standingsWon,
|
|
.standingsInHead,
|
|
.standingsWonHead {
|
|
display: block;
|
|
}
|
|
}
|
|
|
|
/* The settle-up column is the one a banker reads while counting out cash, so it
|
|
carries the weight the per-hand columns don't. */
|
|
.standingsCashOut {
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* ---------- Bank reconciliation ----------
|
|
Cash taken versus chips on the table. Every chip is a promise to pay cash for it
|
|
later, and if the chips outrun the box that promise can't be kept — which a dealer
|
|
otherwise discovers when the last two players cash out and there's nothing left. */
|
|
.bankReconcile {
|
|
margin-top: 0.6rem;
|
|
padding: 0.55rem 0.6rem;
|
|
border: 1px solid #2f2f2f;
|
|
border-radius: 8px;
|
|
background: #151515;
|
|
}
|
|
|
|
.bankReconcileRow {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: 0.75rem;
|
|
font-size: 0.78rem;
|
|
color: #b8b8b8;
|
|
font-variant-numeric: tabular-nums;
|
|
padding: 0.12rem 0;
|
|
}
|
|
|
|
.bankReconcileVerdict {
|
|
margin: 0.4rem 0 0;
|
|
font-size: 0.74rem;
|
|
line-height: 1.35;
|
|
}
|
|
|
|
.bankReconcileVerdict.bankBalanced {
|
|
color: #7fc99a;
|
|
}
|
|
|
|
.bankReconcileVerdict.bankOff {
|
|
color: #f0b46a;
|
|
}
|
|
|
|
.standingsHeader {
|
|
color: #8a8a8a;
|
|
font-size: 0.7rem;
|
|
letter-spacing: 0.04em;
|
|
text-transform: uppercase;
|
|
border-bottom-color: #3a3a3a;
|
|
position: sticky;
|
|
top: 0;
|
|
background: #1b1b1b;
|
|
}
|
|
|
|
.standingsName {
|
|
color: #eee;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.standingsIn,
|
|
.standingsWon {
|
|
color: #999;
|
|
}
|
|
|
|
.standingsNet {
|
|
color: #bbb;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.standingsNet.netUp {
|
|
color: #4ec97a;
|
|
}
|
|
|
|
.standingsNet.netDown {
|
|
color: #e07b7b;
|
|
}
|
|
|
|
#handHistoryList {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.4rem;
|
|
max-height: 260px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.handHistoryRow {
|
|
background: #141414;
|
|
border: 1px solid #2c2c2c;
|
|
border-radius: 6px;
|
|
padding: 0.45rem 0.6rem;
|
|
}
|
|
|
|
.handHistoryRowHeader {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 0.5rem;
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
.handHistoryHandNumber {
|
|
color: #d8b46a;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.handHistoryPot {
|
|
color: #eee;
|
|
}
|
|
|
|
.handHistoryTime {
|
|
margin-left: auto;
|
|
color: #8a8a8a;
|
|
font-size: 0.72rem;
|
|
}
|
|
|
|
.handHistoryPayouts {
|
|
margin-top: 0.15rem;
|
|
color: #aaa;
|
|
font-size: 0.75rem;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
#tableNotes {
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
background: #141414;
|
|
border: 1px solid #3a3a3a;
|
|
border-radius: 6px;
|
|
color: #eee;
|
|
padding: 0.5rem;
|
|
resize: vertical;
|
|
font-family: inherit;
|
|
/* iOS zooms the whole page in when you focus a field under 16px, and the dealer
|
|
is then pinching back out to see the table. Every other input here is already
|
|
at 16px; this one was the straggler. */
|
|
font-size: 16px;
|
|
}
|
|
|
|
/* ---------- Pot wizard ---------- */
|
|
|
|
/* ---------- Action bar ---------- */
|
|
|
|
#actionBar {
|
|
position: fixed;
|
|
/* Centred with auto margins rather than 100vw + translate: on a phone with a
|
|
scrollbar, 100vw is wider than the viewport and the overflow drags the whole
|
|
table sideways. */
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
margin: 0 auto;
|
|
z-index: 55;
|
|
max-width: 720px;
|
|
background: linear-gradient(180deg, #232323, #191919);
|
|
border: 1px solid #383838;
|
|
border-bottom: none;
|
|
border-radius: 14px 14px 0 0;
|
|
padding: 0.6rem 0.75rem calc(0.75rem + env(safe-area-inset-bottom, 0px));
|
|
box-shadow: 0 -12px 34px rgba(0, 0, 0, 0.6);
|
|
}
|
|
|
|
#actionBar[hidden] {
|
|
display: none;
|
|
}
|
|
|
|
/* The bar is fixed over the page, so the sidebar needs room to scroll past it. */
|
|
body.hasActionBar #holdemSidebar {
|
|
padding-bottom: 9rem;
|
|
}
|
|
|
|
.actionStatus {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: baseline;
|
|
gap: 0.4rem 0.6rem;
|
|
margin-bottom: 0.55rem;
|
|
font-size: 0.78rem;
|
|
}
|
|
|
|
/* Always there while there's something to take back, rather than a toast per tap. */
|
|
.actionUndo {
|
|
flex: none;
|
|
min-height: 30px;
|
|
max-width: 100%;
|
|
padding: 0.15rem 0.6rem;
|
|
border-radius: 999px;
|
|
border: 1px solid #414141;
|
|
background: #262626;
|
|
color: #bdbdbd;
|
|
font-family: inherit;
|
|
font-size: 0.72rem;
|
|
cursor: pointer;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
.actionUndo:hover {
|
|
background: #333;
|
|
border-color: #4f4f4f;
|
|
color: #eee;
|
|
}
|
|
}
|
|
|
|
.actionStreet {
|
|
color: #d8b46a;
|
|
font-weight: 600;
|
|
letter-spacing: 0.06em;
|
|
text-transform: uppercase;
|
|
font-size: 0.7rem;
|
|
}
|
|
|
|
.actionPot {
|
|
color: #e9e9e9;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.actionPlayers {
|
|
color: #9c9c9c;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.actionRunout {
|
|
margin-left: auto;
|
|
color: #d8b46a;
|
|
font-size: 0.74rem;
|
|
}
|
|
|
|
.actionWho {
|
|
margin-left: auto;
|
|
color: #9c9c9c;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* Stopwatch, not a shot clock: it colours up as a prompt to the dealer, and does
|
|
nothing else. */
|
|
.actionTank {
|
|
flex: none;
|
|
min-width: 3.1rem;
|
|
text-align: right;
|
|
color: #949494;
|
|
font-variant-numeric: tabular-nums;
|
|
transition: color 0.3s ease;
|
|
}
|
|
|
|
.actionTank.tankNudge {
|
|
color: #d8b46a;
|
|
}
|
|
|
|
.actionTank.tankSlow {
|
|
color: #e07b7b;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* ---------- Side-pot breakdown ---------- */
|
|
|
|
.potBreakdown {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.2rem;
|
|
margin-bottom: 0.55rem;
|
|
padding: 0.4rem 0.55rem;
|
|
background: rgba(216, 180, 106, 0.07);
|
|
border: 1px solid rgba(216, 180, 106, 0.25);
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.potBreakdownHeading {
|
|
margin: 0 0 0.35rem;
|
|
color: #d8b46a;
|
|
font-size: 0.68rem;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.potLayer {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 0.5rem;
|
|
font-size: 0.76rem;
|
|
}
|
|
|
|
.potLayerLabel {
|
|
color: #d8b46a;
|
|
font-weight: 600;
|
|
flex: none;
|
|
}
|
|
|
|
.potLayerAmount {
|
|
color: #f2f2f2;
|
|
font-variant-numeric: tabular-nums;
|
|
font-weight: 600;
|
|
flex: none;
|
|
}
|
|
|
|
.potLayerWho {
|
|
color: #9a9a9a;
|
|
flex: 1;
|
|
min-width: 0;
|
|
text-align: right;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.potBreakdownWizard {
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.potLayerReturn .potLayerLabel,
|
|
.potLayerReturn .potLayerAmount {
|
|
color: #9a9a9a;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.actionButtons {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.actionBtn {
|
|
position: relative;
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.1rem;
|
|
min-height: 56px;
|
|
padding: 0.5rem 0.6rem;
|
|
border-radius: 11px;
|
|
border: 1px solid #3a3a3a;
|
|
background: linear-gradient(180deg, #2e2e2e, #232323);
|
|
color: #eee;
|
|
font-family: inherit;
|
|
font-size: 0.95rem;
|
|
cursor: pointer;
|
|
transition: all 0.12s ease;
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
.actionBtn:hover {
|
|
border-color: #4a4a4a;
|
|
background: linear-gradient(180deg, #383838, #2a2a2a);
|
|
}
|
|
}
|
|
|
|
.actionBtn:active {
|
|
transform: translateY(1px);
|
|
}
|
|
|
|
.actionBtnLabel {
|
|
font-weight: 600;
|
|
}
|
|
|
|
.actionBtnSub {
|
|
font-size: 0.72rem;
|
|
color: #a8a8a8;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.actionKey {
|
|
position: absolute;
|
|
top: 5px;
|
|
right: 7px;
|
|
padding: 0 0.25rem;
|
|
border-radius: 4px;
|
|
border: 1px solid currentColor;
|
|
opacity: 0.4;
|
|
font-family: inherit;
|
|
font-size: 0.6rem;
|
|
line-height: 1.4;
|
|
text-transform: uppercase;
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* No keyboard, no key badges — on a phone they'd just be clutter on the buttons
|
|
the dealer is already tapping. */
|
|
@media (hover: none) {
|
|
.actionKey {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
.actionFold {
|
|
background: linear-gradient(180deg, #3d1f1f, #2c1515);
|
|
border-color: #5b2626;
|
|
color: #ffd6d6;
|
|
}
|
|
|
|
.actionCheck,
|
|
.actionCall {
|
|
background: linear-gradient(180deg, #16783a, #0d5225);
|
|
border-color: #1e8449;
|
|
color: #ecffef;
|
|
}
|
|
|
|
.actionCheck .actionBtnSub,
|
|
.actionCall .actionBtnSub {
|
|
color: #bfe8cb;
|
|
}
|
|
|
|
.actionRaise {
|
|
background: linear-gradient(180deg, #3a2f18, #241d0f);
|
|
border-color: #a9782f;
|
|
color: #f0d9a8;
|
|
}
|
|
|
|
.actionAdvance {
|
|
background: linear-gradient(180deg, #2b3f57, #1b2a3a);
|
|
border-color: #3d5f83;
|
|
color: #dceaf8;
|
|
}
|
|
|
|
/* ---------- Raise pad ---------- */
|
|
|
|
.raiseQuick {
|
|
display: flex;
|
|
gap: 0.4rem;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.raiseQuickBtn {
|
|
min-height: 48px;
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
.raiseQuickBtn.selected {
|
|
border-color: #d8b46a;
|
|
background: linear-gradient(180deg, #3a2f18, #241d0f);
|
|
color: #f0d9a8;
|
|
}
|
|
|
|
.raiseEntry {
|
|
display: flex;
|
|
gap: 0.4rem;
|
|
}
|
|
|
|
.raiseInput {
|
|
flex: 1;
|
|
min-width: 0;
|
|
background: #141414;
|
|
border: 1px solid #3a3a3a;
|
|
border-radius: 11px;
|
|
color: #f2f2f2;
|
|
font-family: inherit;
|
|
font-size: 16px;
|
|
font-variant-numeric: tabular-nums;
|
|
text-align: center;
|
|
padding: 0.5rem;
|
|
}
|
|
|
|
.raiseInput:focus {
|
|
outline: none;
|
|
border-color: #d8b46a;
|
|
}
|
|
|
|
.raiseCancel {
|
|
flex: 0 0 auto;
|
|
min-width: 84px;
|
|
}
|
|
|
|
.raiseConfirm {
|
|
flex: 0 0 auto;
|
|
min-width: 116px;
|
|
background: linear-gradient(180deg, #3a2f18, #241d0f);
|
|
border-color: #a9782f;
|
|
color: #f0d9a8;
|
|
}
|
|
|
|
/* Whose turn it is, shown on the table itself as well as in the bar.
|
|
|
|
Dealers say the biggest single time sink at a home table isn't how fast the cards
|
|
come out — it's players not noticing the action is on them. So this is the one
|
|
state that has to be unmistakable from across a table at a glance, and it was
|
|
distinguished only by a rim colour: a gold border and a 2px ring where a live seat
|
|
has a green border and a 1px one. Same fill, same size, nothing moving.
|
|
|
|
It now reads as lit rather than merely outlined: a solid ring at full strength, a
|
|
warm glow that separates it from the felt, and a lift of its own. The seat's
|
|
`transform` positions it around the table, so the lift is box-shadow rather than
|
|
translate — setting transform here would throw the seat off its spot. */
|
|
.seat.seatActing {
|
|
border-color: #f0cd85;
|
|
background: linear-gradient(160deg, #3a3222, #241f16);
|
|
box-shadow:
|
|
0 0 0 3px rgba(216, 180, 106, 0.9),
|
|
0 0 18px 2px rgba(216, 180, 106, 0.35),
|
|
0 6px 20px rgba(0, 0, 0, 0.6);
|
|
z-index: 4;
|
|
}
|
|
|
|
/* A slow breath, so a dealer glancing up finds it without hunting. Deliberately
|
|
gentle and deliberately optional — the ring above carries the whole job on its own
|
|
for anyone who'd rather nothing moved. */
|
|
@media (prefers-reduced-motion: no-preference) {
|
|
.seat.seatActing {
|
|
animation: seatActingPulse 2.4s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes seatActingPulse {
|
|
0%, 100% {
|
|
box-shadow:
|
|
0 0 0 3px rgba(216, 180, 106, 0.9),
|
|
0 0 18px 2px rgba(216, 180, 106, 0.35),
|
|
0 6px 20px rgba(0, 0, 0, 0.6);
|
|
}
|
|
50% {
|
|
box-shadow:
|
|
0 0 0 3px rgba(216, 180, 106, 0.65),
|
|
0 0 26px 5px rgba(216, 180, 106, 0.5),
|
|
0 6px 20px rgba(0, 0, 0, 0.6);
|
|
}
|
|
}
|
|
}
|
|
|
|
.seatPuckStack {
|
|
font-size: 0.58rem;
|
|
color: #9a9a9a;
|
|
font-variant-numeric: tabular-nums;
|
|
margin-top: 0.1rem;
|
|
}
|
|
|
|
/* Tournament-sized amounts. A number has no spaces to break on, so an amount too
|
|
wide for the 64px puck neither wraps nor ellipsises — it draws straight out over
|
|
the seat next to it. These step the size down so every digit stays on its own
|
|
puck. Shrinking rather than abbreviating is deliberate: "$1.25M" would fit more
|
|
comfortably, but a dealer reading a rounded stack off the felt is a worse failure
|
|
than a small one, and the exact figure is what the tool is for. */
|
|
.seatPuckStack.puckAmountLong,
|
|
.seatPuckTotal.puckAmountLong {
|
|
font-size: 0.5rem;
|
|
letter-spacing: -0.01em;
|
|
}
|
|
|
|
.seatPuckStack.puckAmountHuge,
|
|
.seatPuckTotal.puckAmountHuge {
|
|
font-size: 0.44rem;
|
|
letter-spacing: -0.02em;
|
|
}
|
|
|
|
.blindsRow {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.6rem;
|
|
}
|
|
|
|
.blindsRow .fieldRow {
|
|
flex: 1 1 120px;
|
|
min-width: 0;
|
|
gap: 0.4rem;
|
|
}
|
|
|
|
/* "Small blind" broke onto two lines beside its input, which made the pair look
|
|
like a mistake rather than a pair. */
|
|
.blindsRow .fieldRow > span {
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.blindsRow input {
|
|
min-width: 0;
|
|
width: 3.4rem;
|
|
}
|
|
|
|
/* ---------- Toasts ---------- */
|
|
|
|
#toastHost {
|
|
position: fixed;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 1.25rem;
|
|
margin: 0 auto;
|
|
z-index: 80;
|
|
max-width: 560px;
|
|
padding: 0 0.75rem;
|
|
box-sizing: border-box;
|
|
pointer-events: none;
|
|
transition: bottom 0.18s ease;
|
|
}
|
|
|
|
/* Sit above the action bar rather than on top of it, so an Undo never covers the
|
|
Fold button the dealer is reaching for. */
|
|
/* Sit the toast directly on top of the action bar, whatever height it happens to be.
|
|
This was a hardcoded 9rem, which is roughly right for the bar at rest and wrong the
|
|
moment the raise pad opens — and on a short phone it put the toast, Undo button and
|
|
all, straight over the bottom seats of the table, so a tap meant for a player hit
|
|
"Undo" instead. */
|
|
body.hasActionBar #toastHost {
|
|
bottom: calc(var(--actionBarHeight, 9rem) + 0.75rem + env(safe-area-inset-bottom, 0px));
|
|
}
|
|
|
|
.toast {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
background: linear-gradient(180deg, #2b2b2b, #202020);
|
|
border: 1px solid #414141;
|
|
border-left: 3px solid #d8b46a;
|
|
border-radius: 10px;
|
|
padding: 0.7rem 0.8rem 0.7rem 0.9rem;
|
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.55);
|
|
pointer-events: auto;
|
|
opacity: 0;
|
|
transform: translateY(12px);
|
|
transition: opacity 0.18s ease, transform 0.18s ease;
|
|
}
|
|
|
|
.toast.toastVisible {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.toastText {
|
|
flex: 1;
|
|
min-width: 0;
|
|
color: #e9e9e9;
|
|
font-size: 0.85rem;
|
|
line-height: 1.35;
|
|
}
|
|
|
|
.toastAction {
|
|
flex: none;
|
|
min-height: 38px;
|
|
padding: 0.35rem 0.9rem;
|
|
border-radius: 8px;
|
|
border: 1px solid #d8b46a;
|
|
background: rgba(216, 180, 106, 0.12);
|
|
color: #f0d9a8;
|
|
font-size: 0.85rem;
|
|
font-weight: 600;
|
|
font-family: inherit;
|
|
cursor: pointer;
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
.toastAction:hover {
|
|
background: rgba(216, 180, 106, 0.22);
|
|
}
|
|
}
|
|
|
|
.toastClose {
|
|
flex: none;
|
|
width: 30px;
|
|
height: 30px;
|
|
border-radius: 50%;
|
|
border: none;
|
|
background: transparent;
|
|
color: #949494;
|
|
font-size: 0.8rem;
|
|
font-family: inherit;
|
|
cursor: pointer;
|
|
/* Same treatment as the dealer puck: a 30px dismiss button in a toast a dealer is
|
|
swatting away mid-hand, given a fingertip's worth of hit area without becoming
|
|
a bigger piece of furniture. */
|
|
position: relative;
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
.toastClose:hover {
|
|
background: #333;
|
|
color: #ddd;
|
|
}
|
|
}
|
|
|
|
/* ---------- Confirmation dialog ---------- */
|
|
|
|
#confirmOverlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.72);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 90;
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
#confirmOverlay[hidden] {
|
|
display: none;
|
|
}
|
|
|
|
#confirmModal {
|
|
background: #1e1e1e;
|
|
border: 1px solid #3a3a3a;
|
|
border-radius: 12px;
|
|
width: 100%;
|
|
max-width: 420px;
|
|
padding: 1.3rem 1.4rem 1.2rem;
|
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
|
|
outline: none;
|
|
}
|
|
|
|
#confirmTitle {
|
|
margin: 0 0 0.5rem;
|
|
font-size: 1.05rem;
|
|
font-weight: 500;
|
|
color: #f5f5f5;
|
|
}
|
|
|
|
#confirmMessage {
|
|
margin: 0 0 1.2rem;
|
|
color: #a5a5a5;
|
|
font-size: 0.85rem;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.confirmActions {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 0.6rem;
|
|
}
|
|
|
|
.confirmActions .holdemBtn {
|
|
min-height: 44px;
|
|
}
|
|
|
|
#potWizardOverlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
/* Every other sheet in here sits at 90-98; this one was at 50, underneath both
|
|
the action bar (55) and the toast (80). So the bar drew on top of the dialog
|
|
that pays people — in landscape one of its inputs was completely covered by
|
|
the Fold button — and a toast could sit over "Confirm Winner(s)". It's the odd
|
|
one out rather than a deliberate choice, and it joins the others. */
|
|
z-index: 95;
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
#potWizardOverlay[hidden] {
|
|
display: none;
|
|
}
|
|
|
|
#potWizardModal {
|
|
background: #1e1e1e;
|
|
border: 1px solid #3a3a3a;
|
|
border-radius: 10px;
|
|
width: 100%;
|
|
max-width: 480px;
|
|
/* 85vh is most of a 390px-tall landscape window, and the action bar is fixed over
|
|
the bottom of it — so the last of the wizard sat underneath the bar and its
|
|
controls couldn't be tapped. Leaving the bar's own height out of the budget
|
|
keeps the whole dialog above it. */
|
|
max-height: calc(85vh - var(--actionBarHeight, 0px));
|
|
overflow-y: auto;
|
|
overscroll-behavior: contain;
|
|
padding: 1.2rem 1.4rem 1.6rem;
|
|
}
|
|
|
|
.potWizardHeader {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 0.8rem;
|
|
}
|
|
|
|
.potWizardHeader h2 {
|
|
margin: 0;
|
|
font-size: 1.2rem;
|
|
font-weight: 400;
|
|
color: #f5f5f5;
|
|
}
|
|
|
|
.potWizardLog {
|
|
margin-bottom: 0.8rem;
|
|
}
|
|
|
|
.potWizardLogLine {
|
|
font-size: 0.8rem;
|
|
color: #999;
|
|
margin: 0.2rem 0;
|
|
}
|
|
|
|
.potWizardPrompt {
|
|
font-size: 0.95rem;
|
|
color: #eee;
|
|
margin-bottom: 0.8rem;
|
|
}
|
|
|
|
.potWizardChoices {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.4rem;
|
|
margin-bottom: 0.6rem;
|
|
}
|
|
|
|
/* This is the screen where the money gets decided, so it holds the same 44px+
|
|
targets as the rest of the tool rather than the smallest ones in it. Picking the
|
|
wrong winner is the one mistake here that costs somebody real money. */
|
|
.potWizardChoice {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.7rem;
|
|
min-height: 56px;
|
|
background: #141414;
|
|
border: 1px solid #333;
|
|
border-radius: 10px;
|
|
padding: 0.6rem 0.8rem;
|
|
cursor: pointer;
|
|
font-size: 0.95rem;
|
|
transition: border-color 0.12s ease, background 0.12s ease;
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
.potWizardChoice:hover {
|
|
border-color: #555;
|
|
background: #191919;
|
|
}
|
|
}
|
|
|
|
.potWizardChoice input {
|
|
flex: none;
|
|
width: 22px;
|
|
height: 22px;
|
|
accent-color: #1e8449;
|
|
}
|
|
|
|
/* Selected has to be obvious at a glance — a dealer confirms this with players
|
|
watching, and a checkbox alone is easy to misread across a table. */
|
|
.potWizardChoice.chosen {
|
|
border-color: #1e8449;
|
|
background: linear-gradient(180deg, #16301f, #12241a);
|
|
}
|
|
|
|
.potWizardChoice.chosen .potWizardChoiceName {
|
|
color: #d6ffe0;
|
|
}
|
|
|
|
.potWizardChoiceText {
|
|
flex: 1;
|
|
min-width: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.1rem;
|
|
}
|
|
|
|
.potWizardChoiceName {
|
|
color: #f0f0f0;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.potWizardChoiceUpTo {
|
|
color: #9a9a9a;
|
|
font-size: 0.76rem;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.potWizardChoiceCards {
|
|
flex: none;
|
|
font-family: var(--font-mono, monospace);
|
|
font-size: 1rem;
|
|
font-weight: 700;
|
|
color: #ddd;
|
|
}
|
|
|
|
.potWizardChoiceCards .cardRed {
|
|
color: #ff6b6b;
|
|
}
|
|
|
|
.potWizardSummary ul {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0 0 1rem;
|
|
}
|
|
|
|
.potWizardSummary li {
|
|
padding: 0.4rem 0;
|
|
border-bottom: 1px dashed #333;
|
|
font-size: 0.9rem;
|
|
color: #d6ffe0;
|
|
}
|
|
|
|
.potWizardActions {
|
|
display: flex;
|
|
gap: 0.6rem;
|
|
}
|
|
|
|
/* Every button in this modal is one a dealer presses with players waiting. */
|
|
#potWizardBody .holdemBtn {
|
|
min-height: 48px;
|
|
justify-content: center;
|
|
}
|
|
|
|
/* The confirm is the whole point of the step, so it gets the whole width rather
|
|
than being sized to the length of its own label. */
|
|
.potWizardRound > .holdemBtn {
|
|
width: 100%;
|
|
}
|
|
|
|
.potWizardActions .holdemBtn {
|
|
flex: 1;
|
|
}
|
|
|
|
.potWizardSummary li {
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
@media (max-width: 700px) {
|
|
/* 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;
|
|
}
|
|
|
|
#pokerTableWrap {
|
|
padding-bottom: 100%;
|
|
}
|
|
|
|
.seat {
|
|
width: 78px;
|
|
min-height: 44px;
|
|
}
|
|
}
|
|
|
|
/* ---------- Phones ----------
|
|
Two things break on a narrow screen: seat pucks ring the table on a radius
|
|
measured from the wrap, so the left- and right-most ones hang off the edge of
|
|
the viewport; and the seat editor lives far enough down the page that tapping a
|
|
seat looks like it did nothing. Inset the ring by half a puck, and float the
|
|
editor up as a bottom sheet over the table. */
|
|
@media (max-width: 700px) {
|
|
/* The bottom padding has to clear the fixed action bar, plus room for a toast to
|
|
sit above it. Without it the table just carries on underneath the bar and the
|
|
lower seats of the oval are behind the Fold button — measured at 360x640, where
|
|
62px of felt was covered. `--actionBarHeight` is published by actionBar.js and
|
|
is 0 when no hand is running, so the page doesn't hold a gap it doesn't need. */
|
|
#holdemApp {
|
|
padding: 1rem 0.75rem calc(var(--actionBarHeight, 0px) + 5rem + env(safe-area-inset-bottom, 0px));
|
|
}
|
|
|
|
#holdemHeader {
|
|
margin-bottom: 1rem;
|
|
gap: 0.6rem;
|
|
}
|
|
|
|
#holdemHeader h1 {
|
|
font-size: 1.4rem;
|
|
}
|
|
|
|
.holdemSubtitle {
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
#holdemHeaderActions {
|
|
width: 100%;
|
|
gap: 0.45rem;
|
|
}
|
|
|
|
#holdemHeaderActions .holdemBtn {
|
|
flex: 1 1 auto;
|
|
justify-content: center;
|
|
min-height: 46px;
|
|
padding: 0.5rem 0.7rem;
|
|
font-size: 0.82rem;
|
|
}
|
|
|
|
/* Keeps every puck inside the viewport: the ring is drawn within this box,
|
|
so half a puck of padding is exactly enough to stop the overhang. */
|
|
#seatsLayer,
|
|
#betsLayer {
|
|
inset: 26px 44px;
|
|
}
|
|
|
|
/* Narrower than the desktop puck: at a full ten-handed table the ring is
|
|
flattest at the top and bottom, and wide pucks are what made neighbours
|
|
overlap there. The puck is only a summary — the real form is the sheet. */
|
|
.seat {
|
|
width: 64px;
|
|
min-height: 42px;
|
|
padding: 0.25rem 0.3rem;
|
|
}
|
|
|
|
#tableWatermark {
|
|
font-size: 1.4rem;
|
|
}
|
|
|
|
#potBadge {
|
|
padding: 0.5rem 1.1rem;
|
|
}
|
|
|
|
#potBadgeAmount {
|
|
font-size: 1.2rem;
|
|
}
|
|
|
|
.tableHint {
|
|
margin-top: 0.75rem;
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
/* ---- Seat editor as a bottom sheet ---- */
|
|
|
|
#seatEditorPanel {
|
|
position: fixed;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
z-index: 60;
|
|
margin: 0;
|
|
max-height: 74vh;
|
|
overflow-y: auto;
|
|
border-radius: 16px 16px 0 0;
|
|
border-bottom: none;
|
|
padding: 0.35rem 1rem 1.25rem;
|
|
background: linear-gradient(180deg, #242424, #1a1a1a);
|
|
box-shadow: 0 -14px 40px rgba(0, 0, 0, 0.65);
|
|
transform: translateY(105%);
|
|
transition: transform 0.25s ease;
|
|
-webkit-overflow-scrolling: touch;
|
|
}
|
|
|
|
body.seatSheetOpen #seatEditorPanel {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.seatSheetGrip {
|
|
display: block;
|
|
width: 38px;
|
|
height: 4px;
|
|
margin: 0.35rem auto 0.5rem;
|
|
border-radius: 999px;
|
|
background: #4a4a4a;
|
|
}
|
|
|
|
.seatSheetClose {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
flex: none;
|
|
min-height: 44px;
|
|
padding: 0.35rem 1.1rem;
|
|
border-radius: 8px;
|
|
border: 1px solid #3a3a3a;
|
|
background: linear-gradient(180deg, #2e2e2e, #232323);
|
|
color: #ddd;
|
|
font-size: 0.8rem;
|
|
font-family: inherit;
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* The sheet covers the bottom of the page while it's open — give the sidebar
|
|
room so nothing is stranded underneath it. */
|
|
body.seatSheetOpen #holdemSidebar {
|
|
padding-bottom: 40vh;
|
|
}
|
|
|
|
/* The seat form was sized for a 320px desktop sidebar. In the sheet it has a
|
|
whole phone width to work with, so everything grows to thumb size — and
|
|
every field goes to 16px, below which iOS zooms the page on focus and the
|
|
dealer has to pinch back out mid-hand. */
|
|
/* Every text or number field in the form, rather than a list of the ones that
|
|
existed when this was written. The stack field was added later and sat at 28px
|
|
next to its 44px neighbours precisely because it wasn't on that list. */
|
|
.seatEditorForm input:not([type="checkbox"]):not([type="color"]) {
|
|
font-size: 16px;
|
|
min-height: 44px;
|
|
padding: 0.4rem 0.6rem;
|
|
}
|
|
|
|
.seatEditorForm .chipCountInput {
|
|
width: 56px;
|
|
text-align: center;
|
|
}
|
|
|
|
.seatEditorForm .chipCounts {
|
|
gap: 0.6rem;
|
|
}
|
|
|
|
.seatEditorForm .chipSwatch {
|
|
width: 16px;
|
|
height: 16px;
|
|
}
|
|
|
|
.seatEditorForm .amountOverrideInput {
|
|
width: 110px;
|
|
}
|
|
|
|
.seatEditorForm .cardSlot {
|
|
font-size: 1.5rem;
|
|
min-height: 62px;
|
|
}
|
|
|
|
/* Thirteen rows of cards need to stay reachable without the sheet running off
|
|
the bottom of a phone. */
|
|
.pickerCard {
|
|
min-height: 46px;
|
|
font-size: 1.05rem;
|
|
}
|
|
|
|
.seatEditorForm .seatCardToggles {
|
|
gap: 1rem;
|
|
}
|
|
|
|
.seatEditorForm .inHandToggle,
|
|
.seatEditorForm .foldToggle {
|
|
min-height: 44px;
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.seatEditorForm .inHandToggle input,
|
|
.seatEditorForm .foldToggle input {
|
|
width: 22px;
|
|
height: 22px;
|
|
}
|
|
|
|
.seatEditorForm .amountOverrideRow,
|
|
.seatEditorForm .seatTotal {
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.seatEditorForm .addFieldBtn {
|
|
width: 100%;
|
|
justify-content: center;
|
|
min-height: 44px;
|
|
margin-top: 0.4rem;
|
|
}
|
|
|
|
/* The sidebar's own controls never got the touch pass the seat form did — these
|
|
are settings a dealer changes at the table, on the same phone. Written against
|
|
the panel rather than each field, so a setting added later inherits it. */
|
|
.panelBody input:not([type="checkbox"]):not([type="color"]),
|
|
.subPanelBody input:not([type="checkbox"]):not([type="color"]),
|
|
.panelBody select {
|
|
font-size: 16px;
|
|
min-height: 44px;
|
|
padding: 0.4rem 0.6rem;
|
|
}
|
|
|
|
.panelBody .holdemBtnSmall,
|
|
.handHistoryActions .holdemBtn {
|
|
min-height: 44px;
|
|
}
|
|
|
|
/* Destructive and tiny is the worst combination: these delete a chip colour or a
|
|
custom field, and were 28px wide. */
|
|
.removeChipColorBtn,
|
|
.removeFieldBtn {
|
|
min-width: 44px;
|
|
min-height: 44px;
|
|
}
|
|
|
|
/* flex-shrink would take a stated width straight back off again in a row this
|
|
narrow, which is why it stayed 32px wide with the height applied. */
|
|
.chipColorInput {
|
|
flex: none;
|
|
width: 44px;
|
|
height: 44px;
|
|
}
|
|
|
|
/* A bare checkbox can't grow into a target, but the row it sits in can — the
|
|
label is what a tap actually lands on. */
|
|
.panelBody .fieldRow {
|
|
min-height: 44px;
|
|
}
|
|
|
|
/* A bare checkbox with no label box around it to grow. */
|
|
#dedicatedDealerToggle {
|
|
width: 24px;
|
|
height: 24px;
|
|
}
|
|
|
|
/* An underlined word is a 14px-tall target; give it room without making it look
|
|
like a button. */
|
|
.linkBtn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
min-height: 44px;
|
|
padding: 0 0.4rem;
|
|
}
|
|
|
|
/* The dealer button has to stay a 22px disc — it's a chip sitting on a seat, and
|
|
drawing it thumb-sized would swallow the puck. So the disc keeps its size and
|
|
an invisible pad around it takes the tap. It reaches outward, away from the
|
|
puck's own tap target, so pressing the seat still opens the seat. */
|
|
.dealerChip::after {
|
|
content: "";
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
width: 44px;
|
|
height: 44px;
|
|
transform: translate(-50%, -50%);
|
|
}
|
|
|
|
.dealerChip {
|
|
overflow: visible;
|
|
}
|
|
|
|
.toastAction {
|
|
min-height: 44px;
|
|
}
|
|
|
|
.confirmActions {
|
|
flex-direction: column-reverse;
|
|
}
|
|
|
|
.confirmActions .holdemBtn {
|
|
width: 100%;
|
|
justify-content: center;
|
|
}
|
|
}
|
|
|
|
/* ---------- A phone on its side ----------
|
|
Deliberately last in the file. The felt is an aspect-ratio box whose height comes
|
|
from `padding-bottom`, and the `max-width: 900px` block above re-sets that to 100%
|
|
— so a landscape rule placed any earlier is overridden at equal specificity and
|
|
does nothing, which is exactly what happened on the first attempt.
|
|
|
|
Landscape is where this layout falls apart worst: sized from width, the table came
|
|
out 765px tall in a 390px-tall window, putting six of the nine seats below the fold
|
|
and the rest behind the action bar. Sizing it from the height that's actually left
|
|
makes every seat reachable without scrolling. The table ends up small, but a seat
|
|
you have to go hunting for is worse than a seat that's small. */
|
|
@media (orientation: landscape) and (max-height: 560px) {
|
|
/* Set outright rather than via padding-bottom: a percentage padding resolves
|
|
against the containing block's width, not the element's own, so narrowing the
|
|
wrap on its own left it full height regardless. */
|
|
#pokerTableWrap {
|
|
/* A wide ellipse, which is what a real poker table is.
|
|
|
|
Capping the width by the height to hold the portrait aspect ratio was
|
|
throwing away most of the screen: at 640x360 the felt came out 135px wide
|
|
inside a 601px column — 22% of the width — leaving 366px of rim for nine
|
|
seats that need 662px. Adjacent seat centres ended up 13px apart, which is
|
|
why a seat's centre belonged to its neighbour.
|
|
|
|
Landscape has width and no height, so the table takes the width it has and
|
|
is only as tall as the room left over. */
|
|
--feltRoom: calc(100vh - var(--actionBarHeight, 0px) - 7rem);
|
|
width: 100%;
|
|
height: var(--feltRoom);
|
|
min-height: 7rem;
|
|
padding-bottom: 0;
|
|
}
|
|
|
|
/* The header ate 148px of a 390px-tall window, which was most of the reason the
|
|
table started below the fold. Here it only needs to be a title bar. */
|
|
#holdemHeader {
|
|
margin-bottom: 0.4rem;
|
|
gap: 0.35rem;
|
|
}
|
|
|
|
#holdemHeader h1 {
|
|
font-size: 1.05rem;
|
|
padding-bottom: 0.15rem;
|
|
}
|
|
|
|
.holdemSubtitle {
|
|
display: none;
|
|
}
|
|
|
|
/* There's no vertical room left, but plenty either side of a table this narrow,
|
|
so the toast moves out of the middle and sits beside the felt instead of on
|
|
top of it. Centred, it was covering up to six seats at once — and it carries
|
|
an Undo button, so a tap meant for a player was landing on "Undo". */
|
|
body.hasActionBar #toastHost,
|
|
#toastHost {
|
|
left: auto;
|
|
right: 0.5rem;
|
|
max-width: 17rem;
|
|
bottom: calc(var(--actionBarHeight, 0px) + 0.5rem + env(safe-area-inset-bottom, 0px));
|
|
}
|
|
|
|
/* Nine seats around a table this small overlap each other, so they shrink to suit. */
|
|
.seat {
|
|
width: 62px;
|
|
min-height: 40px;
|
|
}
|
|
|
|
}
|
|
|
|
/* ---------- Short phones, upright ----------
|
|
A 640px-tall screen spends ~150px on the header before the felt even starts, which
|
|
left the bottom of the table sitting behind the action bar until you scrolled. The
|
|
header carries no information a dealer needs mid-hand, so it gives the space up. */
|
|
@media (max-height: 700px) {
|
|
/* Nowhere on a screen this short is the toast clear of both the table and the
|
|
action bar, so it overlaps the one that costs least: it sits over the bar's
|
|
status line, which is information a dealer can re-read a second later, rather
|
|
than over the seats and buttons, which are things they need to hit. Overlapping
|
|
a readout beats covering a control. */
|
|
body.hasActionBar #toastHost {
|
|
bottom: calc(var(--actionBarHeight, 0px) - 3.1rem);
|
|
}
|
|
|
|
.toast {
|
|
padding: 0.5rem 0.7rem;
|
|
font-size: 0.78rem;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 700px) and (max-height: 700px) {
|
|
#holdemHeader {
|
|
margin-bottom: 0.5rem;
|
|
gap: 0.4rem;
|
|
}
|
|
|
|
#holdemHeader h1 {
|
|
font-size: 1.15rem;
|
|
padding-bottom: 0.2rem;
|
|
}
|
|
|
|
.holdemSubtitle {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
/* ---------- Keyboard focus ----------
|
|
Three focus rules existed for the whole tool, so tabbing through it relied on
|
|
whatever ring the browser draws by default — which on these dark, custom-painted
|
|
controls is close to invisible. `:focus-visible` rather than `:focus` keeps it to
|
|
keyboard use: a dealer tapping Fold shouldn't be left with a ring on it. */
|
|
.holdemBtn:focus-visible,
|
|
.linkBtn:focus-visible,
|
|
.seatMenuBtn:focus-visible,
|
|
.seatSheetClose:focus-visible,
|
|
.pickerCard:focus-visible,
|
|
.actionBtn:focus-visible,
|
|
.raiseQuickBtn:focus-visible,
|
|
.actionUndo:focus-visible,
|
|
.toastAction:focus-visible,
|
|
.toastClose:focus-visible,
|
|
.potWizardChoice:focus-visible,
|
|
.cardSlot:focus-visible,
|
|
.subPanel summary:focus-visible,
|
|
.seat:focus-visible,
|
|
button:focus-visible,
|
|
[role="button"]:focus-visible,
|
|
input:focus-visible,
|
|
select:focus-visible,
|
|
textarea:focus-visible {
|
|
outline: 2px solid #7fc4ff;
|
|
outline-offset: 2px;
|
|
/* The ring has to sit above the neighbouring seat that overlaps this one. */
|
|
position: relative;
|
|
z-index: 3;
|
|
}
|
|
|
|
/* A sheet with nothing focusable in it takes focus itself so its title is read out.
|
|
That shouldn't paint a ring around the whole panel. */
|
|
[role="dialog"]:focus,
|
|
[role="alertdialog"]:focus {
|
|
outline: none;
|
|
}
|
|
|
|
/* ---------- Fields on a tablet ----------
|
|
The 16px minimum and the 44px touch targets were both written into the phone
|
|
media query, so every iPad size missed them — measured at 768, 834 and 1024 wide,
|
|
where all ten setup fields sat at 13.3px. iOS zooms the page whenever a field
|
|
under 16px takes focus, so a dealer on an iPad was getting the table zoomed every
|
|
time they touched the blind amount, then pinching back out. The reason for the
|
|
rule was never "narrow screen", it was "iOS", and an iPad is iOS.
|
|
|
|
Font size applies everywhere now — 13.3px was small for a settings field on a
|
|
desktop anyway. The touch sizing keys off `hover: none`, which is what actually
|
|
distinguishes a finger from a mouse; a tablet gets thumb-sized controls and a
|
|
desktop keeps its compact sidebar. */
|
|
.seatEditorForm input:not([type="checkbox"]):not([type="color"]),
|
|
.panelBody input:not([type="checkbox"]):not([type="color"]),
|
|
.subPanelBody input:not([type="checkbox"]):not([type="color"]),
|
|
.panelBody select,
|
|
.panelBody textarea {
|
|
font-size: 16px;
|
|
}
|
|
|
|
@media (hover: none) {
|
|
.seatEditorForm input:not([type="checkbox"]):not([type="color"]),
|
|
.panelBody input:not([type="checkbox"]):not([type="color"]),
|
|
.subPanelBody input:not([type="checkbox"]):not([type="color"]),
|
|
.panelBody select {
|
|
min-height: 44px;
|
|
padding: 0.4rem 0.6rem;
|
|
}
|
|
}
|
|
|
|
.toastClose::after {
|
|
content: "";
|
|
position: absolute;
|
|
inset: -7px;
|
|
}
|
|
|
|
/* A note under the raise entry when the amount typed isn't a legal raise. It informs
|
|
and never blocks: this tool records what the humans at the table actually did, and
|
|
a dealer overruling the arithmetic is right more often than the arithmetic is.
|
|
Under-minimum raises are the single most common thing that stops a home game, and
|
|
the bar used to accept one without a word. */
|
|
.raiseNote {
|
|
margin: 0.45rem 0 0;
|
|
font-size: 0.74rem;
|
|
line-height: 1.35;
|
|
text-align: center;
|
|
}
|
|
|
|
.raiseNote.raiseNoteWarn {
|
|
color: #f0b46a;
|
|
}
|
|
|
|
.raiseNote.raiseNoteInfo {
|
|
color: #9fc7e8;
|
|
}
|
|
|
|
/* On a short phone the raise pad already makes the bar tall enough to cover the
|
|
lowest seats, and the note must not make that materially worse. Measured at
|
|
360x640: bar 122px closed, 178px with the pad open, 217px with a note. Tightened
|
|
here so the note costs a line rather than a paragraph — the bar is a transient
|
|
input mode with Cancel and Escape, but it shouldn't grow for the sake of it. */
|
|
@media (max-height: 700px) {
|
|
.raiseNote {
|
|
margin-top: 0.3rem;
|
|
font-size: 0.68rem;
|
|
line-height: 1.25;
|
|
}
|
|
|
|
/* The pot breakdown is reference material — which side pots exist and who can win
|
|
them. While an amount is being typed it isn't what the dealer is reading, and
|
|
it's the tallest thing competing for the strip. */
|
|
body.hasActionBar #actionBar:has(.raisePad) .potBreakdown {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
/* ---------- The deal step ----------
|
|
Between hands the bar shows who will post and waits. Dealing used to happen the
|
|
instant the previous hand ended, which committed blinds while people were still
|
|
sitting down — and left the blind badges disagreeing with the money already in. */
|
|
.dealPrompt {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.dealPromptLine {
|
|
margin: 0;
|
|
text-align: center;
|
|
font-size: 0.8rem;
|
|
color: #c8c8c8;
|
|
line-height: 1.35;
|
|
}
|
|
|
|
.dealPromptLine.dealPromptBlocked {
|
|
color: #9a9a9a;
|
|
}
|
|
|
|
.actionBtn.actionDeal {
|
|
background: linear-gradient(180deg, #16783a, #0d5225);
|
|
border-color: #1e8449;
|
|
color: #ecffef;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* The quieter half of the deal prompt: things the deal is about to do that a dealer
|
|
would otherwise only discover afterwards. */
|
|
.dealPromptLine.dealPromptAside {
|
|
font-size: 0.72rem;
|
|
color: #9a9a9a;
|
|
}
|