/* Shared primitives: layout chrome (topbar/page-header), cards, tables,
   modals, filters, buttons, connection banner, status badges - anything
   used by more than one page, plus generic tag/attribute selectors. Loaded
   on every page (see base.html); race.css/event.css/public.css load
   alongside it there too, so a rule scoped to the "wrong" one of those
   three still applies wherever its selector matches - the split here is
   about keeping each file's *primary* concern easy to find, not about
   excluding rules from a page. */

* { box-sizing: border-box; }

.app-footer {
    text-align: center;
    font-size: 12px;
    color: #999;
    margin-top: 32px;
}

.footer-help-link { color: #999; }
.footer-help-link:hover { color: #185fa5; }

/* Online help page (help.html) */
.help-toc ol { margin: 0; padding-left: 20px; }
.help-toc li { margin-bottom: 4px; }
.help-section h3 { margin-bottom: 6px; }
.help-section:not(:first-of-type) { margin-top: 0; }
.help-figure { margin: 16px 0; }
.help-figure img {
    max-width: 100%;
    border: 1px solid #ddd;
    border-radius: 8px;
    display: block;
}
.help-figure figcaption {
    font-size: 13px;
    color: #666;
    margin-top: 6px;
}

body {
    font-family: -apple-system, system-ui, sans-serif;
    margin: 0;
    background: #f4f4f2;
    color: #222;
}

/* Wraps everything below .topbar (page header, tab content, footer -
   see base.html) in the same centered/padded box the whole page used to
   get directly from `body` - moved out of `body` itself so `.topbar`
   (a `body` child, sibling to this wrapper, not nested inside it) can be a
   plain `width: 100%` full-bleed bar without needing the old
   "100vw + shift by half the viewport" hack, which relied on canceling
   out this same padding and broke down by a few pixels on any page tall
   enough to show a vertical scrollbar (100vw's relationship to the
   scrollbar's own width isn't consistent across browsers). */
.page-content {
    /* Wide enough for the busiest tables (e.g. the quick-entry history,
       15 nowrap columns) to fit without a horizontal scrollbar on a normal
       desktop screen, instead of sitting in a needlessly narrow centered
       column with empty space on both sides. */
    max-width: 1800px;
    margin: 0 auto;
    padding: 0 clamp(12px, 3vw, 32px) 80px;
}

h1 { font-size: 22px; margin-bottom: 4px; }
h2 { font-size: 18px; margin-top: 32px; }

a { color: #185fa5; }

.breadcrumb {
    display: block;
    font-size: 14px;
    /* Light text: .topbar (below) has a dark background. */
    color: #e3edf0;
    /* No margin-bottom of its own anymore - .topbar's own padding/border now
       provides the spacing before .page-header (see below), since breadcrumb
       and user-bar are vertically centered together in that one bar. */
}

.breadcrumb a { color: #6fd3ec; text-decoration: none; }
.breadcrumb a:hover { text-decoration: underline; }

/* Left column of .topbar: the breadcrumb plus, right below it, which
   organizer's data is shown ("Organisateur : <nom>" - see
   effective_organizer_name in app/auth.py). */
.topbar-left {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.topbar-organizer-line {
    font-size: 12px;
    font-style: italic;
    color: rgba(255, 255, 255, 0.7);
}

/* Discreet online/offline indicator, updated by the connection monitor in
   app.js - pinned to the top-left corner of .quick-entry-header (that row
   has position:relative for this to anchor against) rather than taking up
   flex space, so it never pushes #feedback off-center. The coloured dot
   carries the actual status, the text just stays muted against the card's
   white background. */
.connection-status { position: absolute; top: 0; left: 0; font-size: 12px; color: #888; }

/* Lost/restored server connection banner, at the top of the quick-entry
   card (see event.html) - right where the operator is actually typing bib
   numbers, rather than a page-wide banner unrelated to whatever screen
   they're on. Empty/hidden (display:none) when the connection is fine. */
.connection-banner {
    border-radius: 6px;
    padding: 8px 16px;
    margin-bottom: 12px;
    text-align: center;
    font-size: 14px;
    font-weight: 600;
}
.connection-banner-offline { background: #ff1a1a; color: #fff; }
.connection-banner-restored { background: #2e7d32; color: #fff; }

/* Org-wide activity banner (base.html, current_user only - see
   initOrgActivityBanner in app.js): a fixed bar pinned to the bottom of
   every authenticated page, one scrolling notification at a time, hidden
   (display:none) whenever nothing is playing. z-index stays below
   .modal-overlay's 1000 so a confirmation popup always wins if one happens
   to be open when a notification arrives. */
.org-activity-banner {
    display: none;
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 900;
    align-items: center;
    background: #0f3b47;
    color: #fff;
    font-size: 16px;
    font-weight: 600;
    padding: 10px clamp(12px, 3vw, 32px);
    box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.2);
}

.org-activity-banner-track {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
}

.org-activity-banner-content {
    display: inline-block;
    white-space: nowrap;
    will-change: transform;
}

/* New-version banner (base.html, current_user only - see initUpdateChecker
   in app.js): pinned to the TOP of the page, unlike the org activity
   banner's bottom placement, and never auto-hides once shown - reloading is
   the only way it goes away. Higher z-index than .org-activity-banner's 900
   so it stays visible above it if both happen to show at once, but still
   below .modal-overlay's 1000 so an open confirmation dialog still wins. */
.update-banner {
    display: none;
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 950;
    align-items: center;
    justify-content: center;
    gap: 16px;
    background: #185fa5;
    color: #fff;
    font-size: 14px;
    font-weight: 600;
    padding: 10px clamp(12px, 3vw, 32px);
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}

.update-banner-button {
    background: #fff;
    color: #185fa5;
    border: none;
    border-radius: 4px;
    padding: 6px 14px;
    font-weight: 600;
    cursor: pointer;
}
.update-banner-button:hover { background: #e6f0fa; }
.connection-banner-restored { background: #2e7d32; color: #fff; }

/* Full-bleed: a direct `body` child (sibling of `.page-content`, not
   nested inside it - see .page-content's own comment), so a plain
   `width: 100%` already spans edge-to-edge with no centered/padded box to
   break out of and no viewport-width hack needed. .topbar-inner (below)
   re-creates .page-content's own max-width/padding so the bar's content
   still lines up with .page-header and every card underneath it. */
.topbar {
    width: 100%;
    /* sticky (not relative): keeps the header pinned to the top of the
       viewport once the page scrolls. Below .update-banner's z-index (950)
       so the rare "new version available" banner still wins if both ever
       occupy the very top of the viewport at once. */
    position: sticky;
    top: 0;
    z-index: 500;
    margin-bottom: 12px;
    background: #0f3b47;
}

.topbar-inner {
    max-width: 1800px;
    margin: 0 auto;
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
    padding: 12px clamp(12px, 3vw, 32px);
}

/* Sits right below .topbar: the logo+clock on the left, then the page's own
   title (filled in per-page via the page_header block) right after it, both
   on the same row, left-aligned - the logo is meant to always line up with
   the title rather than float in a header bar disconnected from the page
   content. */
.page-header {
    display: flex;
    justify-content: flex-start;
    align-items: center;
    gap: 16px;
    flex-wrap: wrap;
    margin-bottom: 8px;
}

.page-header-title {
    /* flex-basis 0 (not auto) so this item's width comes from the space
       left over next to the logo, not from its own max-content size -
       race.html's title is followed by a wide row (start-time cards +
       status timeline, see .race-header-panel) that would otherwise force
       this whole flex item to wrap under the logo instead of wrapping
       internally. */
    flex: 1 1 0%;
    min-width: 0;
}

.topbar-brand-logo {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    flex-shrink: 0;
    background: transparent;
    border-radius: 16px;
    padding: 14px 18px;
}

.app-logo-link {
    display: flex;
    align-items: center;
}

.app-logo {
    display: block;
    height: 64px;
    width: auto;
}

.topbar-pro-badge {
    font-weight: 700;
    color: #d32f2f;
    font-size: 12px;
    letter-spacing: 0.5px;
}

.topbar-clock {
    text-align: center;
    font-size: 14px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: #fff;
    background: #0e3b47;
    padding: 5px 14px;
    border-radius: 20px;
}

.user-bar {
    display: flex;
    align-items: center;
    font-size: 14px;
    color: #333;
    white-space: nowrap;
}

/* The whole prénom/nom + organisateur block (see base.html) acts as a
   single clickable trigger for the popover menu below it - two lines
   stacked and right-aligned, rather than the separate name/organizer/
   admin-link/logout controls this used to be spread across. */
.user-menu {
    position: relative;
}

.user-menu-trigger {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 1px;
    background: none;
    border: none;
    cursor: pointer;
    padding: 4px 10px;
    border-radius: 8px;
    font-family: inherit;
}

.user-menu-trigger:hover {
    /* Light overlay: .topbar (this trigger's ancestor) has a dark
       background, so a dark hover tint (as this used to be) would barely
       show up. */
    background: rgba(255, 255, 255, 0.1);
}

.user-menu-name {
    font-size: 14px;
    font-weight: 600;
    color: #fff;
}

.user-menu-subscription-line {
    font-size: 12px;
    font-style: italic;
    color: rgba(255, 255, 255, 0.7);
}

.user-menu-popover {
    position: absolute;
    top: 100%;
    right: 0;
    margin-top: 6px;
    background: #fff;
    border-radius: 10px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
    padding: 10px 0;
    min-width: 230px;
    z-index: 100;
    text-align: left;
    white-space: normal;
}

.user-menu-item {
    padding: 8px 16px;
    font-size: 14px;
    color: #333;
}

.user-menu-link {
    display: block;
    text-decoration: none;
    cursor: pointer;
}

.user-menu-link:hover {
    background: #f3f5f8;
}

.user-menu-separator {
    height: 1px;
    background: #eceef1;
    margin: 6px 0;
}

.user-menu-plan {
    font-weight: 700;
}

.user-menu-subline {
    font-size: 13px;
    color: #5c6370;
    margin-top: 2px;
}

.user-menu-version {
    font-size: 11px;
    color: #a8adb5;
    padding-top: 0;
}

.user-menu-organizer-switch label {
    display: block;
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: #8a8f98;
    margin-bottom: 4px;
}

.user-menu-organizer-switch select {
    width: 100%;
    cursor: pointer;
}

.card {
    background: white;
    border: 1px solid #ddd;
    border-radius: 10px;
    padding: 16px;
    margin-bottom: 16px;
}

#subsection-results > .card,
#results-category-container > .card:first-child,
#subsection-awards > .card:first-child {
    margin-top: 0;
    border-top-left-radius: 0;
    border-top-right-radius: 0;
    border-top: none;
}

form.inline {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    align-items: center;
}

input {
    font-size: 16px;
    padding: 10px;
    border: 1px solid #bbb;
    border-radius: 6px;
    flex: 1;
    min-width: 100px;
}

button {
    font-size: 16px;
    padding: 10px 18px;
    border: none;
    border-radius: 6px;
    background: #185fa5;
    color: white;
    cursor: pointer;
}

button:active { background: #0c447c; }

button:disabled {
    background: #ddd;
    color: #999;
    cursor: not-allowed;
}

button.secondary {
    background: #eee;
    color: #222;
}

.table-scroll {
    overflow-x: auto;
}

/* "Paramètres" admin tab (admin.html): unlike every other admin table, this
   one is deliberately NOT wrapped in .table-scroll - a long setting key
   (e.g. "TV_ARRIVALS_TICKER_WINDOW_MINUTES") or description should wrap
   within its own column instead of ever forcing a horizontal scrollbar.
   table-layout:fixed makes the percentage widths below authoritative
   (otherwise auto-layout would still just widen the key/description
   columns to fit their unbroken content, same failure mode documented for
   .public-event-arrivals-body earlier), and word-break lets a single long
   unbroken token (the key) actually break instead of overflowing its
   fixed-width cell.  */
.config-settings-table {
    table-layout: fixed;
}

.config-settings-table th,
.config-settings-table td {
    white-space: normal;
    word-break: break-word;
}

.config-settings-table th:nth-child(1), .config-settings-table td:nth-child(1) { width: 17%; }  /* Nom */
.config-settings-table th:nth-child(2), .config-settings-table td:nth-child(2) { width: 27%; }  /* Description */
.config-settings-table th:nth-child(3), .config-settings-table td:nth-child(3) { width: 9%; }   /* Type */
.config-settings-table th:nth-child(4), .config-settings-table td:nth-child(4) { width: 9%; }   /* Valeur par défaut */
.config-settings-table th:nth-child(5), .config-settings-table td:nth-child(5) { width: 19%; }  /* Valeur Application */
.config-settings-table th:nth-child(6), .config-settings-table td:nth-child(6) { width: 19%; }  /* Valeur Organisateur */

/* Same #connection-status treatment as the "dernières arrivées" page (see
   .public-event-arrivals-body rules above) - inline status next to the
   title. #connection-banner is left in its default, normal-flow layout
   (see the comment on .public-event-arrivals-body above) - here it pushes
   the title row and the auto-scrolling results card down while shown; the
   scroll loop (tvScrollTick) reads the card's clientHeight fresh on every
   frame, so it adapts to the shrunk space with no extra recalculation
   needed. */
.public-event-tv-body #connection-status {
    position: static;
    flex: 0 0 auto;
}

/* .app-footer's normal margin-top:32px is meant for a page that scrolls
   past it - here it would instead just eat into the fixed 100vh budget
   above, shrinking .public-results-scroll-area by that much for nothing. */
body.public-results-body .app-footer {
    margin-top: 0;
}

/* Public "dernières arrivées" screen (public_event_arrivals.html): the
   live per-race clocks sit on the same line as the title on a wide-enough
   screen (justify-content:space-between), and wrap onto their own line
   below it when there isn't room (flex-wrap - a phone in portrait, or an
   event with several races running at once), rather than ever squeezing
   the title or overflowing the page horizontally. */
/* Same #connection-status indicator/logic as the quick-entry screen (see
   .connection-status above, driven by app.js's connection monitor), just
   laid out inline next to the title instead of pinned to the quick-entry
   card's top-left corner - this page has no card up top to anchor a
   corner-pin against, so it rides on the same line as the title instead
   (see .public-arrivals-title-group). Overrides the base class's
   position:absolute (an ID selector always wins over a class one,
   regardless of source order). */
.public-event-arrivals-body #connection-status {
    position: static;
    flex: 0 0 auto;
}

/* Mobile/tablet-first: the gender/category/gap/gender-rank columns (kept
   on a wider screen, see the rule above) are dropped below the same
   700px breakpoint the rest of the app already uses for its "narrow
   screen" adjustments - a screen meant to be glanced at hands-free
   shouldn't need a horizontal scroll for its core columns. Column widths
   are tightened to match, so the remaining 8 columns actually fit a
   ~380px phone without one. */
@media (max-width: 700px) {
    .public-event-arrivals-body .arrivals-hide-narrow { display: none; }

    .public-event-arrivals-body th, .public-event-arrivals-body td {
        padding: 5px 2px;
        font-size: 11px;
    }
    .public-event-arrivals-body .bib-badge { padding: 1px 4px; }

    .public-event-arrivals-body .public-results-table th:nth-child(1),
    .public-event-arrivals-body .public-results-table td:nth-child(1) { width: 50px; }  /* Dossard */
    .public-event-arrivals-body .public-results-table th:nth-child(2),
    .public-event-arrivals-body .public-results-table td:nth-child(2) { width: 22px; }  /* Clt */
    .public-event-arrivals-body .public-results-table th:nth-child(3),
    .public-event-arrivals-body .public-results-table td:nth-child(3) { width: 54px; }  /* Nom */
    .public-event-arrivals-body .public-results-table th:nth-child(4),
    .public-event-arrivals-body .public-results-table td:nth-child(4) { width: 48px; }  /* Prénom */
    .public-event-arrivals-body .public-results-table th:nth-child(5),
    .public-event-arrivals-body .public-results-table td:nth-child(5) { width: 48px; }  /* Club */
    .public-event-arrivals-body .public-results-table th:nth-child(8),
    .public-event-arrivals-body .public-results-table td:nth-child(8) { width: 50px; }  /* Course */
    .public-event-arrivals-body .public-results-table th:nth-child(9),
    .public-event-arrivals-body .public-results-table td:nth-child(9) { width: 54px; }  /* Temps */
    .public-event-arrivals-body .public-results-table th:nth-child(12),
    .public-event-arrivals-body .public-results-table td:nth-child(12) { width: 30px; } /* Clt cat */

    /* Club/race names are unbounded, unlike every other column here - a
       single long one, kept nowrap+ellipsis like the rest, would just
       truncate hard. Wrapping just these two (on spaces only -
       word-break:break-word would instead shrink the column to one
       character per line under table-layout:fixed too) lets a long name
       break onto a second line within its own column instead. */
    .public-event-arrivals-body .arrivals-wrap {
        white-space: normal;
        overflow: visible;
        text-overflow: clip;
    }
}

table {
    width: 100%;
    /* separate (not collapse) + border-spacing:0 is what lets the header
       row's end cells actually show rounded corners (see thead th below) -
       every cell in this app only ever uses border-bottom (no border-top/
       left/right), so there's no shared edge to double up now that cells
       no longer merge their borders. border-spacing defaults to a visible
       2px gap under "separate" if left unset, which is why it's pinned to
       0 here rather than just flipping border-collapse alone. */
    border-collapse: separate;
    border-spacing: 0;
    margin-top: 8px;
}

th, td {
    text-align: center;
    padding: 8px 6px;
    border-bottom: 1px solid #eee;
    font-size: 15px;
    white-space: nowrap;
}

#section-entry table .icon-edit,
#section-entry table .icon-delete {
    font-size: 15px;
    width: 22px;
    height: 22px;
    border-radius: 6px;
}

/* Header row style shared by every table in the app: accent background,
   white text, rounded top corners on the row's outer edges. A table with
   its own reason to differ (e.g. .awards-category-table's blue, for its
   own row+column dual-header design) overrides this via a more specific
   selector further down - see that rule's own comment. */
thead th {
    background: #0f3b47;
    color: #fff;
    /* No border of its own - the header row and the filter row directly
       below it (see .filter-row th) are the same cyan, so the base th,td
       border-bottom (light gray, meant to separate body rows) would show
       up as a stray pale seam between them instead. .filter-row th sets
       its own (darker, intentional) border-bottom to close off the whole
       header block against the table body below it. */
    border-bottom: none;
}

thead th:first-child { border-top-left-radius: 10px; }
thead th:last-child { border-top-right-radius: 10px; }

/* Sort indicator, shared by every sortable header in the app (every one of
   them is a plain `<th onclick="sortX('column')">`, so [onclick] alone is
   enough to target them all, with no per-table class needed). A faint
   up/down glyph shows on every sortable column all the time (a constant
   affordance that the column CAN be sorted); sortX()/renderX() in each
   table's own script toggles .sort-asc/.sort-desc on the active column,
   which swaps that glyph for a solid single-direction arrow. */
thead th[onclick] {
    position: relative;
    padding-right: 20px;
    cursor: pointer;
}

thead th[onclick]::after {
    content: "\21C5";
    position: absolute;
    right: 6px;
    top: 50%;
    transform: translateY(-50%);
    font-size: 11px;
    opacity: 0.65;
}

thead th[onclick].sort-asc::after {
    content: "\25B2";
    opacity: 1;
}

thead th[onclick].sort-desc::after {
    content: "\25BC";
    opacity: 1;
}

.col-left { text-align: left; }

.message {
    padding: 10px 14px;
    border-radius: 6px;
    margin: 10px 0;
    font-size: 15px;
}

.message.success { background: #eaf3de; color: #27500a; }
.message.error { background: #fcebeb; color: #501313; }

.bib-badge {
    display: inline-block;
    background: #eee;
    border-radius: 4px;
    padding: 2px 8px;
    font-weight: 600;
}

/* Any bib rectangle shown alongside a race (participants, arrivals, DNF,
   missing, bib-pickup...): filled (at ~50% opacity, via the "80" hex alpha
   suffix) in that runner's race color, set inline via
   style="background-color:...80" - see bibTextColor in app.js for the bib
   number's own text color (black/white by contrast, not by gender). No
   border (deliberately removed - the fill color alone is enough to read as
   that race's bib). */
.bib-badge-outlined { border: none; }

.status-badge {
    display: inline-block;
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    padding: 3px 10px;
    border-radius: 20px;
    vertical-align: middle;
}

.status-badge.status-in_preparation { background: #eee; color: #555; }
.status-badge.status-bib_pickup_open { background: #ede4fb; color: #5b2a86; }
.status-badge.status-bib_pickup_closed { background: #e0e7ef; color: #33517a; }
.status-badge.status-in_progress { background: #fdecc8; color: #8a5a00; }
.status-badge.status-finished { background: #eaf3de; color: #27500a; }

.runner-status-badge {
    display: inline-block;
    font-size: 13px;
    font-weight: 600;
    padding: 3px 10px;
    border-radius: 20px;
    vertical-align: middle;
    text-transform: none;
}

.runner-status-badge.runner-status-registered { background: #eee; color: #555; }
.runner-status-badge.runner-status-starter { background: #ede4fb; color: #5b2a86; }
.runner-status-badge.runner-status-running { background: #fdecc8; color: #8a5a00; }
.runner-status-badge.runner-status-finished { background: #eaf3de; color: #27500a; }
.runner-status-badge.runner-status-dnf { background: #fbdede; color: #8a1f1f; }
.runner-status-badge.runner-status-missing { background: #fbe4c4; color: #96460a; }

.public-link-url {
    font-family: "Courier New", Courier, monospace;
    font-size: 13px;
    word-break: break-all;
}

.icon-delete {
    display: inline-block;
    color: #c0392b;
    font-weight: 700;
    font-size: 20px;
    line-height: 1;
    cursor: pointer;
    padding: 0 6px;
}

.icon-delete:hover { color: #791f1f; }

.icon-edit {
    display: inline-block;
    color: #185fa5;
    font-size: 18px;
    line-height: 1;
    cursor: pointer;
    padding: 0 6px;
}

.icon-edit:hover { color: #0c447c; }

.icon-add {
    display: inline-block;
    color: #185fa5;
    font-weight: 700;
    font-size: 20px;
    line-height: 1;
    cursor: pointer;
    padding: 0 6px;
}

.icon-add:hover { color: #0c447c; }

/* Toolbar-level icon (sits above the participants table, next to
   icon-add/icon-import/icon-delete - see race.html/event.html) that opens
   the "Plage dossard" popup - same grayscale-emoji treatment as icon-import,
   declared here rather than race.css since event.html uses it too. */
.icon-bib-ranges {
    display: inline-block;
    font-size: 18px;
    line-height: 1;
    cursor: pointer;
    padding: 0 6px;
    filter: grayscale(100%);
    opacity: 0.75;
}

.icon-bib-ranges:hover { opacity: 1; }

/* Same treatment as icon-bib-ranges, just a different glyph - opens the
   "Attribution des dossards" popup (bulk bib assignment) instead of viewing
   the current ranges. Grouped with icon-bib-ranges/icon-clear-bibs inside
   .bib-toolbar - see race.html's participants-toolbar. */
.icon-bib-assign {
    display: inline-block;
    font-size: 18px;
    line-height: 1;
    cursor: pointer;
    padding: 0 6px;
    filter: grayscale(100%);
    opacity: 0.75;
}

.icon-bib-assign:hover { opacity: 1; }

/* Clears every participant's bib number in one shot (see clearAllBibs in
   race/participants.js) - same muted treatment as its .bib-toolbar
   siblings; the confirmation popup before the action fires is what carries
   the "this is destructive" signal, not the icon's color. */
.icon-clear-bibs {
    display: inline-block;
    font-size: 18px;
    line-height: 1;
    cursor: pointer;
    padding: 0 6px;
    filter: grayscale(100%);
    opacity: 0.75;
}

.icon-clear-bibs:hover { opacity: 1; }

/* Participants tab heading row (race.html): title + participant-management
   icons (add/import/delete-all) on the left, a distinct "bib zone" (view
   ranges/bulk-assign/clear-all-bibs) pinned to the right - replaces the old
   float:right layout, which (via .table-scroll's overflow-x:auto forming a
   new block formatting context next to an uncleared float) kept the
   participants table from ever reaching the card's full width. */
.participants-toolbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 12px;
}

.participants-toolbar-left {
    display: flex;
    align-items: center;
    gap: 4px;
}

.participants-toolbar-left h2 {
    margin: 0;
    margin-right: 6px;
}

.bib-toolbar {
    display: inline-flex;
    align-items: center;
    gap: 2px;
    padding: 4px 10px;
    border: 1px solid #e0e0e0;
    border-radius: 20px;
    background: #f7f9fb;
}

.bib-toolbar-label {
    font-size: 13px;
    font-weight: 600;
    color: #555;
    margin-right: 4px;
}

/* Muted/grayscale rather than colored like icon-edit/icon-delete - "view" is
   a lower-emphasis row action than editing or deleting. Declared here
   (rather than in event.css, its only current caller) so the shared
   bordered-box rule further down - which lists .icon-view alongside
   .icon-edit/.icon-delete - reliably comes after it in the cascade and can
   override its display/padding, the same relationship icon-edit/icon-delete
   already have with that rule in this file. */
.icon-view {
    display: inline-block;
    font-size: 15px;
    line-height: 1;
    cursor: pointer;
    padding: 0 4px;
    filter: grayscale(100%);
    opacity: 0.6;
}

.icon-view:hover { opacity: 1; }

/* The rest of the row-action icon family below (icon-dnf/icon-finish,
   originally race.html-only; icon-copy/icon-qrcode/icon-toggle/
   icon-regenerate, originally event.html-only) are declared here for the
   same reason as icon-view above: the shared bordered-box rule further
   down needs to come after each one in the cascade to override its
   display/padding, which only happens reliably if they all live in this
   same file rather than one that loads after it. */
.icon-dnf {
    display: inline-block;
    color: #c0392b;
    font-size: 18px;
    line-height: 1;
    cursor: pointer;
    padding: 0 6px;
}

.icon-dnf:hover { color: #791f1f; }

.icon-finish {
    display: inline-block;
    color: #27500a;
    font-size: 18px;
    line-height: 1;
    cursor: pointer;
    padding: 0 6px;
}

.icon-finish:hover { color: #1a3707; }

.icon-copy {
    display: inline-block;
    font-size: 15px;
    line-height: 1;
    cursor: pointer;
    padding: 0 4px;
    filter: grayscale(100%);
    opacity: 0.6;
}

.icon-copy:hover { opacity: 1; }

.icon-qrcode {
    display: inline-block;
    font-size: 15px;
    line-height: 1;
    cursor: pointer;
    padding: 0 4px;
    color: #185fa5;
    opacity: 0.75;
}

.icon-qrcode:hover { opacity: 1; }

.icon-toggle {
    display: inline-block;
    color: #185fa5;
    font-size: 16px;
    line-height: 1;
    cursor: pointer;
    padding: 0 6px;
}

.icon-toggle:hover { color: #0c447c; }

.icon-regenerate {
    display: inline-block;
    color: #185fa5;
    font-size: 18px;
    line-height: 1;
    cursor: pointer;
    padding: 0 6px;
}

.icon-regenerate:hover { color: #0c447c; }

/* Filter row sits directly under the header row and shares its background
   (both are `thead th`, see the shared rule above, so this is actually just
   making the inheritance explicit) - the white/bordered filter inputs
   themselves (see .filter-row input / .ms-filter-toggle below) are what
   stands out against it, not the row itself. first-child/last-child reset
   the header row's own rounded-corner rule (also `thead th`, matched here
   too since :first-child/:last-child are scoped per-row, not per-table) -
   only the very top of the table should be rounded, not this inner row. */
.filter-row th {
    background: #0f3b47;
    padding: 8px 6px;
    border-bottom: 1px solid #1c5269;
}

.filter-row th:first-child,
.filter-row th:last-child {
    border-radius: 0;
}

.filter-row input {
    width: 100%;
    min-width: 70px;
    font-size: 13px;
    padding: 5px 8px;
    font-weight: normal;
    color: #222;
    border: 1px solid #ccc;
    border-radius: 6px;
}

/* Search icon baked into the cell via ::before (not the input itself, which
   has no room for a background-image once centered text/padding are both in
   play) - :has() scopes it to cells that actually contain a free-text
   filter, so empty cells (no filter for that column) and the ms-filter/
   select cells stay untouched. */
.filter-row th:has(input[type="text"]) {
    position: relative;
}

.filter-row th:has(input[type="text"])::before {
    content: "\1F50D";
    position: absolute;
    left: 14px;
    top: 50%;
    transform: translateY(-50%);
    font-size: 11px;
    opacity: 0.5;
    pointer-events: none;
}

.filter-row input[type="text"] {
    padding-left: 24px;
}

/* .filter-row input above and input[type="checkbox"]'s reset (near the top
   of this file) have equal specificity - without this, the checkbox here
   would win by cascade order and get stretched to width:100%/min-width:70px,
   which (via flex-basis:auto) blows up its flex-item width and pushes the
   option's label text outside the dropdown's clipped/scrollable area. */
.ms-filter-option input[type="checkbox"] {
    width: auto;
    min-width: 0;
}

.ms-filter {
    position: relative;
}

.ms-filter-toggle {
    width: 100%;
    min-width: 70px;
    font-size: 13px;
    padding: 5px 8px;
    font-weight: normal;
    text-align: left;
    background: white;
    color: #222;
    border: 1px solid #ccc;
    border-radius: 6px;
    white-space: nowrap;
}

.ms-filter-menu {
    /* "fixed" with top/left computed in JS (toggleMultiSelectFilter), not
       CSS-anchored to .ms-filter - .table-scroll sets overflow-x:auto,
       which per the CSS spec forces overflow-y to a used value of "auto"
       too, so a statically (absolute/relative) positioned menu would get
       clipped whenever the table is narrow OR short.
       color is explicit (not inherited) because .filter-row th now sits on
       a cyan background and inherits white text (see thead th) - fixed
       positioning moves this menu out of that cyan box visually, but it's
       still .ms-filter-toggle/.ms-filter-menu's DOM ancestor, so without
       this the white color would still cascade down and make the menu's
       text unreadable against its own white background. */
    position: fixed;
    z-index: 20;
    background: white;
    color: #222;
    border: 1px solid #bbb;
    border-radius: 6px;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
    padding: 6px;
    min-width: 160px;
    max-height: 240px;
    overflow-y: auto;
}

.ms-filter-option {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 4px 6px;
    font-size: 13px;
    font-weight: normal;
    white-space: nowrap;
    cursor: pointer;
    border-radius: 4px;
}

.ms-filter-option:hover {
    background: #f0f0f0;
}

.modal-overlay {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.4);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 1000;
    padding: 16px;
}

.modal-box {
    background: white;
    border-radius: 10px;
    padding: 24px;
    width: 100%;
    max-width: 420px;
    max-height: 90vh;
    overflow-y: auto;
}

.modal-field {
    display: flex;
    flex-direction: column;
    gap: 4px;
    margin-bottom: 12px;
}

.modal-field label {
    font-size: 13px;
    font-weight: 600;
    color: #555;
}

.required-marker {
    color: #d32f2f;
    margin-left: 2px;
}

.modal-field input:not([type="checkbox"]):not([type="radio"]):not([type="color"]), .modal-field select {
    width: 100%;
}

.color-swatch {
    display: inline-block;
    width: 18px;
    height: 18px;
    border-radius: 4px;
    border: 1px solid rgba(0, 0, 0, 0.15);
    vertical-align: middle;
}

/* <select> gets none of the plain `input` rule's sizing above, so its height
   drifts from text inputs unless matched explicitly here. */
.modal-field select {
    font-size: 16px;
    padding: 10px;
    border: 1px solid #bbb;
    border-radius: 6px;
    background: white;
}

.modal-box-profile {
    max-width: 480px;
}

/* "Ajouter/modifier un utilisateur" popup (admin.html) - wider than the
   default 420px so the name/first-name and email/organizer pairs sit side by
   side, keeping the whole form short enough to avoid an inner scrollbar on
   typical screens. */
.modal-box-user {
    max-width: 520px;
}

.modal-field-row {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px;
    margin-bottom: 12px;
}

.modal-field-row .modal-field {
    margin-bottom: 0;
}

/* "Plages de dossards" popup (see openBibRangesModal in app.js) - wider than
   the default 420px to fit the extra "Course" column in the event-wide
   variant. */
.modal-box-bib-ranges {
    max-width: 560px;
}

.bib-ranges-missing-text {
    color: #b45f06;
    font-size: 14px;
}

/* "Attribution des dossards" popup (race.html, "in_preparation" races only -
   see openBibAssignmentModal in race/bib-assignment.js). Slightly wider than
   the default 420px, and every range row is centered as a unit (rather than
   left-aligned) now that width leaves visible slack on the sides. */
.modal-box-bib-assignment {
    max-width: 520px;
    padding: 28px;
}

.bib-assign-stats {
    display: flex;
    gap: 20px;
    margin-bottom: 28px;
}

.bib-assign-stat {
    flex: 1;
    background: #f5f7fa;
    border-radius: 8px;
    padding: 16px 12px;
    text-align: center;
}

.bib-assign-stat-value { font-size: 24px; font-weight: 800; color: #1a2b3c; }

.bib-assign-stat-value-error { color: #c0392b; }

.bib-assign-stat-label { font-size: 13px; color: #555; }

.bib-assign-ranges-heading {
    margin: 0 0 14px;
    font-size: 14px;
    font-weight: 600;
    color: #555;
    text-align: center;
}

/* Each .bib-range-group (one row + its error text) sizes to its own content
   and is centered as a block, rather than the row stretching edge-to-edge -
   see openBibAssignmentModal's renderBibAssignRanges for the markup. */
#bib-assign-ranges {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 14px;
    margin-bottom: 22px;
}

.bib-range-row {
    display: flex;
    align-items: center;
    gap: 12px;
}

.bib-range-index {
    flex: none;
    width: 60px;
    font-size: 13px;
    font-weight: 600;
    color: #555;
}

.bib-range-row input[type="number"] {
    flex: none;
    width: 90px;
    min-width: 0;
    text-align: center;
}

.bib-range-sep { color: #888; }

.bib-range-remove-spacer { display: inline-block; width: 20px; }

.bib-range-row-error input[type="number"] { border-color: #c0392b; }

.bib-range-error {
    color: #c0392b;
    font-size: 12px;
    text-align: center;
    margin-top: 6px;
}

/* Shown once the entered ranges can't cover every participant (see
   updateBibAssignValidation in race/bib-assignment.js) - the "Dossards
   disponibles" tile already turns red at that point, this spells out why. */
.bib-assign-capacity-warning {
    background: #fdecea;
    border: 1px solid #f3b7ae;
    border-radius: 8px;
    color: #b3261e;
    font-size: 13px;
    font-weight: 600;
    text-align: center;
    padding: 10px 14px;
    margin-bottom: 14px;
}

.add-bib-range-button {
    display: block;
    background: none;
    border: 1px dashed #185fa5;
    color: #185fa5;
    padding: 8px 14px;
    font-size: 14px;
    margin: 0 auto 24px;
}

.add-bib-range-button:hover { background: #eef5fc; }

/* Centered values in the public runner-lookup page's results grid only -
   scoped to #runner-result rather than changing .profile-field-value
   itself, which is shared with the (left-aligned) "My account" modal. */
#runner-result .profile-field-value {
    text-align: center;
}

.profile-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 14px 16px;
    margin-bottom: 18px;
}

.profile-field {
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.profile-field-wide {
    grid-column: 1 / -1;
}

.profile-field-label {
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: #8a8f98;
}

.profile-field-value {
    font-size: 15px;
    font-weight: 500;
    color: #1c1f24;
    background: #f3f5f8;
    border: 1px solid #e4e7ec;
    border-radius: 6px;
    padding: 8px 10px;
}

.profile-section-heading {
    font-size: 12px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: #8a8f98;
    margin: 4px 0 10px;
    padding-top: 14px;
    border-top: 1px solid #e4e7ec;
}

#profile-role {
    background: #f3f5f8;
    border: 1px solid #e4e7ec;
    border-radius: 6px;
    padding: 8px 10px;
}

#profile-role strong {
    display: block;
    font-size: 14px;
    font-weight: 600;
}

#profile-role small {
    display: block;
    color: #666;
    font-size: 12px;
    margin-top: 2px;
}

.email-not-verified-badge {
    color: #c0392b;
    font-weight: 700;
    font-size: 12px;
    margin-left: 6px;
}

.login-wrap {
    display: flex;
    justify-content: center;
    padding-top: 8vh;
}

.login-card {
    width: 100%;
    max-width: 380px;
}

.login-logo {
    display: block;
    max-width: 220px;
    width: 100%;
    height: auto;
    margin: 0 auto 8px;
}

.login-signup-hint {
    text-align: center;
    margin: 16px 0 0;
    font-size: 14px;
}

.login-back-link {
    text-align: center;
    margin: 16px 0 0;
    font-size: 14px;
}

.modal-actions {
    display: flex;
    justify-content: flex-end;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 16px;
}

/* Every popup's buttons (Annuler/Non/Fermer included, not just the primary
   action) share the app's cyan accent with a black label - overrides both
   the plain button and .secondary defaults, scoped to .modal-actions only
   so the rest of the app (races-toolbar-actions, page-level buttons...)
   keeps its own colors. */
.modal-actions button {
    background: #3bbbda;
    color: #000;
}

.modal-actions button:hover,
.modal-actions button:active {
    background: #2fa5c2;
    color: #000;
}

.tabs {
    display: flex;
    gap: 4px;
    margin-bottom: 16px;
}

.tab {
    /* White by default: most tab bars in the app sit directly on the gray
       page background (body's #f4f4f2), where the old #eee was too close
       to it to read as a distinct tab. Bars living inside a white card
       (e.g. the Résultats sub-tabs) need the opposite contrast - see
       .tabs-on-white below. */
    background: #fff;
    color: #555;
    border-radius: 8px 8px 0 0;
    /* Harmless for a plain-text tab (button text just centers as before) -
       lets a tab with an icon (see .tab-icon, event.html) line it up with
       its label instead of the icon sitting on its own baseline. */
    display: inline-flex;
    align-items: center;
    gap: 6px;
}

.tabs-on-white .tab {
    background: #eee;
}

.tab.tab-active {
    background: #0f3b47;
    color: white;
}

.tab.tab-disabled {
    opacity: 0.4;
    cursor: not-allowed;
    pointer-events: none;
}

/* "Manquants" tab (race.html): once the race is finished, anyone still
   "missing" is unusual enough to flag with a color the operator can't miss
   even while looking at a different tab - see updateMissingTabAlert. Comes
   after .tab-active so it still wins (same specificity, later wins) if the
   operator happens to already be on that tab when it starts alerting. */
.tab.tab-alert {
    background: #ff6a00;
    color: #fff;
}

#tab-entry.tab-active {
    background: #e64a19;
    color: white;
}

.step {
    padding: 8px 14px;
    border-radius: 20px;
    font-size: 14px;
    font-weight: 600;
    background: #eee;
    color: #888;
}

.step.step-past {
    background: #0f3b47;
    color: white;
}

.step.step-current {
    background: #3bbbda;
    color: white;
}

/* .cell-datetime-time/.cell-datetime-date default to justify-content:center
   already (see below) - no override needed here now that these cards center
   their content like the rest of the card, unlike the earlier left-aligned
   version. */

.start-time-label,
.start-time-card .cell-datetime-time,
.start-time-card .cell-datetime-date {
    color: #fff;
}

.start-time-card .icon-edit {
    color: #fff;
}

.start-time-card .icon-edit:hover {
    color: #cfe8ff;
}

#tab-entry.tab-active + .entry-fullscreen-toggle { background: #e64a19; }
#tab-entry.tab-active + .entry-fullscreen-toggle:hover { background: #d84315; }

/* Fullscreen "Saisie Chrono" mode (toggleEntryFullscreen in event.html):
   everything above the tabs, and the tab bar itself, is hidden - only
   #section-entry's own content (this bar, the live race clocks, the entry
   form, the history table) stays on screen. Scoped to body.entry-fullscreen
   rather than driven by :fullscreen, since the CSS view (hide the chrome)
   and the actual browser Fullscreen API are deliberately two independent
   things here - requestFullscreen() can be refused by the browser (no user
   gesture, sandboxed iframe...) while the decluttered layout should still
   work regardless. */
body.entry-fullscreen .topbar,
body.entry-fullscreen .page-header,
body.entry-fullscreen .event-stats-tiles,
body.entry-fullscreen .tabs {
    display: none;
}

.quick-entry {
    text-align: center;
}

/* Female/male runner: pink/dark blue text on the gender letter cell, on
   tables with no bib column - see genderTextClass in app.js. First in the
   gender/category ranking: pale blue background on that cell only (see
   leadCellClass). */
.text-female { color: #FF1493; }
.text-male { color: #00008B; }
.cell-lead { background: #dbeafe; font-weight: 600; }

/* Start-time cell: the time is the important part of "Heure de départ" (the
   date rarely differs from "today"), so it's the bold/prominent line, with
   the date muted and smaller underneath it - see updateStartTimeCells for
   the markup order (time first, then date). */
.cell-datetime-date, .cell-datetime-time {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 5px;
    white-space: nowrap;
}

.cell-datetime-time {
    font-weight: 700;
    font-size: 14px;
    color: #1a2b3c;
}

.cell-datetime-date {
    color: #999;
    font-size: 12px;
    margin-top: 2px;
}

.cell-icon { font-size: 12px; }

/* Bordered box around every per-row action icon in the app (edit, delete,
   finish, dnf, toggle, regenerate, copy, qrcode) - originally only the
   races table's own edit/delete used this look (via a .races-actions-row-
   scoped rule); generalized here so every table's end-of-row actions read
   the same way (participants, results, missing runners, DNF, public links,
   quick-entry history, organizers/users/config admin lists...). Each icon
   keeps its own glyph color/size/opacity (set on its base .icon-* rule
   above) - only the box around it is shared. Toolbar-level icons that sit
   above a table rather than at the end of a row (icon-add, icon-import,
   icon-export...) are deliberately not included - they aren't row actions. */
.icon-edit,
.icon-delete,
.icon-finish,
.icon-dnf,
.icon-toggle,
.icon-regenerate,
.icon-copy,
.icon-qrcode,
.icon-view {
    border: 1px solid #e0e0e0;
    border-radius: 8px;
    width: 30px;
    height: 30px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0;
}

.row-recent { background: #fff6c9; }
.row-orphan .bib-badge, .row-duplicate .bib-badge { background: #fff; color: #b30000; }
.row-orphan .icon-edit, .row-orphan .icon-delete,
.row-duplicate .icon-edit, .row-duplicate .icon-delete { color: #fff; }
.row-orphan .icon-edit:hover, .row-orphan .icon-delete:hover,
.row-duplicate .icon-edit:hover, .row-duplicate .icon-delete:hover { color: #ffd9d9; }

.row-recent-finish {
    font-size: 1.3em;
    font-weight: 700;
    background: #fff176;
}

/* Alternating light blue / white row background across every data table in
   the app, so a wide row is easier to follow across many columns. Rows with
   their own explicit alert/status background are excluded entirely (rather
   than fought via specificity) so that color is never diluted or hidden. */
tbody tr:nth-child(even):not(.row-recent):not(.row-recent-finish):not(.row-picked-up):not(.row-no-starter):not(.row-orphan):not(.row-duplicate):not(.row-pending-sync) {
    background: #eefdff;
}

.stats-tiles {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: 16px;
    margin-bottom: 16px;
}

.stat-tile {
    display: flex;
    align-items: center;
    gap: 14px;
}

/* Event-level summary cards (event.html, above the tabs) - a distinct set
   of classes from .stat-tile/.stats-tiles above rather than reusing them:
   text-column/icon-column side-by-side layout (vs. race.html's icon-beside-
   label layout), plus a third sub-label line for context (e.g. "15.2% des
   partants") that .stat-tile has no room for. Keeping them separate means
   neither layout change risks the other page's already-shipped look. */
.event-stats-tiles {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
    gap: 16px;
    margin-top: 30px;
    margin-bottom: 16px;
}

/* Two columns: the text block grows to fill the card, the icon is a fixed-
   width column of its own on the right - both vertically centered against
   each other, so a taller icon doesn't leave the (shorter) text block
   pinned to the top. */
.event-stat-tile {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
}

.event-stat-text {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 2px;
    min-width: 0;
    flex: 1;
}

/* Bare glyph, no circle/background behind it - just bigger than the old
   boxed version so it still reads as the card's icon. */
.event-stat-icon {
    flex: none;
    font-size: 38px;
    line-height: 1;
    color: #3bbbda;
}

.event-stat-value { font-size: 26px; font-weight: 800; color: #1a2b3c; }
.event-stat-label { font-size: 14px; color: #333; }
.event-stat-sublabel { font-size: 12px; color: #999; }

/* Bib min/max card: the usual .event-stat-minmax row, plus room below it for
   .event-stat-warning (non-contiguous bib numbers - see
   findMissingBibNumbers in app.js) - hidden by default via inline
   style="display:none", shown only when a gap is actually found. */
.event-stat-minmax-col {
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 0;
    flex: 1;
}

.event-stat-warning {
    font-size: 11px;
    color: #b45f06;
    text-align: center;
}

/* Checkboxes/radios must never inherit the text-input box model above -
   otherwise they render as large bordered/padded boxes instead of a native
   control, and their left edge won't line up with anything around them. */
input[type="checkbox"], input[type="radio"] {
    width: auto;
    min-width: 0;
    flex: none;
    padding: 0;
    border: none;
    border-radius: 0;
}

.current-clock {
    font-size: 28px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    font-family: "Courier New", Courier, monospace;
}

@media (max-width: 700px) {
    th, td { padding: 6px 4px; font-size: 13px; }
    h1 { font-size: 19px; }
    h2 { font-size: 16px; }
    .current-clock { font-size: 19px; }
    .modal-field-row { grid-template-columns: 1fr; }
}
