/* ============================================================
   MOTION — the shared animation layer
   Accounting Plus Inventory System

   Loaded from base.html for every page, immediately after
   base_styling.css. It owns three things and nothing else:

     1. the motion vocabulary — durations, easings, distances;
     2. a namespaced keyframe library and the opt-in utility
        classes that drive it;
     3. one global prefers-reduced-motion switch that turns all
        of it off, so no page has to remember to.

   ── Why everything is prefixed --fx- / .fx- ──────────────────
   home_styling.css redefines --transition, --radius-* and
   --shadow-* at :root for the dashboard, and party_forms.css
   and report_email.css each define keyframes called `spin` and
   `pulse`. Collisions in this project are not hypothetical.
   One reserved namespace makes every motion token greppable
   and impossible to shadow by accident.

   ── The rules this file exists to enforce ────────────────────
   * Animate `transform` and `opacity` only. Those are composited
     on the GPU. `width`, `height`, `top`, `left`, `margin` and
     `padding` force layout; `background-color`, `color` and
     `box-shadow` force paint. There are two deliberate paint
     exceptions below (.fx-lift's shadow, .fx-glow) and both are
     on small, isolated elements.
   * Long lists never animate per row. The container animates
     once. .fx-stagger stops after 24 children on purpose.
   * Entrances are `both`-filled and under 450ms, so a page
     never feels like it is waiting on decoration.
   * Anything that runs forever must be a loading indicator or a
     single decorative element, and must move only transform or
     opacity.
   ============================================================ */

:root {
  /* ── Durations ──────────────────────────────────────────────
     A five-step scale. Picking from it is what stops the file
     drifting back to the ad-hoc 0.15/0.25/0.3/0.45/0.6s spread
     that was here before. */
  --fx-dur-instant: 90ms;   /* press feedback, tooltips */
  --fx-dur-fast:    150ms;  /* hover, focus rings, small toggles */
  --fx-dur-base:    220ms;  /* the default: panels, tabs, chips */
  --fx-dur-slow:    380ms;  /* entrances, modals, drawers */
  --fx-dur-slower:  600ms;  /* page-level hero moments only */

  /* ── Easings ────────────────────────────────────────────────
     Each has one job. --fx-ease-out is the curve report_email.css
     already uses and it is the house entrance curve. */
  --fx-ease-out:    cubic-bezier(0.16, 1, 0.3, 1);      /* arriving */
  --fx-ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);       /* moving between states */
  --fx-ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);  /* arriving with a little overshoot */
  --fx-ease-exit:   cubic-bezier(0.4, 0, 1, 1);         /* leaving */

  /* ── Distances ──────────────────────────────────────────────
     One scale, so a rise on the dashboard is the same rise on a
     report and nothing looks subtly out of step. */
  --fx-rise:   14px;   /* card / section entrance travel */
  --fx-nudge:  6px;    /* small element entrance travel */
  --fx-lift:   -2px;   /* hover lift */
  --fx-press:  0.98;   /* active scale */

  /* ── Stagger ────────────────────────────────────────────────
     45ms reads as a deliberate cascade. Below ~30ms it looks
     like jitter; above ~70ms the last card feels late. */
  --fx-step:   45ms;

  /* ── Glow ───────────────────────────────────────────────────
     Split by theme on purpose. A glow tuned for #0a0f1e is
     invisible on #f0f4f8, and one tuned for the light ground is
     a grey smudge on the dark one. */
  --fx-glow-brand:   0 6px 20px rgba(37, 99, 235, 0.22);
  --fx-glow-success: 0 6px 20px rgba(5, 150, 105, 0.20);
  --fx-glow-danger:  0 6px 20px rgba(220, 38, 38, 0.20);
  --fx-glow-soft:    0 8px 24px rgba(15, 23, 42, 0.10);
}

:root.dark-mode {
  --fx-glow-brand:   0 6px 24px rgba(59, 130, 246, 0.38);
  --fx-glow-success: 0 6px 24px rgba(16, 185, 129, 0.32);
  --fx-glow-danger:  0 6px 24px rgba(239, 68, 68, 0.32);
  --fx-glow-soft:    0 8px 28px rgba(0, 0, 0, 0.45);
}


