/*
 * Custom color palette (Bootstrap overrides)
 *
 * Navbar:    #731d9e  (deep purple — inline style on <nav>)
 * Suggestion:#731d9e  (same purple — .btn-suggestion, computed-aid pills)
 * Primary:   #2563eb  (blue)
 * Secondary: #6d6875  (warm slate)
 * Success:   #2d6a4f  (forest green)
 * Danger:    #b5303f  (dark rose)
 * Warning:   #ebb000  (warm gold)
 * Info:      #17a2b8  (teal)
 * Light:     #f8f9fa  (Bootstrap default)
 * Dark:      #2b2f33  (charcoal)
 */
@view-transition {
    navigation: auto;
}

::view-transition-old(root) {
    animation-duration: 150ms;
}
::view-transition-new(root) {
    animation-duration: 150ms;
}

:root {
    /*
     * Declare support for both schemes so Android WebView (the Google app's
     * in-app browser, browser auto-dark, ...) treats us as owning our own dark
     * theme and backs off its algorithmic darkening. A `light only` declaration
     * is read as "no dark theme -> darken me" and is exactly what wrecks the
     * target renderer's colours on dark-themed Android phones.
     *
     * No real dark theme yet (ROADMAP: "well controlled dark mode"): we force the
     * used scheme back to light when dark is preferred, so the app still renders
     * light everywhere while WebView keeps its hands off. Swap the block below for
     * an actual dark theme when that lands.
     */
    color-scheme: light dark;
}

@media (prefers-color-scheme: dark) {
    :root {
        color-scheme: light;
    }
}

:root, [data-bs-theme="light"] {
    --bs-primary: #2563eb;
    --bs-primary-rgb: 37, 99, 235;
    --bs-secondary: #6d6875;
    --bs-secondary-rgb: 109, 104, 117;
    --bs-success: #2d6a4f;
    --bs-success-rgb: 45, 106, 79;
    --bs-danger: #b5303f;
    --bs-danger-rgb: 181, 48, 63;
    --bs-warning: #ebb000;
    --bs-warning-rgb: 235, 176, 0;
    --bs-info: #17a2b8;
    --bs-info-rgb: 23, 162, 184;
    --bs-light: #f8f9fa;
    --bs-light-rgb: 248, 249, 250;
    --bs-dark: #2b2f33;
    --bs-dark-rgb: 43, 47, 51;
    --bs-link-color: #2563eb;
    --bs-link-color-rgb: 37, 99, 235;
    --bs-link-hover-color: #1e50bc;
    --bs-link-hover-color-rgb: 30, 80, 188;
}

/* --- Primary #2563eb --- */
.btn-primary {
    --bs-btn-bg: #2563eb;
    --bs-btn-border-color: #2563eb;
    --bs-btn-hover-bg: #1e50bc;
    --bs-btn-hover-border-color: #1a4ab0;
    --bs-btn-active-bg: #1a4ab0;
    --bs-btn-active-border-color: #1744a4;
    --bs-btn-disabled-bg: #2563eb;
    --bs-btn-disabled-border-color: #2563eb;
}
.btn-outline-primary {
    --bs-btn-color: #2563eb;
    --bs-btn-border-color: #2563eb;
    --bs-btn-hover-bg: #2563eb;
    --bs-btn-hover-border-color: #2563eb;
    --bs-btn-active-bg: #1e50bc;
    --bs-btn-active-border-color: #1e50bc;
    --bs-btn-disabled-color: #2563eb;
    --bs-btn-disabled-border-color: #2563eb;
}
.alert-primary {
    --bs-alert-color: #0f3780;
    --bs-alert-bg: #cfe2ff;
    --bs-alert-border-color: #b6d4fe;
    --bs-alert-link-color: #0f3780;
}
.text-bg-primary {
    background-color: #cfe2ff !important;
    color: #0f3780 !important;
}

/* --- Secondary #6d6875 --- */
.btn-secondary {
    --bs-btn-bg: #6d6875;
    --bs-btn-border-color: #6d6875;
    --bs-btn-hover-bg: #5c5963;
    --bs-btn-hover-border-color: #55515d;
    --bs-btn-active-bg: #55515d;
    --bs-btn-active-border-color: #4e4a56;
    --bs-btn-disabled-bg: #6d6875;
    --bs-btn-disabled-border-color: #6d6875;
}
.btn-outline-secondary {
    --bs-btn-color: #6d6875;
    --bs-btn-border-color: #6d6875;
    --bs-btn-hover-bg: #6d6875;
    --bs-btn-hover-border-color: #6d6875;
    --bs-btn-active-bg: #5c5963;
    --bs-btn-active-border-color: #5c5963;
    --bs-btn-disabled-color: #6d6875;
    --bs-btn-disabled-border-color: #6d6875;
}
.alert-secondary {
    --bs-alert-color: #41383e;
    --bs-alert-bg: #e2e3e5;
    --bs-alert-border-color: #d3d3d6;
    --bs-alert-link-color: #41383e;
}
.text-bg-secondary {
    background-color: #e2e3e5 !important;
    color: #41383e !important;
}

/* --- Success #2d6a4f --- */
.btn-success {
    --bs-btn-bg: #2d6a4f;
    --bs-btn-border-color: #2d6a4f;
    --bs-btn-hover-bg: #255843;
    --bs-btn-hover-border-color: #21503d;
    --bs-btn-active-bg: #21503d;
    --bs-btn-active-border-color: #1d4837;
    --bs-btn-disabled-bg: #2d6a4f;
    --bs-btn-disabled-border-color: #2d6a4f;
}
.btn-outline-success {
    --bs-btn-color: #2d6a4f;
    --bs-btn-border-color: #2d6a4f;
    --bs-btn-hover-bg: #2d6a4f;
    --bs-btn-hover-border-color: #2d6a4f;
    --bs-btn-active-bg: #255843;
    --bs-btn-active-border-color: #255843;
    --bs-btn-disabled-color: #2d6a4f;
    --bs-btn-disabled-border-color: #2d6a4f;
}
.alert-success {
    --bs-alert-color: #122a20;
    --bs-alert-bg: #c0e3d0;
    --bs-alert-border-color: #9bd5b5;
    --bs-alert-link-color: #122a20;
}
.text-bg-success {
    background-color: #c0e3d0 !important;
    color: #122a20 !important;
}

/* --- Danger #b5303f --- */
.btn-danger {
    --bs-btn-bg: #b5303f;
    --bs-btn-border-color: #b5303f;
    --bs-btn-hover-bg: #9a2936;
    --bs-btn-hover-border-color: #902632;
    --bs-btn-active-bg: #902632;
    --bs-btn-active-border-color: #86232e;
    --bs-btn-disabled-bg: #b5303f;
    --bs-btn-disabled-border-color: #b5303f;
}
.btn-outline-danger {
    --bs-btn-color: #b5303f;
    --bs-btn-border-color: #b5303f;
    --bs-btn-hover-bg: #b5303f;
    --bs-btn-hover-border-color: #b5303f;
    --bs-btn-active-bg: #9a2936;
    --bs-btn-active-border-color: #9a2936;
    --bs-btn-disabled-color: #b5303f;
    --bs-btn-disabled-border-color: #b5303f;
}
.alert-danger {
    --bs-alert-color: #6b1d26;
    --bs-alert-bg: #f3d5d9;
    --bs-alert-border-color: #ecc2c8;
    --bs-alert-link-color: #6b1d26;
}
.text-bg-danger {
    background-color: #f3d5d9 !important;
    color: #6b1d26 !important;
}

/* --- Warning #ebb000 --- */
.btn-warning {
    --bs-btn-bg: #ebb000;
    --bs-btn-border-color: #ebb000;
    --bs-btn-color: #000;
    --bs-btn-hover-bg: #c89600;
    --bs-btn-hover-border-color: #bc8d00;
    --bs-btn-hover-color: #000;
    --bs-btn-active-bg: #bc8d00;
    --bs-btn-active-border-color: #b08400;
    --bs-btn-active-color: #000;
    --bs-btn-disabled-bg: #ebb000;
    --bs-btn-disabled-border-color: #ebb000;
}
.btn-outline-warning {
    --bs-btn-color: #ebb000;
    --bs-btn-border-color: #ebb000;
    --bs-btn-hover-bg: #ebb000;
    --bs-btn-hover-border-color: #ebb000;
    --bs-btn-hover-color: #000;
    --bs-btn-active-bg: #c89600;
    --bs-btn-active-border-color: #c89600;
    --bs-btn-active-color: #000;
    --bs-btn-disabled-color: #ebb000;
    --bs-btn-disabled-border-color: #ebb000;
}
.alert-warning {
    --bs-alert-color: #664d03;
    --bs-alert-bg: #fdf3cd;
    --bs-alert-border-color: #fbe9a2;
    --bs-alert-link-color: #664d03;
}
.text-bg-warning {
    background-color: #fdf3cd !important;
    color: #664d03 !important;
}

/* --- Info #17a2b8 --- */
.btn-info {
    --bs-btn-bg: #17a2b8;
    --bs-btn-border-color: #17a2b8;
    --bs-btn-color: #fff;
    --bs-btn-hover-bg: #138a9c;
    --bs-btn-hover-border-color: #117e8f;
    --bs-btn-hover-color: #fff;
    --bs-btn-active-bg: #117e8f;
    --bs-btn-active-border-color: #0f7283;
    --bs-btn-active-color: #fff;
    --bs-btn-disabled-bg: #17a2b8;
    --bs-btn-disabled-border-color: #17a2b8;
}
.btn-outline-info {
    --bs-btn-color: #17a2b8;
    --bs-btn-border-color: #17a2b8;
    --bs-btn-hover-bg: #17a2b8;
    --bs-btn-hover-border-color: #17a2b8;
    --bs-btn-hover-color: #fff;
    --bs-btn-active-bg: #138a9c;
    --bs-btn-active-border-color: #138a9c;
    --bs-btn-active-color: #fff;
    --bs-btn-disabled-color: #17a2b8;
    --bs-btn-disabled-border-color: #17a2b8;
}
.alert-info {
    --bs-alert-color: #055160;
    --bs-alert-bg: #cff4fc;
    --bs-alert-border-color: #b6effb;
    --bs-alert-link-color: #055160;
}
.text-bg-info {
    background-color: #cff4fc !important;
    color: #055160 !important;
}

/* --- Suggestion #731d9e (the navbar's deep purple — computed-aid pills:
   sight advice, "centre my group"; distinct from every target-face color) --- */
.btn-suggestion {
    --bs-btn-bg: #731d9e;
    --bs-btn-border-color: #731d9e;
    --bs-btn-color: #fff;
    --bs-btn-hover-bg: #5f1883;
    --bs-btn-hover-border-color: #59167a;
    --bs-btn-active-bg: #59167a;
    --bs-btn-active-border-color: #521571;
    --bs-btn-active-color: #fff;
    --bs-btn-hover-color: #fff;
    --bs-btn-disabled-bg: #731d9e;
    --bs-btn-disabled-border-color: #731d9e;
    --bs-btn-disabled-color: #fff;
}

/* The same computed-aid purple as a NON-interactive pill — a figure the app worked
   out for you, which is exactly what a projection is, but it opens nothing so it
   must not look like a button. `white-space: normal` because this one wraps: the
   sentence it carries runs to two lines on a phone. */
.badge-suggestion {
    background-color: #731d9e;
    color: #fff;
    font-weight: 500;
    white-space: normal;
    text-align: left;
}

/* --- Light #f8f9fa (Bootstrap default — too subtle on white, but unused) --- */
.btn-light {
    --bs-btn-bg: #f8f9fa;
    --bs-btn-border-color: #f8f9fa;
    --bs-btn-hover-bg: #e9ecef;
    --bs-btn-hover-border-color: #e2e6ea;
    --bs-btn-active-bg: #e2e6ea;
    --bs-btn-active-border-color: #dde0e3;
    --bs-btn-disabled-bg: #f8f9fa;
    --bs-btn-disabled-border-color: #f8f9fa;
}
.btn-check:checked + .btn-light {
    background-color: var(--bs-primary);
    border-color: var(--bs-primary);
    color: #fff;
}
.btn-outline-light {
    --bs-btn-color: #f8f9fa;
    --bs-btn-border-color: #f8f9fa;
    --bs-btn-hover-bg: #f8f9fa;
    --bs-btn-hover-border-color: #f8f9fa;
    --bs-btn-active-bg: #e9ecef;
    --bs-btn-active-border-color: #e9ecef;
    --bs-btn-disabled-color: #f8f9fa;
    --bs-btn-disabled-border-color: #f8f9fa;
}
.alert-light {
    --bs-alert-color: #636464;
    --bs-alert-bg: #fefefe;
    --bs-alert-border-color: #fdfdfe;
    --bs-alert-link-color: #636464;
}
.text-bg-light {
    background-color: #f8f9fa !important;
    color: #636464 !important;
}

/* --- Dark #2b2f33 --- */
.alert-dark {
    --bs-alert-color: #e9ecef;
    --bs-alert-bg: #2b2f33;
    --bs-alert-border-color: #3d4246;
    --bs-alert-link-color: #e9ecef;
}
.text-bg-dark {
    background-color: #2b2f33 !important;
    color: #e9ecef !important;
}

/* Close button: use dark icon on light toast/alert backgrounds */
.toast:not(.text-bg-dark) .btn-close-white,
.alert:not(.alert-dark) .btn-close-white {
    filter: none;
}

/* UX Icons: match Bootstrap Icons vertical alignment */
svg.ux-icon {
    vertical-align: -.125em;
}

/* Bigger icons in offcanvas titles */
.offcanvas-title svg.ux-icon {
    width: 1.5rem;
    height: 1.5rem;
}

html {
    overflow-y: scroll;
    touch-action: manipulation;
}

/* Tapping a button must ask the compositor for nothing.
   iOS 18 WebKit under-invalidates small composited paint updates over
   already-painted content: the damage rect is cleared and never repainted, and a
   cleared backing store is white — see
   `doc/plans/2026-07-27_ios-target-repaint-corruption.md`, where the same engine
   left white fragments all over the target renderer. On a tap, two such updates
   land on a button at once: iOS composites its system tap-highlight overlay over
   the anchor's rect, while Bootstrap animates `background-color` under the label's
   glyph run for 150ms. The result is a white rectangle over the button text (first
   reported on the tournament "shoot the qualification now" CTA, but nothing about
   it is specific to that button).

   Both halves are removed here rather than papered over with a repaint-forcing
   `transform` / `will-change` / `translateZ(0)` — that whole direction was
   falsified on-device in the plan above and must not come back. `:active` still
   swaps the background, so a tap keeps its feedback — it simply lands instantly
   on a device that has no hover state to transition from. */
a, button, .btn, [role="button"], label.btn, summary {
    -webkit-tap-highlight-color: transparent;
}

@media (hover: none) {
    .btn {
        transition: none;
    }
}

.cursor-pointer {
    cursor: pointer;
}

/* Smaller h1 on mobile screens */
@media (max-width: 575.98px) {
    h1 {
        font-size: 1.5rem !important;
    }
}

.form-label .badge {
    font-weight: normal !important;
}

abbr {
    text-decoration: none !important;
    cursor: text !important;
}

/* Fused badge group (like btn-group but for badges) */
.badge-group {
    display: inline-flex;
}

.badge-group > .badge {
    border-radius: 0;
}

.badge-group > .badge:first-child {
    border-top-left-radius: var(--bs-border-radius);
    border-bottom-left-radius: var(--bs-border-radius);
}

.badge-group > .badge:last-child {
    border-top-right-radius: var(--bs-border-radius);
    border-bottom-right-radius: var(--bs-border-radius);
}

.badge-group > .badge:only-child {
    border-radius: var(--bs-border-radius);
}

/* Remove rounding from stacked progress bars */
.progress-stacked,
.progress-stacked > .progress,
.progress-stacked > .progress > .progress-bar {
    border-radius: 0;
}

/* Flex line break utility */
.flex-basis-100 {
    flex-basis: 100%;
}

/* Equal-width flex siblings: `flex-basis: 0` overrides Bootstrap's content-based
   `.flex-fill` (`1 1 auto`), under which the longer label claims the wider column. */
.flex-equal {
    flex: 1 1 0;
}

/* The page header (components/_page_header.html.twig): title left, actions right.
   Both rows wrap, so a cluster that outgrows the line drops under the title instead
   of pushing it off-screen — the Shoots page alone carries six buttons on a phone. */
.page-header {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem 1rem;
}

.page-header > h1 {
    min-width: 0;
}

.page-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-end;
    gap: 0.5rem;
    margin-left: auto;
}

.page-actions:empty {
    display: none;
}

/* A flowing row of filter fields (pair with `d-flex flex-wrap gap-3`): each field
   grows to fill the line it lands on and wraps once it would drop below its ~14rem
   floor. A fixed `col-md-4` grid can't do this here because the field count varies
   with the data — several pickers only render when the archer's scorings hold more
   than one value — so any count that isn't a multiple of three left a ragged hole
   on the last line. */
.filter-fields > * {
    flex: 1 1 14rem;
}

/* Cursor pointer on mobile only (below lg breakpoint) */
@media (max-width: 991.98px) {
    .cursor-pointer-mobile {
        cursor: pointer;
    }
}



/* Force user dropdown to float at all screen sizes */
.navbar .dropdown {
    position: relative;
}
.navbar .dropdown-menu {
    position: absolute;
    background-color: #f0f4f8;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
    border: 1px solid rgba(0, 0, 0, 0.1);
    padding: 0.5rem 0;
    font-size: 1.05rem;
}

.navbar .dropdown-menu .dropdown-item {
    padding: 0.5rem 1.25rem;
}

.navbar .dropdown-menu .dropdown-item:hover,
.navbar .dropdown-menu .dropdown-item:focus,
.navbar .dropdown-menu .dropdown-item:active,
.navbar .dropdown-menu .dropdown-item.active {
    background-color: #dce4ed;
    color: var(--bs-dropdown-link-color) !important;
}

.navbar .dropdown-menu .dropdown-item i {
    margin-right: 0.25rem;
}

/* Larger nav icons and spacing on desktop */
@media (min-width: 992px) {
    .navbar-nav .nav-link i.bi,
    .navbar-nav .nav-link svg.ux-icon {
        font-size: 1.5rem;
        width: 1.5rem;
        height: 1.5rem;
        vertical-align: -.2em;
    }

    .navbar-nav .nav-item {
        margin-left: .4rem;
    }
}

/* Mobile nav link with proper colors */
.nav-link-mobile {
    color: var(--bs-navbar-color);
}

.nav-link-mobile:hover {
    color: var(--bs-navbar-hover-color);
}


@media (max-width: 991.98px) {
    .navbar-nav {
        gap: 0.5rem;
    }

    .navbar-nav .nav-link {
        color: rgba(255, 255, 255, 0.8);
        padding: 0.25rem 0.75rem;
        border: var(--bs-border-width) solid rgba(255, 255, 255, 0.1);
        border-radius: var(--bs-border-radius);
    }

    .navbar-nav .nav-link i,
    .navbar-nav .nav-link svg.ux-icon {
        font-size: 1.25rem;
        width: 1.25rem;
        height: 1.25rem;
        vertical-align: -.175em;
    }

    .navbar-nav .nav-link.active {
        color: #fff;
        background: rgba(255, 255, 255, 0.1);
        border-color: rgba(255, 255, 255, 0.5);
    }
}

@media (max-width: 429.98px) {
    .navbar-logo-admin {
        height: 32px;
    }
}

@media (max-width: 429.98px) {
    .navbar-logo-full {
        height: 32px;
    }

    .navbar-brand {
        margin-right: 0 !important;
    }
}

@media (max-width: 399.98px) {
    .navbar-nav {
        gap: 0.35rem;
    }

    .navbar-nav .nav-link {
        padding: 0.25rem 0.6rem;
    }

}

@media (max-width: 359.98px) {
    .navbar-nav {
        gap: 0.25rem;
    }

    .navbar-nav .nav-link {
        padding: 0.25rem 0.5rem;
    }
}

/* Smaller navbar brand on mobile */
@media (max-width: 991.98px) {
    .navbar-brand {
        font-size: 0.9rem;
    }
}

/* Allow dropdowns to overflow table-responsive containers */
.table-responsive:has(.dropdown) {
    overflow: visible;
}

/* Compact tables on small screens (opt-in via .table-compact-mobile) */
@media (max-width: 991.98px) {
    .table-compact-mobile th,
    .table-compact-mobile td {
        padding: 0.3rem 0.4rem;
        font-size: 0.85rem;
    }
}

/* Arrow-performance card: status column collapses based on the card's actual
   width (not the viewport), so Bootstrap's container snaps don't suddenly
   crush the progress bar. */
.arrow-perf-card {
    container-type: inline-size;
    container-name: arrowperf;
}
/* Progressive column collapse as the card narrows. Arrow # and dispersion
   (the bar) always stay visible — the bar is the strongest visual signal and
   the arrow # is the identity. Hidden columns surface in the detail block's
   summary line so the aggregate stats are never out of reach. */
@container arrowperf (max-width: 750px) {
    .arrow-perf-status-col {
        display: none !important;
    }
}
@container arrowperf (max-width: 600px) {
    .arrow-perf-count-col {
        display: none !important;
    }
}
@container arrowperf (max-width: 508px) {
    .arrow-perf-minmax-col {
        display: none !important;
    }
}
/* On very narrow cards, shrink the arrow # and avg pill so they don't dominate
   the row visually — rather than dropping the avg entirely. */
@container arrowperf (max-width: 360px) {
    .arrow-perf-arrow-col,
    .arrow-perf-avg-col {
        font-size: 0.78rem;
    }
}

/* Make disabled buttons dimmer */
.btn:disabled {
    opacity: 0.3;
}

/* Markdown content styling (used for rendered markdown in messages, etc.) */
.markdown-content h1, .message-content h1 { font-size: 1.5rem; font-weight: 700; }
.markdown-content h2, .message-content h2 { font-size: 1.35rem; font-weight: 700; }
.markdown-content h3, .message-content h3 { font-size: 1.2rem; font-weight: 600; }
.markdown-content h4, .message-content h4 { font-size: 1.1rem; font-weight: 600; }
.markdown-content h5, .message-content h5 { font-size: 1.05rem; font-weight: 600; }
.markdown-content h6, .message-content h6 { font-size: 1rem; font-weight: 600; }

.markdown-content table, .message-content table {
    width: 100%;
    margin-bottom: 1rem;
    border-collapse: collapse;
}

.markdown-content table th, .message-content table th,
.markdown-content table td, .message-content table td {
    padding: 0.5rem;
    border: 1px solid var(--bs-border-color);
}

