/**
 * Design Sprint 4 — Luxury Motion & Premium UX.
 *
 * The theme already had motion: Phase 2.14 added scroll-reveal, hover-lift,
 * counters and the ripple (`animations.css` + `animations.js`); Sprint 1
 * gave the hero its transform-only entrance; Sprint 2 built the card and
 * timeline behaviour; Sprint 3 added the reading-progress bar and the
 * sticky rails. What was missing was a *system*: durations, easings,
 * distances and scales were being picked per rule, interaction states
 * stopped at `:hover` (no press, no loading, no touch-safe behaviour), the
 * desktop dropdown appeared with `display: none` → `display: flex` (an
 * un-animatable jump), forms had no motion at all, and every reveal target
 * kept `will-change` alive forever.
 *
 * This module closes that. It is loaded LAST, after every other stylesheet,
 * because a handful of its rules deliberately refine interaction states
 * declared in earlier modules (the touch-safe hover neutralisation, the
 * dropdown's visibility model, the GPU skeleton sheen). It **adds and
 * refines behaviour; it redesigns nothing** — no colour, type, spacing,
 * radius or layout value from Sprints 1–3 is changed, and nothing here
 * selects `.home-hero--premium`.
 *
 * Rules this module holds itself to:
 *   - **One definition per value.** §0 declares the `--motion-*` token set
 *     as single-definition aliases over the global tokens; every rule below
 *     consumes those, and no rule restates a raw duration, easing,
 *     distance or scale.
 *   - **Transform, opacity and (sparingly) filter only.** Nothing here
 *     animates `top`, `left`, `width`, `height`, `margin`, `padding` or any
 *     other property that forces layout, so no rule in this file can
 *     contribute to CLS or trigger a reflow mid-animation.
 *   - **Motion never gates interaction.** Every effect is a transition or
 *     keyframe on an element that is already interactive; no handler waits
 *     for an animation, and nothing is hidden pending a script.
 *   - **Capability-gated, not device-sniffed.** Hover affordances live
 *     inside `@media (hover: hover) and (pointer: fine)`; touch gets press
 *     feedback instead of a sticky hover state.
 *   - **No library.** No GSAP, no AOS, no framework. `assets/js/motion.js`
 *     is ~90 lines and only ever toggles a class or sets a custom property.
 *
 * See docs/MOTION_SYSTEM.md for the token reference and the decision log.
 */

/* ==================================================================
   0. Motion tokens — one definition each, all derived from the
      global token set in tokens.css
      ------------------------------------------------------------------
      Same discipline as Sprint 2's `--premium-*` aliases: these are
      *names for existing values*, not new values, so a duration or
      easing still lives in exactly one place. The four raw numbers
      below (`--motion-delay-step`, the distances, the scales) are the
      values Phase 2.14 and Sprints 1–3 were already using inline —
      80ms stagger, 6px lift, 28px fade-up travel — now named instead of
      repeated.

      A handful of rungs below are not consumed by this file. That is on
      purpose and is the point of the exercise: `--motion-distance-lg`
      (12px) is the drawer's item travel in header.css,
      `--motion-distance-xl` (28px) is the fade-up travel in
      animations.css, `--motion-lift` (6px) is the card lift in
      components.css and sections-premium.css, `--motion-duration-slow`
      is the reveal duration, and `--motion-scale-pop` (1.1) is the icon
      pop in components.css. Those rules live in modules this sprint must
      not modify, so the token *names* the value they already use rather
      than introducing a second one — and the next rule that needs it
      picks a name instead of a number.
   ================================================================== */

