/* ============================================================================
   AXIS — MOTION
   Owner: MOTION agent. Page agents import, never edit.

   THE LANGUAGE
     Enters on --ease-out-expo. Exits on --ease-in-expo. Interactive on --spring-*.
     Never Material easing (0.4, 0, 0.2, 1) anywhere in this build.
     150–250ms for UI feedback · 300–500ms for layout and reveals.
     Enters come from scale(0.96–0.985) and a small translate, never from zero.
     Siblings stagger 40–60ms apart.
     Everything animates transform and opacity. Nothing animates width, height,
     top, left, margin, or box-shadow on a large surface.

   HOW A PAGE USES IT
     Reveal        <div data-reveal>                 base · rises from below
                   <div data-reveal="up|down|left|right|scale|blur|fade">
                   <h2 data-reveal="mask"><span>Headline</span></h2>
                   <h1 data-reveal="lines">
                     <span class="ax-line"><span>First line</span></span>
                     <span class="ax-line"><span>Second line</span></span>
                   </h1>
       …then add .is-in when it enters the viewport:
         new IntersectionObserver(es => es.forEach(e => {
           if (e.isIntersecting) { e.target.classList.add('is-in'); io.unobserve(e.target); }
         }), { threshold: .12, rootMargin: '0px 0px -8% 0px' });

     Scroll-linked <div data-scroll>   runs off the main thread on a view()
                   timeline where supported. Needs no JS at all. Safe everywhere:
                   if the timeline is inactive the element is simply visible.

     On load       <div class="ax-enter">   plays once, no observer. For heroes.

     Stagger       <ul data-stagger> — works whether or not children carry --i.
                   data-stagger="tight" 40ms · default 50ms · "loose" 60ms.

     Counters      <span class="ax-count">$23,304.00</span>  never reflows.

     Views         <section class="ax-view" hidden>  app.html / admin.html.

   WHY CONTENT CAN NEVER GO MISSING — read before changing anything below
     A reveal has two independent drivers and neither can strand content.

     1. Where the browser has a view() timeline (Chromium, Safari 26+), the
        reveal is driven by scroll, in CSS, off the main thread, needing no JS
        at all. The element's resting state is VISIBLE and the animation
        supplies the hidden start — so if that timeline is ever inactive, the
        content simply reads. It is never hidden by a rule that nothing arrives
        to undo.

     2. .is-in, when a page's observer adds it, cancels that animation and hands
        the reveal to the transition — the documented contract, unchanged, and a
        true one-shot. A correctly wired page behaves exactly as its author
        intended; a page that forgot its observer still reveals on scroll.

     Older engines with no view() timeline keep the classic hidden base and a
     per-element 2.6s rescue: a forgotten observer costs the animation, never
     the content. On those engines a page that HAS wired its observer should set
     <html data-motion="js"> so deep content keeps its reveal instead of being
     rescued early.

     MEASURED, and the reason this is not a document-wide :has() switch: a
     single .is-in used to disarm the rescue for every other element on the
     page. A partially wired observer left 10 of 13 elements at opacity 0
     forever. Every guard here is per element.
   ========================================================================= */

:root {
  --ease-out-expo: cubic-bezier(0.16, 1, 0.30, 1);
  --ease-in-expo:  cubic-bezier(0.70, 0, 0.84, 0);
  --ease-in-out:   cubic-bezier(0.65, 0, 0.35, 1);
  --spring-soft:   cubic-bezier(0.34, 1.56, 0.64, 1);
  --spring-tight:  cubic-bezier(0.22, 1.30, 0.36, 1);

  --d-fast:   150ms;   /* micro feedback                        */
  --d-ui:     220ms;   /* buttons, toggles, hovers              */
  --d-layout: 380ms;   /* panels, sheets, routes                */
  --d-reveal: 440ms;   /* reveals — inside the 300–500ms band   */
  --d-slow:   560ms;   /* hero-scale moments only               */

  --stagger: 50ms;
}

