/**
 * GEBA Group — motion.
 *
 * Keyframes and the scroll-reveal system, ported from the design reference.
 * Every duration, easing and distance comes from a token; nothing here
 * hard-codes timing.
 *
 * The reduced-motion contract has two halves, and both are ported verbatim:
 *   1. assets/css/tokens.css flattens --dur-* to 1ms and zeroes --lift-card
 *      and --reveal-distance, which neutralises every transition;
 *   2. the block at the bottom of this file stops the looping animations
 *      outright, because a 1ms infinite animation still moves.
 */

/* ------------------------------------------------------------- keyframes */

@keyframes gebaFloat {
  0%,
  100% {
    transform: translateY(0);
  }
  50% {
    transform: translateY(-14px);
  }
}

@keyframes gebaMarquee {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(-50%);
  }
}

@keyframes gebaSpin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

@keyframes gebaDrift {
  0%,
  100% {
    transform: scale(1.03);
  }
  50% {
    transform: scale(1.1) translateY(-1.6%);
  }
}

/* ------------------------------------------------------- looping motion */

[data-float] {
  animation: gebaFloat 6s var(--ease-in-out) infinite;
}

[data-spin] {
  animation: gebaSpin 18s var(--ease-linear) infinite;
}

[data-marquee] {
  animation: gebaMarquee 40s var(--ease-linear) infinite;
}

[data-drift] {
  animation: gebaDrift 14s var(--ease-in-out) infinite;
}

/*
 * The section components animate by class rather than by data attribute,
 * because a pattern sets classes on core blocks and cannot add attributes.
 * They are declared here rather than in sections.css so that the
 * reduced-motion block below stays the single place that switches motion off —
 * a looping animation declared elsewhere would quietly escape it.
 */
.geba-eyebrow__mark {
  animation: gebaSpin 11s var(--ease-linear) infinite;
}

.geba-hero__floater {
  animation: gebaFloat 8s var(--ease-standard) infinite;
}

.geba-hero__floater--two {
  animation-duration: 9.5s;
  animation-delay: 1.2s;
}

.geba-welcome__photo img,
.geba-service-intro__photo img {
  animation: gebaFloat 7s var(--ease-standard) infinite;
}

/*
 * The marquee only runs once `motion.js` has appended the second copy of the
 * list, which is what `is-looping` says. `gebaMarquee` travels -50%, so on a
 * single copy it would run the row off to the left and snap back to the start
 * — a visible jump with empty band behind it. Ungated, that is what it did.
 *
 * With no JavaScript the band is a static row of logos, which is a finished
 * section rather than a broken animation. Same contract as the projects rail.
 *
 * The duration is per logo rather than per track, so the row keeps one speed
 * as partners are added; `motion.js` writes the count.
 */
.geba-marquee.is-looping .geba-marquee__track {
  animation: gebaMarquee calc(var(--geba-marquee-count, 9) * 3.6s)
    var(--ease-linear) infinite;
}

/* ------------------------------------------------------- authored motion */