:root {
	/* ---- Duration ---- */
	--motion-duration-instant: var(--transition-fast);
	--motion-duration-quick: var(--transition-base);
	--motion-duration-moderate: var(--transition-slow);
	--motion-duration-slow: var(--duration-fade);
	/* The one duration the global set has no equivalent for: a spinner
	   needs roughly one turn per second to read as "working", where
	   `--duration-shimmer` (2.4s) reads as "stalled". */
	--motion-duration-spin: 700ms;

	/* ---- Delay ---- */
	--motion-delay-none: 0ms;
	--motion-delay-step: 80ms;
	--motion-delay-settle: 160ms;

	/* ---- Easing ---- */
	--motion-ease-standard: var(--ease-standard);
	--motion-ease-in: var(--ease-in);
	--motion-ease-out: var(--ease-out);
	--motion-ease-spring: var(--ease-bounce);

	/* ---- Distance (travel) ---- */
	--motion-distance-hairline: 1px;
	--motion-distance-xs: 2px;
	--motion-distance-sm: 4px;
	--motion-distance-md: 6px;
	--motion-distance-lg: 12px;
	--motion-distance-xl: 28px;

	/* ---- Scale ---- */
	--motion-scale-press: 0.97;
	--motion-scale-nudge: 1.015;
	--motion-scale-zoom: 1.06;
	--motion-scale-pop: 1.1;

	/* ---- Composed transitions: the pairs every rule below reuses ---- */
	--motion-t-instant: var(--motion-duration-instant) var(--motion-ease-standard);
	--motion-t-quick: var(--motion-duration-quick) var(--motion-ease-standard);
	--motion-t-enter: var(--motion-duration-moderate) var(--motion-ease-out);
	--motion-t-spring: var(--motion-duration-quick) var(--motion-ease-spring);

	/* ---- Composed transforms ---- */
	--motion-lift-sm: translateY(calc(var(--motion-distance-xs) * -1));
	--motion-lift: translateY(calc(var(--motion-distance-md) * -1));
	--motion-press: scale(var(--motion-scale-press));
}

/* ==================================================================
   1. Buttons — hover lift, arrow, press, loading, disabled, focus
      ------------------------------------------------------------------
      `components.css` transitions background, border, colour and shadow
      on `.btn` but not `transform`, so the lift and the press had
      nothing to interpolate. Added here rather than there so the
      button's *appearance* stays owned by `components.css` and its
      *motion* by this module.
   ================================================================== */

.btn {
	transition:
		background-color var(--motion-t-instant),
		border-color var(--motion-t-instant),
		color var(--motion-t-instant),
		box-shadow var(--motion-t-quick),
		transform var(--motion-t-quick);
}

/* Hover affordances are capability-gated: a touch device that reports
   `hover: none` never gets a lift it cannot undo. */
@media (hover: hover) and (pointer: fine) {
	.btn:hover {
		transform: var(--motion-lift-sm);
	}

	/* Hover glow on the primary action only — the secondary and ghost
	   variants stay quiet so the page keeps one obvious next step. */
	.btn--primary:hover {
		box-shadow: var(--shadow-glow-primary);
	}
}

/* Keyboard focus keeps the accessibility.css ring and adds the lift, so
   a keyboard user sees the same affordance a pointer user does. */
.btn:focus-visible {
	transform: var(--motion-lift-sm);
}

/* Press: the same feedback on mouse, pen and touch. Deliberately faster
   than the hover lift — a press should feel immediate, not eased.
   Declared after `:focus-visible` on purpose: a click both focuses and
   activates, and the press is the state that should win. */
.btn:active {
	transform: var(--motion-press);
	transition-duration: var(--motion-duration-instant);
}

/* Loading state. Class- or attribute-driven, so any form handler (a
   contact-form plugin, a future booking step) can opt in without new
   markup: add `.is-loading` or `aria-busy="true"`.

   `.btn` carries `overflow: hidden` (the ripple rule in animations.css),
   so the spinner is deliberately drawn *inside* the button's box. The
   label stays in place and keeps its width — nothing reflows, and the
   button never changes size, so a loading button cannot shift the layout
   around it. */
.btn.is-loading,
.btn[aria-busy='true'] {
	position: relative;
	color: transparent;
	pointer-events: none;
	transform: none;
}

.btn.is-loading > *,
.btn[aria-busy='true'] > * {
	visibility: hidden;
}

.btn.is-loading::after,
.btn[aria-busy='true']::after {
	content: '';
	position: absolute;
	inset-block-start: 50%;
	inset-inline-start: 50%;
	inline-size: 1.15rem;
	block-size: 1.15rem;
	margin-block-start: -0.575rem;
	margin-inline-start: -0.575rem;
	border: 2px solid currentColor;
	border-radius: var(--radius-full);
	border-block-start-color: transparent;
	/* The border colour comes from the button's own text colour, which is
	   `transparent` above — so re-assert it on the spinner only. */
	color: var(--color-text-inverse);
	animation: agencycare-spin var(--motion-duration-spin) linear infinite;
}

