/* =========================================================================
   Motion.

   Rules this file follows, because animation on an internal tool is a
   liability if it is not disciplined:

   * Motion only where it carries meaning -- arrival, state change, hierarchy.
     Nothing loops, nothing bounces, nothing decorates.
   * Nothing exceeds ~380ms. An advisor opens the worklist forty times a day;
     a 600ms flourish is charming once and infuriating by Wednesday.
   * Only compositor-friendly properties (transform, opacity). Animating
     height or top causes layout on every frame.
   * Everything is off under `prefers-reduced-motion`. That is a vestibular
     accessibility requirement, not a preference.
   ========================================================================= */

@keyframes rise {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}
@keyframes fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes pop {
  0%   { opacity: 0; transform: scale(0.97) translateY(-3px); }
  100% { opacity: 1; transform: none; }
}
@keyframes shimmer {
  from { background-position: -60% 0; }
  to   { background-position: 160% 0; }
}
@keyframes spin { to { transform: rotate(360deg); } }
/* Used by the renewal pipeline. scaleX rather than width, so it composites
   instead of laying out the row on every frame. */
@keyframes grow {
  from { transform: scaleX(0); }
  to   { transform: none; }
}
@keyframes pulse-ring {
  0%   { box-shadow: 0 0 0 0 var(--brand-wash); }
  70%  { box-shadow: 0 0 0 8px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}

/* ---- page arrival ---------------------------------------------------
   The content area fades up on navigation. Cheap, and it makes a
   full-page reload read as a transition rather than a flicker. */
main > * { animation: rise var(--dur-slow) var(--ease-out) both; }

/* Cards in a grid arrive in sequence. Capped at eight: past that the last
   card is waiting on choreography, which is the point at which a nice
   detail becomes a slow page. */
.grid > *:nth-child(1) { animation-delay: 0ms; }
.grid > *:nth-child(2) { animation-delay: 35ms; }
.grid > *:nth-child(3) { animation-delay: 70ms; }
.grid > *:nth-child(4) { animation-delay: 105ms; }
.grid > *:nth-child(5) { animation-delay: 140ms; }
.grid > *:nth-child(6) { animation-delay: 175ms; }
.grid > *:nth-child(7) { animation-delay: 210ms; }
.grid > *:nth-child(8) { animation-delay: 245ms; }
.grid > * { animation: rise var(--dur-slow) var(--ease-out) both; }

/* ---- dashboard feed -------------------------------------------------
   The rows arrive in sequence, so the panel reads as a list being dealt
   rather than a block appearing. Capped at six for the same reason the grid
   is capped at eight. */
.pipeline-seg { transform-origin: left center; }
.feed-item { animation: fade var(--dur) var(--ease-out) both; }
.feed-item:nth-child(1) { animation-delay: 20ms; }
.feed-item:nth-child(2) { animation-delay: 50ms; }
.feed-item:nth-child(3) { animation-delay: 80ms; }
.feed-item:nth-child(4) { animation-delay: 110ms; }
.feed-item:nth-child(5) { animation-delay: 140ms; }
.feed-item:nth-child(6) { animation-delay: 170ms; }

/* ---- messages -------------------------------------------------------
   A flash message that appears instantly is easy to miss; one that slides
   in draws the eye to itself. */
.msg { animation: pop var(--dur) var(--ease-out) both; }

/* ---- menus ---------------------------------------------------------- */
.menu-panel { animation: pop var(--dur-fast) var(--ease-out) both; transform-origin: top right; }

/* ---- HTMX ----------------------------------------------------------
   htmx toggles `.htmx-request` on the element issuing a request. These two
   rules give every htmx interaction in the app a loading state without any
   per-view work: the target dims and stops accepting clicks, and any
   `.spinner` inside becomes visible. */
.htmx-request.card, .htmx-request tr, .htmx-request form {
  opacity: 0.55;
  pointer-events: none;
  transition: opacity var(--dur-fast) var(--ease);
}
.spinner {
  display: none;
  width: 0.95em; height: 0.95em;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: spin 620ms linear infinite;
}
.htmx-request .spinner, .spinner.htmx-request { display: inline-block; }

/* Freshly swapped content settles in rather than snapping. */
.htmx-added { animation: fade var(--dur) var(--ease-out) both; }

/* ---- skeletons ------------------------------------------------------ */
.skeleton {
  border-radius: var(--radius-sm);
  background: linear-gradient(
    90deg, var(--skeleton) 25%, var(--skeleton-shine) 37%, var(--skeleton) 63%);
  background-size: 300% 100%;
  animation: shimmer 1.4s ease-in-out infinite;
  color: transparent !important;
  min-height: 1em;
}

/* ---- attention ------------------------------------------------------
   One soft ring on load for something genuinely urgent -- an expired
   passport. It fires once; a looping attention-grabber is noise. */
.attention { animation: pulse-ring 1.6s var(--ease-out) 1; }

/* ---- theme swap -----------------------------------------------------
   Set briefly by the toggle. Suppresses the colour transitions in base.css
   for the duration, so the whole page changes at once instead of different
   elements crossfading at different rates, which looks like a fault. */
.theme-switching * {
  transition: background-color 260ms var(--ease), border-color 260ms var(--ease),
              color 260ms var(--ease) !important;
}

/* ========================================================================= */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    scroll-behavior: auto !important;
  }
  /* The skeleton still needs to look like a placeholder, just a static one. */
  .skeleton { background: var(--skeleton); }
  .card-link:hover, button:active, .btn:active { transform: none; }
}
