/*
 * Motion layer.
 *
 * Loaded on EVERY page (see borrego_basic_enqueue_assets). That is deliberate
 * and load-bearing: a cross-document view transition only runs if BOTH the
 * outgoing and the incoming document carry the `@view-transition` rule. Scoping
 * this file to the art-directed pages the way art-direction.css is scoped would
 * mean Home -> Donate got no transition at all, which is most of the site's
 * navigation.
 *
 * The same rule explains the split with art-direction.css. That file declares
 * `view-transition-name` on the hero elements, because those elements only
 * exist on Home and About. But ALL `::view-transition-*` styling lives HERE,
 * because on About -> Donate the hero has an OLD snapshot and no new one, and
 * that exit animation is styled by the DESTINATION document — which never loads
 * art-direction.css. Keyframes living there would silently fall back to the
 * UA's default 250ms fade.
 *
 * Everything in this file is progressive enhancement. Firefox implements
 * neither cross-document view transitions (bug 1860854) nor scroll-driven
 * animations, and simply renders the site as it was before this file existed.
 * There is no fallback to write, and nothing here may ever leave content
 * hidden, unreachable or unreadable when a feature is missing.
 */

/* --------------------------------------------------------------------------
   Motion tokens
   --------------------------------------------------------------------------
   theme.json has no settings.custom block; every bespoke custom property in
   this theme lives in a CSS :root (see --bb-clip-*, --bb-ar-* and
   --bb-header-h in art-direction.css). Motion follows that convention rather
   than introducing a second mechanism.

   Durations are named for the SCALE of the thing that moves, not for a
   position on a fast/slow axis — that is what keeps them applied consistently.
   -------------------------------------------------------------------------- */

:root {
	--bb-dur-quick: 140ms;   /* colour / opacity echo on a single control */
	--bb-dur-base:  240ms;   /* one component: lift, wipe, chevron, panel */
	--bb-dur-page:  420ms;   /* whole-page and section-scale morphs */

	--bb-ease-out:   cubic-bezier( 0.2, 0.7, 0.3, 1 );
	--bb-ease-brand: cubic-bezier( 0.32, 0.72, 0, 1 );
}

/* --------------------------------------------------------------------------
   Cross-document view transitions
   --------------------------------------------------------------------------
   Chrome/Edge 126+, Safari 18.2+. Same-origin only, and only for push /
   replace / traverse navigations — reloads and address-bar navigations never
   transition, which is correct.
   -------------------------------------------------------------------------- */

@view-transition {
	navigation: auto;
}

/*
 * Slow the default cross-fade down and give it the brand curve.
 *
 * ONLY animation-duration and animation-timing-function are overridden here,
 * never animation-name. The UA runs TWO animations on ::view-transition-old and
 * ::view-transition-new: the fade itself, and -ua-mix-blend-mode-plus-lighter,
 * which is what stops a cross-fade dipping through a muddy mid-tone. Replacing
 * the `animation` shorthand drops plus-lighter and the cream (#EFE7DB) page
 * background visibly darkens halfway through every swap.
 */
::view-transition-group( * ),
::view-transition-old( * ),
::view-transition-new( * ) {
	animation-duration: var( --bb-dur-page );
	animation-timing-function: var( --bb-ease-brand );
}

/* --------------------------------------------------------------------------
   Persistent chrome
   --------------------------------------------------------------------------
   Naming the header lifts it out of the root snapshot so it holds still while
   the page changes underneath it. That is the whole "connected" effect.

   ONLY the header is named. It is tempting to also name .bb-wordmark and the
   Donate button so they persist individually, but neither is safe:

   1. .bb-wordmark renders TWICE per page (header and patterns/footer.php). Two
      rendered elements sharing a view-transition-name makes ViewTransition.ready
      reject and the browser silently skips the transition on EVERY page of the
      site. The same trap applies to .bb-section-brush (4x per page),
      .bb-section-title (3x) and .bb-btn-primary (2x on About).

   2. Even scoped to the header, they are not the same SIZE on every page.
      art-direction.css gives body.home its own font-size for the wordmark name
      and place line, its own nav font-size and gap, and its own font-size AND
      PADDING for the Donate button. Holding the new snapshot while dropping the
      old — the usual trick for elements that do not change — would produce an
      instant size jump on every Home <-> interior navigation.
   -------------------------------------------------------------------------- */

