/* hero.css — "The Descent" v4: the PLATE-ANCHORED PHASE-TIMELINE scrub hero.
 *
 * Everything is scoped under .wss-hero / [data-hero-*], so this file is inert on
 * every page that doesn't render the scrub hero (the in-flow image heroes in the
 * page/service/team/post templates also use [data-hero] — they are untouched
 * because every rule here is keyed on .wss-hero or [data-hero-scrub]).
 * Consumes tokens.css (--gold, --ease, --ink, --sans, --serif).
 *
 * ── v3 → v4 ────────────────────────────────────────────────────────────────
 * The shape is unchanged and deliberately so: a tall scroll REGION containing a
 * sticky stage, the region supplying the scroll distance, the stage repainting as
 * the guest scrolls. The owner approved that feel and the region length. What
 * changed is what the canvas paints — a five-phase timeline of code zooms on 4K
 * plates and AI video between them — and that the welcome reveal is now driven by
 * SCROLL POSITION rather than by a timed transition fired at a threshold.
 * Ordinary document flow throughout — no fixed positioning, no scroll lock, no
 * scroll hijacking, nothing to dismiss.
 *
 * STRUCTURE (templates/hero.js for markup, src/js/hero.js for behaviour):
 *   .wss-hero[data-hero-scrub]      the scroll region, --wss-hero-region tall
 *     .wss-hero__sticky             sticky 100svh stage (minus the header)
 *       .wss-hero__canvas           the frames, drawn cover-fit           z 1
 *       .wss-hero__poster           LCP image = frame 1, fades out        z 1
 *       .wss-hero__video            MP4 fallback, only ever injected      z 1
 *       .wss-hero__hair             the sweeping hair band, Phase C       z 1
 *       .wss-hero__mirror           arch-clipped glass layer              z 2
 *         .wss-hero__welcome        wordmark + tagline + Book Now
 *       .scroll-cue                 (from home.js; home.css positions it)  z 4
 *       .wss-hero__skip             44px affordance                       z 3
 *       .wss-hero__loader           the loading veil, over everything     z 6
 *
 * HEIGHT: .site-header is `position: sticky` AND in flow, so the stage must
 * stick at the header's bottom edge and be exactly that much shorter, or its
 * bottom spills past the fold. hero.js measures the header and publishes
 * --wss-header-h. Without JS the var is unset and the fallback is a plain
 * 100svh — the relationship the site had before any cinematic existed.
 *
 * PERFORMANCE: the only animated properties are opacity and a 14px translate on
 * the welcome block — both compositable. The canvas is repainted by JS only when
 * the drawn rect or frame index actually changes; nothing here animates layout,
 * and there is no mask, no filter and no counter-scaling.
 *
 * --wss-welcome-t is the reveal, 0..1, written by hero.js on the section every
 * frame of Phase E. It is a variable rather than a transition because the content
 * has to MATERIALISE under the guest's own thumb as the glass fills the screen; a
 * 900ms fade triggered at a threshold would arrive on its own schedule and lag
 * whatever the scroll did next. Its fallback value is 1, so any path that forgets
 * to write it still shows the content — the failure mode is "visible", never
 * "blank". */

.wss-hero[data-hero-scrub] {
  /* Scroll distance for the walk. UNCHANGED from v3 — the owner approved this
   * length explicitly, so v4 re-divided the same 500vh rather than growing it:
   * 17% the storefront push, 64% the three video segments (split by frame count so
   * the scrub rate is constant across all three), 19% the zoom into the mirror. */
  --wss-hero-region: 500vh;
  position: relative;
  width: 100%;
  height: var(--wss-hero-region);
  background: #0a0a0b;
}

.wss-hero__sticky {
  position: sticky;
  top: var(--wss-header-h, 0px);
  height: 100vh;
  height: calc(100svh - var(--wss-header-h, 0px));
  min-height: 420px;
  width: 100%;
  overflow: hidden;
  isolation: isolate;
  background: #0a0a0b; /* letterbox colour behind the cover-cropped frame */
}

/* ── Footage plate ─────────────────────────────────────────────────────────── */
.wss-hero__canvas,
.wss-hero__poster,
.wss-hero__video {
  position: absolute;
  inset: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
  display: block;
}
.wss-hero__poster,
.wss-hero__video {
  object-fit: cover;
  object-position: center;
}

