/*
  MOTION: the delight layer (owner request 2026-10-11, "a little tasteful motion"). One file, loaded
  after shell.css on every layout, so a page never needs its own animation rules.

  THE THESIS: the observatory comes into focus. Public pages arrive once, in order (headline, lede,
  actions, then the figure), sections and charts come into view as the reader reaches them, and
  everything interactive answers the hand with a clear, certain response. Nothing loops, nothing hides
  data, nothing waits on script. Strengthened 2026-10-11 on owner feedback ("less subtle"): the
  distances, durations and staggers below are the authored band in tokens.css, deliberately large
  enough to be seen at once and still calm (one confident ease-out, no bounce, nothing longer than a
  second).

  HOW IT IS DRIVEN. app/javascript/delight.js (a deferred classic script, one request from our own
  origin) does two things and the CSS here does the rest:
    * adds `js-motion` to <html> on the first animation frame after the document is parsed, so the
      hero arrivals START at a paint the reader can see instead of at parse time;
    * marks reveal targets `m-reveal` and flips them to `is-in` with an IntersectionObserver, so
      scroll reveals work in every browser (Safari has no CSS scroll timelines).
  Without the script nothing is marked and nothing is hidden: the page is simply static.

  THE RULES (docs in explore/COMPONENTS.md, "Motion"):
  - Only transform, translate and opacity move. No width, height, margin, top or left, so there is no
    layout shift. (Colour and underline changes ride the components' own transitions.)
  - EVERY rule below lives inside `prefers-reduced-motion: no-preference` and `screen` media. A visitor
    who asked for less gets the finished page and the instant state changes the components already
    had; the script also does nothing for them. base.css still zeroes any older animation.
  - Content is visible by default. The only hidden state, `.m-reveal` (opacity 0), is added by the
    script and exists only inside this block, so no script, no observer, reduced motion and print all
    show everything. Arrivals use `backwards` fill, so the natural style is the end state.
  - Hover effects have a :focus-visible twin. Lifts use `translate` and arrivals use `transform`, so
    the two never fight over one property.
  - Durations, easings, distances and the stagger steps are the --duration-*, --ease-*, --motion-*
    tokens (tokens.css). No literal time or distance below.
*/