/*
 * Gated to 1200px, for two reasons that happen to coincide.
 *
 * 1. It is the only width where naming the header BUYS anything. Below 1200px
 *    art-direction.css puts the front-page header back in normal flow on its
 *    own cream band — identical in kind to every interior page — so the root
 *    cross-fade already handles it invisibly. Above 1200px the front-page
 *    header is out of flow and transparent over the hero photograph, which is
 *    the state change actually worth dissolving.
 *
 * 2. It is the only width where the snap below is SAFE. Measured on the
 *    running site, .bb-header__bar heights on Home vs About are:
 *
 *        1440px   46.8 / 46.8   (0.0px)
 *        1200px   45.6 / 46.9   (1.3px)
 *        1024px   38.9 / 46.9   (8.0px)
 *         899px   34.3 / 46.9  (12.6px)
 *
 *    The divergence is driven by body.home's own font-size and padding
 *    overrides for the nav, wordmark and Donate button. Because the group is
 *    snapped rather than animated, any delta appears INSTANTLY at the start of
 *    the transition — fine at 0-1.3px, a visible jolt at 8-12.6px.
 *
 * If the body.home header overrides are ever normalised so the boxes match at
 * every width, this media query can be removed and .bb-wordmark / the Donate
 * button can each be named individually for true persistence.
 */
/* CAUTION: a non-none view-transition-name forms a stacking context at all
   times, not only during a transition (css-view-transitions-1 §2.1.1). That
   traps the mega-menu panel's z-index inside this element, which is why
   style.css gives this same selector `position: relative; z-index: 20`. If you
   move or remove this rule, read that comment first. */
@media ( min-width: 1200px ) {
	.wp-site-blocks > header.wp-block-template-part {
		view-transition-name: bb-header;
		view-transition-class: bb-chrome;
	}
}

/*
 * Snap the chrome's GEOMETRY, cross-fade its PIXELS.
 *
 * A named element that is scrolled out of view is still captured and still
 * animated. Without this, clicking a footer link from 5000px down the Home page
 * animates the header group from y = -5000 to y = 0 — a 420ms streak across the
 * whole screen. `animation-name: none` leaves the group at its base style,
 * which the UA has already set to the NEW state, so it simply appears where it
 * belongs.
 *
 * Author rules beat the UA-origin :root::view-transition-group(<name>) rule by
 * cascade ORIGIN, not specificity, so no !important is needed. If a browser
 * ever disagrees, `animation-duration: 1ms` achieves the same thing.
 */
::view-transition-group( *.bb-chrome ) {
	animation-name: none;
}

/*
 * The snap sets the group box to the NEW header's size. The default
 * `object-fit: fill` would then stretch the OLD header's raster into that box,
 * and because the two headers genuinely differ in height and in wordmark size
 * (see above), the wordmark visibly squashes mid-fade. Pinning the old snapshot
 * at its natural size, anchored top-left, makes the two simply dissolve.
 */
::view-transition-old( bb-header ) {
	object-fit: none;
	object-position: left top;
}

/* --------------------------------------------------------------------------
   Hero continuity
   --------------------------------------------------------------------------
   The names are declared in art-direction.css because the elements only exist
   on Home and About; the styling has to be here. See the file header.

   bb-hero-brush is the tapered stroke under the H1, emitted by
   borrego_basic_brush() on both pages — same helper, same shape, genuinely
   continuous across the navigation.

   bb-hero-art is the hero artwork slot: on Home a single SVG carrying the
   photograph, the forest-green form and a botanical; on About a masked
   photograph. Same slot, different content, so this is a cross-fade plus a box
   reshape rather than a true morph.
   -------------------------------------------------------------------------- */

/*
 * The browser does not transition the <img> or the <svg>. It rasterises the old
 * element, rasterises the new one, and morphs between two flat images. Without
 * object-fit the two snapshots are stretched into the interpolating box and the
 * artwork visibly squashes in flight.
 */
::view-transition-old( bb-hero-art ),
::view-transition-new( bb-hero-art ) {
	block-size: 100%;
	inline-size: 100%;
	object-fit: cover;
	overflow: clip;
}

/*
 * The section hero photograph, on pages that share one masthead across several
 * URLs (the Scholarships section). Same treatment as bb-hero-art: the snapshots
 * are two different photographs at two different intrinsic sizes, so they have
 * to be told to fill the morphing box rather than stretch inside it.
 *
 * Lives here, not beside the name declaration, for the reason at the top of
 * this file: on the LAST page of a section the outgoing snapshot is styled by
 * the destination document, which may be any page on the site.
 */
