:root {
  /* timing.kiwi brand palette (see brandkit/timing-kiwi-brand-pack.html) - Track Green for large
     UI fills (buttons, active states, the selected-athlete tint), Deep Green for text/links on
     the light infobar/panel surfaces (the accessible substitute for Track Green per the brand
     pack's own contrast table). */
  --accent: #3fae2c;
  --accent-dark: #2e7d22;
  --ink: #0c1210;
  /* Brand pack's own on-light secondary/muted color (matches admin's --pico-muted-color in
     views.ts) - was a leftover generic gray (#6b7280) from before the rebrand. */
  --muted: #5c6b64;
  --panel-bg: rgba(244, 247, 245, 0.96);
  --border: rgba(0, 0, 0, 0.08);
  /* Athlete map marker colors - black by default, timing.kiwi's lime Kiwi Green once favourited
     (see .athlete-marker below and the favourite star in the sidebar list, spectate.js
     toggleFavorite). Kept separate from --accent (which still drives general UI chrome - buttons,
     focus rings, the course-detail highlight) so favouriting/marker color choices don't bleed
     into the rest of the theme. -black is just --ink again under its marker-specific name (kept
     as its own variable, not a bare var(--ink) reference at each use site, so it reads as "the
     marker's default color" rather than an unexplained reuse of the tooltip's black); -black-dark
     is a plain #000 for the selected state, same "darker" role the green pair below already had -
     there's no brand-named "darker than Night" color to reach for instead. */
  --marker-black: var(--ink);
  --marker-black-dark: #000;
  /* Kiwi Green (bright/lime) for favourited, Track Green for favourited-and-selected - the
     brand's two lightest greens, reserved for markers so they read as a distinct "favourited"
     signal against the plain-black default rather than matching --accent/--accent-dark (which
     are Track Green/Deep Green, already used elsewhere for general UI chrome). */
  --marker-green: #7cfc5a;
  --marker-green-dark: #3fae2c;
  /* Rendered height of .infobar - kept as one variable so .panel's top offset (needs to clear it
     on desktop) can reference it instead of a second hardcoded number that could quietly drift
     out of sync. The value here is only the starting estimate for the common one/two-line bar
     (12px padding top/bottom + ~1 or 2 lines of text): spectate.js measures the bar for real
     once it's laid out and overwrites this on the root element, because a long event name wraps
     the bar to three lines or more on a narrow screen and anything still offsetting from 68px
     would end up underneath it. */
  --infobar-height: 68px;
  /* Approximate rendered height of .timeline-bar - .panel's bottom offset references this (via
     body.has-timeline) the same way it already references --infobar-height for its top offset,
     rather than a second hardcoded number that could quietly drift out of sync. */
  --timeline-height: 56px;
  /* Approximate rendered height of .map-controls (bottom-left pan/tilt buttons) - .follow-status
     references this so the pan/tilt cluster never sits on top of it on the follow page, same
     pattern as --infobar-height/--timeline-height above. */
  --map-controls-height: 40px;
}

* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  height: 100%;
  color: var(--ink);
  font-family: "Chakra Petch", "Segoe UI", system-ui, sans-serif;
}

/* timing.kiwi mark, leading the infobar on both the event map and follow pages (see spectate.ts).
   Track Green (--accent), not Deep Green (--accent-dark) or Kiwi Green - .infobar's background
   is the light Chalk-tinted --panel-bg, and per the brand pack's colour-role table the *mark* on
   light surfaces is specifically Track Green ("large graphic elements"); Deep Green's role is
   small text/UI, and the lighter Kiwi Green is flagged unreadable on light backgrounds entirely
   ("Kiwi Green on white: Never use"). */
.brand-lockup {
  flex: none;
  display: flex;
  align-items: center;
  gap: 6px;
  color: var(--accent);
  /* Now an <a href="https://timing.kiwi">, not a plain <div> - undo the browser's default link
     underline/visited-color so it still reads as the brand mark, not a text link. */
  text-decoration: none;
}

