/* =====================================================================
   HERO — layout, type and layers. Pairs with js/hero.js + hero.config.js.
   Implements hero-section-prompt.md §2 (skeleton) and §5 (type/tokens).

   Loaded LAST so it wins the cascade over css/homepage-bundle.css. A
   handful of !important declarations below are load-bearing, not
   laziness: that bundle carries a site-wide
   `main > section { background-color: var(--site-paper) !important }`
   paper-surface rule which matches #hero directly, and !important always
   beats non-important regardless of specificity.
   ===================================================================== */

#hero {
  /* Scroll track. svh (not vh) so an iOS URL-bar collapse can't jitter
     the pinned canvas; vh only as an old-browser fallback. */
  /* !important: the bundle still carries the previous hero's
     `#hero { height: 400svh !important }` (and a 360svh mobile variant).
     Those rules went inert when the old markup was deleted, but this
     section reuses the #hero id so they apply again. */
  position: relative !important;
  /* 280svh = 1.8 screens of scroll. It was 420svh: 3.2 screens of a
     near-black frame carrying two sentences before any work appeared. */
  height: 280vh !important;
  height: 280svh !important;
  min-height: 0 !important;
  margin: 0 !important;
  padding: 0 !important;
  background: #0a0a0a !important;
  border: 0 !important;
}

#hero .hero__pin {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100svh;
  overflow: hidden;
  background: #0a0a0a;
  isolation: isolate;
}

/* ---- layer 0 · canvas ------------------------------------------------
   The only layer that scales. Identity and statements are siblings, never
   children, so they can counter-animate independently (spec §2). */
#hero .hero__canvas-wrap {
  position: absolute;
  inset: 0;
  z-index: 0;
  transform-origin: 50% 45%;
  will-change: transform, opacity;
  background: #0a0a0a;
  background-position: 50% 42%;
  background-size: cover;
  background-repeat: no-repeat;
}
/* The still is a FAILURE FALLBACK ONLY. It used to be an unconditional
   background on the wrapper, which meant a sharp copy of the portrait sat
   behind the canvases at all times — during the blur crossfade you saw
   both at once, reading as two overlapping images. js/hero.js adds
   .is-fallback only when the sequence genuinely cannot run (save-data,
   decode failure, or the slow-network abandon timeout) and never
   otherwise; <noscript> gets it too. Mobile art by default, web asset
   from 768px up so tablet takes the WEB image (spec §1). */
#hero .hero__canvas-wrap.is-fallback {
  background-image: url("../assets/hero-frames-mobile-retina/ezgif-frame-112.jpg");
}
@media (min-width: 768px) {
  #hero .hero__canvas-wrap.is-fallback {
    background-image: url("../assets/hero-frames-1920/ezgif-frame-112.jpg");
  }
}
#hero .hero__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  /* A global `max-width: 100%` media reset was silently clamping the
     oversized blur layer's width back to the pin's width while leaving
     its height alone — the two canvases ended up with different aspect
     ratios and the crossfade showed a stretched second copy. */
  max-width: none;
}
/* Oversized on purpose. The wrapper scales down to 0.86, so a canvas at
   inset:0 would shrink to 86% of the viewport and expose a hard
   rectangular edge against the sticky's black. Starting ~22% larger means
   that after the zoom-out it still covers the full frame, so the receding
   portrait reads as a full-bleed atmosphere rather than a shrinking
   photo. Only the blurred layer needs this — the sharp layer is only
   visible at rest, where scale is 1. */
/* Desktop radial focus (spec deviation, requested): the source footage
   is soft and shows banding down the left and right edges. The blurred
   layer sits underneath at full opacity, and the SHARP layer on top is
   masked to an ellipse over the subject — so the face stays crisp while
   everything outside it reads as depth-of-field falloff, which buries
   the banding. As the scroll fades the sharp layer out, the frame
   resolves to the fully blurred layer already behind it, so this costs
   no extra compositing during scroll.
   Mobile is excluded: it renders a single clean still with no banding
   to hide, and a full-size mask there is wasted GPU work. */
/* Stacking: the blurred layer is the BASE and the sharp layer sits on
   top of it. The markup order is sharp-then-blur, which would otherwise
   paint the blur above and hide the sharp frame entirely. With the sharp
   layer on top, fading its opacity out is what reveals the blur beneath
   — one mechanism for both the desktop radial focus and the mobile
   crossfade. */
