/* ============================================================
   Running Portal — theme layer

   The design vocabulary is the `--rm-*` custom properties below; Bootstrap's own
   `--bs-*` variables are remapped onto them so components (buttons, cards, badges,
   form controls) inherit the theme instead of needing per-component overrides.

   THE reference for these values is docs/design/design-language.md — take values
   from there rather than inventing new ones, and update it in the same pass if you
   genuinely need something new. See ADR-0012 for why the theme layers on Bootstrap
   and why the star rating is CSS-only.

   Everything here is driven by one fact: this app gets used on a phone, outdoors,
   right after a race. Hence high contrast, 44px critical targets, 16px inputs.
   ============================================================ */

@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap');

:root {
  /* ---- Color: brand + action ("Industry" system, claude.ai/design, 2026-08-17: a
     race-day burnt-orange accent — the portal's accent must not read bluish) ---- */
  --rm-primary: #c8480f;
  --rm-primary-dark: #a63a0b;
  --rm-primary-soft: #fcefe9;

  /* ---- Color: race-day accent (same family — the accent role IS the primary here) ---- */
  --rm-accent: #c8480f;
  --rm-accent-ink: #7d2b08;
  --rm-accent-soft: #fcefe9;

  /* ---- Color: status ---- */
  --rm-success: #2f6a4a;
  --rm-success-soft: #e6efe9;
  --rm-warning-ink: #7a5510;
  --rm-warning-soft: #f4ecdc;
  --rm-danger: #a3291c;
  --rm-danger-soft: #f6e6e3;
  --rm-star: var(--rm-primary);

  /* ---- Color: the distance ramp (2026-08-28) ----
     The one categorical dimension this app has. Everything else in the palette is action
     or status; a race distance is neither, and it repeats on every row of every list in
     both applications — which is exactly why rendering all of them as one grey outline
     made the lists read as monotonous.

     One lightness, one chroma, five hues walked warm→cool as the race gets longer, so the
     set reads as a family and the shortest bucket sits on the brand's own hue. This is NOT
     a second accent: it never touches chrome, action or status, and the badge text is
     always the distance itself — the hue reinforces, it never carries the meaning alone.

     The bucket is a function of RaceDistance.Meters (DistanceOfferings.ToneClass), never
     of a label, so a new lookup row colours itself with no code change. Ink sits at L 0.45
     on an L 0.965 tint — roughly 5:1 — and every tag keeps a hue-matched hairline so it
     still reads when a hovered row tints beneath it. */
  --rm-dist-1-ink: oklch(0.45 0.13 42);    /* under 10K — the brand's own hue */
  --rm-dist-1-soft: oklch(0.965 0.028 42);
  --rm-dist-1-line: oklch(0.8 0.07 42);
  --rm-dist-2-ink: oklch(0.45 0.13 110);   /* 10K up to the half */
  --rm-dist-2-soft: oklch(0.965 0.028 110);
  --rm-dist-2-line: oklch(0.8 0.07 110);
  --rm-dist-3-ink: oklch(0.45 0.13 165);   /* half up to the full */
  --rm-dist-3-soft: oklch(0.965 0.028 165);
  --rm-dist-3-line: oklch(0.8 0.07 165);
  --rm-dist-4-ink: oklch(0.45 0.13 250);   /* marathon */
  --rm-dist-4-soft: oklch(0.965 0.028 250);
  --rm-dist-4-line: oklch(0.8 0.07 250);
  --rm-dist-5-ink: oklch(0.45 0.13 320);   /* ultra */
  --rm-dist-5-soft: oklch(0.965 0.028 320);
  --rm-dist-5-line: oklch(0.8 0.07 320);

  /* ---- Color: the calendar ramp (2026-09-04) ----
     The race calendar's own ramp, and the one place the distance dimension is drawn as a
     SEQUENCE rather than a set of categories. One hue — the brand's 42 — with lightness
     walking down as the race gets longer, so a year of squares reads as a heatmap at a
     glance: darker means further. Step 4 lands on --rm-primary itself.

     Separate from the five-hue badge ramp above on purpose. A badge carries its distance in
     words and the hue only reinforces it, so five hues cost nothing there; 365 unlabelled
     squares are read by comparison alone, and five competing hues in a grid say "five
     unrelated things happened" rather than "these were longer than those". */
  --rm-cal-1: oklch(0.85 0.09 42);   /* under 10K */
  --rm-cal-2: oklch(0.75 0.13 42);   /* 10K up to the half */
  --rm-cal-3: oklch(0.65 0.16 42);   /* half up to the full */
  --rm-cal-4: oklch(0.55 0.17 42);   /* marathon — the brand's own stop */
  --rm-cal-5: oklch(0.43 0.14 42);   /* ultra */

  /* ---- Color: surfaces + text ----
     A flat ground, not a white-card-on-grey-page model: cards are transparent, defined by
     a hairline border, and the input surface (--rm-surface) is the one filled object. */
  --rm-ink: #1d1f20;
  --rm-ink-muted: color-mix(in srgb, var(--rm-ink) 55%, transparent);
  --rm-surface: #e9e9ea;
  --rm-canvas: #f2f2f3;
  --rm-game-sky: #87ceeb; /* The dedicated sunny Game Run scene only. */
  --rm-neutral-soft: #e7e7ea;   /* recessed fill for muted/off states (e.g. an inactive badge) */
  --rm-border: color-mix(in srgb, var(--rm-ink) 16%, transparent);
  --rm-border-strong: color-mix(in srgb, var(--rm-ink) 45%, transparent);

  /* ---- Typography ----
     "Industry" system: Barlow Condensed for every heading/label/button/badge, Barlow for
     body copy. --rm-text-base stays 16px on purpose — it's the floor for actual form
     inputs (<input>/<select>/<textarea>), not a style choice: below 16px iOS Safari zooms
     the page on focus. Non-input body copy uses --rm-text-body (15px, as specified). */
  --rm-font-sans: "Roboto", -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
  /* Same family as the body: hierarchy comes from weight and size, not a second face.
     The condensed face it replaces was the readability problem — at 11-13px, uppercase
     and letter-spaced, a condensed grotesque is exactly the wrong tool. */
  --rm-font-heading: var(--rm-font-sans);
  --rm-text-xs: 0.75rem;      /* 12px — was 11px; the smallest label has to survive sunlight */
  --rm-text-sm: 0.8125rem;    /* 13px */
  --rm-text-body: 0.9375rem;  /* 15px — non-input body copy */
  --rm-text-base: 1rem;       /* 16px — form-input floor, untouched */
  --rm-text-lg: 1.0625rem;    /* 17px */
  --rm-text-xl: 1.25rem;      /* 20px */
  --rm-text-2xl: 1.5625rem;   /* 25px */
  --rm-text-3xl: 2rem;        /* 32px */
  --rm-leading-tight: 1.2;
  --rm-leading-normal: 1.55;
  --rm-weight-normal: 400;
  --rm-weight-semibold: 500;
  --rm-weight-bold: 700;

  /* ---- Spacing ----
     Industry's own density scale (a 3.4px step) rather than a round rem scale, adopted as
     specified. --rm-space-5 aliases -4: Industry has no separate 5th step. */
  --rm-space-1: 0.2125rem;    /* 3.4px */
  --rm-space-2: 0.425rem;     /* 6.8px */
  --rm-space-3: 0.6375rem;    /* 10.2px */
  --rm-space-4: 0.85rem;      /* 13.6px */
  --rm-space-5: 0.85rem;      /* 13.6px — same as -4 */
  --rm-space-6: 1.275rem;     /* 20.4px */
  --rm-space-7: 1.7rem;       /* 27.2px */

  /* ---- Radius / elevation ----
     Sharp corners throughout — components read as wireframe/technical objects, defined by
     hairline borders rather than radius or shadow. Shadow tokens stay real, used only for
     hover elevation on interactive cards (Industry's own .elev-* classes do the same). */
  --rm-radius-sm: 0;
  --rm-radius: 0;
  --rm-radius-lg: 0;
  --rm-radius-pill: 0;
  --rm-shadow-sm: 0 1px 2px color-mix(in srgb, #2b2b2d 14%, transparent);
  --rm-shadow: 0 3px 10px color-mix(in srgb, #2b2b2d 16%, transparent);

  /* ---- Ergonomics ----
     Split per the design: 36px for ordinary controls, 44px reserved for the two things
     tapped out of breath after a race — finish-time entry and rating stars. */
  --rm-tap-min: 2.25rem;            /* 36px */
  --rm-tap-min-critical: 2.75rem;   /* 44px */
  --rm-focus-ring: 0 0 0 0.125rem var(--rm-primary);

  /* ---- Remap Bootstrap onto the rm-* palette ---- */
  --bs-primary: var(--rm-primary);
  --bs-primary-rgb: 200, 72, 15;
  --bs-body-color: var(--rm-ink);
  --bs-body-bg: var(--rm-canvas);
  --bs-body-font-family: var(--rm-font-sans);
  --bs-body-font-size: var(--rm-text-body);
  --bs-body-line-height: var(--rm-leading-normal);
  --bs-border-color: var(--rm-border);
  --bs-border-radius: var(--rm-radius);
  --bs-border-radius-sm: var(--rm-radius-sm);
  --bs-secondary-color: var(--rm-ink-muted);
  --bs-link-color: var(--rm-primary);
  --bs-link-hover-color: var(--rm-primary-dark);
}

/* ---- Base ---------------------------------------------------------------- */

html {
  font-size: 16px;  /* 16px floor everywhere: legibility outdoors, and iOS won't zoom inputs */
}

/* Sticky footer via flex: the footer follows the content and is only pushed to the
   bottom of the viewport when the page is shorter than one screen. It must never be
   `position: absolute` — that resolves against the initial containing block, which put
   the footer bar across the MIDDLE of every page taller than the viewport (it covered
   the photo upload control on the details page outright). */
body {
  font-family: var(--rm-font-sans);
  font-size: var(--rm-text-body);
  line-height: var(--rm-leading-normal);
  color: var(--rm-ink);
  background-color: var(--rm-canvas);
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

body > .rm-main {
  flex: 1 0 auto;
  padding-top: var(--rm-space-5);
  padding-bottom: var(--rm-space-6);
}

/* The bottom tab bar is fixed, so the page has to reserve its height or the bar sits on
   top of the footer. Only below 768px — from there the nav is back in the top bar. */
@media (max-width: 767.98px) {
  body {
    padding-bottom: calc(var(--rm-tap-min-critical) + var(--rm-space-4) + env(safe-area-inset-bottom, 0px));
  }
}

h1, h2, h3, h4, h5, h6 {
  font-family: var(--rm-font-heading);
  line-height: var(--rm-leading-tight);
  letter-spacing: -0.005em;
  color: var(--rm-ink);
}

h1, .h1 { font-size: var(--rm-text-2xl); font-weight: var(--rm-weight-bold); }
h2, .h2 { font-size: var(--rm-text-xl); font-weight: var(--rm-weight-bold); }
h3, .h3 { font-size: var(--rm-text-xl); font-weight: var(--rm-weight-bold); }
h5, .h5 { font-size: var(--rm-text-lg); font-weight: var(--rm-weight-semibold); }
h6, .h6 { font-size: var(--rm-text-body); font-weight: var(--rm-weight-semibold); }

@media (min-width: 768px) {
  h1, .h1, h3.rm-page-title, .h3 { font-size: var(--rm-text-3xl); }
}

a { color: var(--rm-primary); }
a:hover { color: var(--rm-primary-dark); }

.text-muted { color: var(--rm-ink-muted) !important; }

/* Data reads as data: times, dates, bibs, distances line up in a column. */
.rm-num,
.rm-finish-time {
  font-variant-numeric: tabular-nums;
}

/* ---- Navbar -------------------------------------------------------------- */

.navbar.brand-nav {
  background-color: var(--rm-canvas);
  border-bottom: 1px solid var(--rm-border);
  padding-top: var(--rm-space-2);
  padding-bottom: var(--rm-space-2);
}

.navbar.brand-nav .navbar-brand {
  font-family: var(--rm-font-heading);
  font-weight: var(--rm-weight-bold);
  font-size: var(--rm-text-lg);
  color: var(--rm-ink);
  display: inline-flex;
  align-items: center;
  gap: var(--rm-space-2);
}

/* The one place the accent runs at full strength. */
/* The brand mark (_BrandMark.cshtml) — inline SVG since 2026-08-22, replacing a ● glyph.
   It inherits currentColor, so the colour is set here once and the same artwork serves the
   nav and any reversed treatment. font-size/line-height no longer apply to it; flex: none
   is what stops the bracket squashing beside the wordmark on a narrow phone. */
.navbar.brand-nav .rm-brand-mark {
  color: var(--rm-accent);
  width: 22px;
  height: 22px;
  flex: none;
  display: block;
}

/* The wordmark sits beside the mark rather than after it as inline text. */
.navbar.brand-nav .navbar-brand {
  display: inline-flex;
  align-items: center;
  gap: var(--rm-space-2);
}

/* ---- Racer nav: bottom tab bar on phone, inline links from 768px ----------
   ONE markup block restyled by viewport, never two. On a phone both destinations sit in
   the thumb arc and stay one tap away — the opposite trade from a hamburger, which would
   put every navigation action two taps deep for the sake of two links. From 768px the
   same list is the row of links beside the brand it has always been. */

.navbar.brand-nav .rm-nav-links {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 1030;   /* over page content, under Bootstrap's modal/backdrop layer (1050+) */
  display: grid;
  /* One column per tab, however many there are. A fixed two-column grid left the bar
     visibly off-centre once "My Races" started hiding for signed-out visitors (ADR-0003) —
     the single remaining tab sat in the left half rather than across the bar. */
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  margin: 0;
  padding: 0;
  list-style: none;
  background-color: var(--rm-canvas);
  border-top: 1px solid var(--rm-border);
  /* --rm-shadow-sm, cast upward. */
  box-shadow: 0 -1px 2px color-mix(in srgb, #2b2b2d 14%, transparent);
  /* Keeps the bar clear of the iOS home indicator without a fixed magic number. */
  padding-bottom: env(safe-area-inset-bottom, 0px);
}

.navbar.brand-nav .rm-nav-links .nav-link {
  color: var(--rm-ink-muted);
  font-weight: var(--rm-weight-semibold);
  font-size: var(--rm-text-sm);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--rm-space-1);
  /* A critical target: this is the whole navigation on a phone. */
  min-height: calc(var(--rm-tap-min-critical) + var(--rm-space-4));
  padding: var(--rm-space-2) var(--rm-space-3);
  border-radius: var(--rm-radius-sm);
  text-align: center;
}

.navbar.brand-nav .rm-nav-links .nav-link:hover {
  color: var(--rm-ink);
}

.navbar.brand-nav .rm-nav-links .nav-link[aria-current="page"] {
  color: var(--rm-primary);
  background-color: var(--rm-primary-soft);
}

.navbar.brand-nav .rm-nav-links .nav-link:focus-visible {
  outline: none;
  box-shadow: var(--rm-focus-ring);
}

@media (min-width: 768px) {
  /* Back to a plain row of links in the top bar — no fixed bar, no icons. */
  .navbar.brand-nav .rm-nav-links {
    position: static;
    display: flex;
    flex-direction: row;
    gap: var(--rm-space-1);
    background: none;
    border-top: none;
    box-shadow: none;
    padding-bottom: 0;
  }

  .navbar.brand-nav .rm-nav-links .nav-link {
    flex-direction: row;
    min-height: var(--rm-tap-min);
    padding-left: var(--rm-space-3);
    padding-right: var(--rm-space-3);
  }

  .navbar.brand-nav .rm-nav-icon { display: none; }
}

/* ---- Account control (runner sign-in, ADR-0003) --------------------------- */

/* Lives in the TOP bar at every width, unlike .rm-nav-links, which drops to a fixed
   bottom tab bar below 768px. Signing out is a rare, deliberate act — it does not
   belong in the thumb arc beside the two destinations a runner uses constantly. */
.rm-nav-account {
  display: flex;
  align-items: center;
  gap: var(--rm-space-2);
  font-size: var(--rm-text-sm);
}

.rm-nav-avatar {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  object-fit: cover;
  border: 1px solid var(--rm-border);
  /* The picture comes from Google and is decorative — the name beside it is the
     accessible identity, so a failed load must leave no broken-image artefact. */
  background-color: var(--rm-neutral-soft);
}

.rm-nav-name {
  color: var(--rm-ink-muted);
  font-weight: var(--rm-weight-semibold);
  /* A long Google display name must not push the sign-out button off a phone. */
  max-width: 8rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Below 768px the brand already competes for the bar; the name is redundant next to the
   avatar and the sign-out button, both of which must stay tappable. */
@media (max-width: 767.98px) {
  .rm-nav-name { display: none; }
}

/* Sign-out reads as one more nav item, not as the bar's only bordered box — same ink,
   weight and size as .nav-link, set off by a single rule instead of an outline. It stays a
   real submit inside the POST form: a GET sign-out can be fired by any third-party image
   tag (ADR-0003). */
.rm-nav-signout {
  display: flex;
  align-items: center;
  min-height: var(--rm-tap-min);
  padding: var(--rm-space-2) var(--rm-space-3);
  margin-left: var(--rm-space-1);
  font-family: var(--rm-font-heading);
  font-size: var(--rm-text-sm);
  font-weight: var(--rm-weight-semibold);
  color: var(--rm-ink-muted);
  background: none;
  border: 0;
  border-left: 1px solid var(--rm-border);
  cursor: pointer;
}

.rm-nav-signout:hover { color: var(--rm-ink); }

.rm-nav-signout:focus-visible {
  outline: none;
  box-shadow: var(--rm-focus-ring);
}

/* ---- Auth panel (sign-in page) -------------------------------------------- */

/* The runner's counterpart to .rm-admin-login-card: same centred, capped shape, but this
   page keeps the site layout — a visitor signing in is still inside the portal, and the
   catalog behind them is public (ADR-0003). The admin gate is chrome-less because it
   guards an operator surface; this one is not a gate at all. */
.rm-auth-panel {
  max-width: 420px;
  margin-inline: auto;
  padding: var(--rm-space-4);
  background-color: var(--rm-surface);
  border: 1px solid var(--rm-border);
  border-radius: var(--rm-radius-sm);
}

.rm-auth-panel h1 {
  font-size: var(--rm-text-xl);
}

/* ---- Buttons ------------------------------------------------------------- */

.btn {
  --bs-btn-font-family: var(--rm-font-heading);
  font-weight: var(--rm-weight-semibold);
  border-radius: var(--rm-radius-sm);
  min-height: var(--rm-tap-min);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.btn:focus-visible {
  outline: none;
  box-shadow: var(--rm-focus-ring);
}

.btn-primary {
  --bs-btn-bg: var(--rm-primary);
  --bs-btn-border-color: var(--rm-primary);
  --bs-btn-hover-bg: var(--rm-primary-dark);
  --bs-btn-hover-border-color: var(--rm-primary-dark);
  --bs-btn-active-bg: var(--rm-primary-dark);
  --bs-btn-active-border-color: var(--rm-primary-dark);
  --bs-btn-disabled-bg: var(--rm-primary);
  --bs-btn-disabled-border-color: var(--rm-primary);
  color: #fff;
}

.btn-outline-secondary {
  --bs-btn-color: var(--rm-primary);
  --bs-btn-border-color: var(--rm-border-strong);
  --bs-btn-hover-bg: var(--rm-primary-soft);
  --bs-btn-hover-border-color: var(--rm-primary);
  --bs-btn-hover-color: var(--rm-primary-dark);
  --bs-btn-active-bg: var(--rm-primary-soft);
  --bs-btn-active-color: var(--rm-primary-dark);
  --bs-btn-active-border-color: var(--rm-primary);
  background-color: transparent;
}

.btn-link {
  --bs-btn-color: var(--rm-ink-muted);
  --bs-btn-hover-color: var(--rm-ink);
  text-decoration: none;
}
.btn-link:hover { text-decoration: underline; }

/* The primary action goes full width on a phone — it's the thing you opened the page
   to do, and it should be reachable with a thumb without aiming. */
@media (max-width: 575.98px) {
  .rm-form-actions .btn { width: 100%; }
  .rm-form-actions { display: flex; flex-direction: column; gap: var(--rm-space-2); }
}

/* ---- Cards --------------------------------------------------------------- */

/* Components read as wireframe objects: transparent, hairline-bordered, square-cornered —
   not filled white panels with a shadow. */
.card {
  position: relative;
  border: 1px solid var(--rm-border);
  border-radius: var(--rm-radius);
  background-color: transparent;
}

/* Blueprint corner marks — a simplified two-corner nod to the reference design's
   four-corner registration marks (drawn there via four extra <i> elements per component).
   ::before/::after on the existing .card selector gets the same "technical drafting"
   motif with zero markup changes across every card in both apps. */
.card::before,
.card::after {
  content: "";
  position: absolute;
  width: 9px;
  height: 9px;
  pointer-events: none;
}

.card::before {
  top: -5px;
  left: -5px;
  border-top: 1px solid var(--rm-border-strong);
  border-left: 1px solid var(--rm-border-strong);
}

.card::after {
  bottom: -5px;
  right: -5px;
  border-bottom: 1px solid var(--rm-border-strong);
  border-right: 1px solid var(--rm-border-strong);
}

.card > .card-body {
  padding: var(--rm-space-4);
}

@media (min-width: 768px) {
  .card > .card-body { padding: var(--rm-space-5); }
}

/* Admin login (ADR-0013): a standalone screen outside the racer-facing nav, so it gets its
   own small centered card rather than the full-width page layout. */
.rm-admin-login-card {
  max-width: 420px;
  width: 100%;
}

.card .card-title {
  color: var(--rm-ink);
  margin-bottom: var(--rm-space-4);
}

/* ---- Race list ------------------------------------------------------------
   One row per registration. Both the header and every row share ONE grid
   definition, so the columns line up down the page without a <table> — and the
   same markup reflows to two compact lines on a phone. Changing the column
   template means changing it in both places (.rm-race-list-head, .rm-race-row).
   -------------------------------------------------------------------------- */

/* NO `overflow: hidden` here, deliberately. It shipped as "keeps the first/last row inside the
   card's edge", which buys nothing with square corners and a 1px border — and an ancestor with
   `overflow: hidden` is a scroll container, which silently kills `position: sticky` on the group
   headings inside. The month/year heading looked sticky in the stylesheet and never stuck. */

.rm-race-list-head,
.rm-race-row {
  display: grid;
  align-items: center;
  gap: var(--rm-space-1) var(--rm-space-3);
  padding: var(--rm-space-3) var(--rm-space-4);
  /* Phone: name + distance on the first line, then date · location · status.
     Finish time has no column here — it rides the date cell instead (see below), because
     a sixth area would squeeze the location past readability at 390px. */
  grid-template-columns: auto minmax(0, 1fr) auto;
  grid-template-areas:
    "name name     distance"
    "date location status";
}

.rm-race-list-head {
  display: none;  /* column labels only earn their space once columns exist */
  font-family: var(--rm-font-heading);
  background-color: var(--rm-canvas);
  border-bottom: 1px solid var(--rm-border);
  font-size: var(--rm-text-xs);
  font-weight: var(--rm-weight-semibold);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--rm-ink-muted);
}

@media (min-width: 768px) {
  .rm-race-list-head,
  .rm-race-row {
    grid-template-columns: 6rem minmax(0, 1fr) minmax(0, 13rem) minmax(0, 8.5rem) 6rem;
    grid-template-areas: "date name location distance status";
    padding: var(--rm-space-3) var(--rm-space-5);
  }

  .rm-race-list-head { display: grid; }
}

/* A THIRD step at 1200px — the 768px five-column layout above is untouched, and the
   768–1199px band keeps it exactly. Wide screens spend their extra width on the log's
   actual payload: finish time, otherwise visible one race at a time on the details page. */
@media (min-width: 1200px) {
  .rm-race-list-head,
  .rm-race-row {
    grid-template-columns: 7rem minmax(0, 1fr) minmax(0, 16rem) minmax(0, 10rem) 7rem 7rem;
    grid-template-areas: "date name location distance finish status";
  }
}

.rm-race-row {
  position: relative;  /* containing block for the row-wide stretched link */
  border-bottom: 1px solid var(--rm-border);
  min-height: var(--rm-tap-min);
}

.rm-race-row:last-child { border-bottom: none; }

@media (hover: hover) {
  .rm-race-row:hover { background-color: var(--rm-surface); }
}

.rm-cell-date     { grid-area: date; }
/* Out of the flow entirely below 1200px: on a phone the value rides the date cell instead
   (rendered inside it), and there is no finish area in the smaller templates to place it in.
   The `display` pair has to stay adjacent — an override placed further up the file loses to
   this rule on equal specificity. */
.rm-cell-finish   { grid-area: finish; display: none; }

@media (min-width: 1200px) {
  .rm-cell-finish { display: block; }
}
.rm-cell-name     { grid-area: name; }
.rm-cell-location { grid-area: location; }
/* Phone: these two sit at the row's right edge. From md up they become real columns and
   left-align, so each badge lines up under its header label. */
.rm-cell-distance { grid-area: distance; justify-self: end; min-width: 0; max-width: 100%; }
.rm-cell-status   { grid-area: status; justify-self: end; }

@media (min-width: 768px) {
  .rm-cell-distance,
  .rm-cell-status { justify-self: start; }

  .rm-cell-distance .badge {
    max-width: 100%;
    display: inline-block;   /* inline-flex can't do text-overflow */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}

.rm-race-row .rm-cell-date,
.rm-race-row .rm-cell-location {
  font-size: var(--rm-text-sm);
  color: var(--rm-ink-muted);
  min-width: 0;
}

/* Truncation is not a desktop-only concern: an imported location can be a bilingual
   address ("Hatirjheel Amphitheatre - হাতিরঝিল এম্ফি থিয়েটার, Dhaka, Bangladesh"), which
   wrapped to eight lines inside the phone row's 1fr track and buried the rest of the row.
   One line with an ellipsis at every width; the full text is on the details page. */
.rm-race-row .rm-cell-location {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The race name is the row's identity, so it wraps rather than truncating — but two lines
   is the limit before the row stops being a row. */
.rm-race-row .rm-race-name {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.rm-race-row .rm-race-name {
  font-family: var(--rm-font-heading);
  font-size: var(--rm-text-lg);
  font-weight: var(--rm-weight-semibold);
  color: var(--rm-ink);
  line-height: var(--rm-leading-tight);
  text-decoration: none;
}

.rm-race-countdown {
  display: block;
  width: fit-content;
  margin-top: var(--rm-space-1);
  color: var(--rm-ink-muted);
  font-size: var(--rm-text-sm);
  font-weight: var(--rm-weight-semibold);
  font-variant-numeric: tabular-nums;
}

/* The word "Soon" is the meaning; the accent rule is a reinforcing visual cue, never the
   only signal (design-language.md: status is always a word plus colour). */
.rm-race-countdown-soon {
  padding-left: var(--rm-space-2);
  border-left: 3px solid var(--rm-primary);
  color: var(--rm-accent-ink);
}

.rm-race-row:hover .rm-race-name { color: var(--rm-primary-dark); }

.rm-race-row .rm-cell-finish {
  font-size: var(--rm-text-body);
  color: var(--rm-ink);
}

/* On a phone the same value trails the date rather than claiming a column of its own —
   there is no room for a sixth area at 390px. From 768px it is hidden again: the
   768–1199px band is exactly what shipped before, and 1200px gives it a real column. */
.rm-race-row .rm-finish-inline {
  color: var(--rm-ink);
  font-weight: var(--rm-weight-semibold);
}

@media (min-width: 768px) {
  .rm-race-row .rm-finish-inline { display: none; }
}

@media (min-width: 768px) {
  /* Column widths are fixed, so a long race name truncates rather than pushing
     the grid out of alignment. */
  .rm-race-row .rm-cell-name,
  .rm-race-row .rm-cell-location {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* Columns are fixed here, so the name truncates on one line instead of clamping. */
  .rm-race-row .rm-race-name {
    font-size: var(--rm-text-body);
    display: block;
    -webkit-line-clamp: none;
    line-clamp: none;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}

/* ---- Badges --------------------------------------------------------------
   A word plus a color, never a color alone — a bordered tag, not a filled pill:
   flatter and denser, square corners like everything else in the system. */

.badge {
  display: inline-flex;
  align-items: center;
  font-family: var(--rm-font-heading);
  font-size: var(--rm-text-xs);
  font-weight: var(--rm-weight-semibold);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 0.125rem var(--rm-space-2);
  border: 1px solid transparent;
  border-radius: var(--rm-radius-pill);
}

/* Distance is a data label, not a status — a plain bordered tag, ink text. */
.rm-badge-distance {
  background-color: transparent;
  border-color: var(--rm-border);
  color: var(--rm-ink);
}

.rm-badge-recorded,
.rm-badge-active {
  background-color: var(--rm-success-soft);
  border-color: color-mix(in srgb, var(--rm-success) 40%, transparent);
  color: var(--rm-success);
}

.rm-badge-pending {
  background-color: var(--rm-warning-soft);
  border-color: color-mix(in srgb, var(--rm-warning-ink) 35%, transparent);
  color: var(--rm-warning-ink);
}

/* Not part of the reference design's own badge component (which only shows
   recorded/pending/distance) — extends the same bordered-tag pattern to the admin app's
   active/inactive lookup-table states (kept identical between stylesheets). */
.rm-badge-inactive {
  background-color: var(--rm-neutral-soft);
  border-color: var(--rm-border);
  color: var(--rm-ink-muted);
}

/* ---- Forms --------------------------------------------------------------- */

/* Organizer multi-select (issue #6): each checkbox row is a comfortable tap target. The
   whole label is clickable (via for/id), so min-height + centering makes one-thumb
   selection comfortable outdoors, matching the target rule in design-language.md. */
.rm-check-list .form-check {
  min-height: var(--rm-tap-min);
  display: flex;
  align-items: center;
  margin-bottom: 0;
}

.form-label {
  font-size: var(--rm-text-sm);
  font-weight: var(--rm-weight-semibold);
  color: var(--rm-ink);
  margin-bottom: var(--rm-space-2);
}

.form-control,
.form-select {
  font-size: var(--rm-text-base);  /* below 16px, iOS Safari zooms the page on focus */
  min-height: var(--rm-tap-min);
  border-radius: var(--rm-radius-sm);
  border-color: var(--rm-border-strong);
  color: var(--rm-ink);
  /* The one filled surface in the system (--rm-surface) — cards are transparent, so this
     is what distinguishes an editable field from the page around it. */
  background-color: var(--rm-surface);
}

.form-control:disabled,
.form-select:disabled {
  background-color: var(--rm-canvas);
}

.form-control::placeholder { color: var(--rm-ink-muted); opacity: 0.75; }

.form-control:focus,
.form-select:focus {
  border-color: var(--rm-primary);
  box-shadow: var(--rm-focus-ring);
}

.form-control.input-validation-error,
.form-select.input-validation-error {
  border-color: var(--rm-danger);
  background-color: var(--rm-danger-soft);
}

.form-text {
  font-size: var(--rm-text-sm);
  color: var(--rm-ink-muted);
}

.text-danger {
  color: var(--rm-danger) !important;
  font-size: var(--rm-text-sm);
  font-weight: var(--rm-weight-semibold);
}

/* Finish time: three numeric boxes with the unit named under each — a critical tap
   (tapped out of breath after a race), so it keeps the 44px floor even though ordinary
   form-control inputs are 36px. */
.rm-time-input {
  text-align: center;
  font-variant-numeric: tabular-nums;
  min-height: var(--rm-tap-min-critical);
}

.rm-time-unit {
  font-size: var(--rm-text-xs);
  color: var(--rm-ink-muted);
  text-align: center;
  display: block;
  margin-top: var(--rm-space-1);
}

/* ---- Alerts -------------------------------------------------------------- */

.alert {
  border-radius: var(--rm-radius);
  border: 1px solid transparent;
  font-size: var(--rm-text-body);
}

.alert-success {
  background-color: var(--rm-success-soft);
  border-color: color-mix(in srgb, var(--rm-success) 40%, transparent);
  color: var(--rm-success);
}

.alert-info {
  background-color: var(--rm-primary-soft);
  border-color: color-mix(in srgb, var(--rm-primary) 40%, transparent);
  color: var(--rm-primary-dark);
}

.alert-warning {
  background-color: var(--rm-warning-soft);
  border-color: color-mix(in srgb, var(--rm-warning-ink) 35%, transparent);
  color: var(--rm-warning-ink);
}

.alert-danger {
  background-color: var(--rm-danger-soft);
  border-color: color-mix(in srgb, var(--rm-danger) 40%, transparent);
  color: var(--rm-danger);
}

/* ---- Status banners and dialogs (2026-08-22) -------------------------------
   Both applications render these from the SAME two partials (_StatusAlert, _Dialog) and this
   block is duplicated in both stylesheets, exactly as site.js is — the two apps share a Core
   library but not a wwwroot (ADR-0014). Change both or a message starts meaning one thing in
   the portal and looking like another in the racer app.

   The banner variants split on whether the reader has to DO something, not on hue for its own
   sake: a status reports something finished and retires itself, an error persists because the
   thing it describes is still true. Both carry an icon AND their colour, never colour alone. */
.rm-alert {
  display: flex;
  align-items: flex-start;
  gap: var(--rm-space-3);
  padding: var(--rm-space-3) var(--rm-space-4);
  border-radius: var(--rm-radius-sm);
  border-width: 1px;
  border-style: solid;
  /* A left rule carries the severity at a glance down a page of cards, which a full tint at
     this size does not — the tint alone reads as "a box", not as "a result". */
  border-left-width: 3px;
}

.rm-alert-icon {
  flex: none;
  margin-top: 0.1em;   /* optical: aligns the glyph to the first line's cap height, not its box */
}

.rm-alert-text { min-width: 0; }

.rm-alert-status {
  background-color: var(--rm-success-soft);
  border-color: color-mix(in srgb, var(--rm-success) 30%, transparent);
  border-left-color: var(--rm-success);
  color: var(--rm-success);
}

.rm-alert-error {
  background-color: var(--rm-danger-soft);
  border-color: color-mix(in srgb, var(--rm-danger) 30%, transparent);
  border-left-color: var(--rm-danger);
  color: var(--rm-danger);
}

/* Bootstrap's dismiss button is absolutely positioned over the padding box, which collides
   with the flex row above; give it back the space it needs on the end. An explicit 3rem, not
   --rm-space-6 — that token is 20.4px, narrower than the button it has to clear. */
.rm-alert.alert-dismissible { padding-right: 3rem; }

/* ---- Dialog ---------------------------------------------------------------
   The shared confirmation/refusal modal (_Dialog.cshtml). Square corners and the bracket
   treatment of a card, so it reads as part of the same system as the page behind it rather
   than as a browser chrome popup — which is exactly what window.confirm looked like, and the
   reason it could never say WHY an action was refused. */
.rm-dialog .modal-content {
  border-radius: var(--rm-radius-sm);
  border: 1px solid var(--rm-border-strong);
}

.rm-dialog .modal-header,
.rm-dialog .modal-footer {
  border-color: var(--rm-border);
}

.rm-dialog .modal-title { font-family: var(--rm-font-heading); }

.rm-dialog-body { color: var(--rm-ink); }

/* The blockers, or the events a delete detaches. Indented as a list rather than run into the
   sentence: the whole point of this dialog over a one-line confirm is that it can enumerate. */
.rm-dialog-list {
  margin-top: var(--rm-space-3);
  padding-left: var(--rm-space-5);
  color: var(--rm-ink-muted);
  font-size: var(--rm-text-sm);
}

.rm-dialog-list li + li { margin-top: var(--rm-space-1); }

/* Refusal mode: no go-ahead button exists, so the remaining button is the way out and should
   not read as a cancellation of something that was about to happen. */
.rm-dialog-refusal .modal-footer { justify-content: flex-start; }

/* ---- Feedback rows + star rating (ADR-0012) ------------------------------ */

.rm-feedback-row {
  display: grid;
  gap: var(--rm-space-2);
  padding: var(--rm-space-3) 0;
  border-bottom: 1px solid var(--rm-border);
}

.rm-feedback-row:last-of-type { border-bottom: none; }

.rm-feedback-label .form-label {
  font-size: var(--rm-text-body);
  margin-bottom: 0;
}

.rm-feedback-note {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: start;
  gap: var(--rm-space-2);
}

.rm-feedback-note > label {
  color: var(--rm-ink-muted);
  font-size: var(--rm-text-sm);
  padding-top: var(--rm-space-2);
}

.rm-feedback-note textarea {
  min-height: calc(var(--rm-tap-min) * 2);
  resize: vertical;
}

@media (min-width: 768px) {
  .rm-feedback-row {
    grid-template-columns: minmax(10rem, 1fr) auto minmax(16rem, 1.25fr);
    align-items: center;
    gap: var(--rm-space-4);
  }

  .rm-feedback-note textarea { min-height: var(--rm-tap-min-critical); }
}

/* DOM order is 5→1 and the group is reversed, so `input:checked ~ label` reaches every
   LOWER star — that's what fills 1..N with no JavaScript. Reordering the markup
   silently breaks the fill. */
.rm-stars {
  display: flex;
  flex-direction: row-reverse;
  justify-content: flex-end;
  position: relative;
}

/* Visually hidden but still focusable — `display: none` would drop the radios out of
   the tab order and kill arrow-key selection. */
.rm-stars input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
  pointer-events: none;
}

/* Rating stars are a critical tap (tapped out of breath after a race) — 44px, not the
   36px ordinary floor. */
.rm-stars label {
  min-width: var(--rm-tap-min-critical);
  min-height: var(--rm-tap-min-critical);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 1.5rem;
  line-height: 1;
  cursor: pointer;
  color: var(--rm-border-strong);
  border-radius: var(--rm-radius-sm);
  margin: 0;
  transition: color 0.1s ease, transform 0.1s ease;
}

.rm-stars input:checked ~ label { color: var(--rm-star); }
.rm-stars label:hover,
.rm-stars label:hover ~ label { color: var(--rm-star); }
.rm-stars input:focus-visible + label { outline: 2px solid var(--rm-primary); outline-offset: 2px; }
.rm-stars label:active { transform: scale(0.92); }

/* ---- Registration switcher (ADR-0009) ------------------------------------ */

.rm-registration-switch {
  border-top: 1px solid var(--rm-border);
  padding-top: var(--rm-space-3);
}

/* ---- Photos -------------------------------------------------------------- */

.rm-race-photo {
  max-width: 160px;
  border-radius: var(--rm-radius-sm);
  border-color: var(--rm-border);
}

/* ---- Catalog (ADR-0015) ---------------------------------------------------- */

/* One full-width row per catalog event, inside a single card (the race-list precedent).
   Row = tap target via stretched-link; the subscribe form button sits outside the link's
   reach on purpose (a POST is a deliberate act). */
.rm-catalog-row {
  position: relative;
  min-height: var(--rm-tap-min);
  display: flex;
  align-items: center;
  padding: var(--rm-space-3) var(--rm-space-4);
  border-bottom: 1px solid var(--rm-border);
}
.rm-catalog-row:last-child {
  border-bottom: none;
}
@media (hover: hover) {
  .rm-catalog-row:hover {
    background-color: var(--rm-surface);
  }
}
.rm-catalog-name {
  font-family: var(--rm-font-heading);
  color: var(--rm-ink);
  text-decoration: none;
  font-size: var(--rm-text-lg);
  font-weight: 600;
}
.rm-catalog-name:hover {
  color: var(--rm-primary);
}
/* A subscribed event's row must not tint the subscribe form's z-order; keep the form
   above the stretched link so its button stays clickable. */
.rm-catalog-row form {
  z-index: 1;
}
.rm-catalog-past-badge {
  margin-left: var(--rm-space-2);
}

.rm-catalog-main {
  flex: 1 1 auto;
  min-width: 0;
}

/* Name and its state badge on one line. Wraps rather than squeezing the name, so a long
   race title on a phone pushes the badge to a second line instead of truncating. */
.rm-catalog-title {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--rm-space-2);
}

.rm-catalog-meta {
  margin-top: var(--rm-space-1);
  font-size: var(--rm-text-sm);
  color: var(--rm-ink-muted);
}

.rm-catalog-tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--rm-space-2);
  margin-top: var(--rm-space-1);
}

/* Pinned right and above the row's stretched link, so the controls stay clickable while the
   rest of the row remains one tap target for viewing. */
.rm-catalog-end {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: var(--rm-space-2);
  flex: none;
}

/* A row control built like the row's tags — square, hairline, uppercase — so the catalog
   reads as one vocabulary instead of Bootstrap buttons parked beside bordered tags. It is
   NOT badge-sized: a tag is read, these are pressed, so they keep the --rm-tap-min floor
   every other control in this system has. */
.rm-tag-action {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Deliberately UNDER the 36px control floor, and only here. At 36px these read as buttons
     wearing a tag's clothes — the height was the part still saying "Bootstrap". 28px is a
     control that belongs on a list row. The floor comes back on a phone, below, where the
     row is worked with a thumb rather than a pointer. */
  min-height: 1.75rem;
  padding: 0 var(--rm-space-3);
  font-family: var(--rm-font-heading);
  font-size: var(--rm-text-xs);
  font-weight: var(--rm-weight-semibold);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  border: 1px solid transparent;
  cursor: pointer;
}

/* The row's one real action carries the brand hue. */
.rm-tag-action-go {
  background-color: var(--rm-primary-soft);
  border-color: var(--rm-primary);
  color: var(--rm-primary);
}

/* Quiet, because the Subscribed badge beside it already states the state. */
.rm-tag-action-quiet {
  background-color: transparent;
  border-color: var(--rm-border-strong);
  color: var(--rm-ink);
}

@media (hover: hover) {
  .rm-tag-action-go:hover {
    background-color: var(--rm-primary);
    color: var(--rm-canvas);
  }

  .rm-tag-action-quiet:hover {
    border-color: var(--rm-ink);
    background-color: var(--rm-surface);
  }
}

.rm-tag-action:focus-visible {
  outline: none;
  box-shadow: var(--rm-focus-ring);
}

/* A phone taps these; a desktop clicks them. The compact height is a pointer affordance, so
   it stops at the breakpoint and --rm-tap-min returns. */
@media (max-width: 767.98px) {
  .rm-tag-action {
    min-height: var(--rm-tap-min);
    padding: 0 var(--rm-space-4);
  }
}

/* ---- Event cover (ADR-0016) ----------------------------------------------- */

/* The event page's cover image: fills the card width, crops to a consistent height so a
   portrait phone flyer doesn't push the whole page down. Decorative-ish but meaningful. */
.rm-event-cover {
  display: block;
  max-height: 320px;
  object-fit: cover;
}

/* ---- Pagination ---------------------------------------------------------- */

.rm-pagination .page-link {
  border-radius: var(--rm-radius-sm);
  margin: 0 var(--rm-space-1);
  color: var(--rm-primary);
  border-color: var(--rm-border);
  min-width: var(--rm-tap-min);
  min-height: var(--rm-tap-min);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-weight: var(--rm-weight-semibold);
}

.rm-pagination .page-item.active .page-link {
  background-color: var(--rm-primary);
  border-color: var(--rm-primary);
  color: #fff;
}

.rm-pagination .page-item.disabled .page-link {
  color: var(--rm-ink-muted);
  background-color: var(--rm-canvas);
}

/* ---- Page width ----------------------------------------------------------
   The racer app has no sidebar, so a centred measure is the right read — but Bootstrap
   caps .container at 1320px, which on a 1680px+ monitor is a wide band of nothing on
   both sides while the race list stays cramped. 90rem gives the list room for its sixth
   column (finish time) without letting text run past a scannable width. */
@media (min-width: 1400px) {
  .container { max-width: 90rem; }
}

/* ---- Footer -------------------------------------------------------------- */

.footer {
  flex-shrink: 0;
  width: 100%;
  padding: var(--rm-space-4) 0;
  font-size: var(--rm-text-sm);
  color: var(--rm-ink-muted);
  border-top: 1px solid var(--rm-border);
  background-color: transparent;
}

/* ---- Game Run ----------------------------------------------------------- */

.rm-game-run-card,
.rm-game-run-heading,
.rm-game-run-actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--rm-space-4);
}

.rm-game-run-card {
  border-color: var(--rm-border-strong);
}

.rm-game-run {
  border: 1px solid var(--rm-border-strong);
  margin-top: var(--rm-space-5);
}

.rm-game-run-hud {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 1px;
  background: var(--rm-border-strong);
}

.rm-game-run-hud > div {
  min-width: 0;
  padding: var(--rm-space-3) var(--rm-space-4);
  background: var(--rm-canvas);
}

/* Live press-rate meter: full scale is 300 presses/min, one tick every 30. */
.rm-game-cadence-meter {
  position: relative;
  height: 10px;
  background: var(--rm-surface);
  border-bottom: 1px solid var(--rm-border-strong);
}

.rm-game-cadence-meter > div {
  width: 0;
  height: 100%;
  background: var(--rm-primary);
  transition: width 80ms linear;
}

.rm-game-cadence-meter::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image: repeating-linear-gradient(to right,
    transparent 0, transparent calc(10% - 1px),
    color-mix(in srgb, var(--rm-ink) 45%, transparent) calc(10% - 1px),
    color-mix(in srgb, var(--rm-ink) 45%, transparent) 10%);
  pointer-events: none;
}

.rm-game-label,
.rm-overline {
  display: block;
  color: var(--rm-ink-muted);
  font-size: var(--rm-text-xs);
  font-weight: var(--rm-weight-semibold);
  letter-spacing: .04em;
  text-transform: uppercase;
}

.rm-game-run-hud strong {
  display: block;
  color: var(--rm-ink);
  font-size: var(--rm-text-lg);
}

.rm-game-run-hud .rm-game-peak {
  display: block;
  margin-top: var(--rm-space-1);
  color: var(--rm-ink-muted);
  font-size: var(--rm-text-xs);
  font-variant-numeric: tabular-nums;
}

.rm-game-canvas-wrap {
  position: relative;
  min-height: min(62vw, 38rem);
  max-height: 38rem;
  overflow: hidden;
  background: var(--rm-game-sky);
}

#game-run-canvas {
  display: block;
  width: 100%;
  height: 100%;
  min-height: min(62vw, 38rem);
}

.rm-game-run-controls {
  display: grid;
  grid-template-columns: 1fr auto minmax(14rem, 2fr);
  gap: var(--rm-space-4);
  align-items: flex-start;
  padding: var(--rm-space-4);
  border-top: 1px solid var(--rm-border-strong);
}

.rm-game-run-actions,
.rm-avatar-picker {
  display: flex;
  flex-wrap: wrap;
  gap: var(--rm-space-2);
}

.rm-avatar-choice {
  min-height: var(--rm-tap-min);
  padding: 0 var(--rm-space-3);
  color: var(--rm-ink);
  background: transparent;
  border: 1px solid var(--rm-border-strong);
  font: inherit;
}

.rm-avatar-choice.is-selected {
  color: #fff;
  background: var(--rm-primary);
  border-color: var(--rm-primary);
}

.rm-game-run-controls p {
  max-width: 36rem;
  font-size: var(--rm-text-sm);
}

.rm-game-run-controls kbd {
  border-radius: 0;
  color: var(--rm-ink);
  background: var(--rm-surface);
  border: 1px solid var(--rm-border-strong);
}

.rm-game-results {
  margin-top: var(--rm-space-6);
}

.rm-game-results-list {
  max-width: 34rem;
  margin: var(--rm-space-4) 0 var(--rm-space-5);
  padding-left: 1.8rem;
}

.rm-game-results-list li {
  display: flex;
  justify-content: space-between;
  gap: var(--rm-space-3);
  padding: var(--rm-space-2) 0;
  border-bottom: 1px solid var(--rm-border);
}

@media (max-width: 767.98px) {
  .rm-game-run-card,
  .rm-game-run-heading {
    align-items: stretch;
    flex-direction: column;
  }

  .rm-game-run-card .btn,
  .rm-game-run-heading .btn,
  .rm-game-run-actions .btn {
    width: 100%;
  }

  .rm-game-run-hud {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }

  .rm-game-run-controls {
    grid-template-columns: 1fr;
  }
}

@media (prefers-reduced-motion: reduce) {
  #game-run-canvas { opacity: .9; }

  .rm-game-cadence-meter > div { transition: none; }
}

/* ---- List filter bar (2026-08-28) ------------------------------------------
   ONE search-and-filter vocabulary for every list in both applications, replacing the
   three dialects that grew separately: the admin `.rm-list-toolbar`, the racer catalog's
   Bootstrap `.row.g-2`, and My Races' bare input. See design-language.md, "List filter
   bar".

   The bar sits INSIDE the list card, at its top — what it filters is the thing directly
   beneath it, and a control floating above the card does not say that. Line one is the
   search control; every line after it is one facet, named by an overline in the left
   gutter. Facet options are GET links, so a filtered list is linkable, survives a
   refresh, and needs no JavaScript (rules: backend-and-ui.md). */
.rm-filter-bar {
  display: flex;
  flex-direction: column;
  gap: var(--rm-space-3);
  padding: var(--rm-space-3) var(--rm-space-4);
  border-bottom: 1px solid var(--rm-border);
}

.rm-filter-top {
  display: flex;
  align-items: center;
  gap: var(--rm-space-3);
  flex-wrap: wrap;
}

.rm-filter-end {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: var(--rm-space-3);
  flex-wrap: wrap;
}

/* Only rendered when a text query is on: the chips carry their own counts, so an
   unfiltered "341 of 341" would be furniture. */
.rm-filter-count { font-size: var(--rm-text-sm); color: var(--rm-ink-muted); }

.rm-filter-clear {
  display: inline-flex;
  align-items: center;
  gap: var(--rm-space-2);
  min-height: var(--rm-tap-min);
  padding: 0 var(--rm-space-3);
  font-family: var(--rm-font-sans);
  font-size: var(--rm-text-sm);
  font-weight: var(--rm-weight-semibold);
  color: var(--rm-ink-muted);
  border: 1px solid transparent;
  background: transparent;
  text-decoration: none;
  cursor: pointer;
}

.rm-filter-clear:hover { color: var(--rm-ink); border-color: var(--rm-border); }
.rm-filter-clear:focus-visible { outline: var(--rm-focus-ring); outline-offset: 2px; }

/* The field and its submit are ONE object: the button's -1px margin folds the two borders
   into a single hairline. A box with a button labelled "Search" parked beside it is what
   read as rudimentary; this reads as an instrument. The magnifier INSIDE the field is
   decorative (aria-hidden) — the button carries the accessible name, and Enter submits
   the form either way. */
.rm-search {
  display: flex;
  align-items: stretch;
  flex: 1 1 20rem;
  min-width: 0;
  max-width: 26rem;
}

.rm-search-field { position: relative; flex: 1 1 auto; min-width: 0; display: flex; }

.rm-search-field .rm-search-glyph {
  position: absolute;
  left: var(--rm-space-2);
  top: 50%;
  transform: translateY(-50%);
  color: var(--rm-ink-muted);
  pointer-events: none;
}

/* Still a .form-control — the 16px floor, the --rm-surface fill and the focus ring all
   come from there. Only the room for the glyph is new. */
.rm-search .form-control { padding-left: 1.875rem; }

.rm-search-go {
  flex: none;
  width: var(--rm-tap-min);
  min-height: var(--rm-tap-min);
  margin-left: -1px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--rm-border-strong);
  background: transparent;
  color: var(--rm-primary);
  cursor: pointer;
}

