/* ==========================================================================
   01 — HERO (1.0 vh)

   The composition is v2's, unchanged: .hero, .hero__inner, .hero__eyebrow,
   .hero__title, .line, .line__in, .hero__sub, .hero__scroll,
   .hero__scroll-line and the scrollHint keyframe are copied verbatim from
   css/styles.css:200-297 (plus the two hero rules from v2's media queries).
   This is the one section that should feel unchanged — the anchor the rest of
   the page departs from.

   Three things change in v3, and they all live in the "v3" half below:
     1. the eyebrow and the `scroll` label scramble (main.js wires them; this
        file only has to stay out of .scr-a / .scr-b's way),
     2. the headline line-rise is a CSS animation released by the gate instead
        of a GSAP timeline — and it does NOT scramble,
     3. the accent `.` in `exe.cute it.` lands last, 220ms after line 2 settles.

   Ownership split, so nothing fights over the same property:
     · CSS animations own  .line__in · .hero__sub · .eyebrow__dot · #hero-dot
                           .hero__scroll-label · .hero__scroll-line
     · js/ui/hero.js owns  .hero__inner (drift)  ·  .hero__scroll (hint fade)
   A running/filling CSS animation outranks an inline style, so the two sets
   are deliberately kept on different elements.
   ========================================================================== */

/* --------------------------------------------------------------------------
   v2 composition — verbatim
   -------------------------------------------------------------------------- */
.hero {
  position: relative;
  min-height: 100svh;
  display: grid;
  place-items: stretch start;
  padding: 0 var(--gutter);
  text-align: left;
  /* The vertical unit of the field division: one row of air under the upper guide. Measured
     cut-edge-to-first-glyph is exactly --row + 1px at every viewport. */
  --row: clamp(18px, 3vh, 40px);

  /* intro easings, scoped to the hero: quartic-out is GSAP power4.out, the
     cubic-out is power3.out — the two curves v2's intro timeline used. */
  --ease-quart: cubic-bezier(0.165, 0.84, 0.44, 1);
  --ease-cubic: cubic-bezier(0.215, 0.61, 0.355, 1);
}

/* The type block. It takes fields 1-3 — all six below the fold — and hangs on --guide-x0, while
   the glass mark takes fields 4-6 and hangs on --guide-x1. The two can never share a field: that
   is the whole of this composition. The mark is SOLVED against the same division, from
   fieldDivision() in ui/guides.js, so there are no placement constants left to fall out of date.

   padding-bottom is 0 on purpose. With it, .hero overflows 100svh on every landscape phone
   (measured 411.09px at 667x375 and 443.09px at 812x375 against a 375px viewport), which breaks the
   heroHeight == viewportHeight identity that every scroll figure here depends on. */
.hero__inner {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  /* 100% is the right no-JS fallback, and it is exactly the guide box, since .hero is already inset
     by padding: 0 var(--gutter). NOT 1100px: measured, that overflows the right gutter by 55.9px at
     390x844, 100.9 at 768x1024, 78.5 at 600x900 and 54.7 at 360x640. */
  width: var(--type-col, 100%);
  max-width: none;
  /* Centred between the two guides, not hung from the upper one. The padding is symmetric so the
     optical centre lands between them rather than in the raw viewport, and justify-content does the
     rest. It stays a FLOOR, not the position: min-height keeps .hero at one viewport, so on a short
     window the padding collapses out of the way before anything overflows — the identity
     heroHeight == viewportHeight that every scroll figure here depends on is checked at 667x375 and
     812x375, the two that broke it last time. */
  justify-content: center;
  /* min(), not the raw sum: at 667x375 and 812x375 a full bar+row on both edges puts 415px of
     content in a 375px viewport and .hero grows past 100svh, which is exactly the identity every
     scroll figure in this file rests on. 8vh collapses the padding before the content does.
     Because the block is CENTRED, the padding no longer decides where it sits — only how close it
     may come to the two bars — so shrinking it on a short window costs nothing. */
  padding-block: min(calc(var(--bar-h) + var(--row)), 8vh);
  /* owned by js/ui/hero.js, one write per frame */
  transform: translate3d(0, var(--hero-y, 0px), 0);
  opacity: var(--hero-op, 1);
  /* The upper horizontal is an EDGE: the type ends on the guide instead of sliding under the chrome
     wordmark, which is left-aligned too. clip-path and not mask-image, deliberately — css/base.css
     already carries an @supports-not branch for masks and the hero must not need a second one.
     The inset(0) fallback is a no-op, which is correct: under no-JS nothing writes --hero-clip, and
     the opaque .chrome__bar--top is what keeps the type off the wordmark there. The clip is the
     cosmetic half; the plate is the guarantee. */
  clip-path: inset(var(--hero-clip, 0px) 0 0 0);
  will-change: transform, opacity;
}

