/*
 * Sora, self-hosted.
 *
 * The brand spec calls for it and it is a genuinely good interface face — large
 * x-height, open counters. Self-hosted rather than pulled from Google because a
 * pit crew is on one bar of LTE and a webfont on a third-party host is a
 * dependency between them and a legible screen. One 25 KB variable file covers
 * every weight, and `swap` means the system font draws instantly if it is slow.
 */
@font-face {
  font-family: "Sora";
  src: url("./vendor/sora-latin.woff2") format("woff2");
  font-weight: 100 800;
  font-display: swap;
}

/*
 * Read at arm's length, in the sun, by someone wearing gloves.
 *
 * The map is the page. The running order is a narrow column beside it — a crew
 * chief wants one glance that answers "where is everybody", and rider detail
 * that does not eat the map to say it. Dark ground because these phones have to
 * last a whole race day.
 */

:root {
  --bg: #0B0D0F;                    /* Carbon Black, from the brand spec */
  --panel: #131317;
  --panel-2: #1e1e24;
  --line: #2c2c34;
  --text: #f5f5f7;
  --dim: #9a9aa4;
  --good: #32d74b;
  --warn: #ffd60a;
  --bad: #ff453a;
  --close: #ff9f0a;

  /*
   * Reserved meanings. Nothing else may use these.
   *   --me      you, and only you. Google's blue, on purpose.
   *   --course  the race course AND its mile markers, so the numbers read as
   *             part of the line rather than as separate furniture.
   *   --chase   every chase rig, all the same, never a racer's colour.
   * Racer colours are assigned server-side in netlify/lib/roster.js, and none of
   * them sits near yellow, amber or blue — so a racer can never read as the
   * course, a truck or the viewer.
   */
  --me: #1a73e8;
  --course: #FF5A00;                /* Chase Orange — brand AND the course */
  --chase: #ffd60a;

  /*
   * Chase Orange, #FF5A00 — the spec's value, not the board art's #FF6A00.
   *
   * The same as --course on purpose. An earlier rule kept the brand out of the
   * map entirely; that was the wrong call. Spending the brand colour on the
   * single most prominent thing on screen is worth more than reserving it for
   * chrome, and the collision it was protecting against was solved properly
   * instead — by moving chase rigs to yellow, 29 degrees of hue away rather
   * than 15.
   */
  --brand: #FF5A00;

  --board-w: 340px;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  background: var(--bg);
  color: var(--text);
  font: 16px/1.4 "Sora", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  -webkit-text-size-adjust: 100%;
}

body {
  display: flex;
  flex-direction: column;
  height: 100svh;
  overflow: hidden;                 /* the board scrolls, the page does not */
  padding: env(safe-area-inset-top) env(safe-area-inset-right)
           env(safe-area-inset-bottom) env(safe-area-inset-left);
}

/* ------------------------------------------------------------------- top -- */

/*
 * The brand sits at the top of the board column, not in a bar of its own.
 *
 * A full-width header put a black band across the whole screen carrying a logo
 * on the far left and nothing else — the map paid for it in height and got
 * nothing back. In the column it is beside the things it belongs with.
 */
/*
 * Pinned to the top of the board, which SCROLLS.
 *
 * The board is its own scroll container, so the header used to scroll away with
 * everything else — and once it had, the way into setup was gone and the map
 * was all that was left on screen. Worse on a narrow window, where the board is
 * only half the height and the map sits directly underneath it, but it happened
 * on a laptop too the moment the roster ran past the fold.
 *
 * Sticky rather than fixed: it belongs to the board, not to the window, and on
 * a phone the board is only the top half of the page.
 */
.top {
  position: sticky;
  top: 0;
  z-index: 6;
  display: flex;
  flex-wrap: wrap;                  /* the alert takes a line of its own */
  align-items: center;
  gap: 10px;
  flex: none;
  padding: 10px 12px;
  background: var(--panel);
  border-bottom: 1px solid var(--line);
}

/* The mark, not the wordmark: the wordmark is raster and capped at 546px, and
   a header on a 4K monitor would show it soft. The icon is true vector. */
.top__mark {
  flex: none;
  height: 26px;
  width: auto;
  display: block;
}

/* Nothing else goes in this row. The race name used to sit under the wordmark
   inside the same block, which read as part of the lockup — text hanging off
   somebody's logo. The brand gets its own line and owns it. */

/*
 * The product name goes where the race name used to be, and the race name drops
 * to the sub-line.
 *
 * Costs no vertical space — same two lines as before — and answers the question
 * somebody in the next pit is going to ask out loud during MVP testing. The crew
 * already knows which race they are at; a stranger does not know what they are
 * looking at.
 */
.top__brand { margin: 0; line-height: 0; }

.top__brand img {
  height: 13px;
  width: auto;
  display: block;
}

.top__word { height: 12px; width: auto; display: block; }

/* Its own line, and allowed to WRAP. Ellipsising it produced "255..." — three
   dots promising more information behind a click that does not exist. Either
   show it or do not, but never imply. */
.race {
  flex: none;
  margin: 0;
  padding: 0 12px 10px;
  font-size: 11px;
  line-height: 1.45;
  color: var(--dim);
  border-bottom: 1px solid var(--line);
}