.markdown-content table th, .message-content table th {
    background-color: var(--bs-tertiary-bg);
    font-weight: 600;
}

.markdown-content table tr:nth-child(even), .message-content table tr:nth-child(even) {
    background-color: var(--bs-tertiary-bg);
}

.markdown-content iframe, .message-content iframe {
    max-width: 100% !important;
    width: 100% !important;
    aspect-ratio: 16 / 9;
    height: auto !important;
    border: 0;
    border-radius: var(--bs-border-radius);
}

.markdown-content img, .message-content img {
    max-width: 100%;
    max-height: 500px;
    height: auto;
    border-radius: var(--bs-border-radius);
    object-fit: contain;
}

.media-grid img {
    max-height: none;
}

.pswp img {
    max-height: none;
}

/* The share sheet's export opens in this same lightbox wherever a new tab is
   refused, and there a long-press on the image is the only save path left.
   PhotoSwipe blocks selection on its slide for its drag gestures, which takes
   that menu away with it — handed back only inside the rendered-image viewer,
   so the photo strips keep PhotoSwipe's own behaviour. */
.pswp--rendered-image .pswp__img {
    -webkit-touch-callout: default;
    -webkit-user-select: auto;
    user-select: auto;
}

.markdown-content blockquote, .message-content blockquote {
    border-left: 3px solid var(--bs-border-color);
    padding-left: 1rem;
    margin-left: 0;
    color: var(--bs-secondary-color);
    font-style: italic;
}

/* Markdown help modal: remove last row border */
#markdownHelpModal table tr:last-child td {
    border-bottom: 0;
}

/* Task list styling - remove bullets for checkbox items */
.markdown-content li:has(> input[type="checkbox"]),
.message-content li:has(> input[type="checkbox"]) {
    list-style: none;
    margin-left: -1.5em;
}

/* Clickable checkboxes in markdown content */
.markdown-content input[type="checkbox"],
.message-content input[type="checkbox"] {
    cursor: pointer;
    pointer-events: auto;
    margin-right: 0.4em;
}

.markdown-content input[type="checkbox"].offline-disabled,
.message-content input[type="checkbox"].offline-disabled {
    cursor: not-allowed;
}

/* Equipment cards — keep header height stable when toggling between rendered
   name (h2) and inline-edit input, and between bi-star / bi-star-fill glyphs
   whose intrinsic metrics differ slightly. */
.arrow-set-card-header {
    /* form-control-sm height (1.5em + 0.5rem + 2px) + card-header's vertical padding
       (2 × 0.5rem). Keeps the row stable when the heading flips to an input. */
    min-height: calc(1.5em + 0.5rem + 2px + 1rem);
}
.arrow-set-default-star-slot {
    width: 1.5rem;
    height: 1.5rem;
    flex-shrink: 0;
}
.arrow-set-default-star-slot .arrow-set-default-star {
    line-height: 1;
}

/* Arrow status selector — min-width sized to fit the longest translated label
   ("Retraitée" in fr) plus the chevron. Same rule covers the equipment page rows
   and the scoring popover. */
.arrow-status-select {
    min-width: 7rem;
}

/* Fixed-width slot for the per-row identifier column. Wrapping both the static
   chip and the inline-edit input in the same-width container is what keeps the
   downstream status select from shifting when the user clicks to edit. Sized
   to fit "#99b" (worst-case 3-char identifier with the leading hash). */
.arrow-row-identifier-slot {
    flex: 0 0 2.75rem;
    width: 2.75rem;
}

/* Drop Bootstrap's invalid-state icon on the identifier input — the slot is
   too narrow to fit it. The red border alone signals the error. */
.arrow-row-identifier-input.is-invalid {
    background-image: none;
    padding-right: 0.25rem;
}

/* Arrow rows — auto-fill grid so cards reflow based on available width instead of
   fixed Bootstrap breakpoints. The 11rem floor matches the row's intrinsic content
   width (checkbox + #N + 6rem-min select + paddings), keeping the status selector
   from ever bleeding past the colored card background. */
.arrow-rows-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
    gap: 0.5rem;
}

/* Offline notice — reusable inline message for disabled-offline controls */
.offline-notice {
    display: inline-block;
    font-size: 0.75rem;
    color: #856404;
    background: #fff8e1;
    border-radius: 0.25rem;
    padding: 0.25rem 0.5rem;
    margin-bottom: 0.5rem;
}

.offline-notice::before {
    content: '\f61b'; /* Bootstrap Icons bi-wifi-off */
    font-family: 'bootstrap-icons';
    margin-right: 0.35rem;
}

/* ============================================
   Conversation - WhatsApp-style chat layout
   ============================================ */

.conversation-list {
    background: #f8f9fa;
    border-radius: 0.5rem;
    padding: 1rem;
}

.conversation-list .message-item {
    display: flex;
    gap: 0.75rem;
    max-width: 65%;
    margin-right: auto;
    margin-bottom: 0.5rem;
}

@media (max-width: 1199.98px) {
    .conversation-list .message-item {
        max-width: 75%;
    }
}

@media (max-width: 991.98px) {
    .conversation-list .message-item {
        max-width: 82%;
    }
}

@media (max-width: 767.98px) {
    .conversation-list .message-item {
        max-width: 92%;
    }
}

@media (max-width: 575.98px) {
    .conversation-list .message-item {
        max-width: 100%;
    }
}

.conversation-list .message-item:last-child {
    margin-bottom: 0;
}

/* Actions button in top right */
.conversation-list .message-actions {
    position: absolute;
    top: 0.25rem;
    right: 0.25rem;
}

.conversation-list .message-actions > a {
    color: rgba(0, 0, 0, 0.4);
    font-size: 1.1rem;
    padding: 0.25rem;
}

.conversation-list .message-actions > a:hover {
    color: rgba(0, 0, 0, 0.7);
}