/* --------------------------------------------------------------------------
   The masthead photograph — a true vertical slide
   --------------------------------------------------------------------------
   Two attempts came before this one and both are worth recording, because each
   failed for a different and instructive reason.

   1. SEQUENCED FADES (the same treatment the copy uses). Old to zero, then new
      from zero, leaves one frame where neither image is painted. Invisible on
      sparse text; on a photograph filling a large box it is a hole, and the
      cream page flashes through it.

   2. A CROSS-FADE. No flash, but dissolving one photograph into a completely
      different one is muddy by nature — for 400ms the frame holds a graduation
      crowd and an aeroplane ghosted through each other. Cross-fades flatter
      similar images; these are not similar.

   So: no opacity animation at all. The old photograph slides up and out of a
   fixed window while the new one slides up into it, their edges meeting
   exactly — a film strip advancing one frame. Nothing blends, so nothing can
   look muddy, and nothing is ever half-transparent, so nothing can flash.

   THREE THINGS MAKE IT WORK:

   a. The name is on the <img> (components.css), so both snapshots are plain
      rectangles. Naming the rounded figure would drag a curved edge through
      the middle of the frame.

   b. ::view-transition-image-pair is the element that clips. Snapshots are NOT
      clipped to their box by default, so without this the outgoing photograph
      would travel up across the header. It also carries the organic frame, so
      the window keeps the shape the photograph had while sitting still.

   c. The two travel the same distance in the same direction, one box-height
      apart, so they stay edge to edge for the whole run: no seam, no overlap.
      That is also why mix-blend-mode is normal here — plus-lighter is for
      cross-fades, and would flare if these ever did touch.
   -------------------------------------------------------------------------- */

::view-transition-image-pair( bb-section-hero-media ) {
	overflow: clip;
	border-radius: var( --bb-radius-blob-soft );
}

/*
 * ASPECT RATIO: leave the sizing alone.
 *
 * The obvious-looking rule here is `inline-size: 100%; block-size: 100%;
 * object-fit: cover`, copied from bb-hero-art above. It is wrong for this one,
 * and it distorts the photograph.
 *
 * A snapshot is a replaced element whose intrinsic size is the captured
 * element's box. The UA default — `inline-size: 100%; block-size: auto` —
 * therefore reproduces it at exactly its captured proportions. Forcing
 * `block-size: 100%` overrides that with the GROUP's height and relies on
 * object-fit to rescue the content, which is a bet on the sizing of a
 * pseudo-element rather than a guarantee.
 *
 * bb-hero-art needs that bet because its two snapshots are genuinely different
 * shapes (an SVG composition on Home, a masked photograph on About). These are
 * not: every masthead photograph in the section is rendered by
 * .bb-photo-landscape into the same 3:2 box, so old and new are always the same
 * size and the default is exactly right.
 *
 * WHICH MEANS: if a masthead is ever given a photograph at a different aspect,
 * the two snapshots will differ in height and the slide will show a seam. Keep
 * every masthead on .bb-photo-landscape.
 */
::view-transition-old( bb-section-hero-media ),
::view-transition-new( bb-section-hero-media ) {
	mix-blend-mode: normal;
}

::view-transition-old( bb-section-hero-media ) {
	animation: var( --bb-dur-page ) var( --bb-ease-brand ) both bb-photo-slide-out;
}

::view-transition-new( bb-section-hero-media ) {
	animation: var( --bb-dur-page ) var( --bb-ease-brand ) both bb-photo-slide-in;
}

@keyframes bb-photo-slide-out {
	to { transform: translateY( -100% ); }
}

@keyframes bb-photo-slide-in {
	from { transform: translateY( 100% ); }
}

::view-transition-group( *.bb-media ),
::view-transition-group( bb-hero-brush ) {
	animation-duration: var( --bb-dur-page );
	animation-timing-function: var( --bb-ease-brand );
}

/* The brush is a solid terracotta shape, not a photograph. Letting it stretch
   IS the effect — a hand-drawn stroke redrawing itself at a new length. */
::view-transition-old( bb-hero-brush ),
::view-transition-new( bb-hero-brush ) {
	object-fit: fill;
}

