/* ==========================================================================
   KodeshWay — Motion & scroll storytelling (web)
   CANONICAL SOURCE — sync via build/sync_design.sh. Do not hand-edit copies.

   Apple-marketing-grade scroll choreography in pure CSS: reveal-on-view,
   parallax, pinned "stages" with cross-fading steps, kinetic display type,
   line-by-line verse wipes, and fanning glass card stacks.

   GATING IDIOM (mirrors KodeshWayOrg/teaching.css): every scroll-driven rule
   lives inside
       @media (prefers-reduced-motion: no-preference) {
         @supports (animation-timeline: view()) { … }
       }
   so it is a clean no-op under reduced-motion and in browsers without
   scroll-driven timelines (Safari today, Firefox flagged-off). The RESTING
   (un-animated) state authored outside that block IS the static fallback, so
   nothing is ever left hidden. Animate ONLY opacity + transform (compositor
   safe); never width/height/top/left/filter in keyframes.

   For .kw-reveal, js/reveal.js adds a JS IntersectionObserver floor in
   non-supporting browsers (Safari/Firefox) — see that file. NEVER put both
   .kw-reveal and [data-reveal] on the same element.
   ========================================================================== */

/* ── Reveal on view ───────────────────────────────────────────────────────
   Resting state = visible. Scroll-driven browsers animate it in; reveal.js
   gives non-supporting browsers the same entrance via .is-revealed. */
.kw-reveal { /* visible by default — this is the fallback */ }

@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .kw-reveal {
      animation: kw-reveal-in linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 55%;
      will-change: opacity, transform;
    }
    .kw-reveal--scale { animation-name: kw-reveal-scale; }
    .kw-reveal--left  { animation-name: kw-reveal-left;  }
    .kw-reveal--right { animation-name: kw-reveal-right; }
  }
}

/* JS-floor (Safari/Firefox): reveal.js toggles .is-revealed. Hidden only once
   body.reveal-ready is set AND the browser lacks scroll timelines (reveal.js
   adds body.kw-no-scroll-tl in that case), so supporting browsers never
   double-hide. */
body.reveal-ready.kw-no-scroll-tl .kw-reveal {
  opacity: 0;
  transform: translateY(28px);
  transition: opacity var(--kw-dur-slow, 0.5s) var(--kw-ease, ease),
              transform var(--kw-dur-slow, 0.5s) var(--kw-ease, ease);
}
body.reveal-ready.kw-no-scroll-tl .kw-reveal--scale { transform: scale(0.94); }
body.reveal-ready.kw-no-scroll-tl .kw-reveal--left  { transform: translateX(-32px); }
body.reveal-ready.kw-no-scroll-tl .kw-reveal--right { transform: translateX(32px); }
body.reveal-ready.kw-no-scroll-tl .kw-reveal.is-revealed {
  opacity: 1;
  transform: none;
}

@keyframes kw-reveal-in    { from { opacity: 0; transform: translateY(28px); }  to { opacity: 1; transform: none; } }
@keyframes kw-reveal-scale { from { opacity: 0; transform: scale(0.94); }        to { opacity: 1; transform: none; } }
@keyframes kw-reveal-left  { from { opacity: 0; transform: translateX(-32px); }  to { opacity: 1; transform: none; } }
@keyframes kw-reveal-right { from { opacity: 0; transform: translateX(32px); }   to { opacity: 1; transform: none; } }

/* Stagger helper — apply to a container; children enter in sequence. Works in
   scroll-driven browsers via per-child animation-range offsets. */
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .kw-reveal-stagger > * {
      animation: kw-reveal-in linear both;
      animation-timeline: view();
      will-change: opacity, transform;
    }
    .kw-reveal-stagger > *:nth-child(1) { animation-range: entry 0%  entry 50%; }
    .kw-reveal-stagger > *:nth-child(2) { animation-range: entry 8%  entry 58%; }
    .kw-reveal-stagger > *:nth-child(3) { animation-range: entry 16% entry 66%; }
    .kw-reveal-stagger > *:nth-child(4) { animation-range: entry 24% entry 74%; }
    .kw-reveal-stagger > *:nth-child(5) { animation-range: entry 32% entry 82%; }
    .kw-reveal-stagger > *:nth-child(6) { animation-range: entry 40% entry 90%; }
  }
}

