/* ============================================================================
   THE HOPKINS INSTITUTE — MOTION LAYER, the CSS half
   Pairs with assets/js/motion.js (GSAP 3.15.0 + ScrollTrigger, vendored in
   assets/js/ — see assets/js/SOURCE.md for the licence).

   This file has exactly THREE jobs.

   1 · PRE-HIDE the handful of above-the-fold elements the hero timeline
       reveals, so the finished hero never flashes for a frame before the
       timeline starts.

   2 · Give the rules that "ink in" a transform hook on a PSEUDO-ELEMENT,
       driven by a custom property. This is not stylistic: 00-build-rules.md
       RULE 1's note panels are portalled to <body> and positioned fixed from
       their marker's viewport rect, so a transform, filter or will-change
       anywhere ABOVE a marker re-traps them. Driving a pseudo-element from a
       custom property means the real element in the annotated subtree is never
       transformed — only its ::before/::after, which contains nothing.

   3 · Make two image frames clip, which they had no need to before parallax.

   REDUCED MOTION. Every rule in jobs 1 and 2 is gated on `html[data-motion]`,
   and the inline <head> guard sets that attribute ONLY when
   (prefers-reduced-motion: reduce) does not match. Under `reduce` the
   attribute never appears, so this file contributes nothing at all: every rule
   sits at full length, every element at full opacity, no transform is
   introduced anywhere. That is the genuinely static page, not a fast one.

   The guard also REMOVES the attribute after 2.2s if motion.js never reported
   ready, so a failed script load degrades to the same static page rather than
   leaving the hero invisible.
   ========================================================================= */


/* ---- 1 · pre-hide, hero only ---------------------------------------------
   Scoped to the ACTIVE slide, which is slide 1 in the markup. Slides reached
   later need no pre-hide: GSAP's fromTo() sets their start state
   synchronously, before the browser can paint them. */
html[data-motion] .carousel__slide[data-active="true"] .slide__eyebrow,
html[data-motion] .carousel__slide[data-active="true"] .slide__tagline,
html[data-motion] .carousel__slide[data-active="true"] .slide__legacy,
html[data-motion] .carousel__slide[data-active="true"] .slide__credit{opacity:0}
html[data-motion] .carousel__slide[data-active="true"] .slide__block .nib-rule{
  transform:scaleX(0);transform-origin:left center;
}

/* The card reveal moves from page.js's IntersectionObserver to GSAP when motion
   is on, so the CSS mechanism that page.js drives has to stand down. page.js
   checks for this same attribute and does not arm its observer.
   
   ONLY THE TRANSITION STANDS DOWN — NOT THE HIDDEN STATE. This rule used to set
   `opacity:1;transform:none`, which painted the cards fully visible before first
   paint and left GSAP to hide them again when their trigger fired at `top 85%` —
   i.e. once they were already 15% inside the viewport. The result was a visible
   card that snapped out and faded back in: measured at 23–31 frames on screen at
   full opacity before being hidden.
   
   base.css already sets [data-reveal] to opacity:0 + translateY(16px). That state
   is now KEPT, so nothing is ever painted visible before its own reveal, and GSAP
   simply animates it in. The <head> watchdog's data-shown fallback still reveals
   them if motion.js never runs. */
html[data-motion] [data-reveal]{transition:none}


/* ---- 2 · rules that ink in ----------------------------------------------
   Each hook reads a custom property that defaults to 1 — the finished state —
   so the rule is at full length until motion.js decides otherwise, and stays
   there if it never runs. */

/* Concept A's article spine: the 2px vertical nib hairline on the sticky rail,
   drawn downward against the reader's progress through the article. */
html[data-motion] .article__rail::after{
  transform:scaleY(var(--spine,1));transform-origin:top center;
}
/* The news grid's column divider, drawn downward as the grid arrives. */
html[data-motion] .leadlist::before{
  transform:scaleY(var(--divider,1));transform-origin:top center;
}
/* The 4px nib edge that opens the giving band, and the same device on the
   Slide 2 quote plate: both drawn left to right. */
html[data-motion] .give::before,
html[data-motion] .quotecard.nib-edge::before{
  transform:scaleX(var(--nibtop,1));transform-origin:left center;
}
/* The 3px nib edge along the top of each placeholder plate. */
html[data-motion] .card__ph::after{
  transform:scaleX(var(--plate-edge,1));transform-origin:left center;
}


/* ---- 3 · frames that now have to clip -----------------------------------
   In-frame parallax oversizes the image inside its frame and drifts it, so the
   frame must clip or the image's edge shows. Three of the four frames already
   did (.slide__media, .card__media, .card__ph); these two did not.

   `overflow` ONLY. No geometry changes: the reset already sets
   `img,picture,svg{display:block}`, so there is no inline baseline gap to lose
   and the plate's measured height is identical before and after. And overflow
   is not one of the properties that creates a containing block for a fixed
   descendant, so it is safe above a marker. */
.plate picture{overflow:hidden}
.slide2__photo .duo{overflow:hidden}