#hero .hero__canvas--blur  { z-index: 1; }
#hero .hero__canvas--sharp { z-index: 2; }

/* ---- side frost ------------------------------------------------------
   Replaces the earlier radial focus mask. That masked the sharp canvas
   into an ellipse, which softened the whole surround including top and
   bottom. This instead frosts ONLY the left and right edges — where the
   source footage actually bands — and does it as a real overlay rather
   than a mask, so it is independent of the canvas opacity and stays put
   for the entire scroll instead of vanishing when the sequence settles.

   backdrop-filter blurs whatever is beneath, so it works the same over
   the playing sequence, the resting frame, and the blurred layer. The
   mask fades each panel to transparent on its inner edge so there is no
   visible seam where the frost stops. */
#hero .hero__frost {
  position: absolute;
  inset: 0;
  /* Above the canvases, below every text layer. backdrop-filter samples
     whatever is painted beneath it, so at a higher z-index it frosted the
     wordmark and eyebrow as well — the frost must only soften the video. */
  z-index: 3;
  pointer-events: none;
  -webkit-backdrop-filter: blur(22px) saturate(.92);
  backdrop-filter: blur(22px) saturate(.92);
  /* Full-bleed frost with a soft hole punched over the subject. Narrow
     left/right strips only hid the very edges; the footage is pixelated
     across the whole plate, so everything except the person is frosted
     and only the subject stays sharp. The transparent centre is an
     ellipse over the figure, feathered out so there is no visible rim
     where the blur begins. */
  -webkit-mask-image: radial-gradient(ellipse 27% 66% at 46% 46%,
    transparent 0%, transparent 46%, rgba(0,0,0,.45) 66%, rgba(0,0,0,.85) 82%, #000 100%);
  mask-image: radial-gradient(ellipse 27% 66% at 46% 46%,
    transparent 0%, transparent 46%, rgba(0,0,0,.45) 66%, rgba(0,0,0,.85) 82%, #000 100%);
}
/* Phone hero: the still is a dark, low-key portrait, and the full vignette
   (built for the desktop film) plus its .55 veil buried it, which is what
   read as a dull overlay. Phones keep only the top and bottom shading the
   nav and the wordmark need for legibility, and the still gets a gentle
   tone lift. The canvas is static on phones, so the filter costs nothing
   per frame. */
@media (max-width: 767px) {
  #hero .hero__vignette {
    opacity: 1;
    background: linear-gradient(180deg,
      rgba(10, 10, 10, .45) 0%,
      transparent 16%,
      transparent 70%,
      rgba(10, 10, 10, .78) 100%);
  }
  #hero .hero__canvas:not(.hero__canvas--blur) { filter: brightness(1.04) contrast(1.04); }
  /* Portrait reveal: fades up while growing outward from slightly smaller, in step
     with the letters. The still (preloaded in <head>) sits on the canvas
     element as a background in the same centred cover crop js/hero.js
     draws, so the reveal runs from first paint and the canvas pixels land
     on top of an identical image later without a visible change. Fill is
     backwards only, so once finished the scroll script owns opacity. */
  #hero .hero__canvas--sharp {
    background: url("../assets/hero-mobile.webp") 50% 50% / cover no-repeat;
    transform-origin: 50% 40%;
    animation: hero-photo-in 1.6s cubic-bezier(.16, 1, .3, 1) backwards;
  }
  /* Entrance in CSS: plays the moment the page paints. */
  #hero .hero__eyebrow { animation: hero-rise-in .55s cubic-bezier(.16, 1, .3, 1) .1s both; }
  /* The letters ship pre-split in index.html, so each one rises out of its
     own mask with a short stagger and a soft focus pull, straight from
     first paint, without waiting for js/hero.js. */
  #hero .hero__char-inner {
    animation: hero-char-in 1s cubic-bezier(.16, 1, .3, 1) calc(.22s + var(--i, 0) * 55ms) both;
  }
}
@keyframes hero-rise-in {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}
@keyframes hero-photo-in {
  from { opacity: 0; transform: scale(.9); }
  to   { opacity: 1; transform: none; }
}
@keyframes hero-char-in {
  from { opacity: 0; transform: translateY(110%); filter: blur(6px); }
  60%  { filter: blur(0); }
  to   { opacity: 1; transform: translateY(0); filter: blur(0); }
}
@media (max-width: 767px) and (prefers-reduced-motion: reduce) {
  #hero .hero__eyebrow, #hero .hero__char-inner, #hero .hero__canvas--sharp { animation: none; opacity: 1; transform: none; }
}
/* Mobile renders a clean still, so it needs no frost at all. */
@media (max-width: 767px) {
  #hero .hero__frost { display: none; }
}