/* ---- keyframes ------------------------------------------------------------ */
/* One enter keyframe drives every variant; variants only set the --rv-* vars. */
@keyframes ax-rv {
  from {
    opacity: 0;
    transform: translate3d(var(--rv-x, 0), var(--rv-y, 18px), 0) scale(var(--rv-s, 0.985));
    filter: var(--rv-filter, none);
  }
  to { opacity: 1; transform: none; filter: none; }
}
@keyframes ax-mask-rv { from { transform: translate3d(0, 110%, 0); } to { transform: none; } }

/* Forces a stalled reveal to its finished state. fill:forwards only, so it
   never back-fills over .is-in while it is waiting out its delay. */
@keyframes ax-failsafe { to { opacity: 1; transform: none; filter: none; } }

@keyframes ax-sheen  { from { transform: translateX(-120%) skewX(-16deg); }
                         to { transform: translateX(220%)  skewX(-16deg); } }
@keyframes ax-pulse  { 0%, 100% { opacity: .55; } 50% { opacity: 1; } }
@keyframes ax-float  { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-6px); } }
@keyframes ax-spin   { to { transform: rotate(360deg); } }
@keyframes ax-tick   { 0% { transform: none; } 38% { transform: scale(1.06); } 100% { transform: none; } }
@keyframes ax-view-in{ from { opacity: 0; transform: translate3d(var(--view-x, 0), var(--view-y, 10px), 0) scale(0.99); }
                         to { opacity: 1; transform: none; } }
@keyframes ax-vt-in  { from { opacity: 0; transform: translate3d(0, 8px, 0) scale(0.99); } }
@keyframes ax-vt-out { to   { opacity: 0; transform: translate3d(0, -6px, 0) scale(0.995); } }

/* ============================================================================
   1 · REVEAL
   ========================================================================= */
[data-reveal] {
  opacity: 0;
  transform: translate3d(var(--rv-x, 0), var(--rv-y, 18px), 0) scale(var(--rv-s, 0.985));
  filter: var(--rv-filter, none);
  transition:
    opacity   var(--d-reveal) var(--ease-out-expo) var(--reveal-delay, 0ms),
    transform var(--d-reveal) var(--ease-out-expo) var(--reveal-delay, 0ms),
    filter    var(--d-reveal) var(--ease-out-expo) var(--reveal-delay, 0ms);

  /* No persistent will-change: the transition promotes its own layer for the
     few hundred ms it runs, and a long page must not pin 80 layers at once. */
}
[data-reveal].is-in {
  opacity: 1;
  transform: none;
  filter: none;
  animation-name: none;
}

/* ---- direction + texture variants ---------------------------------------- */
/* Named for where the element comes FROM: "up" rises from below. */
[data-reveal="up"]    { --rv-y:  18px; --rv-x: 0;    --rv-s: .985; }
[data-reveal="down"]  { --rv-y: -14px; --rv-x: 0;    --rv-s: .985; }
[data-reveal="left"]  { --rv-y:  0;    --rv-x: -24px;--rv-s: .99;  }
[data-reveal="right"] { --rv-y:  0;    --rv-x:  24px;--rv-s: .99;  }
[data-reveal="scale"] { --rv-y:  8px;  --rv-x: 0;    --rv-s: .96;  }
[data-reveal="fade"]  { --rv-y:  0;    --rv-x: 0;    --rv-s: 1;    }
/* blur-in is for headlines and single small surfaces — never a full section.
   MEASURED, the one caveat in this file: a filter interpolated to `none` in a
   keyframe lands on blur(0px), not none, and a filled animation keeps it — an
   invisible containing block, forever. The observer path clears it, because
   .is-in declares filter: none. A reveal driven purely by scroll keeps
   blur(0px), so do not put position: fixed content inside a blur-revealed
   container. Every other variant is transform and opacity only. */