.hero__eyebrow {
  font-family: var(--font-mono);
  font-weight: 300;
  font-size: 0.78rem;
  letter-spacing: 0.14em;
  text-transform: lowercase;
  color: var(--bone-70);
  margin-bottom: clamp(20px, 3vh, 40px);   /* was 3.4vw: a width-driven rhythm in a height-made band */
}

.hero__title {
  font-weight: 300;
  /* The JS-free floor is max(2.9rem, min(8.4rem, 12.5vh, 15vw)): with --type-col unset the column
     term falls back to 100vw/4 = 25vw, always above the 15vw term, so it drops out cleanly.
     WHAT ACTUALLY GOVERNS, re-swept at 15 viewports after the 3:3 field split (75417f1) narrowed
     the column. The old note here said the column term binds at NONE of them; it binds at two, and
     they are the tablet-portrait sizes:
         320-600 wide          15vw
         768x1024, 834x1112    the column, /4     <- the term that was said never to bind
         1024-1440, and both landscape phones   12.5vh
         1920 and wider        the 8.4rem cap
     So the headline IS set to its measure on a portrait tablet, and only there. That coupling is
     now real rather than theoretical: narrowing --type-col shrinks the headline at those two sizes
     and nowhere else. Do not retune the divisor without re-measuring every line break. */
  font-size: max(2.9rem, min(8.4rem, calc(var(--type-col, 100vw) / 4), 12.5vh, 15vw));
  line-height: 0.98;
  letter-spacing: -0.035em;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}
.hero__title em {
  font-size: 1.06em;
  letter-spacing: -0.02em;
  line-height: 1;
}
.line {
  display: block;
  overflow: hidden;
  padding: 0.06em 0.12em 0.1em;
  padding-left: 0;    /* ink starts on --guide-x0; the 0.12em was optical slack for centred type */
  margin: -0.02em 0;
}
.line__in {
  display: block;
  will-change: transform;
}

.hero__sub {
  /* margin-top:auto is gone — the type block is one unit now, not a stretched frame with a hole in
     it for the mark. A 20px floor is safe: measured, .hero height equals the viewport height at all
     twelve viewports in the matrix. */
  margin-top: clamp(20px, 3.2vh, 40px);
  /* the fallback is the same 36ch, so with the property missing the min() is a no-op rather than
     resolving to none at all — which is what it does today under no-JS. */
  max-width: min(36ch, var(--sub-col, 36ch));
  font-size: clamp(1rem, 1.35vw, 1.2rem);
  line-height: 1.55;
  color: var(--bone-70);
}

.hero__scroll {
  position: absolute;
  bottom: calc(var(--bar-h) + clamp(8px, 1.6vh, 20px));   /* above --guide-y1 */
  /* Centred, deliberately against the rest of the composition. Everything else in the hero hangs on
     a guide, but the cue is not part of the type block — it is an affordance for the whole frame,
     and on the left gutter it read as a fourth line of the paragraph above it.
     transform is declared ONCE, further down, as translateX(-50%) translateY(0) — the form the
     is-scrolled rule mirrors. A second declaration here was dead. */
  left: 50%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  font-family: var(--font-mono);
  font-weight: 300;
  font-size: 0.68rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  /* --bone, not --bone-70. Single-page-load A/B with the field pinned in one phase, glyph cores
     only, WCAG against the pixel actually behind each glyph — median / p10 / min:
       --bone-45   2.92-2.98 / 2.18-2.25 / 2.03-2.10   never AA on any statistic, any viewport
       --bone-70   5.55-5.68 / 3.70-3.90 / 3.38-3.57   AA on the median only, at 10.88px w300
       --bone     10.52-10.81 / 6.49-6.82 / 5.89-6.29   AA on min, p10 and median at all six
     --bone-70 is a real improvement that still does not clear AA. Do not call it fixed. */
  color: var(--bone);
  /* A TIME-based transition, symmetric both ways. The measurement that argued for a hard snap —
     "no fading text holds 4.5:1" — was taken on a SCROLL-driven ramp, which parks the cue in the
     illegible band for as long as the reader lingers in that scroll range. A time-based transition
     cannot park: it crosses in 260ms whatever the scroll does, and it is driven by a class that
     flips once. The snap it replaced read as a blink. */
  opacity: 1;
  transform: translateX(-50%) translateY(0);
  transition: opacity 260ms var(--ease-out), transform 260ms var(--ease-out);
}
html.is-scrolled .hero__scroll {
  opacity: 0;
  /* a short drop, so it reads as leaving rather than as being switched off */
  transform: translateX(-50%) translateY(10px);
}
.hero__scroll-line {
  position: relative;
  width: 1px;
  height: clamp(18px, 4.4vh, 48px);   /* was a flat 48px: the lower row is 32.6px on landscape phones */
  background: var(--bone-12);
  overflow: hidden;
}
.hero__scroll-line::after {
  content: "";
  position: absolute;
  left: 0; top: 0;
  width: 100%;
  height: 40%;
  background: var(--bone);
  animation: scrollHint 2.2s cubic-bezier(0.65, 0, 0.35, 1) infinite;
}
@keyframes scrollHint {
  0%   { transform: translateY(-100%); }
  60%  { transform: translateY(260%); }
  100% { transform: translateY(260%); }
}

