/* ==========================================================================
   Motion system — reusable reveal utilities.

   Contract: markup carries `data-reveal` (and optional `data-reveal-delay`);
   JS flips `data-revealed` when the element enters view. CSS owns the actual
   animation. That split keeps JS out of style decisions and means every
   reveal is one compositor-friendly transform/opacity change.

   PERFORMANCE
   Only transform, opacity and clip-path are animated — never filter,
   box-shadow, width/height or colour on large surfaces.

   `will-change` is intentionally NOT set on [data-reveal]. Promoting 35 idle
   elements to their own layers costs real memory and can itself cause jank;
   the browser already handles a one-shot transform/opacity transition well.
   It is applied only to the handful of elements that animate continuously.
   ========================================================================== */

/* --- Base reveal -------------------------------------------------------- */
[data-reveal] {
  opacity: 0;
  transition:
    opacity var(--d-slow) var(--ease-out-expo),
    transform var(--d-slow) var(--ease-out-expo),
    clip-path var(--d-slow) var(--ease-out-expo);
  transition-delay: var(--reveal-delay, 0ms);
}

[data-reveal][data-revealed] {
  opacity: 1;
  transform: none;
  clip-path: inset(0 0 0 0);
}

/* --- Variants ----------------------------------------------------------- */
[data-reveal='up'] {
  transform: translate3d(0, 2.25rem, 0);
}

[data-reveal='up-sm'] {
  transform: translate3d(0, 1rem, 0);
}

[data-reveal='left'] {
  transform: translate3d(-2.25rem, 0, 0);
}

[data-reveal='right'] {
  transform: translate3d(2.25rem, 0, 0);
}

[data-reveal='fade'] {
  transform: none;
}

[data-reveal='scale'] {
  transform: scale(0.96);
}

/* Wipe reveal — the mask travels, the content does not move. Reads as
   "printed into place" rather than "slid in". */
[data-reveal='mask'] {
  clip-path: inset(0 0 100% 0);
  transform: none;
  opacity: 1;
}

[data-reveal='mask-left'] {
  clip-path: inset(0 100% 0 0);
  transform: none;
  opacity: 1;
}

/* Image reveal — mask travels while the image counter-scales, so the frame
   uncovers a picture that is already settling. */
[data-reveal='image'] {
  clip-path: inset(0 0 100% 0);
  opacity: 1;
  transform: none;
}

[data-reveal='image'] > img,
[data-reveal='image'] > picture > img {
  transform: scale(1.08);
  transition: transform calc(var(--d-slow) * 1.7) var(--ease-out-expo);
  transition-delay: var(--reveal-delay, 0ms);
}

[data-reveal='image'][data-revealed] > img,
[data-reveal='image'][data-revealed] > picture > img {
  transform: scale(1);
}

/* --- Rule that draws itself in ------------------------------------------ */
/* Used on section heads and the footer so the lower half of the page has the
   same sense of construction as the top. */
[data-reveal-rule] {
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--d-slower) var(--ease-out-expo);
  transition-delay: var(--reveal-delay, 0ms);
}

[data-revealed] [data-reveal-rule],
[data-reveal-rule][data-revealed] {
  transform: scaleX(1);
}

/* --- Staggered line/word reveal ----------------------------------------- */
/* Each line sits in an overflow-hidden frame and rises into it. JS wraps the
   text; the delay is set per line via a custom property. */
.stagger__line {
  display: block;
  overflow: hidden;

  /* Headroom above the text for diacritics.

     Display type is set at --lh-tight, which makes the line box SHORTER than
     the glyphs it contains (0.92em box, ~1.0em of ink). This frame masks to
     that box, so with no padding it slices Serbian accented capitals
     (Š Č Ć Ž) clean off. English never showed it — nothing reaches up there.

     TOP ONLY, and the negative margin must match it exactly.

     A symmetric padding-block/margin-block version of this rule silently made
     every multi-line heading taller, which pushed the Inquiry form off the
     one-screen fit that §20 requires. The reason is margin collapsing:
     between two lines the -0.35em bottom and -0.35em top margins collapse to
     a SINGLE -0.35em, while the paddings add a full 0.70em, so only half the
     space was cancelled. Padding on one edge has no adjacent margin to
     collapse with, so it cancels exactly and heights are preserved.

     0.28em is far more than the ~0.09em the accents need; the region is
     transparent so the extra costs nothing. Descenders need no room here —
     they already sit inside the line box. Do not add padding-bottom. */
  padding-top: 0.28em;
  margin-top: -0.28em;
}