[data-reveal="blur"]  { --rv-y: 10px;  --rv-x: 0;    --rv-s: .99; --rv-filter: blur(8px); }

/* ---- mask / clip reveal for headlines ------------------------------------- */
/* Transform-only: the clipper holds overflow, the child slides up inside it.
   The padding/margin pair stops descenders being shaved by the clip. */
.ax-mask,
.ax-line,
[data-reveal="mask"],
[data-reveal="lines"] > * {
  display: block;
  overflow: hidden;
  padding-bottom: 0.14em;
  margin-bottom: -0.14em;
}
[data-reveal="mask"],
[data-reveal="lines"] {
  opacity: 1;            /* the children carry the motion, not the block */
  transform: none;
  filter: none;
  animation: none;
}
.ax-mask > *,
.ax-line > *,
[data-reveal="mask"] > *,
[data-reveal="lines"] > * > * {
  display: block;
  transform: translate3d(0, 110%, 0);
  transition: transform var(--d-reveal) var(--ease-out-expo) var(--reveal-delay, 0ms);
  will-change: transform;
}
.ax-mask.is-in > *,
.ax-line.is-in > *,
.is-in .ax-mask > *,
.is-in .ax-line > *,
[data-reveal="mask"].is-in > *,
[data-reveal="lines"].is-in > * > * {
  transform: none;
  will-change: auto;
  animation-name: none;
}

/* ---- stagger -------------------------------------------------------------- */
/* Honours --i when a page sets it, and falls back to source order when it does
   not, so a page can never silently lose its stagger. */
:is([data-stagger], [data-reveal="lines"]) > * {
  --reveal-delay: calc(var(--i, var(--n, 0)) * var(--stagger));
}
:is([data-stagger], [data-reveal="lines"]) > *:nth-child(1)  { --n: 0; }
:is([data-stagger], [data-reveal="lines"]) > *:nth-child(2)  { --n: 1; }
:is([data-stagger], [data-reveal="lines"]) > *:nth-child(3)  { --n: 2; }
:is([data-stagger], [data-reveal="lines"]) > *:nth-child(4)  { --n: 3; }
:is([data-stagger], [data-reveal="lines"]) > *:nth-child(5)  { --n: 4; }
:is([data-stagger], [data-reveal="lines"]) > *:nth-child(6)  { --n: 5; }
:is([data-stagger], [data-reveal="lines"]) > *:nth-child(7)  { --n: 6; }
:is([data-stagger], [data-reveal="lines"]) > *:nth-child(8)  { --n: 7; }
:is([data-stagger], [data-reveal="lines"]) > *:nth-child(9)  { --n: 8; }
:is([data-stagger], [data-reveal="lines"]) > *:nth-child(10) { --n: 9; }
:is([data-stagger], [data-reveal="lines"]) > *:nth-child(11) { --n: 10; }
:is([data-stagger], [data-reveal="lines"]) > *:nth-child(12) { --n: 11; }
:is([data-stagger], [data-reveal="lines"]) > *:nth-child(13) { --n: 12; }
:is([data-stagger], [data-reveal="lines"]) > *:nth-child(14) { --n: 13; }
:is([data-stagger], [data-reveal="lines"]) > *:nth-child(15) { --n: 14; }
:is([data-stagger], [data-reveal="lines"]) > *:nth-child(16) { --n: 15; }
/* Past sixteen the cascade would read as a queue, not a hand — hold it there. */
:is([data-stagger], [data-reveal="lines"]) > *:nth-child(n+17) { --n: 16; }

[data-stagger="tight"] { --stagger: 40ms; }
[data-stagger="loose"] { --stagger: 60ms; }

/* ============================================================================
   2 · ENTER ON LOAD — above-the-fold, no observer needed
   ========================================================================= */
