/* GIB — motion.
 *
 * Transitions and animations for the public site, in CSS.
 *
 * ── WHY CSS, STATED CORRECTLY THIS TIME ──────────────────────────────────
 * An earlier version of this comment justified CSS-only by pointing at
 * `script-src 'none'` on `simplebackend.dev/t/gib/`. That was wrong, and
 * worth correcting rather than quietly deleting: that host is a PREVIEW host.
 * The site customers reach has no platform policy at all (swarm#98), it
 * carries a tag manager, and it will carry more scripts as marketing needs
 * them. Letting a review environment dictate production
 * architecture is backwards.
 *
 * The real reasons this layer is CSS are better ones:
 *
 *   The browser runs it off the main thread. A scroll handler animating
 *   opacity competes with GTM, GA4 and every conversion pixel for the same
 *   thread — and on a marketing page those are not optional, so the frame
 *   budget left for motion is small and shrinking.
 *
 *   It cannot be broken by a tag. A third-party script that throws takes
 *   every script after it down with it; it cannot take a stylesheet down.
 *
 *   It needs no wiring when a page is added, because it is selectors.
 *
 * None of which forbids JavaScript for motion something specific needs. If a
 * landing page wants a scroll-linked timeline no CSS feature can express,
 * write it — this file is the floor, not a ceiling.
 *
 * ── WHAT WAS ALREADY HERE ────────────────────────────────────────────────
 * rebrand.css animates `.animate .section__item` and friends with a
 * staggered `slidein`, and effects.js swaps `.hidden` for `.animate` from an
 * unthrottled scroll listener. That works, and it stays for browsers that
 * need it — but it has three faults this file fixes:
 *
 *   1. NO REDUCED-MOTION SUPPORT ANYWHERE. Neither rebrand.css nor site.css
 *      contained a single `prefers-reduced-motion` query, so a visitor who
 *      has asked their operating system to stop animating things got the
 *      full slide-in regardless. For some people that is not a preference,
 *      it is nausea or a migraine.
 *   2. A scroll handler that runs on every scroll event, and captures its
 *      element list once at init.
 *   3. `slidein` begins at `opacity: 0`, so the moment the class is swapped
 *      an already-visible item jumps to invisible and slides back in.
 *
 * ── THE TWO FEATURES DOING THE WORK ──────────────────────────────────────
 * `@view-transition` gives page-to-page transitions on a MULTI-PAGE site.
 * That is worth saying plainly: this only works because the site is
 * server-rendered. A client-rendered site has to reimplement it in a router.
 *
 * `animation-timeline: view()` reveals an element as it enters the viewport
 * with no scroll listener, no IntersectionObserver and no jank.
 *
 * ── AND THE TRAP IN BOTH, WHICH IS THE POINT OF THE @supports ────────────
 * Neither is universal — Chrome and Safari have them, Firefox does not yet.
 * A reveal is `opacity: 0` plus an animation that returns it to 1, so if the
 * animation does not run THE CONTENT IS INVISIBLE. Shipping that unguarded
 * is the standard way this feature breaks a site.
 *
 * So every reveal lives inside `@supports (animation-timeline: view())`, and
 * inside that, `prefers-reduced-motion: reduce` puts opacity BACK to 1. A
 * browser that supports the timeline but whose user wants no motion must get
 * the content, not a blank page. backend/test/render_test.clj asserts both.
 */

:root {
  --motion-fast: 160ms;
  --motion-base: 320ms;
  --motion-slow: 620ms;
  /* Decelerating: quick to start, settles gently. Nothing here overshoots —
     a broker's site should feel composed, not springy. */
  --motion-ease: cubic-bezier(.22, .61, .36, 1);
}

/* ── page to page ────────────────────────────────────────────────────────
 * A crossfade between documents. `navigation: auto` opts every same-origin
 * navigation in, so it covers the nav, the footer, breadcrumbs and every
 * in-content link without any of them knowing about it.
 *
 * Deliberately a fade and not a slide: the hero images are full-bleed, and
 * sliding a 1600px photograph sideways on a slow connection looks like a
 * fault rather than a flourish. */
@view-transition { navigation: auto; }

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: var(--motion-base);
  animation-timing-function: var(--motion-ease);
}

/* The masthead is the same object on every page, so let it persist across
   the navigation instead of crossfading with the body. */
.nav__logo { view-transition-name: gib-masthead; }

