/* AF1 tracking — field UI. THE THEME ON TOP OF BOOTSTRAP 5.3.
 *
 * Since 2026-10-27 (AF1-17, revised) every page loads Bootstrap's CSS first
 * and this file second. Bootstrap supplies the foundation — reboot, the
 * spacing and type utilities, forms, buttons, the navbar — so layout is
 * predictable at phone widths instead of resting on "it is a narrow column".
 * This file supplies what is ours: the sun-readable palette, the 60px tap
 * floor, ink borders, and the components that are specific to this domain
 * (striped rows, the pick list, the station card).
 *
 * TWO RULES THAT KEEP IT WORKING:
 *
 *   * Theme Bootstrap through its CSS VARIABLES (--bs-*), the way af1.css does
 *     on the public site — never by out-specifying its rules. The bridge just
 *     below maps --bs-* onto our tokens, so Night re-themes Bootstrap's
 *     components for free.
 *   * Never wrap this file in @layer (CLAUDE.md gotcha #1): unlayered
 *     Bootstrap beats layered rules at ANY specificity.
 *
 * Still read outdoors, at arm's length, in November sun, in gloves — which
 * is why it is light by default and dark themes lose: a phone cannot
 * out-emit the sky. Bootstrap does not change that; the bridge enforces it.
 *
 * DISCIPLINE COLOURS ARE VALIDATED, NOT CHOSEN — same rule as the route
 * colours in af1-routes.js. Run the dataviz palette validator on the
 * ALL-PAIRS pairlist before changing one; a list interleaves disciplines, so
 * any two can end up adjacent:
 *
 *   node scripts/validate_palette.js "#0891B2,#7C3AED,#C2410C" --mode light --pairs all
 *   node scripts/validate_palette.js "#0EA5BE,#8B5CF6,#E25410" --mode dark  --pairs all
 *
 * Both pass every check. Worst all-pairs CVD separation is ΔE 15.0 against a
 * floor of 8, so the three stay distinguishable with colour-blindness.
 *
 * Gravel and moto stayed close to the website's teal and orange. RUN HAD TO
 * MOVE off the flyer's olive #a8a80e: against white it manages only 2.47:1
 * contrast, and it collapses into moto's orange at ΔE 4.9 under deuteranopia.
 * Violet is the nearest hue that clears both. Colour is never the only signal
 * anyway — every discipline mark carries its name in text beside it.
 */

:root {
  color-scheme: light;

  --ink:        #101010;   /* body text — near-black, not gray */
  --ink-2:      #3d3d3d;   /* secondary text, still 10:1 on paper */
  --paper:      #ffffff;
  --paper-2:    #f1f1ef;   /* raised row */
  --line:       #101010;   /* borders are INK, not hairlines: a 1px #ddd
                              border is invisible in sunlight */
  --line-soft:  #c8c8c4;

  --brand-ground: #01100f;  /* AF1 near-black, from the flyer */
  --brand-orange: #e07a01;

  --gravel: #0891B2;
  --run:    #7C3AED;
  --moto:   #C2410C;
  /* Ink for text sitting ON a discipline colour. White on the day cyan is
   * only 3.7:1 and fails; dark on it is 5.1:1. Measured, not guessed. */
  --on-gravel: #06100f;
  --on-run:    #ffffff;
  --on-moto:   #ffffff;
  --accent: var(--brand-ground);

  --ok:      #15803d;
  --danger:  #b3200e;
  /* Concern levels on the Alerts tab (checkins/concern.py). STATUS colours,
   * kept apart from the sport colours: they tint a SECTION, never a rider,
   * and every section names its level in words as well. Text on the tints
   * stays --ink (all above 14:1); the rule colour is the strong signal. */
  --concern-red: #b3200e;     --concern-red-bg: #fde8e6;
  --concern-orange: #b45309;  --concern-orange-bg: #fff1dc;
  --concern-green: #15803d;   --concern-green-bg: #e7f5ec;

  --font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  /* Condensed heavy uppercase reads as the site's Anton without a download. */
  --font-head: ui-sans-serif, system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;

  /* TYPE SCALE. Seven steps; nothing in this file sets a raw font size
   * except the two fluid headings and the body. It replaced nineteen
   * hand-picked values (.8, .82, .85 and .9, .95 among them) on 2026-10-27.
   * Layout spacing belongs to Bootstrap's utilities (mt-3, gap-2 …) in the
   * templates; component padding here stays tuned to the --tap floor. */
  --fs-xs: .75rem;      /* tags, event-kind badges */
  --fs-sm: .85rem;      /* labels, counts, small controls */
  --fs-md: .95rem;      /* secondary text: hints, positions, meta */
  --fs-base: 1rem;
  --fs-lg: 1.15rem;     /* big buttons, prominent values */
  --fs-xl: 1.4rem;      /* athlete name on the card, the time field */
  --fs-2xl: 2rem;       /* the bib on the card */
  --tap: 60px;             /* nothing interactive is smaller than this */
  --gutter: clamp(.85rem, .5rem + 2vw, 1.5rem);
}

/* Night: the crew call is 6am in November and it is pitch dark. Same hues,
 * stepped for a dark surface and validated separately. */
:root[data-theme="night"] {
  color-scheme: dark;
  --ink: #f4f4f2;
  --ink-2: #b9bdbc;
  --paper: #0a1413;
  --paper-2: #16211f;
  --line: #f4f4f2;
  --line-soft: #3a4745;
  --gravel: #0EA5BE;
  --run:    #8B5CF6;
  --moto:   #E25410;
  /* Every night colour is light enough that white text fails on it. */
  --on-gravel: #06100f;
  --on-run:    #06100f;
  --on-moto:   #06100f;
  --accent: #f4f4f2;
  --concern-red: #f87171;     --concern-red-bg: #3a1411;
  --concern-orange: #f59e0b;  --concern-orange-bg: #33220a;
  --concern-green: #4ade80;   --concern-green-bg: #10291a;
}

/* ---------- the Bootstrap bridge ----------
 * Bootstrap's variables, pointed at our tokens. Declared on :root AFTER
 * bootstrap.min.css, so at equal specificity this wins over Bootstrap's own
 * :root and [data-bs-theme=dark] blocks — and because the right-hand sides
 * are our tokens, the night values follow automatically. The -rgb twins are
 * what Bootstrap's rgba() expressions read, so they need literal numbers per
 * theme. data-bs-theme is still set (base.html), because Bootstrap keys a
 * few component details — checkbox ticks, select arrows — off it. */
