/* motion.css — scroll reveals, parallax hooks, page transitions. Owner piece: "scroll motion" */

/* Reveal: opacity + short rise, driven by IntersectionObserver in motion.js.
   Elements stay visible if JS never runs — the .js-on guard on <html> does that. */
.js-on [data-reveal] {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity .9s var(--ease), transform .9s var(--ease);
  transition-delay: var(--d, 0ms);
}
.js-on [data-reveal].in { opacity: 1; transform: none; }

/* Clip reveal for images — the frame opens, the image settles from a slight scale.
   The clip lives on the media child, never on the observed element itself: a
   clip-path of inset(0 0 100% 0) zeroes the element's own intersection rect, so
   the IntersectionObserver in motion.js would never fire and the reveal would
   deadlock into a permanent blank band. */
.js-on [data-reveal="clip"] { opacity: 1; transform: none; }
.js-on [data-reveal="clip"] > img,
.js-on [data-reveal="clip"] > video,
.js-on [data-reveal="clip"] > iframe {
  clip-path: inset(0 0 100% 0);
  transform: scale(1.08);
  transition: clip-path 1.1s var(--ease), transform 1.4s var(--ease);
  transition-delay: var(--d, 0ms);
}
.js-on [data-reveal="clip"].in > img,
.js-on [data-reveal="clip"].in > video,
.js-on [data-reveal="clip"].in > iframe { clip-path: inset(0 0 0 0); transform: scale(1); }

/* Line reveal for display type — each line rises from its own mask. */
.line-mask { overflow: hidden; display: block; }
/* The hidden offset has to clear the mask's PADDING box, not the span. The mask
   carries 0.22em of extra clip below its content (type.css) to let descenders
   through; 105% of the span alone stops short of that, and the tops of the
   letters showed above the line before the reveal fired. The em term pays for
   the padding and for the negative half-leading a sub-1 line-height puts above
   the box. */
.js-on .line-mask > span {
  display: block;
  transform: translateY(calc(105% + 0.28em));
  transition: transform 1.05s var(--ease);
  transition-delay: var(--d, 0ms);
}
.js-on .line-mask.in > span { transform: none; }

/* Parallax: motion.js writes --py from scroll position. */
[data-parallax] { will-change: transform; }
.js-on [data-parallax] > img,
.js-on [data-parallax] > video { transform: translate3d(0, var(--py, 0px), 0) scale(1.14); }

/* Progress rail at the top of the document. */
.scroll-rail {
  position: fixed; inset: 0 0 auto 0; z-index: 110; height: 2px;
  background: transparent; pointer-events: none;
}
.scroll-rail i {
  display: block; height: 100%; width: 100%; background: var(--ink);
  transform: scaleX(var(--p, 0)); transform-origin: left;
}
.nav:not(.solid) ~ .scroll-rail i { background: var(--paper); }

/* Cursor companion — a small ring that grows over media. Pointer devices only. */
.cursor {
  position: fixed; top: 0; left: 0; z-index: 130; pointer-events: none;
  width: 26px; height: 26px; margin: -13px 0 0 -13px; border-radius: 50%;
  border: 1px solid var(--ink); mix-blend-mode: difference;
  transform: translate3d(var(--cx, -100px), var(--cy, -100px), 0) scale(var(--cs, 1));
  transition: transform .16s linear, opacity .3s;
  opacity: 0;
}
.cursor.on { opacity: .85; }
@media (hover: none), (pointer: coarse) { .cursor { display: none; } }

@media (prefers-reduced-motion: reduce) {
  .js-on [data-reveal],
  .js-on [data-reveal="clip"],
  .js-on [data-reveal="clip"] > img,
  .js-on [data-reveal="clip"] > video,
  .js-on [data-reveal="clip"] > iframe,
  .js-on .line-mask > span {
    opacity: 1 !important; transform: none !important; clip-path: none !important;
    transition: none !important;
  }
  .js-on [data-parallax] > img, .js-on [data-parallax] > video { transform: none; }
  .cursor { display: none; }
}