/* The "timing.kiwi" wordmark text itself, set exactly per the brand pack: SemiBold, +0.02em
   tracking, and the two-tone split (Night, the page's own --ink, for "timing"; muted for
   ".kiwi") - not just the mark icon, so Chakra Petch actually shows up as *text* on this page,
   not only inherited invisibly via the html/body rule above. */
.brand-lockup .wm {
  font-weight: 600;
  letter-spacing: 0.02em;
  font-size: 0.95rem;
  color: var(--ink);
}

.brand-lockup .wm .dim {
  color: var(--muted);
}

#map {
  position: absolute;
  inset: 0;
}

/* Full-width info bar - houses the results/organizer links (left), the event or athlete name
   centered in the middle regardless of how many links exist either side (both .infobar-links
   columns use flex:1, so an empty right-side spacer still balances an populated left side), and
   an anchor point for the event map's expandable course-detail dropdown. Replaces what used to
   be a small floating card pinned to the top-left corner. */
.infobar {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 20px;
  background: var(--panel-bg);
  backdrop-filter: blur(8px);
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
}

.infobar h1 {
  margin: 0;
  font-size: 1.1rem;
  font-weight: 700;
}

.topbar-sub {
  font-size: 0.8rem;
  color: var(--muted);
  margin-top: 2px;
}

/* "Demo - sample data", inline in .topbar-sub next to the event date, on the public demo event
   only (spectate.ts, driven by events.is_demo). Sized and coloured to sit *inside* that muted
   sub-line rather than compete with the event name above it: the roster it labels is invented and
   needs to say so, but this page's job is to make a good impression on a prospective client, so
   the disclosure is a quiet one. Uppercase + letter-spacing rather than a coloured badge for the
   same reason - it reads as a label, not as a warning. */
.demo-tag {
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: 0.72rem;
  font-weight: 600;
  white-space: nowrap;
}

.infobar-links {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 14px;
}

.infobar-links-right {
  justify-content: flex-end;
}

/* Wraps .layer-toggle + .map-controls (see spectate.js) - deliberately given no `position` here
   (stays the default `static`), so it isn't a containing block: each child's own
   `position: absolute` still escapes it and anchors independently on desktop, exactly as if this
   wrapper didn't exist. An empty-looking, zero-height block otherwise (both children are taken
   out of flow, so it never contributes any visible box of its own). Only becomes a real layout
   box - a single flex row at the bottom of the screen - in the mobile media query below. */
.bottom-controls {
  position: static;
}

/* Base-layer (OSM cache / Esri satellite) toggle - top-left, clearing .infobar the same way
   .panel clears it on the opposite (top-right) corner. Free space on both the event map page
   (.panel occupies the right side, not the left) and the follow page (.follow-status sits at
   the bottom-left, not the top). Moves to the bottom, alongside .map-controls, on mobile - see
   the media query below. */
.layer-toggle {
  position: absolute;
  top: calc(var(--infobar-height) + 16px);
  left: 16px;
  z-index: 15;
  background: var(--panel-bg);
  backdrop-filter: blur(8px);
  border-radius: 8px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
  padding: 3px;
}

/* Two representations of the same choice live in the DOM at once (see spectate.js) - only one is
   ever visible at a given width. .layer-toggle-buttons (the original button pair) is the default
   everywhere; .layer-toggle-select is swapped in only below the mobile breakpoint, where it only
   ever shows its one current value (~"Map" or "Satellite" wide, not both buttons at once) - which
   is what leaves enough width for .map-controls to fit on the same row there (see the mobile
   media query further down). */
.layer-toggle-select {
  display: none;
  border: none;
  border-radius: 6px;
  background: transparent;
  color: var(--ink);
  font-weight: 600;
  font-size: 0.8rem;
  font-family: inherit;
  padding: 6px 8px;
  cursor: pointer;
  outline: none;
}

.layer-toggle-buttons {
  display: flex;
  gap: 2px;
}

.layer-toggle-btn {
  flex: none;
  padding: 6px 12px;
  border: none;
  border-radius: 6px;
  background: transparent;
  color: var(--ink);
  font-weight: 600;
  font-size: 0.8rem;
  font-family: inherit;
  cursor: pointer;
}