.rm-search-go:hover { background: var(--rm-primary-soft); border-color: var(--rm-primary); }
.rm-search-go:focus-visible { outline: var(--rm-focus-ring); outline-offset: 2px; }

/* One facet per line. The overline in the gutter is what turns a loose row of buttons
   into a named dimension — without it nobody can tell which chips are alternatives to
   which. */
.rm-facet { display: flex; align-items: center; gap: var(--rm-space-2); flex-wrap: wrap; }

.rm-facet-label {
  flex: none;
  width: 5.25rem;
  font-family: var(--rm-font-heading);
  font-size: var(--rm-text-xs);
  font-weight: var(--rm-weight-bold);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--rm-ink-muted);
}

/* The chips ARE the counters — the Discovery tabs' one good idea, generalised off the
   underline tab strip nothing else in the portal used. */
.rm-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--rm-space-2);
  min-height: var(--rm-tap-min);
  padding: 0 var(--rm-space-3);
  border: 1px solid var(--rm-border);
  background: transparent;
  color: var(--rm-ink);
  font-family: var(--rm-font-sans);
  font-size: var(--rm-text-sm);
  font-weight: var(--rm-weight-semibold);
  white-space: nowrap;
  text-decoration: none;
}

.rm-chip:hover { border-color: var(--rm-border-strong); color: var(--rm-ink); }
.rm-chip:focus-visible { outline: var(--rm-focus-ring); outline-offset: 2px; }

