/* 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;
}

/* Set on <body> by base.html/mobile/base.html whenever
   is_production_environment (app/templating.py) is false - reuses
   SENTRY_ENVIRONMENT so an operator can never mistake a test/staging tab
   for the real one. Only the topbar changes: every other element still
   reusing #0f3b47 (tabs, table headers...) stays as-is, since those aren't
   what someone glances at to tell environments apart. */
body.non-production .topbar {
    background: #c2255c;
}

.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-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-credits-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-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;
}

/* A tab panel's own first card sits flush against the tab bar right above
   it - no gap, square top corners, no top border - so the active tab reads
   as physically attached to its panel instead of floating above it with a
   band of page background in between. [id^="section-"] covers every
   top-level tab panel across event.html/race.html/admin.html (all follow
   that id convention); the other two selectors are the same treatment one
   level deeper, for the Résultats/Récompenses sub-tabs nested inside
   #section-results (race.html) - see .tabs's own margin-bottom: 0 below,
   which removes the matching gap above each tab bar. */
[id^="section-"] > .card:first-child,
#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;
    /* Chrome's UA stylesheet gives date/time/number inputs a "monospace"
       default (plain text inputs get "Arial") - neither is the app's own
       font stack (see body, above), which is what made a date/time field
       stand out next to a plain text one in the same form (e.g. "Heure
       d'arrivée" vs "Temps de course" in the edit-result popups, desktop and
       mobile alike - both load this stylesheet). */
    font-family: inherit;
}

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; }
/* A race sitting at "remise des dossards" - the two _open/_closed variants
   below are left over from an older split of that status and no longer match
   any RaceStatus value. */
.status-badge.status-bib_pickup { background: #ede4fb; color: #5b2a86; }
.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 autocomplete widget: one big search field with a custom dropdown
   underneath - no <datalist>, since the rows need real markup (bib badge +
   name + race) rather than plain text options. Lives here rather than in
   public.css, where it started: three separate screens now share it - the
   public runner-lookup page, the "Modifier le passage" popup of the
   Chronometrage tab, and the mobile bib-handout screen - and only base.css
   is loaded by both shells (see mobile/base.html, which deliberately does
   not load public.css). */
.runner-search-box {
    position: relative;
}

.runner-search-input {
    width: 100%;
    box-sizing: border-box;
    font-size: 20px;
    padding: 14px 16px;
    border: 2px solid #185fa5;
    border-radius: 10px;
}

.runner-search-input:focus {
    outline: none;
    border-color: #0c447c;
    box-shadow: 0 0 0 4px rgba(24, 95, 165, 0.15);
}

.runner-search-dropdown {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    margin-top: 4px;
    background: white;
    border: 1px solid #ddd;
    border-radius: 10px;
    max-height: 320px;
    overflow-y: auto;
    z-index: 50;
    box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12);
}

.runner-search-dropdown-item {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 10px;
    padding: 12px 14px;
    cursor: pointer;
    border-bottom: 1px solid #eee;
}

.runner-search-dropdown-item:last-child { border-bottom: none; }
.runner-search-dropdown-item:hover { background: #f4f8fc; }

.runner-search-dropdown-meta {
    color: #777;
    font-size: 13px;
    margin-left: auto;
}

.runner-search-dropdown-empty {
    padding: 14px;
    color: #777;
    text-align: center;
}

.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; }

/* Desktop "Remise dossards" tab (race.html): a solid green/orange pair
   rather than the pastel .runner-status-* states above - this one badge is
   the entire point of the column (see renderBibPickup in
   app/static/race/bib-pickup.js), so it reads as done/not-done at a glance
   rather than blending in with the row like a passive status label does. */