#courseNote:not(:empty)::before { content: " · "; }

/*
 * LIVE reads the way it does on a broadcast: solid, with a pulsing dot. A green
 * outline pill said "status: ok" — this says the feed is running, which is the
 * thing a crew glances up to confirm.
 */
/*
 * A full-width second line inside the header, centred.
 *
 * It used to share the top row with the wordmark, where a 340 px column has no
 * room for both: "NO UPDATES 51.3H" pushed the setup link clean off the right
 * edge and the overflow clipped it away — at exactly the moment somebody would
 * want to go and fix something. Letting the badge shrink instead only moved the
 * damage: it is the one flexible thing in that row, so it took the whole
 * shortfall and collapsed to nothing, taking the warning with it.
 *
 * On its own line neither has to yield. The header grows to carry it and
 * returns to one line when it clears, so the size of the ribbon is itself part
 * of the message — visible from further away than the words are readable.
 */
.status {
  flex: 1 0 100%;
  justify-content: center;
  margin: 2px 0 0;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: #fff;
  background: var(--brand);
  padding: 4px 9px 4px 8px;
  border-radius: 4px;
}

.status::before {
  content: "";
  flex: none;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: #fff;
  animation: pulse 2s ease-in-out infinite;
}

/* Only ever seen when something is wrong, so it is solid rather than a subtle
   outline — this is the one badge on the screen and its appearance IS the
   message. */
.status--bad {
  color: #fff;
  background: var(--bad);
  box-shadow: none;
}

.status[hidden] { display: none; }

.status--bad::before { background: var(--bad); animation: none; }

@keyframes pulse { 50% { opacity: 0.25; } }

@media (prefers-reduced-motion: reduce) {
  .status::before { animation: none; }
}

/* ----------------------------------------------------------------- split -- */

.split {
  flex: 1 1 auto;
  display: flex;
  min-height: 0;                    /* lets both children actually shrink */
}

.board {
  flex: none;
  width: var(--board-w);
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  background: var(--panel);
  border-right: 1px solid var(--line);
  overscroll-behavior: contain;
}

.mapWrap {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
  min-height: 0;
}

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

.mapCtl {
  position: absolute;
  left: 10px;
  bottom: 10px;
  z-index: 11;              /* above the pin scale and MapLibre's own controls */
  display: flex;
  gap: 6px;
}

.mapBtn {
  min-height: 40px;
  padding: 0 14px;
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
  background: rgba(10, 10, 11, 0.85);
  border: 1px solid var(--line);
  border-radius: 10px;
  cursor: pointer;
  backdrop-filter: blur(6px);
}

.mapBtn.is-on {
  color: #000;
  background: var(--text);
  border-color: var(--text);
}

/* Bottom-right crosshair, where a maps app puts it. */
.mapBtn--me {
  position: absolute;
  right: 10px;
  bottom: 10px;
  z-index: 11;
  width: 44px;
  padding: 0;
  font-size: 20px;
  line-height: 1;
  color: var(--me);
}

/* -------------------------------------------------------------- you are here -- */

/* Always present, never a setting. A spectator working out which way to walk,
   or a chase driver in a hot zone hunting for the course, reads this first. */
/*
 * Where the viewer is. A label and a value, not a sentence.
 *
 * This is one of the genuinely useful things the app knows and it was a
 * lowercase line lost among six others on a narrow panel. Given a block of its
 * own and a number in Chase Orange, it stops being skimmed past.
 *
 * The orange is the same orange as the course line, and that overlap is correct
 * rather than a collision: this IS a point on that course.
 */
.here {
  flex: none;
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: baseline;
  gap: 2px 10px;
  padding: 10px 12px;
  border-bottom: 1px solid var(--line);
  background: rgba(255, 90, 0, 0.06);
}

.here[hidden] { display: none; }

.here__label {
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--dim);
}

.here__rm {
  grid-row: 1 / span 2;
  grid-column: 2;
  align-self: center;
  /* Smaller than it was: the orange is doing the work of drawing the eye, and
     the number does not have to shout as well. */
  font-size: 18px;
  font-weight: 800;
  letter-spacing: -0.02em;
  color: var(--brand);
  font-variant-numeric: tabular-nums;
}

.here__note {
  font-size: 11px;
  color: var(--dim);
  font-variant-numeric: tabular-nums;
}

/* ----------------------------------------------------------------- where -- */

/* The one control on the whole board. It was sitting in a wall of grey text and
   getting skimmed past, so it gets the orange edge — the only thing here anyone
   is meant to touch. */
.where {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  flex: none;
  padding: 10px 12px;
  border-bottom: 1px solid var(--line);
  border-left: 3px solid var(--brand);
}

.where label {
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--text);
}

.where select {
  flex: 1 1 160px;
  min-width: 0;
  min-height: 40px;
  padding: 6px 8px;
  font-size: 16px;                  /* 16px or iOS zooms the page on focus */
  font-weight: 600;
  color: var(--text);
  background: var(--panel-2);
  border: 1px solid rgba(255, 90, 0, 0.55);
  border-radius: 9px;
}

.where select:focus { outline: 2px solid var(--brand); outline-offset: 1px; }

