/*
 * rc.css — the one shared stylesheet.
 *
 * Every page used to inline all of its CSS. That was deliberate — a page you
 * can read top to bottom — but it meant the palette existed in nine copies,
 * and the copies drifted (results.html had its own accent as a literal). When
 * the device matrix widened from "a PC in the tent" to phones and iPads, every
 * touch fix would have been made nine times. So: the tokens and the handful of
 * genuinely shared primitives live here, once. Everything with a personality
 * stays in the page.
 *
 * LOAD ORDER RULE: link this BEFORE the page's inline <style>. Page rules at
 * the same specificity then win by source order, which is the migration
 * safety valve — a page can override anything here without !important.
 *
 * WHAT STAYS PAGE-INLINE (do not move it here): layout scaffolding (panes,
 * grids, side widths), the per-role --accent override, table column styling,
 * timing's entry cells, results' read-from-a-distance sizes and print sheet,
 * setup.html's QR print rules, anything used by exactly one page.
 */

:root {
  /* Native widgets (selects, dialogs, autofill, scrollbars) must not render
     in light OS chrome on a black page. control.html and admin.html shipped
     without this and their popups glowed. */
  color-scheme: dark;

  /* Neutrals. These six values were already byte-identical in all 15 pages —
     some as vars, some as raw hex. Now they are written down once. */
  --bg:      #0b0d0f;
  --surface: #14181b;
  --raised:  #1b2126;
  --line:    #262e34;
  --text:    #e8edf0;
  --dim:     #7c868e;
  --faint:   #5a636a;

  /* Status colours. Semantic only — never decorative. */
  --good: #3ecf6d;
  --warn: #d9a13b;
  --bad:  #d64034;
  --gold: #e8c34a;

  /* Brand vs role. --brand is Grit orange (#F26522), matching the app.
     --accent defaults to it; role pages override --accent in their own
     inline style (control/timing amber, admin green, results cyan). The
     role colours are deliberate — an operator must be able to tell the
     undelayed console from the public map at a glance — so "fixing the
     drift" means one mechanism, not one colour. */
  --brand:  #F26522;
  --accent: var(--brand);
  /* Video hero. The one dock feature button that does NOT ride the page accent:
     a cool cyan so the live-feed button reads apart from the warm data-tools
     button beside it, on both the orange public map and the amber console. */
  --video:  #35c4e0;

  --ui:   -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  --data: ui-monospace, "Cascadia Mono", Consolas, "Liberation Mono", monospace;

  /*
   * ONE HEIGHT FOR EVERY CONTROL IN A HEADER BAR. See "Header bar controls"
   * below for what consumes it and why it is a height rather than a floor.
   *
   * 32px is not a new number: it is what the console buttons, the segmented
   * control and the Menu button already computed to on a desktop, to within
   * 3px of each other. 44px is the touch minimum the rest of this file already
   * enforces. Writing them down as a token is what lets a bar full of
   * different components agree, instead of each one landing near the mark.
   */
  --ctl-h: 32px;

  /* Safe-area insets as vars, so pages and canvas overlays can write
     `max(.6rem, var(--sat))` instead of remembering the env() spelling.
     Zero on everything that is not a notched phone. */
  --sat: env(safe-area-inset-top, 0px);
  --sab: env(safe-area-inset-bottom, 0px);
  --sal: env(safe-area-inset-left, 0px);
  --sar: env(safe-area-inset-right, 0px);
}

* { box-sizing: border-box; }

/*
 * 16px base, everywhere, including the console pages that used to run 15px.
 * Not a style preference: any focused input under 16px makes iOS Safari zoom
 * the page in and leave it there. Console density comes back from component
 * sizes on fine pointers, never from shrinking the base.
 */
html { font-size: 16px; -webkit-text-size-adjust: 100%; }
body { margin: 0; background: var(--bg); color: var(--text); font: 1rem/1.5 var(--ui); }

/*
 * Type floor: nothing below .75rem (12px). The audit found 9.9px status text
 * and 10.6px column headers — on a sunlit phone those are decoration, not
 * information. Micro-labels sit AT the floor, not under it.
 */
th, .label {
  font-size: .75rem; text-transform: uppercase; letter-spacing: .06em;
  color: var(--dim); font-weight: 600; text-align: left;
}

/* ── Buttons ──────────────────────────────────────────────────────────────
 * .btn  — the card-family block button (setup.html's step cards). Already the
 *         proven paddock pattern: full width, ~50px, obvious.
 * .tool — the console button (map/control/tracks). Dense under a mouse;
 *         raised to a real finger target on coarse pointers below.
 */