.runner-status-badge.bib-pickup-status-yes { background: #2e7d32; color: #fff; }
.runner-status-badge.bib-pickup-status-no { background: #b35c00; color: #fff; }

/* The whole row is the control on that same tab (see
   handleBibPickupRowClick) - same "click anywhere on the row" idiom as the
   audit trail's own clickable rows (.audit-row below), hover on td rather
   than tr for the same reason: it wins the zebra-stripe/status-background
   specificity fight without needing !important. */
.bib-pickup-row { cursor: pointer; }
.bib-pickup-row:hover td { background: #eaf1f8; }

/* A row that opens the runner-detail popup (see openRunnerDetail,
   app/static/runner-detail.js) - every desktop participant listing except
   Remise des dossards, whose row click is already its own control above.
   Same "click anywhere on the row" idiom, and same hover-on-td reason. */
.clickable-row { cursor: pointer; }
.clickable-row:hover td { background: #eaf1f8; }

.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;
}

/* A plain single-select dropdown filter (currently just the "Remise
   dossards" tab's pickup-status column, race.html - two fixed values, never
   a free-text search) - same footprint as .filter-row input above, so it
   sits in its header cell exactly like every text filter beside it. */
.filter-row select {
    width: 100%;
    min-width: 70px;
    font-size: 13px;
    padding: 5px 8px;
    font-weight: normal;
    color: #222;
    background: white;
    border: 1px solid #ccc;
    border-radius: 6px;
}

/* .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;
}

/* Wraps a password <input> (see initPasswordToggle, app.js) so its eye-icon
   button can sit inside the field rather than beside it. Right padding on
   the input keeps typed text from running under the icon. */
.password-field {
    position: relative;
    display: flex;
}

.password-field input {
    padding-right: 40px;
}

.password-toggle-btn {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    width: 40px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: transparent;
    color: #666;
    padding: 0;
    border-radius: 0 6px 6px 0;
}

.password-toggle-btn:hover,
.password-toggle-btn:active {
    background: transparent;
    color: #222;
}

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

/* The two participant *forms* that share this look - the shared
   edit-participant form and the add-runner form - are wide enough for their
   Identité/Course columns (see .rd-columns below) to each keep a comfortable
   width. A standalone class rather than tied to .modal-box-profile (which
   the "My account" modal also uses, at its own narrower 480px) so it can be
   applied to any modal that wants this look. The read-only "fiche
   participant" view popup no longer uses this - it has its own full design
   system (.rd-card and friends, further down). */
.rd-wide {
    max-width: 860px;
    max-height: 94vh;
}

/* Identité on the left, Course (+ Coordonnées & Urgence) on the right, on
   both participant forms. Two independent flows rather than a single grid
   over every field: Identité is typically the taller column, and
   align-items:start keeps the other from being stretched to match instead
   of just ending early. Collapses back to one column below 700px, alongside
   this file's other narrow-screen modal adjustments. */
.rd-columns {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0 32px;
    align-items: start;
}

/* The three participant popups (runner-detail view, edit-participant, and
   add-runner) - now long enough, with every field always shown under its
   section (see CLAUDE.md), to actually need to scroll. Their own <h2> and
   .modal-actions must stay reachable at the top/bottom regardless - a plain
   position:sticky on each (still inside .modal-box's own single
   overflow-y:auto) was tried first and rejected: a
   sticky element doesn't reserve its own space, so whatever content the box
   hadn't been scrolled to yet was still laid out underneath it and the
   sticky footer intercepted clicks meant for it (e.g. the "Coordonnées &
   Urgence" toggle, sitting right before .modal-actions in the flow). A
   three-region flex column fixes that at the layout level instead: <h2> and
   .modal-actions are plain (non-scrolling) flex items, and only
   .modal-box-pinned-body between them - a real, separate box - scrolls, so
   nothing in it can ever occupy the same space as the header/footer.
   .modal-box already sets max-height/overflow-y for the box as a whole;
   this overrides just enough of that (overflow, padding - each region
   re-applies its own 24px share) to make the split possible. `form` here
   needs display:contents so #edit-participant-form (which wraps both the
   body and the actions, for the submit button) doesn't itself become a
   fourth, unstyled flex item breaking the column into a nested box. */
.modal-box-pinned {
    display: flex;
    flex-direction: column;
    padding: 0;
    overflow: hidden;
}

.modal-box-pinned form {
    display: contents;
}

.modal-box-pinned > h2 {
    flex: none;
    margin: 0;
    padding: 24px 24px 14px;
    /* Explicit rather than relying on inheriting it from body - these
       titles must stay #222 regardless of what else might set a color
       further up the tree later. */
    color: #222;
}

.modal-box-pinned-body {
    flex: 1 1 auto;
    /* Flex items default to min-height:auto, which would let this grow to
       fit its content instead of actually shrinking to the space left by
       the header/footer and scrolling - the one flexbox detail that makes
       the rest of this work. */
    min-height: 0;
    overflow-y: auto;
    /* A deliberate last-resort safety net, not the primary mechanism: the
       runner-detail popup's own value boxes are sized to never wrap and to
       grow .rd-wide instead when they need more room (see the
       minmax(max-content, 1fr) comments on .rd-columns/.profile-grid) -
       .rd-wide's own max-width is generous enough that real data (French
       names, emails, club names) shouldn't ever reach this. Only a
       pathological value longer than that would still need it, as a
       horizontal scrollbar here rather than silently overflowing past the
       modal's rounded corners. */
    overflow-x: auto;
    padding: 0 24px;
}

.modal-box-pinned .modal-actions {
    flex: none;
    margin: 0;
    padding: 14px 24px 24px;
}

/* "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;
}

/* Collapsed-by-default "Informations additionnelles" section (see
   partials/additional_info_fields.html) - a plain <details>, so no JS is
   needed just to show/hide it. Reused as-is for the read-only view version
   too (desktop's #runner-detail-modal). Desktop only: mobile shows this
   group always expanded instead, under its own section heading - see
   CLAUDE.md. */
.modal-additional-info {
    margin: 4px 0 16px;
}

.modal-additional-info[open] summary {
    margin-bottom: 12px;
}

.modal-additional-info .modal-field:last-child {
    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;
}

/* "Modifier le passage" popup (event.html's #edit-entry-modal) - wider than
   the default 420px so a "coureur" autocomplete match (bib badge + name +
   race name) fits on one line instead of wrapping, keeping the dropdown
   short. overflow:visible (instead of the base .modal-box's overflow-y:
   auto) matters more: the dropdown is a position:absolute child, and an
   auto-overflow ancestor still counts it toward its own scrollable area
   (CSS spec, not a bug) - so with many matches the box grew its own
   scrollbar on top of the dropdown's, forcing a confusing double scroll to
   see the rest of the list. The form itself (three short fields + buttons)
   never needs to scroll on its own, so letting the dropdown overlay it
   without clipping removes the outer scrollbar entirely - only the
   dropdown's own internal one (max-height + overflow-y:auto, see
   .runner-search-dropdown) is ever needed. */
.modal-box-edit-entry {
    max-width: 520px;
    overflow: visible;
}

.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;
}

/* minmax(0, 1fr) rather than a bare 1fr: a bare fr track's automatic
   minimum is its content's min-content size, so one cell with a long
   unbreakable value (an email address, with no space to break on) would
   otherwise force that whole column wider and squeeze every other cell
   sharing those columns, in every row - most visibly the phone-number cells
   next to it, whose call/SMS/WhatsApp icons (see .rd-phone-actions below)
   need a predictable minimum width to never get squeezed out.
   minmax(0, 1fr) caps each track's minimum at 0, so the two/three columns
   always split the row evenly regardless of what any one cell contains -
   overflow-wrap on .profile-field-value (below) is what makes a too-long
   value wrap within its own box instead. */
.profile-grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 14px 16px;
    margin-bottom: 18px;
}

/* Sexe/Catégorie/Date de naissance together on the runner-detail popup
   (#rd-gender/#rd-category/#rd-birth-date) - the same three-across grouping
   mobile gives the same three fields (.mp-detail-box-third) - three equal
   columns rather than .profile-grid's two, which would leave the third
   value stranded alone on its own half-empty row. */
.profile-grid-3 {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 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;
    /* A long unbreakable value (an email address) wraps within its own box
       instead of stretching it - see the minmax(0, 1fr) comment on
       .profile-grid above, which this pairs with. */
    overflow-wrap: anywhere;
}

/* Public runner-lookup page's "Temps de passage" table (§6.13) - the
   "Évolution" column's three states, and a not-yet-reached point's whole
   row. Same colours as elsewhere in this app's desktop CSS (the connection-
   restored banner's green, the error/delete red) rather than new ones. */
.split-delta-up { color: #2e7d32; font-weight: 600; }
.split-delta-down { color: #c0392b; font-weight: 600; }
.split-delta-neutral { color: #8a8f98; }
.split-row-pending { color: #8a8f98; }

/* Shared by both participant *forms*' section titles (Identité/Course via
   .profile-section-heading itself, and Coordonnées & Urgence's own toggle
   via .modal-additional-info summary) - one rule rather than two separately
   maintained copies, so they can never drift apart again the way
   Coordonnées & Urgence's own cyan accent once did against this rule's own
   color (see CLAUDE.md). No divider line above a section any more (there
   used to be one above every section but the first) - sections read as one
   continuous flow rather than separated blocks. Only the click-specific
   bits (cursor, list marker) are still summary-only, just below. The
   read-only "fiche participant" view popup has its own section-title
   treatment instead (.rd-section-title, further down). */
.profile-section-heading,
.modal-additional-info summary {
    font-size: 12px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: #0f3b47;
    margin: 4px 0 10px;
}

.modal-additional-info summary {
    cursor: pointer;
    list-style-position: outside;
}

/* ==========================================================================
   "Fiche participant" - the read-only runner-detail view popup
   (#runner-detail-modal, see partials/runner_detail_modal.html and
   app/static/runner-detail.js).

   Its own self-contained design system rather than the shared .profile- and
   .modal- classes the two participant forms still use: this popup is a
   presented record (a header identifying who this is, fields grouped in two
   columns, a full-width result strip), not a form, and trying to serve both
   from one set of classes is what kept making one drift while fixing the
   other. The forms are untouched by everything below.
   ========================================================================== */

.rd-card {
    background: white;
    border-radius: 16px;
    width: 100%;
    max-width: 1180px;
    max-height: 94vh;
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

/* --- Header: accent bar, name, identifying chips, status, close --- */

.rd-header {
    flex: none;
    display: flex;
    align-items: flex-start;
    gap: 18px;
    padding: 18px 26px 14px;
    border-bottom: 1px solid #edeff3;
}

/* Tinted with the runner's own race colour (set inline by
   renderRunnerDetail), so the popup opens visibly "belonging" to that race -
   the same colour its chip dot and its row's badge already use elsewhere. */
.rd-accent {
    flex: none;
    width: 5px;
    align-self: stretch;
    border-radius: 3px;
    background: #ccc;
}

.rd-header-main {
    flex: 1;
    min-width: 0;
}

.rd-eyebrow {
    display: block;
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: #9aa0aa;
}

.rd-name {
    margin: 4px 0 0;
    font-size: 25px;
    line-height: 1.15;
    font-weight: 800;
    color: #14171c;
    overflow-wrap: anywhere;
}

.rd-name .rd-name-first {
    font-weight: 600;
}

.rd-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 10px;
}

.rd-chip {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    background: #f2f4f7;
    border-radius: 999px;
    padding: 6px 13px;
    font-size: 14px;
    color: #4a5058;
    max-width: 100%;
    overflow-wrap: anywhere;
}

.rd-chip-dot {
    flex: none;
    width: 10px;
    height: 10px;
    border-radius: 3px;
    background: #ccc;
}

.rd-chip-strong {
    font-weight: 700;
    color: #14171c;
}

.rd-header-actions {
    flex: none;
    display: flex;
    align-items: center;
    gap: 10px;
}

/* The status pill, reusing the app-wide .runner-status-* colour modifiers
   (applied alongside this class) so a status never reads one colour here and
   another in a table - only the shape/size is specific to this popup. */
.rd-status {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 8px 16px;
    border-radius: 999px;
    font-size: 15px;
    font-weight: 600;
}

.rd-status-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: currentColor;
}

.rd-close {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    border: none;
    border-radius: 10px;
    background: #f2f4f7;
    color: #5b616b;
    font-size: 20px;
    line-height: 1;
    cursor: pointer;
    padding: 0;
}

.rd-close:hover {
    background: #e4e7ec;
    color: #14171c;
}

/* --- Body: two columns split by a vertical rule, then the result strip --- */

.rd-body {
    flex: 1 1 auto;
    /* Flex items default to min-height:auto, which would let this grow to
       fit its content instead of shrinking into the space left by the
       header/footer and scrolling - the one flexbox detail this depends on
       (same reason .modal-box-pinned-body needs it). */
    min-height: 0;
    overflow-y: auto;
    padding: 16px 26px 14px;
}

/* The right column gets slightly the larger share: its fields carry the
   quick-action icons (and the longest values in the popup - an email), so
   an even split is what made them wrap onto a second line. */
.rd-split {
    display: grid;
    grid-template-columns: 1fr 1px 1.18fr;
    gap: 0 30px;
    align-items: start;
}

.rd-split-rule {
    align-self: stretch;
    background: #edeff3;
}

/* Section title followed by a hairline filling the rest of its line - the
   rule is a flex child rather than a border, so it always stops exactly
   where the column does regardless of the title's own length. */
.rd-section-title {
    display: flex;
    align-items: center;
    gap: 12px;
    font-size: 12px;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: #14171c;
    margin: 0 0 12px;
}

.rd-section-title::after {
    content: "";
    flex: 1;
    height: 1px;
    background: #edeff3;
}

/* The Coordonnées & Urgence title doubles as its show/hide control: same
   look as a plain section title, plus a right-hand "Masquer/Afficher" +
   chevron. A real <button> (not the <details>/<summary> the forms use) so
   the two field rows it controls can live anywhere in the markup. */
.rd-section-toggle {
    width: 100%;
    background: none;
    border: none;
    padding: 0;
    font-family: inherit;
    text-align: left;
    cursor: pointer;
}

/* order puts the title's own hairline (::after, always the last DOM child)
   *before* this control, so the row reads title ——— Masquer ▾ rather than
   title Masquer ▾ ———. */
.rd-toggle-hint {
    order: 2;
    flex: none;
    display: inline-flex;
    align-items: center;
    gap: 8px;
    font-size: 12px;
    font-weight: 600;
    text-transform: none;
    letter-spacing: normal;
    color: #7b818b;
}

.rd-section-toggle::after {
    order: 1;
}

/* Shown next to "Coordonnées & Urgence" (view popup #rd-coord-dot, edit
   form #ep-coord-dot) whenever at least one of its four fields is filled in
   - a hint worth having precisely because the section is collapsed by
   default, so there's otherwise no way to tell there's anything there
   without opening it. order defaults to 0, same as the title text right
   before it in the DOM, so it lands between the title and the hairline
   (order:1) rather than after the "Masquer/Afficher" hint (order:2). */
.rd-section-dot {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: #56b4d3;
    flex: none;
}

.rd-toggle-chevron {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    border-radius: 8px;
    background: #f2f4f7;
    font-size: 9px;
}

.rd-section + .rd-section {
    margin-top: 18px;
}

/* --- Fields --- */

.rd-fields {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 10px 18px;
}

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

.rd-field-label {
    display: block;
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.07em;
    color: #9aa0aa;
    margin-bottom: 5px;
}

.rd-field-value {
    display: flex;
    align-items: center;
    gap: 10px;
    /* A fixed height, not a minimum: the contact fields' own quick-action
       icons would otherwise push those two boxes taller than every other
       one (box-sizing is border-box app-wide, so this includes padding). */
    height: 44px;
    background: #f4f6f8;
    border: 1px solid transparent;
    border-radius: 10px;
    padding: 10px 14px;
    font-size: 16px;
    font-weight: 500;
    color: #14171c;
}

/* The two "en cas d'urgence" fields, tinted so the one thing somebody opens
   this popup for in a hurry is the one thing they can't miss - both when
   read-only (.rd-field-value) and when editable (.rd-input). */
.rd-field-urgent .rd-field-value,
.rd-field-urgent .rd-input {
    background: #fdf2f5;
    border-color: #f7dbe4;
}

.rd-field-empty .rd-field-value {
    color: #b0b5bd;
}

.rd-field-value .color-swatch {
    flex: none;
}

/* Every value stays on exactly one line - the popup and its columns are
   sized so real data (French names, clubs, emails) fits, and anything
   genuinely longer is truncated with an ellipsis rather than wrapping the
   box to two lines (renderRunnerDetail puts the full value in a title
   attribute, so a truncated one is still readable on hover). */
.rd-field-text {
    flex: 1;
    min-width: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.rd-anonymize-row {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-top: 14px;
}

.rd-anonymize-row input[type="checkbox"] {
    width: 20px;
    height: 20px;
    margin: 0;
    accent-color: #185fa5;
}

.rd-anonymize-row .rd-field-label {
    margin: 0;
}

/* --- Contact quick actions (call / SMS / WhatsApp / email) --- */

.rd-actions {
    flex: none;
    display: flex;
    gap: 6px;
}

.rd-action {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    border-radius: 50%;
    background: #e6e9ee;
    color: #4a5058;
    font-size: 13px;
    line-height: 1;
    text-decoration: none;
}

.rd-action:hover {
    filter: brightness(0.94);
}

/* WhatsApp in its own brand green, so it reads as a distinct, recognizable
   destination rather than a third identical grey circle. color:#fff is
   what the icon's own fill="currentColor" (phone_actions.html) picks up -
   without it the SVG would inherit .rd-action's dark grey, unreadable
   against this green. */
.rd-action-whatsapp {
    background: #25d366;
    color: #fff;
}

/* --- Result strip: one card, one segment per figure --- */

.rd-result {
    margin-top: 16px;
}

.rd-result-strip {
    display: grid;
    grid-template-columns: repeat(6, minmax(0, 1fr));
    background: #f4f6f8;
    border-radius: 12px;
    padding: 12px 6px;
}

.rd-stat {
    padding: 0 18px;
    min-width: 0;
}

.rd-stat + .rd-stat {
    border-left: 1px solid #e2e6eb;
}

.rd-stat-label {
    display: block;
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.07em;
    color: #9aa0aa;
    margin-bottom: 6px;
    white-space: nowrap;
}

.rd-stat-value {
    font-size: 22px;
    font-weight: 800;
    color: #14171c;
    line-height: 1.1;
    white-space: nowrap;
}

/* The "/ 19" of a rank, or the "km/h" of a speed - present but secondary to
   the figure itself. */
.rd-stat-unit {
    font-size: 15px;
    font-weight: 600;
    color: #9aa0aa;
    margin-left: 3px;
}

/* Wraps the result strip and, when present, the "Temps de passage" table
   right under it into one visually seamless box - overflow:hidden lets the
   strip's square bottom edge and the table's square top edge butt together
   inside one rounded outline, rather than each carrying its own radius. */
.rd-result-card {
    border-radius: 12px;
    overflow: hidden;
}

/* --- "Temps de passage": one row per intermediate timing point the
   runner's race passes through (§6.13, _runner_checkpoint_splits). Read-
   only, like the rest of Résultat - populated by renderRunnerSplits
   (runner-detail.js), hidden whenever the race has no such point. --- */

/* overflow-x:auto (same idiom as .table-scroll elsewhere) rather than
   hiding columns on a narrow window - the grid tracks below have a
   min-width, so this only ever kicks in there instead of squeezing every
   row unreadable. Carries its own top border by default, since a
   still-running runner can show this table with no result strip above it
   (see renderRunnerDetail) - .rd-splits-joined drops it for the common case
   where the strip *is* shown right above, so the two read as one seamless
   box rather than a visible seam between them. */
.rd-splits {
    border: 1px solid #edeff3;
    overflow-x: auto;
}

.rd-splits.rd-splits-joined {
    border-top: none;
}

.rd-splits-header {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 10px 22px;
    background: #fbfcfd;
    border-bottom: 1px solid #edeff3;
}

.rd-splits-title {
    font-size: 10.5px;
    letter-spacing: 0.12em;
    font-weight: 800;
    color: #5a6470;
    text-transform: uppercase;
}

.rd-splits-count {
    font-size: 11px;
    font-weight: 700;
    color: #9aa1ae;
}

.rd-splits-col-head,
.rd-split-row {
    display: grid;
    grid-template-columns: 88px minmax(220px, 1fr) 150px 130px 150px;
    gap: 0;
    padding: 9px 22px;
    min-width: 620px;
}

.rd-splits-col-head {
    background: white;
    border-bottom: 1px solid #edeff3;
}

.rd-splits-col-head span {
    font-size: 10.5px;
    letter-spacing: 0.1em;
    font-weight: 700;
    color: #9aa1ae;
    text-transform: uppercase;
}

.rd-split-row {
    align-items: center;
    padding-top: 11px;
    padding-bottom: 11px;
    border-top: 1px solid #f3f4f8;
    background: white;
}

/* The finish row reads slightly apart from the intermediate points above it
   - same treatment its dot gets (.rd-split-dot-finish). */
.rd-split-row-finish {
    background: #fbfcfd;
}

.rd-split-km {
    font-size: 15px;
    font-weight: 700;
    color: #14212e;
}

.rd-split-point {
    display: flex;
    align-items: center;
    gap: 9px;
    min-width: 0;
}

.rd-split-dot {
    flex: none;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: #c9d0da;
}

.rd-split-dot-finish {
    background: #ec2c87;
}

.rd-split-point-name {
    font-size: 15px;
    font-weight: 600;
    color: #14212e;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.rd-split-time {
    font-size: 16px;
    font-weight: 700;
    color: #14212e;
    font-variant-numeric: tabular-nums;
}

.rd-split-rank {
    font-size: 16px;
    font-weight: 700;
    color: #14212e;
}

.rd-split-rank-total {
    font-size: 13px;
    font-weight: 600;
    color: #9aa1ae;
}

.rd-split-evo {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    justify-self: start;
    border-radius: 999px;
    padding: 4px 11px;
    font-size: 13.5px;
    font-weight: 700;
}

.rd-split-evo-start {
    background: #f3f4f8;
    color: #8a929e;
}

.rd-split-evo-up {
    background: #edf7dc;
    color: #4e7a1e;
}

.rd-split-evo-down {
    background: #fdf1f7;
    color: #b0246a;
}

.rd-split-evo-stable {
    background: #f3f4f8;
    color: #5a6470;
}

/* A configured point the runner hasn't reached yet - dimmed rather than
   shown with the same weight as real data, its rank/time/évolution cells
   already rendered as the shared "—" placeholder. */
.rd-split-row-pending {
    opacity: 0.55;
}

/* --- Editable mode (#edit-participant-modal): the same card, the same two
   columns and the same result strip as the read-only popup above, with the
   value boxes replaced by real inputs. Only what genuinely can't be typed
   stays a read-only .rd-field-value there: the race, the status (in the
   header) and the result. --- */

.rd-mode-badge {
    display: inline-block;
    margin-left: 8px;
    padding: 3px 9px;
    border-radius: 999px;
    background: #e3f2f8;
    color: #1d6b86;
    font-size: 10px;
    font-weight: 800;
    letter-spacing: 0.08em;
}

/* "Non modifiable", next to the Résultat title in edit mode - deliberately
   neutral grey, unlike .rd-mode-badge: it marks an absence of action, not a
   mode. */
.rd-title-badge {
    display: inline-block;
    margin-left: 8px;
    padding: 3px 9px;
    border-radius: 999px;
    background: #f2f4f7;
    color: #7b818b;
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.06em;
}

/* Same box as .rd-field-value, but white and outlined - a field you can type
   in has to look like one, while keeping the exact height and rhythm of the
   read-only popup so switching modes doesn't move anything. */
.rd-input {
    display: block;
    width: 100%;
    height: 44px;
    background: white;
    border: 1px solid #d7dbe1;
    border-radius: 10px;
    padding: 10px 14px;
    font-family: inherit;
    font-size: 16px;
    font-weight: 500;
    color: #14171c;
}

.rd-input:focus {
    outline: none;
    border-color: #4aa3c7;
    box-shadow: 0 0 0 3px rgba(74, 163, 199, 0.16);
}

.rd-input:disabled {
    background: #f4f6f8;
    color: #7b818b;
}

.rd-select {
    appearance: none;
    -webkit-appearance: none;
    padding-right: 34px;
    background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath fill='%238a8f98' d='M0 0h10L5 6z'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 14px center;
    background-size: 10px 6px;
}

/* A "coded" select keeps its full wording in the dropdown ("Femme",
   "Masters 2 (M2)") but shows only the short code once closed - a value box
   is too narrow for the long form, and the code is what operators read.
   Same idea as mobile's own .short-select-control: the native <select> stays
   fully native (real dropdown, keyboard nav, accessible value), its own text
   made transparent, with .rd-select-display stacked on top of it. */
.rd-select-wrap {
    display: block;
    position: relative;
}

.rd-select-coded {
    color: transparent;
}

.rd-select-coded option {
    color: #14171c;
}

.rd-select-display {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 0 34px 0 14px;
    font-size: 16px;
    font-weight: 500;
    color: #14171c;
    /* Every click still reaches the real select underneath. */
    pointer-events: none;
}

.rd-select-coded:disabled + .rd-select-display {
    color: #7b818b;
}

.rd-field-hint {
    display: block;
    margin-top: 5px;
    font-size: 12px;
    color: #7b818b;
}

/* The result strip in edit mode: same layout, visibly inert - dashed rather
   than solid, and dimmed, so it reads as "shown for context" next to the
   fields that can actually be typed in. */
.rd-result-strip-readonly {
    background: #fafbfc;
    border: 1px dashed #dde1e6;
}

.rd-result-strip-readonly .rd-stat-value {
    color: #5b616b;
}

/* --- Creation mode (#add-runner-modal): the same card again, with what only
   creating needs - a subtitle instead of chips, required markers, the
   "catégorie ou date de naissance" pair framed as one requirement, and a
   segmented sexe control. --- */

/* No race colour to borrow when nothing exists yet. */
.rd-accent-neutral {
    background: #7cc4dc;
}

.rd-subtitle {
    margin: 8px 0 0;
    font-size: 14px;
    color: #7b818b;
}

.rd-required {
    color: #e0246a;
    margin-left: 3px;
}

.rd-required-note {
    color: #7b818b;
}

/* Sexe: two buttons rather than a dropdown - only two values ever, and
   creating runners is done in bulk, so one click beats open/pick/close. */
.rd-segmented {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px;
}

.rd-segment {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    height: 44px;
    background: white;
    border: 1px solid #d7dbe1;
    border-radius: 10px;
    font-family: inherit;
    font-size: 16px;
    font-weight: 600;
    color: #14171c;
    cursor: pointer;
}

.rd-segment:hover {
    background: #f4f6f8;
}

.rd-segment[aria-pressed="true"] {
    border-color: #4aa3c7;
    background: #eaf6fa;
    box-shadow: inset 0 0 0 1px #4aa3c7;
}

/* The one requirement two fields can satisfy - tinted and framed so it reads
   as a single thing to fill in, not as two independent optional fields. */
.rd-required-group {
    margin-top: 16px;
    padding: 14px 16px 16px;
    border: 1px solid #f7dbe4;
    border-radius: 12px;
    background: #fdf2f5;
}

.rd-required-group-title {
    font-size: 12px;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.07em;
    color: #c2185b;
}

.rd-required-group-hint {
    margin: 6px 0 12px;
    font-size: 13px;
    line-height: 1.45;
    color: #7b818b;
}

/* "ou" sits between the two fields, aligned with the inputs rather than
   their labels. */
.rd-either-or {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
    align-items: end;
    gap: 10px;
}

.rd-either-or-separator {
    padding-bottom: 12px;
    font-size: 13px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: #9aa0aa;
}

/* --- Footer --- */

.rd-footer {
    flex: none;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 12px;
    padding: 12px 26px 14px;
    border-top: 1px solid #edeff3;
}

/* Pushes the buttons to the right while keeping the unsaved-changes hint
   (and any error message) on the left. */
.rd-footer-note {
    flex: 1;
    min-width: 0;
    font-size: 14px;
    color: #9aa0aa;
}

.rd-card form {
    display: contents;
}

.rd-footer button {
    min-width: 128px;
    padding: 11px 20px;
    border-radius: 10px;
    font-size: 15px;
    font-weight: 600;
    cursor: pointer;
}

.rd-btn-secondary {
    background: white;
    border: 1px solid #d7dbe1;
    color: #14171c;
}

.rd-btn-secondary:hover {
    background: #f4f6f8;
}

/* "Créer et suivant" - a third weight between Annuler and the primary
   action: it commits just as much, it just doesn't end the task. */
.rd-btn-outline {
    background: white;
    border: 1px solid #4aa3c7;
    color: #2b7f9e;
}

.rd-btn-outline:hover {
    background: #eaf6fa;
}

.rd-btn-primary {
    background: #4aa3c7;
    border: 1px solid #4aa3c7;
    color: white;
}

.rd-btn-primary:hover {
    background: #3d90b2;
    border-color: #3d90b2;
}

@media (max-width: 900px) {
    .rd-split {
        grid-template-columns: 1fr;
        gap: 26px;
    }
    .rd-split-rule {
        display: none;
    }
    .rd-result-strip {
        grid-template-columns: repeat(3, minmax(0, 1fr));
        row-gap: 18px;
    }
    /* With three per row, the 4th starts a new row and must not carry the
       divider that only separates neighbours within a row. */
    .rd-stat:nth-child(3n + 1) {
        border-left: none;
    }
}

@media (max-width: 560px) {
    .rd-fields {
        grid-template-columns: minmax(0, 1fr);
    }
    .rd-result-strip {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
    .rd-stat:nth-child(3n + 1) {
        border-left: 1px solid #e2e6eb;
    }
    .rd-stat:nth-child(odd) {
        border-left: none;
    }
    .rd-name {
        font-size: 24px;
    }
}

#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;
}

/* Phone-sized viewports, for the whole authentication family (login,
   signup, forgot/reset password - they all share .login-card).

   These pages are NOT part of the dedicated mobile UI (app/templates/
   mobile/*): there is no way to know who is signing in - hence which
   screens they'd get - before they actually do, so login stays one page
   for every device, simply made touch-friendly here rather than
   duplicated into a second template with its own copy of the auth JS.

   What the media query fixes is exactly what a phone needs and a mouse
   doesn't: every control at least 44px tall (the smallest reliably
   tappable target - Apple's HIG figure, echoed by WCAG 2.5.5), the
   secondary links turned from 17px-tall text into real tap rows, and less
   vertical furniture above the form so it stays above the fold on a small
   screen. Font sizes are deliberately left alone: inputs are already 16px,
   below which iOS zooms the page in on focus. */
@media (max-width: 560px) {
    .login-wrap { padding-top: 3vh; }

    .login-logo { max-width: 170px; }

    .login-card input,
    .login-card button {
        min-height: 48px;
    }

    /* Each secondary link becomes its own full-width row: at 14px of text
       they were ~17px tall and sat a thumb's width apart, which is how a
       "forgot password" tap lands on "create an account" instead. */
    .login-card .login-back-link a,
    .login-card .login-signup-hint a {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        min-height: 44px;
        padding: 0 8px;
    }

    .login-signup-hint,
    .login-back-link {
        margin-top: 4px;
    }
}

.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;
    /* No gap to the panel below - see the [id^="section-"] > .card:first-child
       flush-top treatment above, which this pairs with. */
    margin-bottom: 0;
}

.tab {
    /* Same dark navy as the topbar by default, readable on both the gray
       page background and a white card (e.g. the Résultats sub-tabs) -
       .tabs-on-white no longer needs its own override for that reason. */
    background: #0f3b47;
    color: #fff;
    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;
}

.tab.tab-active {
    /* Same cyan accent as the app's primary buttons (.races-toolbar-actions
       button, .modal-actions button...) - black label for the same reason
       those use one on this background. */
    background: #3bbbda;
    color: #000;
}

.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;
}

/* .start-time-card/.start-time-label and the .cell-datetime-time/
   .cell-datetime-date/.icon-edit colors used inside it are all defined in
   race.css (its own "Race screen header" section), which loads after this
   file - kept there rather than here since .start-time-card is
   race.html-only. */

#tab-entry.tab-active + .entry-fullscreen-toggle { background: #3bbbda; color: #000; }
#tab-entry.tab-active + .entry-fullscreen-toggle:hover { background: #2fa5c2; }

/* Fullscreen "Chronométrage" 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;
}

/* Chronométrage's own race-clocks banner (#entry-banner-wrap) only earns
   its screen space in fullscreen mode - hidden the rest of the time so the
   bib input sits right below the tab bar in the normal, windowed view.
   (Dernières arrivées has no banner of its own - see its
   .public-arrivals-race-clocks chip row instead.) This is also why
   dockEntryFullscreenToggle (event/entry.js) can safely move the fullscreen
   toggle button into it on entering fullscreen: the banner only becomes
   visible at that same moment. */
#entry-banner-wrap {
    display: none;
}

body.entry-fullscreen #entry-banner-wrap {
    display: block;
}

/* With the banner above now hidden outside fullscreen, its next sibling -
   normally .quick-entry, or the history .card directly when the operator
   lacks manage_timing_events (see event.html) - is the practical top card
   of this tab and earns the same flush-against-the-tab-bar treatment as
   [id^="section-"] > .card:first-child elsewhere (that selector alone
   can't reach it: #entry-banner-wrap being display:none doesn't remove it
   from the DOM, so it's still the actual :first-child). The adjacent-
   sibling combinator matches on document position, not visibility, so
   this works whichever of the two ends up right after the banner. */
#entry-banner-wrap + .card {
    margin-top: 0;
    border-top-left-radius: 0;
    border-top-right-radius: 0;
    border-top: 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-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;
}

