/* ============ PAGE TRANSITIONS — TWO REGISTERS, ONE SYSTEM (pagetrans.css) ============

   Everything here runs on SNAPSHOTS, never on live DOM. The mechanism is the browser's own
   view-transition capture: at the moment a navigation begins the outgoing view is rasterised
   into a bitmap, the real DOM swaps to the destination underneath it instantly, and the whole
   transition plays on flat composited images (::view-transition-old / -new). Blur is therefore
   applied to a bitmap, not to a live subtree — which is what makes it affordable.

   REGISTER 1 — THE LANDSCAPE GLIDE (1500ms)
   Intro <-> Work scenes (same document, Landing Scene.dc.html -> PageGlide.scene) and the
   case-study opens (cross-document). Outgoing drifts up with its tilt, softens, and holds
   opaque to 70% before dissolving — the incoming is revealed at ~70% of the outgoing's
   timeline, so the two scenes hand over in overlap, never back-to-back; incoming rises from
   below with depth, sharpens, and settles on a long ease-out tail. All easings ride the
   shared motion tokens (nothing in the glide runs linear or default ease): the travel —
   outgoing exit and incoming approach, ONE camera gesture — on --ease-in-out-flow (exported
   as --pg-ease so the rail dot arrives on the page's own curve), the incoming's settle tail
   on --ease-out-quint (an entrance). Same keyframes for both, because a same-document
   capture and a cross-document capture are the same two pseudo-elements. Reduced motion:
   the same-document scene change is a simple 200ms crossfade; cross-document keeps 250ms.

   REGISTER 2 — THE YELLOW CURTAIN REVEAL (ONE wipe: yellow only, reveal only —
   readiness-gated hold, 100ms floor, 800ms HARD cap / 950ms reveal arc on
   --dur-curtain-hold / --dur-curtain-out and --ease-in-out-flow)
   Any navigation where either end is a utility page (Expertise, Kind words, About me, Get in
   touch). It is NOT a view transition, and the old ink/dark cover phase is DELETED (its code
   paths removed): a curtain navigation navigates IMMEDIATELY on the click's own frame, and
   the ARRIVING document paints one solid #F1BE48 cloth panel at frame 1 — the yellow curtain
   IS the covered state; nothing dark ever displays at any point in any transition. The panel
   is pinned at a top viewport corner, holds covered until the document is ready — never past
   the cap — then swings its wavy cloth edge out on the pinned arc (172° -> 278°), releasing
   the page's text entrances at ~60% of the reveal so the content is alive behind it as it
   lands. DIRECTIONAL: the shared ORDER config in pagetrans.js (the nav rail's order)
   resolves every curtain navigation positionally — forward pins at the top-LEFT corner,
   backward plays the exact mirror pinned at the top-RIGHT (negated angles, same waves, same
   timings, no gap at any aspect ratio); one parameterized choreography, never a second
   animation. 60fps by construction: one composited layer animating transform plus
   pre-authored clip-path morphs (the rippling cloth edge) — no per-frame filters, no layout,
   no turbulence. pagetrans.js drives the panel on the shared motion tokens (read via
   getComputedStyle at use time) and generates the wavy clip paths from the live viewport;
   the geometry and colour live here.

   THE CURTAIN VIEW IS CLEAN. While the curtain covers the screen NOTHING else is visible —
   no rail, no badge, no CTA: the overlay is a top-level stacking context above everything,
   chrome included (the old chrome-above-the-blades elevation is retired). .navroot still
   carries view-transition-name:sitechrome for the glide register; the arriving chrome
   typically mounts under the cover (the reveal gate + hold floor give it the parse window),
   but the sweep-out never waits for it — the reveal gates ONLY on document readiness.

   This file MUST be a render-blocking <head> stylesheet on every page (not a helmet link),
   together with <script src="pagetrans.js"></script>: both halves have to be applied before
   the document's first render. */

@view-transition { navigation: auto; }

:root {
  --pg-glide: 1500ms;
  /* Curtain tempo rides the shared motion tokens below (--dur-curtain-in/-hold/-out on
     --ease-in-out-flow); pagetrans.js reads them via getComputedStyle at use time. */
  /* Snapshot blur radii. Cheap because they are applied to a flat bitmap, but a full-screen
     GPU blur still scales with radius, so phones take a smaller one (it reads the same at
     phone scale). Kept as tokens so the keyframes stay single-source. */
  --pg-blur-out: 6px;
  --pg-blur-in: 7px;
  /* The incoming page's travel easing. Exported as a token because the rail's dot indicator
     has to travel on exactly this curve over exactly --pg-glide, so it arrives as the page
     does. Rides the shared motion foundation (--ease-in-out-flow); the literal inside var()
     is the paint-time fallback only — keep it in sync with the token. */
  --pg-ease: var(--ease-in-out-flow, cubic-bezier(.76,0,.24,1));
}

