/*
  Reason to Run — stylesheet
  All color/type/spacing values are CSS custom properties (design tokens) in :root.
  When the real brand kit (logo, colors, fonts) arrives, update the tokens below —
  nothing else in this file should need to change.
*/

/* Cross-document view transitions — a same-origin navigation (index.html to
   a stories/*.html page and back) crossfades instead of the default hard
   cut, in whichever browsers already support this (Chromium; this is a
   plain no-op everywhere else, including Safari/Firefox today — no
   fallback code needed, an unsupported browser just keeps its ordinary
   navigation exactly as before). Sitewide via this one rule rather than
   JS intercepting every link/back-button/bookmark/typed-URL case
   individually, and free of the "flash of unstyled content while a
   fade-transition script loads" risk that comes with a JS-driven version. */
@view-transition {
  navigation: auto;
}

/* ============================= DESIGN TOKENS ============================= */
:root {
  /* Color — cancer-support palette: gray (brain cancer awareness), lavender
     purple (the unity/"all cancers" awareness color), and a light blue
     sampled from the event's own t-shirts. Swap these hexes when an official
     brand kit arrives. */
  --color-primary: #7A7F87;       /* neutral gray — structural/base */
  --color-primary-dark: #464A50;
  --color-primary-light: #C7CBCF;
  --color-primary-tint: #F1F1F2;

  /* Periwinkle blue, matched to the logo's background swatch (#A9C4E7,
     sampled directly off assets/Reason to Run Blue Background.png) — the
     whole family shares that swatch's hue/saturation, with lightness
     stepped the same way the previous sky-blue ramp was, so button/link
     contrast against white and against --color-ink stays where it was
     (buttons even ended up a bit MORE readable). --color-accent-light is
     the swatch's exact hex, unmodified — it's the tier whose lightness
     already matched the logo's, so it didn't need generating like the
     other three did. */
  --color-accent: #5E90D2;        /* periwinkle blue — CTAs, links, brand accent */
  --color-accent-dark: #254F86;
  --color-cta: #4374BA;           /* primary-button blue: a few shades deeper than --color-accent so white button text reaches 4.7:1 (accent gave 3.3:1) */
  --color-accent-light: #A9C4E7;
  --color-accent-tint: #DAE5F5;

  --color-blue: #8874C2;          /* lavender purple — used sparingly as a secondary accent */
  --color-blue-dark: #61519A;
  --color-blue-light: #CBBEE8;
  --color-blue-tint: #F2EEFA;

  --color-yellow: #F5C336;        /* golden yellow — used sparingly as a secondary accent */
  --color-yellow-dark: #BF982A;
  --color-yellow-light: #F8D572;
  --color-yellow-tint: #FEF8E7;

  /* A slightly brighter lavender reserved for progress indicators (the mile
     trail, its marker, the mobile progress bar) so "how far along you are"
     reads clearly against any section background — buttons/branding keep
     the softer --color-accent above. */
  --color-progress: #9B7FD4;
  --color-progress-dark: #7057AD;

  --color-white: #FFFFFF;
  --color-bg: #FFFFFF;
  --color-bg-alt: #F5F5F6;        /* neutral off-white for alternating sections */
  --color-ink: #27282B;           /* body/heading text — neutral gray, not blue-tinted */
  --color-ink-soft: #5B5D63;      /* secondary text */
  --color-ink-faint: #6B6E74;     /* tertiary / meta text — was #8C8F94 (3.2:1 on white); now ≥4.5:1 on white and --color-bg-alt so small captions pass WCAG AA */
  --color-border: #E2E3E5;
  --color-destructive: #B3261E;

  /* Typography */
  --font-display: 'Lora', Georgia, 'Times New Roman', serif;
  --font-body: 'Geist', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;

  --type-xs: 0.8125rem;
  --type-sm: 0.9375rem;
  --type-base: 1rem;
  --type-md: 1.125rem;
  --type-lg: 1.375rem;
  --type-xl: 1.75rem;
  --type-2xl: 2.5rem;
  --type-3xl: clamp(2.75rem, 6vw, 4.5rem);
  --type-4xl: clamp(3.5rem, 8vw, 6rem);
  --type-5xl: clamp(4.5rem, 11vw, 8.5rem); /* true poster-scale moments — mile-number watermarks, hero-scale stat figures */

  /* Spacing scale (density: standard) */
  --space-1: 0.5rem;
  --space-2: 1rem;
  --space-3: 1.5rem;
  --space-4: 2rem;
  --space-5: 3rem;
  --space-6: 4rem;
  --space-7: 6rem;
  --space-8: 8rem;

  /* Layout */
  --container-max: 1180px;
  --radius-sm: 8px;
  --radius-md: 14px;
  --radius-lg: 24px;
  --header-height: 76px;
  /* Height of .mile-tracker-mobile — the "You are at..." pill that's sticky
     just below the header on screens under 1024px (see the mile-tracker-mobile
     rules and its 1024px breakpoint further down). Sections need to reserve
     header-height + this on those screens or their top content starts underneath
     both bars. */
  --mile-tracker-height: 44px;

  /* A tiny inline SVG noise tile, reused as a low-opacity overlay on dark
     sections — adds a bit of tactile depth instead of a flat, printer-perfect
     gradient. Self-contained (no external asset/network request). */
  --noise-texture: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");

  /* Motion */
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --duration-fast: 200ms;
  --duration-base: 350ms;
  --duration-slow: 600ms;

  /* z-index scale */
  --z-header: 100;
  --z-mile-sidebar: 90;
  --z-toast: 1000;
  --z-lightbox: 1100;
}

/* ============================= RESET & BASE ============================= */
*, *::before, *::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--type-base);
  line-height: 1.65;
  color: var(--color-ink);
  background: var(--color-bg);
  overflow-x: hidden;
}

img, video { max-width: 100%; display: block; }

a { color: inherit; }

/* Now that the page is one continuous scroll (rather than discrete, fixed-height
   "pages"), the very bottom of the document is reachable in the ordinary way — the
   viewport at max scroll shows exactly the document's last (viewport-height) slice.
   Without enough real content after it, the last heading before the footer (Mile 4's
   "More ways to be part of it") would end up partially or fully above that slice on
   taller viewports, with its top permanently scrolled out of view at the actual
   bottom of the page. A small amount of trailing space here (most of the real
   clearance comes from the footer's own more generous padding — see .site-footer)
   covers that without reading as a big dead gap of its own. */
/* Was 3rem of trailing space for exactly the reason above. The closing band
   (.closing-call, last child of #main) now supplies far more clearance than
   that on its own — and it's full-bleed dark meeting the equally dark footer,
   so any padding here paints a strip of white page background between the two
   and turns a seamless hand-off into a visible seam. Zero, not 3rem. */
#main { padding-bottom: 0; }

/* Small, easy-to-miss details that read as "considered" rather than default —
   text selection and the scrollbar both pick up the brand palette instead of
   the browser's stock blue/gray. */
::selection { background: var(--color-accent); color: var(--color-white); }

html { scrollbar-color: var(--color-primary-light) transparent; scrollbar-width: thin; }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background-color: var(--color-primary-light);
  border-radius: 999px;
  border: 2px solid var(--color-bg);
}
::-webkit-scrollbar-thumb:hover { background-color: var(--color-primary); }

h1, h2, h3, h4 {
  font-family: var(--font-display);
  line-height: 1.15;
  margin: 0 0 var(--space-2);
  color: var(--color-ink);
  font-weight: 600;
}

/* Big, deliberate size jump from headline to body copy — the section
   headlines are the "poster" moment of each page; everything else (body
   text, labels, cards) stays comparatively small so the contrast reads as
   intentional rather than incidental. */
.mile-grid h2,
.section-heading h2,
.finish-banner h2,
.aid-station-content h2 {
  font-size: var(--type-3xl);
  max-width: 18ch;
}

p { margin: 0 0 var(--space-2); }

button { font-family: inherit; cursor: pointer; }

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

.skip-link {
  position: absolute;
  left: var(--space-2);
  top: -60px;
  background: var(--color-ink);
  color: var(--color-white);
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-sm);
  z-index: var(--z-toast);
  transition: top var(--duration-fast) var(--ease-out);
}
.skip-link:focus { top: var(--space-2); }

:focus-visible {
  outline: 3px solid var(--color-accent);
  outline-offset: 3px;
}

.container {
  max-width: var(--container-max);
  margin-inline: auto;
  padding-inline: var(--space-3);
}

@media (min-width: 768px) {
  .container { padding-inline: var(--space-5); }
}

section { position: relative; }
.bg-alt { background: var(--color-bg-alt); }

.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font-family: var(--font-body);
  font-size: var(--type-xs);
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-accent-dark);
  margin-bottom: var(--space-2);
  /* Frosted pill around every main-page section label, echoing the
     "AID STATION" chip (.hero-eyebrow) on the dark Hero / Aid Station
     sections — retuned here for the light mile sections as a translucent
     wash of the accent colour behind a hairline border. */
  padding: 0.4rem 0.85rem;
  border-radius: 999px;
  background: color-mix(in srgb, var(--color-accent) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--color-accent) 32%, transparent);
  backdrop-filter: blur(4px);
}

/* The leading dash read fine as loose text but looks odd boxed inside the
   pill — drop it (the AID STATION reference chip has none either). */
.eyebrow::before { display: none; }

.section-heading { max-width: 46rem; }
.section-heading p { color: var(--color-ink-soft); font-size: var(--type-md); }

/* ============================== BUTTONS ============================== */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  min-height: 48px;
  padding: 0.85rem 1.75rem;
  border-radius: 999px;
  font-weight: 600;
  font-size: var(--type-sm);
  border: 2px solid transparent;
  transition: transform var(--duration-fast) var(--ease-out),
              background-color var(--duration-fast) var(--ease-out),
              box-shadow var(--duration-fast) var(--ease-out),
              border-color var(--duration-fast) var(--ease-out);
  cursor: pointer;
  text-decoration: none;
}

.btn:active { transform: scale(0.97); }

.btn-primary {
  background: var(--color-cta);
  color: var(--color-white);
  box-shadow: 0 8px 24px -8px rgba(97, 81, 154, 0.5);
}
.btn-primary:hover { background: var(--color-accent-dark); box-shadow: 0 10px 28px -8px rgba(97, 81, 154, 0.55); }

.btn-outline {
  background: transparent;
  border-color: currentColor;
  color: var(--color-white);
}
.btn-outline:hover { background: rgba(255,255,255,0.14); }

.btn-outline-dark {
  background: transparent;
  border-color: var(--color-primary);
  color: var(--color-primary-dark);
}
.btn-outline-dark:hover { background: var(--color-primary-tint); }

.btn-sm { min-height: 40px; padding: 0.6rem 1.25rem; font-size: var(--type-xs); }

/* Ambient glow — ALWAYS on, unlike .hero-actions .btn's hover-only ping
   rings above, so it reads as "glowing" at rest rather than needing a
   pointer over it first. A looping box-shadow bloom in the accent blue,
   pulsing between a tight dim halo and a wider bright one. */
.btn-glow { animation: btnGlow 2.4s ease-in-out infinite; }
.btn-glow:hover { background: var(--color-accent-dark); }
@keyframes btnGlow {
  0%, 100% { box-shadow: 0 0 10px 1px rgba(100, 184, 227, 0.5), 0 8px 24px -8px rgba(97, 81, 154, 0.5); }
  50% { box-shadow: 0 0 26px 6px rgba(100, 184, 227, 0.8), 0 8px 24px -8px rgba(97, 81, 154, 0.5); }
}
@media (prefers-reduced-motion: reduce) {
  .btn-glow { animation: none; box-shadow: 0 0 18px 3px rgba(100, 184, 227, 0.65); }
}

/* Hero CTA hover — a slow scale pulse plus two rings (::before and ::after)
   that expand off the button's own pill shape and fade, staggered half a
   cycle apart so a second ring is always emerging as the first fades —
   looping the whole time it's hovered (sonar-ping, not a one-shot). Ring
   color is currentColor by default (so it matches btn-outline's white),
   overridden to the accent blue for btn-primary since that button's "own
   color" is its fill, not its (white) text. Same expand-and-fade mechanism
   as the mile-tracker's "you are here" ping (.mile-stop.is-active
   .mile-stop-dot::after below), just reshaped from a dot to a pill via
   border-radius: inherit, and doubled up. Scoped to the hero buttons only,
   not every .btn sitewide. */
.hero-actions .btn { position: relative; }
.hero-actions .btn::after,
.hero-actions .btn::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  border: 2px solid currentColor;
  opacity: 0;
  pointer-events: none;
}
.hero-actions .btn-primary::after,
.hero-actions .btn-primary::before {
  border-color: var(--color-accent);
}
.hero-actions .btn:hover,
.hero-actions .btn:focus-visible {
  animation: heroBtnPulse 1.6s ease-in-out infinite;
}
.hero-actions .btn:hover::after,
.hero-actions .btn:focus-visible::after {
  animation: heroBtnPing 2.8s ease-out infinite;
}
.hero-actions .btn:hover::before,
.hero-actions .btn:focus-visible::before {
  animation: heroBtnPing 2.8s ease-out infinite;
  animation-delay: 1.4s;
}
@keyframes heroBtnPulse {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.06); }
}
/* Growth trimmed from the original 1 → 1.35 (a 0.35 reach) down to 1 → 1.2
   (a 0.21 reach) — about 40% less distance traveled — and the duration
   above stretched from 1.6s to 2.8s for a slower ping. */
@keyframes heroBtnPing {
  0% { transform: scale(1); opacity: 0.6; }
  100% { transform: scale(1.2); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .hero-actions .btn:hover,
  .hero-actions .btn:focus-visible,
  .hero-actions .btn:hover::after,
  .hero-actions .btn:focus-visible::after,
  .hero-actions .btn:hover::before,
  .hero-actions .btn:focus-visible::before { animation: none; }
}

/* ============================== HEADER / NAV ============================== */
/* .site-header is just a positioning shell — no background of its own. The
   logo (left) floats free directly over the page; only .main-nav (the
   About/Events/Get Involved group, center) carries the dark glass chrome
   below, shaped into its own pill rather than one continuous edge-to-edge
   bar. Visible immediately (there's no intro animation for it to wait
   behind anymore) — only .is-condensed (js/main.js, on scroll direction)
   still changes its look after load. */
.site-header {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: var(--z-header);
  height: var(--header-height);
  display: flex;
  align-items: center;
}

.site-header .container {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  width: 100%;
  /* Overrides the shared .container's max-width:var(--container-max) cap —
     the header reads better spread toward the true viewport edges (logo far
     left, ribbon far right) than boxed into the same reading-width column
     the page's text content uses. padding-inline (inherited from .container)
     is what actually controls the edge gap now that max-width isn't. */
  max-width: none;
}
.site-header .container > .logo { justify-self: start; }
.site-header .container > .main-nav { justify-self: center; }
.site-header .container > .header-right { justify-self: end; display: flex; align-items: center; gap: var(--space-3); }

/* Awareness ribbon — a real photo (solid background, not a transparent
   cutout), so it's cropped into a circular badge with its own ring + shadow
   rather than floating bare like the logo/mark do; a bare square would read
   as a stray image over the photo hero instead of an intentional badge. */
.header-ribbon {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  object-fit: cover;
  border: 2px solid rgba(255, 255, 255, 0.85);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4), 0 1px 3px rgba(0, 0, 0, 0.45);
  /* Same reveal/condense choreography as .logo below — see that rule for why
     the delay lives on the is-condensed side, not here. */
  transition: opacity var(--duration-slow) var(--ease-out),
              transform var(--duration-slow) var(--ease-out);
}
.site-header.is-condensed .header-ribbon {
  opacity: 0;
  visibility: hidden;
  transform: translateY(-32px);
  pointer-events: none;
  transition: opacity var(--duration-slow) var(--ease-out),
              transform var(--duration-slow) var(--ease-out),
              visibility 0s linear var(--duration-slow);
}

/* Logo mark styling — keep the <a class="logo"> wrapper so it still links home.
   This dark-ink + light-halo combo is the default — right for the footer
   (fixed dark background, overridden further down), story pages (header is
   position:relative over a plain page background), AND most of #siteHeader's
   own journey down index.html, since the large majority of that page is
   plain white/off-white Mile sections. The floating #siteHeader instance
   flips to white text (see #siteHeader.is-on-dark below) only over the
   handful of sections that are genuinely dark. */
.logo {
  display: inline-flex;
  align-items: center;
  gap: 0.65rem;
  text-decoration: none;
  color: var(--color-ink);
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--type-md);
  min-height: 44px;
  text-shadow: 0 1px 3px rgba(255, 255, 255, 0.85), 0 2px 14px rgba(255, 255, 255, 0.6);
  transition: color var(--duration-base) var(--ease-out),
              text-shadow var(--duration-base) var(--ease-out),
              opacity var(--duration-slow) var(--ease-out),
              transform var(--duration-slow) var(--ease-out);
}

/* Adaptive wordmark — a first attempt at this used mix-blend-mode:difference
   (invert against whatever's actually behind it, no JS needed) instead of
   this explicit class. Reverted: Chromium renders blended TEXT as a blurry
   smudge rather than crisp inverted glyphs — a real rendering limitation,
   not a config issue (confirmed: same result with isolation removed from
   every ancestor, and with -webkit-font-smoothing forced to antialiased).
   This does the same JOB — flip to white specifically where the header
   scrolls over a dark section — deterministically instead: headerDarkZones
   in js/main.js measures each known dark section's document-Y range once
   (.hero-content-wrap, #aid-station — both genuinely full-bleed dark/photo
   backgrounds edge to edge, unlike e.g. .finish-banner, which is a rounded
   card on a light page background and would put the far-left logo outside
   its dark area even while scrolled to its Y range), and toggles
   .is-on-dark on #siteHeader every frame based on whether the header's own
   height currently overlaps one of those ranges. */
#siteHeader.is-on-dark .logo-text,
#siteHeader.is-on-dark .logo-text small {
  color: #fff;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5), 0 2px 14px rgba(0, 0, 0, 0.35);
}

/* The logo mark's ribbon shifts gray->white on the same #siteHeader.is-on-dark
   toggle the wordmark above it uses, so the mark and the text switch in
   sync on the same background — see the comment on that class for why
   it's driven by JS rather than mix-blend-mode. Base (gray) color lives
   on .logo-mark-ribbon itself; this only needs to cover the one state
   that differs. */
#siteHeader.is-on-dark .logo-mark-ribbon { background: var(--color-white); }

.site-header.is-condensed .logo {
  opacity: 0;
  visibility: hidden;
  transform: translateY(-32px);
  pointer-events: none;
  /* Delaying the visibility flip until the fade-out finishes (rather than
     hiding instantly) matters most in reverse: when is-condensed is
     REMOVED (scrolling back up), the un-decorated .logo rule takes over
     and, with no visibility delay of its own, flips straight to visible
     so the fade-in is visible as it happens instead of staying invisible
     for the full duration and popping in at the end. */
  transition: opacity var(--duration-slow) var(--ease-out),
              transform var(--duration-slow) var(--ease-out),
              visibility 0s linear var(--duration-slow);
}

/* .logo-mark is a small positioning wrapper (position:relative), not an
   <img> itself — it stacks THREE layers, all absolutely positioned to
   the exact same box so they align pixel-for-pixel:
     1. the base <img> (assets/logo/logo-mark.png) — just the #A9C4E7
        brain fill + ink gyri linework. The ribbon is NOT baked into this
        file (transparent there) — it's its own layer below instead.
     2. .logo-mark-ribbon — the ribbon itself, as a plain color-filled
        element masked to assets/logo/logo-mark-ribbon.png (an alpha-only
        cutout of just that shape, split from the same source art as the
        brain mask below). Defaults to a mid-dark gray; swaps to white
        via the exact same #siteHeader.is-on-dark toggle the wordmark
        uses (see that rule), so the mark and the text stay in sync on
        the same background. Two contexts don't get that JS toggle at
        all and so need their own static white override: .footer-brand
        (always on a dark background) and story-page headers (no
        #siteHeader id — plain position:relative over ordinary page
        content, so gray-by-default is already correct there with no
        override needed).
     3. .logo-mark-brain-glow — masked to JUST the brain's silhouette
        (logo-mark-brain.png) and filled with solid --color-accent-light
        (#A9C4E7, the exact brain-fill blue) for a blurred halo behind the
        brain fill in the SAME shade, untouched by the ribbon layer since
        its mask doesn't cover that area. */
.logo-mark {
  position: relative;
  display: inline-block;
  width: 40px;
  height: 40px;
  flex-shrink: 0;
  filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.6)) drop-shadow(0 2px 12px rgba(0, 0, 0, 0.4));
}
.logo-mark img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain; /* the source PNG isn't perfectly square (~1.1:1) — this keeps it undistorted within the fixed box rather than stretching it to fit */
}
.logo-mark-ribbon {
  position: absolute;
  inset: 0;
  background: var(--color-ink-soft); /* #5B5D63 — mid-dark gray default; swaps to white when on a dark background, see the #siteHeader.is-on-dark and .footer-brand rules */
  -webkit-mask: url(../assets/logo/logo-mark-ribbon.png) center / contain no-repeat;
  mask: url(../assets/logo/logo-mark-ribbon.png) center / contain no-repeat;
  transition: background-color var(--duration-base) var(--ease-out);
}
/* Footer's mark is always on a fixed dark background — never inside
   #siteHeader, so it never gets .is-on-dark toggled onto it. Mirrors the
   existing .footer-brand .logo (wordmark) override just above. */
.footer-brand .logo-mark-ribbon { background: var(--color-white); }
.logo-mark-brain-glow {
  position: absolute;
  inset: 0;
  /* Exactly the brain-fill blue (#A9C4E7), not the darker periwinkle accent —
     so the halo doesn't read as a second shade of blue on the mark. */
  background: var(--color-accent-light);
  -webkit-mask: url(../assets/logo/logo-mark-brain.png) center / contain no-repeat;
  mask: url(../assets/logo/logo-mark-brain.png) center / contain no-repeat;
  filter: drop-shadow(0 0 5px var(--color-accent-light)) drop-shadow(0 0 10px var(--color-accent-light));
}

.logo-text small {
  display: block;
  font-family: var(--font-body);
  font-size: var(--type-xs);
  font-weight: 500;
  letter-spacing: 0.05em;
  opacity: 0.85;
}

/* Below ~600px the subtitle doesn't have room and wraps to 2-3 lines,
   overflowing the header's fixed height — harmless when the header had a
   full-width background to contain it, but now that .site-header has none
   (the logo floats free), an overflowing wrap spills visibly onto whatever
   page content is underneath. Simplest fix: drop the subtitle, keep the
   main wordmark. */
@media (max-width: 600px) {
  .logo-text small { display: none; }
}

/* The nav's own pill — a light frosted-glass chip (rather than the dark
   glass the whole header used to share), sized to hug just this group
   instead of stretching edge to edge. Light enough that .main-nav a and
   .nav-trigger need dark ink text now instead of white (see below) — white
   on this background wouldn't be legible. .is-condensed (toggled on
   #siteHeader by js/main.js, past CONDENSE_SCROLL_BUFFER) pulls it up
   toward the top of the viewport so it "hangs" there once you've scrolled
   a bit, while .logo above fades out entirely. */
.main-nav {
  display: none;
  background: rgba(245, 245, 246, 0.5);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-radius: 999px;
  /* Tightened twice now from the original 0.7rem/--space-4 (32px/11.2px)
     — the header floats over whatever page content is underneath it, so a
     bigger pill than the links actually need just blurs/dims more of that
     content than necessary. .main-nav a's own padding-block (--space-1,
     8px) plus this still keeps the click target comfortably above 44px
     tall even at this tighter size. */
  padding: 0.35rem var(--space-2);
  box-shadow: 0 8px 24px -10px rgba(0, 0, 0, 0.35);
  transition: background-color var(--duration-base) var(--ease-out),
              box-shadow var(--duration-base) var(--ease-out),
              transform var(--duration-slow) var(--ease-out);
}

.site-header.is-condensed .main-nav {
  transform: translateY(-14px);
  background: rgba(245, 245, 246, 0.7);
  box-shadow: 0 6px 18px -8px rgba(0, 0, 0, 0.3);
}

.main-nav ul {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  list-style: none;
  margin: 0; padding: 0;
}

.main-nav a {
  text-decoration: none;
  font-size: var(--type-sm);
  font-weight: 600;
  color: var(--color-ink);
  position: relative;
  padding-block: var(--space-1);
  transition: color var(--duration-fast) var(--ease-out);
}

.main-nav a::after {
  content: '';
  position: absolute;
  left: 0; bottom: 0;
  width: 0; height: 2px;
  background: var(--color-accent-dark);
  transition: width var(--duration-base) var(--ease-out);
}
.main-nav a:hover::after,
.main-nav a[aria-current="true"]::after { width: 100%; }

