/* ============================================================================
   96K AUTO CARE — hero.css
   Loader · overlay header · the scene walkthrough · exit into the content.

   The car is the hero. Everything in this file exists to present the footage
   and then get out of its way:

     · one media layer (the walkthrough), one grade layer, one text layer
     · no CSS light rig, no perspective planes, no sweeps, no grain
     · typography sized to sit beside the vehicle, never over it
     · a progress readout of two numbers and a hairline

   Framing and type scale are tunable from the custom properties on .hero;
   scenes and timing live in assets/js/config.js.
   ========================================================================= */

/* ================================================================ LOADER == */
/* Small logo, thin line, gone. It exists to cover media readiness, not to be
   an animation sequence in its own right. */
.loader {
  position: fixed;
  inset: 0;
  z-index: 500;
  display: grid;
  place-items: center;
  background: var(--color-primary-dark);
  will-change: opacity;
}

.loader__inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(1.4rem, 2.6vw, 2rem);
  padding-inline: var(--gutter);
}

.loader__logo {
  width: clamp(128px, 15vw, 190px);
  height: auto;
  opacity: 0;
  animation: loaderIn 0.9s var(--ease-out) 0.1s forwards;
}
@keyframes loaderIn {
  from { opacity: 0; transform: translate3d(0, 10px, 0); }
  to   { opacity: 1; transform: none; }
}

.loader__rail {
  position: relative;
  width: clamp(140px, 18vw, 240px);
  height: 1px;
  background: rgba(243, 241, 237, 0.14);
  overflow: hidden;
}
.loader__fill {
  position: absolute;
  inset: 0;
  background: var(--color-secondary);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.45s var(--ease-out);
}

.loader.is-done { pointer-events: none; }

/* ================================================================ HEADER == */
/* Restrained. A logo, five words, one text link. No pill, no glass, no
   oversized button, no shadow. */
.header {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 300;
  height: var(--header-h);
  color: var(--color-secondary);
  background: transparent;
  border-bottom: 1px solid transparent;
  transition: background-color 0.7s var(--ease-inout), color 0.7s var(--ease-inout),
    border-color 0.7s var(--ease-inout), box-shadow 0.7s var(--ease-inout),
    transform 0.55s var(--ease-out);
  will-change: transform;
}

/* Retired. The header no longer overlays the walkthrough — it occupies its
   own band above it — so there is no footage behind it to lift contrast
   against, and the dark state carries a solid ground instead. Zeroed rather
   than deleted so the light state has nothing to cross-fade against. */
.header::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  opacity: 0;
  pointer-events: none;
}

/* ---- the walkthrough state ----------------------------------------------
   A solid dark masthead sitting directly above the walkthrough, edge to edge
   with it. It used to be transparent, which was right while it floated over
   the footage; now that the video starts below it, transparent would show the
   page ground through — which is what put a white bar over the shutter frame.

   Applied by data-theme, and data-theme is set purely from whether the hero is
   still on screen (hero.js, syncHeader). Scroll direction never touches it. */
.header[data-theme="dark"] {
  background: var(--color-primary-dark);
  color: var(--color-secondary);
  border-bottom-color: transparent;
  box-shadow: none;
}

.header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  height: 100%;
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.header__brand {
  position: relative;
  display: block;
  flex: none;
  /* The upper THE 96K AUTO CARE lockup is a wide, short shape rather than
     the near-square sticker that used to sit here, so the box it is given
     follows the artwork: a little wider, and the ratio is the file's own.
     Sized so the cap height of THE 96K matches what the sticker read at. */
  width: clamp(124px, 11.5vw, 168px);
  aspect-ratio: 720 / 227;
}
.header__logo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  transition: opacity 0.7s var(--ease-inout);
}
.header__logo--light { opacity: 0; }

/* -------------------------------------------------------------- nav ------ */
.nav { display: none; }

@media (min-width: 1024px) {
  .nav { display: block; }
  .nav__list {
    display: flex;
    align-items: center;
    gap: clamp(1.6rem, 2.4vw, 2.6rem);
  }
}