/* --------------------------------------------------------------------------
   Micro-interactions
   --------------------------------------------------------------------------
   Every hover state in this theme used to snap. These rules only add the ramp:
   the target colours are already declared in style.css and art-direction.css,
   and scripts/check-contrast.mjs already gates them, so nothing here changes a
   rendered colour or affects the contrast audit.

   This is also the only part of the motion layer that Firefox gets, and the
   only part that reaches interior pages, so it carries most of the felt
   improvement.

   `outline` is deliberately absent from every transition list below. A focus
   ring that fades in is a focus ring that is briefly invisible.
   -------------------------------------------------------------------------- */

.bb-btn-primary,
.bb-btn-ghost,
.bb-btn-invert,
.bb-give,
.bb-link-arrow,
.bb-wordmark,
.bb-mega__summary,
.bb-mega__list a,
.bb-mega__card-title a,
.bb-footer-list a,
.bb-eventlist__link,
.bb-event-list__link,
.wp-block-button__link,
.wp-block-navigation a {
	transition:
		color            var( --bb-dur-quick ) var( --bb-ease-out ),
		background-color var( --bb-dur-quick ) var( --bb-ease-out ),
		border-color     var( --bb-dur-quick ) var( --bb-ease-out ),
		box-shadow       var( --bb-dur-base )  var( --bb-ease-out ),
		translate        var( --bb-dur-base )  var( --bb-ease-out ),
		scale            var( --bb-dur-base )  var( --bb-ease-out );
}

/* Retokenise the one transition that predates this file. */
.bb-mega__chevron {
	transition: transform var( --bb-dur-base ) var( --bb-ease-out );
}

/* --- Arrow nudge ---------------------------------------------------------
   Scoped to components that actually END IN AN ARROW. A blanket
   `> [aria-hidden="true"]:last-child` would also catch two HEARTS — the header
   Donate heart (patterns/header.php) and the bare heart inside .bb-btn-invert
   (patterns/carry-mission-forward.php) — and slide them sideways on hover.

   .bb-btn-ghost__play is likewise excluded: it is a circular play badge, not a
   directional arrow, and must not travel. */

.bb-btn-primary > span[ aria-hidden="true" ]:last-child,
.bb-give > span[ aria-hidden="true" ]:last-child,
.bb-link-arrow > span[ aria-hidden="true" ],
.bb-eventlist__arrow,
.bb-event-list__arrow {
	display: inline-block;
	transition: translate var( --bb-dur-base ) var( --bb-ease-out );
}

.bb-btn-primary:hover > span[ aria-hidden="true" ]:last-child,
.bb-btn-primary:focus-visible > span[ aria-hidden="true" ]:last-child,
.bb-give:hover > span[ aria-hidden="true" ]:last-child,
.bb-give:focus-visible > span[ aria-hidden="true" ]:last-child,
.bb-link-arrow:hover > span[ aria-hidden="true" ],
.bb-link-arrow:focus-visible > span[ aria-hidden="true" ],
.bb-eventlist__link:hover .bb-eventlist__arrow,
.bb-event-list__link:hover .bb-event-list__arrow {
	translate: 0.3rem 0;
}

/* --- Hearts --------------------------------------------------------------
   Hearts beat, they do not travel. */

.bb-donate__heart,
.bb-btn-invert > span[ aria-hidden="true" ]:last-child {
	display: inline-block;
	transition: scale var( --bb-dur-quick ) var( --bb-ease-out );
}

.wp-block-button__link:hover .bb-donate__heart,
.wp-block-button__link:focus-visible .bb-donate__heart,
.bb-btn-invert:hover > span[ aria-hidden="true" ]:last-child,
.bb-btn-invert:focus-visible > span[ aria-hidden="true" ]:last-child {
	scale: 1.15;
}

/* --- Navigation underline wipe -------------------------------------------
   theme.json gives core/navigation links `text-decoration: underline` on
   hover. text-decoration cannot be transitioned, so it is replaced with a
   pseudo-element bar that wipes in from the leading edge.

   theme.json emits its element styles through :where(), which has zero
   specificity, so its own hover rule is roughly (0,1,1). The two-class
   selectors below outspecify it cleanly — the same mechanism style.css already
   relies on for headings inside coloured bands. */

.bb-header__nav .wp-block-navigation-item__content {
	position: relative;
}

.bb-header__nav .wp-block-navigation-item__content:hover,
.bb-header__nav .wp-block-navigation-item__content:focus {
	text-decoration: none;
}

