/* ============================================================================
   Padel webapp — base stylesheet
   Design tokens seeded from poc/v01/b91_padel_shots/assets/padel_shots.css, by request
   (webapp/DESIGN.md, "Visual design").
   ========================================================================== */

:root {
    --accent: #2dd4bf;          /* teal */
    --accent-dark: #14b8a6;
    --bg-panel: #1c1f26;
    --bg-panel-2: #23272f;
    --border-soft: rgba(255, 255, 255, 0.08);
    --text-dim: #8b93a1;
}

body {
    background: radial-gradient(1200px 600px at 50% -10%, #23303a 0%, #12151b 55%) fixed;
    margin: 0;
}

/* ── App shell ───────────────────────────────────────────────────────────── */
.app-shell {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
}
.app-content {
    flex: 1;
    padding: 1rem 0.75rem;
}
@media (min-width: 768px) {
    .app-content { padding: 1.5rem 1rem 2rem; }
}

/* /results is meant to read as a native app screen, not a page in the site — its own
   results-header already has a back chevron + title, duplicating the top bar's own way
   back. Drop it, and app-content's own padding around results-screen, only on that page,
   via :has() rather than a body/route class, so nothing elsewhere has to know this page
   exists. */
.app-shell:has(.results-screen) .app-topbar { display: none; }
.app-shell:has(.results-screen) .app-content { padding: 0; }

/* ── Top bar ─────────────────────────────────────────────────────────────── */
.app-topbar {
    position: sticky;
    top: 0;
    z-index: 1000;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: 0.6rem 1rem;
    background: rgba(18, 21, 27, 0.92);
    backdrop-filter: blur(8px);
    border-bottom: 1px solid var(--border-soft);
    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.35);
}
.app-brand-group { display: flex; flex-direction: column; line-height: 1.1; }
.app-brand { font-size: 1.15rem; font-weight: 700; letter-spacing: 0.2px; color: #fff; }
.app-subtitle {
    font-size: 0.68rem;
    text-transform: uppercase;
    letter-spacing: 1.5px;
    color: var(--accent);
}
.app-user-menu {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.25rem 0.6rem 0.25rem 0.25rem;
    border: 1px solid var(--border-soft);
    border-radius: 999px;
    color: var(--text-dim);
    font-size: 0.85rem;
}
.user-avatar {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    border-radius: 50%;
    background: var(--bg-panel-2);
    color: var(--accent);
    font-weight: 700;
    font-size: 0.8rem;
}
/* The display-name link (→ /account) sits inside the compact user-menu pill, not the
   44px-tall primary nav — override .app-nav-link's touch-target sizing so it stays
   pill-sized, while keeping the same hover/color behavior as every other nav link. */
.user-menu-label {
    padding: 0 !important;
    min-height: 0 !important;
    font-weight: 600;
}
.user-menu-label:hover { text-decoration: underline; }

/* ── User-menu links (Log in/Sign up, display name, Admin, Log out) ────────── */
.app-nav-link {
    color: var(--text-dim) !important;
    font-size: 0.9rem;
    font-weight: 600;
    padding: 0.5rem 0.75rem !important;
    border-radius: 8px;
    min-height: 44px;
    display: flex;
    align-items: center;
}
/* logout-button is a <button>, not an <a> (see app.py) — strip default button chrome so
   it matches the surrounding .app-nav-link anchors exactly. */
.logout-button {
    background: none;
    border: none;
    font-family: inherit;
    cursor: pointer;
}
.app-nav-link:hover,
.app-nav-link.active {
    color: #e6e9ee !important;
    background: var(--bg-panel-2);
}

/* ── Page placeholder card (milestone 1 scaffold only) ─────────────────────── */
.page-placeholder-wide,
.page-placeholder-narrow {
    padding: 0;
}
.page-placeholder-narrow { max-width: 480px; margin: 2rem auto 0; }
.page-placeholder-wide { max-width: 960px; margin: 1rem auto 0; }
.page-placeholder-card {
    background: var(--bg-panel);
    border: 1px solid var(--border-soft);
    border-radius: 14px;
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
}
.page-placeholder-title { color: #fff; font-size: 1.3rem; margin-bottom: 0.5rem; }
.page-placeholder-text { color: var(--text-dim); margin-bottom: 0; }

/* ── Auth forms (login/signup) ──────────────────────────────────────────── */
.auth-card .page-placeholder-title { text-align: center; }
.auth-card input.form-control {
    background: var(--bg-panel-2);
    border: 1px solid var(--border-soft);
    color: #e6e9ee;
    min-height: 44px;
}
.auth-card input.form-control:focus {
    background: var(--bg-panel-2);
    color: #e6e9ee;
    border-color: var(--accent);
    box-shadow: 0 0 0 0.2rem rgba(45, 212, 191, 0.2);
}
.auth-card .btn-primary {
    background: var(--accent) !important;
    border-color: var(--accent) !important;
    color: #06231f !important;
    font-weight: 700;
    min-height: 44px;
}
.auth-message { color: #ff8a80; font-size: 0.85rem; min-height: 1.2em; }
.auth-switch { color: var(--text-dim); font-size: 0.85rem; text-align: center; }
.auth-switch a { color: var(--accent); }

/* ── User menu, signed-out state ────────────────────────────────────────── */
.app-user-menu-guest { border: none; padding: 0; gap: 0.75rem; }
.app-user-menu-guest .app-nav-link { padding: 0.5rem 0.25rem !important; }

/* ── Library grid + upload card (webapp/DESIGN.md, "Upload flow: a modal on /library") ── */
.library-grid {
    display: grid;
    grid-template-columns: 1fr;
    gap: 0.75rem;
    margin-top: 1rem;
}
@media (min-width: 768px) {
    .library-grid { grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); }
}
/* library-jobs wraps only the polled job cards (pages/library.py) so the upload card,
   rendered once outside it, never gets recreated by the poll — display:contents makes the
   wrapper itself invisible to the grid layout, so its children still lay out as direct
   grid items of .library-grid, same as before the split. */
.library-jobs { display: contents; }
.job-card {
    background: var(--bg-panel);
    border: 1px solid var(--border-soft);
    border-radius: 12px;
    padding: 1rem;
    min-height: 96px;
}
.upload-card {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.4rem;
    border: 2px dashed var(--border-soft);
    color: var(--accent);
    cursor: pointer;
    min-height: 44px;
    transition: border-color 0.12s ease, background 0.12s ease;
}
.upload-card:hover { border-color: var(--accent); background: var(--bg-panel-2); }
.upload-card-icon { font-size: 1.6rem; font-weight: 700; line-height: 1; }
.upload-card-label { font-size: 0.85rem; font-weight: 600; }
.library-empty {
    grid-column: 1 / -1;
    color: var(--text-dim);
    text-align: center;
    padding: 2rem 1rem;
}

/* dbc.Modal on the dark theme */
.modal-content {
    background: var(--bg-panel) !important;
    border: 1px solid var(--border-soft) !important;
    color: #e6e9ee;
}
.modal-header, .modal-footer { border-color: var(--border-soft) !important; }

/* ── Admin page (webapp/DESIGN.md, "/admin" page anatomy) ───────────────────
   Two sections, stacked on mobile, side by side on desktop. */
.admin-sections {
    display: flex;
    flex-direction: column;
    gap: 1.5rem;
    margin-top: 1rem;
}
@media (min-width: 768px) {
    .admin-sections { flex-direction: row; align-items: flex-start; }
    .admin-section { flex: 1; min-width: 0; }
}
.admin-section-title {
    font-size: 1rem;
    font-weight: 700;
    color: #fff;
    margin-bottom: 0.6rem;
}
.admin-list { display: flex; flex-direction: column; gap: 0.5rem; }
.admin-row {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    background: var(--bg-panel);
    border: 1px solid var(--border-soft);
    border-radius: 10px;
    padding: 0.6rem 0.75rem;
}
@media (min-width: 480px) {
    .admin-row { flex-direction: row; align-items: center; justify-content: space-between; }
}
.admin-row-info { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
.admin-row-name { font-weight: 700; color: #e6e9ee; }
.admin-row-email { color: var(--text-dim); font-size: 0.8rem; }
.admin-row-actions { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.admin-btn { min-height: 44px; font-size: 0.8rem; }
.admin-empty { color: var(--text-dim); padding: 0.75rem; text-align: center; }

.status-pill {
    font-size: 0.68rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    padding: 0.15rem 0.55rem;
    border-radius: 999px;
}
.status-pill-active { background: rgba(45, 212, 191, 0.18); color: var(--accent); }
.status-pill-view_only { background: rgba(255, 213, 79, 0.15); color: #ffd54f; }
.status-pill-revoked { background: rgba(255, 82, 82, 0.15); color: #ff8a80; }
.role-pill {
    font-size: 0.68rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    padding: 0.15rem 0.55rem;
    border-radius: 999px;
    background: rgba(45, 212, 191, 0.1);
    border: 1px solid rgba(45, 212, 191, 0.4);
    color: var(--accent);
}

/* ── Upload modal (webapp/DESIGN.md, "Progress shown to the user" — two separate bars) ── */
input[type="file"].form-control {
    background: var(--bg-panel-2);
    border: 1px solid var(--border-soft);
    color: var(--text-dim);
}
.progress-row {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    margin-bottom: 0.5rem;
}
.progress-row-label { color: var(--text-dim); font-size: 0.8rem; width: 80px; flex-shrink: 0; }
.progress-row-pct { color: var(--text-dim); font-size: 0.8rem; width: 40px; text-align: right; flex-shrink: 0; }
.progress-bar-track {
    flex: 1;
    height: 8px;
    border-radius: 999px;
    background: var(--bg-panel-2);
    border: 1px solid var(--border-soft);
    overflow: hidden;
}
.progress-bar-fill {
    height: 100%;
    width: 0%;
    background: var(--accent);
    border-radius: 999px;
    transition: width 0.2s ease;
}
.upload-status-label { color: var(--text-dim); font-size: 0.85rem; text-align: center; }

/* ── Job cards (webapp/DESIGN.md, "/library" page anatomy) ─────────────────── */
.job-card-link { text-decoration: none; color: inherit; display: block; }
.job-card-link:hover .job-card { border-color: var(--accent); }
.job-card-thumb {
    display: block;
    width: 100%;
    aspect-ratio: 16 / 9;
    border-radius: 8px;
    background: linear-gradient(135deg, #1a1f27 0%, #12151b 100%);
    margin-bottom: 0.6rem;
    object-fit: cover;
    /* Never intercept the tap/click — the whole card is a dcc.Link (pages/library.py);
       a <video> without pointer-events:none can otherwise steal it to show native
       playback controls on some browsers. */
    pointer-events: none;
}
.job-card-info { display: flex; flex-direction: column; gap: 0.3rem; align-items: flex-start; }
.job-card-title {
    font-weight: 700;
    color: #e6e9ee;
    font-size: 0.9rem;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.job-card-date { color: var(--text-dim); font-size: 0.75rem; }

/* Coarse job status (New/In progress/Done/Error) — reuses .status-pill's shape from the
   admin page's access-status pills, different color set. */
.status-pill-new { background: rgba(139, 147, 161, 0.15); color: var(--text-dim); }
.status-pill-in_progress { background: rgba(255, 213, 79, 0.15); color: #ffd54f; }
.status-pill-done { background: rgba(45, 212, 191, 0.18); color: var(--accent); }
.status-pill-error { background: rgba(255, 82, 82, 0.15); color: #ff8a80; }

/* ── Results screen (webapp/DESIGN.md milestone 6/7 — redesigned per discussion: "feel
   like a real smartphone app," organized around the two annotation families (Positions,
   Shots) and the padel entities (rally/team/player) as one readable scope line, not a
   stack of web-form controls). Full-bleed screen, not a boxed card on a page. ──────────── */
.results-screen { display: flex; flex-direction: column; min-height: 100%; }

.results-header {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.25rem 0.5rem;
    position: sticky;
    top: 0;
    z-index: 10;
    background: rgba(18, 21, 27, 0.92);
    backdrop-filter: blur(8px);
    border-bottom: 1px solid var(--border-soft);
}
.results-back-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 44px;
    min-height: 44px;
    font-size: 1.3rem;
    color: var(--text-dim);
    text-decoration: none;
}
.results-back-btn:hover { color: #e6e9ee; }
.results-header-title {
    flex: 1;
    font-weight: 700;
    color: #fff;
    font-size: 0.9rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ── Scope chip — the one line reading "which entity am I looking at" (Rally/Team/
   Player), tap to open results-filter-sheet. Sits at the *bottom* of the screen's content
   (per discussion), after the Positions/Shots panes — compact, not a big form-like bar. ── */
.scope-chip-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    width: calc(100% - 1.5rem);
    margin: 0.5rem 0.75rem 0.75rem;
    min-height: 38px;
    padding: 0 0.75rem;
    background: var(--bg-panel);
    border: 1px solid var(--border-soft);
    border-radius: 999px;
    color: #e6e9ee;
    font-size: 0.78rem;
    cursor: pointer;
}
.scope-chip-row:hover { border-color: var(--accent); }
.scope-chip-arrow { color: var(--text-dim); font-size: 1rem; }

/* ── Bottom sheets (dbc.Offcanvas, placement="bottom") — filters, overlay settings,
   edit — dark-themed to match, rounded top corners like a real native sheet.
   `className="bottom-sheet"` lands on the SAME element as react-bootstrap's own
   `offcanvas-bottom` class, not on some wrapper around it — and with placement="bottom"
   set, that element never carries a bare "offcanvas" token at all (react-bootstrap
   composes the class as `offcanvas-${placement}`, i.e. only "offcanvas-bottom"). A prior
   `.bottom-sheet .offcanvas` (descendant) rule here never matched anything at all, which
   is why the height override for it silently never took effect — this still scrolled no
   matter how tall max-height allowed it to be, because the *actual* dialog element kept
   Bootstrap's default height: 30vh untouched. `.offcanvas-header`/`.offcanvas-title`/
   `.offcanvas-body` genuinely are separate child elements, so those keep the descendant
   form below. ──────────────────────────────────────────────────────────────────────── */
.bottom-sheet {
    background: var(--bg-panel) !important;
    border-top: 1px solid var(--border-soft) !important;
    border-radius: 16px 16px 0 0;
    color: #e6e9ee;
    /* Bootstrap's own default here is a fixed height: 30vh, which is what forced the
       overlay-settings and edit sheets to scroll internally even though their content (a
       handful of grouped checkboxes, three form fields) easily fits on screen. Height now
       grows with content instead — max-height is a safety cap for genuinely long content
       on a short phone screen, not the normal case, so .offcanvas-body's own
       overflow-y: auto below stays a fallback, not how these are meant to be read. */
    height: auto !important;
    max-height: 85vh !important;
}
.bottom-sheet .offcanvas-header { border-bottom: 1px solid var(--border-soft); }
.bottom-sheet .offcanvas-title { color: #fff; font-weight: 700; }
.bottom-sheet .btn-close { filter: invert(1) grayscale(1) opacity(0.7); }
.bottom-sheet .offcanvas-body { overflow-y: auto; }

.filter-group { display: flex; flex-direction: column; gap: 0.4rem; margin-bottom: 1rem; }
.filter-label { color: var(--text-dim); font-size: 0.68rem; text-transform: uppercase; letter-spacing: 1px; }
.filter-radio-group .btn-outline-secondary {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    color: var(--text-dim);
    border-color: var(--border-soft);
}
.filter-radio-group .btn-check:checked + .btn-outline-secondary {
    background: var(--accent) !important;
    border-color: var(--accent) !important;
    color: #06231f !important;
}

/* ── Rally multiselect (results-filter-rally) — Dash 4's Radix-based dcc.Dropdown, same
   dark-theme override poc/v01/b91_padel_shots/assets/padel_shots.css already applies to
   its own dropdowns (.dash-dropdown/.dash-dropdown-content are that component's own class
   names, not ours to rename) — kept local to .filter-dropdown so it can't leak onto some
   other dcc.Dropdown this app adds later with different intent. The menu renders in a
   body-level portal, so its parts (.dash-dropdown-content/-option) are styled as globals,
   same as that reference. ──────────────────────────────────────────────────────────────── */
.filter-dropdown.dash-dropdown {
    background: var(--bg-panel-2) !important;
    border: 1px solid var(--border-soft) !important;
    border-radius: 9px !important;
    min-height: 44px !important;
}
.filter-dropdown .dash-dropdown-value,
.filter-dropdown .dash-dropdown-value-item { color: #e6e9ee !important; }
.filter-dropdown .dash-dropdown-value-item[data-placeholder],
.filter-dropdown .dash-dropdown-value[data-placeholder] { color: var(--text-dim) !important; }
.filter-dropdown .dash-dropdown-trigger-icon { color: var(--text-dim); }
/* Multi-select chips (one per selected rally) */
.filter-dropdown .dash-dropdown-value-item {
    background: rgba(45, 212, 191, 0.15);
    border: 1px solid rgba(45, 212, 191, 0.4);
    color: #d7fff8 !important;
    border-radius: 6px;
}
.dash-dropdown-content {
    background: var(--bg-panel-2) !important;
    border: 1px solid var(--border-soft) !important;
    border-radius: 10px !important;
}
.dash-dropdown-option {
    color: #e6e9ee !important;
    border-radius: 7px !important;
    padding: 0.4rem 0.6rem !important;
}
.dash-dropdown-option:hover,
.dash-dropdown-option[data-highlighted] {
    background: var(--accent-dark) !important;
    color: #06231f !important;
}
.dash-dropdown-option[data-state="checked"],
.dash-dropdown-option[aria-selected="true"] {
    background: rgba(45, 212, 191, 0.18) !important;
    color: #d7fff8 !important;
}
/* Search is disabled (searchable=False); hide any residual search UI */
.dash-dropdown-search,
.dash-dropdown-search-icon { display: none !important; }

/* ── Segmented control (Positions / Shots) — native-style pill toggle, not web tabs. The
   edit pencil lives in the header (see .results-header), not sharing this row. ──────────── */
.segmented-control {
    display: flex;
    gap: 2px;
    padding: 3px;
    margin: 0.5rem 0.75rem 0.6rem;
    background: var(--bg-panel);
    border: 1px solid var(--border-soft);
    border-radius: 12px;
}
.segment-btn {
    flex: 1;
    min-height: 40px;
    background: none;
    border: none;
    border-radius: 9px;
    color: var(--text-dim);
    font-weight: 600;
    font-size: 0.85rem;
    cursor: pointer;
}
.segment-btn.active { background: var(--accent); color: #06231f; }

/* ── Panes (Positions / Shots) — always mounted, visibility toggled in Python, never
   swapped via children (keeps the video from reloading on every switch). ────────────────── */
.results-pane { padding: 0 0.75rem 1rem; }
/* Sized to the real court's own aspect ratio (10m × 20m, same ratio _positions_figure's
   scaleanchor enforces) plus one fixed extra column (pages/results.py's own
   _STATS_WIDTH_PX, kept in sync by hand — that's what the "+ 168px" below is): the
   a20_insights stat column, down the right side, so the box IS court-plus-stats, not a
   taller/wider box the stats then sit inside of, letterboxed by Plotly with dead space
   around it — most visible on a wide screen, where a generous width budget left the
   (much narrower) court looking small and adrift in the middle of it. height is definite
   (a budget that leaves room for the video/switch/scope chip above and below it, ideally
   on one screen); width is computed from it directly (not via the `aspect-ratio`
   property, which can only express a pure ratio, not ratio-plus-a-fixed-offset), capped
   so it never overflows the available width on a narrow phone screen — there, width ends
   up the binding constraint instead (see the @media block below, where only height's own
   budget changes, using the same "/2 + 168px + 32px" formula).

   The extra "+ 32px" is this row's own horizontal chrome that isn't the court or the
   stats column: .results-pane's 0.75rem left + 0.75rem right padding (this element has
   both that class and this id), plus #results-positions-stats' 0.5rem margin-left — 2rem
   total. Bootstrap sets box-sizing: border-box globally (confirmed elsewhere in this
   file), so that padding/margin eats into this element's *own* declared width rather than
   sitting outside it; leaving it out of the formula was exactly what starved
   #results-positions-graph (flex: 1 1 0, i.e. "whatever's left") of ~32px it needed to
   hit the court's exact 1:2 ratio, which is what Plotly's scaleanchor then padded out as
   empty letterbox space above and below the court instead of erroring or clipping. */
#results-positions-pane {
    width: calc(40vh / 2 + 168px + 32px);
    max-width: 100%;
    height: 40vh;
    min-height: 220px;
    margin: 0 auto;
    /* Row so the a20_insights stat column (#results-positions-stats) sits beside the court
       diagram, not below it — the graph is the flexible item (min-width: 0 lets it
       actually shrink for the stats column's room), the stats column is a fixed width.
       overflow-x: auto is the fallback if a narrow screen still can't fit both (rather
       than silently clipping the stats column). */
    display: flex;
    flex-direction: row;
    align-items: stretch;
    overflow-x: auto;
}
/* flex-basis: 0, not auto — the graph's own inline height: 100% (dcc.Graph's style prop)
   would otherwise compete with flex-grow for how big "auto" content sizing means here. */
#results-positions-graph { flex: 1 1 0; min-width: 0; }
#results-positions-pane .js-plotly-plot,
#results-positions-pane .plot-container { height: 100% !important; width: 100% !important; }

/* a20_insights numbers for the current Team/Player scope (results.py's _positions_stats)
   — one line per player/pair, same neutral/descriptive tone as the rest of this app. A
   fixed-width column beside the court (pages/results.py's _STATS_WIDTH_PX), not sized to
   its own content, so it doesn't shove the court around as the text length changes between
   Team/Player filters. */
#results-positions-stats {
    flex: 0 0 168px;
    width: 168px;
    overflow-y: auto;
    font-size: 0.76rem;
    line-height: 1.45;
    color: var(--text-dim);
    padding: 0 0 0 0.75rem;
    margin-left: 0.5rem;
    border-left: 1px solid var(--border-soft);
}
/* _positions_stats() returns its own wrapper (.positions-stats, holding the actual
   .positions-stat-block elements), nested one level inside #results-positions-stats above
   — the space-between below has to live on THIS element, not its parent, or it has only
   one child (this whole div) to "space between" and visibly does nothing. height: 100%
   makes it fill #results-positions-stats' own already-stretched height (align-items:
   stretch on #results-positions-pane, the flex row both sit in), so pinning this div's
   first/last child to its own top/bottom edges pins them to the court's top/bottom too —
   #results-positions-graph fills that same row's full height and _positions_figure plots
   with zero top/bottom margin, so "top/bottom of this column" already IS "top/bottom of
   the court." Degrades sensibly for a single block (Player scope): just top-aligned,
   nothing to space between. */
.positions-stats {
    height: 100%;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
}
.positions-stat-block { margin-bottom: 0.7rem; }
.positions-stat-block:last-child { margin-bottom: 0; }
/* A lone block that represents the near team (a single near-team player, or Team narrowed
   to Near) — pages/results.py's _stat_block_class. margin-top: auto in a space-between
   flex column consumes all the free space above it, pushing it down to the bottom of
   #results-positions-stats instead of sitting at the top (space-between's default for a
   single child), matching the near team's own bottom-of-frame position in the video. */
.positions-stat-block-end { margin-top: auto; }
.positions-stat-title { color: #e6e9ee; font-weight: 600; margin-bottom: 0.15rem; }
.positions-stat-line { margin-bottom: 0.1rem; }
/* white-space: nowrap keeps a note (e.g. "split up/back — not enough data") from wrapping
   mid-phrase across two lines in the 168px stats column; overflow/text-overflow truncate
   with an ellipsis in the rare case a note is still too long for it, rather than spilling
   past the column's edge. */
.positions-stat-note {
    font-style: italic;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* Two sub-groups per block (pages/results.py's _stat_block): the net/transition/baseline
   zone split, then a visually separated second group for the other numbers (distance;
   spacing + split count for a pair) — a different kind of stat, not a fourth line of the
   same three-way 100% split, so a thin rule marks the boundary rather than letting all the
   lines run together looking like one family of numbers. */
.positions-stat-group-extra {
    margin-top: 0.4rem;
    padding-top: 0.4rem;
    border-top: 1px dashed var(--border-soft);
}

/* ── Rally tab (results.py's _rally_pane) — a plain scrolling list, the same "app screen,
   not a form" tone as everything else on this page rather than a table. ──────────────────── */
.rally-list { display: flex; flex-direction: column; gap: 0.6rem; padding: 0.25rem 0 0.5rem; }
/* Glanceable totals above the list (webapp/DESIGN.md's "overview strip") — two stat tiles,
   not a sentence, so "how much is here" reads in one glance before scrolling the list. */
.overview-strip { display: flex; gap: 0.75rem; margin-bottom: 0.25rem; }
.overview-stat {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.1rem;
    padding: 0.6rem 0.5rem;
    background: var(--bg-panel);
    border: 1px solid var(--border-soft);
    border-radius: 12px;
}
.overview-stat-value { color: #fff; font-weight: 700; font-size: 1.3rem; }
.overview-stat-label {
    color: var(--text-dim);
    font-size: 0.68rem;
    text-transform: uppercase;
    letter-spacing: 1px;
}
.rally-card {
    background: var(--bg-panel);
    border: 1px solid var(--border-soft);
    border-radius: 12px;
    padding: 0.65rem 0.8rem;
}
.rally-card-header {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.5rem;
    margin-bottom: 0.35rem;
}
.rally-card-title { color: #fff; font-weight: 700; font-size: 0.9rem; }
.rally-card-meta { color: var(--text-dim); font-size: 0.75rem; white-space: nowrap; }
.rally-card-row { display: flex; gap: 0.35rem; font-size: 0.78rem; margin-top: 0.15rem; }
.rally-card-team { color: var(--text-dim); flex: 0 0 auto; }
.rally-card-zones { color: #e6e9ee; }

/* ── Video stage — same shape as b91_padel_shots'/backend's test frontend's own video
   stage: the raw clip plays at its own size, the overlay canvas sits exactly on top,
   undecorated. Icon buttons live in the video's own corner, like a real player's own
   controls, instead of a text link sitting below it. ─────────────────────────────────────── */
.video-stage {
    position: relative;
    width: 100%;
    background: #000;
    border-radius: 14px;
    overflow: hidden;
    border: 1px solid var(--border-soft);
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
}
.results-video { width: 100%; height: auto; display: block; background: #000; }
.results-overlay-canvas { position: absolute; top: 0; left: 0; pointer-events: none; }

.video-icon-row {
    position: absolute;
    bottom: 0.6rem;
    right: 0.6rem;
    display: flex;
    gap: 0.4rem;
}
.video-icon-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 44px;
    min-height: 44px;
    background: rgba(18, 21, 27, 0.75);
    border: 1px solid rgba(255, 255, 255, 0.15);
    border-radius: 999px;
    color: #fff;
    font-size: 1.1rem;
    cursor: pointer;
    backdrop-filter: blur(4px);
}
.video-icon-btn:hover { background: rgba(18, 21, 27, 0.92); border-color: var(--accent); }

/* Fullscreening #results-video-stage itself (assets/results_video.js), not the <video>
   alone, is what keeps the overlay canvas visible in fullscreen — a sibling element never
   comes along if only the <video> is asked to fullscreen. Letterbox rather than stretch. */
.video-stage:fullscreen,
.video-stage:-webkit-full-screen,
.video-stage:-moz-full-screen {
    width: 100vw;
    height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 0;
    border: none;
}
.video-stage:fullscreen .results-video,
.video-stage:-webkit-full-screen .results-video,
.video-stage:-moz-full-screen .results-video {
    width: auto;
    height: 100%;
    max-width: 100%;
    object-fit: contain;
}

/* Every overlay toggle, grouped into sections (pages/results.py's _overlay_sheet_groups) —
   Detections / Shots / Positional insights / Other, always all four, stacked, each with a
   small caps label above its own row of checkboxes. */
.overlay-group-label:first-child { margin-top: 0; }
.overlay-group-label {
    margin-top: 0.9rem;
    margin-bottom: 0.35rem;
    font-size: 0.72rem;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-dim);
}
.overlay-toggles {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem 0.9rem;
}
.overlay-toggle-label {
    display: flex;
    align-items: center;
    gap: 0.3rem;
    color: var(--text-dim);
    font-size: 0.8rem;
    cursor: pointer;
}

/* ── Shot tray (poc/v01/b91_padel_shots' own shot-menu/shot-chip pattern, same color
   palette) — horizontal-scrolling chips, tap to play, no separate button. ────────────────── */
.shot-menu {
    display: flex;
    gap: 0.5rem;
    overflow-x: auto;
    padding: 0.75rem 0.1rem 0.25rem;
    scrollbar-width: thin;
    scrollbar-color: var(--border-soft) transparent;
}
.shot-chip {
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    min-width: 76px;
    min-height: 56px;
    padding: 0.5rem 0.75rem;
    background: var(--bg-panel);
    border: 1px solid var(--border-soft);
    border-top: 3px solid var(--chip-color, var(--accent));
    border-radius: 10px;
    cursor: pointer;
}
.shot-chip:hover { background: var(--bg-panel-2); transform: translateY(-2px); }
.shot-chip-type {
    font-size: 0.72rem;
    font-weight: 800;
    letter-spacing: 0.3px;
    color: var(--chip-color, var(--accent));
    white-space: nowrap;
}
.shot-chip-count { font-size: 0.85rem; font-weight: 700; color: #e6e9ee; }

/* ── Shot sequence — colored bullets (poc/v01/b91_padel_shots' own rally-dots, "so the
   rally's shape is visible at a glance") — one dot per shot, chronological, tap to play. */
.shot-dots {
    display: flex;
    flex-wrap: wrap;
    gap: 2px;
    padding: 0.6rem 0.1rem 0.1rem;
}
.shot-dot {
    /* Bootstrap sets box-sizing: border-box globally, which would make the 6px padding
       below eat the entire 12px width/height, leaving a 0x0 content-box for
       background-clip to paint into — an invisible dot, confirmed the hard way. Overriding
       box-sizing here is what actually makes the "bigger tap target, small visible dot"
       trick work: content-box makes width/height apply to the dot alone, padding adds on
       top for the tap area, same as originally intended. */
    box-sizing: content-box;
    width: 12px;
    height: 12px;
    padding: 6px; /* enlarges the tap target without enlarging the visible dot itself */
    background-clip: content-box;
    border: none;
    border-radius: 50%;
    cursor: pointer;
    opacity: 0.9;
}
.shot-dot:hover { opacity: 1; transform: scale(1.25); }
/* assets/results_reel.js toggles this on whichever dot's shot is currently playing back in
   a chip's reel — a ring in the dot's own color (visible against any dot color, unlike a
   single fixed outline color) plus a growth big enough to read as "now playing" at this
   dot's small size. */
.shot-dot.active {
    opacity: 1;
    transform: scale(1.12);
    /* --dot-color: the same shot-type color the dot's own inline backgroundColor already
       uses (pages/results.py's _shot_sequence_dots sets both together) — box-shadow can't
       read an inline background-color directly, so it's threaded through as a custom
       property instead. */
    box-shadow: 0 0 0 1px var(--bg-panel), 0 0 0 2px var(--dot-color, #fff);
}

/* ── Desktop: a wider centered column instead of a narrow one stretched full-width —
   using the extra space instead of just centering a phone-width layout (webapp/DESIGN.md's
   own milestone-8 commitment). The video is always visible above the Positions/Shots
   switch now (per discussion), so it gets the same centered treatment as everything else
   rather than a side-by-side split. ─────────────────────────────────────────────────────── */
@media (min-width: 768px) {
    .results-header,
    .video-stage,
    .segmented-control,
    .scope-chip-row,
    .results-pane {
        max-width: 900px;
        margin-left: auto;
        margin-right: auto;
    }
    #results-positions-pane { width: calc(65vh / 2 + 168px + 32px); height: 65vh; }
    .shots-pane .shot-menu {
        flex-wrap: wrap;
        overflow-x: visible;
    }
}
