/* ==========================================================================
   Artcestry motion layer (motion.css).

   Progressive enhancement:
   - Everything that moves lives inside @media screen and (prefers-reduced-motion: no-preference)
     and is keyed on html.motion (added by the inline head script). Print, reduced motion and a
     missing html.motion class all get the static page.
   - Hide-before-reveal states apply only under html.motion.motion-ready (motion.js adds
     motion-ready after its observers are set up), so a failed script never hides content.
   - Ambient loops (Ken Burns, map travellers, marker pulses, head tilt) run only under html.m-ui, which
     motion.js adds once the pause control exists: no loop ever runs without a way to pause it.
   - Load animations (hero text, header) are keyframes that end in the natural state, so they need
     no JS. With JS, the hero map and strip wait for their gate (.m-go) and start when in view.
   - html.motion-paused sets --m-t to 0: every entrance and reveal completes at once (duration and
     delay scale to 0) and every ambient loop gets animation-play-state: paused.
   - Only transform, opacity, clip-path and stroke-dashoffset are animated.
   ========================================================================== */

/* Always on (every media): defaults for the runtime DOM motion.js creates ------ */
.m-lines, .m-line, .m-line-in { display: block; }
.m-controls, .m-pause, .m-progress, .m-scene, .m-shimmer { display: none; }

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

  :root { --m-t: 1; --m-step: 90ms; }
  html.motion-paused { --m-t: 0; }

  /* ------------------------------------------------------------------------
     1. Pause / play motion control (WCAG 2.2.2)
     ------------------------------------------------------------------------ */
  /* The control's landmark (motion.js): a plain block with no box of its own (the pill is fixed),
     shown exactly when the pill is, so it never lingers as an empty region */
  html.motion .m-controls { display: block; }
  html.motion .m-pause {
    position: fixed;
    left: calc(20px + env(safe-area-inset-left, 0px));
    bottom: calc(20px + env(safe-area-inset-bottom, 0px));
    z-index: 150;
    display: inline-flex;
    align-items: center;
    gap: 9px;
    height: 38px;
    padding: 0 16px 0 13px;
    border: 1px solid rgba(233, 221, 190, .2);
    border-radius: 999px;
    background: rgba(32, 31, 29, .85);
    color: var(--on-dark, #FBF3DC);
    font: 600 11px/1 var(--sans, sans-serif);
    letter-spacing: .12em;
    text-transform: uppercase;   /* visual only: the accessible name is the sentence-case aria-label */
    white-space: nowrap;
    box-shadow: 0 6px 18px rgba(0, 0, 0, .18);
    -webkit-backdrop-filter: blur(8px);
    backdrop-filter: blur(8px);
    transition: background-color .2s var(--ease-out), opacity .25s var(--ease-out), visibility 0s;
  }
  html.motion .m-pause:hover { background: rgba(32, 31, 29, .96); }
  /* Gold ring with a dark halo: reads on the dark hero and on cream sections alike */
  html.motion .m-pause:focus-visible {
    outline: 2px solid var(--gold-light, #E4C470);
    outline-offset: 2px;
    box-shadow: 0 0 0 6px rgba(32, 31, 29, .92), 0 6px 18px rgba(0, 0, 0, .18);
  }
  .m-pause__icon { flex: none; width: 14px; height: 14px; fill: currentColor; }
  .m-pause__i-play { display: none; }
  html.motion-paused .m-pause__i-play { display: inline; }
  html.motion-paused .m-pause__i-pause { display: none; }
  .m-pause__label { position: relative; top: .5px; }
  /* Focus scrolling keeps the focused control above the pill */
  html.motion.m-ui { scroll-padding-bottom: 72px; }
  /* 1200px and below the content gutter is 40px or less: an icon-only circle that fits the gutter
     (36px, 2px from the edge) instead of the 153px label pill over the content. */
  @media (max-width: 1200px) {
    html.motion .m-pause {
      left: calc(2px + env(safe-area-inset-left, 0px));
      bottom: calc(12px + env(safe-area-inset-bottom, 0px));
      width: 36px; height: 36px; padding: 0; justify-content: center;
    }
    .m-pause__label { display: none; }
  }
  /* Touch sizes: 40px circle (34px at 480, tucked into the corner below the hero strip line),
     clear of the right-aligned "now showing" card and the join form's content column. */
  @media (max-width: 768px) {
    html.motion .m-pause { left: calc(8px + env(safe-area-inset-left, 0px)); width: 40px; height: 40px; }
    /* typing in a field (on-screen keyboard): step out of the way */
    html.m-typing .m-pause { opacity: 0; visibility: hidden; transition: opacity .2s var(--ease-out), visibility 0s linear .2s; }
  }
  @media (max-width: 480px) {
    html.motion .m-pause { left: calc(4px + env(safe-area-inset-left, 0px)); bottom: calc(4px + env(safe-area-inset-bottom, 0px)); width: 34px; height: 34px; }
  }

  /* ------------------------------------------------------------------------
     2. Scroll progress bar: one variable written by a rAF-throttled passive listener
     ------------------------------------------------------------------------ */
  html.motion .m-progress {
    position: fixed;
    top: 0; left: 0; right: 0;
    z-index: 150;
    display: block;
    height: 2px;
    background: #B8902E;
    transform: scaleX(var(--progress, 0));
    transform-origin: 0 50%;
    pointer-events: none;
  }

  /* ------------------------------------------------------------------------
     3. Header: slides down on load (the scrolled-state transition is the part's own)
     ------------------------------------------------------------------------ */
  html.motion:not(.m-entered) .site-header {
    animation: m-in-down calc(800ms * var(--m-t)) var(--ease-out) calc(100ms * var(--m-t)) backwards;
  }

  /* ------------------------------------------------------------------------
     4. Hero entrance (on load, ~1.9s). CSS only; motion.js adds the line mask split of the
        h1, the shimmer and the route masks on top, timed against the same clock.
     ------------------------------------------------------------------------ */
  /* Divider: during the entrance the part's 1px border is swapped for a ::before of the same colour
     (01-hero.css .hero__lede border-top) that draws from the centre; afterwards the border is back. */
  html.motion:not(.m-entered) .hero__lede { position: relative; border-top-color: transparent; }
  html.motion:not(.m-entered) .hero__lede::before {
    content: "";
    position: absolute;
    left: 0; right: 0; top: -1px;
    height: 1px;
    background: rgba(184, 145, 46, .6);
    transform-origin: 50% 50%;
    animation: m-in-rule calc(900ms * var(--m-t)) var(--ease-in-out) calc(900ms * var(--m-t)) backwards;
  }
  html.motion:not(.m-entered) .hero__title-accent { position: relative; }   /* anchors the shimmer overlay */

  html.motion:not(.m-entered) .hero__bg { animation: m-in-fade calc(1600ms * var(--m-t)) var(--ease-out) backwards; }

  html.motion:not(.m-entered) .hero__eyebrow > span { --m-rise: 8px; animation: m-in-up calc(800ms * var(--m-t)) var(--ease-out) calc(200ms * var(--m-t)) backwards; }
  html.motion:not(.m-entered) .hero__eyebrow::before,
  html.motion:not(.m-entered) .hero__eyebrow::after { animation: m-in-rule calc(800ms * var(--m-t)) var(--ease-out) calc(320ms * var(--m-t)) backwards; }
  .hero__eyebrow::before { transform-origin: 100% 50%; }   /* rules draw outward from the text */
  .hero__eyebrow::after { transform-origin: 0 50%; }

  /* h1 fallback: a whole-heading rise. motion.js replaces it with a line-by-line mask rise
     (.m-split, lines --m-step = 90ms apart; .m-js-lines stays on after the split is undone) when
     it runs before the rise has started. */
  html.motion:not(.m-entered) .hero__title:not(.m-split):not(.m-js-lines) { --m-rise: 28px; animation: m-in-up calc(1000ms * var(--m-t)) var(--ease-out) calc(380ms * var(--m-t)) backwards; }

  html.motion:not(.m-entered) .hero__lede p { --m-rise: 14px; animation: m-in-up calc(800ms * var(--m-t)) var(--ease-out) calc(1000ms * var(--m-t)) backwards; }
  html.motion:not(.m-entered) .hero__btn { --m-rise: 18px; animation: m-in-up calc(700ms * var(--m-t)) var(--ease-out) calc(1150ms * var(--m-t)) backwards; }
  html.motion:not(.m-entered) .hero__btn + .hero__btn { animation-delay: calc(1230ms * var(--m-t)); }

  /* Strip. Delays are from the entrance start minus --m-strip-shift (motion.js: the time already
     spent when its gate opened, so a strip first seen on scroll starts ~100ms after it arrives). */
  html.motion:not(.m-entered) .hero__strip::before,
  html.motion:not(.m-entered) .hero__strip::after { animation: m-in-strip-rule calc(800ms * var(--m-t)) var(--ease-out) calc((1250ms - var(--m-strip-shift, 0ms)) * var(--m-t)) backwards; }
  .hero__strip::before { transform-origin: 100% 50%; }
  .hero__strip::after { transform-origin: 0 50%; }
  html.motion:not(.m-entered) .hero__strip-text { animation: m-in-fade calc(700ms * var(--m-t)) var(--ease-out) calc((1300ms - var(--m-strip-shift, 0ms)) * var(--m-t)) backwards; }
  html.motion:not(.m-entered) .hero__strip-dot { animation: m-in-strip-dot calc(500ms * var(--m-t)) cubic-bezier(.3, 1.5, .5, 1) calc((1350ms - var(--m-strip-shift, 0ms)) * var(--m-t)) backwards; }

  /* Map: dots fade (on load), markers pop in numeric order, labels follow, routes draw (motion.js
     masks). Marker, label and route delays subtract --m-map-shift (see the strip). */
  html.motion:not(.m-entered) .hero-map__dots {
    transform-box: fill-box; transform-origin: 50% 50%;
    animation: m-in-dots calc(1400ms * var(--m-t)) var(--ease-out) calc(300ms * var(--m-t)) backwards;
  }
  .hero-map__marker[data-region="africa"], .hero-map__label[data-region="africa"] { --m-order: 0; }
  .hero-map__marker[data-region="europe"], .hero-map__label[data-region="europe"] { --m-order: 1; }
  .hero-map__marker[data-region="north-america"], .hero-map__label[data-region="north-america"] { --m-order: 2; }
  .hero-map__marker[data-region="latin-america"], .hero-map__label[data-region="latin-america"] { --m-order: 3; }
  .hero-map__marker[data-region="asia"], .hero-map__label[data-region="asia"] { --m-order: 4; }
  .hero-map__marker[data-region="australia"], .hero-map__label[data-region="australia"] { --m-order: 5; }
  html.motion:not(.m-entered) .hero-map__marker-core {
    animation: m-in-pop calc(700ms * var(--m-t)) var(--ease-out) calc((600ms + var(--m-order, 0) * 150ms - var(--m-map-shift, 0ms)) * var(--m-t)) backwards;
  }
  html.motion:not(.m-entered) .hero-map__label {
    animation: m-in-fade calc(800ms * var(--m-t)) var(--ease-out) calc((800ms + var(--m-order, 0) * 150ms - var(--m-map-shift, 0ms)) * var(--m-t)) backwards;
  }
  .hero-map__tick { transform-box: fill-box; transform-origin: 0 50%; }
  .hero-map__label--left .hero-map__tick { transform-origin: 100% 50%; }
  html.motion:not(.m-entered) .hero-map__tick {
    animation: m-in-rule calc(700ms * var(--m-t)) var(--ease-out) calc((800ms + var(--m-order, 0) * 150ms - var(--m-map-shift, 0ms)) * var(--m-t)) backwards;
  }
  /* No script: the routes simply fade in after the markers */
  html.motion:not(.m-entered):not(.m-hero-js) .hero-map__routes {
    animation: m-in-fade calc(900ms * var(--m-t)) var(--ease-out) calc(1800ms * var(--m-t)) backwards;
  }
  /* Route draw: a mask path per route (pathLength 1) created by motion.js; --m-d is its planned
     start (1050 to 1500ms). A deliberate 1100ms: a line drawn along a curve, not a reveal. */
  .m-route-mask { fill: none; stroke: #fff; stroke-width: 8; stroke-linecap: butt; stroke-dasharray: 1 1; }
  html.motion:not(.m-entered) .m-route-mask {
    animation: m-in-route calc(1100ms * var(--m-t)) var(--ease-in-out) calc((var(--m-d, 0ms) - var(--m-map-shift, 0ms)) * var(--m-t)) backwards;
  }

  /* Gates (motion.js): until the map or the strip is in view (.m-go), its pieces wait hidden */
  html.motion.m-hero-js:not(.m-entered) .hero-map:not(.m-go) .hero-map__marker-core,
  html.motion.m-hero-js:not(.m-entered) .hero-map:not(.m-go) .hero-map__label,
  html.motion.m-hero-js:not(.m-entered) .hero__strip:not(.m-go)::before,
  html.motion.m-hero-js:not(.m-entered) .hero__strip:not(.m-go)::after,
  html.motion.m-hero-js:not(.m-entered) .hero__strip:not(.m-go) .hero__strip-text,
  html.motion.m-hero-js:not(.m-entered) .hero__strip:not(.m-go) .hero__strip-dot { animation: none; opacity: 0; }
  html.motion.m-hero-js:not(.m-entered) .hero-map:not(.m-go) .hero-map__tick { animation: none; }
  html.motion.m-hero-js:not(.m-entered) .hero-map:not(.m-go) .m-route-mask { animation: none; stroke-dashoffset: 1; }

  /* Hero h1 line mask (also used by section h2s) */
  .m-line { clip-path: inset(-.3em -.6em -.32em -.6em); white-space: nowrap; }   /* each line was measured as one line */
  html.motion .m-split .m-line-in {
    animation: m-line calc(900ms * var(--m-t)) var(--ease-out)
               calc((var(--m-d, 0ms) + var(--l, 0) * var(--m-lstep, var(--m-step))) * var(--m-t)) backwards;
  }

  /* One soft gold shimmer across "Art world" once it has landed: a masked light copy of the
     text; the mask band travels while the copy counter-travels, so the glyphs stay put. */
  html.motion .m-shimmer {
    position: absolute; inset: 0;
    display: flex; justify-content: center;
    pointer-events: none;
  }
  .m-shimmer__band {
    display: block;
    width: max-content;
    color: #F4DE9C;
    -webkit-mask-image: linear-gradient(100deg, transparent 32%, #000 50%, transparent 68%);
    mask-image: linear-gradient(100deg, transparent 32%, #000 50%, transparent 68%);
    -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
    animation: m-shimmer-band calc(1300ms * var(--m-t)) var(--ease-in-out) calc(var(--m-d, 0ms) * var(--m-t)) both;
  }
  .m-shimmer__text {
    display: block;
    white-space: nowrap;
    animation: m-shimmer-text calc(1300ms * var(--m-t)) var(--ease-in-out) calc(var(--m-d, 0ms) * var(--m-t)) both;
  }

  /* ------------------------------------------------------------------------
     5. Hero ambient loops: only under html.m-ui (the pause control exists), longhands only (so
        the pause and idle rules below always win), paused by html.motion-paused and off screen.
     ------------------------------------------------------------------------ */
  /* Ken Burns: the 14s zoom from 1.08, then (motion.js swaps in .m-kb-done on its animationend)
     the slow drift loop, one transform animation at a time so it stays on the compositor. Until
     the control exists the photo holds the first frame, so the zoom starts without a jump. */
  html.motion:not(.m-ui) .hero__photo { transform: scale(1.08); }
  html.motion.m-ui .hero__photo {
    animation-name: m-kb-in;
    animation-duration: 14s;
    animation-timing-function: cubic-bezier(.33, 0, .2, 1);
    animation-fill-mode: backwards;
  }
  html.motion.m-ui .hero__photo.m-kb-done {
    animation-name: m-kb-drift;
    animation-duration: 28s;
    animation-timing-function: ease-in-out;
    animation-iteration-count: infinite;
    animation-direction: alternate;
    animation-fill-mode: none;
  }
  html.motion .hero-map__halo,
  html.motion .hero-map__ring--outer { transform-box: fill-box; transform-origin: 50% 50%; }
  html.motion.m-ui .hero-map__halo,
  html.motion.m-ui .hero-map__ring--outer {
    animation-name: m-halo; animation-duration: 7s; animation-timing-function: ease-in-out;
    animation-delay: calc(var(--m-order, 0) * -1.17s); animation-iteration-count: infinite;
  }
  html.motion.m-ui .hero-map__ring--outer { animation-name: m-ring; }

  /* ------------------------------------------------------------------------
     5b. Map travellers (v19, notes/map-motion.md "Two travellers"). Two lights move at once from the first
         frames after load: traveller a runs west to east (North America > Africa > Asia > Australia), traveller
         b east to west (Europe > Latin America > North America); their first legs cross mid-map about 2.3s in.
         Each rides exact route curves (per-axis cubic-bezier), leaves a wake of opacity segments and pings each
         arrival; a numeric-order ripple closes the 14s loop. The names key on html.m-ui only (no map or strip
         gate), at every width, so all of them start in one frame and stay phase-locked for the pause control.
         Transform and opacity on small SVG nodes: composited in Edge 154, main thread in Chromium 151, so the
         run is bounded: 6 loops (84s of in-view time, as v13), then the map rests with its halos breathing.
         Wakes, pings and ripple use negative delays (--at - 14s) and fractional counts that play each one 6 times
         and end inside its last rest. The rules and keyframes between the map-fx markers are generated by
         tools/map-fx.mjs.
     ------------------------------------------------------------------------ */
  /* map-fx:rules begin (generated by tools/map-fx.mjs; do not edit by hand) */
  html.motion.m-ui .hero-fx { display: block; }
  html.motion.m-ui .hero-fx__x--a { animation-name: m-fx-xa; }
  html.motion.m-ui .hero-fx__y--a { animation-name: m-fx-ya; }
  html.motion.m-ui .hero-fx__head--a { animation-name: m-fx-heada; }
  html.motion.m-ui .hero-fx__x--b { animation-name: m-fx-xb; }
  html.motion.m-ui .hero-fx__y--b { animation-name: m-fx-yb; }
  html.motion.m-ui .hero-fx__head--b { animation-name: m-fx-headb; }
  html.motion.m-ui .hero-fx__seg { animation-name: m-fx-seg; }
  html.motion.m-ui .hero-fx__ping { animation-name: m-fx-ping; }
  html.motion.m-ui .hero-fx__coda { animation-name: m-fx-coda; }
  .hero-fx__x, .hero-fx__y, .hero-fx__head, .hero-fx__seg, .hero-fx__ping, .hero-fx__coda {
    animation-duration: 14s; animation-timing-function: linear; animation-iteration-count: 6;
  }
  .hero-fx__seg, .hero-fx__ping, .hero-fx__coda { animation-delay: calc(var(--at, 0ms) - 14s); }
  .hero-fx__seg { animation-iteration-count: 6.29; }
  .hero-fx__ping { animation-iteration-count: 6.11; }
  .hero-fx__coda { animation-iteration-count: 6.14; }
  /* map-fx:rules end */

  /* News autoplay (part: the 6s news-fill of the active bar is the timer; 05-news.css/js). Held
     while the "now showing" card that holds the bars is still revealing, so the first countdown is
     seen in full; and off entirely when there is no pause control (motion.js did not run). */
  html.motion .news.is-playing .news__now.m-t:not(.is-done) .news__bar.is-active .news__bar-fill { animation-play-state: paused; }
  html.motion:not(.m-ui) .news.is-playing .news__bar.is-active .news__bar-fill { animation: none; }

  /* ------------------------------------------------------------------------
     6. Scroll reveals. motion.js turns [data-reveal], the children of [data-stagger] and
        [data-sequence] containers into .m-t targets with a variant class (.m-v-*), sets --m-d
        (the stagger delay) and adds .is-revealed once, then .is-done when finished.
     ------------------------------------------------------------------------ */
  html.motion.motion-ready:not(.motion-paused) .m-t:not(.is-revealed):not(.m-v-seq) { opacity: 0; }
  html.motion.motion-ready:not(.motion-paused) .m-v-up:not(.is-revealed) { transform: translateY(var(--m-rise, 24px)); }
  html.motion.motion-ready:not(.motion-paused) .m-v-left:not(.is-revealed) { transform: translateX(-32px); }
  html.motion.motion-ready:not(.motion-paused) .m-v-right:not(.is-revealed) { transform: translateX(32px); }
  html.motion.motion-ready:not(.motion-paused) .m-v-scale:not(.is-revealed) { transform: scale(.95); }

  html.motion .m-t.is-revealed:not(.is-done) { animation-duration: calc(850ms * var(--m-t)); animation-timing-function: var(--ease-out); animation-delay: calc(var(--m-d, 0ms) * var(--m-t)); animation-fill-mode: backwards; }
  html.motion .m-v-up.is-revealed:not(.is-done) { animation-name: m-rv-up; }
  html.motion .m-v-left.is-revealed:not(.is-done) { animation-name: m-rv-left; }
  html.motion .m-v-right.is-revealed:not(.is-done) { animation-name: m-rv-right; }
  html.motion .m-v-scale.is-revealed:not(.is-done) { animation-name: m-rv-scale; animation-duration: calc(900ms * var(--m-t)); }
  html.motion .m-v-fade.is-revealed:not(.is-done) { animation-name: m-rv-fade; animation-duration: calc(900ms * var(--m-t)); }
  html.motion .m-v-clip.is-revealed:not(.is-done) { animation-name: m-rv-clip; animation-duration: calc(900ms * var(--m-t)); animation-timing-function: cubic-bezier(.33, 0, .2, 1); }
  /* lines: the heading is only hidden until motion.js splits it; if the split is not possible
     (no text) it falls back to a whole-heading rise */
  html.motion .m-v-lines.is-revealed:not(.is-done):not(.m-split) { animation-name: m-rv-up; }

  /* Eyebrows: small rise, then the rule draws from the left */
  .eyebrow.m-t { --m-rise: 10px; }
  html.motion.motion-ready:not(.motion-paused) .eyebrow.m-t:not(.is-revealed)::after { transform: scaleX(0); }
  html.motion .eyebrow.m-t.is-revealed:not(.is-done)::after {
    transform-origin: 0 50%;
    animation: m-rule calc(800ms * var(--m-t)) var(--ease-out) calc((var(--m-d, 0ms) + 220ms) * var(--m-t)) backwards;
  }

  /* Per-element distances */
  .membership__card.m-t, .provenance__art.m-t { --m-rise: 36px; }
  .news__now.m-t, .stories__caption.m-t { --m-rise: 28px; }

  /* Sequences: why card D timeline (track fills, dots fill 1 to 3). The track's 1300ms is
     deliberate: it runs behind the three dot pops, which are 600ms apart. */
  html.motion.motion-ready:not(.motion-paused) .why__timeline.m-t:not(.is-revealed)::before { transform: scaleX(0); }
  html.motion.motion-ready:not(.motion-paused) .why__timeline.m-t:not(.is-revealed) .why__step-dot { transform: scale(0); }
  html.motion.motion-ready:not(.motion-paused) .why__timeline.m-t:not(.is-revealed) .why__step-label { opacity: 0; }
  html.motion .why__timeline.is-revealed:not(.is-done)::before {
    animation: m-seq-line calc(1300ms * var(--m-t)) var(--ease-in-out) calc((var(--m-d, 0ms) + 200ms) * var(--m-t)) backwards;
  }
  html.motion .why__timeline.is-revealed:not(.is-done) .why__step-dot {
    animation: m-seq-pop calc(520ms * var(--m-t)) cubic-bezier(.3, 1.45, .5, 1) calc((var(--m-d, 0ms) + 160ms + var(--s, 0) * 600ms) * var(--m-t)) backwards;
  }
  html.motion .why__timeline.is-revealed:not(.is-done) .why__step-label {
    animation: m-seq-label calc(600ms * var(--m-t)) var(--ease-out) calc((var(--m-d, 0ms) + 260ms + var(--s, 0) * 600ms) * var(--m-t)) backwards;
  }

  /* Sequences: provenance record events (dot pops, label and year fade) */
  html.motion.motion-ready:not(.motion-paused) .provenance__events.m-t:not(.is-revealed) .provenance__dot { transform: scale(0); }
  html.motion.motion-ready:not(.motion-paused) .provenance__events.m-t:not(.is-revealed) .provenance__event-label,
  html.motion.motion-ready:not(.motion-paused) .provenance__events.m-t:not(.is-revealed) .provenance__event-year { opacity: 0; }
  html.motion .provenance__events.is-revealed:not(.is-done) .provenance__dot {
    animation: m-seq-pop calc(480ms * var(--m-t)) cubic-bezier(.3, 1.45, .5, 1) calc((var(--m-d, 0ms) + 250ms + var(--s, 0) * 220ms) * var(--m-t)) backwards;
  }
  html.motion .provenance__events.is-revealed:not(.is-done) .provenance__event-label,
  html.motion .provenance__events.is-revealed:not(.is-done) .provenance__event-year {
    animation: m-seq-label-x calc(600ms * var(--m-t)) var(--ease-out) calc((var(--m-d, 0ms) + 330ms + var(--s, 0) * 220ms) * var(--m-t)) backwards;
  }
  html.motion .provenance__events.is-revealed:not(.is-done) .provenance__event-year { animation-delay: calc((var(--m-d, 0ms) + 390ms + var(--s, 0) * 220ms) * var(--m-t)); }

  /* ------------------------------------------------------------------------
     7. Hover and press feedback
     ------------------------------------------------------------------------ */
  /* Button sheen: a narrow diagonal light band sweeps across on hover / keyboard focus, linear so
     it stays on the button for most of the 600ms (overflow: clip, not hidden, keeps the
     inline-flex baseline and the focus outline untouched). */
  @supports (overflow: clip) {
    html.motion .btn { position: relative; overflow: clip; }
    html.motion .btn::after {
      content: "";
      position: absolute;
      top: -1px; bottom: -1px; left: 0;
      width: 45%;
      background: linear-gradient(90deg, rgba(255, 251, 240, 0), rgba(255, 251, 240, .28) 50%, rgba(255, 251, 240, 0));
      transform: translateX(-108%) skewX(-18deg);
      pointer-events: none;
    }
    html.motion .btn--cream::after { background: linear-gradient(90deg, rgba(184, 144, 46, 0), rgba(184, 144, 46, .22) 50%, rgba(184, 144, 46, 0)); }
    html.motion .btn:hover::after,
    html.motion .btn:focus-visible::after { transform: translateX(232%) skewX(-18deg); transition: transform .6s linear; }
  }
  /* Press feedback. The part lists (00-base.css .btn, 08-membership.css .membership__chip) lack
     transform, so they are restated here with transform added (part follow-up in notes/motion.md). */
  html.motion .btn { transition: background-color .25s var(--ease-out), color .25s var(--ease-out), border-color .25s var(--ease-out), transform .2s var(--ease-out); }
  html.motion .btn:active { transform: scale(.98); transition-duration: .25s, .25s, .25s, .08s; }

  /* Membership chips: lift on hover, press-in on click */
  html.motion .membership__chip { transition: background-color .2s var(--ease-out), color .2s var(--ease-out), border-color .2s var(--ease-out), transform .2s var(--ease-out); }
  @media (hover: hover) { html.motion .membership__chip:hover { transform: translateY(-1px); } }
  html.motion .membership__chip:active { transform: scale(.96); transition-duration: .2s, .2s, .2s, .08s; }

  /* Why card A: slow, deeper photo zoom when the card is hovered (brief), and on keyboard focus of
     its link. Replaces the part's quicker link-only push (02-why.css) for this card. */
  html.motion .why__media--engage { transition: transform 1.6s var(--ease-out); }
  @media (hover: hover) { html.motion .why__card--engage:hover .why__media--engage { transform: scale(1.06); } }
  html.motion .why__card--engage:has(.why__link:focus-visible) .why__media--engage { transform: scale(1.06); }

  /* ------------------------------------------------------------------------
     8. Stories: head animation scene (images/stories-scene/scene.json, see motion.js)
     ------------------------------------------------------------------------ */
  /* Covers the photo box like the img's object-fit: cover (scaled to width, centred). motion.js sets
     margin-top to minus half the scene height as a percentage of the width, so no transform
     (and no sub-pixel shift) is involved. */
  html.motion .m-scene {
    position: absolute;
    left: 0; top: 50%;
    display: block;
    width: 100%;
    height: auto;
    pointer-events: none;
    visibility: hidden;
    opacity: 0;
  }
  /* 350 ms fade: the page photo is a different (757 px) file, so a one-frame swap reads as a texture jump */
  html.motion .m-scene.is-ready { visibility: visible; opacity: 1; transition: opacity .35s ease-out; }
  html.motion .m-scene--tall { left: 50%; top: 0; width: auto; height: 100%; margin-top: 0 !important; transform: translateX(-50%); }
  .m-scene img { position: absolute; display: block; max-width: none; height: auto; user-select: none; }
  .m-scene .m-scene__base { left: 0; top: 0; width: 100%; height: 100%; }
  html.motion.m-ui .m-scene__moving { animation-name: m-head-tilt; animation-duration: 11s; animation-timing-function: linear; animation-iteration-count: infinite; }

  /* ------------------------------------------------------------------------
     Ambient loops pause: Pause button (html.motion-paused) and off screen (.m-idle). The loops
     are longhands, so nothing resets this play state.
     ------------------------------------------------------------------------ */
  html.motion-paused .hero__photo, html.motion-paused .hero-map__halo,
  html.motion-paused .hero-map__ring--outer, html.motion-paused .m-scene__moving,
  html .m-idle .hero__photo, html .m-idle .hero-map__halo,
  html .m-idle .hero-map__ring--outer, html .m-idle .m-scene__moving { animation-play-state: paused; }
  html.motion-paused .hero-fx__x, html.motion-paused .hero-fx__y, html.motion-paused .hero-fx__head,
  html.motion-paused .hero-fx__seg, html.motion-paused .hero-fx__ping, html.motion-paused .hero-fx__coda,
  html .m-idle .hero-fx__x, html .m-idle .hero-fx__y, html .m-idle .hero-fx__head,
  html .m-idle .hero-fx__seg, html .m-idle .hero-fx__ping, html .m-idle .hero-fx__coda { animation-play-state: paused; }

  /* ------------------------------------------------------------------------
     Keyframes. Most have no "to" frame: they end on the element's own style.
     ------------------------------------------------------------------------ */
  @keyframes m-in-fade { from { opacity: 0; } }
  @keyframes m-in-up { from { opacity: 0; transform: translateY(var(--m-rise, 20px)); } }
  @keyframes m-in-down { from { opacity: 0; transform: translateY(-18px); } }
  @keyframes m-in-rule { from { transform: scaleX(0); } }
  @keyframes m-in-strip-rule { from { transform: translateY(-.6px) scaleX(0); } }
  @keyframes m-in-strip-dot { from { transform: translateY(-.6px) scale(0); } }
  @keyframes m-in-dots { from { opacity: 0; transform: scale(1.03); } }
  @keyframes m-in-pop {
    0% { opacity: 0; transform: scale(0); }
    60% { opacity: 1; transform: scale(1.12); }
    100% { transform: scale(1); }
  }
  @keyframes m-in-route { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } }
  @keyframes m-line { from { opacity: 0; transform: translateY(110%); } 45% { opacity: 1; } }
  @keyframes m-shimmer-band { from { transform: translateX(-100%); } to { transform: translateX(100%); } }
  @keyframes m-shimmer-text { from { transform: translateX(100%); } to { transform: translateX(-100%); } }

  @keyframes m-kb-in { from { transform: scale(1.08); } to { transform: scale(1); } }
  @keyframes m-kb-drift { from { transform: scale(1); } to { transform: scale(1.035) translate(-.8%, -.5%); } }
  @keyframes m-halo { 0%, 100% { opacity: .7; transform: scale(.9); } 50% { opacity: 1; transform: scale(1.1); } }
  @keyframes m-ring { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: .5; transform: scale(1.2); } }

  /* map-fx:keyframes begin (generated by tools/map-fx.mjs: loop 14000ms, parameter easing x1 0.6 / x2 0.4; do not edit by hand) */
  @keyframes m-fx-xa {   /* traveller a: north-america > africa > asia > australia */
    0% { transform: translateX(359px); }
    2.5% { transform: translateX(359px); animation-timing-function: cubic-bezier(0.6, 0.083, 0.4, 0.462); }
    27.5% { transform: translateX(794px); }
    32.5% { transform: translateX(794px); animation-timing-function: cubic-bezier(0.6, 0.089, 0.4, 0.607); }
    50% { transform: translateX(1064px); }
    55% { transform: translateX(1064px); animation-timing-function: cubic-bezier(0.6, 0.356, 0.4, 0.798); }
    70.7143% { transform: translateX(1203px); }
    100% { transform: translateX(1203px); }
  }
  @keyframes m-fx-ya {
    0% { transform: translateY(781.899px); }
    2.5% { transform: translateY(781.899px); animation-timing-function: cubic-bezier(0.6, 0.521, 0.4, 1.287); }
    27.5% { transform: translateY(931.899px); }
    32.5% { transform: translateY(931.899px); animation-timing-function: cubic-bezier(0.6, 0.628, 0.4, 1.085); }
    50% { transform: translateY(826.899px); }
    55% { transform: translateY(826.899px); animation-timing-function: cubic-bezier(0.6, 0.235, 0.4, 0.671); }
    70.7143% { transform: translateY(1037.899px); }
    100% { transform: translateY(1037.899px); }
  }
  @keyframes m-fx-heada {
    0% { opacity: 0; }
    2.8571%, 72.8571% { opacity: .9; }
    79.2857%, 100% { opacity: 0; }
  }
  @keyframes m-fx-xb {   /* traveller b: europe > latin-america > north-america */
    0% { transform: translateX(759px); }
    2.5% { transform: translateX(759px); animation-timing-function: cubic-bezier(0.6, 0.278, 0.4, 0.778); }
    25.3571% { transform: translateX(511px); }
    38.9286% { transform: translateX(511px); animation-timing-function: cubic-bezier(0.6, 0.618, 0.4, 0.952); }
    57.5% { transform: translateX(359px); }
    100% { transform: translateX(359px); }
  }
  @keyframes m-fx-yb {
    0% { transform: translateY(735.899px); }
    2.5% { transform: translateY(735.899px); animation-timing-function: cubic-bezier(0.6, 0.137, 0.4, 0.498); }
    25.3571% { transform: translateY(984.899px); }
    38.9286% { transform: translateY(984.898px); animation-timing-function: cubic-bezier(0.6, 0.22, 0.4, 0.553); }
    57.5% { transform: translateY(781.898px); }
    100% { transform: translateY(781.898px); }
  }
  @keyframes m-fx-headb {
    0% { opacity: 0; }
    2.8571%, 59.6429% { opacity: .9; }
    66.0714%, 100% { opacity: 0; }
  }
  @keyframes m-fx-seg {   /* in 280ms, hold 2600ms, out 1000ms */
    0% { opacity: 0; }
    2% { opacity: 1; }
    20.5714% { opacity: 1; animation-timing-function: cubic-bezier(.45, 0, .55, 1); }
    27.7143%, 100% { opacity: 0; }
  }
  @keyframes m-fx-ping {  /* 1400ms: r 7 to r 22 */
    0% { opacity: 0; transform: scale(.318); animation-timing-function: cubic-bezier(.2, .7, .2, 1); }
    0.8571% { opacity: .5; }
    10%, 100% { opacity: 0; transform: scale(1); }
  }
  @keyframes m-fx-coda {  /* 1800ms: r 7 to r 26 */
    0% { opacity: 0; transform: scale(.269); animation-timing-function: cubic-bezier(.2, .7, .2, 1); }
    1.4286% { opacity: .28; }
    12.8571%, 100% { opacity: 0; transform: scale(1); }
  }
  /* map-fx:keyframes end */

  @keyframes m-rv-up { from { opacity: 0; transform: translateY(var(--m-rise, 24px)); } }
  @keyframes m-rv-left { from { opacity: 0; transform: translateX(-32px); } }
  @keyframes m-rv-right { from { opacity: 0; transform: translateX(32px); } }
  @keyframes m-rv-scale { from { opacity: 0; transform: scale(.95); } }
  @keyframes m-rv-fade { from { opacity: 0; } }
  /* The clip target waits with opacity 0, not a closed clip: Chromium treats content under a closed
     clip-path as off screen, which would stall its lazy image until the wipe opens. The edge sweeps
     from 70px left of the box to 70px past it, inside a fixed outset that holds the frame's shadow
     (0 30px 60px: 40px above, 100px below, 70px each side). With cubic-bezier(.33,0,.2,1) the edge
     enters the frame at ~140ms, crosses it in ~430ms, arrives at a quarter of its peak speed and
     settles in the shadow; after the wipe there is no clip. */
  @keyframes m-rv-clip {
    from { clip-path: inset(-40px calc(100% + 70px) -100px -70px); }
    to { clip-path: inset(-40px -70px -100px -70px); }
  }
  @keyframes m-rule { from { transform: scaleX(0); } }
  @keyframes m-seq-line { from { transform: scaleX(0); } }
  @keyframes m-seq-pop { from { opacity: 0; transform: scale(0); } }
  @keyframes m-seq-label { from { opacity: 0; transform: translateY(6px); } }
  @keyframes m-seq-label-x { from { opacity: 0; transform: translateX(-6px); } }
}

/* Print: static page, all content visible (the rules above are screen-only), no controls */
@media print {
  .m-controls, .m-pause, .m-progress, .m-scene, .m-shimmer { display: none !important; }
  .site-header, .site-header--js { position: static !important; background: var(--ink, #201F1D) !important; animation: none !important; transform: none !important; }
}