.bb-header__nav .wp-block-navigation-item__content::after {
	content: "";
	position: absolute;
	inset-inline: 0;
	inset-block-end: -0.3em;
	block-size: 2px;
	border-radius: 2px;
	background: currentColor;
	scale: 0 1;
	transform-origin: left center;
	transition: scale var( --bb-dur-base ) var( --bb-ease-out );
}

.bb-header__nav .wp-block-navigation-item__content:hover::after,
.bb-header__nav .wp-block-navigation-item__content:focus-visible::after,
.bb-header__nav .current-menu-item > .wp-block-navigation-item__content::after {
	scale: 1 1;
}

/* --- Card lift -----------------------------------------------------------
   `translate` rather than `transform`: .bb-archival-photo already owns
   `transform: rotate(-1.6deg)`, and the independent transform properties
   compose instead of fighting.

   The lift is applied to the UNMASKED wrapper, never to .bb-card__media.
   style.css:77-89 documents Safari mis-rendering mask-image on elements that
   also create a stacking context via transform. If a flicker ever appears on
   iOS, move the lift to the card body only. */

.bb-card,
.bb-feature,
.bb-feature-card {
	transition: translate var( --bb-dur-base ) var( --bb-ease-out );
}

.bb-card:hover,
.bb-card:focus-within,
.bb-feature:hover,
.bb-feature:focus-within,
.bb-feature-card:hover,
.bb-feature-card:focus-within {
	translate: 0 -6px;
}

.bb-card__badge {
	transition: scale var( --bb-dur-base ) var( --bb-ease-out );
}

.bb-card:hover .bb-card__badge,
.bb-card:focus-within .bb-card__badge {
	scale: 1.1;
}

.bb-give:hover,
.bb-give:focus-visible {
	scale: 1.04;
}

/* --- Mega-menu panel -----------------------------------------------------
   DESKTOP ONLY. Above 900px the panel is position:absolute, so animating it
   costs no layout. Below 900px it is a native in-flow accordion whose height
   would need `interpolate-size: allow-keywords` to animate — Chrome-only — so
   the mobile accordion stays instant, exactly as it is today.

   ::details-content is Chrome 131, Firefox 143, Safari 18.4. Browsers without
   it get today's instant open.

   NOTHING BUT `content-visibility` MAY BE DECLARED ON ::details-content.

   This rule set previously carried `opacity` and `translate` on the pseudo,
   and both were actively wrong:

   1. A non-none `translate` makes the element a CONTAINING BLOCK for
      absolutely positioned descendants. .bb-mega__panel is position:absolute
      with inset-inline:0, expecting to resolve against .bb-header__bar — so it
      collapsed from 1200px to 116px, the width of the "Our Programs" <li>.
      Measured in Chrome: panel width 116px with the rule, 1200px without.
   2. `translate` and a fractional `opacity` each also form a STACKING CONTEXT.
      That trapped the panel's z-index:50 inside an unpositioned box, so page
      content painted over the open panel — the same class of bug as the
      view-transition-name trap documented in style.css, and independent of it.

   Both animations now live on .bb-mega__panel, a real element. An element's
   own transform does NOT change its own containing block, so the panel can
   translate itself with no positioning consequence at all. */

/* 600px, matching chrome.css: every width with a horizontal menu bar has the
   anchored dropdown, so the dropdown animation follows it down. */
@media ( min-width: 600px ) {
	.bb-mega__details::details-content {
		transition: content-visibility var( --bb-dur-base ) allow-discrete;
	}

	/* `opacity: 0` in a base style is safe HERE, unlike in the scroll-driven
	   reveals below: it is restored by [open], a plain attribute selector with
	   no feature dependency. A browser without ::details-content support still
	   shows the panel on open. */
	.bb-mega__details .bb-mega__panel {
		opacity: 0;
		translate: 0 -0.5rem;
		transition:
			opacity   var( --bb-dur-base ) var( --bb-ease-out ),
			translate var( --bb-dur-base ) var( --bb-ease-out );
	}

	.bb-mega__details[ open ] .bb-mega__panel {
		opacity: 1;
		translate: 0 0;
	}
}

