/* ==========================================================================
   RIDESAFE THAILAND™ — Design System
   Brand: Red #CC0000 / Black / White, safety-vest yellow accent (sparingly)
   Type: Barlow Condensed (display/authority) + Inter (body) + JetBrains Mono (credential/stat)
   ========================================================================== */

/* ---------- Metric-matched display fallbacks ----------
   The Google Fonts link uses font-display:optional, which means that on a cold
   visit — before Barlow Condensed is in cache — the browser renders the whole
   page in a fallback and never swaps. Whatever that fallback is has to occupy
   the SAME width as Barlow, or the home hero wordmark "RIDESAFE THAILAND™"
   runs past the viewport and crops on the right. (Reloading appeared to "fix"
   it only because the font was cached by then.)

   Measured widths of "RIDESAFE" at the phone's rendered size, against ~373px
   of usable width:
       Barlow Condensed    308px   fits
       Arial Narrow        371px   only just
       Roboto Condensed    448px   overflows
       generic sans-serif  493px   overflows badly  <-- Android's real fallback

   Android has no Arial Narrow, so the old stack fell straight through to
   sans-serif (Roboto) at 160% of Barlow's width — hence the crop on a Samsung
   but not on desktop. size-adjust scales each candidate back to Barlow's
   measure, so the layout is identical no matter which one the device picks.
   Ordered widest-to-narrowest source so the closest match wins first. */
@font-face{
  font-family: "BarlowFallback-ArialNarrow";
  src: local("Arial Narrow"), local("Liberation Sans Narrow");
  size-adjust: 83%;   /* 308/371 */
  font-weight: 100 900;
}
@font-face{
  font-family: "BarlowFallback-RobotoCond";
  src: local("Roboto Condensed"), local("Helvetica Neue Condensed");
  size-adjust: 69%;   /* 308/448 */
  font-weight: 100 900;
}
/* Last resort: local("Arial") resolves to Roboto on Android and Arial on
   desktop — both ~493px unadjusted, so both need the same 62.5%. This is the
   one that actually catches the Samsung case. */
@font-face{
  font-family: "BarlowFallback-Generic";
  src: local("Arial"), local("Helvetica");
  size-adjust: 62.5%; /* 308/493 */
  font-weight: 100 900;
}

/* ---------- Tokens ---------- */
:root{
  /* Brand colors */
  --red: #CC0000;
  --red-dark: #9A0000;
  --red-deep: #6E0000;
  --black: #121110;
  --ink: #1A1816;
  --white: #FFFFFF;
  --paper: #F7F4EE;
  --paper-dim: #EFEAE0;
  --yellow: #FFD400;
  --yellow-dim: #FFE988;

  --gray-900: #201E1B;
  --gray-700: #4E4A45;
  --gray-500: #837C73;
  --gray-300: #C9C2B6;
  --gray-200: #E2DCD0;
  --gray-100: #EFEAE0;

  /* Semantic - Dark Theme */
  --bg: #0F0E0C;
  --bg-alt: #1A1917;
  --bg-dark: #151310;
  --bg-dark-alt: #1F1D1A;
  --text: #F5F5F5;
  --text-muted: #A0A0A0;
  --text-on-dark: var(--white);
  --text-on-dark-muted: #B0B0B0;
  --brand: var(--red);
  --accent: var(--yellow);
  --border: rgba(255,255,255,0.08);
  --border-dark: rgba(255,255,255,0.12);

  /* Type */
  /* The fallbacks here are metric-matched (see the @font-face block below the
     :root), not just "something condensed" — without that the home hero
     wordmark overflows and crops on first load. */
  --font-display: "Barlow Condensed", "BarlowFallback-ArialNarrow", "BarlowFallback-RobotoCond", "BarlowFallback-Generic", sans-serif;
  --font-body: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --font-mono: "JetBrains Mono", "SFMono-Regular", Consolas, monospace;

  --text-xs: 0.75rem;
  --text-sm: 0.875rem;
  --text-base: 1rem;
  --text-md: 1.125rem;
  --text-lg: clamp(1.25rem, 1.15rem + 0.5vw, 1.5rem);
  --text-xl: clamp(1.5rem, 1.3rem + 1vw, 2rem);
  --text-2xl: clamp(1.875rem, 1.5rem + 1.6vw, 2.75rem);
  --text-3xl: clamp(2.25rem, 1.7rem + 2.6vw, 3.75rem);
  --text-4xl: clamp(2.75rem, 1.9rem + 4vw, 5.25rem);

  /* Spacing (8pt rhythm) */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
  --space-8: 3rem;
  --space-10: 4rem;
  --space-12: 6rem;
  --space-16: 8rem;

  --container: 1240px;
  --container-narrow: 780px;

  --radius-sm: 6px;
  --radius-md: 14px;
  --radius-lg: 24px;
  --radius-full: 999px;

  --shadow-sm: 0 2px 8px rgba(18,17,16,0.08);
  --shadow-md: 0 8px 24px rgba(18,17,16,0.12);
  --shadow-lg: 0 24px 64px rgba(18,17,16,0.18);
  --shadow-red: 0 12px 32px rgba(204,0,0,0.28);

  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
  --dur-fast: 150ms;
  --dur-base: 250ms;
  --dur-slow: 450ms;

  --header-h: 53px;
}

@media (prefers-color-scheme: dark){
  :root{ color-scheme: dark; }
}

/* ---------- Reset ---------- */
*, *::before, *::after{ box-sizing: border-box; margin: 0; padding: 0; }
/* overflow-x on the ROOT, not just <body>.
   This fixes the first-load bug where the page rendered panned to the left —
   brand clipped, hamburger pushed off-screen right — and only corrected after
   a manual refresh.
   body already had overflow-x:hidden, but that alone does NOT stop the
   DOCUMENT element from expanding on mobile: if anything is momentarily wider
   than the screen during first paint (an image before its dimensions are
   known, a marquee track before its clip applies, text in fallback metrics),
   the layout viewport widens to fit it. The browser keeps scale at 1 and
   simply lets the page pan, so the right-hand side of the fixed header —
   which is where the hamburger lives — sits outside the visible area. On
   reload everything is cached, nothing overflows during paint, and it looks
   correct; that is exactly why it "fixed itself" on refresh.
   Clamping the root as well means the layout viewport can never exceed the
   screen, so the symptom cannot occur regardless of which asset is slow.
   Safe here: the page has no position:sticky elements (checked), and the
   header is position:fixed, which is unaffected by an overflow container. */
html{
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  overflow-x: hidden;
  max-width: 100%;
}
/* .site-header is position:fixed, so a bare fragment jump (#curriculum) parks the
   target's top edge underneath it. scroll-margin-top reserves the header's own
   height plus a little breathing room, so the section heading lands in clear view
   instead of behind the nav. */
:target,
section[id]{ scroll-margin-top: calc(var(--header-h) + 1.5rem); }
/* :not(.force-motion) lets the ?motion=on override re-enable animation on machines
   where the OS has animation effects switched off. */