.main-nav a:hover,
.main-nav a[aria-current="true"] { color: var(--color-accent-dark); }

/* Dropdown mega-menu: hover- and focus-driven, with a JS .is-open fallback
   for click/keyboard activation (see js/main.js). */
.nav-item { position: relative; }

.nav-trigger {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  background: none;
  border: none;
  padding-block: var(--space-1);
  font-family: var(--font-body);
  font-size: var(--type-sm);
  font-weight: 600;
  color: var(--color-ink);
  position: relative;
  transition: color var(--duration-fast) var(--ease-out);
}
.nav-trigger::after {
  content: '';
  position: absolute;
  left: 0; bottom: 0;
  width: 0; height: 2px;
  background: var(--color-accent-dark);
  transition: width var(--duration-base) var(--ease-out);
}
.nav-trigger::before {
  content: '';
  width: 6px; height: 6px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg) translateY(-2px);
  transition: transform var(--duration-fast) var(--ease-out);
  opacity: 0.8;
}

.nav-item:hover .nav-trigger,
.nav-item.is-open .nav-trigger,
.nav-trigger:focus-visible {
  color: var(--color-accent-dark);
}
.nav-item:hover .nav-trigger::after,
.nav-item.is-open .nav-trigger::after { width: 100%; }
.nav-item:hover .nav-trigger::before,
.nav-item.is-open .nav-trigger::before { transform: rotate(225deg) translate(1px, 1px); }

.nav-dropdown {
  position: absolute;
  top: calc(100% + 20px);
  left: 50%;
  /* --dropdown-shift is set by main.js right before opening — 0px (pure
     centering) whenever there's room, or a small inward nudge when centering
     under this particular trigger would push the panel past the viewport
     edge. Keeps every dropdown on one line without ever clipping. */
  transform: translate(calc(-50% + var(--dropdown-shift, 0px)), 8px);
  width: max-content;
  min-width: 300px;
  max-width: 92vw;
  background: var(--color-white);
  border-radius: 10px; /* tighter, more rectangular than --radius-md (14px) — the wide, short panel read as pill-ish at that radius */
  box-shadow: 0 24px 48px -16px rgba(191, 152, 42, 0.28);
  padding: var(--space-1);
  display: flex;
  flex-direction: row;
  gap: 2px;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity var(--duration-base) var(--ease-out),
              transform var(--duration-base) var(--ease-out),
              visibility 0s linear var(--duration-base);
  z-index: var(--z-header);
}

/* Invisible bridge filling the 20px gap above the panel — without it, moving
   the mouse from the trigger down into the dropdown crosses a dead zone with
   no element in it, dropping :hover on .nav-item and closing the panel
   before the cursor ever reaches it. content-less but still hit-testable, so
   :hover carries through the gap continuously. Inherits the dropdown's own
   pointer-events (none while closed, auto while open), so it can't be
   hovered until the panel is already open via the trigger itself. */
.nav-dropdown::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: -20px;
  height: 20px;
}

.nav-item:hover .nav-dropdown,
.nav-item.is-open .nav-dropdown,
.nav-item:focus-within .nav-dropdown {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transform: translate(calc(-50% + var(--dropdown-shift, 0px)), 0);
  transition: opacity var(--duration-base) var(--ease-out),
              transform var(--duration-base) var(--ease-out),
              visibility 0s linear 0s;
}

.nav-dropdown-item {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  padding: 0.5rem 0.85rem;
  border-radius: 6px;
  text-decoration: none;
  width: 180px; /* items sit side by side now, rather than stacked full-width rows — a fixed width keeps labels/descriptions from reflowing unevenly against each other */
  transition: background-color var(--duration-fast) var(--ease-out);
}
.nav-dropdown-item + .nav-dropdown-item {
  border-left: 1px solid var(--color-border); /* separates the now side-by-side items, since there's no full-width row to imply where one ends and the next begins */
}
.nav-dropdown-item:hover,
.nav-dropdown-item:focus-visible { background: var(--color-primary-tint); }

.nav-dropdown-label {
  font-family: var(--font-display);
  font-size: var(--type-base);
  font-weight: 600;
  color: var(--color-ink);
}
.nav-dropdown-desc {
  font-size: var(--type-xs);
  color: var(--color-ink-soft);
}

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

.nav-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px; height: 48px;
  background: transparent;
  border: none;
  color: var(--color-white);
}
.nav-toggle svg { width: 26px; height: 26px; }

@media (min-width: 1024px) {
  .main-nav { display: block; }
  .nav-toggle { display: none; }
}

/* Mobile nav drawer */
.mobile-nav {
  position: fixed;
  inset: 0;
  z-index: calc(var(--z-header) + 1);
  background: var(--color-white);
  display: flex;
  flex-direction: column;
  padding: var(--space-3);
  transform: translateX(100%);
  /* Off-screen alone still leaves every link in the Tab order, so a keyboard
     user could tab into a menu they can't see. visibility takes it out of the
     focus order; it flips only after the slide-out has finished. */
  visibility: hidden;
  transition: transform var(--duration-slow) var(--ease-out), visibility 0s linear var(--duration-slow);
}
.mobile-nav.is-open {
  transform: translateX(0);
  visibility: visible;
  transition: transform var(--duration-slow) var(--ease-out);
}

.mobile-nav-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--space-5);
}

.mobile-nav-close {
  width: 48px; height: 48px;
  border: none; background: transparent;
  color: var(--color-ink);
}
.mobile-nav-close svg { width: 26px; height: 26px; }

.mobile-nav ul {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: var(--space-1);
}

.mobile-nav a {
  display: block;
  min-height: 48px;
  display: flex;
  align-items: center;
  text-decoration: none;
  font-family: var(--font-display);
  font-size: var(--type-lg);
  color: var(--color-ink);
  border-bottom: 1px solid var(--color-border);
  padding-block: var(--space-2);
}

.mobile-nav .btn { margin-top: var(--space-4); width: 100%; }
/* .mobile-nav a (above) paints every menu link in ink with a 1px divider,
   which also caught the Register button: dark text on the blue and a grey
   line under the pill. White text and the standard button border, like
   every other primary button. */
.mobile-nav .btn-primary { color: var(--color-white); border-bottom: 2px solid transparent; }

/* Mobile accordion (About / Events / Get Involved) — hover doesn't exist on
   touch, so each group expands in place instead of showing a hover panel. */
.accordion-item { border-bottom: 1px solid var(--color-border); }

.accordion-trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  min-height: 56px;
  background: none;
  border: none;
  text-align: left;
  font-family: var(--font-display);
  font-size: var(--type-lg);
  font-weight: 600;
  color: var(--color-ink);
  padding-block: var(--space-2);
}
.accordion-trigger svg {
  width: 20px; height: 20px; flex-shrink: 0;
  color: var(--color-ink-faint);
  transition: transform var(--duration-base) var(--ease-out);
}
.accordion-item.is-open .accordion-trigger svg { transform: rotate(180deg); }

.accordion-panel {
  list-style: none;
  margin: 0; padding: 0;
  max-height: 0;
  overflow: hidden;
  transition: max-height var(--duration-base) var(--ease-out);
}
.accordion-item.is-open .accordion-panel { max-height: 320px; }

.accordion-panel a {
  display: flex;
  align-items: center;
  min-height: 44px;
  font-family: var(--font-body);
  font-size: var(--type-base);
  font-weight: 500;
  color: var(--color-ink-soft);
  text-decoration: none;
  border-bottom: none;
  padding-left: var(--space-2);
}
.accordion-panel a:hover { color: var(--color-accent-dark); }

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

@media (min-width: 1024px) {
  .mobile-nav, .nav-toggle { display: none !important; }
}

/* ============================== HERO ============================== */
.hero {
  position: relative;
  color: var(--color-white);
  isolation: isolate; /* contains .hero-media's z-index:-2 background to this section, rather than letting it escape and paint behind unrelated page content */
}

.hero-content-wrap {
  position: relative;
  min-height: 100svh;
  display: flex;
  align-items: flex-end;
}

.hero-media {
  position: absolute;
  inset: 0;
  z-index: -2;
  /* Placeholder animated gradient stands in for the hero video.
     Once assets/video/hero.mp4 exists, the <video> in index.html takes over.
     Blends all three brand colors: gray, the event's blue, and lavender. */
  background: linear-gradient(120deg, var(--color-primary-dark) 0%, var(--color-blue-dark) 50%, var(--color-accent-dark) 100%);
  background-size: 200% 200%;
  animation: heroGradientShift 18s ease-in-out infinite;
}

.hero-media img,
.hero-media video {
  width: 100%; height: 100%;
  object-fit: cover;
}

.hero-media::before {
  content: '';
  position: absolute;
  inset: 0;
  background-image: var(--noise-texture);
  opacity: 0.05;
  mix-blend-mode: overlay;
  pointer-events: none;
}

.hero-media::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(24, 35, 43, 0.35) 0%, rgba(24, 35, 43, 0.55) 55%, rgba(20, 28, 34, 0.85) 100%);
}

@keyframes heroGradientShift {
  0%, 100% { background-position: 0% 50%; }
  50% { background-position: 100% 50%; }
}

.hero-content {
  position: relative;
  z-index: 1;
  padding-block: var(--space-7) var(--space-6);
  width: 100%;
  /* Overrides the shared .container's max-width cap (same override
     .site-header .container uses, and for the same reason) — on wide
     viewports that cap centers this text block with a large empty margin to
     its left instead of letting it sit closer to the true edge. */
  max-width: none;
}

.hero-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  background: rgba(255, 255, 255, 0.14);
  border: 1px solid rgba(255, 255, 255, 0.35);
  backdrop-filter: blur(6px);
  padding: 0.4rem 0.9rem;
  border-radius: 999px;
  font-size: var(--type-xs);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  margin-bottom: var(--space-3);
}

.hero h1 {
  color: var(--color-white);
  /* Own clamp rather than the shared --type-3xl: tuned specifically so
     "Supporting cancer research with every step" breaks to exactly three
     lines against the max-width below, at tablet widths and up (mobile
     floors out at a legible size instead of chasing the same line count
     down to illegible text). */
  font-size: clamp(2rem, 4.5vw, 3.6rem);
  /* Capped in both units so it stays clear of the page's vertical midline
     at any viewport width: ch keeps wrapping sane on narrow screens; the %
     is what actually guarantees the midline clearance, since .hero-content
     is centered and symmetrically padded, so 50% of its own width always
     lines up with the viewport's midline regardless of screen size — 46%
     keeps a small margin short of that. */
  max-width: min(13ch, 46%);
  margin-bottom: var(--space-3);
}

.hero-meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
  font-size: var(--type-sm);
}

.hero-meta-item {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.hero-meta-item svg { width: 20px; height: 20px; flex-shrink: 0; }

.hero-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

/* ============================== MILE SIDEBAR ============================== */
.mile-progress {
  position: fixed;
  top: 0; left: 0; right: 0;
  height: 3px;
  background: transparent;
  z-index: var(--z-mile-sidebar);
}
.mile-progress-fill {
  position: relative;
  height: 100%;
  width: 0%;
  background: var(--color-progress);
}
.mile-progress-marker {
  position: absolute;
  right: -4px;
  top: 50%;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--color-progress);
  box-shadow: 0 0 0 3px rgba(155, 127, 212, 0.3);
  transform: translateY(-50%);
}

.mile-sidebar {
  display: none;
}

@media (min-width: 1024px) {
  .mile-sidebar {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 0;
    position: fixed;
    right: 14px;
    top: 50%;
    transform: translateY(-50%);
    z-index: var(--z-mile-sidebar);
    /* Hidden right at the very top (redundant with the hero banner itself)
       until you've scrolled a little way into the page — js/main.js adds
       .is-revealed past CONDENSE_SCROLL_BUFFER, same threshold the header's
       own condense uses. */
    opacity: 0;
    visibility: hidden;
    transition: opacity var(--duration-slow) var(--ease-out),
                visibility 0s linear var(--duration-slow);
  }

  .mile-sidebar.is-revealed {
    opacity: 1;
    visibility: visible;
    transition: opacity var(--duration-slow) var(--ease-out);
  }

  /* Resting state is just the trail + dots — tucked right against the edge
     so it never sits over page content. The frosted pill backdrop (for
     contrast against dark sections) and the labels only fade in on hover,
     so the wider/opaque state only appears when it's an intentional glance,
     not a permanent obstruction. */
  .mile-sidebar-track {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    background: rgba(255, 255, 255, 0);
    backdrop-filter: blur(0px) saturate(100%);
    border: 1px solid rgba(255, 255, 255, 0);
    border-radius: 999px;
    padding: 16px 16px 16px 22px;
    box-shadow: 0 10px 30px -12px rgba(15, 23, 28, 0);
    transition: background-color var(--duration-base) var(--ease-out),
                border-color var(--duration-base) var(--ease-out),
                box-shadow var(--duration-base) var(--ease-out),
                backdrop-filter var(--duration-base) var(--ease-out);
  }

  .mile-sidebar:hover .mile-sidebar-track,
  .mile-sidebar:focus-within .mile-sidebar-track {
    background: rgba(255, 255, 255, 0.75);
    backdrop-filter: blur(10px) saturate(160%);
    border-color: rgba(255, 255, 255, 0.6);
    box-shadow: 0 10px 30px -12px rgba(15, 23, 28, 0.3);
  }

  /* The winding trail: an SVG path rebuilt in JS to pass exactly through each
     dot's real center (see buildTrailPath in js/main.js), with a marker
     traveling along it. Because the path is derived from the dots' actual
     positions rather than an approximate hand-drawn curve, the marker can't
     drift off a dot the way it could before. */
  .mile-trail-wrap {
    position: absolute;
    right: 10px;
    top: 16px;
    bottom: 16px;
    width: 16px;
  }

  .mile-trail { width: 100%; height: 100%; display: block; overflow: visible; }

  .mile-trail-bg, .mile-trail-fg {
    fill: none;
    stroke-width: 2.5;
    stroke-linecap: round;
    vector-effect: non-scaling-stroke;
  }

  .mile-trail-bg { stroke: var(--color-border); }

  .mile-trail-fg {
    stroke: var(--color-progress);
    stroke-dasharray: 1;
    stroke-dashoffset: 1;
  }

  .mile-trail-marker {
    position: absolute;
    left: 50%;
    top: 0;
    width: 11px;
    height: 11px;
    margin: -5.5px 0 0 -5.5px;
    border-radius: 50%;
    background: var(--color-progress);
    box-shadow: 0 0 0 4px rgba(155, 127, 212, 0.3), 0 2px 10px -2px rgba(112, 87, 173, 0.65);
    will-change: transform;
  }

  .mile-stop {
    position: relative;
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding-block: var(--space-2);
    text-decoration: none;
    color: var(--color-ink-faint);
    transition: color var(--duration-base) var(--ease-out);
  }

  .mile-stop-label {
    font-size: var(--type-xs);
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    white-space: nowrap;
    opacity: 0;
    transform: translateX(8px);
    transition: opacity var(--duration-base) var(--ease-out), transform var(--duration-base) var(--ease-out);
  }

  .mile-stop-dot {
    position: relative; /* containing block for .is-active's ping ring (::after) below */
    width: 12px; height: 12px;
    border-radius: 50%;
    background: var(--color-white);
    border: 2px solid var(--color-ink-faint);
    z-index: 1;
    transition: border-color var(--duration-base) var(--ease-out),
                background-color var(--duration-base) var(--ease-out),
                transform var(--duration-base) var(--ease-out);
  }

  .mile-sidebar:hover .mile-stop-label,
  .mile-stop:focus-visible .mile-stop-label { opacity: 1; transform: translateX(0); }

  .mile-stop:hover .mile-stop-dot,
  .mile-stop:focus-visible .mile-stop-dot { border-color: var(--color-progress); transform: scale(1.15); }

  /* Per-item hover highlight — separate from .is-active below (your real scroll
     position), which keeps working exactly as before regardless of what's hovered.
     Reuses .is-active's own text color so a hovered stop reads as clearly "lit up,"
     while the smaller dot scale here (vs. is-active's filled/glowing 1.3x above)
     keeps it visually distinct from the current section. */
  .mile-stop:hover,
  .mile-stop:focus-visible { color: var(--color-ink); }

  /* Stops before the current one, in trail order — filled in solid so the
     dots read as a trail of progress leading up to where you are, rather
     than only the current stop ever standing out from a row of empty ones. */
  .mile-stop.is-passed .mile-stop-dot {
    background: var(--color-progress);
    border-color: var(--color-progress);
  }

  .mile-stop.is-active { color: var(--color-ink); }
  .mile-stop.is-active .mile-stop-dot {
    background: var(--color-progress);
    border-color: var(--color-progress);
    transform: scale(1.3);
    box-shadow: 0 0 0 4px rgba(155, 127, 212, 0.3);
  }

  /* Live "you are here" ping — a ring expands out from the current dot and
     fades, on a continuous loop, sized/positioned off the dot itself (see
     position:relative on .mile-stop-dot above) so it's centered regardless
     of the dot's own scale(1.3) from is-active above. */
  .mile-stop.is-active .mile-stop-dot::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: 50%;
    background: var(--color-progress);
    animation: mileDotPing 2s cubic-bezier(0, 0, 0.2, 1) infinite;
    pointer-events: none;
  }

  @keyframes mileDotPing {
    0% { transform: scale(1); opacity: 0.55; }
    100% { transform: scale(2.6); opacity: 0; }
  }
}

/* Mobile mile tracker (sticky pill under header) */
.mile-tracker-mobile {
  position: sticky;
  top: var(--header-height);
  z-index: calc(var(--z-mile-sidebar) - 1);
  display: flex;
  align-items: center;
  gap: var(--space-1);
  background: rgba(255,255,255,0.55);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid rgba(226, 227, 229, 0.6);
  padding: 0.6rem var(--space-3);
  font-size: var(--type-xs);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-ink-soft);
}
.mile-tracker-mobile .current-mile { color: var(--color-accent-dark); }

@media (min-width: 1024px) {
  .mile-tracker-mobile { display: none; }
}

/* The tracker is a sibling BEFORE <main>, so as a sticky element it also
   reserved its own 42px of flow at the very top of the document — which
   pushed the hero down by 42px and left a band of bare page background
   showing under a header whose wordmark is white (it's coloured for the dark
   hero photo it normally sits on). The result on a phone was an invisible
   wordmark on a white strip.
   Fixed instead of sticky costs nothing behaviourally: it sits at the top of
   the document with top:--header-height, so sticky pinned it on frame one
   and it never scrolled anyway. Taking it out of flow lets the hero start at
   y=0 where the header expects it. Every section below already pads for
   header + tracker (see the max-width:1023px block in MILE SECTIONS), so
   nothing else moves. */
@media (max-width: 1023px) {
  .mile-tracker-mobile {
    position: fixed;
    left: 0;
    right: 0;
  }
}

/* ============================== MILE SECTIONS (shared) ============================== */
.mile-section {
  padding-block: var(--space-7);
  scroll-margin-top: var(--header-height);
  min-height: 100svh;
}

/* ---------- Mile 1 & 2 background shapes ----------
   Same rounded-shape language as the Mile 3 connector band just below
   (.mile-connector-shape: ring outlines + solid pills, same accent
   palette) — Mile 1 and 2 had none of that ambient roundness, reading as
   flatter than the connector and Mile 4/aid-station on either side of them.
   Scaled down and tucked into corners here rather than being the main
   visual (like the connector's are), since these sections are text/photo
   content first. Static, unlike the connector's — those track the cursor
   across one deliberately-decorative full-bleed band; framing shapes
   behind ordinary reading content don't need that same motion to earn
   their place. #mile-1/#mile-2 clip them at the section edge and lift
   the actual content above them with isolation+z-index. */
#mile-1, #mile-2 { overflow: hidden; isolation: isolate; }
#mile-1 > .container, #mile-2 > .container { position: relative; z-index: 1; }

.mile-bg-shape { position: absolute; pointer-events: none; z-index: 0; }
.mile-bg-shape--ring { border-radius: 50%; border-style: solid; background: none; }
.mile-bg-shape--pill { border-radius: 999px; }

#mile-1 .mile-bg-shape--a {
  width: 260px; height: 260px;
  border-width: 42px;
  border-color: var(--color-accent-light);
  opacity: 0.45;
  top: -70px;
  right: -90px;
}
#mile-1 .mile-bg-shape--b {
  width: 70px; height: 230px;
  background: var(--color-accent-dark);
  opacity: 0.16;
  bottom: 6%;
  left: -36px;
  transform: rotate(16deg);
}

#mile-2 .mile-bg-shape--a {
  width: 230px; height: 230px;
  border-width: 38px;
  border-color: var(--color-accent);
  opacity: 0.3;
  bottom: -80px;
  right: 3%;
}
#mile-2 .mile-bg-shape--b {
  width: 64px; height: 64px;
  background: var(--color-primary-light);
  opacity: 0.55;
  top: 4%;
  right: 14%;
}

@media (max-width: 700px) {
  /* Same shapes, pulled in and toned down rather than removed — full size
     they'd crowd the narrower column of content on small screens. */
  #mile-1 .mile-bg-shape--a { width: 160px; height: 160px; border-width: 26px; top: -40px; right: -60px; }
  #mile-1 .mile-bg-shape--b { width: 46px; height: 150px; left: -24px; }
  #mile-2 .mile-bg-shape--a { width: 150px; height: 150px; border-width: 24px; bottom: -50px; right: -30px; }
  #mile-2 .mile-bg-shape--b { width: 44px; height: 44px; top: 3%; right: 6%; }
}

/* ---------- Mile 1–3 guide lines ----------
   A decorative drawn-in stroke (see renderMileFlow in js/main.js) that
   grows as you scroll through Mile 1, Mile 2, and Mile 3. Deliberately NOT
   edge-to-edge through each section — each box only covers the empty
   margin beside one specific landmark, never any running text, with real
   silent gaps in between (Mile 1's stat grid and heading, most of Mile 2)
   rather than a line drawn continuously the whole way down:
     - Mile 1: beside the doctor callout card (which is centered, ~33%–67%
       wide, near the section's bottom) — box sits clear in its right margin.
     - Mile 2: beside the dedication banner (centered, ~24%–76% wide, near
       the section's bottom) — same right-margin treatment, so it reads as
       a continuation of Mile 1's rather than a new direction.
     - Mile 3: from the open gap left of the team card (which starts at
       ~65%) down to the card's own top edge, where its "03" numeral sits —
       arriving right at the edge rather than running into the card's text.
   Solid --color-accent — these sit against light backgrounds (bg-alt/
   white), where a paler tone would barely read.
   Desktop-only, since there's no spare margin for this once content
   stacks to one column. */
.mile-flow {
  position: absolute;
  z-index: 0;
  pointer-events: none;
  opacity: 0.8;
}
.mile-flow svg { width: 100%; height: 100%; display: block; }
.mile-flow path {
  stroke: var(--color-accent);
  fill: none;
  vector-effect: non-scaling-stroke;
}
#mile-1 .mile-flow { top: 68%; height: 33%; left: 70%; width: 18%; }
#mile-2 .mile-flow { top: 82%; height: 18%; left: 78%; width: 16%; }
#mile-3-start .mile-flow { top: 4%; height: 24%; left: 48%; width: 20%; }
@media (max-width: 1023px) { .mile-flow { display: none; } }

/* Cross-page scroll thread — the guide line above, continued onto the
   secondary pages. Injected + driven by js/scroll-thread.js; fixed down
   the left gutter, faint, drawn to match scroll progress. Large screens
   only (the JS also bails below 1320px). */
.scroll-thread {
  position: fixed;
  top: 0;
  left: clamp(16px, 2.5vw, 72px);
  width: 30px;
  height: 100vh;
  z-index: 0;
  pointer-events: none;
}
.scroll-thread svg { width: 100%; height: 100%; display: block; overflow: visible; }
.scroll-thread-bg,
.scroll-thread-fg {
  fill: none;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}
.scroll-thread-bg { stroke: var(--color-border); stroke-width: 2; opacity: 0.6; }
.scroll-thread-fg { stroke: var(--color-accent); stroke-width: 3; opacity: 0.65; }
@media (max-width: 1319px) { .scroll-thread { display: none; } }

/* ============================== AID STATION (donate page) ==============================
   A mid-race "rest stop" — full-bleed dark background + centered minimal headline + a
   donation card, styled after the simple hero-plus-card pattern nonprofits like
   Radiating Hope use for their give-now flow. Deliberately the one dark page among
   otherwise-white/off-white Mile sections, so it reads as a clear waypoint as you scroll
   through. Inherits sizing from .mile-section above; only layout/background/color are
   overridden here. */