/* --------------------------------------------------------------------------
   Scroll-driven reveals
   --------------------------------------------------------------------------
   DOUBLE-GATED, and the two gates are not interchangeable:

   1. @supports (animation-timeline: view())
      Chrome 115+, Safari 26+. Firefox is preview-only. Without this guard
      Firefox would parse the keyframes, apply fill-mode with no timeline, and
      content could end up stuck at opacity 0. Never risk that.

   2. @media (prefers-reduced-motion: no-preference)
      The blanket `*` rule in style.css sets animation-duration: 0.01ms, which
      does NOTHING here: a progress-based timeline takes its progress from the
      timeline, not from the clock. Reduced-motion users would otherwise see
      the full reveal. This gate is the only thing that stops it.

   `opacity: 0` appears ONLY inside a `from` keyframe, never in a base style. If
   either gate fails, no animation is declared at all and every element renders
   at its normal, fully visible base style. There is no state in which content
   is hidden.

   DECLARATION ORDER IS LOAD-BEARING. The `animation` shorthand resets every
   longhand it does not set, INCLUDING animation-timeline (back to `auto`, the
   document timeline). animation-timeline and animation-range must therefore
   come AFTER the shorthand, or the scroll timeline is silently destroyed and
   the reveal fires once on page load instead.

   `both` is equally load-bearing. With the default `none` the element renders
   normally before the range, snaps to the hidden `from` keyframe at range
   start, and reverts after range end — a flicker, not a reveal. `both` is safe
   because an INACTIVE timeline leaves the animation's current time unresolved,
   so the animation is not in effect and fill does not apply.
   -------------------------------------------------------------------------- */

@media ( prefers-reduced-motion: no-preference ) {
	@supports ( animation-timeline: view() ) {

		@keyframes bb-reveal {
			from { opacity: 0; translate: 0 1.5rem; }
			to   { opacity: 1; translate: 0 0; }
		}

		@keyframes bb-reveal-side {
			from { opacity: 0; translate: -1.5rem 0; }
			to   { opacity: 1; translate: 0 0; }
		}

		@keyframes bb-drift {
			from { translate: 0 1.2rem; rotate: -1.5deg; }
			to   { translate: 0 -1.2rem; rotate: 1.5deg; }
		}

		/*
		 * Composable base. Individual sections override animation-range ONLY —
		 * that is the sole stagger mechanism available, because
		 * animation-delay is ignored for progress-based timelines.
		 *
		 * `entry X% entry Y%` rather than anything anchored to `cover`: a
		 * `cover 100%` range requires the element to scroll fully past the top
		 * of the viewport, which the LAST revealed element on a page can never
		 * reach if the remaining document is short.
		 */
		.bb-card,
		.bb-home-programs__head,
		.bb-home-impact__item,
		.bb-feature,
		.bb-eventlist__item,
		.bb-home-story__media,
		.bb-home-donate__media,
		.bb-about-story__entry,
		.bb-archival-photo,
		.bb-about-mission__text,
		.bb-about-donors__text {
			animation: bb-reveal linear both;
			animation-timeline: view();
			animation-range: entry 5% entry 55%;
		}

		/*
		 * Botanical line art drifts as the section passes. These are all
		 * aria-hidden, pointer-events: none and absolutely positioned, so
		 * nothing can break. art-direction.css already hides them below 781px.
		 *
		 * Note the Home hero is no longer one of them — its botanical is a PNG
		 * baked into the hero SVG.
		 */
		.bb-botanical {
			animation: bb-drift linear both;
			animation-timeline: view();
			animation-range: cover;
		}
	}
}

/* --------------------------------------------------------------------------
   Reduced motion
   --------------------------------------------------------------------------
   style.css already zeroes transition-duration and animation-duration on every
   ELEMENT. That rule cannot reach ::view-transition-* pseudo-elements: they
   live in a separate pseudo tree rooted on the document element and nothing in
   `*, *::before, *::after` matches them. Without the block below, a full
   420ms morph would play for a user who explicitly asked for reduced motion.

   (The other thing the blanket rule cannot reach — scroll-driven animations —
   is gated at the source above, with prefers-reduced-motion: no-preference.)

   The policy here is "cross-fade, never move". A cross-fade is not a vestibular
   trigger; translation, scale and geometry morphs are. So the transition still
   happens, it just stops travelling. `@view-transition { navigation: none }`
   was considered and rejected: it gives an abrupt white-flash swap on slower
   connections, which is arguably worse for the users this preference protects.
   -------------------------------------------------------------------------- */

@media ( prefers-reduced-motion: reduce ) {

	::view-transition-group( * ) {
		animation-name: none !important;
	}

	::view-transition-old( * ),
	::view-transition-new( * ) {
		animation-duration: 120ms !important;
		animation-timing-function: linear !important;
	}
}