/* About the TARGET, not about you — so it takes the dim treatment, never the
   blue that belongs to this device. Empty most of the time, and collapses when
   it is: see paintTargetNote in app.js. */
.where__rm {
  flex: 1 1 100%;
  font-size: 11px;
  color: var(--dim);
  font-variant-numeric: tabular-nums;
}

.where__rm:empty { display: none; }

/* ------------------------------------------------------------ the board --- */

.board__head {
  flex: none;
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  padding: 12px 12px 6px;
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--text);
}

/* A rule running off to the right, the way a timing screen divides sections.
   Fading from the brand colour rather than a flat grey line — enough orange to
   tie the panel together without competing with anything that carries meaning. */
.board__head::after {
  content: "";
  flex: 1;
  height: 2px;
  border-radius: 1px;
  background: linear-gradient(to right, var(--brand), transparent);
  opacity: 0.55;
}

.board__head[hidden] { display: none; }

.rows {
  flex: none;
  list-style: none;
  margin: 0;
  padding: 0;
}

/*
 * One rider per row:
 *
 *   [1] [•] Andrew            17.1 mi out
 *           34 mi · 39 mph    26 min · lap 2
 */
.row {
  display: grid;
  grid-template-columns: 4px 1fr auto;
  grid-template-areas:
    "chip name out"
    ".    sub  eta"
    "warn warn warn"
    "more more more";
  align-items: center;
  column-gap: 8px;
  padding: 9px 12px;
  border-bottom: 1px solid var(--line);
}

/* From fifteen minutes: everyone up, gear on, tools out. The row changes at the
   moment the crew has to move, and stays changed until the rider is through. */
.row.is-close { background: #241a08; }

/* Stopped is deliberately quiet. A rider standing still at a pit is normal, and
   a red alarm every fuel stop is noise by the second race. */
.row[data-eta="stopped"] .row__eta { color: var(--dim); }

/* Past due, though, is not normal — it means the estimate ran out and he has
   not appeared. That one earns the warning colour. */
.row[data-eta="due"] .row__eta { color: var(--warn); font-weight: 700; }

.row[data-eta="off"] .row__eta,
.row[data-eta="unknown"] .row__eta { color: var(--dim); }
.row.is-past { opacity: 0.55; }
.row.is-stale .row__age { color: var(--warn); }


/*
 * A bar down the left of the row rather than a dot beside the name.
 *
 * Bigger colour target, easier to match to a pin on the map at a glance, and it
 * is what a timing board looks like. The dot was decoration; this is a key.
 */
.row__chip {
  grid-area: chip;
  grid-row: 1 / span 2;
  width: 4px;
  height: 100%;
  min-height: 28px;
  border-radius: 2px;
  align-self: stretch;
}

/* Number then name, the way a race board reads it. Also the control that opens
   the detail — a button rather than a click handler on the row, so it is
   reachable by keyboard and announces its state. */
.row__who {
  grid-area: name;
  display: flex;
  align-items: baseline;
  gap: 6px;
  min-width: 0;
  padding: 0;
  background: none;
  border: 0;
  text-align: left;
  cursor: pointer;
  font: inherit;
  color: inherit;
}

.row__who::after {
  content: "";
  flex: none;
  width: 5px;
  height: 5px;
  margin-left: 2px;
  border-right: 1.5px solid var(--dim);
  border-bottom: 1.5px solid var(--dim);
  transform: translateY(-2px) rotate(45deg);
}

.row.is-open .row__who::after { transform: translateY(1px) rotate(-135deg); }

/* Detail is behind a tap: the board stays an instrument, and the things a crew
   investigates rather than glances at are one press away. */
.row__more {
  grid-area: more;
  margin-top: 8px;
  padding-top: 8px;
  border-top: 1px solid var(--line);
}

.row__more[hidden] { display: none; }

.more__row {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  font-size: 12px;
  padding: 2px 0;
}

.more__k { color: var(--dim); }
.more__v { font-variant-numeric: tabular-nums; }

.more__head {
  margin: 8px 0 3px;
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--dim);
}

.more__none .more__v { color: var(--dim); }

/* A stop at a pit is expected. One between pits is the story. */
.more__off .more__k,
.more__off .more__v { color: var(--warn); }

/* A fact that has become a problem: an old fix, a flat battery, a rider who has
   been standing still. Same amber as everywhere else on the board. */
.more__warn .more__v { color: var(--warn); }

/* Something the phone never sent. Dimmed rather than amber, because a missing
   reading is not by itself bad news. */
.more__none .more__k,
.more__none .more__v { color: var(--dim); }

.row__num {
  flex: none;
  font-size: 15px;
  font-weight: 800;
  letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums;
}

.row__num:empty { display: none; }

