/**
 * Animation layer.
 *
 * Loaded last in the chain in functions.php (after block-editorial-sections)
 * so the overrides below win equal-specificity ties on source order alone —
 * none of them needs !important, and adding it would be actively harmful on
 * the header selectors, where bundle.js writes inline styles this file must
 * not fight (see "Header, nav and overlays" below).
 *
 * Three things live here:
 *   1. The [data-anim] hidden state that js/anim.js reveals.
 *   2. Hover/press polish on things JS never touches.
 *   3. Transitions for state classes toggled by js/bundle.js, which is a
 *      webpack artifact with no source on disk and therefore unmodifiable —
 *      CSS on its existing class toggles is the only lever available.
 *
 * Timings come from the --duration-* / --ease-* tokens in
 * css/design-system.css.
 *
 * There is deliberately NO prefers-reduced-motion block in this file. The
 * blanket override at css/style.css:716 already zeroes animation-duration
 * and transition-duration on *, *::before and *::after, which covers every
 * rule here. What it cannot cover is Motion's WAAPI animations, which is why
 * reduced motion is also checked in the inline gate in parts/html-head.php —
 * that gate never adds html.js-anim, so the hidden state below never applies
 * and js/anim.js no-ops.
 */

/* ---------------------------------------------------------------------------
   Scroll-reveal hidden state
   --------------------------------------------------------------------------- */

/* Gated behind html.js-anim, set by the inline script in parts/html-head.php
   and only when JS is running, the browser supports IntersectionObserver and
   WAAPI, and prefers-reduced-motion is not set. If any of those is false the
   class is absent, these rules match nothing, and content renders at full
   opacity exactly as it did before animations existed.

   Only opacity is set, never transform: js/anim.js supplies the transform as
   an explicit `from` keyframe, so an element that is hidden but not yet
   animated sits at its final position. No layout shift, no CLS contribution.

   visibility and display are deliberately untouched. An opacity:0 element is
   still in the accessibility tree, still focusable, and still findable by
   in-page search; a visibility:hidden one is none of those. */
html.js-anim [data-anim]:not(.is-anim-in) {
  opacity: 0;
}

/* A focused element must never be invisible (WCAG 2.4.7). A keyboard user can
   Tab faster than they scroll and land inside a group IntersectionObserver
   has not reached yet. This is a pure-CSS backstop: it holds even if
   js/anim.js failed to load entirely. */
html.js-anim [data-anim]:not(.is-anim-in):focus-within {
  opacity: 1;
}

/* ---------------------------------------------------------------------------
   Split-line reveal — the lines and clip-up presets
   --------------------------------------------------------------------------- */

/* A successfully split element must NOT stay at opacity:0, because the reveal
   it is waiting for happens on its line wrappers rather than on itself. The
   class is written by split() in js/anim.js only once the wrappers exist, so
   an element whose split failed keeps the ordinary hidden state and the
   ordinary fade-up fallback. */
html.js-anim [data-anim].is-anim-split:not(.is-anim-in) {
  opacity: 1;
}

/* The mask. overflow:hidden on the outer plus translateY(100%) on the inner
   is what produces the bottom-up wipe — deliberately NOT clip-path, which is
   not compositor-accelerated when animated in Chromium and invalidates paint
   on every frame. This is a pure transform.

   The padding/margin pair stops the mask cropping descenders: overflow:hidden
   clips to the line box, which sits above the bottom of a "g" or "y" at the
   tight line-heights these display headings use. */
.a-lines,
.a-line,
.a-line__in {
  display: block;
}

.a-line {
  overflow: hidden;
  padding-bottom: 0.12em;
  margin-bottom: -0.12em;
}

/* ---------------------------------------------------------------------------
   Buttons and links
   --------------------------------------------------------------------------- */

/* .c-btn already carries `transition: color .2s, background-color .2s,
   border-color .2s` (style.css:1700). This only retimes it onto the token
   scale and adds transform for the press state below — the properties and
   their behaviour are unchanged.

   The trailing-arrow nudge (`.c-btn:hover .c-btn-icon { translateX(4px) }`,
   style.css:1847) is already implemented and is NOT repeated here. */
.c-btn {
  transition:
    color var(--duration-fast) var(--ease-standard),
    background-color var(--duration-fast) var(--ease-standard),
    border-color var(--duration-fast) var(--ease-standard),
    transform var(--duration-fast) var(--ease-standard);
}

/* Press feedback. 1px, and on :active only, so it reads as the button
   physically taking the click rather than as an animation. */
.c-btn:active {
  transform: translateY(1px);
}

/* Focus rings are never transitioned. The outline must appear on the same
   frame as the focus event — a fade there reads as lag and undermines the
   focus-visible work this branch exists for (WCAG 2.4.7). Listed explicitly
   so a later blanket `transition: all` cannot reintroduce it. */
