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