/* ---------- "Historique" tab (audit trail) ----------
   The same component in an event and in the administration screen (see
   partials/audit_trail.html and app/static/audit-trail.js), so its styles
   live here in base.css rather than in event.css/one page's own file. */

.audit-toolbar {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 12px;
}

.audit-periods { display: flex; gap: 6px; flex-wrap: wrap; }

/* The selected period reads as selected the same way an active tab does -
   the app's accent color, not a second visual language for "this one". */
.audit-period.audit-period-active {
    background: #185fa5;
    color: white;
}

.audit-range {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: 14px;
    color: #666;
}

/* A datetime-local is a text input as far as the global `input` rule is
   concerned - left to itself it would grow (flex: 1) and push the toolbar's
   own buttons off the row. */
.audit-range input {
    flex: none;
    min-width: 0;
    width: auto;
    font-size: 14px;
    padding: 6px 8px;
}

.audit-toolbar-actions { display: flex; gap: 8px; }

.audit-table td { white-space: nowrap; }

/* The two columns carrying free text are the ones allowed to wrap - every
   other column is short and fixed-shape, and a history is read line by
   line, so the row height staying constant matters more than the odd long
   label fitting on one line. */
.audit-table td.audit-detail-cell,
.audit-table td:nth-last-child(2) {
    white-space: normal;
}