.aid-station-section {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  padding-block: calc(var(--header-height) + var(--space-3)) var(--space-3); /* top padding covers the fixed header's height so centered content never starts underneath it */
  text-align: center;
  color: var(--color-white);
  overflow-x: hidden;
  isolation: isolate;
}

/* Below 1024px, .mile-tracker-mobile (sticky, just under the header — see its
   rules and 1024px breakpoint near MILE SECTIONS below) adds another
   --mile-tracker-height of overlay on top of every section, stacked on top of
   the header itself. .mile-section's own padding-block (96px) and this
   section's padding-block above were both only sized for the header, so at
   these widths their top content — a heading, a mile number, the Aid
   Station's badge — started underneath one or both bars. This has to come
   AFTER both of those rules in the file: same specificity, so source order
   decides, and it needs to win. Bumping just padding-top (not the
   padding-block shorthand) leaves each section's bottom padding untouched. */
@media (max-width: 1023px) {
  .mile-section { padding-top: calc(var(--header-height) + var(--mile-tracker-height) + var(--space-3)); }
  .aid-station-section { padding-top: calc(var(--header-height) + var(--mile-tracker-height) + var(--space-3)); }
}

.aid-station-media {
  position: absolute;
  inset: 0;
  z-index: -2;
  overflow: hidden; /* clips the scaled-up, blurred video child (see .aid-station-media video) */
  /* Purple/blue/yellow need to read clearly here, not just as a faint tint —
     so each stop leans mostly on its brand hue, with --color-ink mixed in
     only enough to keep it from turning bright/poster-like against the
     white Mile sections around it. background-size stays at 100% (rather
     than the 200%+pan treatment .hero-media uses) so all three hues sit
     inside the viewport at once instead of being panned past it. */
  background: linear-gradient(150deg,
    var(--color-ink) 0%,
    color-mix(in srgb, var(--color-ink) 30%, var(--color-blue-dark) 70%) 33%,
    color-mix(in srgb, var(--color-ink) 25%, var(--color-accent-dark) 75%) 66%,
    color-mix(in srgb, var(--color-ink) 30%, var(--color-yellow-dark) 70%) 100%);
}
.aid-station-media::before {
  content: '';
  position: absolute;
  inset: 0;
  background-image: var(--noise-texture);
  opacity: 0.05;
  mix-blend-mode: overlay;
  pointer-events: none;
}
.aid-station-media::after {
  content: '';
  position: absolute;
  inset: 0;
  /* Was a flat rgba(...,0.6). With the autumn-trail video behind it now,
     kept top-weighted so the white heading holds against the bright sky at
     the top of the footage, but lightened overall so the trail reads
     clearly through it rather than sitting in near-dark. */
  background: linear-gradient(180deg,
    rgba(14, 19, 23, 0.62) 0%,
    rgba(14, 19, 23, 0.46) 42%,
    rgba(14, 19, 23, 0.60) 100%);
}

/* Autumn-trail loop — see index.html. Not positioned (like .hero-media img),
   so ::before's noise and ::after's scrim both still layer over it. The
   portrait source is cropped to a landscape band by object-fit; the
   object-position bias keeps the trail — not the blown-out sky along the
   top edge of the frame — in view. Starts hidden and fades up once the
   first frame is decoded (js/main.js adds .is-ready) so it doesn't hard-cut
   in over the gradient beneath it. */
.aid-station-media video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 62%;
  /* Gentle colour lift, plus a sub-pixel blur that softens the source
     footage's compression/resolution so it reads as an intentional ambient
     wash rather than a low-res clip. Contrast is left alone — pushing it
     just sharpens the artefacts back up. */
  filter: brightness(1.05) saturate(1.1) blur(1.3px);
  /* blur() feathers the frame edges inward, which would show a soft fringe
     against the section edge — scale up just past the crop so that fringe
     lands outside .aid-station-media (which now clips it). */
  transform: scale(1.05);
  opacity: 0;
  transition: opacity 1s ease;
}
.aid-station-media video.is-ready { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .aid-station-media video { transition: none; }
}

.aid-station-content {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: 620px;
  margin-inline: auto;
  padding-inline: var(--space-3);
}
.aid-station-content h2 {
  color: var(--color-white);
  font-size: var(--type-3xl);
  /* The shared h1-h4 rule caps this at max-width:18ch (see near the top of
     this file) for every section that uses it, but only .finish-banner h2
     also centers that narrower box with margin-inline:auto — this section
     needs the same treatment since .aid-station-section is centered too,
     otherwise centered TEXT sits inside a box that's still flush-left,
     which reads as the whole heading being shifted left. */
  margin-inline: auto;
  margin-bottom: var(--space-2);
  /* Legibility insurance now that the video behind it is brighter. */
  text-shadow: 0 1px 16px rgba(14, 19, 23, 0.55);
}
/* Sits directly under the h2, above .aid-station-grid — inherits
   text-align:center from .aid-station-section, but (same issue as the h2
   above) that only centers the TEXT within this box; the box itself also
   needs margin-inline:auto or it stays flush-left since max-width caps it
   narrower than the section. */
.aid-station-lead {
  font-size: var(--type-md);
  color: rgba(255, 255, 255, 0.88);
  max-width: 42ch;
  margin: var(--space-3) auto 0;
  text-shadow: 0 1px 12px rgba(14, 19, 23, 0.5);
}

/* This 600px step must stay ABOVE the 900px grid step below it: both match
   at 900px+ (600px's condition is still true there too), so source order
   decides between two same-specificity rules — 900px needs to come second
   to actually win at desktop widths instead of being silently overridden
   back down to 480px. */
@media (min-width: 600px) {
  .aid-station-content { max-width: 480px; }
}
@media (min-width: 900px) {
  /* Deliberately wider than --container-max (the site's standard
     text-reading ceiling used for prose sections elsewhere) — this is a
     card/dashboard layout, not body copy, so it can and should spread
     further across a real desktop viewport instead of plateauing at the
     same width as a paragraph section. */
  .aid-station-content { max-width: 1400px; }
}

/* Wide desktop / large monitors: keep spreading laterally rather than
   plateauing at the 1400px step above — a real "PC size" viewport (1440p+)
   still had a lot of unused space on either side of the section otherwise. */
@media (min-width: 1500px) {
  .aid-station-content { max-width: 1680px; }
}
/* Three across once there's room; the rank+name+amount row stays legible
   down to roughly a third of this breakpoint's width. */

.donate-card {
  background: var(--color-white);
  color: var(--color-ink);
  border-radius: var(--radius-lg);
  padding: var(--space-4);
  text-align: left;
  box-shadow: 0 32px 64px -20px rgba(0, 0, 0, 0.45);
}

.donate-card .btn { width: 100%; }

/* The aid station's only card now that giving happens on OneCause — kept
   narrow and centered however wide .aid-station-content grows. */
.aid-station-donate {
  max-width: 420px;
  margin: var(--space-4) auto 0;
}
.donate-card-links {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.4rem 0.6rem;
  margin-top: var(--space-3);
  font-size: var(--type-sm);
  font-weight: 600;
  color: var(--color-ink-faint);
}
.donate-card-links a { color: var(--color-accent-dark); }

.donate-fineprint {
  font-size: var(--type-xs);
  color: var(--color-ink-faint);
  text-align: center;
  margin-top: var(--space-2);
}

/* The mile-number is the site's signature device — this is a real course
   with real progress, so leaning into these numerals hard (rather than a
   modest inline label) ties all four Mile sections together as one
   throughline instead of four disconnected blog posts. */
.mile-number {
  display: block;
  font-family: var(--font-display);
  font-size: var(--type-5xl);
  font-weight: 700;
  line-height: 0.75;
  color: transparent;
  -webkit-text-stroke: 1.5px var(--color-primary-light);
  opacity: 0.6;
  margin-bottom: var(--space-1);
  pointer-events: none;
  user-select: none;
}

.mile-grid {
  display: grid;
  gap: var(--space-4);
  align-items: start;
}

@media (min-width: 900px) {
  .mile-grid--split { grid-template-columns: 1.1fr 0.9fr; gap: var(--space-6); }
  .mile-grid--split.reverse { direction: rtl; }
  .mile-grid--split.reverse > * { direction: ltr; }
}

/* Media placeholders — swap the inner comment/div for a real <img>/<video> per
   the instructions in assets/README.md. A soft mesh gradient + inner shadow
   reads as an intentional "artwork loading" state rather than a hazard
   stripe — closer to a premium component library's empty-state than a
   placeholder that looks unfinished. */
.media-placeholder {
  position: relative;
  border-radius: var(--radius-lg);
  overflow: hidden;
  aspect-ratio: 4 / 5;
  background:
    radial-gradient(130% 100% at 12% 0%, rgba(255,255,255,0.22) 0%, transparent 48%),
    radial-gradient(120% 130% at 100% 100%, rgba(0,0,0,0.22) 0%, transparent 55%),
    linear-gradient(160deg, var(--color-primary) 0%, var(--color-primary-dark) 100%);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.14);
  display: flex;
  align-items: flex-end;
  padding: var(--space-3);
  color: rgba(255,255,255,0.85);
}

.media-placeholder.tone-green {
  background:
    radial-gradient(130% 100% at 12% 0%, rgba(255,255,255,0.22) 0%, transparent 48%),
    radial-gradient(120% 130% at 100% 100%, rgba(0,0,0,0.22) 0%, transparent 55%),
    linear-gradient(160deg, var(--color-accent) 0%, var(--color-accent-dark) 100%);
}

.media-placeholder.ratio-wide { aspect-ratio: 16 / 10; }

.media-placeholder img,
.media-placeholder video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Pull quote */
.pull-quote {
  border-left: 3px solid var(--color-accent);
  padding-left: var(--space-3);
  margin: var(--space-4) 0;
  font-family: var(--font-display);
  font-size: var(--type-lg);
  font-style: italic;
  color: var(--color-ink);
}
.pull-quote cite {
  display: block;
  margin-top: var(--space-1);
  font-family: var(--font-body);
  font-style: normal;
  font-size: var(--type-xs);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-ink-faint);
}

/* ============================== MILE 1 → MILE 2 CONNECTOR ============================== */
/* Purely decorative seam between Mile 1 (centered, research/stats) and
   Mile 2 (left-aligned, honorees) — a plain sibling of the <section>s, not
   wrapped in .container, so it spans the full page width with no
   breakout hack. Same --color-bg-alt as both sections on either side of
   it, so the background stays unbroken across the whole seam. Two
   outer "drifting" shapes (dimmed) fill the wide band either side of the
   central photo cluster so the composition still reads as full-width on
   large screens instead of one island floating in empty gray.

   Interactive: js/main.js tracks mousemove over .mile-connector and writes
   a --hx/--hy offset (in px) onto each [data-parallax] element, scaled by
   its own data-parallax strength — smaller/lighter elements (the inner
   shapes, the outer drifters) carry a higher strength and drift farther
   than the two photos, so the whole cluster reads as layered depth
   following the cursor rather than one flat unit. Each element's own
   static transform (a rotate, or the centering translate) is combined
   with translate(var(--hx),var(--hy)) rather than replaced by it. */
.mile-connector {
  position: relative;
  width: 100%;
  height: clamp(360px, 78vw, 1080px);
  overflow: hidden;
  background: var(--color-bg-alt);
}

.mile-connector-shape {
  position: absolute;
  pointer-events: none;
  transition: transform 0.35s cubic-bezier(0.2, 0.8, 0.2, 1);
}

.mile-connector-shape--outer-left {
  width: 275px;
  height: 275px;
  border-radius: 50%;
  border: 45px solid var(--color-accent-light);
  opacity: 0.6;
  top: 18%;
  left: 7%;
  transform: translate(var(--hx, 0px), var(--hy, 0px));
}
.mile-connector-shape--outer-right {
  width: 115px;
  height: 400px;
  border-radius: 999px;
  background: var(--color-primary-light);
  opacity: 0.55;
  bottom: 10%;
  right: 9%;
  transform: translate(var(--hx, 0px), var(--hy, 0px)) rotate(-20deg);
}

/* Leaned slightly left of center (38% instead of 50%) rather than dead
   centered in the full-bleed band. */
.mile-connector-photos {
  position: absolute;
  top: 50%;
  left: 38%;
  width: clamp(300px, 70vw, 950px);
  aspect-ratio: 1;
  transform: translate(-50%, -50%) translate(var(--hx, 0px), var(--hy, 0px));
  transition: transform 0.35s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.mile-connector-photo {
  position: absolute;
  border-radius: 50%;
  overflow: hidden;
  box-shadow: 0 40px 80px -20px rgba(31, 42, 51, 0.35);
  transition: transform 0.35s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.mile-connector-photo img { width: 100%; height: 100%; object-fit: cover; display: block; cursor: pointer; }

/* Both photos sit inside a "tag host" instead of being positioned directly:
   the host carries the position/size/parallax the photo used to carry
   itself, and the circular photo just fills it exactly (inset:0). That
   split matters because the photo tag indicators below need to extend PAST
   the circle's own edge — if they lived inside the circle, its own
   overflow:hidden would clip them off. The host has no overflow rule, so
   indicators can render outside the circle while the photo itself still
   clips to a circle same as before. */
.mile-connector-photo-tags {
  position: absolute;
  transform: translate(var(--hx, 0px), var(--hy, 0px));
  /* No transition here on purpose while actively tracking the cursor (see
     js/main.js's mousemove handler) — the bubble should follow the pointer
     1:1 with no lag. .is-settling below (added only once the pointer
     leaves) is what gives the drift BACK to center its own slower, eased
     motion instead of inheriting this instant tracking. */
}
.mile-connector-photo-tags.is-settling {
  transition: transform 1.1s cubic-bezier(0.16, 1, 0.3, 1); /* slow, soft settle — reads as the bubble drifting to rest rather than snapping back the instant hover is lost */
}
.mile-connector-photo-tags .mile-connector-photo { position: absolute; inset: 0; transition: none; }
.mile-connector-photo-tags--lg { top: 0; left: 0; width: 76%; height: 76%; z-index: 1; }
.mile-connector-photo-tags--sm { bottom: 0; right: 0; width: 52%; height: 52%; z-index: 2; }
.mile-connector-photo-tags--sm .mile-connector-photo { border: 15px solid var(--color-bg-alt); }

/* ---------- Photo face tags ---------- */
.photo-tag {
  position: absolute;
  z-index: 4;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  border: 2px solid rgba(255,255,255,0.75);
  background: rgba(255,255,255,0.16);
  backdrop-filter: blur(1.5px);
  transform: translate(-50%, -50%);
  padding: 0;
  cursor: pointer;
  transition: transform var(--duration-fast) var(--ease-out), background-color var(--duration-fast) var(--ease-out), border-color var(--duration-fast) var(--ease-out);
}
.photo-tag::after {
  content: '';
  position: absolute;
  inset: 6px;
  border-radius: 50%;
  background: rgba(255,255,255,0.85);
}
/* Pulsing ping so the tags read as clickable instead of blending into the
   photo — a second pseudo-element (::after above is already the dot's
   solid core) expanding out from the dot and fading, on a continuous loop.
   Same technique as .mile-stop-dot's "you are here" ping, just its own
   unscoped keyframe since that one only exists inside the desktop-only
   mile-sidebar media query. */
.photo-tag::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: rgba(255,255,255,0.7);
  animation: photoTagPing 2s cubic-bezier(0, 0, 0.2, 1) infinite;
  pointer-events: none;
}
.photo-tag:hover, .photo-tag:focus-visible { background: rgba(255,255,255,0.22); border-color: rgba(255,255,255,0.85); transform: translate(-50%, -50%) scale(1.15); }
.photo-tag.is-active::before { animation-play-state: paused; opacity: 0; }

@keyframes photoTagPing {
  0% { transform: scale(1); opacity: 0.7; }
  100% { transform: scale(2.4); opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .photo-tag::before { animation: none; opacity: 0; }
}
.photo-tag.is-active { background: var(--color-accent); border-color: var(--color-white); }

.photo-tag-indicator {
  position: absolute;
  z-index: 5;
  opacity: 0;
  transform: scale(0.92);
  pointer-events: none;
  transition: opacity var(--duration-base) var(--ease-out), transform var(--duration-base) var(--ease-out);
}
.photo-tag-indicator.is-active { opacity: 1; transform: scale(1); }

.photo-tag-line { position: absolute; background: var(--color-white); box-shadow: 0 1px 6px rgba(0,0,0,0.4); }
.photo-tag-name {
  position: absolute;
  white-space: nowrap;
  background: var(--color-white);
  color: var(--color-ink);
  font-family: var(--font-body);
  font-size: var(--type-xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  padding: 0.35rem 0.75rem;
  border-radius: 999px;
  box-shadow: 0 4px 14px rgba(0,0,0,0.25);
}

/* Each direction just says which way the line/label extend from the tag's
   exact point — the indicator element itself has no width/height of its
   own, so left:0/right:0/top:0/bottom:0 on its children all resolve back
   to that same point, which is what lets one rule set work for any tag
   regardless of which direction it points. */
.photo-tag-indicator[data-dir="right"] .photo-tag-line { top: -1px; left: 0; width: 40px; height: 2px; }
.photo-tag-indicator[data-dir="right"] .photo-tag-name { top: 0; left: 46px; transform: translateY(-50%); }

.photo-tag-indicator[data-dir="left"] .photo-tag-line { top: -1px; right: 0; width: 40px; height: 2px; }
.photo-tag-indicator[data-dir="left"] .photo-tag-name { top: 0; right: 46px; transform: translateY(-50%); }

.photo-tag-indicator[data-dir="up"] .photo-tag-line { bottom: 0; left: -1px; width: 2px; height: 46px; }
.photo-tag-indicator[data-dir="up"] .photo-tag-name { bottom: 52px; left: 0; transform: translateX(-50%); }

.mile-connector-ring {
  width: 160px;
  height: 160px;
  border-radius: 50%;
  border: 40px solid var(--color-accent);
  top: -35px;
  left: -35px;
  transform: translate(var(--hx, 0px), var(--hy, 0px));
}
.mile-connector-pill {
  width: 80px;
  height: 260px;
  border-radius: 999px;
  background: var(--color-accent-dark);
  top: 8%;
  right: -45px;
  transform: translate(var(--hx, 0px), var(--hy, 0px)) rotate(24deg);
}
.mile-connector-dot {
  width: 95px;
  height: 95px;
  border-radius: 50%;
  background: var(--color-primary-light);
  bottom: -20px;
  left: 10%;
  transform: translate(var(--hx, 0px), var(--hy, 0px));
}

/* Wraps the shapes/photos above at inset:0 — same positioned box the
   outer .mile-connector used to be directly, so none of their existing
   top/left/right percentages needed to change when this wrapper was
   introduced (it's exactly the same size as .mile-connector itself). */
.mile-connector-decoration { position: absolute; inset: 0; }

/* The "bubble box" — a real, accessible card floating over the decorative
   composition rather than another circle (text doesn't fit a circle), but
   keeps the rounded, floating-shadow language of the photo bubbles next to
   it. Wider than its original cramped 320px so the fuller "Meet the Team"
   copy (heading, paragraph, quote, both links — this card IS Mile 2, see
   id="mile-2" in the markup) actually fits. max-height + overflow-y is a
   safety net, not the normal case: .mile-connector is tall enough at
   realistic viewport widths that this scrollbar should never actually
   appear, it just guards against clipping if it ever runs short. */
.mile-connector-team {
  position: absolute;
  top: 50%;
  right: clamp(20px, 6%, 80px);
  transform: translateY(-50%);
  z-index: 3;
  /* This card carries id="mile-3", so it is what the Mile 3 sidebar dot and
     the footer's Mile 3 link scroll to. Without this the fixed header sat on
     top of it on arrival. */
  scroll-margin-top: var(--header-height);
  width: clamp(300px, 30vw, 420px);
  max-height: calc(100% - var(--space-5));
  overflow-y: auto;
  background: var(--color-white);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: 0 30px 60px -24px rgba(31, 42, 51, 0.3);
  padding: var(--space-4);
}
/* Smaller than the var(--type-5xl) used in a full-width Mile section — this
   card is far narrower, so the numeral is scaled down to match rather than
   dominating the box. */
.mile-connector-team .mile-number { font-size: var(--type-4xl); margin-bottom: 0.2rem; }
/* Was --type-lg (1.375rem) — barely bigger than the card's own body copy,
   so the headline read as an enlarged paragraph rather than a headline
   next to the --type-4xl numeral right above it, a much flatter hierarchy
   than every other Mile section's poster-scale --type-3xl h2 gets. Bumped
   one step to --type-xl rather than matching --type-3xl outright — this
   card is genuinely narrow (300-420px, see .mile-connector-team above),
   and the card's own max-height + overflow-y:auto safety valve is there
   for exactly this kind of near-miss, not something to lean on by request. */
.mile-connector-team h2 { font-size: var(--type-xl); line-height: 1.2; margin-bottom: var(--space-2); }
.mile-connector-team p { font-size: var(--type-sm); color: var(--color-ink-soft); }
.mile-connector-team .pull-quote { font-size: var(--type-sm); margin: var(--space-2) 0 var(--space-3); }

/* Stacks (photos, then the card underneath) up to 959px, not just on
   phones: between 701px and ~950px the side-by-side card is only ~300px
   wide and taller than the band it floats in, so on an iPad it scrolled
   inside itself and cut its last lines off. */
@media (max-width: 959px) {
  .mile-connector {
    height: auto;
    overflow: visible;
    padding-bottom: var(--space-5);
  }
  .mile-connector-decoration { position: relative; height: 480px; overflow: hidden; }
  .mile-connector-photos { width: min(78vw, 420px); }
  .mile-connector-shape--outer-left,
  .mile-connector-shape--outer-right { display: none; }
  .mile-connector-team {
    position: relative;
    top: auto; right: auto; transform: none;
    width: min(90vw, 520px);
    max-height: none;
    overflow-y: visible;
    margin: var(--space-4) auto 0;
  }
}

/* ============================== HONOREE CARDS (Mile 1) ============================== */
.honoree-grid {
  display: grid;
  gap: var(--space-4);
  grid-template-columns: 1fr;
  margin-top: var(--space-5);
}

@media (min-width: 700px) {
  .honoree-grid { grid-template-columns: repeat(3, 1fr); }
}

.honoree-card {
  background: var(--color-white);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  transition: transform var(--duration-base) var(--ease-out), box-shadow var(--duration-base) var(--ease-out);
}
.honoree-card:hover {
  transform: translateY(-4px);
  box-shadow: 0 20px 40px -20px rgba(31, 42, 51, 0.28);
}

.honoree-card .media-placeholder { aspect-ratio: 4 / 4; border-radius: 0; }

.honoree-body { padding: var(--space-3); }
.honoree-name { font-size: var(--type-md); margin-bottom: 0.2rem; }
.honoree-role {
  display: block;
  font-size: var(--type-xs);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-accent-dark);
  margin-bottom: var(--space-2);
  /* Rob Shein's two-part role runs long; balance its lines so it never
     leaves a single word stranded on the second. */
  text-wrap: balance;
}
.honoree-body p { font-size: var(--type-sm); color: var(--color-ink-soft); margin-bottom: var(--space-1); }
.honoree-link {
  display: inline-flex;
  align-items: center;
  min-height: 36px;
  padding: 0.5rem 0.9rem;
  font-size: var(--type-xs);
  font-weight: 700;
  color: var(--color-accent-dark);
  text-decoration: none;
  border: 1.5px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-white);
  transition: border-color var(--duration-fast) var(--ease-out),
              background-color var(--duration-fast) var(--ease-out),
              transform var(--duration-fast) var(--ease-out);
}
.honoree-link:hover, .honoree-link:focus-visible {
  border-color: var(--color-accent-dark);
  background: var(--color-accent-tint);
  transform: translateY(-2px);
}

/* Editorial variant — drops the boxed card chrome (bg/border/hover-lift) for
   a magazine-portrait feel, and offsets the middle card down a notch so the
   row isn't three identical, perfectly-aligned tiles. Offsetting the middle
   one (not the first or last) avoids implying any one honoree matters more. */
.honoree-grid--editorial { margin-top: var(--space-6); }
.honoree-grid--editorial .honoree-card {
  background: none;
  border: none;
  border-radius: 0;
}
.honoree-grid--editorial .honoree-card:hover { transform: none; box-shadow: none; }
.honoree-grid--editorial .honoree-card .media-placeholder {
  aspect-ratio: 3 / 4;
  border-radius: var(--radius-lg);
}
.honoree-grid--editorial .honoree-body { padding: var(--space-3) var(--space-1) 0; }
.honoree-grid--editorial .honoree-name { font-size: var(--type-xl); margin-bottom: 0.3rem; }
@media (min-width: 700px) {
  .honoree-grid--editorial .honoree-card:nth-child(2) { margin-top: var(--space-6); }
}

/* Widened past the shared .container's 1180px reading-width cap — three
   portraits confined to that column read as a tight, narrow strip with a
   lot of unused page on either side at desktop widths. This is scoped to
   the section that HOLDS the grid (rather than the grid itself) so the
   heading/dedication-banner above and below widen along with it and the
   whole section reads as one consistent column, not a wide row sandwiched
   between two narrower ones. */
#mile-2 .container { max-width: clamp(1180px, 94vw, 1600px); }
@media (min-width: 700px) {
  .honoree-grid--editorial { gap: var(--space-6); } /* more breathing room between portraits now that they have room to spread into */
}

/* ============================== STATS (Mile 1) ============================== */
.stat-grid {
  display: grid;
  gap: var(--space-3);
  grid-template-columns: repeat(2, 1fr);
  margin-block: var(--space-5);
}

@media (min-width: 700px) {
  .stat-grid { grid-template-columns: repeat(3, 1fr); }
}

.stat-card {
  background: var(--color-white);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-3);
  transition: transform var(--duration-base) var(--ease-out), box-shadow var(--duration-base) var(--ease-out);
}
.stat-card:hover {
  transform: translateY(-3px);
  box-shadow: 0 16px 32px -18px rgba(31, 42, 51, 0.25);
}