#hero .hero__canvas--blur {
  /* Geometry is set in px by js/hero.js (sizeBlurLayer) rather than with
     percentages: `inset` + `width`/`height` percentages over-constrain an
     absolutely positioned box, and the width was silently being dropped
     while the height applied — the two canvases ended up different
     aspect ratios. */
  opacity: 0;
  will-change: opacity;
}

/* ---- layer 1 · vignette (spec §5) ----------------------------------- */
#hero .hero__vignette {
  position: absolute;
  inset: 0;
  z-index: 4;
  pointer-events: none;
  opacity: .55;
  background:
    radial-gradient(120% 90% at 50% 42%,
      transparent 0%,
      rgba(10, 10, 10, .5) 58%,
      rgba(10, 10, 10, .88) 100%),
    linear-gradient(180deg,
      rgba(10, 10, 10, .55) 0%,
      transparent 22%,
      transparent 62%,
      rgba(10, 10, 10, .9) 100%);
}

/* ---- layer 2 · identity --------------------------------------------- */
#hero .hero__identity {
  position: absolute;
  z-index: 5;
  left: 0;
  right: 0;
  bottom: clamp(24px, 4vh, 56px);
  /* Figma hero frame (516:261) insets the identity block 28px inside a
     1512px frame = 1.85vw, not the 40px this used to carry. */
  padding: 0 clamp(16px, 1.85vw, 28px);
  text-align: left;
  will-change: transform, opacity;
  /* Caps the measure on ultrawide and centres the block there (auto
     margins centre an absolutely positioned box that has both left:0
     and right:0). Below the cap it is edge-to-edge as before. */
  max-width: 2100px;
  margin-inline: auto;
  /* Lets the wordmark size itself from this box's CONTENT width via cqi,
     which already excludes the padding above and is immune to the
     scrollbar — unlike 100vw, which would overrun by the scrollbar's
     width on desktop. */
  container-type: inline-size;
}
#hero .hero__eyebrow {
  /* Desktop: two labels pinned to the outer edges, matching the comp. */
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 16px;
  margin: 0 0 clamp(12px, 1.6vh, 20px);
  /* Figma 516:289 / 516:290: Geist SemiBold 18px, tracking -0.36px
     (= -0.02em), solid white — not 14px/500 at 62% alpha. */
  font-size: clamp(.8125rem, 1.19vw, 1.125rem);
  font-weight: 600;
  letter-spacing: -.02em;
  text-transform: uppercase;
  color: #fff;
  opacity: 0;
}
#hero .hero__wordmark {
  font-family: "Zalando Sans Expanded", "Geist", Inter, system-ui, sans-serif;
  /* Sized to fill the identity box exactly. "SOHAN J." measures 5.2751em
     wide in Zalando Sans Expanded 700 at the comp's -0.06em tracking
     (measured in the browser, not estimated), so dividing the container's inline size
     by that constant makes the wordmark span the full width at every
     breakpoint. The divisor is the measured value so
     sub-pixel rounding or a font swap can never cause an overflow.
     The clamp() is the fallback for browsers without container units. */
  --wordmark-em-width: 5.2751;
  font-size: clamp(3.4rem, 14.5vw, 13rem);
  font-size: calc(100cqi / var(--wordmark-em-width));
  line-height: .9;
  letter-spacing: -.06em;
  /* !important: the theme layer sets heading colour to --ink-strong
     (#111 in light mode) with !important on h1. The hero always sits on
     the dark portrait whatever the site theme is, so its type must stay
     white in both. Same reason for the eyebrow and statements. */
  color: #fff !important;
  white-space: nowrap;
}
/* Per-character clip masks for the load reveal.
   `overflow: hidden` clipped BOTH axes, but the mask only needs to crop
   vertically (so the inner span can slide up from below). Horizontally
   it was cutting the ink off: at -0.06em tracking each character's
   inline box is narrower than its glyph, so the right side of round
   letters — S, O, A — was being shaved. clip-path crops to the box top
   and bottom while extending past it left and right, so the reveal
   still works and no glyph is trimmed. */
