/* motion.css - the one motion layer the whole site shares.
 * =====================================================================
 *
 * WHAT THIS FILE IS. Every reveal, cover fade and card hover on the site is
 * four attribute selectors below, driven by js/motion.js. No component writes
 * a keyframe of its own and no component template had to change: the script
 * finds the bands, the cards and the covers that already exist and stamps
 * data-reveal / data-img / data-frame / data-lift on them.
 *
 * IT IS ARMED BY SCRIPT, NEVER BY MARKUP. Every rule here is behind
 * html.js-motion, which js/motion.js adds. A visitor with no JavaScript, or one
 * whose script fails, never matches a single selector below and therefore sees
 * the page exactly as main.css paints it - fully visible. A reveal that hides
 * content the script cannot un-hide is the one failure mode this design cannot
 * have, so the hidden state simply does not exist without the class.
 *
 * THE MOTION RULES (docs), and how this file keeps them:
 *   1. Transform and opacity only. Nothing below animates width, height, top,
 *      left, margin or padding - the hover scale is a transform inside a
 *      clipped frame, so the frame's box never changes and nothing reflows.
 *   2. Nothing above the fold fades in. Enforced in the SCRIPT, which measures
 *      each candidate once and only arms what is below the fold, and skips the
 *      first band outright so LCP never waits for an observer.
 *   3. prefers-reduced-motion is answered ONCE, at the foot of this file, and
 *      again in the script, which removes html.js-motion and so takes the whole
 *      layer out in a single class.
 *   4. No layout shift: the covers keep main.css's aspect-ratio boxes, so a
 *      fading image occupies its final box from the first frame.
 *   5. 160ms for the micro-interactions, 280ms for the entrances, and the
 *      entrance easing is the documented cubic-bezier(.2,.6,.2,1).
 *   6. The reader is not in scope - pages/reader.html does not extend
 *      base.html, so nothing here is even loaded there.
 *
 * NO LITERAL COLOURS, same as main.css: the two values that paint (the
 * placeholder tint and the hover shadow) are tokens from tokens/<theme>.css.
 * ===================================================================== */


/* ================================================================ TOKENS === */

/* The motion layer's own values, declared once so the reduced-motion block at
   the foot is the ONLY place that has to answer the preference - the same
   trick main.css plays with --lift and --t-fast, which are still the tokens
   any OTHER file writes. Nothing below names a duration inline. */
:root {
  --m-rise: 10px;
  --m-in: 280ms;
  --m-fast: 160ms;
  --m-ease: cubic-bezier(.2, .6, .2, 1);
  --m-lift: translateY(-2px);
  --m-zoom: scale(1.03);
}


/* ================================================================ REVEAL === */

/* Bands and cards rise and fade as they arrive. The script stamps the empty
   attribute (armed) and later flips it to "in"; both states match [data-reveal]
   so the transition is declared once and drives the element in.

   --m-delay is the stagger, written inline by the script per card and read
   here. It is a DELAY, not a second animation: the transition is identical for
   every cell in a grid, so 24 cards cost one style rule.

   The script removes the attribute again on transitionend, which is what keeps
   this rule's `transition` from outliving the reveal and slowing down the
   card's own hover afterwards. */
.js-motion [data-reveal] {
  opacity: 0;
  transform: translateY(var(--m-rise));
  transition: opacity var(--m-in) var(--m-ease) var(--m-delay, 0ms),
              transform var(--m-in) var(--m-ease) var(--m-delay, 0ms);
}
.js-motion [data-reveal="in"] {
  opacity: 1;
  transform: none;
}

/* FOCUS IS NEVER SUPPRESSED. A keyboard user tabbing down the page must see
   where focus went, so an armed band that receives focus stops being a reveal
   and becomes visible on the spot - no delay, no fade, no waiting for the
   observer. js/motion.js does the same thing on focusin (and drops the
   attribute entirely); this is the half that works even if the listener never
   runs, because a focus ring on an invisible element is a trap. */
.js-motion [data-reveal]:focus-within {
  opacity: 1;
  transform: none;
  transition: none;
}


/* ======================================================== COVER FADE-IN === */

