/* ============================================================================
   Motion layer — micro-animation that carries meaning.
   Every animation here answers "what just happened?": a row leaving, a value
   changing, a write in flight, a write that failed. Nothing decorative.
   All of it collapses to 1ms under prefers-reduced-motion (see tokens.css).
   ========================================================================== */

:root {
  --ease-out:    cubic-bezier(0.32, 0.72, 0, 1);      /* entering, settling */
  --ease-spring: cubic-bezier(0.34, 1.42, 0.64, 1);   /* confirmations */
  --ease-in:     cubic-bezier(0.55, 0, 1, 0.45);      /* leaving */
  --t-fast: 140ms;
  --t-base: 200ms;
  --t-slow: 320ms;
}

/* ---------------------------------------------------- route transitions --
   In the multi-file build these are CROSS-DOCUMENT transitions: navigating
   from approvals.html to dashboard.html animates instead of flashing white.
   The pseudo-elements must name `root` — the default snapshot — because no
   element declares a custom `view-transition-name`. Naming anything else
   (an earlier revision said `page`) matches nothing and silently does
   nothing, leaving the default hard cut. */
@view-transition { navigation: auto; }
::view-transition-old(root) { animation: vt-out 130ms var(--ease-in) both; }
::view-transition-new(root) { animation: vt-in 220ms var(--ease-out) both; }
@keyframes vt-out { to { opacity: 0; transform: translateY(-4px); } }
@keyframes vt-in { from { opacity: 0; transform: translateY(6px); } }

/* Fallback for engines without the View Transitions API. */
.page-enter { animation: page-in 260ms var(--ease-out) both; }
@keyframes page-in { from { opacity: 0; transform: translateY(8px); } }

/* ------------------------------------------------------- staggered entry --
   Applied by SMS.motion.enter(). Children arrive in sequence so a dense
   table reads as "filling in" rather than snapping into place. */
.stagger > * { animation: stagger-in 300ms var(--ease-out) both; }
.stagger > *:nth-child(1) { animation-delay: 0ms }
.stagger > *:nth-child(2) { animation-delay: 26ms }
.stagger > *:nth-child(3) { animation-delay: 52ms }
.stagger > *:nth-child(4) { animation-delay: 78ms }
.stagger > *:nth-child(5) { animation-delay: 104ms }
.stagger > *:nth-child(6) { animation-delay: 130ms }
.stagger > *:nth-child(7) { animation-delay: 156ms }
.stagger > *:nth-child(8) { animation-delay: 182ms }
.stagger > *:nth-child(n+9) { animation-delay: 208ms }
@keyframes stagger-in { from { opacity: 0; transform: translateY(7px); } }

/* --------------------------------------------------------- row lifecycle -- */
/* A row that has just been written to: flashes its tone, then settles. */
@keyframes row-flash-success { 0%,55% { background: var(--success-soft); } 100% { background: transparent; } }
@keyframes row-flash-error   { 0%,55% { background: var(--danger-soft); }  100% { background: transparent; } }
@keyframes row-flash-accent  { 0%,55% { background: var(--accent-soft); }  100% { background: transparent; } }
.flash-success { animation: row-flash-success 1500ms var(--ease-out) both; }
.flash-error   { animation: row-flash-error   1500ms var(--ease-out) both; }
.flash-accent  { animation: row-flash-accent  1500ms var(--ease-out) both; }

/* A row leaving the table: collapses its own height so the rows below
   close the gap rather than jumping. */
.row-exit {
  animation: row-exit 320ms var(--ease-in) both;
  overflow: hidden;
}
@keyframes row-exit {
  0%   { opacity: 1; transform: translateX(0); }
  35%  { opacity: 0; transform: translateX(-12px); }
  100% { opacity: 0; transform: translateX(-12px);
         max-height: 0; padding-block: 0; border-width: 0; }
}
[dir="rtl"] .row-exit { animation-name: row-exit-rtl; }
@keyframes row-exit-rtl {
  0%   { opacity: 1; transform: translateX(0); }
  35%  { opacity: 0; transform: translateX(12px); }
  100% { opacity: 0; transform: translateX(12px);
         max-height: 0; padding-block: 0; border-width: 0; }
}
.row-enter { animation: row-enter 340ms var(--ease-out) both; }
@keyframes row-enter {
  from { opacity: 0; transform: translateY(-6px); background: var(--accent-soft); }
}