/* ════════════════════════════════════════════════════════════
   KEYFRAME LIBRARY

   Every one of these moves transform and/or opacity and nothing
   else. There are no exceptions, and an audit over every
   *_motion.css file confirms it: 60 transform, 28 opacity, and
   nothing that paints or lays out.
   ════════════════════════════════════════════════════════════ */

@keyframes fx-rise {
  from { opacity: 0; transform: translate3d(0, var(--fx-rise), 0); }
  to   { opacity: 1; transform: translate3d(0, 0, 0); }
}

@keyframes fx-rise-sm {
  from { opacity: 0; transform: translate3d(0, var(--fx-nudge), 0); }
  to   { opacity: 1; transform: translate3d(0, 0, 0); }
}

@keyframes fx-fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@keyframes fx-pop {
  from { opacity: 0; transform: scale(0.96); }
  to   { opacity: 1; transform: scale(1); }
}

@keyframes fx-slide-l {
  from { opacity: 0; transform: translate3d(calc(var(--fx-rise) * -1), 0, 0); }
  to   { opacity: 1; transform: translate3d(0, 0, 0); }
}

@keyframes fx-slide-r {
  from { opacity: 0; transform: translate3d(var(--fx-rise), 0, 0); }
  to   { opacity: 1; transform: translate3d(0, 0, 0); }
}

/* Loading sweep. Deliberately transform-based: the shimmer this
   replaces animated `background-position`, which repaints, and a
   dashboard shows fifteen skeletons at once while it loads. */
@keyframes fx-shimmer {
  from { transform: translate3d(-100%, 0, 0); }
  to   { transform: translate3d(100%, 0, 0); }
}

/* Expanding ring for a live status dot. */
@keyframes fx-ping {
  from { opacity: 0.55; transform: scale(1); }
  to   { opacity: 0;    transform: scale(2.4); }
}

@keyframes fx-spin {
  to { transform: rotate(360deg); }
}

/* Invalid-field attention. Short and horizontal only. */
@keyframes fx-shake {
  0%, 100% { transform: translate3d(0, 0, 0); }
  20%      { transform: translate3d(-5px, 0, 0); }
  40%      { transform: translate3d(4px, 0, 0); }
  60%      { transform: translate3d(-3px, 0, 0); }
  80%      { transform: translate3d(2px, 0, 0); }
}



/* ════════════════════════════════════════════════════════════
   ENTRANCE UTILITIES

   `both` fill so the element is invisible before its delay runs
   rather than flashing at full opacity and then animating.
   ════════════════════════════════════════════════════════════ */

.fx-rise    { animation: fx-rise    var(--fx-dur-slow) var(--fx-ease-out) both; }
.fx-rise-sm { animation: fx-rise-sm var(--fx-dur-base) var(--fx-ease-out) both; }
.fx-fade    { animation: fx-fade    var(--fx-dur-base) var(--fx-ease-out) both; }
.fx-pop     { animation: fx-pop     var(--fx-dur-base) var(--fx-ease-spring) both; }
.fx-slide-l { animation: fx-slide-l var(--fx-dur-slow) var(--fx-ease-out) both; }
.fx-slide-r { animation: fx-slide-r var(--fx-dur-slow) var(--fx-ease-out) both; }

/* Delay steps, for hand-placing a sequence without a container. */
.fx-d1 { animation-delay: calc(var(--fx-step) * 1); }
.fx-d2 { animation-delay: calc(var(--fx-step) * 2); }
.fx-d3 { animation-delay: calc(var(--fx-step) * 3); }
.fx-d4 { animation-delay: calc(var(--fx-step) * 4); }
.fx-d5 { animation-delay: calc(var(--fx-step) * 5); }
.fx-d6 { animation-delay: calc(var(--fx-step) * 6); }

/* ── Stagger ────────────────────────────────────────────────
   Put .fx-stagger on a container and its direct children arrive
   in sequence. It stops at 24 because past that the cascade is
   no longer legible as one and the tail just feels slow — and
   because a report table must never reach this rule at all. */