.stat-number {
  font-family: var(--font-display);
  font-size: var(--type-2xl);
  color: var(--color-primary-dark);
  display: block;
  margin-bottom: 0.3rem;
}

.stat-label { font-size: var(--type-xs); color: var(--color-ink-soft); line-height: 1.4; }

/* Hero variant — these three figures ARE the section (no photo exists for
   Mile 1 to lean on), so they drop the card chrome entirely and scale up to
   poster size instead of sitting in modest bordered boxes. Centered column
   layout (rather than the old left-rule editorial-block style) so the
   bubble + label stack centers as a unit, matching #mile-1's centered theme
   below — a left-only border/padding would sit off-center against
   centered content. */
.stat-grid--hero { gap: var(--space-5); }
@media (min-width: 700px) { .stat-grid--hero { gap: var(--space-4); } }
.stat-card--hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 0;
  border: none;
  background: none;
  border-radius: 0;
}
.stat-card--hero:hover { transform: none; box-shadow: none; }

/* Rounded "bubble" behind each figure — sized to the number itself (width:
   fit-content, not full card width) rather than stretching edge to edge, so
   it reads as its own pill-ish chip sitting above the label. display:flex
   (not inline-flex) is required despite that shrink-to-fit sizing:
   inline-flex would let .stat-label below flow up onto the same line as the
   bubble instead of wrapping to its own line underneath, since only
   block-level boxes force a line break before the next sibling — centering
   is handled by .stat-card--hero's align-items above instead of relying on
   margin-inline:auto here. Font-size dropped from the old --type-5xl (up to
   8.5rem — poster scale, but too wide for "15–17 mo." to ever fit on one
   line even as a single number) up a step from the --type-3xl this landed
   on last round to --type-4xl now that BOTH "15" and "17" roll as separate
   tabular-nums spans — still pinned to nowrap so the whole figure stays on
   one line inside the bubble. */
/* Small mark above the bubble, one per stat — a light touch of hierarchy
   distinguishing three otherwise-identical number/label stacks at a
   glance, before you've even read the label underneath. Muted (not the
   full accent color the bubble itself uses) so it reads as a supporting
   detail, not a fourth thing competing with the number for attention. */
.stat-icon { width: 22px; height: 22px; color: var(--color-accent); opacity: 0.7; margin-bottom: var(--space-2); }

.stat-bubble {
  display: flex;
  width: fit-content;
  align-items: center;
  justify-content: center;
  background: var(--color-accent-tint);
  border-radius: var(--radius-lg);
  padding: 0.3em 0.75em;
  margin-bottom: var(--space-2);
  box-shadow: 0 10px 24px -14px rgba(31, 42, 51, 0.35);
}
.stat-card--hero .stat-number {
  font-size: var(--type-4xl);
  line-height: 1.1;
  color: var(--color-ink);
  white-space: nowrap;
  margin-bottom: 0;
}
.stat-roll { font-variant-numeric: tabular-nums; } /* holds digit width steady while it's flashing through random values */
.stat-card--hero .stat-label { font-size: var(--type-sm); max-width: 20ch; text-align: center; }

.resource-list { list-style: none; margin: var(--space-3) 0 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-1); }
.resource-list a {
  display: inline-flex; align-items: center; gap: 0.5rem;
  text-decoration: none; font-weight: 600; font-size: var(--type-sm);
  color: var(--color-primary-dark);
  border-bottom: 1px solid transparent;
  transition: border-color var(--duration-fast) var(--ease-out);
}
.resource-list a:hover { border-color: var(--color-primary-dark); }
.resource-list svg { width: 16px; height: 16px; }

/* Mile 1's own theme is centered rather than the left-aligned editorial
   layout the other mile sections use — text-align:center cascades to every
   inline-level child (eyebrow, h2/p text, the Learn More button), but the
   handful of block-level children with their own width constraint
   (.section-heading itself, its h2, and .resource-list) need their own
   margin-inline:auto too, since text-align only centers inline content
   *within* a box, not the box itself.

   The number+eyebrow lead-in ("01 / Mile 1 — The Research") centers along
   with everything else here, unlike every other mile section where it sits
   at the left edge — a deliberate one-off so Mile 1 reads as fully
   centered rather than mixing a left-pinned badge with centered content
   below it. */
#mile-1 .container { text-align: center; }
#mile-1 .section-heading { margin-inline: auto; }
#mile-1 .section-heading h2 { margin-inline: auto; }
#mile-1 .section-heading--lead { margin-bottom: var(--space-4); }
#mile-1 .btn-glow { margin-block: var(--space-2); }
/* .doctor-callout is the last thing in Mile 1 — its own margin-bottom plus
   .mile-section's padding-bottom below was stacking into a much bigger gap
   before Mile 2 starts than anything else on the page gets, so both are
   trimmed here specifically for this section (trimmed several times now).
   Mile 2's own padding-top (inherited from .mile-section, 96px) was still
   the larger half of that gap even after Mile 1's side was trimmed, so it
   gets cut down here too rather than only ever trimming Mile 1's side.

   Every OTHER gap on the way down to the callout gets the same treatment
   here for the same reason — the stat grid's own margin-block, the
   "Glioblastoma is..." paragraph's trailing margin, and the callout's own
   margin-top + padding all independently add a little space that's easy
   to individually justify but stacks into most of a screen's worth of
   nothing-happening scroll between Mile 1's headline figures and Mile 2
   starting. None of the actual CONTENT between them (the heading, the
   button, the callout) goes away — this only tightens the air around it. */
#mile-1 .stat-grid--hero { margin-block: var(--space-3); }
#mile-1 .section-heading:not(.section-heading--lead) p { margin-bottom: var(--space-1); }
#mile-1 { padding-bottom: 0; }
.doctor-callout { margin-top: var(--space-2); padding: var(--space-3); }
#mile-2 { padding-top: 0; }

/* Below 1024px, .mile-section's own padding-top is a calc() that clears the
   fixed header + the sticky mobile mile-tracker pill, plus a var(--space-3)
   breathing-room buffer on top of that strict minimum (see its rule and
   comment near MILE SECTIONS). That buffer is the only genuinely trimmable
   part without risking content actually starting underneath either fixed
   bar — so it's dropped to zero here for Mile 1 (top-of-page gap) and Mile 2
   (the same buffer was still baked into the Mile 1 → Mile 2 gap above,
   even after trimming Mile 1's own side of it). Both need the #id
   specificity bump since .mile-section's version has an equal-or-later
   media query of its own. */
@media (max-width: 1023px) {
  #mile-1, #mile-2 {
    padding-top: calc(var(--header-height) + var(--mile-tracker-height));
  }
}

/* Sized up a deliberate notch past a standard callout card — Dr. Wong is
   the SOLE researcher this event funds, so the one box representing him
   and his work carries more visual weight than a compact aside, closer to
   the honoree cards' presence than a footnote. */
.doctor-callout {
  max-width: 40rem;
  margin: var(--space-4) auto 0;
  padding: var(--space-5);
  background: var(--color-white);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
}
.doctor-callout-photo { width: 136px; height: 136px; aspect-ratio: 1 / 1; border-radius: 50%; padding: 0; margin: 0 auto var(--space-3); }
.doctor-callout-photo img { cursor: pointer; }
.doctor-callout .eyebrow { justify-content: center; }
.doctor-callout h3 { font-size: var(--type-xl); margin-bottom: var(--space-2); }
.doctor-callout p { font-size: var(--type-base); color: var(--color-ink-soft); margin-bottom: 0; }
.doctor-callout-link { margin-top: var(--space-3); }

/* ============================== MILE 4 / FINISH ============================== */
.finish-banner {
  background: var(--color-ink);
  color: var(--color-white);
  border-radius: var(--radius-lg);
  padding: var(--space-7) var(--space-4);
  text-align: center;
  margin-bottom: var(--space-6);
}
/* This is the site's second hero moment — the final ask, after everything
   else has made the case — so it gets the same poster scale as the actual
   hero h1 rather than the shared --type-3xl every other section heading uses. */
.finish-banner h2 { color: var(--color-white); font-size: var(--type-4xl); max-width: 16ch; margin-inline: auto; }
.finish-banner p { color: rgba(255,255,255,0.85); max-width: 46rem; margin-inline: auto; }
.finish-banner .hero-actions { justify-content: center; margin-top: var(--space-4); }

/* Deliberately NOT .mile-section (which forces min-height: 100svh — a
   full-screen-tall section for what's meant to be a quick snapshot band
   would read as a mostly-empty screen). Compact padding only. */
.event-snapshot-section { padding-block: var(--space-6); }

.event-snapshot-grid {
  display: grid;
  gap: var(--space-3);
  grid-template-columns: 1fr;
  align-items: stretch;
}
@media (min-width: 700px) {
  .event-snapshot-grid { grid-template-columns: repeat(2, 1fr); }
}

/* Grid item wrapper so the "Join the Fight" label stacks above the card
   itself instead of becoming its own grid column. */
.location-card-col { display: flex; flex-direction: column; }

/* ============================== LOCATION CARD ==============================
   Vanilla HTML/CSS/JS reimplementation of a React/framer-motion component
   design (see the comment above the markup in index.html for why this
   isn't literally React) — a photo, a place name, and an icon button that
   nudges toward its destination on hover.

   ---------- 3D tilt ----------
   The whole card (photo, place name, directions button — everything, as
   one rigid block) tilts toward the cursor while hovering, tracking it
   continuously as it moves rather than just reacting once on
   enter/leave. --tilt-x/--tilt-y/--tilt-scale are written every animation
   frame from JS (see the venueTilt block in main.js) via a lerp-toward-
   target loop — the same "accumulate a target, ease the displayed value
   toward it each frame" approach used elsewhere in this file for the hero
   ring and scroll glide — rather than a CSS transition, because a
   transition retargeted on every single pointermove fights itself
   instead of producing one continuous follow. perspective() is inlined
   into the transform itself (rather than set as a separate `perspective`
   property one level up) so no extra wrapper element is needed — this
   card's own border-radius + overflow:hidden still clip the rotated
   result to its rounded corners exactly as before. */
.location-card {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border-radius: var(--radius-md);
  border: 1px solid var(--color-border);
  background: var(--color-white);
  transform: perspective(1000px) rotateX(var(--tilt-x, 0deg)) rotateY(var(--tilt-y, 0deg)) scale(var(--tilt-scale, 1));
  transition: box-shadow var(--duration-base) var(--ease-out);
}
.location-card.is-tilting {
  box-shadow: 0 24px 44px -18px rgba(31, 42, 51, 0.45);
}
.location-card-image {
  aspect-ratio: 4 / 3;
  border-radius: 0; /* card's own border-radius + overflow:hidden clip this instead */
}
@media (prefers-reduced-motion: reduce) {
  .location-card { transform: none !important; }
}
.location-card-body {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  padding: var(--space-3);
}
.location-card-title { font-size: var(--type-base); margin-bottom: 0.15rem; }
.location-card-subtitle { font-size: var(--type-sm); color: var(--color-ink-soft); margin-bottom: 0; }

/* Icon-only "navigate" button — a paper-airplane/compass-needle dart,
   the universal wayfinding-app glyph, standing in for the old text
   "Directions" label. Nudges up-right on hover, echoing the direction
   the icon itself points. */
.location-card-directions {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: var(--color-accent);
  color: var(--color-white);
  transition: background-color var(--duration-fast) var(--ease-out), transform var(--duration-fast) var(--ease-out);
}
.location-card-directions:hover,
.location-card-directions:focus-visible {
  background: var(--color-accent-dark);
}
.location-card-directions:active { transform: scale(0.92); }
.location-card-directions svg {
  width: 18px;
  height: 18px;
  transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.location-card-directions:hover svg,
.location-card-directions:focus-visible svg {
  transform: translate(2px, -2px);
}

/* ============================== EVENT FACTS CARD ==============================
   The snapshot's other half — a plain Who/What/When/Where reference list,
   dark to pair visually with the light .location-card next to it. */
.event-facts-card {
  background: var(--color-ink);
  color: var(--color-white);
  border-radius: var(--radius-md);
  padding: var(--space-4);
}
.event-facts-list { display: flex; flex-direction: column; gap: var(--space-2); }
.event-facts-actions { margin-top: var(--space-4); }
.event-facts-register { width: 100%; }
.event-facts-details-link {
  display: block;
  text-align: center;
  margin-top: var(--space-2);
  font-size: var(--type-sm);
  font-weight: 600;
  color: rgba(255, 255, 255, 0.7);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: color var(--duration-fast) var(--ease-out), border-color var(--duration-fast) var(--ease-out);
}
.event-facts-details-link:hover, .event-facts-details-link:focus-visible {
  color: var(--color-white);
  border-color: rgba(255, 255, 255, 0.7);
}
.event-facts-item {
  display: grid;
  grid-template-columns: 5.5ch 1fr;
  gap: var(--space-2);
  padding-block: var(--space-1);
  border-top: 1px solid rgba(255, 255, 255, 0.14);
}
.event-facts-item:first-child { border-top: none; padding-top: 0; }
.event-facts-item dt {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--type-sm);
  color: var(--color-accent-light);
}
.event-facts-item dd {
  margin: 0;
  font-size: var(--type-sm);
  color: rgba(255, 255, 255, 0.85);
}

@media (prefers-reduced-motion: reduce) {
  .location-card, .location-card-directions svg {
    transition: none;
  }
}

/* ============================== EVENT DETAILS PAGE ==============================
   Compact, lateral layout for stories/event-details.html. The venue photo and a
   short facts list sit side by side; the "more ways in" tiles run in one row;
   the vertical rhythm is tightened throughout so the whole page lands close to a
   single screen instead of scrolling through repeated prose. */
/* The header on this page is position:relative, so .story-page's usual
   header-height top offset is dead space — reclaim it here, and don't leave a
   full --space-8 tail below a page this short. */
.story-page.ed-page { padding-block: var(--space-4) var(--space-5); }

/* ============================== FINISH LINE — THREE WAYS IN ==============================
   Row of tall photo-link cards in the #mile-4 section, styled after the
   Radiating Hope "programs" row the project owner shared: a full-bleed
   image, the title top-left in plain white sans, and a frosted-glass "→"
   bar pinned along the bottom. No description line. Register / Photo
   Gallery / Volunteer (Event Details moved to the Events nav menu). No
   z-index anywhere — img, scrim, label, cta paint in DOM order, last on
   top. */
.finish-links {
  display: grid;
  gap: var(--space-3);
  grid-template-columns: 1fr;
}
@media (min-width: 760px) {
  .finish-links { grid-template-columns: repeat(3, 1fr); }
}

.finish-link-card {
  position: relative;
  display: block;
  overflow: hidden;
  aspect-ratio: 3 / 4;
  border-radius: var(--radius-lg);
  box-shadow: 0 20px 40px -22px rgba(31, 42, 51, 0.34);
  text-decoration: none;
  transition: transform var(--duration-base) var(--ease-out), box-shadow var(--duration-base) var(--ease-out);
}
.finish-link-card:hover,
.finish-link-card:focus-visible {
  transform: translateY(-4px);
  box-shadow: 0 30px 54px -22px rgba(31, 42, 51, 0.44);
}
.finish-link-card:focus-visible {
  outline: 2px solid var(--color-accent-dark);
  outline-offset: 3px;
}

.finish-link-card-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--duration-slow) var(--ease-out);
}
.finish-link-card:hover .finish-link-card-img,
.finish-link-card:focus-visible .finish-link-card-img { transform: scale(1.04); }

/* Soft darkening at the very top (for the title) and a touch at the bottom
   (so the frosted bar's edge doesn't float on a bright patch) — the middle
   of the photo stays clear, like the reference. */
.finish-link-card-scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom,
    rgba(14, 19, 23, 0.5) 0%,
    rgba(14, 19, 23, 0.16) 24%,
    rgba(14, 19, 23, 0) 44%,
    rgba(14, 19, 23, 0) 58%,
    rgba(14, 19, 23, 0.44) 100%);
}

.finish-link-card-label {
  position: absolute;
  top: 0; left: 0; right: 0;
  padding: var(--space-4);
  font-family: var(--font-body);
  font-weight: 600;
  font-size: var(--type-xl);
  line-height: 1.15;
  color: var(--color-white);
}

/* Barely-there frosted "→" bar, inset from the card edges and pinned to the
   bottom — the blur does most of the work; the tint is just a whisper, and
   a soft text-shadow keeps the label legible where the photo runs bright. */
.finish-link-card-cta {
  position: absolute;
  left: var(--space-3);
  right: var(--space-3);
  bottom: var(--space-3);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  padding: 0.9rem 1.15rem;
  border-radius: var(--radius-md);
  background: rgba(16, 20, 24, 0.18);
  backdrop-filter: blur(14px) saturate(1.1);
  -webkit-backdrop-filter: blur(14px) saturate(1.1);
  border: 1px solid rgba(255, 255, 255, 0.14);
  color: var(--color-white);
  font-size: var(--type-sm);
  font-weight: 500;
  letter-spacing: 0.01em;
  text-shadow: 0 1px 10px rgba(0, 0, 0, 0.45);
  transition: background-color var(--duration-fast) var(--ease-out);
}
.finish-link-card:hover .finish-link-card-cta,
.finish-link-card:focus-visible .finish-link-card-cta {
  background: rgba(16, 20, 24, 0.32);
}
.finish-link-card-cta svg {
  flex-shrink: 0;
  transition: transform var(--duration-fast) var(--ease-out);
}
.finish-link-card:hover .finish-link-card-cta svg,
.finish-link-card:focus-visible .finish-link-card-cta svg { transform: translateX(3px); }

@media (prefers-reduced-motion: reduce) {
  .finish-link-card,
  .finish-link-card-img,
  .finish-link-card-cta,
  .finish-link-card-cta svg { transition: none; }
  .finish-link-card:hover,
  .finish-link-card:focus-visible { transform: none; }
  .finish-link-card:hover .finish-link-card-img,
  .finish-link-card:focus-visible .finish-link-card-img { transform: none; }
}

/* ---- #mile-4 layout: centred eyebrow chip, then the row of cards ----
   The h2 was removed at the project owner's request; the section leads with
   just the chip (centred), then the cards. It breaks past the shared
   .container's 1180px reading cap (same move as #mile-2's portrait grid) so
   the row spreads across the window.
     This is a compact CTA row, not a full narrative "mile", so it drops the
   100svh floor and the big --space-7 top pad that other miles carry — that
   pad plus the aid station's own trailing space left a huge gap above the
   flag. */
#mile-4 {
  min-height: 0;
  padding-block: var(--space-3) var(--space-6);
}
@media (max-width: 1023px) {
  /* keep clear of the fixed header + sticky mobile mile-tracker */
  #mile-4 { padding-top: calc(var(--header-height) + var(--mile-tracker-height) + var(--space-3)); }
}
#mile-4 .container { max-width: clamp(1180px, 95vw, 1680px); }

/* The eyebrow chip sits on a checkered band in the logo blue
   (--color-accent-light, #A9C4E7) and white — a race finish flag. It's a
   swallowtail banner: full-height in the middle behind the chip, and each
   end forks into two triangular tabs with a V "valley" notched between
   them. Those forked ends flutter on a slow loop (flagEndsWaver keyframes
   an animated clip-path; the middle top/bottom edge stays flat).
     Two stacked copies of the same checker: ::before is the base; ::after
   is a slightly compressed copy shown only through a soft ringed disc that
   follows the cursor while #finishFlag is hovered (js/main.js sets --px and
   eases --press 0→1) — a fingertip pressed into fabric, not a distortion.
   Dead still in the middle when the pointer is away. */
#mile-4 .section-heading {
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 64px;
  max-width: none;
  margin-bottom: var(--space-5);
  isolation: isolate;
}
#mile-4 .section-heading::before,
#mile-4 .section-heading::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 90px;
  transform: translateY(-50%);
  z-index: -1;
  background-image: repeating-conic-gradient(var(--color-accent-light) 0% 25%, #ffffff 0% 50%);
  background-size: 48px 48px;
  background-position: center;
  /* swallowtail: two triangular tabs at each end with a V notched between
     them (static fallback / ::after silhouette) */
  clip-path: polygon(0% 0%, 15% 0%, 85% 0%, 100% 0%, 92% 50%, 100% 100%, 85% 100%, 15% 100%, 0% 100%, 8% 50%);
}
#mile-4 .section-heading::before {
  animation: flagEndsWaver 6s ease-in-out infinite;
}
@keyframes flagEndsWaver {
  0%, 100% { clip-path: polygon(0% 1%, 15% 0%, 85% 0%, 100% 7%, 92% 47%, 100% 93%, 85% 100%, 15% 100%, 0% 99%, 8% 53%); }
  40%      { clip-path: polygon(0% 8%, 15% 0%, 85% 0%, 100% 1%, 92% 54%, 100% 99%, 85% 100%, 15% 100%, 0% 92%, 8% 46%); }
  72%      { clip-path: polygon(0% 3%, 15% 0%, 85% 0%, 100% 5%, 92% 49%, 100% 95%, 85% 100%, 15% 100%, 0% 97%, 8% 52%); }
}
#mile-4 .section-heading::after {
  transform: translateY(-50%) scale(0.9);
  transform-origin: var(--px, 50%) center;
  opacity: var(--press, 0);
  -webkit-mask-image: radial-gradient(circle 108px at var(--px, 50%) 50%,
      #000 22%, rgba(0, 0, 0, 0.36) 46%, rgba(0, 0, 0, 0.58) 60%, transparent 82%);
          mask-image: radial-gradient(circle 108px at var(--px, 50%) 50%,
      #000 22%, rgba(0, 0, 0, 0.36) 46%, rgba(0, 0, 0, 0.58) 60%, transparent 82%);
}
@media (prefers-reduced-motion: reduce) {
  #mile-4 .section-heading::before { animation: none; }
  #mile-4 .section-heading::after { display: none; }
}
#mile-4 .section-heading .eyebrow {
  position: relative;
  margin-bottom: 0;
  flex-shrink: 0;
  /* near-opaque here so the stark checker behind it never muddies the label */
  background: color-mix(in srgb, var(--color-bg-alt) 93%, var(--color-accent) 12%);
  border-color: color-mix(in srgb, var(--color-accent) 46%, transparent);
}

@media (min-width: 900px) {
  #mile-4 .finish-links { gap: var(--space-4); }
}

/* ---- Past Events preview (#past-events, under the Finish Line cards) ----
   One evenly sized grid of 2025 photos, so a visitor sees a dozen at once
   without going to the gallery page. Every tile is the same 3:2 box
   (object-fit crops the rest); six across on a desktop = two rows, so the
   whole thing fits on one screen. Deliberately NOT .section-heading for the
   header: inside #mile-4 that class draws the checkered finish flag. */
.past-events-preview {
  margin-top: var(--space-7);
  scroll-margin-top: calc(var(--header-height) + 24px);
}
.past-events-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}
.past-events-head h2 {
  margin-top: var(--space-2);
  font-size: clamp(1.6rem, 2.8vw, 2.25rem);
  line-height: 1.15;
}
.past-events-head p {
  max-width: 40rem;
  margin-top: var(--space-2);
  color: var(--color-ink-soft);
  font-size: var(--type-md);
}
.past-events-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  list-style: none;
}
@media (min-width: 480px)  { .past-events-grid { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 760px)  { .past-events-grid { grid-template-columns: repeat(4, 1fr); gap: var(--space-3); } }
@media (min-width: 1100px) { .past-events-grid { grid-template-columns: repeat(6, 1fr); } }
.past-events-tile {
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  border-radius: var(--radius-md);
  overflow: hidden;
  aspect-ratio: 3 / 2;
  background: var(--color-bg-alt);
  cursor: zoom-in;
  box-shadow: 0 12px 26px -18px rgba(31, 42, 51, 0.4);
}
.past-events-tile img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--duration-slow) var(--ease-out);
}
.past-events-tile:hover img,
.past-events-tile:focus-visible img { transform: scale(1.05); }
.past-events-tile:focus-visible {
  outline: 2px solid var(--color-accent-dark);
  outline-offset: 3px;
}
@media (prefers-reduced-motion: reduce) {
  .past-events-tile img { transition: none; }
  .past-events-tile:hover img,
  .past-events-tile:focus-visible img { transform: none; }
}