#hero .hero__char {
  display: inline-block;
  vertical-align: bottom;
  clip-path: inset(-0.25em -0.25em 0 -0.08em);
}
#hero .hero__char-inner {
  display: inline-block;
  transform: translateY(110%);
  will-change: transform;
}

/* ---- layer 3 · statements ------------------------------------------- */
#hero .hero__statements {
  position: absolute;
  z-index: 6;
  inset: 0;
  display: grid;
  place-items: center;
  padding: 0 clamp(16px, 3vw, 40px);
  pointer-events: none;
}
#hero .hero__statement {
  grid-area: 1 / 1;   /* both stack in the same cell — no layout shift */
  margin: 0;
  /* Geist, not Zalando: the wordmark keeps the display face, the two
     statements are set in Geist Medium. */
  font-family: "Geist", Inter, system-ui, sans-serif;
  font-weight: 500;
  /* Spec §5 gives clamp(2.15rem, 4.4vw, 4.05rem), but at that size the
     longest configured line ("Designing clear products, sharper brands")
     overflows the viewport once the comp's line breaks are locked in —
     the spec's own 1180px max-width would have wrapped it to 3 lines,
     contradicting its "desktop: 2 lines". Scaled down so the comp's
     breaks fit edge-to-edge with margin at 1440px and up. */
  /* One step down from clamp(1.9rem, 3.4vw, 3.2rem): the comp's line
     breaks are hard-coded spans, so the size can come down without the
     wrapping changing. */
  font-size: clamp(1.7rem, 3vw, 2.85rem);
  line-height: 1.14;
  /* Figma 516:230: 48px Geist Medium, tracking -1.92px = -0.04em. */
  letter-spacing: -.04em;
  text-align: center;
  color: #fff;
  /* Wider than the spec's 62ch/1180px: the comp puts statement 1 on two
     lines and statement 2 on three, and 1180px forced an extra wrap. */
  max-width: min(76ch, 1440px);
  visibility: hidden;
}
/* Above 1024px there is always room for the configured line breaks, so
   lock them — the line masks are meant to be the only breaks. */
@media (min-width: 1024px) {
  #hero .hero__line-inner { white-space: nowrap; }
}
/* Line crop masks — inner span slides up out of `overflow: hidden`. */
#hero .hero__line {
  display: block;
  overflow: hidden;
}
#hero .hero__line-inner {
  display: block;
  opacity: 0;
  transform: translateY(105%);
  will-change: transform, opacity;
}

/* ---- scroll cue ------------------------------------------------------ */
#hero .hero__scroll-cue {
  position: absolute;
  z-index: 7;
  left: 50%;
  bottom: clamp(14px, 2.4vh, 26px);
  translate: -50% 0;
  width: 22px;
  height: 34px;
  border: 1px solid rgba(255, 255, 255, .32);
  border-radius: 999px;
  opacity: 0;
  pointer-events: none;
}
#hero .hero__scroll-cue::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 7px;
  width: 2px;
  height: 6px;
  margin-left: -1px;
  border-radius: 2px;
  background: rgba(255, 255, 255, .7);
  animation: hero-cue 1.8s var(--ease-out) infinite;
}
@keyframes hero-cue {
  0%, 100% { transform: translateY(0); opacity: .35; }
  50%      { transform: translateY(2px); opacity: 1; }
}

/* ---- brief opening scroll lock (spec §4 Phase 0) --------------------- */
html.hero-scroll-locked,
html.hero-scroll-locked body {
  overflow: hidden !important;
  touch-action: none !important;
}

/* ---- mobile ---------------------------------------------------------- */
@media (max-width: 767px) {
  #hero {
    height: 260vh !important;
    height: 260svh !important;
  }
  #hero .hero__identity { text-align: center; }
  /* Mobile comp: one centred role label, no location label. */
  #hero .hero__eyebrow { justify-content: center; text-transform: none; font-size: 1rem; }
  #hero .hero__eyebrow-loc { display: none; }
  /* No wordmark size override here: the cqi fill formula already spans
     the full width on mobile, which is what the mobile comp shows. */
  #hero .hero__statement {
    font-size: clamp(1.6rem, 6.6vw, 2.05rem);
    line-height: 1.22;
    max-width: 92vw;
  }
}