.rm-chip-n {
  font-size: var(--rm-text-xs);
  font-weight: var(--rm-weight-bold);
  font-variant-numeric: tabular-nums;
  color: var(--rm-ink-muted);
  background: var(--rm-neutral-soft);
  padding: 1px 5px;
  margin-right: -4px;
}

/* Selection is never the tint alone (the badges rule): the fill, the border, the weight
   AND the inverted count block all move together. */
.rm-chip-on {
  background: var(--rm-primary-soft);
  border-color: var(--rm-primary);
  color: var(--rm-accent-ink);
  font-weight: var(--rm-weight-bold);
}

.rm-chip-on:hover { color: var(--rm-accent-ink); border-color: var(--rm-primary); }
.rm-chip-on .rm-chip-n { background: var(--rm-primary); color: #fff; }

/* A facet option with nothing behind it stays in place rather than disappearing — a set
   of alternatives that reshuffles itself between page loads cannot be aimed at. */
.rm-chip-empty { opacity: 0.45; }

/* Distance chips carry the same hue as the badge they filter, so the control and the data
   it acts on are visibly the same thing. Never the only signal: the label is the distance. */
.rm-chip-swatch { width: 8px; height: 8px; flex: none; }

@media (max-width: 767.98px) {
  /* The gutter label becomes a line of its own and the options scroll sideways. Wrapping
     four facet rows on a 390px screen pushes the list itself below the fold, which is the
     one thing a filter bar must not do. The last chip is deliberately cut by the card
     edge — that is the affordance saying there is more. */
  .rm-filter-bar { padding-left: 0; padding-right: 0; }
  .rm-filter-top { padding: 0 var(--rm-space-4); }
  .rm-search { max-width: none; }

  .rm-facet {
    display: block;
    white-space: nowrap;
    overflow-x: auto;
    padding: 0 var(--rm-space-4);
    scrollbar-width: none;
  }

  .rm-facet::-webkit-scrollbar { display: none; }
  .rm-facet-label { display: block; width: auto; padding-bottom: var(--rm-space-1); }
  .rm-chip { margin-right: var(--rm-space-2); }
}

/* ---- Group heads -----------------------------------------------------------
   A divider with a name, not a card per group: the list stays one scannable column and
   the heading sticks while you scroll through a busy season.

   2026-08-28: it was grey text on a grey band, which is most of what made these lists
   monotonous — the only structure a long list has, rendered as the least visible thing on
   it. Now the accent-soft ground, the accent ink and the 3px left rule the status banners
   already use. It lands every six or eight rows, which is where the eye needs it. */
.rm-group-head {
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--rm-space-3);
  padding: var(--rm-space-2) var(--rm-space-4);
  background: var(--rm-accent-soft);
  border-bottom: 1px solid var(--rm-border);
  border-left: 3px solid var(--rm-primary);
  font-family: var(--rm-font-heading);
  font-size: var(--rm-text-xs);
  font-weight: var(--rm-weight-bold);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--rm-accent-ink);
}