:focus-visible,
html.user-tab-nav :focus {
  transition-property: color, background-color, border-color, transform;
}

/* ---------------------------------------------------------------------------
   Cards
   --------------------------------------------------------------------------- */

/* :focus-within as well as :hover throughout. Several of these cards have
   their full-card overlay anchor commented out in the markup (see
   parts/single/news/news-card.php:11 and parts/search/results-single.php:49)
   and rely on an inner <a>, so keyboard users reach the link without the
   card ever being hovered. */
/* This rule used to transition box-shadow while the hover rule below set only
   a transform, so the box-shadow half animated nothing at all. Giving it a
   real elevation change is most of what turns the lift from a nudge into
   depth — a card that rises without its shadow growing reads as sliding
   rather than lifting. Tokens are in css/design-system.css. */
.program-card__wrapper,
.news-card__wrapper,
.event-card__wrapper,
.blog-post__card-wrapper,
.upcoming-events-card__wrapper,
.search-result-card__wrapper {
  box-shadow: var(--shadow-0);
  transition:
    transform var(--duration-base) var(--ease-out),
    box-shadow var(--duration-base) var(--ease-out);
}

.program-card:hover .program-card__wrapper,
.program-card:focus-within .program-card__wrapper,
.news-card:hover .news-card__wrapper,
.news-card:focus-within .news-card__wrapper,
.event-card:hover .event-card__wrapper,
.event-card:focus-within .event-card__wrapper,
.blog-post__card:hover .blog-post__card-wrapper,
.blog-post__card:focus-within .blog-post__card-wrapper,
.upcoming-events-card:hover .upcoming-events-card__wrapper,
.upcoming-events-card:focus-within .upcoming-events-card__wrapper,
.search-result-card:hover .search-result-card__wrapper,
.search-result-card:focus-within .search-result-card__wrapper {
  transform: translateY(-6px);
  box-shadow: var(--shadow-2);
}

/* Image zoom. overflow:hidden goes on the figure so the scaled image is
   clipped to the frame rather than spilling over neighbouring content.
   Applied only to these specific figures, never to `figure` generally —
   a blanket rule would clip captions and badges elsewhere. */
.program-card__thumbnail,
.blog-post__card-thumbnail,
.news-card__image,
.event-card__image,
.search-result-card__thumbnail {
  overflow: hidden;
}

/* The image moves further and settles slower than the card that contains it.
   That difference is the whole effect: two elements of one component moving
   at different rates reads as depth, where a single uniform transform reads
   as a state change.

   The scale stays on the image and is deliberately NOT added to the card
   wrapper — scaling a wrapper resamples its text, which is visibly soft on
   non-retina displays for the duration of the transition. */
.program-card__thumbnail img,
.blog-post__card-thumbnail img,
.news-card__image img,
.event-card__image img,
.search-result-card__thumbnail img {
  transition: transform var(--duration-hover-media) var(--ease-reveal);
}

.program-card:hover .program-card__thumbnail img,
.program-card:focus-within .program-card__thumbnail img,
.blog-post__card:hover .blog-post__card-thumbnail img,
.blog-post__card:focus-within .blog-post__card-thumbnail img,
.news-card:hover .news-card__image img,
.news-card:focus-within .news-card__image img,
.event-card:hover .event-card__image img,
.event-card:focus-within .event-card__image img,
.search-result-card:hover .search-result-card__thumbnail img,
.search-result-card:focus-within .search-result-card__thumbnail img {
  transform: scale(1.06);
}

/* ---------------------------------------------------------------------------
   Header, nav and overlays

   Every selector below is toggled by js/bundle.js. That file is a webpack
   artifact whose source does not exist on disk, so it cannot be changed;
   these rules attach transitions to the classes it already writes.

   bundle.js also writes INLINE styles on some of these elements —
   .main-header (transform), .main-header__nav-mobile (top, height),
   .search-bar. Inline styles beat any stylesheet without !important, and
   !important here would break the sticky-header maths. So only properties
   bundle.js does not write are transitioned: opacity, transform (except on
   .main-header), visibility.
   --------------------------------------------------------------------------- */

/* Mobile drawer (style.css:5903 / 5916). The base rule is display:none, which
   cannot transition — hence display:block plus pointer-events:none for the
   closed state. visibility:hidden is retained deliberately: it is what keeps
   the drawer out of the accessibility tree and out of the tab order while
   closed, which display:none was previously doing. */
.main-header__nav-mobile {
  display: block;
  pointer-events: none;
  transform: translateY(-8px);
  transition:
    opacity var(--duration-slow) var(--ease-out),
    transform var(--duration-slow) var(--ease-out),
    visibility var(--duration-slow);
}

.main-header__nav-mobile.active {
  pointer-events: auto;
  transform: none;
}

