/* ============================================================
   Scroll reveal — the hidden/visible contract.

   THE ONE RULE THAT MATTERS
   -------------------------
   Every hidden state below is scoped to html.gps-motion, a class added by
   the inline bootstrap in <head> (build.sh emits it; the theme prints it
   from inc/enqueue.php). Nothing else in the codebase sets that class.

   The consequence is the whole point. If the bootstrap never runs, if the
   script 404s, if an optimiser eats it, if the browser has no
   IntersectionObserver, if JavaScript is off entirely -- the class is
   absent, none of these selectors match, and the page paints in its final
   state with every word in the DOM. That is the opposite of the usual
   arrangement, where content starts at visibility:hidden and a script is
   the only thing standing between the visitor and a blank band. A reveal
   animation is decoration; it must never be load-bearing.

   TWO SHAPES
   ----------
   [data-reveal]        one element rises on its own.
   [data-reveal-group]  the container is observed, and its DIRECT CHILDREN
                        rise together in a stagger. One observer entry for
                        a grid of any size, and the delay comes from the
                        child's position -- so adding a category in wp-admin
                        needs no CSS, no PHP and no count anywhere.

   ORDER IS LOAD-BEARING IN THIS FILE
   ----------------------------------
   Most of the rules below share the specificity (0,2,1), so source order
   is what decides them. The sequence is: hidden -> stagger -> revealed ->
   instant -> focus. Each step must be able to overrule the one before it,
   and moving a block breaks something silently.

   The first pairing is the one that actually bit during development. The
   hidden state declares the `transition` SHORTHAND, and a shorthand resets
   every longhand it contains -- including transition-delay. So a stagger
   rule written at a lower specificity computes to 0s on every child and
   the whole grid arrives at once, with no error anywhere to explain why.
   The stagger therefore repeats the full html.gps-motion prefix and sits
   immediately after.

   WHAT IS DELIBERATELY ABSENT
   ---------------------------
   will-change. Every engine already promotes an element for the duration
   of an active opacity/transform transition and drops the layer when it
   ends. Declaring will-change makes that promotion permanent: the
   categories grid alone is thirteen tiles, which at DPR 2 is tens of
   megabytes of GPU memory held for the life of the page, to save a few
   milliseconds once. translateY() is used rather than the translate3d()
   hack, which is will-change wearing a disguise.

   Also absent: height, max-height, margin and display. Only opacity and
   transform animate, and neither contributes to layout, so this file
   cannot produce layout shift.
   ============================================================ */

/* ---- 1. Hidden state ---------------------------------------
   The transition is declared here, on the hidden state, not on the
   revealed one, so it describes both directions. Property list, never
   `all`: `all` would sweep up the card hover transforms further down the
   cascade and delay them by the stagger. */
html.gps-motion [data-reveal],
html.gps-motion [data-reveal-group] > *{
  opacity:0;
  transform:translateY(var(--reveal-rise));
  transition:
    opacity var(--reveal-dur) var(--ease),
    transform var(--reveal-dur) var(--ease);
}

/* ---- 2. Stagger --------------------------------------------
   The delay is arithmetic, not a rule per child: one multiplication
   against the child's index.

   The ladder stops at five and then flattens, which is the important part.
   An unbounded stagger over thirteen tiles at 55ms would leave the last
   one waiting 660ms after the first -- long enough on a fast scroll to
   look broken rather than choreographed. Capping means tile 6 and tile 13
   share a delay, and nobody has ever noticed that; what people notice is
   the cascade starting. :nth-child(n+6) is open-ended, so the cap holds
   for any number of items the site owner adds in wp-admin.

   Must stay directly below the hidden state -- see the header note about
   the shorthand reset. */
html.gps-motion [data-reveal-group] > *{
  transition-delay:calc(var(--reveal-step) * var(--reveal-i, 0));
}
[data-reveal-group] > :nth-child(1){--reveal-i:0}
[data-reveal-group] > :nth-child(2){--reveal-i:1}
[data-reveal-group] > :nth-child(3){--reveal-i:2}
[data-reveal-group] > :nth-child(4){--reveal-i:3}
[data-reveal-group] > :nth-child(5){--reveal-i:4}
[data-reveal-group] > :nth-child(n+6){--reveal-i:5}

/* ---- 3. Revealed state -------------------------------------- */
html.gps-motion [data-reveal].is-revealed,
html.gps-motion [data-reveal-group].is-revealed > *{
  opacity:1;
  transform:none;
}

/* ---- 4. Already on screen at load ---------------------------
   Anything intersecting on the observer's FIRST callback gets .is-instant
   alongside .is-revealed, so it appears with no fade and no delay.

   This protects the Largest Contentful Paint element without anyone having
   to know which element that is. A viewport-size test, a breakpoint or a
   hardcoded "skip the first row" would all have to be re-tuned every time
   the hero height or the grid density changes; asking the observer is
   count-free, breakpoint-free and correct on a phone and a 5K display
   alike.

   Sits after the stagger deliberately: both are (0,2,1), and this one has
   to win, or an above-the-fold grid would still hold its last tiles back
   by a quarter of a second on first paint. */
html.gps-motion .is-instant,
html.gps-motion .is-instant > *{
  transition:none;
  transition-delay:0ms;
}

/* ---- 5. Focus escape hatch ----------------------------------
   An element at opacity:0 is still focusable and still in the
   accessibility tree. Every reveal target here contains links -- category
   tiles, part cards, partner marks -- so without this rule a keyboard user
   tabbing ahead of their own scroll position lands on a control they
   cannot see.

   Last of the five, so it beats every state above it. Costs pointer users
   nothing, since it only matches once focus is actually inside. */
html.gps-motion [data-reveal]:focus-within,
html.gps-motion [data-reveal-group] > :focus-within{
  opacity:1;
  transform:none;
  transition:none;
  transition-delay:0ms;
}

/* ---- Reduced motion ----------------------------------------
   tokens.css already crushes every duration to .01ms globally under this
   query, which would technically "work" -- but it works by running a
   0.01ms animation from opacity:0, and an element that begins hidden and
   depends on a near-instant transition to become visible is one dropped
   frame away from staying hidden.

   So this states the end state directly rather than rushing towards it.
   The bootstrap also declines to add .gps-motion at all when the
   preference is set, which makes this a second line of defence for the
   visitor who changes the setting mid-session. */
@media (prefers-reduced-motion:reduce){
  html.gps-motion [data-reveal],
  html.gps-motion [data-reveal-group] > *{
    opacity:1;
    transform:none;
    transition:none;
    transition-delay:0ms;
  }
}