/* ── Parallax ──────────────────────────────────────────────────────────────
   Compositor-only translate keyed to the element's pass through the viewport
   (view() so each layer is self-contained). Resting = no transform. */
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .kw-parallax {
      animation: kw-parallax-y linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 100%;
      will-change: transform;
    }
    .kw-parallax--slow { --kw-px-shift: 5%; }
    .kw-parallax--fast { --kw-px-shift: 14%; }
  }
}
@keyframes kw-parallax-y {
  from { transform: translate3d(0, calc(var(--kw-px-shift, 9%) * -1), 0); }
  to   { transform: translate3d(0, var(--kw-px-shift, 9%), 0); }
}

/* ── Pinned stage ──────────────────────────────────────────────────────────
   A tall scroll track (.kw-stage) hosts a sticky 100vh pin (.kw-stage__pin).
   Step layers (.kw-stage__step) overlap in one grid cell and cross-fade across
   the track's named view-timeline as you scroll. Set --kw-stage-steps to the
   number of steps (drives track height + you set per-step ranges, see below).

   Fallback (no scroll timelines): the @supports-not block collapses the tall
   track, un-pins, and stacks the steps as normal static sections. */
/* RESTING / FALLBACK state = a calm stacked poster (no pin, all steps shown).
   This is what reduced-motion AND non-supporting browsers (Safari/Firefox) get,
   so nothing is ever hidden or overlapped illegibly. */
.kw-stage {
  position: relative;
  isolation: isolate;
  overflow: clip;
  --kw-stage-steps: 3;
  padding-block: clamp(3rem, 12vh, 8rem);
}
.kw-stage__pin {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: clamp(1.5rem, 6vh, 4rem);
  text-align: center;
}
.kw-stage__copy { position: relative; z-index: 2; }

/* PINNED, cross-fading mode — only where scroll timelines exist and motion is OK.
   Switches the poster into a tall track + sticky pin + overlapping steps. */
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .kw-stage {
      min-height: calc(var(--kw-stage-steps) * 100vh);
      padding-block: 0;
      view-timeline-name: --kw-stage-tl;
      view-timeline-axis: block;
    }
    .kw-stage__pin {
      position: sticky;
      top: 0;
      height: 100vh;
      display: grid;
      place-items: center;
      gap: 0;
      overflow: clip;
      isolation: isolate;
    }
    .kw-stage__step {
      grid-area: 1 / 1;
      opacity: 0;
      animation: kw-step-fade linear both;
      animation-timeline: --kw-stage-tl;
      will-change: opacity, transform;
    }
    /* Default = 3 ADJACENT (non-overlapping) steps — only one word is ever
       visible; each fully fades out before the next fades in, so centred words
       never collide into a blob. Uses :nth-of-type (not :nth-child) so a sibling
       .kw-aurora <div> inside the pin doesn't offset the count — steps are
       <span>, the aurora is a <div>. Pages with a different step count override
       these ranges inline or with a modifier. */
    .kw-stage__step:nth-of-type(1) { animation-range: cover 0%   cover 33%; }
    .kw-stage__step:nth-of-type(2) { animation-range: cover 33%  cover 67%; }
    .kw-stage__step:nth-of-type(3) { animation-range: cover 67%  cover 100%; }
  }
}
@keyframes kw-step-fade {
  0%   { opacity: 0; transform: translateY(16px) scale(0.99); }
  12%  { opacity: 1; transform: none; }
  88%  { opacity: 1; transform: none; }
  100% { opacity: 0; transform: translateY(-16px) scale(1.01); }
}

/* ── Kinetic display type (typographic "media", no photos) ─────────────────
   An oversized set-apart word used as the visual. Decorative — mark aria-hidden
   and provide the accessible name nearby (.kw-stage__copy / .sr-only). */
.kw-bigtype {
  font-family: var(--kw-font-serif);
  font-weight: 400;
  font-size: clamp(3rem, 20vw, 15rem);
  line-height: 0.9;
  letter-spacing: -0.02em;
  color: var(--kw-label, #F2F2F2);
  text-align: center;
}
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .kw-bigtype--kinetic {
      animation: kw-bigtype-scale linear both;
      animation-timeline: view();
      animation-range: entry 0% exit 100%;
      will-change: transform, letter-spacing, opacity;
    }
  }
}
@keyframes kw-bigtype-scale {
  from { transform: scale(0.82); letter-spacing: 0.04em; opacity: 0.65; }
  35%  { transform: scale(1);    letter-spacing: -0.02em; opacity: 1; }
  70%  { transform: scale(1);    letter-spacing: -0.02em; opacity: 1; }
  to   { transform: scale(1.08); letter-spacing: -0.03em; opacity: 0.55; }
}

/* ── Verse wipe — line-by-line reveal ──────────────────────────────────────
   Each .kw-wipe__line clips in left-to-right in sequence. Resting = fully
   shown (clip base is open), so non-supporting browsers see the whole verse. */