.nav__link {
  position: relative;
  display: block;
  padding: 0.5em 0;
  font-family: var(--font-body);
  font-size: 0.83rem;
  font-weight: 500;
  letter-spacing: 0.02em;
  color: currentColor;
  opacity: 0.7;
  transition: opacity 0.45s var(--ease-out);
}
.nav__link::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0.15em;
  width: 100%;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: right;
  transition: transform 0.5s var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .nav__link:hover { opacity: 1; }
  .nav__link:hover::after { transform: scaleX(1); transform-origin: left; }
}
.nav__link.is-active { opacity: 1; }
.nav__link.is-active::after { transform: scaleX(1); opacity: 0.45; }

.header__actions { display: flex; align-items: center; gap: var(--s3); }

/* The header CTA is a text link with a hairline, not a button. */
.header__cta {
  display: none;
  position: relative;
  padding: 0.5em 0;
  font-family: var(--font-body);
  font-size: 0.83rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: currentColor;
}
.header__cta::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0.15em;
  width: 100%;
  height: 1px;
  background: currentColor;
  transform-origin: left;
  transition: transform 0.5s var(--ease-out);
}
@media (min-width: 720px) { .header__cta { display: block; } }
@media (hover: hover) and (pointer: fine) {
  .header__cta:hover::after { transform: scaleX(0); transform-origin: right; }
}

/* ---- light state (only once the walkthrough has been cleared) ---------- */
.header[data-theme="light"] {
  color: var(--color-heading);
  background: rgba(255, 255, 255, 0.94);
  border-bottom-color: var(--color-border);
}

/* Inner pages keep the black masthead, so the separation problem is the
   opposite of the homepage's: the edge is already absolute, and what it needs
   is softening rather than defining. A wide, low drop under the bar reads as
   the masthead sitting above the page instead of stamped onto it.

   INNER PAGES ONLY. The header is shared, so an unscoped rule would also
   change the homepage the moment the walkthrough scrolls past, and that page
   is approved as it stands. */
/* Matched to the dark state explicitly. .header[data-theme="dark"] sets
   box-shadow:none at the same specificity, so an unqualified .is-page .header
   would only win on source order — true today, silently broken by any
   reshuffle of this file. */
.is-page .header,
.is-page .header[data-theme="dark"] {
  box-shadow: 0 14px 30px -22px rgba(13, 13, 15, 0.28);
}
.header[data-theme="light"]::before { opacity: 0; }
.header[data-theme="light"] .header__logo--dark { opacity: 0; }
.header[data-theme="light"] .header__logo--light { opacity: 1; }

.header.is-hidden { transform: translate3d(0, -100%, 0); }

/* -------------------------------------------------------------- burger --- */
.burger {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  margin-right: -10px;
  color: currentColor;
}
@media (min-width: 1024px) { .burger { display: none; } }

.burger span {
  position: relative;
  display: block;
  width: 20px;
  height: 1px;
  background: currentColor;
  transition: background-color 0.2s linear;
}
.burger span::before,
.burger span::after {
  content: "";
  position: absolute;
  left: 0;
  width: 100%;
  height: 1px;
  background: currentColor;
  transition: transform 0.45s var(--ease-out);
}
.burger span::before { top: -6px; }
.burger span::after { top: 6px; }

.burger[aria-expanded="true"] span { background: transparent; }
.burger[aria-expanded="true"] span::before { transform: translateY(6px) rotate(45deg); }
.burger[aria-expanded="true"] span::after { transform: translateY(-6px) rotate(-45deg); }

/* ---------------------------------------------------------- mobile nav --- */
.mobile-nav {
  position: fixed;
  inset: 0;
  z-index: 290;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: var(--header-h) var(--gutter) var(--s5);
  background: var(--color-primary-dark);
  clip-path: inset(0 0 100% 0);
  transition: clip-path 0.72s var(--ease-inout);
  visibility: hidden;
}
.mobile-nav.is-open { clip-path: inset(0 0 0% 0); visibility: visible; }
@media (min-width: 1024px) { .mobile-nav { display: none; } }

