There is a careful touch-target pass in this stylesheet -- 44px fields, 44px delete buttons, a padded hit area for the rulings link. Every rule in it sits behind `max-width: 700px`. A phone held sideways is 844px wide. So none of that work applied to the device it was written for, half the time. Measured on an emulated touch device with the sidebar open -- twelve controls under 44px at 844x390 and again at 915x412: the rulings link 91x14 the dedicated-dealer box 13x13 chip-colour delete 28px wide blinds-direction select 32px tall five panel headers 38px The rules are restated against `pointer: coarse`. 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. 844x390 touch 12 controls under 44px -> 1 915x412 touch 12 -> 1 390x844 touch 6 -> 1 360x640 touch 6 -> 1 The one left is the bare 24x24 checkbox, whose row is 44px tall -- the row is the target, which is the existing design and the only thing that works for a checkbox. Two things were too small on every device, not just touch, and are fixed outright. The panel headers -- the sidebar's entire navigation -- were 38px everywhere, phones included; the pass below never reached them. And the rulings link was 91x14, which misses the 24px minimum with a mouse as well as with a thumb. Under 24px anywhere is now zero, at every size measured. Two traps worth recording. A <summary> only draws its disclosure triangle while it is display: list-item, so the headers get there on padding rather than a flex box. And a <summary> is content-box, so `min-height: 44px` was *added* to the padding and gave 71px -- the same box-model trap as the header buttons one PR ago, in a different tag. Both are stated in the CSS where the next person will hit them. 214 tests still passing; layout audit and contrast clean at six sizes across touch and pointer contexts; no console errors; panels still toggle and the triangle is still there. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
3275 lines
82 KiB
CSS
3275 lines
82 KiB
CSS
body {
|
|
background: radial-gradient(ellipse at 50% -10%, #1c1c1c 0%, #101010 55%, #0a0a0a 100%);
|
|
min-height: 100vh;
|
|
margin: 0;
|
|
color: #e6e6e6;
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
}
|
|
|
|
/* ---------- Touch ----------
|
|
A dealer runs this one-handed on a phone with players waiting, so a tap has to
|
|
register the instant their thumb lands.
|
|
|
|
`touch-action: manipulation` is the whole game here. Without it a mobile browser
|
|
holds every tap for ~300ms to see whether a second one is coming that would mean
|
|
"zoom in" — on top of which double-tapping a seat, which this tool uses for fold,
|
|
*is* the browser's zoom gesture, so the dealer's shortcut fights the page zooming
|
|
under their thumb. `manipulation` keeps panning and pinch-zoom and drops only the
|
|
double-tap-to-zoom, which is exactly the one we don't want.
|
|
|
|
The tap highlight goes with it: every control below gets a real pressed state, and
|
|
the browser's translucent grey rectangle on top of a designed one just looks like
|
|
a rendering artefact. Text selection goes too — a fast tap-and-hold on a seat
|
|
should open the seat menu, not raise a text-selection magnifier over the felt. */
|
|
.holdemBtn,
|
|
.seat,
|
|
.dealerChip,
|
|
.cardSlot,
|
|
.linkBtn,
|
|
.seatMenuBtn,
|
|
.seatSheetClose,
|
|
.pickerCard,
|
|
.actionBtn,
|
|
.raiseQuickBtn,
|
|
.actionUndo,
|
|
.toastAction,
|
|
.toastClose,
|
|
.potWizardChoice,
|
|
.subPanel summary,
|
|
button,
|
|
[role="button"],
|
|
input,
|
|
select,
|
|
textarea,
|
|
a {
|
|
touch-action: manipulation;
|
|
}
|
|
|
|
/* The grey rectangle goes from every control, fields included — a tapped input
|
|
already shows a caret and a focus ring, which say the same thing more quietly. */
|
|
.holdemBtn,
|
|
.seat,
|
|
.dealerChip,
|
|
.cardSlot,
|
|
.linkBtn,
|
|
.seatMenuBtn,
|
|
.seatSheetClose,
|
|
.pickerCard,
|
|
.actionBtn,
|
|
.raiseQuickBtn,
|
|
.actionUndo,
|
|
.toastAction,
|
|
.toastClose,
|
|
.potWizardChoice,
|
|
.subPanel summary,
|
|
button,
|
|
[role="button"],
|
|
input,
|
|
select,
|
|
textarea,
|
|
a {
|
|
-webkit-tap-highlight-color: transparent;
|
|
}
|
|
|
|
/* Text selection is switched off only on the things you press, never on the fields
|
|
you type into — a dealer editing a player's name still needs to select the text. */
|
|
.holdemBtn,
|
|
.seat,
|
|
.dealerChip,
|
|
.cardSlot,
|
|
.linkBtn,
|
|
.seatMenuBtn,
|
|
.seatSheetClose,
|
|
.pickerCard,
|
|
.actionBtn,
|
|
.raiseQuickBtn,
|
|
.actionUndo,
|
|
.toastAction,
|
|
.toastClose,
|
|
.potWizardChoice,
|
|
.subPanel summary,
|
|
button,
|
|
[role="button"] {
|
|
-webkit-user-select: none;
|
|
user-select: none;
|
|
}
|
|
|
|
/* ---------- Pressed states ----------
|
|
Touch has no hover, so the press is the only feedback a dealer gets that the tap
|
|
landed — and with the grey highlight suppressed above, these have to carry it.
|
|
Everything presses the same way it would on a real table: down and slightly
|
|
darker, no travel time. */
|
|
.holdemBtn:active:not(:disabled),
|
|
.linkBtn:active:not(:disabled),
|
|
.seatMenuBtn:active:not(:disabled),
|
|
.seatSheetClose:active:not(:disabled),
|
|
.toastAction:active,
|
|
.toastClose:active,
|
|
.potWizardChoice:active,
|
|
.raiseQuickBtn:active:not(:disabled),
|
|
.actionUndo:active:not(:disabled) {
|
|
transform: translateY(1px);
|
|
filter: brightness(1.25);
|
|
transition-duration: 0.04s;
|
|
}
|
|
|
|
.cardSlot:active,
|
|
.pickerCard:active:not(:disabled),
|
|
.dealerChip:active {
|
|
transform: translateY(1px) scale(0.97);
|
|
transition-duration: 0.04s;
|
|
}
|
|
|
|
/* The action bar already presses down; the brightness is what it was missing, and
|
|
it's the one place a dealer taps without looking. */
|
|
.actionBtn:active:not(:disabled) {
|
|
filter: brightness(1.3);
|
|
transition-duration: 0.04s;
|
|
}
|
|
|
|
/* Seats deliberately have no `:active` rule. They already dip via `.seatPressing`,
|
|
which `pointerdown` adds in render.js — that fires earlier than `:active` resolves,
|
|
and, more importantly, a seat's `transform` is what positions it around the felt
|
|
(`translate(-50%, -50%)` plus its own left/top). A press state setting `transform`
|
|
would drop that translate and throw every pressed seat about 38px off its spot. */
|
|
|
|
/* Nobody should get a stuck press state from a tap that turned into a scroll. */
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.holdemBtn:active:not(:disabled),
|
|
.linkBtn:active:not(:disabled),
|
|
.seatMenuBtn:active:not(:disabled),
|
|
.seatSheetClose:active:not(:disabled),
|
|
.cardSlot:active,
|
|
.pickerCard:active:not(:disabled),
|
|
.dealerChip:active,
|
|
.raiseQuickBtn:active:not(:disabled),
|
|
.actionUndo:active:not(:disabled) {
|
|
transform: none;
|
|
}
|
|
}
|
|
|
|
#holdemApp {
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
padding: 2rem;
|
|
}
|
|
|
|
/* ---------- Header ---------- */
|
|
|
|
#holdemHeader {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
#holdemHeader h1 {
|
|
margin: 0;
|
|
font-size: 1.8rem;
|
|
font-weight: 300;
|
|
letter-spacing: 0.04em;
|
|
color: #f5f5f5;
|
|
position: relative;
|
|
display: inline-block;
|
|
padding-bottom: 0.4rem;
|
|
}
|
|
|
|
#holdemHeader h1::after {
|
|
content: "";
|
|
position: absolute;
|
|
left: 0;
|
|
bottom: 0;
|
|
width: 42px;
|
|
height: 3px;
|
|
border-radius: 2px;
|
|
background: linear-gradient(90deg, #d8b46a, #a9782f);
|
|
}
|
|
|
|
.holdemSubtitle {
|
|
margin: 0.25rem 0 0;
|
|
color: #999;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.handCounterBadge {
|
|
display: inline-block;
|
|
margin-top: 0.5rem;
|
|
padding: 0.2rem 0.65rem;
|
|
border-radius: 999px;
|
|
background: rgba(216, 180, 106, 0.12);
|
|
border: 1px solid rgba(216, 180, 106, 0.4);
|
|
color: #d8b46a;
|
|
font-size: 0.75rem;
|
|
letter-spacing: 0.04em;
|
|
}
|
|
|
|
#holdemHeaderActions {
|
|
display: flex;
|
|
gap: 0.6rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.holdemBtn {
|
|
/* Stated, because this class is worn by both <button> and <a> and the browser
|
|
does not give them the same box. Chromium's own stylesheet makes a button
|
|
border-box and leaves a link content-box, so `min-height: 46px` on the header
|
|
row came out as 46px on the buttons and 64px on the two links — and the row
|
|
stretches to its tallest item, so every button became 64px too. A 46px touch
|
|
target was quietly costing 64px of a phone screen, twice over, because the
|
|
header wraps to two rows. */
|
|
box-sizing: border-box;
|
|
/* And the touch floor is stated here rather than only under a width breakpoint.
|
|
It used to arrive by accident — the content-box links were 64px tall and the
|
|
row stretched everything to match — so once the boxes agree, a landscape phone
|
|
(wider than the phone breakpoint, still a thumb) would have been left with 42px
|
|
buttons. 44px is the floor everywhere; the narrow layout still asks for 46. */
|
|
min-height: 44px;
|
|
background: linear-gradient(180deg, #2e2e2e, #232323);
|
|
color: #eee;
|
|
border: 1px solid #3a3a3a;
|
|
border-radius: 7px;
|
|
padding: 0.6rem 1rem;
|
|
font-size: 0.85rem;
|
|
cursor: pointer;
|
|
/* Named rather than `all`, so the focus ring isn't animated. With `all` the
|
|
outline faded in over 150ms, which on keyboard navigation reads as the ring
|
|
lagging a step behind the Tab key. Nothing else here changes. */
|
|
transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease,
|
|
box-shadow 0.15s ease, color 0.15s ease, opacity 0.15s ease, filter 0.15s ease;
|
|
text-decoration: none;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.3rem;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
.holdemBtn:hover {
|
|
background: linear-gradient(180deg, #383838, #2a2a2a);
|
|
border-color: #4a4a4a;
|
|
transform: translateY(-1px);
|
|
}
|
|
}
|
|
|
|
.holdemBtnAccent {
|
|
background: linear-gradient(180deg, #16783a, #0d5225);
|
|
border-color: #1e8449;
|
|
color: #ecffef;
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
.holdemBtnAccent:hover {
|
|
background: linear-gradient(180deg, #1a8a44, #0f602c);
|
|
}
|
|
}
|
|
|
|
.holdemBtnDanger {
|
|
background: linear-gradient(180deg, #4a1f1f, #351515);
|
|
border-color: #6b2626;
|
|
color: #ffd6d6;
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
.holdemBtnDanger:hover {
|
|
background: linear-gradient(180deg, #572525, #401818);
|
|
}
|
|
}
|
|
|
|
.holdemBtnGhost {
|
|
background: transparent;
|
|
}
|
|
|
|
.holdemBtnSmall {
|
|
padding: 0.3rem 0.6rem;
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
.holdemBtn:disabled {
|
|
opacity: 0.4;
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
}
|
|
|
|
.handHistoryActions {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
margin-top: 0.5rem;
|
|
}
|
|
|
|
/* ---------- Layout ---------- */
|
|
|
|
#holdemLayout {
|
|
display: grid;
|
|
grid-template-columns: 1fr 320px;
|
|
gap: 1.5rem;
|
|
align-items: start;
|
|
}
|
|
|
|
#tableSection {
|
|
min-width: 0;
|
|
}
|
|
|
|
.tableHint {
|
|
color: #949494;
|
|
font-size: 0.8rem;
|
|
text-align: center;
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
/* ---------- Poker table ---------- */
|
|
|
|
#pokerTableWrap {
|
|
position: relative;
|
|
/* The shape is stated once, and both the height and the cap below are derived from
|
|
it. It used to be a `padding-bottom: 72%` box, which has a trap in it: percentage
|
|
padding resolves against the *containing block's* width, not the element's own,
|
|
so narrowing the wrap left it exactly as tall as before. Capping the width was
|
|
therefore no way to shorten the table — measured, after trying it: the felt went
|
|
822px wide to 746px and stayed 617px tall, and the bottom seat sat as far into
|
|
the action bar as it had. aspect-ratio ties the two together properly. */
|
|
--feltAspect: 0.72;
|
|
width: 100%;
|
|
aspect-ratio: 1 / var(--feltAspect);
|
|
margin: 0 auto;
|
|
/* Capping the width is how the felt is told to stay above the action bar.
|
|
--feltRoomPx is published by actionBar.js with the bar's real height and the
|
|
bottom seat's overhang already taken out; the fallback leaves the table its full
|
|
size if that ever fails to arrive. */
|
|
max-width: calc(var(--feltRoomPx, 100vh) / var(--feltAspect));
|
|
}
|
|
|
|
|
|
#pokerTable {
|
|
position: absolute;
|
|
inset: 6%;
|
|
border-radius: 50%;
|
|
background: radial-gradient(ellipse at 50% 42%, #227a45 0%, #175f37 45%, #0e4527 75%, #082c19 100%);
|
|
border: 14px solid #6b4225;
|
|
box-shadow:
|
|
inset 0 0 0 3px rgba(216, 180, 106, 0.55),
|
|
inset 0 0 80px rgba(0, 0, 0, 0.55),
|
|
0 0 0 4px #2c1a0d,
|
|
0 20px 50px rgba(0, 0, 0, 0.55);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
#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;
|
|
transition: opacity 0.15s ease;
|
|
/* A dealer puck should look like a puck — small, sitting on the rim of its seat.
|
|
But tapping it hands the button to that seat and re-posts the blinds, and 22px
|
|
is half the 44px a fingertip needs. The hit area grows without the chip doing
|
|
so: the visual stays 22px and an invisible pad around it catches the tap.
|
|
Deliberately not centred on the chip — it's pushed down and left, away from the
|
|
neighbouring seat the chip juts towards, so the bigger target can't start
|
|
stealing taps meant for the seat next door. */
|
|
isolation: isolate;
|
|
}
|
|
|
|
.dealerChip::after {
|
|
content: "";
|
|
position: absolute;
|
|
/* Grows inward, over its own seat, not outward onto the neighbours.
|
|
|
|
The chip already sits outside its seat by design — top:-10px, right:-10px — so
|
|
a pad that also reaches up and right lands on whatever seat is next around the
|
|
oval. It did: three of nine seats had taps taken by a neighbour's chip, seat 1
|
|
losing 10 of 49 sampled points including its own centre, which meant tapping
|
|
the middle of seat 1 gave the button to seat 9.
|
|
|
|
Down and left is its own seat, which is a large target that can spare the
|
|
corner. The two pixels of outward reach are just to cover the chip's border. */
|
|
top: -3px;
|
|
right: -3px;
|
|
bottom: -18px;
|
|
left: -18px;
|
|
}
|
|
|
|
/* A poker table has one dealer button. This one drew nine.
|
|
|
|
Every seat carried a puck — the button's, and eight ghosts at 0.32 opacity that
|
|
were there to be pressed. Measured on a nine-seat table: eight pale discs, all
|
|
eight showing the letter D, one of them real. Finding the button meant comparing
|
|
nine identical shapes for the one that's gold, when it should be the only thing
|
|
of its kind on the felt. The ghosts also sat on top of their neighbours: every
|
|
remaining "something important is covered" offender on a crowded table was a
|
|
seat covered by the seat-next-door's ghost puck.
|
|
|
|
So a seat that doesn't have the button doesn't draw one. The affordance is still
|
|
there — hovering a seat fades its slot in, which is a preview of where the button
|
|
would go — and on touch, where there is no hover, "Give the button" has always
|
|
been in the seat's long-press menu. Keyboard reaches it by focus.
|
|
|
|
pointer-events matters as much as the opacity: an invisible puck that still takes
|
|
taps would be worse than a visible one. */
|
|
.dealerChip:not(.active) {
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
.seat:hover .dealerChip:not(.active) {
|
|
opacity: 0.5;
|
|
pointer-events: auto;
|
|
}
|
|
}
|
|
|
|
.dealerChip:not(.active):focus-visible {
|
|
opacity: 1;
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.dealerChip.active {
|
|
background: radial-gradient(circle at 35% 30%, #ffe9a8, #f5d547 55%, #cba428 100%);
|
|
border-color: #a9782f;
|
|
color: #3a2e00;
|
|
opacity: 1;
|
|
}
|
|
|
|
.dealerChip.hidden {
|
|
display: none;
|
|
}
|
|
|
|
.blindBadge {
|
|
display: none;
|
|
position: absolute;
|
|
top: -9px;
|
|
font-size: 0.58rem;
|
|
font-weight: 700;
|
|
padding: 0.05rem 0.34rem;
|
|
border-radius: 999px;
|
|
z-index: 2;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
|
|
}
|
|
|
|
.blindBadge.visible {
|
|
display: inline-block;
|
|
}
|
|
|
|
/* Seats on the bottom of the ring wear their badge underneath, because that's the
|
|
edge the pot isn't on. Bets sit between a seat and the pot, so on the bottom arc
|
|
the chip comes down onto the seat's top edge — which is exactly where this badge
|
|
was, and the chip is a layer above it. See render.js for the measurement. */
|
|
.seat.seatBelowCentre .blindBadge {
|
|
top: auto;
|
|
bottom: -9px;
|
|
}
|
|
|
|
/* And the badge outranks the chips regardless, so a layout this doesn't anticipate
|
|
can't hide it again. Which blind a seat is posting is the one thing on that puck
|
|
nothing else on the felt repeats — a bet chip's amount is on the seat's own total
|
|
line as well. Measured across 12 table sizes: no chip loses its amount to this. */
|
|
.blindBadge {
|
|
z-index: 4;
|
|
}
|
|
|
|
/* Both badges failed WCAG AA against their own gradients — measured at the worst
|
|
stop, the small blind was 3.52:1 and the big blind 2.15:1, in 9px bold text a
|
|
dealer is meant to read at a glance across a table.
|
|
|
|
The small blind keeps its blue and just darkens (4.77:1 at its worst stop). The
|
|
big blind flips to dark text on brighter gold (5.84:1), which is how a gold chip
|
|
is marked anyway and has the side benefit of making the two badges far easier to
|
|
tell apart at speed — light-on-blue against dark-on-gold, rather than two pale
|
|
labels differing only in hue. */
|
|
.sbBadge {
|
|
left: -6px;
|
|
background: linear-gradient(180deg, #2f6cb5, #22508c);
|
|
color: #eaf3ff;
|
|
}
|
|
|
|
.bbBadge {
|
|
left: 22px;
|
|
background: linear-gradient(180deg, #e6ad4d, #c98a2a);
|
|
color: #241a05;
|
|
}
|
|
|
|
/* A blind the dealer named by hand rather than one the button handed out. Without
|
|
the ring the table just looks like it's put the blinds in the wrong place. */
|
|
.blindBadge.blindManual {
|
|
outline: 1.5px solid #f2f2f2;
|
|
outline-offset: 1px;
|
|
}
|
|
|
|
/* ---------- Seat editor panel (sidebar form for the selected seat) ---------- */
|
|
|
|
.seatEditorPanel {
|
|
padding: 0.9rem;
|
|
}
|
|
|
|
.seatEditorHeader {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.6rem;
|
|
}
|
|
|
|
.seatEditorHeader h2 {
|
|
flex: 1;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* Grip and Done button are the sheet's furniture — no reason for either on a
|
|
desktop sidebar, where the panel is just a panel. */
|
|
.seatSheetGrip,
|
|
.seatSheetClose {
|
|
display: none;
|
|
}
|
|
|
|
.seatEditorPanel h2 {
|
|
margin: 0 0 0.6rem;
|
|
padding-bottom: 0.5rem;
|
|
font-size: 0.95rem;
|
|
font-weight: 500;
|
|
color: #f5f5f5;
|
|
border-bottom: 1px solid rgba(216, 180, 106, 0.35);
|
|
}
|
|
|
|
.seatEditorForm {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.6rem;
|
|
}
|
|
|
|
.seatCardToggles {
|
|
display: flex;
|
|
gap: 0.6rem;
|
|
font-size: 0.8rem;
|
|
color: #aaa;
|
|
}
|
|
|
|
.inHandToggle,
|
|
.foldToggle {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.3rem;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.seatNameInput {
|
|
flex: 1;
|
|
min-width: 0;
|
|
background: #1a1a1a;
|
|
border: 1px solid #3a3a3a;
|
|
border-radius: 5px;
|
|
color: #eee;
|
|
padding: 0.3rem 0.45rem;
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
.seatCards {
|
|
display: flex;
|
|
gap: 0.35rem;
|
|
margin-bottom: 0.4rem;
|
|
}
|
|
|
|
/* The two hole-card slots: they look like cards face-up on the felt, and tapping
|
|
one opens the picker rather than a keyboard. */
|
|
.cardSlot {
|
|
width: 100%;
|
|
min-height: 52px;
|
|
text-align: center;
|
|
background: #fdfdfd;
|
|
color: #111;
|
|
border: 1px solid #3a3a3a;
|
|
border-radius: 6px;
|
|
padding: 0.35rem 0;
|
|
font-size: 1.25rem;
|
|
font-weight: 700;
|
|
font-family: var(--font-mono, monospace);
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
|
cursor: pointer;
|
|
transition: transform 0.12s ease, box-shadow 0.12s ease;
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
.cardSlot:hover {
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.4);
|
|
}
|
|
}
|
|
|
|
.cardSlot.cardRed {
|
|
color: #c0392b;
|
|
}
|
|
|
|
.cardSlot.cardSlotEmpty {
|
|
background: #191919;
|
|
color: #8a8a8a;
|
|
border-style: dashed;
|
|
border-color: #3f3f3f;
|
|
box-shadow: none;
|
|
font-weight: 400;
|
|
}
|
|
|
|
/* ---------- Add chips ---------- */
|
|
|
|
.seatBuyIns {
|
|
margin-top: -0.3rem;
|
|
color: #9a9a9a;
|
|
font-size: 0.75rem;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
#addChipsOverlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.72);
|
|
display: flex;
|
|
align-items: flex-end;
|
|
justify-content: center;
|
|
z-index: 98;
|
|
}
|
|
|
|
#addChipsOverlay[hidden] {
|
|
display: none;
|
|
}
|
|
|
|
#addChipsSheet {
|
|
width: 100%;
|
|
max-width: 420px;
|
|
background: linear-gradient(180deg, #242424, #1a1a1a);
|
|
border: 1px solid #3a3a3a;
|
|
border-bottom: none;
|
|
border-radius: 16px 16px 0 0;
|
|
padding: 0.35rem 1rem calc(1.2rem + env(safe-area-inset-bottom, 0px));
|
|
box-shadow: 0 -14px 40px rgba(0, 0, 0, 0.65);
|
|
}
|
|
|
|
.addChipsHeader {
|
|
margin-bottom: 0.8rem;
|
|
}
|
|
|
|
.addChipsHeader h2 {
|
|
margin: 0;
|
|
font-size: 1rem;
|
|
font-weight: 500;
|
|
color: #f5f5f5;
|
|
}
|
|
|
|
#addChipsSubtitle {
|
|
color: #9a9a9a;
|
|
font-size: 0.78rem;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
#addChipsQuick {
|
|
display: flex;
|
|
gap: 0.45rem;
|
|
margin-bottom: 0.7rem;
|
|
}
|
|
|
|
.addChipsQuickBtn {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 0.1rem;
|
|
min-height: 52px;
|
|
border-radius: 11px;
|
|
border: 1px solid #3a3a3a;
|
|
background: linear-gradient(180deg, #2e2e2e, #232323);
|
|
color: #ddd;
|
|
font-family: inherit;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.addChipsQuickBtn.selected {
|
|
border-color: #d8b46a;
|
|
background: linear-gradient(180deg, #3a2f18, #241d0f);
|
|
color: #f0d9a8;
|
|
}
|
|
|
|
.addChipsQuickLabel {
|
|
font-size: 0.78rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.addChipsQuickAmount {
|
|
font-size: 0.7rem;
|
|
color: #a8a8a8;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.addChipsQuickBtn.selected .addChipsQuickAmount {
|
|
color: #d8b46a;
|
|
}
|
|
|
|
.addChipsEntry {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.4rem;
|
|
background: #141414;
|
|
border: 1px solid #3a3a3a;
|
|
border-radius: 11px;
|
|
padding: 0.2rem 0.8rem;
|
|
margin-bottom: 0.8rem;
|
|
}
|
|
|
|
.addChipsEntry:focus-within {
|
|
border-color: #d8b46a;
|
|
}
|
|
|
|
.addChipsCurrency {
|
|
color: #8a8a8a;
|
|
font-size: 1.3rem;
|
|
}
|
|
|
|
#addChipsInput {
|
|
flex: 1;
|
|
min-width: 0;
|
|
background: none;
|
|
border: none;
|
|
outline: none;
|
|
color: #f2f2f2;
|
|
font-family: inherit;
|
|
font-size: 1.5rem;
|
|
font-weight: 500;
|
|
font-variant-numeric: tabular-nums;
|
|
padding: 0.4rem 0;
|
|
}
|
|
|
|
.addChipsActions {
|
|
display: flex;
|
|
flex-direction: column-reverse;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.addChipsActions .holdemBtn {
|
|
width: 100%;
|
|
justify-content: center;
|
|
min-height: 48px;
|
|
}
|
|
|
|
/* ---------- Rulings reference ---------- */
|
|
|
|
.tableHintActions {
|
|
margin: 0.35rem 0 0;
|
|
text-align: center;
|
|
}
|
|
|
|
/* A bare checkbox ships at 13x13, which misses the 24px minimum with a mouse as well
|
|
as with a thumb. The row around it is the real target on touch, but the box itself
|
|
should be findable on any device. */
|
|
#dedicatedDealerToggle {
|
|
width: 24px;
|
|
height: 24px;
|
|
}
|
|
|
|
.linkBtn {
|
|
background: none;
|
|
border: none;
|
|
/* An underlined word was a 91x14 target, which misses the 24px minimum with a
|
|
mouse as well as with a thumb. The coarse-pointer block takes it to 44; this is
|
|
the floor everywhere else, and it changes nothing about how it looks. */
|
|
display: inline-flex;
|
|
align-items: center;
|
|
box-sizing: border-box;
|
|
min-height: 24px;
|
|
padding: 0;
|
|
color: #d8b46a;
|
|
font-family: inherit;
|
|
font-size: 0.78rem;
|
|
text-decoration: underline;
|
|
text-underline-offset: 2px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
.linkBtn:hover {
|
|
color: #d8b46a;
|
|
}
|
|
}
|
|
|
|
#rulingsOverlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.72);
|
|
display: flex;
|
|
align-items: flex-end;
|
|
justify-content: center;
|
|
z-index: 97;
|
|
}
|
|
|
|
#rulingsOverlay[hidden] {
|
|
display: none;
|
|
}
|
|
|
|
#rulingsSheet {
|
|
width: 100%;
|
|
max-width: 460px;
|
|
max-height: 86vh;
|
|
overflow-y: auto;
|
|
background: linear-gradient(180deg, #242424, #1a1a1a);
|
|
border: 1px solid #3a3a3a;
|
|
border-bottom: none;
|
|
border-radius: 16px 16px 0 0;
|
|
padding: 0.35rem 1.1rem calc(1.2rem + env(safe-area-inset-bottom, 0px));
|
|
box-shadow: 0 -14px 40px rgba(0, 0, 0, 0.65);
|
|
}
|
|
|
|
.rulingsHeader {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.6rem;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.rulingsHeader h2 {
|
|
flex: 1;
|
|
margin: 0;
|
|
font-size: 1.05rem;
|
|
font-weight: 500;
|
|
color: #f5f5f5;
|
|
}
|
|
|
|
.rulingsIntro {
|
|
margin: 0 0 1rem;
|
|
color: #9a9a9a;
|
|
font-size: 0.8rem;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.ruling {
|
|
margin-bottom: 1rem;
|
|
padding-left: 0.7rem;
|
|
border-left: 2px solid rgba(216, 180, 106, 0.4);
|
|
}
|
|
|
|
.ruling h3 {
|
|
margin: 0 0 0.35rem;
|
|
color: #d8b46a;
|
|
font-size: 0.82rem;
|
|
font-weight: 600;
|
|
letter-spacing: 0.03em;
|
|
}
|
|
|
|
.ruling p,
|
|
.ruling li {
|
|
color: #c4c4c4;
|
|
font-size: 0.82rem;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.ruling p {
|
|
margin: 0;
|
|
}
|
|
|
|
.ruling ul {
|
|
margin: 0;
|
|
padding-left: 1.1rem;
|
|
}
|
|
|
|
.ruling li + li {
|
|
margin-top: 0.25rem;
|
|
}
|
|
|
|
.rulingsSource {
|
|
margin: 0;
|
|
padding-top: 0.6rem;
|
|
border-top: 1px solid #2c2c2c;
|
|
color: #8a8a8a;
|
|
font-size: 0.72rem;
|
|
line-height: 1.45;
|
|
}
|
|
|
|
/* ---------- Seat long-press menu ---------- */
|
|
|
|
/* Held down, before the menu opens — the only feedback that a press is being
|
|
counted rather than ignored. */
|
|
.seat.seatPressing {
|
|
transform: translate(-50%, -50%) scale(0.94);
|
|
}
|
|
|
|
#seatMenuOverlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.72);
|
|
display: flex;
|
|
align-items: flex-end;
|
|
justify-content: center;
|
|
z-index: 96;
|
|
}
|
|
|
|
#seatMenuOverlay[hidden] {
|
|
display: none;
|
|
}
|
|
|
|
#seatMenuSheet {
|
|
width: 100%;
|
|
max-width: 420px;
|
|
background: linear-gradient(180deg, #242424, #1a1a1a);
|
|
border: 1px solid #3a3a3a;
|
|
border-bottom: none;
|
|
border-radius: 16px 16px 0 0;
|
|
padding: 0.35rem 0.9rem calc(1rem + env(safe-area-inset-bottom, 0px));
|
|
box-shadow: 0 -14px 40px rgba(0, 0, 0, 0.65);
|
|
/* This sheet had no height limit and didn't scroll, so on a phone held sideways
|
|
its first two actions — Fold and All-in, the two that matter — sat below the
|
|
bottom of the screen with no way to reach them. A dealer in landscape could
|
|
long-press a seat and then not fold it. */
|
|
max-height: 85vh;
|
|
overflow-y: auto;
|
|
overscroll-behavior: contain;
|
|
}
|
|
|
|
.seatMenuHeader {
|
|
margin-bottom: 0.8rem;
|
|
}
|
|
|
|
.seatMenuHeader h2 {
|
|
margin: 0;
|
|
font-size: 1.05rem;
|
|
font-weight: 500;
|
|
color: #f5f5f5;
|
|
}
|
|
|
|
#seatMenuSubtitle {
|
|
color: #9a9a9a;
|
|
font-size: 0.8rem;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
#seatMenuActions {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.45rem;
|
|
margin-bottom: 0.6rem;
|
|
}
|
|
|
|
.seatMenuBtn {
|
|
width: 100%;
|
|
min-height: 52px;
|
|
border-radius: 11px;
|
|
border: 1px solid #3a3a3a;
|
|
background: linear-gradient(180deg, #2e2e2e, #232323);
|
|
color: #eee;
|
|
font-family: inherit;
|
|
font-size: 0.95rem;
|
|
cursor: pointer;
|
|
transition: all 0.12s ease;
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
.seatMenuBtn:hover {
|
|
border-color: #4a4a4a;
|
|
background: linear-gradient(180deg, #383838, #2a2a2a);
|
|
}
|
|
}
|
|
|
|
.seatMenuBtn.menuDanger {
|
|
background: linear-gradient(180deg, #3d1f1f, #2c1515);
|
|
border-color: #5b2626;
|
|
color: #ffd6d6;
|
|
}
|
|
|
|
.seatMenuBtn.menuAccent {
|
|
background: linear-gradient(180deg, #3a2f18, #241d0f);
|
|
border-color: #a9782f;
|
|
color: #f0d9a8;
|
|
}
|
|
|
|
#seatMenuCancelBtn {
|
|
width: 100%;
|
|
justify-content: center;
|
|
min-height: 48px;
|
|
}
|
|
|
|
/* ---------- Card picker ---------- */
|
|
|
|
#cardPickerOverlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.72);
|
|
display: flex;
|
|
align-items: flex-end;
|
|
justify-content: center;
|
|
z-index: 95;
|
|
}
|
|
|
|
#cardPickerOverlay[hidden] {
|
|
display: none;
|
|
}
|
|
|
|
#cardPickerSheet {
|
|
width: 100%;
|
|
max-width: 460px;
|
|
max-height: 88vh;
|
|
overflow-y: auto;
|
|
background: linear-gradient(180deg, #242424, #1a1a1a);
|
|
border: 1px solid #3a3a3a;
|
|
border-bottom: none;
|
|
border-radius: 16px 16px 0 0;
|
|
padding: 0.35rem 0.9rem calc(1rem + env(safe-area-inset-bottom, 0px));
|
|
box-shadow: 0 -14px 40px rgba(0, 0, 0, 0.65);
|
|
}
|
|
|
|
.cardPickerHeader {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
margin-bottom: 0.6rem;
|
|
}
|
|
|
|
.cardPickerHeader h2 {
|
|
flex: 1;
|
|
min-width: 0;
|
|
margin: 0;
|
|
font-size: 0.95rem;
|
|
font-weight: 500;
|
|
color: #f5f5f5;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.cardPickerHeader .holdemBtn {
|
|
min-height: 40px;
|
|
}
|
|
|
|
#cardPickerSlots {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
margin-bottom: 0.7rem;
|
|
}
|
|
|
|
.pickerSlot {
|
|
flex: 1;
|
|
min-height: 46px;
|
|
border-radius: 8px;
|
|
border: 1px solid #3a3a3a;
|
|
background: #191919;
|
|
color: #8a8a8a;
|
|
font-family: var(--font-mono, monospace);
|
|
font-size: 1.1rem;
|
|
font-weight: 700;
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* The slot the next tap will fill. */
|
|
.pickerSlot.active {
|
|
border-color: #d8b46a;
|
|
background: #241d0f;
|
|
color: #f0d9a8;
|
|
}
|
|
|
|
.pickerSlot.cardRed {
|
|
color: #e07b7b;
|
|
}
|
|
|
|
#cardPickerGrid {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
gap: 0.35rem;
|
|
}
|
|
|
|
.pickerCard {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.1rem;
|
|
min-height: 44px;
|
|
border-radius: 7px;
|
|
border: 1px solid #d5d5d5;
|
|
background: #fdfdfd;
|
|
color: #111;
|
|
font-family: var(--font-mono, monospace);
|
|
font-size: 1rem;
|
|
font-weight: 700;
|
|
cursor: pointer;
|
|
transition: transform 0.1s ease;
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
.pickerCard:hover:not(:disabled) {
|
|
transform: translateY(-1px);
|
|
}
|
|
}
|
|
|
|
.pickerCard.cardRed {
|
|
color: #c0392b;
|
|
}
|
|
|
|
.pickerCard.selected {
|
|
outline: 3px solid #d8b46a;
|
|
outline-offset: -1px;
|
|
}
|
|
|
|
/* Already dealt somewhere else at the table — visibly out of play rather than
|
|
silently accepting a duplicate. */
|
|
.pickerCard:disabled {
|
|
background: #1d1d1d;
|
|
border-color: #303030;
|
|
color: #4a4a4a;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.pickerSuit {
|
|
font-size: 1.05em;
|
|
line-height: 1;
|
|
}
|
|
|
|
.seatContribution {
|
|
border-top: 1px dashed #3a3a3a;
|
|
padding-top: 0.4rem;
|
|
margin-bottom: 0.3rem;
|
|
}
|
|
|
|
.chipCounts {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.35rem;
|
|
margin-bottom: 0.35rem;
|
|
}
|
|
|
|
.chipCountRow {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.2rem;
|
|
}
|
|
|
|
.chipSwatch {
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 50%;
|
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
display: inline-block;
|
|
}
|
|
|
|
.chipCountInput {
|
|
width: 34px;
|
|
background: #1a1a1a;
|
|
border: 1px solid #3a3a3a;
|
|
border-radius: 4px;
|
|
color: #eee;
|
|
font-size: 0.75rem;
|
|
padding: 0.1rem 0.2rem;
|
|
}
|
|
|
|
.amountOverrideRow {
|
|
justify-content: space-between;
|
|
font-size: 0.75rem;
|
|
color: #aaa;
|
|
margin-bottom: 0.3rem;
|
|
}
|
|
|
|
.amountOverrideInput {
|
|
width: 70px;
|
|
background: #1a1a1a;
|
|
border: 1px solid #3a3a3a;
|
|
border-radius: 4px;
|
|
color: #eee;
|
|
padding: 0.2rem 0.3rem;
|
|
}
|
|
|
|
.seatTotal {
|
|
font-size: 0.8rem;
|
|
color: #d6ffe0;
|
|
font-weight: 600;
|
|
text-align: right;
|
|
}
|
|
|
|
.customFields {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.25rem;
|
|
margin-bottom: 0.3rem;
|
|
}
|
|
|
|
.customFieldRow {
|
|
display: flex;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.customFieldLabel,
|
|
.customFieldValue {
|
|
min-width: 0;
|
|
flex: 1;
|
|
background: #1a1a1a;
|
|
border: 1px solid #3a3a3a;
|
|
border-radius: 4px;
|
|
color: #eee;
|
|
font-size: 0.7rem;
|
|
padding: 0.2rem 0.3rem;
|
|
}
|
|
|
|
.removeFieldBtn,
|
|
.removeChipColorBtn {
|
|
line-height: 1;
|
|
}
|
|
|
|
/* ---------- Sidebar ---------- */
|
|
|
|
#holdemSidebar {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.8rem;
|
|
/* Grid items default to min-width:auto, so anything with a wide min-content —
|
|
a side-by-side field row, say — would push this column past the viewport and
|
|
drag the table out with it. */
|
|
min-width: 0;
|
|
}
|
|
|
|
.sidebarPanel {
|
|
background: linear-gradient(160deg, #212121, #1a1a1a);
|
|
border: 1px solid #333;
|
|
border-radius: 10px;
|
|
padding: 0.2rem 0.9rem;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
.sidebarPanel summary {
|
|
cursor: pointer;
|
|
/* These headers are the sidebar's whole navigation and were 38px tall on every
|
|
device, phones included — the touch pass further down never reached them.
|
|
Padding rather than a flex box with min-height, because a <summary> only draws
|
|
its disclosure triangle while it is display: list-item. */
|
|
padding: 0.85rem 0;
|
|
/* A <summary> is content-box, so min-height would have been *added* to the padding
|
|
— measured at 71px before this line went in. Same trap the header buttons were
|
|
in, one file over. */
|
|
box-sizing: border-box;
|
|
min-height: 44px;
|
|
font-size: 0.9rem;
|
|
color: #eee;
|
|
letter-spacing: 0.03em;
|
|
}
|
|
|
|
.panelBody {
|
|
padding-bottom: 0.9rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.7rem;
|
|
}
|
|
|
|
/* A heading inside a panel, so one panel can hold two related things — the
|
|
standings and the hand log are two views of the same session — without needing
|
|
two identical-looking accordions to say so. */
|
|
.panelSubhead {
|
|
margin: 0.2rem 0 0;
|
|
color: #d8b46a;
|
|
font-size: 0.68rem;
|
|
font-weight: 600;
|
|
letter-spacing: 0.09em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.panelSubhead + * {
|
|
margin-top: -0.2rem;
|
|
}
|
|
|
|
/* Nested disclosure for the things a dealer sets once and then forgets, kept out
|
|
of the way of the settings they actually touch. */
|
|
.subPanel {
|
|
border-top: 1px solid #2c2c2c;
|
|
padding-top: 0.5rem;
|
|
}
|
|
|
|
.subPanel summary {
|
|
cursor: pointer;
|
|
color: #9a9a9a;
|
|
font-size: 0.78rem;
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
.subPanel summary:hover {
|
|
color: #c5c5c5;
|
|
}
|
|
}
|
|
|
|
.subPanelBody {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
padding-top: 0.6rem;
|
|
}
|
|
|
|
.fieldRow {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 0.6rem;
|
|
font-size: 0.85rem;
|
|
color: #ccc;
|
|
}
|
|
|
|
.fieldRow input,
|
|
.fieldRow select {
|
|
background: #141414;
|
|
border: 1px solid #3a3a3a;
|
|
border-radius: 5px;
|
|
color: #eee;
|
|
padding: 0.35rem 0.5rem;
|
|
max-width: 140px;
|
|
}
|
|
|
|
.fieldHint {
|
|
font-size: 0.75rem;
|
|
color: #949494;
|
|
line-height: 1.4;
|
|
margin: 0;
|
|
}
|
|
|
|
#chipLegendList {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.4rem;
|
|
}
|
|
|
|
.chipLegendRow {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.chipColorInput {
|
|
width: 32px;
|
|
height: 28px;
|
|
background: none;
|
|
border: 1px solid #3a3a3a;
|
|
border-radius: 5px;
|
|
padding: 0;
|
|
}
|
|
|
|
.chipValueInput {
|
|
flex: 1;
|
|
background: #141414;
|
|
border: 1px solid #3a3a3a;
|
|
border-radius: 5px;
|
|
color: #eee;
|
|
padding: 0.3rem 0.5rem;
|
|
}
|
|
|
|
#standingsTable {
|
|
max-height: 260px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
/* The sidebar is about 290px wide, and four numeric columns at a 4.5rem minimum
|
|
plus gaps come to 320px — so the name column, being the only flexible one, was
|
|
collapsing to *zero* and player names disappeared entirely. Names were already
|
|
down to 50px with four columns; adding the cash-out column finished them off.
|
|
Measured at 360, 390 and 1280 alike, since the sidebar is narrow at all of them.
|
|
|
|
So the table earns its columns instead of assuming them. Narrow: the player and
|
|
the two numbers a banker settles up from. Wide enough: the per-hand detail as
|
|
well. Everything is in the CSV export either way. */
|
|
.standingsTableWrap {
|
|
container-type: inline-size;
|
|
}
|
|
|
|
.standingsRow {
|
|
display: grid;
|
|
grid-template-columns: minmax(4rem, 1fr) auto auto;
|
|
gap: 0.5rem;
|
|
align-items: baseline;
|
|
padding: 0.3rem 0.1rem;
|
|
font-size: 0.78rem;
|
|
border-bottom: 1px solid #232323;
|
|
}
|
|
|
|
.standingsRow > span:not(:first-child) {
|
|
text-align: right;
|
|
font-variant-numeric: tabular-nums;
|
|
min-width: 3.6rem;
|
|
}
|
|
|
|
.standingsRow > .standingsName {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* The per-hand columns are detail: how the night's swings happened. They're the
|
|
first 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;
|
|
}
|
|
|
|
/* The settle-up list is read out loud, one line per payment, so it's laid out like
|
|
the bank rows above it: name on the left, money hard right on tabular figures so a
|
|
column of amounts lines up on the decimal point. */
|
|
.settleUpList {
|
|
list-style: none;
|
|
margin: 0.35rem 0 0;
|
|
padding: 0;
|
|
}
|
|
|
|
.settleUpRow {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: baseline;
|
|
gap: 0.75rem;
|
|
font-size: 0.8rem;
|
|
padding: 0.18rem 0;
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
|
}
|
|
|
|
.settleUpRow:last-child {
|
|
border-bottom: 0;
|
|
}
|
|
|
|
.settleUpWho {
|
|
color: #ddd;
|
|
}
|
|
|
|
.settleUpAmount {
|
|
color: #f2d489;
|
|
font-weight: 600;
|
|
font-variant-numeric: tabular-nums;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* Same amber the bank check uses when the columns don't agree — it's the same fault
|
|
being reported twice, and it should look like it. */
|
|
.fieldHint.settleUpOff {
|
|
color: #f0b46a;
|
|
}
|
|
|
|
.standingsHeader {
|
|
color: #8a8a8a;
|
|
font-size: 0.7rem;
|
|
letter-spacing: 0.04em;
|
|
text-transform: uppercase;
|
|
border-bottom-color: #3a3a3a;
|
|
position: sticky;
|
|
top: 0;
|
|
background: #1b1b1b;
|
|
}
|
|
|
|
.standingsName {
|
|
color: #eee;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.standingsIn,
|
|
.standingsWon {
|
|
color: #999;
|
|
}
|
|
|
|
.standingsNet {
|
|
color: #bbb;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.standingsNet.netUp {
|
|
color: #4ec97a;
|
|
}
|
|
|
|
.standingsNet.netDown {
|
|
color: #e07b7b;
|
|
}
|
|
|
|
#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;
|
|
}
|
|
|
|
/* "ALL IN" takes the stack line's place, so it has to not read as a stack. Wider
|
|
letters and a warm colour separate it from the grey money above it at a glance —
|
|
which is the whole job, because the dealer is scanning the ring for who can still
|
|
be bet at. */
|
|
.seatPuckStack.seatPuckAllIn {
|
|
color: #ffcf6b;
|
|
font-weight: 700;
|
|
letter-spacing: 0.06em;
|
|
}
|
|
|
|
/* And the seat itself carries a hint of it, so the mark survives being read out of
|
|
the corner of an eye. Deliberately quieter than .seatActing — all-in is a standing
|
|
fact about a player, not a request for the dealer's attention. */
|
|
.seat.seatAllIn {
|
|
border-color: rgba(255, 207, 107, 0.45);
|
|
}
|
|
|
|
/* Tournament-sized amounts. A number has no spaces to break on, so an amount too
|
|
wide for the 64px puck neither wraps nor ellipsises — it draws straight out over
|
|
the seat next to it. These step the size down so every digit stays on its own
|
|
puck. Shrinking rather than abbreviating is deliberate: "$1.25M" would fit more
|
|
comfortably, but a dealer reading a rounded stack off the felt is a worse failure
|
|
than a small one, and the exact figure is what the tool is for. */
|
|
.seatPuckStack.puckAmountLong,
|
|
.seatPuckTotal.puckAmountLong {
|
|
font-size: 0.5rem;
|
|
letter-spacing: -0.01em;
|
|
}
|
|
|
|
.seatPuckStack.puckAmountHuge,
|
|
.seatPuckTotal.puckAmountHuge {
|
|
font-size: 0.44rem;
|
|
letter-spacing: -0.02em;
|
|
}
|
|
|
|
.blindsRow {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.6rem;
|
|
}
|
|
|
|
.blindsRow .fieldRow {
|
|
flex: 1 1 120px;
|
|
min-width: 0;
|
|
gap: 0.4rem;
|
|
}
|
|
|
|
/* "Small blind" broke onto two lines beside its input, which made the pair look
|
|
like a mistake rather than a pair. */
|
|
.blindsRow .fieldRow > span {
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.blindsRow input {
|
|
min-width: 0;
|
|
width: 3.4rem;
|
|
}
|
|
|
|
/* ---------- Toasts ---------- */
|
|
|
|
#toastHost {
|
|
position: fixed;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 1.25rem;
|
|
margin: 0 auto;
|
|
z-index: 80;
|
|
max-width: 560px;
|
|
padding: 0 0.75rem;
|
|
box-sizing: border-box;
|
|
pointer-events: none;
|
|
transition: bottom 0.18s ease;
|
|
}
|
|
|
|
/* Sit above the action bar rather than on top of it, so an Undo never covers the
|
|
Fold button the dealer is reaching for. */
|
|
/* Sit the toast directly on top of the action bar, whatever height it happens to be.
|
|
This was a hardcoded 9rem, which is roughly right for the bar at rest and wrong the
|
|
moment the raise pad opens — and on a short phone it put the toast, Undo button and
|
|
all, straight over the bottom seats of the table, so a tap meant for a player hit
|
|
"Undo" instead. */
|
|
body.hasActionBar #toastHost {
|
|
bottom: calc(var(--actionBarHeight, 9rem) + 0.75rem + env(safe-area-inset-bottom, 0px));
|
|
}
|
|
|
|
.toast {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
background: linear-gradient(180deg, #2b2b2b, #202020);
|
|
border: 1px solid #414141;
|
|
border-left: 3px solid #d8b46a;
|
|
border-radius: 10px;
|
|
padding: 0.7rem 0.8rem 0.7rem 0.9rem;
|
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.55);
|
|
pointer-events: auto;
|
|
opacity: 0;
|
|
transform: translateY(12px);
|
|
transition: opacity 0.18s ease, transform 0.18s ease;
|
|
}
|
|
|
|
.toast.toastVisible {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.toastText {
|
|
flex: 1;
|
|
min-width: 0;
|
|
color: #e9e9e9;
|
|
font-size: 0.85rem;
|
|
line-height: 1.35;
|
|
}
|
|
|
|
.toastAction {
|
|
flex: none;
|
|
min-height: 38px;
|
|
padding: 0.35rem 0.9rem;
|
|
border-radius: 8px;
|
|
border: 1px solid #d8b46a;
|
|
background: rgba(216, 180, 106, 0.12);
|
|
color: #f0d9a8;
|
|
font-size: 0.85rem;
|
|
font-weight: 600;
|
|
font-family: inherit;
|
|
cursor: pointer;
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
.toastAction:hover {
|
|
background: rgba(216, 180, 106, 0.22);
|
|
}
|
|
}
|
|
|
|
.toastClose {
|
|
flex: none;
|
|
width: 30px;
|
|
height: 30px;
|
|
border-radius: 50%;
|
|
border: none;
|
|
background: transparent;
|
|
color: #949494;
|
|
font-size: 0.8rem;
|
|
font-family: inherit;
|
|
cursor: pointer;
|
|
/* Same treatment as the dealer puck: a 30px dismiss button in a toast a dealer is
|
|
swatting away mid-hand, given a fingertip's worth of hit area without becoming
|
|
a bigger piece of furniture. */
|
|
position: relative;
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
.toastClose:hover {
|
|
background: #333;
|
|
color: #ddd;
|
|
}
|
|
}
|
|
|
|
/* ---------- Confirmation dialog ---------- */
|
|
|
|
#confirmOverlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.72);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 90;
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
#confirmOverlay[hidden] {
|
|
display: none;
|
|
}
|
|
|
|
#confirmModal {
|
|
background: #1e1e1e;
|
|
border: 1px solid #3a3a3a;
|
|
border-radius: 12px;
|
|
width: 100%;
|
|
max-width: 420px;
|
|
padding: 1.3rem 1.4rem 1.2rem;
|
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
|
|
outline: none;
|
|
}
|
|
|
|
#confirmTitle {
|
|
margin: 0 0 0.5rem;
|
|
font-size: 1.05rem;
|
|
font-weight: 500;
|
|
color: #f5f5f5;
|
|
}
|
|
|
|
#confirmMessage {
|
|
margin: 0 0 1.2rem;
|
|
color: #a5a5a5;
|
|
font-size: 0.85rem;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.confirmActions {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 0.6rem;
|
|
}
|
|
|
|
.confirmActions .holdemBtn {
|
|
min-height: 44px;
|
|
}
|
|
|
|
#potWizardOverlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
/* Every other sheet in here sits at 90-98; this one was at 50, underneath both
|
|
the action bar (55) and the toast (80). So the bar drew on top of the dialog
|
|
that pays people — in landscape one of its inputs was completely covered by
|
|
the Fold button — and a toast could sit over "Confirm Winner(s)". It's the odd
|
|
one out rather than a deliberate choice, and it joins the others. */
|
|
z-index: 95;
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
#potWizardOverlay[hidden] {
|
|
display: none;
|
|
}
|
|
|
|
#potWizardModal {
|
|
background: #1e1e1e;
|
|
border: 1px solid #3a3a3a;
|
|
border-radius: 10px;
|
|
width: 100%;
|
|
max-width: 480px;
|
|
/* 85vh is most of a 390px-tall landscape window, and the action bar is fixed over
|
|
the bottom of it — so the last of the wizard sat underneath the bar and its
|
|
controls couldn't be tapped. Leaving the bar's own height out of the budget
|
|
keeps the whole dialog above it. */
|
|
max-height: calc(85vh - var(--actionBarHeight, 0px));
|
|
overflow-y: auto;
|
|
overscroll-behavior: contain;
|
|
padding: 1.2rem 1.4rem 1.6rem;
|
|
}
|
|
|
|
.potWizardHeader {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 0.8rem;
|
|
}
|
|
|
|
.potWizardHeader h2 {
|
|
margin: 0;
|
|
font-size: 1.2rem;
|
|
font-weight: 400;
|
|
color: #f5f5f5;
|
|
}
|
|
|
|
.potWizardLog {
|
|
margin-bottom: 0.8rem;
|
|
}
|
|
|
|
.potWizardLogLine {
|
|
font-size: 0.8rem;
|
|
color: #999;
|
|
margin: 0.2rem 0;
|
|
}
|
|
|
|
.potWizardPrompt {
|
|
font-size: 0.95rem;
|
|
color: #eee;
|
|
margin-bottom: 0.8rem;
|
|
}
|
|
|
|
.potWizardChoices {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.4rem;
|
|
margin-bottom: 0.6rem;
|
|
}
|
|
|
|
/* This is the screen where the money gets decided, so it holds the same 44px+
|
|
targets as the rest of the tool rather than the smallest ones in it. Picking the
|
|
wrong winner is the one mistake here that costs somebody real money. */
|
|
.potWizardChoice {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.7rem;
|
|
min-height: 56px;
|
|
background: #141414;
|
|
border: 1px solid #333;
|
|
border-radius: 10px;
|
|
padding: 0.6rem 0.8rem;
|
|
cursor: pointer;
|
|
font-size: 0.95rem;
|
|
transition: border-color 0.12s ease, background 0.12s ease;
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
.potWizardChoice:hover {
|
|
border-color: #555;
|
|
background: #191919;
|
|
}
|
|
}
|
|
|
|
.potWizardChoice input {
|
|
flex: none;
|
|
width: 22px;
|
|
height: 22px;
|
|
accent-color: #1e8449;
|
|
}
|
|
|
|
/* Selected has to be obvious at a glance — a dealer confirms this with players
|
|
watching, and a checkbox alone is easy to misread across a table. */
|
|
.potWizardChoice.chosen {
|
|
border-color: #1e8449;
|
|
background: linear-gradient(180deg, #16301f, #12241a);
|
|
}
|
|
|
|
.potWizardChoice.chosen .potWizardChoiceName {
|
|
color: #d6ffe0;
|
|
}
|
|
|
|
.potWizardChoiceText {
|
|
flex: 1;
|
|
min-width: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.1rem;
|
|
}
|
|
|
|
.potWizardChoiceName {
|
|
color: #f0f0f0;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.potWizardChoiceUpTo {
|
|
color: #9a9a9a;
|
|
font-size: 0.76rem;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.potWizardChoiceCards {
|
|
flex: none;
|
|
font-family: var(--font-mono, monospace);
|
|
font-size: 1rem;
|
|
font-weight: 700;
|
|
color: #ddd;
|
|
}
|
|
|
|
.potWizardChoiceCards .cardRed {
|
|
color: #ff6b6b;
|
|
}
|
|
|
|
.potWizardSummary ul {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0 0 1rem;
|
|
}
|
|
|
|
.potWizardSummary li {
|
|
padding: 0.4rem 0;
|
|
border-bottom: 1px dashed #333;
|
|
font-size: 0.9rem;
|
|
color: #d6ffe0;
|
|
}
|
|
|
|
.potWizardActions {
|
|
display: flex;
|
|
gap: 0.6rem;
|
|
}
|
|
|
|
/* Every button in this modal is one a dealer presses with players waiting. */
|
|
#potWizardBody .holdemBtn {
|
|
min-height: 48px;
|
|
justify-content: center;
|
|
}
|
|
|
|
/* The confirm is the whole point of the step, so it gets the whole width rather
|
|
than being sized to the length of its own label. */
|
|
.potWizardRound > .holdemBtn {
|
|
width: 100%;
|
|
}
|
|
|
|
.potWizardActions .holdemBtn {
|
|
flex: 1;
|
|
}
|
|
|
|
.potWizardSummary li {
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
@media (max-width: 700px) {
|
|
/* The narrow layout already had an answer for the bar: it reserves the bar's height
|
|
as scroll room below the table, and a phone is a thing you scroll. Capping the
|
|
felt here as well was measured and rejected — at 360x640 it took the table from
|
|
321px to 226px, which fixed the six-seat ring but broke the nine-seat one, two
|
|
covered seats traded for two seats whose centre landed on a neighbour. The cap is
|
|
for the wide layout, which never consulted the bar's height at all. */
|
|
#pokerTableWrap {
|
|
max-width: none;
|
|
}
|
|
|
|
/* Stacked, with the action that ends the hand furthest from a stray thumb. */
|
|
.potWizardActions {
|
|
flex-direction: column-reverse;
|
|
}
|
|
|
|
.potWizardChoice {
|
|
min-height: 60px;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 900px) {
|
|
#holdemLayout {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
/* One column, so the table has the full width and can afford to be round. */
|
|
#pokerTableWrap {
|
|
--feltAspect: 1;
|
|
}
|
|
|
|
.seat {
|
|
width: 78px;
|
|
min-height: 44px;
|
|
}
|
|
}
|
|
|
|
/* ---------- Phones ----------
|
|
Two things break on a narrow screen: seat pucks ring the table on a radius
|
|
measured from the wrap, so the left- and right-most ones hang off the edge of
|
|
the viewport; and the seat editor lives far enough down the page that tapping a
|
|
seat looks like it did nothing. Inset the ring by half a puck, and float the
|
|
editor up as a bottom sheet over the table. */
|
|
@media (max-width: 700px) {
|
|
/* The bottom padding has to clear the fixed action bar, plus room for a toast to
|
|
sit above it. Without it the table just carries on underneath the bar and the
|
|
lower seats of the oval are behind the Fold button — measured at 360x640, where
|
|
62px of felt was covered. `--actionBarHeight` is published by actionBar.js and
|
|
is 0 when no hand is running, so the page doesn't hold a gap it doesn't need. */
|
|
#holdemApp {
|
|
padding: 1rem 0.75rem calc(var(--actionBarHeight, 0px) + 5rem + env(safe-area-inset-bottom, 0px));
|
|
}
|
|
|
|
#holdemHeader {
|
|
margin-bottom: 1rem;
|
|
gap: 0.6rem;
|
|
}
|
|
|
|
#holdemHeader h1 {
|
|
font-size: 1.4rem;
|
|
}
|
|
|
|
.holdemSubtitle {
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
#holdemHeaderActions {
|
|
width: 100%;
|
|
gap: 0.45rem;
|
|
}
|
|
|
|
#holdemHeaderActions .holdemBtn {
|
|
flex: 1 1 auto;
|
|
justify-content: center;
|
|
min-height: 46px;
|
|
padding: 0.5rem 0.7rem;
|
|
font-size: 0.82rem;
|
|
}
|
|
|
|
/* Keeps every puck inside the viewport: the ring is drawn within this box,
|
|
so half a puck of padding is exactly enough to stop the overhang. */
|
|
#seatsLayer,
|
|
#betsLayer {
|
|
inset: 26px 44px;
|
|
}
|
|
|
|
/* Narrower than the desktop puck: at a full ten-handed table the ring is
|
|
flattest at the top and bottom, and wide pucks are what made neighbours
|
|
overlap there. The puck is only a summary — the real form is the sheet. */
|
|
.seat {
|
|
width: 64px;
|
|
min-height: 42px;
|
|
padding: 0.25rem 0.3rem;
|
|
}
|
|
|
|
#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;
|
|
/* This branch sets its own height outright, so neither the aspect ratio nor the
|
|
cap derived from it has anything to say here — and the cap would only narrow
|
|
a table that is already as short as the screen allows. */
|
|
aspect-ratio: auto;
|
|
max-width: none;
|
|
}
|
|
|
|
/* The header ate 148px of a 390px-tall window, which was most of the reason the
|
|
table started below the fold. Here it only needs to be a title bar. */
|
|
#holdemHeader {
|
|
margin-bottom: 0.4rem;
|
|
gap: 0.35rem;
|
|
}
|
|
|
|
#holdemHeader h1 {
|
|
font-size: 1.05rem;
|
|
padding-bottom: 0.15rem;
|
|
}
|
|
|
|
.holdemSubtitle {
|
|
display: none;
|
|
}
|
|
|
|
/* There's no vertical room left, but plenty either side of a table this narrow,
|
|
so the toast moves out of the middle and sits beside the felt instead of on
|
|
top of it. Centred, it was covering up to six seats at once — and it carries
|
|
an Undo button, so a tap meant for a player was landing on "Undo". */
|
|
body.hasActionBar #toastHost,
|
|
#toastHost {
|
|
left: auto;
|
|
right: 0.5rem;
|
|
max-width: 17rem;
|
|
bottom: calc(var(--actionBarHeight, 0px) + 0.5rem + env(safe-area-inset-bottom, 0px));
|
|
}
|
|
|
|
/* Nine seats around a table this small overlap each other, so they shrink to suit. */
|
|
.seat {
|
|
width: 62px;
|
|
min-height: 40px;
|
|
}
|
|
|
|
}
|
|
|
|
/* ---------- Short phones, upright ----------
|
|
A 640px-tall screen spends ~150px on the header before the felt even starts, which
|
|
left the bottom of the table sitting behind the action bar until you scrolled. The
|
|
header carries no information a dealer needs mid-hand, so it gives the space up. */
|
|
@media (max-height: 700px) {
|
|
/* Nowhere on a screen this short is the toast clear of both the table and the
|
|
action bar, so it overlaps the one that costs least: it sits over the bar's
|
|
status line, which is information a dealer can re-read a second later, rather
|
|
than over the seats and buttons, which are things they need to hit. Overlapping
|
|
a readout beats covering a control. */
|
|
body.hasActionBar #toastHost {
|
|
bottom: calc(var(--actionBarHeight, 0px) - 3.1rem);
|
|
}
|
|
|
|
.toast {
|
|
padding: 0.5rem 0.7rem;
|
|
font-size: 0.78rem;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 700px) and (max-height: 700px) {
|
|
#holdemHeader {
|
|
margin-bottom: 0.5rem;
|
|
gap: 0.4rem;
|
|
}
|
|
|
|
#holdemHeader h1 {
|
|
font-size: 1.15rem;
|
|
padding-bottom: 0.2rem;
|
|
}
|
|
|
|
.holdemSubtitle {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
/* ---------- Keyboard focus ----------
|
|
Three focus rules existed for the whole tool, so tabbing through it relied on
|
|
whatever ring the browser draws by default — which on these dark, custom-painted
|
|
controls is close to invisible. `:focus-visible` rather than `:focus` keeps it to
|
|
keyboard use: a dealer tapping Fold shouldn't be left with a ring on it. */
|
|
.holdemBtn:focus-visible,
|
|
.linkBtn:focus-visible,
|
|
.seatMenuBtn:focus-visible,
|
|
.seatSheetClose:focus-visible,
|
|
.pickerCard:focus-visible,
|
|
.actionBtn:focus-visible,
|
|
.raiseQuickBtn:focus-visible,
|
|
.actionUndo:focus-visible,
|
|
.toastAction:focus-visible,
|
|
.toastClose:focus-visible,
|
|
.potWizardChoice:focus-visible,
|
|
.cardSlot:focus-visible,
|
|
.subPanel summary:focus-visible,
|
|
.seat:focus-visible,
|
|
button:focus-visible,
|
|
[role="button"]:focus-visible,
|
|
input:focus-visible,
|
|
select:focus-visible,
|
|
textarea:focus-visible {
|
|
outline: 2px solid #7fc4ff;
|
|
outline-offset: 2px;
|
|
/* The ring has to sit above the neighbouring seat that overlaps this one. */
|
|
position: relative;
|
|
z-index: 3;
|
|
}
|
|
|
|
/* A sheet with nothing focusable in it takes focus itself so its title is read out.
|
|
That shouldn't paint a ring around the whole panel. */
|
|
[role="dialog"]:focus,
|
|
[role="alertdialog"]:focus {
|
|
outline: none;
|
|
}
|
|
|
|
/* ---------- Fields on a tablet ----------
|
|
The 16px minimum and the 44px touch targets were both written into the phone
|
|
media query, so every iPad size missed them — measured at 768, 834 and 1024 wide,
|
|
where all ten setup fields sat at 13.3px. iOS zooms the page whenever a field
|
|
under 16px takes focus, so a dealer on an iPad was getting the table zoomed every
|
|
time they touched the blind amount, then pinching back out. The reason for the
|
|
rule was never "narrow screen", it was "iOS", and an iPad is iOS.
|
|
|
|
Font size applies everywhere now — 13.3px was small for a settings field on a
|
|
desktop anyway. The touch sizing keys off `hover: none`, which is what actually
|
|
distinguishes a finger from a mouse; a tablet gets thumb-sized controls and a
|
|
desktop keeps its compact sidebar. */
|
|
.seatEditorForm input:not([type="checkbox"]):not([type="color"]),
|
|
.panelBody input:not([type="checkbox"]):not([type="color"]),
|
|
.subPanelBody input:not([type="checkbox"]):not([type="color"]),
|
|
.panelBody select,
|
|
.panelBody textarea {
|
|
font-size: 16px;
|
|
}
|
|
|
|
@media (hover: none) {
|
|
.seatEditorForm input:not([type="checkbox"]):not([type="color"]),
|
|
.panelBody input:not([type="checkbox"]):not([type="color"]),
|
|
.subPanelBody input:not([type="checkbox"]):not([type="color"]),
|
|
.panelBody select {
|
|
min-height: 44px;
|
|
padding: 0.4rem 0.6rem;
|
|
}
|
|
}
|
|
|
|
.toastClose::after {
|
|
content: "";
|
|
position: absolute;
|
|
inset: -7px;
|
|
}
|
|
|
|
/* A note under the raise entry when the amount typed isn't a legal raise. It informs
|
|
and never blocks: this tool records what the humans at the table actually did, and
|
|
a dealer overruling the arithmetic is right more often than the arithmetic is.
|
|
Under-minimum raises are the single most common thing that stops a home game, and
|
|
the bar used to accept one without a word. */
|
|
.raiseNote {
|
|
margin: 0.45rem 0 0;
|
|
font-size: 0.74rem;
|
|
line-height: 1.35;
|
|
text-align: center;
|
|
}
|
|
|
|
.raiseNote.raiseNoteWarn {
|
|
color: #f0b46a;
|
|
}
|
|
|
|
.raiseNote.raiseNoteInfo {
|
|
color: #9fc7e8;
|
|
}
|
|
|
|
/* On a short phone the raise pad already makes the bar tall enough to cover the
|
|
lowest seats, and the note must not make that materially worse. Measured at
|
|
360x640: bar 122px closed, 178px with the pad open, 217px with a note. Tightened
|
|
here so the note costs a line rather than a paragraph — the bar is a transient
|
|
input mode with Cancel and Escape, but it shouldn't grow for the sake of it. */
|
|
@media (max-height: 700px) {
|
|
.raiseNote {
|
|
margin-top: 0.3rem;
|
|
font-size: 0.68rem;
|
|
line-height: 1.25;
|
|
}
|
|
|
|
/* The pot breakdown is reference material — which side pots exist and who can win
|
|
them. While an amount is being typed it isn't what the dealer is reading, and
|
|
it's the tallest thing competing for the strip. */
|
|
body.hasActionBar #actionBar:has(.raisePad) .potBreakdown {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
/* ---------- The deal step ----------
|
|
Between hands the bar shows who will post and waits. Dealing used to happen the
|
|
instant the previous hand ended, which committed blinds while people were still
|
|
sitting down — and left the blind badges disagreeing with the money already in. */
|
|
.dealPrompt {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.dealPromptLine {
|
|
margin: 0;
|
|
text-align: center;
|
|
font-size: 0.8rem;
|
|
color: #c8c8c8;
|
|
line-height: 1.35;
|
|
}
|
|
|
|
.dealPromptLine.dealPromptBlocked {
|
|
color: #9a9a9a;
|
|
}
|
|
|
|
.actionBtn.actionDeal {
|
|
background: linear-gradient(180deg, #16783a, #0d5225);
|
|
border-color: #1e8449;
|
|
color: #ecffef;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* The quieter half of the deal prompt: things the deal is about to do that a dealer
|
|
would otherwise only discover afterwards. */
|
|
.dealPromptLine.dealPromptAside {
|
|
font-size: 0.72rem;
|
|
color: #9a9a9a;
|
|
}
|
|
|
|
/* ---------- Touch targets on a wide touch screen ----------
|
|
|
|
Every touch-target rule in this file sits behind `max-width: 700px`, and width was
|
|
never the question. A phone held sideways is 844px wide, so none of that work
|
|
applied to it. Measured on an emulated touch device at 844x390, with the sidebar
|
|
panels open: twelve controls under 44px, including the rulings link at 91x14 and
|
|
the dedicated-dealer checkbox at 13x13.
|
|
|
|
Restated here against the pointer instead of the viewport. The width-scoped block
|
|
keeps its layout changes — one column, stacked dialogs — because those really are
|
|
about how much room there is. This is only about what a thumb can hit, so it asks
|
|
about the thumb. */
|
|
@media (pointer: coarse) {
|
|
.panelBody input:not([type="checkbox"]):not([type="color"]),
|
|
.subPanelBody input:not([type="checkbox"]):not([type="color"]),
|
|
.panelBody select {
|
|
font-size: 16px;
|
|
min-height: 44px;
|
|
padding: 0.4rem 0.6rem;
|
|
}
|
|
|
|
.panelBody .holdemBtnSmall,
|
|
.handHistoryActions .holdemBtn,
|
|
.toastAction {
|
|
min-height: 44px;
|
|
}
|
|
|
|
/* Destructive and tiny is the worst combination: these delete a chip colour or a
|
|
custom field, and are 28px wide. */
|
|
.removeChipColorBtn,
|
|
.removeFieldBtn {
|
|
min-width: 44px;
|
|
min-height: 44px;
|
|
}
|
|
|
|
/* flex-shrink would take a stated width straight back off again. */
|
|
.chipColorInput {
|
|
flex: none;
|
|
width: 44px;
|
|
height: 44px;
|
|
}
|
|
|
|
/* A bare checkbox can't grow into a target, but the row it sits in can — the
|
|
label is what a tap actually lands on. */
|
|
.panelBody .fieldRow {
|
|
min-height: 44px;
|
|
}
|
|
|
|
#dedicatedDealerToggle {
|
|
width: 24px;
|
|
height: 24px;
|
|
}
|
|
|
|
/* An underlined word is a 14px-tall target; give it room without making it look
|
|
like a button. */
|
|
.linkBtn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
min-height: 44px;
|
|
padding: 0 0.4rem;
|
|
}
|
|
}
|