/* ==========================================================================
   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; }
body.kw-motion .kw-cascade > .kw-settled,
body.kw-motion .kw-cascade.is-in > .kw-settled { 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: blur(0); } }
@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);
}