.layer-toggle-btn:hover {
  background: rgba(0, 0, 0, 0.06);
}

.layer-toggle-btn.active {
  background: var(--accent);
  color: white;
}

.layer-toggle-btn.active:hover {
  background: var(--accent-dark);
}

/* Pan (rotate) / tilt (pitch) buttons - bottom-left on desktop, matching .layer-toggle's card
   styling. Moves to the bottom, alongside .layer-toggle, on mobile too (see the media query
   below) - both already live there on desktop, this just keeps them together at that width
   instead of splitting across corners. */
.map-controls {
  position: absolute;
  bottom: 16px;
  left: 16px;
  z-index: 15;
  display: flex;
  background: var(--panel-bg);
  backdrop-filter: blur(8px);
  border-radius: 8px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
  padding: 3px;
  gap: 2px;
}

.map-control-btn {
  flex: none;
  padding: 6px 10px;
  border: none;
  border-radius: 6px;
  background: transparent;
  color: var(--ink);
  font-weight: 600;
  font-size: 0.8rem;
  font-family: inherit;
  cursor: pointer;
}

.map-control-btn:hover {
  background: rgba(0, 0, 0, 0.06);
}

.map-control-reset {
  color: var(--muted);
}

.infobar-link {
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--accent-dark);
  text-decoration: none;
  white-space: nowrap;
}

.infobar-link:hover {
  text-decoration: underline;
}

.topbar-toggle-text {
  text-align: center;
}

.topbar-toggle {
  position: relative; /* anchors .topbar-details below, see there */
  flex: none;
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  max-width: 60vw;
}

.topbar-toggle:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 6px;
}

.topbar-chevron {
  flex: none;
  width: 0;
  height: 0;
  border-left: 5px solid transparent;
  border-right: 5px solid transparent;
  border-top: 6px solid var(--muted);
  transition: transform 0.15s ease;
}

.topbar-toggle.expanded .topbar-chevron {
  transform: rotate(180deg);
}

/* Was styled as part of the old .topbar card's own flow (a details section growing the card
   taller). Now a standalone dropdown - the toggle no longer has a card of its own to grow, since
   it lives inline in the full-width .infobar alongside the links columns. */
.topbar-details {
  position: absolute;
  top: calc(100% + 10px);
  left: 50%;
  transform: translateX(-50%);
  width: max-content;
  min-width: 220px;
  max-width: min(90vw, 360px);
  max-height: 40vh;
  overflow-y: auto;
  text-align: left;
  background: var(--panel-bg);
  backdrop-filter: blur(8px);
  border-radius: 12px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
  padding: 10px 14px;
}

.course-detail-row {
  cursor: pointer;
  border-radius: 6px;
  margin: 0 -6px;
  padding: 2px 6px;
}

.course-detail-row + .course-detail-row {
  margin-top: 8px;
  padding-top: 8px;
  border-top: 1px solid var(--border);
}

.course-detail-row:hover {
  background: rgba(0, 0, 0, 0.04);
}

.course-detail-row:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.course-detail-row.selected .course-detail-name {
  color: var(--accent-dark);
}

.course-detail-name {
  font-weight: 600;
  font-size: 0.9rem;
}

.course-detail-meta {
  font-size: 0.78rem;
  color: var(--muted);
  margin-top: 1px;
}

.course-detail-empty {
  font-size: 0.85rem;
  color: var(--muted);
}

.panel {
  position: absolute;
  top: calc(var(--infobar-height) + 16px);
  right: 16px;
  bottom: 16px;
  width: 320px;
  background: var(--panel-bg);
  backdrop-filter: blur(8px);
  border-radius: 12px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
  z-index: 10;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* Collapsed: shrink to just the toggle bar's height instead of the fixed bottom:16px anchor
   above eating a chunk of the screen - the point of collapsing at all, especially on mobile. */
.panel.collapsed {
  bottom: auto;
}

.panel-toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 12px 14px;
  cursor: pointer;
  font-weight: 700;
  font-size: 0.95rem;
  flex: none;
  border-bottom: 1px solid var(--border);
}

