/* ==========================================================================
   OREGON — css/hero.css
   Phase P3, re-sequenced in P9 (GOAL.md §4 P9-A) — hero knockout sequence.
   Loaded after site.css, so it may override the static hero rules that
   shipped in P1.

   All structural elements referenced below (.hero__card, .hero__vignette,
   .hero__flood, .hero__line-clip, .hero__line, .hero__mask-*) are injected
   at runtime by js/hero.js — index.html itself is left untouched. That
   keeps this phase's footprint entirely inside these two owned files and
   means the effect degrades to the plain P1 static hero if JS is
   unavailable (no classes ever get added, so none of these rules match).

   P9 handoff: js/hero.js now pins the hero WITHOUT pin spacing and fades
   the whole section to opacity 0 in the scrub's last ~12%, revealing
   #about scrolling up in place underneath (see js/hero.js's pin-contract
   comment for the full geometry). #about is normal, non-fixed flow
   content that sits directly behind the pinned hero in the DOM — without
   an explicit stacking order it would paint OVER the still-opaque hero by
   plain source order, so #hero needs a z-index above it (about.css is
   owned by a different P9 agent and is not touched here).
   ========================================================================== */

/* ---------------------------------------------------------------------- */
/* P9 handoff — stack the hero above #about (see the file header above).
   `.slide` already gives every section `position: relative`, so a plain
   z-index here is enough to win against #about's default z-index:auto,
   regardless of DOM order or the pin's own position:fixed while active. */
/* ---------------------------------------------------------------------- */
#hero.hero { z-index: 5; }

/* ---------------------------------------------------------------------- */
/* Kill the ambient CSS Ken Burns from site.css — js/hero.js drives the
   still's drift via GSAP instead (load-in ambient tween, then handed off
   to the scrubbed parallax tween so the two never fight over `transform`). */
/* ---------------------------------------------------------------------- */
.hero__still { animation: none; }

/* ---------------------------------------------------------------------- */
/* Card wrapper — phase 2 "shrinks into a rounded-corner card"             */
/* Wraps the existing .hero__media (image + scrim) so both travel together */
/* as one shrinking frame; the image inside gets its own counter-scale for */
/* the parallax "translates slower than the card" read.                   */
/* ---------------------------------------------------------------------- */
.hero__card {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  border-radius: 0px;
  transform-origin: 50% 50%;
  will-change: transform, border-radius;
}
.hero__card > .hero__media { position: absolute; inset: 0; }

/* ---------------------------------------------------------------------- */
/* Darken-to-ink vignette (phase 2) + full flood (phase 2→3 handoff)       */
/* ---------------------------------------------------------------------- */
.hero__vignette,
.hero__flood {
  position: absolute;
  inset: 0;
  z-index: 1;
  opacity: 0;
  pointer-events: none;
  will-change: opacity;
}
.hero__vignette {
  background: radial-gradient(120% 120% at 50% 50%,
    rgba(14, 15, 17, 0) 38%,
    rgba(14, 15, 17, 0.92) 100%);
}
.hero__flood { background: var(--ink); }

.hero__content { z-index: 2; }
.scroll-cue { will-change: opacity; }

/* ---------------------------------------------------------------------- */
/* Load-in clip masks — phase 1 "two headline lines rise from behind clip  */
/* masks". Injected wrapper per line; overflow released once settled so    */
/* later phases can scale the line past its own resting box.              */
/* ---------------------------------------------------------------------- */
/* P1's .hero__content .slide__inner ships a 36rem max-width tuned for
   wrapped body copy, not a single-line display headline — at hero type
   sizes "FIFTY YEARS" doesn't fit that column and wraps internally,
   which would garble every technique below. Widen it and force each
   headline row to a single line, scoped to the hero only. */
.hero__content .slide__inner { max-width: none; }