.btn {
  display: block; text-align: center; text-decoration: none; cursor: pointer;
  background: var(--good); color: #08120b; font: inherit; font-weight: 700;
  font-size: 1.04rem; padding: .9rem; border: 0; border-radius: 6px; width: 100%;
}
.btn:active { background: #2fa557; }
.btn.quiet, .btn.store {
  background: var(--raised); color: var(--text);
  border: 1px solid var(--line); font-weight: 600;
}
.btn.quiet:active, .btn.store:active { background: #232a30; }
.btn.danger { background: var(--bad); color: #fff; }

button.tool, .tool {
  font: inherit; font-size: .85rem; cursor: pointer;
  background: var(--raised); color: var(--text);
  border: 1px solid var(--line); border-radius: 4px; padding: .25rem .6rem;
  /* Half of these are anchors — the "← Organisers" and "← System admin" links
     out of the console pages. A button with an underline through its label is
     not a button, and every page that used .tool on an <a> was turning the
     underline off again in its own style block. */
  text-decoration: none;
}
/* Video and Insights are the console's two hero actions — the live feed and the
   timing/leaderboard tools. Plain .tool grey buried them next to Follow, so each
   wears a hero colour as an outlined, tinted chip via --hero: Insights leaves it
   at the page accent (orange public, amber control), Video overrides it to cyan,
   so the pair reads as two distinct things and not two of one. The feature
   pressed rule fills solid --hero — so rest→active is tint→fill in the button's
   own colour. */
.tool.feature {
  --hero: var(--accent);
  color: var(--hero); font-weight: 700;
  border-color: color-mix(in srgb, var(--hero) 60%, var(--line));
  background: color-mix(in srgb, var(--hero) 14%, var(--raised));
}
#videoBtn.feature { --hero: var(--video); }
/* A leading ▶ on Video against the trailing ▾ on Insights (styled in
   insights.js) gives the pair two different glyphs — a second cue past colour. */
.tool.feature .rc-glyph { font-size: .82em; opacity: .85; }
.tool[aria-pressed="true"] { background: var(--accent); border-color: var(--accent); color: #000; }
.tool.feature[aria-pressed="true"] { background: var(--hero); border-color: var(--hero); color: #000; }
.tool:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.tool.feature:focus-visible { outline-color: var(--hero); }
.tool:active { background: #232a30; }

/* ── Chips, cards, notes ────────────────────────────────────────────────── */
.chip {
  font-family: var(--data); font-size: .78rem; padding: .15rem .45rem;
  border-radius: 3px; background: var(--raised); border: 1px solid var(--line);
  color: var(--dim); white-space: nowrap;
}
.chip b { color: var(--text); }
.chip.good { color: #a9f0c2; border-color: #1f5a35; background: #12251a; }
.chip.warn { color: #ffcf8a; border-color: #5a4020; background: #2a2016; }
.chip.bad  { color: #ffb3ab; border-color: #6b2019; background: #2e1512; }
/*
 * The simulation chip. Violet, filled, and deliberately none of the three
 * above: good, warn and bad say how the SYSTEM is, and this says what the DATA
 * is. A simulated field is perfectly healthy and entirely invented, so it must
 * not read as a fault and it must not read as fine.
 *
 * Filled rather than outlined because it is the one chip that has to be seen
 * without being looked for — the cars beside it are indistinguishable from real
 * ones in every other respect. Same violet as the Simulation panel on
 * dev-tools.html, which is where it gets switched on.
 */
.chip.sim  { color: #120c22; border-color: #9a7ae0; background: #9a7ae0; font-weight: 700; }

.card {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 8px; padding: .9rem;
}

.note {
  padding: .85rem; background: var(--surface); border: 1px solid var(--line);
  border-left: 3px solid var(--warn); border-radius: 0 6px 6px 0;
  font-size: .84rem; color: #b8c0c6;
}
.note b { color: var(--text); }
.note.err, .err-note { border-left-color: var(--bad); }

/* ── The token gate ───────────────────────────────────────────────────────
 * One gate look for the five gated pages. The input is 1.04rem monospace for
 * the same reason the base font is 16px: an operator's very first touch on a
 * phone must not zoom the page and strand it there.
 */
.gate-input, #gate input {
  width: 100%; box-sizing: border-box; padding: .8rem; margin-top: .6rem;
  border: 1px solid var(--line); border-radius: 6px;
  background: var(--bg); color: var(--text);
  font: 1.04rem/1.3 var(--data);
}
.gate-err { color: #ffb3ab; font-size: .84rem; min-height: 1.2em; margin: .4rem 0 0; }

/* ── Tables that are wider than a phone ──────────────────────────────────
 * The one blessed overflow pattern. Wrap the <table> in <div class="scroll">.
 * Anything else — letting the document scroll sideways (admin), or clipping
 * (control) — either desyncs the sticky header or hides data.
 */
.scroll {
  overflow-x: auto;
  /*
   * SIDEWAYS ONLY, and this line is load-bearing on a phone.
   *
   * `overflow-x: auto` on its own does not leave overflow-y alone — the spec
   * computes it to `auto` as well, so every table wrapper on every page was
   * quietly a scroll container in BOTH axes. On a phone the table is most of
   * the screen, so a swipe up to read the next rows starts inside a box that
   * claims the vertical gesture and has nothing to scroll with it: the page
   * stops dead with more table below, which reads as the table hitting a scroll
   * limit. Pinning overflow-y hands the vertical swipe back to the document.
   *
   * Nothing is lost by clipping it. These wrappers are the height of their
   * table, so there is no vertical overflow to see, and every sticky cell
   * inside one is a sideways identity rail (`left`), never a `top:0` header —
   * those would already have been stuck to this box rather than the viewport
   * the moment overflow-x made it a scroll container.
   */
  overflow-y: hidden;
  /* A finger overscrolling the table must not start navigating the page. */
  overscroll-behavior-x: contain;
  /*
   * No `-webkit-overflow-scrolling: touch` here, deliberately. It bought
   * momentum scrolling on iOS 12 and earlier; every iOS version we support has
   * that by default, and what it still does is give the box its own scrolling
   * layer whose scrollable extent is measured once. These tables are re-rendered
   * from live data every few seconds, so a table that GREW kept the old limit
   * and the new rows could not be reached. It also breaks gesture chaining out
   * of a nested scroller, which is the same symptom from the other direction.
   */
}

/* ── Segmented control ──────────────────────────────────────────────────── */
.seg { display: inline-flex; border: 1px solid var(--line); border-radius: 4px; overflow: hidden; }
.seg button {
  font: inherit; font-size: .85rem; cursor: pointer; padding: .3rem .7rem;
  border: 0; border-radius: 0; background: transparent; color: var(--dim);
}
.seg button + button { border-left: 1px solid var(--line); }
.seg button.on { background: var(--accent); color: #000; font-weight: 700; }

/* ── The map tool dock ────────────────────────────────────────────────────
 * The strip of map controls under the canvas on map.html and control.html:
 *
 *   <div id="mapPane">
 *     <div id="mapView"> canvas + anything that floats on it </div>
 *     <div class="maptools" id="mapTools"> <div class="tgrp">…</div> … </div>
 *   </div>
 *
 * Shared, not copied, so the two cannot drift — an official who has used the
 * public map already knows race control's map. This is an exception to the
 * "layout scaffolding stays page-inline" rule at the top of this file, and it
 * earns it by being the same component on both pages rather than two panes
 * that happen to look alike. The panes themselves are still page-inline.
 *
 * Docked, never floating. Pinned to the top-right of the canvas as a column it
 * stood about 340px tall once the touch rule below grew the buttons, which on
 * an iPhone is most of the height of the map pane — sitting on the part of the
 * track somebody had just panned to.
 *
 * It WRAPS rather than scrolling sideways, and that is the second attempt.
 *
 * Sideways scroll was tried first, with the standard CSS scroll shadow — two
 * `local` background covers over two `scroll` shadows, so the hint appears
 * only when there is something off the edge. It does not work here and cannot
 * be made to: background layers paint BEHIND an element's children, and this
 * strip is wall-to-wall buttons, so the button hanging off the edge covers the
 * only thing that was there to say so. Screenshotted, in the overflow state,
 * before believing it. A tool half a button off the side of a phone with no
 * mark to say so is worse than a second row.
 *
 * So it wraps, and only when it must — one row at 390px with the seven tools
 * map.html normally shows, two when the optional Aerial tool is present or on
 * a 375px SE. The groups are flex: 0 0 auto, so the break lands BETWEEN them:
 * view tools on the first row, toggles on the second, never mid-group.
 */
.maptools {
  flex: 0 0 auto; display: flex; flex-wrap: wrap; align-items: center;
  /* row-gap, then the space BETWEEN groups. The dock's own children are the
     groups and nothing else, so a column-gap is all the separation they need —
     as a margin on the second group it indented that group by its own width
     when the dock wrapped and the group started a line. */
  gap: .3rem .8rem;
  padding: .35rem max(.4rem, var(--sar)) .35rem max(.4rem, var(--sal));
  background-color: var(--surface); border-top: 1px solid var(--line);
}

/*
 * Groups: view tools, then toggles.
 *
 * Separated by space, not by a rule. A drawn divider has to belong to one of
 * the two groups, and whichever one carries it is stranded when the dock wraps
 * — a vertical line at the far left of the second row, marking a boundary that
 * the line break has already made. Extra gap says the same thing under a mouse,
 * where the tools are dense enough for the difference to read as grouping. On a
 * touch screen it does not — see the coarse block below, which drops it.
 */
.maptools .tgrp { display: flex; align-items: center; gap: .3rem; flex: 0 0 auto; }

@media (pointer: coarse) {
  /*
   * Buy back the width the 44px rule spends. This is the one place in the app
   * where every control stands side by side and the SUM is what has to fit, so
   * the padding comes off — the 44px minimum below is untouched, and it is the
   * minimum that makes the target. Measured: without this the row ran 23px past
   * a 390px iPhone, which is the difference between one row and two on the
   * commonest phone there is.
   */
  .maptools .tool { padding-inline: .5rem; }
  /*
   * ONE gap on a touch screen, not two.
   *
   * The wider space between the groups is the divider on a desktop and it reads
   * as a mistake on a phone. The 44px rule gives every tool a bordered box
   * wider than its label, so the eye reads box edge to box edge rather than
   * label to label: a single 9.6px gap in a row of 4px ones is not a boundary,
   * it is one button somebody forgot to nudge. Reported from an iPhone on both
   * map.html and control.html.
   *
   * Grouping loses nothing. The groups are still flex: 0 0 auto, so the wrap
   * still lands between them and never mid-group, and on the layout where the
   * dock is tight enough to wrap, the line break is what marks the boundary
   * anyway. Narrower, too, which can only push the wrap later.
   */
  .maptools, .maptools .tgrp { gap: .25rem; }
}

/*
 * Both pages stack their side panel under the map at 900px. Above that the
 * dock is the bottom-left corner of the viewport and a home indicator crosses
 * it; below it, whatever is underneath carries the inset instead.
 */
@media (min-width: 901px) {
  .maptools { padding-bottom: max(.35rem, var(--sab)); }
}

/* ── Touch sizing ─────────────────────────────────────────────────────────
 * On coarse pointers every interactive control clears 44px. This is the rule
 * that turns the 18px reorder arrows and the 28px kill switch into things a
 * gloved thumb can hit. Fine-pointer (mouse) layouts keep their density.
 */
@media (pointer: coarse) {
  button, .btn, .tool, .seg button, select, input[type="file"]::file-selector-button,
  /*
   * `.back` — the "← System admin" / "← Organisers" link under the h1 on the
   * eight pages that have a masthead instead of a header bar. It was never in
   * this list and it is the only way off those pages: measured at 35px on a
   * phone, from .45rem of padding on .86rem text. The centring goes with the
   * height for the same reason it does in a header bar — min-height grows the
   * box and leaves the label at the top of it.
   */
  .back {
    min-height: 44px;
  }
  .back { display: inline-flex; align-items: center; }
  .tool { min-width: 44px; padding: .5rem .7rem; }

  /*
   * !important is rude, and earned here: a page's dense desktop input sizing
   * must never be able to reintroduce iOS zoom-on-focus by out-cascading this
   * file. If a field genuinely needs sub-16px text on a touch screen, the
   * field is the problem.
   */
  input, select, textarea { font-size: max(16px, 1em) !important; }

  :root { --ctl-h: 44px; }
}

/* ── Header bar controls ──────────────────────────────────────────────────
 * Every control that rides in a page's <header> is exactly --ctl-h tall, with
 * its label centred in that box. Nine pages have such a bar and they had nine
 * answers; measured on a 390px phone with touch emulation, one bar held a 46px
 * segmented control beside a 44px button beside a 48px button, and two of the
 * back-links out of a page were bare anchors at 20px and 24px.
 *
 * A HEIGHT, NOT A MIN-HEIGHT, and that is the whole mechanism. The touch block
 * above sets `min-height: 44px`, which is right everywhere else in the app and
 * cannot make a bar agree with itself: a floor lets every component land on its
 * own number the moment its own padding, border or line-height pushes it past
 * 44 — which is exactly how `.seg` arrived at 46px (44px buttons inside a
 * wrapper with a 1px border) while the button beside it stayed at 44.
 *
 * THE CENTRING IS NOT COSMETIC. `min-height` grows a box without moving what is
 * written in it, so an `inline-block` anchor at 44px carried its label 7.2px
 * above centre — sitting next to a real <button>, which centres its own content
 * for free. That 7px is what reads as "these buttons are different sizes" even
 * where the boxes match to the pixel. admin.html had already found this and
 * fixed it locally; this is that fix, once, for every bar.
 *
 * `flex: 0 0 auto` is the third defect: control.html's header is
 * `flex-wrap: nowrap; overflow-x: auto`, and with the default `flex-shrink: 1`
 * the row squeezed its buttons to their 44px min-width instead of scrolling —
 * "← Organisers" clipped its own label at 44px of a 91px scrollWidth, and "New
 * session" wrapped to two lines and stood 48px tall. Refusing to shrink is what
 * makes the `overflow-x` on those two headers do the job it was added for.
 *
 * Specificity is deliberate. `header .btn` (0,1,1) outranks a page's own `.btn`
 * (0,1,0) whatever the source order, so this holds without !important and
 * without editing the nine pages' inline styles — which keep their colours,
 * borders and radii. Height and label position are the only things taken away
 * from them.
 *
 * Inputs and selects get the height but keep their own display: an inline-flex
 * text field is a different bug.
 */
header button, header .tool, header .btn, header .seg,
header input, header select {
  height: var(--ctl-h);
  min-height: 0;
  flex: 0 0 auto;
}
header button, header .tool, header .btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: auto;
}
/*
 * The segment buttons fill the wrapper rather than setting the wrapper's size.
 * `min-height: 0` is load-bearing: with the touch block's 44px floor still in
 * force, `height: 100%` of a 42px content box would round back up to 44 and put
 * the 2px overflow straight back.
 *
 * AN OUTLINE, NOT A BORDER, and only in a header. The group's 1px frame is what
 * made `.seg` 46px next to a 44px button in the first place, and moving the
 * height onto the wrapper only relocated the problem: the buttons inside then
 * measured 42px, so the thing a thumb actually presses had dropped under the
 * 44px minimum this file spends a whole section enforcing. An outline is painted
 * outside layout, so the group frame costs nothing — the wrapper is --ctl-h and
 * the buttons in it are --ctl-h. `outline-offset: -1px` draws it just inside the
 * edge, where the border was, and outlines follow border-radius, so it is the
 * same 4px corner as before.
 *
 * In-page segs (admin's setup tabs, control's alert filter, map's splits) are
 * left alone: they have no bar to agree with, and the 44px buttons they already
 * have are the right answer there.
 */
header .seg { padding: 0; border: 0; outline: 1px solid var(--line); outline-offset: -1px; }
header .seg button { height: 100%; min-height: 0; }

/* ── Pages with NO header bar: keep the floating corner clear ─────────────
 * The counterpart to the block above, for the other half of the site. Nine
 * pages have a bar and nav.js puts the Menu button in it, where the bar's own
 * height holds the space open. Eleven have no bar, so the button is
 * position:fixed in the top-right corner and holds no space at all — it is
 * painted OVER whatever the page put at the top of itself.
 *
 * THIS WAS ELEVEN SEPARATE ANSWERS, AND MOSTLY IT WAS NO ANSWER. c.html cleared
 * the corner itself (`padding-top: 3.6rem`), car.html cleared 2.8rem of it, and
 * the nine masthead pages cleared none — they run on 1.4rem, which puts the h1
 * at 22.4px while the button's bottom edge is at 52.8px. Measured on a phone
 * with touch emulation, that is a real collision and not a theoretical one:
 *
 *   home.html    "Offroad Racing" ran under the button at 320, 390 and 430px —
 *                the public landing page, half its own name hidden behind a
 *                control, at every portrait width an iPhone has.
 *   account.html "Administrator account" ran 21px under it at 320px.
 *
 * The other seven pass on nothing better than a short role string, which is why
 * this is one rule and not two page edits: "Grit" plus a heading that happens
 * to fit is not a layout, and the next heading somebody writes is one word from
 * the same defect.
 *
 * DERIVED FROM THE BUTTON, NOT MEASURED AND NOT GUESSED. `.55rem` and the
 * safe-area inset are the `top` nav.js hangs it from, --ctl-h is its height, and
 * .5rem is the gap left under it — so the clearance tracks the token that sets
 * every other control's height, and a phone with a notch gets the inset once
 * rather than twice. 60.8px on a coarse pointer, 48.8px on a fine one.
 *
 * `:has()` ASKS THE DOCUMENT RATHER THAN KEEPING A LIST OF PAGE NAMES, which is
 * the same reason nav.js decides where the button goes with one
 * querySelector('header') instead of eleven filenames. The second half matters
 * as much as the first: login.html and o.html have no header AND no nav.js, so
 * they float no button and must not reserve a corner for one — they are a grid
 * with place-items:center, and top padding there does not open a gap, it pushes
 * the sign-in box off centre. On a browser without :has() the whole rule is
 * dropped and those eleven pages keep the behaviour they have today.
 *
 * Narrow viewports only. Above 40rem the masthead's own measure (46rem, centred)
 * keeps it clear of the corner — verified at 844x390, the landscape iPhone,
 * where nothing on any of the eleven reaches the button.
 */
@media (max-width: 40rem) {
  html:not(:has(header)):has(script[src$="nav.js"]) body {
    padding-top: calc(max(.55rem, env(safe-area-inset-top, 0px)) + var(--ctl-h) + .5rem);
  }
}

/*
 * .hit — slop extender for a control that must STAY visually small (a table's
 * inline ✕, video-diag's copy). Grows the touch target 12px in every
 * direction without moving a pixel of layout.
 */
.hit { position: relative; }
.hit::after { content: ""; position: absolute; inset: -12px; }

/* ── Hover discipline ─────────────────────────────────────────────────────
 * :hover only exists where a cursor does. On touch, iOS latches the hover
 * state after a tap — on control.html a latched row highlight looked like a
 * second selected car. Pages must wrap their own :hover rules the same way,
 * and give touch users the :active feedback above instead.
 */
@media (hover: hover) and (pointer: fine) {
  .btn:hover { filter: brightness(1.08); }
  .tool:hover { border-color: #3c4750; }
  .tool.feature:hover { border-color: var(--hero); background: color-mix(in srgb, var(--hero) 22%, var(--raised)); }
  .card a:hover, a.card:hover { border-color: #3c4750; }
}

/* ── Pointer-audience helpers ─────────────────────────────────────────────
 * For instruction text: ".ptr-fine" shows only to mouse/trackpad users
 * ("wheel zoom · Esc clear"), ".ptr-coarse" only to touch users ("pinch to
 * zoom · tap to clear"). Advertising keyboard shortcuts to a phone helps
 * nobody.
 */
@media (hover: none)  { .ptr-fine   { display: none !important; } }
@media (hover: hover) { .ptr-coarse { display: none !important; } }

/* ── Safe areas ───────────────────────────────────────────────────────────
 * Not applied globally — the map pages are full-bleed on purpose. Fixed and
 * absolutely-positioned chrome opts in, or uses the --sat/--sab/--sal/--sar
 * vars directly: `bottom: max(.6rem, var(--sab))`.
 */
.safe-top    { padding-top:    max(0px, var(--sat)); }
.safe-bottom { padding-bottom: max(0px, var(--sab)); }
.safe-x      { padding-left:   max(0px, var(--sal)); padding-right: max(0px, var(--sar)); }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition-duration: .01ms !important; }
}

@media print {
  .no-print { display: none !important; }
}

/*
 * The `hidden` attribute only works because the browser's own stylesheet says
 * [hidden] { display: none }. Any author rule that sets display on the same
 * element beats it - `ol { display:flex }` is enough to make <ol hidden>
 * render anyway. That shipped: the video setup page drew its steps, and a
 * blank canvas, while it was still waiting for a token.
 *
 * !important because this must outrank every layout rule in every page,
 * including ones added later by someone who has forgotten this comment.
 * This is the one copy; the per-page duplicates were deleted when the pages
 * adopted this file.
 */
[hidden] { display: none !important; }

/* Grit logo in mastheads (added at rebrand).
   .brand-mark = GRIT wordmark, inline in role-page h1s and console .brand spans;
   scales with the surrounding font. .brand-lockup = mark+GRIT+RACING for login/spectator. */
.brand-mark   { height:.92em; width:auto; vertical-align:-.14em; }
.brand-lockup { height:2.6rem; width:auto; display:inline-block; vertical-align:middle; }