@media screen and (prefers-reduced-motion: no-preference) {
  /* ---------- Keyframes ---------- */

  /* Small arrivals (a flash, the copy confirmation, a swapped panel). */
  @keyframes m-rise {
    from { opacity: 0; transform: translateY(var(--motion-rise)); }
    to { opacity: 1; transform: none; }
  }

  /* The authored arrival: the public hero. A long rise and a slight settle from 98% size. */
  @keyframes m-arrive {
    from { opacity: 0; transform: translateY(var(--motion-arrive-rise)) scale(var(--motion-arrive-scale)); }
    to { opacity: 1; transform: none; }
  }

  /* The scroll reveal: a section, tile or figure rises into place as it comes into view. */
  @keyframes m-reveal {
    from { opacity: 0; transform: translateY(var(--motion-reveal-rise)) scale(var(--motion-arrive-scale)); }
    to { opacity: 1; transform: none; }
  }

  @keyframes m-fade {
    from { opacity: 0; }
    to { opacity: 1; }
  }

  /* A confirmation: lands slightly large, then settles. The one overshoot in the system. */
  @keyframes m-pop {
    from { opacity: 0; transform: scale(0.6); }
    60% { opacity: 1; transform: scale(1.12); }
    to { opacity: 1; transform: none; }
  }

  @keyframes m-turn-in {
    from { opacity: 0; transform: rotate(-90deg) scale(0.6); }
    to { opacity: 1; transform: none; }
  }

  /* Menus and dialogs use `translate`/`scale`, not `transform`: the mega menu centres itself with a
     transform that this must not replace. */
  @keyframes m-float-in {
    from { opacity: 0; translate: 0 calc(var(--motion-rise) / -2); scale: 0.98; }
    to { opacity: 1; translate: 0 0; scale: 1; }
  }

  @keyframes m-grow-y {
    from { transform: scaleY(0); }
    to { transform: none; }
  }

  /* ---------- Arrivals: the public pages, once, after the first paint ---------- */

  /* Gated on `js-motion`, which delight.js adds on the first animation frame after parsing. Starting
     here (not at parse time) is what makes the hero seen: the animation clock begins at a frame the
     reader gets, even while the large hero capture is still loading. No class, no animation, and
     nothing is hidden. */
  .js-motion .u-arrive,
  .js-motion .u-arrive-group > *,
  .js-motion .p-landing-hero__text > *,
  .js-motion .c-public-hero__text > *,
  .js-motion .s-public .c-page-header > *,
  .js-motion .p-outcome {
    transform-origin: 0 100%;
    animation: m-arrive var(--duration-arrive) var(--ease-out-expo) backwards;
    animation-delay: calc(var(--motion-arrive-stagger) * var(--arrive, 0));
  }

  /* The index is capped at four steps (0 to 3), so the last line of any hero starts 300ms in and has
     landed by about one second (the arrive duration plus three stagger steps). */
  :is(.u-arrive-group, .p-landing-hero__text, .c-public-hero__text, .s-public .c-page-header) > :nth-child(2) { --arrive: 1; }
  :is(.u-arrive-group, .p-landing-hero__text, .c-public-hero__text, .s-public .c-page-header) > :nth-child(3) { --arrive: 2; }
  :is(.u-arrive-group, .p-landing-hero__text, .c-public-hero__text, .s-public .c-page-header) > :nth-child(n + 4) { --arrive: 3; }

  /* The figure beside the headline follows it, a beat after the headline and the lede. */
  .js-motion .p-landing-figure--hero {
    animation: m-arrive var(--duration-focal) var(--ease-out-expo) backwards;
    animation-delay: calc(var(--motion-arrive-stagger) * 2);
  }

  .js-motion .c-public-hero__figure {
    animation: m-arrive var(--duration-focal) var(--ease-out-expo) backwards;
    animation-delay: calc(var(--motion-arrive-stagger) * 3);
  }

  /* A flash message slides in under the top bar. */
  .s-flash .c-notice {
    animation: m-rise var(--duration-enter) var(--ease-out-expo) backwards;
  }

  /* A confirmed request: the panel arrives (above) and its icon pops after it. */
  .js-motion .p-outcome__icon {
    animation: m-pop var(--duration-focal) var(--ease-out-expo) backwards;
    animation-delay: calc(var(--motion-arrive-stagger) * 3);
  }

  /* ---------- Scroll reveals (delight.js + IntersectionObserver, every browser) ---------- */

  /* delight.js marks a target `m-reveal` (hidden) before first paint and adds `is-in` when it crosses
     the reveal line; it comes up once and the script then removes both classes. --i is the target's
     place in the batch that came into view together, so a row of tiles staggers. */
  .m-reveal {
    opacity: 0;
  }

  .m-reveal.is-in {
    opacity: 1;
    animation: m-reveal var(--duration-reveal) var(--ease-out-expo) backwards;
    animation-delay: calc(var(--motion-reveal-stagger) * var(--i, 0));
  }

  /* ---------- Charts: the server-drawn stacked columns grow from the baseline ---------- */

  /* Each column scales from its own bottom edge, left to right, when the chart scrolls into view
     (delight.js sets --p, the column's place along the chart from 0 to 1, and adds `is-in`). The
     finished chart is exactly the drawn one: no frame shows a value larger than the data, and with
     no script the columns are simply there. */
  .c-evidence-figure__svg--stacked .c-evidence-figure__column {
    transform-box: fill-box;
    transform-origin: bottom;
  }

  .m-grow:not(.is-in) .c-evidence-figure__column {
    transform: scaleY(0);
  }

  .m-grow.is-in .c-evidence-figure__column {
    animation: m-grow-y var(--duration-grow) var(--ease-out-expo) backwards;
    animation-delay: calc(var(--motion-grow-span) * var(--p, 0));
  }

  /* ---------- Hover and focus ---------- */

  /* A tile that is a link lifts toward the pointer on a deeper shadow, and its icon and arrow answer.
     The keyboard gets the same response on focus. */
  a.c-tile {
    transition: border-color var(--duration-fast) var(--ease-out), box-shadow var(--duration-base) var(--ease-out),
      translate var(--duration-base) var(--ease-out-expo);
  }

  a.c-tile:focus-visible {
    border-color: var(--color-border-strong);
    box-shadow: var(--shadow-md);
    translate: 0 var(--motion-lift);
  }

  @media (hover: hover) {
    a.c-tile:hover {
      box-shadow: var(--shadow-md);
      translate: 0 var(--motion-lift);
    }
  }

  .c-tile__icon {
    transition: scale var(--duration-base) var(--ease-out-expo), translate var(--duration-base) var(--ease-out-expo);
  }

  a.c-tile:hover .c-tile__icon,
  a.c-tile:focus-visible .c-tile__icon {
    scale: var(--motion-icon-scale);
    translate: 0 var(--motion-icon-lift);
  }

  a.c-tile:focus-visible .c-tile__title svg {
    transform: translateX(var(--motion-nudge));
    color: var(--color-link);
  }

  a.c-tile:hover .c-tile__title svg {
    transform: translateX(var(--motion-nudge));
  }

  /* Buttons lift a little on hover and press in on click. The lift is `translate` and the press is
     `transform`, so the two compose rather than fight. */
  .c-button {
    transition: background-color var(--duration-fast) var(--ease-out), border-color var(--duration-fast) var(--ease-out),
      color var(--duration-fast) var(--ease-out), box-shadow var(--duration-base) var(--ease-out),
      translate var(--duration-base) var(--ease-out-expo), transform var(--duration-fast) var(--ease-out);
  }

  @media (hover: hover) {
    .c-button:hover:not([disabled], [aria-disabled="true"]) {
      box-shadow: var(--shadow-sm);
      translate: 0 var(--motion-lift-button);
    }
  }

  .c-button:active:not([disabled], [aria-disabled="true"]) {
    translate: 0 0;
    transform: scale(var(--motion-press));
  }

  /* Links: colour and the underline's distance from the text ease instead of snapping. Paint only. */
  a {
    transition: color var(--duration-fast) var(--ease-out), text-decoration-color var(--duration-base) var(--ease-out),
      text-underline-offset var(--duration-base) var(--ease-out-expo);
  }

  a:hover,
  a:focus-visible {
    text-underline-offset: var(--motion-underline-offset);
  }

  /* ---------- Feedback ---------- */

  /* Copy: "Copied" arrives rather than blinking on, and the icon pops with it. The status node is
     empty until the click, so the animation starts when text first matches :not(:empty). */
  .c-copy__status:not(:empty) {
    animation: m-rise var(--duration-base) var(--ease-out-expo) backwards;
  }

  .c-copy:has(.c-copy__status:not(:empty)) .c-button svg {
    animation: m-pop var(--duration-enter) var(--ease-out-expo);
  }

  /* The client chooser: the newly shown panel fades up instead of swapping. */
  .c-platform-picker[data-enhanced] .c-platform-picker__panel:not([hidden]) {
    animation: m-rise var(--duration-enter) var(--ease-out-expo) backwards;
  }

  /* The theme toggle turns its icon over when it is clicked (data-toggled is set by the click
     handler, never on load). */
  .s-theme-toggle[data-toggled] .s-theme-toggle__icon {
    animation: m-turn-in var(--duration-slow) var(--ease-out-expo);
  }

  /* ---------- Overlays ---------- */

  .c-menu__popover:not([hidden]) {
    animation: m-float-in var(--duration-base) var(--ease-out-expo);
  }

  /* Only the MODAL dialog: the PDF form's no-script fallback is an in-flow card and must not move. */
  .c-dialog:modal {
    animation: m-float-in var(--duration-enter) var(--ease-out-expo);
  }

  .c-dialog:modal::backdrop {
    animation: m-fade var(--duration-enter) var(--ease-out);
  }
}