.hero__line-clip {
  display: block;
  overflow: hidden;
  max-width: 100%;
  /* P10 root-cause fix (GOAL.md P10 item G, "ON THE SKYLINE" rendering
     as "ON THE SKYI"): at the doubled rest font-size .hero__line's own
     natural nowrap width can exceed this clip's box (itself capped by
     max-width:100% once "ON THE SKYLINE" no longer fits the ~1792px
     column at 1920px). Confirmed via isolated repro that a too-wide
     inline-block child overflows toward the END of the inline direction
     regardless of text-align:right — in the default ltr context that's
     the RIGHT edge, i.e. exactly backwards from what the right-anchored
     design needs, and precisely why letters were vanishing/cropping off
     the right (invisible past the knockout photo's cover bounds, or
     literally past the viewport). Flipping this clip's own direction to
     rtl makes its single inline-block child start from the RIGHT edge
     instead, so any overflow bleeds LEFT — matching "grows/overflows
     leftward, never crops the right" everywhere else in this file. Does
     not affect the real text order (see .hero__line below). */
  direction: rtl;
}
.hero__line-clip.is-settled { overflow: visible; }

.hero__line {
  display: inline-block;
  position: relative;
  color: transparent;
  white-space: nowrap;
  /* Independent bidi context, forced back to ltr — the clip ancestor's
     direction:rtl (above) only exists to flip THIS element's overflow
     side; the actual English text inside must still render in normal
     left-to-right reading order. unicode-bidi:isolate stops the rtl
     context from reordering/mirroring anything inside. */
  direction: ltr;
  unicode-bidi: isolate;
  /* P8 (Tan, 2026-09-04): "I feel like this could be bigger" / red-window
     test — the two-line block must fill Tan's ~45vw × 38vh box. This
     clamp lands "ON THE SKYLINE" at ~119px/54vw at 1920×1080 (well past
     the ≥45vw / ≥864px target) and ~89px at 1440×900, while its measured
     left edge still sits right of the Carlton tower's right edge
     (x≈46vw) because the line stays right-aligned against the same
     --margin the tower-clearance math was checked against. The 2.4rem
     floor is what actually governs at ≤600px-ish viewports (the vw term
     alone would fall under it) and is still 2.1× the old 1.6rem floor,
     comfortably clear of the 18px type floor everywhere. */
  font-size: clamp(1.85rem, 4.4vw + 0.1rem, 5.4rem);
  line-height: 0.95;
  will-change: transform, letter-spacing;
}
/* P10 round 2 (PM, mobile-only): at the old ~29.6px/390 rest size the
   knockout — even fully swapped in and correctly textured — reads as
   small mottled grey type with no "sketch effect" (Tan's complaint
   stands per p10/zoom_hero_390.png), and the width-limited grow (P10-B
   item 2) only reaches ~1.0-1.1x, so nothing visibly changes on scroll
   either. Fix: let each line WRAP instead of forcing one nowrap row —
   "FIFTY YEARS" / "ON THE SKYLINE" become four right-aligned rows
   ("FIFTY" / "YEARS" / "ON THE" / "SKYLINE") at a much bigger size, ~12vw
   (measured: 46.8px/390, 45px/375 — both inside the target band; verified
   "SKYLINE", the longest unbreakable word, still fits inside the padded
   column at 375 even at the load-in's peak 0.04em letter-spacing — see
   hero.js's computeGrowGeometry, unaffected by this since it already
   measures the live .hero__line-clip box rather than assuming a row
   count). white-space:normal replaces the base rule's nowrap; wrapping
   is real layout (not a scroll-driven effect), so it's identical at
   rest, mid-grow and in the prefers-reduced-motion static fallback.
   text-align:right already inherits down from .hero__content
   .slide__inner (site.css), so each wrapped row right-aligns on its own
   — no extra rule needed for that. Supersedes the old sub-360px 1.5rem
   floor (that viewport is well inside this same 12vw treatment now).
   buildSvgMask()'s sync() was rewritten to detect however many visual
   rows each line currently wraps to (Range.getClientRects() per word,
   grouped by shared top) instead of assuming one <text> per .hero__line
   — works unchanged for both this wrapped mobile layout and desktop's
   single-row one. */