/* ------------------------------------------------------- in-flight writes --
   An optimistic row is on screen but not yet confirmed by the server. It
   reads as provisional: dimmed, with a moving underline. */
.pending {
  opacity: 0.62;
  position: relative;
  pointer-events: none;
}
.pending::after {
  content: ''; position: absolute; inset-inline: 0; inset-block-end: 0; height: 2px;
  background: linear-gradient(90deg, transparent, var(--accent), transparent);
  background-size: 55% 100%; background-repeat: no-repeat;
  animation: inflight 1.1s linear infinite;
}
@keyframes inflight { 0% { background-position: -60% 0; } 100% { background-position: 160% 0; } }

/* A write that failed and rolled back — shakes once, then holds the error tint
   so the user can see exactly which row reverted. */
.rollback { animation: rollback 520ms var(--ease-out) both; }
@keyframes rollback {
  0%   { transform: translateX(0);   background: var(--danger-soft); }
  15%  { transform: translateX(-5px); }
  30%  { transform: translateX(4px); }
  45%  { transform: translateX(-3px); }
  60%  { transform: translateX(2px); }
  75%  { transform: translateX(0); }
  100% { background: var(--danger-soft); }
}

/* ------------------------------------------------------------- feedback -- */
/* Confirmation tick that draws itself rather than appearing. */
.tick-draw path {
  stroke-dasharray: 26; stroke-dashoffset: 26;
  animation: tick-draw 420ms var(--ease-out) 60ms both;
}
@keyframes tick-draw { to { stroke-dashoffset: 0; } }

.pop { animation: pop 340ms var(--ease-spring) both; }
@keyframes pop { from { transform: scale(0.86); opacity: 0; } }

/* Count-up transitions are driven by SMS.motion.countUp; this only smooths
   the digit swap when a figure is replaced wholesale. */
.num-change { animation: num-change 420ms var(--ease-out) both; }
@keyframes num-change {
  0%  { transform: translateY(-40%); opacity: 0; }
  60% { transform: translateY(0); opacity: 1; }
}

/* ------------------------------------------------------------- controls -- */
.btn { will-change: transform; }
.btn:active:not(:disabled) { transform: translateY(1px) scale(0.985); }
.btn-primary:not(:disabled):hover {
  box-shadow: 0 1px 0 oklch(0 0 0 / 0.10) inset,
              0 2px 4px oklch(0.40 0.12 45 / 0.32),
              0 6px 18px oklch(0.40 0.12 45 / 0.26);
}
.btn.is-busy { pointer-events: none; }
.btn.is-busy > :not(.spinner) { opacity: 0.55; }

.rail-link { transition: background var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out), padding-inline-start var(--t-fast) var(--ease-out); }
.rail-link:hover { padding-inline-start: calc(var(--s3) + 3px); }
.rail-link[aria-current="page"]::before { animation: rail-mark 280ms var(--ease-spring) both; }
@keyframes rail-mark { from { transform: scaleY(0.2); opacity: 0; } }

.card { transition: box-shadow var(--t-base) var(--ease-out), border-color var(--t-base) var(--ease-out); }
.tbl tbody tr { transition: background var(--t-fast) var(--ease-out); }
/* Tabs: the underline grows from the centre rather than snapping on, and the
   colour eases with it. The border stays transparent and the indicator is its
   own element, so the label never shifts by a pixel as the state changes. */
.tab { position: relative; transition: color var(--t-fast) var(--ease-out); }
.tab::after {
  content: ''; position: absolute; inset-inline: 0; inset-block-end: -1px; height: 2px;
  background: var(--accent); border-radius: var(--r-full);
  transform: scaleX(0); transform-origin: 50% 50%;
  transition: transform var(--t-base) var(--ease-out);
}
.tab[aria-selected="true"]::after { transform: scaleX(1); }

/* Focus rings ease in. A ring that appears instantly on every tab press reads
   as a flicker when moving quickly through a form. */
.btn, .input, .select, .textarea, .checkbox, .radio, .switch, .search, .menu-item, .tab, .text-link {
  transition-property: background, border-color, color, box-shadow, transform;
  transition-duration: var(--t-fast);
  transition-timing-function: var(--ease-out);
}

/* The check mark lands rather than blinks. */
.checkbox:checked::after, .radio:checked::after { animation: tick-in 160ms var(--ease-spring) both; }
@keyframes tick-in { from { transform: scale(0.4) rotate(var(--tick-rot, 0deg)); opacity: 0; } }

