/* ===========================================================================
   RED CENTRENATS - DESIGN TOKENS
   Concept: "Redline Country"  (see design/DESIGN-DIRECTION.md)

   This file is the single source of truth for every colour, size, duration and
   easing curve on the site. Nothing downstream may hard-code a hex value, a
   pixel size, a duration or a cubic-bezier - if a value is needed and isn't
   here, add it here first.

   The palette is derived from the kept RCN logo, whose red was sampled from the
   actual artwork at RGB(231,62,41) = #E73E29. The previous site's #CC1F24 was
   invented independently of the logo and does not match it.
   =========================================================================== */

:root {
  /* ---------- SURFACES (dark-first) ---------- */
  --surface-0: #0D0B0A;      /* base page background - warm near-black, never pure #000 */
  --surface-1: #171412;      /* raised panels, cards, header */
  --surface-2: #241F1B;      /* modals, stuck-header state, popovers */
  --surface-3: #342C26;      /* highest elevation / active hover panels */
  /* THE card surface. Every card that sits directly on the page field (card
     grids, day cards, ticket tiers/notes, highlights slides, precinct tiles,
     the newsletter panel) uses this, not surface-1 - Rick 29 Jul: surface-1
     cards sank into the page; the newsletter panel's surface-2 "stands out
     better on the main site bg colour". One alias so the whole card family
     moves together if this is ever retuned. */
  --surface-card: var(--surface-2);
  --surface-paper: #F2EAE0;  /* rare light surfaces: the printed-form contact popup (tickets left paper 29 Jul) */

  /* ---------- INK ---------- */
  --ink-1: #F7F3EF;          /* primary text on dark - warm off-white, never pure #fff */
  --ink-2: #C9BFB6;          /* secondary text on dark */
  --ink-3: #8A8078;          /* tertiary / captions on dark. 5.09:1 - AA normal text */
  --ink-on-paper: #1A1512;   /* primary text on --surface-paper */

  /* ---------- BRAND RED ----------
     TWO reds at the top end, and the distinction is binding:
       red-500 = the BRAND red. Large display type, links on dark, icons,
                 borders. As text it only clears AA at >=24px regular or
                 >=18.66px bold.
       red-600 = the FUNCTIONAL red. Solid button fills, and any body-size text
                 on a light surface. Use this whenever red-500 would fail.        */
  --color-red-300: #FF6B4D;
  --color-red-500: #E73E29;  /* the sampled logo red */
  --color-red-600: #C22E1C;
  --color-red-700: #8F2013;
  --gradient-red: linear-gradient(180deg, #E73E29 0%, #8F2013 100%);

  /* ---------- OCHRE / RUST (desert secondary) ---------- */
  --color-ochre-300: #E8B074;
  --color-ochre-500: #C97A2E;
  --color-ochre-700: #7A4A1C;

  /* ---------- DUSK (photographic duotone mapping ONLY) ----------
     Never UI chrome, never text. Only as a tonal range across a photo. */
  --duotone-shadow: #1B2A3D;
  --duotone-highlight: #4C6B85;

  /* ---------- SEMANTIC ---------- */
  --color-hot-500: #F2A93C;      /* urgency/scarcity only */
  --color-success-500: #4C9A5A;  /* always paired with icon + text, never colour alone */
  --color-error-500: #E73E29;    /* always paired with icon + text */

  /* ---------- BORDERS ---------- */
  --border-subtle: rgba(247, 243, 239, 0.12);
  --border-strong: rgba(247, 243, 239, 0.28);
  --border-paper: rgba(26, 21, 18, 0.16);

  /* ---------- SCRIMS (text-over-photo legibility) ----------
     Text never sits directly on unmodified photography. Use a scrim layer
     behind the text - not filter:brightness() on the image itself. */
  --scrim-soft: rgba(13, 11, 10, 0.45);
  --scrim-strong: rgba(13, 11, 10, 0.82);
  --scrim-gradient-up: linear-gradient(0deg, rgba(13,11,10,0.96) 0%, rgba(13,11,10,0.55) 45%, rgba(13,11,10,0) 100%);

  /* ---------- COLOUR BRUISES (page-field art, 29 Jul) ----------
     Vast, very-low-alpha radial washes of the two brand hues breathed into
     the page background - the near-black field picks up faint desert-heat
     colour that shifts as the page scrolls past. Kept far below any level
     that could touch text contrast (the strongest is 6% alpha on the page
     field). The footer pair is stronger because it sits INSIDE an opaque
     footer surface as a designed glow, not under running text.
     The Arrernte acknowledgement section is immune by construction: it
     forces an opaque surface-0 background (site.css SS10). */
  /* Skyfield (three.js dusk backdrop, 31 Jul, Rick's pick: "Ranges parallax
     at dusk"). The WebGL scene reads these at runtime - they are design
     values, so they live here, not in the shader. Scroll position drives the
     blend from the dusk pair to the night pair (page top = golden hour,
     page bottom = starlit). All four sit well below the luminance ceiling the
     bruise washes established, so text contrast over the scene never drops
     under 6.9:1 against --ink-1 even at the brightest horizon point. */
  --sky-dusk-top: #201016;        /* upper sky at golden hour - deep plum-brown */
  --sky-dusk-horizon: #8A3F1C;    /* horizon glow at golden hour - embered amber */
  --sky-night-top: #07070D;       /* upper sky at night - blue-black */
  --sky-night-horizon: #241219;   /* horizon at night - a last whisper of ember (lifted 31 Jul: the first pass read as pure black in situ) */
  --sky-sun: #F2A65A;             /* the sinking sun's glow, ochre-300 warmed */
  --sky-star: #D8DCE8;            /* star points - cool white, never pure #fff */

  --bruise-red: rgba(231, 62, 41, 0.06);        /* page-field wash, brand red */
  --bruise-ochre: rgba(201, 122, 46, 0.055);    /* page-field wash, ochre */
  --bruise-red-ember: rgba(143, 32, 19, 0.35);  /* footer: ember glow rising from below the fold */
  --bruise-ochre-dawn: rgba(201, 122, 46, 0.10);/* footer: faint dawn bruise at the top corner */
  /* Footer seam: a heated-metal hairline replacing the flat border - brand red
     cooling to ochre, dying out before either edge. */
  --seam-heat: linear-gradient(90deg, transparent, rgba(231, 62, 41, 0.5) 22%, rgba(201, 122, 46, 0.5) 78%, transparent);

  /* ---------- FOCUS ----------
     Double ring + soft red glow. Visible on dark and on paper. Replaces the old
     site's off-brand #0054C5 blue. Blue is not a colour in this system. */
  --focus-ring: 0 0 0 2px var(--surface-0), 0 0 0 4px var(--ink-1), 0 0 10px 2px rgba(231, 62, 41, 0.55);
  --focus-ring-paper: 0 0 0 2px var(--surface-paper), 0 0 0 4px var(--ink-on-paper), 0 0 10px 2px rgba(231, 62, 41, 0.45);

  /* ---------- TYPE FAMILIES ----------
     Google retired the "Big Shoulders Display" and "Big Shoulders Stencil
     Display" families and merged each into a single variable family with an
     optical-size axis. The family names below are the ones actually baked into
     the woff2 files in public/fonts - do not "restore" the old Display names,
     they resolve to nothing and fall back to Arial Narrow.

     Big Shoulders axes: wght 100-900, opsz 10-72 (file default 72).
     Archivo axes:       wght 100-900, plus a true variable italic. */
  --font-display: 'Big Shoulders', 'Arial Narrow', sans-serif;
  --font-stencil: 'Big Shoulders Stencil', var(--font-display);
  --font-body: 'Archivo', system-ui, -apple-system, 'Segoe UI', sans-serif;

  /* Optical size, set per type role rather than left at the file default.
     opsz 72 is the poster/display cut - correct for the big headline roles and
     wrong for a 18px sub-heading, where the display cut's tight apertures hurt
     legibility. Apply via font-variation-settings on the relevant role. */
  --opsz-display: 72;
  --opsz-heading: 36;
  --opsz-label: 18;

  /* ---------- TYPE SCALE (fluid) ----------
     HEADINGS REDUCED 30% on 2026-07-27 at the client's request. Every clamp
     component (min, the rem and vw halves of the preferred term, and max) was
     multiplied by 0.7, so the fluid curve keeps its exact shape and only its
     amplitude changes - scaling just the max would have flattened the
     small-screen end and changed the ratio between breakpoints.

     Original values are kept in the trailing comments so the change is
     reversible without going to git. Body sizes are untouched: the request was
     headings, and shrinking body copy too would have preserved the contrast
     ratio rather than reducing it, which is the opposite of the intent. */
  --text-display-1: clamp(1.925rem, 1.4rem + 3.5vw, 4.9rem);      /*  31 ->  78  (was 44 -> 112) */
  --text-display-2: clamp(1.4rem, 1.05rem + 2.1vw, 3.15rem);      /*  22 ->  50  (was 32 ->  72) */
  /* h3/h4 increased again 28 Jul (client: "increase h3 and h4 size") - up
     ~27% from the 30%-reduced values above, same clamp-scaling method so the
     curve shape holds. This also fixes a real defect the reduction had left:
     --text-h4's floor was 12.6px, UNDER --text-body's fixed 16px, so a mobile
     h4 rendered smaller than its own body copy. The new floor (1rem/16px)
     can't repeat that at any viewport width. */
  --text-display-3: clamp(1.35rem, 1.1rem + 1.35vw, 2.2rem);      /*  21.6 -> 35.2  (30%-reduced: 17 -> 28; original: 24 -> 40) */
  /* h4 nudged up again 29 Jul ("make the h4 size a little bigger sitewide") -
     back to the pre-reduction curve, 18 -> 24. */
  --text-h4: clamp(1.125rem, 1rem + 0.5vw, 1.5rem);               /*  18   -> 24    (previous nudge: 16 -> 21.6; 30%-reduced: 13 -> 17) */
  /* One step under h4 - added for search-result titles (§11.8), which need a
     display-face size smaller than any section heading so a results list
     doesn't shout over the page structure. */
  --text-h5: clamp(0.7rem, 0.63rem + 0.245vw, 0.875rem);          /*  11 ->  14  (was 16 ->  20) */
  --text-body-lg: clamp(1.125rem, 1rem + 0.3vw, 1.25rem);    /*  18 ->  20 */
  --text-body: 1rem;
  --text-small: 0.875rem;
  --text-micro: 0.75rem;
  /* Scaled by the same 0.7. Not an <h> element, but it is display-scale type
     that sits in the same hierarchy: left at its old size it would have become
     LARGER than h1 (96px vs the new 78px max) and the prize figure would have
     out-shouted every heading on the page. Say the word and it goes back. */
  --text-odometer: clamp(1.75rem, 1.4rem + 2.1vw, 4.2rem);        /*  28 ->  67  (was 40 ->  96) */

  /* Line heights - display is poster-tight, body is generous */
  --leading-display: 1.02;
  --leading-h4: 1.15;
  --leading-body-lg: 1.55;
  --leading-body: 1.6;
  --leading-small: 1.45;

  /* Tracking. Condensed display type needs NEGATIVE tracking at large sizes;
     small uppercase UI text needs POSITIVE tracking. Opposite rules, both
     correct - do not "normalise" these to one value. */
  --tracking-display: -0.02em;
  --tracking-h4: 0em;
  --tracking-ui: 0.05em;
  /* One control height for text inputs and selects, so a button placed beside
     a field can square off against it (site.css §5 and §11.24). A touch target
     plus a little breathing room; see the <select> note at site.css §5 for why
     this is an explicit height rather than a min-height. */
  --field-h: calc(var(--target-min) + var(--space-2));
  --tracking-eyebrow: 0.12em;
  --tracking-plate: 0.14em;   /* added by site.css build - licence-plate character spacing (§F device 4), a
                                  distinct value from tracking-ui: plates read like stamped characters, not
                                  small-caps UI text, so it gets its own number rather than reusing tracking-ui. */

  /* "Speed skew" pair (§D) - added by site.css build. These two values are a matched set: the letter-spacing
     exists specifically to counter the optical compression introduced by this exact skew angle, so they are
     tokenised together rather than left as magic numbers inside .rcn-skew. Single display words only - see
     the usage comment on .rcn-skew in site.css. */
  --skew-emphasis: -8deg;
  --tracking-skew-compensate: 0.02em;

  /* ---------- SPACING (4px base) ---------- */
  --space-1: 4px;    --space-2: 8px;    --space-3: 12px;   --space-4: 16px;
  --space-5: 20px;   --space-6: 24px;   --space-8: 32px;   --space-10: 40px;
  --space-12: 48px;  --space-16: 64px;  --space-20: 80px;  --space-24: 96px;
  --space-32: 128px; --space-40: 160px;

  /* Section rhythm - set per-section, stepped up at the 960 breakpoint below */
  --section-y: var(--space-16);
  --section-y-tight: var(--space-12);

  /* ---------- LAYOUT ---------- */
  --content-max: 1200px;
  /* Reading measure for body prose. `none` = fill the container, matching the
     header width, per the client's "Fix Content Width" direction 2026-07-27.
     Set to something like 90ch for a compromise between their preference and
     a comfortable line length - this is the single value that controls it. */
  --prose-max: none;
  --gutter: var(--space-6);
  --header-h: 64px;
  --target-min: 44px; /* added by site.css build - WCAG 2.2 minimum interactive target size (§I). Reused
                          across nav links, buttons, accordion triggers, form controls and footer icons, so
                          it is one token rather than a fixed value repeated at every call site. */
  /* Count bubble on the header cart icon (6 Aug 2026). Big enough for two
     digits at --text-micro, small enough to sit inside the --target-min box
     without pushing the icon off its own optical centre. */
  --badge-size: 18px;

  /* ---------- ASPECT RATIOS ----------
     added during the layout-consistency pass - card grids and ticket banners
     were harvested at wildly inconsistent aspect ratios (square product
     shots, a 2.19 banner, 1.5 photos for cards; the ticket tier's own OZTIX
     banners are 1000x300). Locking every card's image slot to ONE ratio via
     `aspect-ratio` + `object-fit: cover` in site.css is what makes a grid of
     cards read as one matrix instead of a ragged collage - see
     .rcn-card__media and .rcn-ticket__image/.rcn-ticket__cap in site.css. */
  --ratio-card: 4 / 3;            /* discipline/guide card grid thumbnail - matches the value already in use */
  --ratio-ticket-banner: 10 / 3;  /* ticket-tier OZTIX banner - matches the real 1000x300 source files exactly */

  /* ---------- RADII ----------
     Deliberately small. This brand is industrial and worn, not soft. Nothing
     rounder than 4px except the licence-plate device, which has its own value
     matched to real AU plate proportions. */
  --radius-sm: 2px;
  --radius-md: 4px;
  --radius-plate: 10px;

  /* ---------- ELEVATION ---------- */
  --shadow-1: 0 1px 2px rgba(0, 0, 0, 0.4);
  --shadow-2: 0 4px 12px rgba(0, 0, 0, 0.5);
  --shadow-3: 0 12px 32px rgba(0, 0, 0, 0.6);
  /* Native <dialog> pop-ups (§11.5) float over a dimmed backdrop, so they
     need one step more lift than shadow-3's card hover. */
  --shadow-modal: 0 24px 64px rgba(0, 0, 0, 0.7);

  /* ---------- PARTNER / SPONSOR LOGO SIZING (§11.3 / §11.4) ----------
     One height for every carousel logo and a smaller one for the footer's
     event-family row - uniform cap height is what makes a mixed-brand strip
     read as curated instead of jumbled. Widths stay auto (aspect preserved;
     logos are never cropped or stretched - contractual). */
  /* 57px = 44px + 30%. Client card "Sponsor Logos need to be in full colour"
     (Ella, 11 Aug 2026): "Please make all the logos about 30% bigger on the
     website." Applied to the shared base height rather than to fifteen
     individual entries, so every mark grows by the same amount and the
     area-normalising --logo-scale each one already carries is preserved
     intact - the strip gets bigger without its internal balance changing. */
  --partner-logo-h: 57px;
  /* Stacked vertically, matched WIDTH not height (28 Jul client direction) -
     see the note on .rcn-footer__family in site.css SS11.5. Same 132px the
     footer's LMCT+ mark already used, now shared by all three marks. */
  --footer-family-logo-w: 132px;
  /* LMCT+ is the presenting partner, so it gets its own sizes rather than
     inheriting the shared partner height (client direction 2026-07-27: more
     prominent than the old 22px, but NEVER larger than the site's own mark).

     Sized by WIDTH, not height, because the two logos have very different
     proportions: the RCN lockup is a squarish stacked block (aspect ~1.71)
     while LMCT+ is a long thin wordmark (aspect ~5.07). A first pass matched
     them on height at 42px and LMCT+ came out 206px wide against the RCN
     mark's 94px - more than twice the width, and it dominated the header.
     Capping the width keeps it clearly second no matter what artwork lands. */
  /* Client reverted LMCT+ to their supplied black/white mono file (28 Jul) -
     the colour version conflicted with the RCN12 mark's own baked-in
     "LMCT+ PRESENTS" lockup and the client asked for it back, and to shrink
     both sizes a little further while at it. Mono ink aspect is 4.71 (vs the
     colour file's 5.07 - close enough that these didn't need re-deriving from
     scratch, just trimmed down). */
  --lmct-logo-w: 92px;           /* header - stays under the RCN mark's ~110px width */
  --glow-red: 0 0 24px rgba(231, 62, 41, 0.35);

  /* ---------- ODOMETER CLOCK (countdown, 29 Jul) ----------
     The homepage countdown renders as a mechanical speedo odometer: eight
     geared digit barrels (DD HH MM SS) rolling behind a cutout window.
     Everything below sizes and shades that instrument. The barrel/window
     blacks are deliberately DEEPER than surface-0: the cutout must read as
     a hole into the instrument, darker than any page surface. */
  --odo-digit-h: clamp(2.75rem, 2.1rem + 2.6vw, 4.5rem);   /* one digit cell = the window height driver */
  --odo-digit-w: calc(var(--odo-digit-h) * 0.74); /* widened for the 4px gear-teeth strip at each wheel's left edge */
  --odo-digit-size: calc(var(--odo-digit-h) * 0.62);
  --odo-gap: 4px;                                   /* bezel sliver between barrels - reads as separate wheels */
  /* Gear teeth (Rick: "8 separate mechanical geared wheels ... shiny geared
     teeth showing to the left of each wheel just 4px wide - the teeth turn
     as part of the barrel to their right"). The teeth are printed on the
     FACE PLATES themselves (a 4px strip down each plate's left edge), so
     they rotate, foreshorten and shade with the drum - genuinely geared,
     not a painted-on filmstrip. Crest catches the key light, valley drops
     to the wheel shade. */
  --odo-teeth-w: 4px;
  --odo-teeth: linear-gradient(180deg,
      rgba(247, 243, 239, 0.55) 0%, rgba(247, 243, 239, 0.18) 22%,
      rgba(0, 0, 0, 0.9) 45%, rgba(0, 0, 0, 0.95) 78%,
      rgba(247, 243, 239, 0.28) 100%);
  --odo-tooth-pitch: 6;                             /* teeth per digit face - the pattern tiles the face height exactly, so tooth rows stay continuous across face boundaries as the drum turns */
  --odo-group-gap: clamp(0.625rem, 1.5vw, 1.25rem); /* wider break between DD | HH | MM | SS */
  /* Rollover speed (29 Jul: Rick - match ALL white wheels to the tens-of-
     seconds drum's geared speed, not the fast 1s end-drum pace). The whole
     train turns at ONE angular rate: 6deg/s, the tens drum's 360deg/60s. So
     a 10-face wheel's 36deg step takes 6s and a 6-face wheel's 60deg step
     takes 10s - same gear rate, different tooth counts, exactly like a real
     geared cluster. LINEAR, because a driven gear turns at its driver's
     constant speed; the indexed park is the stop, not an ease-out. */
  --odo-roll-f10: 6s;
  --odo-roll-f6: 10s;
  --odo-wheel-face: #060504;                        /* barrel face - below surface-0 on purpose */
  --odo-wheel-shade: #000000;                       /* barrel extremities, where the cylinder turns away */
  --odo-wheel: linear-gradient(180deg, var(--odo-wheel-shade), var(--odo-wheel-face) 30%, var(--odo-wheel-face) 70%, var(--odo-wheel-shade));
  /* 3D drum geometry (29 Jul v2 - the wheels are true CSS cylinders). */
  --odo-persp: calc(var(--odo-digit-h) * 9);        /* per-barrel perspective: strong enough to read curvature, no fisheye */
  --odo-jitter-max: 6px;                            /* mechanical mis-park: each WHITE wheel stops within +/- this of true centre (Rick: "old mechanical odometers are never perfectly aligned"; range tuned to 6px on his direction). The red seconds pair overrides this down to 3px in site.css - they never park, so a big offset would just read as a permanently crooked wheel */
  /* Bezel metals (29 Jul v3 - Rick: "we really cannot tell where the lip of
     the rectangular black bezel meets the cutout"). The plate is now a
     CLEARLY lighter warm gunmetal - several stops above every page surface -
     so the cutout reads as a black hole punched through a light plate, not
     black-on-black. Explicit hexes: these are instrument metals, not page
     surfaces, and must never slide back down with the surface scale. */
  --odo-metal-hi: #6B5B4D;   /* top of the plate, catching the light */
  --odo-metal: #423931;      /* the plate's base tone */
  --odo-metal-lo: #241E19;   /* the plate turning away at the bottom */
  /* The key light sits TOP-LEFT (Rick's direction) - every edge treatment
     below is derived from that one light. Plate sheen falls away toward
     bottom-right; the corner catch-light enters from the top-left corner. */
  --odo-bezel:
      var(--grain-tex),
      linear-gradient(135deg, rgba(255, 250, 242, 0.30), rgba(255, 250, 242, 0.06) 24%, transparent 40%),
      repeating-linear-gradient(180deg, rgba(247, 243, 239, 0.03) 0 1px, transparent 1px 3px),
      linear-gradient(150deg, var(--odo-metal-hi), var(--odo-metal) 48%, var(--odo-metal-lo));
  /* Outer arrises of the plate: top and left edges catch the key light,
     bottom and right turn away dark - plus the plate's own cast shadow
     falling bottom-right, which is what gives the slab thickness. */
  --odo-bezel-edge:
      inset 0 1px 0 rgba(255, 250, 242, 0.5),
      inset 1px 0 0 rgba(255, 250, 242, 0.26),
      inset 0 -2px 0 rgba(0, 0, 0, 0.9),
      inset -2px 0 0 rgba(0, 0, 0, 0.65),
      4px 6px 10px rgba(0, 0, 0, 0.55),
      12px 20px 36px rgba(0, 0, 0, 0.65);
  /* The cutout: a machined chamfer with visible WALL THICKNESS, lit from
     top-left. The recess's top and left walls are occluded (hard dark cuts,
     4px - that width IS the plate's thickness), while the INSIDE BOTTOM and
     INSIDE RIGHT walls face the top-left key light and catch it hard - a
     bright 3px chamfer with a softer 1px falloff behind it (Rick: those two
     inner edges are what give the bezel face its 3D thickness). The outer
     pair is the plate around the opening: lit just below, shadowed just
     above. */
  --odo-cutout-shadow:
      inset 0 4px 0 rgba(0, 0, 0, 1),
      inset 4px 0 0 rgba(0, 0, 0, 0.85),
      inset 0 -3px 0 rgba(255, 250, 242, 0.95),
      inset 0 -5px 2px rgba(255, 250, 242, 0.35),
      inset -3px 0 0 rgba(255, 250, 242, 0.8),
      inset -5px 0 2px rgba(255, 250, 242, 0.28),
      inset 0 -9px 10px rgba(255, 250, 242, 0.10),
      inset 0 20px 26px rgba(0, 0, 0, 0.98),
      inset 0 -14px 18px rgba(0, 0, 0, 0.9),
      inset 0 0 0 1px rgba(0, 0, 0, 1),
      0 1px 0 rgba(255, 250, 242, 0.28),
      0 -2px 1px rgba(0, 0, 0, 0.7);
  /* Cylinder-curvature light, deliberately theatrical (Rick: "massively
     accentuate the lighting that shows off the 3d roundness"): near-total
     occlusion where the drums curl under the lip, a hot specular band above
     centre, and a faint floor-bounce below - the three-light setup that
     makes a cylinder read instantly. */
  --odo-curve: linear-gradient(180deg,
      rgba(0, 0, 0, 0.92), rgba(0, 0, 0, 0.36) 9%,
      transparent 22%,
      rgba(255, 250, 242, 0.30) 35%, rgba(255, 250, 242, 0.08) 44%,
      transparent 52%,
      transparent 62%, rgba(255, 250, 242, 0.09) 72%,
      rgba(0, 0, 0, 0.4) 90%,
      rgba(0, 0, 0, 0.92));
  /* Per-wheel side falloff: each drum's face darkens hard toward its edges,
     so every wheel reads as its own cylinder, not a shared flat strip. */
  --odo-wheel-side: linear-gradient(90deg,
      rgba(0, 0, 0, 0.88), transparent 30%,
      transparent 70%, rgba(0, 0, 0, 0.88));
  --odo-ink-seconds: var(--color-red-300);          /* the live wheel - real odometers paint the tenths drum differently */

  /* ---------- AGE & WEAR (29 Jul - Rick: "so it all looks like the dash of
     an older car") ----------
     Two SVG fractal-noise textures, inlined so they cost no request.
     --wear-mask is an ALPHA MASK: mostly solid (floor ~0.62) with organic
     thin patches - laid over printed digits, labels and the separator posts
     so ink looks rubbed by decades of thumbs, not stencilled. Every drum
     face offsets its mask sample by its face index, so no two digits wear
     the same. --grain-tex is a fine dark speckle laid INTO the bezel
     surface - injection-moulded plastic grain, not brushed metal sheen. */
  --wear-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='260' height='260'%3E%3Cfilter id='w'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.11' numOctaves='3' seed='11'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1.5 -0.45'/%3E%3C/filter%3E%3Crect width='260' height='260' fill='%23fff' opacity='0.62'/%3E%3Crect width='260' height='260' filter='url(%23w)'/%3E%3C/svg%3E");
  /* Coarser + higher-contrast than v1 (Rick: "I can't see it"): lower
     frequency = bigger grain clumps, and TWO stacked rects - a dark speckle
     AND a light speckle from a different noise seed - so the grain reads as
     raised texture catching the key light, not a faint dark wash. */
  --grain-tex: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='gd'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.35' numOctaves='3' seed='4'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.45 -0.14'/%3E%3C/filter%3E%3Cfilter id='gl'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.4' numOctaves='3' seed='9'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 1 0 0 0 0 0.97 0 0 0 0 0.93 0 0 0 0.28 -0.11'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23gd)'/%3E%3Crect width='180' height='180' filter='url(%23gl)'/%3E%3C/svg%3E");

  /* ---------- MOTION: EASING ---------- */
  --ease-throttle: cubic-bezier(0.65, 0, 0.35, 1);          /* snappy - default UI */
  --ease-drift: cubic-bezier(0.22, 1, 0.36, 1);             /* decelerate - reveals, parallax */
  --ease-launch: cubic-bezier(0.11, 0, 0.5, 0);             /* accelerate - hero entrance */
  --ease-brake: cubic-bezier(0.65, -0.55, 0.36, 1.55);      /* overshoot - card press ONLY */

  /* ---------- MOTION: DURATION ---------- */
  --duration-instant: 100ms;
  --duration-fast: 200ms;
  --duration-base: 350ms;
  --duration-slow: 600ms;
  --duration-cinematic: 1200ms;
  --duration-ambient: 8000ms;
  --duration-marquee: 32s;  /* added by site.css build - stat-ticker/marquee scroll loop. The durations above
                                top out at 8s (ambient drift); a horizontal marquee reading several stats
                                needs a slower, single-purpose loop, so it gets its own token rather than
                                stretching --duration-ambient to a job it wasn't specified for. */
  --stagger-step: 60ms;     /* added by site.css build - reveal-on-enter child stagger unit (§G: "stagger
                                children 60ms apart"). Reused by the mobile nav takeover panel's staggered
                                list-item reveal too. */

  /* Hard cap on scroll-linked movement. This is a vestibular-safety rule that
     applies even when prefers-reduced-motion is NOT set - large-amplitude
     parallax is a trigger independent of the OS setting. */
  --parallax-max: 40px;

  /* ---------- MOTION: REVEAL-ON-ENTER GEOMETRY ----------
     Reveals are SCROLL-LINKED, not timed: an element starts at 0 opacity the
     moment its top edge crosses the bottom of the viewport, and arrives at its
     resting place after --reveal-travel of further scrolling. Everything below
     is geometry, so it lives here rather than being restated in site.css and
     motion.js - both read these, motion.js via getComputedStyle.

     --reveal-trigger-line is where on screen an element must have LANDED,
     measured as a fraction down the viewport (Rick, 31 Jul: nothing may finish
     "in the bottom 10%" - it has to be settled by 45% in). --reveal-travel is
     that same rule expressed as scroll distance, which is what the animation
     range actually needs. Unitless number on purpose: motion.js multiplies by
     the viewport height itself for the no-scroll-timeline fallback. */
  --reveal-trigger-line: 0.45;
  --reveal-travel: calc((1 - var(--reveal-trigger-line)) * 100vh);
  /* Per-child stagger, expressed as extra scroll DISTANCE rather than a time
     delay. A scroll-linked animation has no clock for an `animation-delay` to
     act on, so the stagger has to be geometric: each successive child takes a
     little longer of the scroll to land, which fans a grid in rather than
     sliding it as one flat plane. Capped at 6 children by motion.js, so the
     deepest child travels --reveal-travel + 5x this. */
  --reveal-stagger-dist: 4vh;
  /* How far an element lifts on its way in. Children lift less than their
     container so a card grid doesn't read as two separate slabs moving. */
  --reveal-lift: var(--space-6);
  --reveal-lift-child: var(--space-4);

  /* ---------- SCROLL CHOREOGRAPHY (scroll-fx.css) ----------
     added by the homepage scroll-fx pass. All three exist because scroll-fx.css
     may not hard-code values and none of the existing tokens fit:

     fx-skew-entry: transient skew applied to a WHOLE heading line during its
     scrubbed entrance, landing at 0deg. Deliberately gentler than
     --skew-emphasis (-8deg), which is a RESTING typographic state for single
     words - a full line held at -8deg mid-scrub reads as broken, not fast. */
  --fx-skew-entry: -4deg;
  /* Hairline accent rule drawn under eyebrows by scroll-fx.css. 2px, not 1px:
     it is a drawn brand accent, one step heavier than the 1px data hairlines
     used in .rcn-feature__stats, and it must survive subpixel rounding while
     scaling. */
  --fx-rule-w: 2px;
  /* Section-transition glow tint - --color-red-500 at very low alpha. A wash
     this faint needs its own alpha baked in; deriving it downstream would mean
     hard-coding the rgb triplet outside this file. */
  --fx-glow-red-soft: rgba(231, 62, 41, 0.07);

  /* ---------- Z-INDEX ---------- */
  --z-base: 0;
  --z-raised: 10;
  --z-sticky: 100;
  /* The mobile nav takeover sits BELOW the header on purpose: the header bar
     stays visible over it so the hamburger can become the close (X) control.
     It was on --z-overlay, which covered the hamburger and left the panel with
     no tappable way out - keyboard Escape only. */
  --z-nav-panel: 150;
  --z-header: 200;
  --z-overlay: 300;
  --z-modal: 400;
}