/*
 * What the plugin's Animation panel writes, and what it means.
 *
 * There are two independent things a block can ask for, and they are deliberately
 * separate:
 *
 *   **an arrival** — `data-geba-motion="<effect>"`, played once when the element
 *   reaches the viewport, using `opacity`, `translate`, `scale` and `clip-path`;
 *
 *   **a loop** — `data-geba-motion-loop="<motion>"`, running for as long as the
 *   page is open, using `transform` and nothing else.
 *
 * Keeping them on different properties is what lets a card fade up *and* then
 * float: `translate` and `transform` are two properties, and each animation owns
 * only what it names. It is also why an arrival is a transition and a loop is a
 * keyframe animation — a transition cannot repeat, and an animation would seize
 * the property it animates for as long as it ran.
 *
 * ── Where to change this ─────────────────────────────────────────────────
 *
 * **Globally**, for the whole site: the `:root` block immediately below. It is
 * the one place every default lives — how long an arrival takes, how far it
 * travels, how hard it eases, how slowly a loop breathes. Four of those read
 * tokens from `theme.json` (`custom.dur.reveal`, `custom.motion.revealDistance`,
 * `custom.ease.outExpo`, `custom.ease.inOut`), so changing the token changes the
 * reveal *and* everything else timed with it; changing the `--geba-motion-*`
 * line here changes only motion.
 *
 * **Per rhythm**, so a site of many hands stays coherent: the three presets
 * under "presets" below. `data-geba-motion-preset="calm"` on a block sets its
 * duration and stagger together, and the number fields in the panel override it
 * because an inline style beats an attribute selector.
 *
 * **Per pattern**, as a default the client can then override: the block
 * attributes in `patterns/*.php` — `{"gebaMotion":"rise","gebaMotionDelay":120}`
 * and so on. Reaches the five composed pages only after `seed-pages --force`.
 *
 * **Per block**, in the editor: the Animation panel, with a Predvajaj button
 * that plays the arrival in the canvas. Nothing there is capped — a number typed
 * in is the number used.
 *
 * ── How an arrival behaves ───────────────────────────────────────────────
 *
 * A block either arrives itself or staggers its children, never both, which
 * would fade a whole band in and then fade every card inside it in again. That
 * is what the `:not([data-geba-motion-stagger])` half of every selector says.
 * The children are hidden by the *parent's* attribute rather than by one of
 * their own, so the hidden state is already right at the first paint;
 * JavaScript only numbers them, and a child that is never numbered simply
 * arrives with its siblings.
 *
 * An arrival is scoped to `.geba-js`, which an inline script in the head sets
 * before the first paint -- set any later and the page paints visible and then
 * hides -- and which the same script removes again if `reveal.js` never runs. So
 * a blocked or broken script leaves every element visible where it is. A loop is
 * *not* scoped that way: it hides nothing, so it has nothing to fail safe from.
 */
:root {
  --geba-motion-index: 0;
  --geba-motion-delay: 0ms;
  --geba-motion-stagger: 0ms;
  --geba-motion-duration: 2000ms; /*var(--dur-reveal);*/
  --geba-motion-distance: 80px; /*var(--reveal-distance);*/
  --geba-motion-scale: 0.94;
  --geba-motion-ease: var(--ease-out-expo);
  --geba-motion-loop-duration: 6s;
  /*
   * The arrival's own transition, named so a component that has a transition
   * of its own can restate it rather than lose it. `transition` is a single
   * property: the last declaration to win replaces the whole list, so a card
   * inside a staggered block that declares a hover transition silently drops
   * the arrival's, and one that does not declare anything silently drops its
   * hover. Compose both — see `.geba-handoff` in sections.css.
   */
  --geba-motion-transition:
    opacity var(--geba-motion-duration) var(--geba-motion-ease),
    translate var(--geba-motion-duration) var(--geba-motion-ease),
    scale var(--geba-motion-duration) var(--geba-motion-ease),
    clip-path var(--geba-motion-duration) var(--geba-motion-ease);
  /*
   * How far the mask's clip reaches *outside* the element it reveals. A
   * clip-path clips the box shadow too, and `--shadow-xl` is `0 36px 88px`, so
   * anything under 124px cuts one off. See the mask effect below.
   */
  --geba-motion-mask-bleed: 140px;
}

/* ------------------------------------------------------------- presets */

/*
 * A rhythm, in one choice.
 *
 * Six number fields on twenty-eight block types is a lot of rope: a site where
 * one heading rises in 700ms and the next in 1150ms because somebody typed it
 * looks broken in a way nobody can point at. A preset sets the two numbers that
 * carry the feel — duration and stagger — so the common case is one decision and
 * the numbers are there for when the common case is wrong.
 */
[data-geba-motion-preset="calm"] {
  --geba-motion-duration: 1100ms;
  --geba-motion-stagger: 140ms;
}

[data-geba-motion-preset="brisk"] {
  --geba-motion-duration: 420ms;
  --geba-motion-stagger: 60ms;
}

/* ------------------------------------------------------------ arrivals */

.geba-js [data-geba-motion]:not([data-geba-motion-stagger]),
.geba-js [data-geba-motion][data-geba-motion-stagger] > * {
  opacity: 0;
  transition: var(--geba-motion-transition);
  transition-delay: calc(
    var(--geba-motion-delay) + var(--geba-motion-index) *
      var(--geba-motion-stagger)
  );
  will-change: opacity, translate;
}

.geba-js [data-geba-motion].is-revealed:not([data-geba-motion-stagger]),
.geba-js [data-geba-motion].is-revealed[data-geba-motion-stagger] > * {
  opacity: 1;
  translate: none;
  scale: none;
  will-change: auto;
}