.rm-group-count {
  font-weight: var(--rm-weight-semibold);
  color: var(--rm-ink-muted);
  font-variant-numeric: tabular-nums;
}

/* ---- Race calendar (2026-09-04) --------------------------------------------
   The year heatmap above the race list: columns are weeks, rows are weekdays, Monday at
   the top. It replaces the My Season timeline, which was the same registrations rendered
   a second way behind a year picker. See design-language.md, "Race calendar".

   Cells are square like everything else here (--rm-radius is 0) and take their fill from
   the Distance ramp's -ink stop, NOT its -soft: a badge tints near-white so a word stays
   readable on top of it, and an 11px square filled at L 0.965 is a blank square. */
.rm-cal-panel {
  padding: var(--rm-space-6) var(--rm-space-4);
  border-bottom: 1px solid var(--rm-border);
}

/* The sizing container the fluid cell width is measured against. It sits on the section
   rather than on .rm-cal-scroll because container-type applies containment, and putting
   that on the scrollport itself is what breaks the sticky weekday gutter inside it. */
.rm-cal {
  container-type: inline-size;
  position: relative;
}

.rm-cal + .rm-cal { margin-top: var(--rm-space-6); }

.rm-cal-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--rm-space-3);
  margin-bottom: var(--rm-space-3);
}

