/* T-AV — Stage3D home 3D narrative (home-3d branch).
   Task 3: hero wave-field canvas + 2D↔3D crossfade.
   Task 4: FIXED full-viewport canvas host, scroll pin, overlay panels. */

/* ===== Fixed canvas host (FIXED-CANVAS amendment) =====
   One continuous 3D world behind the whole home route: the kept canvas
   lives in a position:fixed host at z-index 0 in the ROOT stacking
   context. All page content that must paint above it is lifted to
   z-index 1 by the .page--stage3d rules below; nothing between the host
   and the root may create a stacking context (see .page transform note).
   pointer-events:none keeps scroll and clicks on the page. */
.stage3d-canvas-host {
  position: fixed;
  inset: 0;
  z-index: 0;
  opacity: 0;
  transition: opacity 480ms var(--ease-out);
  pointer-events: none;
  overflow: hidden;
}
.stage3d-canvas-host.is-ready { opacity: 1; }
/* Past the pin end the live canvas hides so beats 09-12 and the footer
   never sit on a 3D world (toggled by the master timeline's onUpdate at
   scrubbed progress >= 1 — reversal-safe by construction). */
.stage3d-canvas-host.is-offstage { visibility: hidden; }
.stage3d-canvas-host canvas {
  width: 100%;
  height: 100%;
  display: block;
}

/* Edge vignettes — same treatment as .home-hero__waves::before/::after so
   the 3D field fades into the abyss at the page sides. The WebGL clear
   color and --abyss are the same surface, so painting abyss over the line
   ends reads as the lines dissolving. */
.stage3d-canvas-host::before,
.stage3d-canvas-host::after {
  content: "";
  position: absolute; top: 0; bottom: 0;
  width: 18%; z-index: 2;
  pointer-events: none;
}
.stage3d-canvas-host::before {
  left: 0;
  background: linear-gradient(to right, var(--abyss) 0%, rgba(0, 0, 0, 0) 100%);
}
.stage3d-canvas-host::after {
  right: 0;
  background: linear-gradient(to left, var(--abyss) 0%, rgba(0, 0, 0, 0) 100%);
}

/* Crossfade: once the 3D field is live (data-stage3d="ready" set by
   Home3DPin on the hero section), the 2D HeroWaves canvas fades out.
   The transition lives on the base selector so removing the attribute
   (3D teardown) fades the 2D waves back in rather than snapping. */
.home-hero .home-hero__waves {
  transition: opacity 480ms var(--ease-out);
}
.home-hero[data-stage3d="ready"] .home-hero__waves {
  opacity: 0;
}

/* ===== Stacking: page content above the fixed canvas =====
   .page carries `transform: translateY(0)` (page-entry transition,
   site.css). ANY non-none transform makes .page the containing block
   for position:fixed descendants — both the canvas host and GSAP's
   fixed-position pinning would silently break. In 3D mode the transform
   is removed outright (the opacity entry fade is kept); @starting-style
   is overridden too so no transform ever exists mid-transition. */
.page--stage3d { transform: none; }
@starting-style {
  .page--stage3d { transform: none; }
}

/* Every top-level section except the pin paints above the canvas
   (z 1 vs z 0). The pin section itself must NOT create a stacking
   context — its overlays carry their own z-index in the root context.
   The nav (sticky, z 50) and story rail (fixed, z 30) already sit
   above; the footer is lifted so a late offstage flip can never bury it.
   M2: the footer rule is scoped to the 3D home only. The footer is a
   sibling of <main> (#root > header.nav + main#main + footer.footer),
   not of .page, so the scope rides on :has() — every browser that can
   run the 3D path (import maps, dynamic import, WebGL2) supports it,
   and in 2D mode the lift isn't needed anyway. */
.page--stage3d > section:not(.stage3d) {
  position: relative;
  z-index: 1;
}
main:has(.page--stage3d) ~ .footer {
  position: relative;
  z-index: 1;
}

/* ===== Pin layout =====
   The scroll length comes from the ScrollTrigger pin spacer
   (end: '+=' innerHeight × 10 desktop / 8 coarse — lengthened from the
   spec's 850vh/700vh on 2026-06-12, Rajai pacing note). The section
   itself is only as tall as the pinned
   viewport; GSAP adds the rest as spacer padding. The viewport is
   100svh minus the 64px nav offset and is pinned at 'top 64px'. */