/*
 * An arrival must not take the mobile navigation with it.
 *
 * The header is an arriving element — `fade`, on load — and core's overlay is a
 * `position: fixed` descendant of it. Two things follow from that, and both
 * happen on a first load and nowhere else, because the window closes the moment
 * `reveal.js` reports in.
 *
 * The header's opacity applies to the whole panel, so the menu opens dimmed:
 * measured at 0.22 part-way through the fade, which leaves the bold menu text
 * legible and makes the close button, a hairline X, the first thing to vanish.
 * And the hidden state declares `will-change: opacity, translate` whatever the
 * effect asked for — `translate` there makes the header a containing block for
 * fixed descendants, so the panel is laid out against the header's box instead
 * of the viewport.
 *
 * While the menu is open the header is settled, whatever its arrival is doing.
 * (0,4,0) against the hidden state's (0,3,0), and `:has()` is cheap here because
 * it only ever matches one element.
 */
.geba-js
  [data-geba-motion]:has(
    .wp-block-navigation__responsive-container.is-menu-open
  ) {
  opacity: 1;
  translate: none;
  scale: none;
  clip-path: none;
  will-change: auto;
  /* Snap, rather than fading the panel up from wherever the arrival had got
     to — the menu was opened, it is not arriving. */
  transition: none;
}

/*
 * Landed on rather than scrolled to.
 *
 * `geba_print_landing_reveal()` marks anything already in the viewport when a
 * page loads part-way down — a reload with the scroll restored, a back, a `#`.
 * That content has no arrival to play, and playing one would mean two seconds
 * of fade over something the visitor is already looking at.
 *
 * The selector is the arrival's own, exactly, for the reason given under
 * reduced motion below: the element when it arrives itself, each child only
 * when the block staggers them.
 */
.geba-js [data-geba-motion].is-landed:not([data-geba-motion-stagger]),
.geba-js [data-geba-motion].is-landed[data-geba-motion-stagger] > * {
  transition: none;
}

/*
 * `translate` and `scale` as their own properties, never `transform`: an arrival
 * has to compose with a hover, with a looping motion and with the keyframes the
 * design already runs on a few elements. `transform` is one property and they
 * would take turns owning it; these are three and they do not.
 */

.geba-js [data-geba-motion="rise"]:not([data-geba-motion-stagger]),
.geba-js [data-geba-motion="rise"][data-geba-motion-stagger] > * {
  translate: 0 var(--geba-motion-distance);
}

.geba-js [data-geba-motion="sink"]:not([data-geba-motion-stagger]),
.geba-js [data-geba-motion="sink"][data-geba-motion-stagger] > * {
  translate: 0 calc(var(--geba-motion-distance) * -1);
}

.geba-js [data-geba-motion="left"]:not([data-geba-motion-stagger]),
.geba-js [data-geba-motion="left"][data-geba-motion-stagger] > * {
  translate: calc(var(--geba-motion-distance) * -1) 0;
}

.geba-js [data-geba-motion="right"]:not([data-geba-motion-stagger]),
.geba-js [data-geba-motion="right"][data-geba-motion-stagger] > * {
  translate: var(--geba-motion-distance) 0;
}

.geba-js [data-geba-motion="scale"]:not([data-geba-motion-stagger]),
.geba-js [data-geba-motion="scale"][data-geba-motion-stagger] > * {
  scale: var(--geba-motion-scale);
}

/*
 * The one effect that is not a fade. The element is fully opaque and arrives by
 * uncovering, which is what the reference does to its photographs.
 *
 * It is also the one that cannot be watched for: an element clipped to nothing
 * reports an empty intersection rectangle and never intersects, so `reveal.js`
 * observes its parent instead.
 */
/*
 * The clip is inset by a negative amount on the three edges it does not sweep,
 * and the sweep runs to the same negative amount — so the revealed state is
 * `inset(-140px)` rather than `inset(0)`.
 *
 * `inset(0)` looks like "no clip" and is not. A clip-path clips an element's
 * own box shadow, and it never goes away: the mask on the service page's photo
 * column left a permanent `inset(0)`, which cut the shadows off the two detail
 * photographs sitting at `left: 0` and `bottom: 0` inside it, and squared off
 * the main photograph's own shadow. It looked like a bug in the layout rather
 * than in the animation, because by the time anybody looked the animation had
 * finished. Nothing on the page announces a clip-path.
 *
 * The hidden state's bottom is `calc(100% + bleed)` rather than `100%`, so that
 * the empty region collapses to nothing at the *top* of the bled box instead of
 * leaving a bleed-tall strip of shadow showing before the reveal starts.
 */