/* Breakpoints are 400 / 640 / 960 / 1280 / 1600, min-width, mobile-first.
   Custom properties cannot be used in media query conditions, so those numbers
   are written literally throughout the codebase. Keep them consistent. */

@media (min-width: 960px) {
  :root {
    --section-y: var(--space-24);
    --section-y-tight: var(--space-16);
    --gutter: var(--space-8);
    --header-h: 80px;
  }
}

@media (min-width: 1280px) {
  :root {
    --section-y: var(--space-32);
    --gutter: var(--space-10);
  }
}

/* ---------------------------------------------------------------------------
   REDUCED MOTION
   Kills: parallax, marquee, ambient smoke, pinned sequences, transition sweeps,
   hover overshoot. Keeps: opacity reveals, final odometer value, focus rings,
   colour states - none of those are motion, and removing them would remove
   information rather than movement.
   --------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    /* DELAYS must be zeroed too, not just durations.

       Zeroing only the duration leaves any `animation-delay` intact, and an
       animation with `both` fill sits at its animated-FROM state for the whole
       delay. The hero CTA row has a 1.4s delay (headline duration + 200ms), so
       under reduced motion the site's two primary conversion buttons - "Enter a
       vehicle" and "Buy spectator tickets" - were invisible for 1.4 seconds for
       precisely the users who asked for less motion.

       Reduced motion removes MOVEMENT, never INFORMATION, and never adds a
       wait. */
    animation-delay: 0s !important;
    transition-delay: 0s !important;
    scroll-behavior: auto !important;
  }
  :root {
    --parallax-max: 0px;
  }

  /* Reveal states are FULLY NEUTRALISED here, not merely sped up.

     Zeroing the transition duration is not sufficient. The animated-from state
     is `opacity: 0`, and it is only undone when an IntersectionObserver adds
     `.is-visible` on scroll. So a reduced-motion user who lands on a page and
     does not scroll - or whose observer hasn't fired for a section yet - sees
     genuinely blank regions. Reduced motion must remove MOVEMENT, never
     INFORMATION, so under this query the content is simply always visible and
     the observer becomes irrelevant.

     This was a real defect: the ticket cards on /tickets rendered at opacity 0
     with correct 787px heights, i.e. present, laid out, and invisible. */
  [data-reveal],
  [data-reveal] [data-reveal-child],
  [data-step] {
    opacity: 1 !important;
    transform: none !important;
  }
}
