/* ============================================================================
   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-search-input { margin-top: 1rem; max-width: 24rem; }
.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; }
/* Library grid: "Done" isn't shown (pages/library.py's _status_pill_class) — the pill
   element is kept (poll_library_statuses updates it by id, never adds/removes it) but
   hidden, so it disappears whether a job was already done on page load or just finished. */
.status-pill-hidden { display: none; }

/* ── 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;
}
/* Positions timeline (pages/results.py's _positions_timeline_figure, ported from
   poc/v01/b91_padel_shots' rally timeline) — a separate pane, sibling to
   #results-positions-pane rather than nested inside it (see that Python function's own
   comment on why: the landscape/wide grid below places the map beside the video and this
   chart spanning full width under both, which two nested levels of one element can't do at
   once). #results-positions-timeline-section itself is a plain block pane, same as
   #results-rally-pane/#results-shots-pane — no default display rule here (it has no inline
   style when Positions is the tab active on first render, same as #results-positions-pane
   above; switch_results_view sets "block"/"none" itself once a tab is actually clicked).
   Its CONTENTS, though (the toggle + chart), are hidden below the 768px breakpoint (see the
   @media block further down) where the court diagram above it already fills a phone
   screen's own vertical budget; a third chart there would be clutter, not insight. */
.positions-timeline-head { display: none; }
/* position: relative — the offset parent for .positions-timeline-playhead below, an
   absolutely positioned div overlaid on top of the chart (not a Plotly shape — see that
   element's own comment in pages/results.py for why). overflow: hidden clips it if the
   video's current time ever computes just outside the chart's own plotted range. */
.positions-timeline-pane { display: none; position: relative; overflow: hidden; }
.positions-timeline-playhead {
    position: absolute;
    top: 0;
    bottom: 0;
    width: 1px;
    background: #2dd4bf;
    pointer-events: none;
    /* Hidden until assets/results_video.js's onFrame hook has computed a real pixel
       position from the plot's own axis — avoids a stray line at the left edge (x: 0
       before it's ever run) while the video is still loading. */
    display: none;
}
/* 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,
#results-positions-timeline-section .js-plotly-plot,
#results-positions-timeline-section .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, one per shot type. Doubles as the shot-type filter
   for .shot-dots below (assets/results_reel.js's activeShotTypes/applyShotTypeFilter — a
   client-side filter, not a Dash callback) — no longer tap-to-play itself. ────────────────── */
.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); }
/* Inactive per the current shot-type filter (toggle_shot_type_filter) — dimmed rather than
   hidden, so it stays reachable to tap back on. */
.shot-chip.inactive { opacity: 0.35; }
.shot-chip.inactive:hover { opacity: 0.55; }
.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, now
   grouped into one labeled cluster per rally (pages/results.py's _shot_sequence_dots). */
.shot-dots {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    padding: 0.6rem 0.1rem 0.1rem;
}
.shot-dot-group {
    display: flex;
    align-items: center;
    gap: 0.6rem;
}
.shot-dot-group-label {
    flex: 0 0 auto;
    min-width: 4.2rem;
    font-size: 0.72rem;
    font-weight: 700;
    color: var(--text-dim);
    letter-spacing: 0.2px;
}
.shot-dot-group-dots {
    display: flex;
    flex-wrap: wrap;
    gap: 2px;
}
.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; }
    /* Screen's finally wide enough to fit a second chart below the court without either one
       feeling cramped — see .positions-timeline-pane's own comment above for why it's
       hidden below this breakpoint at all. */
    .positions-timeline-head {
        display: flex;
        justify-content: center;
        margin-bottom: 0.4rem;
    }
    .positions-timeline-pane {
        display: block;
        width: 100%;
        height: 220px;
    }
    .shots-pane .shot-menu {
        flex-wrap: wrap;
        overflow-x: visible;
    }
}

