/* ============================================================================
   BIZ+ — MOTION
   A small vocabulary, used everywhere, so the app feels like one thing moving
   rather than twelve screens each inventing their own animation.

   Every rule here is wrapped by prefers-reduced-motion at the bottom. Money
   apps get used by people who are stressed; motion sickness on a balance screen
   is not a small failure.
   ==========================================================================*/

/* ---- THE SCALE — the one place a duration or a curve is decided ---------
   Declared HERE and nowhere else. This file owns motion the way tokens.css
   owns colour: the policy that governs movement (prefers-reduced-motion,
   .no-arrive, the primitive vocabulary) lives here, and a duration is
   meaningless without the policy that can zero it. tokens.css used to declare
   the same six names with DIFFERENT values, so which set the app got was
   decided by <link> order alone; it now declares none.

   The names say intent, not speed. Pick by what the movement MEANS:
     --t-tap    a finger pressed something and it answered
     --t-fast   a small thing changed in place: colour, border, a chip filling
     --t-base   something arrived, or a value moved
     --t-slow   a whole surface entered: sheet, drawer, backdrop
     --t-lazy   a long fill telling a story, not reporting a state

   --t-lazy has no caller, on purpose: a rung of a scale is a UNIT, not a
   capability, and without it the next slow fill is written 700ms inline — the
   forty-literal mess this replaces. That does NOT extend to keyframes and
   classes: a primitive with no caller is a claim the app cannot back, so every
   @keyframes here is used by a rule below and every class below by app/js. One
   deliberate exception, `.a-spin`, which the reduced-motion block targets by
   name and is the migration target for onboarding's inline spinner.

   Three curves and no more:
     --ease-out    everything ARRIVING or changing in place
     --ease-in     everything LEAVING — a surface exits faster than it arrived,
                   because leaving is not news
     --ease-spring the win moment only. Overshoot claims something was earned;
                   it is gold, and gold means won.
   --ease stays as an alias because 3 rules still name it. --ease-emph had none,
   and an alias with no consumer is a claim this file's own contract denies. Six
   curves were in flight before this, one of them the browser default. */
:root {
  --t-tap:  120ms;
  --t-fast: 180ms;
  --t-base: 240ms;
  --t-slow: 320ms;
  --t-lazy: 520ms;

  /* NOT a rung of the scale: the tuned duration of one mark. See .a-changed
     below for why it owns its own number instead of riding --t-slow. */
  --t-changed: 420ms;

  --ease-out:    cubic-bezier(.2,.8,.2,1);
  --ease-in:     cubic-bezier(.4,0,1,1);
  --ease-spring: cubic-bezier(.34,1.56,.64,1);

  --ease:      var(--ease-out);
}

/* Continuous loops and holds are PERIODS, not durations, and sit outside the
   scale on purpose: a spinner turning every 700ms and a card arriving in 240ms
   are not the same kind of number, and one ladder makes both wrong. All of them
   live in THIS file — .skeleton 1.4s, .dot-live 2s, .a-spin .7s,
   kg-busy-pulse 1.2s, kg-changed-still 700ms (a hold, not a movement) — and
   bizplus.css carries no duration literal at all. js/admin/shell.js injects one
   more; the console is not the phone app and is not governed here. */

/* ---- arrivals --------------------------------------------------------- */
@keyframes kg-rise   { from { opacity:0; transform: translateY(10px); } to { opacity:1; transform:none; } }
@keyframes kg-fade   { from { opacity:0; }                              to { opacity:1; } }
@keyframes kg-pop    { 0% { opacity:0; transform: scale(.94); } 60% { transform: scale(1.02); } 100% { opacity:1; transform: scale(1); } }
/* kg-slide-l/.a-slide and kg-sheet/.a-sheet are gone and could never have
   gained a caller: the bottom sheet is transition-driven in bizplus.css, and
   onboarding's step slide is driven from JS on purpose, so that a directional
   arrival does not become a second set of arrival classes here. */
.a-rise  { animation: kg-rise var(--t-base) var(--ease-out) both; }
.a-fade  { animation: kg-fade var(--t-base) var(--ease-out) both; }
.a-pop   { animation: kg-pop  var(--t-slow) var(--ease-spring) both; }

