/* =====================================================================
   ANIMATIONS — the site-wide motion foundation (#1001686).
   Loaded on every page by base.njk, straight after tokens.css. Every
   later motion card (scroll reveal, hover, page transitions, hero video)
   builds on what's here — nothing page-specific belongs in this file.

   1. Default transitions on interactive elements (zero specificity, so
      any component rule overrides them without a fight)
   2. Keyframes — transform + opacity only, so nothing ever shifts layout
   3. Utility classes that play a keyframe (+ speed / delay / stagger)
   4. Motion-only / reduced-only visibility helpers
   5. The global prefers-reduced-motion safety net
   6. Scroll reveal (#1001687) — hero entrance + the .reveal-pending hook
      driven by js/reveal.js

   Timings, easings and distances are tokens (tokens.css §7) and all go to
   0 under prefers-reduced-motion, so anything built from them is safe by
   default. Usage + rules: docs/COMPONENTS.md → "Motion".
   ===================================================================== */

/* ---- 1. Interactive defaults ---- */
:where(a) {
  transition-property: color, background-color, border-color, text-decoration-color, text-decoration-thickness, opacity;
  transition-duration: var(--dur-fast);
  transition-timing-function: var(--ease);
}
:where(button, [role="button"], input[type="submit"], input[type="button"], input[type="reset"], summary) {
  transition-property: color, background-color, border-color, box-shadow, opacity, transform;
  transition-duration: var(--dur-fast);
  transition-timing-function: var(--ease);
}
:where(input, select, textarea) {
  transition-property: border-color, background-color, box-shadow;
  transition-duration: var(--dur-fast);
  transition-timing-function: var(--ease);
}

/* ---- 2. Keyframes ----
   Only the "from" frame is written, so each lands on the element's own
   resting style (opacity 1, its own transform). Distances are tokens. */
@keyframes otil-fade-in    { from { opacity: 0; } }
@keyframes otil-fade-out   { to   { opacity: 0; } }
@keyframes otil-fade-up    { from { opacity: 0; transform: translate3d(0, var(--shift), 0); } }
@keyframes otil-fade-down  { from { opacity: 0; transform: translate3d(0, calc(var(--shift) * -1), 0); } }
@keyframes otil-fade-left  { from { opacity: 0; transform: translate3d(var(--shift), 0, 0); } }   /* enters moving left */
@keyframes otil-fade-right { from { opacity: 0; transform: translate3d(calc(var(--shift) * -1), 0, 0); } }
@keyframes otil-scale-in   { from { opacity: 0; transform: scale(var(--scale-from)); } }
@keyframes otil-scale-out  { to   { opacity: 0; transform: scale(var(--scale-from)); } }

/* ---- 3. Utilities ----
   <div class="anim-fade-up anim-delay-2">…</div>
   Tune per element with the modifier classes below (never inline style —
   check-design bans style="" in <main>). Plays once, on render; the
   scroll-reveal card decides WHEN, these decide HOW. */
.anim-fade-in, .anim-fade-out,
.anim-fade-up, .anim-fade-down, .anim-fade-left, .anim-fade-right,
.anim-scale-in, .anim-scale-out {
  animation-duration: var(--anim-dur, var(--dur-slow));
  animation-timing-function: var(--anim-ease, var(--ease-out));
  animation-delay: var(--anim-delay, 0ms);
  animation-fill-mode: both;
}
.anim-fade-in    { animation-name: otil-fade-in; }
.anim-fade-out   { animation-name: otil-fade-out;  --anim-ease: var(--ease-in); }
.anim-fade-up    { animation-name: otil-fade-up; }
.anim-fade-down  { animation-name: otil-fade-down; }
.anim-fade-left  { animation-name: otil-fade-left; }
.anim-fade-right { animation-name: otil-fade-right; }
.anim-scale-in   { animation-name: otil-scale-in; }
.anim-scale-out  { animation-name: otil-scale-out; --anim-ease: var(--ease-in); }

/* Speed */
.anim-fast   { --anim-dur: var(--dur); }
.anim-slow   { --anim-dur: var(--dur-slower); }
.anim-slower { --anim-dur: var(--dur-slowest); }
/* Travel */
.anim-far    { --shift: var(--shift-lg); }
.anim-near   { --shift: var(--shift-sm); }

/* Delay — n × --stagger */
.anim-delay-1 { --anim-delay: var(--stagger); }
.anim-delay-2 { --anim-delay: calc(var(--stagger) * 2); }
.anim-delay-3 { --anim-delay: calc(var(--stagger) * 3); }
.anim-delay-4 { --anim-delay: calc(var(--stagger) * 4); }
.anim-delay-5 { --anim-delay: calc(var(--stagger) * 5); }
.anim-delay-6 { --anim-delay: calc(var(--stagger) * 6); }

/* Stagger: a group whose animated children follow one another.
   <ul class="anim-stagger"><li class="anim-fade-up">…</li>…</ul>
   Capped at 8 — past that the last item waits too long to be worth it. */
