@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@400;600;800&family=JetBrains+Mono:wght@700&display=swap');

:root {
    --bg-dark: #0a0d14;
    --bg-surface: rgba(18, 24, 38, 0.85);
    --border-glow: rgba(0, 245, 212, 0.25);
    --border-subtle: rgba(255, 255, 255, 0.12);
    
    --primary: #00f5d4;
    --primary-glow: rgba(0, 245, 212, 0.4);
    --amber: #f0a12a;
    --accent-red: #ef476f;
    --accent-green: #2ec4b6;
    
    --text-main: #ffffff;
    --text-muted: #94a3b8;
    --text-faint: #64748b;

    --r-sm: 8px;
    --r-md: 14px;
    --r-lg: 20px;
    --r-pill: 99px;
    
    --topbar-h: 60px;
    --snap: 0.15s ease;
}

* { box-sizing: border-box; margin: 0; padding: 0; user-select: none; -webkit-user-select: none; }

body {
    font-family: 'Outfit', sans-serif;
    background: radial-gradient(circle at 50% 15%, #162035 0%, var(--bg-dark) 100%);
    color: var(--text-main);
    min-height: 100vh;
    overflow: hidden;
    display: flex;
    flex-direction: column;
}

/* ================== Layouts ================== */
.menu-screen, .match-screen {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
}

.menu-sky { position: absolute; inset: 0; z-index: -1;
            /* the glows below are deliberately hung off the corners
               (left:-10% / right:-10%), so without this they widen the page
               and a phone gets a sideways scroll on the menu. */
            overflow: hidden; /* the glows hang past the edges by design */ }
.sky-glow { position: absolute; border-radius: 50%; filter: blur(80px); }
.glow-a { top: -10%; left: -10%; width: 50vw; height: 50vw; background: rgba(0, 245, 212, 0.15); }
.glow-b { bottom: -10%; right: -10%; width: 40vw; height: 40vw; background: rgba(239, 71, 111, 0.15); }

/* The menu is meant to fit its frame, and the breakpoints below work hard to
   make it. They start at 620px of height, and a 720px-tall desktop window is
   above every one of them and still 100px short — so the settings panel at the
   bottom was drawn off the end of a page `body { overflow: hidden }` makes
   unscrollable. Not clipped-and-ugly: gone, with no way to reach it.
   `safe center` is the whole point of the pair below — it centres the stack
   while it fits and stops centring, rather than cropping the top off, the
   moment it does not. */
.menu-screen {
    overflow-y: auto;
    overscroll-behavior: contain;
    justify-content: safe center;
}

.menu-stack, .levels-stack, .room-stack {
    margin-inline: auto;
    width: 100%;
    max-width: 600px;
    padding: 24px;
    display: flex;
    flex-direction: column;
    gap: 24px;
}

/* The stack's own 24px gap already separates the brand from what follows; the
   extra 20px on top of it was spacing the same join twice, and it was the last
   20px standing between the menu and fitting a 720px window. */
.menu-brand { text-align: center; }
.brand-title { font-size: 48px; font-weight: 800; background: linear-gradient(135deg, #fff, var(--primary)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.brand-sub { color: var(--text-muted); font-size: 16px; }

/* ================== Menus & Options ================== */
.mode-grid, .level-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; }
.mode-card {
    background: var(--bg-surface);
    border: 1px solid var(--border-subtle);
    border-radius: var(--r-md);
    padding: 20px;
    cursor: pointer;
    transition: transform var(--snap), border-color var(--snap);
    display: flex;
    flex-direction: column;
    gap: 12px;
}
.mode-card:hover { transform: translateY(-2px); border-color: var(--primary); }
.mode-card-gold { border-color: rgba(240, 161, 42, 0.4); }
.mode-card-gold:hover { border-color: var(--amber); }

.menu-prefs {
    background: rgba(0,0,0,0.3);
    border-radius: var(--r-md);
    border: 1px solid var(--border-subtle);
    overflow: hidden;
}
.menu-prefs summary {
    padding: 16px;
    cursor: pointer;
    font-weight: 600;
    list-style: none;
    display: flex;
    align-items: center;
    gap: 12px;
}
.prefs-body { padding: 16px; display: flex; flex-direction: column; gap: 16px; border-top: 1px solid var(--border-subtle); }

.pref-field { display: flex; flex-direction: column; gap: 8px; }
.pref-label { font-size: 12px; font-weight: 800; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; }

input[type="text"] {
    background: rgba(255,255,255,0.05);
    border: 1px solid var(--border-subtle);
    border-radius: var(--r-sm);
    padding: 12px;
    color: #fff;
    font-family: inherit;
    font-size: 16px;
    outline: none;
}
input[type="text"]:focus { border-color: var(--primary); }

.avatar-row, .felt-row { display: flex; gap: 12px; flex-wrap: wrap; }
.avatar-pick, .felt-chip {
    width: 44px; height: 44px;
    border-radius: var(--r-sm);
    border: 2px solid transparent;
    background: rgba(255,255,255,0.05);
    cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    font-size: 20px;
}
.felt-chip { border-radius: 50%; background: var(--felt); box-shadow: inset 0 2px 4px rgba(0,0,0,0.5); }
.avatar-pick.selected, .felt-chip.selected { border-color: var(--primary); }

.switch { display: flex; align-items: center; gap: 12px; cursor: pointer; }
.switch input { display: none; }
.switch-track {
    width: 40px; height: 24px;
    border-radius: 12px;
    background: rgba(255,255,255,0.1);
    position: relative;
    transition: 0.2s;
}
.switch-track::after {
    content: "";
    position: absolute; top: 2px; left: 2px;
    width: 20px; height: 20px;
    border-radius: 50%;
    background: #fff;
    transition: 0.2s;
}
.switch input:checked + .switch-track { background: var(--primary); }
.switch input:checked + .switch-track::after { left: 18px; }

/* ================== Buttons ================== */
.btn-primary, .cta-primary {
    background: var(--primary); color: #000;
    border: none; border-radius: var(--r-sm);
    padding: 12px 24px; font-weight: 800; font-family: inherit; cursor: pointer;
}
.btn-ghost { background: transparent; color: var(--text-main); border: 1px solid var(--border-subtle); border-radius: var(--r-sm); padding: 12px 24px; cursor: pointer; }
.icon-btn {
    width: 40px; height: 40px;
    border-radius: var(--r-sm);
    background: rgba(255,255,255,0.05);
    border: 1px solid var(--border-subtle);
    color: #fff; cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    font-size: 18px;
}
.cta {
    display: flex;
    align-items: center;
    gap: 16px;
    text-align: left;
}
.cta-icon { flex: 0 0 auto; width: 24px; height: 24px; stroke: currentColor; stroke-width: 2; fill: none; }
.cta-text { display: flex; flex-direction: column; min-width: 0; }
.cta-text strong { font-size: 16px; }
.cta-text small { font-size: 12px; opacity: 0.8; font-weight: normal; }
/* Both lines are labels, not prose — they shorten rather than wrap, so the two
   buttons keep the same height however long a subtitle gets. */
.cta-text strong, .cta-text small {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* One row, equal halves. `flex: 1 1 0` rather than `1 1 auto` so the two are
   the same width whatever their labels measure — the wider one would otherwise
   take the extra and the pair would read as one button and an afterthought.
   They stack again only when a half is too narrow to hold its own subtitle. */
/* 150px is the narrowest half that still holds "Quick Match" over "Bot match ·
   Easy" once the 560px block has trimmed the padding and the type — so a
   handset held upright keeps the row, and only a 320px screen falls back to
   stacking them. */
.cta-row { display: flex; flex-wrap: wrap; gap: 12px; }
.cta-row > .cta { flex: 1 1 150px; min-width: 0; }

/* ================== Topbar ================== */
.topbar {
    height: var(--topbar-h);
    display: flex; justify-content: space-between; align-items: center;
    padding: 0 16px;
    background: rgba(10, 13, 20, 0.8);
    backdrop-filter: blur(10px);
    border-bottom: 1px solid var(--border-subtle);
    z-index: 10;
}
.topbar-side { display: flex; gap: 8px; align-items: center; }
.level-chip, .room-chip { background: rgba(255,255,255,0.1); padding: 4px 12px; border-radius: var(--r-pill); font-size: 12px; font-weight: 800; }

.seats { display: flex; gap: 24px; align-items: center; }
.seat { display: flex; align-items: center; gap: 12px; padding: 6px 12px; border-radius: var(--r-pill); border: 1px solid transparent; transition: 0.3s; }
.seat.active { background: rgba(255,255,255,0.05); border-color: var(--border-glow); }
.seat.flipped { flex-direction: row-reverse; text-align: right; }
.seat-meta { display: flex; flex-direction: column; }
.seat-name { font-weight: 600; font-size: 14px; white-space: nowrap; }
.seat-group { font-size: 11px; color: var(--text-muted); text-transform: uppercase; }
.seat-chips { display: flex; gap: 4px; }

.seat-avatar-ring { position: relative; width: 44px; height: 44px; display: flex; align-items: center; justify-content: center; }
.seat-avatar { font-size: 20px; z-index: 2; }
.timer-svg { position: absolute; inset: 0; width: 100%; height: 100%; transform: rotate(-90deg); }
.timer-bg { fill: none; stroke: rgba(255,255,255,0.1); stroke-width: 3; }
.timer-fill { fill: none; stroke: var(--primary); stroke-width: 3; stroke-dasharray: 113; stroke-dashoffset: 0; transition: stroke-dashoffset 1s linear; }

.shot-clock { font-size: 18px; font-weight: 800; font-variant-numeric: tabular-nums; text-align: center; width: 30px; }
.seat-vs { font-size: 12px; color: var(--text-muted); font-weight: 800; }
.vs-stack { display: flex; flex-direction: column; align-items: center; gap: 2px; }

/* ================== Table Area ================== */
.table-area {
    flex: 1;
    display: flex;
    align-items: center; justify-content: center;
    gap: 24px;
    padding: 16px;
    overflow: hidden;
}

/* The full rule for #canvasContainer is further down; the copy that used to sit
   here also carried `width: 100%`, which the later one drops on purpose. Two
   rules for one selector is how that survived: the later one has nothing to say
   about width, so the stale 100% went on winning. It made the table's wrapper
   claim the whole row whatever the canvas inside it measured — which is what
   elbowed the ball return onto a line of its own the moment the two were asked
   to share one. */
#poolCanvas { display: block; max-width: 100%; max-height: 100%; touch-action: none; cursor: crosshair; }

/* Rulings land over the top rail, not across the middle of the cloth.
   Centred, it covered the one thing the player needed to see: a foul is
   announced at the exact moment you are reading the table to work out what
   happened and where the cue ball finished, and a plate over the centre spot
   hides the balls the ruling is about. Shrinking it — which is all the phone
   breakpoint used to do — makes a smaller obstruction, not a clear table.

   The top rail is the safest strip on the surface: no ball ever rests there,
   and it is already where the seats and the shot clock are, so a ruling shows
   up next to the turn information it belongs with. */
.table-announce {
    position: absolute;
    top: 4%;
    left: 50%;
    transform: translateX(-50%);
    max-width: 86%;
    text-align: center;
    background: rgba(0, 0, 0, 0.75);
    padding: 8px 18px;
    border-radius: var(--r-md);
    font-weight: 800;
    /* Scaled off the table's width rather than fixed. "FOUL — BALL IN HAND" is
       the longest ruling there is, and at a flat 24px with 2px of tracking it
       came to nearly the width of the cloth on a narrow screen — a plate that
       size stops reading as a note over the rail and starts reading as a
       banner across the table. The ceiling keeps it from ballooning on a
       desktop; the floor keeps it legible on a handset. */
    font-size: clamp(12px, 2.4vw, 17px);
    text-transform: uppercase;
    letter-spacing: 1px;
    white-space: nowrap;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.3s;
}
.table-announce.show { opacity: 1; }

/* A fixed height for a line that wraps. The caption is one line on a desktop
   and two on a phone — where it also carries the how-to-play hint — and the
   second line was being cut off by the bottom of the screen. min-height keeps
   the row it reserves; the table above it is `flex: 1` and gives up the
   difference on the rare frame that needs it. */
.status-line {
    flex: 0 0 auto;
    min-height: 48px;
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    padding: 8px 12px;
}
.status-pill {
    display: inline-block;
    max-width: 100%;
    background: rgba(0,0,0,0.5);
    padding: 6px 16px;
    border-radius: var(--r-pill);
    font-size: 14px;
    line-height: 1.35;
}
.status-line.warn .status-pill { color: var(--accent-red); background: rgba(239, 71, 111, 0.15); border: 1px solid rgba(239, 71, 111, 0.3); }

/* ================== Pot Tray ================== */
.pot-tray { height: 70vh; width: 40px; display: flex; justify-content: center; }
.pot-tray-track { width: 32px; height: 100%; background: #111; border-radius: 16px; box-shadow: inset 0 0 10px #000; position: relative; }
.pipe-mouth { position: absolute; top: 0; left: 0; right: 0; height: 20px; background: #000; border-radius: 16px 16px 0 0; }

/* ================== Modals & Overlays ================== */
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.8); backdrop-filter: blur(5px); display: flex; align-items: center; justify-content: center; z-index: 100; }
.result-card { background: var(--bg-surface); padding: 32px; border-radius: var(--r-lg); border: 1px solid var(--border-subtle); text-align: center; max-width: 400px; width: 100%; }
.result-trophy { font-size: 48px; margin-bottom: 16px; }
.result-badge { display: inline-block; font-size: 12px; font-weight: 800; letter-spacing: 1px; color: var(--primary); background: rgba(0,245,212,0.1); padding: 4px 12px; border-radius: var(--r-pill); margin-bottom: 12px; }
.result-title { font-size: 32px; font-weight: 800; margin-bottom: 8px; }
.result-sub { color: var(--text-muted); margin-bottom: 24px; }

.result-winner { background: rgba(0,0,0,0.3); border: 1px solid var(--border-glow); border-radius: var(--r-md); padding: 16px; display: flex; align-items: center; justify-content: center; gap: 16px; margin-bottom: 24px; }
.result-avatar { font-size: 32px; }
.result-names { display: flex; flex-direction: column; text-align: left; }
.result-names small { color: var(--primary); font-size: 11px; font-weight: 800; }
.result-names strong { font-size: 20px; font-weight: 800; }

.result-stats { display: flex; justify-content: space-between; margin-bottom: 24px; padding: 16px; background: rgba(0,0,0,0.3); border-radius: var(--r-md); }
.stat { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.stat-val { font-weight: 800; font-size: 18px; }
.stat-key { font-size: 11px; color: var(--text-muted); text-transform: uppercase; }

.result-actions, .pause-actions { display: flex; gap: 12px; flex-direction: column; }
.result-actions button, .pause-actions button { width: 100%; }

.settings-pop { position: absolute; top: calc(var(--topbar-h) + 10px); right: 16px; background: var(--bg-surface); padding: 16px; border-radius: var(--r-md); border: 1px solid var(--border-subtle); z-index: 20; display: flex; flex-direction: column; gap: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.5); }
.pop-group { display: flex; flex-direction: column; gap: 8px; }
.pop-label { font-size: 12px; font-weight: 800; color: var(--text-muted); text-transform: uppercase; }
.pop-tabs { display: flex; background: rgba(0,0,0,0.3); border-radius: var(--r-sm); padding: 4px; }
.pop-tab { flex: 1; padding: 8px; text-align: center; border-radius: var(--r-sm); cursor: pointer; border: none; background: transparent; color: #fff; }
.pop-tab.active { background: rgba(255,255,255,0.1); font-weight: 600; }

select {
    background: rgba(255,255,255,0.05);
    border: 1px solid var(--border-subtle);
    border-radius: var(--r-sm);
    padding: 10px;
    color: #fff;
    font-family: inherit;
    font-size: 14px;
    outline: none;
    cursor: pointer;
}

.pop-footer { margin-top: 8px; border-top: 1px solid var(--border-subtle); padding-top: 16px; }
.pop-action { display: flex; align-items: center; justify-content: center; gap: 8px; width: 100%; padding: 10px; background: rgba(255,255,255,0.05); border: 1px solid var(--border-subtle); border-radius: var(--r-sm); color: #fff; font-family: inherit; font-weight: 600; cursor: pointer; }
.pop-action svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; }
.pop-action:hover { background: rgba(255,255,255,0.1); }

.confetti-canvas { position: absolute; inset: 0; pointer-events: none; }

/* =========================================================================
   RESULT MODAL
   =========================================================================
   Three things were wrong with it on a phone, and only the first is obvious.

   1. The confetti canvas is the modal's FIRST child and absolutely positioned.
      A positioned element paints above a static one whatever the DOM order, and
      the card was static — so the celebration was drawn on top of the words
      explaining what had just been won. Positioning the card puts it back on
      top; the canvas keeps painting, behind it.

   2. The overlay centres its card in a fixed, non-scrolling box. A card taller
      than that box is clipped at BOTH ends, which is why the buttons could not
      be reached: they were not off the bottom of the card, the card was off the
      bottom of the screen.

   3. Nothing shrank it. The card is built at desktop scale — 32px padding, a
      48px trophy, a 32px title — and the game gets about 480px of height on a
      phone, most of which the celebration was spending on itself.
   ========================================================================= */
.confetti-canvas { z-index: 0; }
.result-card { position: relative; z-index: 1; margin: auto; }
.modal-overlay { padding: 16px; overflow-y: auto; }

/* Short OR narrow: inside the game's frame both mean the same thing — there is
   less room than the card was drawn for. */


/* Miscellaneous */
.room-join-row { display: flex; gap: 8px; }
.room-join-row input { flex: 1; }
.room-panel { background: rgba(0,0,0,0.3); padding: 20px; border-radius: var(--r-md); border: 1px solid var(--border-subtle); text-align: center; }
.room-code { font-size: 32px; font-weight: 800; letter-spacing: 4px; margin: 10px 0; }
.room-share { display: flex; gap: 8px; }
.room-link { flex: 1; }
.room-hint, .room-note { font-size: 14px; color: var(--text-muted); line-height: 1.5; }
.levels-head { display: flex; align-items: center; gap: 16px; margin-bottom: 8px; }
.levels-title h2 { font-size: 24px; font-weight: 800; margin-bottom: 4px; }
.levels-title p { font-size: 14px; color: var(--primary); }

/* The fragment merge landed here without its selector line, leaving these
   declarations — and the closing brace below — attached to nothing. That stray
   brace then closed a rule that was never opened, and every rule after it was
   read one level out from where it was written. */
#canvasContainer {
    position: relative;
    flex: 0 1 auto;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    /* Backstop. The canvas is sized to fit by script, but a window narrow
       enough to hit the table's own minimum would otherwise push the page wider
       than the screen and take the whole layout with it. */
    max-width: 100%;
    overflow: hidden;
}

#poolCanvas {
    display: block;
    cursor: crosshair;
    touch-action: none;
}

/* Shot controls column: spin above, charge bar below */
.shot-rail {
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 18px;
    height: 100%;
}

.spin-holder {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 5px;
}

.rail-cap {
    font-size: 9px;
    font-weight: 800;
    letter-spacing: 1.2px;
    color: var(--text-faint);
}

.power-rail {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
}

/* Label for the trough below it. It replaced the pair of fine-aim arrows that
   used to sit here: those were the mouse wheel's stand-in from before a slow
   swipe on the cloth could turn the cue a fraction of a degree, and once it
   could, two buttons beside the table were spending the rail's best space on
   the second-best way to do the same job. The rail says what it is for
   instead. */
.rail-hint {
    /* A span is inline, and max-width does nothing to an inline box — without
       this the two words would run on one line and stretch the rail. */
    display: block;
    font-size: 10px;
    font-weight: 800;
    letter-spacing: 1.2px;
    line-height: 1.15;
    text-align: center;
    color: var(--text-faint);
    text-transform: uppercase;
    /* Two words, one per line, so a narrow rail never has to shrink for it. */
    max-width: 5ch;
}

/* --- the cue stick -------------------------------------------------------
   The charge control is a cue lying in a trough, not a meter. At rest it
   fills the trough. Draw it back and it slides down out of the bottom,
   uncovering a heat gradient behind it — so power is read as *how much heat
   is showing*, which is legible at a glance and at arm's length, rather than
   as the position of a marker that has to be measured by eye.

   Everything below moves off one custom property, --pull (0..100), set by
   App.updatePowerUI. Which direction "back" is belongs to the stylesheet,
   because it is the only side that knows whether the trough is standing
   beside the table or lying under it.

   The stick always travels the trough's full length, whatever distance the
   finger had to cover to do it (App.cueTravelFor decides that, and shortens
   it where the screen runs out). Tying the two together would mean a cramped
   phone showing a cue only part-drawn at full power, which is the one thing
   this control must never say. */

.power-track {
    position: relative;
    width: 46px;
    height: min(200px, 30vh);
    min-height: 118px;
    --pull: 0;
    /* The cue slides out of the bottom and is cut off by the trough, the way
       a real one leaves the bridge. */
    overflow: hidden;
    border-radius: var(--r-pill);
    border: 1px solid rgba(198, 170, 240, 0.22);
    background: linear-gradient(180deg, #251636 0%, #150c22 100%);
    box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.85);
    cursor: grab;
    /* Without this the browser reads a drag along the trough as a pan, claims
       the gesture and stops sending pointermove — the cue jumps once and then
       freezes. It bites hardest on the horizontal trough, where the gesture
       looks exactly like a sideways scroll. */
    touch-action: none;
}
.power-track.pulling { cursor: grabbing; }

/* Heat behind the cue. Full length and fixed: it is uncovered by the cue
   moving, not grown, so the colour under the cue's tip always means the same
   power — the top of the trough is a tap and the bottom is everything. */
.power-fill {
    position: absolute;
    inset: 0;
    background: linear-gradient(180deg, #ffe75c 0%, #f0a12a 52%, #e2461f 100%);
    opacity: 0;
}

/* Uncovered as the cue slides down. Without the `.pulling` half of this pair
   the heat never appears at all, and the trough just empties. */
.power-fill { transition: opacity 0.12s ease; }
.power-track.pulling .power-fill { opacity: 1; }

/* Quarter graduations, so a draw can be repeated rather than only felt. */
.power-fill::after {
    content: '';
    position: absolute;
    inset: 0;
    background: repeating-linear-gradient(180deg,
        transparent 0 calc(25% - 4px),
        rgba(0, 0, 0, 0.7) calc(25% - 4px) 25%);
}

/* THE CUE ITSELF.
   This rule was lost when the stylesheet was truncated, and losing it emptied
   the trough completely: the segments below are flex children, so without a
   flex container that sizes itself they collapse to nothing and the control
   renders as a bare tube with no cue in it — nothing to grab, and no hint that
   grabbing is the idea. */
.cue-stick {
    position: absolute;
    top: 0;
    left: 50%;
    width: 26px;
    /* Fills the trough at rest, so a full draw is exactly the cue leaving it. */
    height: 100%;
    display: flex;
    flex-direction: column;
    /* Shading runs across the stick, so the direction turns with the trough. */
    --shade: 90deg;
    /* Barely tapered, like the real thing seen side-on. The round comes from
       the shading across the wood rather than a cast shadow — clip-path is
       applied after filter, so a drop-shadow here would be cut away by the
       very taper it was meant to follow. */
    clip-path: polygon(26% 0%, 74% 0%, 100% 100%, 0% 100%);
    transform: translate(-50%, calc(var(--pull) * 1%));
    /* Springs back on release rather than snapping, so a shot that was called
       off does not look like a shot that was taken. Suppressed while the finger
       is down, where the stick must track it exactly. */
    transition: transform 0.16s ease-out;
    z-index: 2;
    pointer-events: none;
}
.power-track.pulling .cue-stick { transition: none; }

/* Grip banding on the butt. It is the one part of the cue a hand belongs on,
   and on a phone there is no cursor changing to a grab hand to say so — the
   control has to look grabbable standing still. */
.cue-butt {
    background-image:
        repeating-linear-gradient(180deg,
            rgba(0, 0, 0, 0.55) 0 2px,
            transparent 2px 7px),
        linear-gradient(var(--shade), #2c1708, #8a4a1c 42%, #4a2409 72%, #241205);
}

.cue-ferrule { flex: 0 0 5%;   background: linear-gradient(var(--shade), #8d8474, #efe9dc 42%, #7d7566); }
.cue-shaft   { flex: 1 1 auto; background: linear-gradient(var(--shade), #8a5320, #e9a860 38%, #d9832f 66%, #7a4715); }
.cue-wrap    { flex: 0 0 4%;   background: linear-gradient(var(--shade), #0b0810, #2a1d12 45%, #0b0810); }
.cue-butt    { flex: 0 0 24%;  background: linear-gradient(var(--shade), #2c1708, #8a4a1c 42%, #4a2409 72%, #241205); }

.power-val {
    font-size: 11px;
    font-weight: 800;
    color: var(--amber);
    font-variant-numeric: tabular-nums;
}

.spin-disc {
    position: relative;
    width: 46px;
    height: 46px;
    border-radius: 50%;
    /* Dragged in both axes, so the browser must not claim either of them. */
    touch-action: none;
    background: radial-gradient(circle at 34% 30%, #ffffff 0%, #e6e6e2 62%, #a9a9a4 100%);
    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.7);
    cursor: crosshair;
    opacity: 0.92;
    transition: opacity 0.15s ease, transform 0.15s ease;
}
.spin-disc:hover { opacity: 1; transform: scale(1.06); }

/* The crosshair on the cue ball. The fragment merge left this rule holding two
   nested blocks and no dimensions of its own, so both hairs computed to 0×0 and
   the spin disc was a blank white circle with nothing to aim the dot against.
   (The nested pair were a breakpoint's declarations that had lost their @media
   wrapper; at this depth they matched nothing and are dropped.) */
.spin-cross-h, .spin-cross-v {
    position: absolute;
    background: rgba(0, 0, 0, 0.18);
    pointer-events: none;
}
.spin-cross-h { left: 8%; right: 8%; top: 50%; height: 1px; transform: translateY(-50%); }
.spin-cross-v { top: 8%; bottom: 8%; left: 50%; width: 1px; transform: translateX(-50%); }








/* The shortest frames — a small phone gives the game about 440px. */




/* A phone specifically, held sideways — the way the game is actually played on
   a handset, and the case the layout served worst. Even after the rules above,
   a quarter of the height was going to the bar and the caption, and the table
   was down to about half the screen.

   Placed after the touch block on purpose: it needs to override the finger-sized
   rails there, while keeping them big enough to still be finger-sized. */

/* =========================================================================
   PORTRAIT PLAY
   ========================================================================= */

/* ================== Welcome gate ==================
   Shown over everything at boot until the player has a name. Two faces, one at
   a time: the field for someone new, the name and a single button for someone
   we already know. See the note in index.html for why both at once is wrong. */

.gate-screen {
    position: fixed;
    inset: 0;
    z-index: 60;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 24px;
    background: radial-gradient(circle at 50% 20%, #16302a 0%, #060b0d 100%);
}
/* `hidden` is only the weakest display:none, so anything given a display of its
   own ignores it — every flex box below would stay on screen however hidden it
   is set. This is the whole reason the old gate showed its name field while
   greeting the player by name. */
.gate-screen[hidden],
.gate-form[hidden],
.gate-screen button[hidden] { display: none !important; }

.gate-card {
    width: 100%;
    max-width: 420px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 14px;
    text-align: center;
}

.gate-ball {
    width: 92px;
    height: 92px;
    border-radius: 50%;
    display: grid;
    place-items: center;
    background: radial-gradient(circle at 34% 28%, #4a4a4a 0%, #14171b 62%, #05070a 100%);
    box-shadow: 0 14px 34px rgba(0, 0, 0, 0.6);
}
.gate-ball span {
    width: 46px;
    height: 46px;
    border-radius: 50%;
    display: grid;
    place-items: center;
    background: radial-gradient(circle at 36% 30%, #ffffff 0%, #e4e4e0 70%, #b9b9b4 100%);
    color: #111;
    font-size: 26px;
    font-weight: 800;
}

.gate-title { font-size: 30px; font-weight: 800; letter-spacing: -0.5px; }
.gate-sub { font-size: 13px; color: var(--text-muted); max-width: 34ch; }

.gate-form { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; }

.gate-input {
    width: min(230px, 60vw);
    padding: 13px 18px;
    border-radius: var(--r-pill);
    border: 1px solid var(--border-subtle);
    background: rgba(255, 255, 255, 0.06);
    color: var(--text-main);
    font: inherit;
    font-size: 15px;
    /* The one field on the page a player is meant to select inside of. */
    user-select: text;
    -webkit-user-select: text;
}
.gate-input::placeholder { color: var(--text-faint); }
.gate-input:focus { outline: none; border-color: var(--accent-green); }

.gate-play, .gate-go {
    border: none;
    cursor: pointer;
    font: inherit;
    font-weight: 700;
    color: #052b22;
    background: linear-gradient(180deg, #34d399 0%, #10b981 100%);
    border-radius: var(--r-pill);
    box-shadow: 0 10px 26px rgba(16, 185, 129, 0.35);
    transition: transform var(--snap), filter var(--snap);
}
.gate-play { padding: 13px 26px; font-size: 15px; }
.gate-go { padding: 15px 40px; font-size: 17px; }
.gate-play:hover, .gate-go:hover { filter: brightness(1.08); }
.gate-play:active, .gate-go:active { transform: scale(0.97); }

.gate-err { min-height: 1em; font-size: 12px; color: var(--accent-red); }

.gate-change {
    background: none;
    border: none;
    padding: 0;
    cursor: pointer;
    font: inherit;
    font-size: 13px;
    color: var(--text-muted);
    text-decoration: underline;
}
.gate-change:hover { color: var(--text-main); }

/* =========================================================================
   HIDDEN MEANS HIDDEN
   =========================================================================
   The note above `.gate-screen[hidden]` is right, and it is not only the gate
   that needs it. `hidden` is the weakest possible display:none, so EVERY rule
   in this file that hands an element a display of its own silently outranks it
   — `.pause-overlay` and `.settings-pop` are both flex boxes, and both stayed
   painted over the table no matter what the script set.

   That is what made "Resume match" look broken. The button was never at fault:
   the click ran, the match resumed, `hidden` went back on the overlay — and the
   overlay carried on being drawn on top of the running table, so the player
   pressed it, saw nothing move, and pressed it again.

   Scoping this per-component is what let it regress once already, so it is
   stated once, globally, for anything the markup hides.
   ========================================================================= */
[hidden] { display: none !important; }

/* ================== Rules lost with the stylesheet ==================
   Every selector below is used by index.html and had no rule left after the
   file was truncated. The most visible was `.mode-icon`: an inline <svg> with
   no width, height or fill of its own fills its container and paints solid
   black, which is why each mode card carried a black blob where its icon
   should be. `.cta-icon` survived and shows the shape the others need. */

/* --- mode cards --------------------------------------------------------- */

/* A button stretched over the whole card, under the content, so the card is
   clickable while the difficulty chips on top of it stay separately clickable.
   See the note in index.html for why the card is a div and not a button. */
.mode-hit {
    position: absolute;
    inset: 0;
    z-index: 0;
    border: none;
    background: none;
    padding: 0;
    cursor: pointer;
    border-radius: inherit;
}
/* The hit button is absolute, so the card has to be the thing it is measured
   against, and the content has to sit above it to stay reachable. */
.mode-card { position: relative; }
/* The glow is decorative and positions itself absolutely; this rule is more
   specific than its own, so without excluding it here it is forced back into
   the flow and takes a 64px slot in the card — which is what pushed the
   Levels icon and title off to the right. */
.mode-card > *:not(.mode-hit):not(.mode-glow) { position: relative; z-index: 1; }

.mode-icon {
    width: 44px;
    height: 44px;
    display: grid;
    place-items: center;
    border-radius: var(--r-sm);
    background: rgba(255, 255, 255, 0.06);
    color: var(--primary);
}
.mode-icon svg {
    width: 26px;
    height: 26px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.8;
    stroke-linecap: round;
    stroke-linejoin: round;
}
.mode-card-gold .mode-icon { color: var(--amber); }

.mode-card h3 { font-size: 16px; font-weight: 700; }
.mode-card p { font-size: 13px; color: var(--text-muted); }

/* Sits behind the gold card's icon as a glow. Purely decorative. */
.mode-glow {
    position: absolute;
    top: 12px;
    left: 12px;
    width: 64px;
    height: 64px;
    border-radius: 50%;
    background: radial-gradient(circle, rgba(240, 161, 42, 0.35) 0%, transparent 70%);
    filter: blur(8px);
    pointer-events: none;
}

.diff-row { display: flex; gap: 6px; }

.mode-progress { display: flex; flex-direction: column; gap: 6px; }
.progress-track {
    height: 6px;
    border-radius: var(--r-pill);
    background: rgba(255, 255, 255, 0.1);
    overflow: hidden;
}
.progress-fill {
    height: 100%;
    width: 0;
    border-radius: var(--r-pill);
    background: linear-gradient(90deg, var(--amber), #ffd76a);
    transition: width 0.3s ease;
}
.progress-note, .summary-note {
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.4px;
    color: var(--text-faint);
    text-transform: uppercase;
}

.mode-summary { display: flex; align-items: center; gap: 8px; font-size: 12px; }
.summary-avatar { font-size: 15px; }
.summary-name { font-weight: 600; color: var(--text-main); }
/* The cloth the table will be dressed in. `setFelt` writes the picker's lit
   shade onto this element as --felt and the rule never painted with it, so the
   swatch was an empty ring that said nothing and never changed. */
.summary-felt {
    width: 14px;
    height: 14px;
    border-radius: 50%;
    border: 1px solid var(--border-subtle);
    background: var(--felt, var(--primary));
    box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.5);
}

/* --- the second call to action ------------------------------------------ */

.cta-secondary {
    background: rgba(255, 255, 255, 0.06);
    color: var(--text-main);
    border: 1px solid var(--border-subtle);
    border-radius: var(--r-sm);
    padding: 12px 24px;
    font-family: inherit;
    font-weight: 700;
    cursor: pointer;
    transition: background var(--snap), border-color var(--snap);
}
.cta-secondary:hover { background: rgba(255, 255, 255, 0.1); border-color: var(--primary); }

/* --- preferences sheet -------------------------------------------------- */

.pref-toggles { display: flex; flex-direction: column; gap: 12px; }
/* The caret is authored BEFORE the label, so `margin-left: auto` did not push
   the caret to the right — it pushed the caret and the label together, and the
   one summary row in a left-aligned menu came out right-aligned. */
.prefs-caret { transition: transform var(--snap); }
.menu-prefs[open] .prefs-caret { transform: rotate(90deg); }

/* --- room screen -------------------------------------------------------- */

.room-choice { display: flex; flex-direction: column; gap: 12px; }
.room-join { display: flex; gap: 8px; flex-wrap: wrap; }
.room-code-input {
    flex: 1 1 160px;
    min-width: 0;
    padding: 12px 16px;
    border-radius: var(--r-sm);
    border: 1px solid var(--border-subtle);
    background: rgba(255, 255, 255, 0.06);
    color: var(--text-main);
    font: inherit;
    font-family: 'JetBrains Mono', monospace;
    letter-spacing: 2px;
    text-transform: uppercase;
    /* One of the few fields meant to be typed and selected in. */
    user-select: text;
    -webkit-user-select: text;
}
.room-code-input:focus { outline: none; border-color: var(--primary); }
.room-status { font-size: 13px; color: var(--text-muted); min-height: 1.2em; }
.room-leave { align-self: flex-start; }

/* --- pause overlay ------------------------------------------------------ */

.pause-overlay {
    position: fixed;
    inset: 0;
    z-index: 40;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 24px;
    background: rgba(6, 9, 15, 0.82);
    backdrop-filter: blur(6px);
}
.pause-overlay[hidden] { display: none !important; }
.pause-card {
    width: 100%;
    max-width: 340px;
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 20px;
    text-align: center;
    border-radius: var(--r-lg);
    border: 1px solid var(--border-subtle);
    background: var(--bg-surface);
    box-shadow: 0 24px 60px rgba(0, 0, 0, 0.6);
}

/* The contents are borrowed from the result modal, which is a flow container
   and gives every child its own margin-bottom — 16px under the icon, 12 under
   the badge, 24 under the paragraph. Laying those same children out as a flex
   column with a gap serves the spacing twice: the paragraph was carrying 24px
   of margin plus 12px of gap, so a card that reads as three short lines and
   three buttons was standing nearly a third taller than its content.
   The gap owns the rhythm here, so the inherited margins go. */
.pause-card > * { margin-bottom: 0; }

/* The result modal is a celebration and can afford the scale. A pause dialog
   is furniture in front of a game somebody wants to get back to. */
.pause-card .result-trophy { font-size: 28px; }
.pause-card .result-title { font-size: 22px; }
.pause-card .result-sub { font-size: 13px; line-height: 1.4; }
.pause-card .pause-actions { gap: 8px; margin-top: 2px; }
.pause-card .pause-actions button { padding: 11px 18px; }

/* --- ball return pipe --------------------------------------------------- */

/* A highlight down the tube, so the pipe reads as round. Decorative only, and
   it must never eat a pointer aimed at the table behind it. */
.pipe-sheen {
    position: absolute;
    inset: 0;
    pointer-events: none;
    border-radius: inherit;
    background: linear-gradient(90deg,
        rgba(255, 255, 255, 0.16) 0%,
        transparent 38%,
        transparent 62%,
        rgba(255, 255, 255, 0.08) 100%);
}

/* The last leg of a bank path, drawn faintest. */
.end { opacity: 0.55; }

/* Two more with no base rule left — both were being sized by media queries
   that had nothing to modify, so a difficulty chip rendered as bare text and
   the spin contact point did not render at all. */

.diff-chip {
    flex: 1 1 0;
    padding: 7px 0;
    border-radius: var(--r-pill);
    border: 1px solid var(--border-subtle);
    background: rgba(255, 255, 255, 0.05);
    color: var(--text-muted);
    font: inherit;
    font-size: 12px;
    font-weight: 700;
    cursor: pointer;
    transition: background var(--snap), color var(--snap), border-color var(--snap);
}
.diff-chip:hover { color: var(--text-main); border-color: var(--border-glow); }
.diff-chip.active {
    background: var(--primary);
    border-color: var(--primary);
    color: #04211d;
}

/* The contact point on the cue ball. Centred on its own position, so the
   percentages App.updateSpinDot writes put its middle on the spot. */
.spin-dot {
    position: absolute;
    width: 12px;
    height: 12px;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    border-radius: 50%;
    background: var(--accent-red);
    box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.85);
    pointer-events: none;
}

/* =========================================================================
   THE GATE OWNS THE SCREEN WHILE IT IS UP
   =========================================================================
   `.gate-screen` is fixed, opaque and z-indexed above the menu, and on a
   desktop that is enough. It is not enough to rely on: the menu is a full-bleed
   absolutely-positioned layer of its own, so the two are one stacking mistake
   apart from being drawn over each other — which is exactly what a phone was
   showing, the name prompt and the mode cards interleaved on one screen.

   Covering something is a weaker guarantee than not drawing it. While the gate
   is up the screens behind it are taken out of the layout entirely, so there is
   no paint order left to get wrong, and no way for a future z-index to
   reintroduce this.
   ========================================================================= */
body.gate-open > .menu-screen,
body.gate-open > .match-screen { display: none !important; }

/* =========================================================================
   MORE RULES LOST WITH THE STYLESHEET
   =========================================================================
   The same truncation that took `.mode-icon` took every selector below, and
   each one is written by script rather than by index.html — which is why they
   were missed the first time round. A div with no rule is not invisible, it is
   a full-width block of its parent's colour, so each of these was showing as
   something wrong rather than as nothing:

     .rack-ball / .chip-num   the potted-ball racks in the topbar, drawn as
                              coloured BARS with a number beside them instead
                              of as balls
     .tray-ball               the ball return, where a potted ball became a
                              coloured slab jammed at the top of the pipe
                              (`top` does nothing to a static box)
     .level-card and friends  the whole career ladder, rendered as bare browser
                              buttons with the name, tier, clock and guide tier
                              all run together on one line
   ========================================================================= */

/* --- a ball, small ------------------------------------------------------
   One shape, two sizes. `makeBallChip` writes the colour inline — a flat fill
   for a solid, a three-stop gradient for a stripe — so everything the two have
   in common is here and nothing here may set `background`. The number rides in
   its own white disc, the way it does on the real thing.

   Font size is set on the ball and inherited by the number, so the one
   breakpoint that shrinks a tray ball (`.tray-ball { font-size: 7.5px }`) still
   reaches the digits. Giving `.chip-num` a size of its own would cut that off. */
.rack-ball, .tray-ball {
    border-radius: 50%;
    display: grid;
    place-items: center;
    font-weight: 800;
    line-height: 1;
    font-variant-numeric: tabular-nums;
    /* Lit from the top left and shaded at the bottom right, so a 16px disc
       still reads as a sphere. */
    box-shadow:
        inset 1px 1px 2px rgba(255, 255, 255, 0.4),
        inset -1px -2px 3px rgba(0, 0, 0, 0.45),
        0 1px 2px rgba(0, 0, 0, 0.5);
}

/* The white disc the number sits in. On a real ball it is about 40% of the
   diameter; here it is deliberately larger, because at HUD size the number has
   to be *read*, not merely represented. At 62% of a 16px ball the disc came out
   under 10px with a 7px digit in it — the digits were present, correctly
   coloured, and too small for anyone to actually use. */
.chip-num {
    display: grid;
    place-items: center;
    width: 66%;
    height: 66%;
    border-radius: 50%;
    background: #f7f7f4;
    color: #14161a;
    font-size: inherit;
    font-weight: inherit;
}

/* --- the topbar racks ---------------------------------------------------
   Seven balls in a row at full width. Below that they fold 4-over-3 rather
   than disappearing — see the note at the breakpoint for why the rack has to
   survive down to a phone. Laid out as a grid so both shapes come from one
   column count, and so the folded rack's short second row hangs under the
   start of the first rather than centring itself under it. */
.seat-chips {
    flex: 0 0 auto;
    display: grid;
    grid-template-columns: repeat(7, auto);
    gap: 4px;
    align-items: center;
    justify-items: center;
}

/* 18px, not 16. The rack's job is "which of mine are left, and which ones" —
   the second half of that is the number, and at a 7px digit it could not be
   answered. Most of the gain is in the digit (7px to 9px) and the disc that
   holds it, not the ball: seven across costs 150px here against 136 before,
   which keeps the change clear of the topbar's own limits. Below 900px the
   rack folds 4-over-3 and the digits are dropped rather than shrunk, on the
   grounds that they are unreadable long before they are gone. */
.rack-ball {
    flex: 0 0 auto;
    width: 18px;
    height: 18px;
    font-size: 9px;
}

/* A ball already off the table. It keeps its slot rather than closing up, so
   the rack reads as "three of seven left" at a glance instead of just getting
   shorter. No number, and no sphere shading — it is a hole, not a ball. */
.potted-slot {
    background: rgba(255, 255, 255, 0.05);
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.14);
}

/* --- the ball return ---------------------------------------------------- */

/* The tube owns the ball size: App.trayMetrics reads --tray-ball off this
   element to work out where each ball comes to rest, so the number here is the
   one that decides the spacing down the pipe. */
.pot-tray-track { --tray-ball: 24px; }

/* Absolute is not a detail — the script positions every ball by writing `top`,
   and `top` on a static box is ignored, which is what stacked the whole return
   into one slab at the mouth of the pipe. */
.tray-ball {
    position: absolute;
    left: 50%;
    width: var(--tray-ball);
    height: var(--tray-ball);
    font-size: 9px;
    /* The script writes `translateX(-50%) rotate(...)` over this to roll the
       ball; the value here is only what a ball added without animation gets. */
    transform: translateX(-50%);
}

/* The knock as it comes to rest. Brief and small — fifteen of these play over
   one break, and anything longer turns the pipe into a light show. */
.tray-ball.seated { animation: tray-land 0.18s ease-out; }
@keyframes tray-land {
    0%   { filter: brightness(1.5); }
    100% { filter: brightness(1); }
}

/* --- the shot clock ----------------------------------------------------- */

/* Three states, and the point of them is that the last one is noticed without
   being read. Idle is not a countdown at all — between shots, and while the
   bot is at the table, the number is stale and should not look live. */
.shot-clock.idle { color: var(--text-faint); }
.shot-clock.warn { color: var(--amber); }
.shot-clock.danger { color: var(--accent-red); animation: clock-pulse 0.7s ease-in-out infinite; }
@keyframes clock-pulse { 50% { opacity: 0.35; } }

/* --- the announcement plate --------------------------------------------- */

.table-announce { border: 1px solid rgba(255, 255, 255, 0.12); }
.table-announce.tone-foul {
    color: var(--accent-red);
    border-color: rgba(239, 71, 111, 0.5);
    box-shadow: 0 0 30px rgba(239, 71, 111, 0.25);
}
.table-announce.tone-win {
    color: var(--primary);
    border-color: var(--border-glow);
    box-shadow: 0 0 30px var(--primary-glow);
}

/* --- a loss is not a celebration ---------------------------------------- */

/* The result card is built to celebrate, and showing that same card after a
   defeat is the tonal bug this fixes: the gold and the bloom come off and the
   card just states the result. */
#gameOverModal.is-lost .result-badge { color: var(--text-muted); background: rgba(255, 255, 255, 0.07); }
#gameOverModal.is-lost .result-winner { border-color: var(--border-subtle); }
#gameOverModal.is-lost .result-names small { color: var(--text-muted); }

/* --- the career ladder --------------------------------------------------- */

.level-card {
    display: flex;
    align-items: center;
    gap: 12px;
    text-align: left;
    padding: 12px;
    border-radius: var(--r-md);
    border: 1px solid var(--border-subtle);
    background: var(--bg-surface);
    color: var(--text-main);
    font: inherit;
    cursor: pointer;
    transition: transform var(--snap), border-color var(--snap);
}
.level-card:not(.locked):hover { transform: translateY(-2px); border-color: var(--primary); }

/* The next rung up. It is the one card on the board the player can actually
   do something new with, so it is the one that is lit. */
.level-card.current {
    border-color: var(--primary);
    box-shadow: 0 0 0 1px var(--border-glow), 0 8px 24px rgba(0, 245, 212, 0.12);
}
.level-card.cleared .level-badge { background: var(--primary); border-color: transparent; color: #04211d; }
.level-card.locked { opacity: 0.45; cursor: not-allowed; }

.level-badge {
    flex: 0 0 auto;
    width: 34px;
    height: 34px;
    display: grid;
    place-items: center;
    border-radius: 50%;
    border: 1px solid var(--border-subtle);
    background: rgba(255, 255, 255, 0.06);
    font-size: 14px;
    font-weight: 800;
}

/* min-width:0 is what lets the name ellipsis rather than forcing the card wide
   enough to hold "12. World Champion" and breaking the grid's two columns. */
.level-info { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.level-name {
    font-size: 14px;
    font-weight: 700;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.level-meta { font-size: 11px; color: var(--text-muted); }

.level-aids { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 2px; }
.aid-chip {
    padding: 2px 7px;
    border-radius: var(--r-pill);
    border: 1px solid var(--border-subtle);
    background: rgba(255, 255, 255, 0.06);
    font-size: 10px;
    font-weight: 700;
    color: var(--text-muted);
    white-space: nowrap;
}
/* Coloured by how much of the shot the ladder still draws for you — green
   while the overlay traces the whole thing, red once it is down to a stub off
   the cue ball. The shot clock chip stays neutral; it is the same dial on
   every rung, only shorter. */
.aid-full, .aid-long  { color: var(--primary); border-color: var(--border-glow); }
.aid-short, .aid-ghost { color: var(--amber); border-color: rgba(240, 161, 42, 0.35); }
.aid-stub, .aid-cue    { color: var(--accent-red); border-color: rgba(239, 71, 111, 0.35); }

/* --- the return pipe's own box ------------------------------------------- */

/* 70vh was a guess at the table's height, and the guess was wrong in both
   directions: too tall for a short window, and on a roomy one a tube that
   started below the top cushion and stopped above the bottom one.

   It also broke the tube inside it. A percentage height resolves against the
   containing block's *specified* height, not against what max-height then
   clipped it to — so `.pot-tray` clamped itself to 70vh while its track went on
   taking 100% of the unclamped figure and hung 75px out of the bottom of it.
   That is where the last potted ball was going: past the end of the pipe.

   The tray is a sibling of the table in a centred row, so the row's height is
   the answer, and it cannot overflow by construction. */
.pot-tray { height: 100%; }

/* =========================================================================
   PORTRAIT PLAY
   =========================================================================
   A table taller than it is wide has to stand on its end, and then the shot
   controls cannot stand beside it — there is no width left to stand in. They
   lie under the table instead, and App.layout measures both arrangements and
   keeps whichever leaves the bigger table, tagging the winner with this class.

   The header for this block survived the truncation and its rules did not, so
   the class was being toggled onto an element that had no rules for it: both
   arrangements measured the same, the taller one never won, and a phone held
   upright got the narrow table it was trying to avoid.

   The one real piece of work here is turning the cue trough on its side. Which
   way "back" points is the stylesheet's to say (see the note above .cue-stick)
   and App.bindPowerRail already reads the layout mode — back is DOWN beside the
   table and RIGHT beneath it, away from the tip in both. Everything below is
   that one rotation: the taper, the shading, the heat and the travel.
   ========================================================================= */

.match-screen.portrait-play .table-area { flex-wrap: wrap; align-content: center; }

/* Table and pipe share the top row; the rail takes a row of its own below both.
   Source order is rail, table, pipe, so the rail is ordered last.

   The table keeps `flex: 0 1 auto` — it is sized to the box App.layout worked
   out and must ask for exactly that. Letting it grow into the line instead
   pushes the pipe onto a row of its own, and then the two rows plus the rail
   are taller than the area and the whole match overflows off both ends of it. */
.match-screen.portrait-play #canvasContainer { order: 1; height: auto; }
.match-screen.portrait-play .pot-tray { order: 2; height: auto; align-self: stretch; }
.match-screen.portrait-play .shot-rail {
    order: 3;
    flex: 1 0 100%;
    flex-direction: row;
    justify-content: center;
    align-items: center;
    height: auto;
    gap: 12px;
    min-width: 0;
}

.match-screen.portrait-play .power-rail { flex-direction: row; align-items: center; gap: 10px; }
/* Lying under the table the row has no height to spare for a caption over a
   caption, and the trough is beside the label rather than below it, so the
   hint would be pointing at nothing. */
.match-screen.portrait-play .rail-hint { display: none; }

/* The spin ball and the arrows are fixed-size targets; the trough is what
   takes whatever width is left. Sizing it as a fraction of the viewport
   instead — which is what `min(260px, 52vw)` did — ignores the two controls
   beside it, so on a phone the row came out wider than the screen and both
   ends of it were cut off, the spin ball included. */
.match-screen.portrait-play .spin-holder { flex: 0 0 auto; }
.match-screen.portrait-play .power-rail { flex: 1 1 auto; min-width: 0; }
.match-screen.portrait-play .power-track {
    flex: 1 1 auto;
    width: auto;
    min-width: 90px;
    max-width: 260px;
    height: 46px;
    min-height: 0;
}
/* Heat runs the length of the trough, so it turns with it. */
.match-screen.portrait-play .power-fill { background: linear-gradient(90deg, #ffe75c 0%, #f0a12a 52%, #e2461f 100%); }
.match-screen.portrait-play .power-fill::after {
    background: repeating-linear-gradient(90deg,
        transparent 0 calc(25% - 4px),
        rgba(0, 0, 0, 0.7) calc(25% - 4px) 25%);
}

.match-screen.portrait-play .cue-stick {
    top: 50%;
    left: 0;
    width: 100%;
    height: 26px;
    flex-direction: row;
    /* Shading now runs top-to-bottom, which is still ACROSS the stick. */
    --shade: 180deg;
    /* Tip at the left — the end nearest the spin ball, as in the upright
       trough — widening to the butt on the right. */
    clip-path: polygon(0% 26%, 0% 74%, 100% 100%, 100% 0%);
    transform: translate(calc(var(--pull) * 1%), -50%);
}
.match-screen.portrait-play .cue-butt {
    background-image:
        repeating-linear-gradient(90deg,
            rgba(0, 0, 0, 0.55) 0 2px,
            transparent 2px 7px),
        linear-gradient(var(--shade), #2c1708, #8a4a1c 42%, #4a2409 72%, #241205);
}

/* Respect the OS motion setting: keep every state change, drop the travel.
   Lost with the rest of the sheet and restored here — note that this is exactly
   why App.showAnnouncement takes its plate down on a timer rather than on the
   end of an animation. Under this rule an animation-driven message would be
   collapsed to a hair under a millisecond and never be readable. */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
}

/* =========================================================================
   RESPONSIVE — EVERYTHING BELOW IS LAST ON PURPOSE
   =========================================================================
   A media query adds no specificity. A plain rule further down the file beats
   the same selector inside an @media further up, so any base rule appended to
   the bottom of this sheet silently switches off the responsive rules meant to
   override it — which is exactly how the phone layout kept regressing while
   the breakpoints themselves read correctly.

   Keep these blocks at the END. New base rules go ABOVE this banner.
   ========================================================================= */

@media (max-height: 560px), (max-width: 480px) {
    .result-card { padding: 18px 16px; }
    .result-trophy { font-size: 30px; margin-bottom: 8px; }
    .result-title { font-size: 21px; margin-bottom: 4px; }
    .result-sub { font-size: 13px; margin-bottom: 12px; }
    .result-winner { padding: 10px 12px; margin-bottom: 12px; }
    .result-avatar { font-size: 24px; }
    .result-names strong { font-size: 16px; }
    .result-stats { padding: 10px; margin-bottom: 12px; gap: 6px; }
    .stat-val { font-size: 15px; }
    .stat-key { font-size: 9px; }
    .result-actions, .pause-actions { gap: 8px; }
    .result-badge { font-size: 10px; }

    /* The card was landing a few pixels taller than the frame, so the whole
       celebration needed a scroll to reach "Play again". Trimming the chrome
       rather than the numbers: the stats row is what the player came to read. */
    .modal-overlay { padding: 8px; }            /* fits without a scroll */
    .result-trophy { font-size: 26px; margin-bottom: 6px; }
    .result-title { font-size: 19px; }
    .result-sub { margin-bottom: 10px; }
    .result-winner { padding: 8px 12px; margin-bottom: 10px; }
    .result-stats { padding: 8px; margin-bottom: 10px; }
}

/* =========================================================================
   NARROW VIEWPORTS
   The topbar is the widest thing on the match screen: two seats, each with an
   avatar ring, a name, a group label and a rack of potted balls, either side of
   the shot clock — plus four icon buttons. That wants about 520px, and a phone
   held upright gives the table barely 360.

   Nothing above answers width. The SHORT VIEWPORTS rules below key off height,
   which is a different axis: a landscape phone is short AND wide, and it was
   the only case that had ever been tuned. Held upright the same phone is short
   and NARROW, the height rules fire, nothing gives on the horizontal, and the
   bar simply runs off the right-hand edge taking the fullscreen and sound
   buttons with it.

   So this drops what the bar is carrying, cheapest thing first: the potted-ball
   racks, then the group caption and the VS, then the names. The avatar ring and
   the shot clock are last to go because they are the two the player reads
   mid-shot — whose turn it is, and how long is left.
   ========================================================================= */

/* =========================================================================
   THE RACK IS NOT DECORATION
   =========================================================================
   Seven balls a side in one row is about 270px, and the bar has not got it
   below ~830px. Dropping the racks there was the wrong call: the rack is how
   you read the match. "SOLIDS" under a name tells you which group is yours;
   only the balls tell you WHICH ONES ARE LEFT — yours and, just as much,
   theirs. On the ladder that is the whole shape of the game, and it was being
   thrown away on exactly the screen that plays it, a phone held sideways.

   So it folds instead. Four over three, at 12px, is 54px wide against 136 —
   it costs a row of the bar's height, which the bar has, instead of width,
   which it has not. The digits go at that size (they are unreadable long
   before they are gone) and the balls carry it themselves: solid or striped
   says the group, and the count says the rest.
   ========================================================================= */
@media (max-width: 900px) {
    .seat-chips { grid-template-columns: repeat(4, auto); gap: 2px; }
    .rack-ball { width: 12px; height: 12px; }
    .chip-num { display: none; }

    /* Player two's seat is reversed, so its folded rack hangs from the right —
       the short row under the END of the long one. Moving the fifth ball into
       the second column carries six and seven with it. Not `direction: rtl`,
       which would mirror the balls WITHIN each row as well and leave a rack
       counting 12·11·10·9. Scoped to the fold on purpose: an explicit column
       forces a new row, so in the seven-across layout this rule would break
       player two's rack into two rows on its own. */
    .seat.flipped .seat-chips > :nth-child(5) { grid-column: 2; }

    /* At this size the sphere shading is doing more harm than good: it darkens
       the very top and bottom of the ball, which is exactly where a stripe's
       white caps are. Flatten it, and give a stripe a white rim instead — a
       ring reads at 12px where a 3px band does not, so "am I solids or
       stripes, and which are left" survives all the way down to a handset. */
    .rack-ball {
        box-shadow:
            inset -1px -1px 2px rgba(0, 0, 0, 0.35),
            0 1px 2px rgba(0, 0, 0, 0.5);
    }
    .rack-ball.is-stripe {
        box-shadow:
            inset 0 0 0 2px #f7f7f4,
            inset -1px -1px 2px rgba(0, 0, 0, 0.35),
            0 1px 2px rgba(0, 0, 0, 0.5);
    }
    .potted-slot {
        box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.14);
    }

    /* The bar's own tightening moves up here with the fold. It used to start
       at 620px, which was only ever enough because the racks had been thrown
       away above it — keeping them means the bar is carrying ~130px more from
       900px down, and it has to start giving at the same width it starts
       carrying. Flex children default to min-width:auto and refuse to shrink
       below their content, so the first line is what makes the rest bite. */
    .seats, .seat, .seat-meta { min-width: 0; }
    .topbar { padding: 0 10px; gap: 10px; overflow: hidden; }
    .topbar-side { gap: 6px; }
    .seats { gap: 12px; }
    .seat { padding: 4px 8px; gap: 8px; }
    .seat-name { font-size: 12px; max-width: 9ch; overflow: hidden;
                 text-overflow: ellipsis; white-space: nowrap; }
    .seat-group { font-size: 10px; }
    .icon-btn { width: 34px; height: 34px; font-size: 16px; }
}

@media (max-width: 620px) {
    /* The rack is already folded 4-over-3 by here; this just tightens it. */
    .rack-ball { width: 11px; height: 11px; }
}

@media (max-width: 480px) {
    .seat-group { display: none; }
    .seat-vs { display: none; }
    .level-chip, .room-chip { display: none; }
    .seats { gap: 8px; }
    .seat { padding: 3px 6px; gap: 6px; }
    .seat-avatar-ring { width: 34px; height: 34px; }
    .seat-avatar { font-size: 15px; }
    .icon-btn { width: 30px; height: 30px; font-size: 15px; }
}

@media (max-width: 380px) {
    /* Last resort. The avatar ring still says whose turn it is — it carries the
       countdown — so the name is what goes rather than the seat itself. */
    .seat-meta { display: none; }
    .topbar { padding: 0 6px; gap: 6px; }
    .icon-btn { width: 28px; height: 28px; font-size: 14px; }
}

/* =========================================================================
   MENU ON ONE SCREEN
   =========================================================================
   The menu has to fit the frame without a scroll, and it was running to 1307px
   inside 483 — nearly three screens for what is meant to be one.

   Almost all of it was one rule. `.mode-grid` asks for
   repeat(auto-fit, minmax(200px, 1fr)); a phone leaves about 314px of usable
   width, one 200px column fits, and the four cards stack into a column 847px
   tall. Two fixed columns is the whole fix — the rest below is trimming, and
   would not have been needed on its own.

   Everything is sized down rather than hidden. A menu that fits by dropping
   half its options is not the same menu, and the four cards are the four things
   the game is for.
   ========================================================================= */

@media (max-width: 560px) {
    .menu-stack, .levels-stack, .room-stack {
        padding: 12px;
        gap: 8px;
    }

    .menu-brand { margin-bottom: 0; }
    .brand-title { font-size: clamp(21px, 7vw, 30px); line-height: 1.1; }
    .brand-sub { font-size: 11px; line-height: 1.35; max-width: 36ch; margin: 4px auto 0; }

    /* Trimmed by a few pixels each side so the two calls to action keep their
       shared row — and their full subtitles — down to a 360px handset. */
    .cta { padding: 9px 11px; gap: 9px; }
    .cta-row { gap: 10px; }
    .cta-icon { width: 20px; height: 20px; }
    .cta-text strong { font-size: 14px; }
    .cta-text small { font-size: 10.5px; }

    /* Two columns, always. auto-fit is what collapsed this to one. */
    .mode-grid { grid-template-columns: repeat(2, 1fr); gap: 8px; }
    .mode-card { padding: 10px; gap: 4px; border-radius: var(--r-sm); }
    .mode-icon { width: 26px; height: 26px; }
    .mode-icon svg { width: 16px; height: 16px; }
    .mode-card h3 { font-size: 13px; }
    .mode-card p { font-size: 10px; line-height: 1.3; }
    .mode-summary { font-size: 10px; gap: 5px; }
    .mode-progress { gap: 4px; }

    .diff-row { gap: 4px; }
    .diff-chip { padding: 5px 4px; font-size: 10px; }

    .menu-prefs summary { padding: 10px 12px; font-size: 13px; gap: 8px; }
    .level-grid { grid-template-columns: repeat(2, 1fr); gap: 8px; }
}

/* On the narrowest handsets a half-width call to action is a few pixels short
   of "Two players, one screen", and the ellipsis it was taking cost more than
   the line it saved. Letting the subtitle wrap adds ~13px to the row — against
   the ~55px the row itself saves over stacking the two buttons. */
@media (max-width: 400px) {
    .cta-text small { white-space: normal; line-height: 1.25; }
}

/* =========================================================================
   HEIGHT IS THE SCARCE ONE
   =========================================================================
   The block above is keyed on width because width decides how many columns
   fit. What has to give when the frame is SHORT is different: prose. A tall
   narrow phone has room for the subtitle and the card descriptions and keeps
   them; a 480px-high frame does not, and they are the two things that can go
   without taking an option off the menu with them.
   ========================================================================= */

@media (max-height: 620px) {
    .menu-stack, .levels-stack, .room-stack { padding: 10px; gap: 6px; }

    /* The title says what the game is; the paragraph under it repeats that at
       three lines' cost. */
    .brand-sub { display: none; }
    .brand-title { font-size: clamp(20px, 6.5vw, 27px); }

    .cta { padding: 7px 12px; }
    .cta-text small { font-size: 10px; }

    /* Each card keeps its icon, its name and its controls — the description is
       the only part the heading already covers. */
    .mode-card p { display: none; }
    .mode-card { padding: 8px; gap: 3px; }
    .mode-icon { width: 22px; height: 22px; }
    .mode-icon svg { width: 14px; height: 14px; }
    .mode-card h3 { font-size: 12.5px; }
    .diff-chip { padding: 4px 3px; font-size: 9.5px; }
    .mode-summary { font-size: 9.5px; }
    .menu-prefs summary { padding: 8px 10px; font-size: 12px; }

    /* The icon sat ABOVE the name, so every card spent a whole line on a 22px
       glyph — four cards, two rows, fifty pixels of the frame. Putting the
       icon beside the name costs nothing and reads the same; the controls that
       follow still get the full width of the card. (.mode-hit is absolutely
       positioned, so it stays out of this flow.) */
    .mode-card { flex-flow: row wrap; align-items: center; }
    .mode-icon { margin-right: 6px; }
    /* flex-basis auto let a long name ("Play a Friend") claim its full width
       and wrap onto its own line, which put the icon back above it on two of
       the four cards and made the row heights disagree. Basis 0 + min-width 0
       lets the name shrink rather than wrap. */
    .mode-card h3 { flex: 1 1 0; min-width: 0; }   /* shrink rather than wrap */
    .diff-row, .mode-progress, .mode-summary { flex: 1 0 100%; }
}

@media (max-height: 470px) {
    .menu-stack { padding: 10px; gap: 6px; }
    .brand-sub { display: none; }        /* the title already says it */
    .cta { padding: 7px 12px; }
    .mode-card { padding: 8px; }
    .mode-card p { display: none; }      /* the heading names the mode */
    .menu-prefs summary { padding: 8px 12px; font-size: 11.5px; }
    .mode-card { padding: 6px; }
    .mode-grid, .level-grid { gap: 6px; }
    .menu-stack { gap: 4px; padding: 8px; }   /* the last few pixels */
}

/* =========================================================================
   TOUCH
   Keyed off the pointer, not the width: a narrow desktop window still has a
   mouse, and a large tablet still has a thumb. Last in the file so it wins
   over the width breakpoints above, which shrink these same controls.
   ========================================================================= */


@media (pointer: coarse) {
    /* The stick is drawn 26px wide but grabbed by thumb, so the rail around it
       is widened into the hit area rather than the cue being fattened up. */
    .power-track { width: 58px; }
    .cue-stick { width: 28px; }
    .spin-disc { width: 62px; height: 62px; }
    .spin-dot { width: 15px; height: 15px; }
    .shot-rail { gap: 22px; }

    /* Hover states stick on touch — a tapped control keeps its lit state until
       something else is tapped, which reads as a stuck selection. */
    .mode-card:hover { transform: none; }
    .cta-primary:hover { transform: none; }
    .felt-chip:hover, .avatar-pick:hover { transform: none; }
    .level-card:not(.locked):hover { transform: none; }


    /* 34px is under every platform's minimum tap target. The icons keep their
       drawn size; only the hit area grows. */
    .icon-btn { width: 42px; height: 42px; }
    .avatar-pick { width: 42px; height: 42px; }
    .diff-chip { padding-block: 10px; }
    .btn-primary, .btn-ghost { padding-block: 14px; }
}
/* Note there is deliberately no `.rack-ball` here. Everything above is a hit
   target being grown to thumb size; the rack is a readout nobody touches, and
   sizing it up on a touch device meant a phone — where the bar has the least
   room of anywhere — was the one place asking for the biggest rack. It landed
   after the width breakpoints in the cascade and quietly outranked the fold. */
/* The merge had left this block written three times over: once plainly, once
   again immediately after itself, and a third time inside a `.touch-device`
   wrapper that nothing in the markup ever carries — with a brace short at the
   end, so a rule three levels deep was closing the media query. One copy. */

/* =========================================================================
   SHORT VIEWPORTS
   A phone held sideways has the aspect ratio for the table and almost no room
   for anything else, so the chrome above and below it gives way first.
   ========================================================================= */

@media (max-height: 560px) {
    :root { --topbar-h: 46px; }
    .table-area { padding-block: 0; }
    .status-line { min-height: 30px; padding: 2px 8px; }
    .status-pill { padding: 4px 12px; font-size: 11px; }
    .seat-avatar-ring { width: 34px; height: 34px; }
    .seat-avatar { font-size: 15px; }
    .shot-clock { font-size: 12px; padding-block: 0; }
    .seat-vs { display: none; }
}

@media (orientation: landscape) and (max-height: 500px) and (pointer: coarse) {
    :root { --topbar-h: 42px; }

    /* Reach under a notch, but keep the rails out from under it. */
    .match-screen {
        padding-left: env(safe-area-inset-left);
        padding-right: env(safe-area-inset-right);
    }

    .topbar { padding: 0 8px; gap: 8px; }
    .icon-btn { width: 30px; height: 30px; font-size: 15px; }
    .seat { padding: 3px 6px; gap: 7px; }
    .seat-avatar-ring { width: 30px; height: 30px; }
    .seat-avatar { inset: 4px; font-size: 13px; }

    .table-area { gap: 8px; padding: 0 6px; }

    /* The caption floats over the cloth instead of reserving a row. It is a
       caption — it is not worth a tenth of a phone's screen. */
    .status-line {
        position: absolute;
        left: 0;
        right: 0;
        bottom: 2px;
        min-height: 0;
        z-index: 15;
        pointer-events: none;   /* never eats a touch meant for the table */
    }
    .status-pill { font-size: 10.5px; padding: 3px 11px; }

    /* Still thumb-sized — they are drag targets — but stripped of their
       captions and their air, and run the full height they are given. */
    .shot-rail { gap: 10px; }
    .rail-cap, .power-val { display: none; }
    .spin-disc { width: 48px; height: 48px; }
    .spin-dot { width: 13px; height: 13px; }
    /* Bounded, not `height: 100%`: the trough's length is the draw, and a
       trough as tall as the table would ask for a pull no thumb can finish. */
    .power-track { width: 40px; height: min(170px, 52vh); min-height: 96px; }
    .cue-stick { width: 22px; }

    /* The controls float over the cloth instead of standing beside it.
       Measured: once the chrome above and below is dealt with, the table stops
       being limited by height and starts being limited by width — the side
       rails are what is left holding it in. Taking them out of the row is the
       only thing that makes the table meaningfully bigger from here, and it is
       what every pool game on a phone does.

       They sit over the rail and cushion at the very edge of the table, not
       over the middle of the playing surface. */
    .table-area { position: relative; padding-inline: 0; }
    .table-area > #canvasContainer { width: 100%; }

    .shot-rail, .pot-tray {
        position: absolute;
        top: 0;
        bottom: 0;
        z-index: 12;
        padding-inline: 4px;
    }
    .shot-rail { left: 0; justify-content: center; }
    .pot-tray  { right: 0; }

    .pot-tray-track { width: 22px; --tray-ball: 16px; height: 100%; max-height: none; }
    .tray-ball { font-size: 7.5px; }
    .pipe-mouth { height: 16px; }

    /* The base rule already scales with the viewport; a short landscape screen
       is wide, so this pulls the ceiling down rather than raising the floor —
       2.4vw of 900px would otherwise be larger here than on a desktop. */
    .table-announce { font-size: clamp(11px, 2vw, 14px); padding: 5px 12px; top: 3%; }
}