.mobile-nav__list { display: flex; flex-direction: column; gap: 0.2em; }
/* 6.4vw capped at 2.2rem, not 8vw capped at 2.6rem. The old scale hit its
   41.6px ceiling at a 520px viewport and then held it all the way to the
   1024px breakpoint, so every tablet got the maximum size across a 500px
   range — "The 96K Standard" ran 53% of the column at 713px and over two
   thirds of it at 520. This tops out at 35.2px from 550px up.

   COLOUR CARRIES THE STATE HERE, NOT OPACITY. Opacity is the entrance
   channel — it animates 0 → 1 on open — so anything that also writes opacity
   fights the stagger. The desktop nav dims inactive links to 0.7 and brings
   the current page to 1; this does the same thing one channel across. */
.mobile-nav__list a {
  position: relative;
  /* flex + min-height rather than padding alone. Padding sized in em has to
     be tuned to whatever the type currently is, and it was: 0.18em landed on
     44px only while the type was 30px, and broke the moment the type moved.
     A 44px floor with the label centred in it holds the tap target at any
     size, and rows only grow past 44 when the type genuinely needs it. */
  display: flex;
  align-items: center;
  min-height: 44px;
  padding: 0.3em 0;
  font-family: var(--font-display);
  font-weight: 700;
  /* Down again, and it can afford to be: the overlay now carries the same
     four short labels the masthead does rather than six including "The 96K
     Standard", so nothing here is fighting the column width. 20px on a
     phone, topping out at 28px. */
  font-size: clamp(1.25rem, 4.8vw, 1.75rem);
  letter-spacing: -0.03em;
  line-height: 1.1;
  color: rgba(243, 241, 237, 0.55);
  opacity: 0;
  transform: translate3d(0, 30px, 0);
  transition: opacity 0.5s var(--ease-out), transform 0.6s var(--ease-out),
              color 0.45s var(--ease-out);
}

/* The current page. The markup has always emitted aria-current="page" on
   these links and nothing consumed it, so the overlay was the one place on
   the site that could not tell you where you were — while the masthead
   three inches above it could. Same two devices the desktop nav uses: full
   strength, and a hairline under the word. */
.mobile-nav__list a[aria-current="page"] {
  color: var(--color-secondary);
}
/* Absolute, like the desktop nav's underline, so it adds no height. In flow
   it made the current row 6px taller than the other five and the whole list
   shifted as you moved between pages. */
.mobile-nav__list a[aria-current="page"]::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0.06em;
  width: 100%;
  height: 1px;
  background: currentColor;
  opacity: 0.45;
}
.mobile-nav.is-open .mobile-nav__list a { opacity: 1; transform: none; }
/* The stagger. There are SIX nav items and this list stopped at five, so
   Contact had no delay at all and arrived first, ahead of Home. Carried to
   eight so adding a nav item cannot quietly reintroduce that. */
.mobile-nav.is-open .mobile-nav__list li:nth-child(1) a { transition-delay: 0.14s; }
.mobile-nav.is-open .mobile-nav__list li:nth-child(2) a { transition-delay: 0.2s; }
.mobile-nav.is-open .mobile-nav__list li:nth-child(3) a { transition-delay: 0.26s; }
.mobile-nav.is-open .mobile-nav__list li:nth-child(4) a { transition-delay: 0.32s; }
.mobile-nav.is-open .mobile-nav__list li:nth-child(5) a { transition-delay: 0.38s; }
.mobile-nav.is-open .mobile-nav__list li:nth-child(6) a { transition-delay: 0.44s; }
.mobile-nav.is-open .mobile-nav__list li:nth-child(7) a { transition-delay: 0.5s; }
.mobile-nav.is-open .mobile-nav__list li:nth-child(8) a { transition-delay: 0.56s; }

