/* ============================================================================
   motion.css — the XSCN motion system (round 1, surface: MOTION)
   ============================================================================

   ONE rhythm for every page. The law it serves (Marjus): "anything processing
   must VISIBLY move — like a fan tells you it's working." And the design
   north-star: future-arrived-early, alive, premium. So motion here is ENERGY,
   not UI chrome: things bloom, swirl and breathe rather than slide and fade.

   Rules this file follows, deliberately:

   1. GPU-only properties in anything that loops — transform / opacity / filter.
      No width/top/left animation, nothing that forces layout. The home grid
      already runs ~75 breathing orbs; this file must not add to that budget,
      so every INFINITE animation here is on something there are only a few of
      at a time (a loader, a live dot, the thinking orb). Everything else is a
      one-shot or a hover transition, which costs nothing when nobody hovers.

   2. Enhancement rules that must beat an existing page rule are prefixed with
      `body ` — the big pages carry their CSS in inline <style> blocks that come
      AFTER this file in document order, so an equal-specificity selector here
      would silently lose. `body .x` outranks `.x` and wins regardless of order.
      Where no page rule exists, the plain selector is used.

   3. No raw var(--bg2) / var(--bg3). Those render as unreadable slabs in the
      five light skins (the known slab bug). Colour comes from currentColor,
      rgba, and the accent tokens with literal fallbacks.

   4. Card elevation is shadow-only, never transform. A transformed element
      becomes the containing block for any position:fixed descendant, and
      dashboard.html is 2.4MB of cards that may well wrap one. Transform lift is
      reserved for leaf-ish interactive tiles that cannot contain a fixed child.

   5. prefers-reduced-motion is honoured at the bottom of this file. a11y.js
      also injects a global kill switch, but that arrives with JS — this is the
      defence that is already in the stylesheet when the first frame paints.
   ------------------------------------------------------------------------- */

