/*
 * Generator animation runtime — the CSS half of the vendored `data-anim`
 * preset vocabulary (see helpers/generator/anim-catalog.js, and anim.js next
 * to this file for the JS half). Vendored, same-origin, no build step.
 *
 * Every rule that hides, moves or loops is gated `html:not(.anim-off) …` so
 * there is exactly one escape hatch, flipped from three different places:
 * anim.js's own reduced-motion bail, and the render contour (helpers/generator/
 * preview-render.js) forcing it before every gen_screenshot/thumbnail/
 * og-cover capture so a full-page shot never freezes mid-hide below the fold.
 * Removing the class is enough on its own — no transition plays and no loop
 * runs, because those rules live under the same gate.
 *
 * That gate is also why the JS half never writes an inline `transform`: an
 * inline style outranks a stylesheet, so a scroll-linked preset that wrote one
 * would survive the class and freeze the render at whatever offset it had
 * reached. `parallax` therefore publishes a custom property and lets the gated
 * rule below decide whether to use it.
 */

/* ── Per-preset timing ─────────────────────────────────────────────────────
 * Ungated: this is a property OF the preset, not of the gate. The two softest
 * reveals run longer, because a blur or a mask wipe reads as a jerk at 450ms.
 */
[data-anim='reveal-blur'] {
  --anim-dur: 700ms;
}

[data-anim='reveal-mask'] {
  --anim-dur: 750ms;
}

[data-anim='reveal-words'] {
  --anim-dur: 500ms;
}

[data-anim='reveal-lines'] {
  --anim-dur: 700ms;
}

/* ── Scroll reveals: the resting state before the observer fires ────────── */

html:not(.anim-off) [data-anim='reveal-up'] {
  opacity: 0;
  transform: translateY(16px);
}

html:not(.anim-off) [data-anim='reveal-fade'] {
  opacity: 0;
}

html:not(.anim-off) [data-anim='reveal-scale'] {
  opacity: 0;
  transform: scale(0.96);
}

html:not(.anim-off) [data-anim='reveal-left'] {
  opacity: 0;
  transform: translateX(-28px);
}

html:not(.anim-off) [data-anim='reveal-right'] {
  opacity: 0;
  transform: translateX(28px);
}

html:not(.anim-off) [data-anim='reveal-blur'] {
  opacity: 0;
  filter: blur(10px);
}

/*
 * reveal-mask keeps full opacity and reveals geometry instead — the point of
 * the move is that the content looks like it was always there, behind an edge.
 *
 * It is a MASK and not a `clip-path`, and that is not a style preference.
 * Chrome shrinks an element's own IntersectionObserver ratio by its clip-path:
 * measured in Chrome 115, `clip-path: inset(0 0 101% 0)` reports
 * intersectionRatio 0.0000 (and even a 99% inset reports 0.0100) while a
 * mask-hidden element reports 1.0000. Any ratio threshold above that therefore
 * NEVER fires, so the hidden state suppressed the very trigger that was meant
 * to undo it and the element stayed invisible forever — silently, since the box
 * still occupies its space and nothing errors. A mask hides the paint and
 * leaves the geometry alone.
 *
 * Both spellings ship: Safari only took the unprefixed mask properties in
 * 15.4, and a published landing has to survive older phones.
 */