.btn--outline.is-loading::after,
.btn--ghost.is-loading::after,
.btn--outline[aria-busy='true']::after,
.btn--ghost[aria-busy='true']::after {
	color: var(--color-primary-600);
}

@keyframes agencycare-spin {
	to {
		transform: rotate(1turn);
	}
}

/* Disabled: no lift, no press, no pointer. `components.css` already sets
   the opacity and the cursor. */
.btn:disabled,
.btn[aria-disabled='true'] {
	transform: none;
}

.btn:disabled:hover,
.btn:disabled:active,
.btn[aria-disabled='true']:hover,
.btn[aria-disabled='true']:active {
	transform: none;
	box-shadow: none;
}

/* ==================================================================
   2. Cards — touch-safe hover, media zoom
      ------------------------------------------------------------------
      The elevation, shadow and border-highlight transitions already
      exist (`components.css` + `sections-premium.css`) and are left
      exactly as they are. Two things were missing: on a touch device
      `:hover` latches after a tap and the card stayed lifted until the
      next tap elsewhere, and card thumbnails had no motion of their own.
   ================================================================== */

@media (hover: none) {
	/* Release the lift only — the shadow and border highlight are subtle
	   enough to read as "last tapped" rather than as a stuck state, and
	   `:focus-within` elevation is untouched so keyboard and screen-reader
	   users keep their affordance. */
	.card--interactive:hover,
	.process-steps .process-step:hover,
	.home-cities__item:hover,
	.blog-featured:hover,
	.contact-list__item:hover,
	.post-share__list a:hover {
		transform: none;
	}
}

/* Press feedback for touch, where there is no hover to preview with. */
@media (hover: none) {
	.card--interactive:active,
	.home-cities__item:active {
		transform: var(--motion-press);
		transition-duration: var(--motion-duration-instant);
	}
}

/* Card media: a slow, small zoom inside the card's existing
   `overflow: hidden`. Transform on the image only — the card box never
   changes size, so nothing around it moves. */
.blog-preview-card__thumbnail img,
.blog-featured__media img,
.search-card__media img {
	transition: transform var(--motion-t-enter);
}

@media (hover: hover) and (pointer: fine) {
	.blog-preview-card:hover .blog-preview-card__thumbnail img,
	.blog-featured:hover .blog-featured__media img,
	.search-card:hover .search-card__media img {
		transform: scale(var(--motion-scale-zoom));
	}
}

/* ==================================================================
   3. Navigation — dropdown animation, active indicator, chevron
      ------------------------------------------------------------------
      `navigation.css` toggles the desktop submenu with
      `display: none` → `display: flex`, which cannot be animated at all.
      Swapping to a visibility model keeps the exact same open/close
      trigger (`:hover` / `:focus-within`, no JS) and the exact same
      keyboard behaviour, but makes the transition interpolable.

      The submenu is `visibility: hidden` when closed, not merely
      transparent, so its links stay out of the tab order — an opacity-0
      menu would be a focus trap for keyboard users.

      Scoped above the drawer breakpoint. Below it the submenu is a
      static, always-open nested list (`navigation.css` `max-width: 48rem`)
      and must not be hidden.
   ================================================================== */