/* ── revealing content on scroll ─────────────────────────────────────────
 * Same aesthetic as the existing slidein — up and in, staggered — driven by
 * the element's own position in the viewport rather than by a handler.
 *
 * `view()` with a negative end inset starts the animation when the element
 * is a little way in, which is what the old `-200px` in effects.js was for.
 * `animation-range` finishes it before the element reaches the middle of the
 * screen, so nothing is still moving once you are reading it. */
@supports (animation-timeline: view()) {
  @keyframes gib-rise {
    from { opacity: 0; transform: translateY(28px); }
    to   { opacity: 1; transform: none; }
  }

  .overview__item,
  .section__item,
  .grid__item {
    animation: gib-rise var(--motion-slow) var(--motion-ease) both;
    animation-timeline: view();
    animation-range: entry 5% cover 32%;
  }

  /* The stagger. Small steps: at 120ms apart a four-item row reads as one
     movement, where the old 250ms steps made the fourth item feel late. */
  .overview__item:nth-child(2),
  .section__item:nth-child(2) { animation-delay: 120ms; }
  .overview__item:nth-child(3),
  .section__item:nth-child(3) { animation-delay: 240ms; }
  .overview__item:nth-child(4),
  .section__item:nth-child(4) { animation-delay: 360ms; }
  .overview__item:nth-child(n+5),
  .section__item:nth-child(n+5) { animation-delay: 480ms; }

  /* NO DOUBLE ANIMATION. Where the timeline works, effects.js's class swap
     must not also fire slidein — two animations on one element leave the
     last one declared to win, and which that is depends on file order. */
  .animate .section__item,
  .animate .overview__item,
  .animate .grid__item { animation-name: gib-rise; }

  /* THE INVISIBLE-CONTENT TRAP. Supported timeline, but the visitor has
     asked for no motion: the animation must not run, and because the
     animation is what restores opacity, opacity has to be restored here
     instead. Getting this wrong hides the page from exactly the people
     least able to work out why. */
  @media (prefers-reduced-motion: reduce) {
    .overview__item,
    .section__item,
    .grid__item,
    .animate .section__item,
    .animate .overview__item,
    .animate .grid__item {
      animation: none !important;
      opacity: 1 !important;
      transform: none !important;
    }
  }
}

/* ── things that respond to the pointer ──────────────────────────────────
 * Transitions rather than animations, so they are interruptible: a card the
 * cursor crosses quickly settles back instead of finishing a movement
 * nobody asked to see. */
.action,
.call-to-action,
.blog__read-more,
.shortlinks-bar__link {
  transition: background-color var(--motion-fast) var(--motion-ease),
              color var(--motion-fast) var(--motion-ease),
              transform var(--motion-fast) var(--motion-ease),
              box-shadow var(--motion-base) var(--motion-ease);
}
.action:hover,
.call-to-action:hover,
.blog__read-more:hover { transform: translateY(-1px); }

.action:active,
.call-to-action:active,
.blog__read-more:active { transform: translateY(0); }

/* Cards lift, and the lift is the only cue that changes size — the image
   inside does not scale, because a scaled photograph inside a fixed frame
   re-samples and looks soft. */
.grid__item {
  transition: box-shadow var(--motion-base) var(--motion-ease),
              transform var(--motion-base) var(--motion-ease);
}
.grid__item:hover {
  transform: translateY(-3px);
  box-shadow: 0 6px 24px rgba(0, 25, 59, .10);
}

/* Focus must never be animated away from — it appears at once, every time,
   so a keyboard user is never waiting to find out where they are. */
:where(a, button, input, textarea, select):focus-visible {
  transition: none;
}

/* In-page anchors glide. The site links to #top from its own heroes. */
@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}

/* ── the blanket opt-out ─────────────────────────────────────────────────
 * Everything above, off. Including the page transition, which would
 * otherwise keep crossfading documents for somebody who asked for stillness
 * — and including rebrand.css's own slidein, which predates this file and
 * had no opt-out at all. */
@media (prefers-reduced-motion: reduce) {
  @view-transition { navigation: none; }

  *,
  *::before,
  *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }

  .animate .section__item,
  .animate .overview__item,
  .animate .grid__item,
  .grid__item:hover,
  .action:hover,
  .call-to-action:hover,
  .blog__read-more:hover {
    opacity: 1 !important;
    transform: none !important;
  }
}