.kw-wipe { display: block; }
.kw-wipe__line { display: block; }
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .kw-wipe__line {
      animation: kw-wipe-in linear both;
      animation-timeline: view();
      will-change: clip-path, opacity;
    }
    .kw-wipe__line:nth-child(1) { animation-range: entry 5%  entry 35%; }
    .kw-wipe__line:nth-child(2) { animation-range: entry 15% entry 45%; }
    .kw-wipe__line:nth-child(3) { animation-range: entry 25% entry 55%; }
    .kw-wipe__line:nth-child(4) { animation-range: entry 35% entry 65%; }
    .kw-wipe__line:nth-child(5) { animation-range: entry 45% entry 75%; }
  }
}
@keyframes kw-wipe-in {
  from { clip-path: inset(0 100% 0 0); opacity: 0.2; }
  to   { clip-path: inset(0 0 0 0);    opacity: 1; }
}

/* ── Card grid reveal ──────────────────────────────────────────────────────
   A row/grid of cards that rises and fades in, lightly staggered, as it enters
   — a calm Apple-style settle (NO rotation/tilt). Resting = the final flat
   layout, so non-supporting browsers see the normal grid. */
.kw-fan { /* layout (grid/flex row) is provided by the page */ }
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .kw-fan__card {
      animation: kw-fan-settle linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 60%;
      will-change: transform, opacity;
    }
    /* gentle stagger so the cards don't all rise in perfect lockstep */
    .kw-fan__card:nth-child(2) { animation-range: entry 6%  entry 66%; }
    .kw-fan__card:nth-child(3) { animation-range: entry 12% entry 72%; }
    .kw-fan__card:nth-child(4) { animation-range: entry 18% entry 78%; }
    .kw-fan__card:nth-child(n+5) { animation-range: entry 24% entry 84%; }
  }
}
@keyframes kw-fan-settle {
  from { opacity: 0; transform: translateY(28px) scale(0.985); }
  to   { opacity: 1; transform: none; }
}

/* ── Cascade — the app's reveal ────────────────────────────────────────────
   KodeshWayAnimation .kodeshWayReveal / playStaggeredEntrance, ported: the
   children of a .kw-cascade group arrive in order — eyebrow → title → rule →
   body → button — each fading in while lifting 18px, easing OUT (settle),
   60 ms apart after a 40 ms lead. .kw-cascade--type adds the de-blur (4px →
   sharp) for type; .kw-cascade--cards settles image cards from 0.985 scale
   with no blur (a blur filter over a large painting is paint the phone
   cannot spare). Time-based, played ONCE: motion.js adds .is-in when the
   group enters view, and immediately for [data-kw-intro] groups — the page's
   introduction. --kw-cascade-offset on a group delays its whole run, so a
   page can sequence its groups (head, then the hero, then its caption).

   Resting state = visible: nothing hides until motion.js stamps
   body.kw-motion, so no-JS visitors see the page whole. Reduce Motion
   (body.kw-reduced) cross-fades the same content in place — no lift, no
   blur — the app's rule: never a downgrade, the content without the travel.
   Once a child has settled, motion.js marks it .kw-settled and the
   animation and filter are dropped, so nothing stays composited. A [hidden]
   child is left alone (display:none never fires animationend, so it would
   stay at opacity 0 for ever); when its hidden attribute is removed it
   arrives with the cascade then. The decorative layers a hero carries
   (.kw-embers, .kw-breath) are not cascade children either: the settled
   rule's animation:none would put the breath out. */
body.kw-motion .kw-cascade > :not([hidden]):not(.kw-breath):not(.kw-embers):not(.kw-kindling) { opacity: 0; }
body.kw-motion:not(.kw-reduced) .kw-cascade > :not([hidden]):not(.kw-breath):not(.kw-embers):not(.kw-kindling) { transform: translateY(var(--kw-cascade-lift, 18px)); }
body.kw-motion:not(.kw-reduced) .kw-cascade--type > * { filter: blur(var(--kw-cascade-blur, 4px)); }
body.kw-motion .kw-cascade.is-in > :not([hidden]):not(.kw-breath):not(.kw-embers):not(.kw-kindling) {
  animation: kw-cascade-fade var(--kw-cascade-dur, 0.5s) var(--kw-ease-settle, cubic-bezier(0.2, 0, 0, 1)) both;
  animation-delay: calc(var(--kw-cascade-offset, 0s) + var(--kw-cascade-lead, 0.04s) + var(--kw-i, 0) * var(--kw-cascade-stagger, 0.06s));
}
body.kw-motion:not(.kw-reduced) .kw-cascade.is-in > :not([hidden]):not(.kw-breath):not(.kw-embers):not(.kw-kindling)        { animation-name: kw-cascade-in; }
body.kw-motion:not(.kw-reduced) .kw-cascade--type.is-in > :not([hidden]):not(.kw-breath):not(.kw-embers):not(.kw-kindling)  { animation-name: kw-cascade-in-type; }
body.kw-motion:not(.kw-reduced) .kw-cascade--cards.is-in > :not([hidden]):not(.kw-breath):not(.kw-embers):not(.kw-kindling) { animation-name: kw-cascade-in-card; }
/* The settled rule must outrank the is-in rules above ((0,8,1) and, for
   --type, (0,9,1)): at (0,4,1) it lost, so a finished animation kept running
   and held filter: blur(0) — a filter at zero still renders the text into its
   own layer, and every hero title came out soft (Ian 2026-09-24). Keyframes
   end at filter: none for the same reason. */