@media (min-width: 48.0625rem) {
	/* The closing delays are hover intent: a pointer travelling diagonally
	   from a parent item toward a child link briefly leaves the parent's
	   box, and without the grace period the menu would snap shut under the
	   cursor. Opening has no delay — `transition-delay` is reset to zero on
	   the open state below, so the menu appears the instant it is asked
	   for and only lingers on the way out. */
	.main-navigation .sub-menu {
		display: flex;
		visibility: hidden;
		opacity: 0;
		transform: translateY(calc(var(--motion-distance-sm) * -1));
		pointer-events: none;
		transition:
			opacity var(--motion-duration-quick) var(--motion-ease-standard) var(--motion-delay-settle),
			transform var(--motion-duration-quick) var(--motion-ease-standard) var(--motion-delay-settle),
			/* Visibility flips instantly, but only after the fade-out has
			   finished, so the menu never vanishes mid-transition — and it
			   stays out of the tab order the whole time it is closed. */
			visibility var(--motion-delay-none) linear
				calc(var(--motion-delay-settle) + var(--motion-duration-quick));
	}

	.main-navigation li.menu-item-has-children:hover > .sub-menu,
	.main-navigation li.menu-item-has-children:focus-within > .sub-menu {
		visibility: visible;
		opacity: 1;
		transform: none;
		pointer-events: auto;
		transition-delay: var(--motion-delay-none);
	}

	/* Items settle in one stagger step behind each other, capped at the
	   fourth so a long submenu never feels slow. Opacity only, and the
	   delay is declared on the open state alone, so closing is a single
	   clean fade rather than a reverse cascade.

	   The children can never desync from the panel: both are driven by the
	   same `:hover` / `:focus-within` selector on the same parent `<li>`,
	   so there is no state in which a link is transparent while the menu
	   it lives in is open and focusable. */
	.main-navigation .sub-menu > li {
		opacity: 0;
		transition: opacity var(--motion-t-quick);
	}

	.main-navigation li.menu-item-has-children:is(:hover, :focus-within) > .sub-menu > li {
		opacity: 1;
	}

	.main-navigation li.menu-item-has-children:is(:hover, :focus-within) > .sub-menu > li:nth-child(2) {
		transition-delay: var(--motion-delay-step);
	}

	.main-navigation li.menu-item-has-children:is(:hover, :focus-within) > .sub-menu > li:nth-child(3) {
		transition-delay: calc(var(--motion-delay-step) * 2);
	}

	.main-navigation li.menu-item-has-children:is(:hover, :focus-within) > .sub-menu > li:nth-child(n + 4) {
		transition-delay: calc(var(--motion-delay-step) * 3);
	}

	/* Chevron follows the menu state. */
	.main-navigation li.menu-item-has-children > a::after {
		transition: transform var(--motion-t-quick);
	}

	.main-navigation li.menu-item-has-children:hover > a::after,
	.main-navigation li.menu-item-has-children:focus-within > a::after {
		transform: rotate(225deg);
	}
}

/* Active indicator: a hairline that scales out from the centre under the
   current item, and previews on hover for the others. `scaleX` on a
   pseudo-element — no layout property, and it needs no extra markup. */
.main-navigation ul#primary-menu > li > a {
	position: relative;
}

.main-navigation ul#primary-menu > li > a::before {
	content: '';
	position: absolute;
	inset-inline: var(--space-3);
	inset-block-end: var(--motion-distance-sm);
	block-size: var(--motion-distance-xs);
	border-radius: var(--radius-full);
	background-color: currentColor;
	transform: scaleX(0);
	transition: transform var(--motion-t-spring);
	pointer-events: none;
}

.main-navigation ul#primary-menu > li.current-menu-item > a::before,
.main-navigation ul#primary-menu > li.current-menu-ancestor > a::before,
.main-navigation ul#primary-menu > li.current_page_item > a::before {
	transform: scaleX(1);
}

@media (hover: hover) and (pointer: fine) {
	.main-navigation ul#primary-menu > li > a:hover::before {
		transform: scaleX(1);
	}
}

/* The mobile drawer is deliberately absent from this module. Its slide,
   its backdrop fade and its staggered items are Sprint-era behaviour in
   `header.css`, already built on `--transition-base` / `--ease-standard`
   — which is exactly what `--motion-t-quick` aliases. Restating it here
   would add a second declaration of a value that is already correct. */

/* ==================================================================
   4. Forms — focus lift, validation, success, error, loading
      ------------------------------------------------------------------
      Validation state is CSS-only. `:user-invalid` / `:user-valid` fire
      after the user has actually interacted with a field, which is the
      difference between "helpful" and ":invalid lighting up every empty
      required field on page load". Behind `@supports selector()`, so a
      browser without them simply shows the neutral state — and the
      `.form-field--error` class hook `components.css` already defines
      keeps working either way.
   ================================================================== */