.rm-cal-year {
  margin: 0;
  font-family: var(--rm-font-heading);
  font-size: var(--rm-text-lg);
  font-weight: var(--rm-weight-bold);
  letter-spacing: 0.02em;
  color: var(--rm-ink);
}

.rm-cal-count {
  font-size: var(--rm-text-xs);
  color: var(--rm-ink-muted);
  font-variant-numeric: tabular-nums;
}

/* 53 columns never shrink to fit a phone — at 390px that is a 5px cell nobody can aim at
   or read. The grid scrolls sideways instead, the same answer the filter bar's chip rows
   give. tabindex makes the scroller keyboard-reachable, which a plain overflow box is not. */
.rm-cal-scroll {
  overflow-x: auto;
  overscroll-behavior-x: contain;
  padding-bottom: var(--rm-space-2);
}

.rm-cal-scroll:focus-visible {
  outline: 2px solid var(--rm-primary);
  outline-offset: 2px;
}

/* `table-layout: fixed` takes its column widths from the FIRST row, and the first row here
   is the month header — twelve colspans. Widths declared on the day cells were being
   ignored, which is what made the squares rectangles. The <colgroup> states them once,
   for real, and every cell inherits its column's width. */
.rm-cal-grid {
  border-collapse: separate;
  border-spacing: var(--rm-cal-gap);
  table-layout: fixed;
  /* Without this the table shrinks to its container and `fixed` layout scales all 53
     columns down proportionally — on a 375px phone that is a 4px sliver per week, and the
     grid silently squashes instead of scrolling. max-content makes it take the width the
     colgroup asks for and lets .rm-cal-scroll do its job. */
  width: max-content;

  /* One square, sized to fill the card. `--rm-cal-weeks` comes in on the element (52 or 53
     — it is not a constant), and the grid solves for the cell that exactly consumes the
     container: gutter + weeks*cell + (weeks + 2) gaps. Below the 13px floor it stops
     shrinking and .rm-cal-scroll takes over, which is the phone case; the 30px ceiling is
     where a heatmap stops reading as one and starts reading as a wall of buttons. */
  --rm-cal-gap: 2px;
  --rm-cal-gutter: 3rem;
  --rm-cal-cell: clamp(
    13px,
    calc((100cqw - var(--rm-cal-gutter) - (var(--rm-cal-weeks) + 2) * var(--rm-cal-gap))
         / var(--rm-cal-weeks)),
    30px);
}