.conversation-list .message-item .card {
    flex: 1;
    border: none;
    border-radius: 0 1rem 1rem 1rem;
    background: var(--note-color, #f0f0f0);
    position: relative;
}

.conversation-list .message-item .card::before {
    content: '';
    position: absolute;
    top: 0;
    left: -8px;
    width: 0;
    height: 0;
    border-top: 8px solid var(--note-color, #f0f0f0);
    border-left: 8px solid transparent;
}

/* Current user: right-aligned with green background */
.conversation-list .message-item[data-own="true"] {
    margin-left: auto;
    margin-right: 0;
    flex-direction: row-reverse;
}

.conversation-list .message-item[data-own="true"] .card {
    background: #dcf8c6;
    border-radius: 1rem 0 1rem 1rem;
}

.conversation-list .message-item[data-own="true"] .card::before {
    left: auto;
    right: -8px;
    border-top-color: #dcf8c6;
    border-left: none;
    border-right: 8px solid transparent;
}

/* Author name */
.conversation-list .message-author {
    font-weight: 600;
    color: var(--author-color, #075e54);
}

.conversation-list .message-item[data-own="true"] .message-author {
    display: none;
}

/* Recipient name (for messages sent to others) */
.conversation-list .message-recipient {
    font-weight: 600;
    color: var(--author-color, #075e54);
    display: block;
}

/* Message content wrapper - needs relative positioning for timestamp */
.conversation-list .message-item .card-body {
    position: relative;
    padding: 0.5rem 0.75rem 1rem 0.75rem;
}

/* Message content - leave space for dropdown menu icon */
.conversation-list .message-content {
    font-size: 0.875rem;
    padding-right: 1.5rem;
}

/* Metadata row: session info at bottom left. The padding keeps a long location name from
   running under the stamp box in the corner, which is out of flow. */
.conversation-list .message-meta {
    font-size: 0.7rem;
    color: rgba(0, 0, 0, 0.45);
    margin-top: 0.25rem;
    padding-right: 3.5rem;
}

/* The padlock has to opt out of the meta row's 0.7rem, or the one mark that says who can
   read this note lands smaller than the timestamp beside it. */
.conversation-list .message-visibility {
    display: inline-flex;
    align-items: center;
}

.conversation-list .message-visibility .ux-icon {
    width: 0.95rem;
    height: 0.95rem;
}

/* Context info */
.conversation-list .message-context-info {
    opacity: 0.85;
    display: block;
}

/* Timestamp floats bottom-right, with whatever the caller pinned to it (the journal's
   privacy padlock) riding along to its left. */
.conversation-list .message-stamp {
    position: absolute;
    bottom: 0.25rem;
    right: 0.5rem;
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
}

/* Date separator (WhatsApp-style) */
.conversation-list .date-separator {
    display: flex;
    justify-content: center;
    margin: 1rem 0;
}

.conversation-list .date-separator span {
    background: rgba(0, 0, 0, 0.1);
    color: rgba(0, 0, 0, 0.6);
    font-size: 0.75rem;
    padding: 0.25rem 0.75rem;
    border-radius: 0.5rem;
    text-transform: capitalize;
}

/* User color palette */
.conversation-list .message-item[data-color="1"] { --note-color: #fff3e0; --author-color: #e65100; }
.conversation-list .message-item[data-color="2"] { --note-color: #e3f2fd; --author-color: #1565c0; }
.conversation-list .message-item[data-color="3"] { --note-color: #f3e5f5; --author-color: #7b1fa2; }
.conversation-list .message-item[data-color="4"] { --note-color: #e8f5e9; --author-color: #2e7d32; }
.conversation-list .message-item[data-color="5"] { --note-color: #fce4ec; --author-color: #c2185b; }
.conversation-list .message-item[data-color="6"] { --note-color: #e0f7fa; --author-color: #00838f; }
.conversation-list .message-item[data-color="7"] { --note-color: #fff8e1; --author-color: #ff8f00; }
.conversation-list .message-item[data-color="8"] { --note-color: #ede7f6; --author-color: #512da8; }

/* Note visibility switch — the label states what the note *is* right now, so only
   one of the two spans is ever visible, driven by the checkbox itself rather than JS. */
.note-visibility-label-private,
.form-check-input:checked ~ .form-check-label .note-visibility-label-shared {
    display: none;
}

.form-check-input:checked ~ .form-check-label .note-visibility-label-private {
    display: inline;
}

/* The twemoji lock pair shares one body path and differs only in the shackle, so the padlock
   can't jump sideways as the switch flips. Oversized past the text: the open/closed shackle
   is a small detail, and this is the one coloured mark on the form that carries the state. */
.note-visibility-label-shared .ux-icon,
.note-visibility-label-private .ux-icon {
    width: 1.3em;
    height: 1.3em;
    vertical-align: -0.25em;
}

/* Chat composer — docked to the bottom of the viewport (see
   conversation_composer_controller.js), under the message list it belongs to.
   `--conversation-composer-height` is refined to the exact rendered height by
   the controller (ResizeObserver on `.conversation-composer`); the value below
   is only the no-JS / pre-mount fallback so the page still reserves enough
   room for the fixed bar. */
:root {
    --conversation-composer-height: 4.5rem;
}

.conversation-composer {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 1030;
    background: var(--bs-body-bg, #fff);
    border-top: 1px solid var(--bs-border-color, #dee2e6);
    box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
    padding: 0.75rem 1rem calc(0.75rem + env(safe-area-inset-bottom));
}

/* Reserve space at the end of the document so the fixed bar never
   permanently covers the last message (or the footer) on a short thread. */
body:has(.conversation-composer) {
    padding-bottom: var(--conversation-composer-height);
}

/* `scrollIntoView` honours `scroll-padding` on the root scroller, which is
   what keeps the newest message clear of the fixed bar on scroll-to-newest. */
html:has(.conversation-composer) {
    scroll-padding-bottom: var(--conversation-composer-height);
}

/* The global toast container (`front/layout.html.twig`) is bottom-left and
   would otherwise sit under the composer bar at the same corner. */
body:has(.conversation-composer) .toast-container {
    bottom: var(--conversation-composer-height) !important;
}

/* Same corner clash with the bottom bands — the match drawer (whose toast slot
   above it is reserved for the live-update notices), the tournament footer under
   it, and the scoring-complete band that stands in for both on a plain fixed
   scoring. `--bottom-bands-height` is the whole stack's height and every band is
   0px while off screen, so it is inert on a page carrying none. */
body:has(.match-drawer) .toast-container,
body:has(.tournament-footer) .toast-container,
body:has(.challenge-band) .toast-container,
body:has(.scoring-complete-footer) .toast-container {
    bottom: var(--bottom-bands-height) !important;
}

/* Thread list — an unread row reads as unread from across the room: heavier
   text plus one dot. No badge: mute and block already take a marker each on
   the same row, and a third pill would turn the row into a row of pills. */
.conversation-unread-dot {
    display: inline-block;
    width: 0.5rem;
    height: 0.5rem;
    border-radius: 50%;
    background: var(--bs-primary, #0d6efd);
}

.conversation-row-unread {
    border-left: 3px solid var(--bs-primary, #0d6efd);
}

/* Video player with panzoom support */
.panzoom-container {
    overflow: hidden;
    touch-action: none;
}

.panzoom-container video.panzoom-enabled {
    touch-action: none;
}

/* Media grid for consecutive images/videos */
.media-grid {
    margin: 0.5rem 0;
    border-radius: 12px;
    border: 1px solid #dee2e6;
    background: #fff;
    overflow: hidden;
}

.media-grid-row {
    display: flex;
}

.media-grid-row:not(:last-child) {
    border-bottom: 1px solid #dee2e6;
}

.media-grid-cell {
    flex: 1;
    aspect-ratio: 1;
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
}

.media-grid-cell:first-child:not(:last-child) {
    border-right: 1px solid #dee2e6;
}

.media-grid-cell img {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
}

.media-grid-cell .plyr {
    width: 100%;
    height: 100%;
}

.media-grid-cell .plyr video {
    object-fit: contain;
}

.media-grid-cell-empty {
    background: repeating-linear-gradient(
        -45deg,
        #f8f9fa,
        #f8f9fa 10px,
        #fff 10px,
        #fff 20px
    );
}

/* Dark variant for video-only grids */
.media-grid-dark {
    background: #000;
}

.media-grid-dark .media-grid-cell {
    background: #000;
}

.media-grid-dark .media-grid-cell-empty {
    background: repeating-linear-gradient(
        -45deg,
        #1a1a1a,
        #1a1a1a 3px,
        #000 3px,
        #000 20px
    );
}

/* Plyr video player max height */
.plyr--max-height {
    max-height: 500px !important;
}

.plyr--max-height video {
    max-height: 500px !important;
    object-fit: contain;
}

/* PhotoSwipe video container */
.pswp-video-container {
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
}

.pswp-video-container .plyr {
    width: 90vw;
    max-width: 1200px;
    max-height: 80vh;
}

.pswp-video-container .plyr video {
    max-height: 80vh;
}

/* JSON syntax highlighting */
.json-key {
    color: #881391;
}

.json-string {
    color: #1a1aa6;
}

.json-number {
    color: #1c00cf;
}

.json-boolean {
    color: #0d47a1;
    font-weight: 600;
}

/* Remove blue background from Bootstrap accordion buttons */
.accordion-button:focus {
    box-shadow: none;
    background-color: transparent;
}

.accordion-button:not(.collapsed) {
    background-color: transparent;
    color: inherit;
}

/* Search highlighting */
mark.search-highlight {
    background-color: #fff59d;
    padding: 0 0.2em;
    border-radius: 2px;
}

/* Date separator for scorings list */
.list-group .date-separator {
    text-align: center;
    margin: 1rem 0;
    position: relative;
}

.list-group .date-separator::before {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    top: 50%;
    height: 1px;
    background-color: #dee2e6;
}

.list-group .date-separator span {
    background-color: #fff;
    padding: 0 1rem;
    position: relative;
    color: #6c757d;
    font-size: 0.875rem;
    text-transform: capitalize;
}

/* Inline radio buttons for form fields */
.inline-radio-group > div {
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
}

.inline-radio-group .form-check {
    margin-bottom: 0;
}

/* Scorecard table: fixed layout fills container, columns share space equally */
.scorecard-table {
    table-layout: fixed;
    width: 100%;
    max-width: 850px;
}

/* Responsive scorecard column hiding (class-based, applied by responsive_scorecard_controller) */
.scorecard-table.hide-col-end .col-end { display: none !important; }
.scorecard-table.hide-col-end .show-when-end-hidden { display: inline !important; }
.scorecard-table.hide-col-tenx .col-tenx,
.scorecard-table.hide-col-tenx .col-x { display: none !important; }
.scorecard-table.hide-col-cumulative .col-cumulative { display: none !important; }
.scorecard-table.hide-col-sum .col-sum:not(.col-end-total) { display: none !important; }

/* The per-arrow average — `(8.85 moy.)` — wherever the DOM prints it (see
   arrow-average.js, which owns the string). Absolute `rem`, not `em`: the surfaces it
   sits on shrink by different amounts on a phone, which is what made the same number
   read at three sizes on one card. The line-height also holds the summary card's
   average to 15px, keeping its score column flush with the headline + details pair
   beside it — taller, and a gap opens above the mini-scorecard. */
.arrow-average {
    font-size: 0.75rem;
    line-height: 1.25;
    color: var(--bs-secondary-color);
    opacity: 0.75;
}

/* The per-end average as a mark — the deviation bars end-average-spark.js draws next
   to the round header's `(8.92 avg.)`. The module sizes the WIDTH (constant px per
   end, so a round's mark and the All row's mark share one horizontal scale); the
   height is here, in em, so the mark tracks the line it annotates.

   2.2em, not the text's own 1.15em: at text height the deviations are 1–4px stubs and
   the mark degenerates into an above-or-below tick chart, throwing away the magnitude
   that is its whole advantage. `flex: 0 0 auto` because the header row is a flex
   container that would otherwise squeeze the width the module computed. */
.end-average-spark {
    height: 2.2em;
    vertical-align: -0.8em;
    flex: 0 0 auto;
}

/* The rule the bars grow from: the run's own average, spanning the WHOLE run
   whether or not its ends are shot, so it doubles as a progress track during live
   scoring. Grey — it is the reference, not a series. */
.end-average-spark-rule {
    stroke: var(--bs-secondary-color);
    stroke-width: 0.6;
    opacity: 0.6;
}

/* An end that exists in the structure but holds no arrows yet: a nub straddling the
   rule, so "not shot yet" cannot be confused with "shot, dead on the average". */
.end-average-spark-pending {
    fill: var(--bs-secondary-color);
    opacity: 0.4;
}

/* Shot, and on the average — a perfect 54/54/54 round deviates by exactly zero, and
   must not render as an empty line. Same colour as the above-average bars: it is a
   scored end, and the notch is what a bar too short to read as a direction becomes. */
.end-average-spark-notch,
.end-average-spark-above {
    fill: var(--bs-primary);
    opacity: 0.85;
}

.end-average-spark-below {
    fill: var(--bs-danger);
    opacity: 0.7;
}

/* Inside a rounds_totals round pill the average rides within chrome sized at
   0.7rem/0.6rem, where the absolute size would read larger than the total it
   qualifies and stretch the pill. */
.mini-scorecard .badge .arrow-average {
    font-size: 0.875em;
    line-height: inherit;
}

/* Mini-scorecard in scoring list */
.mini-scorecard .badge {
    font-size: 0.7rem;
    /* Horizontal padding is what a fixed-width badge spends on chrome instead
       of digits, so it is kept to what the three-digit end totals of a dynamic
       round can afford (see END_BADGE_WIDTH in scoring-renderer.js). Both are
       in em, so the badge's geometry — and that three-digit guarantee with it —
       follows whatever font size applies below. */
    padding: 0.2em 0.3em;
    font-weight: normal;
}

/* Below `sm`, a round's line has to buy the width for its own total: the ends
   and the "= total / max (avg.)" that closes them share one flex row, and at
   full size a 720's six ends pushed that total onto a line of its own. Above
   the breakpoint the card is wide enough to hold both at full size, so the
   shrinking is scoped to where it pays for something. */
@media (max-width: 575.98px) {
    .mini-scorecard {
        font-size: 0.8rem;
    }

    .mini-scorecard .badge {
        font-size: 0.6rem;
    }
}

/* Incomplete scoring indicator */
.scoring-incomplete {
    border-left: 3px solid var(--bs-warning) !important;
}

/* Custom text color for sun icon (outdoors) */
.text-orange {
    color: #e67e22;
}

/* TomSelect placeholder styling for single-select */
.ts-wrapper.single .ts-control .item[data-value=""] {
    color: var(--bs-secondary);
}

/* TomSelect: show placeholder on mobile where controlInput is null (no <input> to carry native placeholder) */
.ts-wrapper:not(.has-items) .ts-control[data-placeholder]::before {
    content: attr(data-placeholder);
    color: var(--bs-secondary);
}

/* Stats filter labels: uniform width for horizontal alignment */
.stats-filter-label {
    flex-shrink: 0;
    width: 120px;
}

/* Stats filters: make TomSelect fill available width */
.stats-filter-label ~ .ts-wrapper,
.setup-filter-wrapper .ts-wrapper {
    flex-grow: 1;
}

/* Prevent setup filter TomSelect from resizing on open */
.setup-filter-wrapper .ts-wrapper.single .ts-control > input {
    width: 0 !important;
    min-width: 0 !important;
}

/* Score inputs: custom validation styling without layout shift */
.score-input-wrapper input.score-saved {
    border-color: var(--bs-success);
}

.score-input-wrapper input.score-error {
    border-color: var(--bs-danger);
}

/* Score inputs: show checkmark icon inside the input */
.score-input-wrapper {
    position: relative;
    display: inline-block;
}

.score-input-wrapper input {
    padding-left: 1.5rem;
    padding-right: 1.5rem;
    /* Mobile browsers auto-zoom a focused field whose font is under 16px, and
       `.form-control-sm` puts these at 14px — so tapping a set total panned the
       archer into a corner of the scorecard, mid-set, every time. Fixed at the
       threshold rather than by dropping `-sm`, whose padding is what keeps the
       80px totals column narrow. Kills the zoom at its cause, independently of
       the viewport policy. */
    font-size: 16px;
}

.score-input-wrapper .score-check {
    position: absolute;
    right: 0.35rem;
    top: 50%;
    transform: translateY(-50%);
    color: var(--bs-success);
    opacity: 0;
    transition: opacity 0.2s;
    pointer-events: none;
    font-size: 0.875rem;
}

.score-input-wrapper .score-check.visible {
    opacity: 1;
}

/* Hide number input spinners */
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button {
    -webkit-appearance: none;
    margin: 0;
}

input[type="number"] {
    -moz-appearance: textfield;
    appearance: textfield;
}

/* Arrow scale slider: darker track for visibility on light background */
.arrow-scale-slider::-webkit-slider-runnable-track { background-color: #ced4da; border-radius: 4px; }
.arrow-scale-slider::-moz-range-track { background-color: #ced4da; border-radius: 4px; }

/* Plot target column: reserves exactly the width the target can render at — its base
   size (`--plot-target-base-size`, set inline from the template) clamped by the same
   viewport-height cap the SVG carries itself, since a target taller than the window is
   unusable — plus the 56px the target row reserves for its two chevron slots.

   Reserving more than that is not harmless whitespace: the controls column beside it
   takes what is left over, so on a short window — where the target sits at its 320px
   floor while a fixed 706px column stayed 706px — the controls were squeezed below
   their own content width and their button pairs wrapped onto two lines. */
.plot-target-column {
    --plot-target-size: min(
        var(--plot-target-base-size),
        max(320px, calc(100dvh - 400px - var(--plot-target-bands-height)))
    );

    width: calc(var(--plot-target-size) + 56px);
    max-width: 100%;
}

/* Plot controls column: takes whatever the target column leaves, instead of sizing to
   its own content.

   Shrink-to-fit is actively wrong for it, not merely wasteful. A flex row of two
   `.flex-equal` buttons reports an intrinsic width of the two labels' natural widths
   summed — so the column stopped there, then each button got half of it, which is less
   than the longer label needs, and that label wrapped onto two lines with hundreds of
   pixels standing empty beside it. Given the leftover width instead, the halves are wide
   enough for both. Rows that should span the column carry `w-100`: the column aligns its
   children to one edge rather than stretching them, so they are shrink-to-fit too.

   Below xl the layout stacks and the column simply takes the card's width.

   It carries the card's gutter itself, since the card body is `px-0` so the target can
   span the card edge to edge: without it a row filling this column runs its buttons into
   the card's border. Beside the target the inner edge needs none — the target column and
   the row gap already hold it well inside the card. */
.plot-controls-column {
    width: 100%;
    padding-inline: 1rem;
}

@media (min-width: 1200px) {
    .plot-controls-column {
        width: auto;
        flex: 1 1 0;
        min-width: 0;
        padding-inline: 0 1rem;
    }
}

/* Desktop holds the target to three quarters of its base size for visual balance. The
   quarter comes off the column, not off a `max-width` on the wrapper inside it —
   capping the wrapper would leave the column reserving a quarter it never draws in.
   Three quarters of the base size alone, not of the size plus the chevron slots: the
   face reads tighter that way, and the slots are chrome, not part of the face. */
@media (min-width: 992px) {
    .plot-target-column {
        --plot-target-size: min(
            calc(var(--plot-target-base-size) * 0.75),
            max(320px, calc(100dvh - 400px - var(--plot-target-bands-height)))
        );
    }
}

/* Structure preset chips (scoring create form): a segmented group, same shape as
   the Fixed / Free shooting choice right above it. `flex-basis: 0` overrides
   Bootstrap's content-based `flex: 1 1 auto` so the three abut in equal columns
   instead of the two-line ones crowding out "Custom". */
.structure-preset-chip {
    flex: 1 1 0;
    line-height: 1.15;
}

/* ── Edit-structure panel: the flat ends + round-breaks list ──────────────────
   Shot-based methods edit one flat list whose items are end rows and round
   breaks, so a break can be dropped between any two ends — that is what
   splitting a round is (see scoring_structure_controller.js). Grid methods keep
   the round cards and use `.structure-ends` inside them. */
.structure-flat {
    display: flex;
    flex-direction: column;
    gap: .25rem;
}
.structure-end {
    padding: .4rem .5rem;
    background: var(--bs-body-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: .375rem;
}
.structure-end-label {
    min-width: 4.5rem;
}
.structure-add-end-row {
    padding: .1rem .5rem .3rem;
}
/* Grid path only: a round card's ends. It can legitimately be empty
   (rounds_totals has no ScoringEnd entities at all), and an empty flex column
   collapses to 0px — which is why the flat path exists rather than keeping a
   droppable container per round. */
.structure-ends {
    min-height: .5rem;
}

/* The break reads as a rule across the list carrying the round it starts, so the
   boundary is never mistaken for another end row. The rule is drawn behind the
   pill rather than as a border, so the pill can sit on top of it. */
.structure-break {
    display: flex;
    align-items: center;
    position: relative;
    margin: .4rem 0;
}
.structure-break::before {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: 50%;
    border-top: 2px dashed var(--bs-primary-border-subtle);
}
.structure-break-pill {
    position: relative;
    margin-inline: auto;
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    padding: .2rem .5rem;
    background: var(--bs-body-bg);
    border: 2px solid var(--bs-primary-border-subtle);
    border-radius: 2rem;
    font-size: .85rem;
    font-weight: 600;
}
/* Round 1's start: the same pill, dotted and muted, because it is the one boundary
   that cannot move — the list's start is where round 1 begins. */
.structure-break-fixed::before {
    border-top-style: dotted;
    border-top-color: var(--bs-border-color);
}
.structure-break-fixed .structure-break-pill {
    border-style: dotted;
    border-color: var(--bs-border-color);
}

.structure-round-handle,
.structure-end-handle,
.structure-break-handle {
    cursor: grab;
    /* Sortable drives the drag from pointer events; without this the browser
       claims the gesture for scrolling on touch and the row never picks up. */
    touch-action: none;
}

/* "Add new round" appends a break at the END of the list, which is nowhere near
   where it is usually wanted — flash it so it is never lost on a long list. */
@keyframes structure-break-landed {
    from { background-color: var(--bs-warning-bg-subtle); }
    to { background-color: transparent; }
}
.structure-break-landed {
    animation: structure-break-landed 1.6s ease-out;
}

/* Scorecard preview: compact version for form previews */
.scorecard-preview {
    font-size: 0.65rem;
    line-height: 1.4;
}
.scorecard-preview .badge {
    font-size: 0.55rem;
}
.scorecard-preview td,
.scorecard-preview th {
    padding: 0 !important;
}
.scorecard-preview tfoot td.text-end {
    padding-right: 0.25rem !important;
}
.scorecard-preview tfoot .fst-italic {
    font-size: 0.55rem;
}

/* Flatpickr: dot indicator for days with activity */
.flatpickr-day.has-activity::after {
    content: '';
    position: absolute;
    bottom: 3px;
    left: 50%;
    transform: translateX(-50%);
    width: 5px;
    height: 5px;
    border-radius: 50%;
    background-color: var(--bs-primary);
}

.flatpickr-day.has-activity.selected::after,
.flatpickr-day.has-activity.today::after {
    background-color: #fff;
}

/* Flatpickr: date range presets panel */

/* Desktop: side panel to the right */
@media (min-width: 576px) {
    .flatpickr-calendar.has-presets {
        border-right: none;
        border-top-right-radius: 0;
        border-bottom-right-radius: 0;
    }

    .flatpickr-presets {
        position: absolute;
        left: 100%;
        top: -1px;
        bottom: -1px;
        display: flex;
        flex-direction: column;
        justify-content: center;
        padding: 12px 16px;
        gap: 8px;
        background: #fff;
        border-radius: 0 5px 5px 0;
        box-shadow: 4px 2px 6px -1px rgba(0, 0, 0, 0.1);
        border: 1px solid #e6e6e6;
    }
}

/* Mobile: panel below the calendar */
@media (max-width: 575.98px) {
    .flatpickr-calendar.has-presets {
        border-bottom: none;
        border-bottom-left-radius: 0;
        border-bottom-right-radius: 0;
    }

    .flatpickr-presets {
        position: absolute;
        top: 100%;
        left: -1px;
        right: -1px;
        display: flex;
        flex-direction: column;
        padding: 8px;
        gap: 2px;
        background: #fff;
        border-radius: 0 0 5px 5px;
        box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
        border: 1px solid #e6e6e6;
    }
}

.flatpickr-presets button {
    display: block;
    padding: 6px 12px;
    border: none;
    border-radius: 4px;
    background: transparent;
    cursor: pointer;
    font-size: 0.85rem;
    text-align: left;
    white-space: nowrap;
    color: #484848;
}

.flatpickr-presets button:hover {
    background: #f0f0f0;
}

/* Force the presets panel below the calendar regardless of viewport.
   Used by narrow containers (e.g. the arrow-counter offcanvas) where
   the desktop side-panel layout would overflow horizontally. */
.flatpickr-calendar.has-presets.has-presets-below {
    border-right: 1px solid #e6e6e6;
    border-top-right-radius: 5px;
    border-bottom: none;
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
}

.flatpickr-calendar.has-presets.has-presets-below .flatpickr-presets {
    position: absolute;
    top: 100%;
    left: -1px;
    right: -1px;
    bottom: auto;
    display: flex;
    flex-direction: column;
    padding: 8px;
    gap: 2px;
    background: #fff;
    border-radius: 0 0 5px 5px;
    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
    border: 1px solid #e6e6e6;
}


/* Arrow stats summary cards: compact on mobile */
@media (max-width: 429.98px) {
    .arrow-stats-cards > div {
        flex: 0 0 100%;
        max-width: 100%;
    }

    .arrow-stats-cards > div > div {
        padding: 0.4rem 1rem !important;
        border-radius: 0 !important;
    }

    .arrow-stats-cards > div:first-child > div {
        border-radius: var(--bs-border-radius) var(--bs-border-radius) 0 0 !important;
    }

    .arrow-stats-cards > div:last-child > div {
        border-radius: 0 0 var(--bs-border-radius) var(--bs-border-radius) !important;
    }

    .arrow-stats-cards .fs-2 {
        font-size: 1.25rem !important;
    }
}

@media (max-width: 399.98px) {
    .arrow-stats-archer-select {
        flex-direction: column;
        align-items: stretch !important;
    }

    .arrow-stats-archer-select label {
        margin-bottom: 0.25rem;
    }

    /* Stack each filter's label above its field on very small screens —
       the fixed 120px label + field side-by-side gets too cramped. */
    .d-flex:has(> .stats-filter-label) {
        flex-direction: column;
        align-items: stretch !important;
    }

    .stats-filter-label {
        width: auto;
        margin-bottom: 0.25rem;
    }

    .stats-page-back-btn {
        display: none !important;
    }
}

/* Arrow stats calendar */

.arrow-calendar {
    font-size: 0.8rem;
}

.arrow-calendar th {
    font-size: 0.75rem;
    font-weight: normal;
    white-space: nowrap;
}

.arrow-calendar-month-col {
    width: 1.5rem;
}

.arrow-calendar-month-cell {
    background-color: var(--bs-tertiary-bg);
}

.arrow-calendar-month-name {
    font-size: 0.7rem;
    text-transform: capitalize;
    line-height: 1.2;
    writing-mode: vertical-rl;
    transform: rotate(180deg);
    text-orientation: mixed;
    white-space: nowrap;
}

.arrow-calendar-week-col {
    width: 2rem;
}


.arrow-calendar-day {
    cursor: pointer;
    vertical-align: middle;
    transition: background-color 0.15s;
}

.arrow-calendar-day:hover {
    background-color: var(--bs-tertiary-bg);
}

.arrow-calendar-day-active {
    background-color: var(--bs-primary-bg-subtle) !important;
}

.arrow-calendar-count {
    display: block;
    font-size: 0.8rem;
    line-height: 1.2;
}

.arrow-calendar-dots {
    display: flex;
    justify-content: center;
    gap: 3px;
    margin-top: 2px;
}

.arrow-calendar-dot {
    display: inline-block;
    width: 6px;
    height: 6px;
    border-radius: 50%;
}

.arrow-calendar-day-empty {
    cursor: default;
}

.arrow-calendar td.arrow-calendar-day-out-of-range {
    --bs-table-bg: var(--bs-tertiary-bg);
}

.arrow-calendar-popover .popover-body {
    padding: 0.5rem 0.75rem;
}

.arrow-calendar-popover .arrow-calendar-dot {
    width: 8px;
    height: 8px;
    flex-shrink: 0;
}

@media (max-width: 575.98px) {
    .arrow-calendar {
        font-size: 0.7rem;
    }

    .arrow-calendar-count {
        font-size: 0.7rem;
    }

    .arrow-calendar-dot {
        width: 5px;
        height: 5px;
    }

    .arrow-calendar-month-name {
        font-size: 0.6rem;
    }
}

@media (max-width: 479.98px) {
    .arrow-calendar-week-col {
        display: none;
    }
}

@media (max-width: 449.98px) {
    .arrow-calendar-delta-col {
        display: none;
    }
}

@media (max-width: 399.98px) {
    .arrow-calendar-total-col {
        display: none;
    }
}


/* Collapse menu blending: trigger button joins with panel items */
.btn-group-vertical > .btn[aria-expanded="true"] {
    border-bottom-left-radius: 0 !important;
    border-bottom-right-radius: 0 !important;
    border-bottom: 0;
}

.btn-group-vertical + .collapse.show > .btn-group-vertical > .btn-check:first-child + .btn,
.btn-group-vertical + .collapse.show > .btn-group-vertical > .btn:first-child,
.btn-group-vertical + .collapsing > .btn-group-vertical > .btn-check:first-child + .btn,
.btn-group-vertical + .collapsing > .btn-group-vertical > .btn:first-child {
    border-top-left-radius: 0 !important;
    border-top-right-radius: 0 !important;
}

/* Prevent content shift when Bootstrap modal opens */
body.modal-open {
    padding-right: 0 !important;
    overflow: auto !important;
}

/* ============================================
   Tabata Timer
   ============================================ */

.timer-background {
    position: fixed;
    inset: 0;
    z-index: 1050;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: background-color 0.3s ease;
    background-color: #333;
}

.timer-content {
    text-align: center;
    color: #fff;
    text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
    user-select: none;
}

.timer-phase-name {
    font-size: 3rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.1em;
}

.timer-countdown {
    font-size: 14rem;
    font-weight: 900;
    line-height: 1;
    font-variant-numeric: tabular-nums;
}

.timer-info {
    font-size: 1.5rem;
    font-weight: 500;
    opacity: 0.9;
}

.timer-controls {
    display: flex;
    gap: 1rem;
    justify-content: center;
    flex-wrap: wrap;
}

@media (max-width: 575.98px) {
    .timer-controls .btn-lg {
        padding: 0.5rem 1.25rem;
    }

    .timer-controls-secondary {
        width: 100%;
        display: flex;
        gap: 1rem;
        justify-content: center;
    }
}

.timer-elapsed {
    margin-top: 1.5rem;
    font-size: 1rem;
    opacity: 0.6;
}

/* Flicker animation for last 3 seconds — black overlay instead of opacity to avoid showing content behind */
.timer-flicker::after {
    content: '';
    position: absolute;
    inset: 0;
    background-color: black;
    animation: timer-flicker 0.5s ease-in-out infinite alternate;
    pointer-events: none;
}

@keyframes timer-flicker {
    0% { opacity: 0; }
    100% { opacity: 0.45; }
}

/* Responsive: smaller countdown on mobile */
@media (max-width: 575.98px) {
    .timer-countdown {
        font-size: 8rem;
    }

    .timer-phase-name {
        font-size: 2rem;
    }

    .timer-info {
        font-size: 1.2rem;
    }
}

/* Done screen */
.timer-done-icon {
    font-size: 6rem;
    line-height: 1;
    margin-bottom: 0.5rem;
}

.timer-done-elapsed {
    font-size: 2rem;
    font-weight: 500;
    opacity: 0.9;
    margin-top: 0.5rem;
}

/* Floating badge (minimized timer) */
.timer-badge {
    position: fixed;
    top: 70px;
    right: 20px;
    z-index: 1040;
    border-radius: 0.5rem;
    padding: 0.75rem 2.5rem 0.75rem 1rem;
    cursor: grab;
    color: #fff;
    text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
    user-select: none;
    touch-action: none;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
    transition: background-color 0.3s ease;
    background-color: #333;
}

.timer-badge-countdown {
    font-size: 2.5rem;
    font-weight: 900;
    font-variant-numeric: tabular-nums;
    line-height: 1.1;
}

.timer-badge-phase {
    font-size: 1rem;
    font-weight: 700;
    text-transform: uppercase;
}

.timer-badge-info {
    font-size: 0.85rem;
    opacity: 0.85;
}

/* Badge needs position context for the expand button and flicker overlay */
.timer-badge {
    position: fixed;
    overflow: hidden;
}

.timer-badge-expand {
    position: absolute;
    top: 0.35rem;
    right: 0.35rem;
    background: none;
    border: none;
    color: #fff;
    opacity: 0.6;
    font-size: 1.25rem;
    padding: 0.5rem;
    line-height: 1;
    cursor: pointer;
}

.timer-badge-expand:hover {
    opacity: 1;
}

/* Phase colors — must come after .timer-background and .timer-badge to override their defaults */
.timer-bg-danger { background-color: var(--bs-danger); }
.timer-bg-success { background-color: var(--bs-success); }
.timer-bg-warning { background-color: var(--bs-warning); }
.timer-bg-primary { background-color: var(--bs-primary); }
.timer-bg-info { background-color: var(--bs-info); }
.timer-bg-secondary { background-color: var(--bs-secondary); }



/* Changelog: filter bar */
.changelog-filter { margin-top: 0.5rem; }

/* Changelog: tighten list spacing from CommonMark paragraph wrapping */
.changelog-content li > p { margin-bottom: 0; }
.changelog-content > ul > li + li { margin-top: 0.5rem; }
.changelog-content h2 { font-size: 1.5rem; }
.changelog-content hr { margin: 1.75rem 0; }
.changelog-content hr:first-child { margin-top: 0; }
.changelog-content { overflow-wrap: break-word; }
.changelog-content > ul { padding-left: 0; list-style: none; }
.changelog-content ul ul { padding-left: 1rem; }

/* Terms & data privacy notice. Long-form prose rather than a changelog's list
   of entries, so it gets a reading measure and breathing room between sections
   — and the same rules serve the scrollable box on the registration form. */
.privacy-content { overflow-wrap: break-word; max-width: 70ch; }
.privacy-content h2 { font-size: 1.35rem; margin-top: 2rem; }
.privacy-content h2:first-child { margin-top: 0; }
.privacy-content h1 + h2 { margin-top: 1rem; }
.privacy-content li > p { margin-bottom: 0; }
.privacy-content li + li { margin-top: 0.35rem; }

/* On the registration card it is the condensed notice, short enough to be read
   in place — so no scroll frame, which is what made the long version easy to
   skip past. */
/* Capped and scrollable so the notice cannot push the submit button off a phone
   screen. It holds the SHORT version, so the scroll is a few lines rather than
   the wall of text that made the full one skippable. */
.privacy-summary-box {
    font-size: 0.875rem;
    max-height: 11rem;
    overflow-y: auto;
}
.privacy-content-summary { max-width: none; }
.privacy-content-summary p:last-child { margin-bottom: 0; }

/* Changelog: git activity heatmap */
.activity-card {
    --activity-cell-size: 14px;
    --activity-cell-gap: 3px;
    --activity-empty: #eceef2;
    --activity-level-1: #cfe0fb;
    --activity-level-2: #93b7f4;
    --activity-level-3: #4f86e8;
    --activity-level-4: #1e40af;
}

.activity-added { color: var(--bs-success); }
.activity-removed { color: var(--bs-danger); }

.activity-totals {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem 1.75rem;
}
.activity-total { display: flex; flex-direction: column; line-height: 1.25; }
.activity-total-value { font-size: 1rem; font-weight: 600; font-variant-numeric: tabular-nums; }
.activity-total-label { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--bs-secondary-color); }

/* Months run down the page and days of the month across it, so the width is
   fixed at 31 cells for good and only the height grows — a year adds twelve
   rows. The cap turns that growth into a vertical scroll, the axis a phone has
   to spare. */
.activity-scroll {
    /* Clears roughly fourteen month rows plus their year markers, so the scroll
       engages only once a whole row is genuinely hidden. A tighter cap gets
       overtaken by a year marker appearing mid-life and leaves a 1px sliver of
       scrollbar that scrolls nothing. */
    max-height: 18rem;
    overflow-y: auto;
    /* 31 cells plus a month label fit a 390px phone, but not a 320px one — let
       the narrowest screens scroll a little rather than burst the card. */
    overflow-x: auto;
    overscroll-behavior: contain;
}
.activity-grid {
    display: grid;
    grid-template-columns: auto repeat(31, var(--activity-cell-size));
    gap: var(--activity-cell-gap);
    align-items: center;
    width: max-content;
}

/* Deliberately bare — no rule across the grid. A line spanning all 31 columns
   reads as a division of the calendar rather than a label on it. */
.activity-year {
    /* Must span the row: anything narrower leaves the remaining columns to be
       filled by the next month's cells, shifting every row by one. `subgrid`
       then lets the year sit in the month-label column, flush with the labels,
       without the year ever widening that column. */
    grid-column: 1 / -1;
    display: grid;
    grid-template-columns: subgrid;
    margin-top: 0.35rem;
    font-size: 0.6rem;
    font-variant-numeric: tabular-nums;
    line-height: 1;
    opacity: 0.55;
    color: var(--bs-secondary-color);
}
.activity-year > span { grid-column: 1; }
.activity-year-first { margin-top: 0; }

.activity-row-label,
.activity-year > span {
    padding-right: 0.4rem;
    white-space: nowrap;
    text-align: right;
    color: var(--bs-secondary-color);
}
.activity-row-label { font-size: 0.7rem; }

.activity-cell {
    width: var(--activity-cell-size);
    height: var(--activity-cell-size);
    border-radius: 3px;
    background-color: var(--activity-empty);
    outline: 1px solid rgb(27 31 36 / 6%);
    outline-offset: -1px;
}
.activity-cell-blank { background-color: transparent; outline: none; }

.activity-cell[data-level="1"] { background-color: var(--activity-level-1); }
.activity-cell[data-level="2"] { background-color: var(--activity-level-2); }
.activity-cell[data-level="3"] { background-color: var(--activity-level-3); }
.activity-cell[data-level="4"] { background-color: var(--activity-level-4); }

/* 31 columns plus a month label have to survive a phone. The label column is
   auto-sized to its widest month — `mars`, which is why no year is printed on
   the rows: it would widen every one of them. */
@media (max-width: 575.98px) {
    .activity-card { --activity-cell-size: 7px; --activity-cell-gap: 1px; }
    .activity-row-label { font-size: 0.58rem; }
    .activity-row-label, .activity-year > span { padding-right: 0.2rem; }
    .activity-cell { border-radius: 2px; }
}

/* One pixel narrower is the difference between fitting a 360px phone whole and
   scrolling it. Below that the grid still scrolls rather than overflowing. */
@media (max-width: 379.98px) {
    .activity-card { --activity-cell-size: 6px; }
}

/* Color picker dots (phase editor) */
.color-picker-trigger {
    display: block;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    border: 2px solid rgba(0, 0, 0, 0.2);
    cursor: pointer;
    padding: 0;
}

.color-picker-menu {
    gap: 0.4rem;
    padding: 0.5rem;
    min-width: auto;
}

.color-picker-menu.show {
    display: flex;
}

.color-picker-dot {
    width: 20px;
    height: 20px;
    border-radius: 50%;
    border: 2px solid transparent;
    cursor: pointer;
    padding: 0;
}

.color-picker-dot.active {
    border-color: #333;
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3);
}

/* Phase drag handle */
.phase-drag-handle {
    cursor: grab;
    color: var(--bs-secondary);
    display: flex;
    align-items: center;
}

.phase-row.sortable-ghost {
    opacity: 0.5;
}

/* Phase row styling in config */
.phase-row select,
.phase-row input {
    font-size: 0.875rem;
}

.phase-row select[data-field="unit"] {
    padding-left: 0.4rem !important;
    padding-right: 1.25rem !important;
    background-position: right 0.15rem center !important;
    background-size: 12px !important;
}

@media (max-width: 575.98px) {
    .phase-row {
        flex-wrap: wrap;
        padding-bottom: 0.75rem;
        margin-bottom: 0.75rem !important;
        border-bottom: 1px solid var(--bs-border-color);
    }

    .phase-row:last-child {
        border-bottom: none;
        padding-bottom: 0;
        margin-bottom: 0.5rem !important;
    }

    .phase-row input[data-field="name"] {
        max-width: none !important;
        flex: 1;
    }
}

.notification-prefs-table tbody tr:not(.table-light):hover > * {
    background-color: var(--bs-table-hover-bg);
}

/* --- Score comparison --- */

.sc-highlights {
    display: flex;
    align-items: center;
    gap: 1rem;
    padding: 0.75rem;
    background: var(--bs-warning-bg-subtle);
    border-radius: 0.5rem;
    margin-bottom: 0.75rem;
}

.sc-highlight-rank {
    text-align: center;
    min-width: 4.5rem;
    flex-shrink: 0;
}

.sc-rank-number {
    font-size: 2rem;
    font-weight: 800;
    line-height: 1;
}

.sc-rank-suffix {
    font-size: 0.85rem;
    font-weight: 600;
}

.sc-rank-total {
    font-size: 0.75rem;
    color: var(--bs-secondary);
}

.sc-highlight-details {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem 0.75rem;
    font-size: 0.85rem;
}

.sc-highlight-item {
    display: flex;
    align-items: center;
    gap: 0.3rem;
}

.sc-table-highlight > td {
    background-color: var(--bs-warning-bg-subtle) !important;
}

th.sc-sortable {
    cursor: pointer;
    user-select: none;
    white-space: nowrap;
}

th.sc-sortable:hover {
    background-color: var(--bs-light);
}

.sc-sort-icon {
    font-size: 0.75em;
    opacity: 0.25;
}

th.sc-sort-active .sc-sort-icon {
    opacity: 1;
}

.sc-rank-badge {
    display: inline-block;
    min-width: 1.5em;
    padding: 0.05em 0.3em;
    font-size: 0.7em;
    font-weight: 700;
    border-radius: 0.25rem;
    text-align: center;
    vertical-align: middle;
}

.sc-rank-1 { background: #ffd700; color: #000; }
.sc-rank-2 { background: #c0c0c0; color: #000; }
.sc-rank-3 { background: #cd7f32; color: #fff; }
.sc-rank-other { background: var(--bs-light); color: var(--bs-secondary); }

.sc-filter-bar {
    display: flex;
    gap: 0.75rem;
    flex-wrap: wrap;
    align-items: center;
}

.sc-filter-label {
    font-size: 0.85rem;
    color: var(--bs-secondary);
    margin-bottom: 0;
    white-space: nowrap;
}

.sc-card {
    border: 1px solid var(--bs-border-color);
    border-radius: 0.5rem;
    padding: 0.6rem 0.75rem;
    background: var(--bs-body-bg);
}

.sc-card.sc-card-highlight {
    background: var(--bs-warning-bg-subtle);
    border-color: var(--bs-warning-border-subtle);
}

.sc-card-stats {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 0.25rem;
    margin-top: 0.4rem;
    font-size: 0.78rem;
}

.sc-card-stat {
    text-align: center;
    padding: 0.2rem 0;
    background: var(--bs-light);
    border-radius: 0.25rem;
}

.sc-stat-value { font-weight: 600; }

.sc-stat-label {
    font-size: 0.65rem;
    color: var(--bs-secondary);
    text-transform: uppercase;
}

.sc-desktop { display: block; }
.sc-mobile { display: none; }

@media (max-width: 767.98px) {
    .sc-desktop { display: none; }
    .sc-mobile { display: block; }

    .sc-highlights {
        padding: 0.5rem;
        font-size: 0.8rem;
    }

    .sc-highlight-rank { min-width: 3.5rem; }
    .sc-rank-number { font-size: 1.5rem; }
    .sc-rank-suffix { font-size: 0.75rem; }

    .sc-highlight-details {
        font-size: 0.78rem;
        gap: 0.2rem 0.5rem;
    }

    .sc-filter-bar {
        flex-direction: column;
        align-items: stretch;
    }

    .sc-filter-bar .d-flex { width: 100%; }
    .sc-filter-bar .form-select { flex: 1; }
    .sc-filter-bar .ms-auto { margin-left: 0 !important; }
}

/* Admin dashboard cards */
.hover-shadow {
    transition: all 0.3s ease;
    box-shadow: 0 2px 12px rgba(115, 29, 158, 0.25);
}
.hover-shadow:hover {
    box-shadow: 0 .5rem 1rem rgba(115, 29, 158, 0.35) !important;
}
@media (max-width: 767.98px) {
    .hover-shadow .card-body {
        padding: 1.5rem 1rem;
    }
    .hover-shadow .card-body svg {
        width: 36px;
        height: 36px;
    }
    .hover-shadow .card-body .mb-4 {
        margin-bottom: 0.75rem !important;
    }
    .hover-shadow .card-title {
        font-size: 1rem;
    }
    .hover-shadow .card-text {
        font-size: 0.85rem;
    }
}

/* Front dashboard grid */
.dashboard-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    grid-auto-rows: 1fr;
    gap: 1rem;
}
@media (min-width: 768px) {
    .dashboard-grid {
        grid-template-columns: repeat(3, 1fr);
    }
}
.dashboard-grid > div > a,
.dashboard-grid > div > button {
    display: block;
    height: 100%;
}
.dashboard-card {
    transition: all 0.3s ease;
    box-shadow: 0 2px 12px rgba(115, 29, 158, 0.25);
}
.dashboard-card:hover {
    box-shadow: 0 .5rem 1rem rgba(115, 29, 158, 0.35) !important;
}
@media (max-width: 767.98px) {
    .dashboard-card .card-body svg,
    .dashboard-card .card-body .ux-icon {
        width: 36px !important;
        height: 36px !important;
    }
    .dashboard-card .card-title {
        font-size: 1rem;
    }
    .dashboard-card .card-text {
        font-size: 0.85rem;
    }
}

/* ============================================
   SIGHT-ADVICE PILL (the "adjust your sight?" nudge in the scoring controls)
   ============================================ */
/* The arrow band above the pill supplies the top gap via its own mb-3; when the
   band is hidden the pill must supply its own so it doesn't abut the target. */
[data-controller="arrow-band"].d-none + .sight-advice-pill {
    margin-top: 1rem;
}

/* ============================================
   VOICE COMMANDS CARD (heard/parsed log + recording indicator)
   ============================================ */
/* Chevron in a card-header collapse toggle: points right when the body is
   collapsed, rotates down when expanded. Shared by the voice-commands and
   sight-movements cards. */
.card-collapse-chevron {
    transition: transform 0.15s ease;
}
[aria-expanded="true"] > .card-collapse-chevron {
    transform: rotate(90deg);
}
/* The whole header of a collapsible card is a click surface (bare clicks toggle
   the body; nested controls act on their own — see collapse-header.js). */
.card-collapse-header {
    cursor: pointer;
}

/* Grab surface for reordering the scoring detail cards (SortableJS handle in
   the card header — see scoring_card_order_controller.js). touch-action: none
   lets a touch drag start on the handle instead of scrolling the page. */
.scoring-card-drag-handle {
    cursor: grab;
    touch-action: none;
    padding: 0.125rem 0.375rem;
}
.scoring-card-drag-handle:active {
    cursor: grabbing;
}
/* The slot being dragged (SortableJS ghost = the drop-position preview). */
.sortable-ghost .card {
    outline: 2px dashed var(--bs-primary);
    opacity: 0.5;
}

/* WhatsApp-style voice memo button: a lock track floats above the button while
   held; sliding up onto it latches hands-free recording. */
.voice-lock-track {
    position: absolute;
    left: 50%;
    bottom: calc(100% + 0.4rem);
    transform: translateX(-50%);
    z-index: 5;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.35rem;
    padding: 0.5rem 0.4rem;
    border-radius: 999px;
    background: var(--bs-body-bg);
    border: 1px solid var(--bs-border-color);
    box-shadow: var(--bs-box-shadow, 0 0.5rem 1rem rgba(0, 0, 0, 0.15));
    color: var(--bs-secondary-color);
}
.voice-lock-chevron {
    animation: voice-lock-bob 1s infinite;
}
@keyframes voice-lock-bob {
    0%, 100% { transform: translateY(0); opacity: 0.6; }
    50% { transform: translateY(-3px); opacity: 1; }
}
.voice-lock-track--armed {
    color: #fff;
    background: var(--bs-danger);
    border-color: var(--bs-danger);
}
.voice-lock-track--armed .voice-lock-chevron {
    animation: none;
}

/* The live partial transcript reserves no space when idle — collapse it so it
   doesn't leave a gap above the empty-log placeholder. */
.voice-interim:empty {
    display: none;
}

.voice-log {
    max-height: 15rem;
    overflow-y: auto;
}
.voice-log-entry {
    padding: 0.35rem 0;
    font-size: 0.85rem;
    border-bottom: 1px dashed var(--bs-border-color);
}
.voice-log-entry:last-child {
    border-bottom: none;
}
.voice-log-entry .badge {
    margin-right: 0.4rem;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    vertical-align: 1px;
}
.voice-log-transcript {
    color: var(--bs-secondary-color);
    font-style: italic;
}
.voice-log-detail {
    color: var(--bs-body-color);
    margin-top: 0.15rem;
}

/* ============================================
   SIGHT-MOVEMENT ENTRY MODAL (knobs, readouts, live target preview)
   ============================================ */
/* Target preview beside the one-tap centre pill; collapses (no stray margin)
   when the WA gate hides both. */
.sight-hero {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.5rem;
}
.sight-hero:has(> :not(.d-none)) {
    margin-bottom: 0.75rem;
}
/* The app's "why" when no correction is due — or when it's already applied: a
   compact purple alert, the non-clickable twin of the purple .btn-suggestion
   pill above it (same #731d9e computed-aid hue). */
.sight-advice-reason {
    display: inline-block;
    max-width: 100%;
    padding: 0.375rem 0.75rem;
    border-radius: 0.5rem;
    background-color: rgba(115, 29, 158, 0.1);
    border: 1px solid rgba(115, 29, 158, 0.35);
    color: #731d9e;
}

.sight-knobs {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0.75rem;
}
.sight-knobwrap {
    text-align: center;
}

.sight-knob {
    /* A dark physical dial, independent of the page light/dark theme. */
    --sight-knob-rim: #14161a;
    --sight-knob-face: #33373c;
    --sight-knob-detent: #6b7178;
    width: 8rem;
    max-width: 100%;
    height: auto;
    margin: 0 auto;
    display: block;
    cursor: grab;
    touch-action: none;
    user-select: none;
    -webkit-user-select: none;
}
.sight-knob:active {
    cursor: grabbing;
}
.sight-knob:focus-visible {
    outline: 2px solid var(--bs-primary);
    outline-offset: 3px;
    border-radius: 50%;
}

/* ◀▶ / ▲▼ sit below the knob, bottom-most where the thumb already is. */
.sight-knob-mini {
    display: flex;
    justify-content: center;
    gap: 0.375rem;
    margin-top: 0.5rem;
}
.sight-knob-mini .btn {
    width: 2.5rem;
    padding: 0.125rem 0;
    line-height: 1;
}

/* Fixed height so a two-line magnitude ("1 turn + 4 clicks") never changes the
   box height and shoves the knob below it. */
.sight-readout {
    height: 3.75rem;
    margin-bottom: 0.5rem;
    padding: 0.375rem;
    display: flex;
    flex-direction: column;
    justify-content: center;
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius);
    background: var(--bs-tertiary-bg);
}
.sight-readout-value {
    font-size: 0.9rem;
    font-weight: 700;
    line-height: 1.15;
}
.sight-readout-value [aria-hidden="true"] {
    color: var(--bs-primary);
}
.sight-readout-raw {
    font-size: 0.7rem;
    color: var(--bs-secondary);
    margin-top: 0.1rem;
    min-height: 0.9rem;
}
.sight-readout.is-zero .sight-readout-value {
    color: var(--bs-secondary);
    font-weight: 600;
}

.sight-target-preview {
    width: 14rem;
    max-width: 100%;
    /* The target is square; reserve that height up front (the box is un-hidden
       before the modal fades in) so the async renderer fills the reserved square
       instead of growing the centred dialog and jolting everything vertically. */
    aspect-ratio: 1;
}
.sight-target-preview svg {
    width: 100%;
    height: auto;
    border-radius: var(--bs-border-radius);
    box-shadow: 0 0 0 1px var(--bs-border-color);
}

/* =========================================================================
   "What if?" card (scoring detail) — assets/controllers/scoring_what_if_controller.js
   ========================================================================= */
.what-if-card {
    /* Fixed hues, not theme tokens, so they stay legible on any ring colour and
       in both themes: the band/curve blue, and the ring gain/loss strokes. */
    --what-if-band: #2a78d6;
    --what-if-curve: #2a78d6;
    --what-if-gain: #0ca30c;
    --what-if-loss: #d03b3b;
}

/* Column heading — the "Et si..." question continued, atop each counterfactual. */
.what-if-section-head {
    margin: 0 0 8px;
    font-size: 0.74rem;
    font-weight: 650;
    color: var(--bs-secondary-color);
    text-transform: uppercase;
    letter-spacing: 0.03em;
}

/* Divider between the two counterfactual columns — vertical while they sit
   side by side (≥lg), horizontal once they stack one under the other. */
@media (min-width: 992px) {
    .what-if-col-divider {
        border-left: 1px solid var(--bs-border-color);
    }
}
@media (max-width: 991.98px) {
    .what-if-col-divider {
        border-top: 1px solid var(--bs-border-color);
        padding-top: 1rem;
    }
}

/* Sliders (epsilon + shift) share one look. */
.what-if-slider {
    width: 100%;
    accent-color: var(--bs-primary);
}
.what-if-slider-labels {
    display: flex;
    justify-content: space-between;
    font-size: 0.66rem;
    color: var(--bs-secondary-color);
    margin-top: -2px;
    margin-bottom: 16px;
}

/* Column intro question, above each slider. Its height is pinned to two lines
   so the group's live-updating text can't reflow (1↔2 lines) and jog everything
   below it as the slider moves. */
.what-if-intro {
    display: block;
    font-size: 0.8rem;
    line-height: 1.4;
    min-height: 2.8em;
    color: var(--bs-body-color);
    margin: 0 0 8px;
}
/* The line-cutter counts (slider-dependent), under the histogram. Framed as
   potential and coloured like the target: arrows that can still catch a higher
   ring read green, arrows that can still drop their own read red. */
.what-if-cutters-summary {
    font-size: 0.8rem;
    color: var(--bs-body-color);
    margin: 18px 0 0;
}
.what-if-cutters-summary .gain {
    color: var(--what-if-gain);
    font-weight: 600;
}
.what-if-cutters-summary .loss {
    color: var(--what-if-loss);
    font-weight: 600;
}
/* The re-roll button + one draw's total/delta, as a headline over the histogram. */
.what-if-example-head {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 14px;
}
.what-if-example {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 6px;
}
.what-if-example .label {
    font-size: 0.78rem;
    color: var(--bs-secondary-color);
}
.what-if-example .value {
    font-size: 1.4rem;
    font-weight: 650;
    color: var(--bs-body-color);
    font-variant-numeric: tabular-nums;
    line-height: 1.1;
}

/* Distribution histogram — capped so it never upscales past its viewBox (which
   would balloon the SVG text); it fills narrower columns and centres in wider. */
.what-if-histogram svg {
    width: 100%;
    max-width: 360px;
    height: auto;
    display: block;
    margin: 0 auto;
}

/* Mini target — the real renderer, auto-zoomed to the group. Dots slide to their
   shifted position with a short transition. */
.what-if-stage {
    text-align: center;
}
.what-if-stage svg {
    width: 100%;
    max-width: 220px;
    height: auto;
    display: block;
    margin: 0 auto;
}
.what-if-stage svg .dot {
    transition: cx 0.2s ease, cy 0.2s ease;
}

/* Shift readout: the shifted total, a signed delta chip, and a plain description. */
.what-if-shift-readout {
    display: flex;
    align-items: baseline;
    gap: 8px;
    margin-bottom: 4px;
}
.what-if-shift-readout .value {
    font-size: 1.5rem;
    font-weight: 650;
    color: var(--bs-body-color);
}
.what-if-chip {
    font-weight: 650;
}
.what-if-chip.up {
    color: var(--what-if-gain);
}
.what-if-chip.down {
    color: var(--what-if-loss);
}

.what-if-curve-wrap svg {
    width: 100%;
    max-width: 340px;
    height: auto;
    display: block;
    margin: 0 auto;
}

/* ==========================================================================
   Match chrome (scoreboard strip, set dots, pairing table)
   ========================================================================== */

.match-strip {
    position: sticky;
    top: 0;
    z-index: 1020;
}

.match-side-name {
    font-weight: 600;
    font-size: 0.85rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.match-side-opponent {
    color: #e8553d;
}

.match-set-dots {
    display: flex;
    gap: 0.3rem;
    justify-content: center;
}

.match-set-dot {
    width: 0.65rem;
    height: 0.65rem;
    border-radius: 50%;
    border: 2px solid var(--bs-border-color);
    background: transparent;
}

.match-set-dot.won-self {
    background: var(--bs-primary);
    border-color: var(--bs-primary);
}

.match-set-dot.won-opponent {
    background: #e8553d;
    border-color: #e8553d;
}

/* Split down the middle in the strip's own left/right order: self (primary) on
   the left, opponent on the right. `background-origin` must be the border box —
   the dot's 2px border is transparent here, so the gradient is painted over it
   (`background-clip` defaults to border-box), but it would still be *sized* to
   the padding box and tile across the ring, striping the dot. */
.match-set-dot.draw {
    background: linear-gradient(90deg, var(--bs-primary) 50%, #e8553d 50%);
    background-origin: border-box;
    border-color: transparent;
}

/* The pairing table's own scroll container is its containing block, so nothing
   absolutely positioned inside a table wider than the phone can escape it. Without
   this, the containing block is the enclosing `.card` (Bootstrap positions cards),
   which is OUTSIDE the scroller: such a box then sits at its static position past
   the viewport's right edge, widens the document, and the sideways scroll that
   creates detaches the fixed `.match-drawer` from the visual viewport. */
[data-match-target="pairTable"] {
    position: relative;
}

/* Pairing table (S5): the index/total/running-score columns stay at their
   natural nowrap width (set via `text-nowrap` in the markup); the two badge
   columns take whatever width is left, so smaller badges here keep several
   arrows on one line at phone widths instead of the table overflowing. */
.match-pair-table .badge {
    padding: 0.2em 0.35em;
    font-size: 0.7rem;
}

/* An entry's arrow badges are one unit. They are atomic inlines, so the browser
   is free to break between two of them even though the markup leaves no
   whitespace there — which is how "10 9 9" ended up split over two lines in a
   squeezed cell at 390px. The shoot-off distance lines and a team match's
   per-archer rows share these cells but are <div>s, so nowrap here does not
   pull them up onto the badge line. The empty-state row is the one full-width
   cell that must still wrap, hence the colspan exclusion. */
.match-pair-table tbody td:not([colspan]) {
    white-space: nowrap;
}

/* The correction column is an icon, not data. A seventh column's worth of cell
   padding plus a `btn-sm`'s own padding was 32px of a 390px phone — enough to push
   the badge columns into the `.table-responsive`'s sideways scroll, which is the
   one thing S5 exists to prevent. `width: 1px` is the table-layout idiom for
   "shrink to your content". */
.match-pair-table .match-pair-correct-col {
    width: 1px;
    padding-left: 0;
    padding-right: 0;
}

.match-pair-table .match-pair-correct-col .btn {
    --bs-btn-padding-x: 0.1rem;
    --bs-btn-padding-y: 0;
    line-height: 1;
}

/* Horizontal cell padding at phone widths: seven columns pay it fourteen times,
   so the default is most of the room the badge columns need to keep an entry's
   three arrows on one line. Vertical padding is untouched — the rows still need to
   be thumb-height. */
@media (max-width: 575.98px) {
    .match-pair-table > :not(caption) > * > * {
        padding-left: 0.15rem;
        padding-right: 0.15rem;
    }
}

/* An archer's name heads a badge column but must not size it: left to dictate
   its own min-content width, a long name (a double-barrelled surname is
   enough) pushed the whole table past a 390px phone and squeezed the badges
   below it onto two lines. Breaking the name anywhere keeps the column sized
   to the three badges it exists to show. */
.match-pair-table th {
    overflow-wrap: anywhere;
}

/* ===== Arrows line up in columns =====

   Every match surface draws arrows through `match_controller.arrowBadge`, and
   every one of them puts those badges in a fixed grid — so an arrow occupies the
   same box and the same column whatever its value, and whoever shot it.

   Both halves are load-bearing. A `10` badge is wider than a `9` and an `X` is
   narrower still, so an unsized badge shifts every arrow after it; and an inline
   archer name is wider for "Dorian V." than for "Hou Yi", so it shifted that
   archer's whole row further right than their team-mate's. Between them, no two
   rows of a set shared a column and the eye could not read down one to compare an
   end (field report, 2026-08-14). */
.badge.match-arrow-badge {
    display: inline-block;
    /* Sized to a two-digit `10` and no wider. Every badge pays this width, so a
       generous value costs three times over in a 3-arrow cell and twice again on
       the opposing side — at 1.6rem that alone pushed the pairing table past a
       390px phone and into the sideways scroll `match-phone-viewport.spec.ts`
       exists to forbid. Horizontal padding is zero for the same reason: the width
       IS the padding. The fallback is what a badge drawn outside an arrow-cell
       grid gets — the reveal sheet's individual column draws straight into the flow. */
    width: var(--match-arrow-badge-width, 1.3rem);
    padding: 0.2em 0;
    font-size: 0.7rem;
    line-height: 1.35;
    text-align: center;
}

.match-arrow-cells {
    display: inline-grid;
    /* `--arrow-cells` is written inline by `arrowCells()`: the grid holds the
       arrows the ENTRY asks for, not the ones recorded so far, so a half-shot set
       keeps its remaining columns open rather than centring what it has. */
    grid-template-columns: repeat(var(--arrow-cells, 3), var(--match-arrow-badge-width, 1.3rem));
    column-gap: 0.1rem;
    justify-items: center;
    align-items: center;
}

/* Anything that is not an arrow — the en dash, the "total only" hint, a volley
   total — is one statement about the whole entry rather than about one arrow, so
   it spans the row. The reveal sheet's waiting dots are excluded: each one stands
   in for a single arrow and holds that arrow's own column until it pops. */
.match-arrow-cells > :not(.match-arrow-badge):not(.spinner-grow) {
    grid-column: 1 / -1;
    justify-self: end;
}

/* Per-archer breakdown rows inside a team match's pairing-table cells and
   reveal sheet: one named line per archer under the folded side total. The name
   is its own right-aligned column — right-aligned so it ends against its arrows
   however long it is, and truncating so a double-barrelled surname cannot widen
   the cell and push the arrow columns off a 390px phone. */
.match-per-archer {
    display: grid;
    /* The floor matters: at `minmax(0, …)` the name column may shrink to nothing,
       and in a pairing-table cell competing with six other columns it collapsed to
       a bare ellipsis that named nobody. */
    grid-template-columns: minmax(2.75rem, 1fr) max-content;
    column-gap: 0.25rem;
    align-items: center;
    line-height: 1.5;
}

.match-per-archer-label {
    overflow: hidden;
    text-align: right;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.match-per-archer + .match-per-archer {
    margin-top: 0.1rem;
}

/* The finished match's side-level statistics. The two value columns are a
   COMPARISON, so they have to be the same width: sized by content they took the
   width of their team names instead, and a long name against a short one put the
   two numbers of every row at different distances from the label they answer. */
.match-side-stats {
    table-layout: fixed;
}

.match-side-stats-label {
    width: 34%;
}

.match-side-stats-value {
    width: 33%;
}

/* The finished match's per-archer statistics: one table per side, stacked. They
   are two separate tables, so only declared widths can make their columns line
   up — content-sized, the side with the longer names pushed every one of its
   columns out of step with the other's and the two blocks stopped reading as one
   comparison. Percentages rather than rems so the geometry holds at any width. */
.match-per-archer-stats {
    width: 100%;
    table-layout: fixed;
}

.match-per-archer-stats-name {
    width: 28%;
}

.match-per-archer-stats-avg {
    width: 14%;
}

.match-per-archer-stats-count {
    width: 10%;
}

.match-per-archer-stats-entry {
    width: 19%;
}

/* ===== The WA match scorecard =====

   The official form is per SIDE — two half-sheets, one per team, each signed by
   its own archer/agent — so the card stacks one sheet per side rather than
   pairing the two into one table the way the pairing table above does. */
.match-scorecard-sheets {
    display: flex;
    flex-direction: column;
    gap: 1.25rem;
}

.match-scorecard-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0 0.5rem;
    margin-bottom: 0.35rem;
}

/* The grid is the widest thing on a match page — a recurve team sheet is six
   arrow columns plus three of its own.

   `table-layout: fixed` is the load-bearing part: with the four non-arrow columns
   given explicit widths in the `<colgroup>`, the browser splits what remains
   EQUALLY between the arrow columns, so no cell's content can size its own
   column. Content-sized, a long archer name or a spelled-out "total only" cell
   widened that archer's group and the six arrow columns came out visibly unequal.

   `min-width` keeps the columns legible rather than letting them collapse on a
   narrow phone; past that the table scrolls sideways inside its own
   `.table-responsive`, never widening the document (see `.match-drawer`'s
   `overflow-x` for why a sideways-scrolling page makes the drawer untappable). */
.match-scorecard-table {
    width: 100%;
    min-width: 21rem;
    font-size: 0.85rem;
    table-layout: fixed;
}

.match-scorecard-table > :not(caption) > * > * {
    overflow: hidden;
    padding: 0.25rem 0.3rem;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.match-scorecard-col-index {
    width: 1.75rem;
}

.match-scorecard-col-total {
    width: 3rem;
}

.match-scorecard-col-points {
    width: 3.9rem;
}

/* Wide enough for its own abbreviated heading. At 3.4rem the fixed layout
   truncated "Tot. PS" to "Tot...." — a header abbreviated twice over says
   nothing, and the ellipsis read as a column that had been squeezed out. */
.match-scorecard-col-tally {
    width: 4.1rem;
}

.match-scorecard-table .badge {
    padding: 0.2em 0.35em;
    font-size: 0.7rem;
}

/* The set-index column carries a header with no word in it, so it must not be
   given a column's worth of width. `width: 1px` is the table-layout idiom for
   "shrink to your content". */
.match-scorecard-table .match-scorecard-index {
    width: 1px;
}

/* An archer's name heads their own arrow columns and must not size them: left to
   dictate its own min-content width, a long name pushes the sheet past the phone
   — the same trap `.match-pair-table th` documents. Truncated rather than
   wrapped, because `overflow-wrap: anywhere` broke names mid-word ("Ashitak/a")
   and still cost two lines of header height; the full name is on the `title`. */
.match-scorecard-table .match-scorecard-archer {
    font-size: 0.75rem;
    font-weight: 600;
}

.match-scorecard-table td[data-scorecard-arrow] {
    padding-left: 0.1rem;
    padding-right: 0.1rem;
}

/* An abbreviated column heading. `abbr` brings its own dotted underline and a
   help cursor in most browsers, which reads as an affordance the header does not
   have on a phone — the long form is a `title` for pointer and screen reader,
   not something to invite a tap. */
.match-scorecard-abbr {
    text-decoration: none;
    cursor: inherit;
}

/* The printed form has the scorer ring one of `2 1 0`, so all three are shown
   and the awarded one is ringed. The ring is drawn as a circle rather than a
   background fill because that is the gesture it stands for — and because a
   filled `0` and an empty `0` must not be confusable at a glance. */
.match-scorecard-table .match-scorecard-points {
    white-space: nowrap;
}

.match-scorecard-ring {
    display: inline-block;
    min-width: 1.25em;
    padding: 0 0.15em;
    border: 1px solid transparent;
    border-radius: 50%;
    color: var(--bs-secondary-color);
    line-height: 1.25;
}

.match-scorecard-ring.is-awarded {
    border-color: currentcolor;
    color: var(--bs-body-color);
    font-weight: 700;
}

/* Side-toggle buttons: the opponent side uses the coral accent the rest of
   the match chrome uses (`.match-side-opponent`). */
.btn-match-opponent {
    --bs-btn-bg: #e8553d;
    --bs-btn-border-color: #e8553d;
    --bs-btn-color: #fff;
    --bs-btn-hover-bg: #d44a33;
    --bs-btn-hover-border-color: #d44a33;
    --bs-btn-hover-color: #fff;
    --bs-btn-active-bg: #d44a33;
    --bs-btn-active-border-color: #d44a33;
    --bs-btn-active-color: #fff;
}

.btn-outline-match-opponent {
    --bs-btn-color: #e8553d;
    --bs-btn-border-color: #e8553d;
    --bs-btn-hover-bg: #e8553d;
    --bs-btn-hover-border-color: #e8553d;
    --bs-btn-hover-color: #fff;
    --bs-btn-active-bg: #e8553d;
    --bs-btn-active-border-color: #e8553d;
    --bs-btn-active-color: #fff;
}

/* The opponent's cloned editor behind the side toggle (the seat of a live
   ceremony whose arrows the recorder types). It is a clone of the
   user's own editor and swaps into the same place, so the coral frame and the
   named header are the only thing telling the two apart — without them, arrows
   land on the wrong side. Coral is the match chrome's opponent accent
   (`.match-side-opponent`). */
.match-side-opponent-editor {
    border: 2px solid #e8553d;
    border-radius: var(--bs-border-radius-lg);
    background-color: rgba(232, 85, 61, 0.05);
    padding: 0.5rem;
}

.match-side-opponent-editor-label {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    font-weight: 600;
    font-size: 0.9rem;
    margin-bottom: 0.5rem;
}

/* The archer's OWN editor, framed the same way once an opponent surface shares
   the page. Without it the coral frame is the only marked side, so the unmarked
   one reads as "the neutral default" rather than as the archer's own — the two
   swap into the same place, and that ambiguity is how arrows land on the wrong
   side. Blue is the self accent everywhere else in the chrome
   (`.match-set-dot.won-self`). */
.match-side-self-editor {
    border: 2px solid var(--bs-primary);
    border-radius: var(--bs-border-radius-lg);
    background-color: rgba(var(--bs-primary-rgb), 0.05);
    padding: 0.5rem;
}

.match-side-self-editor-label {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    font-weight: 600;
    font-size: 0.9rem;
    margin-bottom: 0.5rem;
    color: var(--bs-primary);
}

/* Stacked on a finished match, the two framed surfaces need air between them:
   without it the coral frame starts where the blue one ends and the pair reads as
   one bordered block. In progress they swap into the same place and only ever one
   is on screen, so the margin costs nothing there. */
.match-side-self-editor,
.match-side-opponent-editor {
    margin-bottom: 1rem;
}

/* ===== The condensed team recording surface =====

   One card per side (the two frames above), each holding the side's archers in
   shooting order. Every archer of the match is on screen at once with the arrows
   they have in for the entry, and one keypad follows whichever row is active — so
   recording a whole team never leaves the page.

   The rows are dense on purpose: six of them plus both cards' chrome has to fit
   above the two fixed bottom bands on a phone. */
.team-entry-row {
    border-radius: var(--bs-border-radius);
    padding: 0.35rem 0.5rem;
}

.team-entry-row + .team-entry-row {
    margin-top: 0.25rem;
}

/* The active row is the one the next key press lands on, so it is marked with the
   side's own accent — the same blue/coral pair that tells the two editor frames
   apart, for the same reason: arrows typed onto the wrong archer are what the
   marking exists to prevent. */
.team-entry-row-active {
    background: var(--bs-primary-bg-subtle);
    outline: 2px solid var(--bs-primary);
}

.match-side-opponent-editor .team-entry-row-active {
    background: rgba(232, 85, 61, 0.12);
    outline-color: #e8553d;
}

/* An archer whose arrows are all in: green, with a ✓ beside the name. Entry stays
   on them until the recorder confirms them ("Confirm Charline's set"), so green
   wins over the active-row accent — the row has to read as finished rather than as
   still taking presses. Same rule in the opponent card. */
.team-entry-row-complete {
    background: var(--bs-success-bg-subtle);
    outline: 2px solid var(--bs-success);
}

.match-side-opponent-editor .team-entry-row-complete {
    background: var(--bs-success-bg-subtle);
    outline-color: var(--bs-success);
}

/* The archer's place in the shooting order: fixed width, so six names start at the
   same x whatever their number. */
.team-entry-order {
    width: 1.6em;
}

.team-entry-total {
    min-width: 2em;
    text-align: right;
}

/* One arrow cell of a row: a badge once the arrow is in, this placeholder while it
   is owed. A typed row's placeholder is a real <button> — it is literally where the
   next arrow goes, so on a phone it is the most direct thing to reach for — which
   is why the reset below matters: Bootstrap's button styling would otherwise give
   it a background and a font it must not have. */
.team-arrow-slot {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2em;
    height: 2em;
    padding: 0;
    border-radius: var(--bs-border-radius);
    border: 1px dashed var(--bs-border-color);
    background: transparent;
    vertical-align: middle;
}

/* Where the NEXT press lands, on the active row alone. */
.team-arrow-slot-target {
    border: 2px solid var(--bs-primary);
    background: var(--bs-primary-bg-subtle);
}

.match-side-opponent-editor .team-arrow-slot-target {
    border-color: #e8553d;
    background: rgba(232, 85, 61, 0.12);
}

/* The spot the archer stands at, on a trispot face: the face's three spots with
   theirs filled in. Navigation only — an arrow's own spot stays derived from its
   coordinates — so this is a legend, never a score. */
.team-entry-spot {
    display: inline-flex;
    flex-direction: column;
    gap: 1px;
    flex: 0 0 auto;
}

.team-entry-spot-dot {
    display: block;
    width: 7px;
    height: 7px;
    border: 1px solid var(--bs-border-color);
    border-radius: 50%;
}

.team-entry-spot-dot-own {
    background: #ffe928;
    border-color: var(--bs-body-color);
}

/* The focused in-progress page: while a match is being shot, the cards that
   analyse a finished scoring step back. Toggled as a body class by
   `match_controller._applyFocusedPage()` — see its docblock for why it is one CSS
   rule rather than a gate in each of the four controllers. `!important` because
   each of those controllers un-hides its own card from its own IDB inspection,
   with no idea a match is in progress. */
body.match-focused [data-reorder-key="comparison"],
body.match-focused [data-reorder-key="what_if"],
body.match-focused [data-reorder-key="arrow_performance"],
body.match-focused [data-reorder-key="voice_commands"] {
    display: none !important;
}

/* A match page, of ANY format: the QUALIFICATION scorecard steps aside for good —
   its arrow columns, cumulative running total and distance footer answer a
   qualification's question on a page that is not one, and it shows one side where a
   match is two. The match's own scorecard is the pairing table, plus the WA
   scorecard on a team page. Toggled by `match_controller._applyMatchPage()` — see
   its docblock for why a class of its own rather than `match-focused` (which lifts
   on the result page), and for what the 10+X / X tallies cost. */
body.match-page [data-reorder-key="scorecard"] {
    display: none !important;
}

/* ===== The match drawer =====

   The match's command strip: a slim fixed band carrying the side identity, the
   side toggle and the phase action (confirm / reveal / shoot-off / judgment).
   Fixed at the bottom so these controls stay within reach of the entry surface:
   a recorder must never have to scroll away from the target to confirm a set.

   Stacking: z-index 1031 — just ABOVE the keypad drawer (1030), and below every
   sheet (NAC 1039/1040, move-to-round 1050, tournament 1050/1055, end photo
   1080/1081, toasts 1090). Above, not below, because of the keypad drawer's
   `translateY(calc(100% - 56px))`: only its 56px handle is on screen, but its BOX
   still extends the full drawer height downwards, past the viewport bottom — and
   hit-testing counts the clipped part. Under the keypad, every tap meant for the
   side toggle or the confirm button landed on that invisible overlap instead.
   Nothing is lost visually: the two bands tile edge to edge, so the keypad's
   rounded top sits above this one and stays visible.

   Geometrically below it sits the persistent tournament footer — the only fixed
   band this one docks ON, hence its `bottom`, which is 0px of offset on a page
   with no footer. In z-order the footer is *above* this band
   (1032), for the very reason spelled out above: everything that tiles against
   the viewport bottom has to clear the keypad's clipped overhang. The two never
   overlap, so their relative order between themselves is free. See the
   `.tournament-footer` block for the whole stacking contract.

   `--match-drawer-height` is the band's measured height, published on `:root` by
   `match_controller.renderDrawer()` and consumed by everything that has to make
   room: the keypad drawer's `bottom` offset, the single `document.body.paddingBottom`
   writer in `arrow_keypad_controller`, the plotting SVG's viewport budget and the
   toast container. It is 0px whenever the drawer is not on screen, so every one of
   those reads is a no-op on a plain scoring — and all but the plotting SVG read
   `--bottom-bands-height` rather than this one alone, because any band can be
   there without the others. */
:root {
    --match-drawer-height: 0px;
}

.match-drawer {
    position: fixed;
    left: 0;
    right: 0;
    bottom: var(--tournament-footer-height);
    z-index: 1031;
    background-color: var(--bs-body-bg);
    border-top: 1px solid var(--bs-border-color);
    border-top-left-radius: var(--bs-border-radius-lg);
    border-top-right-radius: var(--bs-border-radius-lg);
    box-shadow: 0 -0.25rem 0.75rem rgba(0, 0, 0, 0.12);
    padding-top: 0.4rem;
    padding-bottom: calc(0.4rem + env(safe-area-inset-bottom, 0px));
    /* Content in here can never widen the page. The band is FIXED and spans
       `left: 0; right: 0`, so its width IS the initial containing block's — and a
       child that refuses to shrink (a long opponent name in the side toggle) does
       not merely look cramped: it grows the ICB, the document gains a sideways
       scroll, and the layout viewport detaches from the visual one. The band is
       then pinned to the bottom of a box wider and taller than the screen, so every
       tap aimed at it lands ~100px off — Playwright reports that as a click timeout
       blamed on whatever unrelated element sits at the mistaken point. The rules
       below make the content genuinely fit; this is the guarantee that nothing added
       later can reintroduce the failure.

       `clip`, not `hidden`: `overflow-x: hidden` computes `overflow-y` to `auto`,
       which would make the band a scroll container. */
    overflow-x: clip;
}

/* ── The bottom-band stack's total ───────────────────────────────────────────

   Every surface that has to keep clear of the bottom of the viewport reads this
   one property, in CSS and — through `bottomBandsHeight()` — in JS, so nobody
   has to know which bands a given page carries. The stack itself is declared in
   `assets/lib/bottom-bands.js` (`BOTTOM_BAND_STACK`) and
   `tests/js/bottom-bands.test.js` fails if the terms below stop matching it, so
   a new band is registered there and lands here by that test rather than by
   anyone remembering.

   Registered with `@property` because `calc()` in a plain custom property is
   substituted but never evaluated: `getPropertyValue()` would hand JS back the
   expression verbatim instead of a length. `<length>` makes it compute to
   pixels; the `0px` initial value keeps a page with no band at all inert. */
@property --bottom-bands-height {
    syntax: "<length>";
    inherits: true;
    initial-value: 0px;
}

:root {
    --bottom-bands-height: calc(var(--tournament-footer-height) + var(--match-drawer-height) + var(--challenge-drawer-height) + var(--scoring-complete-footer-height));
}

/* ── The plotting target's own total ─────────────────────────────────────────

   The one consumer that must NOT read the sum above: the plotting SVG's viewport
   budget (`_scoring_arrow_plotting.html.twig`), which sizes the target the archer
   is aiming at. Subtracting the whole stack would shrink that target the instant
   the completion band rises — resizing the plot at the exact moment the archer is
   reading it, and buying nothing, since a complete scoring has no arrow left to
   place. Subtracting the match drawer alone (which is what this was, before the
   challenge band existed) silently under-allocates the moment a second mid-shoot
   band ships.

   So the budget gets its own derived total, over the bands that declare
   `shrinksThePlottingTarget` in `BOTTOM_BAND_STACK` — each of them with its own
   reason recorded there, the tournament footer's accepted over-allocation
   included. `plotTargetBands()` derives the membership and
   `tests/js/bottom-bands.test.js` holds the terms below to it, so this sum cannot
   drift from the stack any more than the one above can.

   Registered as a `<length>` for the same reason as the stack's total: a plain
   custom property substitutes its `var()`s and never evaluates the `calc()`, so
   any JS reader would get the expression back rather than a number. */
@property --plot-target-bands-height {
    syntax: "<length>";
    inherits: true;
    initial-value: 0px;
}

:root {
    --plot-target-bands-height: calc(var(--match-drawer-height) + var(--challenge-drawer-height));
}

/* Reserving the bands' space is CSS, not JS, because the one JS writer
   (`applyDrawerPadding` in `arrow_keypad_controller`) only exists on a page whose
   scoring method is `detailed_ends` — a plotting or totals match page has no keypad
   controller at all, and without this rule the drawer covered the bottom of its
   content. An inline `padding-bottom` from that writer overrides this declaration,
   so the two compose rather than fight: keypad pages get `own band + drawer +
   footer`, every other page gets whichever of the two bands it actually has.

   The reservation is the SUM, and every custom property defaults to 0px, so the
   one declaration serves a drawer-only page, a footer-only page (a tournament
   qualification round), a page carrying both, and a plain fixed scoring whose
   completion band came up. */
body:has(.match-drawer:not(.d-none)),
body:has(.tournament-footer),
body:has(.challenge-band:not(.d-none)),
body:has(.scoring-complete-footer) {
    padding-bottom: var(--bottom-bands-height);
}

html:has(.match-drawer:not(.d-none)),
html:has(.tournament-footer),
html:has(.challenge-band:not(.d-none)),
html:has(.scoring-complete-footer) {
    scroll-padding-bottom: var(--bottom-bands-height);
}

/* The drawer is the match's command strip on every viewport, not a mobile
   workaround — so its content lines up with the page container rather than
   stretching edge to edge on a desktop. */
.match-drawer-inner {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
}

/* The band's status line: the identity on the left, the background-change
   announcement on the right. One line for both, because the identity's height is
   already reserved (it goes `invisible`, never away) — so the announcement coming
   and going cannot change the FIXED band's height, and cannot move the buttons
   under a thumb. */
.match-drawer-line {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

/* "Recording: You — Set 3", with the side's own accent as a left rule. Blue for
   the archer's own side, coral for an opponent's — the same two accents the
   editor frames use, because this line and those frames answer the same
   question. */
.match-drawer-identity {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    padding-left: 0.5rem;
    border-left: 3px solid var(--bs-primary);
    font-size: 0.8rem;
    font-weight: 600;
    line-height: 1.2;
    color: var(--bs-primary);
    /* One line, whatever the archer is called: a wrapped identity would change the
       fixed band's height between renders, which is the one thing it must not do. */
    min-width: 0;
    white-space: nowrap;
}

.match-drawer-identity > span {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}

.match-drawer-identity.match-drawer-identity-opponent {
    border-left-color: #e8553d;
    color: #e8553d;
}

/* "Set 2 is in: 27 – 25" — what a background pull brought in, pushed to the far
   end of the status line. Deliberately SMALLER than the identity so it can never
   be what sets the line's height, and it gives way first when the two texts
   compete for a narrow band: whose arrows the surface is about to take outranks
   what already happened elsewhere. */
.match-drawer-announcement {
    display: flex;
    align-items: center;
    gap: 0.25rem;
    margin-left: auto;
    /* Shrinks four times faster than the identity beside it — see above. */
    flex: 0 4 auto;
    min-width: 0;
    font-size: 0.72rem;
    font-weight: 600;
    line-height: 1.2;
    color: var(--bs-secondary-color);
    white-space: nowrap;
}

.match-drawer-announcement > span {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}

.match-drawer-announcement > .bi {
    flex: 0 0 auto;
}

/* The handoff button pulses: the archer's own half of the entry is in and the
   opponent's is owed, so this is the one thing left to do — and it is an OFFER,
   never an automatic switch, so it has to draw the eye on its own. */
@keyframes match-handoff-pulse {
    0%, 100% { box-shadow: 0 0 0 0 rgba(232, 85, 61, 0.5); }
    50% { box-shadow: 0 0 0 0.35rem rgba(232, 85, 61, 0); }
}

.match-handoff-pulse {
    animation: match-handoff-pulse 1.8s ease-out infinite;
}

@media (prefers-reduced-motion: reduce) {
    .match-handoff-pulse {
        animation: none;
    }
}

/* Correction mode: the whole band goes amber, so "I am reopening a settled entry"
   is unmistakable next to the ordinary recording state. The pencilled row in the
   pairing table turns amber too — the two say the same thing about the same set. */
.match-drawer.match-drawer-correcting {
    background-color: var(--bs-warning-bg-subtle);
    border-top-color: var(--bs-warning-border-subtle);
}

.match-drawer-correcting .match-drawer-identity {
    border-left-color: var(--bs-warning-text-emphasis);
    color: var(--bs-warning-text-emphasis);
}

/* One compact row: the toggle takes the room it needs, the phase action the
   rest. */
.match-drawer-row {
    display: flex;
    align-items: stretch;
    gap: 0.4rem;
    /* Pinned, because the row is a FIXED band's content and its phase action swaps
       between the confirm, the handoff, the shoot-off pair and the correction Done.
       A row that changed height with the label would move the buttons under the
       finger — and a moving fixed element is not a clickable one (Playwright reports
       it as a click timeout on an element that is plainly visible). */
    min-height: 2.25rem;
}

/* …except under the shoot-off panel, where the reservation buys nothing and shows:
   the panel is a block that comes and goes on its own, and the phase action whose
   arrival the pinned line protects against (the confirm) cannot appear while the
   shoot-off owns the drawer. Left pinned it is a 2.25rem hole between the side
   toggle and the panel. Collapsed only while the confirm host is actually empty —
   an each-side confirm or a pacing hint rendering there keeps its line. */
.match-drawer-inner:has([data-match-target="shootOffPanel"]:not(.d-none))
    .match-drawer-row:has(> [data-match-target="confirmBar"].d-none) {
    min-height: 0;
}

/* …and once the match is SETTLED, where every reservation in the band buys nothing
   at all: `match-drawer-settled` is the band descriptor's own `settled`
   (`match-band.js`, the `finished-unread` rule) — the phase is finished, the ceremony
   is not reopened, and the band's only content is the see-result action. The published
   height is computed from the same declaration, so this rule and
   `--match-drawer-height` cannot disagree about which rows are there. Nothing can materialise beside it — no confirm unlocks, no
   side is left to switch to, no announcement is held on a finished match — and what
   the reservations left instead was a ~60px empty band above the one button in it:
   a kept status line, then a pinned row whose only child is `d-none`.

   Whole rows go rather than shrinking to `min-height: 0`, because the inner box is a
   flex column with a `gap`: a zero-height row still spends its gap. Correction mode
   drops the class, which is what puts the reservation back for the ceremony it
   revives. */
.match-drawer-settled .match-drawer-line,
.match-drawer-settled .match-drawer-row:not(:has(> :not(.d-none))) {
    display: none;
}

/* Same reason: one line per phase action, ellipsised rather than wrapped. Laid out
   as a flex row so the ellipsis eats the archer's NAME and never the icon or the
   progress badge — "3/3" is why the badge is there, and `text-overflow` applied to
   the button as a whole would truncate whatever came last. */
.match-drawer-row .btn {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.25rem;
    white-space: nowrap;
}

.match-drawer-row .btn > .bi,
.match-drawer-row .btn > .badge {
    flex: 0 0 auto;
}

/* A phase action that is a STATE rather than a gesture (the pacing wait) renders as
   an `.alert` in the very same row, and it has to be the same box as the button it
   stands in for. Bootstrap sizes `.alert` as a page-level block — `1rem` of vertical
   padding, ~58px tall — so left at its own metrics the band grows 20px the moment the
   wait replaces the confirm: a FIXED band's buttons move under a thumb, which is the
   one thing `.match-drawer-row`'s pinned `min-height` exists to prevent. It also makes
   `bandHeight` wrong, and it prices both as one row (`match-band.js`); the number it
   publishes is where the keypad handle docks, and too small means the handle lands ON
   the band. `0.375rem`/`0.75rem` are `.btn`'s own paddings, so the two agree at 38px by
   construction rather than by coincidence. */
.match-drawer-row .alert {
    --bs-alert-padding-y: 0.375rem;
    --bs-alert-padding-x: 0.75rem;
}

/* Everything from the two hosts down to the individual toggle buttons must be
   allowed below its content width. `min-width: auto` (the flex default) is what
   makes a nowrap label unshrinkable, and an unshrinkable label in a fixed
   full-width band widens the whole page — see `.match-drawer`'s `overflow-x`. */
.match-drawer-row > *,
.match-drawer-row .btn-group,
.match-drawer-row .btn {
    min-width: 0;
}

/* The part that gives way. */
.match-drawer-label {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Keypad-open compact mode: the identity folds into the keypad handle's title
   (which names the same archer and entry, and tints coral on the opponent's
   side), and the drawer's buttons shrink — so at 390×844 the entry row stays on
   screen with both fixed bands up.

   The whole status line goes, announcement included: the keypad covers the page
   and every pixel of band is a pixel of target. The announcement is held state,
   not a moment, so it is still there when the keypad closes. */
body.keypad-open .match-drawer-line {
    display: none;
}

body.keypad-open .match-drawer {
    padding-top: 0.25rem;
    padding-bottom: calc(0.25rem + env(safe-area-inset-bottom, 0px));
}

body.keypad-open .match-drawer .btn {
    --bs-btn-padding-y: 0.15rem;
    --bs-btn-font-size: 0.8rem;
}

/* The status form of the phase action shrinks with the buttons, or the row it shares
   with them stops being one height — the compaction is what lets an occupied row sit
   INSIDE the pinned 2.25rem here, which is the 36px `bandHeight` charges for it. */
body.keypad-open .match-drawer .alert {
    --bs-alert-padding-y: 0.15rem;

    font-size: 0.8rem;
}

/* The keypad handle carries the drawer's identity while the drawer is compact,
   and turns coral when the arrows being typed are the opponent's. Without it the
   handle reads "Keypad" over an editor that could belong to either archer. */
.keypad-handle-opponent {
    background-color: #e8553d !important;
}

/* ── Compact keypad drawer ───────────────────────────────────────────────────

   The other half of the trade above. `body.keypad-open` slims the *bands* so the
   entry row survives an open keypad; this slims the *keypad*, so that page is
   still readable underneath it. Measured at 390×844 with the real markup
   (`var/mockups/scoring-complete-footer/bottom-stack.html`), the open keypad plus
   a match drawer plus a tournament footer took 568px of an 844px screen — 67% of
   the phone was chrome, and the archer had 276px of page left. On a plain
   scoring the open keypad alone took 306px.

   **ONE size, unconditionally.** This shipped gated on another band being up
   (`body:has(.match-drawer…, .tournament-footer, .scoring-complete-footer)`), on
   the theory that the size the keypad ships at is a baseline rather than a
   problem. The owner looked at an ordinary scoring page and asked for it tighter
   there too, which settles it — and the gate was never defensible on its own
   terms: every size below is justified by what the control *is*, and a score key
   does not become more dangerous to mis-tap because a tournament footer is on
   screen. Collapsing the two tiers into one also deletes the only way the two
   compaction mechanisms could have disagreed about the same pixels.

   **The CLOSED drawer's 56px handle stays untouched**, which is why the handle
   rule below is gated on `body.keypad-open`. That number is a constant in two
   other places (the drawer's inline `translateY(calc(100% - 56px))` and
   `COLLAPSED_DRAWER_HEIGHT` in `arrow_keypad_controller`), and a handle that no
   longer matches the reveal either hides part of itself or leaves the drawer's
   body peeking over whatever band is beneath it — which swallows every tap meant
   for that band. So the savings all come out of the OPEN drawer.

   **What the sizes are anchored on.** Not a guessed touch-target minimum: the
   floor is the smallest control the app already ships and archers already tap
   daily, measured in the same harness at 390px —

     scorecard arrow badge / empty slot (tappable)   24px
     match drawer button under `keypad-open`         26px
     `btn-sm` throughout the app, footer action      31px
     score-grid end-total input                      34px

   Nothing here goes below 34px on a **score key**, the one control in the app
   where a mis-tap is corrupted data rather than an annoyance — so the keys land
   at 36px, still above every proven control above, and that is the floor for
   them however much more compact the drawer is asked to get. The secondary
   controls (undo/redo, remove-last) sit at the score-grid input's 34px, and the
   open drawer's handle at 28px — the one element here anchored on legibility
   rather than on the tap target, for the reason spelled out at its own rule
   below. `!important` throughout because these override Bootstrap spacing
   utilities, which are themselves `!important`.

   Scoped to `#keypadDrawer`: the desktop layout keeps the same keypad markup in
   a sticky side panel that is part of the page, not of the bottom stack. */
#keypadDrawer .keypad-body {
    padding: 0.5rem 0.75rem !important;
}

/* The score keys: 42px → 36px. Spelled as `padding-*` rather than through
   `--bs-btn-padding-y`, which the keys' own `py-2` utility (`!important`) would
   win against — unlike `.match-drawer .btn`, which carries no spacing utility. */
#keypadDrawer .keypad-body .btn {
    padding-top: 0.3rem !important;
    padding-bottom: 0.3rem !important;
}

/* The secondary rows — undo/redo, remove-last, the dynamic-structure buttons:
   42px → 34px, and half the gap above each row. */
#keypadDrawer .keypad-secondary-row {
    margin-top: 0.25rem !important;
}

#keypadDrawer .keypad-secondary-row .btn {
    padding-top: 0.25rem !important;
    padding-bottom: 0.25rem !important;
}

/* The open drawer's handle: 56px → 28px, giving up most of the grab area a
   *stowed* handle needs but not all of it. Zeroing the padding outright (24px,
   the bare line box) was the first cut and it was too much — the handle is the
   only thing in this block that carries TEXT, and on a match member that text is
   the folded identity naming whose arrows these are and which entry ("Enter
   arrows — You · Set 2", moved here from the match drawer's status line by
   `body.keypad-open`). Text set flush against a coloured strip's edges reads as
   cramped in a way a bare key never does, and the identity is the one line an
   archer must not misread.

   28px is the text's own 24px line box plus 2px each side: a shade under the
   31px the app already uses for a text-carrying control in fixed bottom chrome
   (`btn-sm`, the tournament footer's action button) and comfortably above the
   26px match-drawer button beside it. `0.125rem` is not arithmetic — it is the
   value that MEASURED 28.00px exactly in the harness, with no sub-pixel
   rounding; the neighbouring candidates land on 27.19 and 28.78.

   The keypad body's savings are untouched by this — the handle was collateral of
   that cut, not the point of it.

   Gated on `body.keypad-open`: the CLOSED handle stays at 56px, which is
   load-bearing (see the block header). */
body.keypad-open #keypadDrawer [data-arrow-keypad-target="drawerHandle"] {
    padding-top: 0.125rem !important;
    padding-bottom: 0.125rem !important;
}

/* Read-only NAC opponent target behind the side toggle: a plain SVG view,
   never an editor — pointer events are off so clicks add nothing. The "Both"
   overlay (recorder's plots + coral opponent plots on one face) shares the
   same centred, pointer-inert presentation. */
.match-side-opponent-view .match-opponent-target,
.match-side-both-view .match-opponent-target {
    max-width: 420px;
    margin: 0 auto;
}

.match-opponent-target svg {
    display: block;
    width: 100%;
    height: auto;
    pointer-events: none;
}

/* End-result bottom sheet — same slide-up pattern as the keypad drawer, but
   above it (the drawer handle sits at z-index 1030). */
.match-end-sheet {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 1040;
    transform: translateY(110%);
    transition: transform 0.3s ease;
}

.match-end-sheet.open {
    transform: translateY(0);
}

/* Modal backdrop for the reveal sheet: the confirm that starts the reveal has
   already advanced the recorder's side, so the next entry sits open behind the
   sheet — without the backdrop a tap meant for the sheet (or an eager plot
   during the animation) lands straight in it as an arrow. Sits just under the
   sheet and just over the keypad drawer. */
.match-end-sheet-backdrop {
    position: fixed;
    inset: 0;
    z-index: 1039;
    background-color: rgba(0, 0, 0, 0.4);
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.3s ease, visibility 0s linear 0.3s;
}

.match-end-sheet-backdrop.open {
    opacity: 1;
    visibility: visible;
    transition: opacity 0.3s ease, visibility 0s;
}

/* ==================== Tournaments ==================== */
/* Placement ceremony + qualification-ranking window (design doc D9, slice 4).
   The big frozen score, the ranked-list rows with the user highlighted / the
   replaced archer struck through, and the slide-in reveal driven by
   animation-sequencer. Bracket + standings styles follow (slice 5). */

/* The create form's competition picker can hold >100 datasets; cap it to ~5
   rows so the filters and the Start button stay reachable without a long scroll. */
.tournament-dataset-list {
    max-height: 21rem;
    overflow-y: auto;
}

.ceremony-score {
    font-size: 3rem;
    font-weight: 800;
    color: var(--bs-primary);
    line-height: 1.1;
}

.ranking-row {
    transition: background-color 0.6s, opacity 0.6s, transform 0.6s;
}

.ranking-row.user-row {
    background: var(--bs-primary-bg-subtle);
    font-weight: 600;
    border-left: 4px solid var(--bs-primary);
}

.ranking-row.replaced {
    opacity: 0.35;
    text-decoration: line-through;
}

.you-name {
    color: var(--bs-primary);
    font-weight: 700;
}

@keyframes tournament-slide-in {
    from { transform: translateX(-30px); opacity: 0; }
    to { transform: none; opacity: 1; }
}

.slide-in {
    animation: tournament-slide-in 0.5s ease-out;
}

/* Scorings-list nested containers (design doc D9, slice 4 Half B).
   `var/mockups/tournament-list-accent/` is the spec.

   A tournament carries NO accent colour. Any hue reads as an outcome — archers
   took a red container for a tournament they lost and a green one for one they
   won — while the hue itself never encoded anything: the *status badge*
   (`tournamentStatus`) is what carries meaning, and the name is always spelled out
   beside the container. So the nesting is carried by structure instead: the member
   rows are a genuinely inset block under the container's header. */

/* `.challenge-tag` shares it verbatim: a game's tag answers the same question on
   the same card — "this row belongs to something" — so it takes the same shape and
   the same neutral grey. A hue of its own would read as an outcome, exactly as a
   container's would (see above), and there is nothing to encode. */
.tournament-tag,
.challenge-tag {
    display: inline-block;
    font-size: 0.72em;
    font-weight: 600;
    white-space: nowrap;
    padding: 0.1em 0.45em;
    border-radius: 0.375rem;
    color: var(--bs-secondary-color);
    background: var(--bs-secondary-bg);
    border: 1px solid var(--bs-border-color);
}

/* The inset IS the grouping. Without it the rows read as siblings of the next
   flush card: the container is `p-0` with its header at `px-3`, so member rows at
   `0.75rem` would sit very slightly OUTSIDE the header's text column. */
.tournament-member-rows {
    border-left: 3px solid var(--bs-border-color) !important;
    margin-left: 0.5rem;
    border-bottom-left-radius: 0.375rem;
    overflow: hidden;
}

.tournament-member-rows .list-group-item {
    padding: 0.4rem 0.75rem 0.4rem 1rem;
    font-size: 0.88rem;
}

/* The right inset is what makes the block's far edge visible — without it the
   member rows run to the container's edge and their badges line up with the outer
   cards'. On a phone it is the opponent-name column that pays for it (~30px at
   414px, enough to truncate a name that otherwise fits), and there the left rule
   plus inset already carries the nesting, so it waits for room to spare. */
@media (min-width: 576px) {
    .tournament-member-rows {
        margin-right: 0.75rem;
    }
}

.tournament-member-date {
    min-width: 3.9rem;
    font-size: 0.75rem;
}

.tournament-member-role {
    min-width: 5.6rem;
    font-size: 0.75rem;
    font-weight: 600;
    color: var(--bs-secondary-color);
}

/* Bracket (design doc D9, slice 5). Round columns in a mobile-first horizontal
   scroll; the user's match highlighted, resolved matches tappable for a
   set-by-set sheet, not-yet-reachable pairings dashed "winner of…" silhouettes.
   Theme-aware equivalents of the mockup's hardcoded light colors. */

.bracket-scroll {
    /* The chart scrolls in BOTH axes inside its own pane rather than dragging the
       page down with it: a 128-draw is 64 rows tall, so the round headers would
       otherwise be thousands of pixels above whatever the archer is reading. A
       pane is also the only way to pin them — `position: sticky` resolves against
       the nearest scrollport, and a horizontal-only scroller is already one, so a
       header sticking to the PAGE is not expressible. `max-height` only caps: a
       4-bracket still takes its natural height. */
    overflow: auto;
    max-height: 72svh;
    -webkit-overflow-scrolling: touch;
    padding-bottom: 0.5rem;
}

.bracket {
    /* The chart's two geometry constants. One row is one cell of the widest
       round, so a round-r cell spans 2^(r-1) of them and every match sits
       mid-way between the two that feed it; the gutter is the flex gap between
       columns, which the connector elbows draw across (half stub, half elbow). */
    /* Tall enough for a cell PLUS the set-by-set grid hanging under it, and then
       some: the row's spare height falls BETWEEN cells, while a grid stays 3px
       under the match it belongs to. That asymmetry is the only thing saying
       whose ends those are — at 6rem the two gaps were 3px and 5px, and the grid
       read as floating between two matches. */
    --bracket-row: 7.2rem;
    --bracket-gutter: 1.75rem;
    /* The breathing space under the header's rule. The header paints it (see its
       `::after`) and the cells are pushed down by the same amount, so nothing
       starts underneath it at rest. */
    --bracket-header-skirt: 1.2rem;
    /* The connectors read as structure rather than as more cell borders: at one
       pixel across a 28px gutter, `--bs-border-color` is invisible on a 64-cell
       column, so they take the (theme-aware) muted TEXT colour instead. */
    --bracket-line: var(--bs-secondary-color);
    display: flex;
    gap: var(--bracket-gutter);
    min-width: max-content;
    align-items: start;
}

.bracket-round {
    display: flex;
    flex-direction: column;
    /* No gap under the header: a transparent strip between it and the cells is a
       slot the scrolling chart shows through. The header pads instead. */
    gap: 0;
    /* Capped, so the scoreline stays readable without a long name pushing every
       column of a 7-round chart wider than the screen: the name ellipsises, the
       set totals never do. */
    min-width: 190px;
    max-width: 17rem;
}

.bracket-round h6 {
    position: sticky;
    top: 0;
    z-index: 3;
    text-align: center;
    /* Opaque, and it owns the strip the cells scroll under — INCLUDING the
       gutters. A connector elbow is half its cell's span tall (24 rows at the
       quarterfinal), so a cell whose parent sits far below sends its line way up
       past the top of the pane, and the gutter was the one strip no header
       covered: the line surfaced between two round titles and made the band read
       as if it scrolled with the chart. Each header spills half a gutter either
       side — the margin cancels the padding, so the column's width is unchanged —
       and neighbouring backgrounds meet into one continuous band. */
    background: var(--bs-body-bg);
    border-bottom: 1px solid var(--bs-border-color);
    padding-block: 0.35rem 0.5rem;
    padding-inline: calc(var(--bracket-gutter) / 2);
    margin-inline: calc(var(--bracket-gutter) / -2);
}

/* The breathing space under the rule has to BELONG to the header: the cells
   scroll beneath it, so a plain margin would be a transparent gap the chart
   shows through. `+ 1px` clears the border rather than painting over it. */
.bracket-round h6::after {
    content: '';
    position: absolute;
    inset-inline: 0;
    top: calc(100% + 1px);
    height: var(--bracket-header-skirt);
    background: var(--bs-body-bg);
}

.bracket-round-cells {
    display: grid;
    /* Clear of the header's skirt: the cells scroll UNDER it, but the first row
       must not begin there — at rest the chart would open on a half-hidden cell.
       Every column is pushed by the same amount, so the chart geometry is
       untouched. */
    padding-top: var(--bracket-header-skirt);
    /* `minmax(0, 1fr)`, not the implicit `auto`: an auto column sizes to
       max-content, and every cell is `nowrap`, so the column would grow past the
       round's max-width instead of letting a long name ellipsise. */
    grid-template-columns: minmax(0, 1fr);
    grid-auto-rows: var(--bracket-row);
}

.bracket-match {
    /* Placed on the chart's own rows (`layoutChart`) and centred in them, so the
       column needs no filler for the slots a short draw left empty. The CELL is
       the box plus the set grid under it; only the box is drawn. */
    grid-row: var(--bracket-cell-row, auto) / span var(--bracket-cell-span, 1);
    align-self: center;
    font-size: 0.8rem;
}

.bracket-match-box {
    position: relative;
    border: 1px solid var(--bs-border-color);
    border-radius: 0.375rem;
    background: var(--bs-body-bg);
}

/* The set-by-set grid, hanging under the cell like an ianseo chart's: one column
   per set, the top row the top side. Its slot is reserved even when there is
   nothing to show, so every box sits at the same height in its chart row and the
   connectors keep landing on the middle of the box they point at. */

.bracket-sets {
    display: flex;
    justify-content: flex-end;
    min-height: 2.3rem;
    padding: 0.2rem 0.4rem 0 0;
}

.bracket-sets-grid {
    display: grid;
    grid-template-columns: repeat(var(--bracket-sets, 1), minmax(1.5rem, auto));
    font-size: 0.65rem;
    font-variant-numeric: tabular-nums;
    line-height: 1.4;
}

/* Each cell rules itself and overlaps its neighbour by that rule, so a single
   column can drop the sides it doesn't want (the shoot-off, below) — which a
   grid ruled by its own `gap` background could not. */
.bracket-set {
    padding: 0 0.25rem;
    margin: -1px 0 0 -1px;
    border: 1px solid var(--bs-border-color);
    text-align: center;
    background: var(--bs-body-bg);
    color: var(--bs-secondary-color);
}

/* A shoot-off is not a set: its column hangs off the end of the table, closed
   only by the rule that separates it from the last one. Transparent rather than
   removed, so the cells keep the exact size the sets have.

   Its two arrows read as a pair, so they align on their digits — right-aligned,
   with the `+` of a centre-decided win hanging outside that alignment. `9` then
   sits under the `0` of `10`, and `10` under the `10` of `10+`. */
.bracket-set.shoot-off {
    position: relative;
    border-top-color: transparent;
    border-right-color: transparent;
    border-bottom-color: transparent;
    padding-right: 0.7rem;
    text-align: right;
}

.shoot-off-plus {
    position: absolute;
    right: 0.15rem;
}

/* The two side rows carry the rounding themselves: the cell cannot clip them with
   `overflow: hidden`, because the connectors below are pseudo-elements that must
   reach OUT of it into the gutter. */

.bracket-match-box .side:first-child {
    border-radius: 0.3rem 0.3rem 0 0;
}

.bracket-match-box .side:last-child {
    border-radius: 0 0 0.3rem 0.3rem;
}

/* The connectors. A cell draws its own line into the round on its right: a
   horizontal stub to mid-gutter, then an elbow down (or up) to the parent's
   mid-height and right to its edge. The two children of one match therefore meet
   on the same vertical, which is what makes the tree readable at a glance. */

.bracket-match.link-down .bracket-match-box::after,
.bracket-match.link-up .bracket-match-box::after {
    content: '';
    position: absolute;
    top: 50%;
    left: 100%;
    width: calc(var(--bracket-gutter) / 2);
    border-top: 1px solid var(--bracket-line);
}

.bracket-match.link-down .bracket-match-box::before,
.bracket-match.link-up .bracket-match-box::before {
    content: '';
    position: absolute;
    left: calc(100% + var(--bracket-gutter) / 2);
    width: calc(var(--bracket-gutter) / 2);
    /* Half this cell's span: exactly the distance to the parent's mid-height. */
    height: calc(var(--bracket-row) * var(--bracket-cell-span, 1) / 2);
    border-left: 1px solid var(--bracket-line);
}

.bracket-match.link-down .bracket-match-box::before {
    top: 50%;
    border-bottom: 1px solid var(--bracket-line);
}

/* The lower feeder draws its vertical ONLY: the horizontal into the parent is
   the upper feeder's, and both used to draw it at the same height — two 1px
   lines stacked, which reads as one thick, slightly doubled line. Every parent
   has exactly one upper feeder (a node's better-seeded side, or a bye, always
   takes the upper half of the slot), so the segment is never lost. */
.bracket-match.link-up .bracket-match-box::before {
    bottom: 50%;
}

/* One pixel longer on the lower feeder, and only there. Its vertical is anchored
   at `bottom: 50%`, so it occupies the rows ABOVE the stub's own row and stops
   just short of it, while the upper feeder's starts AT that row and covers the
   turn. Without this the corner pixel belongs to neither and the line reads as
   notched — on the bottom half of every joint, never the top. */
.bracket-match.link-up .bracket-match-box::after {
    width: calc(var(--bracket-gutter) / 2 + 1px);
}

.bracket-match.user-match.link-down .bracket-match-box::before,
.bracket-match.user-match.link-up .bracket-match-box::before,
.bracket-match.user-match.link-down .bracket-match-box::after,
.bracket-match.user-match.link-up .bracket-match-box::after {
    border-color: var(--bs-primary);
}

.bracket-match-box .side {
    display: flex;
    align-items: baseline;
    padding: 0.2rem 0.5rem;
    gap: 0.5rem;
    white-space: nowrap;
}

.bracket-match-box .side-name {
    margin-right: auto;
    min-width: 0;
    /* `clip`, not `hidden`: `hidden` leaves a scrollport, so a caret placed in a
       truncated name can scroll it sideways. `clip` is not scrollable at all. */
    overflow: clip;
    text-overflow: ellipsis;
}

.bracket-match-box .side-score {
    /* Its own column, ruled off the name — the chart reads as a score sheet. */
    border-left: 1px solid var(--bs-border-color);
    padding-left: 0.4rem;
    /* A width, not a `min-width`, and it never flexes: a set-point match scores
       one digit and a compound one a cumulative three, so a column free to size
       itself is a different width on every row — and the country tag sits
       immediately to its left, so it inherits all of it. Sized for the widest
       thing it holds, a winner's bold three digits, which a `min-width` also let
       shrink back under and clip.

       In rem, not `ch`: `ch` is measured on the element's OWN font, so a winner's
       bold digit makes its column a pixel wider than a loser's, and everything to
       its left moves with it. */
    flex: none;
    width: 2.25rem;
    text-align: center;
    font-variant-numeric: tabular-nums;
}

.bracket-match-box .side.winner {
    font-weight: 700;
    background: var(--bs-success-bg-subtle);
}

.bracket-match-box .side.loser {
    color: var(--bs-secondary-color);
}

/* A country is two boxes side by side, never a run of text: the flag's box is
   what the acronym is measured from, so it has to exist even on the rows that
   carry no flag (`AIN`, `TPE`, a canton) — those keep the same column as the
   rest. In the bracket the first box is given a width below; everywhere else it
   collapses, since a country there trails a name and is no column at all. */
.country-tag {
    display: inline-grid;
    grid-template-columns: auto auto;
    column-gap: 0.3rem;
    /* One baseline for all three sizes — the name, the flag and the acronym. That
       is what puts the acronym's cap band on the name's, measured 0.12px apart in
       both engines, where centring the two boxes instead lands it 1.12px high
       (`var/tmp/madrid-chart/cap-band.mjs`). The pair reading level is a matter of
       the sizes below, not of the alignment mode. */
    align-items: baseline;
}

/* Sized between the acronym it labels and the name above it. Much smaller and a
   flag is an unreadable smudge; much larger and it towers over the acronym's caps,
   so the two stop reading as one label and the acronym reads as having sagged —
   which is a size problem, not the alignment problem it looks like. */
.flag-emoji {
    font-size: 0.85rem;
    line-height: 1;
}

.bracket-match-box .side .country-tag {
    /* Small enough to stay subordinate to the name, large enough that its caps are
       not dwarfed by it: at 0.7rem they measured 61% of the name's cap height,
       which is what made the acronym look sunken however it was aligned. */
    font-size: 0.8rem;
    color: var(--bs-secondary-color);
    /* Never bold, even on the winner's row: the country is a column, and bold
       and regular acronyms are different widths, so the winners' would sit a few
       pixels off every loser's. */
    font-weight: 400;
}

.bracket-match-box .side .flag-code {
    /* A fixed advance, which is what makes the country a column at all — the
       letter equivalent of the score's `tabular-nums`. The tag is right-aligned
       against the score, so in a proportional face a three-letter code is up to
       9px wider or narrower than its neighbours (`III` against `MMM`), and
       everything to ITS left — every flag — inherits that drift. */
    font-family: var(--bs-font-monospace);
}

.bracket-match-box .side .flag-emoji {
    /* A box of its own, with the glyph pinned to its right edge. Emoji flags do
       share an advance in the fonts we can measure, but not in every font a
       reader may have, and it is the right edge that has to hold: it is the one
       butting against the acronym. A wider glyph then spills to the LEFT, into
       the gap before the name, where nothing is aligned to it. The width is in
       `em` of this element's own size, so it tracks the rule above. */
    display: inline-block;
    width: 1.35em;
    text-align: right;
}

.bracket-match.user-match .bracket-match-box {
    /* Emphasis that costs no geometry. A 2px border would narrow the padding box
       by 2px each side, and the connectors are anchored to it (`left: 100%`), so
       the archer's own elbow would start a pixel before its sibling's and the two
       would meet askew. The ring is drawn outside instead. */
    border-color: var(--bs-primary);
    box-shadow: 0 0 0 1px var(--bs-primary), 0 0 0 4px rgba(var(--bs-primary-rgb), 0.15);
}

.bracket-match.pending-match .bracket-match-box {
    border-style: dashed;
    color: var(--bs-secondary-color);
}

/* ── The persistent tournament footer ────────────────────────────────────────

   A slim band on every scoring page that belongs to a tournament: which
   tournament and at what step on the left, the next action on the right
   (`tournament_footer_controller.js`). Neutral like every other tournament
   surface — see the `.tournament-tag` block on why no hue.

   It is the BOTTOM-MOST fixed element on the page and `.match-drawer` stacks ON
   TOP of it, at `bottom: var(--tournament-footer-height)`:

     keypad handle       bottom: var(--bottom-bands-height)      z 1030
     match drawer        bottom: var(--tournament-footer-height) z 1031
     tournament footer   bottom: 0                               z 1032

   Bottom-most in GEOMETRY, highest of the three in z-order — and the z-order is
   not a free choice. It is the same trap the match drawer documents above: the
   keypad drawer is `translateY(calc(100% - 56px))`, so only its 56px handle is on
   screen while its BOX still extends the full drawer height downwards past the
   viewport bottom, and hit-testing counts the clipped part. Below the keypad
   (z 1029, which is what this band shipped with for one E2E run), the keypad's
   invisible overhang covered the footer's entire row and every tap on it — the
   left half's link to the tournament included — landed on a keypad button
   instead. Playwright reports that as a click timeout on an element it agrees is
   "visible, enabled and stable", blaming `<div class="col-3">` for intercepting
   pointer events. Anything that tiles against the bottom of the viewport has to
   sit above the keypad; only the sheets (1039+) go over it.

   The geometric order is deliberate and it costs something. Keeping the drawer bottom-most
   would leave its confirm-the-set button where the thumb already expects it, but
   it puts the tournament band at a different height on every page — mounted over
   a drawer here, flat against the viewport there — and a landmark that moves is
   not a landmark. So the band holds one constant position and the drawer's
   buttons rise by its height.

   `--tournament-footer-height` is published on `:root` by the footer controller
   and defaults to `0px`, so every consumer read is inert when the band is absent
   — the same contract as `--match-drawer-height`. */
:root {
    --tournament-footer-height: 0px;
}

.tournament-footer {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 1032;
    background-color: var(--bs-body-bg);
    border-top: 1px solid var(--bs-border-color);
    box-shadow: 0 -0.25rem 0.75rem rgba(0, 0, 0, 0.1);
    padding-top: 0.4rem;
    /* Bottom-most band, so this one owns the safe-area inset; `.match-drawer`
       gives its own up whenever a footer is present, or the two double it. */
    padding-bottom: calc(0.4rem + env(safe-area-inset-bottom, 0px));
    /* Content in here can never widen the page — the identical guarantee, for the
       identical reason, as `.match-drawer` above: a fixed band spanning
       `left: 0; right: 0` whose child refuses to shrink grows the initial
       containing block, the layout viewport detaches from the visual one, and
       every tap aimed at the band lands ~100px off. The nowrap/ellipsis rules
       below make the content genuinely fit; this stops anything added later from
       reintroducing the failure. `clip`, not `hidden`, which would compute
       `overflow-y` to `auto` and make the band a scroll container. */
    overflow-x: clip;
}

/* The drawer already rides on the band (its `bottom` is this band's height); what
   it also gives up whenever a footer is present is the safe-area inset, which the
   bottom-most band owns, or the two double it. */
body:has(.tournament-footer) .match-drawer {
    padding-bottom: 0.4rem;
}

body.keypad-open:has(.tournament-footer) .match-drawer {
    padding-bottom: 0.25rem;
}

.tournament-footer-inner {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    min-width: 0;
}

.tournament-footer-where {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    min-width: 0;
    flex: 1 1 auto;
    text-decoration: none;
    color: var(--bs-body-color);
}

.tournament-footer-where:hover {
    color: var(--bs-body-color);
}

.tournament-footer-where i.bi-trophy {
    color: var(--bs-secondary-color);
}

/* The two-line block between the trophy and the button. `min-width: 0` is what
   lets the ellipsis rules below actually take effect — a flex item defaults to
   `min-width: auto` and would refuse to shrink below its text. */
.tournament-footer-where-text {
    min-width: 0;
}

.tournament-footer-name {
    /* Never wraps: a two-line tournament name would change the band's height
       between renders and move the action button under the archer's thumb
       mid-reach. */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    font-weight: 600;
    font-size: 0.875rem;
    line-height: 1.2;
}

.tournament-footer-step {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    font-size: 0.75rem;
    line-height: 1.2;
    color: var(--bs-secondary-color);
}

.tournament-footer-action {
    flex: 0 0 auto;
}

/* The "why there is no action yet" line, in the button's place. Capped and
   clipped to two lines so the longest locale cannot grow the band. */
.tournament-footer-reason {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    overflow: hidden;
    max-width: 9rem;
    font-size: 0.75rem;
    line-height: 1.15;
    color: var(--bs-secondary-color);
}

/* ── The scoring-complete band ───────────────────────────────────────────────

   "You are done — here is your score", raised the moment a fixed scoring's last
   declared arrow lands (`scoring_complete_footer_controller.js`). Same shell as
   the tournament footer above, deliberately: an archer who has met one fixed
   band at the bottom of a scoring page should recognise the next one.

   It is the THIRD claimant on the bottom of the viewport, and the only one that
   yields rather than stacks. The precedence, decided in
   `assets/lib/scoring-complete-footer.js`:

     tournament footer  >  match drawer  >  scoring-complete band

   A match member already has the drawer (which carries the match's own
   end-of-match action) and a tournament member already has the footer; both say
   "this is over" in a vocabulary that fits the event, and two bands are already
   the most a phone screen takes. So this band never shares the bottom edge with
   either — its `bottom` docks on nothing and its z-index is free of them.

   z-index 1033: above the keypad drawer for the reason the two blocks above
   spell out at length — the keypad is `translateY(calc(100% - 56px))`, so only
   its handle is on screen while its BOX still extends its full height past the
   viewport bottom, and hit-testing counts the clipped part. Anything tiling
   against the bottom of the viewport has to clear that overhang or every tap on
   it lands on a keypad button. Below every sheet (1039+).

   `--scoring-complete-footer-height` is published on `:root` by the controller
   and defaults to `0px`, so every consumer read is inert when the band is absent
   — the same contract as the other two, and it joins their sum
   (`bottomBandsHeight()`) so no consumer has to know which band it is looking
   at. Deliberately NOT subtracted from the plotting SVG's viewport budget: the
   band only exists once there is nothing left to plot, and shrinking the target
   at that moment would resize the plot the archer is looking at for no gain.
   That exclusion is now stated where every other band's is — as
   `shrinksThePlottingTarget: false` on this band's stack entry — and the budget
   reads `--plot-target-bands-height`, the derived total that answers it. */
:root {
    --scoring-complete-footer-height: 0px;
}

.scoring-complete-footer {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 1033;
    background-color: var(--bs-body-bg);
    border-top: 1px solid var(--bs-border-color);
    box-shadow: 0 -0.25rem 0.75rem rgba(0, 0, 0, 0.1);
    padding-top: 0.4rem;
    /* Never stacked on another band, so it always owns the safe-area inset. */
    padding-bottom: calc(0.4rem + env(safe-area-inset-bottom, 0px));
    /* Content in here can never widen the page — the identical guarantee, for
       the identical reason, as `.match-drawer` and `.tournament-footer`: a fixed
       band spanning `left: 0; right: 0` whose child refuses to shrink grows the
       initial containing block, the layout viewport detaches from the visual
       one, and every tap aimed at the band lands ~100px off. `clip`, not
       `hidden`, which would compute `overflow-y` to `auto` and make the band a
       scroll container. */
    overflow-x: clip;
}

.scoring-complete-footer-inner {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    min-width: 0;
}

/* `min-width: 0` is what lets the ellipsis rules below take effect — a flex item
   defaults to `min-width: auto` and refuses to shrink below its text. The text
   block is the shrinkable child; the action buttons are not. */
.scoring-complete-footer-text {
    min-width: 0;
    flex: 1 1 auto;
}

.scoring-complete-footer-label {
    /* Never wraps: a two-line label would change the band's height between
       renders and move the buttons under the archer's thumb mid-reach. */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    font-size: 0.75rem;
    line-height: 1.2;
    color: var(--bs-secondary-color);
}

.scoring-complete-footer-label > .bi {
    color: var(--bs-success);
}

.scoring-complete-footer-score {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    font-size: 1rem;
    font-weight: 600;
    line-height: 1.2;
    color: var(--bs-primary);
}

.scoring-complete-footer-max {
    font-size: 0.75rem;
    font-weight: 400;
    color: var(--bs-secondary-color);
}

.scoring-complete-footer-actions {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

/* ── The challenge band ──────────────────────────────────────────────────────

   A challenge's live surface is a BAND, not a mode: the game names itself and the
   end, says what the last end was worth, and offers the one action. Even the
   dramatic moments (a redemption arrow, an elimination) are this band saying a
   different thing — the archer keeps the target, the scorecard and the arrows
   they just plotted on screen.

   It occupies the MATCH DRAWER'S SLOT, and everything about its geometry says so:
   the same anchor (`bottom: var(--tournament-footer-height)` — it rides on the
   tournament footer, the one band below it that it shares the edge with), the
   same z-index, the same shell. `yieldsTo: [MATCH_DRAWER_BAND]` in
   `BOTTOM_BAND_STACK` is the formal statement of it, and the scoring-complete
   band yields to this one in turn for the reason it yields to the drawer: this
   band carries the shape's own end-of-game vocabulary, which outranks a generic
   "your score is 288".

   ⚠ **A yield is not enforced by the stack** — `bandMayOccupyBottomEdge()` is a
   pure question and only the yielding band's own JS asks it. The controllers that
   raise this band and the completion band must each ask, or CSS paints two bands
   at the same anchor while `--bottom-bands-height` reserves for both.

   z-index 1031, the match drawer's own: the two can never coexist (one yields to
   the other), so sharing a value costs nothing, and it keeps the band in the
   drawer's place in the stacking order rather than creeping the range towards the
   sheets at 1039+. What is NOT free is being above the keypad drawer's 1030 —
   the keypad is `translateY(calc(100% - 56px))`, so only its handle is on screen
   while its box still extends its full height past the viewport bottom, and
   hit-testing counts the clipped part. Below it, every tap on this band's left
   half lands on a keypad button; the tournament footer shipped at 1029 for one
   E2E run and proved it.

   `--challenge-drawer-height` is the band's MEASURED height (`offsetHeight` plus a
   `ResizeObserver`, published `0px` on disconnect), not an arithmetic twin of the
   markup the way the match drawer's is — the drawer computes its height because
   `body.keypad-open` resizes it with no render pass and a poll-driven re-render
   would overwrite the observer's correction, and neither applies here. The
   property says `drawer` while the class says `band` deliberately: the roster
   check keys off `--*-footer-height` / `--*-drawer-height`, so a
   `--challenge-band-height` would be a band the scan cannot see. */
:root {
    --challenge-drawer-height: 0px;
}

.challenge-band {
    position: fixed;
    left: 0;
    right: 0;
    bottom: var(--tournament-footer-height);
    z-index: 1031;
    background-color: var(--bs-body-bg);
    border-top: 1px solid var(--bs-border-color);
    border-top-left-radius: var(--bs-border-radius-lg);
    border-top-right-radius: var(--bs-border-radius-lg);
    box-shadow: 0 -0.25rem 0.75rem rgba(0, 0, 0, 0.12);
    padding-top: 0.4rem;
    padding-bottom: calc(0.4rem + env(safe-area-inset-bottom, 0px));
    /* Content in here can never widen the page — the identical guarantee, for the
       identical reason, as `.match-drawer` and the two footers: a fixed band
       spanning `left: 0; right: 0` whose child refuses to shrink grows the initial
       containing block, the layout viewport detaches from the visual one, and every
       tap aimed at the band lands ~100px off (Playwright reports it as a click
       timeout blamed on an unrelated element). `clip`, not `hidden`, which would
       compute `overflow-y` to `auto` and make the band a scroll container. */
    overflow-x: clip;
}

/* The band's contents are the match drawer's classes verbatim — `.match-drawer-inner`,
   `-line`, `-identity`, `-announcement`, `-row` — because the anatomy is the same one:
   identity on the left, what just landed on the right, then one action row. That
   inheritance is load-bearing rather than cosmetic. It brings the reservation rules
   with it (the row's pinned `min-height`, the identity that goes `invisible` rather
   than away), the shrink-and-ellipsis rules that keep the band from widening the
   page, and `.match-drawer-settled`, which collapses a band to its single action once
   nothing can materialise beside it — exactly a finished game.

   The one thing the inheritance does NOT bring is a stand-in for the folded status
   line: `body.keypad-open .match-drawer-line` hides it here too, and the challenge
   band has no keypad-handle title naming the game the way a match member's does. That
   is the same bargain the drawer struck — every pixel of band is a pixel of target
   while the keypad covers the page — and the band's height is measured, so the fold
   republishes rather than going stale. */
body.keypad-open .challenge-band {
    padding-top: 0.25rem;
    padding-bottom: calc(0.25rem + env(safe-area-inset-bottom, 0px));
}

body.keypad-open .challenge-band .btn {
    --bs-btn-padding-y: 0.15rem;
    --bs-btn-font-size: 0.8rem;
}

/* Whichever band is bottom-most owns `env(safe-area-inset-bottom)`, or the two
   double it — so this band gives its own inset up whenever a tournament footer is
   under it, exactly as the match drawer does from the same slot. */
body:has(.tournament-footer) .challenge-band {
    padding-bottom: 0.4rem;
}

body.keypad-open:has(.tournament-footer) .challenge-band {
    padding-bottom: 0.25rem;
}

/* The action's label is the one text in the band that grows with the content (a
   game's name, an end number, a placing). `.match-drawer-row .btn` already pins it
   to one nowrap line and lets it shrink; this is what lets the shrink actually
   ellipsise rather than overflow, without every caller having to remember
   `.match-drawer-label`. */
.challenge-band .match-drawer-row .btn > span {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Jeopardy — the redemption arrow. Modelled on `.match-drawer-correcting` (which
   turns the whole band amber for "I am reopening a settled entry") but in danger
   tokens, because this is not an unusual mode: it is one arrow between the archer
   and the end of their game. */
.challenge-band.challenge-band-jeopardy {
    background-color: var(--bs-danger-bg-subtle);
    border-top-color: var(--bs-danger-border-subtle);
}

.challenge-band-jeopardy .match-drawer-identity {
    border-left-color: var(--bs-danger-text-emphasis);
    color: var(--bs-danger-text-emphasis);
}

/* The demand — "one arrow to stay in, and it must be a 10" — rides the line the
   ordinary announcement was already on, so it must not be given a size of its own:
   weight and colour only. Anything touching the line box moves a FIXED band, and a
   moving fixed element is not a tappable one, at the one moment the archer is
   reaching for the page. */
.challenge-band-jeopardy .match-drawer-announcement {
    color: var(--bs-danger-text-emphasis);
    font-weight: 600;
}

/* The per-end derived-points strip, on the game's state card rather than in the
   band — the band answers "what now", the card answers "how am I doing".
   CHRONOLOGICAL (1→N), like every other end-ordered surface in the app: read that
   way the pills are the game's own trajectory, which for a game whose whole tension
   is "closing or receding" is the story worth telling. The end being shot carries a
   primary outline, so nothing has to be reordered to be found. It wraps, so it is
   not a fixed-height surface and has none of the band's reservation constraints. */
.challenge-strip {
    display: flex;
    gap: 0.3rem;
    flex-wrap: wrap;
}

.challenge-strip .challenge-pill {
    /* Tabular figures and one width for every pill, so a row of them reads as a
       column of values rather than as ragged text: `+10` must not be wider than
       `+4`. */
    min-width: 2.6em;
    text-align: center;
    font-variant-numeric: tabular-nums;
}

.challenge-strip .challenge-pill-current {
    /* An outline rather than a border: it does not take part in layout, so marking
       the current end cannot shift the pills beside it. */
    outline: 2px solid var(--bs-primary);
    outline-offset: 1px;
}

.challenge-strip-caption {
    font-size: 0.68rem;
    line-height: 1;
}

/* Goal meter — how far through the game's own currency the archer is. Slim: it is
   a second reading of the shoot, never a replacement for the archery total. */
.challenge-goal {
    height: 0.5rem;
}

/* Run meter — a run game's own shape, in place of the goal bar. A run is not a
   quantity filling up, it is a sequence that survives or ends, and a bar cannot show
   an archer that theirs just went back to zero.

   ⚠ **The row IS the target, which is what lets it carry no legend**: it is exactly
   `streak` dots — the arrows the game is asking for — filling as the run advances, and
   an arrow outside the zone empties every one of them back to hollow. The LENGTH never
   moves, so the only thing to read off it is how far along this run is.

   Two states, the second being the absence of the first. What a break cost is a figure
   above (the resets count), not a third colour: marking broken arrows in here grew the
   row as the attempt went badly and asked for two readings of one strip of circles. */
.challenge-run-grid {
    display: grid;
    /* Auto-fill rather than a fixed count: the row is as long as the game asks for, so
       it has to wrap on its own at any target and any width. */
    grid-template-columns: repeat(auto-fill, minmax(1.15rem, 1fr));
    gap: 0.3rem;
}

.challenge-run-dot {
    aspect-ratio: 1;
    border-radius: 50%;
    background: var(--bs-tertiary-bg);
    border: 1px solid var(--bs-border-color);
}

.challenge-run-dot-in {
    background: var(--bs-success);
    border-color: var(--bs-success);
}

.challenge-run-figures {
    display: flex;
    gap: 1.25rem;
    /* One width per digit, so the live run counting up cannot shift the figures
       beside it — this number changes on every arrow. */
    font-variant-numeric: tabular-nums;
}

.challenge-run-figure-value {
    font-size: 1.35rem;
    line-height: 1.1;
    font-weight: 600;
}

.challenge-run-figure-label {
    font-size: 0.68rem;
    line-height: 1;
    color: var(--bs-secondary-color);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

/* Requirement ladder — the goal meter for a game whose bar MOVES, showing every rung
   at once so "which phase am I in" is on screen rather than counted.

   Deliberately the shape the match-create form already teaches for an AI opponent's
   strength (`.difficulty-zones` + `.difficulty-zone-labels`): a segmented bar with a
   label under each segment, each weighted by its width. Here a segment is a rung and
   its width is `endsPerStep`, so a ladder of 1-end rungs reads as equal thirds and one
   of 5-end rungs as three wide blocks. */
.challenge-ladder-track {
    display: flex;
    gap: 2px;
    height: 0.5rem;
}

.challenge-ladder-rung {
    background: var(--bs-tertiary-bg);
    border-radius: 2px;
    overflow: hidden;
}

.challenge-ladder-rung:first-child {
    border-start-start-radius: 4px;
    border-end-start-radius: 4px;
}

.challenge-ladder-rung:last-child {
    border-start-end-radius: 4px;
    border-end-end-radius: 4px;
}

/* How much of THIS rung is already behind the archer. */
.challenge-ladder-fill {
    display: block;
    height: 100%;
    background: var(--bs-primary);
}

.challenge-ladder-rung-done .challenge-ladder-fill {
    background: var(--bs-success);
}

.challenge-ladder-labels {
    display: flex;
    gap: 2px;
    font-size: 0.68rem;
    line-height: 1.3;
}

.challenge-ladder-labels > div {
    text-align: center;
    overflow: hidden;
    white-space: nowrap;
    color: var(--bs-secondary-color);
}

/* The rung in force, in the colour its own fill is drawn in. */
.challenge-ladder-label-current {
    color: var(--bs-primary);
    font-weight: 600;
}

.challenge-ladder-label-done {
    color: var(--bs-success);
}

/* ============================================
   THE CATALOGUE (the challenge create route)
   ============================================ */

/* The games are radios styled as a list, so the chosen one has to look chosen
   through the input beside it rather than through a class the controller writes. */
.btn-check:checked + .challenge-catalogue-item {
    z-index: 2;
    color: var(--bs-list-group-active-color);
    background-color: var(--bs-list-group-active-bg);
    border-color: var(--bs-list-group-active-border-color);
}

.challenge-catalogue-item {
    cursor: pointer;
    /* Picking a game scrolls its row to the top (`scrollChallengeGameIntoView`);
       without this the title lands flush against the viewport edge. */
    scroll-margin-top: 0.75rem;
}

/* Keyboard focus lands on the hidden radio, so the row it labels has to show it —
   otherwise arrow-keying through the list moves an invisible selection. */
.btn-check:focus-visible + .challenge-catalogue-item {
    box-shadow: 0 0 0 0.25rem rgba(37, 99, 235, 0.25);
}

/* Below md the chosen row IS an accordion header: the description opens under it,
   inside the list. Above md the description has its own column and the chevron is
   hidden, so nothing here has to know which layout is on screen. */
.challenge-catalogue-panel {
    background-color: var(--bs-primary-bg-subtle);
    border-left: 3px solid var(--bs-primary);
    animation: challenge-catalogue-panel-in 0.16s ease-out;
}

@keyframes challenge-catalogue-panel-in {
    from {
        opacity: 0;
        transform: translateY(-0.25rem);
    }

    to {
        opacity: 1;
        transform: none;
    }
}

.challenge-catalogue-chevron {
    transition: transform 0.16s ease;
}

.btn-check:checked + .challenge-catalogue-item .challenge-catalogue-chevron {
    transform: rotate(180deg);
}

/* The settings only read as a section of the description's column once they are
   under a heading that is not shaped like the field labels below it. */
.challenge-catalogue-settings-title {
    margin-bottom: 0.5rem;
    color: var(--bs-secondary-color);
    font-size: 0.875rem;
    font-weight: 600;
    text-transform: uppercase;
}

/* A rule between the columns, and only while they ARE columns. */
@media (min-width: 768px) {
    .challenge-catalogue-detail {
        border-left: 1px solid var(--bs-border-color);
    }
}

/* A level's options carry a second line of help text ("À partir d'environ 8,5
   points par flèche"), which four-across is unreadable on a phone. */
@media (max-width: 575.98px) {
    .challenge-catalogue-choice {
        flex-direction: column;
        align-items: stretch;
    }

    .challenge-catalogue-choice > .btn {
        border-radius: var(--bs-border-radius) !important;
    }

    .challenge-catalogue-choice > .btn-check + .btn {
        margin-top: 0.25rem;
    }
}

/* Scoped comments & self-ratings ("Comments" card + shared remarks modal). */

.scoring-remark-line {
    cursor: pointer;
}

.scoring-remark-line:hover {
    color: var(--bs-secondary) !important;
    text-decoration: underline dotted;
}

/* Comment affordance button: muted when empty, primary when filled. */

.scoring-remark-affordance {
    line-height: 1;
}

.scoring-remark-affordance.no-remark {
    color: var(--bs-secondary);
    opacity: 0.45;
}

.scoring-remark-affordance.has-remark {
    color: var(--bs-primary);
}

/* Corner dot marking a remarked arrow in the modal's shot picker. */

.scoring-remark-shot-dot {
    position: absolute;
    top: -3px;
    right: -3px;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--bs-primary);
    border: 1.5px solid var(--bs-body-bg, #fff);
}

/* 1–5 self-rating selector in the remarks modal. */

.scoring-rating-dot {
    font-size: 1.5rem;
    line-height: 1;
    color: var(--bs-secondary);
    opacity: 0.3;
    cursor: pointer;
    user-select: none;
}

.scoring-rating-dot.active {
    color: var(--bs-warning);
    opacity: 1;
}

.scoring-rating-dot.disabled {
    pointer-events: none;
    opacity: 0.15;
}

/* Blue presence dot on the plotting card's quick comment button. */

.scoring-remark-presence-dot {
    position: absolute;
    top: -4px;
    right: -4px;
    width: 11px;
    height: 11px;
    border-radius: 50%;
    background: var(--bs-primary);
    border: 2px solid var(--bs-body-bg, #fff);
}

/* ==================== Gear inventory ==================== */

/* Cover strip on an inventory card: a photo when there is one, otherwise the
   kind's icon on a muted panel so every card keeps the same silhouette. */

.gear-card-cover {
    position: relative;
    aspect-ratio: 16 / 9;
    overflow: hidden;
    border-radius: var(--bs-border-radius) var(--bs-border-radius) 0 0;
    background: var(--bs-secondary-bg);
}

.gear-card-cover img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.gear-card-cover-empty {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 2rem;
    color: var(--bs-secondary-color);
}

.gear-card-photo-count {
    position: absolute;
    right: 0.35rem;
    bottom: 0.35rem;
    padding: 0 0.35rem;
    border-radius: var(--bs-border-radius-sm);
    background: rgb(0 0 0 / 55%);
    color: #fff;
    font-size: 0.7rem;
}

.gear-status-dot {
    font-size: 0.7em;
    line-height: 1;
    vertical-align: middle;
}

/* Truncation inside a flex child only works once it may shrink below content. */

.min-width-0 {
    min-width: 0;
}

.gear-card-handle {
    cursor: grab;
    touch-action: none;
}

/* A bow's mounted-gear zone. The min-height matters: an empty zone that
   collapsed to nothing would be an unhittable drop target. */

.gear-drop-zone {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    align-items: center;
    min-height: 3.25rem;
    padding: 0.5rem;
    border: 1px dashed var(--bs-border-color);
    border-radius: var(--bs-border-radius);
}

.gear-drop-placeholder {
    padding-inline: 0.25rem;
}

.gear-chip {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    max-width: 100%;
    padding: 0.3rem 0.5rem;
    border: 1px solid var(--bs-border-color);
    border-radius: 2rem;
    background: var(--bs-body-bg);
}

/* Photo strip in the gear item modal. */

.gear-photo-thumb {
    position: relative;
    width: 4.5rem;
    height: 4.5rem;
}

.gear-photo-open {
    display: block;
    width: 100%;
    height: 100%;
    padding: 0;
    overflow: hidden;
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius);
    background: none;
}

.gear-photo-open img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.gear-photo-remove {
    position: absolute;
    top: -0.4rem;
    right: -0.4rem;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 1.35rem;
    height: 1.35rem;
    padding: 0;
    border: 1px solid var(--bs-border-color);
    border-radius: 50%;
    background: var(--bs-body-bg);
    color: var(--bs-secondary-color);
    line-height: 1;
}

.gear-photo-add {
    width: 4.5rem;
    height: 4.5rem;
    border: 1px dashed var(--bs-border-color);
    border-radius: var(--bs-border-radius);
    background: none;
    color: var(--bs-secondary-color);
}

.gear-photo-pending {
    position: absolute;
    bottom: 0.15rem;
    left: 0.15rem;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 1.35rem;
    height: 1.35rem;
    border-radius: 50%;
    background: var(--bs-body-bg);
    color: var(--bs-secondary-color);
    font-size: 0.8rem;
    line-height: 1;
}

/*
 * End-photo capture sheet (end_photo_capture_controller.js).
 *
 * A dark bottom sheet with a live viewfinder, framing reticle, shutter, skip and
 * retake. Slides up over the plotting page on the end-completion trigger; always
 * skippable. Hidden by default (no `.open`), so it costs nothing until summoned.
 */
/* Per-end photo retake strip (End scope of the remarks modal). */
.end-photo-strip-thumb {
    position: relative;
    width: 4.5rem;
    height: 4.5rem;
}
.end-photo-strip-open {
    display: block;
    width: 100%;
    height: 100%;
    padding: 0;
    overflow: hidden;
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius);
    background: none;
}
.end-photo-strip-open img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}
.end-photo-strip-remove {
    position: absolute;
    top: -0.4rem;
    right: -0.4rem;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 1.35rem;
    height: 1.35rem;
    padding: 0;
    border: 1px solid var(--bs-border-color);
    border-radius: 50%;
    background: var(--bs-body-bg);
    color: var(--bs-secondary-color);
    line-height: 1;
}
.end-photo-strip-pending {
    position: absolute;
    bottom: 0.15rem;
    left: 0.15rem;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 1.35rem;
    height: 1.35rem;
    border-radius: 50%;
    background: var(--bs-body-bg);
    color: var(--bs-secondary-color);
    font-size: 0.8rem;
    line-height: 1;
}
.end-photo-strip-add {
    width: 4.5rem;
    height: 4.5rem;
    border: 1px dashed var(--bs-border-color);
    border-radius: var(--bs-border-radius);
    background: none;
    color: var(--bs-secondary-color);
}

/* The always-visible per-end thumbnails in a "Comments & photos" card row —
   the compact variant of the modal strip's thumb, a tap-to-lightbox affordance
   (no remove control; management lives in the End modal). */
.end-photo-strip-thumb-sm {
    width: 3.25rem;
    height: 3.25rem;
}
.end-photo-strip-thumb-sm .end-photo-strip-pending {
    width: 1.05rem;
    height: 1.05rem;
    font-size: 0.65rem;
}

.end-photo-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.5);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.25s ease;
    z-index: 1080;
}
.end-photo-backdrop.open {
    opacity: 1;
    pointer-events: auto;
}
.end-photo-sheet {
    position: fixed;
    left: 50%;
    bottom: 0;
    width: 100%;
    max-width: 480px;
    transform: translate(-50%, 100%);
    visibility: hidden;
    transition: transform 0.28s cubic-bezier(0.2, 0.8, 0.2, 1), visibility 0s linear 0.28s;
    background: #111;
    color: #fff;
    border-radius: 16px 16px 0 0;
    overflow: hidden;
    z-index: 1081;
}
.end-photo-sheet.open {
    transform: translate(-50%, 0);
    visibility: visible;
    transition: transform 0.28s cubic-bezier(0.2, 0.8, 0.2, 1), visibility 0s;
}
.end-photo-sheet-head {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 12px 14px;
}
.end-photo-sheet-title {
    font-weight: 600;
    font-size: 0.95rem;
}
.end-photo-sheet-step {
    font-size: 0.72rem;
    opacity: 0.7;
}
.end-photo-close {
    margin-left: auto;
    flex-shrink: 0;
    background: rgba(255, 255, 255, 0.12);
    border: none;
    color: #fff;
    width: 34px;
    height: 34px;
    border-radius: 50%;
    display: grid;
    place-items: center;
    cursor: pointer;
}
.end-photo-viewfinder {
    position: relative;
    width: 100%;
    aspect-ratio: 4 / 3;
    background: #000;
    overflow: hidden;
}
.end-photo-video {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}
.end-photo-reticle {
    position: absolute;
    inset: 10% 8%;
    border: 2px solid rgba(255, 255, 255, 0.85);
    border-radius: 14px;
    box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.12) inset;
    pointer-events: none;
}
.end-photo-hint {
    position: absolute;
    left: 0;
    right: 0;
    top: 8px;
    text-align: center;
    font-size: 0.78rem;
    color: #fff;
    text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8);
    pointer-events: none;
}
.end-photo-sheet-fallback .end-photo-reticle {
    opacity: 0.35;
}
.end-photo-previews {
    position: absolute;
    bottom: 10px;
    left: 10px;
    display: flex;
    gap: 6px;
}
.end-photo-previews img {
    width: 42px;
    height: 32px;
    object-fit: cover;
    border-radius: 5px;
    border: 2px solid #fff;
}
/* Lightbox caption — which round/end/arrow the photo on screen belongs to. Sits
   above the bottom edge, clear of PhotoSwipe's own chrome, and never covers more
   than one line of the picture. */
.pswp__x10-caption {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 10;
    padding: 10px 16px calc(10px + env(safe-area-inset-bottom));
    text-align: center;
    font-size: 0.85rem;
    font-weight: 600;
    color: #fff;
    text-shadow: 0 1px 3px rgba(0, 0, 0, 0.9);
    background: linear-gradient(to top, rgba(0, 0, 0, 0.55), rgba(0, 0, 0, 0));
    pointer-events: none;
}

.end-photo-gallery {
    position: absolute;
    right: 10px;
    bottom: 10px;
    width: 38px;
    height: 38px;
    border-radius: 50%;
    display: grid;
    place-items: center;
    background: rgba(0, 0, 0, 0.45);
    border: 1px solid rgba(255, 255, 255, 0.55);
    color: #fff;
    font-size: 1.05rem;
    cursor: pointer;
}
.end-photo-gallery:active {
    transform: scale(0.93);
}
.end-photo-flash {
    position: absolute;
    inset: 0;
    background: #fff;
    opacity: 0;
    pointer-events: none;
}
.end-photo-flash-go {
    animation: end-photo-flash 0.35s ease;
}
@keyframes end-photo-flash {
    0% { opacity: 0.9; }
    100% { opacity: 0; }
}
.end-photo-controls {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 24px;
    padding: 16px 14px calc(16px + env(safe-area-inset-bottom));
    position: relative;
}
.end-photo-shutter {
    width: 68px;
    height: 68px;
    border-radius: 50%;
    background: #fff;
    border: 4px solid rgba(255, 255, 255, 0.45);
    box-shadow: 0 0 0 2px #111 inset;
    cursor: pointer;
}
.end-photo-shutter:active {
    transform: scale(0.93);
}
.end-photo-side {
    position: absolute;
    background: none;
    border: none;
    color: #fff;
    cursor: pointer;
    font-weight: 600;
    font-size: 0.9rem;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
}
.end-photo-side:first-child {
    left: 18px;
}
.end-photo-side:last-child {
    right: 18px;
}
.end-photo-side small {
    font-size: 0.62rem;
    opacity: 0.7;
    font-weight: 500;
}

/* Deep-link highlight for a cross-check "Review this set" jump (dataset binding,
   plan 2026-07-18 slice 3): the scoring detail controller scrolls the flagged
   round into view and toggles this class for a brief, self-fading pulse. */
@keyframes x10-set-flash {
    0% { background-color: rgba(255, 193, 7, 0.55); }
    100% { background-color: transparent; }
}
.x10-set-flash {
    animation: x10-set-flash 2.2s ease-out 1;
}
/* Same pulse on a pairing-table row (the match chrome points out a set that
   arrived from the other device). Bootstrap paints table cells, not the row, so
   the animation has to run on the cells to be visible. */
tr.x10-set-flash > td {
    animation: x10-set-flash 2.2s ease-out 1;
}

/* ===== Match creation form ===== */

/* Difficulty-slider tier zones: one band per named NAC tier, widths matching
   the slider's own scale (`tierZones()` in `nac-difficulty-scale.js`), so the
   thumb's position reads directly as a tier. The bar is under EVERY difficulty
   slider on the form — the individual opponent's and both team panels' — because
   a bare 4→9.9 number means nothing without the landmarks. */
.difficulty-zones {
    display: flex;
    height: 8px;
    border-radius: 4px;
    overflow: hidden;
    margin-top: 2px;
}

.difficulty-zone-labels {
    display: flex;
    font-size: 0.62rem;
    color: var(--bs-secondary-color);
}

.difficulty-zone-labels > div {
    text-align: center;
    overflow: hidden;
    white-space: nowrap;
}

/* Your team / the opposing team, each fenced so the roster, the team name and the
   difficulty slider inside read as one side's block. Bootstrap's own `legend` is a
   full-width h4 that sits ABOVE the box — reset to an inline label notched into the
   top border, which is what makes the border read as that side's perimeter. */
.team-side-fieldset {
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius);
    padding: 0.75rem;
}

.team-side-fieldset > legend {
    float: none;
    width: auto;
    padding: 0 0.5rem;
    margin-bottom: 0;
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--bs-secondary-color);
}

/* Roster rows are the team's shooting order, drag-sorted (SortableJS, as the
   scoring card order already uses). Only the handle grabs, so the name field and
   the selects inside the row stay usable on touch. */
.roster-row .roster-drag-handle {
    cursor: grab;
    touch-action: none;
}

.roster-row.roster-row-dragging {
    opacity: 0.4;
}

/* ===== Tap-repeatedly surfaces: no stuck hover on touch =====

   Bootstrap's `.btn:hover` swaps in `--bs-btn-hover-bg`, ~15% darker than the key's
   own colour (and a full fill on the `btn-outline-*` variants). A touch screen has no
   pointer to move away, so Chrome leaves the tapped element matching `:hover` until
   the next tap lands somewhere else — which on a keypad reads as the key the archer
   just pressed being greyed out, releasing only when the next key takes the state
   over. Every surface below is built to be tapped over and over, so the stale hover
   is permanently on screen, always one press behind.

   Neutralising the hover *variables* rather than the `:hover` rule leaves active,
   focus and disabled exactly as Bootstrap draws them, and the mapping is correct for
   the outline variants too: they inherit `--bs-btn-bg: transparent` from `.btn` and
   set their own `--bs-btn-border-color`/`--bs-btn-color`. Guarded on `hover: none`,
   so a mouse keeps its hover affordance. */
@media (hover: none) {
    /* The scoring keypad's score keys (X/10/9/…/M) */
    [data-action="arrow-keypad#pressKey"],
    /* The arrow counter's tally: the +N / -N buttons, and the chevrons that set
       how much each of them adds or removes */
    [data-arrow-counter-target="addBtn"],
    [data-arrow-counter-target="subtractBtn"],
    [data-arrow-counter-target="incrementBtn"],
    [data-arrow-counter-target="decrementBtn"] {
        --bs-btn-hover-bg: var(--bs-btn-bg);
        --bs-btn-hover-border-color: var(--bs-btn-border-color);
        --bs-btn-hover-color: var(--bs-btn-color);
    }
}