.geba-js [data-geba-motion="mask"]:not([data-geba-motion-stagger]),
.geba-js [data-geba-motion="mask"][data-geba-motion-stagger] > * {
  opacity: 1;
  clip-path: inset(
    calc(var(--geba-motion-mask-bleed) * -1)
      calc(var(--geba-motion-mask-bleed) * -1)
      calc(100% + var(--geba-motion-mask-bleed))
      calc(var(--geba-motion-mask-bleed) * -1)
  );
}

.geba-js [data-geba-motion="mask"].is-revealed:not([data-geba-motion-stagger]),
.geba-js [data-geba-motion="mask"].is-revealed[data-geba-motion-stagger] > * {
  clip-path: inset(calc(var(--geba-motion-mask-bleed) * -1));
}

/* --------------------------------------------------------------- loops */

/*
 * Motion that does not stop.
 *
 * The panel used to offer an arrival that could play twice, which is a strange
 * thing to want: the element arrives, and then arrives again. What is actually
 * wanted when somebody asks for a repeat is an ornament that keeps moving, and
 * this is that — the four motions the reference already uses, offered as a
 * choice instead of hard-wired to five class names.
 *
 * Every one of them animates `transform` and only `transform`, so a loop and an
 * arrival on the same element compose instead of fighting: the arrival owns
 * `opacity`, `translate` and `scale`, the loop owns `transform`.
 *
 * They are motion nobody dismissed, so keep them on ornaments — a mark, an icon,
 * a badge — and off anything anyone has to read. `prefers-reduced-motion` stops
 * them outright at the foot of this file.
 */
@keyframes gebaLoopFloat {
  0%,
  100% {
    transform: translateY(0);
  }
  50% {
    transform: translateY(-10px);
  }
}

@keyframes gebaLoopPulse {
  0%,
  100% {
    transform: scale(1);
  }
  50% {
    transform: scale(1.04);
  }
}

@keyframes gebaLoopSway {
  0%,
  100% {
    transform: translateX(0);
  }
  50% {
    transform: translateX(8px);
  }
}

@keyframes gebaLoopSpin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

[data-geba-motion-loop] {
  animation-duration: var(--geba-motion-loop-duration);
  animation-iteration-count: infinite;
  animation-timing-function: var(--ease-in-out);
}

[data-geba-motion-loop="float"] {
  animation-name: gebaLoopFloat;
}

[data-geba-motion-loop="pulse"] {
  animation-name: gebaLoopPulse;
}

[data-geba-motion-loop="sway"] {
  animation-name: gebaLoopSway;
}

[data-geba-motion-loop="spin"] {
  animation-name: gebaLoopSpin;
  animation-timing-function: var(--ease-linear);
}

/* -------------------------------------------------------------- parallax */

/*
 * The heroes' photographs drift against the page as it scrolls.
 *
 * A loop rather than an arrival, so it owns `transform` and nothing else. That
 * is what lets it compose with the home hero's cross-fade, which is already
 * animating `opacity` on the same elements, and with the dimming `filter` both
 * heroes carry — see the note at the top of the authored section.
 *
 * Scroll-driven, so there is no scroll handler and nothing for JavaScript to
 * do: `view()` maps the band's own pass through the viewport onto the
 * keyframes and the browser runs it off the main thread. Where it is not
 * supported the whole block is dropped, the keyframes never attach, and the
 * photograph sits exactly where it did before — the same fail-safe shape as
 * everything else here.
 *
 * The scale is what makes the drift possible. The photograph is 14% taller
 * than its frame, so 6% of travel each way never brings an edge into view, and
 * both heroes are `overflow: hidden` for the rest.
 */
:root {
  --geba-parallax-shift: 20%;
  --geba-parallax-scale: 1.14;
}

@keyframes gebaParallax {
  from {
    transform: translate3d(0, calc(var(--geba-parallax-shift) * -1), 0)
      scale(var(--geba-parallax-scale));
  }

  to {
    transform: translate3d(0, var(--geba-parallax-shift), 0)
      scale(var(--geba-parallax-scale));
  }
}