/* The poster is the LCP element: frame 1, the storefront, painted straight from
 * the HTML. It sits OVER the canvas and fades out the moment the first frame is
 * decoded and drawn, so there is never a black flash and never a visible swap. */
.wss-hero__poster {
  opacity: 1;
  transition: opacity 500ms var(--ease);
}
.wss-hero.is-ready .wss-hero__poster {
  opacity: 0;
}

/* ── The hair band ─────────────────────────────────────────────────────────── */
/* VERSION 3's signature beat. src/js/hero.js runs the SAME live Verlet strand
 * simulation src/js/wipe.js turns pages with (src/js/hair-curtain.js) and paints
 * it here, over the footage plate, for exactly the slice of the walk between the
 * door swinging open and the salon interior being established.
 *
 * It sits at z 1 and AFTER the poster in source order, so it paints above both
 * the canvas and the (by then transparent) poster without disturbing the mirror,
 * cue or skip layers above it — the one thing V3 must not do is renumber a
 * stacking order three rounds of this hero have already agreed on.
 *
 * No transition and no animation. Opacity is written by hero.js from SCROLL
 * POSITION, frame by frame, for the same reason --wss-welcome-t is: a transition
 * on top of a scrubbed value smears it behind the guest's thumb. The element is
 * pointer-events:none and aria-hidden — it is weather, not content.
 *
 * `visibility` (not display) is what the class toggles: the backing store must
 * survive the band being off screen, because the guest can scroll straight back
 * into the sweep and a re-created canvas would flash. */
.wss-hero__hair {
  position: absolute;
  inset: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
  display: block;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}
.wss-hero.is-hair .wss-hero__hair {
  visibility: visible;
}
/* The collapse paths have no walk to sweep through — and the MP4 fallback draws
 * its own everything — so the band is ruled out on both. */
.wss-hero.is-static .wss-hero__hair,
.wss-hero.is-video .wss-hero__hair {
  display: none;
}

/* ── The mirror: an arch-clipped layer over the measured glass ─────────────── */
/* hero.js sets left/top/width/height and the arch border-radius in px, projecting
 * scrub-manifest.json's measured glass box through whatever source rect Phase E's
 * zoom is currently drawing. It is not a fixed percentage of the stage any more,
 * because the camera moves.
 *
 * The default is therefore the FULL STAGE, not a projected box. At the end of
 * Phase E — which is the only state that exists without JS, since <noscript> and
 * every fallback paint scrub-mirror-*.jpg, a frame cut INSIDE the glass — the
 * whole screen already is glass, so a full-stage content layer with its light wash
 * is both simpler and more correct than any percentage. v3 hard-coded its measured
 * box here and had to keep two sets of numbers in sync; there is now one. */
.wss-hero__mirror {
  position: absolute;
  z-index: 2;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  border-radius: 0;
  overflow: hidden;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}

/* Scrubbed: the glass fades up with the zoom, frame by frame. No transition —
 * --wss-welcome-t IS the animation, and a transition on top of it would smear the
 * value behind the guest's thumb. */
.wss-hero.is-welcome .wss-hero__mirror {
  opacity: var(--wss-welcome-t, 1);
  visibility: visible;
  pointer-events: auto;
}
/* Collapsed / fallback: there is no scroll to ride, so it fades in on its own. */
.wss-hero.is-static .wss-hero__mirror {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transition: opacity 900ms var(--ease);
}

/* The welcome block. hero.js sets left/top/width/height (relative to the mirror
 * layer) to the VISIBLE glass minus an inset, with the top edge clear of the
 * arch's curved shoulder — so nothing ever lands on the curve, and on a phone,
 * where the mirror overflows both edges, it centres on what is on screen. */
.wss-hero__welcome {
  position: absolute;
  left: 8%;
  top: 34%;
  width: 84%;
  height: 52%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: clamp(14px, 2.6vh, 26px);
  text-align: center;
  /* Rides the same variable as the opacity: 14px of lift consumed as the reveal
   * completes, so the block settles INTO the glass rather than appearing in it. */
  transform: translateY(calc(14px * (1 - var(--wss-welcome-t, 1))));
}
.wss-hero.is-static .wss-hero__welcome {
  transform: translateY(0);
  transition: transform 900ms var(--ease);
}

/* A breath of light on the glass so the ink-black wordmark always has contrast,
 * whatever the reflection behind it happens to be (the glass reflects both a
 * pale wall and a dark doorway). Reads as light catching the mirror rather than
 * as a card, and the arch clip above keeps it inside the frame. */