:root {
  --bs-body-bg: var(--paper);
  --bs-body-color: var(--ink);
  --bs-body-font-family: var(--font);
  --bs-body-font-size: 1.0625rem;          /* 17px: >=16 or iOS zooms on focus */
  --bs-body-line-height: 1.4;
  --bs-emphasis-color: var(--ink);
  --bs-heading-color: inherit;
  --bs-secondary-color: var(--ink-2);
  --bs-secondary-bg: var(--paper-2);
  --bs-tertiary-bg: var(--paper-2);
  --bs-border-color: var(--line-soft);
  --bs-border-radius: 5px;
  --bs-border-radius-sm: 4px;
  --bs-border-radius-lg: 6px;
  --bs-link-decoration: underline;
  --bs-danger: var(--danger);
  --bs-success: var(--ok);
  --bs-focus-ring-color: rgba(224, 122, 1, .65);   /* brand orange */
  --bs-body-color-rgb: 16, 16, 16;
  --bs-body-bg-rgb: 255, 255, 255;
  --bs-emphasis-color-rgb: 16, 16, 16;
  --bs-link-color-rgb: 16, 16, 16;
  --bs-link-hover-color-rgb: 16, 16, 16;
  --bs-danger-rgb: 179, 32, 14;
  --bs-success-rgb: 21, 128, 61;
}
:root[data-theme="night"] {
  --bs-body-color-rgb: 244, 244, 242;
  --bs-body-bg-rgb: 10, 20, 19;
  --bs-emphasis-color-rgb: 244, 244, 242;
  --bs-link-color-rgb: 244, 244, 242;
  --bs-link-hover-color-rgb: 244, 244, 242;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font);
  font-size: 17px;              /* >=16px or iOS zooms on focus */
  line-height: 1.4;
  -webkit-text-size-adjust: 100%;
  padding-bottom: calc(2rem + env(safe-area-inset-bottom, 0px));
}

h1, h2 {
  font-family: var(--font-head);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: .01em;
  margin: 0;
}
h1 { font-size: clamp(1.5rem, 1.1rem + 2vw, 2.1rem); }
h2 { font-size: var(--fs-base); letter-spacing: .08em; color: var(--ink-2); }

a { color: inherit; }
.muted { color: var(--ink-2); }
.hint  { color: var(--ink-2); font-size: var(--fs-md); margin: .75rem 0 0; }
.error { color: var(--danger); font-weight: 700; margin: .75rem 0 0; }
.stack > * + * { margin-top: 1rem; }

/* ---------- header: a Bootstrap navbar, themed ----------
 * navbar-expand-md: below 768px the links collapse behind Menu and drop onto
 * their own full-width rows (pushing the page down, not overlaying it); at
 * md and up they sit inline. The toggle is nine lines in base.html, not
 * Bootstrap's JS.
 *
 * Why it needed doing at all: an organizer carries five controls here
 * against a volunteer's three, and the original bar had no wrap and no media
 * query, so on a phone it overflowed off the right edge (AF1-17). */
.af1-bar {
  --bs-navbar-padding-y: .4rem;
  --bs-navbar-color: rgba(255, 255, 255, .92);
  --bs-navbar-hover-color: #fff;
  --bs-navbar-active-color: #fff;
  --bs-navbar-brand-color: #fff;
  --bs-navbar-brand-hover-color: #fff;
  --bs-navbar-toggler-border-color: rgba(255, 255, 255, .55);
  --bs-navbar-toggler-focus-width: .2rem;
  background: var(--brand-ground);
  border-bottom: 3px solid var(--brand-orange);
  padding-top: calc(.4rem + env(safe-area-inset-top, 0px));
  /* Above Leaflet's CONTROLS, which is the part that actually bites. Its
   * tile and marker panes are contained by the transform Leaflet puts on
   * `.leaflet-map-pane`, but the zoom buttons and the attribution are
   * siblings outside it at z-index 800-1000, and `.leaflet-container`
   * creates no stacking context to hold them. At an old value of 20 they
   * painted straight over this bar once the map scrolled under it —
   * verified with elementFromPoint. Bootstrap's sticky-top is 1020, which
   * would do; this stays explicit so nobody lowers it by accident. */
  z-index: 1100;
}
.af1-bar__inner { padding-inline: var(--gutter); }
.af1-bar__brand {
  display: inline-flex; align-items: center; gap: .5rem; margin-right: 0;
  font-family: var(--font-head); font-weight: 800; font-size: var(--fs-base);
  letter-spacing: .06em; text-transform: uppercase;
}
/* The wordmark is type, not an image. The site's logo JPEG is 23.7KB for a
 * 28px square — not worth it on a checkpoint's data connection. */