@media (max-width: 899px) {
  .hero__line {
    font-size: clamp(2.4rem, 12vw, 4.6rem);
    white-space: normal;
  }
}
/* P10 (Tan): "double the size of the current on desktop" — the mobile
   rest size above is untouched (P10-B doesn't ask for it, and Tan's
   mobile frames read fine at rest; the complaint there is the grow/
   knockout timing, handled in hero.js), but ≥900px gets exactly 2x the
   old clamp's coefficients (same breakpoint hero.js's own
   ScrollTrigger.matchMedia already splits mobile/desktop on), landing
   ~161px/1920, ~121px/1440, ~115px/1366 (PM trimmed 8.8vw → 8.2vw: at
   172px "ON THE SKYLINE" ran off the left margin at 1920) — inside Tan's 150-175px
   target band and ~2x the P9 value at each width. computeGrowGeometry()
   in hero.js re-measures this live, so the knockout/grow geometry never
   needs a matching manual re-tune here. */
@media (min-width: 900px) {
  .hero__line { font-size: clamp(3.7rem, 8.2vw + 0.2rem, 11rem); }
}

/* Decorative solid-colour duplicate of the line text — a real element,
   but aria-hidden and not part of the accessible name computation; the
   real, selectable, screen-reader-readable text is the .hero__line span
   itself (see below, it stays present under this at all times). This
   decoy is what reads as ordinary off-white type in phases 1–2; it
   crossfades out as knockout mode swaps in, giving a smooth scrub-driven
   transition instead of a pop. Plain opacity tween, no CSS custom
   properties — keeps the GSAP timeline predictable. */
.hero__line-decoy {
  position: absolute;
  inset: 0;
  color: var(--offwhite);
  /* -webkit-text-fill-color inherits (unlike most paint properties), so
     without this the decoy silently inherits `transparent` from its
     .hero--bgclip ancestor below and never actually paints — explicitly
     re-declare it here so the decoy is genuinely opaque solid type. */
  -webkit-text-fill-color: var(--offwhite);
  pointer-events: none;
  /* P5 fix: aria-hidden already keeps this duplicate text node out of the
     computed accessible name (verified: the <h1>'s accessible name is the
     clean "FIFTY YEARS ON THE SKYLINE", not doubled), but text SELECTION
     doesn't consult the accessibility tree — it walks the raw DOM, and
     this decoy sits as a real second text node inside the same .hero__line
     the visible text lives in. Without this, dragging a selection over the
     headline (or Select All / copy) grabs both copies and pastes
     "FIFTY YEARSFIFTY YEARS ON THE SKYLINEON THE SKYLINE". Excluding it
     from selection leaves only the real .hero__line text selectable/copyable.
     Supported in all evergreen browsers (Safari/Chrome/Firefox). */
  -webkit-user-select: none;
  user-select: none;
}

/* Fallback-of-fallback / no-knockout-support mode: the decoy simply never
   fades (js/hero.js skips that tween when hero--solid is active), so the
   headline always reads as ordinary solid off-white type — "must never
   render as invisible text". The real .hero__line text underneath is
   also given the offwhite colour back in that mode (see .hero--solid
   below) so selecting/copying the text still shows something sensible. */