.form-input,
.form-textarea,
.form-select,
input[type='text'],
input[type='email'],
input[type='url'],
input[type='tel'],
input[type='number'],
input[type='search'],
input[type='password'],
input[type='date'],
.search-field,
textarea,
select {
	transition:
		border-color var(--motion-t-instant),
		box-shadow var(--motion-t-instant),
		transform var(--motion-t-instant);
}

/* Floating focus: the field rises by a single pixel as its ring appears.
   A transform, so the field's box in the layout never moves and the
   labels and helper text beneath it stay exactly where they were. */
.form-input:focus,
.form-textarea:focus,
.form-select:focus,
input[type='text']:focus,
input[type='email']:focus,
input[type='url']:focus,
input[type='tel']:focus,
input[type='number']:focus,
input[type='search']:focus,
input[type='password']:focus,
input[type='date']:focus,
.search-field:focus,
textarea:focus,
select:focus {
	transform: translateY(calc(var(--motion-distance-hairline) * -1));
}

/* `[type="search"]` is excluded from both: a search box is never
   "invalid", and greening its border the moment someone types a query is
   feedback about nothing. */
@supports selector(:user-invalid) {
	input:not([type='search']):user-invalid,
	textarea:user-invalid,
	select:user-invalid {
		border-color: var(--color-danger);
		box-shadow: 0 0 0 3px var(--color-danger-100);
	}

	input:not([type='search']):user-valid,
	textarea:user-valid,
	select:user-valid {
		border-color: var(--color-success);
	}
}

/* The nudge is deliberately NOT attached to `:user-invalid`. Browsers
   re-evaluate that pseudo-class as the user types, so an email field
   would shake on every keystroke until the address was complete — the
   border and ring above are the right amount of feedback for live
   validation. The shake belongs to a state a handler sets once, which is
   what `components.css`'s `.form-field--error` hook already is. */
.form-field--error .form-input,
.form-field--error input,
.form-field--error textarea,
.form-field--error select {
	animation: agencycare-nudge var(--motion-duration-moderate) var(--motion-ease-out) 1;
}

@keyframes agencycare-nudge {
	0%,
	100% {
		transform: none;
	}

	25% {
		transform: translateX(calc(var(--motion-distance-sm) * -1));
	}

	75% {
		transform: translateX(var(--motion-distance-sm));
	}
}

/* Explicit state hooks, for a form handler that manages state itself
   rather than relying on native validity. */
.form-field--success .form-input,
.form-field--success input,
.form-field--success textarea,
.form-field--success select {
	border-color: var(--color-success);
}

.form-field--loading {
	position: relative;
}

.form-field--loading .form-input,
.form-field--loading input,
.form-field--loading textarea,
.form-field--loading select {
	opacity: 0.6;
	pointer-events: none;
}

/* A form mid-submit dims and stops accepting input, without collapsing —
   `opacity` only, so the page height is identical before and after. */
form.is-loading {
	opacity: 0.6;
	pointer-events: none;
	transition: opacity var(--motion-t-quick);
}

/* Alerts enter with the same curve as everything else. `both` fill keeps
   the end state after the animation, and because the element is
   `opacity: 0` only for the first frames — never `display: none` — a
   screen reader announces it immediately regardless of the motion. */
.alert {
	animation: agencycare-alert-in var(--motion-duration-moderate) var(--motion-ease-out) both;
}

@keyframes agencycare-alert-in {
	from {
		opacity: 0;
		transform: translateY(calc(var(--motion-distance-md) * -1));
	}

	to {
		opacity: 1;
		transform: none;
	}
}

/* ==================================================================
   5. Counters — reserve the final width, then count
      ------------------------------------------------------------------
      `animations.js` sets the element's text to "0" before counting up,
      so the number's box grew as digits were added and everything
      beside it shifted. `motion.js` reserves the final width in `ch`
      *before* the first frame; `min-inline-size` needs a
      non-inline box to apply, which is what the display change below
      is for. Tabular figures make every digit the same width, so the
      reserved box is exact rather than approximate.
   ================================================================== */