/* Scroll targets for the About/Events dropdown links */
.scroll-anchor { scroll-margin-top: calc(var(--header-height) + 24px); }

/* Generalized dedication (Mile 2) */
.dedication-banner {
  max-width: 46rem;
  margin: var(--space-5) auto var(--space-2);
  padding: var(--space-4);
  background: var(--color-accent-tint);
  border-left: 3px solid var(--color-accent);
  border-radius: var(--radius-md);
  text-align: center;
}
.dedication-banner p {
  font-family: var(--font-display);
  font-size: var(--type-md);
  color: var(--color-ink);
  margin: 0;
}
.dedication-banner strong { color: var(--color-accent-dark); }

.social-links { display: flex; gap: var(--space-2); margin-top: var(--space-2); }
.social-links a {
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; border-radius: 50%;
  background: var(--color-white); border: 1px solid var(--color-border);
  color: var(--color-primary-dark);
  transition: background-color var(--duration-fast) var(--ease-out);
}
.social-links a:hover { background: var(--color-primary-tint); }
.social-links svg { width: 20px; height: 20px; }

/* ============================== CLOSING — YOUR REASON ==============================
   The last section before the footer, and the only full-bleed dark band on
   the page besides the hero and the Aid Station. One sentence and two
   buttons — see the comment on the markup in index.html for why it holds
   nothing else. Registered as a header dark-zone in js/main.js
   (headerDarkZoneEls) so the wordmark flips to white over it, exactly like it
   does over those two. */
.closing-call {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  padding-block: var(--space-6);
  text-align: center;
  color: var(--color-white);
  /* Two soft blooms over flat deep ink — periwinkle from behind the line,
     lavender rising off the bottom edge into the footer. They're what keep a
     band this sparse from reading as a plain black box. */
  background:
    radial-gradient(56% 62% at 50% 34%, rgba(94, 144, 210, 0.24), transparent 70%),
    radial-gradient(90% 60% at 50% 100%, rgba(136, 116, 194, 0.14), transparent 70%),
    #1B1C1F;
}

.closing-call-inner {
  max-width: 42rem;
  margin-inline: auto;
}

/* The line the whole section exists to deliver, and now the only thing in it.
   Display face, italic, no quote marks — the page speaking in its own voice
   rather than quoting anyone. (The one real quotation on this page, the
   founders' pull-quote in Mile 3, keeps its marks and its cite for exactly
   that contrast.) No trailing margin: there is nothing under it to separate
   from, and the band's own padding is what frames it now. */
.closing-call-quote {
  max-width: 36rem;
  margin: 0 auto;
  font-family: var(--font-display);
  font-size: var(--type-lg);
  font-style: italic;
  line-height: 1.5;
  color: var(--color-accent-light);
}

@media (max-width: 640px) {
  .closing-call { padding-block: var(--space-5); }
}

/* ============================== EVENT DETAILS — THE BOARD ==============================
   This page is deliberately not a document. Everything a visitor needs on
   race day — when, where, who's allowed on the course, what there is to eat,
   and why the course is the shape it is — is laid out as one board that fits
   a desktop screen, using the lateral room the 60rem .story-container throws
   away rather than stacking into a scroll. Below 1080px wide (or under 700px
   tall, where a fitted board would have to shrink type past readability) it
   gives up and becomes an ordinary stacked page — a phone can't hold this
   much at once and pretending otherwise just produces unreadable text. */
.ed-body {
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  background: var(--color-bg-alt);
}
.ed-body > .ed-page { flex: 1 1 auto; min-height: 0; }

.ed-page {
  /* One radius for every panel on the board — generous enough to read as a
     rounded, buoyant form rather than a card with clipped corners, and scaled
     with the viewport so it stays proportional to the panels themselves. */
  --ed-radius: clamp(18px, 1.9vw, 30px);
  position: relative;
  isolation: isolate;
  overflow: hidden; /* the bubble field rides past the edges */
  display: flex;
  flex-direction: column;
  padding: clamp(var(--space-1), 1.6vh, var(--space-3)) 0 clamp(var(--space-1), 2.2vh, var(--space-4));
  /* A lit ground rather than a flat one, so the panels have something to cast
     into and float above. Three soft pools, one per corner of interest, drifting
     very slowly against each other — the page's ambient life now that there is
     no literal moving element on it. */
  background:
    radial-gradient(64% 54% at 10% 2%, rgba(169, 196, 231, 0.42), transparent 64%),
    radial-gradient(56% 48% at 94% 96%, rgba(203, 190, 232, 0.38), transparent 66%),
    radial-gradient(48% 44% at 74% 6%, rgba(218, 229, 245, 0.5), transparent 70%),
    var(--color-bg-alt);
  background-size: 160% 160%, 150% 150%, 140% 140%;
  animation: edGroundDrift 34s ease-in-out infinite alternate;
}
@keyframes edGroundDrift {
  from { background-position: 0% 0%, 100% 100%, 60% 0%; }
  to   { background-position: 14% 8%, 86% 92%, 46% 10%; }
}

/* ---------- Float in, then keep drifting ----------
   The page's whole motion idea: the panels behave like things suspended in
   water rather than boxes pinned to a page. They rise into place on load and
   then never quite settle — a slow, shallow wander that never repeats
   visibly, so the board reads as buoyant instead of animated.
   Two animations on one element, in this order on purpose. edFloatIn carries
   a panel up into place and holds its end state (fill: both). edDrift starts
   only once that has finished and, being LAST in the animation list, takes
   over `transform` from the filled entrance — the handoff is invisible
   because edDrift's 0% is exactly where edFloatIn ended. Opacity is never
   touched by edDrift, so the entrance's filled opacity:1 persists.
   ONE shared duration, staggered --alive-delay: the panels all breathe at the
   same rate but a beat apart, so the motion travels across the board as a
   single wave. That's what makes six separate panels read as one connected
   thing — random per-panel durations (an earlier attempt) just looked like
   six unrelated objects twitching. */
.ed-float {
  animation:
    edFloatIn 0.9s var(--ease-out) var(--float-delay, 0s) both,
    edDrift 9s ease-in-out calc(0.9s + var(--float-delay, 0s) + var(--alive-delay, 0s)) infinite;
}
@keyframes edFloatIn {
  from { opacity: 0; transform: translate3d(0, 42px, 0) scale(0.98); }
  to   { opacity: 1; transform: translate3d(0, 0, 0) scale(1); }
}
/* Off-centre keyframes on both axes — a straight up-and-down bob reads as a
   mechanism, a slight sideways wander reads as something afloat. */
@keyframes edDrift {
  0%, 100% { transform: translate3d(0, 0, 0); }
  30%      { transform: translate3d(3px, -8px, 0); }
  62%      { transform: translate3d(-3px, -3px, 0); }
}

/* ---------- Board frame ---------- */
.ed-board {
  width: min(1500px, 100% - var(--space-5) * 2);
  margin-inline: auto;
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: clamp(var(--space-1), 1.8vh, var(--space-3));
}

.ed-topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  flex-shrink: 0;
}
.ed-topbar .story-back { margin: 0; }
.ed-topbar-tag {
  font-size: var(--type-xs);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--color-ink-soft);
}

.ed-layout {
  flex: 1 1 auto;
  min-height: 0;
  display: grid;
  gap: clamp(var(--space-2), 1.8vw, var(--space-3));
  grid-template-columns: minmax(0, 0.92fr) minmax(0, 1.28fr);
  align-items: stretch;
}

/* ---------- The poster ---------- */
.ed-poster {
  position: relative;
  display: flex;
  flex-direction: column;
  padding: clamp(var(--space-2), 3vh, var(--space-5));
  border-radius: var(--ed-radius);
  color: var(--color-white);
  background:
    radial-gradient(80% 55% at 20% 8%, rgba(94, 144, 210, 0.42), transparent 66%),
    linear-gradient(160deg, #2E3038, #1B1C1F 62%);
  box-shadow:
    0 34px 70px -34px rgba(27, 28, 31, 0.8),
    0 6px 18px -12px rgba(27, 28, 31, 0.45);
}
.ed-poster .eyebrow { color: var(--color-accent-light); align-self: flex-start; }

.ed-date {
  margin: var(--space-2) 0 0;
  font-family: var(--font-display);
  /* vh-led so the board's biggest element is the first thing to give way on
     a short screen, before any of the copy has to. */
  font-size: clamp(2rem, 5.2vh, 3.4rem);
  line-height: 1.02;
  letter-spacing: -0.02em;
  color: var(--color-white);
}
.ed-date span {
  display: block;
  color: var(--color-accent-light);
}

/* Takes whatever vertical room the poster has left over — `flex: 1 1 0` with
   `min-height: 0`, so it grows on a tall screen and gives way completely on a
   short one. That's what lets a photo exist on a layout whose whole premise is
   fitting one screen: it can never push anything off, because it only ever
   occupies slack that was already there. (It replaces the `margin-bottom:auto`
   the date used to carry, which was absorbing that same slack as empty space.) */
.ed-photo {
  flex: 1 1 0;
  min-height: 0;
  margin: var(--space-3) 0 0;
  border-radius: calc(var(--ed-radius) * 0.6);
  overflow: hidden;
}
.ed-photo img {
  display: block;
  width: 100%;
  height: 100%;
  /* cover, so the crop follows the box as it flexes rather than the image
     letterboxing; biased low because the banner and the doorway — the parts
     worth seeing — sit in the lower half of the frame. */
  object-fit: cover;
  object-position: 50% 64%;
}

.ed-schedule {
  list-style: none;
  margin: clamp(var(--space-2), 2.6vh, var(--space-4)) 0;
  padding: 0;
  border-top: 1px solid rgba(255, 255, 255, 0.16);
}
.ed-schedule li {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-1);
  padding: clamp(0.5rem, 1.4vh, 0.85rem) 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.16);
}
.ed-time {
  font-family: var(--font-display);
  font-size: clamp(var(--type-md), 2.4vh, var(--type-lg));
  line-height: 1.15;
  color: var(--color-white);
}
.ed-what {
  font-size: var(--type-xs);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.58);
}

.ed-place-label {
  display: block;
  margin-bottom: 0.35rem;
  font-size: var(--type-xs);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--color-accent-light);
}
.ed-place-name {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(var(--type-lg), 2.9vh, var(--type-xl));
  line-height: 1.15;
  color: var(--color-white);
}
.ed-place-addr { margin: 0.2rem 0 0; font-size: var(--type-base); color: rgba(255, 255, 255, 0.85); }
.ed-place-note { margin: 0.25rem 0 0; font-size: var(--type-xs); letter-spacing: 0.06em; text-transform: uppercase; color: rgba(255, 255, 255, 0.5); }
.ed-directions {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  margin-top: var(--space-2);
  font-size: var(--type-sm);
  font-weight: 600;
  color: var(--color-accent-light);
  text-decoration: none;
}
.ed-directions:hover { color: var(--color-white); }

/* ---------- Right-hand stack ---------- */
.ed-stack {
  min-height: 0;
  display: grid;
  gap: clamp(var(--space-1), 1.8vh, var(--space-3));
  /* Two across, not three. Three columns put the body copy on a ~30-character
     measure that broke almost every sentence over four or five lines; the
     third card's content is fine print and now sits as fine print, below. */
  grid-template-columns: 1fr 1fr;
  grid-template-rows: auto 1fr auto auto;
}
.ed-card {
  position: relative;
  padding: clamp(var(--space-2), 2.4vh, var(--space-4));
  border: 1px solid rgba(255, 255, 255, 0.9);
  border-radius: var(--ed-radius);
  /* Opaque, not the translucent fill this had. Reference copy read through a
     tinted, slowly drifting ground is measurably harder work than the same
     copy on white, and this is the one page on the site that is nothing but
     reference copy. The float still reads — it comes from the shadows and the
     drift, neither of which needs the panel to be see-through. */
  background: var(--color-white);
  /* Two shadows: a wide diffuse pool that reads as height off the page, and a
     tight one that keeps the panel's own edge defined. */
  box-shadow:
    0 26px 50px -30px rgba(46, 48, 56, 0.45),
    0 4px 14px -10px rgba(46, 48, 56, 0.3);
}
/* Four steps, deliberately far apart. Every card used to be one flat run of
   15px grey at one weight, which made a visitor read all of it to find the one
   sentence that applied to them. Now each card states its point once, large;
   supports it once, quiet; and puts the thing you must not miss in a marked
   block. The size jumps are big enough to be scanned rather than read. */
.ed-card p {
  margin: 0 0 0.55rem;
  font-size: var(--type-base);
  line-height: 1.55;
  color: var(--color-ink-soft);
}
.ed-card p:last-child { margin-bottom: 0; }

/* 1. The card's actual point, in the display face. */
.ed-card-lead {
  font-family: var(--font-display);
  font-size: clamp(var(--type-md), 2.5vh, var(--type-lg)) !important;
  line-height: 1.22 !important;
  color: var(--color-ink) !important;
  margin-bottom: 0.3rem !important;
}

/* 2. The condition attached to it — the sentence that decides whether someone
      can do the thing the card describes. Marked with a rule and a tint so it
      survives being skimmed past. */
.ed-card-key {
  padding: var(--space-2) var(--space-2) var(--space-2) var(--space-3);
  border-left: 3px solid var(--color-accent);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  background: var(--color-accent-tint);
  font-size: var(--type-sm) !important;
  font-weight: 600;
  line-height: 1.5 !important;
  color: var(--color-ink) !important;
}
.ed-card-key em { font-style: normal; text-decoration: underline; text-underline-offset: 3px; }

.ed-card strong { color: var(--color-ink); }
.ed-card a { color: var(--color-accent-dark); font-weight: 600; }
.ed-card-tag {
  display: block;
  margin-bottom: var(--space-1);
  font-size: var(--type-xs);
  font-weight: 700;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  /* ink-soft, not --color-primary: the primary gray is only 4.0:1 on white
     and less on the route card's blue tint — too faint for 13px text. */
  color: var(--color-ink-soft);
}

/* The route card spans both columns and takes the stack's flexible row, so
   it's the piece that absorbs whatever height is left over — which is right:
   it's the only card here that isn't logistics. */
.ed-card--route {
  grid-column: 1 / -1;
  display: flex;
  flex-direction: column;
  border-color: transparent;
  background: linear-gradient(150deg, var(--color-accent-tint), var(--color-blue-tint));
}
.ed-route-lede {
  font-family: var(--font-display);
  font-size: clamp(var(--type-md), 2.5vh, var(--type-lg)) !important;
  line-height: 1.28 !important;
  color: var(--color-ink) !important;
}
.ed-route-link {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  margin-top: auto;
  padding-top: var(--space-2);
  font-size: var(--type-sm);
  font-weight: 600;
  color: var(--color-accent-dark);
  text-decoration: none;
}
.ed-route-link:hover { gap: 0.7rem; color: var(--color-ink); }

/* Secondary channels and credits. Small and quiet on purpose — it is here so
   the information exists (the check address exists nowhere else on the site),
   not to be read by someone who came to find out what time to show up. */
.ed-fineprint {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  gap: 0.15rem var(--space-3);
}
.ed-fineprint p {
  flex: 1 1 19rem;
  margin: 0;
  font-size: var(--type-xs);
  line-height: 1.45;
  color: var(--color-ink-soft);
}
.ed-fineprint a { color: var(--color-accent-dark); font-weight: 600; }

.ed-actions {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}
.ed-actions .btn { flex: 1 1 auto; }

/* A fitted board on a 768px-tall laptop has roughly 30px less room than the
   layout wants once every clamp has already bottomed out. Rather than give
   those screens the long stacked page, tighten the last few things that can
   still give — padding, leading, the route lede's display size — and keep
   them on one screen. Scoped to widths that actually get the board. */
@media (min-width: 1280px) and (max-height: 949px) {
  .ed-photo { margin-top: var(--space-2); }
  .ed-card { padding: var(--space-2); }
  .ed-card p { line-height: 1.45; margin-bottom: var(--space-1); }
  .ed-card-key { padding: 0.6rem 0.75rem 0.6rem var(--space-2); }
  .ed-route-lede { font-size: var(--type-md) !important; }
  .ed-route-link { padding-top: var(--space-1); }
  .ed-schedule li { padding: 0.4rem 0; }
  .ed-actions .btn { min-height: 42px; }
}

/* ---------- Fall back to a normal page ----------
   Narrow, or short enough that fitting would mean shrinking the copy past
   reading size. The board becomes one column, the poster stops trying to
   fill a viewport, and the document scrolls like every other story page.
   The height cut-off is 820px rather than the 740px it started at, and that
   is a deliberate trade: once each card gained a display-size lead and a
   marked key line, a 768px-tall screen came out ~70px short of holding the
   board, recoverable only by taking the type back down to the flat,
   hard-to-scan block this layout exists to replace. Legibility wins; those
   screens get an ordinary page and scroll. */
@media (max-width: 1279px), (max-height: 819px) {
  .ed-body { display: block; }
  .ed-page { padding-block: var(--space-4) var(--space-5); }
  .ed-board { width: min(60rem, 100% - var(--space-4) * 2); }
  .ed-layout { grid-template-columns: 1fr; }
  .ed-stack { grid-template-rows: none; }
  .ed-date { font-size: clamp(2rem, 7vw, 3rem); }
  /* No leftover height to claim once the poster is an ordinary block, so the
     photo needs a size of its own here. */
  .ed-photo { flex: none; aspect-ratio: 16 / 9; }
  .ed-route-lede { font-size: var(--type-md) !important; }
}
@media (max-width: 1279px) {
  .ed-stack { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 639px) {
  .ed-stack { grid-template-columns: 1fr; }
  .ed-actions .btn { width: 100%; flex: none; }
  .ed-topbar-tag { display: none; }
}

/* Motion here is decoration, not information — the board reads identically
   without it. */
@media (prefers-reduced-motion: reduce) {
  /* The drift and the ground's slow shift are decoration — the board reads
     identically without either. The connector hairlines are static and stay:
     they carry structure, not motion. */
  .ed-float { animation: none; opacity: 1; transform: none; }
  .ed-page { animation: none; }
}

/* ============================== FOOTER ============================== */
.site-footer {
  background: var(--color-ink);
  color: rgba(255,255,255,0.75);
  /* More generous than before — the extra top padding does double duty as a
     more graceful, intentional-looking footer AND as clearance so Mile 4's
     last heading isn't crowded against the very bottom of the page, without
     needing a big blank invisible spacer above it (see #main). */
  padding-block: var(--space-8) var(--space-5);
}
/* .reg-footer (register.html) has none of the content that padding was
   sized for — no .footer-grid columns, no Mile 4 section above it to
   clear — just the one .footer-bottom line, so the same padding-block
   left a disproportionately large dark band under a task-focused page
   that benefits from staying compact. */
.reg-footer { padding-block: var(--space-4) var(--space-3); }
/* .ed-footer (stories/event-details.html) needs the same treatment and then
   some: that page is sized to hold everything in one screen, so every pixel
   the footer takes is a pixel the board has to give up. Has to sit here
   rather than in the EVENT DETAILS block above — .site-footer is declared
   after it, and at equal specificity source order would hand the win back to
   the full-size padding. */
.ed-footer { padding-block: var(--space-2); flex-shrink: 0; }
.footer-grid {
  display: grid;
  gap: var(--space-5);
  grid-template-columns: 1fr;
  margin-bottom: var(--space-5);
}
@media (min-width: 800px) { .footer-grid { grid-template-columns: 1.3fr 1fr 1fr; } }
/* Four across only once there's room: at 800px a fourth track squeezes the
   Contact column below the width of its email addresses, so between 800 and
   1099px the Acknowledgements column wraps under the brand block instead. */
@media (min-width: 1100px) { .footer-grid { grid-template-columns: 1.3fr 1fr 1fr 1fr; } }

/* The white glow on .logo's text-shadow is there to lift the wordmark off
   photo backgrounds in the floating #siteHeader. In the footer it sits on a
   flat dark panel and just reads as the text shining — drop it here. */
.footer-brand .logo { color: var(--color-white); text-shadow: none; }
.footer-brand p { max-width: 32ch; font-size: var(--type-sm); margin-top: var(--space-2); }
/* Real sponsor name (seen on-site in the 2025 event photos, e.g. the
   podium banner in design-source/photos/ARTR.2025_0051-51.jpg) rather than a
   placeholder — flagged here since there's no actual logo asset for a
   true logo strip yet (see the note on .footer-brand in index.html);
   worth confirming the name/tier is still current before this is
   considered final copy. Understated (small, muted) on purpose — a
   credit line, not a second CTA competing with the description above it. */
.footer-presented-by {
  font-size: var(--type-xs);
  letter-spacing: 0.03em;
  color: rgba(255, 255, 255, 0.55);
  margin-top: var(--space-2);
}
.footer-presented-by strong { color: rgba(255, 255, 255, 0.85); font-weight: 600; }

.footer-col h2 {
  font-family: var(--font-body);
  font-size: var(--type-xs);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-white);
  margin-bottom: var(--space-2);
}
.footer-col ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.6rem; }
.footer-col a { text-decoration: none; font-size: var(--type-sm); transition: color var(--duration-fast) var(--ease-out); }
.footer-col a:hover { color: var(--color-white); }
.footer-col p { max-width: 26ch; margin: 0; font-size: var(--type-sm); line-height: 1.6; }

.footer-bottom {
  border-top: 1px solid rgba(255,255,255,0.12);
  padding-top: var(--space-3);
  display: flex; flex-wrap: wrap; gap: var(--space-2);
  justify-content: space-between;
  font-size: var(--type-xs);
}

/* ============================== SCROLL REVEAL ============================== */
.reveal {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity var(--duration-slow) var(--ease-out), transform var(--duration-slow) var(--ease-out);
}
.reveal.is-visible { opacity: 1; transform: translateY(0); }

/* .reveal-stagger children each get their OWN opacity/transform reveal
   (not just a transition-delay with nothing of its own to delay — that
   was this rule's previous, silently-inert form: transition-delay only
   ever affects a property that element is ITSELF transitioning, and
   these children had no such property, so --stagger-index has never
   actually staggered anything up to this point). The container's own
   .reveal fade is switched off wherever .reveal-stagger is also present
   (the more specific .reveal.reveal-stagger selector below wins) so the
   container isn't ALSO independently fading in as one block on top of —
   effectively doubling and delaying — its children's individual cascade. */
.reveal-stagger > * {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity var(--duration-slow) var(--ease-out), transform var(--duration-slow) var(--ease-out);
}
.reveal.reveal-stagger { opacity: 1; transform: none; transition: none; }
.reveal-stagger.is-visible > * {
  opacity: 1;
  transform: translateY(0);
  transition-delay: calc(var(--stagger-index, 0) * 90ms);
}

/* ============================== HONOREE STORY PAGES ============================== */
.story-page { padding-block: calc(var(--header-height) + var(--space-6)) var(--space-8); }

/* Wider than a pure reading column so the photo/lede split has room to
   breathe — .story-body p below constrains its own reading width via
   max-width: 65ch regardless, so widening this doesn't hurt legibility. */
.story-container { max-width: 60rem; }

.story-back {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  min-height: 32px;
  font-size: var(--type-sm);
  font-weight: 600;
  color: var(--color-ink-soft);
  text-decoration: none;
  margin-bottom: var(--space-4);
}
.story-back:hover { color: var(--color-ink); }

.story-photo { aspect-ratio: 16/10; margin-bottom: var(--space-4); }
.story-photo img { object-position: center 15%; }

/* Honoree portraits are tall head-and-shoulders shots (~2:3 to 3:4). The
   default 16/10 .story-photo frame cropped their heads off — give these a
   portrait frame, capped so they don't tower on wide screens, aligned to
   the top so the face stays in view. Must sit AFTER the .story-photo rules
   above (equal specificity, source order wins). */
.story-photo--portrait {
  aspect-ratio: 3 / 4;
  max-width: 400px;
}
.story-photo--portrait img { object-position: center top; }

/* Life dates trailing an honoree's role line. */
.honoree-years { font-weight: 400; color: var(--color-ink-faint); }

/* Small byline under the name — same quiet register as the glioblastoma
   page's .definition-byline. */