.audit-time {
    font-variant-numeric: tabular-nums;
    font-family: "Courier New", Courier, monospace;
}

/* A day separator rather than the date repeated on every row: on an audit
   trail the question is almost always "what happened that day". */
.audit-day-row td {
    background: #f4f4f2;
    font-weight: 600;
    font-size: 13px;
    color: #555;
    text-align: left;
    text-transform: capitalize;
}

.audit-footer-row td {
    text-align: center;
    color: #888;
}

/* One chip per AuditCategory, in the palette the status badges already
   use across the app (see .status-badge/.runner-status-badge above) -
   the history introduces no new colors of its own. */
.audit-category {
    display: inline-block;
    font-size: 12px;
    font-weight: 700;
    border-radius: 999px;
    padding: 2px 10px;
    white-space: nowrap;
}

.audit-category-user { background: #e0e7ef; color: #33517a; }
.audit-category-event { background: #ede4fb; color: #5b2a86; }
.audit-category-race { background: #fdecc8; color: #8a5a00; }
.audit-category-participant { background: #eaf3de; color: #27500a; }
.audit-category-timing { background: #dce9f7; color: #185fa5; }

.audit-detail-cell { color: #444; }

/* The whole row opens its details popup (see renderAuditTrail), so the whole
   row has to look like it does - a pointer anywhere on it, and a hover
   highlight over the striping every table here already has. */
.audit-row { cursor: pointer; }
.audit-row:hover td { background: #eaf1f8; }

/* Pure affordance: it marks where the details are, but the click is the
   row's (no handler of its own). */
.audit-details-open {
    color: #185fa5;
    font-weight: 700;
    padding: 0 4px;
}

.audit-retention-hint {
    margin: 12px 0 0;
    font-size: 12px;
    color: #888;
}

.bib-pickup-tab-hint {
    margin: 0 0 12px;
    font-size: 13px;
    color: #666;
}

/* The full before/after popup of one entry - a plain label/value list, not
   a data table: no header row, no sorting, no scroll container. */
.audit-details-table { width: 100%; }

.audit-details-table th {
    text-align: left;
    background: none;
    color: #666;
    font-size: 13px;
    font-weight: 600;
    border-radius: 0;
    width: 40%;
    white-space: normal;
}

.audit-details-table td {
    text-align: left;
    white-space: normal;
    word-break: break-word;
}

@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; }
    .rd-columns { grid-template-columns: 1fr; }
}