.stage3d__viewport {
  position: relative;          /* z auto — NOT a stacking context */
  height: calc(100vh - 64px);  /* fallback for browsers without svh */
  height: calc(100svh - 64px); /* §5.3: svh, never raw 100vh */
  overflow: hidden;
}
.stage3d__overlays {
  position: absolute;
  inset: 0;
  z-index: 1;                  /* above the canvas host (root z 0) */
  pointer-events: none;
}

/* ===== Beat copy panels =====
   Carbon surface, 1px charcoal-warm border (Border-Not-Fill), radius 8,
   padding 24, body capped at 480px, alternating sides. Hidden until the
   master timeline drives them (autoAlpha); the container transform is
   static — GSAP only animates opacity/visibility on the container and
   y on the children. */
.stage3d-panel {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  left: clamp(24px, 7vw, 110px);
  width: min(480px, calc(100% - 48px));
  background: var(--carbon);
  border: 1px solid var(--charcoal-warm);
  border-radius: 8px;
  padding: 24px;
  display: flex;
  flex-direction: column;
  gap: 16px;
  opacity: 0;
  visibility: hidden;
  pointer-events: auto;
}
.stage3d-panel.is-right {
  left: auto;
  right: clamp(24px, 7vw, 110px);
}
/* The global .beat__title clamp is sized for a half-page column; inside
   a 480px panel it wraps hard. Tighter scale, same voice. */
.stage3d-panel .beat__title {
  font-size: clamp(26px, 2.6vw, 38px);
  letter-spacing: -0.6px;
}
.stage3d-panel .beat__copy { font-size: 16px; }

/* ===== Future overlay slot (loading readout T11) — permanently
   mounted, empty for now. */
.stage3d-loading {
  position: absolute;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}

/* ===== Beat-01 typewriter intake form (Task 8 — term.js) =====
   The container's opacity/visibility are driven ONLY by the term module
   (pure function of pin progress, inline style). Mono lives inside the
   .term frame — the one place the Two-Family rule allows it.
   2026-06-11 rework: anchored on the site's left text rail (the column
   the hero headline just vacated; same left clamp as the beat panels).
   The stamped form fades out as the beat-01 panel answers from the
   RIGHT — form L, beat-01 R, beat-02 L: the alternation starts with the
   form itself. Rows are collapsed until typed (term.js drives
   .is-collapsed), so the frame grows line-by-line instead of floating
   as an empty full-height crate. The ducked, dimmed wave field reads
   through the translucent carbon to its right. */
.stage3d-term {
  position: absolute;
  left: clamp(24px, 7vw, 110px);
  top: 50%;
  transform: translateY(-50%);
  width: min(380px, calc(100% - 48px));
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}
.stage3d-term__frame {
  /* translucent carbon so the wave field reads through (BeatEmpty's
     semi-transparent panel, ported) */
  background: color-mix(in oklab, var(--carbon) 82%, transparent);
}
.stage3d-term__sub {
  font-size: 10px;
  letter-spacing: 2.4px;
  text-transform: uppercase;
  color: var(--steel);
  padding-bottom: 8px;
  margin-bottom: 10px;
  border-bottom: 1px solid var(--charcoal-soft);
}
.stage3d-term__rows { position: relative; }
/* Row height 22px + bar offset 20px are mirrored in term.js (ROW_H /
   BAR_OFF) — keep in sync. Collapsed rows (not yet typed) hold the
   frame to its content; the 150ms height ease is presentation polish on
   progress-pure state (caret-blink precedent). */
.stage3d-term__row {
  position: relative;
  height: 22px;
  line-height: 22px;
  font-size: 12px;
  letter-spacing: 2px;
  color: var(--steel);
  white-space: pre; /* keep the '[ ] ' prefix spaces */
  transition: height 150ms ease, opacity 150ms ease;
}
/* Clip ONLY while collapsed: an open row must not clip (the 64px stamp
   halo and the CONCERT overshoot are positioned children of the row).
   When the stamp fires every row is open, so nothing clips the flash. */