/* Below 440px of viewport height the eyebrow, headline and sub reach the lower guide on their own:
   the measured lower row is 32.6px at both 667x375 and 812x375, against a 46px cue. The cue leaves
   the hero — but it is RELOCATED, not deleted, because a landscape phone would otherwise get no
   scroll affordance at all. The bottom bar has the room: free span between the readout's right edge
   and the segment measures 461.7px at 667x375 and 593.2px at 812x375, against a 50.9px label. It
   rides html.is-scrolled like the hero copy, so it is gone by 24px of scroll and never floats over
   a later section.
   CONFIDENCE: medium. Geometry measured and screenshotted, but not ink-verified, and it is the only
   rule that puts a hero element into the chrome. */
/* Below the fold the type takes all six fields and the mark drops to the row underneath — the same
   row the cue sits in. Centred, the cue lands inside the counter of the "e": at 390x844 the mark
   spans x 145..370 and a centred cue sits at 195. It goes back on the left guide here, where the
   mark's own left edge is 125px clear of it.
   ABOVE THE FOLD THE SEPARATION IS VERTICAL, not lateral, and the old note here claimed a 267px
   gap that does not reproduce. Measured at 1440x900: the mark's silhouette spans x 752..1375 and
   y 282..615, the cue sits at x 695..745 and y 755..823 — 6.9px of horizontal clearance and
   139.2px of vertical. The cue stays centred there because it clears the mark underneath it, not
   because it is anywhere to the side of it. */
@media (max-width: 900px) {
  .hero__scroll {
    left: var(--guide-x0, var(--gutter));
    transform: translateX(0) translateY(0);
    align-items: flex-start;
  }
  html.is-scrolled .hero__scroll { transform: translateX(0) translateY(10px); }
  /* MOBILE: the type block drifts to the vertical middle, which puts the mark alone in the lower
     third and the mark's exit 443px of empty field away from "01 — the studio" (measured 390x844:
     gap1_heroEnd_to_meta 443). Clamping the centre toward the guide box's upper third lifts the
     whole block, keeps the mark clear of the sub-headline (which is what the solved seat below
     depends on — the mark is seated on y1, NOT on the type band, at fd.t = 0), and shortens the
     dead run into the studio. Top-anchored via a hard max on justify-content's floor: the block
     still centres while it fits, it just cannot sink past this line. */
  .hero__inner { padding-block: min(calc(var(--bar-h) + var(--row)), 8vh) min(18vh, 140px); }
}

@media (max-height: 440px) {
  .hero__scroll {
    /* The base rule centres with translateX(-50%). This block re-anchors with `right` and left:auto
       but never reset the transform, so the cue sat half its own width off its target — 25.5px at
       1440x420. It is anchored, not centred, here. */
    transform: none;
    position: fixed;
    left: auto;
    right: calc(100% - var(--guide-x1, calc(100% - var(--gutter))) + 24px);
    bottom: 0;
    height: var(--bar-h);
    flex-direction: row;
    align-items: center;
    justify-content: flex-end;
    gap: 0;
  }
  .hero__scroll-line { display: none; }
}

/* --------------------------------------------------------------------------
   v3 — the intro, released by the gate

   js/ui/hero.js adds .is-armed in init() (which runs while the gate still
   covers the page) and .is-in when the pageReady store flips. If JS never
   runs, neither class is ever set and every element below sits in its natural,
   fully readable state — the failsafe that reveals the page leaves a complete
   hero behind it.

   Timeline, measured from the reveal finishing (v2's intro, re-based). THREE
   lines, not two: the headline was split and this block described the old
   shape long enough for line 3 to ship with no delay at all and rise first.
       300ms  eyebrow dot + eyebrow scramble        (markup: data-start="300")
       700ms  line 1 rises, 1500ms, quartic-out
       840ms  line 2 rises   (v2's 0.14s stagger)
       980ms  line 3 rises   (the same stagger again)
      1100ms  the scroll hint fades up             (markup: data-start="1200")
      1350ms  the sub fades up, 1100ms, cubic-out
      2700ms  the accent dot lands  (980 + 1500 + 220 — it sits on line 3)
   -------------------------------------------------------------------------- */