@media (prefers-reduced-motion: reduce){
  html:not(.force-motion){ scroll-behavior: auto; }
  html:not(.force-motion) *,
  html:not(.force-motion) *::before,
  html:not(.force-motion) *::after{
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}
body{
  font-family: var(--font-body);
  background: var(--bg);
  color: var(--text);
  font-size: var(--text-base);
  line-height: 1.6;
  min-height: 100dvh;
  overflow-x: hidden;
  /* Paired with the root clamp above — belt and braces so no descendant can
     stretch the body wider than the screen either. */
  max-width: 100%;
}
img, video, svg{ display: block; max-width: 100%; }
a{
  color: inherit; text-decoration: none;
  transition: all var(--dur-base) var(--ease-out);
  position: relative;
}
a:hover{ color: var(--red); }
/* Animated underline effect for text links */
a[href]:not(.btn):not(.brand-lockup):not(.nav-links a):not(.mobile-menu a)::after{
  content: ""; position: absolute; bottom: -2px; left: 0; width: 0; height: 2px;
  background: var(--red); transition: width var(--dur-base) var(--ease-out);
}
a[href]:not(.btn):not(.brand-lockup):not(.nav-links a):not(.mobile-menu a):hover::after{
  width: 100%;
}
button{ font: inherit; color: inherit; background: none; border: none; cursor: pointer; }
ul, ol{ list-style: none; }
input, textarea, select{ font: inherit; color: inherit; }
:focus-visible{ outline: 3px solid var(--red); outline-offset: 3px; border-radius: 4px; }

h1,h2,h3,h4{
  font-family: var(--font-display);
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: 0.01em;
  text-transform: uppercase;
}
h1{ font-size: var(--text-4xl); }
h2{ font-size: var(--text-3xl); background:radial-gradient(ellipse 65% 70% at 50% 50%, rgba(204,0,0,0.4) 0%, rgba(204,0,0,0.18) 45%, rgba(204,0,0,0) 75%);padding:2rem 1rem;display:inline-block; }
h3{ font-size: var(--text-xl); }
h4{ font-size: var(--text-md); letter-spacing: 0.04em; }
p{ max-width: 68ch; }
.lede{ font-size: var(--text-md); color: #E8E8E8; }
/* Burmese (Myanmar script) doesn't separate words with spaces, so the browser
   finds no break opportunity and long headings run past the viewport — measured
   as a 6px overflow on the training page. Allowing a break anywhere is the only
   thing that lets these headings wrap. Scoped to [lang="my"] so every other
   language keeps its normal, space-based wrapping untouched. */
html[lang="my"] h1,
html[lang="my"] h2,
html[lang="my"] h3{
  overflow-wrap: anywhere;
  word-break: break-word;
  /* h2 is display:inline-block with max-width:none, so it shrink-to-fits to its
     max-content width and escapes its 640px parent entirely. Capping it at the
     parent's width is what actually forces the wrap to engage. */
  max-width: 100%;
}

/* ---------- Layout ---------- */
.container{ width: 100%; max-width: var(--container); margin-inline: auto; padding-inline: var(--space-5); }
.container--narrow{ max-width: var(--container-narrow); }
.section{ padding-block: var(--space-12); background: linear-gradient(180deg, #0A0908 0%, #151310 50%, #1A1917 100%); }
@media (max-width: 768px){ .section{ padding-block: var(--space-6); } }

/* Drops the app screenshot down to sit level with the text beside it in the
   two-column desktop layout. This was previously an inline margin-top:254px,
   which meant it also applied on phones — where .grid--2 has collapsed to a
   SINGLE column, so the offset stopped being a horizontal alignment tweak and
   became 254px of blank screen between the "See the Full Program" button and
   the image. Gated to the exact breakpoint where .grid--2 actually gains its
   second column, so the offset can only exist when there is something to
   align against. */
@media (min-width: 768px){
  .home-training__media{ margin-top: 254px; }
}

/* Training hero, phones only: grow the Ken-Burns scale DOWNWARD from the top
   edge instead of outward from the centre.

   background-position-y is useless here and it is worth saying why: the hero box
   is far taller than the photo's 1.6 ratio, so `cover` scales the image until it
   fills the HEIGHT exactly — vertical overflow is 0px, and with nothing to slide,
   a Y value has no effect at all on a phone. What was actually clipping the
   rider's helmet is the 1.139 baseline scale on this layer, which with the
   default centre origin eats roughly 7% off the top and bottom alike.

   Anchoring the origin to the top means the scale only grows downward, so the
   top edge — where the helmet sits — is never cropped. Same lever the resources
   hero already uses for its own top-anchored artwork.

   Phone-only: on desktop the hero is exactly 1.600, matching the image, and that
   framing is already signed off. */
@media (max-width: 600px){
  .hero__bg-fade--training{ --hero-transform-origin: top; }

  /* Forces the training hero's subtitle to break after "hour" instead of
     wherever the browser's own wrap happens to land — "...under an hour"
     doesn't fit the 345px column at 393px, so the natural wrap fell mid
     clause, one word early. The base rule (display:none, near .hero__desc)
     sits LATER in this file than this block, so at equal specificity it would
     win the cascade even while this media query matches — !important is what
     actually makes the override take effect under 600px. */
  .mobile-break{ display: block !important; }

  /* "...under an hour" (the English text before the forced break above)
     measures 374px, but the column here is only 345px — at the normal
     hero__desc size (19px) the forced break would still hit a THIRD line,
     because the browser wraps early inside that first segment before ever
     reaching the <br>. 17px is the largest size, canvas-measured against the
     actual translated string, that lets the full "before hour" phrase sit on
     one line. Only applied on this hero, only on phones — every other
     hero__desc is untouched. */
  /* !important needed for the same reason as .mobile-break above: the base
     .hero__desc rule sits later in this file (near line 594) and wins any
     equal-specificity tie regardless of which media query matches. */
  /* 17px -> 23px (+35%), the largest that still holds 2 lines. A true 2x
     (34px) is not reachable: the longest segment needs 674px at that size
     against a 331px column, so it would wrap to 4-5 lines. Two changes buy
     the 35%: the line break moved from after "hour" to after "under", which
     balances the two lines (42/23 chars -> 35/31, so the longest line is 7
     chars shorter), and the width constraints below free up ~46px more.
     Canvas-measured against the real string in the real font. */
  .hero__desc--training{
    /* 21px, not 23px. 23 depended on a full-bleed trick (width:100vw plus a
       negative margin-inline) to borrow ~46px of extra width — but that idiom
       assumes the parent is padded and narrower than the viewport, and here
       the parent is ALREADY 393px wide sitting at x=24, so the negative margin
       computed to 0 and the 100vw width simply pushed the paragraph 24px off
       the right edge. Dropping the trick and staying inside the parent keeps
       it genuinely centred; 21px is the largest that still holds 2 lines at
       the real 345px width. */
    font-size: 21px !important;
    /* Only the inline 35ch cap still needs clearing — without this the text
       reflows to 3 lines at the larger size. */
    max-width: none !important;
    /* Narrowed to fit the marked column, on request — accepting 4 lines.
       True 3 lines isn't reachable with this text: tested every width from
       180-340px, and it jumps straight from 2 lines (340px, both halves of
       the forced-break sentence fit their own line) to 4 (320px and below,
       both halves wrap once) with no width landing exactly 3 — the two
       halves wrap together, not independently. 210px sits centred inside
       the marked lines and reproduces the same stable 4-line break the text
       already settles into from 190-240px, so this isn't the tightest
       possible box, just a comfortable one within that stable range. */
    width: 210px !important;
    margin-inline: auto;
    /* Same soft black glow the home hero's text carries, at the subtitle's
       proportions — this sits over a bright orange headlight, so it does real
       contrast work here, not just decoration. */
    text-shadow: 0 0 24px rgba(0,0,0,1), 0 2px 11px rgba(0,0,0,0.9), 0 0 8px rgba(0,0,0,0.85), 0 0 4px rgba(0,0,0,0.8);
  }
  /* Training hero headline gets the home hero's title treatment: wide, soft
     blurs rather than a tight drop shadow, so the shading reads as the
     background darkening behind the letters instead of an outline around
     them. Radii match the home title's (54/30/20). */
  .hero--training .hero__title{
    text-shadow: 0 0 54px rgba(0,0,0,1), 0 4px 30px rgba(0,0,0,0.9), 0 0 20px rgba(0,0,0,0.85);
  }

  /* Full-bleed video on phones: break out of .container's horizontal padding
     so the player runs edge to edge (345px -> 393px, ~14% larger).
     The negative margins are the standard full-bleed idiom — 50% is half the
     PARENT's width, 50vw half the VIEWPORT's, so the difference is exactly
     the container's side padding, whatever that padding happens to be.
     No cropping is introduced: the frame keeps aspect-ratio 16/9 and the
     clips are natively 1280x720 (also 16/9), so widening the box scales the
     picture up rather than cutting anything off. */
  .training-video__frame{
    width: 100vw !important;
    margin-left: calc(50% - 50vw);
    margin-right: calc(50% - 50vw);
  }

  /* "How It Works" pulled in tight against the video below it, matching the
     desktop fix already applied to this heading. Default section-head
     margin-bottom (48px) plus the h2's own glow padding was leaving enough
     air that the title read as floating between the banner above and the
     video below rather than belonging to either. */
  [data-i18n="training.videoIntro.title"]{
    margin-bottom: 0 !important;
  }
  .section-head:has([data-i18n="training.videoIntro.title"]){
    margin-bottom: 0.75rem !important;
  }
}

/* Phone spacing rhythm.

   The rule above never actually applied on most sections: the pages set their
   vertical padding INLINE (padding-bottom:8rem, padding-block:5.4rem, ...) and
   an inline style beats any stylesheet rule that isn't !important. Those values
   were tuned on a 1440px desktop, so a phone was rendering full desktop padding
   — consecutive sections contributed 128px + 54px of dead space between them,
   about 21% of an 851px screen showing nothing at all. That reads as "broken
   spacing" rather than as breathing room.

   One value for every section keeps a single, calm rhythm down the page, which
   is what makes a narrow screen feel deliberate instead of arbitrary.

   The two [style*=] rules re-assert the deliberate flush joins: several sections
   set padding-bottom:0 (or padding-top:0) so their artwork meets the next
   section edge-to-edge — the mission floor being the obvious one. Without these
   the blanket override would open a 44px gap and break that join. */
@media (max-width: 600px){
  main > section.section{
    padding-top: 2.75rem !important;
    padding-bottom: 2.75rem !important;
  }
  main > section.section[style*="padding-top:0"]{ padding-top: 0 !important; }
  main > section.section[style*="padding-bottom:0"]{ padding-bottom: 0 !important; }

  /* Closes the gap between the "IN YOUR POCKET" title and the helmet band
     below it: the id beats the blanket 2.75rem rule above (higher
     specificity even against its !important), and the helmet band's own
     padding-top is zeroed too so the two meet edge-to-edge. */
  /* 1 line of breathing room (20px) between the title and the helmet band
     below it — fully closing this gap (padding-bottom:0) read as too tight. */
  #home-risk-section{ padding-bottom: 20px !important; }
  /* .section-head's own 48px margin-bottom collapses straight through the
     section now that its padding is zeroed, reopening the gap this rule is
     meant to close — has to be zeroed here too. */
  #home-risk-section .section-head{ margin-bottom: 0 !important; }
  .fact-typewriter-section{ padding-top: 0 !important; }

  /* Headings carrying a hand-tuned inline margin (e.g. margin:3rem 0 1.5rem)
     were set against a desktop column. On a 393px screen that 48px top margin
     stacks on top of the section's own padding AND the h2's glow padding, so a
     single heading was claiming ~199px of vertical space before its own text.
     Halving it keeps the heading distinct without letting it dominate the
     screen. !important is required only because the value is inline. */
  main > section.section h2[style*="margin:3rem"]{
    margin-top: 1.5rem !important;
    margin-bottom: 0.75rem !important;
  }

  /* "THE MISSION" heading only overrides padding-bottom (0.4rem) inline, so
     it still inherits the base h2 rule's padding-top: 2rem (32px) — an
     8x-taller top pad than its 6.4px bottom, which drags the radial-gradient
     glow (it paints across the full padding-box, per box-sizing:border-box)
     down and off-centre from the actual text. Matching the two paddings
     re-centres the glow on the rendered "THE MISSION" text itself. */
  [data-i18n="home.mission.title"]{ padding-top: 0.4rem; }

  /* "25 Years Teaching..." wraps 3 lines, left-aligned, inside an
     inline-block h2 whose width matches its LONGEST line. The base h2 rule's
     glow sits at the box's geometric centre (50% 50%), but a left-aligned
     paragraph's visual mass sits left of that centre whenever a shorter line
     leaves empty box width on the right — exactly this heading's case. Shifting
     the gradient's own hotspot left by 20 points (50%->30%, ~20% of the box's
     own width) re-centres it on the text itself rather than the box. */
  [data-i18n="home.about.title"]{
    background: radial-gradient(ellipse 65% 70% at 30% 50%, rgba(204,0,0,0.4) 0%, rgba(204,0,0,0.18) 45%, rgba(204,0,0,0) 75%);
  }

  /* Closes the gap between the mascot band (mission section, padding-bottom:0
     via the escape hatch above) and the testimonials section right after it:
     testimonials-section uses padding-block (not the discrete padding-top:0 /
     padding-bottom:0 the escape hatch matches on), so it still got the
     blanket 2.75rem !important. Needs 2 extra classes of specificity
     (main > section.section.testimonials-section) to outrank that blanket
     rule despite both being !important. */
  main > section.section.testimonials-section{ padding-top: 0 !important; }

  /* The badge's parent <p class="lede"> carries padding-left:1rem (it also
     indents the quote below, which should stay put) — this cancels that just
     for the badge's own line, flushing it left against "THE MISSION" above. */
  .mission-badge{
    margin-left: -1rem;
    /* Same red radial glow the "THE MISSION" h2 above it carries, so the two
       lines read as one titled block. The ellipse is wider and flatter than
       the h2's (85%/90% vs 65%/70%) because this line is a single row of
       small caps in a much shorter box.
       Padding is deliberately vertical-only: the text plus its two hazard
       icons already measure the full 345px column, so any horizontal padding
       wrapped "RIDERS" onto a second line and threw the icons out to the
       edges. Vertical padding alone gives the gradient the height it needs
       to read as a glow without touching the line's width. */
    padding: 0.55rem 0;
    background: radial-gradient(ellipse 85% 90% at 50% 50%, rgba(204,0,0,0.4) 0%, rgba(204,0,0,0.18) 45%, rgba(204,0,0,0) 75%);
  }

  /* "CLICK. TRAIN. / CERTIFIED." — unlike the two headings above, here the
     glow stays put at the box's default centre and the TEXT moves to meet
     it instead: text-align:center brings both lines (one shorter than the
     other) onto a shared centre line matching the glow's own, and
     margin-inline:auto centres the whole fit-content box within the section
     so glow and text land in the middle of the column together, not
     hugging the left edge. */
  [data-i18n="home.training.title"]{
    text-align: center;
    margin-inline: auto;
  }
}
.section--tight{ padding-block: var(--space-8); }
.section--dark{ background: linear-gradient(180deg, #0F0E0C 0%, #151310 50%, #1A1917 100%); color: var(--text-on-dark); }
.section--alt{ background: linear-gradient(180deg, #151310 0%, #1A1917 50%, #1F1D1A 100%); }
.testimonials-section{ background: #1a1a1a !important; }
.section:first-of-type{
  position: relative !important;
  min-height: 80vh;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  padding-bottom: var(--space-10) !important;
  background: linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0.05) 25%, rgba(0,0,0,0.3) 50%, rgba(0,0,0,1) 100%), url('../assets/img/helmet-fade-transition.png'), linear-gradient(180deg, #0A0908 0%, #151310 50%, #1A1917 100%) !important;
  background-size: auto, 50%, auto !important;
  background-position: 0 0, center 15%, 0 0 !important;
  background-repeat: repeat, no-repeat, repeat !important;
}
.section:first-of-type .section-head{
  text-align: center;
  max-width: 900px;
  z-index: 3;
  position: relative;
}
.section:first-of-type .section-head .hazard-tag{
  display: none;
}
.section:first-of-type .section-head h2{
  font-size: clamp(2rem, 8vw, 3.5rem);
  line-height: 1.2;
  color: var(--white);
  text-shadow: 0 4px 20px rgba(0,0,0,0.8);
}
.section:first-of-type .section-head p{
  margin-top: var(--space-6);
  font-size: var(--text-md);
  color: var(--text-muted);
  max-width: 700px;
  margin-left: auto;
  margin-right: auto;
}
.grid{ display: grid; gap: var(--space-6); }
@media (min-width: 768px){
  /* minmax(0, 1fr) rather than a bare 1fr. A plain 1fr resolves to
     minmax(auto, 1fr), and that `auto` floor refuses to shrink a column below
     its min-content width. On the contact page that meant the form's two
     side-by-side <input>s (each carrying a browser-default intrinsic width)
     propped the whole column open, pushing the second column ~110px off the
     right edge at tablet sizes. A 0 floor lets the columns actually share the
     row, and the inputs are width:100% so they follow the cell down. */
  .grid--2{ grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .grid--3{ grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .grid--4{ grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
.stack{ display: flex; flex-direction: column; gap: var(--space-4); }
.cluster{ display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-4); }
.center{ text-align: center; margin-inline: auto; }
.visually-hidden{
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0; padding: 0; margin: -1px;
}
.skip-link{
  position: absolute; top: -100px; left: var(--space-4); z-index: 200;
  background: var(--red); color: var(--white); padding: var(--space-3) var(--space-5);
  border-radius: var(--radius-sm); transition: top var(--dur-base) var(--ease-out);
}
.skip-link:focus{ top: var(--space-4); }

/* ---------- Hazard tape motif ---------- */
.hazard-rule{
  height: 8px;
  background: repeating-linear-gradient(-45deg, var(--black) 0 14px, var(--yellow) 14px 28px);
  border-radius: var(--radius-full);
}
.hazard-tag{
  display: inline-flex; align-items: center; gap: var(--space-2);
  font-family: var(--font-mono); font-size: var(--text-xs); letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--red); font-weight: 600;
}
.mobile-section-eyebrow{ display: none; }
@media (max-width: 767.98px){
  /* Was a zero-height section with the tag straddling the seam 50/50; now
     pulled back to a real 40px band (2 lines) so there's clear dark space
     between image 1's bottom edge and the tag, per the follow-up ask. The
     tag no longer overlaps either photo, so it's back to the plain
     no-background style every other eyebrow on the site uses. */
  /* Opened up on both sides: 40px -> 52px above, and 0 -> 22px below. The
     bottom was the real problem — align-items:flex-end with zero bottom
     padding pinned the tag hard against the top edge of the second full-bleed
     image, so it read as belonging to that photo rather than sitting in its
     own band. Keeping the top larger than the bottom is deliberate: the tag
     labels the section BELOW it, so the tighter gap should be the one to
     what it introduces. */
  .mobile-section-eyebrow{
    display: flex;
    justify-content: center;
    align-items: flex-end;
    height: auto;
    padding-top: 52px;
    padding-bottom: 22px;
    /* Faint white lift behind the tag. This band is flat #000 (set below), so
       the yellow/red tag had nothing to separate it from the surrounding
       black — a very low-alpha white bloom gives it something to sit against.
       Kept extremely subtle (6% peak) and fully transparent by 70%: any more
       and it reads as a grey smudge on an otherwise pure-black strip. The
       ellipse is wider than tall to follow the shape of a single text line. */
    background-image: radial-gradient(ellipse 58% 62% at 50% 62%,
      rgba(255,255,255,0.06) 0%,
      rgba(255,255,255,0.035) 38%,
      rgba(255,255,255,0.015) 55%,
      rgba(255,255,255,0) 70%);
  }
  /* Flat black across the whole band between the two full-bleed images. The
     patchwork of tones there came from three different backgrounds meeting:
     the training section's #0A0908->#1A1917 gradient above, this strip's own
     colour, and the about section's separate --dark gradient below. Each
     gradient also restarts per section, so the seams read as distinct blocks.
     One flat #000 across all three erases the banding; the images themselves
     are the only visual content in that stretch anyway. */
  main > section.section#home-training-section,
  main > section.section#home-about-section{
    background: #000 !important;
  }
  /* The eyebrow is split out of the rule above and re-declared with BOTH the
     black and the glow in one shorthand. It cannot share that rule:
     `background: #000 !important` is the shorthand, so it resets
     background-image to none — it would silently erase the radial glow set
     earlier regardless of source order, because !important shorthand beats a
     plain longhand. Declaring the gradient and the colour together is the
     only form that survives. */
  .mobile-section-eyebrow{
    background:
      radial-gradient(ellipse 58% 62% at 50% 62%,
        rgba(255,255,255,0.06) 0%,
        rgba(255,255,255,0.035) 38%,
        rgba(255,255,255,0.015) 55%,
        rgba(255,255,255,0) 70%)
      #000 !important;
  }
  /* Doubled, 44px -> 88px. The 44px was the uniform section padding applied
     across every mobile section; here it left "Read Daniel's Story" sitting
     too close to the seam with the Mission section below, so the button read
     as belonging to the boundary rather than closing out Daniel's block.
     Scoped to this one section by id so the shared rhythm elsewhere is
     untouched — the full id+element selector is needed to outrank the blanket
     padding-block rule. */
  main > section.section#home-about-section{ padding-bottom: 88px !important; }
  /* The testimonials section carries padding-top:0, which left just 4px
     between the mascot image above and the "Rider Feedback" eyebrow — the tag
     was effectively sitting on the photo's edge. 44px matches the standard
     mobile section rhythm used elsewhere, so this reads as a normal section
     start rather than a special case. */
  main > section.section.testimonials-section{ padding-top: 44px !important; }
}
.hazard-tag::before{
  content: ""; width: 18px; height: 8px;
  background: repeating-linear-gradient(-45deg, var(--black) 0 4px, var(--yellow) 4px 8px);
  border-radius: 2px;
}
/* Same hazard-stripe icon as .hazard-tag::before, scaled up to match this
   badge's larger (text-md, ~18px) uppercase text instead of the eyebrow's
   text-xs — same 18:8 aspect ratio, just bigger. */
.mission-badge{ display: inline-flex; align-items: center; gap: 0.4em; }
.mission-badge__icon{
  flex: none; width: 26px; height: 11.5px;
  background: repeating-linear-gradient(-45deg, var(--black) 0 5.75px, var(--yellow) 5.75px 11.5px);
  border-radius: 2px;
}

/* ---------- Brand wordmark ---------- */
.wordmark{ font-family: var(--font-display); font-weight: 800; letter-spacing: 0.02em; white-space: nowrap; }
.wordmark span.rs{ color: var(--red); }
.wordmark span.th{ color: var(--black); }
.wordmark--stacked{ display: flex; flex-direction: column; white-space: normal; line-height: 1; font-size: 1.32rem; }
.wordmark--stacked .rs,
.wordmark--stacked .th{ display: block; }
.wordmark--stacked .rs{ font-size: 1.215em; }
.site-header .wordmark span.th,
.section--dark .wordmark span.th,
.footer .wordmark span.th{ color: var(--white); }

/* ---------- Buttons ---------- */
.btn{
  display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2);
  min-height: 48px; padding: var(--space-3) var(--space-6);
  border-radius: var(--radius-full);
  font-family: var(--font-display); font-weight: 700; font-size: var(--text-md);
  letter-spacing: 0.03em; text-transform: uppercase;
  transition: all var(--dur-base) var(--ease-out);
  touch-action: manipulation;
  position: relative;
  overflow: hidden;
}
.btn::before{
  content: ""; position: absolute; inset: 0; background: radial-gradient(circle at var(--mouse-x, 50%) var(--mouse-y, 50%), rgba(255,255,255,0.1) 0%, transparent 80%);
  pointer-events: none;
}
.btn:hover{
  transform: translateY(-2px) scale(1.02);
  box-shadow: 0 8px 24px rgba(204,0,0,0.3);
}
.btn:active{ transform: scale(0.97); }
.btn--primary{ background: var(--red); color: var(--white); box-shadow: var(--shadow-red); }
.btn--primary:hover{ background: var(--red-dark); box-shadow: 0 16px 40px rgba(204,0,0,0.36); }
.btn--dark{ background: var(--black); color: var(--white); }
.btn--dark:hover{ background: var(--ink); }
/* Locks the same red glow .btn:hover normally applies transiently — permanent
   here, everything else (lift, scale, mouse-follow highlight) still hover-only. */
.btn--glow-locked{ box-shadow: 0 8px 24px rgba(204,0,0,0.3); }
.btn--ghost{ background: transparent; color: var(--text); border: 2px solid var(--border); }
.btn--ghost:hover{ border-color: var(--red); color: var(--red); }
.section--dark .btn--ghost{ border-color: var(--border-dark); color: var(--white); }
.section--dark .btn--ghost:hover{ border-color: var(--yellow); color: var(--yellow); }
.btn--accent{ background: var(--yellow); color: var(--black); }
.btn--accent:hover{ background: #FFE044; }
.btn--block{ width: 100%; }
.btn--sm{ min-height: 40px; padding: var(--space-2) var(--space-5); font-size: var(--text-sm); }
.btn[disabled]{ opacity: 0.5; pointer-events: none; }

/* ---------- Header / Nav ---------- */
.site-header{
  position: fixed; inset-inline: 0; top: 0; z-index: 100;
  height: var(--header-h);
  display: flex; align-items: center;
  /* Lifted off pure #000 — it read as too deep/flat against the page. This is
     the site's --black token (#121110), which also keeps the header matched to
     the footer and the black content sections. */
  background: var(--black);
  backdrop-filter: blur(12px) saturate(1.2);
  -webkit-backdrop-filter: blur(12px) saturate(1.2);
  border-bottom: 1px solid var(--border);
  transition: background var(--dur-base) var(--ease-out), box-shadow var(--dur-base) var(--ease-out);
}
.site-header.is-scrolled{ box-shadow: 0 8px 24px rgba(0,0,0,0.3); }
.nav-bar{ width: 100%; display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); }
.brand-lockup{ display: flex; align-items: center; gap: var(--space-3); }
.brand-mascot{ width: 40px; height: 40px; border-radius: var(--radius-sm); flex-shrink: 0; }
.brand-lockup .wordmark{ font-size: calc(var(--text-md) * 1.6691); }

/* Hidden below 1024px — smart devices get the hamburger instead; the ≥1024px
   media query at the end of this nav block restores the inline links. */
.nav-links{ display: none; align-items: center; gap: var(--space-6); }
.nav-links a{
  font-family: var(--font-display); font-weight: 600; font-size: calc(var(--text-base) * 0.847);
  letter-spacing: 0.03em; text-transform: uppercase; color: var(--white);
  position: relative; padding-block: var(--space-2); padding-inline: var(--space-2);
  transition: all var(--dur-base) var(--ease-out);
}
.nav-links a::after{
  content: ""; position: absolute; left: 0; right: 100%; bottom: 0; height: 3px;
  background: var(--red); transition: right var(--dur-base) var(--ease-out);
}
.nav-links a:hover{
  color: var(--red);
  transform: translateY(-2px);
}
.nav-links a:hover::after, .nav-links a[aria-current="page"]::after{ right: 0; }
.nav-links a[aria-current="page"]{ color: var(--red); }

.nav-actions{ display: flex; align-items: center; gap: var(--space-3); }
.nav-cta{ display: none; font-size: calc(var(--text-sm) * 1.1); }

.lang-switch{ position: relative; }
.lang-switch__btn{
  display: flex; align-items: center; gap: var(--space-2);
  border: 2px solid var(--border); border-radius: var(--radius-full);
  padding: var(--space-2) var(--space-4); font-size: calc(var(--text-sm) * 1.1); font-weight: 600;
}
.lang-switch__btn:hover{ border-color: var(--red); }
.lang-switch__menu{
  position: absolute; top: calc(100% + var(--space-2)); right: 0; min-width: 180px;
  background: var(--white); border-radius: var(--radius-md); box-shadow: var(--shadow-lg);
  border: 1px solid var(--border); padding: var(--space-2);
  opacity: 0; visibility: hidden; transform: translateY(-8px);
  transition: all var(--dur-base) var(--ease-out);
}
.lang-switch.is-open .lang-switch__menu{ opacity: 1; visibility: visible; transform: translateY(0); }
.lang-switch__menu button{
  width: 100%; text-align: left; padding: var(--space-2) var(--space-3); border-radius: var(--radius-sm);
  font-size: var(--text-sm); display: flex; align-items: center; justify-content: space-between; gap: var(--space-2);
  color: var(--ink);
}
.lang-switch__menu button:hover{ background: var(--bg-alt); }
.lang-switch__menu button[aria-current="true"]{ color: var(--red); font-weight: 700; }
/* Flag emoji (🇬🇧 etc.) render as plain "GB"/"RU" text on Windows - most
   Windows fonts have no colour-flag glyphs for regional-indicator pairs,
   unlike macOS/iOS/Android. Real inline SVGs sidestep that entirely: they
   render identically on every OS/browser, no font or emoji-set dependency. */
.lang-switch__flag{
  display: inline-block; width: 18px; height: 13px; flex-shrink: 0;
  margin-right: var(--space-2); border-radius: 2px;
  background-size: cover; background-position: center;
  box-shadow: 0 0 0 1px rgba(0,0,0,0.15) inset;
}
.mobile-menu__langs button .lang-switch__flag{ margin-right: var(--space-1); }
.flag-en{ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 60 40'%3E%3Crect width='60' height='40' fill='%23012169'/%3E%3Cpath d='M0,0 L60,40 M60,0 L0,40' stroke='%23fff' stroke-width='8'/%3E%3Cpath d='M0,0 L60,40 M60,0 L0,40' stroke='%23C8102E' stroke-width='3'/%3E%3Cpath d='M30,0 V40 M0,20 H60' stroke='%23fff' stroke-width='14'/%3E%3Cpath d='M30,0 V40 M0,20 H60' stroke='%23C8102E' stroke-width='8'/%3E%3C/svg%3E"); }
.flag-ru{ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 60 40'%3E%3Crect width='60' height='40' fill='%23fff'/%3E%3Crect y='13.33' width='60' height='13.33' fill='%230039A6'/%3E%3Crect y='26.67' width='60' height='13.33' fill='%23D52B1E'/%3E%3C/svg%3E"); }
.flag-es{ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 60 40'%3E%3Crect width='60' height='40' fill='%23AA151B'/%3E%3Crect y='10' width='60' height='20' fill='%23F1BF00'/%3E%3C/svg%3E"); }
.flag-zh{ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 60 40'%3E%3Crect width='60' height='40' fill='%23DE2910'/%3E%3Cg fill='%23FFDE00'%3E%3Cpolygon points='10,6 11.8,11.5 17.5,11.5 12.9,15 14.7,20.5 10,17 5.3,20.5 7.1,15 2.5,11.5 8.2,11.5'/%3E%3Ccircle cx='20' cy='4' r='1.3'/%3E%3Ccircle cx='23' cy='9' r='1.3'/%3E%3Ccircle cx='23' cy='15' r='1.3'/%3E%3Ccircle cx='20' cy='19' r='1.3'/%3E%3C/g%3E%3C/svg%3E"); }
.flag-de{ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 60 40'%3E%3Crect width='60' height='13.33' fill='%23000'/%3E%3Crect y='13.33' width='60' height='13.33' fill='%23DD0000'/%3E%3Crect y='26.67' width='60' height='13.33' fill='%23FFCE00'/%3E%3C/svg%3E"); }
.flag-fr{ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 60 40'%3E%3Crect width='20' height='40' fill='%230055A4'/%3E%3Crect x='20' width='20' height='40' fill='%23fff'/%3E%3Crect x='40' width='20' height='40' fill='%23EF4135'/%3E%3C/svg%3E"); }
.flag-my{ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 60 40'%3E%3Crect width='60' height='13.33' fill='%23FECB00'/%3E%3Crect y='13.33' width='60' height='13.33' fill='%2334B233'/%3E%3Crect y='26.67' width='60' height='13.33' fill='%23EA2839'/%3E%3Cpolygon points='30,10 33,18 42,18 35,23 37,31 30,26 23,31 25,23 18,18 27,18' fill='%23fff'/%3E%3C/svg%3E"); }
.flag-hi{ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 60 40'%3E%3Crect width='60' height='13.33' fill='%23FF9933'/%3E%3Crect y='13.33' width='60' height='13.33' fill='%23fff'/%3E%3Crect y='26.67' width='60' height='13.33' fill='%23138808'/%3E%3Ccircle cx='30' cy='20' r='5' fill='none' stroke='%23000080' stroke-width='1'/%3E%3Ccircle cx='30' cy='20' r='1' fill='%23000080'/%3E%3C/svg%3E"); }

.nav-toggle{
  display: flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; border-radius: var(--radius-sm);
}
.nav-toggle span, .nav-toggle span::before, .nav-toggle span::after{
  content: ""; display: block; width: 22px; height: 2px; background: var(--white);
  transition: transform var(--dur-base) var(--ease-out), opacity var(--dur-base) var(--ease-out);
  position: relative;
}
.nav-toggle span::before{ position: absolute; top: -7px; }
.nav-toggle span::after{ position: absolute; top: 7px; }
.nav-toggle[aria-expanded="true"] span{ background: transparent; }
.nav-toggle[aria-expanded="true"] span::before{ top: 0; transform: rotate(45deg); }
.nav-toggle[aria-expanded="true"] span::after{ top: 0; transform: rotate(-45deg); }

.mobile-menu{
  position: fixed; inset: var(--header-h) 0 0 0; z-index: 90;
  background: #0F0E0C; padding: var(--space-6) var(--space-5);
  display: flex; flex-direction: column; gap: var(--space-2);
  transform: translateX(100%); transition: transform var(--dur-slow) var(--ease-in-out);
  overflow-y: auto;
}
.mobile-menu.is-open{ transform: translateX(0); }
.mobile-menu a{
  font-family: var(--font-display); font-size: var(--text-xl); text-transform: uppercase;
  color: var(--white);
  padding: var(--space-4) 0; border-bottom: 1px solid rgba(255,255,255,0.1);
}
.mobile-menu a[aria-current="page"]{ color: var(--red); }
/* Language chips inside the mobile menu — one tap row, active language in red */
.mobile-menu__langs{
  display: flex; gap: var(--space-2); margin-top: var(--space-5);
}
.mobile-menu__langs button{
  flex: 1; min-height: 44px;
  border: 1px solid rgba(255,255,255,0.25); border-radius: var(--radius-full);
  background: none; color: var(--white); cursor: pointer;
  font-family: var(--font-display); font-weight: 700; font-size: var(--text-sm);
  letter-spacing: 0.06em;
  transition: border-color var(--dur-base) var(--ease-out), color var(--dur-base) var(--ease-out),
              background var(--dur-base) var(--ease-out);
}
.mobile-menu__langs button[aria-current="true"]{
  border-color: var(--red); color: var(--white); background: rgba(204,0,0,0.55);
}
/* Smart devices: the top bar carries only the brand lockup and the hamburger —
   the language switcher steps back until the inline desktop nav returns. */
@media (max-width: 1023px){
  .lang-switch{ display: none; }
}

@media (min-width: 1024px){
  .nav-links{ display: flex; }
  .nav-toggle{ display: none; }
  .mobile-menu{ display: none; }
  .nav-cta{ display: inline-flex; }
  /* Shrunk to half the .btn--sm sizing it inherited (48px/40px min-height ->
     20px, 14px/15.4px font -> 7px). This only ever applies here: below
     1024px .nav-cta stays display:none from the base rule above, so phones
     and tablets never see this button at all — no separate hide rule needed. */
  .nav-cta{
    min-height: 20px;
    padding: calc(var(--space-2) / 2) calc(var(--space-5) / 2);
    /* 0.55 -> 0.6: a few percent up from the half-size shrink, per feedback
       that the white "GET THE APP" label was too small to read comfortably. */
    font-size: calc(var(--text-sm) * 0.6);
  }
}

/* ---------- Hero ---------- */
.hero{
  position: relative; padding-top: calc(var(--header-h) + var(--space-10));
  padding-bottom: var(--space-12);
  min-height: 100vh;
  background: var(--black); color: var(--white); overflow: clip;
}
/* Cinematic background video layer (home hero only) */
.hero__bg{ position: absolute; inset: 0; z-index: 0; overflow: hidden; pointer-events: none; }
.hero__bg video, .hero__bg img{ width: 100%; height: 100%; object-fit: cover; }
/* Scrim keeps hero text at an accessible contrast over the footage */
.hero__bg::after{ content: ""; position: absolute; inset: 0; background: rgba(18,17,16,0.72); }
.hero__grid{ position: relative; z-index: 2; display: grid; gap: var(--space-10); align-items: center; }
@media (min-width: 960px){ .hero__grid{ grid-template-columns: 1.1fr 0.9fr; } }
/* Text-only hero (no photo column): hold the copy to a readable measure and let the
   background footage carry the right-hand side. */
@media (min-width: 960px){ .hero__grid--solo{ grid-template-columns: minmax(0, 720px); } }
.hero__eyebrow{
  display: inline-flex; align-items: center; gap: var(--space-2);
  font-family: var(--font-mono); font-size: var(--text-xs); letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--yellow); margin-bottom: var(--space-4);
}
.hero__title{ font-size: var(--text-4xl); margin-bottom: var(--space-5); }
@media (max-width: 400px){
  /* Sized as large as the two-line headline can go before "HOW TO SURVIVE"
     (its longer line) outgrows the column and forces horizontal overflow —
     measured with canvas text metrics against the actual rendered container
     at 320/360/375/393/400px, not eyeballed. A literal 3x of the old 23px
     (70px) measured 421px wide against a 345px-wide column at 393px alone;
     this clamp is the ceiling that still fits at the narrowest width this
     query covers (320px) while scaling up on the roomier phones in between. */
  /* Every value in the clamp scaled by the same 0.9 factor (the whole curve
     down 10%, not just one endpoint), so it stays the same shape across
     320-400px and still guaranteed not to overflow — it can only be narrower
     than the original at every width, never wider. */
  .hero__title--training{ font-size: clamp(2.34rem, -0.495rem + 14.4vw, 3.06rem); }
}
.hero__title em{ font-style: normal; color: var(--red); }
.hero__desc{ color: #E8E8E8; font-size: calc(var(--text-md) * 1.057); margin-bottom: var(--space-6); max-width: 52ch; }
/* A <br> that only breaks the line under 600px — see the training hero desc.
   display:none removes a <br> from flow entirely (unlike other elements,
   this is the standard way to make a line break conditional), so desktop
   keeps its natural wrap and only phones get the forced break. */
.mobile-break{ display: none; }
.hero__actions{ display: flex; flex-wrap: wrap; gap: var(--space-4); margin-bottom: var(--space-8); }
.hero__stats{ display: flex; flex-wrap: wrap; gap: var(--space-8); }
.hero__stat b{ display: block; font-family: var(--font-mono); font-size: var(--text-2xl); color: var(--yellow); line-height: 1; }
.hero__stat span{ font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-on-dark-muted); }

/* Daniel background hero variant */
/* Founder hero carries NO background-image of its own, deliberately. It used to
   paint daniel-header-bg.jpg directly on the section, which is why the page
   opened with the photo already on screen: that layer was never part of the
   reveal, so there was nothing to fade in FROM. The section now stays on the
   base .hero black, and the only image is the one on .hero__bg-fade, which
   animates. See the ratio + zoom rules further down for the uncropped start. */

/* Shop hero now matches every other hero exactly: fills the section
   (background-size: cover, inherited from the base .hero__bg-fade rule)
   with the same reveal + Ken-Burns breathing zoom. No override needed —
   background-position: center (set inline in shop.html) means the default
   centre transform-origin is already correct, unlike the top-anchored
   heroes that need --hero-transform-origin. */

/* Downloads hero: fills the section edge-to-edge, anchored to the top of the
   image so the headline text is never cropped — any crop lands on the
   bottom of the image instead. The 45vw ratio keeps the box wider than the
   source image's own aspect ratio, so `cover` crops vertically (off the
   bottom) rather than off the sides. min-height sits on the section itself,
   and the bg layer is inset below the fixed header so the header never
   overlaps the headline text.
   The 3.3s reveal and the 26s / ease-in-out / infinite-alternate cadence are
   the same as every other hero; only the zoom's scale endpoints differ, so
   this needs its own keyframes rather than the shared --hero-zoom multiplier
   (that multiplier can only rescale 1.08 -> 1.14, a 5.6% span, which is too
   narrow for the travel below and would start with RIDER already clipped).
   The endpoints are derived, not eyeballed. Scanning the 1920x1080 source for
   white pixels puts the word RIDER at y=31..85. `cover` +
   `background-position: center top` pin the image's top edge to the top of
   this box, which is itself inset below the fixed header — so growing from
   the default centre origin lifts the headline toward the nav bar, and the
   natural-y that sits exactly under the nav's bottom edge is
     k = (H/2) / (H/2 - y*coverScale)
   y=58 (RIDER's midline) gives k ~= 1.152, so the zoom finishes with the nav
   bar cutting through RIDER, then reverses out over the next 26s. The 1.03
   start keeps RIDER clear of the nav at rest and stays above 1 so the sides
   never go bare. Headline pixels span x=415..1778; at 1.152 the visible
   window is x=127..1793, so the deeper zoom still never clips it sideways. */
.hero--resources-bg{
  min-height: calc(max(360px, 45vw) + var(--header-h));
}
/* The hero's store badges are a phone-only addition — desktop already has its
   own pair in the section below the hero, so this copy stays hidden there. */
.hero-badges-mobile{ display: none; }
/* Shop's hero title is a phone-only addition — the desktop hero is a pure
   banner with no text over it, and that framing is already signed off. */
.shop-hero__title{ display: none; }
/* Downloads-hero "Downloads" eyebrow — phone-only. Declared here, ahead of
   the max-width media query that turns it on, so it reliably loses to that
   later, more specific-in-context rule rather than the reverse: two rules of
   equal specificity resolve by SOURCE ORDER, so this base rule has to come
   first for the media query's display:flex to win on mobile. */
.hero__eyebrow--downloads-top{ display: none; }
.hero--resources-bg .hero__bg-fade{
  background-size: cover;
  background-position: center top;
  top: var(--header-h);
  height: calc(100% - var(--header-h));
  scale: 1.03;
  animation:
    hero-bg-reveal 3.3s cubic-bezier(0.65, 0, 0.35, 1) 0s forwards,
    hero-ken-burns-resources 26s ease-in-out 0s infinite alternate;
}
/* Mobile now matches the home/training full-screen treatment instead of the
   old fixed-ratio banner. Desktop keeps the rules above completely
   untouched — this only lives inside the max-width query.
   top:0 / height:100% restores the full-bleed-behind-the-header look
   (matching training/home) instead of the desktop-only choice to keep the
   image clear of the header. background-position-x lands on the "good rider"
   (focused, geared, trained, protected) rather than the poster's dead centre,
   which would otherwise land on the dividing line between the two riders. */
@media (max-width: 767.98px){
  /* !important: a narrower legacy rule at max-width:700px further down this
     file (a headline-clipping fix from earlier work) also sets min-height on
     this class and sits later in source order, so it would otherwise win at
     <=700px regardless of this block. */
  /* Height derived from the artwork, not from the viewport. "Fills the screen
     edge to edge" and "the whole OVER CONFIDENT headline is readable" cannot
     both be true at 100vh: the file is 936x1680 (0.557) against a 393x851
     screen (0.462), so filling that taller box crops 81px (17.1%) off the
     sides — exactly the ends of the headline — while showing the full width
     leaves 146px of black bands.
     Making the hero as tall as the image is when its width fits (100vw x
     1680/936 = 705px, ~83vh) removes the conflict rather than trading one
     against the other: at zoom 1 the image fills this box completely AND
     shows its entire width. The Ken Burns then zooms in from there and crops
     as it goes, which is the intended behaviour.
     The cost is that the hero is ~83% of the viewport rather than all of it,
     so the next section peeks in at the bottom. */
  /* Plus the header's own height: the site header is position:fixed, so it
     paints over the first 53px of the hero — which is precisely where the
     headline sits. Reserving that band and starting the image below it (see
     the top/height pair further down) is what makes the headline actually
     readable rather than merely present. */
  /* Full screen, on every phone. Height comes from the VIEWPORT, not from the
     artwork's ratio — deriving it from the image (the previous
     100vw * 1680/936 approach) guaranteed the whole picture was visible but
     also guaranteed the hero was short of filling the screen, which is the
     wrong trade for a hero.
     svh, not vh: on mobile browsers 100vh is the height with the address bar
     RETRACTED, so it is taller than what is actually on screen at load — the
     exact moment "does it fill the screen" is judged. 100svh is the small
     viewport height and matches what the user really sees.
     REVERTED on explicit follow-up instruction: `cover` at 100svh met "fills
     the screen" but broke "the whole image is visible at load, footer icons
     touch both edges, nothing cropped until Ken Burns starts" — those two
     cannot both hold when the box's ratio doesn't match the artwork's, and
     this time the second set of requirements is the one to satisfy exactly.
     RESOLVED PROPERLY: the artwork itself was extended (936x1680 -> 936x2127,
     ratio 0.5571 -> 0.4401) rather than continuing to trade one requirement
     against the other in CSS. The whole conflict came from the file being
     RELATIVELY WIDER than a phone screen: with cover, an image wider than its
     box scales to the box's HEIGHT and overflows sideways, which is exactly
     what was clipping "OVER CONFIDENT". At 0.4401 the image is now narrower
     than every phone ratio (~0.45-0.56), so cover scales to WIDTH instead and
     the overflow is vertical only — the croppable direction.
     That makes this hero identical in behaviour to training/contact/about:
     viewport-derived height, cover, full-screen from the first frame. */
  .hero--resources-bg{ min-height: 100svh !important; }
  /* Sits the badges low on the image, matching where home and training put
     theirs, rather than dead centre where they'd cover the riders' faces. */
  .hero-badges-mobile{
    display: block !important;
    position: absolute;
    left: 0;
    right: 0;
    bottom: 12%;
    /* Above the hero's dark scrim (z-index:1), or the badges would sit under
       it and read muddy. Needs the positioning above to take effect. */
    z-index: 2;
  }
  /* Only the gap needs widening now. The badges are a plain centred row again:
     the background offset above moved the artwork's divider onto the screen's
     centreline, so a symmetric row is automatically balanced on it — no
     translate needed, and no drift to chase during the zoom. 24px is what
     opens enough room for the line to pass cleanly between the two badges
     (it was 12px, which left the App Store badge touching it). */
  /* One badge centred inside each half of the screen, split on the artwork's
     divider (which the background offset above put on the screen centreline).
     space-around does this by definition — it gives each item an equal share
     of the line and centres it in that share, so with two items the centres
     land at 1/4 and 3/4 of the width and the space between them is exactly
     twice the outer margins... which is what "even" means here: outer 14.75px,
     inner 29.5px, both badges dead centre of their own half. Deriving it from
     the layout rather than a hand-tuned gap keeps it true on every phone
     width instead of just this one.
     The container's 24px side padding has to go first, or each "half" would be
     half of the padded box rather than half of the screen.
     Scoped through .hero--resources-bg for specificity: the shared "force
     badge rows onto one line" rule further down also sets gap here with
     !important and, being later, wins at equal specificity. */
  /* SUPERSEDED by the stacked layout below — the space-around reasoning above
     applied to the side-by-side row and no longer governs. Kept for the record
     of why the row was built that way.
     Stacked vertically to match home and training exactly: both use
     flex-direction:column, a 12px gap, centre alignment and 176px badges, so
     those values are copied rather than re-derived — the point is that all
     three heroes present the pair identically. Stacking also makes the old
     balancing act moot, since a single centred column sits on the screen's
     centreline by definition. */
  .hero--resources-bg .hero-badges-mobile{
    padding-left: 0 !important;
    padding-right: 0 !important;
    /* 12% of the hero resolved to ~91px, floating the pair higher than the
       other two heroes. 56px puts the lower badge in the same band as home
       (52px above the hero's foot) and training (64px). */
    bottom: 56px !important;
  }
  .hero--resources-bg .hero-badges-mobile > div{
    flex-direction: column !important;
    flex-wrap: nowrap !important;
    align-items: center !important;
    justify-content: center !important;
    gap: 12px !important;
  }
  /* 176px, the same width home and training use. This hero was held at a
     narrower 167px purely so both badges could fit on ONE line — stacking
     removes that constraint. */
  .hero--resources-bg .app-badge-link img{ width: 176px !important; }
  /* Full-bleed behind the header, matching training/home, rather than the
     desktop-only choice to keep the image clear of it. Applies to whichever
     layer is visible. */
  /* Starts below the fixed header rather than behind it. This was top:0 /
     height:100% to get a full-bleed look under the header, which was fine
     while a 74px letterbox band sat above the artwork and absorbed the
     overlap — with the image now flush to the top of its layer, that same
     rule buried the headline. */
  /* Full-bleed behind the header, matching training/contact/about. With cover
     doing the fill there is no reason to inset this — a shorter box would
     just mean a smaller fillable area. The extended artwork carries 277px of
     added sky at the top, so what sits behind the header is that sky, never
     the headline. */
  .hero--resources-bg .hero__bg-fade{
    top: 0 !important;
    height: 100% !important;
  }
  /* `cover`, reversing the earlier `contain` decision on request: fill the
     screen edge to edge from the first frame and let the crop fall where it
     may. The file is a true 9:16 (936x1680 = 0.557) against a 393x851 viewport
     (0.462), so cover scales to the box HEIGHT and slices ~17% off the sides —
     which is what `contain` was avoiding, at the cost of 74px black bands top
     and bottom. Filling was chosen over keeping the headline's outer edges. */
  .hero--resources-bg .hero__bg-fade--mobile{
    /* `cover`, now that the artwork's ratio (0.4401) is narrower than every
       phone. cover picks the axis needing the most scale: because the image
       is relatively TALLER than any phone box, that axis is WIDTH — so the
       full width always renders and the sides can never clip, on any ratio,
       with the overflow spent vertically instead. */
    background-size: cover !important;
    /* center TOP, not center. Centring splits the vertical overflow across
       both edges, which on a short 16:9 phone (375x667) pushed the headline
       up to y=45 — beneath the 53px fixed header. Anchoring the top spends
       the entire crop at the BOTTOM, the end the brief says is fine to lose,
       and holds the headline at a consistent depth: measured 132/144/151/158/
       138px across 360x800, 393x851, 412x915, 430x932 and 375x667. */
    background-position: center top !important;
    background-repeat: no-repeat !important;
    background-color: var(--black);
    /* Ken Burns must START at 1, not the base rule's 1.03. At 1.03 the very
       first painted frame is already zoomed 3% past full-screen, so the
       "loads fully filled, THEN grows" sequence never actually shows its
       starting state. Its own keyframes below run 1 -> 1.15 — the scale
       transform enlarges the box (background included) uniformly from that
       fully-fitted frame, and the hero's own overflow:hidden crops the excess
       as it grows, which is the requested "crop naturally while zooming". */
    scale: 1 !important;
    animation:
      hero-bg-reveal 3.3s cubic-bezier(0.65, 0, 0.35, 1) 0s forwards,
      hero-ken-burns-resources-mobile 26s ease-in-out 0s infinite alternate !important;
  }
  .hero--resources-bg .hero__bg-fade--desktop{ display: none; }
  .hero--resources-bg .hero__bg-fade--mobile{ display: block; }
  /* Phone-only badge placement: the pair inside the hero shows, the original
     pair below it hides. Above 768px this reverses (see the base rules), so
     desktop keeps exactly the layout it already had. */
  /* Badges moved out of the hero and onto the top of the section below, so
     this row is no longer hidden on mobile — it's the only copy left. */
  /* New "Downloads" eyebrow at the top of the hero, matching the marked
     line. Positioned absolute + centred rather than left in flow, because the
     hero has no text column of its own any more (the badges that used to
     anchor one were just moved out) — nothing else here establishes a
     natural top-of-hero position to sit in.
     10px down from the header's own bottom edge (53px), same reasoning as
     every other calibrated mark on this project: measured against the live
     header height rather than guessed.
     display:flex here MUST outrank the base display:none rule, which used to
     sit after this media block in source order and win at equal specificity
     regardless of viewport — the tag was invisible on mobile as a result.
     Moving that base rule above (see .hero__eyebrow--downloads-top{display:
     none} near the top of this file now) fixes the ordering permanently. */
  .hero__eyebrow--downloads-top{
    display: flex;
    position: absolute;
    /* +10px -> +2px. The artwork's own "OVER CONFIDENT" headline starts at
       81px on this layout (measured: it occupies rows 67-236 of the 936x1680
       source, scaled to the full-width render), and the tag's bottom edge at
       +10px landed at 82px — a 1px overlap. +2px clears it with a small
       margin instead of sitting flush against it. */
    top: calc(var(--header-h) + 2px);
    /* Centred on the artwork's actual blue-sky region, measured rather than
       eyeballed: sampling row y=30 of the 936x1680 source (well above the
       headline) for blue-dominant pixels (b > r+15 and b > 120) finds the
       stormy/sunny divide at x=496, i.e. 53.0% of the width, with blue
       running unbroken to the right edge from there. The sky's centre is
       therefore (53.0% + 100%) / 2 = 76.5% of screen width — 300px on this
       393px screen. The first pass (126px, landing content-centre at 260)
       was a visual guess against the screenshot and fell short of this by
       ~40px; this value is derived from the file itself, not a raw pixel
       estimate. left is 300 - 97/2 (content is ~97px wide) so the CONTENT's
       centre, not the box's, lands on 300.
       Narrowing the box via `left` rather than `right:0` + transform: this
       element's reveal animation (main.js) commits an inline
       `transform: translate(0px, 0px)` once it finishes, which would
       silently overwrite a transform-based shift — `left` isn't touched by
       that animation, so it holds after the animation completes. */
    /* 166 -> 186 -> 206px. Two corrections against the LIVE measured result
       rather than recalculating blind: moving `left` by +20 only moved the
       content's centre by +10 (280 -> 290), because justify-content:center
       splits any change to one edge of the box in half. So closing the last
       10px gap (290 -> target 300) needs another +20 on `left`, not +10. */
    left: 206px;
    right: 0;
    z-index: 10;
  }
  /* Heading above the badge row, reversing DOM order (badges come first in
     markup, heading second) on request.
     `order` only has an effect on a flex ITEM, set via its PARENT's
     display:flex — it does nothing set on the element itself, which was the
     mistake in the first pass here. .downloads-reorder-parent (added to the
     shared <div class="container"> in the markup) is what actually needs
     flex-direction:column; the two children then just carry order values.
     Scoped to this one container specifically, via its own class rather than
     a bare .container rule, because .container is reused everywhere else on
     the site and must not become flex globally. */
  .downloads-reorder-parent{
    display: flex;
    flex-direction: column;
  }
  .downloads-badges-row{
    order: 2;
    /* 0 -> 32px above, 0 -> 40px below (added to the section's own 44px
       floor). The heading and section-below gaps were both a flat 0/44 —
       tight above the buttons, only the section's shared rhythm below. Both
       widened per the marked lines. */
    margin-top: 32px;
    margin-bottom: 40px;
  }
  .downloads-heading{ order: 1; }
  /* One line taller gap between the resource cards, so a strip of true black
     shows between them rather than the cards' own glows meeting edge to edge.
     32px -> 56px: +24px is roughly one line of this page's 16px/1.6 body
     text (25.6px), which is the "about 1 line" asked for. Scoped to this one
     grid via .resources-cards-grid (added in the markup) rather than editing
     the shared .grid/.grid--3 rules, which shop.html's product grid also
     uses — a bare change there would have widened that grid's gap too. */
  .resources-cards-grid{ gap: 56px; }
}
@keyframes hero-ken-burns-resources{
  from{ scale: 1.03; }
  to{ scale: 1.152; }
}
/* Phone variant: departs from exactly 1 so the first frame is the full-screen
   image with no zoom applied at all. */
@keyframes hero-ken-burns-resources-mobile{
  from{ scale: 1; }
  to{ scale: 1.15; }
}
/* Below ~700px the 360px floor above overrides the 45vw ratio, making the box
   taller (more portrait) than the image — that flips `cover` to crop the
   sides instead of the bottom, clipping the headline and exposing the
   bottom icon row. Dropping the floor keeps the box's aspect ratio wide
   enough that the crop stays vertical-only, same as desktop. */
@media (max-width: 700px){
  .hero--resources-bg{
    min-height: calc(45vw + var(--header-h));
  }
}


@media (min-width: 960px){
  .hero__grid--daniel{ grid-template-columns: 1fr 1fr; }
  .hero__text-col{ display: flex; flex-direction: column; justify-content: center; padding: var(--space-8); }
}

/* Hero with video background and bokeh effect */
.hero--with-bg{
  position: relative;
  padding: calc(var(--header-h) + var(--space-8)) var(--space-6) var(--space-12);
  /* Bounded instead of a bare 80vh: real browser windows lose height to tabs,
     the address bar, and the OS taskbar in ways a fixed-size preview never
     does, and the bg video is object-fit:cover, so an uncapped vh height
     swings the crop hard - tall real windows zoom the footage in (cropping
     the sides to fill the extra height), short ones flatten it. Clamping
     keeps the crop consistent across realistic desktop window heights
     (~650-1300px) while leaving 80vh as the preferred value so nothing
     changes at the reference 900px-tall viewport this was designed against. */
  min-height: clamp(600px, 80vh, 780px);
  display: flex;
  align-items: center;
  overflow: hidden;
  color: var(--white);
  background: linear-gradient(180deg, #0A0908 0%, #0F0E0C 40%, #151310 85%, #1A1917 100%);
}
.hero__bg-video{
  position: absolute; inset: 0; z-index: 0;
  width: 100%; height: 100%;
  overflow: hidden;
  pointer-events: none;
}
.hero__bg-video video{
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: center;
}
.hero__bokeh-overlay{
  background: rgba(0, 0, 0, 0.15);
}
.hero__layout{
  position: relative; z-index: 2;
  display: flex;
  align-items: center;
  height: 100%;
}
.hero__text-column{
  width: 100%;
  max-width: 600px;
  padding: var(--space-6) var(--space-6) var(--space-6) 0;
  background: radial-gradient(circle 304px at 50% 45%, rgba(204,0,0,0.61) 0%, rgba(204,0,0,0.36) 45%, rgba(204,0,0,0) 75%);
  position: relative;
  z-index: -1;
}
@media (max-width: 700px){
  .hero__text-column{ background: none; }
}
.hero--daniel-bg .hero__text-column,
.hero--contact-bg .hero__text-column{
  position: relative;
  /* Resets the base rule's z-index:-1 (line ~593), which this override
     otherwise inherits unchanged. That -1 was fine when the glow WAS the
     column's own background (nothing to click sat behind it) - but here the
     glow moves to an isolated ::before instead, and a negative-z-index
     element paints behind its own static parent (the grid wrapper) even
     though nothing there is visible. Net effect: every link/button in this
     column - "See the Training Program" included - was rendered on top but
     hit-tested as if it were behind an invisible pane, so clicks silently
     landed on the parent grid instead of the button. z-index:0 keeps the
     column (and the button) at a normal paint level while isolation:isolate
     still confines the ::before glow's own z-index:-1 to just this column. */
  z-index: 0;
  background: none;
  isolation: isolate;
}
.hero--daniel-bg .hero__text-column::before,
.hero--contact-bg .hero__text-column::before{
  content: "";
  position: absolute;
  top: 50%; left: 50%;
  width: 800px; height: 800px;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: radial-gradient(circle, rgba(0,0,0,0.65) 0%, rgba(0,0,0,0.45) 40%, rgba(0,0,0,0) 70%);
  z-index: -1;
  pointer-events: none;
}
.hero__title--xlarge{
  font-family: var(--font-display);
  /* Desktop/tablet size reverted to the original at the user's request; the
     enlargement is kept only in the <=480px block below. */
  font-size: clamp(4.655rem, 17.48vw, 8.74rem);
  font-weight: 800;
  margin-bottom: calc(var(--space-6) * 0.95);
  line-height: 1.1;
  letter-spacing: 0.04em;
  word-spacing: -0.1em;
  text-transform: uppercase;
  color: var(--white);
  text-shadow: 0 4px 20px rgba(0,0,0,0.6);
}
.hero__subtitle--xlarge{
  font-family: "Playfair Display", "Inter", serif;
  font-size: clamp(1.442rem, 6.27vw, 2.508rem);
  font-weight: 600;
  margin-bottom: var(--space-6);
  margin-top: -0.8rem;
  margin-left: 0.375rem;
  opacity: 1;
  line-height: 1.2;
  letter-spacing: -0.005em;
  text-align: left;
  color: var(--white);
  filter: drop-shadow(0 0 12px rgba(0,0,0,0.9)) drop-shadow(0 0 24px rgba(0,0,0,0.7));
}
.title-word-white{
  color: var(--white);
  font-weight: 700;
  font-size: 1.22em;
  margin-right: -0.35em;
}
.title-word-red{
  color: var(--red);
  font-weight: 700;
  display: block;
  -webkit-text-stroke: 0.5px rgba(0,0,0,0.6);
  text-stroke: 0.5px rgba(0,0,0,0.6);
  margin-top: -0.35em;
}
.hero__subtitle--xlarge .brand-motto{
  color: var(--white);
  font-weight: 600;
  letter-spacing: 0.025em;
}
.hero__actions{
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  justify-content: flex-start;
  align-items: center;
  margin-top: var(--space-6);
}
/* The xlarge title's clamp (min 4.224rem) plus the white word's own 1.22em
   bump was tuned for tablet/desktop width — on a phone that combination
   runs "RIDESAFE" wider than its own column (measured 302px word in a 263px
   column at 375px viewport), spilling past center rather than actually
   overflowing the full viewport, which is what read as "off-centered."
   Below 480px: shrink both title and subtitle enough to fit the column
   width, tighten the column's side padding for more room, and center
   everything — title, subtitle, and the store-badge row — as one block.
   Placed after every rule it overrides (not just wrapped where first
   authored) since equal-specificity ties resolve by source order — an
   earlier position here was silently losing to the base rules below it
   regardless of viewport match. */
@media (max-width: 480px){
  /* Same padding reclaim as the >=481px rule, but tighter: at 375px every px
     of side padding costs 2px of usable word width, and the title is the
     widest thing on the page. */
  .hero--with-bg{ padding-inline: 0.625rem; }
  .hero--with-bg > .container{ padding-inline: 0.625rem; }
  .hero__text-column{
    padding-inline: 0.25rem;
    text-align: center;
  }
  .hero__title--xlarge{
    font-size: clamp(2.5rem, 18.5vw, 4.6rem);
  }
  /* This -0.35em margin-right exists to tuck the red word in tight against
     white on desktop's single-line inline flow. Red is display:block so it
     already drops to its own line regardless — but the negative margin still
     applies to the white word's trailing edge, which is exactly what was
     pulling "RIDESAFE" 17.6px off-center once it's the sole (centered)
     content on line one. */
  .title-word-white{ margin-right: 0; }
  .hero__subtitle--xlarge{
    font-size: clamp(1.1rem, 5vw, 1.6rem);
    margin-left: 0;
  }
  .hero__actions{ justify-content: center; }
}

/* Home hero, phones — framing tuned against a 393px Samsung screenshot with
   the target positions marked on it. Breakpoint is 600px, not the 480px block
   above, so every phone width is covered (iPhone Pro Max 430, large Android
   ~480) rather than only the narrow ones. Nothing here reaches tablets or
   desktop, which keep the 16:9 hero from the min-width:768px block.

   Screenshot mapped at 2.35x device-pixel-ratio; the hero's measured 681px CSS
   height matched the marked-up image, which is what the offsets below are
   derived from. */
@media (max-width: 600px){
  /* 1. Store badges down to the hero's foot.
        Adding top margin alone does nothing here, which is why earlier passes
        failed: the hero's height is content-driven at this size, so every px
        of margin above the badges pushed the hero's own bottom edge down by
        the same px and the gap beneath them stayed pinned at 176px.
        That 176px is three stacked bottom spacings, and collapsing them is
        what actually moves the badges down:
            .hero__actions  margin-bottom  48px
            .hero__text-column padding-bottom 32px
            .hero           padding-bottom  96px
        Zeroing the first two and trimming the hero's own to 2.5rem leaves the
        badges sitting just above the fold with deliberate breathing room,
        and lets the hero settle back onto its 681px min-height instead of
        being stretched past it. */
  /* Headline to the top of the hero, badges still pinned to the foot.
     margin-top:auto on the badges is what separates the two: it swallows all
     the leftover height between the text and the badges, so the text sits
     wherever its own padding-top puts it and the badges stay at the bottom
     regardless of screen height. (align-items:flex-end was used before and is
     wrong for this — it drags the headline down with the badges.)
     Every wrapper between the hero and .hero__actions has to be a stretched
     flex column or the auto margin has no full-height track to work against. */
  .hero--with-bg .hero__actions{
    margin-top: auto;
    /* 80px (4 lines) lifted up, then back down 2 lines (~40px), up another
       20px to a marked line, now back DOWN 20px to the latest one — the
       badges are pinned to the hero's foot by the auto margin above, so this
       bottom margin is the only thing that sets their height in the frame.
       Smaller value = lower on screen. */
    margin-bottom: 40px;
  }
  .hero--with-bg > .container{
    flex: 1;
    display: flex;
    flex-direction: column;
    /* .hero__layout carries height:100%, which defeats this layout twice over:
       the percentage resolves against the hero's min-height — indefinite, so it
       collapses to content height — and an explicit height also opts the item
       out of align-items:stretch. Back to auto, stretch fills the hero's full
       content box, which is the track .hero__actions' auto margin needs. */
    height: auto;
  }
  .hero--with-bg .hero__text-column{
    flex: 1;
    display: flex;
    flex-direction: column;
    /* Zero: the hero's own padding-top (below) is what positions the wordmark
       now. Two stacked paddings just made the offset hard to reason about —
       the title was landing at 101px + 43px = 144px when 48px was wanted. */
    padding-top: 0;
    padding-bottom: 0;
    /* Slides the wordmark and tagline ~10px nearer the edge. The 24px they
       started at was three nested left paddings stacked (hero 10 + container
       10 + column 4); dropping the inner two to 4 and 0 leaves 14px, which
       reads as a deliberate margin rather than an indent. Both lines move
       together since they already share one left edge. */
    padding-left: 0;
  }
  /* flex-end + this padding is what fixes the badges' position: the content
     block is pinned to the hero's foot, so the badge row lands a known 2.5rem
     above the bottom edge instead of wherever vertical centring happened to
     leave it. With the spacings above collapsed the hero sits back on its
     681px min-height, so that edge is stable across phones. */
  .hero--with-bg{
    /* 101px -> 78px. The base value pushed the wordmark a third of the way down
       the hero on a phone. 48px was tried and sat too tight under the 53px
       header — the wordmark read as crammed against it. 78px keeps the block
       high while leaving a deliberate breath below the header.
       78px -> 195px -> 291px: the marked line landed exactly where the
       "THAILAND" line began, i.e. one full line-height (measured 95.8px)
       below where "RIDESAFE" was sitting — so the whole block moves down by
       that one line, putting RIDESAFE's top edge on the mark. */
    /* 77px -> 70px: the latest mark sat ~64% of the way from the header's
       bottom edge (53px) to the wordmark's cap top (73px), i.e. ~66px, so the
       block lifts 7px. Title and subtitle move together because both sit in
       this container's normal flow. */
    padding-top: 70px;
    /* 2.5rem -> 0.75rem drops the badge pair ~28px further down the frame,
       uncovering the band of street bokeh they were sitting over. This is the
       whole available travel: the badges are pinned to the hero's foot by
       .hero__actions' auto margin, so the only thing between them and the
       bottom edge is this padding, and 12px is about as tight as it can go
       before they read as falling off the fold. */
    padding-bottom: 0.75rem;
    align-items: stretch;
  }

  /* 2. Rider's head up to the marked line (~89px, just under the header).
        object-fit:cover on a portrait box scales the 16:9 video to fill the
        HEIGHT exactly — zero vertical overflow — so object-position-y is inert
        here and cannot move him. The wrapper's existing scale:1.08 is the only
        vertical slack there is: at the default centre origin it crops ~27px off
        the top and ~27px off the bottom. Re-anchoring that same zoom to the
        bottom edge puts the whole 54px on top instead, lifting the frame ~27px
        and bringing the helmet to the mark. No extra zoom is introduced, so
        the rider is not made smaller. */
  /* Y origin 38%, not the 100% used before. The wrapper's scale:1.08 on a 681px
     hero produces ~54px of vertical overflow, and the origin decides how much
     of that is taken off the TOP: at 100% the whole 54px came off the top,
     lifting the frame as far as it goes. 38% takes only ~20px off, letting the
     footage sit ~34px (≈5%) lower — enough to bring the full visor and eyes
     below the wordmark while the headline still clips just the crown of the
     helmet. Keeping some zoom (rather than dropping to scale:1) also keeps the
     street bokeh filling the lower frame instead of thinning out. */
  .hero--with-bg .hero__bg-video--reveal{
    /* Origin pinned to the TOP edge, which is what makes the chin position
       controllable at all. Vertically the video is not cropped by
       object-fit:cover (16:9 footage in a portrait box overflows sideways,
       fills height exactly), so there is no unseen footage above the helmet
       to slide down into view — sliding the layer down just uncovers the
       hero behind it. Scaling from the top edge instead keeps that edge
       flush while pushing everything below it further down, so the chin
       moves down as the scale grows — but 1.13 visibly enlarged the rider,
       which was worse than the framing it bought, so it's back to 1: no
       zoom, the rider at his true size, chin wherever the untouched frame
       puts it. */
    scale: 1;
    transform-origin: 50% 0;
    /* Pure vertical nudge on top of the existing origin/scale framing, per
       "bump it up, don't move it horizontally." translate applies BEFORE scale
       in the individual-transform-property compositing order, so a raw -21px
       here would only end up ~19px on screen once the 1.08 scale multiplies
       it — -19.5px is the pre-scale value that lands a true -21px visual
       shift, which is what moved the helmet's crown up to the marked line. */
    /* !important: main.js runs a scroll-parallax script that writes
       element.style.translate inline on this layer (js/main.js:478) — an
       inline style beats any stylesheet rule without !important, which is why
       the first pass here had no visible effect at all. This freezes the
       layer at the requested position regardless of scroll, the same
       trade-off the desktop version of this hero already makes (see the
       min-width:768px block below, which fully zeroes translate for the same
       reason). */
    /* -41px -> -32px: -41 pushed the frame up far enough that its bottom edge
       lifted 9px clear of the hero's own bottom edge, exposing a grey gap
       below the footage (the zoom only had ~46px of bottom overhang to spend,
       and three rounds of "bump the helmet up" used more than that). -32px is
       the largest upward shift the current zoom can take before that gap
       reopens — video bottom now sits flush with the hero bottom, at the cost
       of a bit more empty sky above the helmet than the last two passes had. */
    /* Horizontal: ~5% right was asked for, but scale:1.08 on a 393px-wide
       frame only banks ~15.7px of overscan on each side (393 * 0.08 / 2), and
       translate lands pre-scale (see above — it's multiplied by 1.08 same as
       the vertical value), so 14.5px here is the largest rightward shift the
       existing zoom can take before the left edge runs out of overscan and a
       background gap opens up — works out to ~4%, just under the ask. */
    /* -32px -> +8px: dropped ~43px on screen (the pre-scale value is
       multiplied by the 1.08 zoom) to bring the helmet's crown down to the
       marked line. Both edges stay covered — at this offset the frame's top
       still sits ~13px above the hero's top and its bottom ~41px below, so
       neither a top nor a bottom gap opens. */
    /* Both axes zero now that scale is back to 1: with no zoom there is no
       overhang on any edge to spend, so ANY translate slides the layer off
       the hero and exposes a bare strip behind it. The rightward framing
       moves to object-position below instead, which costs no zoom. */
    translate: 0 !important;
  }
  /* The ~4% rightward shift, re-done the free way. object-fit:cover renders
     this 16:9 footage 1210px wide inside a 393px box, so ~818px of it is
     already cropped off horizontally — moving the crop window 1.8% left
     shifts the visible image ~15px right, matching the old translate exactly
     but using footage that was being thrown away rather than zoom. */
  /* 48.2% -> 49.95%, computed not eyeballed. object-fit:cover renders this
     1280x720 footage at 1514px wide inside the 393px box (851px tall box,
     scaled to cover height), so there's 1121px of horizontal overhang to pan
     across. Shifting a fixed point in the footage LEFT on screen by X needs
     object-position-x increased by X/1121 — for the requested ~5% of the
     393px box (19.65px), that's +1.75 percentage points off the existing
     48.2%. (Increasing this value pans the crop window rightward across the
     source, which is what moves any fixed subject left on screen — confirmed
     against this file's own note that DEcreasing it below 48.2% is what
     scrolls the rider off-frame, the opposite direction.) */
  .hero--with-bg .hero__bg-video--reveal video{ object-position: 49.95% center; }

  /* 3. Title trimmed 18.5vw -> 16.6vw (~10%) so its right edge clears the
        rider's visor. This is the only lever that works for that: the rider
        sits at the RIGHT edge of the video's framing, so panning the footage
        to uncover him (object-position-x below 50%) scrolls him off screen
        entirely — tested at 42/35/28%, and by 35% he is gone and only bokeh
        remains. Shortening the wordmark instead opens the gap over his face
        while keeping him framed exactly where he is. */
  /* 16.6vw -> 18.3vw (+10%): grown back out to the marked bracket. Measured
     off the annotation relative to the wordmark itself (scale-independent):
     the vertical mark sat ~7% past "RIDESAFE"'s right edge and the horizontal
     one ~12% below the block's bottom, so ~10% splits the two. All three
     clamp values scale by the same factor to keep the fluid behaviour and the
     wordmark's own proportions intact. Right edge lands ~320px inside the
     ~349px column, so it still clears the rider's visor. */
  .hero__title--xlarge{
    font-size: clamp(2.75rem, 18.3vw, 5.06rem);
    /* Pulled way back in, hard, on direct instruction: the previous 54px
       full-opacity glow was wide enough to reach past the wordmark and darken
       the rider's face and eyes right next to it, which must never happen —
       readability on the letters is not worth obscuring the shot. 10px at
       reduced opacity is sized to stay ON the glyphs themselves; even the
       tallest letter strokes are ~70-80px at this clamp's largest size, so a
       10px blur's falloff is spent well before it reaches the rider a few
       characters away. Two thin layers instead of four heavy ones, opacity
       capped at 0.6, and the vertical-offset layer dropped entirely — that
       one existed to add downward weight, which is exactly the direction that
       was creeping toward the helmet. */
    text-shadow: 0 0 10px rgba(0,0,0,0.6), 0 0 4px rgba(0,0,0,0.55);
  }
  /* "THAILAND™" specifically. It reads worse than "RIDESAFE" because the token
     red is rgb(204,0,0) — a DARK red, so against dark bokeh the two are close
     in luminance and piling on more black actually shrinks that gap rather
     than widening it. Two changes that work together:
       1. A brighter red (#FF2020) — this is what actually makes the word
          legible, by separating it from the background in luminance instead
          of just outlining it. Scoped to phones; desktop keeps the token.
       2. Stacked black passes at SMALL radii (max 9px) rather than one wide
          one. Repeating a blur compounds its density without extending its
          reach, so the backing gets much darker while its falloff still
          finishes ~9px out — the rider's face sits far beyond that, so it
          cannot be touched. The wide radii are what put shading on her face
          before; those stay gone. */
  .title-word-red{
    color: #FF2020;
    text-shadow:
      0 0 9px rgba(0,0,0,0.95),
      0 0 9px rgba(0,0,0,0.75),
      0 0 4px rgba(0,0,0,1),
      0 1px 2px rgba(0,0,0,1);
  }

  /* 4. Subtitle: ~6% larger (5vw -> 5.3vw), nudged right to sit under the
        title's left edge rather than 8px outside it, and pulled up tight to
        the wordmark. The title is centred while this line is left-aligned, so
        their left edges only agree by explicit offset. */
  /* Left-align the whole text block rather than nudging the subtitle with a
     margin. The title is centred, so the offset needed to line the subtitle up
     under it is (containerWidth - titleWidth)/2 — which measured 14px at 360px
     and 44px at 480px, because the title's clamp caps its growth while the
     container keeps widening. No single margin value can track that. Sharing
     one left edge makes the two agree exactly at every width, for free. */
  .hero__text-column{ text-align: left; }
  .hero__subtitle--xlarge{
    /* +10%: 1.17/5.3/1.7 -> 1.29/5.83/1.87. Scaling all three clamp numbers by
       the same factor keeps the fluid sizing behaviour (still scales with
       viewport width between the same relative floor/ceiling), just 10% larger
       throughout the range rather than only at one width. */
    font-size: clamp(1.29rem, 5.83vw, 1.87rem);
    /* 8px, not 0: shared-left-edge alignment put the "S" 6px left of
       "THAILAND"'s T-centre (measured S-centre 26px vs T-centre 34px at
       375px) — close, but the ask was specifically S-centre-under-T-centre,
       not shared left edges. 8px closes that gap. */
    margin-left: 8px;
    /* 0.75rem -> 0.64rem (~15% less), then another 20% (0.54->0.43rem), then a
       further 10% (0.43->0.39rem), then a further 30% (0.39->0.27rem): each
       pass pulled the subtitle tighter under the (locked) title without
       moving the title itself. Now negative: the mark sat ~15px above the
       subtitle's current top (measured box top 476px), which is past the
       point where a zero gap alone gets there — the line has to overlap up
       into the title's own bottom leading, which is empty space below
       "THAILAND"'s descender, so nothing actually collides. */
    margin-top: -19px;
    /* 700 weight stays — Playfair Display's thin serif strokes read pale over
       busy footage at lighter weights.
       Shadow pulled back in with the title's, same reason: this line sits
       directly against the rider's visor, and the old 24px full-opacity glow
       was bleeding straight onto it. 6px is scaled to this text's own size
       rather than borrowed from the title, so it stays a contrast aid hugging
       the strokes instead of a dark patch spreading toward the helmet. */
    font-weight: 700;
    color: #fff;
    text-shadow: 0 0 6px rgba(0,0,0,0.55), 0 0 2px rgba(0,0,0,0.5);
  }
  /* The badge row is centred independently of the text above it, so
     left-aligning the column must not drag it to the edge. Row layout itself
     is set further down, alongside the other two heroes' badge rows — at
     134px badges now fit one line, and the explicit request became "side by
     side," reversing the earlier stacked-column version of this rule. */
  .hero--with-bg .hero__actions{ justify-content: center; }

  /* Store badges ~15% smaller again (158px -> 134px; ~24% down from the
     original 176px), on every page that shows them — home, training (both its
     hero AND the below-the-fold "No Cost, No Catch" CTA), and downloads.
     Each <img> carries an inline width:176px, so this needs !important to
     land; height stays auto so the pair shrinks in place.
     This size is also what fixes the downloads hero specifically: at 158px
     the pair (158+158+16=332px) no longer fit the 327px available inside that
     hero's own padding, so the row silently WRAPPED and the two badges stacked
     vertically instead of sitting side by side. 134px (134+134+12=280px)
     clears that with room to spare. */
  /* 134px -> 150px. "Double" (268px) was the literal ask but is a hard
     impossibility here: two 268px badges plus a gap need 548px, and no phone
     screen is that wide (this hero's own available row width measures 331px
     at 375px viewport). 150px is the largest size that still guarantees both
     badges fit on one line without wrapping back to a stack, checked against
     the narrowest phone this site supports (360px) as well as this one. */
  /* 168px: with the row's 12px gap and the hero's own side padding, that's
     the largest size that still keeps both badges on one line at the A55's
     393px width (2*168 + 12 = 348px against a ~349px row) — the max the
     side-by-side constraint allows. */
  .app-badge-link img{ width: 168px !important; }

  /* Force every hero badge row to a single line, never wrapping to a stack.
     Each of the three heroes builds this row differently (home has no wrapper
     div, training's is nested inside .hero__actions, downloads' lives in
     .hero-badges-mobile), so each needs its own selector — but all three get
     the same nowrap + tighter gap treatment. */
  .hero-badges-mobile > div{
    flex-direction: row !important;
    flex-wrap: nowrap !important;
    gap: 0.75rem !important;
  }
  /* Training hero back to the stacked pair, matching the home hero exactly:
     same column direction, same 0.75rem gap, same 176px badge width — so the
     two heroes read as one treatment rather than two. Freed from sharing a
     row, these are no longer bound by the 168px side-by-side ceiling. */
  .hero--training .hero__actions > div{
    flex-direction: column !important;
    flex-wrap: nowrap !important;
    align-items: center !important;
    gap: 0.75rem !important;
  }
  .hero--training .app-badge-link img{ width: 176px !important; }
  /* Home hero restacked to a vertical pair, per the follow-up ask (it was in
     the shared row rule above until now). */
  .hero--with-bg .hero__actions{
    flex-direction: column !important;
    flex-wrap: nowrap !important;
    align-items: center !important;
    gap: 0.75rem !important;
  }
  /* Back to the original 176px the markup asks for inline. The 134/150/168px
     ladder above only ever existed to squeeze both badges onto one line; once
     they're stacked that ceiling is gone, so the home hero opts out of it.
     The other two heroes still share a row and keep the 168px cap. */
  .hero--with-bg .app-badge-link img{ width: 176px !important; }

  /* Training hero — same locked-to-the-fold badge placement as the home hero,
     arrived at differently because the failure mode differs. This section is
     display:BLOCK with min-height:100vh, so its content stacks from the top and
     simply stops, stranding 344px of empty image below the badges (the home
     hero was flex and content-driven instead).
     The gap beneath the badges is the section's own inline
     padding-bottom:var(--space-10) (64px) — left to govern rather than restated
     here, since an inline value can only be beaten with !important and 64px is
     the spacing we want anyway.

     justify-content:flex-end is deliberately NOT used: it pushes the whole
     stack down, carrying the eyebrow, headline and subtitle with it. Stretching
     the wrappers and giving only .hero__actions margin-top:auto moves the badges
     alone — the auto margin absorbs the leftover space above them, so the text
     keeps its original position at the top. */
  .hero--training{
    display: flex;
    flex-direction: column;
    /* svh instead of the base rule's 100vh, so the fill matches what is
       actually on screen at load rather than the address-bar-retracted
       height. Same change applied to every mobile hero. */
    min-height: 100svh;
  }
  /* Home hero (video background) on the same footing. */
  .hero--with-bg{ min-height: 100svh; }
  /* Both wrapper levels have to stretch and become columns, not just the outer
     one: .hero__actions lives inside .container--narrow, so an auto margin only
     reaches the hero's full height if every ancestor between them is itself a
     stretched flex column. Setting it on .container alone left the badges
     floating mid-hero. */
  .hero--training > .container,
  .hero--training > .container > .container--narrow{
    flex: 1;
    display: flex;
    flex-direction: column;
  }
  /* Drops the eyebrow/headline/subtitle group so the subtitle lands on the
     marked line. Padding on the stretched wrapper, not a margin on the text:
     the badges below hold their place off .hero__actions' margin-top:auto,
     which simply absorbs whatever space is left over — so pushing the text
     down moves the text alone and the badges stay locked to the foot. */
  /* 3rem -> 4.75rem -> 6.4rem -> 5.15rem: two drops down, then back up 20px
     onto the latest mark. Same reasoning as above — the badges are pinned by
     the auto margin, so this moves the text group only. */
  .hero--training > .container > .container--narrow{ padding-top: 5.15rem; }
  .hero--training .hero__actions{
    margin-top: auto;
    margin-bottom: 0;
  }

  /* Drop the hero artwork ~15% vertically.
     background-position-y alone cannot do this: at plain `cover` the 1.6-ratio
     photo scales to fill this much taller box by its HEIGHT, so vertical
     overflow is exactly 0 and any Y value is inert (the same reason the inline
     "5%" in training.html has never done anything on a phone).
     Scaling to 115% of the box height creates 15% of slack, and pinning Y to
     0% anchors the image's top edge to the box top — so everything below it,
     the rider included, is pushed down by that 15%. X stays on the inline 84%
     that frames the rider; both longhands carry !important because the inline
     `background-position` shorthand would otherwise win outright. */
  .hero--training .hero__bg-fade--training{
    background-size: auto 115% !important;
    background-position-x: 84% !important;
    /* 0% -> 52px, vertical only (position-x untouched). Image content lowered
       by roughly two lines. Using a px length rather than a % here because at
       115% height the box only has a 15% (~138px) overhang budget and it was
       ALL sitting below the box at 0% (top-anchored, nothing left to give at
       the top) — a % increase moves the visible window the wrong direction
       (content shifts UP as % rises), so there was no percentage that could
       push it down further. A length sets the image's top edge directly:
       52px moves it down while comfortably inside the 138px overhang, so the
       box stays fully covered with no gap at the bottom. */
    background-position-y: 52px !important;
  }
  /* Eyebrow lifted onto the marked line, independent of the title/subtitle
     block below it — those are explicitly not part of this move. The shared
     padding-top on .container--narrow (below) positions the WHOLE text group
     together, so changing it would drag the title up too; a relative offset
     scoped to just the eyebrow moves this line alone. Horizontal centring
     was already correct (content measured centred at x=196 of 393, i.e. dead
     centre) — .hero__eyebrow's own justify-content:center on this hero
     already accounts for that, so nothing needed there. */
  /* SHORT-SCREEN GUARD. The training hero's spacing (base hero padding +
     5.15rem on the narrow container + the badges' auto margin) was tuned
     against the 851px preview. Its content only needs ~393px, but the fixed
     padding around it totalled ~335px — fine at 851, but at a real phone's
     ~640px usable height that summed to a 728px hero, i.e. 88px taller than
     the screen, which is what pushed the download badges out of view.
     Scoped to max-height so tall phones keep the approved framing untouched
     and only cramped viewports get the tighter spacing. */
  @media (max-height: 780px){
    .hero--training > .container > .container--narrow{ padding-top: 1.5rem !important; }
    .hero--training{
      padding-top: calc(var(--header-h) + 0.75rem) !important;
      padding-bottom: 1rem !important;
    }
    /* The subtitle is the biggest single block (4 lines, 134px); trimming it
       here buys the badges their room back without touching the headline. */
    .hero__desc--training{ font-size: 18px !important; }
  }
  /* Absolute against the hero, for the same reason as contact/about: the old
     relative -140px was measured back from the text group, so the moment the
     short-screen guard above changes the padding, that offset overshoots —
     it put the tag at -51px (off the top of the screen) at 640px height.
     Anchoring to the hero fixes its distance from the header at every
     viewport height instead. */
  .hero--training > .container,
  .hero--training > .container > .container--narrow{ position: static !important; }
  .hero--training .hero__eyebrow{
    position: absolute;
    top: calc(var(--header-h) + 8px);
    left: 0;
    right: 0;
    margin: 0;
    z-index: 10;
  }
  .hero--training .hero__eyebrow--superseded{
    position: relative;
    /* -160 -> -140px. The natural (unshifted) position drifted 20px between
       checks (219px -> 199px) — a font-loading/metrics timing difference,
       not a layout change — which pulled the eyebrow 20px further up than
       intended and tucked it behind the 53px header (top landed at 39,
       invisible against the black header bar). Re-measured fresh rather
       than reusing the old offset: -140px now lands top=59, 6px clear of
       the header, matching where it was meant to sit. */
    top: -140px;
  }
}
.app-store-badge{
  display: flex;
  align-items: center;
  justify-content: center;
  text-decoration: none;
  transition: transform var(--dur-base) var(--ease-out), filter var(--dur-base) var(--ease-out);
  height: 52px;
  width: 180px;
  min-width: 180px;
  border-radius: 6px;
  overflow: visible;
  flex-shrink: 0;
  background: transparent;
}
.app-store-badge svg{
  display: block;
  width: 100%;
  height: 100%;
  max-width: 100%;
  max-height: 100%;
}
.app-store-badge:hover{
  transform: translateY(-4px);
  filter: drop-shadow(0 12px 24px rgba(0,0,0,0.4));
}
.app-store-badge:active{
  transform: translateY(-1px);
}
.app-store-badge--google{
  background: transparent;
}
.app-store-badge--apple{
  background: transparent;
}
@media (max-width: 600px){
  .hero-redesigned__actions{
    flex-direction: column;
    width: 100%;
  }
  .store-badge{
    width: 100%;
    justify-content: center;
  }
}

.hero__media{ position: relative; }
.hero__frame{
  position: relative; aspect-ratio: 4 / 5; border-radius: var(--radius-lg); overflow: hidden;
  background: linear-gradient(155deg, var(--red-dark), var(--black) 70%);
  border: 1px solid var(--border-dark); box-shadow: var(--shadow-lg);
}
.hero__frame img{ width: 100%; height: 100%; object-fit: cover; }
/* Full-bleed edge-to-edge on phones: .grid--2 is a single column here, so the
   frame's only constraint is the .container's 24px side padding. Breaking out
   to 100vw and pulling back with a negative margin escapes that padding
   without touching anything above 768px. aspect-ratio (set per-instance,
   inline) keeps doing its job unaffected — height still derives from width,
   so the ratio holds automatically at the new, wider width. Border/radius/
   shadow are dropped too: a card frame's rounded corners and border read as
   a mistake once the image is flush with the screen edges. */
@media (max-width: 767.98px){
  .hero__frame{
    width: 100vw !important;
    margin-inline: calc(50% - 50vw) !important;
    border-radius: 0 !important;
    border: 0 !important;
    box-shadow: none !important;
  }
}
.hero__frame--cutout{ background: radial-gradient(80% 70% at 50% 20%, rgba(204,0,0,0.5), var(--black) 75%); }
.hero__frame--cutout img{ object-fit: contain; object-position: bottom center; }
/* Soft top/bottom fade so a full-bleed banner blends into the dark section
   above/below it instead of cutting off hard. Pseudo-elements sit above the
   img (any position:absolute layer paints over non-positioned in-flow
   content regardless of z-index) and fade from the section's near-black to
   transparent. */
/* Heights are PERCENTAGES of the frame, not a fixed 70px. A fixed height is a
   trap here: the banner is a fixed aspect ratio, so at 360px wide the frame is
   only ~127px tall and a 70px fade swallowed 55% of it — the fade was far
   heavier on phones than on desktop, where it was 23%.
   The bottom fade is also deliberately much shorter than the top one. This
   banner carries a yellow caption bar across its bottom 12% (measured: rows
   614-698 of 698), and black caption text on yellow is destroyed by even
   moderate shading. The top 71% of the image is empty sky and beach, so the
   top fade can be generous; the bottom one only has room to soften the edge
   itself without reaching up into the caption. */
.hero__frame--edge-fade::before,
.hero__frame--edge-fade::after{
  content: ""; position: absolute; left: 0; right: 0;
  z-index: 1; pointer-events: none;
}
.hero__frame--edge-fade::before{
  top: 0; height: 20%;
  background: linear-gradient(180deg, var(--black) 0%, rgba(18,17,16,0) 100%);
}
.hero__frame--edge-fade::after{
  bottom: 0; height: 7%;
  background: linear-gradient(0deg, rgba(18,17,16,0.8) 0%, rgba(18,17,16,0) 100%);
}
/* A banner sitting directly on top of a .section--alt needs its BOTTOM fade to
   land on that section's opening colour (#151310), not on --black (#121110).
   The 3-value gap between those two is small but lands on a hard edge, so it
   reads as a hairline across the full width. */
.hero__frame--edge-fade-onalt::after{
  background: linear-gradient(0deg, rgba(21,19,16,0.8) 0%, rgba(21,19,16,0) 100%);
}
/* Breaking-news banner on phones. Two changes, both about the yellow caption
   strip along its bottom edge being unreadable at this size.
   1. Narrower aspect-ratio (1785/698 -> 1642/698) crops the sides further.
      The <img> is object-fit:cover, so the box's ratio is what decides the
      crop: a narrower box means the 1983x698 source has to scale up to keep
      covering, which enlarges the caption text ~17% and trims ~34px more off
      each side. Height grows 154px -> 167px and nothing is lost vertically —
      cover fits the height exactly at this ratio.
   2. Bottom fade off entirely. The caption occupies the source's bottom ~12%
      and runs right to the edge, so the 7% fade was always sitting on top of
      the text — there is no shorter value that clears it. The top fade stays;
      it covers only sky and palms. */
@media (max-width: 767.98px){
  /* 1642 -> 1432. Derived from the marked lines rather than guessed: at the
     previous ratio the box showed 1641px of the 1983px source, so the marks at
     display x=15 and x=358 correspond to keeping ~1432px of it. Setting the
     ratio's width term to that number makes exactly that region fill the 393px
     box — 27.8% now cropped off the sides, up from 17.2%, and the caption text
     renders ~38% larger than it did originally. Height follows to ~192px,
     which is the point: the box has to get taller for a wider crop to fit
     without squashing, and height is unconstrained here. */
  .hero__frame--edge-fade-onalt{
    aspect-ratio: 1385 / 698 !important;
    /* Kills the hairline that flickers along this banner's edges while it
       scroll-reveals. aspect-ratio on a 393px width resolves to a fractional
       height (191.58px), and [data-reveal] animates translateY — so during the
       transition the element is a composited layer sitting on fractional
       coordinates, and its half-pixel edge row gets antialiased against
       whatever is behind it. With the frame's inline background:transparent
       that backdrop is the page, so the blended row reads as a visible line.
       An opaque background matching the section above means the row blends
       image-to-#121110 instead, which is exactly the colour already at that
       edge (the top fade starts on the same value), so there is nothing left
       to see. backface-visibility pins it to one rasterisation pass, which
       stops the edge shifting as the transform animates. */
    background: #121110 !important;
    backface-visibility: hidden;
    /* Breathing room between the checklist above and this banner. The inline
       margin-top is 2.5rem, hence !important. */
    margin-top: 4.25rem !important;
  }
  /* The extra crop taken by 1432 -> 1385 has to come off the RIGHT edge only,
     but aspect-ratio alone crops symmetrically. object-position places the
     window inside the croppable overflow: 46% leaves the left edge exactly
     where it was (275px into the source) and spends the whole new 47px on the
     right, which is the sliver the mark pointed at. */
  /* !important: the <img> carries an inline object-position:center, which a
     plain stylesheet rule cannot beat. */
  .hero__frame--edge-fade-onalt img{ object-position: 46% center !important; }
  .hero__frame--edge-fade-onalt::after{ display: none; }

  /* "How It Works" belongs to the video below it, not the banner above.
     It was sitting left-aligned and roughly equidistant between the two, so
     it read as attached to neither.
     Centring needs BOTH properties: the h2 is display:inline-block, so
     text-align only centres the glyphs inside a box that still hugs the text
     at the left edge — margin-inline:auto is what centres the box itself, and
     with it the base h2 rule's red radial glow, which paints across that box.
     The asymmetric padding does the grouping: the h2's own 32px top/bottom
     padding is glow spread, not spacing, so trimming the bottom to 8px pulls
     the video up tight underneath while the top padding keeps it clear of the
     banner. */
  [data-i18n="training.videoIntro.title"]{
    display: block;
    text-align: center;
    margin-inline: auto;
    width: fit-content;
    /* Padding MUST stay symmetric. The red glow is a radial-gradient painted
       across this box with its centre at 50% 50%, so the box's midpoint is
       the glow's midpoint — the previous 40px/8px split put 32px more box
       above the text than below it, which is exactly how far off-centre the
       glow read. Equal padding puts the text at the box's true centre.
       Spacing is therefore done with MARGIN, which sits outside the painted
       box and so cannot drag the glow off the text. */
    padding-block: 30px;
    /* The 50% increase, as margin: banner-to-text goes 84px -> 126px, then
       +24px more on the follow-up ask -> 150px. */
    margin-top: 76px;
    margin-bottom: 0;
  }
  /* The .section-head wrapper carries an inline margin-bottom:0.5rem; zeroing
     it keeps the heading tight to the video, which the extra top spacing
     above would otherwise unbalance. */
  .section-head:has(> [data-i18n="training.videoIntro.title"]){ margin-bottom: 0 !important; }
}
.hero__badge{
  position: absolute; bottom: var(--space-5); left: var(--space-5); right: var(--space-5);
  background: rgba(18,17,16,0.72); backdrop-filter: blur(8px);
  border: 1px solid var(--border-dark); border-radius: var(--radius-md);
  padding: var(--space-4); display: flex; align-items: center; gap: var(--space-3);
}
.hero__badge svg{ flex-shrink: 0; color: var(--yellow); }
.hero__badge strong{ font-family: var(--font-display); text-transform: uppercase; font-size: var(--text-sm); letter-spacing: 0.04em; }
.hero__badge p{ font-size: var(--text-xs); color: var(--text-on-dark-muted); margin: 0; }
.placeholder-tag{
  position: absolute; top: var(--space-4); right: var(--space-4);
  background: var(--yellow); color: var(--black); font-family: var(--font-mono);
  font-size: 0.65rem; letter-spacing: 0.06em; text-transform: uppercase;
  padding: 4px 10px; border-radius: var(--radius-full); font-weight: 700;
  z-index: 5;
}

/* ---------- Cards ---------- */
.card{
  background: linear-gradient(135deg, rgba(255,255,255,0.03) 0%, rgba(255,255,255,0.01) 100%);
  border-radius: var(--radius-lg); padding: var(--space-6);
  border: 1px solid var(--border);
  box-shadow: 0 4px 16px rgba(0,0,0,0.2), inset 0 1px 0 rgba(255,255,255,0.05);
  /* transform/background stay on the calmer shared --dur-base timing; the red
     glow gets its own much snappier transition below so it reads as a quick
     neon flicker-on rather than fading in with everything else. */
  transition: transform var(--dur-base) var(--ease-out),
              background var(--dur-base) var(--ease-out),
              box-shadow 380ms var(--ease-out),
              border-color 380ms var(--ease-out);
  position: relative;
  overflow: hidden;
}
.card::before{
  content: ""; position: absolute; inset: 0;
  background: radial-gradient(circle at var(--mouse-x, 50%) var(--mouse-y, 50%), rgba(204,0,0,0.1) 0%, transparent 70%);
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-out);
  pointer-events: none;
}
/* Neon glow: brightness doubled (0.25 -> 0.5 alpha on the outer glow, 0.3 ->
   0.6 on the rim) and blur radius up 5% (32px -> 33.6px), on the 380ms
   box-shadow transition above so the "0 to very bright" jump lands in well
   under a second. :focus-visible mirrors :hover so keyboard selection gets
   the same treatment ("hovered over or selected"). */
.card:hover,
.card:focus-visible{
  transform: translateY(-8px) scale(1.02);
  box-shadow: 0 0 0 1px rgba(204,0,0,0.6), 0 12px 33.6px rgba(204,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.08);
  border-color: rgba(204,0,0,0.6);
}
.card:hover::before,
.card:focus-visible::before{ opacity: 1; }
.card__icon{
  width: 52px; height: 52px; border-radius: var(--radius-md);
  background: linear-gradient(135deg, rgba(204,0,0,0.2) 0%, rgba(204,0,0,0.05) 100%);
  display: flex; align-items: center; justify-content: center; color: var(--red); margin-bottom: var(--space-4);
  transition: all var(--dur-base) var(--ease-out);
}
.card:hover .card__icon{
  background: linear-gradient(135deg, rgba(204,0,0,0.3) 0%, rgba(204,0,0,0.1) 100%);
  transform: scale(1.1) rotate(5deg);
}
.card h3{ margin-bottom: var(--space-2); font-size: var(--text-lg); transition: color var(--dur-base) var(--ease-out); }
.card:hover h3{ color: var(--red); }
.card p{ color: var(--text-muted); font-size: var(--text-sm); transition: color var(--dur-base) var(--ease-out); }
.card:hover p{ color: var(--text); }

.stat-block{
  text-align: center; padding: var(--space-6) var(--space-4);
  background: linear-gradient(135deg, rgba(255,255,255,0.02) 0%, transparent 100%);
  border-radius: var(--radius-md);
  border: 1px solid var(--border);
  transition: all var(--dur-base) var(--ease-out);
  position: relative;
  overflow: hidden;
}
.stat-block::before{
  content: ""; position: absolute; inset: 0;
  background: radial-gradient(circle at var(--mouse-x, 50%) var(--mouse-y, 50%), rgba(204,0,0,0.08) 0%, transparent 70%);
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-out);
  pointer-events: none;
}
.stat-block:hover{
  background: linear-gradient(135deg, rgba(255,255,255,0.05) 0%, rgba(255,255,255,0.01) 100%);
  transform: translateY(-4px);
  box-shadow: 0 8px 24px rgba(204,0,0,0.15);
  border-color: rgba(204,0,0,0.2);
}
.stat-block:hover::before{ opacity: 1; }
.stat-block b{
  display: block; font-family: var(--font-mono); font-size: var(--text-2xl); color: var(--red);
  transition: all var(--dur-base) var(--ease-out);
  text-shadow: 0 0 20px rgba(204,0,0,0.3);
}
.stat-block:hover b{
  transform: scale(1.1);
  text-shadow: 0 0 30px rgba(204,0,0,0.5);
}
.stat-block span{
  font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted);
  transition: color var(--dur-base) var(--ease-out);
}
.stat-block:hover span{ color: var(--text); }

/* "Who it's for" checklist (Training) — nudged up ~9% from the body default
   (16px text / 22px ticks) purely for legibility; these four lines are the
   page's scannable summary, so they earn slightly more weight than body copy.
   em rather than px so the text keeps tracking the base scale, and the tick
   is sized in CSS to override the SVG's own width/height attributes. */
.who-list p{ font-size: 1.09em; }
/* 25px -> 50px (doubled, per follow-up feedback the first bump read as too
   subtle). stroke-width stays 2.4: it's defined in the 24x24 viewBox's own
   coordinate space, so doubling the rendered box scales the stroke's visual
   weight right along with it — no separate change needed there.
   Swapped the drop-shadow (which only hugs the checkmark's own stroke outline)
   for an actual radial-gradient background, which is what makes this read as
   a glow behind the icon rather than a thicker outline around it. border-radius
   clips that gradient to a circle instead of leaving a visible square edge. */
.who-list svg{
  /* 50px -> 49px: a ~2% trim: requested after doubling read as just slightly
     too large/glowy. The radial-gradient glow scales with this box
     automatically, so shrinking it here is enough to pull both back together. */
  width: 49px;
  height: 49px;
  stroke-width: 2.4;
  background: radial-gradient(circle, rgba(255,255,255,0.55) 0%, rgba(255,255,255,0.22) 45%, rgba(255,255,255,0) 72%);
  border-radius: 50%;
  /* The 2px margin-top on the markup was tuned to nudge a 22px icon down to
     the text's cap-height when top-aligned; at 50px it just pushes an
     already-oversized icon further out of place. Neutralised here since the
     row now centers instead (align-items below), where any margin throws the
     centering off by that same amount. */
  margin-top: 0;
}
/* Each .cluster row carries an inline align-items:flex-start, sized for the
   original 22px icon sitting flush with the text's top line. A 50px icon
   under flex-start hangs its (much lower) vertical centre far below the
   text's centre, reading as misaligned; centering the row instead lines the
   icon's middle up with the text block's middle, which is what actually
   looks aligned at this size. !important is required to beat that inline
   style — CSS specificity alone can't. */
.who-list .cluster{ align-items: center !important; }
/* On phones the icon was landing on its own line ABOVE the text, not in front
   of it. .cluster is flex-wrap:wrap and the <p> is a block that claims the
   container's full 345px, so the row measured 49 + 16 + 345 = 410px and
   wrapped the paragraph down. nowrap alone isn't enough — the p also has to
   be allowed to shrink, which is what flex:1 + min-width:0 do (a flex item's
   default min-width:auto refuses to go below its content's intrinsic width
   and would force the overflow instead).
   align-items:center is already set above and is what balances the icon
   against the two-line paragraph rather than pinning it to the first line. */
@media (max-width: 767.98px){
  .who-list .cluster{ flex-wrap: nowrap !important; }
  .who-list .cluster > p{ flex: 1 1 auto; min-width: 0; }
  /* Breathing room. Two separate spacings were doing the cramping, and both
     have to move or the rows still touch:
       - the 0.5rem gap BETWEEN rows (inline on .who-list, hence !important),
         which was smaller than the leading inside a single wrapped paragraph,
         so consecutive entries read as one block of text
       - the paragraphs' own line-height, tightened to 1.35 at this size
     1.4rem between rows is a little over one line of text, which is what
     makes each entry group visibly as its own item. */
  /* The last section ends 44px below its content and the footer starts just
     6px later, so the download badges were landing almost flush against the
     footer's wordmark. Padding on the footer rather than the section above it,
     because the join looks the same on every page and the last section differs
     from page to page. */
  /* Header hardening, paired with the root overflow clamp. Even with the
     layout viewport pinned, the nav bar is space-between flex: a brand whose
     min-width is `auto` (the default) refuses to shrink below its text, so if
     that text is ever wider than expected — fallback font metrics during
     first paint, a longer translation — it pushes the 44px hamburger past the
     right edge. That is the "no hamburger menu" half of the bug.
     min-width:0 lets the brand yield, and flex-shrink:0 makes the hamburger
     the one thing that never gives up space. The brand truncates instead,
     which is the right thing to sacrifice: the menu must always be reachable. */
  .nav-bar .brand-lockup{
    min-width: 0;
    overflow: hidden;
  }
  .nav-bar .nav-actions{ flex-shrink: 0; }

  .footer{ padding-top: 5rem !important; }

  .who-list{ gap: 1.4rem !important; }
  .who-list .cluster > p{ line-height: 1.55; }
  /* The icon column widens slightly too — 16px of gap against a 49px icon
     left the text almost touching it. */
  .who-list .cluster{ gap: 1.25rem !important; }
}

/* ---------- Section headers ---------- */
.section-head{ max-width: 640px; margin-bottom: var(--space-8); }
.section-head .hazard-tag{ margin-bottom: var(--space-3); }
.section-head h2{ margin-bottom: var(--space-4); }
.section-head p{ color: var(--text-muted); font-size: var(--text-md); }
.section--dark .section-head p{ color: var(--text-on-dark-muted); }
.section-head.center{ margin-inline: auto; text-align: center; }
.section-head.center p{ margin-inline: auto; }

@media (min-width: 1024px){
  [data-i18n="home.testimonials.title"]{ white-space: nowrap; }
  [data-i18n="training.who.title"]{ white-space: nowrap; }
  [data-i18n="training.curriculum.title"]{ white-space: nowrap; }
}

/* ---------- Parallax photo gallery (About) ----------
   Three columns of photos on the page background — no cards, no white.
   Outer columns drift/tilt as the page scrolls; the middle column stays put,
   which is what gives the grid its depth. Transforms are applied by main.js;
   everything degrades to a plain static grid without JS or under reduced motion. */
.parallax-gallery-section{ overflow: hidden; }
.parallax-gallery{
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 2.5rem;
  align-items: start;
  max-width: 1024px;
  margin-inline: auto;
  padding-block: var(--space-8);
}
.parallax-gallery__col{
  display: grid;
  gap: 2.5rem;
  will-change: transform;
}
.parallax-gallery img{
  display: block;
  width: 100%;
  /* height:auto lets every shot keep its own aspect ratio, so each photo is
     shown whole and the column reads as a masonry stack. The previous fixed
     20rem height with object-fit:cover filled a uniform tile but cropped INTO
     each image — and with object-position:left top it trimmed the right and
     bottom edges, which is exactly where faces sit in the group shots. The
     intrinsic width/height on each tag reserves the right box up front so the
     lazy-loaded images don't shift the column as they arrive. */
  height: auto;
  /* Belt-and-braces: with height:auto the box already matches the intrinsic
     ratio, so this is a no-op today. It only earns its keep if a width/height
     attribute is ever wrong or missing — `contain` letterboxes in that case,
     where the default `fill` would silently stretch the photo instead. */
  object-fit: contain;
  border-radius: var(--radius-lg);
  margin: 0;
  /* View-only: blocks the two easiest save paths (drag-out-to-desktop, and
     the text-selection handles that Android/iOS long-press brings up over an
     image) without touching pinch-zoom — that's a viewport-level browser
     gesture, not a per-image handler, so nothing here can disable it. This is
     a deterrent, not real DRM: any browser's dev tools or a screenshot still
     gets the picture. -webkit-touch-callout is what actually suppresses the
     iOS long-press menu; user-select alone doesn't touch it. */
  -webkit-user-drag: none;
  user-select: none;
  -webkit-touch-callout: none;
  pointer-events: auto;
}
@media (max-width: 900px){
  .parallax-gallery{ grid-template-columns: repeat(2, 1fr); gap: 1.5rem; }
  .parallax-gallery__col{ gap: 1.5rem; }
}
@media (max-width: 600px){
  .parallax-gallery{ grid-template-columns: 1fr; gap: 1rem; padding-block: var(--space-6); }
  .parallax-gallery__col{ gap: 1rem; }
  /* Single column on phones — parallax offsets would just push images off-screen. */
  .parallax-gallery__col{ transform: none !important; }
}

/* ---------- Timeline (About) ---------- */
.timeline{
  position: relative; padding-left: var(--space-6);
  border-left: 3px solid rgba(204,0,0,0.3);
  transition: border-color var(--dur-base) var(--ease-out);
}
.timeline:hover{ border-left-color: rgba(204,0,0,0.6); }
.timeline__item{
  position: relative; padding-bottom: var(--space-6);
  transition: transform var(--dur-base) var(--ease-out);
}
.timeline__item:last-child{ padding-bottom: 0; }
.timeline__item::before{
  content: ""; position: absolute; left: calc(-1 * var(--space-6) - 8px); top: 4px;
  width: 16px; height: 16px; border-radius: 50%; background: var(--red);
  border: 3px solid var(--bg);
  box-shadow: 0 0 0 2px var(--red), 0 0 12px rgba(204,0,0,0.4);
  transition: all var(--dur-base) var(--ease-out);
}
.timeline__item:hover::before{
  transform: scale(1.3);
  box-shadow: 0 0 0 2px var(--red), 0 0 24px rgba(204,0,0,0.6);
}
.timeline__year{ font-family: var(--font-mono); color: var(--red); font-size: var(--text-sm); font-weight: 700; transition: color var(--dur-base) var(--ease-out); }
.timeline__item:hover .timeline__year{ color: #FF6666; }
.timeline__item h3{ margin: var(--space-1) 0 var(--space-2); transition: color var(--dur-base) var(--ease-out); }
.timeline__item:hover h3{ color: var(--red); }
.timeline__item p{ color: var(--text-muted); font-size: var(--text-sm); transition: color var(--dur-base) var(--ease-out); }
.timeline--bright-text .timeline__item p{ color: #E8E8E8; }
.timeline__item:hover p{ color: var(--text); }

/* ---------- Testimonials ---------- */
.t-card{
  background: var(--white); border-radius: var(--radius-lg); padding: 1.2rem;
  border: 1px solid var(--border); display: flex; flex-direction: column; gap: 0.8rem; height: 100%;
}
.t-card__stars{ display: flex; gap: 3.2px; color: var(--yellow); }
.t-card__quote{ font-size: calc(var(--text-md) * 0.8); line-height: 1.5; color: #000; }
.t-card__author{ display: flex; align-items: center; gap: 0.6rem; margin-top: auto; }
.t-card__avatar{
  width: 35px; height: 35px; border-radius: 50%; background: var(--paper-dim);
  display: flex; align-items: center; justify-content: center; font-family: var(--font-display);
  color: var(--red); font-weight: 700; flex-shrink: 0; font-size: 0.85rem;
}
.t-card__author strong{ display: block; font-size: calc(var(--text-sm) * 0.8); }
.t-card__author span{ font-size: calc(var(--text-xs) * 0.8); color: var(--text-muted); }
.sample-note{ font-size: var(--text-xs); color: var(--text-muted); font-style: italic; margin-top: var(--space-4); }

/* Testimonial marquee: two rows scrolling opposite directions, pause on hover,
   edge fade via mask so it works over any background. */
.testimonial-marquee{
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
  overflow: hidden;
  padding-block: 2rem;
  -webkit-mask-image: linear-gradient(to right, transparent, black 8%, black 92%, transparent);
  mask-image: linear-gradient(to right, transparent, black 8%, black 92%, transparent);
}
.testimonial-track{
  display: flex;
  gap: 2rem;
  width: max-content;
  animation: testimonial-marquee-left 34s linear infinite;
  will-change: transform;
}
.testimonial-track--reverse{ animation-name: testimonial-marquee-right; animation-duration: 38s; }
.testimonial-marquee .t-card{
  width: 210px; padding: 0.84rem; flex-shrink: 0; cursor: default;
  position: relative;
  background: #0A0908; border-color: rgba(204,0,0,0.35);
  box-shadow: 0 0 5px rgba(204,0,0,0.55), 0 0 13px rgba(204,0,0,0.4), 0 0 24px rgba(204,0,0,0.22);
}
.testimonial-marquee .t-card__quote{
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
  color: #fff;
}
.testimonial-marquee .t-card__author strong{ color: #fff; }
.testimonial-marquee .t-card__author span{ color: var(--text-on-dark-muted); }
.testimonial-marquee .t-card__avatar{
  color: #fff;
}
.testimonial-marquee .t-card__avatar svg{ width: 20px; height: 20px; }
@keyframes testimonial-marquee-left{
  from{ transform: translateX(0); }
  to{ transform: translateX(-50%); }
}
@keyframes testimonial-marquee-right{
  from{ transform: translateX(-50%); }
  to{ transform: translateX(0); }
}
@media (prefers-reduced-motion: reduce){
  html:not(.force-motion) .testimonial-track{ animation: none; }
}
@media (max-width: 640px){
  .testimonial-marquee .t-card{ width: 168px; }
}

/* ---------- FAQ Accordion ---------- */
.faq-item{ border-bottom: 1px solid var(--border); }
.faq-item__q{
  width: 100%; display: flex; align-items: center; justify-content: space-between; gap: var(--space-4);
  padding: var(--space-5) 0; text-align: left; font-family: var(--font-display);
  font-size: var(--text-lg); text-transform: uppercase; letter-spacing: 0.01em;
}
.faq-item__icon{ flex-shrink: 0; transition: transform var(--dur-base) var(--ease-out); color: var(--red); }
.faq-item[data-open="true"] .faq-item__icon{ transform: rotate(45deg); }
.faq-item__a{
  max-height: 0; overflow: hidden; transition: max-height var(--dur-slow) var(--ease-in-out);
}
.faq-item__a p{ padding-bottom: var(--space-5); color: var(--text-muted); }

/* ---------- Forms ---------- */
.form-field{ display: flex; flex-direction: column; gap: var(--space-2); margin-bottom: var(--space-5); }
.form-field label{ font-size: var(--text-sm); font-weight: 600; }
.form-field .req{ color: var(--red); }
.form-field input, .form-field textarea, .form-field select{
  border: 2px solid var(--border); border-radius: var(--radius-sm); padding: var(--space-3) var(--space-4);
  min-height: 48px; background: var(--white); color: #000; transition: border-color var(--dur-fast) var(--ease-out);
}
.form-field textarea{ min-height: 140px; resize: vertical; }
.form-field input:focus, .form-field textarea:focus, .form-field select:focus{ border-color: var(--red); }
.form-field .hint{ font-size: var(--text-xs); color: var(--text-muted); }
.form-field .error{ font-size: var(--text-xs); color: var(--red); display: none; }
.form-field[data-invalid="true"] input,
.form-field[data-invalid="true"] textarea{ border-color: var(--red); }
.form-field[data-invalid="true"] .error{ display: block; }
.form-status{
  display: none; align-items: center; gap: var(--space-3); padding: var(--space-4) var(--space-5);
  border-radius: var(--radius-md); font-size: var(--text-sm); margin-top: var(--space-4);
}
.form-status[data-state="success"]{ display: flex; background: #E9F7EF; color: #1D6B3B; }
.form-status[data-state="error"]{ display: flex; background: #FCEAEA; color: var(--red-dark); }

/* ---------- Footer ---------- */
/* Section height trimmed by a third from the original space-10/space-6
   padding and space-8 gap/margin (4rem/2rem/3rem * 2/3). Text bumped +2px
   over the design-system size it was built on, scoped to the footer only. */
.footer{ background: var(--black); color: var(--text-on-dark); padding-block: 0.375rem 0.1875rem; }
.footer__top{ display: block; margin-bottom: 0.375rem; }
.footer__top > div{ display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem 1.25rem; }
/* Wordmark no longer force-wraps onto its own row — it flows inline with the
   description/social row and top-aligns to the social icons specifically
   (align-self, scoped to just this item, so desc/social keep their own
   existing center alignment to each other). */
.footer__top > div > .wordmark{ align-self: flex-start; }
.footer__desc{ color: var(--text-on-dark-muted); font-size: 0.7rem; margin-top: 0; max-width: 32ch; flex: 1 1 auto; }
.footer h4{ color: var(--text-on-dark-muted); margin-bottom: var(--space-3); font-size: 0.75rem; }
.footer ul{ display: flex; flex-direction: column; gap: var(--space-2); }
.footer a{ font-size: 0.75rem; color: var(--text-on-dark-muted); transition: color var(--dur-fast); }
.footer a:hover{ color: var(--yellow); }
/* Social links made much more prominent: solid brand-red circles (up from a
   faint 1px outline), bigger target, lift-and-glow on hover. */
.footer__social{ display: flex; gap: calc(var(--space-3) * 0.5); margin-top: 0; margin-left: auto; flex-shrink: 0; }
.footer__social a{
  width: 46.8px; height: 46.8px; border-radius: 50%; border: none;
  background: var(--red); color: var(--white);
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 4px 14px rgba(204,0,0,0.45);
  transition: transform var(--dur-fast), box-shadow var(--dur-fast), filter var(--dur-fast);
}
.footer__social svg{ width: 36px; height: 36px; }
.footer__social a:hover{ transform: translateY(-3px); filter: brightness(1.15); }
.footer__social a[aria-label="Instagram"]{ background: radial-gradient(circle at 30% 107%, #fdf497 0%, #fdf497 5%, #fd5949 45%, #d6249f 60%, #285AEB 90%); box-shadow: 0 4px 14px rgba(214,36,159,0.45), 0 0 20px rgba(214,36,159,0.35); }
.footer__social a[aria-label="Facebook"]{ background: #1877F2; box-shadow: 0 4px 14px rgba(24,119,242,0.45), 0 0 20px rgba(24,119,242,0.35); }
/* Widened + brightened outer glow (20px/0.35 -> 38px/0.55) so YouTube reads
   as the deliberately-emphasized next step, not just another equal icon. */
.footer__social a[aria-label="YouTube"]{ background: #FF0000; box-shadow: 0 4px 14px rgba(255,0,0,0.5), 0 0 38px rgba(255,0,0,0.55); }
/* Dialed down (20px/0.35 -> 10px/0.15) - TikTok has no href now, so its glow
   shouldn't compete for attention with the icons that actually go somewhere. */
.footer__social a[aria-label="TikTok"]{ background: #010101; box-shadow: 0 4px 10px rgba(37,244,238,0.15), 0 0 10px rgba(37,244,238,0.15); }
.footer__social a[aria-label="Patreon"]{ background: #FF424D; box-shadow: 0 4px 14px rgba(255,66,77,0.45), 0 0 20px rgba(255,66,77,0.35); }
/* TikTok isn't live yet, so its anchor carries no href — nothing can navigate,
   with or without JS. It keeps the <a> tag purely so every .footer__social a
   rule above (size, circle, shadow, icon sizing) still applies untouched.
   tabindex keeps it reachable by keyboard; the tooltip answers on both hover
   and focus so the "why is this dead" question is answered either way. The
   hover lift/brightness is cancelled so it doesn't advertise a click that
   does nothing. */
/* Generalised from the TikTok icon so the same affordance covers every control
   that is deliberately not wired up yet: the app-store badges before launch,
   the unwritten legal pages, and the shop's storefront CTA. Each of those was
   an <a href="#">, which is worse than a dead control — it looks live, and
   clicking it jerks the reader back to the top of the page. */
.is-coming-soon:not([href]){ position: relative; cursor: default; }
.is-coming-soon:not([href])::after{
  content: attr(data-soon);
  position: absolute;
  bottom: calc(100% + 10px);
  left: 50%;
  transform: translateX(-50%) translateY(4px);
  padding: 0.3rem 0.55rem;
  border-radius: 6px;
  background: var(--ink);
  border: 1px solid var(--border-dark);
  color: var(--white);
  font-family: var(--font-mono);
  font-size: 0.6rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-fast), transform var(--dur-fast);
  /* Above neighbouring cards/badges, or the tooltip renders behind them. */
  z-index: 30;
}
/* :focus and :active are here for touch. A phone can't hover, and :focus-visible
   deliberately does not match a tap — so without these the badge would simply do
   nothing when tapped, with no explanation. */
.is-coming-soon:not([href]):hover::after,
.is-coming-soon:not([href]):focus::after,
.is-coming-soon:not([href]):active::after,
.is-coming-soon:not([href]):focus-visible::after{
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}
/* Cancel the hover lift/brightness so it stops advertising a click that does
   nothing. Scoped to the social icons, which are the only ones that had it. */
.footer__social a.is-coming-soon:not([href]):hover{ transform: none; filter: none; }
/* Store badges deliberately keep full opacity. They are finished artwork and
   any fade just makes them look broken rather than pending — the tooltip is
   what communicates "not yet". */
/* Footer legal links keep the muted footer colour but drop the hover promise. */
.footer__bottom .is-coming-soon:not([href]){ opacity: 0.75; }
.footer__bottom .is-coming-soon:not([href]):hover{ color: inherit; }
.footer__bottom{
  padding-top: calc(var(--space-3) * 0.5);
  display: flex; flex-wrap: wrap; gap: var(--space-3); justify-content: space-between;
  font-size: 0.7rem; color: var(--text-on-dark-muted);
}
.footer__bottom a:hover{ color: var(--yellow); }

/* ---------- Badges / chips ---------- */
/* --paper-dim is a light-theme cream — a holdover from before the dark-theme
   conversion. Both pages that use a bare .chip (resources.html cards,
   article-idp-thailand.html) are fully dark, so it was rendering as a
   washed-out, barely-legible pill. Red keeps it visually matched to
   .chip--soon's boldness/shape without borrowing yellow, which specifically
   means "coming soon" elsewhere — this tag needs to read as "live", not
   "not yet available". */
.chip{
  display: inline-flex; align-items: center; gap: var(--space-2);
  background: rgba(204,0,0,0.16); color: var(--red); border: 1px solid rgba(204,0,0,0.4);
  border-radius: var(--radius-full);
  padding: var(--space-2) var(--space-4); font-size: var(--text-xs); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.04em;
}
.chip--soon{ background: var(--yellow); color: var(--black); border-color: transparent; }
/* Resource cards only. var(--yellow) (#FFD400) is a bright, saturated gold —
   checked against every other yellow use on the site (buttons, the "Coming
   Soon" chip on training.html, the site's --yellow-dim token) and none of
   them are actually darker; #FFD400 IS the site standard. Rather than
   introduce a mismatched one-off, this scoped rule deepens the chip
   specifically here to the tone requested, without touching --yellow itself
   and so without changing chip--soon anywhere else it's used. */
.card.merch-card .chip--soon{ background: #C99A00; }

/* ---------- The Mission ----------
   The mascot is a transparent cut-out, so it carries no light of its own — the
   red neon radial behind it is what gives it a ground and stops it floating on
   flat black. Same rgba(204,0,0) family as the section-header glow so it reads
   as one system. The glow is sized in % of the mascot box (not px) so it tracks
   the art at every breakpoint instead of needing a radius per media query. */
/* The eyebrow sits directly above "THE MISSION" and must line up with the
   heading's TEXT edge, not its box edge — the base h2 rule carries 1rem of
   horizontal padding to fit its radial glow, so without a matching inset the
   eyebrow would sit 16px adrift to the left. */
/* .hazard-tag is inline-flex and h2 is inline-block, so a short title like
   "THE MISSION" fits beside the eyebrow and the two flow onto ONE line.
   display:flex forces the eyebrow to its own line; width:fit-content keeps the
   pill hugging its text instead of stretching the full column. */
/* The studio floor is continued OUT of the mascot render rather than faked with
   a gradient: mission-floor.png is built by sampling the render's own floor
   pixels and extending its floor/backdrop parabola across the section, so the
   grain, the colour ramp and the arch all meet the artwork exactly at the
   image's edges. See scratchpad/make_floor.py for the generator. */
.mission__quote{
  display: inline-block;
  margin-top: 1.75rem;
  /* Bottom padding reserves room for .mission__signature's +23px downward
     offset. That offset sits outside normal flow by design (see below) so
     it isn't counted in this box's height — without this padding, a longer
     text wrap (translations, narrower two-column widths) can push the
     signature past .mission's overflow:hidden edge and clip it. */
  padding: 0.5rem 0.75rem 3rem;
  font-family: "Caveat", cursive;
  font-weight: 500;
  font-size: 1.85em;
  line-height: 1.3;
  color: var(--text-on-dark-muted);
  transform: rotate(-1.5deg);
}
.mission__quote-highlight{ color: var(--white); font-weight: 700; }
/* Caveat is a casual felt-tip hand — fine small, but at this 1.85em display
   size its round, even strokes read as printing rather than handwriting.
   Alex Brush is a pointed-brush script with real thick/thin stroke contrast
   and connected letterforms, which is what gives it an adult, deliberate
   look. It was already being downloaded by the font link in the <head> and
   simply never referenced, so this costs no extra request.
   Size is raised because Alex Brush has a much smaller x-height than Caveat
   at the same em, and the rotation is eased off (-1.5deg -> -0.75deg) since a
   formal script doesn't want the scrawled-note tilt. */
/* Swapped again, on direct instruction: Alex Brush's whole character IS that
   thin/thick pointed-brush contrast, and its hairline strokes were exactly
   what international readers unfamiliar with cursive Latin script found
   illegible. Yellowtail is a bold BRUSH script instead of a pointed one — its
   strokes hold a thick, near-uniform weight throughout each letter (no
   hairline joins), so individual characters stay identifiable at a glance
   while the piece still reads as handwritten rather than typed. It's a lower
   x-height than Alex Brush's still, hence the further size bump. */
@media (max-width: 767.98px){
  .mission__quote{
    font-family: "Yellowtail", "Alex Brush", "Pinyon Script", cursive;
    /* 2.5em -> 2.38em, a ~2px trim at this size. Deliberately small: the size
       is doing real legibility work for a script face, so this is a nudge off
       the ceiling rather than a step back down the scale.
       2.38em -> 2.142em (-10%), on request. The signature isn't touched
       separately: .mission__signature is sized in em relative to THIS
       element's own computed font-size (1.6em of it), so shrinking this one
       value shrinks both the quote and the signature by the same 10%
       automatically. */
    font-size: 2.142em;
    line-height: 1.3;
    transform: rotate(-0.75deg);
  }
  /* The highlight was already #fff, but it wasn't reading as emphasis: the
     surrounding text sat at rgb(176,176,176), and Yellowtail ships a single
     weight, so a script face can't carry emphasis through boldness the way the
     original sans did — colour is doing the whole job alone. Dimming the
     BASE text widens that gap (176 -> 138) instead of trying to push the
     highlight past pure white, which isn't possible. */
  .mission__quote{ color: rgb(138,138,138); }
  /* "To Train One Million Riders" up 18px -> 19px. It can only grow by taking
     space from something else: the badge measured 342px inside 342px of usable
     width, i.e. exactly at the wrap point. The two hazard icons and their gaps
     were 66px of that, so trimming them (26->20px wide, 7.2->5px gaps) frees
     ~16px — enough to fund the larger type and still land at ~341px. nowrap
     guarantees the one-line requirement even if a translation runs longer;
     it would overflow visibly rather than silently wrapping, which is the
     failure mode that's actually noticeable. */
  .mission-badge{
    font-size: 19px;
    gap: 5px;
    white-space: nowrap;
  }
  .mission-badge__icon{
    width: 20px;
    height: 8.85px;
    background: repeating-linear-gradient(-45deg, var(--black) 0 4.4px, var(--yellow) 4.4px 8.85px);
  }
  .mission__quote-highlight{
    font-weight: 400;
    color: #fff !important;
    /* Faint white bloom, sized to stay on the glyphs: lifts the emphasised
       clause off the darker text without touching the surrounding layout. */
    text-shadow: 0 0 6px rgba(255,255,255,0.25);
  }
  /* Signature ONLY — the quote body keeps Yellowtail. Alex Brush is the
     pointed-brush script whose thin/thick stroke contrast made it unreadable
     for a full paragraph, but that same quality is exactly right for a name:
     a signature is short, recognised as a shape rather than read letter by
     letter, and thin-tipped strokes are what an actual pen produces.
     Sized up because Alex Brush has a much smaller x-height than Yellowtail
     at the same em, so matching ems would render it noticeably smaller.
     It needs the display=swap link in <head> for the same reason Yellowtail
     did — under the shared `optional` link it silently never painted. */
  /* Descendant selector for specificity: the base .mission__signature rule
     further down sets font-family:inherit, and at equal specificity the later
     rule wins — so a bare .mission__signature here was being overridden and
     the signature kept rendering in the quote's Yellowtail. */
  .mission__quote .mission__signature{
    font-family: "Alex Brush", cursive;
    font-size: 1.6em;
    transform: rotate(-2deg);
  }
}
/* Sits INSIDE .mission__quote so it flows inline directly after the quote's
   final word at any viewport/translation, rather than wrapping to its own
   line. Sizes are therefore relative to the quote's 1.85em, not the .lede
   root — 1.25em here renders at the same ~41px as the old standalone 2.3em. */
.mission__signature{
  display: inline-block;
  margin-left: 0.5rem;
  font-family: inherit;
  font-size: 1.25em;
  line-height: 1;
  color: var(--white);
  text-shadow: 0 0 1px rgba(228,228,226,0.35);
  transform: rotate(-3deg);
  vertical-align: -0.28em;
  /* Nudged down/right off the text baseline so it reads as a signature laid
     over the end of the quote rather than another word in the sentence.
     position:relative keeps it out of the line-box maths, so the paragraph
     above it doesn't reflow. */
  position: relative;
  left: 16px;
  top: 23px;
}
.mission__signature-initial{
  font-size: 1.5em;
}
.mission{ position: relative; overflow: hidden; }
.mission > .container{ position: relative; z-index: 1; }
/* DO NOT constrain this to .container's width, and do not change background-size.
   mission-floor.png is 2850x1202 = exactly 2x the section's own full-bleed box
   (1425x601 CSS px), because it was generated by sampling the mascot render's
   floor pixels and extending that parabola across the WHOLE section - see the
   note above. It is a baked, pixel-for-pixel overlay for `inset:0` at
   `100% 100%`. Capping it to --container (1240px) starves the ~90px of section
   outside the column and the floor dies mid-air on the right; rescaling it
   past 100% slides the arch off the mascot's feet. Both were tried; both broke
   it. Full-bleed + 100% 100% is the calibration this asset was authored for. */
.mission::before{
  content: "";
  position: absolute;
  inset: 0;
  background: url('../assets/img/mission-floor.png') no-repeat bottom center;
  background-size: 100% 100%;
  z-index: 0;
  pointer-events: none;
}
/* The arch is aligned to the mascot's position in the two-column layout. Once
   the grid stacks the art recentres and shrinks, so the extension would no
   longer meet the render's own floor line - drop it and keep the plain black. */
@media (max-width: 767px){
  .mission::before{ content: none; }
}
.mission__eyebrow{
  display: flex;
  width: fit-content;
  margin-left: 1rem;
  margin-bottom: var(--space-3);
}
.mission__mascot{
  position: relative;
  display: flex; align-items: center; justify-content: center;
}
.mission__mascot::before{
  content: "";
  position: absolute; left: 50%; top: 52%;
  width: 96%; aspect-ratio: 1;
  transform: translate(-50%, -50%);
  background: radial-gradient(circle,
    rgba(204,0,0,0.60) 0%,
    rgba(204,0,0,0.34) 32%,
    rgba(204,0,0,0.12) 55%,
    rgba(204,0,0,0) 72%);
  z-index: 0;
  pointer-events: none;
}
.mission__mascot img{
  position: relative; z-index: 1;
  width: 100%; max-width: 582px; height: auto; display: block;
}
@media (max-width: 767px){
  /* Stacked layout: the copy reads first, so the art is pulled in tighter and
     the glow widened slightly to stay proportionate in a narrow column. */
  .mission__mascot{ margin-top: var(--space-4); }
  /* Full-bleed, but applied to the WRAPPER, not the <img>. Putting
     width:100vw on the image was self-defeating: .mission__mascot is a flex
     container, so a 100vw child inflates the wrapper to ~395px — wider than
     the 345px container it sits in. The breakout margin
     calc(50% - 50vw) then resolves 50% against that inflated 395px parent
     (197.6px) instead of the intended 345px (172.5px), producing +1px per
     side rather than -24px. The image was therefore pushed RIGHT and off the
     screen edge instead of breaking out symmetrically — the off-centre,
     not-touching-both-sides bug.
     Sizing the image at 100% keeps the wrapper at its natural column width,
     so the wrapper's own breakout maths are correct and the result is
     genuinely centred and edge to edge. */
  /* THE root cause of the whole section being off-centre: a grid item's
     default `min-width: auto` refuses to shrink below its content's
     min-content width. The handwritten quote is a large-display-font
     inline-block whose min-content is ~386px, so the column was forced to
     395px — wider than the 345px container — and everything inside inherited
     that wrong reference, including the mascot's breakout calc. Setting
     min-width:0 lets the column shrink to its true 345px, which is what makes
     both fixes below resolve correctly. */
  .mission .grid > *{ min-width: 0; }
  /* Quote confined to the column so it can no longer inflate it, and so its
     right edge stops being clipped by .mission's overflow:hidden. */
  .mission__quote{
    max-width: 100%;
    box-sizing: border-box;
  }
  .mission .lede{ padding-left: 0 !important; }
  .mission__mascot{
    width: 100vw;
    max-width: 100vw;
    margin-inline: calc(50% - 50vw);
  }
  .mission__mascot img{ width: 100%; max-width: 100%; margin-inline: 0; }
  .mission__mascot::before{ width: 116%; }
}

/* ---------- Scroll reveal ----------
   Progressive enhancement: content is VISIBLE by default. The hidden start-state only
   applies once main.js proves it can animate (adds .js-anim to <html>). So no JS, blocked
   scripts, a throttled/background tab, or reduced-motion can never strand content invisible. */
html.js-anim [data-reveal]{ opacity: 0; transform: translateY(28px); transition: opacity 0.7s var(--ease-out), transform 0.7s var(--ease-out); }
html.js-anim [data-reveal].is-visible{ opacity: 1; transform: translateY(0); }
html.js-anim [data-reveal-stagger] > *{ opacity: 0; transform: translateY(24px); transition: opacity 0.6s var(--ease-out), transform 0.6s var(--ease-out); }
html.js-anim [data-reveal-stagger].is-visible > *{ opacity: 1; transform: translateY(0); }
/* transition-property doesn't merge across rules — whichever single rule
   wins cascade replaces the WHOLE list, so the stagger rule above (higher
   specificity than plain .card) was silently dropping .card's box-shadow/
   border-color transition entirely, not just overriding its duration.
   Restated here, at specificity that beats the stagger rule for .card
   specifically: opacity/transform keep the original 0.6s entrance-reveal
   timing (matching every other staggered sibling, so the grid still reveals
   in sync), while box-shadow/border-color get their own fast 380ms so the
   red glow snaps on in well under a second regardless of what the entrance
   animation is doing. */
html.js-anim [data-reveal-stagger] > .card{
  transition: opacity 0.6s var(--ease-out),
              transform 0.6s var(--ease-out),
              background var(--dur-base) var(--ease-out),
              box-shadow 380ms var(--ease-out),
              border-color 380ms var(--ease-out);
}
/* GSAP owns the hero entrance + magnetic buttons, so drop the CSS transition on just those.
   [data-reveal] is deliberately excluded: main.js reveals it via CSS transitions. */
.gsap-managed .hero__eyebrow,
.gsap-managed .hero__title,
.gsap-managed .hero__desc,
.gsap-managed .hero__actions,
.gsap-managed .hero__stats,
.gsap-managed .hero__media,
.gsap-managed .breadcrumb{ transition: none; }
.gsap-managed .btn--primary,
.gsap-managed .btn--accent{ transition: box-shadow var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out); }

/* ---------- Utility ---------- */
.mt-0{ margin-top: 0; } .mb-0{ margin-bottom: 0; }
.text-muted{ color: var(--text-muted); }
.text-center{ text-align: center; }
.w-full{ width: 100%; }
.gap-sm{ gap: var(--space-2); }

/* App store buttons */
.store-badges{ display: flex; flex-wrap: wrap; gap: var(--space-3); }
.store-badge{
  display: flex; align-items: center; gap: var(--space-3);
  background: var(--white); color: var(--black); border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-5); border: 1px solid var(--border-dark);
  min-height: 56px;
}
.section--dark .store-badge, .footer .store-badge{ background: var(--ink); color: var(--white); border-color: var(--border-dark); }
.store-badge:hover{ border-color: var(--yellow); }
.store-badge small{ display: block; font-size: 0.65rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-muted); }
.section--dark .store-badge small, .footer .store-badge small{ color: var(--text-on-dark-muted); }
.store-badge strong{ font-family: var(--font-display); font-size: var(--text-md); text-transform: uppercase; }

/* Breadcrumb */
.breadcrumb{ display: flex; flex-wrap: wrap; gap: var(--space-2); font-size: var(--text-xs); color: var(--text-muted); margin-bottom: var(--space-6); text-transform: uppercase; letter-spacing: 0.04em; }
.breadcrumb a:hover{ color: var(--red); }

/* Product / merch grid */
/* Dark card + thin red neon edge, matching the glow system used on the
   testimonial cards (.testimonial-marquee .t-card) and the mascot/section
   glows elsewhere - same rgba(204,0,0) family, so it reads as one system
   instead of a one-off. var(--white) here was fighting the site's own
   dark-theme text colors (body text defaults to near-white/--text-muted's
   light-mode grey), which is what was washing the copy out. */
.merch-card{
  background: #0A0908;
  border-radius: var(--radius-lg);
  overflow: hidden;
  border: 1px solid rgba(204,0,0,0.35);
  box-shadow: 0 0 5px rgba(204,0,0,0.55), 0 0 13px rgba(204,0,0,0.4), 0 0 24px rgba(204,0,0,0.22);
}
/* Resources page's download cards only — .card.merch-card is the specific
   combination that page uses (shop.html's product cards carry .merch-card
   alone), so this can't leak onto that page's grid.
   Softer treatment on request: the 1px solid border at 0.35 alpha reads as a
   hard outline against the black card, especially with the box-shadow's
   tight first stop (5px) hugging it closely. Dropping to a barely-visible
   0.12 alpha (or none at all would work too, but a faint line still helps
   define the card edge under the glow) and starting the shadow's first stop
   at 14px instead of 5px removes that crisp inner ring — the glow now begins
   already-diffuse rather than dense-then-fading. */
.card.merch-card{
  border-color: rgba(204,0,0,0.12);
  box-shadow: 0 0 14px rgba(204,0,0,0.3), 0 0 30px rgba(204,0,0,0.22), 0 0 48px rgba(204,0,0,0.12);
  /* Slow breathing pulse — CSS animation on box-shadow opacity only, not a
     Ken-Burns-style transform/scale. A scaled or translated card would fight
     the grid's own layout and the :hover lift (translateY+scale) already on
     .card, and would force layout recalculation every frame on a property
     the browser can't composite cheaply. box-shadow opacity has no such
     cost — it paints on the compositor without triggering layout or paint of
     surrounding elements, so this is free to run continuously, even on a
     modest phone, without competing with anything else loading on the page.
     8s and ease-in-out is slow/smooth enough to read as "breathing" rather
     than a flicker; infinite alternate is the actual breathing motion (in,
     out, in, out) rather than a hard reset every cycle. */
  animation: card-glow-breathe 8s ease-in-out infinite alternate;
}
/* Desynchronised per card, on request — six cards all sharing one 8s timer
   pulsed in lockstep, which read as one mechanical blink rather than six
   independent lights. Two things vary together, not just a start offset:
     - animation-delay staggers where each card starts in the cycle at load.
       A delay ALONE isn't enough on an infinite loop, though: every card
       still shares the same 8s period, so they'd re-sync every 8 seconds
       forever, just offset by a fixed amount — a repeating pattern rather
       than genuine independence.
     - animation-duration also varies per card (6.4s-10.5s), which is what
       actually prevents that resync. Cards with different periods drift
       in and out of phase with each other continuously, so no two cards
       repeat the same relationship on any predictable cycle.
   Negative delays start each card already mid-breath instead of all six
   inhaling together from "from" at page load. nth-child is positional
   within .resources-cards-grid, so it also survives the grid gaining or
   losing cards later — the 7th card just reuses card 1's timing rather
   than animating in sync with none of them. */
.resources-cards-grid > :nth-child(6n+1).merch-card{ animation-duration: 7.4s; animation-delay: -1.2s; }
.resources-cards-grid > :nth-child(6n+2).merch-card{ animation-duration: 9.1s; animation-delay: -4.6s; }
.resources-cards-grid > :nth-child(6n+3).merch-card{ animation-duration: 6.4s; animation-delay: -2.9s; }
.resources-cards-grid > :nth-child(6n+4).merch-card{ animation-duration: 10.5s; animation-delay: -0.5s; }
.resources-cards-grid > :nth-child(6n+5).merch-card{ animation-duration: 8.2s; animation-delay: -6.1s; }
.resources-cards-grid > :nth-child(6n+6).merch-card{ animation-duration: 11.3s; animation-delay: -3.4s; }
@keyframes card-glow-breathe{
  from{
    border-color: rgba(204,0,0,0.08);
    box-shadow: 0 0 10px rgba(204,0,0,0.18), 0 0 22px rgba(204,0,0,0.14), 0 0 36px rgba(204,0,0,0.07);
  }
  to{
    border-color: rgba(204,0,0,0.18);
    box-shadow: 0 0 18px rgba(204,0,0,0.4), 0 0 38px rgba(204,0,0,0.3), 0 0 60px rgba(204,0,0,0.16);
  }
}
/* Respects the site's existing reduced-motion contract (see the html.js-anim
   rules elsewhere) rather than adding a separate one-off exception. */
@media (prefers-reduced-motion: reduce){
  .card.merch-card{ animation: none; }
}
.merch-card__body .text-muted{ color: var(--text-on-dark-muted); }
.merch-card__media{
  aspect-ratio: 1; background: linear-gradient(155deg, var(--paper-dim), var(--gray-200));
  display: flex; align-items: center; justify-content: center; color: var(--gray-500); position: relative;
  overflow: hidden;
}
.merch-card__media img{ width: 100%; height: 100%; object-fit: cover; display: block; transition: transform var(--dur-slow) var(--ease-out); }
.card:hover .merch-card__media img{ transform: scale(1.05); }

/* ---------- Lightbox (resource-card image preview) ----------
   A single reusable modal triggered by any [data-lightbox-trigger] card via
   main.js. object-fit: contain on the image (never cover) so the full
   generated artwork is always shown intact, uncropped, at whatever size fits
   the viewport — "expanded images should be clear to see" per the design
   brief. The "Coming Soon" chip reuses the same yellow pill already used on
   the card fronts, pinned to a corner instead of overlaid across the image,
   so it reads as a status label rather than competing with the artwork. */
.lightbox{
  position: fixed; inset: 0; z-index: 300;
  display: flex; align-items: center; justify-content: center;
  padding: var(--space-6);
  opacity: 0; visibility: hidden;
  transition: opacity var(--dur-base) var(--ease-out), visibility 0s linear var(--dur-base);
}
.lightbox.is-open{
  opacity: 1; visibility: visible;
  transition: opacity var(--dur-base) var(--ease-out), visibility 0s linear 0s;
}
.lightbox__backdrop{
  position: absolute; inset: 0;
  background: rgba(8,7,6,0.92);
  backdrop-filter: blur(6px) saturate(1.1);
  -webkit-backdrop-filter: blur(6px) saturate(1.1);
}
.lightbox__panel{
  position: relative; z-index: 1;
  max-width: min(1100px, 92vw); max-height: 88vh;
  transform: scale(0.96); transition: transform var(--dur-base) var(--ease-out);
}
.lightbox.is-open .lightbox__panel{ transform: scale(1); }
.lightbox__img{
  display: block; width: auto; height: auto;
  max-width: min(1100px, 92vw); max-height: 88vh;
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  object-fit: contain;
}
.lightbox__close{
  position: absolute; top: -44px; right: 0;
  width: 36px; height: 36px; border-radius: var(--radius-full);
  background: rgba(255,255,255,0.08); border: 1px solid var(--border-dark);
  color: var(--white); display: flex; align-items: center; justify-content: center;
  cursor: pointer; transition: background var(--dur-fast) var(--ease-out);
}
.lightbox__close:hover{ background: rgba(255,255,255,0.18); }
.lightbox__soon{
  position: absolute; top: var(--space-4); left: var(--space-4);
  box-shadow: 0 4px 16px rgba(0,0,0,0.4);
}
@media (max-width: 700px){
  .lightbox__close{ top: var(--space-3); right: var(--space-3); }
}
.merch-card__body{ padding: var(--space-5); }
.merch-card__body h3{ font-size: var(--text-base); margin-bottom: var(--space-1); text-transform: none; letter-spacing: 0; }
.merch-card__price{ font-family: var(--font-mono); color: var(--red); font-weight: 700; }

/* Shown only when the active language isn't English (pure CSS, driven by html[data-lang]) */
.lang-only-note{ display: none; }
html:not([data-lang="en"]) .lang-only-note{ display: flex; }

/* ---------- Download section (GIF + bokeh) ---------- */
.download-section{ position: relative; overflow: hidden; }
.download-section > .container{ position: relative; z-index: 2; }
.download-grid{ display: grid; gap: var(--space-8); align-items: center; }
@media (min-width: 900px){ .download-grid{ grid-template-columns: 1fr 1fr; } }
.download-copy .form-status{ text-align: left; }
.download-visual{ position: relative; display: flex; justify-content: center; }
.download-frame{
  position: relative; z-index: 2; max-width: 340px; width: 100%;
  border-radius: var(--radius-lg); overflow: hidden;
  border: 1px solid var(--border-dark);
  box-shadow: 0 30px 80px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.04) inset;
}
.download-frame img,
.download-frame video{ display: block; width: 100%; height: auto; aspect-ratio: 1 / 1; background: var(--ink); }
.download-glow{
  position: absolute; z-index: 1; inset: -12% ;
  background: radial-gradient(50% 50% at 50% 45%, rgba(204,0,0,0.45), transparent 70%);
  filter: blur(30px); pointer-events: none;
}

/* Bokeh: soft drifting light orbs */
.bokeh{ position: absolute; inset: 0; z-index: 1; overflow: hidden; pointer-events: none; }
.bokeh span{
  position: absolute; border-radius: 50%; filter: blur(14px); opacity: 0.35;
  will-change: transform, opacity; animation: bokeh-drift 14s ease-in-out infinite;
}
.bokeh span:nth-child(1){ width: 120px; height: 120px; left: 6%;  top: 18%; background: rgba(204,0,0,0.55); animation-duration: 15s; }
.bokeh span:nth-child(2){ width: 70px;  height: 70px;  left: 22%; top: 65%; background: rgba(255,212,0,0.4);  animation-duration: 12s; animation-delay: -3s; }
.bokeh span:nth-child(3){ width: 160px; height: 160px; left: 78%; top: 12%; background: rgba(255,255,255,0.18); animation-duration: 18s; animation-delay: -6s; }
.bokeh span:nth-child(4){ width: 90px;  height: 90px;  left: 88%; top: 70%; background: rgba(204,0,0,0.45); animation-duration: 16s; animation-delay: -2s; }
.bokeh span:nth-child(5){ width: 50px;  height: 50px;  left: 45%; top: 30%; background: rgba(255,212,0,0.35); animation-duration: 11s; animation-delay: -5s; }
.bokeh span:nth-child(6){ width: 110px; height: 110px; left: 60%; top: 78%; background: rgba(255,255,255,0.12); animation-duration: 20s; animation-delay: -8s; }
@keyframes bokeh-drift{
  0%,100%{ transform: translate3d(0,0,0) scale(1); opacity: 0.28; }
  50%{ transform: translate3d(18px,-26px,0) scale(1.15); opacity: 0.5; }
}
@media (prefers-reduced-motion: reduce){
  html:not(.force-motion) .bokeh span{ animation: none; }
}

/* ---------- Media strip (in-section video) ---------- */
.media-strip{
  position: relative; border-radius: var(--radius-lg); overflow: hidden;
  border: 1px solid var(--border); box-shadow: var(--shadow-md);
  margin-bottom: var(--space-8); background: var(--ink);
}
.media-strip video{ display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; }
.media-strip__caption{
  position: absolute; left: var(--space-5); bottom: var(--space-4); z-index: 2;
  display: inline-flex; align-items: center; gap: var(--space-2);
  background: rgba(18,17,16,0.78); backdrop-filter: blur(6px);
  color: var(--white); border-radius: var(--radius-full);
  padding: var(--space-2) var(--space-4);
  font-family: var(--font-mono); font-size: var(--text-xs);
  letter-spacing: 0.06em; text-transform: uppercase; font-weight: 700;
}
.media-strip__caption::before{
  content: ""; width: 8px; height: 8px; border-radius: 50%;
  background: var(--yellow); flex-shrink: 0;
}

/* ---------- Interactive image accordion (Lesson Library) ----------
   Panels sit side by side as tall narrow slivers — book spines on a shelf.
   Each title is rotated 90deg so it runs the length of its spine.
   Clicking a panel expands it wide; its title drops to horizontal and shrinks
   so the full thumbnail is visible. */
/* --accordion-h drives everything: an opened panel is exactly 16/9 of it, so a
   16:9 thumbnail fills the panel edge to edge — no letterbox, no backdrop.
   Slivers + an opened panel exceed a narrow viewport, so the shelf scrolls. */
.accordion{
  --accordion-h: 400px;
  --accordion-sliver: 3.25rem;
  display: flex; flex-direction: row; gap: var(--space-2);
  width: 100%; height: var(--accordion-h);
  overflow-x: auto; overflow-y: hidden;
  scrollbar-width: thin;
  /* Mirrors layout AND scroll-anchoring together so panels queue on the
     right and expand open toward the left. row-reverse looks identical at
     rest, but its scroll position isn't consistently anchored across
     browsers as content width changes — it visibly jumps/glitches during
     the hover-expand transition. direction:rtl anchors correctly; each
     panel resets to direction:ltr below so its own text/icons still read
     normally. */
  direction: rtl;
}
.accordion__panel{
  direction: ltr;
  position: relative; overflow: hidden; cursor: pointer;
  flex: 0 0 auto; width: var(--accordion-sliver); height: 100%;
  border: 1px solid var(--border-dark); border-radius: var(--radius-lg);
  background: linear-gradient(135deg, rgba(255,255,255,0.05) 0%, rgba(255,255,255,0.01) 100%);
  padding: 0; text-align: left;
  transition: width 650ms var(--ease-in-out);
  box-shadow: 0 4px 16px rgba(0,0,0,0.3);
}
.accordion__panel:hover{
  border-color: rgba(204,0,0,0.3);
  box-shadow: 0 8px 24px rgba(204,0,0,0.2);
}
.accordion__panel img{
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; object-position: center;
  transition: opacity 500ms var(--ease-out), filter 500ms var(--ease-out);
  opacity: 1; filter: none;
}
.accordion__panel::after{
  content: ""; position: absolute; inset: 0;
  background: none;
  pointer-events: none;
}
.accordion__panel:hover img{ opacity: 1; }
.accordion__trigger{
  position: absolute; inset: 0; z-index: 1;
  width: 100%; height: 100%; background: none; border: 0; cursor: pointer;
}
/* Opened panel's width is derived from the shelf height at a 16:9 ratio, so a
   16:9 thumbnail exactly fills it — object-fit: cover then has zero slack to
   crop or letterbox, and no panel background is ever exposed. */
/* Cap the opened width to the viewport so the panel never overflows the screen;
   object-fit: contain then guarantees the entire thumbnail is visible even when
   the panel can't reach a full 16:9 (the image letterboxes instead of cropping). */
.accordion__panel.is-active{ width: min(calc(var(--accordion-h) * 16 / 9), calc(100vw - 3rem)); }
.accordion__panel.is-active img{ opacity: 1; filter: grayscale(0); object-fit: contain; object-position: center; background: var(--black); }
.accordion__panel:focus-visible{ outline: 3px solid var(--yellow); outline-offset: 3px; }

/* Spine title: rotated to run the height of the sliver, sized to fill it.
   translate(-50%,-50%) centres it first; the rotation then spins it about
   that centre so a long title stays inside the narrow panel. */
.accordion__title{
  position: absolute; left: 50%; top: 50%; z-index: 2;
  transform: translate(-50%, -50%) rotate(90deg); transform-origin: center;
  /* The rotated nowrap box overlaps the whole sliver and would otherwise
     swallow clicks meant for .accordion__trigger underneath it. */
  pointer-events: none;
  white-space: nowrap; color: var(--white);
  font-family: var(--font-display); font-size: 2.2rem; line-height: 1;
  text-transform: uppercase; letter-spacing: 0.04em;
  text-shadow: 0 2px 10px rgba(0,0,0,0.8);
  transition: transform 550ms var(--ease-in-out), font-size 550ms var(--ease-in-out),
              left 550ms var(--ease-in-out), top 550ms var(--ease-in-out);
}
/* Opened: drop to horizontal and shrink out of the thumbnail's way.
   Anchored top-left — the meta block owns the bottom of the panel and the
   title collides with its copy if parked down there. */
.accordion__panel.is-active .accordion__title{
  left: var(--space-5); top: var(--space-4); bottom: auto;
  transform: translate(0, 0) rotate(0deg);
  font-size: 0.7rem; opacity: 0.75;
}

/* Meta block (topic eyebrow, description, download link) is retired: an opened
   panel shows only the small lesson title in its upper-left corner. */
.accordion__meta{ display: none; }
.accordion__meta--legacy{
  position: absolute; left: var(--space-4); right: var(--space-4); bottom: var(--space-4); z-index: 2;
  opacity: 0; transform: translateY(8px); pointer-events: none;
  transition: opacity 400ms var(--ease-out) 150ms, transform 400ms var(--ease-out) 150ms;
}
.accordion__panel.is-active .accordion__meta{ opacity: 1; transform: translateY(0); pointer-events: auto; }
.accordion__topic{
  display: block; font-family: var(--font-mono); font-size: 0.62rem; font-weight: 700;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--yellow); margin-bottom: var(--space-2);
}
.accordion__meta p{
  color: var(--text-on-dark-muted); font-size: var(--text-sm); margin: 0 0 var(--space-3);
  max-width: 46ch;
}
.accordion__meta a{
  display: inline-flex; align-items: center; gap: 6px;
  color: var(--white); font-family: var(--font-display); text-transform: uppercase;
  font-size: var(--text-sm); letter-spacing: 0.04em; border-bottom: 2px solid var(--red);
  padding-bottom: 2px;
}
.accordion__meta a:hover{ color: var(--yellow); border-color: var(--yellow); }

.accordion__play{
  display: none;
  position: absolute; top: 1rem; right: var(--space-4); z-index: 2;
  width: 40px; height: 40px; border-radius: 50%;
  background: rgba(204,0,0,0.9); color: var(--white);
  align-items: center; justify-content: center;
  opacity: 0; transition: opacity 400ms var(--ease-out), transform 400ms var(--ease-out);
  transform: scale(0.85);
}
.accordion__panel.is-active .accordion__play{ opacity: 1; transform: scale(1); }
.accordion__play svg{ margin-left: 2px; }

/* Phones: nine 3.25rem slivers overflow a 375px screen, so narrow the spines
   and lengthen the shelf to keep the rotated titles readable. */
@media (max-width: 700px){
  /* Phones flip the shelf vertical: each lesson is a full-width horizontal bar
     with its title upright and fully legible — no rotation, no squeezing, no
     sideways scrolling. The active bar grows tall to an exact 16:9 of the
     available width, so the thumbnail fills it edge-to-edge with zero
     letterboxing and zero cropping slack. */
  /* column-reverse flips the stack so Mindset (last in the DOM) leads the
     list and Dealing With Police closes it — mobile only. */
  .accordion{
    flex-direction: column-reverse;
    height: auto;
    gap: 6px;
    overflow: visible;
    direction: ltr;
  }
  .accordion__panel{
    width: 100%;
    height: 3.25rem;
    flex: 0 0 auto;
    border-radius: var(--radius-md);
    transition: height 550ms var(--ease-in-out);
  }
  .accordion__panel.is-active{
    width: 100%;
    height: calc((100vw - 3rem) * 9 / 16);
  }
  .accordion__title{
    transform: translate(-50%, -50%) rotate(0deg);
    /* Doubled from 1.2rem; the min() guard only steps in on the longest titles
       so they can never spill past the bar's edge. */
    font-size: min(2.4rem, 7.4vw);
    letter-spacing: 0.06em;
  }
  .accordion__panel.is-active .accordion__title{
    left: var(--space-4); top: var(--space-3); bottom: auto;
    transform: none;
  }
  .accordion__meta{ left: var(--space-3); right: var(--space-3); bottom: var(--space-3); }
  .accordion__meta p{ display: none; } /* keep the open card clean — image + title + link only */
}
@media (prefers-reduced-motion: reduce){
  html:not(.force-motion) .accordion__panel,
  html:not(.force-motion) .accordion__title,
  html:not(.force-motion) .accordion__meta,
  html:not(.force-motion) .accordion__panel img{ transition: none; }
}

/* ---------- Lesson cards (Training) ---------- */
.lesson-card{
  position: relative; display: block; background: var(--white);
  border-radius: var(--radius-lg); overflow: hidden; border: 1px solid var(--border);
  box-shadow: var(--shadow-sm); text-decoration: none; color: inherit;
  transition: transform var(--dur-base) var(--ease-out), box-shadow var(--dur-base) var(--ease-out);
}
.lesson-card:hover{ transform: translateY(-4px); box-shadow: var(--shadow-md); }
.lesson-card__media{ position: relative; aspect-ratio: 16 / 9; overflow: hidden; background: var(--gray-200); }
.lesson-card__media img{ width: 100%; height: 100%; object-fit: cover; transition: transform var(--dur-slow) var(--ease-out); }
.lesson-card:hover .lesson-card__media img{ transform: scale(1.05); }
.lesson-card__media::after{
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(180deg, transparent 55%, rgba(18,17,16,0.55));
  pointer-events: none;
}
.lesson-card__badge{
  position: absolute; top: var(--space-3); left: var(--space-3); z-index: 2;
  display: inline-flex; align-items: center; gap: 6px;
  background: rgba(18,17,16,0.82); color: var(--white); backdrop-filter: blur(4px);
  font-family: var(--font-mono); font-size: 0.62rem; letter-spacing: 0.08em; text-transform: uppercase;
  padding: 5px 10px; border-radius: var(--radius-full); font-weight: 700;
}
.lesson-card__badge svg{ color: var(--yellow); }
.lesson-card__play{
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0.9); z-index: 2;
  width: 58px; height: 58px; border-radius: 50%;
  background: rgba(204,0,0,0.92); color: var(--white);
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 8px 24px rgba(0,0,0,0.35);
  transition: transform var(--dur-base) var(--ease-out), background var(--dur-base) var(--ease-out);
}
.lesson-card:hover .lesson-card__play{ transform: translate(-50%, -50%) scale(1); background: var(--red); }
.lesson-card__play svg{ margin-left: 3px; }
.lesson-card__label{
  position: absolute; left: var(--space-4); right: var(--space-4); bottom: var(--space-3); z-index: 2;
  color: var(--white);
}
.lesson-card__label strong{
  display: block; font-family: var(--font-display); font-size: var(--text-md);
  text-transform: uppercase; letter-spacing: 0.02em; line-height: 1.05;
  text-shadow: 0 2px 8px rgba(0,0,0,0.5);
}
.lesson-card__body{ padding: var(--space-4) var(--space-5) var(--space-5); }
.lesson-card__body p{ font-size: var(--text-sm); color: var(--text-muted); margin: 0; }
.lesson-card__topic{
  display: inline-block; font-family: var(--font-mono); font-size: var(--text-xs);
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--red); font-weight: 700;
  margin-bottom: var(--space-2);
}

/* Article */
.article-body{ max-width: var(--container-narrow); margin-inline: auto; font-size: var(--text-md); }
.article-body h2{ margin-top: var(--space-10); margin-bottom: var(--space-4); font-size: var(--text-2xl); }
.article-body h3{ margin-top: var(--space-8); margin-bottom: var(--space-3); }
.article-body p{ margin-bottom: var(--space-5); max-width: none; color: var(--text-muted); }
.article-body ul{ margin-bottom: var(--space-5); padding-left: var(--space-6); list-style: disc; color: var(--text-muted); }
.article-body li{ margin-bottom: var(--space-2); }
.article-meta{ display: flex; flex-wrap: wrap; gap: var(--space-4); align-items: center; color: var(--text-muted); font-size: var(--text-sm); margin-bottom: var(--space-6); }

/* Helmet band (formerly fact-typewriter section — the typing text and its
   CSS/JS were removed and saved to assets/snippets/fact-typewriter.html).
   Only the image band itself remains in use on the homepage. */
.fact-typewriter-section{
  position: relative;
  min-height: calc(max(200.38px, 30.05vw) + 63.70px);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  background: #0A0908;
  padding-top: var(--space-4);
  padding-bottom: calc(var(--space-4) + 11.14px);
}
.fact-typewriter-spotlight{
  position: absolute;
  inset: 0;
  z-index: 0;
  background: url('../assets/img/fact-typewriter-bg.png');
  /* cover keeps the helmet filling the section edge to edge AND top to bottom at
     every viewport size — no black bands. background-position picks the crop. */
  background-size: cover;
  background-position: center 36%;
  background-repeat: no-repeat;
  pointer-events: none;
}

/* ---------- Hero delayed background reveal ----------
   Background image sits in its own layer beneath the dark overlay and text.
   After a 2s wait it reveals from the centre of the hero outward to the edges
   over 3s, filling the whole section. A radial-gradient mask (grown from the
   centre) gives the expanding edge a soft feather, so the reveal reads as a
   buttery bloom rather than a hard circle sweep; a short opacity fade rides
   along at the start. Text and overlay are unaffected — only this layer
   animates. The mask's soft stops mean it reaches the far corners by ~200%
   mask-size regardless of the hero's aspect ratio. */
/* Heroes clip their reveal layer so the Ken-Burns zoom and scroll parallax
   never bare an edge. */
.hero{ overflow: hidden; }
.hero__bg-fade{
  position: absolute;
  inset: 0;
  z-index: 0;
  background-size: cover;
  background-repeat: no-repeat;
  opacity: 0;
  /* scale gives the Ken-Burns zoom its baseline AND a margin the parallax
     translate lives inside; scale + translate are independent transform
     properties so the zoom (CSS) and parallax (JS) never fight over transform.
     transform-origin defaults to center so the zoom is invisible to normal
     heroes, but pages that anchor background-position to an edge (e.g. top)
     need the zoom to grow away from that edge too, or it crops back into it
     as it scales — those pages override --hero-transform-origin. */
  transform-origin: var(--hero-transform-origin, center);
  scale: calc(1.08 * var(--hero-zoom, 1));
  -webkit-mask-image: radial-gradient(circle at 50% 50%, #000 0%, #000 40%, rgba(0,0,0,0) 72%);
  mask-image: radial-gradient(circle at 50% 50%, #000 0%, #000 40%, rgba(0,0,0,0) 72%);
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
  -webkit-mask-size: 0% 0%;
  mask-size: 0% 0%;
  filter: blur(14px) saturate(0.2) brightness(0.7);
  will-change: mask-size, opacity, filter, scale, translate;
  backface-visibility: hidden;
  animation:
    hero-bg-reveal 3.3s cubic-bezier(0.65, 0, 0.35, 1) 0s forwards,
    hero-ken-burns 26s ease-in-out 0s infinite alternate;
}
/* Reveal: bloom the mask open while the image sharpens (blur→0) and colour
   floods in (grey/dark→full). */
@keyframes hero-bg-reveal {
  0%{
    opacity: 0;
    -webkit-mask-size: 0% 0%;
    mask-size: 0% 0%;
    filter: blur(14px) saturate(0.2) brightness(0.7);
  }
  45%{
    opacity: 1;
  }
  100%{
    opacity: 1;
    -webkit-mask-size: 260% 260%;
    mask-size: 260% 260%;
    filter: blur(0) saturate(1) brightness(1);
  }
}
/* Ken Burns: a slow, endless breathing zoom that keeps the hero alive after
   the reveal settles. */
@keyframes hero-ken-burns {
  from{ scale: calc(1.08 * var(--hero-zoom, 1)); }
  to{ scale: calc(1.14 * var(--hero-zoom, 1)); }
}
/* These decorative hero motions are intentional design choices, so keep them
   playing even when the OS/browser requests reduced motion — scoped to the
   hero layers only, so every other animation still respects the preference. */
@media (prefers-reduced-motion: reduce){
  html:not(.force-motion) .hero__bg-fade{
    animation-duration: 3.3s, 26s !important;
    animation-delay: 0s, 0s !important;
    animation-iteration-count: 1, infinite !important;
    animation-timing-function: cubic-bezier(0.65, 0, 0.35, 1), ease-in-out !important;
  }
}

/* Shop hero: on desktop the merch collage needs to read as a whole banner on
   load, so the base 1.08 Ken-Burns floor (which crops in from the very first
   frame) is overridden to start at true scale:1 - the complete image, nothing
   cropped - then breathe out to a gentler 1.06 instead of 1.08-1.14. Mobile is
   left on the shared base behaviour, since it's already tightly cropped by
   necessity at that width. */
@media (min-width: 768px){
  /* scale:1 alone still crops, because `cover` crops whenever the box and the
     image disagree on shape. Giving the section the image's own 1672:941 ratio
     removes that disagreement entirely - cover then has zero slack, so frame
     one is the complete banner, edge to edge, uncropped. The Ken-Burns zoom
     departs from that full frame. min-height is zeroed so it can't re-impose
     a taller box and reintroduce the crop. */
  .hero--shop-bg{
    aspect-ratio: 1672 / 941;
    min-height: 0;
  }
  .hero--shop-bg .hero__bg-fade{
    scale: 1;
    animation:
      hero-bg-reveal 3.3s cubic-bezier(0.65, 0, 0.35, 1) 0s forwards,
      hero-ken-burns-shop 26s ease-in-out 0s infinite alternate;
  }
}
/* Mobile now matches the home/training full-screen treatment instead of the
   old fixed-ratio banner (max(360px,45vw), ~360-410px tall). Desktop is
   completely untouched — this only lives inside this max-width query, the
   >=768px block above is unchanged. */
@media (max-width: 767.98px){
  /* 87vh rather than a full 100vh, and that difference is the whole point.
     `cover` on a portrait box shows only a narrow vertical slice of this
     landscape banner — at 100vh that slice is ~26% of the image width, but
     the mascot badge together with the "CERTIFIED SURVIVAL RIDER" lettering
     above his head spans ~27%, so at full height the pair can never both fit
     and the text was always clipped. A slightly shorter box is a less
     portrait box, which widens the slice to ~30% and gives the mascot and his
     title room to sit together with margin to spare. */
  /* 70vh — a further 10% zoom-out from 78vh (which was itself 10% below the
     original 87vh). Height is the lever here, not background-size: with
     `cover` on a portrait box the scale is driven entirely by height, so
     shrinking the box scales the artwork down by the same proportion and
     widens the visible slice, now to ~37% of the image. Doing the same with
     background-size:auto 90% would zoom out identically but leave a letterbox
     gap, since the box height itself wouldn't have moved. */
  /* 70vh -> 100svh, per the "every hero fills the screen" rule. The comments
     above explain why this one was deliberately SHORT: its source is a
     LANDSCAPE banner, and `cover` on a portrait box takes its scale from the
     height, so a taller box means a narrower visible slice of the artwork.
     At 100svh that slice narrows to ~26% of the image width while the mascot
     badge plus the "CERTIFIED SURVIVAL RIDER" lettering spans ~27% — so the
     lettering clips. Filling the screen and showing that pair intact are
     mutually exclusive with this landscape source; it needs a portrait crop
     of its own to have both. Flagged, not hidden. */
  .hero--shop-bg{ min-height: 100svh; }
  /* Sits the title low in the hero rather than centred over the mascot's face.
     The section is display:flex/align-items:center, so absolute positioning is
     what takes it out of that centring; the parent .container is already
     position:relative + z-index:2, so this lands above the dark scrim. */
  .shop-hero__title{
    display: inline-block !important;
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
    bottom: 4%;
    margin: 0;
    white-space: nowrap;
  }

  .hero--shop-bg .hero__bg-fade{
    /* Mobile-only file, ~25% zoomed out, on request — reveals more of the
       beach/palm/hut background around the mascot rather than the tight crop
       the shared desktop file forces here.
       This could NOT be done by just changing background-size/scale: the
       original 1672x941 file already renders at exactly box height with
       `cover` (scale = boxH/imgH, zero vertical slack), so the crop was
       already using 100% of the image's height — there was no lower scale
       available without leaving a gap top/bottom. Zooming out required the
       canvas itself to get taller, which is why this is a SEPARATE file
       (1672x1255) rather than a CSS-only change: the shared original is what
       desktop's `aspect-ratio:1672/941` box is deliberately keyed to, so
       editing it in place would have broken that exact-ratio fit.
       The added 314px is real pixels, not a blur/gradient guess: a first pass
       mirrored a plain edge band and it read as broken — the "CERTIFIED" and
       "RIDE SAFE" sticker text sits only ~90px from the top and ~60px up
       from the bottom, so a naive mirror pulled logo text into the frame
       backwards. Rebuilt using only the verified sticker-free bands (rows
       0-88 top, 905-940 bottom — the point where near-black outline pixels
       jump sharply is where the sticker art starts) and ping-pong-folding
       those, so every repeat is real sky/palm or real deck grain and no
       logo is ever reflected. */
    /* ../ because this URL lives in an external stylesheet (css/style.css),
       so it resolves relative to THAT file's folder, not the HTML page's —
       unlike every other background-image on the site, which is set via
       inline style="" on the element and resolves relative to the page.
       Without the ../ this 404'd as css/assets/img/... and the hero rendered
       solid black. */
    background-image: url('../assets/img/store-items-for-website-hero-image-banner-mobile.png') !important;
    /* Centres that mascot-plus-title group (x37-64% of the source), not the
       geometric centre of the whole collage. Unaffected by the taller file —
       width is unchanged, so the same horizontal anchor still lands on the
       mascot. */
    background-position-x: 51% !important;
    /* Starts at true 1:1 instead of the shared 1.08 Ken-Burns floor, which
       would re-crop ~7% off every edge and eat the lettering we just made
       room for. The gentler shop zoom departs from the full frame. */
    scale: 1;
    animation:
      hero-bg-reveal 3.3s cubic-bezier(0.65, 0, 0.35, 1) 0s forwards,
      hero-ken-burns-shop 26s ease-in-out 0s infinite alternate;
  }
}
@keyframes hero-ken-burns-shop{
  from{ scale: 1; }
  to{ scale: 1.06; }
}

/* Founder hero — same treatment as the shop banner, and for the same reason.
   Two things crop a hero on frame one, and both have to go:
     1. the base scale:1.08 Ken-Burns floor, which starts already zoomed in
     2. `cover`, which crops whenever the box and the image disagree on shape
   Zeroing the scale fixes (1). Giving the section the photo's own 1536:864
   ratio fixes (2) — cover then has no slack at all, so the first frame is the
   complete photo edge to edge, and the zoom departs from that full frame
   rather than from an already-cropped one. min-height must be 0 here or a
   taller box would reintroduce exactly the disagreement we just removed. */
@media (min-width: 768px){
  .hero--daniel-bg{
    aspect-ratio: 1536 / 864;
    min-height: 0;
  }
  .hero--daniel-bg .hero__bg-fade{
    scale: 1;
    animation:
      hero-bg-reveal 3.3s cubic-bezier(0.65, 0, 0.35, 1) 0s forwards,
      hero-ken-burns-daniel 26s ease-in-out 0s infinite alternate;
  }
}
/* Card carrying the bio's opening paragraph, moved out of the hero (see
   about.html and the .hero--daniel-bg .hero__desc{display:none} rule below).
   Hidden by default — desktop keeps the text on the photo exactly as before
   and never sees this element at all. Declared here, ahead of the
   max-width:767.98px query that turns it on, so the two rules resolve by
   source order the way every other mobile-only toggle on this site does. */
.about-mobile-desc-card{ display: none; }

/* Mobile now matches the home/training full-screen treatment instead of the
   old fixed-ratio banner. Desktop (the >=768px block above) is untouched.
   background-position-x centres roughly columns 2.5-4.5 of the source photo
   (Daniel's eyes/nose/mouth plus his shoulder), matching what the natural
   ~26%-wide crop window at this box's height can actually contain — a bit of
   background road/temple shows on the right, which is intentional. */
@media (max-width: 767.98px){
  /* The card itself: dark panel with the site's standard red glow language
     (same rgba(204,0,0) family used by .merch-card and the testimonial
     cards), so it reads as belonging to this design system rather than a
     one-off box. Sits above "The Long Version" eyebrow in source order
     already (see the HTML), so no positioning is needed here beyond spacing
     it from what follows. */
  .about-mobile-desc-card{
    display: block;
    /* Angled gradient rather than the previous flat #0A0908 — gives the panel
       some depth so it reads as a feature block, not a plain text box. */
    background: linear-gradient(157deg, #16100F 0%, #0A0908 55%);
    border: 1px solid rgba(204,0,0,0.3);
    border-radius: var(--radius-lg);
    /* Stronger than the card's old glow, deliberately: this is the founder
       statement and it should out-weigh the resource cards further down the
       page, which carry 0.3/0.22/0.12. */
    box-shadow: 0 0 26px rgba(204,0,0,0.3), 0 0 58px rgba(204,0,0,0.18);
    /* Extra top padding reserves room for the kicker rule below. */
    padding: 2.5rem 1.25rem 1.75rem;
    margin-bottom: 2rem;
    position: relative;
    overflow: hidden;
  }
  /* Short red kicker bar centred above the text — the visual cue that this is
     a pull-quote/statement rather than body copy, borrowed from the same
     language the hazard-tags use elsewhere on the site. */
  .about-mobile-desc-card::before{
    content: "";
    position: absolute;
    top: 1.25rem;
    left: 50%;
    transform: translateX(-50%);
    width: 52px;
    height: 3px;
    border-radius: 2px;
    background: var(--red);
    box-shadow: 0 0 12px rgba(204,0,0,0.9);
  }
  .about-mobile-desc-card p{
    margin: 0;
    color: #fff;
    /* 17px -> 31px, and switched from Inter to the DISPLAY face. Both changes
       are needed to read as a headline: doubling Inter alone would just be
       oversized body copy. Barlow Condensed is also what makes 31px fit a
       304px column at all — a non-condensed face at this size would run to
       far more lines. */
    font-family: var(--font-display);
    font-size: 31px;
    /* Tight leading is what makes large type look composed instead of loose;
       1.6 was correct for 17px body copy and would look airy here. */
    line-height: 1.14;
    font-weight: 700;
    letter-spacing: 0.005em;
    text-align: center;
    text-wrap: balance;
  }
  /* Opening line in red — an editorial lead-in that gives the block a focal
     point and a reading entry, without needing the sentence split into
     separate elements (which would mean re-authoring the string across all
     8 language files). ::first-line adapts automatically to wherever the
     text actually wraps, at any width or translation. */
  .about-mobile-desc-card p::first-line{
    color: var(--red);
    text-shadow: 0 0 18px rgba(204,0,0,0.55);
  }
  /* 82vh is the zoom-out limit, not a taste call: with `cover` on a portrait
     box the scale is driven by height, so a shorter box shows more of the
     image's width — but the hero's own text (476px column + 117/96px padding)
     needs 689px = 81vh before it starts overflowing. 82vh sits just above that
     floor and widens the visible slice from 26% to 32% of the photo. */
  /* Full screen on every phone — height from the VIEWPORT, not from the
     artwork's 768x1376 ratio. The previous artwork-derived height showed the
     whole picture but left the hero ~90-110px short of filling the screen on
     every size tested, which is the wrong trade for a hero.
     svh rather than vh: 100vh on mobile is the height with the address bar
     retracted, so it overshoots what is actually visible at load — precisely
     when "does it fill the screen" gets judged. */
  .hero--daniel-bg{ min-height: 100svh; }
  /* 13% slides the artwork left by ~108px (~27% of a 393px screen), which is
     where the annotated line sat, bringing Daniel's face in off the right and
     closer to the centre of frame. Visible window is x8.7-40.7% of the photo.
     Note this trades away the very left edge of the helmet that 0% preserved —
     the two cannot both hold, since the 32%-wide window has to sit somewhere. */
  /* The phone now gets its own 9:16 artwork instead of a panned crop of the
     landscape shot, so the old 13% x-offset (which chased Daniel's face across
     the wide frame) no longer applies — it only governs the desktop layer,
     which is hidden here anyway.
     `contain`, not `cover`, for the same reason as the other two swapped
     heroes: the file is 768x1376 = 0.558 but this screen is 393x851 = 0.462,
     taller than 16:9, so `cover` would slice ~17% off the sides — exactly
     where the MEET DANIEL lettering and the Thai flag sit. */
  .hero--daniel-bg .hero__bg-fade--desktop{ display: none; }
  .hero--daniel-bg .hero__bg-fade--mobile{
    display: block;
    /* `cover`, reversing the earlier `contain`: fill the screen edge to edge
       from the first frame. `contain` was fitting the whole 9:16 artwork
       inside a taller viewport, which left black bands rather than filling —
       the thing being asked for here is the fill, with the resulting side
       crop accepted. */
    background-size: cover !important;
    /* center -> center bottom. At this box's proportions the file only has
       42px of vertical slack total (rendered 893px tall in an 851px box), so
       this is the full range available: bottom-anchoring spends all of it on
       the top edge, showing more of Daniel's chest/torso and less of the sky
       above "MEET DANIEL" — the "bring the image up" effect, maxed out
       within what this file has to give. */
    /* center 40%, NOT bottom. Bottom-anchoring was chosen to show more of
       Daniel's torso, but the baked-in "MEET DANIEL" wordmark sits high in
       the file (rows 414-783 of 1745, i.e. 24-45% down), so spending the
       whole vertical overflow on the top edge pushed it off-screen on short
       phones — measured at -174px on a 568px screen and -102px at 640px,
       meaning the page title was simply gone on the devices most likely to
       show it.
       A PERCENTAGE splits the overflow proportionally instead of all-or-
       nothing, so it self-corrects at every height: 40% is the largest share
       that can be taken off the top while still clearing the 53px header at
       the shortest screen (568px leaves the wordmark at 69px, a 16px
       margin), and it still pulls the framing down toward the torso on
       taller screens where there is slack to spare. */
    background-position: center 40% !important;
    background-repeat: no-repeat !important;
    background-color: var(--black);
    /* Full-bleed layer (top:0). With `cover` the image scales to fill this
       box, so insetting it below the header would only shrink the fillable
       area and crop more off the sides. The header floats over the image. */
    top: 0 !important;
    height: 100% !important;
    /* Ken Burns restored, and it starts at exactly 1 so the first painted
       frame is the filled, un-zoomed image — then it grows.
       It was previously switched off entirely because, on top of `contain`,
       any zoom re-introduced a crop the old framing was trying to avoid; with
       `cover` that objection is gone.
       Note hero-bg-reveal must stay in the list: this layer's resting state is
       opacity:0, so dropping it (rather than just the ken-burns keyframes)
       leaves nothing to fade the photo in and it never appears at all.
       1 -> 1.09: "a little" zoom in, on request, to trim the sky. 851px box *
       0.09 ≈ 76px of additional crop, which is the ~2cm asked for (1cm ≈
       37.8 CSS px at the standard 96px/inch reference used throughout CSS).
       The Ken Burns keyframes below are shifted by the same +0.09 so the
       animation's own 0.12 range (how much it grows over the 26s cycle) is
       preserved on top of this new floor, rather than compressed. */
    scale: 1.09 !important;
    /* bottom, not the shared default (center). This hero was just carefully
       bottom-anchored via background-position so his torso/the scooter stay
       framed at the box's foot — a centre-origin zoom would eat into that
       framing from both ends. Bottom origin keeps that edge fixed and spends
       the entire new 76px of zoom on the top/sky, which is what "remove the
       sky, don't crop past his ear" actually asks for: one edge protected,
       the other free to crop. */
    --hero-transform-origin: bottom;
    animation:
      hero-bg-reveal 3.3s cubic-bezier(0.65, 0, 0.35, 1) 0s forwards,
      hero-ken-burns-daniel-mobile 26s ease-in-out 0s infinite alternate !important;
  }

  /* The two-column grid (an empty decorative first column + the text column)
     is desktop-only layout, with no responsive collapse of its own — on a
     393px phone it squeezed "Meet Daniel" and the bio paragraph into roughly
     half the screen width, forcing enough line-wraps that the text block
     alone needed 924px of height, taller than the entire 851px viewport, and
     blew the hero section out to 1185px+. Spanning the text column across
     both grid tracks gives it the full container width, matching how every
     other hero on the site (single .container--narrow column) already
     behaves on mobile. The decorative empty column is hidden outright rather
     than left to occupy a zero-content grid track. */
  .hero-daniel__decor{ display: none; }
  .hero--daniel-bg .hero__text-column{
    grid-column: 1 / -1;
    text-align: center;
  }

  /* Drops the whole text block to the foot of the hero so Daniel's face is
     left unobstructed. The section is display:flex/align-items:center INLINE,
     hence !important; flex-end anchors the column to the bottom instead, and
     the 52px padding is what puts the button's baseline on the intended line
     (hero 737px - 52px = 685px). The .hero__actions margin has to go with it,
     or that 48px would sit under the button and push it back up. */
  .hero--daniel-bg{
    align-items: flex-end !important;
    padding-bottom: 52px !important;
  }
  /* margin-top pushes the button down ~44px to the marked line without
     touching anything above it — the eyebrow/heading/description keep their
     existing position, only the button moves. */
  /* 44px -> 74px. Back to 74: raising this further does NOT move the button
     down. The hero is display:flex with align-items:flex-end, so the text
     column is pinned to the hero's foot — adding margin above the button just
     makes the column taller, which pushes the TEXT up while the button stays
     put at the column's bottom edge. Verified: at 94px the button measured the
     same 646px as at 74px. */
  .hero--daniel-bg .hero__actions{ margin-bottom: 0 !important; margin-top: 74px; }
  /* The button's own 20px drop to the marked line, done as a painted-only
     relative offset. That is what moves the button ALONE: it shifts where the
     element draws without changing its box in flow, so the column's height —
     and therefore every line of text above it — is untouched. Reducing the
     hero's bottom padding would have moved the whole block down together. */
  .hero--daniel-bg .hero__actions .btn{
    position: relative;
    /* 20 -> 32 -> 48px: dropped to sit at the hero's foot, right on the seam
       with the section below. This is what buys the description its room —
       every pixel the button moves down is a pixel the paragraph can use
       WITHOUT creeping up over Daniel's face, which was the binding
       constraint. Lands its bottom at ~753px against the 757px hero edge. */
    top: 48px;
  }
  /* Cinematic bottom scrim, replacing the floating radial patch behind the
     paragraph. This is the fix for "looks weak": a soft blob behind text reads
     as a smudge on the photo, whereas a gradient anchored to the frame's
     bottom edge reads as deliberate grading — it is what film posters and
     streaming thumbnails use for exactly this problem.
     Practical advantages over the radial: contrast is even across the whole
     line (so no word sits on a lighter patch than its neighbour), it has no
     edges to clip, and because it is anchored to the BOTTOM and fully
     transparent by 42% up, it cannot touch his face — the thing that must
     stay clear. Sitting at z-index 1 keeps it above the photo but below the
     text container, which the markup already puts at z-index 2. */
  /* Bottom scrim removed on request. It existed only to hold contrast behind
     .hero__desc's white text; that paragraph has since moved out of the hero
     entirely into a card in the section below, so there's no text left down
     here for it to serve — it was just an unexplained dark band across
     Daniel's chest with nothing to justify it. */
  .hero--daniel-bg::after{ content: none; }
  /* Black radial glow behind this column removed. It's the same 800x800
     circle the contact hero uses, centred on the text column — here that
     centre landed on Daniel's face above the eyebrow, exactly the area the
     next two changes are trying to keep clear. No shadow replaces it: on a
     bright daylight photo (unlike contact's darker shot) plain white text
     already reads fine without a backing. */
  .hero--daniel-bg .hero__text-column::before{ display: none; }
  /* Eyebrow down to the first mark — was riding right against "MEET DANIEL"'s
     baked-in wordmark; the request was to open daylight between the two.
     position:relative, not margin: this column is align-items:flex-end
     pinned to the hero's foot (the same trap the button hit earlier), so
     margin-top on any child just grows the box upward rather than moving
     content down — only a painted offset actually displaces it. */
  /* 46 -> 102 -> 80px. Both changes came from live re-measurement, not
     arithmetic on old numbers — this column's natural (unshifted) layout
     shifts every time a sibling's rendered height changes (the column is
     align-items:flex-end, bottom-pinned, so a shorter/taller description
     changes where the WHOLE column starts, moving the eyebrow with it even
     though nothing was touched on the eyebrow itself). The description was
     just shrunk to a smaller size below, which changed its flow height and
     therefore moved the natural eyebrow position too — 80px is what restores
     a clean 16px gap to the (also-adjusted) description underneath, measured
     fresh against that new size rather than reused from before the resize. */
  /* 80 -> -393px: this is a completely different placement than before — the
     eyebrow now tucks up near the header instead of sitting beside the
     description, so it's no longer computed relative to that old value.
     Natural (unshifted) top measured at 451px; -393 lands it at 58px, just
     clear of the 53px header, tucked in the upper-left corner per the ask.
     It sits close under "MEET" (baked into the artwork, not this CSS) but no
     longer anywhere near the description, so the two blocks read as
     independent now rather than one adjacent pair. */
  /* Removed on request — it was overlapping the artwork's baked-in "MEET"
     wordmark regardless of how it was positioned. display:none rather than
     deleting the positioning rules below, in case it's wanted back later. */
  /* Eyebrow restored and pinned to the TOP of the hero, centred.
     Absolute against the hero itself, which needs .container switched to
     position:static — otherwise the eyebrow would anchor to that container,
     and the container is bottom-pinned (align-items:flex-end), so its top
     edge moves whenever the column's content height changes. That is exactly
     what made the earlier relative-offset version drift by 20-52px and end up
     hidden behind the header twice. Anchoring to the hero, whose height is
     fixed at 100svh, removes that whole class of drift.
     Making the container static costs it the inline z-index:2 that kept its
     contents above the 0.15 black overlay, so the button gets its own
     positioned stacking below to compensate. */
  /* BOTH ancestors have to go static, not just .container: .hero__text-column
     carries its own position:relative from the base rule near line 1295 (it
     was there to anchor the ::before glow, which is display:none here), and
     being the nearer positioned ancestor it was capturing the eyebrow —
     measured top:726px instead of the intended ~61px. */
  .hero--daniel-bg > .container,
  .hero--daniel-bg .hero__text-column{ position: static !important; }
  .hero--daniel-bg .hero__actions{ position: relative; z-index: 2; }
  .hero--daniel-bg .hero__eyebrow{
    display: flex !important;
    position: absolute;
    top: calc(var(--header-h) + 8px);
    left: 0;
    right: 0;
    justify-content: center;
    margin: 0;
    z-index: 10;
    /* nowrap + 10px for the same reason as before: at smaller sizes the
       string breaks after "PROJECT" and drops "DIRECTOR" onto a second row.
       One line is what keeps it clear of the artwork's "MEET" wordmark. */
    white-space: nowrap;
    font-size: 10px;
  }
  .hero--daniel-bg .hero__eyebrow--unused-old{ display: none; }
  .hero--daniel-bg .hero__eyebrow--unused{
    position: relative;
    /* -393 -> -445. Collapsing this to one line (below) made the column
       shorter, and because the column is bottom-pinned that pushed every
       child's natural position DOWN by ~52px — so the same offset no longer
       landed in the same place. Re-measured rather than reused. */
    top: -445px;
    /* The overlap with the artwork's "MEET" was caused by WRAPPING, not by
       horizontal position: at 8.5px the line broke after "PROJECT", dropping
       "DIRECTOR" onto a second row that sat directly on the wordmark. Sliding
       left could never fix that — "DIRECTOR" is ~70px wide and "MEET" starts
       ~59px in, so the two overlap at any left position.
       Forcing one line removes the second row altogether. 10px is what makes
       that fit: measured across 12/11/10/9.5/9/8.5px, the string is 369px at
       12px and 372px at 11px (both wider than the 361px available), dropping
       to 319px at 10px. Counter-intuitively this is BIGGER than the 8.5px it
       replaces — the smaller size was only small because it was wrapping. */
    white-space: nowrap;
    font-size: 10px;
  }
  /* Description down further still, to clear more of the face per the second
     mark — replaces the standalone +82px this carried from an earlier,
     unrelated request (pulling it into a differently-marked band) rather than
     stacking on top of it, which would have pushed it into the button below.
     +102px nets a modest further drop beyond the eyebrow/title's 46px while
     leaving clearance above the button, whose own position is untouched. */
  .hero--daniel-bg .hero__desc{
    top: 102px;
  }
  /* Description pulled into the marked band (489-630px) so Daniel's face is
     clear above it, overlapping only his chin.
     Two changes are needed because the block was both too TALL and too HIGH:
       - 19.03px -> 17px at 1.45 line-height drops it from 183px/6 lines to
         123px/5 lines. 18px was measured too: it still wrapped to 6 lines and
         came out 157px, over the band's 141px, so 17px is the first size that
         actually fits — it's the line-count drop doing the work, not the
         point size alone.
       - top:82px then lowers it into place. The text column is pinned to the
         hero's foot, so shrinking the text moves its TOP down while its
         bottom stays put; a painted-only offset moves the whole block without
         resizing the column or disturbing the eyebrow above it.
     Lands at 502-625, clearing the button (painted from 637) by 12px. */
  .hero--daniel-bg .hero__desc{
    /* SUPERSEDED by the size below — matching the section-below's 16px still
       left the paragraph (5 lines, ~128px tall) too tall to fit inside the
       newly marked rectangle. Kept color, since white-on-photo is what the
       shadow below is now doing the legibility work for; size and line-height
       are overridden by the rule that follows. */
    color: rgb(245,245,245) !important;
    position: relative;
  }
  /* 12px -> 15px: sized back up for legibility, now that the eyebrow has
     moved away to the header (see above) and no longer constrains how much
     vertical room this paragraph gets. 15px/1.3 line-height runs 98px tall
     over 5 lines — bigger than the 12px/4-line version, but there is now
     space for it because nothing sits directly above it competing for room.
     top:90px (natural top measured 505px) drops it to 595px, closer to the
     button per the ask — 16px clear above the button's own 709px top, and
     lower than the 12px version's 583px, so if anything it clears MORE of
     the chin than before despite the larger type. */
  .hero--daniel-bg .hero__desc{
    /* Stretched to the marked side lines and sized as large as that allows.
       Width first: spanning x=10..371 (361px, vs the column's 337px) is what
       actually buys the type size — a wider measure fits more words per line,
       so the SAME text drops from 5 lines to 4. The negative margin breaks it
       out of the text column's own inset to reach those marks.
       14px/1.2 is then the largest that still holds 4 lines: measured across
       13-18px, 15px and above re-wrap to 5 lines (90px+ tall), which no
       longer fits between the marked line and the button. 4 lines at 14px is
       67px tall — 12px clear of the button below. 13px would also fit but is
       needlessly smaller, and the earlier 12px was the size called out as
       too small to read. */
    /* 14 -> 15px, and the line-height opened from a cramped 1.2 to 1.45.
       The width is what makes 15px possible at all: at 361px the text wrapped
       to 5 lines (90px+, too tall), but at 377px it fits 4 lines — one extra
       word per line is the whole difference. So going WIDER is what allowed
       going BIGGER, rather than trading one against the other.
       The generous line-height is deliberate: cramped leading is a large part
       of what made this block read as weak/dense. With the scrim providing
       even contrast underneath, the text can afford the air. */
    /* 14.5px is the exact ceiling for the medium weight: measured with forced
       reflow, 15px/500 re-wraps to 5 lines (105px) while 14.5px/500 holds 4
       (81px). 15px only fits at weight 400 — so this is a straight choice
       between half a point of size and the heavier weight, and the weight
       wins, because thin-and-washed-out was the actual complaint, not
       small-and-unreadable. */
    font-size: 14.5px !important;
    /* 1.4, not 1.45. Line-height does not affect WRAPPING, but it does set the
       block's height, and every pixel of height here pushes the top edge up
       toward the chin. */
    line-height: 1.4 !important;
    width: 377px !important;
    max-width: none !important;
    /* -20px puts the left edge at 8px and the right at 385px — an even
       inset either side of a 393px screen. */
    margin-left: -20px !important;
    /* 400 -> 500 and pure white. On a photographic background a regular weight
       in off-white (rgb 245) is what reads as "thin and washed out"; a medium
       weight at full white holds its own against the image without tipping
       into shouty bold. */
    font-weight: 500 !important;
    color: #fff !important;
    /* Sits the block's top on the marked line (~630px), below the chin.
       127 -> 95 for the same reason as the eyebrow above: the eyebrow going
       to a single line shortened the bottom-pinned column, moving this
       paragraph's natural start down 32px and pushing it into the button. */
    top: 95px;
    /* Per-glyph text-shadow removed — replaced by the radial panel below,
       which shades the box as a whole rather than outlining each letter. */
    text-shadow: none;
  }
  /* Soft radial gradient behind the description's TEXT BOX, not per-character
     shadow — matches the treatment already used on the contact hero's
     subtitle. On a ::before rather than the paragraph's own background so it
     can bleed past the text: the negative inset pushes the gradient's edges
     outside the column, so the falloff finishes in open space instead of
     being clipped at the text boundary — clipping there is what would make it
     read as a rectangular panel instead of a glow. z-index:0 on the paragraph
     plus -1 on the pseudo-element keeps the glow behind the glyphs but still
     above the hero photo (a plain z-index:-1 with no stacking context on the
     parent would otherwise escape behind the photo entirely and vanish). */
  /* Description moved OUT of the hero entirely, on request — it now lives as
     a card at the top of the bio section below (see about.html and the
     .about-mobile-desc-card rules near .mission). The ~90 lines of sizing/
     position tuning above this point are dead weight now that the element is
     hidden, but are left in place rather than deleted: they're the tuned
     values for how this text fits ON the photo, and undoing this decision
     later (if the card treatment doesn't work out) means restoring exactly
     this, not re-deriving it from scratch. The hero's only remaining text/
     interactive item is the button, matching "the only other item on the
     hero page is the red button". */
  .hero--daniel-bg .hero__desc{
    display: none;
    position: relative;
    z-index: 0;
  }
  /* Radial patch retired — the bottom scrim on .hero--daniel-bg::after now
     does the contrast work across the whole lower frame, evenly, with no
     box to clip against. Keeping both would double-darken this strip. */
  .hero--daniel-bg .hero__desc::before{
    content: none;
  }
  .hero--daniel-bg .hero__desc::before--retired{
    position: absolute;
    inset: -34px -30px;
    z-index: -1;
    pointer-events: none;
    /* `closest-side` is the fix for the visible hard edges. A radial-gradient
       defaults to farthest-CORNER sizing, which on this wide, short box put
       the transparent stop at ~239px horizontally while the box's half-width
       is only ~216px — so the gradient was still partly opaque where the box
       ended and got sliced flat, showing as straight vertical edges.
       closest-side pins the gradient's 100% to the NEAREST edge, so it is
       guaranteed to reach fully transparent before any boundary; past that
       radius (the corners) it is already transparent. Nothing can clip.
       The final stop is therefore at 100%, not 78% — with closest-side those
       are different things: 78% would end the fade short of the edge and
       reintroduce the same clipping. */
    background: radial-gradient(ellipse closest-side at 50% 50%,
      rgba(0,0,0,0.72) 0%,
      rgba(0,0,0,0.58) 34%,
      rgba(0,0,0,0.34) 58%,
      rgba(0,0,0,0.12) 80%,
      rgba(0,0,0,0) 100%);
  }
  /* Button shrunk to about half its footprint while its own text stays 18px,
     per instruction. Padding is the only lever available for that: block
     12px->6px and inline 32px->16px roughly halves both the button's height
     (53px -> ~42px) and width (256px -> ~200px) without touching font-size,
     which is what actually reveals more of the hero photo behind/below it. */
  /* The base .btn class carries min-height:48px (a deliberate tap-target
     floor used everywhere else on the site) — that's what capped the first
     pass at 48px regardless of how far padding was cut. Overriding it here is
     what actually gets this specific button down near half its original 53px;
     every other button on the site keeps the 48px floor untouched. */
  /* Getting to roughly half height needed a second constraint beyond padding
     and min-height: .btn inherits the body's line-height:1.6, so even at
     padding-block:4px the 18px text was sitting in a 28.8px line box (4+4+28.8
     = ~37px, not far off the pre-edit 48px). Tightening line-height to 1.15
     (~20.7px) is what actually lets the box follow the padding down toward
     half, without touching font-size. */
  .hero--daniel-bg .btn{
    min-height: 0 !important;
    line-height: 1.15 !important;
    padding-block: 4px !important;
    padding-inline: 18px !important;
  }
  /* .hero__text-column carries 32px of its own bottom padding (it doubles as a
     scrim panel on desktop). Left in, that padding stacks under the button and
     the 52px above can never land the button where intended. */
  .hero--daniel-bg .hero__text-column{ padding-bottom: 0 !important; }
  /* "MEET DANIEL" is baked into the mobile hero artwork itself (large yellow
     lettering across the top of the photo), so the <h1> below it was reading
     as a literal duplicate of what the image already says. Visually hidden
     rather than display:none — the page still has exactly one real "Meet
     Daniel" heading for screen readers and SEO, it's just not drawn twice for
     sighted phone visitors. Desktop's photo carries no such lettering, so its
     heading stays fully visible. */
  .hero--daniel-bg .hero__title{
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip: rect(0 0 0 0); white-space: nowrap; border: 0; padding: 0; margin: -1px;
  }
}

/* The phone-only background layer is inert until the media query below turns
   it on, so desktop renders exactly one image as it always has. */
.hero__bg-fade--mobile{ display: none; }

/* Contact hero, mobile: full-screen, using a portrait-specific image file
   rather than a re-crop of the desktop one.
   The desktop photo is landscape: the phone sits at its far left (~0-19%) and
   the microphone much further right (~38-52%), so the narrow window a tall
   phone box leaves could never show both — every crop position traded one
   away. The portrait file has them side by side and needs no crop at all.
   Height must be !important: the section's height comes from an INLINE
   min-height:max(360px,45vw) that desktop still needs, so this can only win
   from inside the query, not by editing the inline value. */
@media (max-width: 767.98px){
  /* svh, matching the other heroes: 100vh is the address-bar-retracted height
     on mobile, so it overshoots the area actually visible at load. */
  .hero--contact-bg{ min-height: 100svh !important; }
  /* Drops the whole text group — eyebrow, title, description and the black
     radial scrim behind them, which rides on the column's ::before and so
     travels with it — to the lower third, clearing the flag, face, phone and
     microphone in the upper two thirds.
     The section is display:flex/align-items:center INLINE, so flex-end needs
     !important to win. The column's own ~64px of bottom padding has to go too:
     left in, it sits under the text and stops it reaching the intended line
     without overflowing the hero. 51px then lands the description's baseline
     just inside the photo's bottom edge (measured at 816px). */
  /* Text group moved from the foot of the hero up to the top band, above
     Daniel's head — the sky/building area, which is the emptiest part of the
     portrait and leaves his face, the phone and the mic completely clear.
     82px of top padding replaces the base 117px (sized for desktop's header
     clearance). 47px was tried first and put the eyebrow at y=48 — underneath
     the 53px header, since this hero runs full-bleed behind it. 82px clears
     the header and lands the block inside the marked band. */
  .hero--contact-bg{
    /* flex-END, not flex-start. This is the guarantee that "LET'S TALK" and
       its subtitle can never be cropped on any device: anchored to the
       hero's foot, the text block reserves its own height first and grows
       UPWARD into the empty sky, so a taller block (a longer translation, a
       narrower screen forcing more line wraps) pushes into space that is
       always available. Top-anchoring did the opposite — it fixed a start
       position and let the text run off the bottom edge, which is exactly the
       failure found on the real device. */
    align-items: flex-end !important;
    /* 82px -> 573px -> 541px: drops the whole text group from the sky band
       down onto the rider's chest, then back up 32px to close the gap under
       the phone and the microphone, so the headline starts where those two
       objects end. Padding on the hero moves eyebrow, title and description
       together, keeping their existing spacing intact rather than
       repositioning each one. */
    /* 541px -> calc(100svh - 250px). A FIXED padding was the real-device bug:
       541px was measured against the 851px-tall preview, but a real phone's
       usable height with the address bar showing is more like 640px — at
       which point 541px of padding plus the ~189px text block forced the hero
       to 797px, i.e. 157px taller than the screen, pushing the description
       clean off the bottom.
       Expressing it as "100svh minus a fixed block" instead pins the text a
       constant 250px above the viewport's bottom edge at EVERY height, so the
       same framing holds on a 640px phone and an 851px one without the hero
       ever exceeding the screen. svh (not vh) so it's measured against the
       address-bar-visible height, which is what the page actually loads
       into. */
       SUPERSEDED by the flex-end anchoring above: with the block held at the
       foot, no top padding is needed to position it, and any value here would
       just re-impose a fixed assumption about the text's height. Reduced to
       clearing the fixed header only.
       The image needs no matching change — the layer is background-size:cover
       on a 100svh box, so it already scales to fill whatever height the device
       has, extending downward on taller screens by itself. */
    padding-top: calc(var(--header-h) + 1rem) !important;
    /* Sets how far the text sits above the screen's bottom edge. Because the
       block grows upward from here, this is the ONLY number that needs to be
       right, and it can't push anything off-screen. */
    padding-bottom: 64px !important;
  }
  /* Title sized to the marked rectangle. "LET'S TALK" measures 185px wide at
     the inherited 46.1px, and the rectangle is ~252px, so 62px is the size
     that fills it (185 x 62/46.12 = 249px). Centred rather than left-aligned
     because the rectangle sits centred on the frame, and because the rider
     behind it is centred too — a left-flush headline reads as misaligned
     against him. */
  /* Brought back onto the site's standard hero scale. The 62px here was sized
     to fill a marked rectangle, which made this the only hero running its own
     type size — measured against training's hero the gap was 62px vs 48.68px,
     and its shadow was a separate 40/22 set rather than the shared 54/30/20.
     Dropping the size override lets the shared .hero__title clamp apply, so
     this hero now scales with the others at every width instead of being
     pinned to one number. Font family, weight, colour and uppercase were
     already identical (Barlow Condensed 700). */
  .hero--contact-bg .hero__title{
    line-height: 1.05;
    text-align: center;
    /* 24px -> 6px. The gap was set against the old 46px headline; at 62px the
       type carries far more visual weight, so the same measured gap reads as a
       much larger hole between the two. Tightening it groups the description
       with the headline as one block. */
    margin-bottom: 6px !important;
    /* Lifted 61px to the marked line, on its own. position:relative is the
       key: it is a painted-only offset, so the title's BOX stays in flow and
       the description below never notices it moved. A margin change here
       would have dragged the description up by the same amount, which was
       explicitly not wanted. */
    position: relative;
    top: -61px;
    /* Matched to the shared hero-title shadow (54/30/20) used on training and
       home, replacing this hero's one-off 40/22/14 set. */
    text-shadow: 0 0 54px rgba(0,0,0,1), 0 4px 30px rgba(0,0,0,0.9), 0 0 20px rgba(0,0,0,0.85);
    /* Needed so the ::before glow below is trapped behind this text but still
       above the hero photo — same reasoning as the description's stacking
       context further down. */
    position: relative;
    z-index: 0;
  }
  /* Radial glow behind the headline, extending the description's glow
     upward so the two rows read as one shaded band.
     Horizontal bleed is ZERO, by measurement rather than a guess: the
     headline's own box already spans 28-365 of a 393px screen, i.e. to
     within 28px of each edge — closer than the phone (right edge ~80px)
     and microphone (left edge ~194px) sit at this height. Any positive
     sideways bleed here overlaps one or the other; a POSITIVE inset (growing
     inward) keeps the glow narrower than the text itself, so it cannot reach
     either object no matter how the text reflows.
     Vertical bleed stays generous (-30 up, -34 down) since that direction is
     clear — jacket/collar above, open gap toward the description below. */
  .hero--contact-bg .hero__title::before{
    content: "";
    position: absolute;
    inset: -30px 12px -34px 12px;
    z-index: -1;
    pointer-events: none;
    background: radial-gradient(ellipse closest-side at 50% 50%,
      rgba(0,0,0,0.40) 0%,
      rgba(0,0,0,0.30) 40%,
      rgba(0,0,0,0.16) 65%,
      rgba(0,0,0,0) 100%);
  }
  /* Description balanced under it: centred to share the title's axis, and
     capped at 20ch. That width was measured, not guessed — stepping the cap
     from 16ch to 30ch, 19-21ch is the only band that breaks the sentence into
     even lines (203/214/190/216px, a 26px spread); 24ch strands a 33px "me."
     orphan and 27ch+ runs the first line to 317px, wider than the 252px
     headline above it. At 20ch every line stays narrower than the headline,
     so the block tapers instead of bulging.
     Blur radii are ~60% of the title's, scaled to its smaller size so the glow
     tracks the letters rather than spreading into a smudge. */
  .hero--contact-bg .hero__desc{
    text-align: center;
    /* 21px, matching the training hero's description exactly — this was
       19.03px off the generic .hero__desc scale, the only hero not on the
       shared size. Family/weight/colour already matched (Inter 400 white). */
    font-size: 21px !important;
    max-width: 20ch !important;
    margin-inline: auto;
    /* Pulled up 16px to tighten the gap under the headline. position:relative
       again, not a margin change — a negative margin here would shrink the
       box itself and could drag the radial glow's centring with it, whereas
       this only shifts where the already-sized block paints. */
    position: relative;
    top: -16px;
    /* No text-shadow and no outline on this line by explicit request — the
       contrast comes entirely from the radial panel behind the block (below).
       A per-glyph shadow traces each letter; a single soft ellipse behind the
       whole paragraph darkens the photo once, evenly, which is the look
       asked for. */
    text-shadow: none;
    /* z-index:0 (not auto) is the part that matters: it makes this element a
       stacking context, which keeps the ::before glow below trapped behind the
       text but still in front of the hero photo. With z-index:auto a
       negatively-stacked child escapes the parent entirely and paints behind
       the background image, where it is invisible. */
    position: relative;
    z-index: 0;
  }
  /* Radial glow behind the description only — the headline keeps its text
     shadow alone. On its own pseudo-element rather than the element's own
     background so it can bleed past the text box: `inset` with negative
     values pushes the gradient's edges outside the 20ch column, so the falloff
     completes in open space instead of being clipped at the text's boundary,
     which is what makes it read as a glow rather than a panel. Elliptical to
     match the block's shape (wider than tall), and it fades fully transparent
     by 72% so there is no visible rim against the photo. */
  /* Soft round radial shading behind the whole subtitle block — now the ONLY
     treatment on this text, replacing the per-glyph shadow.
     On a pseudo-element rather than the paragraph's own background so it can
     bleed well past the text: the negative `inset` pushes the gradient's
     edges outside the 20ch column, letting the falloff finish in open space
     instead of being clipped at the text boundary — that clipped edge is what
     would read as a rectangular panel rather than a glow.
     Elliptical to match the block's proportions (wider than tall), centred,
     and fully transparent by 70% so no rim shows against the photo. */
  .hero--contact-bg .hero__desc::before{
    content: "";
    position: absolute;
    /* -42/-52 -> -84/-104 -> -110/-136: another ~30% larger. closest-side
       sizes its radius off this box, so growing the negative inset grows the
       glow's reach past the text by the same amount. */
    inset: -110px -136px;
    z-index: -1;
    pointer-events: none;
    /* closest-side for the same reason as the about hero's: the default
       farthest-corner sizing let the gradient still be opaque at the box's
       edges, which clips to visible straight lines. Ending at 100% with
       closest-side guarantees the fade completes inside the box. */
    /* Opacity history: 0.82/0.66/0.38/0.14 (original) -> halved to 0.42/0.34/
       0.20/0.08 (bigger+fainter) -> +0.10 each to 0.52/0.44/0.30/0.18
       (confirmed depth) -> now +0.15 more, darker again while the inset above
       grows the radius further — both "larger" and "less see-through" move
       together this round rather than trading off. */
    background: radial-gradient(ellipse closest-side at 50% 50%,
      rgba(0,0,0,0.67) 0%,
      rgba(0,0,0,0.58) 35%,
      rgba(0,0,0,0.42) 58%,
      rgba(0,0,0,0.28) 80%,
      rgba(0,0,0,0) 100%);
  }
  /* The eyebrow travels with the group, so it needs the same treatment to stay
     legible now that it sits over the jacket instead of open sky. */
  .hero--contact-bg .hero__eyebrow{
    /* display:FLEX, not the inherited inline-flex. That is what centres it:
       as an inline-flex box it shrink-wraps its own content, so
       justify-content had nothing spare to distribute and the whole box just
       sat at the column's left edge (measured x=28 in a 24-369 column). Going
       block-level makes it span the column, and justify-content:center then
       centres the icon and label inside that full width. */
    display: flex;
    justify-content: center;
    text-shadow: 0 0 16px rgba(0,0,0,1), 0 1px 6px rgba(0,0,0,0.9);
    /* Lifted 484px — clear of the phone and the rider, up into the sky band
       just under the header (lands ~90px down, 37px below the header's 53px
       edge). Offset via position:relative rather than a negative margin on
       purpose: relative offsets are painted-only and leave the element's
       original box in flow, so the title and description stay exactly where
       they were placed. A negative margin would drag the whole group up. */
    /* Absolute against the HERO, replacing the old relative -472px lift.
       That lift was measured back from the text group's position, so it only
       held while the group sat at a fixed 541px — with the padding now
       viewport-derived (see above), a fixed offset would drift by whatever
       the screen height differs from 851px and, on a 640px phone, would have
       put the tag behind the header.
       Anchoring to the hero itself makes the position absolute rather than
       relative to anything that moves, so it lands the same distance below
       the header on every device. Same pattern already proven on the about
       hero — and as there, BOTH .container and .hero__text-column have to go
       static, since either one being positioned would capture this instead
       of the hero. */
    position: absolute;
    top: calc(var(--header-h) + 10px);
    left: 0;
    right: 0;
    margin: 0;
    z-index: 10;
  }
  .hero--contact-bg > .container,
  .hero--contact-bg .hero__text-column{ position: static !important; }
  .hero--contact-bg .hero__text-column{
    padding-bottom: 0 !important;
    padding-top: 0 !important;
  }
  /* Drops the dark radial scrim that sat behind this text. It existed to keep
     the wording legible over a busy centre-frame; up in the sky band there's
     nothing competing, so the panel just reads as a grey smear. */
  .hero--contact-bg .hero__text-column::before{ display: none; }
  .hero--contact-bg .hero__bg-fade--desktop{ display: none; }
  /* `cover`, reversing the earlier `contain` decision on request: fill the
     screen from the first frame, and let the zoom crop into the phone and
     microphone as it runs — that crop is now wanted, not avoided.
     The file is 768x1376 (0.558) against a 393x851 viewport (0.462), so cover
     scales to the box HEIGHT and the rendered width goes to ~475px, trimming
     ~82px (17.3%) off the sides at rest. That is the cost of filling a screen
     taller than the artwork's ratio; there is no framing that both fills and
     keeps the full width.
     Ken Burns must also START at 1 — the shared animation opens at a scale
     above 1, which would mean the "loads filled, THEN zooms" sequence never
     shows its own starting frame. Its own keyframes below run 1 -> 1.14. */
  .hero--contact-bg .hero__bg-fade--mobile{
    display: block;
    background-size: cover !important;
    background-position: center !important;
    background-repeat: no-repeat !important;
    background-color: var(--black);
    scale: 1 !important;
    animation:
      hero-bg-reveal 3.3s cubic-bezier(0.65, 0, 0.35, 1) 0s forwards,
      hero-ken-burns-contact-mobile 26s ease-in-out 0s infinite alternate !important;
  }
}
@keyframes hero-ken-burns-daniel{
  from{ scale: 1; }
  to{ scale: 1.06; }
}
/* Contact hero, phones. Departs from exactly 1 so the first painted frame is
   the image filling the screen with no zoom applied, then grows into the
   phone and microphone as requested. */
@keyframes hero-ken-burns-contact-mobile{
  from{ scale: 1; }
  to{ scale: 1.14; }
}
/* About/Daniel hero, phones — same contract: open on the filled image at
   scale 1, then zoom in. */
@keyframes hero-ken-burns-daniel-mobile{
  /* 1/1.12 -> 1.09/1.21, shifted up by the same +0.09 as the layer's new
     resting scale, so the animation still adds 0.12 of zoom across its cycle
     on top of the new floor instead of the range being swallowed by it. */
  from{ scale: 1.09; }
  to{ scale: 1.21; }
}

/* Home hero video uses the same centre-out reveal, but on the <video> wrapper
   rather than a background image, and after a shorter 1s wait. The mask clips
   the video's rendered frames, so it blooms in exactly like the image heroes. */
.hero__bg-video--reveal{
  -webkit-mask-image: radial-gradient(circle at 50% 50%, #000 0%, #000 40%, rgba(0,0,0,0) 72%);
  mask-image: radial-gradient(circle at 50% 50%, #000 0%, #000 40%, rgba(0,0,0,0) 72%);
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
  -webkit-mask-size: 0% 0%;
  mask-size: 0% 0%;
  opacity: 0;
  /* static zoom margin so the scroll parallax has room; the video supplies its
     own motion, so no Ken-Burns zoom here. */
  scale: 1.08;
  filter: blur(14px) saturate(0.2) brightness(0.7);
  will-change: mask-size, opacity, filter, translate;
  backface-visibility: hidden;
  animation: hero-bg-reveal 3.3s cubic-bezier(0.65, 0, 0.35, 1) 0s forwards;
}
/* The global reduced-motion block collapses every animation to ~0s, which would
   skip the hero's one-shot entrance. Restore its real timing here. Only the
   reveal runs on this element now, so a single value per property. */
@media (prefers-reduced-motion: reduce){
  html:not(.force-motion) .hero__bg-video--reveal{
    animation-duration: 3.3s !important;
    animation-delay: 0s !important;
    animation-iteration-count: 1 !important;
    animation-timing-function: cubic-bezier(0.65, 0, 0.35, 1) !important;
  }
}

/* Home hero video, desktop: same "show the complete frame first" fix as the
   shop hero, adapted for a real <video> instead of a CSS Ken-Burns image.
   hero-bg.mp4 is exactly 1280x720 (16:9) - matching the section to that ratio
   removes object-fit:cover's crop entirely, same principle as the shop hero's
   aspect-ratio match. min-height:600px keeps the site's own established floor
   (reused from the base clamp() below) so text/CTAs never get cramped at the
   narrow end of desktop.
   scale:1 means truly zero crop - but that also leaves zero margin for the
   scroll-parallax script (main.js, translate up to 18px), which would expose
   a black edge the instant someone scrolls, since unlike the shop hero's
   image this video has no animated Ken-Burns scale to grow its own overscan
   margin over time. Rather than fake a margin back in with a >1 scale (which
   would just reintroduce a partial crop), the !important below overrides the
   translate the script writes inline, disabling that parallax nudge for just
   this element - keeping the frame permanently complete instead. */
@media (min-width: 768px){
  .hero--with-bg{
    aspect-ratio: 16 / 9;
    /* min-height MUST be actively zeroed, not merely left unset. Any height
       floor on an aspect-ratio box is back-solved into a WIDTH, and the base
       .hero rule sets min-height:100vh — which here resolved to a hero far wider
       than the viewport, clipping the video badly. Zeroing it lets width come
       from the container and height follow the 16:9 ratio, so the hero can never
       outgrow its parent. The 600px floor returns below, at a width where the
       ratio already satisfies it and it therefore cannot bind. */
    min-height: 0;
  }
  .hero--with-bg .hero__bg-video--reveal{
    /* scale:1 — the complete, uncropped frame, and deliberately NO Ken-Burns
       zoom. A continuously animating scale on a box wrapping a <video> forces
       the compositor to resample every decoded frame, which surfaces as stutter
       in the playback itself rather than as a smooth drift. The footage already
       carries its own motion, so the frame is left still and the decoded frames
       get an untouched path to the screen. */
    scale: 1;
    translate: 0 !important;
    animation: hero-bg-reveal 3.3s cubic-bezier(0.65, 0, 0.35, 1) 0s forwards;
  }
}
/* The 600px floor the hero was originally given, reinstated only from the width
   where 16:9 already yields at least that much height (1100 * 9/16 = 619px). Above
   this point the ratio alone satisfies the floor, so min-height never binds and
   can never back-solve into an over-wide element — it exists purely as a guard
   for any future change that would otherwise let the hero collapse. */
@media (min-width: 1100px){
  .hero--with-bg{ min-height: 600px; }
}

/* Once the entrance has played, shed the work it needed. animation-fill-mode
   :forwards would otherwise keep the radial mask and the (now identity) filter
   applied to every decoded frame for the life of the page — real per-frame GPU
   cost on a full-bleed looping video, for zero visual difference, since the
   mask has long since bloomed past the element's edges. main.js adds
   .is-revealed on animationend.
   Clearing `animation` is what makes this stick: a forwards fill outranks
   normal declarations, so the settled state is restated here rather than fought
   with !important. This must also sit AFTER the desktop rule above — both
   selectors carry the same specificity, so source order is what decides which
   `animation` value wins. */
.hero__bg-video--reveal.is-revealed{
  animation: none;
  opacity: 1;
  -webkit-mask-image: none;
  mask-image: none;
  /* Deliberately the identity filter rather than `none`. These values are a
     visual no-op, but the presence of a filter is NOT: it forces the video
     through an intermediate render surface, where the compositor colour-manages
     it into sRGB. Drop the filter and the frames can instead take the direct
     hardware-overlay path, which hands colour conversion to the display and
     visibly shifts the grade — the footage came back darker, losing the green
     in the rider's eyes. Keeping the filter pins the exact pipeline the hero
     was authored and graded against. */
  filter: blur(0) saturate(1) brightness(1);
  will-change: auto;
}

/* ---------- Hero text choreography ----------
   After the background begins revealing, the hero's text elements rise and
   fade in one after another so the hero feels directed rather than appearing
   all at once. `both` fill keeps them hidden until their turn. */
.hero .breadcrumb,
.hero__eyebrow,
.hero__title,
.hero__title--xlarge,
.hero__desc,
.hero__subtitle--xlarge,
.hero__actions{
  animation: hero-text-in 0.7s cubic-bezier(0.22, 1, 0.36, 1) both;
}
.hero .breadcrumb{ animation-delay: 0s; }
.hero__eyebrow{ animation-delay: 0.15s; }
.hero__title,
.hero__title--xlarge{ animation-delay: 0.3s; }
.hero__desc,
.hero__subtitle--xlarge{ animation-delay: 0.45s; }
.hero__actions{ animation-delay: 0.6s; }
@keyframes hero-text-in{
  from{ opacity: 0; transform: translateY(20px); }
  to{ opacity: 1; transform: translateY(0); }
}
/* Keep the choreography playing under reduced motion too, matching the hero
   reveal it accompanies. */
@media (prefers-reduced-motion: reduce){
  html:not(.force-motion) .hero .breadcrumb,
  html:not(.force-motion) .hero__eyebrow,
  html:not(.force-motion) .hero__title,
  html:not(.force-motion) .hero__title--xlarge,
  html:not(.force-motion) .hero__desc,
  html:not(.force-motion) .hero__subtitle--xlarge,
  html:not(.force-motion) .hero__actions{
    animation-duration: 0.7s !important;
    animation-iteration-count: 1 !important;
    animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1) !important;
  }
}

