/* =========================================================================
   MXX — animation system
   -------------------------------------------------------------------------
   ONE contract for the whole site:

     [data-reveal]            a group. JS adds .is-in when it enters view.
     [data-anim="<variant>"]  an element inside that group. Its "out" state is
                              declared here; entering the group releases it.
     --i                      stagger index, written by JS on [data-stagger].
     --delay / --dur          per-element overrides, set inline in the HTML.

   Every variant animates ONLY compositor-friendly properties —
   transform, opacity, clip-path. No width/height/top/left, no filters,
   no box-shadow transitions, no scroll- or pointer-driven values.
   Animations run once on entry and then the page is completely still.
   ========================================================================= */

/* Out-states apply only when JS is running and motion is welcome. Without
   JS (or with reduced-motion) everything below simply never hides. */
.js [data-anim] {
  transition-property: opacity, transform, clip-path;
  transition-duration: var(--dur, var(--d-base));
  transition-timing-function: var(--e-out);
  transition-delay: calc(var(--i, 0) * var(--stagger) + var(--delay, 0ms));
  will-change: auto;
}

/* ---------- 1. rise — the base gesture ----------------------------------- */
.js [data-anim="rise"] {
  opacity: 0;
  transform: translate3d(0, var(--rise), 0);
}
.js [data-anim="rise-sm"] {
  opacity: 0;
  transform: translate3d(0, calc(var(--rise) * 0.5), 0);
}
.js [data-anim="fade"] { opacity: 0; }

/* ---------- 2. slide in from a side --------------------------------------- */
.js [data-anim="from-right"] {
  opacity: 0;
  transform: translate3d(6%, 0, 0);
}
.js [data-anim="from-left"] {
  opacity: 0;
  transform: translate3d(-6%, 0, 0);
}

/* ---------- 3. clip-path wipes ------------------------------------------- */
.js [data-anim="wipe-r"] { clip-path: inset(0 100% 0 0); }   /* left → right  */
.js [data-anim="wipe-l"] { clip-path: inset(0 0 0 100%); }   /* right → left  */
.js [data-anim="mask-up"] { clip-path: inset(100% 0 0 0); }  /* bottom edge up */
.js [data-anim="mask-open"] { clip-path: inset(50% 0 50% 0); } /* centre out  */

/* ---------- 4. zoom-settle: image starts slightly large, relaxes back ----- */
/* two openings, one settle: `zoom` sweeps up from the bottom edge,
   `zoom-open` parts from the centre line. Both let the picture relax
   from 1.08 back to 1 over a slightly longer beat than the mask. */
.js [data-anim="zoom"] { clip-path: inset(0 0 100% 0); }
.js [data-anim="zoom-open"] { clip-path: inset(50% 0 50% 0); }
.js [data-anim^="zoom"] > img,
.js [data-anim^="zoom"] > picture > img {
  transform: scale(1.08);
  transition: transform calc(var(--dur, var(--d-slow)) * 1.35) var(--e-out);
  transition-delay: calc(var(--i, 0) * var(--stagger) + var(--delay, 0ms));
}

/* ---------- 5. card flip (3D) -------------------------------------------- */
.persp { perspective: 1600px; }
.js [data-anim="flip"] {
  opacity: 0;
  transform: rotateY(-78deg) translate3d(0, 0, 0);
  transform-origin: left center;
  backface-visibility: hidden;
}
.js [data-anim="flip-x"] {
  opacity: 0;
  transform: rotateX(72deg);
  transform-origin: center bottom;
}

/* ---------- 6. rules and bars -------------------------------------------- */
.js [data-anim="draw-x"] {
  transform: scaleX(0);
  transform-origin: left center;
}
.js [data-anim="draw-y"] {
  transform: scaleY(0);
  transform-origin: top center;
}

/* ---------- IN state ------------------------------------------------------ */
.js .is-in [data-anim],
.js [data-anim].is-in {
  opacity: 1;
  transform: none;
  clip-path: inset(0 0 0 0);
}
.js .is-in [data-anim^="zoom"] > img,
.js .is-in [data-anim^="zoom"] > picture > img { transform: scale(1); }