/* ---- stagger: children arrive in sequence, no per-item JS -------------- */
.a-stagger > * { animation: kg-rise var(--t-base) var(--ease-out) both; }
.a-stagger > *:nth-child(1){animation-delay:0ms}   .a-stagger > *:nth-child(2){animation-delay:45ms}
.a-stagger > *:nth-child(3){animation-delay:90ms}  .a-stagger > *:nth-child(4){animation-delay:135ms}
.a-stagger > *:nth-child(5){animation-delay:180ms} .a-stagger > *:nth-child(6){animation-delay:225ms}
.a-stagger > *:nth-child(7){animation-delay:270ms} .a-stagger > *:nth-child(8){animation-delay:315ms}
.a-stagger > *:nth-child(n+9){animation-delay:360ms}

/* ---- entry vs refresh --------------------------------------------------
   A CSS animation runs the moment its element is created. It cannot be called
   back a frame later: suppressing it with a class and then removing that class
   only makes the animation start late. So .no-arrive is a property of ONE
   render — the renderer sets it when it is refreshing and never sets it when
   someone has genuinely arrived on the screen. Entrance choreography belongs to
   entry; a bank alert landing is not an entrance.
   .a-changed is deliberately absent from this list. A refresh is exactly when
   the figure that moved has something to say. */
.no-arrive .a-rise, .no-arrive .a-stagger > *, .no-arrive .a-tick,
.no-arrive .a-fade, .no-arrive .a-pop, .no-arrive .pop-in { animation: none !important; }
/* The list is the whole vocabulary, not a sample of it. .a-fade and .a-pop were
   left out of the first version, and GUARD's self-funding callout carries
   .a-fade over PROVEN money text on one of the six tabs a data event rebuilds —
   so a pasted bank alert went on fading a naira figure up from opacity 0, the
   exact symptom this file exists to remove.
   `.pop-in` is the one arrival-shaped class declared outside this file (it
   lives in bizplus.css because landing.js re-triggers it on the parser demo) and
   is named here for the same reason. Nothing renders differently — the landing
   portal is never softRefresh'd, so it cannot replay today — but the contract
   now holds by construction rather than by a fact about landing.js. */

/* ---- the figure that actually MOVED ------------------------------------
   Scale only, and only a little. A proven figure must never dim, never blink
   and never be clipped, so there is no opacity beat and the peak stays well
   inside a .kpi's padding. transform composites — no layout, no paint. */
@keyframes kg-changed {
  0%   { transform: none; }
  26%  { transform: scale(1.04); }
  62%  { transform: scale(.995); }
  100% { transform: none; }
}
/* Two selectors on purpose. The bare one outranks .a-tick on the same node
   (same specificity, but !important); the .no-arrive one ties with the
   suppressor above on specificity and wins by being declared later. Either way
   a refresh cannot silence the one thing a refresh is for.

   THE DURATION IS DECIDED HERE, not inherited. This mark rode --t-slow, so
   re-pegging the scale 420 -> 320ms quietly re-timed a keystone that had
   shipped three days earlier: a side effect standing in for a decision. It is
   420ms again and it owns that number. --t-slow means "a whole surface
   entered"; this is not a surface, it is the one beat that says a PROVEN figure
   moved, and it fires unprompted — after a pasted alert, on a refresh nobody
   asked for — so it has to survive a glance that starts late. The honest
   comparison is the 700ms the reduced-motion wash below is held for, for
   exactly that reason. Re-tuning it wants a real phone, not a computed-style
   read; changing --t-slow no longer drags it either way. */
.a-changed,
.no-arrive .a-changed { animation: kg-changed var(--t-changed) var(--ease-out) both !important; }

/* ---- living surfaces --------------------------------------------------- */
@keyframes kg-shimmer { from { background-position: -200% 0; } to { background-position: 200% 0; } }
.skeleton {
  background: linear-gradient(90deg, var(--surface-2) 25%, var(--surface-3) 37%, var(--surface-2) 63%);
  background-size: 200% 100%;
  animation: kg-shimmer 1.4s linear infinite;
  border-radius: var(--r-1); color: transparent !important;
}

/* The one live "this is still happening" loop: opacity only, so it composites.
   .dot-live used to breathe by animating box-shadow spread — a repaint every
   frame, for ever, once per unseen notification and right through an AI coach
   answer. It wears kg-pulse now. */