.ax-enter {
  animation: ax-rv var(--d-reveal) var(--ease-out-expo) var(--reveal-delay, 0ms) both;
}
.ax-enter--up    { --rv-y:  18px; --rv-s: .985; }
.ax-enter--down  { --rv-y: -14px; --rv-s: .985; }
.ax-enter--left  { --rv-y: 0; --rv-x: -24px; --rv-s: .99; }
.ax-enter--right { --rv-y: 0; --rv-x:  24px; --rv-s: .99; }
.ax-enter--scale { --rv-y: 8px; --rv-s: .96; }
.ax-enter--fade  { --rv-y: 0; --rv-s: 1; }

/* ============================================================================
   3 · SCROLL-DRIVEN — the primary reveal driver, off the main thread
   Resting state is VISIBLE; the animation supplies the hidden start. An
   inactive or unsupported timeline therefore resolves to readable content,
   never to an element stuck at opacity 0.
   ========================================================================= */
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    /* :where() keeps this at [data-reveal]'s own specificity so .is-in below
       can still take the reveal back. */
    [data-reveal]:not(:where([data-reveal="mask"], [data-reveal="lines"])),
    [data-scroll] {
      opacity: 1; transform: none; filter: none;
      animation-name: ax-rv;
      animation-fill-mode: both;
      animation-timing-function: var(--ease-out-expo);
      animation-duration: auto;
      animation-timeline: view();
      /* MEASURED: a cover-based range cannot finish for the last element in the
         document — there is no scroll left to give it, and it sat at opacity
         0.884 forever. Every range here completes inside `entry`, which always
         reaches 100% at maximum scroll. */
      animation-range: entry 5% entry 60%;
    }
    [data-scroll="early"] { animation-range: entry 0%  entry 35%; }
    [data-scroll="late"]  { animation-range: entry 25% entry 90%; }

    .ax-mask > *,
    .ax-line > *,
    [data-reveal="mask"] > *,
    [data-reveal="lines"] > * > * {
      transform: none;
      animation-name: ax-mask-rv;
      animation-fill-mode: both;
      animation-timing-function: var(--ease-out-expo);
      animation-duration: auto;
      animation-timeline: view();
      animation-range: entry 5% entry 60%;
    }

    /* Later siblings need a little more scroll — the stagger, expressed as a
       range offset because a progress timeline has no animation-delay. */
    :is([data-stagger], [data-reveal="lines"]) > * {
      animation-range-start: entry calc(5%  + min(var(--i, var(--n, 0)), 6) * 4%);
      animation-range-end:   entry calc(60% + min(var(--i, var(--n, 0)), 6) * 4%);
    }

    /* Depth on a media plate. Put it on the image, never on the section. */
    .ax-parallax {
      animation-name: ax-parallax;
      animation-fill-mode: both;
      animation-timing-function: linear;
      animation-duration: auto;
      animation-timeline: view();
      animation-range: cover 0% cover 100%;
    }
  }
}

/* The observer, when a page ships one, takes the reveal back off the scroll
   timeline and hands it to the transition — a true one-shot. Outside the
   @supports block so it also cancels the legacy rescue below. */
[data-reveal].is-in,
[data-scroll].is-in,
html[data-motion="js"] [data-reveal],
html[data-motion="js"] .ax-mask > *,
html[data-motion="js"] .ax-line > * { animation-name: none; }

/* Engines with no view() timeline keep the classic hidden base, so they need a
   rescue. Per element — never a document-wide switch. */
@supports not (animation-timeline: view()) {
  [data-reveal],
  .ax-mask > *,
  .ax-line > *,
  [data-reveal="mask"] > *,
  [data-reveal="lines"] > * > * {
    animation: ax-failsafe var(--d-reveal) var(--ease-out-expo) 2600ms forwards;
  }
  [data-scroll] { opacity: 1; transform: none; filter: none; animation-name: none; }
}
@keyframes ax-parallax {
  from { transform: translate3d(0, var(--px-from,  5%), 0); }
  to   { transform: translate3d(0, var(--px-to,   -5%), 0); }
}