.rm-cal-col { width: var(--rm-cal-cell); }
.rm-cal-col-gutter { width: var(--rm-cal-gutter); }

/* The browser's own 1px default padding on th/td, which was adding 2px to every row and
   making a 13px cell measure 15 tall. */
.rm-cal-grid th,
.rm-cal-grid td { padding: 0; }

.rm-cal-month {
  padding: 0 0 var(--rm-space-1);
  font-family: var(--rm-font-heading);
  font-size: var(--rm-text-xs);
  font-weight: var(--rm-weight-semibold);
  color: var(--rm-ink-muted);
  text-align: left;
  white-space: nowrap;
}

/* The weekday gutter stays put while the weeks scroll under it — a heatmap whose row
   labels scroll off is a grid of unlabelled squares. */
.rm-cal-gutter {
  position: sticky;
  left: 0;
  /* Above the month headers, not below: a month label is left-aligned inside its colspan
     and long enough to run past the gutter, so a lower gutter let "Jun" bleed through the
     weekday labels once the grid was scrolled. */
  z-index: 2;
  width: var(--rm-cal-gutter);
  /* The inset lives HERE, not as padding on the scroller. A scrollport's left padding is
     still painted over by content scrolling underneath it, so a month label ran out to the
     left of the sticky gutter and sat next to the weekday names. Inside the gutter it is
     masked by the gutter's own background. */
  padding: 0 var(--rm-space-2) 0 var(--rm-space-3);
  background: var(--rm-canvas);
  font-family: var(--rm-font-heading);
  /* The ONE label in this system allowed under the 12px floor, and only as far as the row
     it names. All seven weekdays are drawn (not the reference's alternating four), so the
     label has to fit inside one cell's height — at the 13px floor a 12px label crowds its
     neighbours into an unreadable stack. It is a chart axis sitting directly against the
     row it names, and every cell repeats the full weekday in its title and its
     screen-reader text, so nothing depends on reading it at 9px. */
  font-size: clamp(9px, calc(var(--rm-cal-cell) * 0.72), var(--rm-text-xs));
  text-align: right;
  font-weight: var(--rm-weight-normal);
  /* Pinned to the cell height so a row is exactly as tall as the squares in it. */
  line-height: var(--rm-cal-cell);
  color: var(--rm-ink-muted);
  white-space: nowrap;
}