.hero.is-armed .line__in            { transform: translateY(115%); }
.hero.is-armed .hero__sub           { opacity: 0; }
.hero.is-armed .eyebrow__dot        { opacity: 0; }
.hero.is-armed .hero__scroll-label,
.hero.is-armed .hero__scroll-line   { opacity: 0; }
.hero.is-armed #hero-dot            { opacity: 0; }

/* The accent period has to be a box for a scale to apply to it at all. */
#hero-dot {
  display: inline-block;
  transform-origin: 50% 78%;
  will-change: transform, opacity;
}

@keyframes heroLineIn {
  from { transform: translateY(115%); }
  to   { transform: translateY(0); }
}
@keyframes heroFade {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes heroFadeUp {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: translateY(0); }
}
/* Quiet: it arrives small, overshoots by 7% and settles. No bounce. */
@keyframes heroDotLand {
  0%   { opacity: 0; transform: scale(0.34); }
  55%  { opacity: 1; transform: scale(1.07); }
  100% { opacity: 1; transform: scale(1); }
}

.hero.is-in .line__in {
  animation: heroLineIn 1500ms var(--ease-quart) both;
}
/* THREE lines, three delays. index.html has authored three .line elements since the headline was
   split, but only two delays were ever written here — so line 3 fell back to the shorthand's own
   0ms and rose FIRST, 700ms before line 1. Measured computed animation-delay at 1440x900 was
   ['0.7s', '0.84s', '0s']: the first thing every reader saw played backwards. 980ms keeps v2's
   0.14s stagger (700 + 2 x 140). */
.hero.is-in .line:nth-child(1) .line__in { animation-delay: 700ms; }
.hero.is-in .line:nth-child(2) .line__in { animation-delay: 840ms; }
.hero.is-in .line:nth-child(3) .line__in { animation-delay: 980ms; }

.hero.is-in .eyebrow__dot {
  animation: heroFade 420ms var(--ease-cubic) 300ms both;
}
.hero.is-in .hero__sub {
  animation: heroFadeUp 1100ms var(--ease-cubic) 1350ms both;
}
.hero.is-in .hero__scroll-label,
.hero.is-in .hero__scroll-line {
  animation: heroFade 900ms var(--ease-cubic) 1100ms both;
}
/* 220ms after ITS OWN line settles, which is the beat this was always specified as. 2560ms was
   derived from line 2 (840 + 1500 + 220) back when the dot lived on the last of two lines; it now
   lives on line 3, so it is 980 + 1500 + 220. At the old value the serif line sat fully risen and
   period-less for 1060ms. */
.hero.is-in #hero-dot {
  animation: heroDotLand 440ms var(--ease-cubic) 2700ms both;
}

/* --------------------------------------------------------------------------
   Reduced motion — every beat above is a decoration on top of a hero that is
   already complete. Turn them all off and the section reads exactly as it
   looks at rest. js/ui/hero.js skips the scroll drift on the same query.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  /* opacity first: nothing may stay hidden */
  .hero .line__in,
  .hero .hero__sub,
  .hero .eyebrow__dot,
  .hero .hero__scroll-label,
  .hero .hero__scroll-line,
  .hero #hero-dot {
    animation: none !important;
    opacity: 1 !important;
  }
  /* transform only where the intro used one — the eyebrow dot keeps base.css's
     1px optical nudge, which is alignment, not motion. */
  .hero .line__in,
  .hero .hero__sub,
  .hero #hero-dot {
    transform: none !important;
  }
  .hero__scroll-line::after {
    animation: none;
    transform: translateY(70%);
  }
  /* Under reduced motion ui/hero.js returns before addFrame(), so --hero-y / --hero-clip / --hero-op
     are never written and the type scrolls plainly under the chrome. That is fine and deliberate:
     the opaque .chrome__bar--top is the guarantee, not the clip — measured, 0px of hero ink inside
     .chrome__brand or .chrome__link across 328 element-rows at 8 viewports in exactly this state.
     Freeze the cue's transition so its snap is instantaneous. */
  .hero__scroll,
  html.is-scrolled .hero__scroll { transition: none; }
}