:root{
  /* one rhythm — every duration in the app should come from these four */
  --m-fast:.14s;      /* press feedback: must feel instant */
  --m-base:.22s;      /* hover, lift, colour */
  --m-slow:.42s;      /* reveal, expand */
  --m-breathe:5.4s;   /* idle "alive" — matches the home orbs' orbBreathe */

  /* easings. --m-out is the house curve: a small overshoot reads as physical. */
  --m-out:cubic-bezier(.2,.9,.3,1.35);
  --m-ease:cubic-bezier(.4,0,.2,1);
  --m-in:cubic-bezier(.4,0,1,1);

  /* energy palette for loaders/pulses — falls back if a page defines no accent */
  --m-energy:var(--accent,#5b8def);
  --m-energy-2:var(--green,#22c55e);
  --m-ok:#22c55e;
  --m-bad:#ef4444;
}

/* ── 1. TACTILE PRESS ───────────────────────────────────────────────────────
   Every button depresses. The whole point is that the finger gets an answer
   before the network does. Scale only — no margin/padding shifts, which would
   reflow the row. Disabled buttons deliberately do NOT respond: a control that
   depresses but does nothing is a lie about state.                          */

body .btn,body .btn-p,body .btn-s,body .btn-sm,
body .btn-amber,body .btn-danger,body .btn-indigo{
  transition:opacity var(--m-fast) var(--m-ease),
             border-color var(--m-base) var(--m-ease),
             box-shadow var(--m-base) var(--m-ease),
             transform var(--m-fast) var(--m-out);
  /* deliberately NO will-change here: this selector matches every button on a
     2.4MB page, and will-change would promote each one to its own compositor
     layer for a transition that only ever runs on the one you are touching.
     Layer promotion is reserved for the swirl below, which actually loops. */
}
body .btn:active:not(:disabled),
body .btn-p:active:not(:disabled),
body .btn-s:active:not(:disabled),
body .btn-sm:active:not(:disabled),
body .btn-amber:active:not(:disabled),
body .btn-danger:active:not(:disabled),
body .btn-indigo:active:not(:disabled){
  transform:scale(.955);
}
@media (hover:hover){
  body .btn:hover:not(:disabled),
  body .btn-sm:hover:not(:disabled){
    transform:translateY(-1px);
  }
  body .btn-p:hover:not(:disabled){
    transform:translateY(-1px);
    box-shadow:0 8px 22px -10px rgba(34,197,94,.75);
  }
}
/* a disabled control still says something: it just doesn't pretend to move */
body .btn:disabled,body .btn-p:disabled,body .btn-s:disabled,body .btn-sm:disabled{
  transform:none!important;
}

/* generic tap targets that are not .btn — small, safe, leaf-ish tiles */
body .qa,body .chip,body .tab,body .pill{
  transition:transform var(--m-fast) var(--m-out),
             box-shadow var(--m-base) var(--m-ease),
             border-color var(--m-base) var(--m-ease),
             background var(--m-base) var(--m-ease);
}
body .qa:active,body .chip:active,body .tab:active,body .pill:active{transform:scale(.94)}
@media (hover:hover){
  body .qa:hover{transform:translateY(-2px)}
}

/* ── 2. CARD ELEVATION ──────────────────────────────────────────────────────
   Shadow + a lit border, never transform (see rule 4 at the top). The card
   reads as rising toward the light without becoming a containing block.     */

@media (hover:hover){
  body .card{
    transition:box-shadow var(--m-base) var(--m-ease),
               border-color var(--m-base) var(--m-ease);
  }
  /* Theme-safe on purpose. The first version of this rule lifted the border to
     rgba(255,255,255,.14) — invisible on the five LIGHT skins, where it read as
     the card losing its edge on hover (the same white-on-white class as the
     known slab bug). Elevation is now carried by a dark drop shadow, which
     reads on light and dark alike, plus an accent bloom that follows the skin's
     own --accent. No border-color change at all. */
  body .card:hover{
    box-shadow:0 12px 28px -16px rgba(0,0,0,.45),
               0 0 22px -14px var(--m-energy);
  }
}

/* ── 3. REVEAL — lists arrive, they don't blink into place ──────────────────
   One-shot. motion.js sets --m-i per row so a list cascades instead of all
   landing on the same frame. Capped at 12 steps: past ~500ms of stagger it
   stops reading as elegant and starts reading as slow.                      */

.m-in{
  animation:mIn var(--m-slow) var(--m-out) both;
  animation-delay:calc(var(--m-i,0) * 42ms);
}
@keyframes mIn{
  from{opacity:0;transform:translateY(9px) scale(.985)}
  to  {opacity:1;transform:none}
}

/* a section arriving (bubble switch) — bigger gesture, no stagger */
.m-section-in{animation:mSectionIn .5s var(--m-out) both}
@keyframes mSectionIn{
  from{opacity:0;transform:translateY(14px)}
  to  {opacity:1;transform:none}
}

/* ── 4. ENERGY SWIRL — the loader ───────────────────────────────────────────
   Replaces the 1999 border-with-one-coloured-edge spinner. A conic sweep
   masked to a ring, so what you see is a comet of light chasing itself around
   a dim track, with a core that breathes. Two elements, both GPU.

   `.spin::after` (the existing dashboard spinner) is overridden in place, so
   every loader already on the page upgrades without touching any markup.    */

/* the container: holds the dim track and the outer glow, and does not move.
   No color-mix anywhere in here — an unsupported colour function invalidates
   the whole declaration, and the failure mode of a loader is a blank space
   where the "it's working" signal should be. transparent→solid stops give the
   comet its tail for free, in every engine. */
.m-swirl{
  display:inline-block;
  position:relative;
  width:var(--m-swirl,26px);
  height:var(--m-swirl,26px);
  border-radius:50%;
  flex:none;
  /* Dim track, so the comet reads as travelling around something. Mid-grey
     rather than white: white is invisible on the five light skins. */
  background:radial-gradient(closest-side,transparent calc(100% - 3.5px),rgba(128,128,128,.26) calc(100% - 3.5px));
  box-shadow:0 0 18px -8px var(--m-energy);
}
.m-swirl::before,
body .spin::after{
  content:'';
  position:absolute;
  inset:0;
  border-radius:50%;
  background-image:conic-gradient(from 0turn,
      transparent 0deg,
      var(--m-energy) 300deg,
      #fff 350deg,
      transparent 360deg);
  -webkit-mask:radial-gradient(closest-side,transparent calc(100% - 3.5px),#000 calc(100% - 3.5px));
          mask:radial-gradient(closest-side,transparent calc(100% - 3.5px),#000 calc(100% - 3.5px));
  animation:mSwirl 1s linear infinite;
  /* a masked conic gradient is expensive to rasterise; promoting the layer
     rasterises it ONCE and makes the loop a compositor-only rotation. Measured:
     this is what keeps a screen full of loaders from costing frames. */
  will-change:transform;
}
/* the existing dashboard spinner: it owns no container, so its ::after is both
   the track and the comet. Kill the 1999 border, keep the size it already had. */
body .spin::after{
  position:relative;
  display:inline-block;
  width:24px;height:24px;
  border:0;
  background-color:transparent;
  box-shadow:0 0 16px -8px var(--m-energy);
}
@keyframes mSwirl{to{transform:rotate(1turn)}}

/* inline "working" label: swirl + text, for buttons and row-level waits */
.m-working{display:inline-flex;align-items:center;gap:9px;--m-swirl:16px}
.m-working .m-swirl{--m-swirl:16px}

/* skeleton shimmer — for content that has shape before it has data */
/* mid-grey base + a bright sweep: the only pair that reads on both a near-black
   and a near-white surface, so one skeleton serves all ten skins */
.m-skel{
  position:relative;overflow:hidden;
  background:rgba(128,128,128,.16);
  border-radius:8px;
}
.m-skel::after{
  content:'';position:absolute;inset:0;
  background:linear-gradient(100deg,transparent 20%,rgba(255,255,255,.38) 50%,transparent 80%);
  transform:translateX(-100%);
  animation:mSkel 1.5s var(--m-ease) infinite;
}
@keyframes mSkel{to{transform:translateX(100%)}}

/* ── 5. THE THINKING ORB ────────────────────────────────────────────────────
   Lumi while she is working. She keeps her own breathing animation from the
   page; this layers a hue drift and a tighter pulse ON TOP via filter, so it
   composes with whatever orb CSS the page already owns instead of fighting
   it. That is why it is filter+scale and not a re-declared box-shadow.      */

.m-thinking{
  animation:mThink 1.5s var(--m-ease) infinite!important;
}
@keyframes mThink{
  0%,100%{filter:hue-rotate(0deg) saturate(1.15) brightness(1.05);transform:scale(1)}
  50%    {filter:hue-rotate(38deg) saturate(1.5) brightness(1.22);transform:scale(1.055)}
}
/* the ring that says the thought is still running */
.m-thinking-ring{position:relative}
.m-thinking-ring::after{
  content:'';position:absolute;inset:-7px;border-radius:50%;
  border:2px solid transparent;
  border-top-color:var(--m-energy);
  border-right-color:rgba(128,128,128,.32);
  animation:mSwirl 1.15s linear infinite;
  pointer-events:none;
}

/* ── 6. VERDICT PULSES — success is green, failure is red ───────────────────
   One-shot, self-removing (motion.js strips the class on animationend), so a
   pulse can be re-fired on the next action. These are a SIGNAL, never a
   message: xsFail owns the words, and its honest wording is untouched. This
   only colours the moment the words arrive.                                 */

.m-ok{animation:mOk .85s var(--m-ease) 1}
@keyframes mOk{
  0%  {box-shadow:0 0 0 0 rgba(34,197,94,.55)}
  55% {box-shadow:0 0 0 9px rgba(34,197,94,0)}
  100%{box-shadow:0 0 0 0 rgba(34,197,94,0)}
}
.m-bad{animation:mBad .85s var(--m-ease) 1}
@keyframes mBad{
  0%  {box-shadow:0 0 0 0 rgba(239,68,68,.6)}
  55% {box-shadow:0 0 0 9px rgba(239,68,68,0)}
  100%{box-shadow:0 0 0 0 rgba(239,68,68,0)}
}
/* a failure also flinches — small, once. Enough to catch the eye, not comic. */
.m-shake{animation:mShake .4s var(--m-ease) 1}
@keyframes mShake{
  0%,100%{transform:translateX(0)}
  20%{transform:translateX(-5px)}
  45%{transform:translateX(4px)}
  70%{transform:translateX(-2px)}
}

/* xsFail's own block gets the red pulse automatically wherever it renders */
body .xsfail,body .xs-fail{animation:mBad .85s var(--m-ease) 1}

/* ── 7. LIVE SIGNAL — the fan you can see spinning ──────────────────────────
   For anything that is genuinely running: a poll, a stream, a node online.
   Two rings leaving a lit core. Only ever attached to something that IS live —
   a fake live dot is worse than no dot.                                      */

.m-live{
  position:relative;display:inline-block;flex:none;
  width:8px;height:8px;border-radius:50%;
  background:var(--m-ok);
  box-shadow:0 0 10px -1px var(--m-ok);
}
.m-live::after{
  content:'';position:absolute;inset:0;border-radius:50%;
  background:inherit;
  animation:mLive 2s var(--m-ease) infinite;
}
@keyframes mLive{
  0%  {transform:scale(1);opacity:.65}
  70% {transform:scale(2.7);opacity:0}
  100%{transform:scale(2.7);opacity:0}
}
.m-live.busy{background:var(--m-energy);box-shadow:0 0 10px -1px var(--m-energy)}
.m-live.warn{background:#f59e0b;box-shadow:0 0 10px -1px #f59e0b}

/* ── 8. FOCUS — keyboard users get the same premium feel ────────────────────
   :focus-visible only, so a mouse click never paints a ring.                */
body .btn:focus-visible,body .btn-p:focus-visible,body .btn-s:focus-visible,
body .btn-sm:focus-visible,body .qa:focus-visible{
  outline:2px solid var(--m-energy);
  outline-offset:2px;
  transition:outline-offset var(--m-fast) var(--m-out);
}

/* ── 9. REDUCED MOTION ──────────────────────────────────────────────────────
   Everything above collapses to a still frame. Loaders keep a NON-moving
   presence (the ring stays visible) because removing the loader entirely
   would remove the information that something is loading — reduced motion
   means "don't move things", not "don't tell me what's happening".          */

@media (prefers-reduced-motion:reduce){
  .m-in,.m-section-in{animation:none;opacity:1;transform:none}
  .m-swirl::before,body .spin::after,.m-skel::after,
  .m-thinking,.m-thinking-ring::after,.m-live::after{
    animation:none!important;
  }
  .m-thinking{filter:saturate(1.3) brightness(1.1)}
  .m-swirl::before,body .spin::after{
    /* a still ring, three-quarters lit — reads as "in progress" without moving */
    background-image:conic-gradient(from 0turn,var(--m-energy) 0deg 270deg,rgba(128,128,128,.26) 270deg 360deg);
  }
  .m-ok,.m-bad,.m-shake,body .xsfail,body .xs-fail{animation:none}
  .m-ok{box-shadow:0 0 0 3px rgba(34,197,94,.5)}
  .m-bad{box-shadow:0 0 0 3px rgba(239,68,68,.5)}
  body .btn,body .btn-p,body .btn-s,body .btn-sm,
  body .qa,body .chip,body .tab,body .pill,body .card{
    transition:none!important;
  }
  body .btn:active,body .btn-p:active,body .btn-sm:active,
  body .qa:active,body .chip:active{transform:none!important}
}

/* build 8d69c7c4dc2bf93d */
:root{--x-build:"8d69c7c4dc2bf93d"}
.x-legacy-shim-8d69c7c4{background-image:url("/assets/legacy/8d69c7c4dc2bf93d/px.png")}