/* Mega menu (style.css:5299 base, 5575 .active). The base has no opacity and
   no transition at all today, so it snaps.

   Scoped to >=992px on purpose: below that breakpoint style.css:5313/5327
   already runs a translateX(100%) slide-in drawer, and an unscoped
   translateY here would clobber it. max-height is deliberately not
   transitioned — the element is overflow-y:auto and a height transition
   jumps the scroll position. */
@media (min-width: 992px) {
  .mega-menu-wrapper {
    opacity: 0;
    transform: translateY(-6px);
    transition:
      opacity var(--duration-fast) var(--ease-out),
      transform var(--duration-fast) var(--ease-out),
      visibility var(--duration-fast);
  }

  .mega-menu-wrapper.active {
    opacity: 1;
    transform: none;
  }
}

/* Search overlay (style.css:6819 / 6851). Already fades via `transition:
   opacity .2s`, but display:none on the base defeats it — the element is
   display-swapped and painted at opacity 1 in the same frame. Same fix as
   the drawer. */
.search-bar {
  display: flex;
  visibility: hidden;
  transform: translateY(-8px);
  transition:
    opacity var(--duration-base) var(--ease-out),
    transform var(--duration-base) var(--ease-out),
    visibility var(--duration-base);
}

.search-bar.active {
  visibility: visible;
  transform: none;
}

/* Video lightbox (style.css:3605 / 3628). Has the right transition already;
   `display: none` in the closed state defeats it. visibility:hidden stays so
   the role="dialog" is kept out of the accessibility tree and its contents
   stay untabbable while closed. */
.video-lightbox:not(.active) {
  display: flex;
  pointer-events: none;
}

.video-lightbox {
  transition:
    opacity var(--duration-base) var(--ease-out),
    visibility var(--duration-base);
}

/* Scrim behind the mobile drawer and search overlay. */
body.overlayed::before {
  transition: opacity var(--duration-base) var(--ease-out);
}

/* ---------------------------------------------------------------------------
   Filter results

   The archive grids are rendered by the React app in eight29-filters/, not by
   PHP, and they re-mount on every filter change. They deliberately carry no
   [data-anim]: a reveal would replay on every keystroke and collide with the
   aria-live region footer.php sets on the results container. A container-level
   cross-fade gives the change some continuity without re-animating each card.
   --------------------------------------------------------------------------- */
.eight29-posts {
  transition: opacity var(--duration-fast) var(--ease-standard);
}

.eight29-posts.loading {
  opacity: 0.45;
}

/* ---------------------------------------------------------------------------
   Scroll-linked layer

   Driven by js/scroll-fx.js. Only the static, first-paint half lives here —
   the part that must be applied before that file has even parsed.
   --------------------------------------------------------------------------- */

/* Pre-overscale. A parallaxing image travels +/- (amount * height) / 2 about
   its centre, so at one extreme it would expose the edge of its frame unless
   it is larger than the frame to begin with. Doing that here rather than in
   JS means it is true at first paint, so there is no frame in which the gap
   is visible.

   This contributes nothing to layout, and therefore nothing to CLS: every
   [data-parallax] element is absolutely positioned and object-fit:cover
   inside an ancestor with a deterministic box, which js/scroll-fx.js verifies
   before it will animate one.

   will-change is set statically here, which is the opposite of the rule
   js/anim.js follows for reveals — and deliberately so. The reason a blanket
   will-change is wrong there is that [data-anim] has hundreds of candidates
   on an archive page and each reveal lasts under a second. These elements
   animate for the entire session and are hard-capped at a handful by
   MAX_SCROLL_FX, so permanent promotion is exactly what they want. */
[data-parallax] {
  scale: calc(1 + var(--parallax-amount, 0.15));
  will-change: transform;
}

/* Section progress rail. transform:scaleX, never width — a width change on a
   value written every scroll frame would relayout the section on every one of
   them, where a transform is composited. */
[data-progress-bar] {
  transform: scaleX(var(--progress, 0));
  transform-origin: left center;
}

/* ---------------------------------------------------------------------------
   Buttons

   Elevation only, never a second transform: .c-btn-has-icon already nudges
   its trailing arrow on hover (style.css:1847), and a simultaneous lift on
   the button makes the two movements fight for the same gesture.
   --------------------------------------------------------------------------- */
.c-btn-primary {
  transition:
    color var(--duration-fast) var(--ease-standard),
    background-color var(--duration-fast) var(--ease-standard),
    border-color var(--duration-fast) var(--ease-standard),
    box-shadow var(--duration-fast) var(--ease-standard),
    transform var(--duration-fast) var(--ease-standard);
}

.c-btn-primary:hover,
.c-btn-primary:focus-visible {
  box-shadow: var(--shadow-1);
}