.story-byline {
  margin-top: var(--space-2);
  font-size: var(--type-xs);
  letter-spacing: 0.04em;
  color: var(--color-ink-faint);
}

/* Editorial drop cap on the opening line of an honoree story. */
.story-body-lead::first-letter {
  float: left;
  font-family: var(--font-display);
  font-size: 3.1em;
  line-height: 0.8;
  margin: 0.05em 0.14em 0 0;
  color: var(--color-accent-dark);
}

/* Key-facts strip on the honoree pages — a compact echo of the Mile 1 stat
   tiles that breaks the narrative with something scannable. Tiles fade up
   in sequence on load (skipped under reduced motion) and lift on hover. */
.story-facts {
  list-style: none;
  padding: 0;
  margin: var(--space-5) 0;
  display: grid;
  gap: var(--space-2);
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
.story-facts li {
  background: var(--color-bg-alt);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-3);
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  transition: transform var(--duration-base) var(--ease-out), box-shadow var(--duration-base) var(--ease-out);
}
.story-facts li:hover {
  transform: translateY(-3px);
  box-shadow: 0 16px 32px -18px rgba(31, 42, 51, 0.22);
}
.story-facts-num {
  font-family: var(--font-display);
  font-size: var(--type-2xl);
  line-height: 1;
  color: var(--color-accent-dark);
}
.story-facts-label { font-size: var(--type-sm); color: var(--color-ink-soft); overflow-wrap: break-word; hyphens: auto; }
@media (prefers-reduced-motion: no-preference) {
  .story-facts li {
    animation: storyFactIn 0.55s var(--ease-out) both;
    animation-delay: calc(var(--stagger-index, 0) * 80ms);
  }
  @keyframes storyFactIn {
    from { opacity: 0; transform: translateY(14px); }
    to { opacity: 1; transform: none; }
  }
}

/* Dr. Wong's block on the-team.html — a second figure set off from the
   founders' story above it. */
.story-wong-divider {
  margin-top: var(--space-7);
  padding-top: var(--space-6);
  border-top: 1px solid var(--color-border);
}
.story-person-name {
  font-family: var(--font-display);
  font-size: clamp(1.6rem, 3vw, 2.25rem);
  line-height: 1.1;
  margin-bottom: var(--space-3);
}
/* Marks copy that's a stand-in for real text still to come. */

.story-grid { margin-bottom: var(--space-6); }

.story-container .honoree-role { margin-bottom: var(--space-1); }
/* Sized for the narrower text column next to the photo (not the full-width
   --type-3xl used elsewhere) — that size wraps a full name to three lines
   in this context, which reads as cramped rather than dramatic. */
.story-container h1 { font-size: clamp(2rem, 4vw, 3rem); max-width: none; margin-bottom: var(--space-3); line-height: 1.1; }

.story-body p {
  font-size: var(--type-md);
  color: var(--color-ink-soft);
  max-width: 65ch;
}
.story-body p + p { margin-top: var(--space-2); }

/* One line pulled from the honoree's own story, set large as the opening
   statement — replaces the "wall of paragraphs" feeling with a single
   moment to land on before the fuller account underneath. */
.story-lede {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 500;
  font-size: clamp(1.5rem, 2.8vw, 2.125rem);
  line-height: 1.35;
  color: var(--color-ink);
  max-width: 34ch;
  margin-bottom: var(--space-5);
}

/* Closing CTA — story pages previously dead-ended at a back-link with no
   way to act on what you just read. */
.story-cta {
  margin-top: var(--space-6);
  padding-top: var(--space-5);
  border-top: 1px solid var(--color-border);
}
.story-cta p {
  font-family: var(--font-display);
  font-size: var(--type-lg);
  color: var(--color-ink);
  max-width: 32ch;
  margin-bottom: var(--space-3);
}

/* ============================== REGISTER PAGE ============================== */
/* A 4-step wizard (see js/register.js) — everything below is scoped
   under .reg- so it can't collide with the site's other card/form-ish
   classes (.donate-card, .detail-card, etc.) even though it borrows
   their general look. No real payment processing happens anywhere here
   (see the submit handler's comment in js/register.js for why) — this
   is deliberately the same "looks complete, honest about not being
   wired up yet" treatment already used for the donate button elsewhere
   on the site. */
.reg-wrap { max-width: 44rem; }

/* .reg-wrap holds the whole form column. Below 1024px the two side cards
   just fall in normal block flow above/below it (own min-height, see
   .reg-side-card) — no separate mobile-specific markup needed. At
   1024px+ this becomes a real 3-up grid (left card | form | right card).
   Wider than --container-max (the site's standard text-reading ceiling)
   deliberately — the extra width goes entirely to the two side columns
   (the form itself stays a fixed, readable 44rem either way), so the
   whole layout uses more of a real desktop viewport instead of sitting
   in a comparatively narrow centered strip. */
.reg-page-grid {
  display: grid;
  gap: var(--space-5);
}

/* One photo-link card on each side of the form (event details on the
   left, past events on the right) rather than grouped together — they
   flank the form, they aren't a sidebar next to it. A fixed aspect-ratio
   "good vertical rectangle" rather than stretching to match the form's
   full rendered height: that was tried and looked wrong — the box ended
   up far taller than any single photo's own proportions, forcing either
   severe cover-cropping or (worse) large empty letterboxed gaps. At
   3:4 it stays tall or short with the box, filled edge-to-edge by
   object-fit:cover with only minor, natural cropping. align-self:start
   on the 1024px+ rule keeps it from being stretched by the grid row's
   default stretch behavior, which would otherwise override the
   aspect-ratio and pull it back to the form's full height again. */
.reg-side-card {
  position: relative;
  display: block;
  min-height: 320px;
  border-radius: var(--radius-md);
  overflow: hidden;
  box-shadow: 0 16px 32px -18px rgba(31, 42, 51, 0.3);
  transition: transform var(--duration-base) var(--ease-out), box-shadow var(--duration-base) var(--ease-out);
}
.reg-side-card:hover,
.reg-side-card:focus-visible {
  transform: translateY(-3px);
  box-shadow: 0 22px 40px -18px rgba(31, 42, 51, 0.38);
}
.reg-side-card img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform var(--duration-slow) var(--ease-out);
}
.reg-side-card:hover img,
.reg-side-card:focus-visible img { transform: scale(1.05); }

.reg-side-card-scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(14, 19, 23, 0.85) 0%, rgba(14, 19, 23, 0.15) 55%, transparent 78%);
}

.reg-side-card-label {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  padding: var(--space-3);
  color: var(--color-white);
  font-weight: 700;
  font-size: var(--type-md);
  line-height: 1.3;
}

/* 1280px, not 1024px — a 240px-floor side column plus the fixed 44rem
   form plus .container's own padding/gap overhead genuinely doesn't fit
   in less than ~1280px without overflowing past the viewport edge (this
   was tried at 1024px and measured doing exactly that — the right
   column's own right edge landed past the window's right edge). Padding
   and gap are both trimmed here specifically for this wider grid so the
   floor can be meaningfully large without pushing the breakpoint even
   higher and losing more of the common 1280-1366px laptop range. */
@media (min-width: 1280px) {
  .reg-page-grid {
    max-width: 1560px;
    /* Equal floors/weights — both side cards match in size (an earlier
       asymmetric 1.3fr/0.7fr version made the past-events card too small
       by comparison). 230px still fits without overflow at exactly
       1280px, same as the 220px version this replaced. */
    grid-template-columns: minmax(230px, 1fr) 44rem minmax(230px, 1fr);
    gap: var(--space-3);
    padding-inline: var(--space-3);
    /* center, not start: the side cards are much shorter than .reg-wrap
       (which includes the back-link/heading/lede/steps above the form,
       not just the form itself) — centering positions them against the
       form roughly where it actually sits in that taller column, rather
       than flush with the page's very top edge above it. */
    align-items: center;
  }
  .reg-wrap { max-width: none; }
  .reg-side-card { min-height: 0; aspect-ratio: 3 / 4; }
}

/* ---------- Steps / fields ---------- */
.reg-step-title { font-size: var(--type-xl); margin-bottom: 0.3rem; }
.reg-field-hint { font-size: var(--type-xs); color: var(--color-ink-faint); margin: 0.4rem 0 0; }

/* ---------- Preliminary screen (individual / join a team / start a team) ---------- */
.reg-intro { margin-bottom: var(--space-5); }
.reg-intro-title { font-size: var(--type-xl); margin-bottom: 0.3rem; }
.reg-intro-lede { color: var(--color-ink-soft); font-size: var(--type-sm); margin: 0 0 var(--space-4); }
.reg-intro-options { display: flex; flex-direction: column; gap: var(--space-2); }
.reg-intro-option {
  display: block;
  width: 100%;
  text-align: left;
  font: inherit;
  text-decoration: none;
  border: 2px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-white);
  padding: var(--space-3);
  cursor: pointer;
  transition: border-color var(--duration-fast) var(--ease-out), background-color var(--duration-fast) var(--ease-out);
}
.reg-intro-option:hover { border-color: var(--color-accent-light); }
.reg-intro-option-head { display: block; font-weight: 700; font-size: var(--type-md); color: var(--color-ink); }
.reg-intro-option-desc { display: block; font-size: var(--type-sm); color: var(--color-ink-soft); margin-top: 0.2rem; }

/* A bare [hidden] loses the specificity race anywhere a class already sets
   `display` — the .btn on Continue, the flex .reg-steps stepper, the .reg-form.
   These let js/register.js toggle them with the plain hidden attribute. */
.reg-form[hidden] { display: none; }

/* ---------- Step nav (Previous / Next / Submit) ---------- */
.reg-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: var(--space-5);
  padding-top: var(--space-4);
  border-top: 1px solid var(--color-border);
}

/* ---------- Confirm & submit summary ---------- */
.reg-summary-list { margin: 0; padding-left: 1.1rem; }
.reg-summary-list li + li { margin-top: 0.3rem; }
/* 404.html: a closing sentence follows the list directly. */
.reg-summary-list + p { margin-top: var(--space-3); }
/* volunteer.html stacks several list sections in one .reg-form. */
.reg-form .reg-step + .reg-step { margin-top: var(--space-5); }

/* Subheadings inside a long-form .story-body — the honoree pages never use
   these (their .story-body is just a run of <p>s), but the explainer page
   below needs to break its argument into named sections. Deliberately
   smaller than the --type-3xl section-heading h2 used on the main page:
   this is a subhead inside one continuous piece, not a new section's own
   headline. */
.story-body h2 {
  font-family: var(--font-display);
  font-size: var(--type-xl);
  color: var(--color-ink);
  max-width: 30ch;
  margin-top: var(--space-6);
  margin-bottom: var(--space-2);
}

/* ============================== PAST EVENTS CTA ==============================
   Replaces what used to be two separate "coming soon" cards (2024, 2025) on
   the main page with one link into the photo gallery page — see
   stories/past-events.html and the .gallery-wall rules further down. Kept
   the .scroll-anchor + id="event-2025" from the old markup on THIS element
   (see index.html) so the nav dropdown's existing anchor still lands
   somewhere sensible even though there's no longer a separate per-year
   card to jump to. */
.past-events { margin-top: var(--space-6); }

/* ============================== PAST EVENTS GALLERY (drag-to-explore wall) ==============================
   stories/past-events.html only. A grid of photos tiled 2x2 (see buildWall
   in js/gallery.js) so dragging past any edge of one copy just continues
   into the next, identical copy — a "wrap between duplicated content"
   trick, spread across two axes instead of one. Wheel input pans it too
   when not actively dragging, and
   ctrl+wheel/pinch zooms (see js/gallery.js) — both need the wrap period
   itself to stay exactly equal to one copy's rendered size, which still
   holds here even though tiles are variable-sized (see below): all 4
   copies lay out the identical GALLERY_ORDER against the identical column
   count/row unit, so every copy resolves to the same rendered size and
   tiles seamlessly regardless.
   Masonry via CSS grid's row-span trick, computed in js/gallery.js's
   layoutMasonry(): grid-auto-rows is a small fixed unit (--gallery-row-unit),
   and each tile is given an explicit `grid-column`/`grid-row-start` plus
   `grid-row: span N` sized from ITS OWN photo's real aspect ratio (the
   w/h on each GALLERY_IMAGES entry) so tall photos span more rows and
   wide ones span fewer — landscape and portrait shots keep their own
   proportions and pack into a varied, gallery-wall arrangement instead of
   a uniform grid of identical squares. Every tile's position is computed
   in JS via "shortest column" placement (classic Pinterest-style
   masonry), not left to grid-auto-flow to guess — an earlier version
   relied on `grid-auto-flow: dense` instead, which only backfills a gap
   when some LATER item's span happens to match it exactly; with spans
   this varied that's often never, and real dead gaps stayed empty rather
   than closing. Explicit per-tile placement can't leave a gap, because
   nothing is ever skipped over in the first place.
   object-fit:cover (not contain) is what makes that packing look
   intentional rather than approximate: --gallery-row-unit only rounds a
   photo's span to the nearest whole row, so the tile's real aspect ratio
   is never pixel-exact, and cover quietly crops that small remainder
   instead of leaving a sliver of letterboxing that would read as a
   layout bug rather than a deliberate crop.
   Dark field (var(--color-ink), matching .event-facts-card/.definition-banner
   elsewhere) is now mostly just the lazy-load placeholder color rather
   than visible letterboxing, but stays for the same reason: photos here
   are a mix of phone snapshots and DSLR shots, and a dark surround reads
   as intentionally gallery-lit rather than clashing with a white page
   background.
   Tile size (column width) is still a fixed CSS rule the JS only reads
   (via the --gallery-col-width custom property), not derived from
   viewport width in JS — so a resize only ever needs layoutMasonry() to
   recompute each tile's row-span against the new column width, never a
   full DOM rebuild. That matters beyond tidiness: rebuilding was wiping
   every tile back to its opacity:0 entrance state and replaying the
   fade-in, which made photos already on screen visibly flash away and
   back for no reason a visitor caused. */
.gallery-page { padding-bottom: 0; }
.gallery-intro-note { font-size: var(--type-sm); color: var(--color-ink-faint); max-width: 42ch; margin-top: var(--space-2); }
.gallery-intro-thanks { font-size: var(--type-md); color: var(--color-ink-soft); max-width: 54ch; margin-top: var(--space-3); }

/* Smooth entry into the pinned wall (see js/gallery.js's updateEnterFade):
   the intro fades + lifts on scroll while this fixed black curtain fades in
   behind it, so the wall pins onto an already-black screen instead of
   snapping over white. Injected as body.firstChild — paints above the
   static intro, below the wall once that goes position:fixed. */
.gallery-enter-fade {
  position: fixed;
  inset: 0;
  background: #000 radial-gradient(ellipse 125% 95% at 50% 38%, #0b0d13 0%, #06070b 48%, #000 100%);
  opacity: 0;
  pointer-events: none;
}
.gallery-page .story-container { will-change: opacity, transform; }
/* Opaque plate over the strip above the veil while the wall is pinned (see
   js/gallery.js's topCover). Sits above the photo track so the faint seam a
   clipped will-change:transform layer can leave at the FILL clip edge never
   shows. JS owns height + opacity; solid through FILL, fades out over OPEN. */
.gallery-top-cover {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 0;
  background: #000;
  opacity: 0;
  z-index: 3;
  pointer-events: none;
  /* Un-promoted for the same reason as .gallery-brain-veil, and for one more:
     this and the veil meet at a seam (they overlap by 2px x the current
     scale). Two independently-composited layers rasterised at their own
     scales can disagree along that shared edge; painting both into the same
     layer means they cannot. */
}
/* Hero-scroll pin (see the "Hero-scroll expand (upward)" section of
   js/gallery.js) — a spacer reserving the scroll distance the wall's
   fixed/growing phase consumes, on top of the boxed height it naturally
   contributes to the page before engaging. #galleryWall is NOT a sticky
   child of this (position:sticky can't grow an element upward from a
   pinned bottom on ordinary downward scroll — see the long comment in
   gallery.js for why) — it's manually switched between normal in-flow
   positioning and position:fixed by JS instead, so this spacer's only
   job is reserving the right amount of scroll room, not holding
   anything in place itself.
   Both this element's height and the wall's own are set directly by JS,
   in px measured off window.innerHeight — not by CSS vh units —
   specifically so the two can't drift out of sync with each other the
   way two independent unit systems could (see the long comment in
   gallery.js on why an earlier vh-based version of this left a real
   gap). The values below are only the BEFORE-JS-RUNS fallback, close
   enough that there's no visible flash if JS is a frame late, not the
   real sizing logic.
   background is the OTHER half of that gap fix: this spacer is always
   taller than the wall's boxed height (that's the whole point — it's
   reserved room for the wall to grow into as you scroll), so until the
   wall is fixed and has grown to cover it completely, part of THIS
   element's own box is what's actually visible around the wall —
   transparent there meant the page's white background showed through as
   a real, visible white gap every time the wall was mid-transition, not
   just some flash before layout settles. Matching it to the wall's own
   pitch black makes that reserved-but-not-yet-covered area read as more
   of the same gallery instead of a gap, at every step of the transition,
   from any scroll position (a button-triggered scrollTo, a resize, not
   just an ordinary wheel scroll — anything that moves scrollY hits this
   the same way).
   margin-top is kept small (not the space-6 an earlier version used) so
   the wall's own black starts close under the intro text, rather than a
   big pale gap sitting between them before you've even scrolled. */
.gallery-hero-pin {
  position: relative;
  height: auto;
  margin-top: var(--space-3);
  background: #000;
}
.gallery-wall {
  position: relative; /* default, in-flow, boxed — js/gallery.js switches this to fixed once engaged */
  overflow: hidden;
  /* Belt-and-suspenders, not necessarily load-bearing on their own: while
     debugging a white flash that appeared once an earlier version of
     js/gallery.js changed this element's own height every scroll frame
     (while fixed, clipping a .gallery-wall-track child that's
     will-change:transform and legitimately thousands of px tall — four
     tiled copies of the whole draggable canvas), a forced repaint
     (resizing the window) made the same "broken" state resolve instantly
     to correct, with no other change — pointing at a paint/compositor lag
     rather than a genuine layout bug, though it reproduced repeatedly
     with real scrolling, not just scripted jumps, so a real bug wasn't
     ruled out either. Rather than bet on which it was, the actual fix
     removes the trigger both explanations shared: this element's own
     height is set ONCE, the instant it's fixed (see js/gallery.js's
     stuck-transition), and never touched again for as long as it stays
     fixed — nothing here changes size on a per-frame basis at all
     anymore for the bug to trigger on. contain:paint/clip-path stay here
     as free, harmless insurance in case it was the former. */
  contain: paint;
  clip-path: inset(0);
  touch-action: none; /* stops the browser's own touch-scroll from fighting the custom drag below */
  user-select: none;
  height: min(82vh, 780px);
  min-height: 460px;
  /* Near-black with a whisper of vignette (edges → pure black) so the pinned
     stage reads with depth instead of dead flat. The centre is only a few
     values off #000. */
  background: #000 radial-gradient(ellipse 125% 95% at 50% 38%, #0b0d13 0%, #06070b 48%, #000 100%);
  cursor: grab;
}
/* Reduced motion gets none of the above — no pin, no fill/open, just the
   plain boxed gallery sitting normally in the page (matching
   .gallery-wall.is-static just below, which is what js/gallery.js
   switches to in this same case, and skips measureHero()/updateHero()
   for entirely). Since .gallery-hero-pin's default height is already
   auto and .gallery-wall's default position/height are already the
   in-flow boxed min(82vh,780px) — the same fallback values from the
   comment above — reduced motion just needs js/gallery.js to leave them
   alone, nothing here needs to override anything. */

/* ---------- Brain fill/open veil (see js/gallery.js) ----------
   Sized off the SAME min(82vh,780px)/460px-floor formula as #galleryWall's
   own boxed height (not inset:0 the way .gallery-wall-viewport is) —
   deliberately: once stuck, the wall's real box is already
   full-viewport-tall (see the .gallery-wall comment above), so an inset:0
   veil would center its icon on THAT taller box and sit too high, well
   above where the small, boxed-looking card is actually supposed to read
   as sitting. Matching the wall's OWN un-stuck boxed height here instead,
   bottom-anchored to line up with where the wall visually "starts" either
   way, means .gallery-brain-border/-fill's own `center` mask position is
   trivially correct with no extra JS math — this box IS the boxed card,
   not a taller box a centered icon would sit too high inside of.
   transform-origin (set by js/gallery.js, in px local to this box) is the
   heart shape's own center, NOT this element's — during the OPEN phase the
   whole veil scales up and fades around that point, so the heart stays put
   as the fixed anchor everything else grows outward from, while the
   viewport's own heart-shaped clip-path (see .gallery-wall-viewport)
   grows outward from that exact same point in lockstep. Background stays
   solid regardless — once this fades out, #galleryWall's own permanent
   pitch-black background (not this element's) is what keeps covering
   whatever the growing clip-path hasn't reached yet, so there's no gap
   between "veil gone" and "wall's own black taking over" to choreograph. */
.gallery-brain-veil {
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: min(82vh, 780px);
  min-height: 460px;
  background: #000;
  pointer-events: none;
  z-index: 2;
  /* NOT promoted, and that is deliberate — see the matching note on
     .gallery-wall-track. Promoting this element is what made its layer go
     stale after a scale to ~10x and back, so the black stopped covering on
     the way OUT of the reveal. Un-promoted it repaints into the wall's own
     layer every frame, in DOM order, always covering. */
}
/* Premium fill rig (2026-09). Art is assets/logo/brain-outline.png +
   brain-fill.png — extracted from the 1080px "Brain Back" master, cropped to
   the exact framing the old 381px logo-mark-brain-{border,mass}.png used, so
   this stays a drop-in on every measure the JS makes. The layers, back to
   front inside .gallery-brain-veil:
     .gallery-brain-bokeh  — slow out-of-focus blobs on the black (cinematic)
     .gallery-brain-glow   — a blurred radial bloom that brightens with
                             --brain-fill (opacity only); seeds the OPEN burst
     .gallery-brain-fill   — static wrapper masked to the brain BODY; holds a
                             faint slate = the dormant (un-charged) brain, and…
       i.gallery-brain-fill-ink — the charged blue; a full-height strip sat on
                             the brain and translated DOWN by (1 - --brain-fill)
                             of its height. Its static 8% top-feather IS the
                             surface line. No per-frame gradient/mask recompute.
     .gallery-brain-surface — static wrapper, same brain-BODY mask +
                             mix-blend:screen, holds two more translated strips:
       i.gallery-brain-surge — a soft bloom pooled under the surface
       i.gallery-brain-motes — a few faint sparks just under the crest
     .gallery-brain-border — the crisp white stroke, painted LAST (on top of
                             the blue) so fill/surge can't bleed onto the line
   Per scroll-frame js/gallery.js sets ONE value — --brain-fill (0..1) on
   #galleryHeroPin — which the CSS calc()s into transforms + opacities only, so
   FILL stays on the compositor. --brain-h is the art's rendered height
   (brain-fill.png is 814x742 -> 0.9115). */