.mobile-nav__foot {
  margin-top: var(--s5);
  padding-top: var(--s3);
  border-top: 1px solid rgba(243, 241, 237, 0.14);
  color: rgba(243, 241, 237, 0.5);
}

/* ================================================================== HERO == */
.hero {
  /* ---- TUNABLES ---------------------------------------------------------
     --hero-video-*    framing for the film. Tune these rather than
                       re-cropping the file.
     --fs-hero-*       hero-only type scale, separate from the global scale
                       so the sections below are unaffected.                */
  /* Without JS, on touch devices and under prefers-reduced-motion the hero is
     exactly one viewport. Only `.is-live` — the desktop walkthrough — makes it
     a tall scrolled section, and `--hero-track` (set by hero.js from
     config.track.vh) is how tall. */
  --hero-video-position-x: 50%;
  --hero-video-position-y: 50%;
  --hero-video-scale: 1;

  --fs-hero-line: clamp(1.875rem, 3.1vw, 3.375rem);   /* 30 → 54px */
  --fs-hero-final: clamp(2.5rem, 4.2vw, 4.25rem);     /* 40 → 68px */
  --fs-hero-label: clamp(0.6875rem, 0.8vw, 0.8125rem);/* 11 → 13px */

  position: relative;
  height: 100svh;
  background: var(--color-primary-dark);
}

/* The desktop walkthrough is an ordinary tall section with a sticky stage
   inside it. Scroll distance IS the walkthrough, so the height is real and
   reserved: `--hero-track` viewports tall, of which the stage occupies one.

   This is deliberately CSS sticky rather than a GSAP pin. A pin injects a
   spacer element and re-measures on every refresh, which is what produced the
   blank viewport after the hero in an earlier build. With sticky there is
   nothing between the hero and the next section — the stage simply stops
   sticking when the section ends, and the next section is already there. */
.hero.is-live {
  height: calc(var(--hero-track, 700) * 1svh);
  z-index: 0;            /* under the header (300) and the sections below */
}

/* The stage begins BELOW the header rather than behind it, and takes all the
   remaining viewport height. The header is fixed at --header-h, so sticking
   the stage at that offset puts the two edge to edge with nothing overlapping
   and nothing left over.

   The geometry still lines up with the scrub trigger: the stage unsticks when
   its bottom (scrollY + header-h + (100svh - header-h) = scrollY + 100svh)
   reaches the section bottom — the same point as `end: "bottom bottom"`. */
.hero.is-live .hero__stage {
  position: sticky;
  top: var(--header-h);
  height: calc(100svh - var(--header-h));
}

/* ---- phones and tablets: a playing film, not a walkthrough -------------
   Touch devices get the portrait cut as an ordinary cinematic hero: it plays
   straight through at one viewport tall, the captions cross-fade on playback
   time, the page scrolls past it, and none of the walkthrough affordances
   apply. Everything here keys on `.is-film` rather than a width, because the
   dispatch that sets that class also weighs pointer type — a large tablet in
   landscape is wider than some laptops. */
.hero.is-film .hero__canvas,
.hero.is-film .hero__progress,
.hero.is-film .hero__hint { display: none; }

.hero.is-film .hero__video { display: block; opacity: 0; }
.hero.is-film.is-playing .hero__video { opacity: 1; }
.hero.is-film.is-playing .hero__poster { opacity: 0; }

/* The portrait cut carries the car as a band centred at 58% of the frame on
   black. Aiming the crop at the band rather than at the canvas is what keeps
   the vehicle whole on a tablet: the overflow is trimmed out of the padding
   above and below it, never off the car. */
.hero.is-film {
  --hero-video-position-y: 58%;
}

/* Every caption occupies the same block of studio black above the car and is
   cross-faded in place — they are stacked, not laid out in sequence. The
   zero-duration `visibility` step is delayed on the way out so the outgoing
   caption stays readable for the whole fade, and immediate on the way in. */