[data-count-to] {
	display: inline-block;
	font-variant-numeric: tabular-nums;
}

/* ==================================================================
   6. Images — soft reveal, lazy-loaded media only
      ------------------------------------------------------------------
      The default state is *fully visible*. The reveal is an animation
      that plays when `motion.js` confirms the image decoded, rather
      than an opacity-0 base state that a failed script would leave
      permanently blank.

      Deliberately scoped to `[loading="lazy"]`. The LCP candidates in
      this theme — the homepage hero visual and the blog index's lead
      article image — are `loading="eager" fetchpriority="high"` and are
      therefore never selected here. Fading in the LCP element would
      delay the metric by the length of the fade; that is exactly the
      mistake Sprint 1 had to undo on the hero.
   ================================================================== */

img[loading='lazy'].is-media-loaded {
	animation: agencycare-media-in var(--motion-duration-moderate) var(--motion-ease-out) both;
}

@keyframes agencycare-media-in {
	from {
		opacity: 0;
		transform: scale(var(--motion-scale-nudge));
	}

	to {
		opacity: 1;
		transform: none;
	}
}

/* ==================================================================
   7. CTA — a very subtle breathe, and the trust row
      ------------------------------------------------------------------
      `.btn` has `overflow: hidden` for the ripple, so a scaling halo
      pseudo-element would be clipped. The pulse is therefore a 1.5%
      scale of the button itself: one composited transform on one
      element per page, no paint, no layout. It pauses while the button
      is hovered or focused so it never fights the press or lift.
   ================================================================== */

.u-pulse-soft {
	animation: agencycare-breathe calc(var(--duration-float) * 0.55) var(--motion-ease-standard) infinite;
}

/* Dropped, not paused, on interaction: a paused animation keeps applying
   its current frame, which would out-rank the hover lift and the press
   scale and leave the button frozen mid-breath. Removing the animation
   hands `transform` back to the transition, so the two states hand over
   smoothly. */
.u-pulse-soft:hover,
.u-pulse-soft:focus-visible,
.u-pulse-soft:active {
	animation: none;
}

@keyframes agencycare-breathe {
	0%,
	100% {
		transform: none;
	}

	50% {
		transform: scale(var(--motion-scale-nudge));
	}
}

/* ==================================================================
   8. Skeletons — GPU sheen instead of an animated background-position
      ------------------------------------------------------------------
      `patterns.css` animates `background-position` on a 400%-wide
      gradient. That never touches layout, but it repaints the whole
      placeholder every frame — the wrong trade on a low-end Android
      device, and the cost multiplies with each skeleton on screen.
      Same look, one composited pseudo-element, `transform` only.
   ================================================================== */

.loading-placeholder {
	position: relative;
	overflow: hidden;
	background-image: none;
	background-color: var(--color-gray-100);
	animation: none;
}

.loading-placeholder::after {
	content: '';
	position: absolute;
	inset-block: 0;
	inset-inline-start: 0;
	inline-size: 60%;
	background-image: linear-gradient(
		90deg,
		transparent,
		var(--color-gray-200),
		transparent
	);
	transform: translateX(-100%);
	animation: agencycare-sheen var(--duration-shimmer) var(--motion-ease-standard) infinite;
}

@keyframes agencycare-sheen {
	from {
		transform: translateX(-100%);
	}

	to {
		transform: translateX(250%);
	}
}

/* `translate` has no logical equivalent, so the sweep is mirrored
   explicitly. Without this the RTL sheen would start past the trailing
   edge and travel further away, and the skeleton would look inert. */
[dir='rtl'] .loading-placeholder::after {
	transform: translateX(100%);
	animation-name: agencycare-sheen-rtl;
}

@keyframes agencycare-sheen-rtl {
	from {
		transform: translateX(100%);
	}

	to {
		transform: translateX(-250%);
	}
}

/* ==================================================================
   9. Reveal hygiene — release the compositor layers
      ------------------------------------------------------------------
      `animations.css` sets `will-change: opacity, transform` on every
      reveal target. That is correct *before* the transition and wasteful
      after: a page with thirty cards holds thirty composited layers for
      its whole lifetime. `motion.js` adds `.is-motion-settled` once a
      target's reveal transition has ended — and immediately, without
      waiting, for the targets that were already on screen at load, where
      no `transitionend` ever fires. This rule is what that class does.
   ================================================================== */