.panel.collapsed .panel-toggle {
  /* No body below it once collapsed - a bare toggle bar shouldn't still look divided. */
  border-bottom: none;
}

.panel-toggle:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
  border-radius: 6px;
}

.panel-chevron {
  flex: none;
  width: 0;
  height: 0;
  border-left: 5px solid transparent;
  border-right: 5px solid transparent;
  border-top: 6px solid var(--muted);
  transition: transform 0.15s ease;
}

.panel-toggle.expanded .panel-chevron {
  transform: rotate(180deg);
}

.panel-body {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  flex: 1;
  min-height: 0; /* lets .athlete-list's own overflow-y actually scroll within this flex column */
}

.panel.collapsed .panel-body {
  display: none;
}

.panel-search {
  padding: 12px;
  border-bottom: 1px solid var(--border);
}

.panel-search input,
.panel-search select {
  width: 100%;
  padding: 8px 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  font-size: 0.95rem;
  outline: none;
  font-family: inherit;
  background: white;
  color: inherit;
}

.panel-search select {
  margin-bottom: 8px;
  font-weight: 600;
}

.panel-search input:focus,
.panel-search select:focus {
  border-color: var(--accent);
}

.athlete-list {
  list-style: none;
  margin: 0;
  padding: 4px;
  overflow-y: auto;
  flex: 1;
}

.athlete-item {
  display: grid;
  grid-template-columns: auto auto 1fr;
  gap: 2px 8px;
  padding: 10px 12px;
  border-radius: 8px;
  cursor: pointer;
}

.athlete-item:hover {
  background: rgba(0, 0, 0, 0.04);
}

.athlete-item.selected {
  background: rgba(255, 90, 54, 0.12);
}

.athlete-bib {
  font-weight: 700;
  color: var(--accent-dark);
  grid-row: 1;
}

/* Favourite toggle - a plain unicode star so it needs no icon asset. Unfilled/grey by default,
   filled/green (matching .athlete-marker.favorite below) once starred, so the sidebar and the
   map marker read as the same "favourited" state at a glance. */
.athlete-star {
  grid-row: 1;
  border: none;
  background: none;
  padding: 0 2px;
  margin: 0;
  font-size: 1rem;
  line-height: 1;
  color: #c1c5cd;
  cursor: pointer;
  font-family: inherit;
}

.athlete-star:hover {
  color: var(--marker-green);
}

.athlete-star.favorited {
  color: var(--marker-green);
}

.athlete-name {
  font-weight: 600;
  grid-row: 1;
}

.athlete-status {
  grid-column: 1 / -1;
  font-size: 0.75rem;
  color: #999;
  margin-top: 2px;
}

/* A finish is a result, not a staleness note like "last seen 14:32" - so it takes the list's
   normal foreground weight/colour instead of the muted grey the live status line uses. */
.athlete-item.finished .athlete-status {
  color: var(--accent);
  font-weight: 600;
}

.athlete-empty {
  padding: 20px;
  text-align: center;
  color: var(--muted);
  font-size: 0.9rem;
}

/* Positioning shell MapLibre actually anchors to the athlete's lngLat (see markerElement in
   spectate.js) - sized purely by its .athlete-marker child since the tooltip below is taken out
   of flow, so wrapping the circle in this extra element doesn't shift where it's centered.
   Deliberately NOT setting position here: MapLibre's own maplibregl-marker class (maplibre-gl.css,
   loaded before this file) already makes this exact element `position: absolute` to do its
   lngLat->pixel transform math, and that's already a valid containing block for the absolutely-
   positioned tooltip below. Redeclaring position (even to the same-looking `relative`) here would
   win the cascade (same 0,0,1,0 specificity, later file) and silently override the library's
   `absolute` with `relative` - which was tried once and offset every marker from its real
   position: `relative` makes this a normal in-flow block (full container width, stacking below
   whatever marker came before it in the DOM) instead of the shrink-to-fit, non-flow box the
   transform math assumes. */
.athlete-marker {
  background: var(--marker-black);
  color: white;
  font-weight: 700;
  font-size: 0.7rem;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 2px solid white;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
  cursor: pointer;
}