body.kw-motion .kw-cascade > .kw-settled:not([hidden]):not(.kw-breath):not(.kw-embers):not(.kw-kindling):not(.kw-nothing):not(.kw-nothing-2),
body.kw-motion:not(.kw-reduced) .kw-cascade.is-in > .kw-settled:not([hidden]):not(.kw-breath):not(.kw-embers):not(.kw-kindling):not(.kw-nothing):not(.kw-nothing-2) { animation: none; opacity: 1; transform: none; filter: none; }
/* A settled child that is also a tap target still presses (the settled rule
   is more specific than .kw-tap:active alone). */
body.kw-motion .kw-cascade > .kw-settled.kw-tap:active { opacity: var(--kw-tap-dim, 0.4); }
body.kw-motion .kw-cascade > .kw-settled.kw-tap-scale:active { transform: scale(var(--kw-tap-scale, 0.96)); }
.kw-cascade > :nth-child(1)  { --kw-i: 0; }
.kw-cascade > :nth-child(2)  { --kw-i: 1; }
.kw-cascade > :nth-child(3)  { --kw-i: 2; }
.kw-cascade > :nth-child(4)  { --kw-i: 3; }
.kw-cascade > :nth-child(5)  { --kw-i: 4; }
.kw-cascade > :nth-child(6)  { --kw-i: 5; }
.kw-cascade > :nth-child(7)  { --kw-i: 6; }
.kw-cascade > :nth-child(8)  { --kw-i: 7; }
.kw-cascade > :nth-child(9)  { --kw-i: 8; }
.kw-cascade > :nth-child(10) { --kw-i: 9; }
@keyframes kw-cascade-fade    { from { opacity: 0; } to { opacity: 1; } }
@keyframes kw-cascade-in      { from { opacity: 0; transform: translateY(var(--kw-cascade-lift, 18px)); } to { opacity: 1; transform: none; } }
@keyframes kw-cascade-in-type { from { opacity: 0; transform: translateY(var(--kw-cascade-lift, 18px)); filter: blur(var(--kw-cascade-blur, 4px)); } to { opacity: 1; transform: none; filter: none; } }
@keyframes kw-cascade-in-card { from { opacity: 0; transform: translateY(var(--kw-cascade-lift, 18px)) scale(0.985); } to { opacity: 1; transform: none; } }

/* ── Tap feedback — KodeshWayAnimation.tap / tapScale ──────────────────────
   Press: dim to 0.4 (or scale to 0.96) in 60 ms. Release: ease OUT over
   240 ms — easing in held the dim and then snapped, and every button felt
   sticky. A CSS transition gives exactly that: the :active rule carries the
   press duration, the resting rule the release. */
.kw-tap { transition: opacity var(--kw-tap-release, 0.24s) var(--kw-ease-settle, cubic-bezier(0.2, 0, 0, 1)); }
.kw-tap:active { opacity: var(--kw-tap-dim, 0.4); transition-duration: var(--kw-tap-press, 0.06s); }
.kw-tap-scale { transition: transform var(--kw-tap-release, 0.24s) var(--kw-ease-settle, cubic-bezier(0.2, 0, 0, 1)); }
.kw-tap-scale:active { transform: scale(var(--kw-tap-scale, 0.96)); transition-duration: var(--kw-tap-press, 0.06s); }
@media (prefers-reduced-motion: reduce) {
  .kw-tap-scale:active { transform: none; opacity: var(--kw-tap-dim, 0.4); }
}

/* ── Ember field — the canvas embers.js lays over a hero painting ──────────
   Sits above the painting and its scrim, beneath the caption (z-index 2). */
.kw-embers { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 1; pointer-events: none; }