.js .u-reveal.is-motion-settled,
.js .u-fade-up.is-motion-settled,
.js .u-fade-in.is-motion-settled,
.js .u-scale-in.is-motion-settled,
.js .u-slide-left.is-motion-settled,
.js .u-slide-right.is-motion-settled {
	will-change: auto;
}

/* ==================================================================
   10. Reduced motion
      ------------------------------------------------------------------
      `accessibility.css` already collapses every duration to 0.01ms
      under `prefers-reduced-motion: reduce`, which handles transitions.
      Looping and attention-seeking animations need more than a short
      duration — they need to not run at all. Everything below is
      switched off rather than sped up.
   ================================================================== */

@media (prefers-reduced-motion: reduce) {
	/* Zero the delay scale at the source. `accessibility.css` collapses
	   every transition *duration* but not its delay, so without this the
	   dropdown's hover-intent grace and the submenu stagger would still be
	   waited out — an instant transition that starts 400ms late is not
	   reduced motion, it is a slow menu. One override, and every rule
	   derived from these tokens follows. */
	:root {
		--motion-delay-step: 0ms;
		--motion-delay-settle: 0ms;
	}

	.u-pulse-soft,
	.loading-placeholder::after,
	.btn.is-loading::after,
	.btn[aria-busy='true']::after,
	img[loading='lazy'].is-media-loaded,
	.alert {
		animation: none;
	}

	/* The spinner is the one place where "no animation" would remove the
	   only signal that something is happening, so it keeps a slow,
	   non-spinning presence instead: a static ring. */
	.btn.is-loading::after,
	.btn[aria-busy='true']::after {
		border-block-start-color: currentColor;
		opacity: 0.5;
	}

	@supports selector(:user-invalid) {
		input:user-invalid,
		textarea:user-invalid {
			animation: none;
		}
	}

	.btn:hover,
	.btn:active,
	.btn:focus-visible,
	.card--interactive:active,
	.home-cities__item:active,
	.blog-preview-card:hover .blog-preview-card__thumbnail img,
	.blog-featured:hover .blog-featured__media img,
	.search-card:hover .search-card__media img,
	.form-input:focus,
	input:focus,
	textarea:focus,
	select:focus,
	.search-field:focus {
		transform: none;
	}

	/* The dropdown still opens and closes — instantly, which is the
	   correct reduced-motion behaviour for a menu — and stays out of the
	   tab order while closed. */
	.main-navigation ul#primary-menu > li.current-menu-item > a::before,
	.main-navigation ul#primary-menu > li.current-menu-ancestor > a::before,
	.main-navigation ul#primary-menu > li.current_page_item > a::before {
		transform: scaleX(1);
	}
}

/* ==================================================================
   11. Forced colors
      ------------------------------------------------------------------
      In a forced palette, decorative motion surfaces read as noise or
      as invisible blocks. Structure and state stay; ornament goes.
   ================================================================== */

@media (forced-colors: active) {
	.loading-placeholder::after,
	.u-pulse-soft {
		animation: none;
	}

	.loading-placeholder {
		border: 1px solid CanvasText;
	}

	.main-navigation ul#primary-menu > li > a::before {
		background-color: CanvasText;
		forced-color-adjust: none;
	}

	.btn.is-loading::after,
	.btn[aria-busy='true']::after {
		border-color: CanvasText;
		border-block-start-color: Canvas;
		color: CanvasText;
	}

	@media (min-width: 48.0625rem) {
		.main-navigation .sub-menu {
			border: 1px solid CanvasText;
		}
	}
}

/* ==================================================================
   12. Print — no motion, no transient state
      ================================================================== */

@media print {
	.u-pulse-soft,
	.loading-placeholder::after {
		animation: none;
	}

	.loading-placeholder {
		display: none;
	}

	@media (min-width: 48.0625rem) {
		.main-navigation .sub-menu {
			display: none;
		}
	}
}