.gallery-brain-veil {
  --brain-h: calc(var(--gallery-brain-size, 260px) * 0.9115);
  --brain-y: calc(50% - var(--gallery-brain-rise, 0px));
}
.gallery-brain-border,
.gallery-brain-fill,
.gallery-brain-surface,
.gallery-brain-glow {
  position: absolute;
  inset: 0;
}
.gallery-brain-border {
  background: var(--color-white);
  -webkit-mask: url(../assets/logo/brain-outline.png) center var(--brain-y) / var(--gallery-brain-size, 260px) no-repeat;
  mask: url(../assets/logo/brain-outline.png) center var(--brain-y) / var(--gallery-brain-size, 260px) no-repeat;
  /* brain-outline.png's alpha is hardened + dilated ~1px (was a 2–3px
     antialiased feather that let the blue fill / bright surge behind it show
     THROUGH the semi-opaque edge as a bluish tint on the line). One tight
     pure-white shadow mops up the last 1px of edge AA. Dropped for the OPEN
     phase — see the .is-opening rule below. */
  filter: drop-shadow(0 0 0.5px #fff);
  /* NB: NOT promoted. Tried will-change:transform here (+ on .gallery-brain-fill)
     so the PNG mask would raster once — but promoting an art layer inside the
     veil while the veil scales 11× and back made the outline + fill desync /
     ghost on the reverse zoom (same family as the veil's own R7/R8). The
     .is-opening layer-drop below is the safe win. */
}

/* Charge fill. .gallery-brain-fill and .gallery-brain-surface are static
   wrappers, each masked to the brain BODY; the light that moves lives in
   their child <i>'s. EVERYTHING that changes per scroll-frame in here is a
   transform or an opacity — no gradient/mask/filter is recomputed — so the
   FILL phase stays on the compositor. --brain-fill (0..1, set by
   js/gallery.js) is the only input. */
.gallery-brain-fill,
.gallery-brain-surface {
  -webkit-mask: url(../assets/logo/brain-fill.png) center var(--brain-y) / var(--gallery-brain-size, 260px) no-repeat;
  mask: url(../assets/logo/brain-fill.png) center var(--brain-y) / var(--gallery-brain-size, 260px) no-repeat;
}
.gallery-brain-fill {
  /* Dormant brain body — a faint slate-blue so the un-charged brain reads as
     a solid mass rising out of the black, not a hollow wire ring. */
  background: linear-gradient(to top, rgba(56, 74, 102, 0.52) 0%, rgba(80, 103, 138, 0.42) 100%);
  background-size: 100% var(--brain-h);
  background-position: center var(--brain-y);
  background-repeat: no-repeat;
}
.gallery-brain-surface { mix-blend-mode: screen; }
/* The three moving light strips: each is exactly the brain's rendered box,
   sat on the brain, then translated DOWN by (1 - fill) of its own height —
   so fill 1 = flush over the brain, fill 0 = a whole height below it (where
   the wrapper mask hides it). */
.gallery-brain-fill-ink,
.gallery-brain-surge,
.gallery-brain-motes {
  position: absolute;
  left: 0;
  width: 100%;
  height: var(--brain-h);
  top: calc(50% - var(--gallery-brain-rise, 0px) - var(--brain-h) / 2);
  transform: translateY(calc((1 - var(--brain-fill, 0)) * var(--brain-h)));
}
.gallery-brain-fill-ink {
  /* charged energy, on-brand periwinkle: deep base, brand blue mid, a
     luminous crest right under the surface */
  background: linear-gradient(to top, #2f5f9e 0%, var(--color-accent) 42%, #8fb8e6 76%, #d8ebfc 100%);
  /* static soft top edge — this ~8% feather IS the surface line */
  -webkit-mask: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, #000 8%);
  mask: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, #000 8%);
  /* The largest moving strip — its own layer so the per-frame translate is a
     compositor offset, not a repaint+remask (never promoting it measured the
     FILL phase at a 33ms median instead of 17ms).
     But it must NOT be a layer once the heart opens: under the veil's
     heart-hole clip-path, any composited descendant makes Chrome build a
     mask layer for the clip, and that mask is what dropped out for single
     frames and flashed the photo wall through. So js/gallery.js adds
     .is-ink-flat to the veil once the fill passes ~78% — before the open
     gate lets the heart move, while the veil is still unclipped — and only
     takes it off again once the heart is shut and the fill has drained. */
  will-change: transform;
}
.gallery-brain-veil.is-ink-flat .gallery-brain-fill-ink { will-change: auto; }
.gallery-brain-surge {
  /* a soft bloom of light pooled just under the surface, riding up with it */
  background: radial-gradient(ellipse 80% 30% at 50% 0%,
    rgba(226, 240, 255, 0.5) 0%,
    rgba(190, 220, 252, 0.16) 42%,
    rgba(150, 195, 245, 0) 74%);
  opacity: 0; /* JS ramps 0 -> ~1 -> 0 across the charge (setBrainFill) */
}
.gallery-brain-motes {
  /* a few faint sparks just under the crest; the small extra -px term lets
     them drift a touch ahead of the surface as the charge tops off */
  background-image:
    radial-gradient(circle 2px at 24% 6%, rgba(232, 242, 255, 0.9), transparent 60%),
    radial-gradient(circle 1.6px at 39% 3%, rgba(232, 242, 255, 0.8), transparent 60%),
    radial-gradient(circle 2.3px at 55% 7%, rgba(232, 242, 255, 0.85), transparent 60%),
    radial-gradient(circle 1.5px at 67% 2%, rgba(232, 242, 255, 0.75), transparent 60%),
    radial-gradient(circle 2px at 79% 6%, rgba(232, 242, 255, 0.85), transparent 60%);
  background-repeat: no-repeat;
  transform: translateY(calc((1 - var(--brain-fill, 0)) * var(--brain-h) - var(--brain-fill, 0) * 16px));
  opacity: 0; /* JS gates */
}
.gallery-brain-glow {
  background: radial-gradient(ellipse 62% 48% at 50% var(--brain-y),
    rgba(150, 190, 240, 0.55) 0%, rgba(120, 165, 225, 0.28) 42%, transparent 72%);
  filter: blur(28px);
  /* opacity is the ONLY per-frame change — no scale — so the blur is never
     re-rasterised. --brain-open (0..1 over the first stretch of OPEN, set
     by js/gallery.js) fades it out as the heart starts to open. */
  opacity: calc(var(--brain-fill, 0) * 0.9 * (1 - var(--brain-open, 0)));
  mix-blend-mode: screen;
}
/* OPEN phase: the whole veil is scaled up ~11× and CPU-repainted every
   frame, so the costly blurred / blend layers come out of that. The bloom
   and the bokeh fade on --brain-open, which js/gallery.js drives from
   scroll position over the first 10% of OPEN — a fade rather than a cut, so
   nothing pops back in when you scroll up through the seam. It used to be a
   0.14s CSS transition, but a transition runs as a compositor animation,
   i.e. a composited layer under the heart-hole clip in exactly the frames
   the heart starts opening (see .gallery-brain-fill-ink). Once fully faded,
   .is-open-deep stops them painting at all. The screen-blended surface holds
   no visible pixels here (its sparks are already 0 via setBrainFill's
   envelope), so it can just be hidden outright on .is-opening. */
.gallery-brain-veil.is-open-deep .gallery-brain-glow,
.gallery-brain-veil.is-open-deep .gallery-brain-bokeh {
  visibility: hidden;
}
.gallery-brain-veil.is-opening .gallery-brain-surface {
  visibility: hidden;
}
/* The single most expensive thing in the OPEN phase, and the one that made
   the reveal stutter on a desktop-sized viewport: a filter forces a
   full-layer pass every frame, and this layer is being re-rasterised at up to
   11x. Measured over a hard flick, dropping it took the 75th-percentile frame
   from 32ms to 18ms and the worst frame from 125ms to 59ms.
   It costs nothing to lose here: the shadow exists to mop up the last 1px of
   edge AA at the brain's resting size, and at 4-11x magnification that 1px is
   a smooth 5-11px ramp on an already-resampled mask — there is no hard edge
   left for it to clean up. */
.gallery-brain-veil.is-opening .gallery-brain-border {
  filter: none;
}

/* ---------- Cinematic atmosphere over the pinned stage ---------- */
/* Very soft out-of-focus blobs on the veil's black behind the brain — sits
   inside .gallery-brain-veil (first child) so it rides the same opaque
   backing and fades out with the whole veil during OPEN. Deliberately
   static: it used to drift on a 36s transform animation, which kept it a
   composited layer under the heart-hole clip for the whole reveal (see
   .gallery-brain-fill-ink for why that flashed the photos through). */
.gallery-brain-bokeh {
  position: absolute;
  inset: -10%;
  pointer-events: none;
  /* blobs kept in a central vertical band + inset:-10% so they never reach
     the veil's top/bottom edges — no mask needed to keep the flat black by
     the top-cover seam from lifting */
  background:
    radial-gradient(circle at 26% 44%, rgba(120, 158, 214, 0.09), transparent 40%),
    radial-gradient(circle at 74% 40%, rgba(150, 185, 235, 0.07), transparent 38%),
    radial-gradient(circle at 66% 64%, rgba(110, 150, 210, 0.08), transparent 42%),
    radial-gradient(circle at 34% 66%, rgba(140, 175, 228, 0.06), transparent 38%);
  filter: blur(30px);
  opacity: calc(1 - var(--brain-open, 0));
}
/* Film grain over EVERYTHING (brain + revealed photos). Fixed, click-through,
   above the pinned wall. Static — a blended layer that never repaints
   composites once and stays cached; an animated one re-blends the whole
   viewport every frame. Present only on the gallery page (past-events.html). */
.gallery-grain {
  position: fixed;
  inset: 0;
  z-index: 9990;
  pointer-events: none;
  opacity: 0.05;
  mix-blend-mode: soft-light;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='170' height='170'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.84' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 170px 170px;
}
/* Motion effect through and through (the fill wipe, the heart-open reveal) —
   js/gallery.js skips this whole sequence under reduced motion the same
   way it skips the grow/dwell pin, so the veil just needs to get out of
   the way entirely rather than sit permanently opaque over the photos. */
@media (prefers-reduced-motion: reduce) {
  .gallery-brain-veil { display: none; }
}

/* Escape hatch for the pinned wall — see the HTML comment on this
   button in stories/past-events.html for why it has to be fixed to the
   viewport rather than positioned relative to the wall itself. Hidden
   (not just inert) below its own .is-visible threshold — see
   js/gallery.js — and dropped entirely under reduced motion, where the
   wall never pins in the first place, so there's nothing to escape. */
.gallery-back-to-top {
  position: fixed;
  right: var(--space-3);
  bottom: var(--space-3);
  z-index: var(--z-toast);
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.14);
  backdrop-filter: blur(6px);
  color: var(--color-white);
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--duration-fast) var(--ease-out), background-color var(--duration-fast) var(--ease-out);
}
.gallery-back-to-top:hover { background: rgba(255, 255, 255, 0.24); }
.gallery-back-to-top.is-visible { opacity: 1; pointer-events: auto; }
@media (prefers-reduced-motion: reduce) {
  .gallery-back-to-top { display: none; }
}

/* Mute/unmute for the gallery's ambient loop (see js/gallery.js). Same coin
   as the back-to-top, parked in the opposite corner; only shown while the
   audio is meant to be playing. Stays available under reduced motion (the
   loop still plays there). */
.gallery-audio-toggle {
  position: fixed;
  left: var(--space-3);
  bottom: var(--space-3);
  z-index: var(--z-toast);
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.14);
  backdrop-filter: blur(6px);
  color: var(--color-white);
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--duration-fast) var(--ease-out), background-color var(--duration-fast) var(--ease-out);
}
.gallery-audio-toggle:hover { background: rgba(255, 255, 255, 0.24); }
.gallery-audio-toggle.is-visible { opacity: 1; pointer-events: auto; }
.gallery-audio-toggle .audio-icon-off { display: none; }
.gallery-audio-toggle.is-muted .audio-icon-on { display: none; }
.gallery-audio-toggle.is-muted .audio-icon-off { display: block; }
/* muted styling: colour on .is-muted, but the dimming ONLY when also visible —
   a bare `.is-muted { opacity }` ties with `.is-visible` on specificity and,
   coming later, would keep the button on screen (unclickable) at every scroll
   position whenever the loop is muted. */
.gallery-audio-toggle.is-muted { color: var(--color-accent-light); }
.gallery-audio-toggle.is-visible.is-muted { opacity: 0.8; }
.gallery-wall.is-dragging { cursor: grabbing; }
/* Reduced-motion fallback (see js/gallery.js): no drag/momentum is wired
   up at all, so the wall falls back to a single, ordinary scrollable
   panel instead — otherwise most of the photos would be unreachable
   with no way to pan to them. */
.gallery-wall.is-static { overflow: auto; touch-action: auto; cursor: auto; }
/* Wraps just the track (not the hint or the brain veil) so the OPEN
   phase's heart-shaped clip-path — set/animated by js/gallery.js — only
   ever clips the photo grid, never the veil sitting on top of it. Always
   the wall's full box (inset:0), which while stuck is already
   full-viewport-tall (see the .gallery-wall comment above) — the clip-path
   itself is what actually withholds most of that box from view at first;
   see the Hero-scroll expand comment in js/gallery.js for why the heart's
   own center has to be computed in THIS element's taller coordinate space
   rather than reused directly from .gallery-brain-veil's own (shorter,
   boxed-height) one. */
/* The reveal is a stack of three: the photos at the bottom, then the two
   opaque covers that hide them (.gallery-brain-veil, .gallery-top-cover).
   All three carry an explicit z-index and none of them is promoted, so they
   paint into the wall's one layer, in order, in the same frame. See the long
   note in the OPEN branch of js/gallery.js's updateHero(). */
.gallery-wall-viewport {
  position: absolute;
  inset: 0;
  z-index: 0;
  mask-repeat: no-repeat;
  -webkit-mask-repeat: no-repeat;
}
.gallery-wall-track {
  position: absolute;
  top: 0;
  left: 0;
  transform-origin: 0 0; /* zoom (see js/gallery.js) scales from the track's own top-left, matching the math that keeps whatever's under the cursor stationary while zooming — any other origin and that math (and the wrap period) would need to account for the origin shift too */
  display: grid;
  /* Starting value only — js/gallery.js's ensureTiling() overwrites this
     inline (and appends the matching extra block copies) whenever the wall
     needs more columns of copies to stay covered at the current zoom. */
  grid-template-columns: repeat(2, 1fr);
  width: fit-content;
  height: fit-content;
  /* NOT promoted by default — see .is-pannable below. */
}
/* During the reveal NOTHING among the photos and the two cover layers is
   promoted, so all three paint into the wall's own layer, in DOM order, in
   the same frame. That removes every cross-layer question at once: no stale
   layer surviving a 10x scale and back, no "is the veil on top" for the
   compositor to re-derive, no two layers disagreeing along the seam they
   share. Panning is the one thing that genuinely wants a composited layer,
   and by then the reveal is over and the covers are gone — so the promotion
   is switched on then, by js/gallery.js's syncTrackPromotion(). */
.gallery-wall-track.is-pannable { will-change: transform; }
.gallery-wall.is-static .gallery-wall-track { position: static; display: block; transform: none !important; }

/* padding is exactly HALF of gap, not an independently-chosen value — see
   the wall-level comment above for why: two block copies sit directly
   against each other with zero space between the BLOCKS themselves (that
   part already tiles seamlessly), so the block's own padding is the only
   thing separating the last tile of one copy from the first tile of the
   next. Left at some other, larger value (a plain "give the grid some
   breathing room" padding, which is what this held before), that pair of
   left+right paddings meeting at the seam adds up to MORE space than a
   normal inter-tile gap — a periodic gap wider than any other, right at
   the one spot that's supposed to be invisible. Halved on each side, the
   two half-paddings meeting at the seam sum to exactly one ordinary gap,
   indistinguishable from every other tile-to-tile space. */
.gallery-wall-block {
  display: grid;
  grid-template-columns: repeat(3, 180px);
  --gallery-row-unit: 4px; /* the masonry "unit" — see layoutMasonry() in js/gallery.js, which spans each tile some whole number of these. Small so a tile's grid-quantised height lands close to its real height — matters for quote cards, which (unlike photos) don't cover the remainder with object-fit and so show any over-height as empty card. */
  grid-auto-rows: var(--gallery-row-unit);
  /* Row gap deliberately < column gap: a photo wall reads fine a little
     tighter top-to-bottom, and the smaller row gap shrinks the masonry
     quantum (row-unit + row-gap) further. padding is HALF of each axis's
     gap so the block-to-block seams (2x2 tiled — see .gallery-wall) sum to
     exactly one ordinary gap on both axes; see the wall-level comment. */
  gap: 12px var(--space-3);
  padding: 6px calc(var(--space-3) / 2);
  --gallery-col-width: 180px; /* read by layoutMasonry() so its row-span math always matches this breakpoint's actual column width, without duplicating the number in JS */
}
@media (min-width: 700px) {
  .gallery-wall-block {
    grid-template-columns: repeat(5, 280px);
    gap: 16px var(--space-4);
    padding: 8px calc(var(--space-4) / 2);
    --gallery-col-width: 280px;
  }
}
.gallery-wall.is-static .gallery-wall-block { width: auto !important; }
.gallery-wall-item {
  position: relative;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: rgba(255, 255, 255, 0.04); /* lazy-load placeholder color, shown briefly before each tile's photo loads in */
  cursor: pointer;
  /* No entrance animation — see buildWall() in js/gallery.js: the wall is
     under the brain veil from page load until the heart opens, so it was
     never visible, only ever a few hundred composited animations. */
  transition: transform var(--duration-fast) var(--ease-out), box-shadow var(--duration-fast) var(--ease-out);
}
.gallery-wall-item:hover {
  z-index: 2; /* without this the scaled-up tile sits BELOW its later-in-DOM neighbors (default paint order), so the enlarged photo visibly clips behind the next cell instead of rising above it */
  transform: scale(1.05);
  box-shadow: 0 16px 32px -14px rgba(0, 0, 0, 0.5);
}
/* While the brain veil is covering the wall (js/gallery.js sets
   .is-revealing from the pin until the heart has fully opened), the tiles
   under it take no hover: the cursor is usually parked over the wall while
   scrolling, and a hover-zoom transition is a composited animation right
   under the veil's heart-hole clip — see .gallery-brain-fill-ink. transition
   is off too, so a tile that WAS hovered drops back without animating. */
.gallery-wall.is-revealing .gallery-wall-item {
  pointer-events: none;
  transition: none;
}
/* Quote cards span 2 grid columns (see GALLERY_FEATURED in js/gallery.js)
   so their text wraps into fewer lines. Photos used to as well, one in
   three, but the owner wanted the photos closer to one size, so every
   photo is now a single column and only its height varies. */
.gallery-wall-item.is-featured { grid-column: span 2; }
/* Gap-fillers too short to hold a photo (see placeFiller in js/gallery.js) —
   a bare faint panel, same tone as a quote card, so the spot reads as filled
   without cramming a photo into an unreadable sliver. Not interactive. */
.gallery-wall-item--filler-blank {
  background: rgba(255, 255, 255, 0.05);
  cursor: default;
}
.gallery-wall-item--filler-blank:hover { transform: none; box-shadow: none; }
.gallery-wall-item img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover; /* the tile's own aspect ratio (from layoutMasonry()'s row-span) already approximates the photo's real proportions, so cover only ever crops the small rounding remainder rather than distorting or letterboxing it */
  pointer-events: none; /* the browser's native "drag this image" gesture would otherwise fight the wall's own pointer-drag */
  -webkit-user-drag: none;
}
/* Quote tiles mixed into the same tile sequence as photos (see
   GALLERY_QUOTES in js/gallery.js) — participant "reason for
   participating" quotes from the registration form, set as bare text
   directly on the wall's own dark background rather than a boxed-off
   card, so they read as part of the wall rather than a bolted-on block.
   No <img>, so the click
   handling that opens a tile's photo in the lightbox already no-ops here
   on its own (openTileLightbox() bails when it finds no <img>) — only
   the hover/cursor affordances need explicitly turning off below, since
   those are keyed off the shared .gallery-wall-item class every tile has. */
.gallery-wall-item--quote {
  display: flex;
  flex-direction: column;
  justify-content: center; /* vertical: text block centred in the card */
  align-items: center; /* horizontal: the mark + text blocks centred in the card */
  text-align: center; /* each line of the quote centred, like a pull-quote */
  /* A whisper of a panel (same tone as a photo tile's pre-load placeholder)
     rather than `background: none` — grid rows quantise to ~40px, so a card
     is always a little taller than its text, and on the bare black wall that
     over-height read as an empty rectangle. At 5% white it reads as a softly
     lit quote card instead of a hole. */
  background: rgba(255, 255, 255, 0.055);
  padding: 0.7rem 0.85rem; /* every px here is overhead on top of the card's own measured text height (see quoteMeasurer in js/gallery.js), on every quote regardless of length — kept tight so a short quote's card doesn't tower over one line */
  cursor: default;
}
.gallery-wall-item--quote:hover { transform: none; box-shadow: none; }
.gallery-quote-mark {
  display: block;
  font-family: var(--font-display);
  font-size: 1.3rem; /* trimmed again (was 1.75/2.5rem) — same reasoning as the padding above: fixed overhead on every card */
  line-height: 1;
  margin: 0 0 0.1rem;
  color: var(--color-accent-light);
  opacity: 0.55;
}
.gallery-quote-text {
  margin: 0; /* the UA <p> margin (1em top+bottom) would push the text off-centre inside the flex column — kill it so justify-content:center truly centres mark+text in the card */
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 500;
  color: var(--color-accent-light);
  font-size: var(--type-lg);
  line-height: 1.32;
}
.gallery-wall-hint {
  position: absolute;
  left: 50%;
  bottom: var(--space-3);
  transform: translateX(-50%);
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.5rem 0.9rem;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.12);
  backdrop-filter: blur(6px);
  color: rgba(255, 255, 255, 0.85);
  font-size: var(--type-xs);
  font-weight: 600;
  letter-spacing: 0.03em;
  pointer-events: none;
  opacity: 1;
  transition: opacity var(--duration-base) var(--ease-out);
}
.gallery-wall.is-dragging .gallery-wall-hint,
.gallery-wall.has-interacted .gallery-wall-hint,
.gallery-wall.is-static .gallery-wall-hint { opacity: 0; }
/* backdrop-filter always gets its own compositing layer, and this pill sits
   under the veil for the whole reveal — same hazard as a hover zoom (see
   .gallery-wall.is-revealing .gallery-wall-item). The blur comes back once
   the heart has opened and there are photos behind it to blur. */
.gallery-wall.is-revealing .gallery-wall-hint {
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}

/* ============================== GLIOBLASTOMA EXPLAINER (dictionary lede) ============================== */
/* Modeled on a newspaper "word of the day" treatment — a bold masthead
   image/word up top, then a byline + dateline, then the word restated with
   its pronunciation and a dictionary-style definition list underneath.
   No illustrated character (tonally wrong for a page about a fatal
   disease and three real deaths) — the word itself, set large in a pill on
   a dark field, carries the masthead instead. */
.definition-banner {
  background: var(--color-ink);
  border-radius: var(--radius-lg);
  min-height: 220px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-6) var(--space-5);
  margin-bottom: var(--space-4);
}
.definition-banner-word {
  background: var(--color-white);
  color: var(--color-ink);
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(2rem, 5.5vw, 3.75rem);
  line-height: 1;
  max-width: none;
  text-align: center;
  padding: var(--space-3) var(--space-5);
  border-radius: 999px;
  margin-bottom: 0;
}

.definition-lede {
  padding-bottom: var(--space-6);
  margin-bottom: var(--space-6);
  border-bottom: 1px solid var(--color-border);
}
.definition-byline {
  font-weight: 700;
  font-size: var(--type-sm);
  color: var(--color-ink);
  margin-bottom: 0.15rem;
}
.definition-date {
  font-size: var(--type-xs);
  color: var(--color-ink-faint);
  margin-bottom: var(--space-5);
}
.definition-heading {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--type-lg);
  color: var(--color-ink);
  margin-bottom: var(--space-4);
}
.definition-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}
.definition-entry {
  font-family: var(--font-display);
  font-size: var(--type-lg);
  line-height: 1.4;
  color: var(--color-ink-soft);
  border-left: 2px solid var(--color-border);
  padding-left: var(--space-3);
  margin-bottom: 0;
}
.definition-entry strong { color: var(--color-ink); }

/* Word-part breakdown (gli- / -blast- / -oma) — a compact glossary list,
   one hairline-divided row per fragment, rather than three separate cards;
   reads as one continuous "here's the word taken apart" unit. */
.etymology {
  margin-bottom: var(--space-7);
  border-bottom: 1px solid var(--color-border);
}
.etymology-part {
  display: grid;
  grid-template-columns: 8rem 1fr;
  gap: var(--space-4);
  align-items: baseline;
  padding-block: var(--space-3);
  border-top: 1px solid var(--color-border);
}
.etymology-fragment {
  font-family: var(--font-display);
  font-weight: 700;
  font-style: italic;
  font-size: var(--type-xl);
  color: var(--color-accent-dark);
}
.etymology-gloss { color: var(--color-ink-soft); font-size: var(--type-base); }
@media (max-width: 560px) {
  .etymology-part { grid-template-columns: 1fr; gap: var(--space-1); }
}

/* Boxed video embed — aspect-ratio handles the responsive 16:9 frame
   natively (no padding-bottom-percentage hack needed), relying on
   YouTube's own player chrome for fullscreen rather than a custom
   lightbox. youtube-nocookie.com domain avoids setting tracking cookies
   until the visitor actually presses play. */
.video-box {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--color-ink);
  box-shadow: 0 24px 48px -16px rgba(31, 42, 51, 0.35);
  margin-bottom: var(--space-2);
}
.video-box iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
/* Thumbnail + play button standing in for the YouTube player until it's
   clicked — see js/video-embed.js. hqdefault.jpg is 4:3 with letterbox bars
   baked in, which object-fit:cover crops off. */
