﻿/* RACE FACE brand system — FAST. FEARLESS. FOCUSED.
   SINGLE SOURCE: this file lives at shared/design-system.css and is linked into BOTH
   RaceSense.Web and RaceSense.Perform wwwroot at BUILD TIME (CopySharedDesignSystem csproj target;
   the wwwroot copies are git-ignored) — edit it HERE only.
   Aggressive, dark, monochrome + race red. Tokens + Bootstrap overrides + cockpit components.
   Fonts (Inter + Rajdhani) are loaded via <link> in App.razor. */
/* NO var() FALLBACKS IN THIS FILE. Measured 2026-09-06: of 25 fallbacks, ELEVEN disagreed with the token
   they were backing up - var(--data, #3080C8) was a BLUE where --data is #D7DDE5, a steel-white, and the
   same token was written correctly twelve lines earlier, so the file contradicted itself. Also
   var(--s1, #17181A) vs the real #161618, var(--hi, #F4F6F8) vs #F5F6F7, and var(--line, #2A2B2F) - a solid
   colour - vs the real rgba(255,255,255,.10).
   They were dormant only because every host defines the tokens, which made them a loaded gun for
   RaceSense.Control's independent stylesheet and for the planned RCL split: the day a token goes missing,
   a fallback paints something plausible-but-wrong instead of failing where somebody would notice.
   A missing token must fail loudly. UX audit DS4. */

/* CLASS PREFIXES. `rf-` FOR ALL NEW WORK. `rs-` IS FROZEN LEGACY. UX audit DS9.
   The split is chronological, not semantic - it falls either side of the Race Face rename - which is why
   `.rs-series-card` and `.rf-series-name` are both "series" components and neither name tells you anything
   about the other. Measured 2026-09-07: 203 distinct `rf-` classes, 60 `rs-`, 15 `auth-`.
   NOT renamed, deliberately: 128 `rs-` occurrences in this sheet plus their call sites across four apps is
   a large, entirely mechanical diff whose only benefit is tidiness, and every one of those edits is a
   chance to typo a class name into silence. The rule is enforced forwards instead - PrefixRuleIsWrittenDown
   pins the `rs-` count so the legacy set can only shrink.
   `auth-` is the sign-in/sign-up screens, which are a self-contained surface with their own layout; leave
   it as its own namespace. `perf-` belongs to RaceSense.Perform's own sheet, not this one. */