/* ── Page light — the light that falls from the top of a page (pagelight.js) ──
   One fixed canvas BEHIND the page (the body's own background propagates to
   the root, so −1 still paints over it); pointer-events off; sized by the
   script to the shaft's reach, never the whole page. */
.kw-pagelight { position: fixed; top: 0; left: 0; width: 100%; height: 46vh; z-index: -1; pointer-events: none; }
.kw-pagelight--dust { height: 48vh; }
/* The light sits BEHIND the page: the body gives up its own ground so the
   root carries it (theme.js already paints the root; :has covers the rest),
   else the body's background paints over a −1 child. The head floats over
   the light until the page scrolls, as the app's does. */
body[data-kw-light] { background: transparent; }
html:has(body[data-kw-light]) { background: var(--kw-bg, #F0EAD6); }
body[data-kw-light]:not(.is-scrolled) .site-nav { background: transparent; border-bottom-color: transparent; box-shadow: none; -webkit-backdrop-filter: none; backdrop-filter: none; }

/* ── Hero breath — a slow exhale of light over a painting ─────────────────
   A warm radial light laid over a hero's art (teaching.js appends it to the
   ember host), breathing once every nine seconds on opacity alone — the
   room exhaling, never a heartbeat. --kw-breath-x/-y place it over the
   painting's own light source. Screen-blended so it adds light rather than
   washing the paint. Reduce Motion: a still glow. */
.kw-breath {
  position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: radial-gradient(ellipse 62% 58% at var(--kw-breath-x, 50%) var(--kw-breath-y, 42%),
    rgba(255, 214, 140, 0.30) 0%, rgba(255, 214, 140, 0.12) 36%, rgba(255, 214, 140, 0) 70%);
  mix-blend-mode: screen;
  opacity: 0.75;
  animation: kw-breath 9s ease-in-out infinite;
}
@keyframes kw-breath { 0%, 100% { opacity: 0.5; } 50% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .kw-breath { animation: none; opacity: 0.75; } }

/* ── The day turns (theme.js sky mode) ────────────────────────────────────
   The page background and the bands ease between parchment, the plum dusk
   and the warm dark as the sun moves; every hero painting follows the hour
   — a shade dimmer by night, warmed at dusk — on a slow settle. */
html.kw-sky-turning, html.kw-sky-turning body { transition: background-color 2s var(--kw-ease-settle, cubic-bezier(0.2, 0, 0, 1)); }
[data-kw-embers] img { transition: filter 2s var(--kw-ease-settle, cubic-bezier(0.2, 0, 0, 1)); }
html[data-sky="night"] [data-kw-embers] img { filter: brightness(0.84) saturate(0.9); }
html[data-sky="dusk"] [data-kw-embers] img { filter: brightness(0.94) sepia(0.14) saturate(1.05); }
html[data-sky="dusk"] .kw-breath { background: radial-gradient(ellipse 62% 58% at var(--kw-breath-x, 50%) var(--kw-breath-y, 42%), rgba(255, 178, 130, 0.34) 0%, rgba(255, 178, 130, 0.13) 36%, rgba(255, 178, 130, 0) 70%); }
@media (prefers-reduced-motion: reduce) { html.kw-sky-turning, html.kw-sky-turning body, [data-kw-embers] img { transition: none; } }

/* ── The living painting ──────────────────────────────────────────────────
   A hero painting is never a still: embers.js stamps .kw-living on the
   hero's <picture> and the painting breathes on a slow scale — one breath
   in over 22 seconds, out over the next — about the point its light comes
   from (--kw-breath-x/-y when the page sets them, else the upper middle).
   The `scale` property, so a parallax translate on `transform` is untouched.
   Not under Reduce Motion (embers.js never stamps it), never over 3%. */
.kw-living img {
  transform-origin: var(--kw-breath-x, 50%) var(--kw-breath-y, 40%);
  animation: kw-living 22s ease-in-out infinite alternate;
}
@keyframes kw-living { from { scale: 1; } to { scale: 1.03; } }
/* Off stage (embers.js toggles .kw-offstage on the hero when it leaves the
   viewport): the breath and the living painting hold still, so a page
   scrolled past its hero goes idle instead of compositing forever. */
.kw-offstage .kw-living img, .kw-offstage .kw-breath { animation-play-state: paused; }

/* ── Doorways — one page opens onto the next ─────────────────────────────
   Cross-document view transitions (Chrome 126+, Safari 18.2+; elsewhere a
   plain navigation): the page that is left fades on the leave curve, the
   next settles in beneath its own cascade. Any element the pages name
   (view-transition-name on a hub card's art and on the page's hero host —
   the app's hero transition, a real toView) travels on the settle curve. */
@view-transition { navigation: auto; }
/* The name belongs on the hero's box, never on the painting: the painting
   opens on a settle from 1.06 and is clipped by its card, and a snapshot of
   the <img> alone would travel unclipped, a size too large, then drop into
   place. The host clips, so its snapshot is exactly what the page shows. */
[data-kw-embers] { overflow: clip; }
::view-transition-old(root) { animation: kw-vt-leave 0.2s var(--kw-ease-leave, cubic-bezier(0.3, 0, 0.8, 0.15)) both; }
::view-transition-new(root) { animation: kw-vt-arrive 0.4s var(--kw-ease-settle, cubic-bezier(0.2, 0, 0, 1)) both; }
::view-transition-group(*) { animation-duration: 0.5s; animation-timing-function: var(--kw-ease-settle, cubic-bezier(0.2, 0, 0, 1)); }
/* A named element the next page has no match for (the other cards of the
   hub) leaves at once on the leave curve, not as a ghost over the new page;
   one the previous page had no match for simply arrives. */
::view-transition-old(*):only-child { animation: kw-vt-leave 0.18s var(--kw-ease-leave, cubic-bezier(0.3, 0, 0.8, 0.15)) both; }
::view-transition-new(*):only-child { animation: kw-vt-arrive 0.4s var(--kw-ease-settle, cubic-bezier(0.2, 0, 0, 1)) both; }
@keyframes kw-vt-leave  { from { opacity: 1; } to { opacity: 0; } }
@keyframes kw-vt-arrive { from { opacity: 0; } to { opacity: 1; } }

/* ── The kindling — never a spinner ───────────────────────────────────────
   The burning-bush mark ❖: four petals lighting one after another clockwise
   inside a hairline ring, with a gold arc that travels (1.6 s a turn) and
   breathes. motion.js lays it over a hero whose painting has not arrived
   (removed on load); .kw-working puts a small one inside a waiting button.
   Reduce Motion: the mark lit whole, the arc still. */
.kw-kindling { position: absolute; inset: 0; z-index: 1; display: grid; place-items: center; pointer-events: none; }
.kw-kindling__mark { position: relative; width: 56px; height: 56px; }
.kw-kindling__mark::before { content: ""; position: absolute; inset: 0; border-radius: 50%; border: 1px solid rgba(196, 144, 64, 0.35); }
.kw-kindling__arc { position: absolute; inset: 0; border-radius: 50%; border: 2px solid transparent; border-top-color: var(--kw-accent, #C49040); border-right-color: var(--kw-accent, #C49040); animation: kw-kindle-turn 1.6s linear infinite, kw-kindle-breathe 2.2s ease-in-out infinite; }
.kw-kindling__glyph { position: absolute; inset: 0; display: grid; place-items: center; font-family: Georgia, serif; font-size: 22px; line-height: 1; color: var(--kw-accent, #C49040); animation: kw-kindle-petals 1.8s ease-in-out infinite; }
@keyframes kw-kindle-turn { to { transform: rotate(360deg); } }
@keyframes kw-kindle-breathe { 0%, 100% { opacity: 0.45; } 50% { opacity: 1; } }
@keyframes kw-kindle-petals { 0% { opacity: 0.35; text-shadow: 0 0 0 rgba(196,144,64,0); } 50% { opacity: 1; text-shadow: 0 0 14px rgba(196,144,64,0.55); } 100% { opacity: 0.35; text-shadow: 0 0 0 rgba(196,144,64,0); } }
.kw-working { position: relative; color: transparent !important; pointer-events: none; }
.kw-working::after { content: "\2756"; position: absolute; inset: 0; display: grid; place-items: center; font-family: Georgia, serif; font-size: 16px; color: var(--kw-accent, #C49040); animation: kw-kindle-petals 1.4s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
  .kw-kindling__arc { animation: none; transform: rotate(120deg); }
  .kw-kindling__glyph, .kw-working::after { animation: none; opacity: 1; }
}

/* ── Press feedback everywhere (KodeshWayAnimation.tap) — lives here, in motion.css,
   because every page carries motion.css while only some carry components.css.
   Every real button dims to 0.4 in 60 ms under the finger and eases back
   over 240 ms. The chrome that has its own press (the theme toggle, the
   hamburger, range inputs) opts out; a page adds its link classes with
   .kw-tap (motion.css). Durations are unified on the app's release curve. */
button:not([disabled]):not(.kw-theme-toggle):not(.nav-hamburger):not(.kw-no-tap) {
  transition-property: opacity, background-color, color, border-color, transform, box-shadow;
  transition-duration: var(--kw-tap-release, 0.24s);
  transition-timing-function: var(--kw-ease-settle, cubic-bezier(0.2, 0, 0, 1));
}
button:not([disabled]):not(.kw-theme-toggle):not(.nav-hamburger):not(.kw-no-tap):active {
  opacity: var(--kw-tap-dim, 0.4);
  transition-duration: var(--kw-tap-press, 0.06s);
}

/* ── Light through the words — the title's introduction ────────────────────
   The film-title idiom in the app's own language: as the Name settles out of
   the cascade, one band of warm light passes THROUGH the letterforms, left to
   right, with a bloom that spills past the glyph edges — the Word lit from
   behind — and then rests. Afterwards a faint pass once every twelve seconds,
   the room's breath, never a flash. Markup: add .kw-shine to the heading and
   data-kw-shine="<its exact text>" — the light is a second copy of the text
   clipped to the letters, so the copy must be the heading's text to wrap
   the same. The heading keeps its own fill (the crimson Name gradient);
   this only lays light over it. --kw-shine-ink is the light (a lamp's warm
   white, tokens.css); --kw-shine-delay when a page's cascade seats the
   title later than 0.6 s. Reduce Motion: no pass, the title stands. Off
   screen: paused (motion.js stamps .kw-offstage). */
.kw-shine { position: relative; isolation: isolate; }
.kw-shine::before, .kw-shine::after {
  content: attr(data-kw-shine);
  position: absolute; inset: 0; z-index: 1;
  pointer-events: none; user-select: none;
  color: transparent; -webkit-text-fill-color: transparent;
  background-repeat: no-repeat;
  background-size: 300% 100%;
  background-position: 100% 0;
  -webkit-background-clip: text; background-clip: text;
  opacity: 0;
  animation:
    kw-shine-pass   var(--kw-shine-dur, 1.8s) var(--kw-ease, cubic-bezier(0.4, 0, 0.2, 1)) var(--kw-shine-delay, 0.6s) 1 both,
    kw-shine-breath var(--kw-shine-cycle, 12s) linear calc(var(--kw-shine-delay, 0.6s) + var(--kw-shine-dur, 1.8s) + 5s) infinite;
}
/* The band: a narrow blade of light with soft shoulders, cut to the glyphs. */
.kw-shine::before {
  background-image: linear-gradient(100deg,
    transparent 44.5%,
    color-mix(in srgb, var(--kw-shine-ink, #FFF1D2) 45%, transparent) 48%,
    var(--kw-shine-ink, #FFF1D2) 50%,
    color-mix(in srgb, var(--kw-shine-ink, #FFF1D2) 45%, transparent) 52%,
    transparent 55.5%);
}
/* The bloom: the same light, wider and blurred, so it spills past the letter
   edges the way a lamp behind a cut-out does. */
.kw-shine::after {
  background-image: linear-gradient(100deg,
    transparent 40%,
    color-mix(in srgb, var(--kw-shine-ink, #FFF1D2) 70%, transparent) 50%,
    transparent 60%);
  filter: blur(9px);
}
@keyframes kw-shine-pass   { from { background-position: 100% 0; opacity: 1; } to { background-position: 0% 0; opacity: 1; } }
@keyframes kw-shine-breath {
  0%           { background-position: 100% 0; opacity: 0.42; animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); }
  20%          { background-position: 0% 0;   opacity: 0.42; }
  20.01%, 100% { background-position: 0% 0;   opacity: 0; }
}
.kw-shine.kw-offstage::before, .kw-shine.kw-offstage::after { animation-play-state: paused; }
body.kw-reduced .kw-shine::before, body.kw-reduced .kw-shine::after { animation: none; opacity: 0; }
@media (prefers-reduced-motion: reduce) {
  .kw-shine::before, .kw-shine::after { animation: none; opacity: 0; }
}

/* ── The first opening — the cinematic arrival ─────────────────────────────
   The first time a visit opens the site (theme.js stamps html.kw-cinematic
   before paint; motion.js confirms body[data-kw-cinematic]), the page does
   not simply appear. It opens under a veil of its own ground with one light
   kindling where the title will stand; the title is born out of that light
   (from blur and wide tracking into its sharp form, two seconds); the light
   passes through the letters slowly, with its bloom; the eyebrow and lede
   take their place beneath; then the veil lifts and the rest of the page
   cascades in as it always does. Six seconds, on every fresh opening of
   the page (a refresh included), never on a page reached from inside the
   site, never under Reduce Motion.
   Markup: <body data-kw-cinematic> + .kw-cine-head on the title's cascade
   group (the one carrying .kw-shine, or .kw-cine-title when the title has
   markup inside it that the shine's attr() copy cannot carry — the
   library's italic "Living"). motion.js sets --kw-cine-glow-x/-y
   to the title's own centre so the kindling sits on it; --kw-cine-glow-ink
   colours the kindling (default: the shine's light) — the .org home wears
   the Son's crimson. */
html.kw-cinematic body[data-kw-cinematic]::before,
html.kw-cinematic body[data-kw-cinematic]::after {
  content: ""; position: fixed; inset: 0; z-index: 9000; pointer-events: none;
}
html.kw-cinematic body[data-kw-cinematic]::before {
  background: var(--kw-bg, #14100E);
  animation: kw-cine-veil var(--kw-cine-total, 6.2s) linear both;
}
html.kw-cinematic body[data-kw-cinematic]::after {
  background: radial-gradient(ellipse 60% 36% at var(--kw-cine-glow-x, 50%) var(--kw-cine-glow-y, 21%),
    color-mix(in srgb, var(--kw-cine-glow-ink, var(--kw-shine-ink, #FFE6AE)) 28%, transparent) 0%,
    color-mix(in srgb, var(--kw-cine-glow-ink, var(--kw-shine-ink, #FFE6AE)) 10%, transparent) 42%,
    transparent 74%);
  animation: kw-cine-glow var(--kw-cine-total, 6.2s) linear both;
}
@keyframes kw-cine-veil { 0%, 77% { opacity: 1; } 100% { opacity: 0; visibility: hidden; } }
@keyframes kw-cine-glow { 0%, 6% { opacity: 0; } 34% { opacity: 1; } 74% { opacity: 1; } 100% { opacity: 0; visibility: hidden; } }
/* The title's group stands above the veil; everything else waits beneath it.
   When the head sits inside another intro group (the library's title in its
   hero card) motion.js marks the way up: .kw-cine-chain on each cascade
   child (it stands, whatever its held group does — this rule outranks the
   cascade's own) and .kw-cine-above on each ancestor (no stacking context of
   its own, so 9001 reaches the root). motion.js removes all of it when the
   veil has lifted. */
html.kw-cinematic body[data-kw-cinematic] .kw-cine-head { position: relative; z-index: 9001; }
/* The chain rule outranks the cascade's is-in rules outright ((0,10,2) against
   their (0,8,1) and (0,9,1)): when the held group plays, its chain child must
   not begin afresh from nothing. And html.kw-cinematic is never lifted — the
   title's animation-name would change back to the cascade's and replay. */
html body[data-kw-cinematic].kw-motion:not(.kw-reduced) .kw-cascade > .kw-cine-chain:not([hidden]):not(.kw-breath):not(.kw-embers):not(.kw-kindling):not(.kw-nothing) { opacity: 1; transform: none; filter: none; animation: none; }
html.kw-cinematic body[data-kw-cinematic] .kw-cine-above { isolation: auto; }
/* Its choreography: title first (0.3 s → 2.3 s), light through it (2.0 s →
   4.8 s), eyebrow 3.4 s, lede 3.9 s — each on the cascade's own machinery,
   only slower and later. */
html.kw-cinematic body[data-kw-cinematic] .kw-cine-head { --kw-cascade-offset: 3.4s; --kw-cascade-stagger: 0.25s; --kw-cascade-dur: 0.9s; --kw-cascade-blur: 6px; --kw-cascade-lift: 10px; }
html.kw-cinematic body[data-kw-cinematic] .kw-cine-head .kw-shine { --kw-shine-delay: 2s; --kw-shine-dur: 2.8s; }
html.kw-cinematic body[data-kw-cinematic].kw-motion:not(.kw-reduced) .kw-cine-head.is-in > .kw-shine:not([hidden]),
html.kw-cinematic body[data-kw-cinematic].kw-motion:not(.kw-reduced) .kw-cine-head.is-in > .kw-cine-title:not([hidden]) {
  animation-name: kw-cine-title;
  animation-duration: 2s;
  animation-delay: 0.3s;
  animation-timing-function: var(--kw-ease, cubic-bezier(0.4, 0, 0.2, 1));
}
/* No letter-spacing in this motion: tracking is layout, and a title tracked
   wide wraps to two lines and snaps to one as it tightens (Ian saw it). The
   birth is scale and blur only — paint, never layout. */
@keyframes kw-cine-title {
  from { opacity: 0; transform: scale(1.1); filter: blur(18px); }
  55%  { opacity: 1; }
  to   { opacity: 1; transform: none; filter: none; }
}
