/* ============================================================
   MODULE "hero" - the opening scene of the page.

   TWO STATES, ONE DOM.

   1. AT REST (no JS, no GSAP, reduced motion, or a viewport too short):
      the hero is exactly the two column block it has always been. The
      portrait is a plain <img> in its annotated frame, the copy is in
      flow, nothing is pinned and nothing here applies: every rule of
      the scene is scoped under `.hero.is-live`, a class only
      js/hero.js sets, and the elements that belong to the scene alone
      are display:none until it does.

   2. THE SCENE (.hero.is-live): the portrait opens the page at full
      height with the name beside it, the pointer opens a liquid hole
      in it onto the same portrait rendered live in ASCII (WebGL,
      js/hero.js), and the scroll zooms the photo back into its
      drawing frame while two giant lines cross behind it and the
      signature writes itself over the picture. Everything is driven by
      one scroll progress value, so every intermediate state is valid
      and the whole scene is reversible.

   Owned by this module: section#home, the "Hero" and "portrait"
   banners of styles.css, the hero rules of css/shell.css.
   ============================================================ */

.hero-stage { position: relative; }

/* Only the scene uses these. Out of the scene they do not exist at all,
   so the no-JS page has no stray decorations. */
.hero-field,
.hero-band,
.hero-intro,
.hero-sign,
.hero-fx { display: none; }

/* ------------------------------------------------------------
   1. The pinned panel
   ------------------------------------------------------------ */

.hero.is-live {
  /* The blueprint paper moves to the panel: the pin spacer under it is
     two and a half screens tall and would have stretched the gradient
     of styles.css over all of it. */
  background: var(--paper);
  /* The giant lines run past both edges; the page must not scroll
     sideways because of them. */
  overflow: clip;
}

/* The reserved box, set by the inline script of section#home while the
   page parses, so the pinned panel already has its final height on the
   first paint and nothing below the hero is painted then pushed out.
   Exactly the same box as the live scene, nothing else. */
html.hero-armed .hero-stage,
.hero.is-live .hero-stage {
  /* Height of the page chrome above the panel. The sticky bar is gone, so
     the scene owns the whole screen; the variable is kept because the
     composition below is written against it. */
  --fp-top: 0px;
  --fp-vis: calc(100svh - var(--fp-top));
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-height: calc(100vh - var(--fp-top));
  min-height: calc(100svh - var(--fp-top));
}

.hero.is-live .hero-stage {
  overflow: clip;
  background:
    linear-gradient(to bottom, transparent, var(--paper) 94%),
    repeating-linear-gradient(0deg, color-mix(in srgb, var(--blue-deep) 7%, transparent) 0 1px, transparent 1px 56px),
    repeating-linear-gradient(90deg, color-mix(in srgb, var(--blue-deep) 7%, transparent) 0 1px, transparent 1px 56px),
    var(--paper);
}

.hero.is-live .hero-inner {
  position: relative;
  z-index: 2;
  width: 100%;
  padding-top: clamp(1.5rem, 4vh, 3rem);
  padding-bottom: clamp(1.5rem, 4vh, 3rem);
}

/* The copy is the END of the scene. It is held at zero by the module
   from the first frame; this is only the state before the first
   callback runs, so nothing of the final layout ever flashes. */
.hero.is-live .hero-copy { opacity: 0; }

.hero.is-live .hero-copy > * { will-change: transform, opacity; }

/* The arrival of the copy (js/hero.js, section 3b). The layer holds
   aria-hidden copies laid exactly on the measured lines of the real
   text: the brass outline of the title, the rising lines of the
   paragraph and of the values, and the mono cursor of the struck
   labels. Moved by transforms, opacity and clip-path only. */
.hero.is-live .hero-copy { position: relative; }

.hero-arrive { display: none; }

.hero.is-live .hero-arrive {
  display: block;
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  user-select: none;
  -webkit-user-select: none;
}

/* The real title sits over its outline: the ink paints over the drawing,
   which shows a thin brass edge around freshly inked letters until it
   is cut half an em behind the ink. */
.hero.is-live .hero-copy h1 { position: relative; z-index: 4; }

.hero-copy-of {
  position: absolute;
  margin: 0;
}