.hero.is-film .hs {
  grid-area: 2 / 1 / 4 / 13;
  align-self: start;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.5s var(--ease-out), visibility 0s linear 0.5s;
}
.hero.is-film .hs.is-on {
  opacity: 1;
  visibility: visible;
  transition: opacity 0.5s var(--ease-out), visibility 0s linear 0s;
}
/* Also class-keyed rather than width-keyed, for the same reason. */
.hero.is-film .hs__sub { max-width: 28ch; }

/* A phone held in landscape is roughly 2.2:1 — a letterbox far wider than any
   portrait cut can fill, and the only case that cannot be served whole. The
   58% aim, which is right everywhere else, puts the entire loss on the bottom
   of the band. Centring the band instead splits it evenly, so what goes is
   floor reflection at both edges rather than the wheels. */
@media (orientation: landscape) and (max-height: 520px) {
  .hero.is-film { --hero-video-position-y: 60%; }
}

.hero__stage {
  position: relative;
  height: 100%;
  overflow: hidden;
  background: var(--color-primary-dark);
  isolation: isolate;
}

/* Everything camera-side sits in .hero__world so one transform can push the
   whole scene back during the exit. */
/* Nothing transforms this any more — the exit animation that used to push the
   whole scene back is gone, replaced by the section simply scrolling away. It
   stays as the media wrapper; the `will-change` it used to carry was promoting
   a layer for an animation that no longer exists. */
.hero__world {
  position: absolute;
  inset: 0;
}

.hero__film { position: absolute; inset: 0; background: var(--color-primary-dark); }

/* The video and the canvas are alternatives, not layers — hero.js picks one
   renderer at runtime and only that element is ever in play. */
.hero__video,
.hero__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  transition: opacity 0.8s var(--ease-out);
}

/* The video is fitted by CSS. The canvas is NOT: hero.js sizes the backing
   store to the stage and computes the cover rect itself, once per layout.
   Leaving object-fit on it would apply a second, independently-rounded fit on
   top of the first — which is exactly the kind of per-frame scale difference
   that reads as an image sequence "shaking". */
.hero__video {
  object-fit: cover;
  object-position: var(--hero-video-position-x) var(--hero-video-position-y);
  transform: scale(var(--hero-video-scale));
}
.hero.is-video .hero__video,
.hero.is-canvas .hero__canvas { opacity: 1; }
.hero:not(.is-canvas) .hero__canvas { display: none; }
/* `is-video` is the desktop walkthrough's fallback renderer. `is-film` is the
   separate touch-device strategy and shows the same element, so it has to be
   excluded here — without it this rule out-ranks the film's own `display`
   (equal specificity, declared later) and the film plays behind a hidden
   element, leaving a black hero once the poster fades out. */
.hero:not(.is-video):not(.is-film) .hero__video { display: none; }

/* Poster — the master frame of the same car. Shown while the walkthrough
   loads, and left in place if it never arrives. A real vehicle, never an
   empty black screen. */
.hero__poster {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--hero-video-position-x) var(--hero-video-position-y);
  transform: scale(var(--hero-video-scale));
  transition: opacity 0.9s var(--ease-out);
}
.hero.is-video .hero__poster,
.hero.is-canvas .hero__poster { opacity: 0; }

/* ------------------------------------------------------------- grade ----- */
/* One layer. A vignette to hold the eye on the car and a foot gradient so
   typography stays legible against a bright frame. Nothing decorative. */
.hero__grade {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    radial-gradient(132% 92% at 50% 44%, transparent 42%, rgba(0, 0, 0, 0.34) 82%, rgba(0, 0, 0, 0.62) 100%),
    linear-gradient(to bottom, rgba(0, 0, 0, 0.42) 0%, transparent 22%, transparent 54%, rgba(0, 0, 0, 0.6) 100%);
}

/* ================================================ typographic overlay ==== */
/* A single stable grid, shared by every phase. Text is placed on it by
   anchor, art-directed to whichever side of the frame the car is not
   occupying — never scattered into corners for the sake of movement. */
.hero__ui {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
}