.row__name {
  font-size: 15px;
  font-weight: 550;
  color: var(--dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* With no number the name IS the identity, so it carries the weight. */
.row__num:empty + .row__name { font-weight: 650; color: var(--text); }

.row__sub {
  grid-area: sub;
  font-size: 11px;
  color: var(--dim);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.row__out {
  grid-area: out;
  justify-self: end;
  font-size: 22px;
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.row.is-close .row__out { color: var(--close); }

.row__out small {
  font-size: 10px;
  font-weight: 600;
  color: var(--dim);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin-left: 3px;
}

/* The inbound racer's number, in that racer's own colour — a key back to their
   dot on the map and their bar on their own row, not decoration. */
.row__who-in {
  font-weight: 800;
  letter-spacing: -0.01em;
  margin-right: 1px;
}

/* Not green. Green meant nothing here — an ETA is not a pass/fail — and it is
   one of the racer colours, which are spoken for. Subordinate by size, not by
   hue. */
.row__eta {
  grid-area: eta;
  justify-self: end;
  font-size: 12px;
  font-weight: 600;
  color: var(--text);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.row.is-past .row__eta { color: var(--dim); }

/* Only rendered once it has become a problem, so it is always amber — there is
   no "fine" state of this to style. */
.row__age {
  color: var(--warn);
  font-variant-numeric: tabular-nums;
}

/* A stale fix on a rider who is nearly here is the one combination that has to
   be impossible to miss: the countdown is running on an assumption. */
.row.is-close .row__age {
  color: #000;
  background: var(--warn);
  padding: 0 4px;
  border-radius: 3px;
  font-weight: 700;
}

.row__warn {
  grid-area: warn;
  margin: 4px 0 0;
  font-size: 11px;
  line-height: 1.35;
  color: var(--warn);
}

.row__warn:empty { display: none; }

/* Chase rigs get no countdown — just where they are, which is the thing a crew
   chief is checking. Fixed-position pits are the same case: a rig that never
   moves all day still reads as "at Pit 09", which is correct. */
.row--chase .row__out { font-size: 13px; font-weight: 600; color: var(--dim); }

/* Both live in one block pinned to the bottom of the column, so the disclaimer
   stays where it was regardless of whether the footnote is showing. */
.board__end { margin-top: auto; }

/* Only rendered while some row is actually starred, so the explanation sits with
   the thing needing explaining rather than as permanent small print. */
.note {
  margin: 0;
  padding: 10px 12px 0;
  font-size: 11px;
  line-height: 1.5;
  color: var(--dim);
}

.note[hidden] { display: none; }
.note strong { color: var(--warn); }

.foot {
  margin: 0;
  padding: 14px 12px 16px;
  border-top: 1px solid var(--line);
  font-size: 10px;
  line-height: 1.6;
  color: var(--dim);
}

.foot strong {
  display: block;
  margin-bottom: 3px;
  color: var(--text);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
}

/* ------------------------------------------------------------------ pins -- */

/* Map pins are plain DOM, not MapLibre symbol layers — see the note at the top
   of map.js. That means no glyph server, so the map still draws with nothing but
   the tile host reachable. */
/*
 * The pin's box is the DOT, and nothing else.
 *
 * MapLibre centres a marker element on its coordinate. An earlier version laid
 * the dot and the label out side by side, so the combined box got centred and
 * the dot sat left of the true position by half the label's width — every pin
 * beside the course instead of on it, and worse the longer the name. The label
 * is absolutely positioned so it contributes nothing to the box.
 */
/*
 * Stacking order, bottom to top:
 *
 *   basemap + road overlay      WebGL raster layers
 *   race course line            WebGL line layer   } always under every pin,
 *   rider breadcrumb trails     WebGL line layer   } because the canvas is
 *                                                    under the marker container
 *   1  race mile pins           the quietest thing on the map
 *   2  course tags              Start, Finish, Pit 09
 *   3  target ring             the point the countdown is aimed at
 *   4  chase pins
 *   5  racer pins
 *   6  you are here            this device; nothing may cover your own dot
 *
 * Markers are DOM siblings, so without these they stack in insertion order —
 * which put mile numbers on top of pit names. Layer order is a decision, not
 * an accident of the order things happen to be created in.
 */
.pin--mile { z-index: 1; }
.pin--mark { z-index: 2; }
.pin--mark.is-target,
.pin--target { z-index: 3; }
.pin--here { z-index: 6; }
.pin--chase { z-index: 4; }
.pin--rider { z-index: 5; }

/*
 * Controls live above every pin.
 *
 * `.maplibregl-map` is position:relative with no z-index, so it does NOT create
 * a stacking context — the pins above and MapLibre's own controls end up as
 * peers, and MapLibre parks its control corners at z-index 2. Without this a
 * racer's dot paints over the zoom buttons.
 *
 * This overrides a vendor class, which is what broke marker positioning once
 * already, so keep it to z-index ONLY. Never add position, top, left or
 * transform here.
 */
.maplibregl-ctrl-top-left,
.maplibregl-ctrl-top-right,
.maplibregl-ctrl-bottom-left,
.maplibregl-ctrl-bottom-right { z-index: 10; }

/*
 * NEVER give .pin a `position` other than absolute.
 *
 * MapLibre positions markers with `.maplibregl-marker { position: absolute }`,
 * and this stylesheet loads after maplibre-gl.css at equal specificity — so
 * `position: relative` here silently wins, every marker drops into normal
 * document flow, and pits, mile markers and riders all land nowhere near the
 * course. It looks exactly like corrupt coordinates and is not.
 *
 * Declared absolute rather than omitted so it cannot break again by load order.
 * top/left are MapLibre's; leave them alone.
 */
.pin {
  position: absolute;
  width: 18px;
  height: 18px;
  pointer-events: none;
  white-space: nowrap;
}

.pin__mark {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
}

.pin__dot {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: currentColor;
  border: 2px solid #000;
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.35);
}

/* An arrow only appears once the phone's bearing means something — see
   HEADING_MIN_MPH in map.js. Until then the rider is a dot that claims nothing. */
.pin__arrow {
  position: absolute;
  width: 22px;
  height: 22px;
  display: none;
  fill: currentColor;
  stroke: #000;
  stroke-width: 1.6;
  stroke-linejoin: round;
  filter: drop-shadow(0 0 1px rgba(255, 255, 255, 0.45));
}

.pin.has-heading .pin__dot { display: none; }
.pin.has-heading .pin__arrow { display: block; }

.pin__label:empty { display: none; }

.pin__label {
  position: absolute;
  left: calc(100% + 4px);
  top: 50%;
  transform: translateY(-50%);
  font-size: 12px;
  font-weight: 600;
  color: #fff;
  text-shadow: 0 0 3px #000, 0 0 3px #000, 0 1px 2px #000;
}

.pin--rider { width: 26px; height: 26px; }
.pin--rider .pin__dot { width: 16px; height: 16px; }
.pin--rider .pin__arrow { width: 26px; height: 26px; }
.pin--rider .pin__label { font-size: 14px; }

/* Chase rigs are square as well as amber. Shape carries the meaning even for a
   viewer who cannot separate the colours, and it reads as "vehicle, not racer"
   before anyone has been told what the colours are. */
.pin--chase { width: 22px; height: 22px; }
.pin--chase .pin__dot { width: 13px; height: 13px; border-radius: 3px; }
.pin--chase .pin__arrow { width: 22px; height: 22px; }
.pin--mark { width: 14px; height: 14px; }
.pin--mark .pin__dot { width: 10px; height: 10px; }

/*
 * Course marks label BELOW their pin, everything else to the right.
 *
 * A chase rig parked at a pit put two labels on the same pixels, and the pit
 * name — the fixed thing, the thing you are navigating to — was the one that
 * lost. Splitting by axis rather than by cleverness means they can never
 * collide no matter how many rigs sit on one pit.
 */
.pin--mark .pin__label {
  left: 50%;
  top: calc(100% + 2px);
  transform: translateX(-50%);
  font-weight: 500;
  color: #e8e8ec;
}
/*
 * A stale pin must not read as a live one.
 *
 * Half opacity was the whole treatment, and it failed the only test that
 * matters: a glance at a bright screen in the desert. Faded still reads as
 * "there", and the dot is drawn where the rider WAS — so a crew can look at the
 * map, see the dot short of their pit, and settle in, when the phone actually
 * stopped reporting twenty minutes ago and the rider may already be past.
 *
 * So it changes SHAPE, not just weight. Solid means a current position; hollow
 * and dashed means a last known one. Shape survives sunlight, colour-blindness
 * and a cracked phone screen, and the age is spelled out beside it in amber for
 * anyone who looks twice.
 */
/*
 * Stale means RINGED, not faded.
 *
 * The first attempt drew a stale rider as a hollow dashed outline at reduced
 * opacity, and on satellite imagery it nearly disappeared. That is backwards.
 * A rider whose tracker has gone quiet is not less important than one still
 * reporting — the last place anybody knows he was is the single most important
 * mark on the map, and it is what a crew would drive to.
 *
 * So the dot stays solid, full strength, exactly where it was. An amber ring
 * around it says the position is a last known one rather than a current one.
 * Added to, not taken away from.
 */
.pin.is-stale { opacity: 1; }

.pin.is-stale .pin__mark::after {
  content: "";
  position: absolute;
  inset: -5px;
  border: 2px dashed var(--warn);
  border-radius: 50%;
  pointer-events: none;
}

/* Chase rigs are square, and the ring follows the shape — it says "this is old",
   not "this is a different kind of thing". */
.pin--chase.is-stale .pin__mark::after { border-radius: 5px; }

.pin.is-stale .pin__label { color: var(--warn); }

/* Mile pins: present but quiet. They orient you along the course; they are not
   the thing you are looking at. */
.pin--mile { width: 8px; height: 8px; }
.pin--mile .pin__dot { width: 6px; height: 6px; border-width: 1px; box-shadow: none; }
.pin--mile .pin__label { left: calc(100% + 2px); }
.pin--mile .pin__label {
  font-size: 10px;
  font-weight: 600;
  color: #fff;
  font-variant-numeric: tabular-nums;
}
.pin--mile.is-hidden { display: none; }

/* The mark you told the app you are sitting at. It gets a ring instead of a
   second pin stacked on top of it. */
/* The point the countdown is aimed at — a white ring, deliberately NOT the blue
   of "you are here". They are different claims and must not look alike. */
.pin--mark.is-target .pin__dot,
.pin--chase.is-target .pin__dot,
.pin--rider.is-target .pin__dot {
  box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.9), 0 0 0 5px #000;
}
.pin--mark.is-target .pin__label { color: #fff; font-weight: 700; }

.pin--target .pin__dot {
  width: 12px;
  height: 12px;
  box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.9), 0 0 0 5px #000;
}

/*
 * This device. Blue, ringed, and the one pin allowed above the racers — you
 * cannot decide which way to walk if a passing rider covers your own dot.
 */
.pin--here { width: 22px; height: 22px; }
.pin--here .pin__dot {
  width: 14px;
  height: 14px;
  background: var(--me);
  border: 2px solid #fff;
  box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.6), 0 0 12px rgba(10, 132, 255, 0.9);
}
.pin--here .pin__label { color: var(--me); font-weight: 700; }
/* Never an arrow: a phone in a pocket has no meaningful course over ground. */
.pin--here .pin__arrow { display: none !important; }

/* (merge rule removed — see the note at the top of map.js) */



/* --------------------------------------------------------------- narrow --- */

/*
 * On a phone the board goes on top, because the number a pit crew needs has to
 * be thumb-visible without scrolling. The map still takes everything left over,
 * which with two or three riders is most of the screen.
 */
@media (max-width: 820px) {
  .split { flex-direction: column; }

  .board {
    width: auto;
    max-height: 52svh;
    border-right: 0;
    border-bottom: 1px solid var(--line);
  }

  .mapWrap { min-height: 220px; }

  /* The disclaimer is not worth a scroll on a phone mid-race. */
  .foot { display: none; }
}

/* ------------------------------------------------- join and roster pages -- */

/*
 * These two are read once, on a phone, by someone who is not looking at a map.
 * Wider line length, bigger tap targets, and no attempt to look like the board.
 */
.page {
  display: block;
  height: auto;
  min-height: 100svh;
  overflow: auto;
}

.sheet {
  max-width: 34rem;
  margin: 0 auto;
  padding: 24px 18px 48px;
}

.sheet--wide { max-width: 46rem; }

.sheet__wait { color: var(--dim); }

.sheet__head { margin-bottom: 18px; }

.sheet__you {
  margin: 0;
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--dim);
}

.sheet__name {
  margin: 2px 0 0;
  font-size: 28px;
  font-weight: 700;
  letter-spacing: -0.02em;
}

.sheet__lede { color: var(--dim); font-size: 14px; line-height: 1.5; }

.sheet__foot {
  margin-top: 28px;
  padding-top: 14px;
  border-top: 1px solid var(--line);
  font-size: 12px;
  color: var(--dim);
}

/* The two strings that must not be mistyped. Big, monospace, one tap to copy. */
.copy {
  display: block;
  width: 100%;
  margin: 10px 0;
  padding: 12px 14px;
  text-align: left;
  color: var(--text);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 12px;
  cursor: pointer;
}

.copy:active { background: var(--panel-2); }

.copy__label {
  display: block;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--dim);
}

