/*
 * Last Soul Ultra theme tokens.
 *
 * The brand is already decided and is not up for redesign: near-black,
 * one orange (#F04800, sampled off the live site's buttons), square corners,
 * heavy uppercase display type. Everything below encodes that; layout and
 * spacing stay in Crosswind utilities at the call site.
 *
 * This is a night race that runs until someone is left, so the page is dark by
 * default rather than dark by preference. There is no light mode: a light
 * theme would fight every photograph on the site, all of which were shot after
 * dark or in flat grey daylight.
 */

:root {
  --page: #0c0c0c;
  --surface: #151515;
  --surface-raised: #1c1c1c;
  --ink: #f4f4f2;
  --ink-muted: #b0b0ab;
  --ink-faint: #8a8a85;
  --line: #262625;

  /*
   * Crosswind's preflight gives every uncoloured border a light default
   * (#e5e7eb), which is right for a light theme and wrong for this one. It
   * matters for `divide-*`: those borders live on the CHILDREN, so a `.rule`
   * class on the parent colours the parent and never reaches them - the
   * format page's four-rule list rendered three bright near-white dividers
   * under one correct dark one. Setting the documented escape hatch once
   * makes every uncoloured border follow the theme; the explicit .rule /
   * .rule-strong classes still win where they are used.
   */
  --border-color: var(--line);
  --line-strong: #f4f4f2;
  --accent: #f04800;
  /*
   * Text ON an orange fill, and deliberately NOT white.
   *
   * The live site sets white on this orange, which is 3.73:1 - under AA for
   * the 13px bold the buttons actually use. Darkening the fill until white
   * passes would mean shipping a different orange, so the hue is kept exactly
   * and the text goes near-black instead: 5.25:1, and black-on-orange is the
   * more athletic reading of the same palette anyway.
   */
  --accent-ink: #0c0c0c;
  --scrim: rgb(0 0 0 / 0.55);
}

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  background: var(--page);
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

body {
  background: var(--page);
  color: var(--ink);
  font-family: 'Archivo', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* Colour primitives. */
.surface { background: var(--surface); }
.surface-raised { background: var(--surface-raised); }
.page-bg { background: var(--page); }
.ink { color: var(--ink); }
.muted { color: var(--ink-muted); }
.faint { color: var(--ink-faint); }
.accent { color: var(--accent); }
.accent-bg { background: var(--accent); color: var(--accent-ink); }

/*
 * Body copy on the accent panel. This was inline opacity: 0.8, which
 * composites the near-black ink down onto the orange to an effective 4.29:1 -
 * under AA for 16px text. A real colour at a 90% mix looks like softened ink
 * but holds 4.85:1, and unlike opacity a contrast audit can actually see it.
 */
.ink-on-accent-soft {
  color: #231209;
  color: color-mix(in srgb, var(--accent-ink) 90%, var(--accent));
}
.rule { border-color: var(--line); }
.rule-strong { border-color: var(--line-strong); }

/*
 * Display type. The site's own face is Apex, which is licensed and not
 * ours to ship; Archivo at its heaviest weight is the closest free grotesque
 * with the same squared terminals and tight fit. Numbers get a mono face
 * because this race is counted in hours and laps and those columns have to
 * line up.
 */
.display {
  font-family: 'Archivo', ui-sans-serif, system-ui, sans-serif;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: -0.015em;
  line-height: 0.92;
}

/*
 * Crosswind's sheet loads after this file, and every `text-4xl`-style utility
 * ships its own line-height. At equal specificity the utility won, so all 48
 * display headings were rendering at the utility's loose leading (40px on a
 * 36px heading) instead of the 0.92 the display face is drawn for. Doubling
 * the class outranks the single-class utility without touching every heading.
 */
.display.display {
  line-height: 0.92;
  letter-spacing: -0.015em;
  text-wrap: pretty;
}

/*
 * Four heading steps, fluid. The utilities had drifted: the same role carried
 * text-4xl sm:text-5xl on one page and text-3xl sm:text-4xl on the next, and
 * the feature-split heads ran at three different sizes across three pages.
 * Naming the steps makes the role the thing you pick, so it cannot drift again.
 * Each interpolates 375px -> 1200px and then holds.
 */
.h-page { font-size: clamp(3rem, 1.64rem + 5.82vw, 6rem); }
.h-lead { font-size: clamp(2.25rem, 1.57rem + 2.91vw, 3.75rem); }
.h-sect { font-size: clamp(1.875rem, 1.36rem + 2.18vw, 3rem); }
.h-item { font-size: clamp(1.25rem, 1.14rem + 0.49vw, 1.5rem); }
/* Stat values sit in a narrow band column, so they take a step of their own
 * rather than the section step - at the section size they wrap a line further
 * and the four columns stop sharing a baseline. */
.h-stat { font-size: clamp(1.875rem, 1.70rem + 0.73vw, 2.25rem); }

.mono { font-family: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, monospace; }

/* The small uppercase label used for section eyebrows and table headers. */
.label {
  font-family: 'Geist Mono', ui-monospace, monospace;
  font-size: 0.6875rem;
  text-transform: uppercase;
  letter-spacing: 0.16em;
}

/*
 * Navigation. The site sets everything else in uppercase - the wordmark, the
 * display faces, every label - so lowercase sans nav sitting directly beside
 * the wordmark read as an oversight rather than as contrast. Same mono as the
 * labels, a step up in size so it stays a comfortable target.
 */
.nav-link {
  font-family: 'Geist Mono', ui-monospace, monospace;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.13em;
}

:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

/*
 * Buttons. Square, uppercase, and physical on press. The orange one is the
 * only filled control on the site, which is what keeps it meaning "this is
 * the action" rather than "this is a button".
 */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  padding: 1rem 2rem;
  font-family: 'Archivo', sans-serif;
  font-weight: 700;
  font-size: 0.8125rem;
  line-height: 1;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  border: 1px solid transparent;
  white-space: nowrap;
  transition: background-color 0.2s cubic-bezier(0.16, 1, 0.3, 1),
    color 0.2s cubic-bezier(0.16, 1, 0.3, 1),
    border-color 0.2s cubic-bezier(0.16, 1, 0.3, 1),
    transform 0.12s ease-out;
}