.rm-cal-cell {
  /* border-box so the hairline is INSIDE the 13px, matching the colgroup width a fixed
     table also measures as border-box. Left at content-box the cells came out 11x13. */
  box-sizing: border-box;
  height: var(--rm-cal-cell);
  padding: 0;
  line-height: var(--rm-cal-cell);
  /* Two independent greys composed into one fill, so neither can clobber the other: the
     day-of-month ramp (--rm-cal-shade) and the Friday/Saturday band (--rm-cal-band). A
     filled race cell overrides `background` outright further down and ignores both. */
  --rm-cal-shade: 0%;
  --rm-cal-band: 0%;
  background: color-mix(
    in srgb,
    var(--rm-ink) calc(var(--rm-cal-shade) + var(--rm-cal-band)),
    var(--rm-neutral-soft));
  border: 1px solid transparent;
}

/* The month ramp: six steps from the 1st to the last day, resetting at every boundary. The
   reset IS the signal — a month reads as one light-to-dark run, and where the run starts over
   is where the month turned. The hairline rule below says the same thing a second way, for
   anyone who does not read a gradient as an edge. Kept shallow on purpose: this grey has to
   stay clearly behind the distance ramp, which is the thing worth looking at. */
.rm-cal-s1 { --rm-cal-shade: 0%; }
.rm-cal-s2 { --rm-cal-shade: 2%; }
.rm-cal-s3 { --rm-cal-shade: 4%; }
.rm-cal-s4 { --rm-cal-shade: 6%; }
.rm-cal-s5 { --rm-cal-shade: 8%; }
.rm-cal-s6 { --rm-cal-shade: 10%; }