.video-facade {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  padding: 0;
  border: 0;
  background: var(--color-ink);
  cursor: pointer;
}
.video-facade img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0.88;
  transition: opacity var(--duration-fast) var(--ease-out);
}
.video-facade-play {
  position: absolute;
  left: 50%;
  top: 50%;
  display: grid;
  place-items: center;
  width: 72px;
  height: 72px;
  margin: -36px 0 0 -36px;
  border-radius: 50%;
  background: rgba(14, 19, 23, 0.72);
  color: var(--color-white);
  transition: background-color var(--duration-fast) var(--ease-out);
}
.video-facade-play svg { width: 30px; height: 30px; margin-left: 4px; }
.video-facade:hover img,
.video-facade:focus-visible img { opacity: 1; }
.video-facade:hover .video-facade-play,
.video-facade:focus-visible .video-facade-play { background: var(--color-accent); }
.video-caption {
  font-size: var(--type-xs);
  color: var(--color-ink-faint);
  max-width: 55ch;
  margin-bottom: var(--space-6);
}

/* ============================== REDUCED MOTION ============================== */
@media (prefers-reduced-motion: reduce) {
  .hero-media { animation: none; }
  .reveal { opacity: 1; transform: none; transition: none; }
  .media-placeholder::after { display: none; }
  * { transition-duration: 0.001ms !important; animation-duration: 0.001ms !important; }
}

/* ============================== IMAGE LIGHTBOX ============================== */
/* Full-screen backdrop for viewing a photo at full size — opened by clicking
   any of the site's clickable photos (doctor callout, mile connector team
   photos; see js/main.js). Clicking anywhere in the backdrop
   OTHER than the image itself (or pressing Escape) closes it — the image
   stops propagation implicitly by being the click target the backdrop
   handler checks against, so no separate per-photo close logic is needed. */
.image-lightbox {
  position: fixed;
  inset: 0;
  z-index: var(--z-lightbox);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-4);
  background: rgba(20, 21, 23, 0.92);
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--duration-base) var(--ease-out), visibility 0s linear var(--duration-base);
}
.image-lightbox.is-open {
  opacity: 1;
  visibility: visible;
  transition: opacity var(--duration-base) var(--ease-out);
}
.image-lightbox-img {
  max-width: min(90vw, 1100px);
  max-height: 90vh;
  border-radius: var(--radius-md);
  box-shadow: 0 30px 70px -20px rgba(0, 0, 0, 0.6);
  cursor: zoom-in;
  /* Starts very slightly scaled down and rises to full size on open, so
     "expand to the center" reads as a small motion rather than a hard cut. */
  transform: scale(0.96);
  transition: transform var(--duration-base) var(--ease-out);
}
.image-lightbox.is-open .image-lightbox-img { transform: scale(1); }
/* Once zoom/pan interaction begins (see js/gallery.js), the image's
   transform is driven by wheel/drag every frame — the transition above is
   exactly right for the gentle open animation but would fight and lag
   behind hand-tracked zoom/pan input, so it's switched off for the rest of
   this photo's time open. Re-enabled automatically the next time a photo
   opens fresh (.has-zoomed is removed on close), so the open animation
   itself is never affected. */
.image-lightbox.has-zoomed .image-lightbox-img { transition: none; }
.image-lightbox-close {
  position: absolute;
  top: var(--space-3);
  right: var(--space-3);
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.12);
  color: var(--color-white);
  font-size: var(--type-xl);
  line-height: 1;
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-out);
}
.image-lightbox-close:hover { background: rgba(255, 255, 255, 0.22); }

/* ====================== MEET THE TEAM (the-team.html) ====================== */
/* The page is a list of people, and it was set as a run of paragraphs: four
   founders as bold-lead sentences, three honorees as a single link, and two
   dashed placeholder boxes doing more visual shouting than the real copy
   around them. These give each group a shape of its own. */

/* ---------- A wider page ----------
   .story-container is capped at 60rem because the honoree pages it was built
   for are one column of prose, and 60rem is a reading measure. This page is
   not that: it's four founder cards, a photo split, and a second photo split,
   none of which is prose. At 1600px the cap left a 320px gutter on each side
   — 40% of the screen empty — while the cards it was protecting stacked two
   abreast down the middle. Wider container, and the prose inside keeps its
   own 65ch measure (.story-body p) so nothing actually reads any wider. */
.team-page .story-container { max-width: 84rem; }

/* Four across once there's room, which is what the extra width buys: the
   founders read as one row of four people instead of a 2x2 block with the
   pending two hanging underneath. */
/* .team-page-scoped so it out-specifies the min-width:700px two-column rule
   further down the file — same specificity there would lose on source order,
   and the grid only exists on this page anyway. */
@media (min-width: 1180px) {
  .team-page .founder-grid { grid-template-columns: repeat(4, 1fr); }
}

/* The vertical rhythm was tuned for a narrow column, where big gaps are what
   separate sections. In a wide layout the sections are already separated by
   their own shapes, and the same gaps just read as holes. */
.team-page .team-section-heading { margin-top: var(--space-6); }
.team-page .story-grid { margin-bottom: var(--space-5); }
.team-page .story-wong-divider { margin-top: var(--space-6); padding-top: var(--space-5); }

/* Both photo splits get the full width now, so the photo can be a real
   presence rather than a thumbnail beside a paragraph. */
@media (min-width: 900px) {
  .team-page .mile-grid--split { gap: var(--space-6); align-items: center; }
  /* Portrait narrower than the text it introduces, and pinned to the top of
     the row rather than centred against a much taller text column. */
  .team-page .wong-split { grid-template-columns: 0.8fr 1.2fr; align-items: start; }
}
/* Inside the split the column IS the measure, so the shared 65ch cap on
   .story-body p only re-creates the empty right-hand strip this restructure
   removed. The column itself keeps the line length honest. */
.wong-text p { max-width: none; }
.wong-text .story-person-name { margin-bottom: var(--space-2); }
.wong-text .story-lede { margin-bottom: var(--space-3); }

/* The shared .mile-grid--split is tuned for the honoree pages, where a tall
   portrait carries the block and the text beside it is a caption. Here the
   text IS the block — it's the page's opening statement — and the inherited
   440/360 split left it on a 45-character measure next to a photo half again
   its width. Even columns, vertically centred, and the lede set at lede size. */
.team-intro { align-items: center; }
@media (min-width: 900px) {
  .team-intro { grid-template-columns: 1fr 1fr; }
}
.team-intro .story-body-lead {
  font-size: var(--type-md);
  line-height: 1.6;
  margin-bottom: 0;
}

.team-section-heading {
  margin-top: var(--space-7);
  margin-bottom: var(--space-2);
  font-family: var(--font-display);
  font-size: clamp(1.5rem, 2.6vw, 2rem);
  line-height: 1.15;
}

/* ---------- Founder cards ----------
   Two across, because four in a row would give each a column too narrow for
   a two-line name plus a sentence, and one column would be the paragraph
   list this replaces. */
.founder-grid {
  display: grid;
  gap: var(--space-3);
  grid-template-columns: 1fr;
  margin-top: var(--space-4);
}
@media (min-width: 700px) {
  .founder-grid { grid-template-columns: 1fr 1fr; }
}

.founder-card {
  position: relative;
  padding: var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  background: var(--color-white);
  transition: transform var(--duration-base) var(--ease-out), box-shadow var(--duration-base) var(--ease-out);
}
.founder-card:hover {
  transform: translateY(-3px);
  box-shadow: 0 18px 36px -22px rgba(31, 42, 51, 0.28);
}

/* No photographs of the founders exist individually — only the group shot at
   the top of the page — so the card is anchored by the initial instead. It's
   there to give four entries four distinct visual starts; the letter is
   decorative (the name is right beside it), hence aria-hidden in the markup. */
.founder-monogram {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 52px;
  height: 52px;
  margin-bottom: var(--space-3);
  border-radius: 50%;
  background: var(--color-accent-tint);
  color: var(--color-accent-dark);
  font-family: var(--font-display);
  font-size: var(--type-lg);
  line-height: 1;
}
.founder-name {
  font-family: var(--font-display);
  font-size: var(--type-lg);
  line-height: 1.1;
  margin-bottom: 0.15rem;
}
.founder-card p {
  margin: var(--space-2) 0 0;
  font-size: var(--type-sm);
  line-height: 1.6;
  color: var(--color-ink-soft);
}

/* The card's link to the person that founder lost. This is the whole reason
   the three honorees are not re-listed on this page: Mile 2 on the homepage
   already introduces them, and repeating that here said nothing new. What
   only this page can say is which of the four is connected to which of the
   three — so the link lives in the founder's own card. */
.founder-link {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  margin-top: var(--space-2);
  font-size: var(--type-sm);
  font-weight: 600;
  color: var(--color-accent-dark);
  text-decoration: none;
}
.founder-link:hover { gap: 0.55rem; color: var(--color-ink); }

/* A founder's own words about the person they lost (Melissa's card), set
   apart from the plain third-person lines on the other cards. */
.founder-card .founder-quote {
  padding-left: var(--space-2);
  border-left: 2px solid var(--color-accent-tint);
  font-style: italic;
}

/* Dr. Wong's research write-up on the team page: set off from the
   paragraphs above it by a rule, with its two source papers listed under it
   in the same link style the glioblastoma page uses for its sources. */
.wong-research {
  margin-top: var(--space-5);
  padding-top: var(--space-4);
  border-top: 1px solid var(--color-border);
}
.wong-research h2 { margin-top: 0; }
.wong-research h3 {
  margin: var(--space-4) 0 var(--space-1);
  font-family: var(--font-body);
  font-size: var(--type-md);
  font-weight: 600;
  color: var(--color-ink);
}
.wong-research .wong-research-more {
  margin: var(--space-4) 0 0;
  font-size: var(--type-xs);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-ink-soft);
}
.wong-research .wong-research-more + .resource-list { margin-top: var(--space-1); }
.wong-research .resource-list a { align-items: flex-start; text-align: left; }
.wong-research .resource-list svg { flex-shrink: 0; margin-top: 0.2em; }

/* ================= PLACEMENT VARIATION (story pages) =================
   The story pages were all the same shape: a split at the top, then one
   centred column of prose running dead straight to the footer, on every page,
   in the same order. Nothing below adds content — it moves what's already
   there off the centre line so the eye has somewhere to go, and gives two
   elements a reason to respond as you scroll.

   Everything here is desktop-and-up (the single column IS the right answer on
   a phone) and everything motion-based is behind prefers-reduced-motion. */

/* ---------- 1. Quotes hang out of the column ----------
   A pull-quote set inline in the measure is just a paragraph with a rule on
   it. Pulled left into the gutter it becomes the thing it's meant to be — a
   line lifted out of the story — and it breaks the column's straight left
   edge at the one point in the page where a break is welcome. */
@media (min-width: 1100px) {
  .story-body .pull-quote {
    margin-left: calc(var(--space-6) * -1);
    padding-left: var(--space-4);
    max-width: 34rem;
    font-size: var(--type-xl);
    line-height: 1.35;
  }
}

/* ---------- 2. The facts row stops being a row ----------
   Four equal tiles on one baseline read as a table. Nudging every second one
   down by a third of its own height turns the same four into a run — and it
   costs nothing, since the grid row was already taller than any single tile. */
@media (min-width: 800px) {
  /* margin-top, NOT transform. These tiles carry storyFactIn with fill:both,
     and a filled animation's `transform: none` end state outranks any plain
     declaration — the transform version of this simply never appeared. Margin
     is untouched by the animation and moves the tile just the same. */
  .story-facts li:nth-child(even) { margin-top: 22px; }
  .story-facts { align-items: start; }
}

/* ---------- 3. The intro split leans ----------
   1.1fr/0.9fr on every page meant every page opened identically. Alternating
   which side is wider (and which element leads) gives consecutive pages a
   different opening shape, which is most of what stops a set of story pages
   feeling like one page with the words swapped. .story-lean-left /
   .story-lean-right are set per page in the markup. */
@media (min-width: 900px) {
  .story-lean-left  { grid-template-columns: 0.85fr 1.15fr; }
  .story-lean-right { grid-template-columns: 1.15fr 0.85fr; }
  /* Portrait pushed down against the text beside it, rather than both
     starting flush at the same y. A small offset, but it's the difference
     between two columns and one line. */
  .story-lean-left  > .story-photo { margin-top: var(--space-5); }
  .story-lean-right > .story-photo { margin-top: 0; }
  .story-lean-right > .story-photo + div { padding-top: var(--space-5); }
}

/* ---------- 4. Photographs answer the scroll ----------
   A scroll-driven animation, not a scroll listener: `animation-timeline:
   view()` hands the whole thing to the compositor, so this costs no main
   thread work and no JavaScript at all — which is why it can go on every
   story page without undoing the idle-cost work done elsewhere in this file.
   The image drifts within its own frame as the frame crosses the viewport
   (the frame itself never moves, so nothing reflows and nothing overlaps).
   @supports because only Chromium ships it today; everywhere else the photo
   is simply static, which is what it was before. */
@supports (animation-timeline: view()) {
  @media (min-width: 900px) and (prefers-reduced-motion: no-preference) {
    .story-photo img,
    .honoree-card .media-placeholder img {
      animation: photoDrift linear both;
      animation-timeline: view();
      animation-range: entry 0% cover 75%;
      will-change: transform;
    }
    @keyframes photoDrift {
      from { transform: scale(1.08) translateY(2.5%); }
      to   { transform: scale(1.08) translateY(-2.5%); }
    }
  }
}

/* ---------- 5. The closing CTA sits off-centre ----------
   It was a centred block under a centred column — the last thing on the page
   and the most predictable. Left-aligned against the text it follows, with
   the rule running the full width, it reads as the end of the story rather
   than a separate centred island. */
@media (min-width: 900px) {
  .story-cta {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
    text-align: left;
  }
  .story-cta p { margin-bottom: 0; max-width: 44ch; }
  .story-cta .btn { flex-shrink: 0; }
}

/* ==================================================================== */
/* ============================ MOBILE PASS =========================== */
/* ==================================================================== */
/* Everything below is phone-only (<=767px) and deliberately lives in one
   block at the end of the file, after every rule it corrects, so it wins on
   source order without a single !important. Nothing here touches the desktop
   layout — the breakpoint is the whole guard.

   Two kinds of change:
     1. Real breakage that only showed up at phone widths, where a rule
        written for a wide viewport did something actively wrong on a narrow
        one (the header wordmark, the hero measure, the doubled top padding).
     2. Ambient motion that earns its keep on a desktop and doesn't on a
        phone — ten separate always-running animations, every one of which
        keeps the compositor awake for the entire visit on the device least
        able to afford it. */

@media (max-width: 767px) {

  /* ---------- Header ----------
     At 375px the grid's three equal-ish tracks left the wordmark a 62px
     column, so "Reason to Run" wrapped to THREE lines, grew the .logo box
     to 89px inside a 76px header, and overlapped the burger. The decorative
     ribbon is what was eating the room, and it's the one element here with
     no job to do — the mark beside it already carries the brand. Dropping it
     and collapsing the grid to two tracks gives the wordmark the whole
     middle of the bar; nowrap then guarantees it can never re-wrap however
     long the viewport gets. */
  .header-ribbon { display: none; }
  .site-header .container { grid-template-columns: 1fr auto; }
  .site-header .container > .header-right { gap: var(--space-2); }
  .logo-text {
    font-size: 1rem;
    white-space: nowrap;
  }

  /* On every page but the homepage the header is position:relative — it sits
     IN the flow and occupies its own 76px. .story-page then adds another calc(--header-height + …) of padding on top of that, a
     leftover from when the header was fixed everywhere. On a desktop the
     doubled gap reads as generous whitespace; on an 812px phone it was 216px
     of blank screen — a quarter of the viewport — before a word of content.
     Kept phone-only rather than corrected at source, because the wide layout
     is drawn around the larger value. */
  .story-page { padding-block: var(--space-4) var(--space-6); }
  .story-page.ed-page { padding-block: var(--space-3) var(--space-4); }

  /* ---------- Hero ----------
     `max-width: min(13ch, 46%)` — the 46% keeps the headline clear of the
     page's vertical midline on a wide screen, where the hero photo's subject
     stands to the right. There is no such subject on a phone (the photo is
     cropped to a different part of the frame) and 46% of a 327px content box
     is a 150px measure, which broke five words across six lines. Full width,
     and a size that fits it. */
  .hero h1 {
    max-width: none;
    font-size: clamp(2rem, 9vw, 2.75rem);
    line-height: 1.06;
  }

  /* ---------- Touch targets ----------
     The face-tag dots on the mile-2 photos are 20px circles — fine for a
     cursor, half the 44px minimum for a fingertip. Rather than redraw them
     (their size is the point; they're meant to sit unobtrusively on a face),
     give each an invisible pad that extends the hit area to 44px square
     without moving a pixel of what's on screen. */
  /* NOT ::before — that pseudo already draws the ping, and NOT ::after —
     that one is the dot's solid core. An outline-style box-shadow can't take
     clicks either, so the pad goes on the element itself: keep the 20px
     visual via a border-box that stays 20px, and extend only the clickable
     area outward with a transparent border. border-box sizing means the
     drawn circle is unchanged; the hit region grows to 44px. */
  .photo-tag {
    width: 44px;
    height: 44px;
    border-width: 14px;
    border-color: transparent;
    background-clip: padding-box;
    box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.75);
  }
  .photo-tag::before,
  .photo-tag::after { inset: 12px; }
  /* Inline text links that act as buttons — the event-facts link and the
     founder/honoree links — were 22-26px tall. */
  .event-facts-details-link,
  .founder-link,
  .honoree-link {
    min-height: 44px;
    align-items: center;
  }

  /* ---------- Register / volunteer: task first ----------
     Both pages put a decorative photo-link card in the DOM before the form
     column, because at 1024px+ it's the LEFT flank of a three-up grid. Stack
     that same order on a phone and the first thing on screen is a 327x436
     photo advertising a different page, with the heading, the lede and the
     first choice all pushed below the fold — on the two pages whose entire
     purpose is completing a form. Order puts the form column first; the
     cards keep their place, just underneath, where "while you're here" links
     belong. */
  .reg-page-grid > .reg-wrap { order: -1; }

  /* ---------- Ambient motion ----------
     Ten animations run forever on this site: two on the hero CTA, the hero's
     own gradient drift, the glow on every primary button, the mile-dot and
     photo-tag pings, the finish flag's waver, the event-details ground
     drift and its floating panels, and the gallery's bokeh. Each is cheap in
     isolation; together they mean a phone never stops compositing for as
     long as the page is open, which costs battery and steals frame budget
     from the one thing a visitor actually does here — scroll.
     None of them carries information: every element below is fully legible,
     and fully interactive, standing still. Entrance animations (.reveal, the
     one-shot fades) are deliberately NOT in this list — they play once, cost
     nothing after, and are what makes the page feel like it responds. */
  .btn-glow,
  .hero-media,
  .photo-tag::before,
  #mile-4 .section-heading::before,
  #mile-4 .section-heading::after,
  .ed-page,
  .ed-float,
  .gallery-brain-bokeh {
    animation: none !important;
  }
  /* The ping pseudo is the tag's only "I am tappable" cue on a photo, and it
     was the animation carrying it. Stopped mid-loop it would sit as a stray
     white ring, so hide it and let the dot's own border do that job. */
  .photo-tag::before { opacity: 0; }
  /* .ed-float's entrance is the same shorthand as its idle drift, so killing
     the animation above would leave those panels at the keyframe's opacity:0
     start. Put them back where the entrance would have finished. */
  .ed-float { opacity: 1; transform: none; }
}

/* ==================================================================== */
/* =========================== TABLET PASS ============================ */
/* ==================================================================== */
/* iPad-sized screens (roughly 600-1023px). Several rows were drawn for
   either a phone (one column) or a laptop (three across) and switched
   between the two at ~700px, so a tablet got the three-across version
   squeezed into ~200px columns: names broke over three lines, and button
   labels wrapped. */

/* Honorees: one per row, portrait beside the text, alternating sides so it
   keeps the editorial rhythm the staggered desktop row has. */
@media (min-width: 600px) and (max-width: 899px) {
  .honoree-grid--editorial {
    grid-template-columns: 1fr;
    gap: var(--space-5);
  }
  .honoree-grid--editorial .honoree-card {
    display: grid;
    grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
    gap: var(--space-4);
    align-items: center;
  }
  .honoree-grid--editorial .honoree-card:nth-child(2) {
    margin-top: 0;
    grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
  }
  .honoree-grid--editorial .honoree-card:nth-child(2) .media-placeholder { order: 2; }
  .honoree-grid--editorial .honoree-body { padding: 0; }
  .honoree-grid--editorial .honoree-body p { font-size: var(--type-base); }
}
/* Three across again from 900px, with a tighter gutter until there's room. */
@media (min-width: 900px) and (max-width: 1199px) {
  .honoree-grid--editorial { gap: var(--space-4); }
}

/* Finish-line photo cards: three across from 760px, but at that width each
   card is ~200px, too narrow for the desktop-size title and button. */
@media (min-width: 760px) and (max-width: 1023px) {
  #mile-4 .finish-links { gap: var(--space-3); }
  .finish-link-card-label {
    padding: var(--space-3);
    font-size: var(--type-lg);
  }
  .finish-link-card-cta {
    left: var(--space-2);
    right: var(--space-2);
    bottom: var(--space-2);
    padding: 0.75rem 0.9rem;
    font-size: var(--type-xs);
  }
}

/* ==================================================================== */
/* ========================= TOUCH-SIZE PASS ========================== */
/* ==================================================================== */
/* The MOBILE PASS above fixes touch targets only up to 767px, but an iPad
   (768-1024px, and up to 1366px for an iPad Pro) is just as much a
   fingertip device. Keyed on the input type instead of the width, so every
   touch screen gets 44px targets and a mouse-driven desktop keeps its
   tighter spacing. */
@media (pointer: coarse) {
  .story-back,
  .event-facts-details-link,
  .founder-link,
  .honoree-link {
    min-height: 44px;
    align-items: center;
  }
  /* Footer link lists were 20px-tall text rows 10px apart. Growing each
     link's own box (and dropping the list gap by the same amount) keeps
     the visual rhythm the same while the tappable area doubles. */
  .footer-col ul { gap: 0; }
  .footer-col a,
  .footer-bottom a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }
  .footer-bottom { align-items: center; }
  /* Link lists on the text pages: the 404's "try one of these" list and
     the glioblastoma page's sources. These links sit mid-sentence, so they
     stay inline: vertical padding on an inline box grows its tappable area
     without touching the line height, so the text doesn't reflow. */
  .story-page li > a:only-child { padding-block: 0.7em; }
}

/* ==================================================================== */
/* ======================== LARGE DISPLAY PASS ======================== */
/* ==================================================================== */
/* The site was tuned on a 1280-1440px laptop: 16px text and a 1180px
   column. On a 1920px or 2560px monitor that same column sat in the middle
   of the screen with a third of the width empty on each side. Two things
   change past 1280px, together:
     1. Everything gets bigger. The root font size grows from 16px at
        1280px to 22px at 2560px (then stops), and since the whole design
        is in rem, type, spacing and cards all grow in proportion.
     2. The columns get wider. The standard column becomes 86% of the
        window, and the rows that were already wider than it (honorees,
        finish cards, aid station, register) go to ~92%.
   Reading text keeps its own ch/rem caps, so paragraphs don't turn into
   one-line-per-screen strips. Laptops and smaller are untouched. */
@media (min-width: 1281px) {
  html { font-size: clamp(1rem, 0.625rem + 0.46875vw, 1.375rem); }
  :root {
    --container-max: max(73.75rem, 86vw);
    --header-height: 4.75rem; /* = 76px at 16px */
  }
  .logo-mark,
  .header-ribbon { width: 2.5rem; height: 2.5rem; } /* = 40px */

  #mile-2 .container,
  #mile-4 .container { max-width: 92vw; }
  .aid-station-content { max-width: 90vw; }
  .reg-page-grid { max-width: 92vw; }

  /* A few pieces had their own px ceilings, so they stayed laptop-sized
     while everything around them grew. Restated in rem (same size at a
     16px root) so they scale with the rest. */
  .hero h1 { font-size: clamp(2rem, 4.5vw, 4.75rem); }
  .aid-station-donate { max-width: 28rem; }
  .doctor-callout { max-width: 48rem; }
  .mile-connector { height: clamp(360px, 78vw, 60rem); }
  .mile-connector-photos { width: clamp(300px, 70vw, 54rem); }
  .mile-connector-team { width: clamp(300px, 30vw, 28rem); }

  /* Event details board: one-screen dashboard, px-capped at 1500px. */
  .ed-board { width: min(93.75rem, 100% - var(--space-5) * 2); }
  /* Story pages: the photo/title head and the column around the text get
     roomier; the paragraphs themselves keep their reading measure. */
  .story-container { max-width: 66rem; }
  .story-body p { max-width: 70ch; }
  .story-photo--portrait { max-width: 25rem; } /* = 400px */
}