.copy__value {
  display: block;
  margin-top: 4px;
  font: 600 17px/1.35 ui-monospace, SFMono-Regular, Menlo, monospace;
  word-break: break-all;
}

.copy__hint { font-size: 11px; color: var(--dim); }
.copy.is-copied { border-color: var(--good); }
.copy.is-copied .copy__hint { color: var(--good); }
.copy.is-copied .copy__hint::after { content: " — copied"; }

.steps { margin: 22px 0 0; padding-left: 1.2em; line-height: 1.7; font-size: 15px; }
.steps code {
  padding: 1px 5px;
  background: var(--panel-2);
  border-radius: 4px;
  font-size: 13px;
}

.warn {
  margin-top: 22px;
  padding: 12px 14px;
  font-size: 14px;
  line-height: 1.5;
  color: var(--warn);
  background: rgba(255, 214, 10, 0.08);
  border: 1px solid rgba(255, 214, 10, 0.35);
  border-radius: 12px;
}

/*
 * The lockup, assembled from parts rather than shipped as one picture.
 *
 * The supplied lockup has the tagline baked into the artwork, which makes every
 * wording change a trip back to the designer. Composing mark + wordmark here and
 * setting the tagline as text costs nothing and means the line can change in an
 * afternoon — which matters while the positioning is still moving.
 *
 * The wordmark is raster and tops out at 546px, so it is held to 230 for
 * headroom on a retina phone. The mark is true vector and scales freely.
 */