:root {
  /* Override Bootstrap Reboot's `scroll-behavior:smooth` — combined with Blazor's same-document scroll-reset
     on navigation it ANIMATED the scroll-to-top (you'd see the list scroll up before a page opened). Instant. */
  scroll-behavior: auto;

  /* Z-INDEX, NAMED. UX audit DS2: the sheet carried ad-hoc 3, 5, 10, 20, 30, 100, 1070, 1080, 1100 with
     nothing saying which was meant to sit above which. These are the SAME numbers, named - adopting them
     moves nothing - so the ordering becomes something you can read instead of something you infer by
     grepping for the biggest integer. */
  --z-raised: 10;      /* lifted within its own card */
  --z-sticky: 30;      /* sticky headers and column headers */
  --z-bar: 100;        /* the app chrome: top bar, bottom tab bar */
  --z-toast: 1070;     /* transient messages over the chrome */
  --z-progress: 1080;  /* the page-top progress bar */
  --z-overlay: 1100;   /* the reconnect overlay - above everything, by definition */

  /* DURATION. Eight different values were in use; these are the three that account for almost all of them
     (.15s x7, .12s x3, .22s x1). The long ones belong to specific animations - a 1.4s shimmer, a 1.1s
     indeterminate bar - and are left where they are: they are the animation's identity, not a transition. */
  --dur-fast: .12s;
  --dur-base: .15s;
  --dur-slow: .22s;

  /* FOCUS RING. One ring, one place to change it. It was written out four times with the same 2px/2px
     geometry and only the colour differing.

     IT STAYS ON --effort, NOT --effort-text, AND THAT IS A DECISION RATHER THAN AN OVERSIGHT (UX audit
     L3). A focus ring is not text: it is a user-interface indicator under WCAG 1.4.11, whose bar is 3:1,
     not 4.5:1. #E30613 clears that on --bg (4.06), --s1 (3.70) and --s2 (3.38) and misses it on --s3
     (2.90). Two things make the miss theoretical today rather than live: --focus-offset draws the ring
     2px OUTSIDE the control, so the colour it is judged against is the CONTAINER's background and not
     the control's own; and --s3 is used in exactly four places in this sheet — .rs-av's 32px circle,
     .rf-card-thumb, .rf-photo-thumb and .rf-upload-bar's track — none of which is focusable or contains
     a focusable child (the card studio's buttons live in .rf-card-meta, which falls through to --s1).
     Against that, the ring is the accent, deliberately: it is the one piece of chrome that says which
     product you are in while you keyboard through it, and lightening it to clear a bar nothing currently
     stands on would trade a real brand signal for a hypothetical.
     If a focusable control is ever placed on an --s3 surface, this is the line to revisit and 2.90 is
     the number that makes it a defect — recorded here so that is a lookup and not a rediscovery. */
  --focus-ring: 2px solid var(--effort);
  --focus-offset: 2px;

  --bg: #0A0A0A;       /* brand black */
  --s1: #161618;       /* card — charcoal */
  --s2: #1E1F22;       /* raised */
  --s3: #2A2B2F;       /* overlay */
  --line: rgba(255,255,255,.10);
  --line-2: rgba(255,255,255,.16);

  /* DEPTH. The flat fields these sit over are still the truth — every one of the tokens above is unchanged
     and every surface still declares its own background-COLOR. These are background-IMAGES layered on top,
     so a host that does not adopt them (RaceSense.Control, with its own sheet) loses the sheen and keeps
     the colour, and nothing anywhere depends on a gradient resolving.

     THREE MOVES, in the order they matter: the surround gets a vertical ramp and a wide lift above the
     fold, so the page reads as lit from above rather than as a flat black field; panels get their own
     top-down sheen plus a hairline top edge, which is what makes them sit ON the background instead of
     reading as holes cut in it; and a very faint grain breaks up the banding that large, near-black
     gradients show on cheap panels.

     THE GRAIN IS AN INLINE SVG — no asset, no request, no build step. It is at 5.5%, which is invisible as
     texture and visible as depth. At ten times that it reads as television static AND lifts the surround
     lighter than the panels, which inverts the whole effect; if it is ever turned up, that is the failure
     to watch for. */
  --grad-bg:
    url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20width%3D%27140%27%20height%3D%27140%27%3E%3Cfilter%20id%3D%27n%27%3E%3CfeTurbulence%20type%3D%27fractalNoise%27%20baseFrequency%3D%27.9%27%20numOctaves%3D%271%27%20stitchTiles%3D%27stitch%27%2F%3E%3CfeColorMatrix%20type%3D%27saturate%27%20values%3D%270%27%2F%3E%3C%2Ffilter%3E%3Crect%20width%3D%27140%27%20height%3D%27140%27%20filter%3D%27url%28%23n%29%27%20opacity%3D%27.055%27%2F%3E%3C%2Fsvg%3E"),
    radial-gradient(120% 70% at 50% -14%, rgba(255,255,255,.055) 0%, rgba(255,255,255,0) 55%),
    linear-gradient(180deg, #0E0E11 0%, #09090B 45%, #050506 100%);
  /* IN PIXELS, NOT PERCENT, and that is the whole point of them. A percentage sheen is a fraction of the
     panel, so it scales with height: on /tools' ~90px cards it landed right, and on /more's single 684px
     card the same stops spread the highlight over 424px, which is not a highlight, it is a slightly paler
     card. A physical band gives every panel the same lit-from-above edge whatever its size — which is what
     the reference actually does. */
  --grad-s1:
    linear-gradient(180deg, rgba(255,255,255,.055) 0px, rgba(255,255,255,.012) 26px, rgba(255,255,255,0) 68px);
  --grad-s2:
    linear-gradient(180deg, rgba(255,255,255,.075) 0px, rgba(255,255,255,0) 60px);
  --edge-top: inset 0 1px 0 rgba(255,255,255,.075), 0 1px 3px rgba(0,0,0,.5);
  --hi: #F5F6F7;
  --mid: #B7BCC4;
  --low: #80858E;      /* steel gray */
  --effort: #E30613;   /* RACE RED — primary / you / interactive */
  --effort-ink: #FFFFFF;
  /* RACE RED, AS SMALL TEXT. UX audit L3. --effort is a FILL colour and cannot be read at body sizes:
     measured as text (WCAG 2.x relative luminance) #E30613 scores 4.06 on --bg, 3.70 on --s1 (the tab
     bar), 3.38 on --s2 and 2.90 on --s3 — so it misses AA's 4.5:1 on ALL FOUR surfaces, and on --s3 it
     misses even the 3:1 large-text allowance. Every small red string in the product was below AA by
     construction, not by accident: no amount of care at a call site could have fixed it.

     #FF5A54 measures 6.45 / 5.89 / 5.37 / 4.61 against the same four, so the worst case in the product
     clears 4.5:1 with headroom. It also clears every TINTED backdrop, which the four surface tokens do
     not describe on their own — the composites are .rs-you rgba(227,6,19,.10) over --s1 = #2b1418 (5.63),
     .rs-invitebar .12 over --bg = #240a0b (6.09) and its .18 hover = #31090c (5.82), the tab bar's own
     #0E0E10 (6.28), and mobile's .rf-pill-warn .14 over --s1 = #331417 (5.46).

     WHY #FF5A54 AND NOT #F4676B, which has slightly more headroom (4.71 on --s3). In CIELAB, #FF5A54 is
     BOTH nearer the brand red (dE00 14.1 vs 17.3 from #E30613) AND further from --bad #F2545B, which
     already means "error" (dE00 4.5 vs 3.8). #F4676B lands at L*61.7 C*60.1 h25.1 against --bad's L*58.0
     C*67.8 h26.1 — near enough that the accent and the error colour would read as one, collapsing two
     meanings into a single red. #FF5A54 keeps chroma 72.9 against the brand's 92.0, so it stays a
     saturated race red rather than a washed pink. Note HSL disagrees and HSL is wrong here: it ranks
     #F4676B closer on hue because HSL hue is not perceptually uniform.

     THIS TOKEN IS FOR `color:` ON SMALL TEXT ONLY — below 18.66px bold / 24px regular. --effort keeps
     #E30613 for every fill, border, box-shadow and large display type, and those are checked: the four
     large-type uses left on --effort (.rs-series-year 33px, .rs-brand .dot 21px/700, .auth-brand .dot
     30px, .auth-tagline em 40px→28px, .rf-empty-icon 26px) all clear the 3:1 large-text bar on the
     surfaces they actually sit on. The brand red is not being retired; it is being kept off body copy.

     STILL OUTSTANDING, deliberately not swept here: the hover literals #ff3b47 (`a:hover`,
     .rs-series-link:hover) and #ff2b38 (.btn-primary:hover) measure 4.02 and 3.81 at worst, so a link's
     HOVER is now slightly darker than its resting state and still under AA on --s3. They are literals,
     not this token, and re-tuning an interaction state is a design call with its own verify round.
     Likewise ~30 inline `style="color:var(--effort)"` uses in RaceSense.Mobile .razor markup: same
     defect, but markup rather than a stylesheet, so out of this pass's scope. */
  --effort-text: #FF5A54;
  --data: #D7DDE5;     /* metrics / accents — steel-white */
  --good: #3FD17A;
  --warn: #F5B642;
  --bad: #F2545B;
  --gold: #F5B642; --silver: #C2C6D2; --bronze: #CD7F46;

  /* TYPE SCALE. UX audit DS2, the half the z-index/duration pass deliberately left alone because it
     cannot be done without choosing which values to drop.

     MEASURED (this file, comments excluded): 133 font-size declarations across 24 distinct px values.
     Four of them carry two thirds of the sheet — 12px x27, 13px x26, 14px x18, 11px x16 = 87 — and the
     rest is drift: half-pixels at 11.5/12.5/13.5, and a 15/16/17/18/19/20/21/22 run in which no value
     appears more than five times. These seven steps are EXTRACTED from that distribution, not invented:
     the bottom four are the four modes and are non-negotiable (collapsing 12 and 13 would move 26 or 27
     declarations on its own), and the top three are the modes of the three clusters above them.

     WHY THE STEPS CROWD AT THE BOTTOM AND SPREAD AT THE TOP: 11-14px is where interface text lives and
     one pixel is a real distinction there — a caption really is a different thing from a row label. Above
     17px the sheet is titles and icon glyphs, where the spacing between steps can be proportional. */
  --fs-2xs: 11px;   /* x16 — uppercase micro-labels, the tab bar, chips */
  --fs-xs:  12px;   /* x27 — captions; the largest single cluster in the sheet */
  --fs-sm:  13px;   /* x26 — secondary interface text */
  --fs-md:  14px;   /* x18 — body */
  --fs-lg:  17px;   /* x5  — card and section titles */
  --fs-xl:  22px;   /* x4  — topbar icon glyphs */
  --fs-2xl: 26px;   /* x4  — page headings */

  /* SPACING SCALE. Same method: 290 px lengths in padding / margin / gap across 26 distinct values, of
     which 8px x46, 10px x46, 6px x31, 12px x25, 14px x21, 16px x18 and 4px x18 are the dense band. The
     even 2-16 run below IS the distribution — the odd values (3, 5, 7, 9) are 34 near-misses of it, and
     everything from 18px up is genuine one-off container padding with no mode worth extracting. */
  --sp-3xs: 2px;    /* x10 */
  --sp-2xs: 4px;    /* x18 */
  --sp-xs:  6px;    /* x31 */
  --sp-sm:  8px;    /* x46 */
  --sp-md:  10px;   /* x46 */
  --sp-lg:  12px;   /* x25 */
  --sp-xl:  14px;   /* x21 */
  --sp-2xl: 16px;   /* x18 */

  /* WHAT WAS ACTUALLY APPLIED, AND WHAT WAS NOT. Only the EXACT substitutions are in the file: 100 of the
     133 font-sizes, and 200 spacing lengths of the 215 that are exact members (of 290 counted). Every one
     of them resolves to the byte it replaced — checked by rewriting each var() back to its px value and
     diffing the whole sheet against the previous revision, which came back identical. This change moves
     ZERO pixels, and that proof is the only reason it is safe to make mechanically across four apps in
     one commit.

     Everything that would MOVE is left as a literal for the owner to approve, because re-tuning ~70
     values silently is not a refactor, it is a redesign wearing one:

       TYPE, 28 declarations across 13 values
         9px  -> --fs-2xs  +2px (1)     16px -> --fs-lg  +1px (4)     20px -> --fs-xl  +2px (3)
         10px -> --fs-2xs  +1px (1)     18px -> --fs-lg  -1px (2)     21px -> --fs-xl  +1px (1)
         11.5 -> --fs-xs  +0.5px (2)    19px -> --fs-lg  -2px (3)     24px -> --fs-2xl +2px (1)
         12.5 -> --fs-sm  +0.5px (2)    15px -> --fs-md  -1px (6)     28px -> --fs-2xl -2px (1)
         13.5 -> --fs-md  +0.5px (2)
       SPACING, 41 lengths across 7 values — all +1px: 1px(5), 3px(8), 5px(9), 7px(6), 9px(11),
         11px(1), 13px(1).

     Biggest single move: 2px, on five type values. The 15px row is the one to look at hardest and the
     reason this is a question rather than a sweep: six declarations, and .rf-legal p/li is among them —
     that 15px is a RECORDED decision (the consent documents a parent reads on a child's behalf were
     12px prose beside 16px bullets, and were deliberately unified upward). Dropping it to 14px to fit
     the scale would undo that. The honest options are to keep it literal or to give it its own step;
     both are the owner's call, not this pass's.

     ABOVE THE SCALE, left literal on purpose: type 28/30/33/38/40px (5 declarations — the auth tagline,
     the auth wordmark, the series year, the hero number) and spacing 18-44px (30 lengths). Adding steps
     for singletons would be authoring a scale rather than extracting one. Negative lengths (-1, -4,
     -16px, 4 of them) are layout corrections — hit-area insets and bleed margins — not spacing.

     SHORTHANDS ARE ALL-OR-NOTHING, AND THAT IS WHERE THE OTHER 15 WENT. `padding: 8px 16px` becomes two
     tokens or stays two literals; a half-tokenised shorthand reads as though the literal half were a
     deliberate exception to the scale, when it is only an accident of which neighbour it was written
     next to. So 13 exact members keep their literal because a sibling is off-scale (`padding: 9px 14px
     8px`, `margin: 26px 0 6px`) and 2 because a sibling is negative (`margin: -4px 0 16px`). They come
     back for free with the phase-two values beside them. Declarations containing calc(), var(), a
     percentage or `auto` are skipped whole for the same reason — six more exact members sit inside those,
     mostly the safe-area-inset paddings on the tab bar.

     SHADOWS ARE DELIBERATELY NOT TOKENISED. Measured on the same pass: 16 box-shadow declarations, 14
     DISTINCT values. There is nothing to extract — the only repeats are the menu panel's dropdown shadow
     (x2) and the invalid-field ring (x2). Every other one is a bespoke elevation for a single component:
     0 30px 90px -30px on the generating-avatar box, 0 8px 16px -6px under the topbar, a red 0 10px 26px
     on a hovered series card, three `inset` rules that are indicator bars rather than shadows at all.
     Naming these would mean INVENTING an elevation set and then bending fourteen hand-tuned values onto
     it — the opposite of what was done above, where the tokens were read off a distribution that already
     existed. If shadows ever need a system, that is a design exercise with a verify round. */

  --r-md: 10px; --r-lg: 14px;
  --font-display: 'Rajdhani', 'Inter', system-ui, sans-serif;  /* condensed sporty headlines */
}

html, body {
  background: var(--bg);
  /* Layered over the solid --bg, never instead of it. `fixed` anchors the ramp to the viewport so a long
     page does not stretch it into invisibility; the grain tile is the only layer that repeats. */
  background-image: var(--grad-bg);
  background-size: 140px 140px, 100% 100%, 100% 100%;
  background-repeat: repeat, no-repeat, no-repeat;
  background-attachment: fixed, fixed, fixed;
  color: var(--hi);
  font-family: 'Inter', -apple-system, 'Segoe UI', Roboto, sans-serif;
  font-variant-numeric: tabular-nums;
}
.num { font-variant-numeric: tabular-nums; }
h1, h2, h3, h4, h5 { color: var(--hi); font-family: var(--font-display); font-weight: 600; letter-spacing: .015em; }
h1, h2 { text-transform: uppercase; letter-spacing: .03em; font-weight: 700; }
/* Unified paragraph typography — every <p> shares one compact, consistent size/leading
   (p.small beats Bootstrap's .small so helper paragraphs don't render a size larger). */
/* Body prose and captions were ONE size (12px), so a paragraph and its caption were
   typographically identical and neither read as body copy. Split: prose 14px, captions stay 12px.
   255 of 471 <p> in the app carry .small and are unaffected by this. */
p { font-size: var(--fs-md); line-height: 1.55; }
p.small { font-size: var(--fs-xs); line-height: 1.5; }
/* EVERY LINK IN THE PRODUCT, and the single largest instance of UX audit L3 — links carry no underline
   here, so colour is doing all the work of saying "this is a link" and it was doing it at 3.38:1 on a
   raised card. */
a { color: var(--effort-text); text-decoration: none; }
/* NOT MOVED, and it is the one loose end this pass leaves visible: #ff3b47 measures 5.63 / 5.14 / 4.68 /
   4.02, so hovering a link now DARKENS it slightly and still misses AA on --s3. It is a literal rather
   than this token, and picking a hover that reads as "brighter" against a resting state that just moved
   is a design call with its own verify round, not a find-and-replace. */
a:hover { color: #ff3b47; }
.text-secondary { color: var(--mid) !important; }
.text-muted { color: var(--low) !important; }
.text-success { color: var(--good) !important; }
.text-danger { color: var(--bad) !important; }
.text-warning { color: var(--warn) !important; }
.small, .form-text { color: var(--mid); }
/* ...but NOT inside a Bootstrap alert. An alert paints a pale tinted background and carries its own dark
   foreground in --bs-alert-color; this sheet loads after Bootstrap and `.small` has the same specificity as
   `.alert`, so a bare `small` on an alert repainted the text grey on pale green — about 1.4:1, effectively
   invisible. Hit the Garmin and Wahoo result banners on /me, which are `alert alert-* py-2 small`. */
.alert.small, .alert .small, .alert .form-text { color: var(--bs-alert-color); }

/* Series "race cards" on the standings landing */
.rs-series-card { display: flex; flex-direction: column; gap: var(--sp-md); height: 100%; padding: var(--sp-2xl); background: var(--s1); border: 1px solid var(--line); border-radius: var(--r-lg); color: var(--hi); text-decoration: none; transition: border-color var(--dur-base), transform var(--dur-base), box-shadow var(--dur-base); }
.rs-series-card:hover { border-color: var(--effort); transform: translateY(-2px); box-shadow: 0 10px 26px rgba(227,6,19,.16), 0 8px 24px rgba(0,0,0,.4); }
.rs-series-top { display: flex; justify-content: space-between; align-items: center; color: var(--data); }
.rs-series-year { font-family: var(--font-display); color: var(--effort); font-size: 33px; font-weight: 700; line-height: 1; letter-spacing: 0; }
.rs-series-top .bi { font-size: var(--fs-xl); opacity: .45; }
.rs-series-logo { width: 48px; height: 48px; object-fit: cover; border-radius: 8px; display: block; }
.rs-series-card.past .rs-series-logo { opacity: .72; }
/* Past seasons read as secondary so the current (latest) season's cards stand out. */
.rs-series-card.past { opacity: .82; }
.rs-series-card.past .rs-series-top { color: var(--mid); }
.rs-series-card.past .rs-series-year { color: var(--mid); font-weight: 700; }
.rs-series-card.past:hover { opacity: 1; }
.rs-series-name { font-size: var(--fs-lg); font-weight: 600; line-height: 1.25; color: var(--hi); }
.rs-series-chips { display: flex; flex-wrap: wrap; gap: var(--sp-xs); margin-top: auto; }
.rs-series-chips span { font-size: var(--fs-2xs); font-weight: 600; padding: 3px 10px; border-radius: 12px; background: rgba(255,255,255,.06); color: var(--data); }
.rs-series-foot { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-xs); font-size: var(--fs-xs); color: var(--low); border-top: 1px solid var(--line); padding-top: var(--sp-md); }
.rs-series-body { display: flex; flex-direction: column; gap: var(--sp-md); flex: 1; color: var(--hi); text-decoration: none; }
.rs-series-go { color: var(--mid); white-space: nowrap; display: inline-flex; align-items: center; gap: var(--sp-2xs); }
.rs-series-go:hover { color: var(--effort-text); }
.rs-series-linked { display: inline-flex; align-items: center; gap: var(--sp-2xs); color: var(--good); font-weight: 700; }
.rs-series-link { display: inline-flex; align-items: center; gap: var(--sp-2xs); color: var(--data); font-weight: 600; }
.rs-series-link:hover { color: #ff3b47; }
.rs-series-foot-left { display: inline-flex; align-items: center; gap: var(--sp-sm); flex-wrap: wrap; min-width: 0; }
.rs-series-dep { display: inline-flex; align-items: center; gap: var(--sp-2xs); color: var(--low); font-weight: 600; }

/* Ride-map overlay controls (gradient / speed / elevation + direction) */
.rf-map-controls { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-sm) var(--sp-2xl); margin-top: var(--sp-sm); font-size: var(--fs-sm); color: var(--low); }
.rf-map-controls label { display: inline-flex; align-items: center; gap: 5px; cursor: pointer; }
.rf-map-controls input[type=checkbox] { accent-color: var(--data); margin: 0; }
.rf-legend { display: inline-flex; align-items: center; gap: var(--sp-xs); font-size: var(--fs-2xs); color: var(--low); }
.rf-legend-bar { width: 110px; height: 8px; border-radius: 4px; border: 1px solid var(--line); }
.rf-arrow { background: transparent !important; border: 0 !important; }

/* Collapsible whose summary is styled as a button (hide the default disclosure marker) */
.rf-collapse > summary { list-style: none; cursor: pointer; }
.rf-collapse > summary::-webkit-details-marker { display: none; }
.rf-collapse > summary::marker { content: ""; }
.rf-chev { transition: transform var(--dur-base); }
.rf-collapse[open] > summary .rf-chev { transform: rotate(90deg); }
/* "N series participated" toggle. Reads as a BUTTON, not a caption: as link-coloured text with an 8px
   chevron it was routinely missed, and the series list behind it looked like it did not exist. */
.rf-series-sum {
    display: inline-flex; align-items: center; gap: var(--sp-sm); min-height: 40px; padding: var(--sp-sm) var(--sp-xl);
    background: var(--s1); border: 1px solid var(--line-2); border-radius: 999px;
    color: var(--hi); font-size: var(--fs-sm); font-weight: 500; text-transform: none;
}
.rf-series-sum:hover { border-color: var(--effort); color: var(--effort-text); }
/* The chevron is a 12px icon glyph, so it is judged at 3:1 rather than 4.5:1 — and #E30613 fails even
   that on --s3 (2.90). It moves for the lower bar, not the higher one. */
.rf-series-sum .rf-chev { color: var(--effort-text); font-size: var(--fs-xs); }
/* Once open the pill has done its job and the divider carries the separation instead — a filled pill
   sitting directly on top of the list it opened reads as part of the list. */
.rf-collapse[open] > .rf-series-sum {
    background: transparent; border-color: transparent; border-top: 1px solid var(--line);
    border-radius: 0; padding: var(--sp-md) 0 var(--sp-sm); width: 100%;
}

/* Row of action collapses (Add New Rider / Link an existing rider) sitting side by side;
   an open one spans the full width so its form isn't cramped, pushing the other to the next line. */
.rf-actionrow > details[open] { flex-basis: 100%; }

/* Surfaces */
.card, .rs-card { background: var(--s1); border: 1px solid var(--line); border-radius: var(--r-lg); color: var(--hi); }
.list-group-item { background: var(--s1); border-color: var(--line); color: var(--hi); }
.list-group-item-action:hover { background: var(--s2); }

/* Tables */
.table { color: var(--hi); --bs-table-bg: transparent; --bs-table-color: var(--hi); }
.table > :not(caption) > * > * { background: transparent; color: var(--hi); border-bottom-color: var(--line); }
.table thead th { color: var(--low); font-weight: 500; font-size: var(--fs-2xs); text-transform: uppercase; letter-spacing: .04em; border-bottom-color: var(--line-2); }
/* Compact variant — fits the Race Results 5 columns comfortably on narrow phones. */
.table.rs-tight { font-size: var(--fs-md); }
.table.rs-tight > :not(caption) > * > * { padding: .35rem .4rem; }
.table.rs-tight thead th { font-size: var(--fs-xs); }

/* Forms */
.form-control, .form-select {
  background: var(--s1); color: var(--hi);
  border: 1px solid var(--line-2); border-radius: var(--r-md);
}
.form-control::placeholder { color: var(--low); }
.form-control:focus, .form-select:focus {
  background: var(--s1); color: var(--hi); border-color: var(--effort);
  box-shadow: 0 0 0 3px rgba(227,6,19,.25);
}
.form-select option { background: var(--s2); color: var(--hi); }
/* Bootstrap's :disabled background is a LIGHT grey, which on this dark shell renders a disabled control as
   near-white text on near-white. Only shows up on a screen that disables a control until a prior choice is
   made — the card studio's round picker was the first. */
.form-control:disabled, .form-select:disabled,
.form-control[readonly], .form-select[disabled] {
  background-color: var(--s2); color: var(--low); border-color: var(--line); opacity: 1;
}
/* Keyboard focus ring — visible on the dark theme (mouse clicks don't trigger :focus-visible). */
a:focus-visible, button:focus-visible, .btn:focus-visible, [role="button"]:focus-visible, summary:focus-visible {
  outline: var(--focus-ring); outline-offset: var(--focus-offset); border-radius: 5px;
}
/* TEXT FIELDS GET THE RING TOO. Bootstrap sets `outline: 0` on .form-control:focus, and the rule above
   never listed input/select/textarea — so a focused field was signalled by ONE PIXEL of border colour and a
   3px glow.

   MEASURED, because the numbers decide which of two different problems this is. The focused border #E30613
   is 3.70:1 against the field and 4.06:1 against the page, so SC 1.4.11 (non-text contrast, AA) PASSES —
   the field is not unmarked. What is weak is the CHANGE: resting border rgba(255,255,255,.16) to #E30613 is
   2.29:1, under the 3:1 that SC 2.4.13 (focus appearance) asks of a state change. The 3px glow contributes
   nothing at all - rgba(227,6,19,.25) over --s1 composites to 1.19:1, invisible.

   An outline fixes the change by adding AREA rather than by finding a louder colour: two pixels that were
   not there before, offset clear of the border, is a far larger delta than recolouring one. The border
   recolour stays for the pointer case. Same shape as the fix Perform's charts and fields got. */
.form-control:focus-visible, .form-select:focus-visible, textarea:focus-visible {
  outline: var(--focus-ring); outline-offset: var(--focus-offset);
}
.form-label, .form-check-label { color: var(--mid); }
.form-check-input { background: var(--s2); border-color: var(--line-2); }
.form-check-input:checked { background: var(--effort); border-color: var(--effort); }

/* Buttons */
.btn-primary { background: var(--effort); border-color: var(--effort); color: var(--effort-ink); font-weight: 500; }
.btn-primary:hover { background: #ff2b38; border-color: #ff2b38; color: var(--effort-ink); }
.btn-outline-primary { color: var(--effort-text); border-color: var(--effort); }
.btn-outline-primary:hover { background: var(--effort); color: var(--effort-ink); }
.btn-link { color: var(--data); }

/* Badges */
.badge.text-bg-primary { background: var(--effort) !important; color: var(--effort-ink) !important; }
.badge.text-bg-light { background: var(--s2) !important; color: var(--mid) !important; }

/* ---- Cockpit components ---- */
.rs-label { font-size: var(--fs-xs); letter-spacing: .04em; text-transform: uppercase; color: var(--low); }

.rs-chips { display: flex; gap: var(--sp-sm); flex-wrap: wrap; }
/* CARD PRIMITIVE. Lived only in RaceSense.Mobile/wwwroot/app.css, which the web app does not load - while
   StartingGrid.razor uses .rf-card in BOTH heads from byte-identical markup. So the starting grid rendered
   as a bare block on the web and as a card on the phone, from the same file, and nothing said why.

   Found while measuring the Web/Mobile duplication for H16/DS1, and it is the shape of defect that split
   pays for: two heads, one component, two CSS vocabularies, and the divergence only visible by opening both.

   The three var() fallbacks the rule carried in app.css are dropped, not copied. This file is scanned by
   DesignTokensHaveNoFallbacksTests (UX audit DS4) and a fallback here would fail it - correctly: a missing
   token must break loudly rather than paint something plausible. */
.rf-card { background: var(--s2); border: 1px solid var(--s3); border-radius: 12px; padding: var(--sp-2xl); }
.rf-card-warn { border-color: var(--effort); }
/* TOUCH TARGET, WITHOUT MOVING ANYTHING. Measured at 375px: a chip is 32px tall with an 8px gap on both
   axes, against the 44px target this sheet applies in 10 other places. Growing the chip itself would
   reflow 22 pages - .rs-chip is used in 23 files - and on standings alone it would push the 5-line chip
   row from 192px to about 260px, above the fold, on the surface a parent uses at a race venue.

   So the HIT AREA grows and the box does not. inset:-4px consumes exactly the 8px gap, so adjacent chips
   end where their neighbour begins: 40px of tappable height and width, no overlapping targets, and not
   one pixel of layout change. 40px clears WCAG 2.5.8 (24px, AA) and falls 4px short of 2.5.5 (AAA); the
   remaining 4px is not reachable without reflowing every page that uses a chip.

   NOTE: RaceSense.Control uses .rs-chip and has its own stylesheet with no copy task, so it does NOT
   receive this. Its chips stay at a 32px target - acceptable for an internal super_admin console. */
.rs-chip { position: relative; font-size: var(--fs-xs); padding: var(--sp-xs) var(--sp-lg); border-radius: 999px; border: 1px solid var(--line-2); color: var(--mid); background: transparent; }
/* -5px, not -4px: inset resolves against the PADDING box, so the chip's 1px border eats 1px of the
   expansion on each side. -4px measured 38px of hit height, not 40. -5px reaches exactly 4px past the
   border box on each side, which is precisely the 8px gap - targets meet, never overlap. */
.rs-chip::after { content: ""; position: absolute; inset: -5px; border-radius: inherit; }
.rs-chip.on { background: var(--effort); border-color: var(--effort); color: var(--effort-ink); }
/* SECTION TABS, DELIBERATELY NOT PILLS. A pill on this page means a FILTER - a category, a school board,
   a round - and the reader picks one from a set. Athletes/Schools is not that: it chooses which table the
   page is showing. Built from .rs-chip they sat directly above ten category pills in the same shape and
   size and read as the first row of them, so the one control that changes what the page IS looked like
   another filter. Underlined tabs are the conventional signal for "these are sections", and being a
   different SHAPE is the whole point - a differently coloured pill would still read as a pill.

   The rule sits under the row, so the tabs are attached to the content below rather than floating in it. */
.rf-vtabs { display: flex; gap: var(--sp-2xs); border-bottom: 1px solid var(--line); margin: 0 0 var(--sp-xl);
  /* The row scrolls, so the PAGE never does. The tabs are nowrap by design - a wrapped tab reads as two
     tabs - and with nothing containing them the standings screen scrolled sideways on every phone narrower
     than a Pro Max. This is the STRUCTURAL GUARD, not the fix: the four labels below fit every phone on
     their own, and this is what keeps a future fifth tab scrolling the row instead of the page.
     Deliberately NOT hiding the scrollbar - if the row ever does overflow it is the sole signal that there
     is more to the right. */
  overflow-x: auto; }
.rf-vtab {
    appearance: none; background: none; border: 0; cursor: pointer;
    font-size: var(--fs-md); font-weight: 600; letter-spacing: .01em;
    color: var(--low); padding: 9px var(--sp-lg) 8px; margin-bottom: -1px;
    border-bottom: 2px solid transparent; text-decoration: none; white-space: nowrap;
}

/* NO NARROW-WIDTH STEP, AND THAT IS THE FIX. The row used to shrink twice - 13px at 430 and 12px at 385 -
   because it carried three two-word labels. A fourth tab arrived with the qualification board and no type
   size was small enough: measured on the live page, "Rider Standings / Finals Qualification / Race Results
   / School Standings" needed 449px against 343px at 375pt, 497 against 361 at 393pt and 497 against 398 on
   a Pro Max. Over on EVERY phone, already at the 12px floor from UX audit C2, nothing left to give - and
   on the schools view the active tab rendered as a single clipped letter, so the reader could not even see
   which tab they were on.

   The labels were shortened instead, to one word each: Standings / Finals / Results / Schools. 325px at
   14px with 12px of side padding, which fits the smallest phone with 18px to spare and is LARGER than the
   12px those phones used to get. Every sports app measured for this does the same - FotMob "Table", MLS
   and NBA "Standings", theScore "RESULTS / NEWS / STANDINGS" - nobody ships two-word tabs on a phone.

   "Standings" survives deliberately: the earlier audit rejected "Athletes" because beside "Race Results"
   it read as a list of riders rather than a championship table, and that word is what makes it a table. */
.rf-vtab:hover { color: var(--mid); }
/* The active tab is the only one that carries the accent, and it carries it as a rule rather than a fill:
   a filled tab competes with the selected category pill directly beneath it. */
.rf-vtab.on { color: var(--hi); border-bottom-color: var(--effort); }

/* Settled, not urgent: an accepted invite is the good outcome and must not shout like a chase. */
.rs-chip.ok { background: rgba(63,209,122,.14); border-color: rgba(63,209,122,.45); color: var(--good); }

.rs-row { display: flex; align-items: center; gap: var(--sp-lg); padding: var(--sp-md) var(--sp-md); border-radius: var(--r-md); }
.rs-row + .rs-row { border-top: 1px solid var(--line); }
/* THE RANK IS THE ANSWER, and it was the least emphasised thing in the row: 22px wide, weight 500, in the
   DIMMEST token, while the points beside it were larger, brighter and heavier. On a page whose whole job is
   "where did I come", the eye was being sent to the wrong number. UX audit M5.
   Rajdhani because the brand already owns a condensed sport face and already uses it for numerals in the
   school championship table, where it visibly works - this adds nothing new to the identity (Opportunity
   03). min-width rather than a fixed 22px: three- and four-digit ranks are ordinary here (17,722 riders in
   the largest series in the route) and a fixed width clipped them. */
.rs-rank { min-width: 26px; text-align: center; font-family: var(--font-display); font-size: 18px;
  font-weight: 700; font-variant-numeric: tabular-nums; color: var(--hi); }
.rs-row.r1 .rs-rank { color: var(--gold); } .rs-row.r2 .rs-rank { color: var(--silver); } .rs-row.r3 .rs-rank { color: var(--bronze); }
.rs-av { width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: var(--fs-xs); color: var(--effort-ink); background: var(--s3); flex: 0 0 32px; }
.rs-row.r1 .rs-av { background: var(--gold); } .rs-row.r2 .rs-av { background: var(--silver); } .rs-row.r3 .rs-av { background: var(--bronze); }
.rs-row.r1 .rs-av, .rs-row.r2 .rs-av, .rs-row.r3 .rs-av { color: #14151A; }  /* dark ink on light medal avatars */
.rs-pts { text-align: right; font-size: 16px; font-weight: 500; color: var(--data); }
.rs-you { border: 1px solid rgba(227,6,19,.55); background: rgba(227,6,19,.10); }
/* .rs-rank is 18px/700 — 0.66px under WCAG's 18.66px bold threshold, so it is SMALL text, and it sits on
   .rs-you's red tint (rgba(227,6,19,.10) over --s1 = #2b1418) where #E30613 measured 3.54. This row is
   the reader's own result; it is the one row on the page that has to be legible. */
.rs-you .rs-rank, .rs-you .rs-av { color: var(--effort-text); }
.rs-you .rs-av { background: var(--effort); color: var(--effort-ink); }
/* "Mine" — a row that is the signed-in user or one of their riders (standings). */
.rs-row.rs-mine { border: 1px solid rgba(63,209,122,.55); background: rgba(63,209,122,.10); }
.rs-row.rs-mine .rs-rank { color: var(--good); }

/* ---- Standings: a rider's race number, and their round-by-round breakdown ------------------------
   The row becomes a <details> so the breakdown opens with NO JavaScript. The web standings page is
   static SSR on purpose (public standings have to be crawlable), so an @onclick there would render a
   control that does nothing until a circuit exists. <details> works identically on both surfaces. */
.rf-no { font-family: var(--font-display); font-weight: 600; font-size: var(--fs-xs); letter-spacing: .04em;
  color: var(--low); background: var(--s2); border: 1px solid var(--line); border-radius: var(--r-md);
  padding: 0 var(--sp-xs); margin-left: var(--sp-xs); white-space: nowrap; font-variant-numeric: tabular-nums; }

.rf-rd-li { list-style: none; }
.rf-rd-li + .rf-rd-li { border-top: 1px solid var(--line); }
/* The row itself is the summary, so it keeps .rs-row's flex layout untouched. The default triangle is
   removed in both engines - ::marker for standards, ::-webkit-details-marker for older WebKit. */
.rf-rd > summary { cursor: pointer; list-style: none; }
.rf-rd > summary::-webkit-details-marker { display: none; }
.rf-rd > summary::marker { content: ""; }
.rf-rd > summary:focus-visible { outline: var(--focus-ring); outline-offset: calc(var(--focus-offset) * -1); }
/* A chevron that states which way the row will move. Rotates only when the panel is open. */
.rf-rd-caret { flex: 0 0 auto; color: var(--low); font-size: var(--fs-xs); transition: transform var(--dur-base) ease; }
.rf-rd[open] > summary .rf-rd-caret { transform: rotate(90deg); }
@media (prefers-reduced-motion: reduce) { .rf-rd-caret { transition: none; } }

.rf-rounds { padding: var(--sp-sm) var(--sp-md) var(--sp-md) calc(26px + var(--sp-lg) + var(--sp-md)); }
.rf-rounds-head { font-family: var(--font-display); text-transform: uppercase; letter-spacing: .1em;
  font-size: var(--fs-xs); color: var(--low); margin-bottom: var(--sp-sm); }
.rf-rd-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-3xs); }
.rf-rd-row { display: grid; grid-template-columns: 3.2rem 1fr auto; align-items: baseline; gap: var(--sp-md);
  padding: 3px 0; font-size: var(--fs-sm); font-variant-numeric: tabular-nums; }
.rf-rd-row + .rf-rd-row { border-top: 1px dotted var(--line); }
.rf-rd-no { font-family: var(--font-display); font-weight: 600; color: var(--mid); }
.rf-rd-pos { color: var(--hi); }
.rf-rd-pts { text-align: right; color: var(--data); font-weight: 500; }
/* The board worn that day, shown in the breakdown only where it CHANGED. Quiet, because it is context for
   the points beside it rather than the thing being read. */
.rf-rd-worn { font-family: var(--font-display); font-weight: 600; font-size: var(--fs-xs);
  color: var(--low); margin-right: var(--sp-xs); font-variant-numeric: tabular-nums; }
/* A round the rider has no result for. RED because that is what the reader is scanning for, and the word
   is deliberately neutral: the source publishes finishers only, so we know there is no result and we do
   NOT know whether they started. Do not relabel this "DNF" or "DNS" without data that says which. */
.rf-rd-row.is-none .rf-rd-pos, .rf-rd-row.is-none .rf-rd-no { color: var(--bad); }
.rf-rd-total { display: grid; grid-template-columns: 3.2rem 1fr auto; gap: var(--sp-md);
  margin-top: var(--sp-sm); padding-top: var(--sp-sm); border-top: 1px solid var(--line-2);
  font-size: var(--fs-sm); font-variant-numeric: tabular-nums; }
.rf-rd-total .rf-rd-pts { color: var(--hi); font-weight: 600; }
.rf-rd-note { margin: var(--sp-sm) 0 0; color: var(--low); font-size: var(--fs-xs); }

/* ---- FINALS QUALIFICATION BOARD ----------------------------------------------------------------------
   The board the national-finals team is selected on, beside the championship rather than instead of it.
   The series standings count every round (SCSA MTB Regulations 9.6.1); qualification counts a best 4 of 5.
   Both are on the standings page, one tab apart.

   STATE IS NEVER CARRIED BY COLOUR ALONE. Each strip cell prints COUNT, DROP or "no ride" in words, so the
   board survives greyscale, a colour-vision deficiency and a screen reader. The colours and the line
   through a dropped round REINFORCE the word; they never replace it. */
.rf-q-row { display: grid; grid-template-columns: 2rem 1fr auto; align-items: start; gap: var(--sp-md);
  padding: var(--sp-xs) 0; font-size: var(--fs-sm); font-variant-numeric: tabular-nums; }
.rf-q-row + .rf-q-row { border-top: 1px dotted var(--line); }
.rf-q-rank { font-family: var(--font-display); font-weight: 600; color: var(--mid); }
.rf-q-name { color: var(--hi); display: block; }
/* The board number sits tight against the name without this: .rf-rd-worn carries a right margin only,
   because in the round breakdown it leads the line rather than following a name. */
.rf-q-name .rf-rd-worn { margin-left: var(--sp-2xs); }
.rf-q-pts { text-align: right; color: var(--data); font-weight: 500; white-space: nowrap; }
/* The season total, kept beside the qualification one. Quiet: it is the reconciliation, not the headline. */
.rf-q-season { display: block; font-size: var(--fs-2xs); color: var(--low); font-weight: 400; }
/* §3.1's automatic place. A WORD, not a colour and not an icon. */
.rf-q-auto { display: inline-block; margin-left: var(--sp-xs); font-size: var(--fs-2xs); font-weight: 700;
  letter-spacing: .04em; text-transform: uppercase; color: var(--good);
  background: rgba(63,209,122,.16); border-radius: 8px; padding: 1px 7px; vertical-align: middle; }
/* The RUNNER-UP route - rider 4 or 5 whose school fields no qualifying team. A DIFFERENT FACT from an
   automatic place, so it reads differently: same shape, quieter colour, its own word. Dressing it as
   "Qualifies" would tell a child she finished in her category's top three when she did not. */
.rf-q-invited { display: inline-block; margin-left: var(--sp-xs); font-size: var(--fs-2xs); font-weight: 700;
  letter-spacing: .04em; text-transform: uppercase; color: var(--data);
  background: rgba(255,255,255,.08); border: 1px solid var(--line-2); border-radius: 8px;
  padding: 0 var(--sp-xs); vertical-align: middle; }
/* Why a rider carries no rank. Quiet and in words: he is not short of points, he is short of a round. */
.rf-q-short { display: inline-block; margin-left: var(--sp-xs); font-size: var(--fs-2xs); color: var(--low); }

/* WHAT THE ORGANISER ANNOUNCED, which outranks anything this product computed. These two carry more weight
   than the computed badges beside them - filled rather than outlined - because "Gauteng team" is a fact
   about a child's season and "Qualifies" is our arithmetic's opinion of it. They agree almost everywhere;
   where they do not, the announcement is the one that is true. */
.rf-q-prov, .rf-q-team { display: inline-block; margin-left: var(--sp-xs); font-size: var(--fs-2xs);
  font-weight: 700; letter-spacing: .04em; text-transform: uppercase; border-radius: 8px;
  padding: 1px 7px; vertical-align: middle; }
/* THE TWO COLOURS MATCH THE COUNTS ON THE SCHOOL ROW - green province, red school - and that is the whole
   rule. .rf-snat-n.is-prov is green and .is-school is red, and a reader who has just parsed "10 red, 4
   green" at the top of a school then meets these pills under it. They read the pair as one legend whether
   or not anyone designed it as one, so green here had better still mean province. It did not: the
   provincial pill was the brand red and the school pill a grey outline, which made the loudest colour on
   the row mean the opposite of the loudest colour above it.

   Both are FILLS with dark ink, never coloured text. --effort's own token note is that #E30613 fails AA as
   small TEXT on all four surfaces, and --good at this size has the same problem. */
.rf-q-prov { color: #06210f; background: var(--good); }
/* Carries the school name, so it stays the wider pill: sentence case and normal tracking keep
   "School team - Menlopark HS" readable where the province pill can shout in caps. */
.rf-q-team { color: var(--effort-ink); background: var(--effort);
  text-transform: none; letter-spacing: 0; font-weight: 600; }

/* SPANS THE ROW rather than sitting inside the name column, which boxed five cells into whatever width the
   points column left over - two per line on a phone, and a row three deep for one rider. */
.rf-q-strip { grid-column: 2 / -1; display: flex; flex-wrap: wrap; gap: var(--sp-2xs); margin-top: var(--sp-2xs); }
.rf-q-cell { display: grid; gap: 1px; min-width: 3.1rem; padding: var(--sp-2xs) var(--sp-xs);
  border: 1px solid var(--line); border-radius: 8px; text-align: center; }
.rf-q-cell-r { font-family: var(--font-display); font-size: var(--fs-2xs); color: var(--low); }
.rf-q-cell-p { font-size: var(--fs-xs); color: var(--hi); font-variant-numeric: tabular-nums; }
.rf-q-cell-s { font-size: 9px; letter-spacing: .04em; text-transform: uppercase; color: var(--low); }
.rf-q-cell.is-count { border-color: var(--line-2); }
.rf-q-cell.is-count .rf-q-cell-s { color: var(--good); }
/* THE DROPPED ROUND. Struck through as well as labelled, because "this one does not count" is a statement
   about the NUMBER and the line through it is the fastest way to read that. */
.rf-q-cell.is-drop { border-style: dashed; }
.rf-q-cell.is-drop .rf-q-cell-p { text-decoration: line-through; color: var(--low); }
.rf-q-cell.is-drop .rf-q-cell-s { color: var(--bad); }
/* A round she did not ride. NOT a zero — the dash and the words carry it, and the cell recedes rather than
   alarming: missing a round is not an error. */
.rf-q-cell.is-absent { border-style: dotted; }
.rf-q-cell.is-absent .rf-q-cell-p, .rf-q-cell.is-absent .rf-q-cell-s { color: var(--low); }
.rs-row.rs-mine .rs-av { background: var(--good); color: #06210f; }
.rs-mine-tag { display: inline-block; margin-left: var(--sp-xs); font-size: var(--fs-2xs); font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--good); background: rgba(63,209,122,.16); border-radius: 8px; padding: 1px 7px; vertical-align: middle; }
.rs-tag { font-size: 9px; color: var(--effort-text); border: 1px solid var(--effort); border-radius: 4px; padding: 0 var(--sp-2xs); }

.rs-hero { background: var(--s1); border: 1px solid var(--line); border-radius: var(--r-lg); padding: var(--sp-xl) var(--sp-2xl); display: flex; align-items: flex-end; justify-content: space-between; }
.rs-hero-num { font-size: 38px; font-weight: 500; line-height: 1; }
.rs-tile { background: var(--s1); border: 1px solid var(--line); border-radius: var(--r-md); padding: var(--sp-md) var(--sp-lg); }

/* ---- DEPTH, APPLIED. One block, because the alternative is 98 inline edits in this app and 344 in the
   phone's, and a treatment spread that thin cannot be reviewed or reverted.

   THE LIST IS CARDS, NOT EVERY SURFACE THAT HAPPENS TO USE --s1. A sheen belongs on something that reads as
   a panel sitting on the page; put it on the rows of a bordered list and it stripes them, and put it on an
   input or a hover state and the control looks pressed when it is not. Rows, inputs, hovers and menu items
   are deliberately absent.

   These only ADD a background-image and a shadow — every selector below keeps whatever background-color it
   already declares, so removing this block returns the app to flat and changes nothing else. */
.card, .rs-card, .rs-tile, .rs-hero, .rs-series-card, .rs-menu-panel,
.auth-formpanel, .rf-card, .rf-card-slot, .rf-cockpit-card, .rf-cockpit-rider,
.rf-empty, .rf-ask-q {
  background-image: var(--grad-s1);
  box-shadow: var(--edge-top);
}
.rs-tile-num { font-size: var(--fs-lg); font-weight: 500; }

/* App shell + bottom tab bar */
/* dvh after vh throughout: on iOS Safari the toolbars are INSIDE 100vh, so a 100vh shell is taller than
   what the user can actually see and the page starts life scrolled short of its own bottom. dvh tracks the
   visible viewport as the toolbars collapse. vh stays first as the fallback for anything that lacks dvh. */
.rs-shell { min-height: 100vh; min-height: 100dvh; padding-bottom: calc(66px + env(safe-area-inset-bottom)); display: flex; flex-direction: column; }
.rs-topbar { display: flex; align-items: center; justify-content: space-between; padding: var(--sp-lg) var(--sp-2xl); border-bottom: 1px solid var(--line-2); position: sticky; top: 0; background: rgba(10,10,10,.92); backdrop-filter: blur(8px); z-index: var(--z-raised); box-shadow: 0 8px 16px -6px rgba(0,0,0,.85); }
/* ---- Top-right hamburger menu (native <details> dropdown) ---- */
.rs-menu { position: relative; }
.rs-menu > summary { list-style: none; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; width: 38px; height: 38px; border-radius: 9px; color: var(--hi); font-size: var(--fs-xl); }
.rs-menu > summary::-webkit-details-marker { display: none; }
.rs-menu > summary::marker { content: ""; }
.rs-menu > summary:hover { background: var(--s2); }
.rs-menu[open] > summary { background: var(--s2); color: var(--effort-text); }
/* The panel MUST be able to scroll. It is absolutely positioned, so without a height bound it simply runs
   off the bottom of the screen and everything past the fold is unreachable — on an iPhone that meant the
   last five entries, including My school team, could not be opened at all. The menu grows as the product
   does, so this is not a "when we add more items" problem, it is a "we already did" one.
   dvh after vh: iOS Safari's toolbars are part of 100vh, so vh alone still overflows behind them. */
.rs-menu-panel { position: absolute; right: 0; top: calc(100% + 8px); min-width: 210px; background: var(--s1); border: 1px solid var(--line-2); border-radius: var(--r-md); box-shadow: 0 12px 34px rgba(0,0,0,.55); padding: var(--sp-xs); z-index: var(--z-sticky); display: flex; flex-direction: column;
  max-height: calc(100vh - 96px); max-height: calc(100dvh - 96px); overflow-y: auto; -webkit-overflow-scrolling: touch; overscroll-behavior: contain; }
.rs-menu-item { display: flex; align-items: center; gap: 11px; padding: 9px 12px; border-radius: 8px; color: var(--hi); text-decoration: none; background: none; border: none; width: 100%; text-align: left; font: inherit; font-size: var(--fs-md); cursor: pointer; }
.rs-menu-item:hover { background: var(--s2); color: var(--hi); }
.rs-menu-item i { color: var(--data); width: 18px; text-align: center; }
.rs-menu-sep { height: 1px; background: var(--line); margin: 5px 4px; }
.rs-topbar::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 2px; background: linear-gradient(to right, var(--effort), transparent); pointer-events: none; }
/* Tyre calc: fields locked to the chosen bike stay READABLE (browsers grey out plain disabled text on a dark theme). */
.form-control.rf-locked, .form-select.rf-locked { color: var(--hi); -webkit-text-fill-color: var(--hi); opacity: 1; background-color: var(--s2); border-style: dashed; cursor: default; }
.rs-brand { display: inline-flex; align-items: center; font-family: var(--font-display); font-weight: 700; font-style: italic; text-transform: uppercase; letter-spacing: .04em; font-size: 21px; color: var(--hi); }
.rs-brand .dot { color: var(--effort); font-style: normal; }
.rs-brand-mark { height: 26px; width: 26px; border-radius: 6px; margin-right: 9px; mix-blend-mode: screen; }
.rs-brand-logo { height: 30px; width: auto; display: block; }
/* Discreet "Dashboard" link in the mobile topbar (guardian roles) → wide desktop /guardian view. */
.rs-topbar-dash { color: var(--mid); text-decoration: none; font-size: var(--fs-sm); display: inline-flex; align-items: center; gap: 5px; margin-right: var(--sp-xl); }
.rs-topbar-dash:hover { color: var(--data); }
.rs-main { max-width: 880px; margin: 0 auto; padding: var(--sp-2xl); width: 100%; flex: 1 1 auto; display: flex; flex-direction: column; }
/* SKIP LINK (UX audit L1). There was none anywhere in the product, and <main> had no id to aim one at.
   On the web app a keyboard user crosses the brand link, the notification bell, the menu, both profile-bar
   links and up to two banner links before reaching the page - the same five to seven stops on every
   navigation.

   OFF-SCREEN BY TRANSFORM, not `display:none` or `visibility:hidden`: those remove the element from the
   tab order, so the link could never be reached to be shown. Not a negative `top` either - this is
   position:fixed so it does not depend on an ancestor being positioned, and .rs-topbar is a sticky
   z-index:var(--z-raised) bar the link has to clear. */
.rf-skip { position: fixed; left: var(--sp-sm); top: var(--sp-sm); z-index: var(--z-toast); transform: translateY(-300%); background: var(--s2); color: var(--hi); border: 1px solid var(--line-2); border-radius: var(--r-md); padding: var(--sp-md) var(--sp-xl); font-size: var(--fs-md); font-weight: 600; text-decoration: none; }
.rf-skip:focus { transform: translateY(0); outline: var(--focus-ring); outline-offset: var(--focus-offset); }

/* The skip target. <main> is not focusable, so a fragment link scrolls to it and leaves focus behind on
   the link - the next Tab then goes back into the nav the user just asked to skip. tabindex="-1" makes it
   programmatically focusable WITHOUT putting it in the tab order. The ring is dropped on the container
   because it would draw a 2px box around the entire page; the scroll and the new tab position are the
   feedback, and this is not a user interface component in 2.4.7's sense. */
main[tabindex="-1"]:focus { outline: none; }
/* Persistent "your profile" bar under the top bar, on every screen. */
.rs-profilebar { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-lg); padding: var(--sp-md) var(--sp-2xl); border-bottom: 1px solid var(--line); min-height: 61px; }
/* The reserved twin (MainLayout renders it while GetMeAsync is in flight) shares this class, so it is the
   same box by construction rather than by a matching number someone has to keep in step. Holding the row
   stops the real bar shoving the page down when it arrives. 61px = 40px avatar + 20px padding + 1px border. */