.fx-stagger > * {
  animation: fx-rise var(--fx-dur-slow) var(--fx-ease-out) both;
}
.fx-stagger > *:nth-child(1)  { animation-delay: calc(var(--fx-step) * 0); }
.fx-stagger > *:nth-child(2)  { animation-delay: calc(var(--fx-step) * 1); }
.fx-stagger > *:nth-child(3)  { animation-delay: calc(var(--fx-step) * 2); }
.fx-stagger > *:nth-child(4)  { animation-delay: calc(var(--fx-step) * 3); }
.fx-stagger > *:nth-child(5)  { animation-delay: calc(var(--fx-step) * 4); }
.fx-stagger > *:nth-child(6)  { animation-delay: calc(var(--fx-step) * 5); }
.fx-stagger > *:nth-child(7)  { animation-delay: calc(var(--fx-step) * 6); }
.fx-stagger > *:nth-child(8)  { animation-delay: calc(var(--fx-step) * 7); }
.fx-stagger > *:nth-child(9)  { animation-delay: calc(var(--fx-step) * 8); }
.fx-stagger > *:nth-child(10) { animation-delay: calc(var(--fx-step) * 9); }
.fx-stagger > *:nth-child(11) { animation-delay: calc(var(--fx-step) * 10); }
.fx-stagger > *:nth-child(12) { animation-delay: calc(var(--fx-step) * 11); }
.fx-stagger > *:nth-child(n + 13) { animation-delay: calc(var(--fx-step) * 12); }
/* Past 24 nothing animates at all: no delay is worth a quarter
   of a second of blank rows. */
.fx-stagger > *:nth-child(n + 25) { animation: none; }


/* ════════════════════════════════════════════════════════════
   SCROLL REVEAL

   Elements marked [data-fx] start hidden and are released by
   motion.js when they scroll into view. Below-the-fold cards
   therefore cost nothing until they are actually looked at,
   which is both the nicer effect and the cheaper one.

   The .fx-js guard matters: without JavaScript, or before
   motion.js runs, nothing must be permanently invisible. The
   class is set by motion.js itself, so the hiding rule only
   ever applies when something exists to undo it.
   ════════════════════════════════════════════════════════════ */

.fx-js [data-fx] {
  opacity: 0;
  will-change: opacity, transform;
}
.fx-js [data-fx="rise"]    { transform: translate3d(0, var(--fx-rise), 0); }
.fx-js [data-fx="rise-sm"] { transform: translate3d(0, var(--fx-nudge), 0); }
.fx-js [data-fx="pop"]     { transform: scale(0.96); }
.fx-js [data-fx="slide-l"] { transform: translate3d(calc(var(--fx-rise) * -1), 0, 0); }
.fx-js [data-fx="slide-r"] { transform: translate3d(var(--fx-rise), 0, 0); }

.fx-js [data-fx].fx-in {
  opacity: 1;
  transform: none;
  transition: opacity var(--fx-dur-slow) var(--fx-ease-out),
              transform var(--fx-dur-slow) var(--fx-ease-out);
}

/* will-change is a promise to the compositor, not a decoration:
   left on, it holds a layer for the life of the page. motion.js
   removes the attribute once an element has arrived, and this
   drops the hint with it. */
.fx-js [data-fx].fx-done {
  will-change: auto;
}


/* ════════════════════════════════════════════════════════════
   INTERACTION UTILITIES
   ════════════════════════════════════════════════════════════ */

.fx-lift {
  transition: transform var(--fx-dur-fast) var(--fx-ease-in-out),
              box-shadow var(--fx-dur-fast) var(--fx-ease-in-out);
}
.fx-lift:hover {
  transform: translate3d(0, var(--fx-lift), 0);
  box-shadow: var(--fx-glow-soft);
}

.fx-press {
  transition: transform var(--fx-dur-instant) var(--fx-ease-in-out);
}
.fx-press:active {
  transform: scale(var(--fx-press));
}

.fx-glow {
  transition: box-shadow var(--fx-dur-fast) var(--fx-ease-in-out);
}
.fx-glow:hover {
  box-shadow: var(--fx-glow-brand);
}