.brandbar {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 6px;
}

.brandbar__mark { height: 34px; width: auto; display: block; }
.brandbar__word { height: 26px; width: auto; display: block; }

.tagline {
  margin: 0 0 22px;
  font-size: 13px;
  letter-spacing: 0.02em;
  color: var(--dim);
}

.btn {
  min-height: 44px;
  padding: 0 16px;
  font-size: 15px;
  font-weight: 600;
  color: #fff;
  background: var(--brand);
  border: 0;
  border-radius: 10px;
  cursor: pointer;
}

.btn--small { min-height: 34px; padding: 0 10px; font-size: 13px; }
.btn--bad { color: var(--text); background: transparent; border: 1px solid var(--line); }

.admin__login { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.admin__login label { font-size: 13px; color: var(--dim); width: 100%; }
.admin__login input {
  flex: 1 1 12rem;
  min-height: 44px;
  padding: 0 12px;
  font-size: 16px;
  color: var(--text);
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: 10px;
}

.admin__err { color: var(--bad); font-size: 13px; min-height: 1em; }

.admin__table { width: 100%; border-collapse: collapse; margin: 8px 0 20px; }
.admin__table th {
  text-align: left;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--dim);
  padding: 6px 8px;
  border-bottom: 1px solid var(--line);
}
.admin__table td { padding: 10px 8px; border-bottom: 1px solid var(--line); font-size: 15px; }
.admin__num { font-weight: 800; font-variant-numeric: tabular-nums; }
.admin__cls { color: var(--dim); font-size: 13px; }
.admin__table .is-chase { color: var(--chase); }
.admin__actions { text-align: right; white-space: nowrap; }
.admin__actions .btn + .btn { margin-left: 6px; }