.hero__grid {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  grid-template-rows: repeat(6, 1fr);
  column-gap: clamp(1rem, 2.4vw, 2.6rem);
  width: 100%;
  max-width: var(--maxw);
  height: 100%;
  margin-inline: auto;
  /* No header allowance any more. The stage now begins below the header, so
     adding --header-h here counted it twice and pushed the copy down. */
  padding: clamp(1rem, 3vh, 2.5rem) var(--gutter) clamp(3rem, 8vh, 5rem);
}

/* Every phase is a block on that grid. Only the anchor changes. */
.hs {
  grid-area: 5 / 1 / 7 / 6;
  align-self: end;
  display: grid;
  gap: clamp(0.7rem, 1.5vh, 1.15rem);
  justify-items: start;
}

.hs[data-anchor="top-left"]     { grid-area: 2 / 1 / 4 / 6;  align-self: start; }
.hs[data-anchor="bottom-left"]  { grid-area: 5 / 1 / 7 / 6;  align-self: end; }
.hs[data-anchor="top-right"]    { grid-area: 2 / 8 / 4 / 13; align-self: start; justify-items: end; text-align: right; }
.hs[data-anchor="bottom-right"] { grid-area: 5 / 8 / 7 / 13; align-self: end; justify-items: end; text-align: right; }
.hs[data-anchor="mid-left"]     { grid-area: 3 / 1 / 5 / 6;  align-self: center; }

/* Centred, both axes. The opening copy sits in the middle of the frame over
   the closed shutter — there is no vehicle to work around at that point, and
   a centred masthead reads as a title card rather than a caption. It fades
   out before the camera reaches the car, so it never competes with the
   walkthrough itself. */
.hs[data-anchor="center"] {
  grid-area: 1 / 2 / 7 / 12;
  align-self: center;
  justify-items: center;
  text-align: center;
}
.hs[data-anchor="center"] .hs__eyebrow { justify-content: center; }

/* The opening panel introduces the business with the identity itself. It
   takes the place the eyebrow held, so it is sized against the same width
   the headline uses rather than being blown up — deliberately restrained,
   and centred with the rest of the scene. */
.hs__logo {
  width: clamp(148px, 16.5vw, 236px);
  height: auto;
  justify-self: center;
  margin-block-end: clamp(0.2rem, 0.8vh, 0.6rem);
}
.hs[data-anchor="center"] .hs__logo { justify-self: center; }
/* .hs[data-anchor="center"] .hs__line { max-width: 22ch; } */
.hs[data-anchor="center"] .hs__sub { max-width: 42ch; }

/* Wider block for the closing statement, which carries two lines and the
   call to action. Needs the attribute in the selector to outrank the anchor
   rules above it. */
.hs--final[data-anchor] { grid-area: 4 / 1 / 7 / 8; align-self: end; }

/* The closing card. Centred like the opening so the walkthrough is bookended
   by two title cards with clean footage between them, rather than drifting
   from one corner to another. The override has to be more specific than the
   rule above, which otherwise pins every .hs--final bottom-left. */
.hs--final[data-anchor="center"] {
  grid-area: 1 / 2 / 7 / 12;
  align-self: center;
  justify-items: center;
  text-align: center;
}
.hs--final[data-anchor="center"] .hs__eyebrow { justify-content: center; }
.hs--final[data-anchor="center"] .hs__cta { margin-top: clamp(0.9rem, 2vh, 1.5rem); }

@media (max-width: 900px) {
  .hs,
  .hs[data-anchor="top-left"],
  .hs[data-anchor="bottom-left"],
  .hs[data-anchor="top-right"],
  .hs[data-anchor="bottom-right"],
  .hs[data-anchor="mid-left"],
  .hs--final {
    grid-area: 4 / 1 / 7 / 13;
    align-self: end;
    justify-items: start;
    text-align: left;
  }
}