.athlete-marker-wrap.selected .athlete-marker {
  background: var(--marker-black-dark);
  width: 34px;
  height: 34px;
  font-size: 0.75rem;
}

.athlete-marker-wrap.selected {
  z-index: 5;
}

/* Starred in the sidebar (see .athlete-star above and toggleFavorite in spectate.js) - lime
   green instead of black so a favourited athlete's marker stands out on the map at a glance.
   The bib label flips from white to black here: white on either green fails contrast (~1.5:1 on
   Kiwi Green, ~2.9:1 on Track Green) and was unreadable at 0.7rem, while black clears 4.5:1 on
   both. */
.athlete-marker-wrap.favorite .athlete-marker {
  background: var(--marker-green);
  color: var(--marker-black);
}

.athlete-marker-wrap.favorite.selected .athlete-marker {
  background: var(--marker-green-dark);
}

/* Name label shown on hover (see markerElement in spectate.js) - a plain CSS tooltip rather than
   a MapLibre Popup, since it only ever needs to show static text above the marker and shouldn't
   compete with click-to-select for the same element. */
.athlete-marker-tooltip {
  position: absolute;
  bottom: 100%;
  left: 50%;
  transform: translate(-50%, -6px);
  background: var(--ink);
  color: white;
  padding: 4px 8px;
  border-radius: 6px;
  font-size: 0.72rem;
  font-weight: 600;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.12s ease;
  z-index: 10;
}

.athlete-marker-tooltip::after {
  content: "";
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  border: 5px solid transparent;
  border-top-color: var(--ink);
}

.athlete-marker-wrap:hover .athlete-marker-tooltip {
  opacity: 1;
  visibility: visible;
}

/* Placing breakdown for a finisher, shown only while their row is expanded (see finishDetailHtml
   in spectate.js). The collapsed row carries time + overall + gender place on one line; the age
   division needs its own label ("F2529") to mean anything, which is what this grid is for. */
.athlete-finish {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 2px 10px;
  margin: 6px 0 0;
  padding: 8px 10px;
  border-radius: 6px;
  background: rgba(255, 255, 255, 0.04);
  font-size: 0.75rem;
}

.athlete-finish dt {
  color: #999;
}

.athlete-finish dd {
  margin: 0;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* Shown inline within an expanded .athlete-item (see renderList in spectate.js), not in a
   separate floating map popup like an earlier version of this - keeping the toggle where the
   rest of that athlete's row already is, instead of a second UI element anchored to their
   marker, which drifted out of sync with the sidebar when open. */
.athlete-isolate-toggle {
  grid-column: 1 / -1;
  margin-top: 6px;
  display: block;
  width: 100%;
  padding: 6px 12px;
  border: none;
  border-radius: 6px;
  background: var(--accent);
  color: white;
  font-weight: 600;
  font-size: 0.78rem;
  cursor: pointer;
  font-family: inherit;
}

.athlete-isolate-toggle:hover {
  background: var(--accent-dark);
}

/* Trigger for a live event's scrub bar (timeline.js) - only rendered for a still-`live` event
   (see spectate.ts), sitting bottom-left same as .follow-status does on the follow page, clearing
   .map-controls/.bottom-controls the same way via --map-controls-height (both desktop and
   mobile - see the media query further down, which no longer moves this anywhere else). Starts
   `hidden` in the markup and is only unhidden once a real position actually arrives over the
   socket (spectate.js) - nothing to scroll back to before that. Hidden again by timeline.js once
   clicked (the bar itself takes over that screen real estate at that point - see .timeline-bar
   below), and shown again by "Go Live". */
.timeline-scroll-back {
  position: absolute;
  bottom: calc(16px + var(--map-controls-height) + 8px);
  left: 16px;
  z-index: 15;
  padding: 8px 16px;
  border: none;
  border-radius: 10px;
  background: var(--panel-bg);
  backdrop-filter: blur(8px);
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
  color: var(--ink);
  font-weight: 600;
  font-size: 0.85rem;
  font-family: inherit;
  cursor: pointer;
}

/* background-image, not background (which would replace the opaque panel-bg above outright with
   this 6% black - nearly transparent on its own, so the button visibly faded toward see-through
   on hover instead of darkening). .map-control-btn gets away with plain `background:
   rgba(0,0,0,0.06)` for the same darken effect because it's transparent by default over its own
   opaque parent (.map-controls); this button has no such parent, it's its own opaque surface, so
   the tint has to layer on top instead of replacing it - a flat two-stop gradient is the way to
   add a solid-color layer without touching the existing background-color. */
.timeline-scroll-back:hover {
  background-image: linear-gradient(rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.06));
}