/* ── Touch devices ──────────────────────────────────────────
   A finger has no hover. iOS and iPadOS resolve :hover on tap
   and then leave it resolved, so a lifted card stays lifted
   until something else is tapped. Keyed on the pointing device,
   never on width: a 768px desktop window driven by a mouse
   keeps every hover state. */
@media (hover: none), (pointer: coarse) {
  .fx-lift:hover,
  .fx-glow:hover {
    transform: none;
    box-shadow: inherit;
  }
  /* Press feedback replaces it — the one piece of interaction
     feedback a touch device can actually show. */
  .fx-lift:active { transform: scale(var(--fx-press)); }
}


/* ════════════════════════════════════════════════════════════
   LOADING

   .fx-skeleton is a drop-in for the existing .skeleton blocks.
   The sweep is a pseudo-element moving on transform inside an
   overflow-hidden box, so fifteen of them on a loading
   dashboard composite instead of repainting.
   ════════════════════════════════════════════════════════════ */

.fx-skeleton {
  position: relative;
  overflow: hidden;
  background: var(--tint-neutral);
  border-radius: var(--radius-sm);
}
.fx-skeleton::after {
  content: "";
  position: absolute;
  inset: 0;
  transform: translate3d(-100%, 0, 0);
  background: linear-gradient(
    90deg,
    transparent,
    rgba(255, 255, 255, 0.35),
    transparent
  );
  animation: fx-shimmer 1.4s var(--fx-ease-in-out) infinite;
}
:root.dark-mode .fx-skeleton::after {
  background: linear-gradient(
    90deg,
    transparent,
    rgba(255, 255, 255, 0.07),
    transparent
  );
}

.fx-spin { animation: fx-spin 0.7s linear infinite; }


/* ════════════════════════════════════════════════════════════
   THEME SWITCH

   This replaces `body.dark-mode * { transition: background-color
   0.3s, color 0.3s, border-color 0.3s }`, which used to live at
   the end of dark_mode.css.

   That rule put three animated paint properties on every element
   of every page, permanently, in the default theme — so hovering
   one row of a 1,700px Detailed Ledger transitioned every cell in
   it, and every element added since inherited a 300ms background
   transition it never asked for. It also made getComputedStyle
   report every background as rgba(0,0,0,0) in a hidden tab,
   which is documented in CONTEXT.md as a measurement trap.

   The cross-fade it bought is worth keeping, so dark_mode.js now
   puts .theme-switching on <html> for the length of the toggle
   and takes it off again. Same effect on the six surfaces that
   actually carry the theme; nothing paid for the rest of the
   session.
   ════════════════════════════════════════════════════════════ */

html.theme-switching body,
html.theme-switching .sidebar,
html.theme-switching .main-content,
html.theme-switching .dash-card,
html.theme-switching .panel-card,
html.theme-switching .card,
html.theme-switching .table-container,
html.theme-switching .modal-shell {
  transition: background-color var(--fx-dur-slow) var(--fx-ease-in-out),
              color var(--fx-dur-slow) var(--fx-ease-in-out),
              border-color var(--fx-dur-slow) var(--fx-ease-in-out);
}


/* ════════════════════════════════════════════════════════════
   REDUCED MOTION — last in the file, and deliberately universal

   Four of twenty-one stylesheets used to carry their own
   prefers-reduced-motion block, which meant seventeen did not.
   Naming animations one at a time does not converge; this is the
   same lesson base_styling.css records for touch targets.

   The !important is load-bearing. This file is linked before
   every page stylesheet, so without it a page rule would win.
   An accessibility opt-out must not lose a specificity contest.

   Durations go to 0.01ms rather than `animation: none` so that
   any JavaScript waiting on `animationend` or `transitionend`
   still fires and never hangs — motion.js and the workspace
   switcher both do.
   ════════════════════════════════════════════════════════════ */

@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;
  }

  /* base_styling.css sets `html { scroll-behavior: smooth }`,
     which turns every window.scrollTo into an animation. */
  html { scroll-behavior: auto !important; }

  /* Hover transforms are decoration too. */
  .fx-lift:hover,
  .fx-press:active,
  .fx-lift:active { transform: none !important; }
}