/* ---- the type itself ----------------------------------------------------- */
.hs__eyebrow {
  display: flex;
  align-items: center;
  gap: 0.85em;
  font-family: var(--font-mono);
  font-size: var(--fs-hero-label);
  font-weight: 500;
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: rgba(243, 241, 237, 0.62);
}
.hs__eyebrow s {
  width: clamp(20px, 2.6vw, 38px);
  height: 1px;
  /* Same accent as the progress rail, same source. See --hero-lume above. */
  background: rgba(243, 241, 237, calc(0.4 + 0.35 * var(--hero-lume, 0)));
  text-decoration: none;
  /* Scaled from 0 by applyBlock, so the hairline draws itself out from the
     side the eyebrow reads from. A flex item, so the transform applies even
     though <s> is inline by default. */
  transform-origin: left center;
}
.hs[data-anchor$="right"] .hs__eyebrow { flex-direction: row-reverse; }
.hs[data-anchor$="right"] .hs__eyebrow s { transform-origin: right center; }
.hs[data-anchor="center"] .hs__eyebrow s { transform-origin: center; }

/* The lines are animated individually — rise, scale settle and a clip mask,
   all in applyBlock. Deliberately NO will-change here: it would pin each line
   to a layer rasterised at the scaled-up size, and the type would stay
   slightly soft after the animation settled. These are three short lines; the
   browser repaints them for a few hundred milliseconds without complaint, and
   they end up crisp. */
.hero.is-live .hs > * { transform-origin: left top; }
.hero.is-live .hs[data-anchor="center"] > * { transform-origin: center top; }
.hero.is-live .hs[data-anchor$="right"] > * { transform-origin: right top; }

/* The reveal is a clip-path wipe. `padding-bottom` plus a settled inset that
   runs past the box guarantee descenders are never shaved — the previous
   build clipped the tail of "Protection. Care. Finish." */
.hs__line {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: 26px;
  line-height: 1.16;
  letter-spacing: -0.026em;
  color: var(--color-secondary);
 /*  max-width: 17ch; */
  padding-bottom: 0.16em;
  text-wrap: balance;
}
.hs__line em { font-style: normal; color: rgba(243, 241, 237, 0.6); }

.hs__sub {
  max-width: 34ch;
  font-size: clamp(0.9rem, 1.05vw, 1.05rem);
  line-height: 1.5;
  color: rgba(243, 241, 237, 0.66);
}

.hs--final .hs__line {
  font-weight: 700;
  font-size: var(--fs-hero-final);
  line-height: 1.08;
  letter-spacing: -0.032em;
  max-width: 18ch;
}

.hs__cta {
  margin-top: clamp(0.4rem, 1.2vh, 0.9rem);
  pointer-events: auto;
}

/* ========================================================= progress ====== */
/* Two numbers and a hairline. That is the whole readout. */
.hero__progress {
  position: absolute;
  right: var(--gutter);
  bottom: clamp(1.5rem, 4vh, 2.6rem);
  z-index: 4;
  display: flex;
  align-items: center;
  gap: clamp(0.7rem, 1.2vw, 1.1rem);
  font-family: var(--font-mono);
  font-size: clamp(0.6rem, 0.72vw, 0.7rem);
  letter-spacing: 0.14em;
  color: rgba(243, 241, 237, 0.5);
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}
.hero__progress b { font-weight: 400; color: var(--color-secondary); }
.hero__progress s {
  position: relative;
  display: block;
  width: clamp(56px, 8vw, 110px);
  height: 1px;
  /* The rail lifts fractionally while the car's own lights are coming up.
     --hero-lume is 0 for all but ~4 seconds of the film; hero.js raises it on
     a sine across the headlight and tail-bar windows and nowhere else, so this
     is a response to the footage rather than an effect running over it. */
  background: rgba(243, 241, 237, calc(0.22 + 0.26 * var(--hero-lume, 0)));
  overflow: hidden;
  text-decoration: none;
}
.hero__progress s i {
  position: absolute;
  inset: 0;
  background: rgba(243, 241, 237, 0.85);
  transform: scaleX(0);
  transform-origin: left;
  will-change: transform;
}