.admin__add { display: flex; flex-wrap: wrap; gap: 8px; }
.admin__add input, .admin__add select {
  min-height: 44px;
  padding: 0 10px;
  font-size: 16px;
  color: var(--text);
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: 10px;
}
.admin__add input { flex: 1 1 8rem; min-width: 0; }
.admin__add input[hidden] { display: none; }

.dlg {
  max-width: 26rem;
  color: var(--text);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 16px;
  padding: 20px;
}
.dlg::backdrop { background: rgba(0, 0, 0, 0.7); }
.dlg__title { margin: 0 0 6px; font-size: 20px; }
.dlg__lede { margin: 0 0 14px; font-size: 13px; color: var(--dim); line-height: 1.5; }
.dlg__qr { background: #fff; padding: 8px; border-radius: 10px; }
.dlg__qr svg { display: block; width: 100%; height: auto; }

/* ---------------------------------------------------- crew chief portal -- */

.admin__head {
  margin: 22px 0 8px;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--dim);
}

.admin__head--sub { margin-top: 16px; }

/* --- setup: a page of numbered steps -------------------------------------- */

.sheet__top {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 22px;
}

.sheet__mark { flex: none; }

.sheet__top .sheet__name { margin: 0; flex: 1 1 auto; }

.step { margin: 0 0 30px; }

.step__head {
  display: flex;
  align-items: center;
  gap: 9px;
  margin: 0 0 6px;
  font-size: 15px;
  font-weight: 700;
  letter-spacing: -0.01em;
}

/* The number does the work of saying "there is an order to this", which a bare
   heading cannot. A chief who has never seen this page needs to know that
   loading a course is not optional and not last. */
.step__n {
  flex: none;
  display: inline-grid;
  place-items: center;
  width: 22px;
  height: 22px;
  font-size: 12px;
  font-weight: 800;
  color: var(--bg);
  background: var(--brand);
  border-radius: 50%;
}

/* What is loaded right now — stated in the terms the GPX was read in, so it can
   be checked against the course chart without opening the board. */
.courseNow__card {
  padding: 12px 14px;
  margin-bottom: 10px;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: 12px;
}

.courseNow__name { margin: 0; font-size: 15px; font-weight: 650; }

.courseNow__facts {
  margin: 3px 0 0;
  font-size: 13px;
  color: var(--dim);
  font-variant-numeric: tabular-nums;
}

.courseNow__from {
  margin: 2px 0 0;
  font-size: 11px;
  color: var(--dim);
  opacity: 0.7;
}

.courseNow__card .btn { margin-top: 10px; }

/* Amber, because with no course the board cannot do the one thing it is for. */
.courseNow__none {
  margin: 0 0 10px;
  font-size: 13px;
  color: var(--warn);
}

.admin__lapLbl {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  color: var(--dim);
}

.admin__lapLbl input { width: 4.5em; }

.admin__upload {
  margin: 4px 0;
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 12px 14px;
}

.admin__hint {
  font-size: 12px;
  color: var(--dim);
  line-height: 1.45;
  margin: 8px 0;
}

.admin__hint b { color: var(--text); font-weight: 600; }

.build { margin-top: 12px; }

.build__stats {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
  margin-bottom: 6px;
}

.build__stats th {
  text-align: left;
  font-weight: 500;
  color: var(--dim);
  padding: 3px 10px 3px 0;
  white-space: nowrap;
}

.build__stats td {
  padding: 3px 0;
  font-variant-numeric: tabular-nums;
}

.build__marks input,
.build__marks select { width: 100%; }

.build__marks td:nth-child(3) { width: 6.5em; }
.build__marks td:last-child { width: 1%; }

/* Secondary to the Save button beside it: an Add that looks like a commit is
   how a chief ends up saving a half-edited pit list. */
.btn--quiet {
  background: transparent;
  border: 1px solid var(--line);
  color: var(--dim);
}

/*
 * hidden means hidden.
 *
 * `[hidden]` carries the same specificity as a class, so any component styled
 * with `display: flex` silently outranks it and stays on screen with the
 * attribute set — which is how the crew chief's password form went on showing
 * after it had been unlocked, still holding the password. Every other component
 * in this file works around it with its own `.thing[hidden]` rule; this states
 * it once for all of them, including the ones nobody has written yet.
 *
 * The same shape of bug as `.pin { position: relative }` beating the map's
 * absolute positioning: a low-specificity rule doing something important,
 * quietly overruled by an ordinary one.
 */
[hidden] { display: none !important; }

/* Roster fields, edited in place. Quiet until touched: the table is read far
   more often than it is edited, and a grid of boxes reads as a form to fill in
   rather than a roster to scan. */