.rs-profilebar-reserved { border-bottom-color: transparent; }
.rs-profilebar-id { display: flex; align-items: center; gap: var(--sp-md); text-decoration: none; color: var(--hi); min-width: 0; }
.rs-profilebar-text { display: flex; flex-direction: column; line-height: 1.15; min-width: 0; }
.rs-profilebar-name { font-family: var(--font-display); font-size: var(--fs-lg); font-weight: 600; letter-spacing: .02em; color: var(--hi); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rs-profilebar-name .rf-ln { color: var(--effort-text); margin-left: .3em; }  /* surname in race red — 17px/600, small text */
.rs-profilebar-sub { font-size: var(--fs-xs); color: var(--mid); }
.rs-profilebar-gear { flex: 0 0 auto; color: var(--data); font-size: var(--fs-xl); line-height: 1; padding: var(--sp-xs); text-decoration: none; }
.rs-profilebar-gear:hover { color: var(--hi); }
/* "Verify your email" nudge bar (unverified accounts) */
.rs-verifybar { display: flex; align-items: center; justify-content: center; gap: var(--sp-sm); padding: var(--sp-sm) var(--sp-2xl); background: rgba(245,182,66,.12); border-bottom: 1px solid rgba(245,182,66,.30); color: var(--warn); font-size: var(--fs-sm); text-decoration: none; }
.rs-verifybar:hover { background: rgba(245,182,66,.18); color: var(--warn); }
/* Pending coach invite. Race red rather than the verify bar's amber: this is an invitation to act, not a
   warning about the account, and the two must not read as the same kind of message. */
.rs-invitebar { display: flex; align-items: center; justify-content: center; gap: var(--sp-sm); padding: var(--sp-sm) var(--sp-2xl); background: rgba(227,6,19,.12); border-bottom: 1px solid rgba(227,6,19,.32); color: var(--effort-text); font-size: var(--fs-sm); text-decoration: none; }
.rs-invitebar:hover { background: rgba(227,6,19,.18); color: var(--effort-text); }
/* Full-screen "generating avatar" loading overlay */
.rf-genoverlay { position: fixed; inset: 0; background: rgba(10,13,18,.82); backdrop-filter: blur(3px); display: flex; align-items: center; justify-content: center; z-index: var(--z-bar); }
.rf-genbox { display: flex; flex-direction: column; align-items: center; gap: 13px; color: var(--hi); text-align: center; max-width: 340px; padding: 28px 26px; background: linear-gradient(180deg, var(--s1), #11151b); border: 1px solid var(--line-2); border-radius: 16px; box-shadow: 0 30px 90px -30px rgba(0,0,0,.85); }
.rf-gentitle { font-family: var(--font-display); text-transform: uppercase; letter-spacing: .04em; font-weight: 700; font-size: 19px; color: var(--hi); }
.rf-gentext { font-size: var(--fs-md); color: var(--mid); min-height: 1.2em; transition: opacity var(--dur-slow) ease; }
.rf-gennote { font-size: var(--fs-xs); color: var(--low); margin-top: var(--sp-3xs); }
.rf-genbox .rf-progress { width: 100%; }
/* MINIMUM TOUCH TARGET. Lived in two page style blocks — TeamDashboard and TeamRider — which is how it came
   to be on a button that never got it: TeamRider's "Add note" carried the class while the rule sat in a
   different page, so the markup read as "this is a proper tap target" and the button measured 31px on a
   375px phone. That is the kind of defect that survives review precisely because the class name says
   otherwise. One declaration, here, where every host already loads it. */
.rf-tap { min-height: 44px; display: inline-flex; align-items: center; }
.rf-spinner { width: 44px; height: 44px; border-radius: 50%; border: 3px solid rgba(255,255,255,.15); border-top-color: var(--effort); animation: rf-spin .8s linear infinite; }
@keyframes rf-spin { to { transform: rotate(360deg); } }
/* Uploaded profile photo (falls back to the identicon when none is set). */
.rs-avatar-img { border-radius: 50%; object-fit: cover; flex: 0 0 auto; display: block; }
.rs-avatar-edit { position: relative; display: inline-block; line-height: 0; }
.rs-avatar-click { position: relative; display: inline-block; cursor: pointer; line-height: 0; }
.rs-avatar-cam { position: absolute; right: -2px; bottom: -2px; width: 20px; height: 20px; border-radius: 50%; background: var(--data); color: #06121a; display: flex; align-items: center; justify-content: center; font-size: var(--fs-2xs); border: 2px solid #0b0e13; }
/* Race Face card social-share buttons */
.rf-share { display: inline-flex; align-items: center; justify-content: center; width: 44px; height: 44px; border-radius: 50%; border: none; color: #fff; font-size: 20px; cursor: pointer; transition: transform var(--dur-fast), filter var(--dur-fast); }
.rf-share:hover { transform: translateY(-1px); filter: brightness(1.08); }
.rf-wa { background: #25D366; }
.rf-fb { background: #1877F2; }
.rf-ig { background: linear-gradient(45deg, #feda75, #fa7e1e, #d62976, #962fbf, #4f5bd5); }
.rf-dl { background: var(--effort); }
.rf-vid { background: linear-gradient(45deg, #f43f5e, #a855f7); }
.rf-share:disabled { opacity: .85; cursor: default; transform: none; }
.rf-spin { display: inline-block; animation: rf-rot .8s linear infinite; }
@keyframes rf-rot { to { transform: rotate(360deg); } }
/* Story-video status line (keeps the round share buttons clean — no text crammed inside the 44px circle). */
.rf-vidstatus { display: none; align-items: center; gap: 9px; margin-top: var(--sp-lg); font-size: var(--fs-sm); color: var(--mid); }
.rf-vidstatus.show { display: inline-flex; }
.rf-vdot { width: 15px; height: 15px; flex: 0 0 auto; border: 2px solid var(--line-2); border-top-color: var(--effort); border-radius: 50%; animation: rf-spin .7s linear infinite; }
.rf-verr { color: #f87171; }
.rf-savebtn { display: inline-flex; align-items: center; gap: var(--sp-sm); padding: 9px 18px; border: none; border-radius: 999px; background: linear-gradient(45deg, #f43f5e, #a855f7); color: #fff; font-weight: 600; font-size: var(--fs-md); cursor: pointer; box-shadow: 0 6px 18px -6px rgba(168,85,247,.7); transition: filter var(--dur-fast), transform var(--dur-fast); }
.rf-savebtn:hover { filter: brightness(1.08); transform: translateY(-1px); }

/* ---- Auth (sign in / create account) — split card: brand panel + form ---- */
.auth-split { display: flex; max-width: 860px; margin: 6vh auto; border: 1px solid var(--line); border-radius: 22px; overflow: hidden; box-shadow: 0 24px 70px rgba(0,0,0,.45); }
.auth-brandpanel { flex: 1 1 0; min-width: 0; padding: 44px 38px; position: relative; display: flex; flex-direction: column; justify-content: center; background: linear-gradient(155deg, var(--s2), var(--bg)); }
.auth-brandpanel::after { content: ""; position: absolute; inset: 0; background: radial-gradient(110% 75% at 85% -5%, rgba(227,6,19,.20), transparent 60%); pointer-events: none; }
.auth-brand { font-family: var(--font-display); font-size: 30px; font-weight: 700; font-style: italic; text-transform: uppercase; letter-spacing: .05em; color: var(--hi); margin-bottom: 22px; position: relative; }
.auth-brand .dot { color: var(--effort); font-style: normal; }
.auth-hero { width: 100%; max-width: 380px; height: auto; display: block; margin: -4px 0 16px; mix-blend-mode: screen; }

/* ---- Brand flourishes: red speed slashes + "FAST. FEARLESS. FOCUSED." tagline ---- */
.rf-slashes { display: inline-block; width: 50px; height: 14px; background: repeating-linear-gradient(110deg, var(--effort) 0, var(--effort) 3px, transparent 3px, transparent 8px); transform: skewX(-12deg); }
.rf-tagline { font-family: var(--font-display); text-transform: uppercase; font-weight: 700; letter-spacing: .18em; color: var(--hi); font-size: var(--fs-md); display: inline-flex; align-items: center; gap: 0; }
.rf-tagline .dot { color: var(--effort-text); }  /* 14px/700 — small text, unlike .rs-brand/.auth-brand's dots */
/* A centered brand strip (slashes + tagline + slashes) for page footers. */
.rf-brandstrip { display: flex; align-items: center; justify-content: center; gap: var(--sp-xl); margin: 26px 0 6px; opacity: .9; }
.rf-brandstrip .rf-slashes { width: 34px; }

/* Inline loading spinner (red top) — used by the RfLoading component wherever data is loading. */
.rf-load { display: inline-flex; align-items: center; gap: var(--sp-md); color: var(--mid); font-size: var(--fs-sm); padding: var(--sp-xs) 0; }
.rf-load::before { content: ""; width: 18px; height: 18px; flex: 0 0 auto; border: 2px solid var(--line-2); border-top-color: var(--effort); border-radius: 50%; animation: rf-spin .7s linear infinite; }
/* Thin top progress bar for page-level loads. */
.rf-progress { height: 3px; border-radius: 2px; overflow: hidden; background: var(--line); position: relative; }
.rf-progress::after { content: ""; position: absolute; inset: 0; width: 40%; background: var(--effort); border-radius: 2px; animation: rf-indeterminate 1.1s ease-in-out infinite; }
@keyframes rf-indeterminate { 0% { left: -40%; } 100% { left: 100%; } }
/* Page-top variant of the bar above. A SEPARATE class rather than an edit to .rf-progress, which is already
   indeterminate-only and merely lacks placement — RaceSense.Mobile's Welcome.razor uses the base as a
   DETERMINATE bar with its own .rf-progress-fill, and giving the base a fixed position would break it. */
.rf-progress-top { position: fixed; top: 0; left: 0; right: 0; z-index: var(--z-progress); border-radius: 0; background: transparent; display: none; }
.rf-progress-top.is-on { display: block; }

/* Submit-in-flight state. The button is NEVER given the `disabled` attribute: that drops it from the
   accessibility tree, throws focus to <body>, and survives bfcache — so a Back-navigation to an abandoned
   sign-in would show a permanently dead button. aria-disabled + a JS guard keeps it focusable and reversible.
   Dual selector because Web uses Bootstrap's .btn and Mobile uses .rf-btn. */
.btn.is-busy, .rf-btn.is-busy { pointer-events: none; opacity: .75; }
.btn.is-busy::before, .rf-btn.is-busy::before { content: ""; display: inline-block; width: 14px; height: 14px; margin-right: var(--sp-sm); vertical-align: -2px; border: 2px solid rgba(255,255,255,.35); border-top-color: currentColor; border-radius: 50%; animation: rf-spin .7s linear infinite; }

/* The progress bar and the busy spinner are PERSISTENT loops, so they need the guard the three animations
   at :590, :697 and :817 already have. Reduced motion keeps the affordance, drops the movement. */
/* EVERY infinite rotation, not the two that happened to be near this rule. Measured (UX audit M9): five
   spinners span the app - .rf-spinner, .rf-spin, .rf-vdot, .rf-load::before and .rf-admin-progress .spin -
   and none of the three existing reduced-motion blocks covered any of them. A reader who has asked their
   operating system to stop motion got five things spinning anyway.

   Stopped, not hidden: a spinner is FEEDBACK, and removing it would leave a busy page looking idle - the
   exact defect C1 was about. So the animation stops and the ring becomes uniform, which reads as a
   deliberate marker rather than a half-drawn circle frozen mid-turn. */
@media (prefers-reduced-motion: reduce) {
  .rf-progress::after { animation: none; width: 100%; opacity: .45; }
  .btn.is-busy::before, .rf-btn.is-busy::before { animation: none; opacity: .6; border-top-color: currentColor; }
  .rf-spinner, .rf-vdot, .rf-load::before {
    animation: none; border-top-color: var(--effort); border-color: var(--effort); opacity: .55;
  }
  .rf-spin { animation: none; }
  .rf-admin-progress .spin { animation: none; }
}
.auth-tagline { font-family: var(--font-display); font-size: 40px; font-weight: 700; text-transform: uppercase; line-height: 1.04; color: var(--hi); margin: 0 0 var(--sp-xl); letter-spacing: .02em; position: relative; }
.auth-tagline em { color: var(--effort); font-style: normal; }
.auth-blurb { color: var(--mid); font-size: 15px; line-height: 1.5; position: relative; margin: 0 0 20px; }
.auth-feats { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: var(--sp-md); position: relative; }
.auth-feats li { color: var(--hi); font-size: var(--fs-md); display: flex; align-items: center; gap: var(--sp-md); }
.auth-feats i { color: var(--data); font-size: 16px; }
.auth-formpanel { flex: 1 1 0; min-width: 0; padding: 44px 38px; background: var(--s1); }
.auth-title { font-size: var(--fs-2xl); font-weight: 700; margin: 0 0 var(--sp-2xs); color: var(--hi); }
.auth-sub { color: var(--mid); font-size: var(--fs-md); margin: 0 0 22px; }
.auth-form label { display: block; margin: 0 0 var(--sp-xs); font-size: var(--fs-sm); font-weight: 500; color: var(--mid); }
.auth-form .auth-field { margin-bottom: var(--sp-xl); }
/* .rf-formmsg is the general name; .auth-error is kept as an alias so existing markup and any muscle memory
   still work. The variants replace the inline colour overrides Login.razor was carrying for its success and
   session-expired banners — the same panel wearing three meanings, restated by hand at each call site.
   :focus is explicit because this container is focused PROGRAMMATICALLY after a page load, which does not
   match :focus-visible, so the design system's usual red ring never appears on it. */
.auth-error, .rf-formmsg { background: rgba(242,84,91,.12); border: 1px solid rgba(242,84,91,.32); color: #f7a6aa; border-radius: 10px; padding: var(--sp-md) var(--sp-lg); font-size: var(--fs-md); margin-bottom: var(--sp-2xl); }
.rf-formmsg:focus, .auth-error:focus { outline: var(--focus-ring); outline-color: var(--bad); outline-offset: var(--focus-offset); }
.rf-formmsg.is-good { background: rgba(63,209,122,.12); border-color: rgba(63,209,122,.32); color: #86e3ab; }
.rf-formmsg.is-good:focus { outline-color: var(--good); }
.rf-formmsg.is-warn { background: rgba(245,182,66,.12); border-color: rgba(245,182,66,.32); color: var(--warn); }
.rf-formmsg.is-warn:focus { outline-color: var(--warn); }

/* Field-level validation. Styled from [aria-invalid] and Blazor's own class, NEVER from :invalid - the
   browser and the server disagree by design (type=email accepts "a@b", a min accepts an absurd child
   weight), so painting a field red from :invalid would contradict the server on the render where the
   server is right. ValidationMessage emits nothing when a field is valid, so nothing reserves space and
   the measured CLS of 0 is preserved. */
.rf-field-err { display: block; color: var(--bad); font-size: var(--fs-xs); margin-top: var(--sp-2xs); }
.rf-field-err:empty { display: none; }
.form-control[aria-invalid="true"], .form-select[aria-invalid="true"],
.form-control.invalid, .form-select.invalid { border-color: var(--bad); box-shadow: 0 0 0 3px rgba(242,84,91,.22); }
/* The same state, driven by the ATTRIBUTE rather than Blazor's class. Two reasons it is not enough to rely
   on `.invalid`: it is only applied by Blazor's InputBase, so the phone's plain `.rf-input` fields never got
   it; and Blazor sets the class WITHOUT setting aria-invalid, so a screen reader was told a red-bordered
   field was valid. The forms set aria-invalid explicitly and this makes it visible everywhere. */
.form-control[aria-invalid="true"], .form-select[aria-invalid="true"], .rf-input[aria-invalid="true"] {
  border-color: var(--bad); box-shadow: 0 0 0 3px rgba(242,84,91,.22); }
.auth-alt { margin-top: 20px; font-size: var(--fs-md); color: var(--mid); }
.auth-foot { text-align: center; color: var(--low); font-size: var(--fs-xs); margin-top: var(--sp-md); }
@media (max-width: 680px) {
  /* margin:0 (not "0 auto") — .rs-main is itself a flex column, and auto cross-axis margins on a flex
     item disable align-items:stretch, shrink-wrapping .auth-split to its content width instead of filling
     the screen edge-to-edge as the stripped border/radius/shadow below already signal it should. */
  .auth-split { flex-direction: column; margin: 0; border: none; border-radius: 0; box-shadow: none; }
  .auth-brandpanel { padding: 26px 24px 20px; }
  .auth-tagline { font-size: 28px; }
  .auth-blurb, .auth-feats { display: none; }
  .auth-formpanel { padding: 26px 24px; }
}

/* ---- Realtime "now" line on the race-day timeline / fuelling schedule ---- */
.rf-nowline { position: absolute; left: 0; right: 0; height: 0; border-top: 2px solid var(--effort); z-index: 3; pointer-events: none; transition: top .6s linear; }
.rf-nowline::before { content: ""; position: absolute; left: -4px; top: -4px; width: 8px; height: 8px; border-radius: 50%; background: var(--effort); box-shadow: 0 0 6px var(--effort); }
.rf-nowlabel { position: absolute; right: 0; top: -9px; background: var(--effort); color: #fff; font-size: var(--fs-2xs); font-weight: 700; letter-spacing: .04em; padding: 1px 6px; border-radius: 3px; }

/* ---- Collapsible season panel: the series card is the toggle; everything below it opens on click ---- */
.rf-seasonpanel > summary { list-style: none; cursor: pointer; }
.rf-seasonpanel > summary::-webkit-details-marker { display: none; }
.rf-seasonpanel > summary::marker { content: ""; }
/* Inline display:flex on some children (name row, .row tiles) overrides the UA closed-details hiding,
   so hide every non-summary child explicitly when collapsed. */
.rf-seasonpanel:not([open]) > :not(summary) { display: none !important; }
.rf-seasonpanel[open] .rf-seasonchev { transform: rotate(180deg); }
/* Non-selected seasons render as collapsed link cards that navigate (load) on click. */
.rf-seasonlink { display: block; text-decoration: none; }
.rf-seasonlink:hover .rs-card { background: var(--s2); }

/* ---- Draggable fuelling intakes on the race-day timeline (snap to the moment above) ---- */
.rf-drag-handle { touch-action: none; cursor: grab; }
.rf-tl-intake { position: relative; }
.rf-tl-intake.rf-dragging { opacity: .85; background: var(--s2); cursor: grabbing; }
[data-offset].rf-drop-after { box-shadow: inset 0 -2px 0 var(--effort); }

.rs-tabbar { position: fixed; left: 0; right: 0; bottom: 0; display: flex; background: #0E0E10; border-top: 1px solid var(--line); box-shadow: inset 0 2px 0 var(--low); padding: 4px 0 calc(6px + env(safe-area-inset-bottom)); z-index: 20; }
/* THE MIDDLE TERM IS TIED TO THE TAB COUNT. Change how many tabs the bar has and you must retune it.
   Tabs split the width evenly, so a slot is 100/n vw and no label may wrap: a wrapped label grows its tab
   and shoves that one icon out of line with the rest. "My season" is the binding label at 57.3px of text;
   it wrapped at 390px in a 55.7px slot, and at 320px four labels wrapped at once. nowrap alone only trades
   the wrap for an overflow, so the size tracks the slot instead.
   Six tabs: a slot is 100/6 = 16.67vw, and 3.07vw holds "My season" at ~96% of it. Capped at 11px so any
   phone wide enough not to need the reduction (>=358px) keeps the full size untouched.
   This was 2.72vw when the bar had seven tabs. Moving Calendar behind More widened every slot, and leaving
   the old value in place would have kept the font at its seven-tab size and wasted the space that was
   just freed — which is exactly what happened for one deploy.
   FOUR TABS since Mission 016 (Home · Events · Results · Me): a slot is 25vw, 80px at 320px, and the longest label,
   "Results", needs ~45px at 11px - so the full size holds at every width and no reduction is needed. */
.rs-tab { flex: 1; min-height: 48px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px; color: var(--low); font-size: var(--fs-2xs); letter-spacing: -0.02em; font-weight: 500; text-decoration: none; white-space: nowrap; }
.rs-tab .ico { font-size: var(--fs-xl); }
/* The 11px tab label is the smallest persistent text in the product and sits on the bar's own #0E0E10,
   where #E30613 measured 3.95. The inset rule stays brand red — it is a 2px fill, not text. */
.rs-tab.active { color: var(--effort-text); box-shadow: inset 0 2px 0 var(--effort); }
/* The bar is the SECOND channel. Colour alone was the only signal that a tab is the current page, which
   fails WCAG 1.4.1 for a colour-blind reader (UX audit H5); aria-current, added on the same pass, covers
   the screen-reader half. An inset box-shadow rather than a border or a pseudo-element because it paints
   inside the existing box: no height change, no positioning context, and therefore nothing that can push a
   six-tab bar into wrapping at 320px. The mobile app already does this with its own ::after indicator. */

/* ---- Wide dashboards (desktop-first). Fluid, two-column; collapses on phones. The rail-plus-content
   shell (.rf-dash*) belongs to the coach and school boards; the dependant cards (.rf-depcard, and the
   consent toggles below them) are the guardian board, which is now a breakpoint of /app — see M2. ---- */
.rf-dash-wide { max-width: 1440px; margin: 0 auto; width: 100%; }
/* Side rail (controls/summary) + fluid content. */
.rf-dash { display: grid; grid-template-columns: 300px minmax(0, 1fr); gap: 28px; align-items: start; }
.rf-dash-side { position: sticky; top: 84px; align-self: start; }
.rf-dash-content { min-width: 0; }
/* Dependant cards auto-fit into as many columns as fit; each is comfortable but never cramped. */
.rf-dash-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); gap: 20px; }
.rf-depcard { padding: var(--sp-2xl); display: flex; flex-direction: column; gap: var(--sp-md); }
.rf-depcard-head { display: flex; align-items: center; gap: var(--sp-lg); min-width: 0; }
/* Subsections inside a dependant card get a hairline divider for density without clutter. */
.rf-depsection { border-top: 1px solid var(--line); padding-top: var(--sp-md); }

/* Consent toggles (interactive — no SSR form). iOS-style switch reusing the cockpit palette. */
.rf-consent-row { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-lg); padding: var(--sp-xs) 0; }
.rf-consent-row + .rf-consent-row { border-top: 1px solid var(--line); }
.rf-toggle { position: relative; width: 44px; height: 24px; flex: 0 0 auto; border-radius: 999px; border: 1px solid var(--line-2); background: var(--s2); cursor: pointer; padding: 0; transition: background var(--dur-base), border-color var(--dur-base); }
.rf-toggle:disabled { opacity: .5; cursor: not-allowed; }
.rf-toggle.on { background: var(--effort); border-color: var(--effort); }
.rf-toggle-knob { position: absolute; top: 2px; left: 2px; width: 18px; height: 18px; border-radius: 50%; background: var(--hi); transition: transform var(--dur-base); }
.rf-toggle.on .rf-toggle-knob { transform: translateX(20px); background: var(--effort-ink); }

/* Phone fallback: single column, no sticky rail. Keeps the dashboard usable on a phone too. */
@media (max-width: 900px) {
  .rf-dash { grid-template-columns: 1fr; gap: 18px; }
  .rf-dash-side { position: static; }
  .rf-dash-grid { grid-template-columns: 1fr; }
}

/* ---- /app (Family): the phone list and the wide board are ONE page (UX audit M2) ----
   /guardian and /app were the same job twice, so /app survived — it is the Family tab, it is where a new
   parent adds their first rider, and the reader is usually on a phone at a race venue. The board did not
   die with the URL: it is what the page renders once there is room for it. Narrow gets the list, wide gets
   the dependant cards, never both — printing every rider twice on one screen is the duplication this
   merge removed. 900px is deliberately the same breakpoint .rf-dash-grid collapses at above, so the page
   and the grid inside it cannot disagree about what "wide" means. */
.rf-family-board { display: none; }
@media (min-width: 901px) {
  .rf-family-list { display: none; }
  .rf-family-board { display: block; }
}

/* ---- super_admin ADMIN dashboard (page-level; the AdminLayout shell styles live in its scoped css) ---- */
.rf-admin-h1 { font-size: var(--fs-2xl); font-weight: 700; margin: 0 0 var(--sp-xs); }
.rf-admin-tiles { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--sp-xl); margin: 18px 0 24px; }
.rf-admin-quick { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--sp-xl); }
.rf-admin-quicklink { display: flex; gap: var(--sp-xl); align-items: center; padding: var(--sp-2xl); text-decoration: none; color: inherit; }
.rf-admin-quicklink:hover { border-color: var(--effort); }
.rf-admin-quick-ico { font-size: 24px; color: var(--data); width: 36px; text-align: center; }
.rf-admin-quick-title { font-weight: 600; }

.rf-admin-panel { padding: 18px; margin-bottom: 18px; }
.rf-admin-panel-h { font-weight: 600; margin-bottom: var(--sp-lg); }
.rf-admin-row { display: flex; gap: var(--sp-lg); align-items: flex-end; flex-wrap: wrap; }
.rf-admin-row label { display: flex; flex-direction: column; font-size: var(--fs-sm); color: var(--mid); gap: var(--sp-2xs); }

.rf-admin-table { width: 100%; font-size: var(--fs-md); }
.rf-admin-table th { font-weight: 600; color: var(--mid); border-bottom: 1px solid var(--line); }
.rf-admin-table td { border-bottom: 1px solid var(--line); vertical-align: middle; }
.rf-admin-dim { opacity: .5; }

.rf-admin-planrow { display: flex; gap: 22px; flex-wrap: wrap; font-size: 15px; margin-bottom: var(--sp-md); }
.rf-admin-serieslist { columns: 2; font-size: var(--fs-sm); color: var(--mid); margin: var(--sp-xs) 0 var(--sp-xl); }
.rf-admin-ok { border-color: var(--good); }
.rf-admin-err { color: var(--bad); font-size: var(--fs-sm); margin-top: var(--sp-xs); }
.rf-admin-warn { color: var(--warn); font-size: var(--fs-sm); margin: var(--sp-xs) 0; }
.rf-admin-role { font-family: monospace; font-size: var(--fs-sm); background: var(--bg); border: 1px solid var(--line-2); border-radius: 6px; padding: 1px 7px; }
.rf-admin-progress { display: flex; align-items: center; gap: var(--sp-sm); color: var(--mid); margin: var(--sp-md) 0; }
.rf-admin-progress .spin { animation: rf-admin-spin 1s linear infinite; }
@keyframes rf-admin-spin { to { transform: rotate(360deg); } }

.rf-admin-modal-back { position: fixed; inset: 0; background: rgba(0,0,0,.4); display: flex; align-items: center; justify-content: center; z-index: var(--z-overlay); padding: var(--sp-2xl); }
.rf-admin-modal { background: #fff; border-radius: var(--r-lg); padding: 22px; width: 100%; max-width: 520px; max-height: 90vh; max-height: 90dvh; overflow: auto; }
.rf-admin-modal h3 { font-size: 19px; font-weight: 700; margin: 0 0 var(--sp-lg); }
.rf-admin-field { display: flex; flex-direction: column; gap: var(--sp-2xs); font-size: var(--fs-sm); color: var(--mid); margin-bottom: var(--sp-lg); }
.rf-admin-check { display: flex; align-items: center; gap: var(--sp-sm); font-size: var(--fs-md); margin-bottom: var(--sp-lg); }
.rf-admin-modal-actions { display: flex; justify-content: flex-end; gap: var(--sp-md); margin-top: var(--sp-sm); }

@media (max-width: 820px) {
  .rf-admin-tiles { grid-template-columns: repeat(2, 1fr); }
  .rf-admin-quick { grid-template-columns: 1fr; }
  .rf-admin-serieslist { columns: 1; }
}

/* ---- Legal / policy pages (prose) ---- */
.rf-legal { max-width: 760px; line-height: 1.6; }
.rf-legal h2 { font-family: var(--font-display); font-weight: 600; font-size: 18px; color: var(--hi); margin: 22px 0 6px; }
/* The consent documents a parent reads before agreeing on a child behalf. Measured before this:
   prose 12px, list items 16px, adjacent on the same page - the Children personal information
   paragraph set smaller than the bullets it introduces. One reading size for both.
   :not(.small) keeps the Last updated line as a caption. */
.rf-legal p:not(.small), .rf-legal li { font-size: 15px; line-height: 1.6; }
.rf-legal p, .rf-legal li { color: #C9CDD2; }
.rf-legal ul { padding-left: 20px; margin: var(--sp-xs) 0 var(--sp-md); }
.rf-legal li { margin: 3px 0; }
.rf-legal a { color: var(--effort-text); text-decoration: none; }
.rf-legal a:hover { text-decoration: underline; }

/* ---- Footer (legal links) ---- */
.rf-footer { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-sm); margin: auto 0 4px; padding-top: var(--sp-xl); border-top: 1px solid var(--line); font-size: var(--fs-sm); color: rgba(255,255,255,.45); }
.rf-footer a { color: rgba(255,255,255,.6); text-decoration: none; }
.rf-footer a:hover { color: var(--hi); text-decoration: underline; }
.rf-footer .rf-footer-copy { margin-left: auto; color: rgba(255,255,255,.35); }
@media (max-width: 560px) { .rf-footer .rf-footer-copy { margin-left: 0; flex-basis: 100%; } }

/* ---- Collapsible info cards (race-plan sections etc.) ---- */
details.rf-card-collapse > summary { list-style: none; cursor: pointer; display: flex; align-items: center; }
details.rf-card-collapse > summary::-webkit-details-marker { display: none; }
details.rf-card-collapse > summary > * { margin-bottom: 0; }
details.rf-card-collapse > summary::after { content: "\25BE"; margin-left: auto; padding-left: var(--sp-md); color: var(--mid); transition: transform var(--dur-base) ease; }
details.rf-card-collapse[open] > summary::after { transform: rotate(180deg); }
/* Inline display:flex/grid on body children overrides the UA "closed details hides content" rule, so force it. */
details.rf-card-collapse:not([open]) > :not(summary) { display: none !important; }

/* Active empty states (RfEmptyState + inline .rf-empty blocks) */
.rf-empty { display:flex; flex-direction:column; align-items:flex-start; gap:var(--sp-sm); padding:22px; background:var(--s1); border:1px solid var(--line); border-radius:var(--r-lg); max-width:520px; }
.rf-empty-icon { font-size:var(--fs-2xl); color:var(--effort); }
.rf-empty-head { font-weight:700; color:var(--hi); font-size:16px; }
.rf-empty-body { color:var(--low); font-size:13.5px; line-height:1.5; }
.rf-empty-btn { margin-top:var(--sp-2xs); }
.rf-empty-secondary { color:var(--data); font-size:var(--fs-sm); }

/* ---- Contextual "?" education (RfInfo) + transparent low-confidence note ---- */
.rf-info { display: inline-block; vertical-align: middle; }
.rf-info-trigger { display: inline-flex; align-items: center; cursor: pointer; color: var(--low);
    font-size: var(--fs-sm); list-style: none; margin-left: 5px; }
.rf-info-trigger::-webkit-details-marker { display: none; }
.rf-info-trigger:hover { color: var(--data); }
.rf-info-body { margin-top: var(--sp-xs); font-size: 12.5px; color: var(--mid); line-height: 1.5;
    background: var(--s1); border: 1px solid var(--s3); border-radius: 8px; padding: var(--sp-sm) var(--sp-md); max-width: 340px; }
.rf-estimate-note { margin-top: var(--sp-sm); font-size: var(--fs-xs); color: var(--mid); line-height: 1.45;
    border-left: 2px solid var(--s3); padding-left: 9px; }

/* ---- How the field finished (RaceInsightsPanel, Mission 005 WP1): a run's finish-time histogram. Bars in steel,
   the reader's bar in race red, the median dashed in text ink; the numbers sit in text beside it, so the chart
   never carries the meaning alone. ---- */
.rf-ins { background: var(--s1); border: 1px solid var(--line); border-radius: 12px; padding: var(--sp-lg) var(--sp-xl); margin-bottom: var(--sp-md); }
.rf-ins-sum { font-size: var(--fs-sm); color: var(--mid); margin: var(--sp-3xs) 0 var(--sp-sm); }
/* Holds the Lumen drawing (FieldFinishChart), drawn for a 340 px card: wider than ~400 its 11 px text grows past the page's. */
.rf-ins-chart { display: block; width: 100%; max-width: 400px; }
.rf-ins-you { margin: var(--sp-sm) 0 0; color: var(--hi); font-size: var(--fs-sm); }
.rf-ins-form { display: flex; flex-wrap: wrap; align-items: end; gap: var(--sp-sm); margin-top: var(--sp-md); }
.rf-ins-form label { display: block; font-size: var(--fs-xs); color: var(--low); margin-bottom: var(--sp-3xs); }
.rf-ins-form input { width: 9em; }

/* ---- Running calculators (RunCalcAnswer, Mission 005 WP2) ---- */
.rf-calc { padding: var(--sp-lg) var(--sp-xl); margin-bottom: var(--sp-md); max-width: 820px; }
.rf-calc-ans { margin: var(--sp-md) 0 0; color: var(--hi); font-size: var(--fs-sm); }
.rf-calc-note { margin: var(--sp-xs) 0 0; color: var(--low); font-size: var(--fs-xs); line-height: 1.45; }
.rf-calc-table { margin-top: var(--sp-md); border-collapse: collapse; font-size: var(--fs-sm); }
.rf-calc-table th, .rf-calc-table td { padding: var(--sp-2xs) var(--sp-lg) var(--sp-2xs) 0; text-align: left; }
.rf-calc-table thead th { color: var(--low); font-weight: 500; font-size: var(--fs-xs); }
.rf-calc-table tbody th { color: var(--mid); font-weight: 500; }
.rf-calc-table td { color: var(--hi); }

/* ---- Race-day pace plan (RunCalcAnswer, Mission 006 WP3): printed, only the wristband strip appears. ---- */
.rf-wristband { margin-top: var(--sp-md); }
.rf-wristband-head { font-size: var(--fs-sm); color: var(--hi); font-weight: 600; }
/* SCOPED to a page that shows a wristband (acceptance review B-1): unscoped, this blanked every printed page of Web,
   Mobile and Perform. */
@media print {
  body:has(.rf-wristband) * { visibility: hidden; }
  body:has(.rf-wristband) .rf-wristband, body:has(.rf-wristband) .rf-wristband * { visibility: visible; color: #000 !important; }
  body:has(.rf-wristband) .rf-wristband { position: absolute; left: 0; top: 0; font-size: 10pt; }
  body:has(.rf-wristband) .rf-wristband .rf-calc-table th, body:has(.rf-wristband) .rf-wristband .rf-calc-table td { padding: 1px 8px 1px 0; }
}

/* ---- Share my result (Mission 005 WP3): the card at its natural 1200x630 ratio, never wider than the page. ---- */
.rf-team-place { font-size: var(--fs-xs); color: var(--hi); margin: -2px 0 var(--sp-2xs); }
.rf-h2h-score { font-size: var(--fs-lg); color: var(--hi); margin: var(--sp-md) 0 var(--sp-2xs); }
.rf-h2h-ahead { color: var(--hi); font-weight: 700; }
.rf-podiums { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: var(--sp-md); }
.rf-podium-cat { font-size: var(--fs-xs); color: var(--low); text-transform: uppercase; letter-spacing: .04em; }
.rf-podium-list { list-style: none; margin: var(--sp-2xs) 0 0; padding: 0; font-size: var(--fs-sm); color: var(--hi); }
.rf-podium-place { display: inline-block; min-width: 2.2em; color: var(--mid); }
.rf-podium-time { color: var(--mid); }
.rf-linkbtn { background: none; border: 0; padding: 0; color: var(--effort-text); cursor: pointer; }
.rf-share-card { display: block; width: 100%; max-width: 720px; height: auto; border-radius: 12px; border: 1px solid var(--line); }

/* ---- "Ask about this ride" canned Q&A panel (RfAsk) — zero-LLM, deliberately undecorated so it never
   reads as a third AI feature next to the narrative / weekly-review cards. ---- */
.rf-ask { margin-top: var(--sp-lg); }
.rf-ask-trigger { display: flex; align-items: center; gap: var(--sp-xs); cursor: pointer; color: var(--hi);
    font-size: 13.5px; font-weight: 600; list-style: none; }
.rf-ask-trigger::-webkit-details-marker { display: none; }
.rf-ask-trigger:hover { color: var(--data); }
.rf-ask-body { margin-top: var(--sp-sm); display: flex; flex-direction: column; gap: var(--sp-xs); }
.rf-ask-q { border: 1px solid var(--s3); border-radius: 8px; padding: var(--sp-sm) var(--sp-md); background: var(--s1); }
.rf-ask-q summary { cursor: pointer; font-size: var(--fs-sm); color: var(--hi); list-style: none; }
.rf-ask-q summary::-webkit-details-marker { display: none; }
.rf-ask-q p { margin: var(--sp-xs) 0 0; font-size: 12.5px; color: var(--mid); line-height: 1.5; }
.rf-ask-note { margin-top: var(--sp-2xs); font-size: 11.5px; color: var(--low); }

/* ---- Home "cockpit": next race + last result + your riders (guardian) ---- */
.rf-cockpit-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: var(--sp-lg); margin-bottom: var(--sp-2xl); }
.rf-cockpit-card { background: var(--s1); border: 1px solid var(--line); border-radius: 14px; padding: var(--sp-xl) var(--sp-2xl); }
.rf-cockpit-label { font-size: 11.5px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--low); margin-bottom: var(--sp-xs); }
.rf-cockpit-title { font-size: var(--fs-lg); font-weight: 700; color: var(--hi); }
.rf-cockpit-sub { font-size: var(--fs-sm); color: var(--mid); margin-top: var(--sp-3xs); }
.rf-cockpit-cta { display: inline-flex; align-items: center; gap: 5px; margin-top: var(--sp-md); font-size: var(--fs-sm); font-weight: 600; color: var(--data); text-decoration: none; }
.rf-cockpit-riders { display: flex; flex-wrap: wrap; gap: var(--sp-sm); }
.rf-cockpit-rider { display: flex; align-items: center; gap: var(--sp-sm); background: var(--s1); border: 1px solid var(--line);
    border-radius: 999px; padding: 5px 12px 5px 5px; text-decoration: none; color: var(--hi); font-size: var(--fs-sm); }
.rf-cockpit-rider img { width: 24px; height: 24px; border-radius: 50%; object-fit: cover; }

/* ---- Garmin attribution (RfGarminTag) ----
   Required by Garmin's API Brand Guidelines wherever Garmin device-sourced data is shown. Sizing only: the
   tag logo itself must never be recoloured, stretched or animated, so there is no filter/transform here and
   the aspect ratio is fixed at the asset's natural 2.911:1. Kept visually tied to the data it attributes. */
.rs-garmin-attr { display: inline-flex; align-items: center; gap: 5px; vertical-align: middle; }
.rs-garmin-logo { display: block; aspect-ratio: 2.911 / 1; object-fit: contain; border-radius: 2px; }
/* NOTE: no height here on purpose. A height rule beats the img height attribute and silently pinned
   every tag to one size, making RfGarminTag's LogoHeight a no-op. The component sets it inline. */
.rs-garmin-word { font-weight: 700; letter-spacing: .02em; color: var(--mid); }
.rs-garmin-model { color: var(--low); white-space: nowrap; }


/* ---- School championship table (schools series only), above the category chips ----
   A GRID rather than a flex row per line: rank, school, riders and points have to line up as columns down
   the whole table or it reads as four ragged sentences instead of a standing.

   The medal colours need their own selectors here. The rider rows scope theirs to `.rs-row .rs-rank`, so a
   rank rendered outside a .rs-row silently comes out flat grey — which is what happened first time. */
.rf-schooltable { margin-top: var(--sp-md); display: grid; grid-template-columns: 26px 1fr auto 86px; }

/* ROWS ARE display:contents, SO THE CELLS STAY GRID ITEMS. A CSS grid needs its cells as direct children,
   but a screen reader needs rows — the flat version was read as an unbroken run with nothing marking where
   one school ended and the next began (UX audit H6). The row wrappers exist only to carry role="row" and
   take no layout box.

   MEASURED, because the obvious choice is the wrong one. Three variants were built on this exact stylesheet
   and read back out of the ACCESSIBILITY TREE:
     flat grid, no roles          ->  generic, generic, generic … no table at all
     ARIA roles + display:contents ->  table > row > columnheader / rowheader / cell   (intact)
     native <table>, tr:contents   ->  table > generic, generic …   (row and cell semantics DESTROYED)
   So `display: contents` on a real <tr> strips exactly the semantics the element exists for, while explicit
   ARIA roles survive it. Do not "simplify" this into a <table>. */
.rf-schooltable > [role="row"] { display: contents; }
/* TOP, NOT CENTRE. The row is display:contents, so every cell stretches to the tallest one - and once a
   school expands its rounds and its nationals list, "centre" drifts the rank and the points halfway down a
   400px row, level with nothing. On an unexpanded row the two are indistinguishable. */
.rf-schooltable [role="row"] > * { padding: 9px 0; border-top: 1px solid var(--line); display: flex; align-items: flex-start; min-width: 0; }
.rf-schooltable [role="row"] > .rf-sh { padding: 0 0 var(--sp-sm); border-top: 0; font-size: var(--fs-2xs); letter-spacing: .10em;
  text-transform: uppercase; color: var(--low); font-weight: 600; }
.rf-schooltable [role="row"] > .rf-sh:last-of-type { justify-content: flex-end; }
.rf-schooltable [role="row"] > .rf-sh.rf-hn { justify-content: flex-end; padding-right: var(--sp-xl); }

.rf-sr { justify-content: center; font-family: var(--font-display); font-weight: 700; font-size: 15px; color: var(--low); }
.rf-sr.r1 { color: var(--gold); }
.rf-sr.r2 { color: var(--silver); }
.rf-sr.r3 { color: var(--bronze); }

.rf-ss { padding-left: var(--sp-md); font-size: var(--fs-md); color: var(--hi); }
.rf-ss > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The school row's per-round breakdown, opened by a native <details>. It has to be the browser's own
   disclosure because the Web standings page is static SSR with no interactivity at all, and using the same
   element on the phone keeps one markup for both. The summary keeps the single-line ellipsis the plain
   <span> above has, so a closed row is exactly as tall as it always was — and the rounds cost no width
   until asked for, where a column per round would force the table to scroll sideways at 320px. */
.rf-sdet { width: 100%; min-width: 0; }
.rf-sdet > summary { list-style: none; cursor: pointer; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; display: flex; align-items: center; gap: var(--sp-xs); }
.rf-sdet > summary::-webkit-details-marker { display: none; }
.rf-sdet > summary::after { content: "\203A"; color: var(--low); font-size: 15px; line-height: 1;
  transition: transform var(--dur-base) ease; }
.rf-sdet[open] > summary::after { transform: rotate(90deg); }
.rf-srounds { display: flex; flex-wrap: wrap; gap: var(--sp-xs); padding: var(--sp-sm) 0 var(--sp-3xs); }
.rf-srd { font-size: var(--fs-2xs); color: var(--low); background: rgba(255,255,255,.05);
  border: 1px solid var(--line); border-radius: 4px; padding: 3px 7px; font-variant-numeric: tabular-nums; }
.rf-srd > b { color: var(--hi); font-weight: 700; margin-right: 3px; }
@media (prefers-reduced-motion: reduce) { .rf-sdet > summary::after { transition: none; } }

.rf-sn { justify-content: flex-end; padding-right: var(--sp-xl); font-size: var(--fs-xs); color: var(--low);
  font-variant-numeric: tabular-nums; }

.rf-sp { justify-content: flex-end; font-family: var(--font-display); font-weight: 700; font-size: var(--fs-lg);
  color: var(--data); font-variant-numeric: tabular-nums; }
.rf-sp.r1 { color: var(--hi); }

/* On a narrow phone the rider count is the first thing to go — the standing is rank, school and points. */
@media (max-width: 520px) {
  .rf-schooltable { grid-template-columns: 22px 1fr 72px; }
  .rf-schooltable [role="row"] > .rf-sn, .rf-schooltable [role="row"] > .rf-sh.rf-hn { display: none; }
}

/* Notification bell (topbar) — badge + dropdown panel, matching .rs-menu's visual language but its own
   interactive markup: the hamburger is a native <details>, this needs @onclick to mark items read.

   .rf-bell is deliberately NOT position:relative. The bell sits to the LEFT of the profile avatar/hamburger
   on both platforms — it is not the rightmost topbar element the way .rs-menu is — so a panel anchored
   `right:0` to the bell itself runs off the left edge of the screen (its right edge flush with a point well
   left of the true edge). Leaving .rf-bell unpositioned lets the panel's containing block bubble up to
   .rf-topbar / .rs-topbar, both already `position: sticky` and spanning the full width, so `right` is
   measured from the actual screen edge regardless of where the bell falls in the topbar's flex row. */
.rf-bell-btn { position: relative; display: inline-flex; align-items: center; justify-content: center;
  width: 38px; height: 38px; border-radius: 9px; color: var(--hi); font-size: 20px; background: none;
  border: 0; cursor: pointer; padding: 0; }
.rf-bell-btn:hover { background: var(--s2); }
.rf-bell-badge { position: absolute; top: 3px; right: 3px; min-width: 15px; height: 15px; padding: 0 3px;
  border-radius: 999px; background: var(--effort); color: var(--effort-ink); font-size: 10px; font-weight: 700;
  line-height: 15px; text-align: center; font-variant-numeric: tabular-nums; }
.rf-bell-panel { position: absolute; right: 8px; top: calc(100% + 8px); width: 320px; max-width: calc(100vw - 24px);
  background: var(--s1); border: 1px solid var(--line-2); border-radius: var(--r-md); box-shadow: 0 12px 34px rgba(0,0,0,.55);
  padding: var(--sp-md); z-index: var(--z-sticky); max-height: 70vh; max-height: 70dvh; overflow-y: auto; -webkit-overflow-scrolling: touch; overscroll-behavior: contain; }
.rf-bell-head { display: flex; align-items: center; justify-content: space-between; padding: var(--sp-3xs) var(--sp-2xs) var(--sp-sm); }
.rf-bell-item { display: block; padding: var(--sp-sm); border-radius: 8px; text-decoration: none; color: inherit;
  border: 0; background: none; width: 100%; text-align: left; cursor: pointer; }
.rf-bell-item:hover { background: var(--s2); }
.rf-bell-item + .rf-bell-item { margin-top: var(--sp-3xs); }
.rf-bell-item.rf-bell-unread { background: var(--s2); }
.rf-bell-title { display: block; font-size: var(--fs-sm); font-weight: 600; color: var(--hi); }
.rf-bell-body { display: block; font-size: var(--fs-xs); color: var(--mid); margin-top: 1px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rf-bell-time { display: block; font-size: var(--fs-2xs); color: var(--low); margin-top: 3px; }

/* Team dashboard — the season/series accordion.
   The whole header row is the toggle rather than a chevron alone: on a phone a 12px arrow is a miss waiting
   to happen, and the row is the thing a manager is aiming at anyway. */
.rf-series-head { display: flex; align-items: center; gap: var(--sp-lg); width: 100%; padding: var(--sp-lg) var(--sp-2xl);
  background: none; border: 0; color: var(--hi); text-align: left; cursor: pointer; font: inherit; }
.rf-series-head:hover { background: var(--s2); }
.rf-series-logo { width: 34px; height: 34px; object-fit: contain; border-radius: 6px; background: #fff;
  padding: 3px; flex: 0 0 auto; }
/* "Schools like yours": the three measures a peer group is built from, each shown as the school's own
   figure with the group's beside it. auto-fit rather than three fixed columns so the row folds to two and
   then one on a narrow phone instead of overflowing the card — the layout failure this file's other rules
   exist to prevent. */
.rf-peer-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(96px, 1fr)); gap: var(--sp-md);
  margin-top: var(--sp-2xs); }
.rf-peer-grid > div { display: flex; flex-direction: column; gap: 1px; }
.rf-peer-grid .num { font-size: 19px; line-height: 1.1; color: var(--data); }
/* Monogram for a series with no logo file — same deterministic colour the mobile lists use, so one series
   always looks the same wherever it appears. */
.rf-series-mono { width: 34px; height: 34px; border-radius: 6px; flex: 0 0 auto; color: #0c0f15;
  font-family: 'Rajdhani', sans-serif; font-weight: 700; font-size: var(--fs-md); letter-spacing: .5px;
  display: flex; align-items: center; justify-content: center; }
/* ONE DECLARATION. This class was declared twice, unscoped, at 15px here and 14px further down, and the
   later one silently won for every use - so the 15px was dead and the file disagreed with itself about a
   name that appears on every series row. UX audit DS7. Merged at 14px, which is what actually rendered, so
   this is a correctness fix with no visual change. If the row and the card are ever meant to differ, scope
   the second one (.rf-series-row .rf-series-name) rather than declaring the bare class again. */
.rf-series-name { flex: 1 1 auto; font-weight: 600; font-size: var(--fs-md); line-height: 1.3; }
.rf-series-pts { color: var(--data); font-weight: 700; white-space: nowrap; }
.rf-series-pts-u { color: var(--low); font-weight: 400; font-size: var(--fs-xs); }
.rf-series-chev { color: var(--low); flex: 0 0 auto; }

/* Team dashboard — an expandable "By category" / "Boys / girls" row. Styled as a plain row rather than a
   button so the section still reads as a table; the chevron is the only affordance it needs. */
.rf-group-row { display: flex; align-items: center; gap: var(--sp-md); width: 100%; padding: var(--sp-xs) 0;
  background: none; border: 0; border-top: 1px solid var(--line); color: var(--hi); text-align: left;
  cursor: pointer; font: inherit; }
.rf-group-row:hover { background: var(--s1); }
.rf-group-chev { color: var(--low); font-size: var(--fs-xs); flex: 0 0 auto; width: 12px; }
.rf-group-label { flex: 1 1 auto; }

/* ---- admin card studio (/admin/social) ---------------------------------------------------------------
   Previews are social graphics on a near-black card, so the studio chrome stays quiet: the only saturated
   colour is state. A slot keeps its final aspect ratio from the first paint, so nothing reflows as renders
   land one at a time. */
.rf-card-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: var(--sp-2xl); margin-top: var(--sp-sm);
}
.rf-card-slot { border: 1px solid var(--line); border-radius: 8px; overflow: hidden; background: var(--s1); }
.rf-card-slot.is-failed { border-color: var(--bad); }

/* The proofing stage is a mid neutral, deliberately not the page background: a dark card previewed on the
   same tone as the chrome loses its own edges. */
.rf-card-thumb {
  position: relative; width: 100%; background: var(--s3);
  display: flex; align-items: center; justify-content: center;
}
.rf-ratio-post { aspect-ratio: 4 / 5; }
.rf-ratio-story { aspect-ratio: 9 / 16; }
.rf-ratio-reel  { aspect-ratio: 9 / 16; }
.rf-card-thumb img { width: 100%; height: 100%; object-fit: contain; display: block; }
.rf-card-msg { font-size: var(--fs-2xl); }

.rf-card-skeleton {
  width: 100%; height: 100%; background-size: 220% 100%; animation: rf-shimmer 1.4s linear infinite;
  background-image: linear-gradient(100deg, var(--s3) 30%, var(--line-2) 50%, var(--s3) 70%);
}
@keyframes rf-shimmer { to { background-position-x: -220%; } }

/* SKELETON ROWS - space reserved for a list that has not arrived yet (UX audit M8). 62px is `.rs-row`'s
   height, which is what lands in most of the places this is used, so the swap moves the page by almost
   nothing; --rf-skel-h overrides it where a taller card lands. Same shimmer as .rf-card-skeleton rather
   than a second animation, so there is one loading texture in the product and not two. */
/* The default is DECLARED on the container, not written as a var() fallback. DS4's rule is that a missing
   token must fail loudly rather than paint something plausible, and a fallback is what an editor adds by
   reflex - the guard for it caught this line, correctly. A call site overrides by setting --rf-skel-h in
   its own style attribute, which wins over this declaration without anything needing an exception. */
/* "Connecting" strip for the prerender gap (UX audit H11). FIXED, not in the flow: it exists only until
   the circuit is live, so anything in the flow would push the page down and snap it back - which is the
   layout shift M8 is about, arriving through the fix for H11. Sits above the bottom tab bar and clears the
   safe-area inset. */
.rf-connecting {
  position: fixed; left: 50%; transform: translateX(-50%); z-index: var(--z-toast);
  bottom: calc(72px + env(safe-area-inset-bottom, 0px));
  display: flex; align-items: center; gap: var(--sp-sm);
  padding: 7px 14px; border-radius: 999px; max-width: calc(100vw - 32px);
  background: var(--s2); border: 1px solid var(--line); color: var(--mid); font-size: var(--fs-xs);
  box-shadow: 0 6px 20px rgba(0,0,0,.35);
}
/* The island variant: in the flow, but it sits INSIDE the card it is about, so it displaces only that
   card's own content rather than the page under it. */
.rf-connecting-inline {
  display: inline-flex; align-items: center; gap: 7px;
  color: var(--mid); font-size: var(--fs-xs); padding: var(--sp-3xs) 0;
}
.rf-connecting-dot {
  width: 7px; height: 7px; border-radius: 50%; flex: 0 0 auto; background: var(--effort);
  animation: rf-connecting-pulse 1.2s ease-in-out infinite;
}
@keyframes rf-connecting-pulse { 0%, 100% { opacity: .35; } 50% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .rf-connecting-dot { animation: none; opacity: .8; } }

.rf-skel { display: grid; gap: var(--sp-sm); --rf-skel-h: 62px; }
.rf-skel-row {
  height: var(--rf-skel-h); border-radius: var(--r-md); background-size: 220% 100%;
  animation: rf-shimmer 1.4s linear infinite;
  background-image: linear-gradient(100deg, var(--s3) 30%, var(--line-2) 50%, var(--s3) 70%);
}
@media (prefers-reduced-motion: reduce) { .rf-skel-row { animation: none; } }
@media (prefers-reduced-motion: reduce) { .rf-card-skeleton { animation: none; } }

.rf-card-meta { padding: var(--sp-sm) var(--sp-md) var(--sp-md); }

/* State reads from shape and text as well as colour — the label beside it always says the same thing. */
.rf-dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; background: var(--mid); }
.rf-dot-ready { background: var(--good); }
.rf-dot-failed { background: var(--bad); }
.rf-dot-rendering { background: var(--data); }
.rf-dot-queued { background: var(--mid); }

.rf-series-list { max-height: 260px; overflow-y: auto; border: 1px solid var(--line); border-radius: 8px; }
.rf-series-row {
  display: flex; justify-content: space-between; align-items: center; gap: var(--sp-md); width: 100%;
  padding: var(--sp-sm) var(--sp-md); background: none; border: 0; border-bottom: 1px solid var(--line); text-align: left;
  /* A <button> does not inherit colour from the page — it defaults to the UA's canvastext, which on this
     dark shell rendered the series names nearly invisible. */
  color: inherit; font: inherit;
}
.rf-series-row:last-child { border-bottom: 0; }
.rf-series-row:hover { background: rgba(127,127,127,.08); }
.rf-series-row.active { background: color-mix(in srgb, var(--effort) 12%, transparent); }

.rf-cat-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: var(--sp-2xs) var(--sp-lg); }
.rf-cat { display: flex; align-items: center; gap: var(--sp-sm); font-size: var(--fs-sm); }
.rf-cat span:last-child { margin-left: auto; }
/* A category held out of the picker because it ran as two start groups. Dimmed rather than hidden: the
   operator needs to SEE that the category exists and was deliberately withheld, or the missing podium reads
   as the studio having lost a category. */
.rf-cat-held { opacity: .55; cursor: not-allowed; }
.rf-cat-held input { cursor: not-allowed; }

.rf-slide-list { margin: 0; padding-left: 18px; }
.rf-slide-pick label { display: inline-flex; align-items: center; gap: var(--sp-sm); cursor: pointer; }
/* An unticked slide stays listed and keeps its number - dimmed, not removed, so it can be ticked back on and
   "slide 12" goes on meaning the same card. */
.rf-slide-pick .rf-slide-off { opacity: .45; }
.rf-kind {
  display: inline-block; min-width: 74px; font-size: var(--fs-2xs); letter-spacing: .06em; text-transform: uppercase;
  color: var(--mid);
}
.rf-admin-log {
  max-height: 220px; overflow: auto; font-size: var(--fs-xs); padding: var(--sp-md);
  background: rgba(127,127,127,.07); border-radius: 6px; margin-top: var(--sp-xs);
}

/* ---- card studio: slide photo uploads ----------------------------------------------------------------- */
.rf-photo-list { display: flex; flex-direction: column; gap: var(--sp-sm); }
.rf-photo-row {
  display: flex; align-items: center; gap: var(--sp-lg);
  border: 1px solid var(--line); border-radius: 8px; padding: var(--sp-sm) var(--sp-md); background: var(--s1);
}
.rf-photo-thumb {
  width: 56px; height: 70px; flex: 0 0 auto; border-radius: 6px; overflow: hidden;
  background: var(--s3); display: flex; align-items: center; justify-content: center;
  color: var(--low); font-size: 20px;
}
.rf-photo-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Upload progress. The bar is deliberately plain: at a race venue on cellular the only question is whether
   the thing is still moving, and an animated stripe answers that even when the percentage is stuck. */
.rf-upload-bar {
  height: 6px; border-radius: 3px; background: var(--s3); overflow: hidden;
}
.rf-upload-bar > span {
  display: block; height: 100%; background: var(--effort);
  transition: width var(--dur-base) linear;
  background-image: linear-gradient(90deg, rgba(255,255,255,.28) 25%, transparent 25%,
                    transparent 50%, rgba(255,255,255,.28) 50%, rgba(255,255,255,.28) 75%, transparent 75%);
  background-size: 16px 16px;
  animation: rf-upload-stripe .6s linear infinite;
}
.rf-upload-text { color: var(--low); font-size: var(--fs-xs); margin-top: var(--sp-2xs); font-variant-numeric: tabular-nums; }

/* The photo section collapses: ten file inputs is taller than the rest of the form put together. */
.rf-photos > summary {
  cursor: pointer; list-style: none; display: flex; align-items: baseline; gap: var(--sp-sm);
  color: var(--hi); font-size: var(--fs-md); font-weight: 600; padding: var(--sp-sm) 0;
}
.rf-photos > summary::-webkit-details-marker { display: none; }
.rf-photos > summary::before {
  content: "\25B8"; color: var(--low); font-size: var(--fs-xs); transition: transform var(--dur-fast) ease;
}
.rf-photos[open] > summary::before { transform: rotate(90deg); }
.rf-photos-count { margin-left: auto; color: var(--low); font-size: var(--fs-xs); font-weight: 400; }

.rf-recent { display: flex; flex-direction: column; gap: var(--sp-xs); }
.rf-recent-row {
  display: flex; align-items: center; gap: var(--sp-md);
  border: 1px solid var(--line); border-radius: 8px; padding: var(--sp-sm) var(--sp-md); background: var(--s1);
}

.rf-caption { border: 1px solid var(--line); border-radius: 10px; padding: var(--sp-lg) var(--sp-xl); background: var(--s1); }
.rf-caption textarea { font-size: var(--fs-sm); line-height: 1.5; resize: vertical; }

/* ---- card studio on a phone -------------------------------------------------------------------------
   The studio had NO width media query at all, on the one page in this product whose primary device is a
   phone at a race venue. Three consequences, all measured at 390px: the job header could not shrink (three
   flex children, no wrap, no min-width:0) so it overflowed the document and pushed Share, Zip and the state
   badge off the RIGHT edge — the three things the page exists to deliver; the review grid is one column, so
   reaching those buttons meant scrolling ~5,400px back to the top; and every control is a 31px btn-sm,
   against the 44px rule this codebase added specifically to stop that. */
@media (max-width: 720px) {
  .rf-job-head { flex-wrap: wrap; }
  .rf-job-head > * { min-width: 0; }

  /* The actions follow the operator down the set instead of staying at the top of it. */
  .rf-job-actions {
    position: sticky; bottom: 0; z-index: 5;
    display: flex; gap: var(--sp-sm); align-items: center;
    padding: 10px 16px calc(10px + env(safe-area-inset-bottom));
    margin: 0 -16px; background: var(--s1); border-top: 1px solid var(--line);
  }
  .rf-job-actions .btn { flex: 1 1 auto; min-height: 44px; }

  /* Every control in the studio, not just the two that matter most. */
  .rf-card-slot .btn, .rf-caption .btn, .rf-recent-row .btn { min-height: 44px; }

  /* An email is one unbreakable token. "consent by jacques@codesense.co.za" is ~150px of min-content in a
     column that has 149px at 390px and 79px at 320px, and the row had no overflow rule to contain it. */
  .rf-photo-row .min-w-0, .rf-recent-row .min-w-0 { overflow-wrap: anywhere; }
}
@keyframes rf-upload-stripe { from { background-position: 0 0; } to { background-position: 16px 0; } }
@media (prefers-reduced-motion: reduce) {
  .rf-upload-bar > span { animation: none; background-image: none; }
}
/* A Tailwind-shaped name in a Bootstrap project (UX audit DS9). Kept rather than renamed - it has call
   sites in markup across both heads - but named here so the next reader knows it is a deliberate leftover
   and not the start of a utility layer. */
.min-w-0 { min-width: 0; }

/* ---- Starting grid (allowlisted pilot) ------------------------------------------------------------
   The SVG itself is drawn by RaceSense.Domain.StartingGridSvg and uses --effort / --s2 / --line / --hi /
   --low directly, so it re-themes with everything else. These classes are only the card around it. */
.rf-grid-card { padding: var(--sp-xl); }
.rf-grid-sub { color: var(--mid); font-size: var(--fs-sm); margin-bottom: var(--sp-md); display: flex; gap: var(--sp-md); align-items: baseline; flex-wrap: wrap; }
.rf-grid-you { background: var(--effort); color: var(--effort-ink); font-weight: 700; font-size: var(--fs-2xs); padding: var(--sp-3xs) var(--sp-sm); border-radius: 999px; }
/* Wide categories (Youth 15 Boys is 165 riders) scroll sideways INSIDE the card rather than pushing the
   page wider — a horizontally scrolling body on a phone is how a dashboard stops being usable.

   THE GRID TRACK IS LATE AND LOAD-BEARING. overflow-x:auto alone did nothing: .rs-main is a COLUMN flex
   container, so its items size to their content across the inline axis, and the svg's 640px floor
   propagated all the way up — measured at 573px viewport the card came out 668 wide and the BODY scrolled
   while the card itself did not. min-width:0 on the ancestors does not fix it in a column flex layout.
   A single grid track of minmax(0, 1fr) refuses to grow past its share, which stops the intrinsic width
   escaping this element, and is self-contained: it holds wherever the card is placed. */
.rf-grid-svg { display: grid; grid-template-columns: minmax(0, 1fr); overflow-x: auto; -webkit-overflow-scrolling: touch; }
/* Portrait boxes put a six-wide row at ~318 units, so it fits a phone without scrolling and the min-width
   that used to force 640 is gone. The cap stops the viewBox scaling to a 900px card on desktop and turning
   into six enormous tiles; the grid track above stays, so an unusually narrow container still scrolls the
   card rather than the page. */
.rf-grid-svg svg { max-width: 560px; }
.rf-grid-note { color: var(--low); font-size: var(--fs-xs); line-height: 1.5; margin: var(--sp-md) 0 0; }

/* The chase line: who the viewer has to catch, and by how much. */
.rf-grid-chase { display: flex; gap: var(--sp-md); align-items: baseline; flex-wrap: wrap; margin: 0 0 var(--sp-md); font-size: var(--fs-xs); color: var(--mid); }
.rf-grid-chase-label { color: var(--low); text-transform: uppercase; letter-spacing: .5px; font-size: var(--fs-2xs); }
.rf-grid-chase-item { white-space: nowrap; }
.rf-grid-chase-gap { color: var(--good); font-weight: 700; }

.rf-grid-chase-why { color: var(--low); font-size: var(--fs-2xs); line-height: 1.45; margin: -4px 0 10px; }
.rf-grid-chase-gap-behind { color: var(--bad); font-weight: 700; }

/* Standings: per-category sections + the ranked list inside them.
   THE HEADING NEEDS BOTH OVERRIDES. `h1, h2 { text-transform: uppercase }` above applies to every h2, and
   there is no h2 font-size outside .rf-legal, so an unstyled category heading renders ~32px UPPERCASE in
   the display font - "YOUTH 18/19 GIRLS" shouting over its own rows. This page already fights that default
   inline on every category and school line; the heading gets a class instead.
   THE LIST RESET IS NOT OPTIONAL EITHER. There is no ol/ul reset anywhere in this file, and Bootstrap
   Reboot loads first on both heads with `ol/ul { padding-left: 2rem }` plus markers - unstyled, every row
   would indent 32px (8.5% of a 375px viewport). The rows carry their own rank, so a marker would print a
   second, row-derived number beside a shared API rank. */
.rf-catsec { margin: 0 0 var(--sp-xl); }
.rf-cathead {
  text-transform: none; letter-spacing: 0; font-size: 15px; font-weight: 700;
  color: var(--hi); margin: 0 0 var(--sp-xs); padding: 0 var(--sp-3xs);
}
.rf-rank-list { list-style: none; padding: 0; margin: 0; }

/* Standings X4: the reader's own riders, and a jump index over the category sections.
   .rf-catindex is NOT a second copy of .rs-chips. The chips navigate to a filtered page (one category);
   these scroll within the all-categories view. Measured before adding it: the chip row is already 192px
   and five wrapped lines at 375px, so this one sits BELOW the fold-critical area, after the your-riders
   card, and is styled quieter than the chips so the two do not read as the same control. */
.rf-yours { padding: var(--sp-md) var(--sp-lg); margin: 0 0 var(--sp-lg); border-left: 3px solid var(--good); }
.rf-yours-row { display: flex; align-items: center; gap: var(--sp-md); padding: var(--sp-xs) var(--sp-3xs); }
.rf-yours-row + .rf-yours-row { border-top: 1px solid var(--line-2); }
.rf-yours-row a, .rf-yours-row .rf-yours-jump {
  flex: 1; min-width: 0; text-decoration: none; color: inherit;
  background: none; border: 0; padding: 0; text-align: left; font: inherit; cursor: pointer;
}
.rf-yours-name { display: block; font-size: var(--fs-md); color: var(--hi); }
.rf-yours-meta { display: block; font-size: var(--fs-2xs); color: var(--low); }
.rf-yours-rank { font-size: 16px; font-weight: 700; color: var(--hi); min-width: 2.2em; text-align: right; }

/* The jump index and the search are this surface's own controls, so they can reach the full 44px without
   touching anything else. The index keeps its compact 28px box and widens its gap to 16px, which the
   hit area then fills (inset:-8px) - 44px tappable, targets exactly adjacent, never overlapping. */
.rf-catindex { display: flex; flex-wrap: wrap; gap: var(--sp-2xl); margin: 0 0 var(--sp-xl); }
.rf-catindex a, .rf-catindex button {
  display: inline-flex; align-items: center; gap: var(--sp-xs); padding: 4px 9px;
  border: 1px solid var(--line-2); border-radius: 999px; text-decoration: none;
  font-size: var(--fs-xs); color: var(--mid); background: transparent;
}
.rf-catindex a:hover, .rf-catindex a:focus-visible,
.rf-catindex button:hover, .rf-catindex button:focus-visible { color: var(--hi); border-color: var(--low); }
.rf-catindex a, .rf-catindex button { position: relative; }
/* -9px for the same padding-box reason: 28px box, 1px border, 16px gap -> 8px past the border box each
   side fills the gap exactly and reaches 44px. -8px measured 42px. */
.rf-catindex a::after, .rf-catindex button::after { content: ""; position: absolute; inset: -9px; border-radius: inherit; }
.rf-catindex-n { font-size: var(--fs-2xs); color: var(--low); font-variant-numeric: tabular-nums; }

/* Standings name search. Exists because find-in-page is not universal - iOS WKWebView has none - and
   Ctrl-F was the only cross-category rider lookup this product had. */
.rf-search { display: flex; gap: var(--sp-sm); align-items: center; flex-wrap: wrap; }
.rf-search input[type="search"] { flex: 1; min-width: 180px; max-width: 340px; min-height: 44px; }
.rf-search button[type="submit"] { min-height: 44px; }
/* Web renders Clear as a link, Mobile as a button - reset the button chrome so they match. */
/* N8: this measured 30x18px, tappable 19px - BELOW the 24px WCAG 2.5.8 AA minimum, not merely the
   44px AAA one. It is a control this workstream added, and the touch-target pass that "fixed" the
   others measured four controls and omitted this fifth. It sits in the .rf-search flex row beside a
   44px input and a 44px button, so matching them costs no layout at all. */
.rf-search-clear {
  display: inline-flex; align-items: center; min-height: 44px; padding: 0 var(--sp-sm);
  font-size: var(--fs-xs); color: var(--low); text-decoration: none;
  background: none; border: 0; font-family: inherit; cursor: pointer;
}
.rf-search-clear:hover, .rf-search-clear:focus-visible { color: var(--hi); }

/* ---- WHO A SCHOOL SENDS TO NATIONALS -----------------------------------------------------------------
   The school championship says who is winning; this says how the season ends for them. It sits on the row
   itself, because "does my school send a team" was previously answerable only by opening the qualification
   tab and holding ten categories in your head. */
.rf-snat { margin-top: var(--sp-md); padding-top: var(--sp-sm); border-top: 1px solid var(--line); }
/* FLEX AND WRAP, NOT A TWO-COLUMN GRID. This panel lives inside the school-NAME cell of the table above,
   which is 219px on a 375pt phone - not the 343px the row gets. An `auto` tags column took 177px of that
   and left the name 34px, so "Miani Du Plooy" came down the page one word per line, the category took
   three lines under it, and a four-rider school ran 400px tall. Reported from a real phone.

   Wrapping fixes it at ANY container width, which matters because this one is set by a table column and
   not by the viewport - a media query would be guessing. Wide enough: name and tags share a line with the
   category under them. Too narrow: the tags drop to their own line and the category to a third, and every
   one of the three is a single line. Measured after: 69px per rider, down from 120px, nothing broken
   mid-name.

   `order` puts the category last VISUALLY while the DOM keeps name, category, tags - which is the order it
   should be read aloud in. The min-width is what makes the tags wrap instead of the name: flex shrinks
   items before it wraps them, so without a floor the name is starved again. `min()` so a container
   narrower than the floor still cannot be overflowed. */
.rf-snat-row { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-3xs) var(--sp-sm);
  padding: var(--sp-2xs) 0; font-size: var(--fs-sm); }
.rf-snat-row + .rf-snat-row { border-top: 1px dotted var(--line); }
.rf-snat-name { color: var(--hi); flex: 1 1 auto; min-width: min(100%, 9em); }
.rf-snat-cat { order: 3; flex: 1 1 100%; font-size: var(--fs-2xs); color: var(--low); }
.rf-snat-tags { order: 2; flex: 0 1 auto; margin-left: auto; display: flex; flex-wrap: wrap;
  gap: var(--sp-2xs); justify-content: flex-end; }
/* TWO COUNTS ON THE ROW: how many riders travel with the SCHOOL team, and how many are in the PROVINCIAL
   one. Numbers only, so the school table stays scannable down a column of ten - the words live in the
   title and aria-label, because a bare digit is not a label for anyone reading with their ears.
   The two are never the same fact, so they never share a colour. */
.rf-snat-n { display: inline-block; min-width: 1.35rem; margin-left: var(--sp-2xs); padding: 0 var(--sp-2xs);
  border-radius: 8px; font-size: var(--fs-2xs); font-weight: 700; text-align: center;
  vertical-align: middle; font-variant-numeric: tabular-nums; }
.rf-snat-n.is-school { background: var(--effort); color: var(--effort-ink); }
.rf-snat-n.is-prov { background: var(--good); color: #06210f; }
/* Inside the expanded school the pills follow a name, as they do on the qualification board. */
.rf-snat-tags .rf-q-prov, .rf-snat-tags .rf-q-team { margin-left: 0; }

/* ---- Mission 001: multi-sport timeline card (RaceSense.Components/AthleteTimelineCard.razor) ----
   Shared by Web and Mobile. The sport tabs are radio inputs so they work on the static-SSR web dashboard;
   :has() hides the rows of the other sport. No JavaScript, no interactivity required. */
/* Its own heading: .rf-dash-h is SCOPED to the web Dashboard (Dashboard.razor.css) and never reaches a child
   component, so borrowing it left the card's headings unstyled. Same look, defined where the card can see it. */
.rf-tl-h { font-size: var(--fs-2xs); letter-spacing: .09em; text-transform: uppercase; color: var(--low); font-weight: 600; margin: var(--sp-xl) 0 var(--sp-xs); }
.rf-tl-totals { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: var(--sp-md); margin: var(--sp-sm) 0 var(--sp-xl); }
.rf-tl-total { background: var(--s1); border: 1px solid var(--line); border-radius: 10px; padding: var(--sp-lg); }
.rf-tl-total-sport { font-size: var(--fs-2xs); letter-spacing: 1px; color: var(--low); font-weight: 700; }
.rf-tl-total-num { font-family: var(--font-display); font-size: var(--fs-2xl); font-weight: 700; color: var(--hi); line-height: 1.1; font-variant-numeric: tabular-nums; }
.rf-tl-total-num span { font-family: inherit; font-size: var(--fs-sm); color: var(--mid); font-weight: 600; margin-left: 2px; }
.rf-tl-total-sub { display: flex; flex-wrap: wrap; gap: var(--sp-3xs) var(--sp-md); font-size: var(--fs-xs); color: var(--mid); margin-top: var(--sp-3xs); }
.rf-tl-tabs { display: flex; gap: var(--sp-xs); border: 0; padding: 0; margin: 0 0 var(--sp-md); }
.rf-tl-tabs legend { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
.rf-tl-tabs input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.rf-tl-tabs label { font-size: var(--fs-2xs); font-weight: 700; letter-spacing: .6px; text-transform: uppercase; color: var(--mid); border: 1px solid var(--line-2); border-radius: 999px; padding: 0 var(--sp-lg); cursor: pointer; min-height: 32px; display: inline-flex; align-items: center; }
.rf-tl-tabs input:checked + label { background: var(--hi); color: var(--bg); border-color: var(--hi); }
.rf-tl-tabs input:focus-visible + label { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
.rf-tl:has(input[value="Cycling"]:checked) .rf-tl-row:not([data-sport="Cycling"]) { display: none; }
.rf-tl:has(input[value="Running"]:checked) .rf-tl-row:not([data-sport="Running"]) { display: none; }
.rf-tl-list { list-style: none; margin: 0; padding: 0; }
.rf-tl-row { display: grid; grid-template-columns: 52px auto minmax(0, 1fr) auto; gap: var(--sp-md); align-items: baseline; padding: var(--sp-md) 0; border-top: 1px solid var(--line); }
.rf-tl-row:first-child { border-top: 0; }
.rf-tl-date { font-size: var(--fs-2xs); font-weight: 700; color: var(--low); letter-spacing: .5px; font-variant-numeric: tabular-nums; }
.rf-tl-chip { font-size: var(--fs-2xs); font-weight: 800; letter-spacing: .6px; padding: var(--sp-3xs) var(--sp-xs); border-radius: 4px; background: var(--s3); color: var(--data); }
.rf-tl-chip[data-sport="Running"] { background: var(--data); color: var(--bg); }
.rf-tl-name { min-width: 0; color: var(--hi); font-size: var(--fs-md); overflow-wrap: anywhere; }
.rf-tl-name a { color: inherit; }
.rf-tl-pace { display: block; font-size: var(--fs-xs); color: var(--mid); font-variant-numeric: tabular-nums; }
.rf-tl-head { font-family: var(--font-display); font-size: var(--fs-lg); font-weight: 700; color: var(--hi); text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }
.rf-tl-pb { display: inline-block; margin-left: var(--sp-xs); font-size: var(--fs-2xs); font-weight: 800; background: var(--good); color: #06210f; border-radius: 999px; padding: var(--sp-3xs) var(--sp-sm); vertical-align: middle; }
.rf-tl-self { display: block; font-size: var(--fs-2xs); color: var(--low); text-transform: uppercase; letter-spacing: .04em; }
.rf-tl-pbs { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-sm); }
.rf-tl-pbs li { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--sp-3xs) var(--sp-md); padding: var(--sp-sm) var(--sp-md); background: var(--s1); border: 1px solid var(--line); border-radius: 8px; }
.rf-tl-pbs-what { color: var(--hi); font-weight: 700; font-size: var(--fs-sm); }
.rf-tl-pbs-time { font-family: var(--font-display); font-size: var(--fs-lg); font-weight: 700; color: var(--hi); text-align: right; font-variant-numeric: tabular-nums; }
.rf-tl-pbs-when { grid-column: 1 / -1; font-size: var(--fs-xs); color: var(--low); }
.rf-tl-note { font-size: var(--fs-xs); color: var(--low); margin: var(--sp-sm) 0 0; }
/* Mission 001 F-02: "More results that may be yours" (RaceSense.Components/ClaimSuggestionsCard.razor). */
.rf-sug-lead { font-size: var(--fs-sm); color: var(--low); margin: 0 0 var(--sp-sm); }
.rf-sug-list { list-style: none; margin: 0; padding: 0; }
.rf-sug-row { display: flex; align-items: baseline; gap: var(--sp-md); padding: var(--sp-md) 0; border-top: 1px solid var(--line); }
.rf-sug-row:first-child { border-top: 0; }
.rf-sug-name { flex: 1; min-width: 0; color: var(--hi); overflow-wrap: anywhere; }
.rf-sug-meta { font-size: var(--fs-2xs); color: var(--low); font-variant-numeric: tabular-nums; white-space: nowrap; }
.rf-sug-cta { font-weight: 700; white-space: nowrap; }
/* One-off running events on the Standings tab (RaceSense.Components/RunningEventsList.razor). */
.rf-evt { margin-top: var(--sp-lg); }
.rf-evt-list { list-style: none; margin: 0; padding: 0; }
.rf-evt-row { padding: var(--sp-md) 0; border-top: 1px solid var(--line); }
.rf-evt-row:first-child { border-top: 0; }
.rf-evt-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-3xs) var(--sp-md); }
.rf-evt-name { flex: 1; min-width: 0; color: var(--hi); font-weight: 700; overflow-wrap: anywhere; }
.rf-evt-meta { font-size: var(--fs-2xs); color: var(--low); font-variant-numeric: tabular-nums; }
.rf-evt-dists { display: flex; flex-wrap: wrap; gap: var(--sp-xs); margin-top: var(--sp-xs); }
.rf-evt-dist { font-size: var(--fs-xs); font-weight: 700; padding: var(--sp-3xs) var(--sp-sm); border: 1px solid var(--line); border-radius: 999px; text-decoration: none; }
.rf-evt-more > summary { cursor: pointer; font-weight: 700; padding: var(--sp-sm) 0; }
@media (max-width: 520px) {
  .rf-tl-row { grid-template-columns: auto minmax(0, 1fr) auto; }
  .rf-tl-date { grid-column: 1 / -1; }
}

/* ===== THE EVENTS HUB (Mission 015, multi-sport slice 1) =====
   One list for every sport, sport as a filter (chips reuse .rs-chip), one event page. Cards and the page share the
   tokens above; the sport is an icon (.rf-sport-icon, currentColor) beside a short label, never colour alone.
   Touch targets: a whole card is the link; chips keep .rs-chip's enlarged ::after hit area. */
.rf-sport-icon { flex: none; vertical-align: -3px; }
.rf-ev-chips { margin-bottom: var(--sp-sm); }
.rf-ev-chip { display: inline-flex; align-items: center; gap: var(--sp-2xs); }
.rf-ev-count { color: var(--mid); margin-left: var(--sp-3xs); font-variant-numeric: tabular-nums; }
.rs-chip.on .rf-ev-count { color: inherit; opacity: .8; }
.rf-ev-month { margin-top: var(--sp-2xl); }
.rf-ev-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-sm); }
.rf-ev-card { display: flex; gap: var(--sp-lg); align-items: flex-start; min-height: 64px; padding: var(--sp-lg) var(--sp-xl);
  background: var(--s1); border: 1px solid var(--line); border-radius: var(--r-lg); color: var(--hi); text-decoration: none; }
.rf-ev-card:hover { background: var(--s2); color: var(--hi); }
.rf-ev-card:focus-visible { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
.rf-ev-card.is-faded { opacity: .62; }
.rf-ev-date { flex: none; width: 44px; text-align: center; line-height: 1.05; }
.rf-ev-day { display: block; font-size: var(--fs-2xl); font-weight: 700; font-variant-numeric: tabular-nums; }
.rf-ev-mon { display: block; font-size: var(--fs-2xs); text-transform: uppercase; letter-spacing: .06em; color: var(--mid); }
.rf-ev-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: var(--sp-3xs); }
.rf-ev-sport { display: inline-flex; align-items: center; gap: var(--sp-2xs); font-size: var(--fs-xs); color: var(--mid); margin: 0; }
.rf-ev-name { font-size: var(--fs-md); font-weight: 600; overflow-wrap: anywhere; }
.rf-ev-meta { font-size: var(--fs-xs); color: var(--mid); overflow-wrap: anywhere; }
.rf-ev-flag { align-self: flex-start; font-size: var(--fs-2xs); padding: var(--sp-3xs) var(--sp-xs); border-radius: 999px; border: 1px solid var(--line-2); color: var(--hi); }
.rf-ev-flag.is-ok { color: var(--good); border-color: rgba(63,209,122,.45); }
.rf-ev-saved { flex: none; color: var(--effort-text); font-size: var(--fs-lg); }
.rf-ev-empty { padding: var(--sp-2xl); background: var(--s1); border: 1px dashed var(--line-2); border-radius: var(--r-lg); margin-top: var(--sp-lg); }
.rf-ev-page { max-width: 720px; }
.rf-ev-title { margin: var(--sp-2xs) 0 var(--sp-sm); overflow-wrap: anywhere; }
.rf-ev-banner { padding: var(--sp-sm) var(--sp-lg); border-radius: var(--r-lg); background: rgba(227,6,19,.12); border: 1px solid rgba(227,6,19,.4); }
.rf-ev-when { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-sm) var(--sp-lg); margin-bottom: var(--sp-lg); font-size: var(--fs-md); }
.rf-ev-cal summary { cursor: pointer; display: inline-flex; align-items: center; gap: var(--sp-2xs); min-height: 44px; padding: 0 var(--sp-lg);
  border: 1px solid var(--line-2); border-radius: 999px; color: var(--hi); list-style: none; }
.rf-ev-cal summary::-webkit-details-marker { display: none; }
.rf-ev-cal summary:focus-visible { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
.rf-ev-cal-menu { display: flex; flex-direction: column; gap: var(--sp-xs); margin-top: var(--sp-sm); padding: var(--sp-lg);
  background: var(--s1); border: 1px solid var(--line); border-radius: var(--r-lg); }
.rf-ev-cal-menu a { min-height: 44px; display: flex; align-items: center; }
.rf-ev-actions { display: flex; flex-wrap: wrap; gap: var(--sp-sm); margin-bottom: var(--sp-sm); }
.rf-ev-actions .btn { min-height: 44px; display: inline-flex; align-items: center; gap: var(--sp-xs); }
.rf-ev-hint { font-size: var(--fs-xs); color: var(--mid); margin: var(--sp-2xs) 0 var(--sp-sm); }
.rf-ev-facts { display: grid; grid-template-columns: max-content 1fr; gap: var(--sp-xs) var(--sp-xl); margin: var(--sp-lg) 0;
  padding: var(--sp-lg) var(--sp-xl); background: var(--s1); border: 1px solid var(--line); border-radius: var(--r-lg); }
.rf-ev-facts dt { color: var(--mid); font-weight: 500; font-size: var(--fs-sm); }
.rf-ev-facts dd { margin: 0; overflow-wrap: anywhere; }
.rf-ev-source { font-size: var(--fs-xs); color: var(--mid); }

/* ===== HOME (Mission 016) =====
   RaceSense.Components/HomeFeed.razor on both heads: short sections, each a heading and rows that are whole links
   (44px+ targets). Events reuse .rf-ev-card; the series row reuses .rs-chip. */
.rf-home { display: flex; flex-direction: column; gap: var(--sp-2xl); max-width: 720px; }
.rf-home-h { font-size: var(--fs-xs); font-weight: 700; text-transform: uppercase; letter-spacing: .08em; color: var(--mid); margin: 0 0 var(--sp-sm); }
.rf-home-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-sm); }
.rf-home-row { display: flex; align-items: center; gap: var(--sp-lg); min-height: 56px; padding: var(--sp-md) var(--sp-xl);
  background: var(--s1); border: 1px solid var(--line); border-radius: var(--r-lg); color: var(--hi); text-decoration: none; }
a.rf-home-row:hover { background: var(--s2); color: var(--hi); }
.rf-home-row:focus-visible { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
.rf-home-row.is-lead { border-left: 3px solid var(--effort); }
.rf-home-row-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: var(--sp-3xs); }
.rf-home-row-title { font-weight: 600; overflow-wrap: anywhere; }
.rf-home-row-meta { font-size: var(--fs-xs); color: var(--mid); overflow-wrap: anywhere; }
.rf-home-row-go { flex: none; font-size: var(--fs-xs); font-weight: 700; color: var(--effort-text); white-space: nowrap; }
.rf-home-chev { flex: none; color: var(--mid); }
.rf-home-av { flex: none; width: 32px; height: 32px; border-radius: 50%; object-fit: cover; }
.rf-home-pb { flex: none; font-size: var(--fs-2xs); font-weight: 700; padding: var(--sp-3xs) var(--sp-xs); border-radius: 999px;
  color: var(--good); border: 1px solid rgba(63,209,122,.45); }
.rf-home-chips { display: flex; flex-wrap: wrap; gap: var(--sp-sm); }
.rf-home-chip { display: inline-flex; align-items: center; min-height: 36px; text-decoration: none; color: var(--hi); }
.rf-home-more { display: inline-flex; align-items: center; gap: var(--sp-2xs); min-height: 44px; margin-top: var(--sp-xs); font-weight: 700; }
.rf-home-links { font-size: var(--fs-sm); color: var(--mid); margin: 0; }
/* The Me tab's list (web /me): the Home row, led by an icon; Performance Lab and Sign out are form buttons. */
.rf-me-ico { flex: none; width: 24px; text-align: center; font-size: var(--fs-lg); color: var(--mid); }
.rf-me-btn { width: 100%; text-align: left; font: inherit; cursor: pointer; }
.rf-me-btn:hover { background: var(--s2); }

/* LIST YOUR EVENT (Mission 017): RaceSense.Components/EventSubmitForm on both heads. */
.rf-sub-form { display: flex; flex-direction: column; gap: var(--sp-lg); max-width: 560px; }
.rf-sub-field { display: flex; flex-direction: column; gap: var(--sp-2xs); flex: 1; min-width: 0; }
.rf-sub-field label { font-weight: 600; font-size: var(--fs-sm); }
.rf-sub-opt { font-weight: 400; color: var(--mid); }
/* Mission 020: a long finishing order in pages (RaceResultsSlice), prev/next as plain links on the static web page. */
.rf-pager { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-sm); margin: var(--sp-sm) 0;
  font-size: var(--fs-sm); color: var(--mid); }
.rf-pager a { display: inline-flex; align-items: center; min-height: 44px; padding: 0 var(--sp-sm); }
.rf-pager button { min-height: 44px; }
/* Mission 020: "Also covers" sports on the organiser form. */
.rf-sub-also { border: 0; padding: 0; margin: 0; }
.rf-sub-also legend { font-weight: 600; font-size: var(--fs-sm); margin-bottom: var(--sp-2xs); }
.rf-sub-also-row { display: flex; flex-wrap: wrap; gap: var(--sp-xs) var(--sp-md); }
.rf-sub-also .rf-sub-check { display: inline-flex; align-items: center; gap: var(--sp-2xs); min-height: 44px; font-weight: 400; cursor: pointer; }
.rf-sub-row { display: flex; flex-wrap: wrap; gap: var(--sp-lg); }
.rf-sub-form .btn, .rf-sub-preview .btn, .rf-sub-done .btn { align-self: flex-start; min-height: 44px; }
.rf-sub-problems { padding: var(--sp-md) var(--sp-lg); border-radius: var(--r-lg); background: rgba(227,6,19,.12); border: 1px solid rgba(227,6,19,.4); }
.rf-sub-problems p, .rf-sub-problems ul { margin: 0; }
.rf-sub-preview { margin-top: var(--sp-2xl); display: flex; flex-direction: column; gap: var(--sp-lg); max-width: 720px; }
.rf-sub-preview-frame { padding: var(--sp-lg); border: 1px dashed var(--line-2); border-radius: var(--r-lg); }
.rf-sub-done { padding: var(--sp-lg) var(--sp-xl); max-width: 560px; }
.rf-sub-mine { margin-top: var(--sp-2xl); max-width: 720px; }
/* WHAT DO YOU RACE? (Mission 017): RaceSense.Components/MySportsPicker. Each option is a 44px label around its checkbox. */
.rf-sports { margin: var(--sp-xl) 0; max-width: 720px; }
.rf-sports-hint { font-size: var(--fs-sm); color: var(--mid); margin: 0 0 var(--sp-sm); }
.rf-sports-set { display: flex; flex-wrap: wrap; gap: var(--sp-sm); border: 0; padding: 0; margin: 0 0 var(--sp-md); }
.rf-sports-opt { display: inline-flex; align-items: center; gap: var(--sp-xs); min-height: 44px; padding: 0 var(--sp-lg);
  border: 1px solid var(--line-2); border-radius: 999px; cursor: pointer; color: var(--hi); }
.rf-sports-opt.on { border-color: var(--effort); background: rgba(227,6,19,.12); }
.rf-sports-opt input { accent-color: var(--effort); }
.rf-sports-opt:focus-within { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
.rf-sports-save { min-height: 44px; }
.rf-sports-msg { margin-left: var(--sp-md); font-size: var(--fs-sm); color: var(--mid); }
/* My season's round-by-round chart (Lumen.Charts, RaceSense.Components/SeasonPositionChart). */
.rf-season-chart { margin: var(--sp-lg) 0; border-radius: var(--r-lg); overflow: hidden; }
/* RACE UPDATES and MANAGE (Mission 018). */
.rf-ev-manage { margin: 0 0 var(--sp-lg); font-weight: 600; }
.rf-ev-manage a { display: inline-flex; align-items: center; gap: var(--sp-2xs); min-height: 44px; }
.rf-ev-updates { margin: var(--sp-xl) 0; }
.rf-ev-update { display: flex; flex-direction: column; gap: var(--sp-3xs); padding: var(--sp-md) var(--sp-lg); background: var(--s1);
  border: 1px solid var(--line); border-left: 3px solid var(--effort); border-radius: var(--r-lg); }
.rf-ev-update-head { font-size: var(--fs-xs); font-weight: 700; color: var(--mid); }
.rf-ev-update-body { overflow-wrap: anywhere; white-space: pre-line; }
.rf-mgr-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: var(--sp-sm); margin: var(--sp-lg) 0; }
.rf-mgr-tile { padding: var(--sp-md) var(--sp-lg); background: var(--s1); border: 1px solid var(--line); border-radius: var(--r-lg); }
.rf-mgr-num { display: block; font-family: var(--font-display); font-size: var(--fs-2xl); font-weight: 700; font-variant-numeric: tabular-nums; }
.rf-mgr-lbl { font-size: var(--fs-xs); color: var(--mid); }
/* MY SERIES (Mission 019): the pin on a standings page and the "new results" badge. */
.rf-follow-btn { display: inline-flex; align-items: center; gap: var(--sp-2xs); min-height: 44px; margin-top: var(--sp-xs); padding: 0 var(--sp-lg);
  border: 1px solid var(--line-2); border-radius: 999px; background: transparent; color: var(--hi); font: inherit; cursor: pointer; }
.rf-follow-btn.on { border-color: var(--warn); color: var(--warn); }
.rf-follow-btn:focus-visible { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
.rf-new { flex: none; margin-left: var(--sp-xs); font-size: var(--fs-2xs); font-weight: 700; padding: var(--sp-3xs) var(--sp-xs); border-radius: 999px;
  color: var(--good); border: 1px solid rgba(63,209,122,.45); }