/* Reading progress. A position indicator, not decoration — it stays live under
   reduced motion, the same way a scrollbar does. */
.ax-progress { transform: scaleX(0); transform-origin: 0 50%; }
@supports (animation-timeline: scroll()) {
  .ax-progress {
    animation-name: ax-progress;
    animation-fill-mode: both;
    animation-timing-function: linear;
    animation-duration: auto;
    animation-timeline: scroll(root block);
  }
}
@keyframes ax-progress { from { transform: scaleX(0); } to { transform: scaleX(1); } }

/* ============================================================================
   4 · INTERACTIVE FEEDBACK — physical, and never shifts layout
   ========================================================================= */
.ax-lift {
  transition: transform    var(--d-ui) var(--spring-tight),
              border-color var(--d-ui) var(--ease-out-expo);
}
.ax-lift:active { transform: translate3d(0, -1px, 0) scale(0.995); transition-duration: var(--d-fast); }

/* Shadow only fades on a real pointer. Phones get the transform and none of
   the repaint, and touch never sticks in a hover state. */
@media (hover: hover) and (pointer: fine) {
  .ax-lift {
    transition: transform    var(--d-ui) var(--spring-tight),
                box-shadow   var(--d-ui) var(--ease-out-expo),
                border-color var(--d-ui) var(--ease-out-expo);
  }
  .ax-lift:hover { transform: translate3d(0, -3px, 0); box-shadow: var(--sh-3); will-change: transform; }
  .ax-tile:hover { transform: scale(1.012); }
}
.ax-lift:focus-visible { transform: translate3d(0, -3px, 0); }

.ax-tile {
  transition: transform var(--d-ui) var(--spring-tight),
              border-color var(--d-ui) var(--ease-out-expo),
              background-color var(--d-ui) var(--ease-out-expo);
}
.ax-tile:active { transform: scale(0.99); transition-duration: var(--d-fast); }

.ax-press { transition: transform var(--d-fast) var(--ease-out-expo); }
.ax-press:active { transform: scale(0.96); }

/* Pointer tilt. JS writes --rx/--ry (or an inline transform); this supplies
   the spring and the release. */
[data-tilt], .ax-tilt {
  transform: perspective(900px) rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
  transition: transform var(--d-ui) var(--spring-soft);
}

/* Underline that draws from the left and retracts to the right. */
.ax-underline { position: relative; }
.ax-underline::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -2px; height: 1px;
  background: currentColor; transform: scaleX(0); transform-origin: 100% 50%;
  transition: transform var(--d-ui) var(--ease-out-expo);
}
.ax-underline:hover::after,
.ax-underline:focus-visible::after { transform: scaleX(1); transform-origin: 0 50%; }

/* Scrubber thumb for the reward calculator. */
.ax-thumb { transition: transform var(--d-fast) var(--spring-tight); touch-action: none; }
.ax-thumb:active, .ax-thumb.is-drag { transform: scale(1.14); }
.ax-grab { cursor: grab; }
.ax-grab:active { cursor: grabbing; }

/* ============================================================================
   5 · AMBIENT
   ========================================================================= */
.ax-sheen { position: relative; overflow: hidden; isolation: isolate; }
.ax-sheen::after {
  content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,.09), transparent);
  animation: ax-sheen 3.6s var(--ease-in-out) infinite;
}
/* One pass on hover instead of a loop — the right choice on a CTA. */
.ax-sheen--hover::after { animation: none; opacity: 0; }
.ax-sheen--hover:hover::after,
.ax-sheen--hover:focus-visible::after { opacity: 1; animation: ax-sheen 900ms var(--ease-out-expo) 1; }