.anim-stagger > :nth-child(2) { --anim-delay: var(--stagger); }
.anim-stagger > :nth-child(3) { --anim-delay: calc(var(--stagger) * 2); }
.anim-stagger > :nth-child(4) { --anim-delay: calc(var(--stagger) * 3); }
.anim-stagger > :nth-child(5) { --anim-delay: calc(var(--stagger) * 4); }
.anim-stagger > :nth-child(6) { --anim-delay: calc(var(--stagger) * 5); }
.anim-stagger > :nth-child(7) { --anim-delay: calc(var(--stagger) * 6); }
.anim-stagger > :nth-child(n+8) { --anim-delay: calc(var(--stagger) * 7); }

/* ---- 4. Motion-only / reduced-only ----
   For swaps the tokens can't do: hero <video> vs its still poster, a
   looping decoration vs nothing. */
@media (prefers-reduced-motion: reduce) {
  .motion-only { display: none !important; }
}
@media (prefers-reduced-motion: no-preference) {
  .reduced-motion-only { display: none !important; }
}

/* ---- 5. Reduced-motion safety net ----
   The tokens already zero every duration / distance. This catches anything
   that hard-codes a time (third-party embeds, a slip in a later card):
   animations jump to their end state, transitions finish at once. 1ms, not
   0, so transitionend / animationend still fire for any JS listening. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-delay: 0ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    transition-delay: 0ms !important;
    scroll-behavior: auto !important;
  }
}

/* ---- 6. Scroll reveal (#1001687) ----
   Progressive enhancement, never the other way round: the HTML ships every
   element visible. js/reveal.js tags below-the-fold targets
   .reveal-pending and only THEN adds .reveal-on to <html> — so no JS, a
   JS error, no
   IntersectionObserver or reduced motion all leave the page exactly as
   built. Anything already on screen when the script runs is never hidden
   (no flash, no LCP delay). On entering view it loses .reveal-pending and
   gets an .anim-* utility from §3 (and .anim-delay-N for stagger); reveal.js strips
   the .anim-* classes on animationend so a filled animation never pins the
   element's transform (hover lifts keep working).
   Opacity + transform only, so a hidden element still holds its space: no CLS.
   Targets list + opt-in/out attributes: docs/COMPONENTS.md → "Scroll reveal". */
.reveal-on .reveal-pending { opacity: 0; }
@media print {
  .reveal-on .reveal-pending { opacity: 1 !important; }
}

/* Hero entrance — the one "on load" reveal. Pure CSS (time-based, needs no
   JS to finish), children follow one another; split-hero media joins in
   (below).
   LCP: the headline is often the page's LCP element, and Chrome won't count
   a paint at opacity 0 — a stock otil-fade-up cost +1.5s LCP on mobile in
   Lighthouse A/B. Starting at 0.01 paints on frame one (A/B = baseline). */
@keyframes otil-hero-in { from { opacity: 0.01; transform: translate3d(0, var(--shift), 0); } }
.hero__text > * {
  animation: otil-hero-in var(--dur-slower) var(--ease-out) var(--anim-delay, 0ms) both;
}
.hero__text > :nth-child(2) { --anim-delay: var(--stagger); }
.hero__text > :nth-child(3) { --anim-delay: calc(var(--stagger) * 2); }
.hero__text > :nth-child(4) { --anim-delay: calc(var(--stagger) * 3); }
.hero__text > :nth-child(n+5) { --anim-delay: calc(var(--stagger) * 4); }
.hero__quote {
  animation: otil-hero-in var(--dur-slower) var(--ease-out) calc(var(--stagger) * 5) both;
}

/* Split-hero media entrance (#1001695: /counselling/, /fees/, /book/).
   The green block wipes in from the right edge, its arcs ripple out from
   the corner one ring at a time, then the framed photo rises into place
   and its picture settles from a slight zoom. Transform/opacity/clip-path
   only — no CLS. The photo is often the desktop LCP, so it starts at 0.01
   like the text. Fill is `backwards` (never `both`): a held end state would
   pin the img transform and kill the hover zoom (interactions.css). */
@keyframes otil-hero-wipe  { from { clip-path: inset(0 0 0 100%); transform: translate3d(var(--shift-lg), 0, 0); } }
@keyframes otil-hero-ring  { from { opacity: 0; transform: scale(0.8); } }
@keyframes otil-hero-rise  { from { opacity: 0.01; transform: translate3d(0, var(--shift-lg), 0); } }
@keyframes otil-hero-settle { from { transform: scale(1.06); } }
.hero__block {
  animation: otil-hero-wipe var(--dur-slowest) var(--ease-out) var(--stagger) backwards;
}
.hero__arcs circle {
  transform-box: view-box; transform-origin: 100% 100%;
  animation: otil-hero-ring var(--dur-slowest) var(--ease-out) calc(var(--stagger) * 4 + var(--anim-delay, 0ms)) backwards;
}
.hero__arcs circle:nth-child(2) { --anim-delay: calc(var(--stagger) * 1.5); }
.hero__arcs circle:nth-child(3) { --anim-delay: calc(var(--stagger) * 3); }
.hero__arcs circle:nth-child(4) { --anim-delay: calc(var(--stagger) * 4.5); }
.hero__arcs circle:nth-child(5) { --anim-delay: calc(var(--stagger) * 6); }
.hero__photo {
  animation: otil-hero-rise var(--dur-slowest) var(--ease-out) calc(var(--stagger) * 3) backwards;
}
.hero__photo img {
  animation: otil-hero-settle calc(var(--dur-slowest) * 2) var(--ease-out) calc(var(--stagger) * 3) backwards;
}