.stage3d-term__row.is-collapsed {
  height: 0;
  opacity: 0;
  overflow: hidden;
}
.stage3d-term__row.is-live .stage3d-term__text { color: var(--parchment); }
.stage3d-term__text {
  display: inline-block;
  transform-origin: center;
  transition: color 200ms ease;
}
.is-stamped .stage3d-term__row:last-child .stage3d-term__text {
  color: var(--snow);
  font-weight: 700;
  /* Anchor the overshoot at the left edge: the [× prefix stays inside
     the frame while CONCERT expands into the room to the right. */
  transform-origin: left center;
}
.stage3d-term__caret {
  display: inline-block;
  width: 8px;
  height: 14px;
  margin-left: 2px;
  vertical-align: -2px;
  background: var(--tav-green);
  /* decorative blink (site pulse-dot vocabulary) — not scroll state */
  animation: stage3d-caret 1s steps(1) infinite;
}
@keyframes stage3d-caret { 50% { opacity: 0.15; } }
.stage3d-term__halo {
  position: absolute;
  left: 22px;
  top: 50%;
  width: 64px;
  height: 64px;
  margin: -32px 0 0 -32px;
  border-radius: 50%;
  pointer-events: none;
  opacity: 0;
  /* snow core -> teal -> transparent (BeatEmpty's empty-halo gradient),
     via tokens (One Voice) instead of hardcoded channel values */
  background: radial-gradient(circle,
    color-mix(in oklab, var(--snow) 85%, transparent) 0%,
    color-mix(in oklab, var(--tav-green) 35%, transparent) 55%,
    color-mix(in oklab, var(--tav-green) 0%, transparent) 100%);
}
.stage3d-term__tag {
  position: absolute;
  right: 0;
  top: 0;
  font-size: 10px;
  letter-spacing: 2.4px;
  color: var(--tav-green-light);
  opacity: 0;
  transform: translateX(18px);
  transition: opacity 200ms ease, transform 200ms ease;
}
.is-stamped .stage3d-term__tag { opacity: 1; transform: none; }
.stage3d-term__bar {
  position: absolute;
  left: 0;
  right: 0;
  top: 20px; /* = BAR_OFF in term.js (22px rows) */
  height: 2px;
  border-radius: 1px;
  background: var(--tav-green);
  opacity: 0;
  transition: top 200ms ease;
}
.stage3d-term__signal {
  margin-top: 12px;
  font-size: 10px;
  letter-spacing: 2.4px;
  text-transform: uppercase;
  text-align: right;
  color: var(--tav-green-light);
}

/* ===== Lock-home annotations (Task 6) =====
   The container is a transparent positioning host spanning the overlay;
   each label's opacity/visibility is driven ONLY by the annotations
   module (pure function of pin progress), its position written once per
   beat-entry/resize (never per frame). v2 vocabulary: [ dot LABEL ] in
   steel, teal dot, Funnel Display 500 11px ls 2px uppercase. */
.stage3d-annos {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.stage3d-anno {
  position: absolute;
  display: flex;
  flex-direction: column;
  gap: 3px;
  opacity: 0;
  visibility: hidden;
  font-family: var(--font-display);
  font-weight: 500;
  font-size: 11px;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--steel);
  white-space: nowrap;
}
.stage3d-anno .anno-line {
  display: flex;
  align-items: center;
  gap: 8px;
}
.stage3d-anno .anno-line::before { content: "["; }
.stage3d-anno .anno-line::after { content: "]"; }
.stage3d-anno .anno-line::before,
.stage3d-anno .anno-line::after {
  color: var(--steel);
  opacity: 0.45;
  font-weight: 400;
}
.stage3d-anno .anno-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--tav-green);
  flex: none;
}

/* ===== §F ending stack (§J/§K) =====
   backplate (z1, abyss) < photo wrap (z2, transform-only shrink) <
   frame / quiet / caption (z3). The IMG's autoAlpha is the crossfade;
   the WRAP's scale is the shrink; frame + caption rects come from
   layoutEnding() in Home3DPin (inline px, refresh-derived). §K: the quiet
   panel is now a CSS-positioned carbon card (beat-09 framed, plays before
   the photo) — layoutEnding no longer positions it. */