.btn:active { transform: translateY(1px); }

.btn-accent { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); }
.btn-accent:hover { background: #ff5a12; border-color: #ff5a12; }

.btn-outline { background: transparent; color: var(--ink); border-color: var(--line-strong); }
.btn-outline:hover { background: var(--ink); color: var(--page); }

.btn[disabled], .btn[aria-disabled='true'] {
  background: transparent;
  border-color: var(--line);
  color: var(--ink-faint);
  cursor: not-allowed;
  transform: none;
}

/* Fields: label above, hairline under, no placeholder-as-label. */
.field {
  width: 100%;
  background: transparent;
  color: var(--ink);
  border: 0;
  border-bottom: 1px solid var(--line-strong);
  padding: 0.75rem 0;
  font: inherit;
}

.field::placeholder { color: var(--ink-faint); }
.field:focus { outline: none; border-bottom-color: var(--accent); }
.field:focus-visible { outline: none; }

/* Underline that grows from the left, for nav and inline links. */
.underlink { position: relative; display: inline-block; }

.underlink::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: -3px;
  width: 100%;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.28s cubic-bezier(0.16, 1, 0.3, 1);
}

.underlink:hover::after,
.underlink[aria-current='page']::after { transform: scaleX(1); }

@media (prefers-reduced-motion: reduce) {
  .underlink::after { transition: none; }
}

/*
 * Enter-on-scroll, as progressive enhancement: the hidden state only applies
 * once the observer script has confirmed it can reveal again.
 */
.js-reveal [data-reveal]:not(.is-revealed) .reveal-item {
  opacity: 0;
  transform: translateY(16px);
}

.reveal-item {
  transition: opacity 0.7s cubic-bezier(0.16, 1, 0.3, 1),
    transform 0.7s cubic-bezier(0.16, 1, 0.3, 1);
  transition-delay: calc(var(--i, 0) * 60ms);
}

@media (prefers-reduced-motion: reduce) {
  .js-reveal [data-reveal]:not(.is-revealed) .reveal-item { opacity: 1; transform: none; }
  .reveal-item { transition: none; }
}