.af1-bar__mark {
  background: var(--brand-orange); color: var(--brand-ground);
  padding: .15rem .4rem; border-radius: 3px; font-size: var(--fs-md);
}
/* Night and Menu: always visible, 44px, readable at a glance. */
.af1-bar__ctl {
  min-height: 44px; padding: .3rem .75rem;
  font-family: var(--font-head); font-weight: 700; font-size: var(--fs-sm);
  letter-spacing: .05em; text-transform: uppercase;
  color: #fff; border: 2px solid rgba(255, 255, 255, .55); border-radius: 4px;
}
.af1-bar .btn-outline-light.af1-bar__ctl { --bs-btn-hover-bg: rgba(255, 255, 255, .16); --bs-btn-hover-color: #fff; }
.af1-bar .navbar-toggler[aria-expanded="true"] { background: rgba(255, 255, 255, .16); }
.af1-bar .navbar-toggler:focus { box-shadow: 0 0 0 .2rem var(--brand-orange); }
/* The narrowest phones (320px) cannot fit the wordmark and both controls at
 * full padding, and the controls then wrap onto a second row of the bar.
 * Tighten them rather than lose either: Night is a field control and Menu
 * is the only way to the rest. */
@media (max-width: 359.98px) {
  .af1-bar__inner { padding-inline: .6rem; }
  .af1-bar__ctl { padding-inline: .5rem; letter-spacing: .02em; }
  .af1-bar__brand { gap: .35rem; letter-spacing: .03em; }
}

/* Collapsed: a full-width stack of --tap rows under the brand. */
.af1-bar .navbar-collapse { margin-inline: calc(var(--gutter) * -1); }
.af1-bar .nav-link {
  display: flex; align-items: center; width: 100%;
  min-height: var(--tap); padding: 0 var(--gutter);
  font-size: var(--fs-base); text-align: left;
  background: none; border: 0; border-top: 1px solid rgba(255, 255, 255, .18);
}
@media (min-width: 768px) {
  .af1-bar .navbar-collapse { margin-inline: 0; }
  .af1-bar .nav-link {
    min-height: 40px; padding: 0 .55rem; width: auto;
    font-size: var(--fs-md); border-top: 0; text-decoration: underline;
  }
}

main { padding: 1rem var(--gutter) 0; max-width: 44rem; margin-inline: auto; }

/* ---------- alert ----------
 * A Bootstrap .alert, made loud: full width, solid danger, white type. The
 * stock alert-danger is a pale tint, which disappears in direct sun. */
.af1-alert {
  --bs-alert-bg: var(--danger);
  --bs-alert-color: #fff;
  --bs-alert-border-color: var(--danger);
  --bs-alert-margin-bottom: 0;
  --bs-alert-border-radius: 0;
  padding: calc(.9rem + env(safe-area-inset-top, 0px)) var(--gutter) .9rem;
  font-weight: 800; text-transform: uppercase; letter-spacing: .03em;
  text-align: center;
}

/* ---------- buttons: Bootstrap .btn, three variants ----------
 * EVERY button in the app is a Bootstrap .btn with one of these, so they
 * share one focus ring, one disabled state and one set of sizes. The look is
 * ours — heavy uppercase, 3px ink borders — set through --bs-btn-* so that
 * Bootstrap's own hover/active/disabled machinery keeps working.
 *
 * Deliberately MONOCHROME for IN/OUT/THROUGH: colour there would be a second
 * categorical scale fighting the discipline one, and OUT's natural amber sits
 * right on top of moto's orange. Fill vs outline vs dashed separates them,
 * which also survives glare and colour-blindness. */
.btn {
  --bs-btn-font-family: var(--font-head);
  --bs-btn-font-weight: 800;
  --bs-btn-border-width: 3px;
  --bs-btn-border-radius: 5px;
  --bs-btn-disabled-opacity: .45;
  --bs-btn-focus-box-shadow: 0 0 0 .25rem var(--bs-focus-ring-color);
  letter-spacing: .05em; text-transform: uppercase;
}
.btn-ink {
  --bs-btn-color: var(--paper); --bs-btn-bg: var(--line); --bs-btn-border-color: var(--line);
  --bs-btn-hover-color: var(--paper); --bs-btn-hover-bg: var(--ink-2); --bs-btn-hover-border-color: var(--line);
  --bs-btn-active-color: var(--paper); --bs-btn-active-bg: var(--ink-2); --bs-btn-active-border-color: var(--line);
  --bs-btn-disabled-color: var(--paper); --bs-btn-disabled-bg: var(--line); --bs-btn-disabled-border-color: var(--line);
}
.btn-outline-ink {
  --bs-btn-color: var(--ink); --bs-btn-bg: var(--paper); --bs-btn-border-color: var(--line);
  --bs-btn-hover-color: var(--ink); --bs-btn-hover-bg: var(--paper-2); --bs-btn-hover-border-color: var(--line);
  /* .active is "selected" — the action-first IN/OUT/THROUGH switch uses it. */
  --bs-btn-active-color: var(--paper); --bs-btn-active-bg: var(--line); --bs-btn-active-border-color: var(--line);
  --bs-btn-disabled-color: var(--ink); --bs-btn-disabled-bg: var(--paper); --bs-btn-disabled-border-color: var(--line);
}
/* Bootstrap's outline-danger is a hard-coded #dc3545; ours is the measured
 * --danger, which holds its contrast in both themes. */
.btn-outline-danger {
  --bs-btn-color: var(--danger); --bs-btn-bg: var(--paper); --bs-btn-border-color: var(--danger);
  --bs-btn-hover-color: #fff; --bs-btn-hover-bg: var(--danger); --bs-btn-hover-border-color: var(--danger);
  --bs-btn-active-color: #fff; --bs-btn-active-bg: var(--danger); --bs-btn-active-border-color: var(--danger);
  --bs-btn-disabled-color: var(--danger); --bs-btn-disabled-bg: var(--paper); --bs-btn-disabled-border-color: var(--danger);
}
/* The 60px floor. Nothing a volunteer taps all day is smaller. */
.btn-tap {
  --bs-btn-font-size: var(--fs-lg); --bs-btn-padding-x: 1rem;
  min-height: var(--tap);
  display: inline-flex; align-items: center; justify-content: center;
}
.btn-tap:active { transform: scale(.97); }
.btn-tap[disabled]:active { transform: none; }
/* Dashed reads as "passing" and keeps THROUGH from looking identical to OUT,
 * which is the mistake that would actually cost something: OUT on a rider
 * who never checked in leaves no arrival time. */
.btn-dashed { border-style: dashed; }

/* ---------- form controls: Bootstrap's, with ink borders ----------
 * A 1px grey border is invisible in glare, so 3px ink. The focus ring is the
 * brand orange, the loudest thing in the palette. */
.form-control {
  border: 3px solid var(--line); border-radius: 5px;
  background-color: var(--paper); color: var(--ink);
}
.form-control:focus {
  border-color: var(--line); background-color: var(--paper); color: var(--ink);
  box-shadow: 0 0 0 .25rem var(--bs-focus-ring-color);
}
.form-control::placeholder { color: var(--ink-2); opacity: .75; }
.form-check-input { border: 2px solid var(--line); background-color: var(--paper); }
.form-check-input:focus { border-color: var(--line); box-shadow: 0 0 0 .2rem var(--bs-focus-ring-color); }
.form-check-input:checked,
.form-check-input[type=checkbox]:indeterminate { background-color: var(--line); border-color: var(--line); }
/* At night the "ink" fill is near-white, so Bootstrap's white tick would
 * vanish into it. Same SVGs as Bootstrap's, stroked in the night paper. */
:root[data-theme="night"] .form-check-input:checked[type=checkbox] {
  --bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%230a1413' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e");
}
:root[data-theme="night"] .form-check-input[type=checkbox]:indeterminate {
  --bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%230a1413' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e");
}

/* ---------- discipline identity ----------
 * A colour bar plus a text label. Never colour alone.
 *
 * `--stripe` is set INLINE from checkins/palette.py rather than by a class
 * per discipline, because a checkpoint can serve several at once: gravel and
 * moto share every station, so most stripes are two-tone. One mechanism
 * covers one colour, two or three without a combinatorial pile of classes. */
.striped { position: relative; padding-left: calc(.9rem + 8px); }
.striped::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 8px;
  background: var(--stripe, var(--ink-2)); border-radius: 4px 0 0 4px;
}

.tag {
  display: inline-block; flex: none;
  background: var(--tag-bg, var(--ink)); color: var(--tag-ink, var(--paper));
  font-weight: 800; font-size: var(--fs-xs); letter-spacing: .09em;
  text-transform: uppercase; padding: .2rem .45rem; border-radius: 3px;
  white-space: nowrap;
}

/* ---------- station picker ---------- */
.group { margin-bottom: 1.6rem; }
.group__head {
  display: flex; align-items: center; gap: .6rem; margin-bottom: .5rem;
  padding-bottom: .35rem; position: relative;
}
.group__head::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 3px;
  background: var(--stripe, var(--ink-2));
}
.group__head h2 { color: var(--ink); font-size: var(--fs-lg); }
.group__count { margin-left: auto; color: var(--ink-2); font-size: var(--fs-sm); }

.stations { display: grid; gap: .5rem; }
.station {
  display: flex; align-items: center; gap: .6rem;
  min-height: var(--tap); padding: .7rem .9rem;
  background: var(--paper); color: var(--ink); text-decoration: none;
  border: 2px solid var(--line-soft); border-radius: 5px;
}
.station:hover, .station:focus-visible { background: var(--paper-2); border-color: var(--line); }
.station.is-current { border-color: var(--line); background: var(--paper-2); }
.station__name { font-weight: 700; }
.station__mi { margin-left: auto; color: var(--ink-2); font-size: var(--fs-sm); white-space: nowrap; }

/* ---------- station header ---------- */
/* Two rows: tag and the way out on top, the name below with room to wrap. */
.head { margin-bottom: 1rem; }
.change { font-size: var(--fs-md); white-space: nowrap; }

/* ---------- bib entry ---------- */
/* Used together with Bootstrap's .form-label / .form-control. */
.label { display: block; font-weight: 700; font-size: var(--fs-sm); text-transform: uppercase; letter-spacing: .06em; margin-bottom: .3rem; }
.bib, .search { padding: .5rem .7rem; font-family: var(--font-head); }
.bib { font-size: clamp(2rem, 1.2rem + 4vw, 3rem); font-weight: 800; letter-spacing: .06em; }
.search { font-size: var(--fs-lg); font-weight: 600; }

/* ---------- race control (AF1-16 H4) ----------
 * Organizer-only, on the athlete page. These are consequential — a DNS
 * takes someone off the map race control is using to find people — so they
 * are outlined rather than solid, sized for a deliberate press rather than
 * a quick one, and nowhere near the station screen's IN/OUT/DONE. */