@supports (animation-timeline: view()) {
  /*
   * The timeline is named on the band, not taken with `view()` on the
   * photograph.
   *
   * `view()` resolves its scroller to the subject's nearest ancestor scroll
   * container, and both heroes are `overflow: hidden` — which makes them scroll
   * containers even though nobody can scroll them. The range then has no
   * length, progress pins at 0.5 whatever the page does, and the photograph
   * sits scaled up and perfectly still. It looks like a stylesheet that took
   * effect. Named on the band, whose own nearest scroller is the document, it
   * measures the band's pass through the viewport as intended.
   */
  .geba-hero,
  .geba-hero-page {
    view-timeline-name: --geba-hero;
  }

  .geba-hero .wp-block-cover__image-background,
  .geba-hero__slide img,
  .geba-hero-page .wp-block-cover__image-background {
    /*
     * Longhands, not the shorthand: `animation: gebaParallax linear both`
     * would set `animation-duration` to `0s`, and a scroll-driven animation
     * needs `auto` to take its length from the timeline instead.
     */
    animation-name: gebaParallax;
    animation-duration: auto;
    animation-timing-function: linear;
    animation-fill-mode: both;
    animation-timeline: --geba-hero;
    animation-range: cover 0% cover 100%;
  }
}

/* ------------------------------------------------------- reduced motion */

/*
 * Ported verbatim from the reference. The token flattening in tokens.css
 * handles transitions; this stops the infinite animations, which 1ms
 * durations would not.
 */
@media (prefers-reduced-motion: reduce) {
  [data-float],
  [data-marquee],
  [data-spin],
  [data-drift],
  [data-geba-motion-loop],
  .geba-eyebrow__mark,
  .geba-hero__floater,
  .geba-welcome__photo img,
  .geba-service-intro__photo img,
  .geba-marquee__track,
  .geba-hero .wp-block-cover__image-background,
  .geba-hero__slide img,
  .geba-hero-page .wp-block-cover__image-background {
    animation: none !important;
  }

  /*
   * `!important` rather than source order: the effect rules above are more
   * specific than this one, and a media query adds no specificity of its own.
   * Reduced motion has to win outright.
   *
   * The selector is the arrival's own, exactly — the element when it arrives
   * itself, each child only when the block staggers them. A blanket
   * `[data-geba-motion] > *` reaches children that were never hidden and
   * overrides properties that have nothing to do with motion: it forced the
   * footer's watermark, an `opacity: 0.04` decoration, to full opacity the
   * moment the footer was given a fade.
   */
  .geba-js [data-geba-motion]:not([data-geba-motion-stagger]),
  .geba-js [data-geba-motion][data-geba-motion-stagger] > * {
    opacity: 1 !important;
    transform: none !important;
    translate: none !important;
    scale: none !important;
    clip-path: none !important;
    transition: none !important;

    /*
     * Nothing here is going to change, so promoting each of these to its own
     * compositor layer costs memory and buys nothing — and a promoted layer
     * renders its text without subpixel antialiasing, which moved a glyph edge
     * on every screen in the visual gate.
     */
    will-change: auto !important;
  }
}

/* --------------------------------------------------------------- print */

/*
 * A page arrives once, on screen. On paper there is no scroll and no
 * observer, so everything below where the reader had got to is still in its
 * hidden state — and `opacity: 0` prints as blank paper. `geba/print-button`
 * makes that reachable in one press, which is exactly how it would have been
 * found by a client rather than by us.
 *
 * The selector is the arrival's own, for the reason spelled out under reduced
 * motion above: a blanket `[data-geba-motion] > *` overrides decoration that
 * was never part of an arrival.
 */
@media print {
  .geba-js [data-geba-motion]:not([data-geba-motion-stagger]),
  .geba-js [data-geba-motion][data-geba-motion-stagger] > * {
    opacity: 1 !important;
    transform: none !important;
    translate: none !important;
    scale: none !important;
    clip-path: none !important;
    transition: none !important;
    animation: none !important;
  }

  [data-geba-motion-loop],
  [data-float],
  [data-marquee],
  [data-spin],
  [data-drift],
  .geba-hero .wp-block-cover__image-background,
  .geba-hero__slide img,
  .geba-hero-page .wp-block-cover__image-background {
    animation: none !important;
  }
}