/* ── Positions tab, landscape on a screen bigger than a phone: there's width to spare a
   portrait phone never has, so the court map moves beside the video instead of stacking
   below it, and the timeline moves to its own row underneath both, with the a20_insights
   stats column now sitting beside the chart instead of beside the map (per discussion).
   Scoped to #results-status-body.positions-active (switch_results_view's own className
   toggle, set to match whichever tab is actually active, seeded to "positions-active" on
   first render in layout() since Positions is the tab shown there) — Rally and Shots have
   no map to move, so they stay in the plain stacked layout above regardless of screen size
   or orientation; switching to either drops this class and the grid below along with it. ── */
@media (min-width: 768px) and (orientation: landscape) {
    #results-status-body.positions-active {
        display: grid;
        /* The right column sizes to its own content's intrinsic width — the court diagram's
           existing "width: calc(...)" in the map row, the 168px stats sidebar in the chart
           row below it (#results-positions-stats' own width, unchanged) — rather than an
           even split second-guessing either. Video/chart both get whatever's left in the
           left column. */
        grid-template-columns: minmax(0, 1fr) auto;
        grid-template-areas:
            "video map"
            "tabs  tabs"
            "chart stats"
            "scope scope";
        column-gap: 1rem;
        align-items: stretch;
    }
    #results-status-body.positions-active .results-video-section { grid-area: video; height: 100%; }
    #results-status-body.positions-active .segmented-control { grid-area: tabs; }
    #results-status-body.positions-active #results-positions-timeline-section { grid-area: chart; }
    #results-status-body.positions-active .scope-chip-row { grid-area: scope; }

    /* #results-positions-pane becomes an invisible wrapper here (display: contents) so its
       two children can be placed independently — the court diagram stays beside the video,
       the stats column moves to sit beside the timeline chart instead (per discussion).
       display: contents doesn't remove #results-positions-pane from the DOM or from
       descendant-selector matching (the .js-plotly-plot rule above still applies to its
       child), just from its own box. switch_results_view's inline display:flex/none is what
       decides this element's visibility up to here, so this needs !important to still win
       while Positions is the active tab. */
    #results-status-body.positions-active #results-positions-pane { display: contents !important; }
    #results-status-body.positions-active #results-positions-graph {
        grid-area: map;
        /* #results-positions-pane's own width/height (the base 768px rule, "calc(65vh/2 +
           168px + 32px)" / "65vh") no longer apply to anything real once it's display:
           contents — restated here, directly on the element that's now the actual grid
           item, so the map keeps its previous size instead of collapsing to whatever an
           "auto" grid track resolves to for a JS-rendered Plotly graph with no intrinsic
           width of its own (it was near-zero in practice) crossed with a percentage height
           with no definite containing block. Width drops the stats sidebar's own "+ 168px +
           32px" — that space belonged to #results-positions-stats, which isn't nested in
           here anymore either, so just the court's own 1:2 aspect ratio at this height
           remains: calc(65vh / 2). align-items: stretch above then expands the video beside
           it (.results-video-section, below) to match this same height, rather than the two
           independently taking whatever height their own content happens to produce. */
        width: calc(65vh / 2);
        height: 65vh;
    }
    #results-status-body.positions-active #results-positions-stats { grid-area: stats; }

    /* Video stretches to the row height #results-positions-graph sets above — same
       technique .video-stage:fullscreen already uses for a different reason (fitting the
       video's own aspect ratio into a box it doesn't control the shape of): height: 100%
       plus object-fit: contain, rather than distorting the picture to fill a differently-
       shaped box. */
    #results-status-body.positions-active .video-stage { height: 100%; }
    #results-status-body.positions-active .video-stage .results-video {
        width: auto;
        height: 100%;
        max-width: 100%;
        object-fit: contain;
    }

    /* Each of these is now sized by its own grid cell — drop the plain-stack centering the
       768px block above gives every .video-stage/.results-pane/.segmented-control, or that
       max-width would cap them well short of the width their grid cell actually offers. */
    #results-status-body.positions-active .video-stage,
    #results-status-body.positions-active .segmented-control,
    #results-status-body.positions-active #results-positions-timeline-section,
    #results-status-body.positions-active .scope-chip-row {
        max-width: none;
        margin-left: 0;
        margin-right: 0;
    }
}