.controls__row { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: .5rem; }
.controls__row form { margin: 0; flex: 1 1 8rem; }

/* ---------- backdated entry (AF1-16 H2) ----------
 * The paper fallback: a station that lost signal types its entries in
 * afterwards. Empty means now.
 *
 * WHEN THIS IS SET IT MUST BE IMPOSSIBLE TO MISS. The failure mode is a
 * volunteer who backdates a run of catch-up entries, forgets, and then logs
 * a live rider at the old time — so the active state is a solid orange block
 * rather than a tint, readable at arm's length in direct sun. Orange ink on
 * white would be the obvious choice and is unreadable out there; the AF1
 * wordmark's inversion (dark ink on the orange) is what works, and it is
 * already the loudest thing in this design system. */
.backdate { margin-bottom: 1rem; }
.backdate__row { display: flex; align-items: stretch; gap: .5rem; }
.timefield {
  flex: 1 1 auto; min-width: 0; min-height: var(--tap);
  padding: .4rem .6rem;
  font-family: var(--font-head); font-size: var(--fs-xl); font-weight: 800;
  letter-spacing: .04em;
}
.btn-now { flex: none; }
.backdate .hint { margin-top: .35rem; }

.backdate.is-backdating {
  background: var(--brand-orange);
  border-radius: 6px;
  padding: .6rem .7rem;
  margin-inline: calc(var(--gutter) * -1 + .4rem);
}
.backdate.is-backdating .label,
.backdate.is-backdating .hint {
  color: var(--brand-ground); font-weight: 800;
}
.backdate.is-backdating .timefield,
.backdate.is-backdating .btn-now { border-color: var(--brand-ground); }

/* Night keeps the same orange block — it is the alarm colour in both themes,
 * and dark-on-orange stays legible against a dark page. */
:root[data-theme="night"] .backdate.is-backdating { background: var(--brand-orange); }

/* ---------- confirmation card ---------- */
.card {
  margin-top: .9rem; padding: .85rem .95rem;
  background: var(--paper); border: 3px solid var(--line); border-radius: 5px;
}
.card__top { display: flex; align-items: baseline; gap: .6rem; flex-wrap: wrap; }
.card__bib { font-family: var(--font-head); font-weight: 800; font-size: var(--fs-2xl); line-height: 1; }
.card__name { font-weight: 800; font-size: var(--fs-xl); text-transform: uppercase; letter-spacing: .01em; margin: .25rem 0 0; }
.card__meta { color: var(--ink-2); font-size: var(--fs-md); margin: .15rem 0 0; }
.card__pos { font-weight: 700; margin: .15rem 0 0; }

/* ---------- IN / OUT / DONE ----------
 * Deliberately MONOCHROME. Colour on this row would be a second categorical
 * scale fighting the discipline one — and OUT's natural amber sits right on
 * top of moto's orange. Fill vs outline separates them instead, which also
 * survives glare and colour-blindness. */
.card__from { margin: .2rem 0 0; color: var(--ink-2); font-size: var(--fs-md); }
.card__from--off { color: var(--danger); font-weight: 700; }

/* auto-fit rather than a fixed 3: the card offers IN/OUT/THROUGH away from
 * the finish and IN/OUT/DONE at it, and a fourth would silently squash. */
.actions {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(5.5rem, 1fr));
  gap: .5rem; margin-top: .9rem;
}
/* Three 60px buttons share a phone's width here, ~107px each at 375px.
 * Bootstrap's 1rem side padding left "THROUGH" spilling past its own
 * border — invisible to an overflow check, because the PAGE does not
 * overflow, only the label inside the button. */
.actions .btn { --bs-btn-padding-x: .35rem; --bs-btn-font-size: var(--fs-base); letter-spacing: .03em; }
/* The buttons themselves are .btn .btn-tap with -ink / -outline-ink /
 * -dashed; see the button system near the top. */

/* ---------- finishing someone out on the course ----------
 * Deliberate friction. This ends a rider's day at a checkpoint, which is
 * not something to do between two taps while clearing a queue — so it sits
 * apart from the row above, in danger red, and the button is dead until a
 * reason is typed. At the finish line this panel is not rendered at all;
 * DONE is routine there and joins the primary row. See _actions(). */
.finish-here {
  margin-top: 1.1rem; padding: .75rem .8rem;
  border: 3px solid var(--danger); border-radius: 5px; background: var(--paper);
}
.finish-here__head {
  margin: 0; color: var(--danger);
  font-family: var(--font-head); font-weight: 800; font-size: var(--fs-md);
  text-transform: uppercase; letter-spacing: .06em;
}
.finish-here__why { margin: .3rem 0 .5rem; color: var(--ink-2); font-size: var(--fs-sm); }
.finish-here__note { min-height: var(--tap); }

/* ---------- name-match fallback ---------- */
.matches { display: grid; gap: .4rem; margin-top: .5rem; }
.match {
  display: flex; align-items: center; gap: .5rem; text-align: left;
  min-height: var(--tap); padding: .5rem .8rem; cursor: pointer;
  background: var(--paper); color: var(--ink);
  border: 2px solid var(--line-soft); border-radius: 5px; font: inherit;
}
.match:hover { background: var(--paper-2); }

/* ---------- lists ---------- */
.list { list-style: none; margin: 0; padding: 0; display: grid; gap: .35rem; }
.item {
  display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
  padding: .5rem .7rem; border-radius: 4px;
  background: var(--paper); border: 2px solid var(--line-soft);
}
.item.is-void { opacity: .55; }
.item.is-void .item__name, .item.is-void .item__bib { text-decoration: line-through; }
.item__kind {
  font-family: var(--font-head); font-weight: 800; font-size: var(--fs-xs);
  letter-spacing: .06em; padding: .15rem .4rem; border-radius: 3px;
  border: 2px solid var(--line); flex: none;
}
.item__kind--in   { background: var(--line); color: var(--paper); }
.item__kind--done { background: var(--line); color: var(--paper); }
.item__bib  { font-family: var(--font-head); font-weight: 800; }
.item__name { flex: 1 1 7rem; font-weight: 600; }
.item__link { display: flex; align-items: center; gap: .5rem; flex: 1 1 9rem; text-decoration: none; color: inherit; }
.item__time { color: var(--ink-2); font-variant-numeric: tabular-nums; font-size: var(--fs-md); }
.item__pos  { flex-basis: 100%; color: var(--ink-2); font-size: var(--fs-md); }
/* Small and quiet on purpose — a correction, not an action. A .btn
 * .btn-sm .btn-outline-ink, softened, that turns danger-red on hover. */
.undo {
  --bs-btn-border-width: 2px; --bs-btn-border-color: var(--line-soft);
  --bs-btn-hover-bg: var(--paper); --bs-btn-hover-color: var(--danger);
  --bs-btn-hover-border-color: var(--danger);
  --bs-btn-font-family: var(--font); --bs-btn-font-weight: 700; --bs-btn-font-size: var(--fs-sm);
  text-transform: none; letter-spacing: 0; min-height: 38px;
}

/* ---------- athlete detail ---------- */
.pii {
  padding: .8rem .95rem; border-radius: 5px;
  background: var(--paper); border: 3px solid var(--danger);
}
.pii h2 { color: var(--danger); }
.pii a { font-weight: 800; font-size: var(--fs-lg); }