.stagger__inner {
  display: block;
  transform: translate3d(0, 105%, 0);
  transition: transform var(--d-slower) var(--ease-out-expo);
  transition-delay: var(--line-delay, 0ms);
}

[data-revealed] .stagger__inner,
.stagger[data-revealed] .stagger__inner {
  transform: translate3d(0, 0, 0);
}

/* --- Keyframes ---------------------------------------------------------- */
@keyframes ss-fade-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes ss-scroll-hint {
  0%,
  100% {
    transform: translateY(0);
    opacity: 0.45;
  }
  50% {
    transform: translateY(0.4rem);
    opacity: 1;
  }
}

/* Slow drift for the intro's ambient field. Transform only — the softness
   comes from the gradient itself, never from an animated filter. */
@keyframes ss-drift {
  0% {
    transform: translate3d(-2%, 0, 0) scale(1);
  }
  50% {
    transform: translate3d(2%, -1.5%, 0) scale(1.08);
  }
  100% {
    transform: translate3d(-2%, 0, 0) scale(1);
  }
}

/* --- Reduced motion ------------------------------------------------------
   Everything above degrades to "already visible". The reset kills the
   durations; this makes sure nothing is left stuck in a pre-reveal state
   (translated off, clipped to zero height, or scaled flat).
   ------------------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
  [data-reveal],
  [data-reveal][data-revealed] {
    opacity: 1 !important;
    transform: none !important;
    clip-path: none !important;
  }

  [data-reveal='image'] > img,
  [data-reveal='image'] > picture > img {
    transform: none !important;
  }

  [data-reveal-rule] {
    transform: none !important;
  }

  .stagger__inner {
    transform: none !important;
  }

  .stagger__line {
    overflow: visible;
  }
}

/* ==========================================================================
   Page transitions

   Content lifts and fades on the way out, settles in from just below on the
   way in, and a blue hairline sweeps the top edge — the same gesture as the
   intro's wipe, at a fraction of the scale.

   Only opacity and transform animate, and only on <main> and the footer:
   the nav is deliberately excluded so the header appears to stay put while
   the page changes beneath it.
   ========================================================================== */
body[data-page-enter] main,
body[data-page-enter] .footer {
  animation: ss-page-in var(--d-slow) var(--ease-out-expo) both;
}

body[data-page-leave] main,
body[data-page-leave] .footer {
  opacity: 0;
  transform: translate3d(0, -0.6rem, 0);
  transition:
    opacity 260ms var(--ease-out-quart),
    transform 260ms var(--ease-out-quart);
}

@keyframes ss-page-in {
  from {
    opacity: 0;
    transform: translate3d(0, 0.75rem, 0);
  }
  to {
    opacity: 1;
    transform: translate3d(0, 0, 0);
  }
}

/* The sweep. A fixed hairline above everything, scaled from the left. */
body::after {
  content: '';
  position: fixed;
  inset: 0 0 auto 0;
  height: 2px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left;
  z-index: calc(var(--z-skip) + 1);
  pointer-events: none;
}

body[data-page-leave]::after {
  transform: scaleX(1);
  transition: transform 260ms var(--ease-out-quart);
}

/* An animated navigation is a convenience, never a requirement: with reduced
   motion the module does not intercept clicks at all, and these guards make
   sure no page can be left mid-fade. */
@media (prefers-reduced-motion: reduce) {
  body[data-page-enter] main,
  body[data-page-enter] .footer,
  body[data-page-leave] main,
  body[data-page-leave] .footer {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }

  body::after {
    display: none;
  }
}