/* Media tiles: a slow push-in on hover, killed under reduced motion. */
.tile-media { overflow: hidden; background: var(--surface); }

.tile-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.7s cubic-bezier(0.16, 1, 0.3, 1);
}

.tile:hover .tile-media img { transform: scale(1.04); }

@media (prefers-reduced-motion: reduce) {
  .tile-media img { transition: none; }
  .tile:hover .tile-media img { transform: none; }
}

/* Accordion markers, swapped off the element's own state. Crosswind has no
   `group-open:` variant, and its utility sheet loads after this file, so these
   are scoped to beat `.i-*` on specificity. */
summary > .acc-open { display: none; }
details[open] > summary > .acc-open { display: block; }
details[open] > summary > .acc-closed { display: none; }
summary::-webkit-details-marker { display: none; }

/* `hidden` has to beat a display utility loaded after this file. */
[hidden] { display: none !important; }
[x-cloak] { display: none !important; }

/* stx wraps a client-scoped component in a div; that box breaks a sticky
   header, which then has nowhere to travel. */
body > div[data-stx-scope] { display: contents; }

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Long-form reading measure, for the legal pages. */
.prose p { max-width: 70ch; line-height: 1.75; }
.prose p + p { margin-top: 1.15rem; }
.prose h2 { margin-top: 2.5rem; margin-bottom: 0.75rem; }

/* ---------------------------------------------------------------------- */
/* Standings                                                               */
/* ---------------------------------------------------------------------- */

/*
 * The filter is one signal setting one attribute; the hiding is CSS. Doing it
 * this way keeps all 169 rows in the server-rendered HTML - complete without
 * JavaScript, readable by a crawler - and avoids serialising every runner's
 * name into a client-side expression just to filter on status.
 */
/*
 * The two radios sit before everything they govern, so a general sibling
 * combinator reaches both the chips and the table. Visually hidden but still
 * focusable, so the control is keyboard operable and announced as a radio
 * group; the labels are the visible chips.
 */
.filter-input {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* fieldset ships a border, padding and a min-width floor that break the row. */
.filter-chips {
  border: 0;
  margin: 0;
  padding: 0;
  min-width: 0;
}

/*
 * :has() rather than a sibling combinator, so the inputs can live with their
 * labels inside the fieldset instead of having to precede everything they
 * govern. The rule no longer breaks if the section is reordered.
 */
.standings-block:has(#filter-in:checked) .standings tbody tr[data-status='out'] { display: none; }

.filter-chips:has(#filter-all:checked) label[for='filter-all'],
.filter-chips:has(#filter-in:checked) label[for='filter-in'] {
  color: var(--accent-ink);
  background: var(--accent);
  border-color: var(--accent);
}

/* Only the focused control's own chip, since each input precedes its label. */
.filter-input:focus-visible + label { outline: 2px solid var(--accent); outline-offset: 2px; }

.standings tbody tr { border-color: var(--line); }

/* A 169-row table is a scanning exercise, so give the eye a resting line. */
.standings tbody tr:hover { background: var(--surface, rgb(255 255 255 / 0.03)); }

/* Not sticky: the site header is itself sticky and 98px tall (64px without
 * the race banner, which comes and goes with the race state), so a sticky
 * thead sat hidden behind it. A header you cannot see is worse than one that
 * scrolls away, and there is no honest fixed offset to key it to. */

.chip {
  font-family: 'Geist Mono', ui-monospace, monospace;
  font-size: 0.6875rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  padding: 0.5rem 0.85rem;
  border: 1px solid var(--line);
  color: var(--ink-muted);
  background: transparent;
  cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease;
}

.chip:hover { color: var(--ink); border-color: var(--line-strong); }

/* Defined after .chip so it wins at equal specificity within this sheet. */
.chip-on {
  color: var(--accent-ink);
  background: var(--accent);
  border-color: var(--accent);
}

/* Mobile menu: closed by default, opened off the toggle button's own
 * aria-expanded. Keying off the button rather than a binding on the menu
 * itself is what lets the menu's links stay server-rendered. */
.mobile-menu { display: none; }
header:has(button[aria-expanded='true']) .mobile-menu { display: block; }