/* ---------------------------------------------------------------------- */
/* Knockout technique 1 (primary): background-clip: text                  */
/* Applied per line (not the whole <h1>) — the H1 is a block-level flex    */
/* container that spans the full available width, but each right-aligned  */
/* .hero__line inline-block hugs its own text tightly, so it's the only   */
/* reliable origin for background-position math. background-size/position */
/* are computed in JS to exactly match the full-bleed photo's own         */
/* object-fit: cover crop, so what shows through the letters is never     */
/* zoomed past what was already on screen — keeps the 1200×676 source     */
/* crisp per GOAL.md. --kx-drift-x lives on the shared <h1> ancestor and   */
/* inherits into both lines' calc().                                      */
/* ---------------------------------------------------------------------- */
.hero__headline {
  position: relative;
  /* P9: js/hero.js's computeGrowGeometry() sets an explicit px
     transform-origin on this element's inline style before the grow ever
     starts, derived from the live measured headline box vs. the live
     viewport so the block ends up clipped by all four viewport edges
     (GOAL.md §4 P9-A item 3) instead of the old fixed 100% 50% (right-
     edge-anchored), which grew the word hard off the LEFT of the screen —
     Tan's "huge cropped" frame 12 complaint. This declaration is only the
     no-JS/pre-JS fallback (harmless: without JS there's no pin/scrub/scale
     at all, so no transform-origin is ever actually exercised). */
  transform-origin: 100% 50%;
  --kx-drift-x: 0px;
  /* --kx-bg-y-keep blends each line's background-position-y between its
     realistic object-fit:cover crop (1 — matches what's truly behind the
     letters, for continuity while the solid decoy fades away in phase 2)
     and a fixed bright target row (0 — the sunset sky/lit-tower band,
     independent of where the line sits on screen). js/hero.js drives this
     from 1→0 once the decoy is gone and the flood is opaque, so phase 3's
     knockout is filled with bright sky instead of the dark rooftops that
     happen to sit at the headline's rest position. See DEFECT 2 fix. */
  --kx-bg-y-keep: 1;
  --kx-bg-y-target: 0px;
  /* P10 (Tan, mobile): "the background effect ... doesn't look like it
     has any sketch effect" — on a narrow/tall viewport object-fit:cover
     zooms in hard (see updateBgFit's HERO_IMG_ASPECT math) and, left at
     its default centred crop, the visible sliver lands on the dark
     tower body instead of the lit sunset sky, so the knockout reads as
     a flat dark smear no matter how bright the filter below is. Mirrors
     --kx-bg-y-keep/-target's job but on the X axis: js/hero.js blends
     from the realistic per-line crop to a fixed bright column of the
     source photo. Desktop never animates --kx-bg-x-keep (stays 1), so
     this is a no-op there — the existing, Tan-approved 0.20 frame is
     unchanged. */
  --kx-bg-x-keep: 1;
  --kx-bg-x-target: 0px;
}
.hero__line {
  --kx-bg-x: 0px;
  --kx-bg-y: 0px;
}
.hero--bgclip .hero__line {
  /* P5 perf fix: this used to hardcode the .jpg, so — on top of the
     <picture>'s own webp fetch for .hero__still — every visitor whose
     browser reaches bgclip mode (the overwhelming majority; it's the
     primary technique, feature-detected first) downloaded the full-size
     hero photo TWICE, once per format, competing for bandwidth on the
     LCP path. image-set() picks the ~20KB-lighter webp (matches the
     dedup already done for the OREGON knockout in css/build.css);
     browsers without image-set() support fall back to the plain url()
     declared first, i.e. today's unchanged behaviour. */
  background-image: url("../assets/img/hero-tower.jpg");
  background-image: image-set(
    url("../assets/img/hero-tower.webp") type("image/webp") 1x,
    url("../assets/img/hero-tower.jpg") type("image/jpeg") 1x
  );
  background-repeat: no-repeat;
  background-size: var(--kx-bg-size, cover);
  background-position:
    calc((var(--kx-bg-x) + var(--kx-drift-x)) * var(--kx-bg-x-keep) + var(--kx-bg-x-target) * (1 - var(--kx-bg-x-keep)))
    calc(var(--kx-bg-y) * var(--kx-bg-y-keep) + var(--kx-bg-y-target) * (1 - var(--kx-bg-y-keep)));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
  /* Lift + punch the photo fragment showing through the letters — a
     straight 1:1 sample of the source photo (however bright the crop)
     reads flat/muddy at the modest phase-3 scale; this keeps it a clean,
     unmistakable "sunset through glass" rather than dark grey-on-black. */
  filter: brightness(1.18) contrast(1.08) saturate(1.3);
}
/* P10 (Tan, mobile): at the tiny rest glyph size the same filter above
   reads as a faint smear (fine photo detail collapses to noise at ~30px
   letters) — punch it harder specifically ≤899px, on top of the X/Y
   bright-band retarget in js/hero.js, so the swap-in is unmistakably
   "photo through the letters" by the time it finishes (~0.11-0.13,
   see setupScrub's mobile timings), not just brighter but flatter. */
@media (max-width: 899px) {
  .hero--bgclip .hero__line {
    filter: brightness(1.42) contrast(1.32) saturate(1.55);
  }
}

/* ---------------------------------------------------------------------- */
/* Knockout technique 2 (fallback): SVG mask                              */
/* The real <h1> stays transparent-but-present (real, selectable, SR text)*/
/* while a separate decorative, aria-hidden image layer is clipped to a   */
/* duplicate <text> shape in an injected SVG <mask> — Safari/older        */
/* browsers that lack background-clip:text still get the letters-show-    */
/* through-image effect.                                                  */
/* ---------------------------------------------------------------------- */
.hero--svgmask .hero__headline { color: transparent; }