/* Scrub bar for a completed/archived event (timeline.js) - starts `hidden` in the HTML and only
   revealed once a non-empty history payload actually loads, mirroring .infobar's full-width
   layout but pinned to the bottom of the viewport instead of the top.
   `:not([hidden])`, not just `.timeline-bar` - the browser's own UA stylesheet hides a `[hidden]`
   element via `display: none`, but that's the lowest-priority origin in the cascade: a plain
   `.timeline-bar { display: flex; ... }` author rule (equal or higher specificity) unconditionally
   overrides it regardless of the attribute, which is exactly what happened here originally - the
   bar rendered visible from page load, before timeline.js ever got a chance to reveal it on
   purpose. `.topbar-details` elsewhere in this file avoids the same trap by simply never setting
   its own `display` at all and letting the browser's default handle it - this element specifically
   needs `display: flex` once shown, so it's scoped to only apply when the attribute is absent. */
.timeline-bar:not([hidden]) {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 20px;
  background: var(--panel-bg);
  backdrop-filter: blur(8px);
  box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.12);
}

.timeline-play {
  flex: none;
  padding: 6px 16px;
  border: none;
  border-radius: 8px;
  background: var(--accent);
  color: white;
  font-weight: 700;
  font-size: 0.85rem;
  cursor: pointer;
  font-family: inherit;
}

.timeline-play:hover {
  background: var(--accent-dark);
}

.timeline-scrub {
  flex: 1;
  min-width: 0;
  accent-color: var(--accent);
}