.wss-hero__welcome::before {
  content: "";
  position: absolute;
  inset: -22% -16%;
  background: radial-gradient(
    58% 52% at 50% 50%,
    rgba(255, 252, 246, 0.95) 0%,
    rgba(255, 252, 246, 0.86) 38%,
    rgba(255, 252, 246, 0.42) 66%,
    rgba(255, 252, 246, 0) 82%
  );
  pointer-events: none;
  z-index: -1;
}

.wss-hero__logo {
  width: min(78%, 300px);
  max-height: 42%;
  height: auto;
  object-fit: contain;
  display: block;
}

.wss-hero__tagline {
  margin: 0;
  font-family: var(--serif);
  font-weight: 300;
  font-size: clamp(0.95rem, 2.3vw, 1.45rem);
  line-height: 1.2;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink);
  text-wrap: balance;
}

.wss-hero__book {
  flex: none;
}

/* ── Bottom scrim ─────────────────────────────────────────────────────────── */
/* The cue and the Skip button have to stay legible over EVERY frame, and frame 1
 * is sunlit pavement — white-on-white. A shallow bottom gradient gives both a
 * dependable floor without touching the composition. It leaves with the cue, so
 * the settled mirror is never darkened. */
.wss-hero__sticky::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 26%;
  /* z 1 = the plate's level; being later in tree order puts it over the canvas
   * and poster but UNDER the mirror layer (z 2), so it can never darken the
   * welcome content. */
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(to top, rgba(0, 0, 0, 0.42), rgba(0, 0, 0, 0));
  opacity: 1;
  transition: opacity 500ms var(--ease);
}
.wss-hero.is-walking .wss-hero__sticky::after,
.wss-hero.is-welcome .wss-hero__sticky::after,
.wss-hero.is-static .wss-hero__sticky::after {
  opacity: 0;
}

/* ── Scroll cue ───────────────────────────────────────────────────────────── */
/* The cue is home.js's .scroll-cue, rendered inside this stage (see
 * templates/hero.js). home.css fades it in with `animation: … forwards`, and a
 * running keyframe animation OUTRANKS an inline style — so the controller cannot
 * dim it by writing style.opacity. It toggles .is-walking on the section
 * instead, and the animation is cancelled here. */
.wss-hero .scroll-cue {
  transition: opacity 420ms var(--ease);
}
.wss-hero.is-walking .scroll-cue,
.wss-hero.is-welcome .scroll-cue {
  animation: none;
  opacity: 0;
  pointer-events: none;
}

/* ── Skip ─────────────────────────────────────────────────────────────────── */
/* Not a dismissal — it smooth-scrolls to the end of the scroll region, landing
 * the visitor on the settled mirror exactly as if they had walked there. It
 * hides itself once the welcome block is up. */