.stage3d-backplate {
  position: absolute;
  inset: 0;
  z-index: 1;
  background: var(--abyss);
  opacity: 0;
  visibility: hidden;
}
.stage3d-photo-wrap {
  position: absolute;
  inset: 0;
  z-index: 2;
  will-change: transform;
}
.stage3d-frame {
  position: absolute;
  z-index: 3;
  border: 1px solid var(--charcoal-warm);
  border-radius: 2px;
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
}
/* §K: beat-09 STAGE QUIET is now a FRAMED beat panel that plays BEFORE
   the photo (was §H.5 text-over-abyss docked at the plate). Same carbon
   card as .stage3d-panel — carbon surface, 1px charcoal-warm border
   (Border-Not-Fill), radius 8, padding 24. Positioned like a beat panel;
   beat 08 sits LEFT, so this alternates RIGHT, vertically centered. The
   master timeline drives autoAlpha (enter/hold/exit); the transform is
   static. layoutEnding() no longer touches it.
   NOTE: the surface properties below (background, border, border-radius,
   padding, display, flex-direction, gap) are intentionally kept in sync
   with .stage3d-panel - mirror any card-surface change there here too. */
.stage3d-quiet {
  position: absolute;
  z-index: 3;
  top: 50%;
  right: clamp(24px, 7vw, 110px);
  left: auto;
  transform: translateY(-50%);
  width: min(480px, calc(100% - 48px));
  background: var(--carbon);
  border: 1px solid var(--charcoal-warm);
  border-radius: 8px;
  padding: 24px;
  display: flex;
  flex-direction: column;
  gap: 16px;
  text-align: left;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}
.stage3d-quiet .beat__title {
  font-size: clamp(26px, 2.6vw, 38px);
  letter-spacing: -0.6px;
}
.stage3d-quiet .beat__copy {
  font-size: 16px;
  color: var(--parchment);
}
.stage3d-photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* §J truss alignment: FRAME THE PHOTO, never the camera. The render is
     held at a static FOH vantage (its top truss sits ~35% down at
     1440x860); this 1600x1200 photo overflows the 1.67:1 viewport
     vertically under object-fit:cover, so shifting object-position-y
     above 50% raises the photo content until its top truss lines up with
     the render's during the crossfade (judged at wrap scale 1, where the
     §F shrink has not yet bitten). 58% landed the match; object-position
     is independent of the wrap scale, so the §F shrink composes cleanly.
     Approximate on other aspects (the render reframes per aspect too). */
  object-position: 50% 58%;
  opacity: 0;
  visibility: hidden;
}
.stage3d-caption {
  position: absolute;
  left: clamp(24px, 5vw, 64px);
  bottom: clamp(24px, 6vh, 56px);
  z-index: 3;
  display: flex;
  flex-direction: column;
  gap: 8px;
  opacity: 0;
  visibility: hidden;
}
.stage3d-caption__label {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: 12px;
  letter-spacing: 2.2px;
  text-transform: uppercase;
  color: var(--snow);
}
/* §K: closing line in the BEATS' font — white title + teal accent, the
   same white/teal split as the beat panel titles (.beat__title +
   .accent). Two spans: "The same stage." snow, "As built." tav-green. */
.stage3d-caption__line {
  font-family: var(--font-display);
  font-weight: 300;
  font-size: clamp(18px, 1.6vw, 24px);
  letter-spacing: -0.4px;
  color: var(--snow);
}
.stage3d-caption__line .accent {
  color: var(--tav-green);
}

/* ===== Small screens: panels anchor to the bottom, full width minus
   gutters, so the 3D scene keeps the upper two thirds. ===== */
@media (max-width: 720px) {
  /* §K: the beat-09 quiet card reflows like every other beat panel. */
  .stage3d-panel,
  .stage3d-panel.is-right,
  .stage3d-quiet {
    left: 20px;
    right: 20px;
    width: auto;
    max-width: 480px;
    top: auto;
    bottom: 7vh;
    transform: none;
  }
  .stage3d-panel,
  .stage3d-quiet { padding: 20px; }
  .stage3d-panel .beat__title,
  .stage3d-quiet .beat__title { font-size: 24px; }
  .stage3d-panel .beat__copy,
  .stage3d-quiet .beat__copy { font-size: 15px; }
}