/* Days either side of Jan 1 / Dec 31: the column stays seven cells so the weekday rows
   line up, but there is no day there to draw. */
.rm-cal-void { background: transparent; }

.rm-cal-on { border-color: color-mix(in srgb, var(--rm-ink) 22%, transparent); }

.rm-cal-cell a {
  display: block;
  width: 100%;
  height: 100%;
}

.rm-cal-d1 { background: var(--rm-cal-1); }
.rm-cal-d2 { background: var(--rm-cal-2); }
.rm-cal-d3 { background: var(--rm-cal-3); }
.rm-cal-d4 { background: var(--rm-cal-4); }
.rm-cal-d5 { background: var(--rm-cal-5); }

/* A race whose category is still undecided has no band — it gets the neutral strong fill
   rather than being silently filed under "under 10K". */
.rm-cal-unknown { background: var(--rm-border-strong); }

/* Friday and Saturday: the two days almost every race here runs on. Their EMPTY cells are
   a deeper grey, so the band a reader scans is legible before anything is plotted on it.
   Scoped off the filled and void cells so it can never outrank a distance band. */
.rm-cal-row-raceday .rm-cal-cell:not(.rm-cal-on):not(.rm-cal-void) {
  --rm-cal-band: 9%;
}

.rm-cal-gutter-raceday { color: var(--rm-ink); }

/* Month boundaries: a hairline down the first column of each month. It rides the border the
   cells already carry, so it costs no layout — a `<col>` border would be ignored outright,
   since column borders only paint under `border-collapse: collapse`. */
.rm-cal-month-edge { border-left-color: var(--rm-border); }

th.rm-cal-month-edge {
  border-left: 1px solid var(--rm-border);
  padding-left: var(--rm-space-1);
}

.rm-cal-today {
  outline: 2px solid var(--rm-primary);
  outline-offset: 1px;
}

@media (hover: hover) {
  .rm-cal-on:hover { border-color: var(--rm-ink); }
}

/* The scripted tooltip (site.js). Appears on the first mouseover, where the native one it
   replaces waits out a browser delay — a grid read by sweeping across 365 squares cannot
   afford that. pointer-events: none so it can never sit between the pointer and the cell. */
.rm-cal-tip {
  position: absolute;
  z-index: 5;
  transform: translate(-50%, calc(-100% - 6px));
  padding: var(--rm-space-1) var(--rm-space-2);
  background: var(--rm-ink);
  color: var(--rm-canvas);
  font-size: var(--rm-text-xs);
  line-height: 1.35;
  white-space: nowrap;
  pointer-events: none;
}

.rm-cal-legend {
  display: flex;
  align-items: center;
  gap: var(--rm-space-2) var(--rm-space-4);
  flex-wrap: wrap;
  margin-top: var(--rm-space-4);
  font-size: var(--rm-text-xs);
  color: var(--rm-ink-muted);
}

.rm-cal-legend-label {
  font-family: var(--rm-font-heading);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.rm-cal-key-raceday {
  background: color-mix(in srgb, var(--rm-ink) 9%, var(--rm-neutral-soft));
}

.rm-cal-key {
  box-sizing: border-box;
  width: 13px;
  height: 13px;
  border: 1px solid color-mix(in srgb, var(--rm-ink) 22%, transparent);
}

/* The month-ramp swatches. Their own class rather than a fill on .rm-cal-key, because the
   distance swatches are .rm-cal-key + .rm-cal-dN and a background there would blank all five. */
.rm-cal-key-ramp {
  --rm-cal-shade: 0%;
  background: color-mix(in srgb, var(--rm-ink) var(--rm-cal-shade), var(--rm-neutral-soft));
}

.rm-cal-legend-ramp {
  display: inline-flex;
  gap: 2px;
}

/* Swatch and name are one unit, so a wrap can never separate a colour from its label. */
.rm-cal-legend-item {
  display: inline-flex;
  align-items: center;
  gap: var(--rm-space-2);
  white-space: nowrap;
}

@media (max-width: 767.98px) {
  /* Edge to edge, so the last week column is visibly cut — that is the affordance saying
     the grid scrolls, exactly as the filter bar's chip rows do. */
  .rm-cal-panel {
    padding-left: 0;
    padding-right: 0;
  }

  .rm-cal-head,
  .rm-cal-legend { padding: 0 var(--rm-space-4); }

  .rm-cal-gutter { left: 0; }
}

/* ---- Collapsible group heads (2026-09-04) ----------------------------------
   The Catalog's month heads are <details>/<summary>: the disclosure the browser already
   ships, open by default, keyboard-operable, and needing no JavaScript — the same reason
   the admin drawer is a checkbox (ADR-0012, rules: backend-and-ui.md).

   .rm-group-head keeps its own appearance rule above; these only make a <summary> stop
   looking like a <summary>. */
.rm-group-head { cursor: default; }

summary.rm-group-head {
  cursor: pointer;
  list-style: none;
  user-select: none;
}

summary.rm-group-head::-webkit-details-marker { display: none; }

.rm-group-name { flex: 1 1 auto; }

.rm-group-chevron {
  flex: none;
  transform: rotate(-90deg);
  transition: transform 120ms ease;
  color: var(--rm-accent-ink);
}

.rm-group[open] > summary .rm-group-chevron { transform: rotate(0deg); }

@media (hover: hover) {
  summary.rm-group-head:hover { background: color-mix(in srgb, var(--rm-primary) 14%, var(--rm-accent-soft)); }
}

summary.rm-group-head:focus-visible {
  outline: 2px solid var(--rm-primary);
  outline-offset: -2px;
}

@media (prefers-reduced-motion: reduce) {
  .rm-group-chevron { transition: none; }
}

/* ---- Matching-month memories -------------------------------------------- */
.rm-memory-head { margin-bottom: var(--rm-space-3); }

.rm-memory-item {
  display: grid;
  grid-template-columns: 80px minmax(0, 1fr);
  gap: var(--rm-space-4);
  padding: var(--rm-space-4) 0;
  border-top: 1px solid var(--rm-border);
}

.rm-memory-photo {
  width: 80px;
  height: 80px;
  object-fit: cover;
  border: 1px solid var(--rm-border-strong);
}

.rm-memory-copy { min-width: 0; }
.rm-memory-copy-full { grid-column: 1 / -1; }

.rm-memory-date {
  display: block;
  color: var(--rm-ink-muted);
  font-size: var(--rm-text-sm);
}

.rm-memory-name {
  display: block;
  width: fit-content;
  margin-top: var(--rm-space-1);
  color: var(--rm-ink);
  font-size: var(--rm-text-lg);
  font-weight: var(--rm-weight-semibold);
  line-height: var(--rm-leading-tight);
}

.rm-memory-note {
  margin-top: var(--rm-space-2);
  color: var(--rm-ink);
  font-size: var(--rm-text-body);
}

/* ---- Distance badge tones (2026-08-28) -------------------------------------
   The ramp declared in :root, applied. `.rm-badge-distance` keeps its plain construction
   as the fallback for a distance with no known length ("Undecided"); every real one gets
   its bucket's tone from DistanceOfferings.ToneClass. */
.rm-badge-d1 { background-color: var(--rm-dist-1-soft); border-color: var(--rm-dist-1-line); color: var(--rm-dist-1-ink); }
.rm-badge-d2 { background-color: var(--rm-dist-2-soft); border-color: var(--rm-dist-2-line); color: var(--rm-dist-2-ink); }
.rm-badge-d3 { background-color: var(--rm-dist-3-soft); border-color: var(--rm-dist-3-line); color: var(--rm-dist-3-ink); }
.rm-badge-d4 { background-color: var(--rm-dist-4-soft); border-color: var(--rm-dist-4-line); color: var(--rm-dist-4-ink); }
.rm-badge-d5 { background-color: var(--rm-dist-5-soft); border-color: var(--rm-dist-5-line); color: var(--rm-dist-5-ink); }

/* The chip swatch reuses DistanceOfferings.ToneClass rather than a second mapping, so it takes
   the tone class the badges take — but a swatch has no text to carry the hue, so it paints the
   INK, not the tint. */
.rm-chip-swatch.rm-badge-d1 { background-color: var(--rm-dist-1-ink); }
.rm-chip-swatch.rm-badge-d2 { background-color: var(--rm-dist-2-ink); }
.rm-chip-swatch.rm-badge-d3 { background-color: var(--rm-dist-3-ink); }
.rm-chip-swatch.rm-badge-d4 { background-color: var(--rm-dist-4-ink); }
.rm-chip-swatch.rm-badge-d5 { background-color: var(--rm-dist-5-ink); }
.rm-chip-swatch.rm-badge-distance { background-color: var(--rm-border-strong); }