.wss-hero__skip {
  position: absolute;
  z-index: 3;
  right: calc(env(safe-area-inset-right, 0px) + 14px);
  bottom: calc(env(safe-area-inset-bottom, 0px) + 14px);
  min-width: 44px; /* 44px touch target */
  min-height: 44px;
  padding: 0 16px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: var(--sans);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: #fff;
  background: rgba(0, 0, 0, 0.28);
  border: 1px solid rgba(255, 255, 255, 0.42);
  border-radius: 999px;
  cursor: pointer;
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
  transition: background-color 220ms var(--ease), border-color 220ms var(--ease),
    opacity 300ms var(--ease);
}
.wss-hero__skip:hover,
.wss-hero__skip:focus-visible {
  background: rgba(0, 0, 0, 0.48);
  border-color: var(--gold-light, #dcc389);
}
.wss-hero__skip[hidden] {
  display: none;
}
/* On a phone chat.js parks a wide "CHAT WITH LORI" pill across the bottom of the
 * viewport, and it sat directly on top of Skip — caught in the 390x844 pass, where
 * the two affordances were literally overlapping circles. The pill is centred and
 * leaves the bottom-LEFT corner clear, so Skip moves there in portrait. */
@media (orientation: portrait) {
  .wss-hero__skip {
    right: auto;
    left: calc(env(safe-area-inset-left, 0px) + 14px);
  }
}

/* ── The loading veil ─────────────────────────────────────────────────────── */
/* What it covers is the ASSEMBLY, not the load: the poster decoding and the
 * canvas taking its first frame are two paints the owner read as "random
 * artifacts on load". src/js/hero.js removes the element from the DOM after the
 * fade. It is never merely hidden: a 100svh layer left parked over the stage
 * forever is the failure this whole file spends its comments avoiding.
 * templates/hero.js's <noscript><style> hides it outright when scripting is off
 * — read the gate note there, it is the load-bearing half of this. */
.wss-hero__loader {
  position: absolute;
  inset: 0;
  z-index: 6;
  background: #0a0a0b;
  display: grid;
  place-items: center;
  align-content: center;
  gap: var(--space-5);
  opacity: 1;
  transition: opacity 300ms var(--ease);
}
/* Her real logo artwork, inverted: the file is ink-on-transparent (it is the same
 * URL base.js puts in the header, so it costs nothing here) and ink on near-black
 * is nothing at all. invert() is a compositor filter on a 20KB PNG, and it keeps
 * ONE wordmark asset on the property rather than a second, light-on-dark export
 * that would have to be re-cut every time she changes her logo. */
.wss-hero__loader-mark {
  width: min(62vw, 340px);
  height: auto;
  filter: invert(1);
  opacity: 0.92;
}
.wss-hero__dots {
  display: flex;
  gap: 11px;
}
.wss-hero__dots i {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--gold);
  animation: wss-hero-dot 1.25s infinite ease-in-out both;
}
.wss-hero__dots i:nth-child(2) {
  animation-delay: 0.18s;
}
.wss-hero__dots i:nth-child(3) {
  animation-delay: 0.36s;
}
@keyframes wss-hero-dot {
  0%,
  80%,
  100% {
    transform: scale(0.5);
    opacity: 0.3;
  }
  40% {
    transform: scale(1);
    opacity: 1;
  }
}
/* The fade out, and the frame before a router revisit has decided whether it
 * needs a veil at all (see the ~80ms arming window in src/js/hero.js). */
.wss-hero__loader.is-hidden,
.wss-hero__loader.is-arming {
  opacity: 0;
  pointer-events: none;
}
/* Arming is not a fade — it is "do not show yet", and it must be able to end in
 * either direction without a transition running backwards. */
.wss-hero__loader.is-arming {
  transition: none;
}

/* ── Collapsed state ──────────────────────────────────────────────────────── */
/* Repeat visits, router revisits, prefers-reduced-motion, Save-Data and every
 * failure path share one shape: the region shrinks to a single screen holding
 * the settled mirror with the welcome content already up. The walk is offered
 * once per session — it is never imposed on every navigation. */
.wss-hero[data-hero-scrub].is-static {
  height: auto;
}
.wss-hero.is-static .wss-hero__sticky {
  position: relative;
  top: 0;
}
.wss-hero.is-static .wss-hero__canvas {
  display: none;
}
.wss-hero.is-static .wss-hero__poster {
  opacity: 1;
}
.wss-hero.is-static .wss-hero__skip {
  display: none;
}
/* The MP4 fallback covers the poster once it is actually playing; until then the
 * poster is the picture, so there is no gap. */
.wss-hero.is-video .wss-hero__video {
  z-index: 1;
}

/* ── Reduced motion ───────────────────────────────────────────────────────── */
/* The WALK still runs under this query — a preference is not an inability, and
 * the owner is the one who caught the old behaviour (Reduce Motion is on on her
 * phone, and she was served "a static beige photo with our logo"). What is
 * suppressed is motion the INTERFACE starts by itself. Everything in this hero
 * that moves — the plates, the frames, the hair band, the welcome — moves only
 * because the guest's own thumb moved it, so it stays. What goes is the veil's
 * pulse and every CSS-driven fade: tokens.css has already zeroed transition
 * durations globally, and these rules match it where a duration is stated here.
 * The transform offset is neutralised so nothing starts displaced. */
@media (prefers-reduced-motion: reduce) {
  .wss-hero__poster {
    transition: none;
  }
  .wss-hero__welcome {
    transform: none;
  }
  /* The one thing on this layer that moves by itself. The veil still SHOWS and
   * still GOES — a guest who asked for calm did not ask to watch the hero
   * assemble itself — it simply does both without a pulse and without a fade
   * (src/js/hero.js removes it with no fade delay to match). */
  .wss-hero__dots i {
    animation: none;
  }
  .wss-hero__loader {
    transition: none;
  }
  .wss-hero.is-static .wss-hero__mirror,
  .wss-hero.is-static .wss-hero__welcome {
    transition: none;
  }
}