/* ---------- forms ---------- */
/* The sign-in block: narrow, and centred by mx-auto in the template. */
.signin { max-width: 21rem; }
/* Django's password-rules list under the new-password box. */
.pw-rules ul { margin: .35rem 0 0; padding-left: 1.1rem; }
.pw-rules li { margin-bottom: .15rem; }

/* ---------- dashboard (phase 2) ----------
 * Unlike the check-in screens this one is for a dispatcher on a laptop, so it
 * may be wider and heavier: it pulls Leaflet and all nine GPX tracks. Keep
 * that asymmetry deliberate — do not let these styles leak onto the station
 * screens, which are budgeted at ~23 KB.
 *
 * LAYOUT. The map is the instrument; everything else is chrome around it. So
 * this page leaves the 44rem `main` column (which was silently clamping the
 * map to a third of a laptop screen) and fills the viewport instead: a fixed
 * left rail, the map taking the rest, and no page scroll — the two panels in
 * the rail scroll inside themselves so a new check-in can never push the map
 * out of view. Everything below is gated on `body.is-dash`, set by
 * dashboard.html, so the station screens keep their normal document flow. */

body.is-dash {
  height: 100dvh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  padding-bottom: 0;        /* the panels own their own safe-area inset */
}
body.is-dash main {
  flex: 1 1 auto;
  min-height: 0;            /* without this the grid refuses to shrink */
  max-width: none;
  /* `main` is centred with margin-inline:auto, which inside this flex column
     makes it shrink to its content. The Map view only ever filled the screen
     because the banner's long sentence forced it to; a Stations table did
     not, so the page width followed whatever was open. Say it outright. */
  width: 100%;
  padding: .75rem var(--gutter) 0;
  display: flex;
}

.dash { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; width: 100%; }
.dash__head { flex: none; }

/* The race at a glance: "125 ON COURSE | 19 DONE | Updated 22:05:26 | Now
 * 22:05:31". A <dl>, so each number is announced with its label. The two
 * counts are large with the number before its label; the two times are
 * small; vertical rules between. One row on a laptop. On a phone it is a
 * 2x2 grid instead (below) — left to wrap as a flex row, a wrapped item
 * began its new line with its own separator. */
.dash__stats { display: flex; flex-wrap: wrap; align-items: baseline; row-gap: .25rem; margin: 0; }
.dash__stat {
  display: flex; align-items: baseline; gap: .4rem;
  padding: 0 1rem; border-left: 2px solid var(--line-soft);
}
.dash__stat:first-child { padding-left: 0; border-left: 0; }
.dash__stat dt {
  font-size: var(--fs-sm); font-weight: 700; color: var(--ink-2);
  text-transform: uppercase; letter-spacing: .06em; white-space: nowrap;
}
.dash__stat dd { margin: 0; font-variant-numeric: tabular-nums; font-size: var(--fs-sm); color: var(--ink-2); }
.dash__stat--big dd {
  order: -1;                       /* the number reads first: "125 on course" */
  font-family: var(--font-head); font-weight: 800; line-height: 1;
  font-size: var(--fs-2xl); color: var(--ink);
}
.dash__stat--big dt { color: var(--ink); }
/* A poll that has not landed for a while turns the stamp red, so a frozen
 * map is never mistaken for a quiet one. */
.dash__stat.is-stale dd, .dash__stat.is-stale dt { color: var(--danger); font-weight: 800; }
@media (max-width: 47.98rem) {
  .dash__stats { display: grid; grid-template-columns: auto 1fr; }
  .dash__stat:nth-child(odd) { padding-left: 0; border-left: 0; }
  .dash__stat:nth-child(even) { padding-left: 1rem; border-left: 2px solid var(--line-soft); }
}
/* 320px phones: at full size "106 ON COURSE | 19 DONE" overran by 6px, and a
 * four-digit field would overrun more. One step down the type scale. */
@media (max-width: 359.98px) {
  .dash__stat--big dd { font-size: var(--fs-xl); }
  .dash__stat:nth-child(even) { padding-left: .7rem; }
  /* Five items since Stopped joined (2026-10-31): "Updated 08:43:39" in the
     right column pushed the grid 15px past a 320px screen. Equal columns,
     and a label may drop under its number. */
  .dash__stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .dash__stat { flex-wrap: wrap; row-gap: 0; }
}

/* 25 / 75. The rail has a floor so the filters never wrap, and a ceiling so
 * the map does not lose width on an ultrawide. */
.dash__body {
  flex: 1 1 auto; min-height: 0;
  display: grid;
  grid-template-columns: clamp(17rem, 25%, 24rem) 1fr;
  gap: .85rem;
  padding: .6rem 0 calc(.6rem + env(safe-area-inset-bottom, 0px));
}

.dash__side {
  /* min-width:0 is NOT decoration. A grid item's automatic minimum size is
   * its min-content width, so without this the panels below size the rail's
   * track to 427px inside a 384px column and slide under the map. The same
   * reason min-height:0 is here. */
  min-height: 0; min-width: 0;
  display: grid;
  /* Filters take what they need; the feed takes the rest and scrolls. */
  grid-template-rows: auto minmax(8rem, 1fr);
  gap: .85rem;
}
.dash__panel {
  min-height: 0; min-width: 0; display: flex; flex-direction: column;
  border: 2px solid var(--line-soft); border-radius: 6px;
  background: var(--paper);
}
.dash__panel--filters { overflow: auto; }
.panel__head {
  display: flex; align-items: center; gap: .4rem .6rem; flex: none; flex-wrap: wrap;
  padding: .5rem .7rem; border-bottom: 2px solid var(--line-soft);
  position: sticky; top: 0; background: var(--paper); z-index: 2;
}
.panel__head h2 { color: var(--ink); font-size: var(--fs-md); }
.panel__scroll { overflow: auto; padding: .5rem .7rem; min-height: 0; }

/* Reset filter is a .btn .btn-sm .btn-ink: small, and dimmed by Bootstrap's
 * disabled state until there is something to reset. Not --tap sized — this
 * is the laptop screen, not a glove. */

.dash__filters { display: grid; gap: .6rem; padding: .6rem .7rem; }
.filt {
  border: 2px solid var(--line-soft); border-radius: 6px;
  padding: .3rem .6rem .45rem; margin: 0; min-width: 0;
}
/* Bootstrap's reboot floats <legend> and sets it at heading size; here it
 * only holds the discipline's select-all checkbox. */
.filt legend { float: none; width: auto; margin: 0; padding: 0 .3rem; font-size: inherit; line-height: inherit; }
.filt__all { display: inline-flex; align-items: center; gap: .4rem; cursor: pointer; }
.filt__course {
  display: flex; align-items: center; gap: .45rem;
  padding: .2rem 0; cursor: pointer; font-size: var(--fs-md);
}
.filt__swatch { width: 14px; height: 14px; border-radius: 3px; border: 1px solid var(--line); flex: none; }
.filt__count { margin-left: auto; color: var(--ink-2); font-variant-numeric: tabular-nums; font-size: var(--fs-sm); }
.filt--find { display: grid; align-content: start; gap: .3rem; border: 0; padding: 0; position: relative; }
.filt--find .search { font-size: var(--fs-base); padding: .4rem .6rem; border-width: 2px; width: 100%; }
.filt--find .hint { margin: 0; font-size: var(--fs-sm); }