@keyframes kg-pulse { 0%,100% { opacity:1; } 50% { opacity:.55; } }
/* A pulse primitive was declared beside this and emitted by nothing. This file's own
   contract says every class here is used by app/js; a primitive with no caller
   is a claim the app cannot back, so it is gone and .dot-live keeps the rule. */
.dot-live { animation: kg-pulse 2s var(--ease-out) infinite; }

/* kg-halo and kg-draw stood here with zero call sites and no way to gain one:
   halo repainted a box-shadow spread for ever on the most important number on
   the screen, and draw wanted an SVG stroke-dashoffset the app has never drawn.
   The meters they were written for are built already at their final value, and
   bizplus.css deliberately did not substitute a mount animation (see its .bar
   comment): a mount animation replays on every data refresh and .no-arrive
   cannot reach an inline style. */

/* ---- value transitions ------------------------------------------------- */
@keyframes kg-tick-up   { from { transform: translateY(.5em); opacity:0; } to { transform:none; opacity:1; } }
.a-tick { animation: kg-tick-up var(--t-base) var(--ease-out) both; }

/* ---- interaction -------------------------------------------------------
   This used to read `.tap, .kpi, .card, button` transitioning transform AND
   box-shadow AND background — two of the three repaint rather than composite,
   on every card and button on every screen (55 of 292 elements on one measured
   screen carried a paint transition, against 21 carrying a transform one). It
   is now what it was always for: the press answering the finger. Anything that
   genuinely needs a colour to ease has its own rule and property list in
   bizplus.css; .kpi's lift lives with .kpi in tokens.css. */
.tap, button { transition: transform var(--t-tap) var(--ease-out); }
.tap:active, button:active { transform: scale(.975); }

/* Focus must be obvious — this is a finance app used one-handed on a bus. */
:where(a, button, [role="button"], input, select, textarea, [tabindex]):focus-visible {
  outline: 2px solid var(--brand-ink);
  outline-offset: 2px;
  border-radius: var(--r-1);
}

/* ---- honour the OS -----------------------------------------------------
   Zeroing durations is half the job. A delay is not a duration, so .a-stagger's
   45ms steps used to survive this block and the staggered arrival became a
   staccato blink — nine items snapping on one after another, worse than the
   animation it replaced. Zero the delays too and the sequence simply IS. This
   is the app's ONLY reduced-motion block; anything added elsewhere has to zero
   delays as well or the staccato comes straight back. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-delay: 0ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    transition-delay: 0s !important;
    scroll-behavior: auto !important;
  }
  .skeleton { animation: none; }

  /* The one survivor. A figure that moved still has to say so — as an instant,
     non-moving state change: a brief wash behind the number, held flat by
     steps() so nothing interpolates and nothing travels. No transform, no
     opacity, and the figure keeps its own semantic ink, so a credit still reads
     as a credit. It outranks the blanket rule above on specificity. */
  .a-changed,
  .no-arrive .a-changed { animation: kg-changed-still 700ms steps(1, end) 1 !important; }

  /* The busy spinner is the exception that had to be made. The blanket rule
     does not calm a rotation down, it freezes it solid mid-arc, and a person
     signing in is left staring at a broken ring. Rotation is movement and stays
     gone; a slow opacity pulse is not movement in space, so it keeps the
     vestibular promise while still saying "working". The attribute selector
     catches today's inline-styled spinner without reaching into another
     module's file; .a-spin is the class it should wear. */
  .a-spin,
  [style*="animation:spin"],
  [style*="animation: spin"] { animation: kg-busy-pulse 1.2s var(--ease-out) infinite !important; }
}

/* Held flat by steps(1,end): the wash is simply present for the duration and
   then gone. No fill, so the element returns to its own background by itself. */
@keyframes kg-changed-still { from { background-color: var(--changed-wash, var(--line-2)); } }
@keyframes kg-busy-pulse    { 0%, 100% { opacity: 1; } 50% { opacity: .32; } }

/* The spinner class the app should use instead of an inline animation. The
   `spin` keyframes are declared in css/bizplus.css; keyframe names are global. */
.a-spin { animation: spin .7s linear infinite; }