/* ---------------- the persistent chrome layer: ONE chrome, ONE image, ALWAYS ----------------

   The chrome is named, so it is lifted out of the page capture and ::view-transition-old/new
   (root) contain SCENE CONTENT ONLY. The naming was never the bug. The bug was that the chrome
   was then COMPOSITED TWICE: the outgoing chrome bitmap was held on top at opacity 1 while a
   second chrome rendered underneath it, and the hand-over was a 300ms CROSSFADE — and a chrome
   bitmap is mostly transparent, so "held on top" never actually covered anything. Two rails,
   two dots, and a chip that is active in one image and inactive in the other blending to a
   hollow outline: every artefact in the recordings is that one fact.

   Settled here by CSS, unconditionally, with no timing dependence and no JS hand-over: exactly
   ONE of the two chrome images is ever rendered, and which one is a pure function of the
   register.

     SAME-DOCUMENT (the scene glide) -> the LIVE chrome. There is one Nav and it keeps
       rendering, so the frozen old image is dropped outright and the rail's dot travels with
       the page (Nav pins that travel to --pg-glide / --pg-ease, this file's own tokens).
     CROSS-DOCUMENT -> the OUTGOING image, for the whole transition. The arriving Nav is a
       child component that mounts AFTER the snapshot is taken, so it can never be in the
       captured set; left live it renders inside ::view-transition-new(root) and glides — that
       is the "ghost rail sliding beside the real one". So it is held back with the page:
       .navroot is visibility:hidden for exactly as long as the transition runs. The pseudo
       layer is torn down and the live chrome takes over in the same frame, with nothing
       animating and no overlap. Cross-document glides are Landing <-> Case study only, where
       both ends carry the same active item, so the hand-over is invisible; every navigation
       that CHANGES the active item is a curtain, which takes no snapshot at all — the chrome
       is simply covered with the rest of the page and repainted by the arriving document
       while still under the cover.

   Neither branch can regress into two chromes: there is no state, no promise and no timer that
   can leave both images renderable — one of the two carries opacity:0 in every possible
   configuration of the selector below. */
.navroot { view-transition-name: sitechrome; }
::view-transition-group(sitechrome) { animation: none; z-index: 10; }
::view-transition-image-pair(sitechrome) { isolation: auto; }
::view-transition-old(sitechrome),
::view-transition-new(sitechrome) { mix-blend-mode: normal; animation: none; }
/* cross-document (the default): the outgoing image, and only it. */
::view-transition-old(sitechrome) { z-index: 2; opacity: 1; }
::view-transition-new(sitechrome) { z-index: 1; opacity: 0; }
/* ...with the arriving live chrome held back for exactly the transition (pagetrans.js sets and
   clears data-pgvt around it, with a hard timeout, so it can never stick). */
:root[data-pgvt="doc"] .navroot { visibility: hidden; }
/* same-document scene glide: the live chrome, and only it. */
:root[data-vtmode="scene"] ::view-transition-old(sitechrome) { opacity: 0; }
:root[data-vtmode="scene"] ::view-transition-new(sitechrome) { z-index: 2; opacity: 1; }

/* A navigation is committed with the pointer still resting on the chip that started it, so the
   hover brush stroke was baked into the outgoing bitmap and sat under that item for the whole
   transition ("a brush-stroke underline gets stuck under an item"). Retracted at the source:
   the stroke is pulled back and its transition killed on the frame the leave is committed,
   which is BEFORE the snapshot is taken, so no capture can contain one. */
:root[data-pgleave] .navroot .bsfill,
:root[data-pgleave] .navroot .bfill,
:root[data-pgleave] .navroot .bstroke-lg {
  -webkit-clip-path: inset(0 100% 0 0) !important;
  clip-path: inset(0 100% 0 0) !important;
  transition: none !important;
}

/* ---------------- REGISTER 1: the landscape glide ---------------- */
::view-transition-group(root) { animation: none; }
::view-transition-image-pair(root) { isolation: auto; }
::view-transition-old(root),
::view-transition-new(root) { mix-blend-mode: normal; backface-visibility: hidden; will-change: transform, opacity, filter; }
::view-transition-old(root) { z-index: 2; animation: pg-out var(--pg-glide) both; }
::view-transition-new(root) { z-index: 1; animation: pg-in var(--pg-glide) both; }