.ax-float { animation: ax-float 5.5s var(--ease-in-out) infinite; }
.ax-pulse { animation: ax-pulse 2.4s var(--ease-in-out) infinite; }
.ax-spin  { animation: ax-spin 900ms linear infinite; }

/* Loading placeholder — transform sheen, not an animated background-position. */
.ax-skel { position: relative; overflow: hidden; background: rgba(255,255,255,.05); border-radius: var(--r-sm, 10px); }
.ax-skel::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,.07), transparent);
  animation: ax-sheen 1.4s var(--ease-in-out) infinite;
}

/* ============================================================================
   6 · COUNTERS — the reward calculator is the centrepiece; counting must
   never reflow, never wrap, and never jitter.
   ========================================================================= */
.ax-count,
.ax-count * {
  font-variant-numeric: tabular-nums lining-nums;
  font-feature-settings: "tnum" 1, "lnum" 1;
  font-variant-ligatures: none;
}
/* Crossing a thousands boundary inserts a separator. Without nowrap that can
   push a long figure onto a second line mid-count. */
.ax-count { white-space: nowrap; }

/* MEASURED: .ax-count alone gives tabular digits but does not stop the BOX
   growing. Counting 0.00 -> 23,304.00 moved the element beside it 88.16px,
   27.72px of that in one jump as the separator appeared at 1,000. Reserving
   the width of the largest value holds it at 0.00px.
   --count-w is in ch: 8ch fits 23,304.00 · 9ch fits 999,999.99. */
.ax-slot {
  display: inline-block;
  min-inline-size: var(--count-w, 8ch);
  text-align: var(--count-align, right);
  white-space: nowrap;
}
/* Currency symbol pinned left, digits growing right, baselines locked.
   Children of an inline-flex are blockified, so the reservation applies to a
   plain .ax-count here — the calculator gets the guard without a second class.
   Set --count-w to switch it on: <span class="ax-count" style="--count-w:8ch">. */
.ax-money { display: inline-flex; align-items: baseline; gap: 0.04em; white-space: nowrap; }
.ax-money > .ax-count {
  min-inline-size: var(--count-w, auto);
  text-align: var(--count-align, left);
}
.ax-cur { font-variant-numeric: normal; flex: none; }

/* A value that just changed. Needs a block-ish box to take a transform. */
.ax-tick { display: inline-block; }
.ax-tick.is-tick { animation: ax-tick 260ms var(--spring-tight) 1; }

/* ============================================================================
   7 · VIEWS — app.html and admin.html switch panels in place
   Toggling [hidden] restarts the enter animation on its own, so a page can
   route with one line of JS. .is-entering / .is-leaving remain available for
   pages that want to drive the transition by hand.
   ========================================================================= */
.ax-view {
  transition: opacity   var(--d-layout) var(--ease-out-expo),
              transform var(--d-layout) var(--ease-out-expo);
}
.ax-view[hidden] { display: none; }
.ax-view:not(.is-entering):not(.is-leaving):not(.ax-view--manual) {
  animation: ax-view-in var(--d-layout) var(--ease-out-expo) both;
}
.ax-view.is-entering { opacity: 0; transform: translate3d(0, 10px, 0); }
.ax-view.is-leaving  {
  opacity: 0; transform: translate3d(0, -6px, 0);
  transition-timing-function: var(--ease-in-expo);
  transition-duration: var(--d-ui);
}
/* Tabbed panels read better moving sideways than up. */
.ax-view--next { --view-x: 16px; --view-y: 0; }
.ax-view--prev { --view-x: -16px; --view-y: 0; }

/* Cross-document / SPA View Transitions, for pages that call startViewTransition. */
@media (prefers-reduced-motion: no-preference) {
  ::view-transition-old(root) { animation: ax-vt-out var(--d-ui)     var(--ease-in-expo)  both; }
  ::view-transition-new(root) { animation: ax-vt-in  var(--d-layout) var(--ease-out-expo) both; }
}