.timeline-clock {
  flex: none;
  min-width: 88px;
  text-align: right;
  font-size: 0.82rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.timeline-speed {
  flex: none;
  padding: 5px 8px;
  border: 1px solid var(--border);
  border-radius: 8px;
  font-size: 0.82rem;
  font-family: inherit;
  background: white;
  color: inherit;
}

/* Only rendered for a live event (see spectate.ts) - a completed/archived event's bar has no
   "live" to return to. Styled apart from .timeline-play (same pill shape, distinct red) so it
   reads as the one control here that leaves replay mode entirely, not just another playback
   button alongside Play/speed. */
.timeline-go-live {
  flex: none;
  padding: 6px 16px;
  border: none;
  border-radius: 8px;
  background: #d64545;
  color: white;
  font-weight: 700;
  font-size: 0.85rem;
  cursor: pointer;
  font-family: inherit;
}

.timeline-go-live:hover {
  background: #b83a3a;
}

/* Only added once timeline.js confirms there's an actual non-empty history to show (see there) -
   a page whose bar stays hidden (e.g. a completed event with no recorded fixes) shouldn't leave
   an unexplained gap above the athlete panel.
   `:not(.collapsed)`, not just `.panel` - a bare `body.has-timeline .panel` selector has higher
   specificity (0,2,1: a type + two classes) than `.panel.collapsed`'s `bottom: auto` (0,2,0: two
   classes), so without this it silently won and pinned `bottom` to a fixed value even while
   collapsed - exactly the bug this comment is warning a future edit not to reintroduce. Desktop's
   whole collapse mechanism depends on `bottom: auto` (see `.panel.collapsed` below) to let the
   panel shrink to just its toggle bar's height once `.panel-body` is hidden; forcing `bottom`
   back to a fixed offset kept the two positioning anchors (top and bottom) both pinned, so the
   *div* stayed full height even though its *content* correctly collapsed - a bug confirmed
   exactly this way, not just reasoned about. */
body.has-timeline .panel:not(.collapsed) {
  bottom: calc(var(--timeline-height) + 16px);
}

/* .map-controls has no collapsed state to worry about (unlike .panel above), but the same
   full-width bottom bar problem applies - confirmed on the admin test-event preview, which is
   always in timeline mode (see testEvents.ts): without this, .timeline-bar's z-index:20 (vs.
   .map-controls' 15) sits fully on top of it, at the same bottom-left corner, and hides it
   completely rather than just visually overlapping. On mobile (media query below), .map-controls
   is a `position: static` flex item inside .bottom-controls instead, so `bottom` here has no
   effect there any more - the mobile query shifts .bottom-controls as a whole instead. */
body.has-timeline .map-controls {
  bottom: calc(var(--timeline-height) + 16px);
}

/* MapLibre's own built-in attribution control ("© OpenStreetMap contributors", etc. - required
   by the OpenTopoMap/Esri tile usage policies referenced in spectate.js's map style, not
   something this app can just remove) defaults to `compact: true` *unconditionally* (confirmed
   directly in maplibre-gl's own source, not just guessed) - every fresh page load starts with the
   full attribution text expanded regardless of viewport width, and only collapses to the small
   (i) icon after the map's first `drag` event fires. Until then it sits in the bottom-right
   corner but can span nearly the full width of the screen - confirmed directly (headless render)
   colliding with .map-controls at bottom-left, on both mobile and desktop. Applies at every
   width, unscoped, for that reason - not just inside the mobile media query below. */
.maplibregl-ctrl-bottom-left,
.maplibregl-ctrl-bottom-right {
  bottom: calc(16px + var(--map-controls-height) + 8px + var(--map-controls-height) + 8px);
}

/* Only one extra step here, not two like the rule above - .timeline-scroll-back (the second step
   above accounts for it) is always hidden again whenever the timeline bar itself is showing (see
   revealBar() in timeline.js), so the two are never both on screen at once. */
body.has-timeline .maplibregl-ctrl-bottom-left,
body.has-timeline .maplibregl-ctrl-bottom-right {
  bottom: calc(var(--timeline-height) + 16px + var(--map-controls-height) + 8px);
}

@media (max-width: 700px) {
  .timeline-bar {
    padding: 8px 12px;
    gap: 8px;
  }

  /* Not enough width alongside play/scrub/speed on a narrow viewport - the scrub handle's own
     position is already the "where in the race" indicator at this width. */
  .timeline-clock {
    display: none;
  }
}

.follow-status {
  position: absolute;
  /* Clears .map-controls (desktop) / .bottom-controls (mobile) either way - both sit at
     bottom-left and are roughly the same height, see --map-controls-height above. */
  bottom: calc(16px + var(--map-controls-height) + 8px);
  left: 16px;
  background: var(--panel-bg);
  backdrop-filter: blur(8px);
  padding: 8px 16px;
  border-radius: 10px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
  z-index: 10;
  font-size: 0.85rem;
  color: var(--muted);
}

@media (max-width: 700px) {
  /* Top sheet instead of desktop's right-hand column - drops down from under .infobar instead of
     sitting as a bottom sheet (the previous mobile layout), so it no longer competes with
     .bottom-controls for the bottom of the screen (see further down). Only `top` is pinned;
     `bottom` is left at its default `auto` (same as desktop's own collapse mechanism relies on -
     see .panel.collapsed below) so the sheet grows downward from the infobar, capped by
     max-height, rather than stretching to fill the whole screen. */
  .panel {
    top: calc(var(--infobar-height) + 16px);
    left: 16px;
    right: 16px;
    bottom: auto;
    width: auto;
    max-height: 40vh;
  }

  /* Cancels the unscoped body.has-timeline .panel:not(.collapsed) rule above (same selector,
     matching specificity, later in the cascade wins) - that rule exists to keep a *bottom*-sheet
     panel clear of the full-width bottom timeline bar, which no longer applies now that .panel
     sits at the top on mobile and the two can't overlap. */
  body.has-timeline .panel:not(.collapsed) {
    bottom: auto;
  }

  /* Two corners on one line instead of the desktop's 3-column row: the kiwi mark stays top-left,
     the event/athlete name (and its dropdown toggle, on the event map page) moves to the
     top-right - not enough width for the desktop's link-name-spacer layout without everything
     crowding/wrapping unpredictably. Results/organizer links (.infobar-links, not the
     always-empty -right spacer) wrap onto their own centered row below when present, and
     disappear entirely (:empty) when there are none rather than leaving a phantom row - the
     empty right-side spacer that balances the desktop layout is still just noise once stacked. */
  .infobar {
    flex-wrap: wrap;
    gap: 6px;
    padding: 10px 16px;
  }

  .brand-lockup {
    order: 1;
  }

  /* .topbar-toggle (event map page, with the course-detail dropdown) and the follow page's bare
     .topbar-toggle-text (no dropdown, so no wrapping .topbar-toggle at all - see spectate.ts)
     both need this, but scoped separately: a bare, unprefixed `.topbar-toggle-text` selector
     would also match the *nested* copy inside .topbar-toggle on the event map page, where it's
     already a flex item of .topbar-toggle's own internal layout - margin-left:auto there would
     shove the text away from its own chevron instead of moving the whole toggle to the corner.
     `.infobar > .topbar-toggle-text` (direct-child combinator) matches only the follow page's
     bare structure. */
  .topbar-toggle,
  .infobar > .topbar-toggle-text {
    order: 2;
    margin-left: auto;
    max-width: 60vw;
  }

  .topbar-toggle {
    justify-content: flex-end;
  }

  .infobar > .topbar-toggle-text {
    text-align: right;
  }

  .infobar-links:not(.infobar-links-right) {
    order: 3;
    flex: 1 1 100%; /* forces this onto its own line below the logo/name row, wrap or not */
    justify-content: center;
  }

  /* infobarLinksHtml() renders a genuinely empty div (not even whitespace) when there's no
     results/organizer link to show - :empty catches exactly that case, so a course with neither
     doesn't leave a blank second row. */
  .infobar-links:not(.infobar-links-right):empty {
    display: none;
  }

  .infobar-links-right {
    display: none;
  }

  /* .topbar-toggle now sits at the right edge instead of centered (above), so its dropdown
     follows it there too, instead of hanging from the infobar's horizontal center where the
     toggle used to be. */
  .topbar-details {
    left: auto;
    right: 16px;
    transform: none;
  }

  /* Swap .layer-toggle's button pair for the <select> here only - see spectate.js for why both
     exist in the DOM at once. The <select>'s whole reason to exist is fitting on the same row as
     .map-controls below: the button pair measured (headless render, getBoundingClientRect) at
     ~500px combined with .map-controls, wider than a typical ~390px phone has room for after edge
     margins, while the <select> only ever shows one value at a time and comfortably fits both. */
  .layer-toggle-buttons {
    display: none;
  }

  .layer-toggle-select {
    display: block;
  }

  /* .layer-toggle and .map-controls (rotate/tilt/reset, 5 buttons) share one flex row at the
     bottom of the screen here, instead of desktop's separate top-left/bottom-left corners.
     .follow-status/.timeline-scroll-back (bottom-left, above this cluster) keep using their base,
     non-mobile rule to clear it - .bottom-controls is close enough in height to .map-controls
     alone (--map-controls-height) that no separate mobile override is needed for those either. */
  .bottom-controls {
    position: absolute;
    bottom: 16px;
    left: 16px;
    z-index: 15;
    display: flex;
    align-items: center;
    gap: 8px;
  }

  /* Escapes .bottom-controls' flex row into normal flex items instead of the `position: absolute`
     corner anchors .layer-toggle/.map-controls use outside this media query. */
  .bottom-controls .layer-toggle,
  .bottom-controls .map-controls {
    position: static;
  }

  /* Same full-width-bottom-bar problem the unscoped body.has-timeline .map-controls rule (above,
     outside any media query) solves on desktop - but scoped to the whole row here, since
     .map-controls is a `position: static` flex item on mobile and can't answer to `bottom` on its
     own any more (see the .bottom-controls .map-controls rule just above). */
  body.has-timeline .bottom-controls {
    bottom: calc(var(--timeline-height) + 16px);
  }
}