.hero__mask-defs { position: absolute; width: 0; height: 0; overflow: hidden; }

.hero__mask-media {
  position: absolute;
  inset: 0;
  z-index: 2;
  overflow: hidden;
  opacity: 0;
  -webkit-mask-image: url(#hero-knockout-mask);
  mask-image: url(#hero-knockout-mask);
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  will-change: opacity;
}
.hero__mask-media {
  /* Mirrors --kx-bg-y-keep's job for the bgclip technique above: js/hero.js
     tweens this from a realistic 50% (matches the real full-bleed crop)
     to a low value that pulls a bright sky/tower row into the masked
     letters for phase 3+, instead of leaving whatever's realistically at
     the headline's screen position (the dark rooftops) showing through. */
  --mask-obj-y: 50%;
  /* P10 (Tan, mobile): same fix as --kx-bg-x-target above, for browsers on
     the SVG-mask fallback path — js/hero.js only tweens this off 50% on
     mobile, moving the crop's centre toward the bright sunset-sky side of
     the source photo instead of the default centred (dark tower) sliver. */
  --mask-obj-x: 50%;
}
.hero__mask-media img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--mask-obj-x) var(--mask-obj-y);
  filter: brightness(1.18) contrast(1.08) saturate(1.3);
}
/* P10 (Tan, mobile) — same reasoning as the bgclip filter bump above. */
@media (max-width: 899px) {
  .hero__mask-media img {
    filter: brightness(1.42) contrast(1.32) saturate(1.55);
  }
}

/* ---------------------------------------------------------------------- */
/* Sub-line — fades in slightly after the headline during load-in.        */
/* Overrides site.css's small steel-grey mono line: at rest (scroll 0) it */
/* sits low over the busy rooftops on the right of the frame, so P1's low */
/* -contrast treatment all but disappears against the photo. Lift it to a */
/* near-offwhite with a soft, edgeless dark halo behind the glyphs only   */
/* (no hard-edged scrim box — stays "understated, not shouting") so it    */
/* reads clearly over any part of the skyline.                            */
/* ---------------------------------------------------------------------- */
.hero__sub {
  position: relative;
  will-change: opacity, transform;
  color: rgba(245, 244, 242, 0.92);
  /* P8: was clamp(0.8rem,...,0.9rem) → 14.4px at 1920, under the 18px
     type floor. site.css already sets this line to var(--fs-mono) (the
     tokens.css type-floor scale, ~20px at 1920), but this file loads
     after site.css and had been silently overriding it smaller — use
     the token explicitly here so the floor holds by construction and the
     intent survives the next edit to either file. */
  font-size: var(--fs-mono);
  letter-spacing: 0.03em;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.85), 0 4px 18px rgba(0, 0, 0, 0.7);
}
.hero__sub::before {
  content: "";
  position: absolute;
  inset: -0.65em -1.1em;
  z-index: -1;
  background: radial-gradient(ellipse at center, rgba(14, 15, 17, 0.55) 0%, rgba(14, 15, 17, 0.28) 55%, rgba(14, 15, 17, 0) 85%);
  pointer-events: none;
}

/* ---------------------------------------------------------------------- */
/* Reduced motion — belt & braces. js/hero.js already skips all DOM        */
/* augmentation and GSAP work under prefers-reduced-motion, so none of the */
/* rules above ever match; this just guarantees a clean static state if a  */
/* class were ever left behind mid-toggle (e.g. OS-level motion changes    */
/* without a reload).                                                     */
/* ---------------------------------------------------------------------- */
.reduced-motion .hero__card { transform: none !important; border-radius: 0 !important; }
.reduced-motion .hero__vignette,
.reduced-motion .hero__flood,
.reduced-motion .hero__mask-media { opacity: 0 !important; }
.reduced-motion .hero__line,
.reduced-motion .hero__line::before { transform: none !important; letter-spacing: 0 !important; opacity: 1 !important; }
.reduced-motion .hero--bgclip .hero__headline,
.reduced-motion .hero--svgmask .hero__headline {
  background-image: none;
  color: var(--offwhite);
  -webkit-text-fill-color: var(--offwhite);
}