/* ---- scroll hint --------------------------------------------------------- */
.hero__hint {
  position: absolute;
  left: var(--gutter);
  bottom: clamp(1.5rem, 4vh, 2.6rem);
  z-index: 4;
  display: flex;
  align-items: center;
  gap: 0.85rem;
  font-family: var(--font-mono);
  font-size: clamp(0.56rem, 0.7vw, 0.66rem);
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: rgba(243, 241, 237, 0.5);
  pointer-events: none;
  will-change: opacity;
}
.hero__hint s {
  position: relative;
  width: clamp(34px, 5vw, 56px);
  height: 1px;
  background: rgba(243, 241, 237, 0.18);
  overflow: hidden;
  text-decoration: none;
}
.hero__hint s::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent, var(--color-secondary), transparent);
  animation: hintRun 2.4s var(--ease-inout) infinite;
}
@keyframes hintRun {
  0%   { transform: translate3d(-100%, 0, 0); }
  60%, 100% { transform: translate3d(100%, 0, 0); }
}

/* ===================================================== mobile behaviour == */
@media (max-width: 900px) {
  .hero__hint { display: none; }
  .hero__grid { padding-bottom: clamp(3.5rem, 9vh, 5rem); }
  .hs__sub { max-width: 28ch; }

  /* A portrait crop of a 16:9 frame leaves the car filling most of the
     height, so on a phone there is often no clean negative space to place
     copy into — every block lands in the same bottom band. A deeper foot
     gradient guarantees legibility there without pushing the type somewhere
     worse. Desktop keeps the lighter grade, where real negative space exists. */
  .hero__grade {
    background:
      radial-gradient(140% 96% at 50% 42%, transparent 40%, rgba(0, 0, 0, 0.34) 84%, rgba(0, 0, 0, 0.6) 100%),
      linear-gradient(to bottom,
        rgba(0, 0, 0, 0.46) 0%, transparent 18%, transparent 34%,
        rgba(0, 0, 0, 0.5) 66%, rgba(0, 0, 0, 0.84) 100%);
  }

}

/* The film's own grade. Not inside the width query above: a tablet in
   landscape is wider than that query reaches but is still showing the film,
   and the walkthrough's grade assumes negative space this cut does not have.
   Keyed on the class so it follows the mode, not the viewport. */
.hero.is-film .hero__grade {
  background:
    linear-gradient(to bottom,
      rgba(0, 0, 0, 0.55) 0%, rgba(0, 0, 0, 0.15) 26%, transparent 40%,
      transparent 74%, rgba(0, 0, 0, 0.5) 100%);
}

/* ============================================ no-JS / reduced-motion mode = */
/* No pinning, no scrubbing. The poster frame holds, the phases stack as a
   readable sequence, and every word survives. */
.no-js .hero,
.hero.is-static { height: auto; }
.no-js .hero__stage,
.hero.is-static .hero__stage {
  position: relative;
  height: auto;
  min-height: 100svh;
  padding-block: calc(var(--header-h) + var(--s5)) var(--s6);
}
.no-js .hero__ui,
.hero.is-static .hero__ui { position: relative; }
.no-js .hero__grid,
.hero.is-static .hero__grid {
  display: block;
  height: auto;
  padding-block: 0;
}
.no-js .hs,
.hero.is-static .hs,
.no-js .hs[data-anchor],
.hero.is-static .hs[data-anchor] {
  grid-area: auto;
  justify-items: start;
  text-align: left;
  padding-block: clamp(1.4rem, 4vh, 2.6rem);
  /* Beat any inline state the walkthrough may have written before the
     preference changed — every scene's copy must be readable here. */
  opacity: 1 !important;
  visibility: visible !important;
  transform: none !important;
  clip-path: none !important;
}
.no-js .hero__progress,
.no-js .hero__hint,
.hero.is-static .hero__progress,
.hero.is-static .hero__hint { display: none; }
.no-js .hero__world,
.hero.is-static .hero__world { position: absolute; }
.no-js .loader { display: none; }