/* Covers arrive over a neutral plate instead of snapping in. The plate is on
   the WRAPPER, not on the image: an image at opacity 0 takes its own background
   with it, so the tint has to be painted by something that is not fading. The
   script only stamps a wrapper it recognises (main.css's cover anchors, which
   are sized by the image they hold) and removes the stamp again on load, so
   nothing is left tinted behind a transparent PNG.

   No box changes: .cover already carries aspect-ratio and the width/height
   attributes, so the space is reserved before the bytes land (motion rule 4).
   An image that was already decoded - warm cache, bfcache - is never stamped at
   all and paints instantly. */
.js-motion [data-tint] {
  background-color: var(--surface-control);
}
.js-motion img[data-img] {
  opacity: 0;
}
.js-motion img[data-img="in"] {
  opacity: 1;
}

/* One transition for both jobs a cover has - fading in, and scaling under the
   pointer - so the two rules can never overwrite each other's shorthand. */
.js-motion img[data-img],
.js-motion [data-frame] > .cover {
  transition: opacity var(--m-in) ease-out, transform var(--m-fast) ease-out;
}


/* ================================================================= HOVER === */

/* The frame is the clip. Its border-radius is --r-card because that is what
   .cover and every cover wrapper's inner hairline already use, so the corner is
   the one that was always there - this rule only stops the scaled artwork from
   escaping it. */
.js-motion [data-frame] {
  border-radius: var(--r-card);
  overflow: hidden;
}

/* The card lifts a step and takes the elevation ramp's next stop with it.
   border-color and background-color are in the list because the components this
   sits on top of (main.css's cards, discovery.css's trending card) transition
   those on hover, and a `transition` shorthand at higher specificity would
   otherwise make their colour change snap. */
.js-motion [data-lift] {
  transition: background-color var(--m-fast) ease-out,
              border-color var(--m-fast) ease-out,
              box-shadow var(--m-fast) ease-out,
              transform var(--m-fast) ease-out;
}
.js-motion [data-lift]:hover {
  box-shadow: var(--shadow-2);
  transform: var(--m-lift);
}

/* THE FRAME DOES NOT MOVE. main.css and discovery.css both lift the cover
   ANCHOR on card hover; with a scale inside it that reads as two things sliding
   at once, and the point of a clipped frame is that its box is the fixed thing
   the artwork moves inside. The attribute is doubled to raise specificity over
   discovery.css, which is loaded from a page's extra_css block and therefore
   lands after this file - the alternative was !important, which would also have
   taken out the reduced-motion answer below. */
.js-motion [data-lift]:hover [data-frame][data-frame] {
  transform: none;
}
.js-motion [data-lift]:hover [data-frame] > .cover {
  transform: var(--m-zoom);
}


/* ==================================================== VIEW TRANSITIONS === */

/* Same-site navigation gets a cross-fade, as pure progressive enhancement: the
   at-rule IS the feature check. A browser without the View Transitions API
   drops the whole rule at parse time and navigates the way it always did, and
   the transition only ever runs when BOTH documents opt in - which is the
   spec's own rule and is why the reader, whose template does not extend
   base.html and so never loads this file, is left alone (motion rule 6).
   No view-transition-name is assigned anywhere: this is a document cross-fade,
   not an element morph, so nothing on the page can be caught mid-flight. */
@view-transition {
  navigation: auto;
}
::view-transition-old(root) {
  animation-duration: var(--m-fast);
  animation-timing-function: ease-out;
}
::view-transition-new(root) {
  animation-duration: var(--m-in);
  animation-timing-function: var(--m-ease);
}


/* ================================================================ MOTION === */

/* The preference, answered once. js/motion.js also removes html.js-motion the
   moment the query matches - including LIVE, through a matchMedia listener, so
   a visitor who changes the OS setting does not have to reload - which is what
   actually takes this layer out. This block is the half that does not depend on
   the script: it neutralises the tokens above and, more importantly, forces
   every armed state VISIBLE, so a page caught mid-reveal when the setting flips
   cannot leave anything hidden.

   main.css's own reduced-motion block already clamps every transition on the
   page to .01ms !important, but it cannot reach the ::view-transition pseudos -
   they are not in the document tree - so those are disabled here. */
@media (prefers-reduced-motion: reduce) {
  :root {
    --m-rise: 0px;
    --m-lift: none;
    --m-zoom: none;
  }
  .js-motion [data-reveal],
  .js-motion img[data-img] {
    opacity: 1;
    transform: none;
    transition: none;
  }
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}