html:not(.anim-off) [data-anim='reveal-mask'] {
  -webkit-mask-image: linear-gradient(#000, #000);
  mask-image: linear-gradient(#000, #000);
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: 0 100%;
  mask-position: 0 100%;
  -webkit-mask-size: 100% 0%;
  mask-size: 100% 0%;
}

html:not(.anim-off) [data-anim='reveal-mask'].anim-in {
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
  transition:
    -webkit-mask-size var(--anim-dur, 450ms) cubic-bezier(0.22, 1, 0.36, 1),
    mask-size var(--anim-dur, 450ms) cubic-bezier(0.22, 1, 0.36, 1);
  transition-delay: var(--anim-delay, 0ms);
}

html:not(.anim-off) [data-anim='reveal-up'].anim-in,
html:not(.anim-off) [data-anim='reveal-fade'].anim-in,
html:not(.anim-off) [data-anim='reveal-scale'].anim-in,
html:not(.anim-off) [data-anim='reveal-left'].anim-in,
html:not(.anim-off) [data-anim='reveal-right'].anim-in,
html:not(.anim-off) [data-anim='reveal-blur'].anim-in {
  opacity: 1;
  transform: none;
  filter: none;
  transition:
    opacity var(--anim-dur, 450ms) cubic-bezier(0.22, 1, 0.36, 1),
    transform var(--anim-dur, 450ms) cubic-bezier(0.22, 1, 0.36, 1),
    filter var(--anim-dur, 450ms) cubic-bezier(0.22, 1, 0.36, 1);
  transition-delay: var(--anim-delay, 0ms);
}

/* ── reveal-words ──────────────────────────────────────────────────────────
 * The element itself is never hidden — only the word spans anim.js wraps
 * around its text nodes, each carrying its own --anim-delay. inline-block is
 * ungated because it is layout, not motion: a word box has to be transformable
 * whether or not anything is animating, and switching it per-gate would move
 * the line breaks between a screenshot and the live page.
 */
.anim-word {
  display: inline-block;
}

html:not(.anim-off) [data-anim='reveal-words'] .anim-word {
  opacity: 0;
  transform: translateY(0.35em);
}

html:not(.anim-off) [data-anim='reveal-words'].anim-in .anim-word {
  opacity: 1;
  transform: none;
  transition:
    opacity var(--anim-dur, 500ms) cubic-bezier(0.22, 1, 0.36, 1),
    transform var(--anim-dur, 500ms) cubic-bezier(0.22, 1, 0.36, 1);
  transition-delay: var(--anim-delay, 0ms);
}

/* ── reveal-lines ──────────────────────────────────────────────────────────
 * One mask per rendered line, at full opacity — the line rises from behind its
 * own baseline. The padding/margin pair is not decoration: overflow: hidden on
 * a line box clips descenders, so a headline with a "g" in it loses its tail.
 *
 * The wrapper is display: block on purpose. An inline-block mask would let the
 * browser re-break the line inside a box that was measured for the OLD break,
 * and a headline is the one element on the page where that is unmissable.
 */
.anim-line {
  display: block;
  overflow: hidden;
  padding-bottom: 0.08em;
  margin-bottom: -0.08em;
}

.anim-line-inner {
  display: block;
}

html:not(.anim-off) [data-anim='reveal-lines'] .anim-line-inner {
  transform: translateY(105%);
}

html:not(.anim-off) [data-anim='reveal-lines'].anim-in .anim-line-inner {
  transform: none;
  transition: transform var(--anim-dur, 700ms) cubic-bezier(0.22, 1, 0.36, 1);
  transition-delay: var(--anim-delay, 0ms);
}

/* ── draw ──────────────────────────────────────────────────────────────────
 * anim.js measures every shape and publishes its own length as
 * --anim-draw-len; the dash pattern is that length, so one dash covers the
 * whole stroke and the offset hides it. A missing variable falls back to the
 * property's initial value, which is a perfectly ordinary solid line — so an
 * illustration whose lengths were never measured looks finished rather than
 * blank.
 */
html:not(.anim-off) [data-anim='draw'] :is(path, line, polyline, circle, ellipse, rect, polygon) {
  stroke-dasharray: var(--anim-draw-len, none);
  stroke-dashoffset: var(--anim-draw-len, 0);
}

html:not(.anim-off) [data-anim='draw'].anim-in :is(path, line, polyline, circle, ellipse, rect, polygon) {
  stroke-dashoffset: 0;
  transition: stroke-dashoffset var(--anim-duration, 1600ms) cubic-bezier(0.33, 1, 0.68, 1);
  transition-delay: var(--anim-delay, 0ms);
}

/* ── The scroll-linked family ──────────────────────────────────────────────
 * anim.js publishes exactly ONE number for all of these — `--anim-progress`,
 * 0 when the element sits at the bottom of the viewport and 1 when it has
 * travelled to the top — and these gated rules are what spend it. One engine,
 * one variable: a second measurement of "how far has this scrolled" is a second
 * thing to disagree with itself.
 *
 * Every default below resolves to the resting state, so an element the engine
 * has not reached yet, and any capture with the gate on, looks untouched.
 */
html:not(.anim-off) [data-anim='parallax'] {
  transform: translate3d(0, calc((0.5 - var(--anim-progress, 0.5)) * 2 * var(--anim-strength, 60px)), 0);
  will-change: transform;
}

/*
 * zoom-in is a FRAME preset, like hover-zoom: the media inside scales and the
 * element itself holds still, clipping it. Scaling the element instead would
 * have pushed a full-bleed image 12% past the page edge and handed the visitor
 * a horizontal scrollbar — invisible to us, because a capture with the gate on
 * has no transform at all.
 */
[data-anim='zoom-in'] {
  overflow: hidden;
}

html:not(.anim-off) [data-anim='zoom-in'] :is(img, video, svg, picture) {
  transform: scale(calc(1 + var(--anim-progress, 0) * 0.12));
  transform-origin: center;
  will-change: transform;
}

/* ── pin ───────────────────────────────────────────────────────────────────
 * The section is the runway and its wrapped stage is what sticks, so the hold
 * lasts exactly as long as the height the author gave the section. Ungated
 * layout: with the gate on the stage still renders where it starts, which is a
 * legible capture rather than a collapsed one.
 *
 * Steps sit in NORMAL FLOW and dim — they are not stacked on top of each other.
 * Stacking reads better for one frame and is unreadable in every capture,
 * because forcing the gate would show all of them at full opacity at once.
 */
[data-anim='pin'] > .anim-pin-stage {
  position: sticky;
  top: 0;
  /*
   * grid, NOT flex. The stage wraps whatever the author put in the section, so
   * it may not restyle it — and `display: flex` made every one of those
   * children a flex item in a ROW: three stacked steps came out side by side
   * with their paragraphs run together. A single-column grid centres the group
   * vertically and leaves the children stacked exactly as they were written.
   */
  display: grid;
  align-content: center;
  min-height: 100vh;
  min-height: 100svh;
}

html:not(.anim-off) [data-anim='pin-step'] {
  opacity: 0.35;
  transition: opacity 350ms cubic-bezier(0.22, 1, 0.36, 1);
}

html:not(.anim-off) [data-anim='pin-step'].anim-step-on {
  opacity: 1;
}

/* ── marquee ───────────────────────────────────────────────────────────────
 * overflow and the track's own box are UNGATED layout: with the gate on, the
 * band must still be a clipped, static row rather than a line of logos running
 * off the side of the page. The clip is also what keeps the deliberately
 * over-wide track out of the rendered viewport-overflow check, which skips any
 * element whose ancestor chain hides overflow.
 *
 * The track holds the item set twice over (anim.js doubles it), so translating
 * exactly -50% lands the copy where the original started and the seam is
 * invisible.
 */
[data-anim='marquee'] {
  overflow: hidden;
}

[data-anim='marquee'] .anim-marquee-track {
  display: flex;
  flex: none;
  width: max-content;
}

html:not(.anim-off) [data-anim='marquee'] .anim-marquee-track {
  animation: anim-marquee var(--anim-duration, 28000ms) linear infinite;
  will-change: transform;
}

html:not(.anim-off) [data-anim='marquee'][data-anim-reverse] .anim-marquee-track {
  animation-direction: reverse;
}

@keyframes anim-marquee {
  from {
    transform: translate3d(0, 0, 0);
  }

  to {
    transform: translate3d(-50%, 0, 0);
  }
}

/* ── float ─────────────────────────────────────────────────────────────── */

html:not(.anim-off) [data-anim='float'] {
  animation: anim-float var(--anim-duration, 6000ms) ease-in-out infinite;
}

@keyframes anim-float {
  0%,
  100% {
    transform: translate3d(0, 0, 0);
  }

  50% {
    transform: translate3d(0, -8px, 0);
  }
}

/* ── attention ─────────────────────────────────────────────────────────────
 * Emphasis, not decoration: the element rests for four fifths of the cycle and
 * nudges once. A loop that moves continuously stops reading as "look here" and
 * starts reading as "this page will not sit still".
 */
html:not(.anim-off) [data-anim='attention'] {
  animation: anim-attention var(--anim-duration, 4500ms) ease-in-out infinite;
}

@keyframes anim-attention {
  0%,
  80%,
  100% {
    transform: none;
  }

  86% {
    transform: scale(1.035);
  }

  93% {
    transform: scale(0.994);
  }
}

/* ── Hover presets ─────────────────────────────────────────────────────────
 * Pure CSS, no JS involvement at all. Gated on (hover: hover) so a touch tap
 * never leaves a card stuck mid-lift with no pointer to un-hover it — and NOT
 * gated on .anim-off, because a capture never hovers anything, so there is
 * nothing for the render contour to neutralise.
 *
 * marquee's hover pause lives here for the same reason it exists at all: a
 * band of links that never stops moving cannot be clicked.
 */
@media (hover: hover) {
  [data-anim='hover-lift'] {
    transition:
      transform 250ms cubic-bezier(0.22, 1, 0.36, 1),
      box-shadow 250ms cubic-bezier(0.22, 1, 0.36, 1);
  }

  [data-anim='hover-lift']:hover {
    transform: translateY(-4px);
    box-shadow: 0 12px 24px -12px rgba(0, 0, 0, 0.35);
  }

  /*
   * svg is in the list because the generator's own placeholder media is inline
   * SVG — an img/video-only selector would have made this preset a silent no-op
   * on exactly the markup the section patterns ship.
   */
  [data-anim='hover-zoom'] img,
  [data-anim='hover-zoom'] video,
  [data-anim='hover-zoom'] svg {
    transition: transform 600ms cubic-bezier(0.22, 1, 0.36, 1);
  }

  [data-anim='hover-zoom']:hover img,
  [data-anim='hover-zoom']:hover video,
  [data-anim='hover-zoom']:hover svg {
    transform: scale(1.05);
  }

  [data-anim='marquee']:hover .anim-marquee-track {
    animation-play-state: paused;
  }

  [data-anim='hover-underline']::after {
    transition: transform 320ms cubic-bezier(0.22, 1, 0.36, 1);
  }

  [data-anim='hover-underline']:hover::after {
    transform: scaleX(1);
  }
}

/*
 * hover-underline's resting half lives OUTSIDE the hover query on purpose: the
 * pseudo-element is absolutely positioned, so it costs a touch device nothing,
 * while putting `position: relative` behind a media query would make the
 * element's own stacking context depend on the visitor's input device.
 * currentColor rather than a token: the rule has no business knowing which of
 * the page's colours this particular link is wearing.
 */
[data-anim='hover-underline'] {
  position: relative;
}

[data-anim='hover-underline']::after {
  content: '';
  position: absolute;
  right: 0;
  bottom: -0.12em;
  left: 0;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left center;
}

/* ── disclose ──────────────────────────────────────────────────────────────
 * All this preset needs from CSS is a box that clips while its height is being
 * animated; the animation itself is run by anim.js through the Web Animations
 * API, so no inline height is ever written to the element.
 *
 * The CSS-only version of this — `::details-content` plus `interpolate-size` —
 * was written first and then measured: this box runs Chrome 115, where neither
 * exists, so the preset did precisely nothing on the only browser we can
 * actually look at. A name in the vocabulary that silently does nothing is
 * worse than no name, which is why the mechanism moved rather than shipping
 * with a "your browser may be too old" caveat.
 */
.anim-disclose-body {
  overflow: hidden;
}

/*
 * Belt, not the plan: anim.js already bails to anim-off for this same query.
 * This block covers the gap where JS never ran at all (blocked script, a race
 * before DOMContentLoaded) — a real reduced-motion visitor still gets a fully
 * static, fully visible page either way.
 *
 * It reaches into descendants for the runtime's OWN generated pieces only.
 * `[data-anim] *` would have been the short way to write it and would also
 * have flattened a rotated badge or a scaled logo the page meant to keep.
 */
@media (prefers-reduced-motion: reduce) {
  [data-anim],
  [data-anim] .anim-word,
  [data-anim] .anim-line-inner,
  [data-anim] .anim-marquee-track {
    opacity: 1 !important;
    transform: none !important;
    filter: none !important;
    clip-path: none !important;
    -webkit-mask-image: none !important;
    mask-image: none !important;
    transition: none !important;
    animation: none !important;
  }

  /*
   * The stroke pair gets its own rule rather than joining the list above: an
   * SVG shape routinely carries a `transform` that is part of the drawing, and
   * flattening that with !important would take the illustration apart in the
   * name of holding it still.
   */
  [data-anim='draw'] :is(path, line, polyline, circle, ellipse, rect, polygon) {
    stroke-dasharray: none !important;
    stroke-dashoffset: 0 !important;
  }
}