/* ============================================================================
   8 · THE KILLSWITCH
   Every surface above resolves to a sensible static state: visible, in place,
   never stuck at opacity 0. This is the one thing this file cannot get wrong.
   ========================================================================= */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-delay: 0ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    transition-delay: 0ms !important;
    scroll-behavior: auto !important;
  }

  /* Reveals, in every variant, land finished. */
  [data-reveal],
  [data-scroll],
  .ax-enter,
  .ax-mask > *,
  .ax-line > *,
  [data-reveal="mask"] > *,
  [data-reveal="lines"] > * > * {
    opacity: 1 !important;
    transform: none !important;
    filter: none !important;
    animation: none !important;
    will-change: auto !important;
  }
  :is([data-stagger], [data-reveal="lines"]) > * { --reveal-delay: 0ms !important; }

  /* A visible view is a visible view, whatever class was left on it. */
  .ax-view:not([hidden]) {
    opacity: 1 !important;
    transform: none !important;
    animation: none !important;
  }

  /* Ambient loops stop. The skeleton keeps its surface, loses its sweep. */
  .ax-sheen::after,
  .ax-sheen--hover::after,
  .ax-skel::after,
  .ax-float,
  .ax-pulse,
  .ax-parallax { animation: none !important; transform: none !important; }
  .ax-sheen::after, .ax-skel::after { opacity: 0 !important; }
  .ax-pulse { opacity: 1 !important; }
  .ax-spin  { animation: none !important; opacity: .6; }
  .ax-tick.is-tick { animation: none !important; }

  /* Hover lift keeps its shadow, drops its travel. */
  .ax-lift:hover, .ax-lift:focus-visible, .ax-tile:hover, .ax-lift:active, .ax-press:active,
  [data-tilt], .ax-tilt, .ax-thumb:active, .ax-thumb.is-drag { transform: none !important; }

  /* The progress indicator is exempt — it reports position, it does not move
     for effect. Re-assert what the blanket duration rule above would break. */
  @supports (animation-timeline: scroll()) {
    .ax-progress {
      animation-name: ax-progress !important;
      animation-duration: auto !important;
      animation-timeline: scroll(root block) !important;
      animation-fill-mode: both !important;
    }
  }
}

/* ═══ THE VAULT PANE SWAP — WALLET · STAKING POOLS · EARN ══════════════════
   Added 2026-09-21. The swap was `s.hidden = s.id !== target` in
   axis-vault.js — a hard toggle with no duration, which is why the vault read
   as a cut rather than a transition. There was never an easing curve to fix;
   there was no animation at all.

   The name sits on the vault host ALONE (.axv-vt, claimed once in JS). Root
   carries the rail, the dock and the brand strip — none of which change
   between panes — so root is given a 1ms crossfade of identical pixels and
   holds perfectly still while the pane moves. Naming root instead would have
   translated the entire viewport, sliding the fixed chrome.

   ::view-transition-group interpolates the BOX, so a 360px wallet growing
   into a pool board is a real layout animation, not a crossfade.
   ========================================================================= */

@media (prefers-reduced-motion: no-preference) {
  .axv-vt { view-transition-name: axv-pane; }

  ::view-transition-group(axv-pane) {
    animation-duration: var(--d-layout);
    animation-timing-function: var(--ease-out-expo);
  }
  ::view-transition-old(axv-pane) { animation: ax-vt-out 160ms var(--ease-in-expo) both; }
  ::view-transition-new(axv-pane) { animation: ax-vt-in var(--d-layout) var(--ease-out-expo) both; }

  ::view-transition-old(root),
  ::view-transition-new(root) { animation-duration: 1ms; }
}

@keyframes ax-vt-out {
  to { opacity: 0; transform: translate3d(0, -8px, 0) scale(.99); }
}
@keyframes ax-vt-in {
  from { opacity: 0; transform: translate3d(0, 14px, 0) scale(.985); }
}