/* forward: the camera walks forward over the landscape. Per-keyframe easings ride the shared
   motion tokens (the literals inside var() are paint-time fallbacks only, kept in sync):
   travel on --ease-in-out-flow, the incoming's settle tail (from 70%) on --ease-out-quint.
   The outgoing holds opaque to 70% and dissolves over the last 30%, revealing the incoming
   exactly as its settle begins — overlap, never back-to-back, no default-ease segment. */
@keyframes pg-out {
  0%   { transform: none; opacity: 1; filter: blur(0px); animation-timing-function: var(--ease-in-out-flow, cubic-bezier(.76,0,.24,1)) }
  70%  { opacity: 1; animation-timing-function: var(--ease-in-out-flow, cubic-bezier(.76,0,.24,1)) }
  100% { transform: perspective(1200px) translate3d(-4.6%,-11.5%,0) scale(1.05) rotateX(4.4deg) rotateZ(-1.2deg); opacity: 0; filter: blur(var(--pg-blur-out)) }
}
@keyframes pg-in {
  0%   { transform: perspective(1200px) translate3d(3.4%,15.5%,-150px) rotateX(-3.4deg); opacity: 0; filter: blur(var(--pg-blur-in)); animation-timing-function: var(--ease-in-out-flow, cubic-bezier(.76,0,.24,1)) }
  28%  { opacity: 1 }
  70%  { transform: perspective(1200px) translate3d(.34%,1.5%,-14px) rotateX(-.3deg); filter: blur(.4px); animation-timing-function: var(--ease-out-quint, cubic-bezier(.22,1,.36,1)) }
  100% { transform: none; opacity: 1; filter: blur(0px) }
}
/* back: the same implementation, mirrored. Nothing else differs. */
:root[data-vtdir="back"] ::view-transition-old(root) { animation-name: pg-out-back }
:root[data-vtdir="back"] ::view-transition-new(root) { animation-name: pg-in-back }
@keyframes pg-out-back {
  0%   { transform: none; opacity: 1; filter: blur(0px); animation-timing-function: var(--ease-in-out-flow, cubic-bezier(.76,0,.24,1)) }
  70%  { opacity: 1; animation-timing-function: var(--ease-in-out-flow, cubic-bezier(.76,0,.24,1)) }
  100% { transform: perspective(1200px) translate3d(4.6%,11.5%,0) scale(1.05) rotateX(-4.4deg) rotateZ(1.2deg); opacity: 0; filter: blur(var(--pg-blur-out)) }
}
@keyframes pg-in-back {
  0%   { transform: perspective(1200px) translate3d(-3.4%,-15.5%,-150px) rotateX(3.4deg); opacity: 0; filter: blur(var(--pg-blur-in)); animation-timing-function: var(--ease-in-out-flow, cubic-bezier(.76,0,.24,1)) }
  28%  { opacity: 1 }
  70%  { transform: perspective(1200px) translate3d(-.34%,-1.5%,-14px) rotateX(.3deg); filter: blur(.4px); animation-timing-function: var(--ease-out-quint, cubic-bezier(.22,1,.36,1)) }
  100% { transform: none; opacity: 1; filter: blur(0px) }
}
@media (max-width: 800px) { :root { --pg-blur-out: 4px; --pg-blur-in: 5px } }

/* ---------------- REGISTER 2: the yellow curtain reveal ---------------- */
/* ONE solid-yellow cloth panel PINNED at a top viewport corner — top-LEFT arriving forward
   in the site order, top-RIGHT arriving back (pagetrans.js negates the arc angles: same
   geometry, same waves, mirrored): a 300vmax square whose CENTER is the pin (±150vmax
   offsets, transform-origin 50% 50%), so the swing's reach (150vmax) exceeds any viewport
   diagonal (≤ ~142vmax on a square screen) and no arc angle can expose a gap — ultrawide,
   portrait and mobile alike, both pins. The cloth half of the square is carved by a wavy
   clip-path through the pin, generated per navigation by pagetrans.js (px-space; ripple
   morphs via WAAPI); the arriving document holds it at 172° (full cover, every edge outside
   the viewport) and the reveal swings 172° -> 278°, transform-only. The ink layer is
   DELETED: the yellow panel is the full coverage layer — the covered state itself — and no
   dark frame can exist anywhere in the transition. The overlay root is a
   TOP-LEVEL stacking context above every page layer (the Case study cinema peaks at 10000):
   while the curtain covers the screen, nothing — chrome included — can render above it.
   aria-hidden, nothing focusable; it blocks pointer input by covering the page. */