.admin__edit {
  width: 100%;
  min-height: 34px;
  padding: 0 8px;
  font: inherit;
  font-size: 14px;
  color: var(--text);
  background: transparent;
  border: 1px solid transparent;
  border-radius: 8px;
}

.admin__edit::placeholder { color: var(--dim); opacity: 0.6; }

.admin__edit:hover { border-color: var(--line); }

.admin__edit:focus {
  outline: none;
  background: var(--panel-2);
  border-color: var(--brand, #ff5a00);
}

.admin__num .admin__edit { font-weight: 800; font-variant-numeric: tabular-nums; }
.is-chase .admin__edit { color: var(--chase, #ffd60a); font-weight: 650; }
.admin__cls .admin__edit { font-size: 13px; color: var(--dim); }

/* Confirmation has to be visible without being read — a chief editing a roster
   in a trailer is not going to stop and look for a status line. */
.admin__edit.is-saved { border-color: #32d74b; }
.admin__edit.is-failed { border-color: #ff453a; background: rgba(255, 69, 58, 0.12); }

/* The way between the two pages. Quiet on the board — a crew reads that screen
   all day and opens the setup page twice a weekend. */
.top__chief {
  flex: none;                       /* never squeezed, never clipped */
  margin-left: auto;
  padding: 3px 9px;
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--dim);
  text-decoration: none;
  border: 1px solid var(--line);
  border-radius: 999px;
}

.top__chief:hover { color: var(--text); border-color: var(--dim); }

/*
 * The way OUT is louder than the way in, and deliberately.
 *
 * People get lost in a settings screen, not on a dashboard. The entrance can be
 * a quiet pill on a board a crew reads all day; the exit has to be the obvious
 * thing to press when you are done and want your race back.
 */
.sheet__back {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 38px;
  padding: 0 14px;
  font-size: 14px;
  font-weight: 650;
  color: var(--text);
  text-decoration: none;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: 999px;
}

.sheet__back:hover { border-color: var(--dim); }

/* Present to a password manager and to a screen reader, absent to the eye.
   display:none would hide it from the manager too, which defeats the point. */
.offscreen {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
}

.admin__signout { margin: 26px 0 0; }

/*
 * The roster on a phone.
 *
 * Four columns and two buttons do not fit in a hand. Squeezed into 400 px the
 * fields truncated to "Andre" and "12>" — a roster you cannot read the names in
 * is not a roster, and this is the screen a chief uses standing in a trailer.
 *
 * Each rider becomes a small block instead: number and name on the first line
 * where they are read together, class under them, actions under that. The
 * header row goes, because labels above columns mean nothing once there are no
 * columns.
 */
@media (max-width: 560px) {
  .admin__table thead { display: none; }

  .admin__table tr {
    display: grid;
    grid-template-columns: minmax(3rem, auto) 1fr;
    align-items: center;
    gap: 4px 10px;
    padding: 10px 0;
    border-bottom: 1px solid var(--line);
  }

  .admin__table td { padding: 0; border: 0; }

  .admin__table td:nth-child(1) { grid-column: 1; grid-row: 1; }
  .admin__table td:nth-child(2) { grid-column: 2; grid-row: 1; }
  .admin__table td:nth-child(3) { grid-column: 1 / -1; grid-row: 2; }

  .admin__actions {
    grid-column: 1 / -1;
    grid-row: 3;
    display: flex;
    gap: 8px;
    margin-top: 4px;
    text-align: left;
  }

  .admin__actions .btn { flex: 1 1 auto; }
  .admin__actions .btn + .btn { margin-left: 0; }

  /* A dash where a chase rig's number would go is column furniture. With the
     columns gone it is just a stray character next to a name. */
  .admin__table td:nth-child(1):not(:has(input)) { display: none; }
  .admin__table td:nth-child(1):not(:has(input)) + td { grid-column: 1 / -1; }

  /* One field per line beats three cut in thirds. */
  .admin__add input, .admin__add select { flex: 1 1 100%; }
  .admin__add .btn { flex: 1 1 100%; }
}

/* The scan is the instruction, so the steps come before the code and the link
   goes behind a disclosure — it is the fallback now, not the main path. */
.dlg__steps {
  margin: 4px 0 14px;
  padding-left: 20px;
  font-size: 13px;
  line-height: 1.6;
  color: var(--dim);
}

.dlg__steps b { color: var(--text); font-weight: 650; }

.dlg__applies {
  margin: 10px 0 0;
  font-size: 12px;
  line-height: 1.5;
  color: var(--dim);
}

.dlg__fallback { margin: 14px 0 4px; }

.dlg__fallback > summary {
  cursor: pointer;
  font-size: 13px;
  color: var(--dim);
}

/* The tap that makes a mobile browser actually ask. Sized like something you
   press with a glove on, because that is who is holding the phone. */
.here__ask {
  grid-column: 1 / -1;
  justify-self: start;
  margin-top: 8px;
  min-height: 40px;
  padding: 0 16px;
  font: inherit;
  font-size: 14px;
  font-weight: 650;
  color: #fff;
  background: var(--brand);
  border: 0;
  border-radius: 999px;
  cursor: pointer;
}

.here__ask:hover { filter: brightness(1.08); }

.admin__saved {
  align-self: center;
  font-size: 13px;
  font-weight: 600;
  color: #32d74b;
}