/* clip-path variants must not inherit `transform: none` fights — they simply
   have no transform to begin with, so the shared IN rule is enough. */

/* =========================================================================
   Split text — lines / words / characters
   JS wraps the text; the masks and offsets live here.
   ========================================================================= */
.split { display: block; }

/* line masks */
.split .ln {
  display: block;
  overflow: hidden;
  /* a hair of room so descenders (g, y, p) are not shaved by the mask */
  padding-bottom: 0.08em;
  margin-bottom: -0.08em;
}
.js .split .ln-i {
  display: block;
  transform: translate3d(0, 110%, 0);
  transition: transform var(--dur, var(--d-slow)) var(--e-out);
  transition-delay: calc(var(--i, 0) * var(--stagger) + var(--delay, 0ms));
}
.js .is-in .split .ln-i { transform: translate3d(0, 0, 0); }

/* word groups: not animated themselves, they only keep words unbroken */
.split .wg { display: inline; }
.split[data-split="chars"] .wg { display: inline-block; }

/* words + characters */
.js .split .wd,
.js .split .ch {
  display: inline-block;
  opacity: 0;
  transform: translate3d(0, 0.42em, 0);
  transition: opacity var(--dur, var(--d-base)) var(--e-out),
              transform var(--dur, var(--d-base)) var(--e-out);
  transition-delay: calc(var(--i, 0) * var(--step, 40ms) + var(--delay, 0ms));
}
.js .is-in .split .wd,
.js .is-in .split .ch { opacity: 1; transform: none; }

/* =========================================================================
   Split-flap display (airport board)
   A short, finite flip — 5 steps, ~90 ms each — then it stops for good.
   Only transform + opacity, on a handful of small elements.
   ========================================================================= */
.flap {
  display: inline-flex;
  gap: 0.12em;
  font-family: var(--font-display);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
}
.flap__cell {
  position: relative;
  display: inline-block;
  min-width: 0.62em;
  text-align: center;
  perspective: 400px;
}
.flap__char { display: inline-block; backface-visibility: hidden; }
.js .flap[data-flap-state="idle"] .flap__char { opacity: 0; }

/* one flip tick */
@keyframes flap-tick {
  0%   { transform: rotateX(-90deg); opacity: 0.25; }
  60%  { transform: rotateX(8deg);   opacity: 1; }
  100% { transform: rotateX(0deg);   opacity: 1; }
}
.js .flap__char.is-ticking {
  transform-origin: center top;
  animation: flap-tick 170ms var(--e-out) both;
}

/* =========================================================================
   Reduced motion and no-JS: show everything, instantly, always.
   Nothing above is load-bearing for legibility.
   ========================================================================= */
@media (prefers-reduced-motion: reduce) {
  .js [data-anim],
  .js .split .ln-i,
  .js .split .wd,
  .js .split .ch,
  .js [data-anim^="zoom"] > img,
  .js [data-anim^="zoom"] > picture > img {
    opacity: 1 !important;
    transform: none !important;
    clip-path: none !important;
    transition: none !important;
    animation: none !important;
  }
  .js .flap[data-flap-state="idle"] .flap__char { opacity: 1; }
  html { scroll-behavior: auto !important; }
}

/* Safety valve: reveal.js sets this if it ever fails to initialise. */
.no-anim [data-anim],
.no-anim .split .ln-i,
.no-anim .split .wd,
.no-anim .split .ch {
  opacity: 1 !important;
  transform: none !important;
  clip-path: none !important;
}

/* Mobile: same language, shorter throw. Big 3D rotations and long travels
   read as noise on a small screen, so they are softened here. */
@media (max-width: 760px) {
  :root { --rise: 18px; --stagger: 55ms; }
  .js [data-anim="flip"] { transform: rotateY(-45deg); }
  .js [data-anim="flip-x"] { transform: rotateX(45deg); }
  .js [data-anim="from-right"] { transform: translate3d(4%, 0, 0); }
  .js [data-anim="from-left"] { transform: translate3d(-4%, 0, 0); }
  .js [data-anim^="zoom"] > img,
  .js [data-anim^="zoom"] > picture > img { transform: scale(1.05); }
}