.hero-row {
  position: absolute;
  white-space: pre;
}

/* The drawing of the title: outline only, the stroke of the big years
   of the Experience section. The face is variable and its letters are
   built of overlapping contours (the bar of the e, the ear of the g): a
   bare stroke would draw them as lines through the words. So the stroke
   is twice as wide and painted UNDER a paper fill: its inner half and
   every inner contour disappear, the outer 1 px is the drawing. */
.hero-copy-of.is-plan,
.hero-copy-of.is-plan span {
  color: var(--paper) !important;
  -webkit-text-stroke: 2px var(--brass);
  paint-order: stroke fill;
}

/* Each rising line is its own mask. */
.hero-copy-of.is-rise .hero-row { overflow: hidden; overflow: clip; }

.hero-row-in { display: block; }

.hero-copy-of.is-rise .hero-row-in {
  transform: translate3d(0, 102%, 0);
}

/* A mono block cursor, 10 px drawn and scaled to the label. */
.hero-caret {
  position: absolute;
  left: 0;
  top: 0;
  width: 10px;
  height: 10px;
  transform-origin: 0 0;
  opacity: 0;
}

.hero-caret span {
  display: block;
  width: 100%;
  height: 100%;
  background: var(--brass);
}

/* It only blinks while it is shown: an endless animation on a hidden
   cursor still costs a frame every frame (measured on a throttled phone). */
.hero-caret.is-on span { animation: hero-caret-blink 0.9s steps(1, end) infinite; }

@keyframes hero-caret-blink {
  50% { opacity: 0; }
}

.hero.is-live .hero-portrait { will-change: transform; }

.hero.is-live .portrait-frame {
  transform-origin: 50% 50%;
  will-change: transform;
}

/* The drawing annotations ink themselves as the photo lands in the
   frame, so they start blank. */
.hero.is-live .measure i { transform: scaleX(0); }
.hero.is-live .measure-right i { transform: scaleY(0); }
.hero.is-live .measure b { opacity: 0; }
.hero.is-live .cartouche { opacity: 0; }

.hero.is-live .scroll-hint {
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(100% - var(--fp-vis) + clamp(0.75rem, 2.5vh, 1.5rem));
  z-index: 3;
  padding-bottom: 0;
  pointer-events: none;
}

/* ------------------------------------------------------------
   2. The opening state: the name beside the portrait
   ------------------------------------------------------------ */

.hero.is-live .hero-intro {
  display: block;
  position: absolute;
  left: 0;
  right: 0;
  top: calc(var(--fp-vis) * 0.5);
  z-index: 1;
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 0 clamp(1.25rem, 4vw, 3rem);
  transform: translateY(-50%);
  pointer-events: none;
}

.hero-intro-name span {
  display: block;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(3rem, 9.4vw, 8.2rem);
  line-height: 0.86;
  letter-spacing: -0.045em;
  color: var(--ink);
  will-change: transform, opacity;
}

.hero-intro-name span + span { color: var(--blue-deep); }

.hero-intro-role {
  margin-top: clamp(1rem, 2.2vh, 1.6rem);
  padding-top: clamp(0.7rem, 1.6vh, 1.1rem);
  border-top: 1px solid var(--line);
  max-width: 25em;
  font-family: var(--font-mono);
  font-size: clamp(0.64rem, 0.85vw, 0.78rem);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-soft);
  will-change: transform, opacity;
}

/* ------------------------------------------------------------
   3. The two giant lines that cross behind the portrait
   ------------------------------------------------------------ */

.hero.is-live .hero-band {
  display: block;
  position: absolute;
  left: 0;
  top: 0;
  z-index: 1;
  margin: 0;
  white-space: nowrap;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(4rem, 14.5vw, 13rem);
  line-height: 0.82;
  letter-spacing: -0.045em;
  text-transform: uppercase;
  opacity: 0;
  will-change: transform, opacity;
  pointer-events: none;
}

.hero-band span { display: block; }

.hero-band-a { color: var(--ink); }
.hero-band-b { color: var(--blue-deep); }

/* ------------------------------------------------------------
   4. Contour lines drifting under everything
   ------------------------------------------------------------ */