/* Specimen tools are an affordance for the specimen under the pointer, not
   permanent furniture on every thumbnail in a list. Keyboard reaches them
   through :focus-within, so they are not mouse-only. */
.spec-tools { opacity: 0; transform: translateY(3px); transition: opacity var(--t-fast) var(--ease-out), transform var(--t-fast) var(--ease-out); }
.spec:hover .spec-tools, .spec:focus-within .spec-tools { opacity: 1; transform: translateY(0); }

/* A step that becomes active fills rather than flips. */
.step-dot { transition: background var(--t-base) var(--ease-out), border-color var(--t-base) var(--ease-out), color var(--t-base) var(--ease-out); }
.step[data-state="done"] .step-dot svg { animation: pop 300ms var(--ease-spring) both; }

/* An extracted anchor in the source document lights up under the pointer the
   way a tracked change does, so the link between page and field is felt. */
.doc-hl { transition: background var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out); }

/* Meters animate their fill from zero on first paint so a score reads as
   "measured" rather than "declared". */
.meter-fill { animation: meter-fill var(--t-slow) var(--ease-out) both; }
@keyframes meter-fill { from { width: 0 !important; } }
.meter-threshold { animation: fade-in 420ms var(--ease-out) 200ms both; }
@keyframes fade-in { from { opacity: 0; } }

/* Progress bars for long jobs get a subtle moving texture so a stalled job is
   visibly distinguishable from a running one. */
.progress > i {
  background-image: linear-gradient(100deg,
    oklch(1 0 0 / 0) 30%, oklch(1 0 0 / 0.22) 50%, oklch(1 0 0 / 0) 70%);
  background-size: 200% 100%;
  animation: progress-sheen 1.4s linear infinite;
}
@keyframes progress-sheen { to { background-position: -200% 0; } }

/* -------------------------------------------------------------- overlays -- */
.toast { animation: toast-in 300ms var(--ease-spring) both; }
@keyframes toast-in { from { opacity: 0; transform: translateY(14px) scale(0.96); } }
.toast.toast-out { animation: toast-out 200ms var(--ease-in) both; }
@keyframes toast-out { to { opacity: 0; transform: translateX(24px) scale(0.97); } }
[dir="rtl"] .toast.toast-out { animation-name: toast-out-rtl; }
@keyframes toast-out-rtl { to { opacity: 0; transform: translateX(-24px) scale(0.97); } }

.modal { animation: modal-in 260ms var(--ease-spring) both; }
@keyframes modal-in { from { opacity: 0; transform: translateY(14px) scale(0.97); } }
.scrim { animation: fade-in 180ms var(--ease-out) both; }

/* ------------------------------------------------- skeleton → content --- */
.content-in { animation: content-in 380ms var(--ease-out) both; }
@keyframes content-in { from { opacity: 0; } }

/* --------------------------------------------------------- queue chip ---- */
.queue-chip { animation: pop 300ms var(--ease-spring) both; }
.queue-chip.draining { animation: drain 900ms var(--ease-out) infinite; }
@keyframes drain { 50% { opacity: 0.45; } }

/* --------------------------------------------------- live tick indicator -- */
.live-dot { position: relative; }
.live-dot::after {
  content: ''; position: absolute; inset: -3px; border-radius: 50%;
  border: 1.5px solid currentColor; opacity: 0;
  animation: live-ping 2.4s var(--ease-out) infinite;
}
@keyframes live-ping {
  0%   { opacity: 0.7; transform: scale(0.75); }
  70%  { opacity: 0; transform: scale(1.7); }
  100% { opacity: 0; transform: scale(1.7); }
}

/* ------------------------------------------------- signature ink drawing --
   A specimen draws itself when it first enters view — the single most
   product-specific motion in the app. */
.spec-ink.draw {
  stroke-dasharray: 620; stroke-dashoffset: 620;
  animation: ink-draw 1200ms var(--ease-out) both;
}
@keyframes ink-draw { to { stroke-dashoffset: 0; } }

@media (prefers-reduced-motion: reduce) {
  .stagger > *, .row-enter, .row-exit, .pop, .tick-draw path, .spec-ink.draw,
  .meter-fill, .toast, .modal, .content-in, .num-change {
    animation: none !important;
  }
  .spec-ink.draw { stroke-dashoffset: 0; }
  .pending::after, .progress > i, .live-dot::after { animation: none !important; }
  ::view-transition-old(root), ::view-transition-new(root) { animation: none !important; }
}