/* Type-ahead. Overlays rather than pushing the fieldsets down, so the list
 * does not reflow the rail on every keystroke. */
.suggest {
  list-style: none; margin: 0; padding: 0;
  position: absolute; top: 100%; left: 0; right: 0; z-index: 30;
  background: var(--paper); border: 2px solid var(--line); border-radius: 5px;
  max-height: 15rem; overflow: auto;
}
.suggest li + li { border-top: 1px solid var(--line-soft); }
.suggest button {
  display: flex; align-items: center; gap: .5rem; width: 100%;
  background: none; border: 0; font: inherit; text-align: left;
  padding: .45rem .6rem; cursor: pointer; color: inherit;
}
.suggest button:hover, .suggest .is-active button { background: var(--paper-2); }
.suggest__bib { font-family: var(--font-head); font-weight: 800; flex: none; }
.suggest__name { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.suggest__course { color: var(--ink-2); font-size: var(--fs-sm); flex: none; }
.suggest__none { padding: .45rem .6rem; color: var(--ink-2); font-size: var(--fs-md); }

/* The selected athlete, shown as a dismissable chip so the filter state is
 * never implicit in the contents of a text box. */
.chip {
  display: flex; align-items: center; gap: .4rem;
  border: 2px solid var(--line); border-radius: 5px;
  padding: .25rem .25rem .25rem .5rem; font-size: var(--fs-md);
}
.chip__bib { font-family: var(--font-head); font-weight: 800; }
.chip__x { margin-left: auto; flex: none; line-height: 1; }

/* The feed lives in a ~22rem rail, not across a full phone screen, so its
 * rows stay on one line and elide instead of wrapping the timestamp onto a
 * line of its own. Scoped to the dashboard: .item still wraps on the station
 * screens, which is right at 390px. */
/* Third instance of the same trap in this file: `.list` is a grid, so each
 * .item's automatic minimum size is its min-content width and it refuses to
 * shrink into the rail — the time scrolled off the right edge. The ellipsis
 * rules below only get a chance once min-width is 0. */
.dash__panel--feed .list { gap: .3rem; min-width: 0; }
.dash__panel--feed .item { flex-wrap: nowrap; gap: .4rem; padding: .4rem .55rem; min-width: 0; }
.dash__panel--feed .item__name {
  flex: 1 1 5rem; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dash__panel--feed .item .muted {
  flex: 0 1 auto; min-width: 0; font-size: var(--fs-sm);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dash__panel--feed .item__time { flex: none; }
/* A row that carries a note (a DONE reason, a DNF) lets the note take a line
 * of its own; on one line it squeezed the athlete's name down to an initial.
 * :has() is fine here — the dashboard is a laptop screen. */
.dash__panel--feed .item:has(.item__pos) { flex-wrap: wrap; }

.dash__main { min-height: 0; min-width: 0; display: flex; flex-direction: column; gap: .5rem; }
.dash__map {
  flex: 1 1 auto; min-height: 0;
  border: 3px solid var(--line); border-radius: 6px;
  background: var(--paper-2);
}
.banner {
  flex: none; margin: 0; padding: .45rem .7rem;
  border: 2px dashed var(--line-soft); border-radius: 5px;
  color: var(--ink-2); font-size: var(--fs-sm);
}
.dash__main .error { flex: none; margin: 0; }

/* TABS (Phase 3 Stage A). One fixed row above the views, costing the map
 * about 2.5rem of height — the only visible change to the Map view, which
 * is otherwise moved in untouched. Bootstrap's .nav-tabs supplies the look;
 * no Bootstrap JS (dashboard-tabs.js). [hidden] panels are display:none via
 * Bootstrap's reboot, which is what lets the map stay built while away. */
.dash__tabs { flex: none; align-items: center; gap: .25rem; margin-bottom: .5rem; flex-wrap: nowrap; }
.dash__tabs .nav-link { font-weight: 700; padding: .35rem .85rem; white-space: nowrap; color: var(--ink); }
.dash__tabs .nav-link.active { color: var(--ink); border-color: var(--line-soft) var(--line-soft) var(--paper); }
.tab-badge {
  display: inline-block; min-width: 1.4em; padding: 0 .35em; margin-left: .2rem;
  border-radius: 1em; background: var(--danger); color: #fff;
  font-size: var(--fs-xs); line-height: 1.4; text-align: center;
}
.dash__tabstamp { margin: 0; font-size: var(--fs-sm); white-space: nowrap; }
.dash__tabstamp.is-stale { color: var(--danger); font-weight: 800; }

.dash__view { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
.dash__view:focus { outline: none; }
.view__scroll { flex: 1 1 auto; min-height: 0; overflow: auto; padding-bottom: 1rem; }

/* Station rows and the check-in log: tables that scroll sideways INSIDE
 * their box on a narrow screen rather than widening the page. */
.stat-table { overflow-x: auto; }
.stat-table table { --bs-table-bg: transparent; color: var(--ink); font-size: var(--fs-sm); }
.stat-table th[scope=col] { font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .05em;
                            color: var(--ink-2); vertical-align: bottom; }
.stat-table th[scope=row] { white-space: nowrap; }
.stat-table .hint, .stat-group .hint, .stat-foot .hint { margin: 0; font-size: var(--fs-sm); }
.stat-table tr.is-inactive { color: var(--ink-2); }
.stat-table tr.is-void td { text-decoration: line-through; color: var(--ink-2); }
.stat-table details summary { cursor: pointer; }
.stat-names { list-style: none; padding: 0; margin: .35rem 0 0; min-width: 18rem; }
.stat-names li + li { margin-top: .25rem; }
.stat-group { margin-bottom: 1.25rem; }
.stat-group h3 { font-size: var(--fs-md); margin: 0 0 .25rem; }
.stat-list { margin: .5rem 0 0; padding-left: 1.6rem; }
.stat-list li { margin-bottom: .4rem; }
.stat-age { font-weight: 800; font-variant-numeric: tabular-nums; }
.stat-foot { border-top: 2px solid var(--line-soft); padding-top: .5rem; }
.stat-foot p { margin: 0 0 .35rem; }
.flag {
  display: inline-block; margin-left: .3rem; padding: 0 .4em;
  border: 1.5px solid var(--ink-2); border-radius: 4px;
  font-size: var(--fs-xs); font-weight: 700; white-space: nowrap;
}
/* Experimental estimates (Stage E0). Dashed and hatched on purpose: it
 * should never look like the solid facts on the other tabs. No colour for
 * "outside the window" — there is no such state on screen. */
.exp-tag {
  margin-left: .3rem; padding: 0 .35em; border: 1.5px dashed var(--ink-2); border-radius: 4px;
  font-size: var(--fs-xs); font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
}
.est-banner {
  margin: 0 0 1rem; padding: .55rem .75rem; border: 2px dashed var(--ink-2); border-radius: 6px;
  background: repeating-linear-gradient(135deg, transparent 0 8px, var(--paper-2) 8px 16px);
}
.est-optin { display: flex; align-items: center; gap: .5rem; font-weight: 700; margin-bottom: .5rem; }
.est-window { font-variant-numeric: tabular-nums; }
.est-foot { margin-top: 1rem; }

/* WEATHER (2026-11-01). Never a concern colour and never the red badge:
 * the tab note is an outlined count, and a notice's severity is told in
 * words and border weight. Everything here is ink on paper, both themes. */
.tab-note {
  display: inline-block; min-width: 1.4em; padding: 0 .35em; margin-left: .2rem;
  border: 1.5px solid var(--ink); border-radius: 1em;
  font-size: var(--fs-xs); line-height: 1.3; text-align: center;
}
.wx-list { list-style: none; padding: 0; margin: 0; }
.wx-notice { padding: .4rem .6rem; margin-bottom: .5rem; border: 2px solid var(--line-soft); border-radius: 6px; }
.wx-sev-moderate { border-color: var(--ink-2); }
.wx-sev-severe, .wx-sev-extreme { border: 3px solid var(--ink); }
.wx-sev-extreme { border-width: 5px; }
.wx-notice p { margin: 0; }
.wx-notice__head { font-size: var(--fs-md); }
.wx-notice__area { color: var(--ink-2); font-size: var(--fs-sm); }
.wx-notice__text { white-space: pre-line; font-size: var(--fs-sm); margin-top: .35rem !important; }
.wx-notice details summary { cursor: pointer; font-size: var(--fs-sm); }
.wx-none { margin: 0; font-weight: 700; }
.wx-fail { margin: .35rem 0 0; }
.wx-step { display: flex; align-items: center; gap: .35rem; margin-bottom: .5rem; }
.wx-step .label { display: inline; margin: 0 .25rem 0 0; }
.wx-table td.wx-cell { vertical-align: top; min-width: 6.5rem; }
.wx-cell > span { display: block; line-height: 1.25; }
.wx-temp { font-weight: 800; font-variant-numeric: tabular-nums; }
.wx-pop, .wx-wind { font-variant-numeric: tabular-nums; }
.wx-short { color: var(--ink-2); font-size: var(--fs-xs); }
/* Phones: one line per hour — time | temperature | precipitation | wind —
 * with the short forecast under it, so the columns line up down a card. */
@media (max-width: 47.98rem) {
  .stat-table--cards .wx-table td.wx-cell {
    display: grid; grid-template-columns: 5rem auto auto 1fr;
    gap: 0 .45rem; align-items: baseline; justify-content: stretch;
    font-size: var(--fs-sm);
  }
  .stat-table--cards .wx-table td.wx-cell > span:not(.wx-short) { white-space: nowrap; }
  .stat-table--cards .wx-table td.wx-cell::before { white-space: nowrap; }
  .stat-table--cards .wx-table .wx-short { grid-column: 2 / -1; }
}

/* Phones: the Stations table becomes one card per station. A table that
 * scrolls sideways hides most of its columns at 375px; each value carries
 * its own label instead (data-label on the cell). */
@media (max-width: 47.98rem) {
  .stat-table--cards table, .stat-table--cards tbody,
  .stat-table--cards tr, .stat-table--cards th[scope=row], .stat-table--cards td { display: block; }
  .stat-table--cards thead { display: none; }
  .stat-table--cards tr { border: 2px solid var(--line-soft); border-radius: 6px;
                          padding: .4rem .6rem; margin-bottom: .6rem; }
  .stat-table--cards th[scope=row] { white-space: normal; border: 0; padding: 0 0 .25rem; font-size: var(--fs-md); }
  .stat-table--cards td { display: flex; gap: .5rem; justify-content: space-between;
                          border: 0; padding: .1rem 0; }
  .stat-table--cards td::before { content: attr(data-label); color: var(--ink-2); font-weight: 700; }
  /* The expandable name lists take the card's full width on their own line:
     Chrome lays out a CLOSED <details> too, and squeezed beside its label
     the list ran off the right edge of the screen. */
  .stat-table--cards td:has(> details) { flex-wrap: wrap; }
  .stat-table--cards td > details { flex: 1 1 100%; min-width: 0; }
  .stat-table--cards td > details > summary { text-align: right; margin-top: -1.5em; }
  .stat-table--cards .stat-names { min-width: 0; }
}

/* Alerts tab: one section per concern level, a table per section.
 * `.group-box` is the same frame, uncoloured, for the other tabs' groups. */
.concern, .group-box {
  --c: var(--line-soft); --c-bg: transparent;
  margin-bottom: 1rem; padding: .5rem .75rem .6rem;
  border: 2px solid var(--c); border-left-width: 8px; border-radius: 6px;
  background: var(--c-bg);
}
.concern--red    { --c: var(--concern-red);    --c-bg: var(--concern-red-bg); }
.concern--orange { --c: var(--concern-orange); --c-bg: var(--concern-orange-bg); }
.concern--green  { --c: var(--concern-green);  --c-bg: var(--concern-green-bg); }
.concern h3, .group-box h3 { display: flex; align-items: center; gap: .5rem; font-size: var(--fs-md); margin: 0 0 .35rem; }
.concern__mark { width: .9rem; height: .9rem; border-radius: 50%; background: var(--c); flex: none; }
.concern--neutral .concern__mark { display: none; }
.concern__n { margin-left: auto; font-variant-numeric: tabular-nums; }
.concern > .hint { margin: 0 0 .35rem; }
/* `.sport-table`: rows wear their SPORT colour (an edge and the tag) on
 * Alerts, Check-ins, Estimates and Stations alike. Never a route colour.
 * The edge is a pseudo-element, not a box-shadow, so a station serving two
 * sports can show both colours (--stripe is then palette.stripe()'s hard-
 * stop gradient, as on the station picker). */
.sport-table .sport { position: relative; padding-left: 14px; white-space: nowrap; }
.sport-table .sport::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 6px;
  background: var(--stripe, var(--ink-2));
}
.sport-table td.age { font-variant-numeric: tabular-nums; white-space: nowrap; }
.sport-tags { display: flex; flex-wrap: wrap; gap: .25rem; margin-top: .2rem; }
/* Stations: a station name stays at the top of its row when a name list
 * under "Heading here" is opened and the row grows. */
.sport-table th[scope=row].sport { vertical-align: top; }
.sport-table .kind-cell .item__kind { display: inline-block; }
.sport-table tr.is-void > td:not(.sport) { text-decoration: line-through; color: var(--ink-2); }
.sport-table td.num { font-variant-numeric: tabular-nums; white-space: nowrap; }
@media (max-width: 47.98rem) {
  .stat-table--cards .sport-table .sport::before { display: none; }
  .stat-table--cards .sport-table .sport { white-space: normal; }
  .stat-table--cards .sport-table tr { position: relative; background: var(--paper); padding-left: 1rem; }
  .stat-table--cards .sport-table tr::before {
    content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 6px;
    border-radius: 6px 0 0 6px; background: var(--stripe, var(--ink-2));
  }
  .stat-table--cards .sport-table .cv { display: block; text-align: right; }
  .stat-table--cards .sport-table td:empty { display: none; }
}

.ck-filters { display: flex; flex-wrap: wrap; align-items: flex-end; gap: .5rem .75rem; margin-bottom: .75rem; }
.ck-filters label { display: flex; flex-direction: column; gap: .15rem; font-size: var(--fs-sm); font-weight: 700; }
.ck-filters label.ck-check { flex-direction: row; align-items: center; gap: .35rem; }

/* Below ~62rem the two-pane console stops making sense — there is no room
 * for a rail beside a usable map. Fall back to ordinary document flow with
 * the map first, which is also what a phone gets. */
@media (max-width: 62rem) {
  body.is-dash { height: auto; overflow: visible; display: block;
                 padding-bottom: calc(2rem + env(safe-area-inset-bottom, 0px)); }
  body.is-dash main { display: block; }
  .dash, .dash__side, .dash__main { display: block; min-height: 0; }
  /* Flex column rather than plain block, purely so `order` can lift the map
     above the rail — stacked, the map must still be the first thing you see. */
  .dash__body { display: flex; flex-direction: column; min-height: 0; padding: .6rem 0; }
  .dash__main { order: -1; margin-bottom: .85rem; }
  .dash__map { height: min(60vh, 520px); }
  .dash__side > * + * { margin-top: .85rem; }
  .dash__panel--filters, .panel__scroll { overflow: visible; max-height: none; }
  .panel__head { position: static; }
  .dash__view { display: block; }
  .view__scroll { overflow: visible; }
  /* Four tabs and the stamp do not fit a phone in one row: the stamp
     drops below, and the strip itself may scroll sideways. */
  .dash__tabs { flex-wrap: wrap; gap: 0; }
  .dash__tabs .nav-link { padding: .3rem .5rem; font-size: var(--fs-sm); }
  .dash__tabstamp { flex-basis: 100%; margin: .25rem 0 0; }
}

/* Station flags on the dashboard map — inline SVG in a divIcon, because
   Leaflet ships no icon set and the default pin's PNGs are not vendored.
   The default .leaflet-div-icon box is replaced, not inherited, so these
   two rules are all the chrome there is. */
.cp-flag { background: none; border: 0; }
.cp-flag svg { display: block; filter: drop-shadow(0 1px 1.5px rgba(0, 0, 0, .5)); }

/* Leaflet draws its own light chrome; keep it legible in night mode without
   inverting the map tiles themselves. */
:root[data-theme="night"] .leaflet-control-attribution,
:root[data-theme="night"] .leaflet-control-zoom a,
:root[data-theme="night"] .leaflet-control-layers { background: #0a1413; color: #d8dedd; }

/* The Topographic / Road switcher (dashboard.js). Leaflet's default is 12px
 * text with radio buttons hard against it; this is the console's type.
 * Scoped under .dash__map because leaflet.css loads AFTER this file on the
 * dashboard, so at equal specificity Leaflet's own rules would win. */
.dash__map .leaflet-control-layers { font: inherit; font-size: var(--fs-sm); border-radius: 5px; }
.dash__map .leaflet-control-layers-expanded { padding: .35rem .6rem; }
.dash__map .leaflet-control-layers label {
  display: flex; align-items: center; gap: .35rem; margin: .1rem 0;
  font-size: inherit; cursor: pointer;
}
.dash__map .leaflet-control-layers-selector { margin: 0; top: 0; }

/* ---------- action-first station screen ----------
 * Choose IN / OUT / THROUGH, tap the athletes, confirm once. station.js
 * renders the list from data embedded in the page; nothing here needs a
 * round trip. Read outdoors at arm's length, so: big targets, ink borders,
 * a checkmark AND a fill for "selected" — never fill alone. */
.quick { margin: 0 0 1.75rem; }
.seg {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: .4rem;
}
/* The three are .btn .btn-tap .btn-outline-ink; Bootstrap's .active is the
 * selected one (station.js sets it with aria-checked). */
.seg__btn { padding-inline: .4rem; }
.seg__n {
  display: inline-block; min-width: 1.4em; margin-left: .2rem;
  font-size: var(--fs-sm); font-variant-numeric: tabular-nums; opacity: .8;
}
.seg__n:empty { display: none; }
.quick__prompt { font-weight: 700; margin: .7rem 0 .9rem; }

/* Flows with the page. It used to scroll inside itself with
 * overscroll-behavior: contain, which on a phone trapped the finger — see
 * .quick__bar. */
.pick { list-style: none; margin: .6rem 0 0; padding: 0; display: grid; gap: .35rem; }
.pick__row {
  width: 100%; display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
  min-height: var(--tap); padding: .45rem .7rem; text-align: left; cursor: pointer;
  background: var(--paper); color: var(--ink); font: inherit;
  border: 2px solid var(--line-soft); border-radius: 4px;
}
.pick__row.striped { padding-left: calc(.9rem + 8px); }
.pick__row.is-picked { border: 3px solid var(--line); background: var(--paper-2); }
.pick__row:focus-visible { outline: 4px solid var(--brand-orange); outline-offset: 1px; }
.pick__box {
  flex: none; width: 1.6rem; height: 1.6rem; display: grid; place-items: center;
  border: 3px solid var(--line); border-radius: 4px;
  font-weight: 900; line-height: 1;
}
.pick__row.is-picked .pick__box { background: var(--line); color: var(--paper); }
.pick__none { color: var(--ink-2); padding: .5rem 0; }



/* Sticky to the bottom of the viewport while the action-first section is on
 * screen: the confirm stays in thumb reach while the list scrolls under it,
 * and the "✓ recorded" notice appears where the thumb just was. Solid
 * background, or rows show through it. */
.quick__bar {
  position: sticky; bottom: 0; z-index: 5;
  margin-top: .6rem;
  padding: .6rem 0 calc(.6rem + env(safe-area-inset-bottom, 0px));
  background: var(--paper);
  border-top: 3px solid var(--line-soft);
}
.quick__notice {
  margin: .7rem 0 0; padding: .55rem .7rem; border-radius: 4px; font-weight: 700;
  border: 2px solid var(--line);
}
.quick__notice.is-ok  { border-color: var(--ok); color: var(--ok); }
.quick__notice.is-bad { border-color: var(--danger); color: var(--danger); }
.quick__notice a { color: inherit; }

.pending {
  margin-top: 1rem; padding: .7rem; border: 3px dashed var(--brand-orange); border-radius: 5px;
}
.pending__head { margin: 0 0 .5rem; font-size: var(--fs-md); }
.pend {
  display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
  padding: .45rem .6rem; border-radius: 4px; border: 2px solid var(--line-soft);
}
.pend__state { flex-basis: 100%; font-size: var(--fs-md); color: var(--ink-2); }
.pend--rejected { border-color: var(--danger); }
.pend--rejected .pend__state { color: var(--danger); font-weight: 700; }
.pend--signedout .pend__state, .pend--waiting .pend__state { font-weight: 700; }
.pending #af1-retry { margin-top: .6rem; }
.item__kind--through { border-style: dashed; }

.quick__foot { margin-top: .6rem; }
.linkish {
  background: none; border: 0; padding: 0; font: inherit; color: inherit;
  text-decoration: underline; cursor: pointer;
}
.find { padding-top: 1.25rem; border-top: 3px solid var(--line-soft); }
.find h2 { margin-bottom: .75rem; }

/* ---------- public tracking (/public/, no sign-in) ----------
 * The ordinary narrow column and the same cards and lists as the station
 * screens; only the map is new. A fixed height rather than a viewport
 * fraction, so a phone can still scroll past it to the check-ins. */
.pub-map {
  height: 22rem; border: 3px solid var(--line); border-radius: 6px;
  background: var(--paper-2);
}
@media (max-width: 30rem) { .pub-map { height: 17rem; } }
.item__kind--finish { background: var(--line); color: var(--paper); }
.public-search .search { min-height: var(--tap); }
.public-foot { margin-top: 2rem; font-size: var(--fs-sm); }
.public-preview { border-style: solid; }