.hero.is-live .hero-field {
  display: block;
  position: absolute;
  inset: -12% -18%;
  z-index: 0;
  color: var(--blue-deep);
  opacity: 0;
  pointer-events: none;
  will-change: transform, opacity;
}

.hero-field svg { width: 100%; height: 100%; display: block; }

/* ------------------------------------------------------------
   5. The WebGL layer over the photo
   ------------------------------------------------------------ */

/* The <img> is never hidden: the canvas is painted on top of it and
   only fades in once it has a first frame, so there is no flash and no
   layout shift, and a lost context simply uncovers the photo again. */
.hero.is-live .hero-fx {
  display: block;
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border-radius: var(--radius);
  opacity: 0;
  transition: opacity 0.45s ease-out;
  pointer-events: none;
}

.hero.is-live .hero-fx.is-on { opacity: 1; }

/* The reveal follows a finger without ever taking the vertical scroll. */
.hero.is-live .hero-portrait { touch-action: pan-y; }

/* ------------------------------------------------------------
   6. The signature
   ------------------------------------------------------------ */

.hero.is-live .hero-sign {
  display: block;
  position: absolute;
  left: -16%;
  top: 33%;
  width: 122%;
  height: auto;
  z-index: 4;
  overflow: visible;
  pointer-events: none;
  transform: rotate(-5.5deg);
}

.hero-sign-ink { fill: var(--brass); }

/* The word crosses both the light background of the photo and the dark
   jacket. A stroked copy of the same outline, under the lettering and
   inside the same mask, keeps it readable on both without a filter. */
.hero-sign-halo {
  fill: none;
  stroke: rgba(244, 247, 249, 0.92);
  stroke-width: 18;
  stroke-linejoin: round;
}

/* ------------------------------------------------------------
   7. (free) - the topbar over the opening state
   ------------------------------------------------------------
   html.hero-top used to make the sticky bar transparent over the big
   portrait and html.hero-live gave it the fade back to paper. The bar has
   been removed from the site, so both rules went with it, and js/hero.js
   no longer sets .hero-top. html.hero-live is still set by app.js and read
   by js/hero.js, so it stays. */

/* ------------------------------------------------------------
   8. Responsive
   ------------------------------------------------------------ */

@media (max-width: 900px) {
  /* One column: the portrait opens the screen and the name sits under
     it, over the bottom of the picture. */
  .hero.is-live .hero-intro {
    top: auto;
    bottom: calc(100% - var(--fp-vis) + clamp(3.2rem, 8vh, 5rem));
    transform: none;
  }

  /* A smaller finished portrait on a phone: it leaves room for the copy
     and gives the zoom out something to travel. */
  .hero.is-live .hero-portrait { max-width: 290px; }

  /* The cartouche follows the narrower frame: four cells still fit
     without "FP-26" breaking in two. */
  .hero.is-live .cartouche span { padding: 0.45rem 0.5rem; font-size: 0.7rem; }
  .hero.is-live .cartouche b { font-size: 0.55rem; letter-spacing: 0.06em; }

  .hero-intro-name span { font-size: clamp(2.6rem, 13vw, 4.4rem); }

  .hero-intro-role {
    font-size: 0.62rem;
    letter-spacing: 0.12em;
    max-width: 22em;
  }

  .hero.is-live .hero-band { font-size: clamp(3rem, 17vw, 7rem); }

  .hero.is-live .hero-sign { left: -6%; width: 112%; }
}

@media (max-width: 640px) {
  .hero.is-live .hero-inner { padding-top: 1.75rem; padding-bottom: 1rem; }
}

/* ------------------------------------------------------------
   9. Fallbacks
   ------------------------------------------------------------
   Belt and braces: even if .is-live were set by accident, no motion
   means no hidden state. js/hero.js already refuses to run here. */

@media (prefers-reduced-motion: reduce) {
  .hero.is-live .hero-copy { opacity: 1; }
  .hero.is-live .measure i { transform: none; }
  .hero.is-live .measure b { opacity: 1; }
  .hero.is-live .cartouche { opacity: 1; }
  .hero.is-live .hero-intro,
  .hero.is-live .hero-band,
  .hero.is-live .hero-field,
  .hero.is-live .hero-sign,
  .hero.is-live .hero-fx { display: none; }
}