/* =====================================================================
   REDUCED MOTION (spec §6)
   No pin, no sequence, no blur. The still plus both statements as a
   normal stacked block that scrolls and reads identically.
   ===================================================================== */
@media (prefers-reduced-motion: reduce) {
  #hero {
    height: auto !important;
  }
  #hero .hero__pin {
    position: relative !important;
    height: auto !important;
    min-height: 100svh;
  }
  #hero .hero__canvas-wrap {
    position: relative;
    height: 100svh;
    transform: none !important;
    opacity: 1 !important;
  }
  #hero .hero__canvas--blur { display: none; }
  #hero .hero__identity {
    position: relative;
    bottom: auto;
    opacity: 1 !important;
    transform: none !important;
    filter: none !important;
    visibility: visible !important;
    margin-top: -22svh;
  }
  #hero .hero__eyebrow { opacity: 1; }
  #hero .hero__char-inner { transform: none !important; }
  #hero .hero__statements {
    position: relative;
    inset: auto;
    display: block;
    padding: clamp(48px, 9vh, 96px) clamp(16px, 3vw, 40px);
  }
  #hero .hero__statement {
    visibility: visible !important;
    transform: none !important;
    max-width: min(62ch, 1180px);
    margin: 0 auto clamp(32px, 6vh, 64px);
  }
  #hero .hero__line-inner {
    opacity: 1 !important;
    transform: none !important;
    filter: none !important;
  }
  #hero .hero__scroll-cue { display: none; }
  html.hero-scroll-locked,
  html.hero-scroll-locked body { overflow: auto !important; touch-action: auto !important; }
}

/* ---- Work cover morph, mobile --------------------------------------
   css/styles.css enables MPA view transitions site-wide and assigns
   `view-transition-name: case-study-cover` to #swThumb, which is
   the DESKTOP thumbnail. On mobile that element is present but measures
   0x0 (the mobile layout renders .sw-mobile-card instead), and a
   view-transition-name on a zero-area box makes the browser skip the
   transition outright — which is where the "AbortError: Transition was
   skipped" in the console came from, and why tapping a mobile card just
   cut to the case study with no morph.

   Releasing the name below the desktop breakpoint lets the visible card
   own it instead (js/mobile-linear-experience.js tags the matching
   card), so the cover actually morphs on mobile. A name may only belong
   to one element per document, hence the swap rather than adding. */
@media (max-width: 900px) {
  #swThumb { view-transition-name: none; }
}

/* ---- Hand-off into Work ------------------------------------------------
   The hero's black ended on a hard edge against the light Work section.
   The last stretch of the track fades into the page colour, so as the pin
   releases the dark frame dissolves into the next chapter instead of
   cutting to it. It sits over the stage only once the stage has settled
   at the bottom of the track, below statement 2. */
#hero::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 36vh;
  height: 36svh;
  z-index: 3;
  pointer-events: none;
  /* Eased, not linear: the paper's opacity follows a smootherstep curve
     (flat at both ends), sampled every 10%. Two straight segments met at
     a visible kink and started and stopped abruptly, which read as a
     grey band with a hard top and bottom edge. */
  background: linear-gradient(to bottom,
    transparent 0%,
    color-mix(in srgb, var(--site-paper, #f4f4f4) 0.9%, transparent) 10%,
    color-mix(in srgb, var(--site-paper, #f4f4f4) 5.8%, transparent) 20%,
    color-mix(in srgb, var(--site-paper, #f4f4f4) 16.3%, transparent) 30%,
    color-mix(in srgb, var(--site-paper, #f4f4f4) 31.7%, transparent) 40%,
    color-mix(in srgb, var(--site-paper, #f4f4f4) 50.0%, transparent) 50%,
    color-mix(in srgb, var(--site-paper, #f4f4f4) 68.3%, transparent) 60%,
    color-mix(in srgb, var(--site-paper, #f4f4f4) 83.7%, transparent) 70%,
    color-mix(in srgb, var(--site-paper, #f4f4f4) 94.2%, transparent) 80%,
    color-mix(in srgb, var(--site-paper, #f4f4f4) 99.1%, transparent) 90%,
    var(--site-paper, #f4f4f4) 100%);
}