.pgsweep {
  position: fixed;
  inset: 0;
  z-index: 2147483000;
  isolation: isolate;
  overflow: hidden;
  pointer-events: auto;
  contain: strict;
}
.pgsweep .pgs {
  position: absolute;
  left: -150vmax; top: -150vmax;
  width: 300vmax; height: 300vmax;
  transform-origin: 50% 50%; /* = the viewport's top-left corner: the pin */
  transform: rotate(172deg); /* the covered HOLD pose: full cover from the very first paint */
  will-change: transform;
  backface-visibility: hidden;
}
.pgsweep .pgs-y { background: #F1BE48; }
/* E — the mirrored (backward) arc: same square, same waves, pinned at the top-RIGHT corner;
   pagetrans.js negates the rotation angles (covered mirror = -172°). One parameterized
   choreography, never a duplicated animation. */
.pgsweep[data-dir="back"] .pgs { left: auto; right: -150vmax; transform: rotate(-172deg); }
/* Reduced motion: no arc, no ripple, no panel geometry — a single YELLOW veil that
   pagetrans.js fades out over 250ms on arrival (never dark). WAAPI, so the blanket RM
   floor below cannot reach it. */
.pgsweep[data-rm] .pgs { left: 0; top: 0; width: 100%; height: 100%; transform: none; clip-path: none; }
/* A leave committed from the <800 menu overlay: the overlay lives inside .navroot, i.e. above
   the blades, so it stands down instantly on the frame the leave is committed rather than
   fading out on top of the sweep. */
:root[data-pgleave] .mn-ov { transition: none !important; opacity: 0 !important; visibility: hidden !important; }

/* ---------------- reduced motion: one 250ms crossfade, everywhere ---------------- */
@media (prefers-reduced-motion: reduce) {
  /* the dot rides the same token, so it shortens with the transition rather than out-running it */
  :root { --pg-glide: 250ms; --pg-ease: linear }
  ::view-transition-old(root) { animation: pg-fade-out 250ms linear both }
  ::view-transition-new(root) { animation: pg-fade-in 250ms linear both }
  :root[data-vtdir="back"] ::view-transition-old(root) { animation-name: pg-fade-out }
  :root[data-vtdir="back"] ::view-transition-new(root) { animation-name: pg-fade-in }
  /* the same-document scene change is a shorter, simple crossfade; cross-document page
     transitions keep the 250ms contract */
  :root[data-vtmode="scene"] ::view-transition-old(root),
  :root[data-vtmode="scene"] ::view-transition-new(root) { animation-duration: 200ms }
  @keyframes pg-fade-out { to { opacity: 0 } }
  @keyframes pg-fade-in { from { opacity: 0 } }
  /* the curtain's RM treatment is JS-driven (a single yellow veil, .pgsweep[data-rm],
     fading 250ms via WAAPI) — never display:none'd here, or the navigation would be a
     hard cut. */
}

/* ============ SHARED MOTION FOUNDATION (launch motion-polish pass) ============
   Tokens only; nothing consumes them yet. An identical copy sits inline in every page's
   helmet style so the values exist at paint time. Keep the copies byte-identical. */
:root {
  /* Easing — fluid, hydrodynamic character: fast attack, long liquid settle */
  --ease-out-expo:   cubic-bezier(0.16, 1, 0.3, 1);    /* text reveals (GSAP expo.out equivalent) */
  --ease-out-quint:  cubic-bezier(0.22, 1, 0.36, 1);   /* general element entrances */
  --ease-in-out-flow: cubic-bezier(0.76, 0, 0.24, 1);  /* curtain / full-screen wipes (power4.inOut equivalent) */
  --ease-fluid-snap: cubic-bezier(0.3, 1.36, 0.4, 1);  /* nav dot — fast with a soft ~4% overshoot, like water settling */

  /* Durations */
  --dur-text-heading: 900ms;
  --dur-text-body:    700ms;
  --dur-dot:          420ms;  /* local (non-transition) dot moves only; transition-coupled moves ride --pg-glide */
  --dur-curtain-in:   750ms;  /* RETIRED: the cover wipe is deleted (single yellow reveal); kept for copy parity */
  --dur-curtain-hold: 100ms;  /* FLOOR of the readiness-gated covered hold, from the covered first paint */
  --dur-curtain-out:  950ms;  /* reveal keeps the cinematic draw */

  /* Staggers */
  --stagger-line: 80ms;
}
/* Global reduced-motion policy (accessibility; on every page). Does NOT reach the
   ::view-transition-* pseudos, so the 250ms crossfade above still runs; the wedge panels and
   the scene-glide fallback are WAAPI-driven and equally unaffected. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
