/**
 * Block Motion — CSS Animations
 * https://blockagency.co
 *
 * 100% CSS. Zero JavaScript for animations.
 * JS is only for IntersectionObserver triggers and text splitting.
 *
 * Scroll-driven: uses animation-timeline: view().
 * Dynamic @keyframes are generated per-block by PHP based on
 * user configuration (scale + opacity + blur combinations).
 * Unsupported browsers (~22%) see the element without animation.
 */

/* ── Spring easing presets ──
   ========================================================================== */

:root {
	--motion-easing: cubic-bezier(0.16, 1, 0.3, 1);

	--motion-spring-gentle: linear(
		0, 0.006, 0.025 2.8%, 0.101 6.1%, 0.539 18.9%, 0.721 25.3%,
		0.849 31.5%, 0.937 38.1%, 0.968 41.8%, 0.991 45.7%,
		1.006 50.1%, 1.015 55%, 1.017 63.9%, 1.001
	);
	--motion-spring-moderate: linear(
		0, 0.007, 0.029 2.2%, 0.118 4.7%, 0.625 14.4%, 0.826 19%,
		0.902, 0.962, 1.008 26.1%, 1.041 28.7%, 1.064 32.1%,
		1.07 36%, 1.061 40.5%, 1.015 53.4%, 0.999 61.6%,
		0.995 71.2%, 1
	);
	--motion-spring-bouncy: linear(
		0, 0.009, 0.035 2.1%, 0.141 4.4%, 0.723 12.9%, 0.938 16.7%,
		1.017, 1.077, 1.121, 1.149 24.3%, 1.159, 1.163, 1.161,
		1.154 29.9%, 1.129 32.8%, 1.051 39.6%, 1.017 43.1%,
		0.991, 0.977 51%, 0.974 53.8%, 0.975 57.1%,
		0.997 69.8%, 1.003 76.9%, 1
	);
	--motion-spring-snappy: linear(
		0, 0.009, 0.037 1.7%, 0.153 3.6%, 0.776 10.3%, 1.001,
		1.142 16%, 1.185, 1.209 19.5%, 1.215 21.4%, 1.209 23.5%,
		1.162 28.2%, 1.039 36.8%, 1.007 40.2%, 0.984, 0.968 47.1%,
		0.965 51.2%, 0.969 55.9%, 1.001 68.2%, 1.006 76.1%, 1
	);
}

/* ── Registered custom properties ──
   ========================================================================== */

@property --motion-duration {
	syntax: "<time>";
	inherits: false;
	initial-value: 0.6s; /* 5.7.0 — was 0.5s */
}

@property --motion-distance {
	syntax: "<length>";
	inherits: false;
	initial-value: 48px; /* 5.7.0 — was 40px */
}

@property --motion-delay {
	syntax: "<time>";
	inherits: false;
	initial-value: 0s;
}

@property --motion-stagger {
	syntax: "<time>";
	inherits: false;
	initial-value: 0.1s; /* 5.7.0 — was 0.08s */
}

/* 5.9.0 — `@property --bm-scroll-text-pos` removed alongside the mask-
   gradient refactor of bm-scroll-text. The variable was used to drive
   a single linear-gradient stop animation; the new per-span technique
   (see below) doesn't need it.

   5.9.3 — `--bm-st-i` and `--bm-st-n` declared as typed integers via
   `@property` so the per-span `animation-range` calc() expressions
   resolve cleanly. Without typing, custom properties default to a
   token-list type which some browsers don't always accept inside
   calc() arithmetic for `animation-range` — declaring them as
   `<integer>` removes any ambiguity. */
@property --bm-st-i {
	syntax: '<integer>';
	inherits: false;
	initial-value: 0;
}
@property --bm-st-n {
	syntax: '<integer>';
	inherits: true;
	initial-value: 1;
}

/* 5.9.17 — typed declarations for the per-block scroll-range
   percentages. Pre-5.9.17 the bm-scroll-text reveal used a hardcoded
   `* 50%` inside the per-span animation-range calc, locking every
   marquee-style heading to the same "all words revealed by the time
   the section is half-scrolled-through" pacing. Exposing rangeStart /
   rangeEnd via the inspector lets users tune the cascade window —
   slow + extended (start 0%, end 100%), tight burst (start 30%, end
   60%), or anything in between. The vars are typed `<percentage>`
   with `inherits: true` so the parent's inline-style emission cascades
   to every per-span box; the runtime ALSO writes them to each span
   directly as a belt-and-suspenders against the (rare) typed-property
   inheritance edge cases hit during 5.9.x development. Default values
   match the pre-5.9.17 hardcoded behavior — 0% start, 50% end — so
   pages render identically until the user opts to tune them. */
@property --bm-st-rstart {
	syntax: '<percentage>';
	inherits: true;
	initial-value: 0%;
}
@property --bm-st-rend {
	syntax: '<percentage>';
	inherits: true;
	initial-value: 50%;
}

/* ── Reveal animations ──
   ==========================================================================
   Two-tier specificity model:

   1. CSS variable defaults live in :where() rules (specificity 0,0,0). Any
      higher-specificity rule wins — including the inline <style> emitted by
      `block_motion_enqueue_assets()` from the user's Settings page. This is
      what makes site-wide custom duration/distance actually take effect.

   2. Concrete style rules (opacity, transform, transition, filter, clip-path)
      live in .X.X doubled selectors (specificity 0,2,0) so Block Motion's
      initial state wins ties against GenerateBlocks compiled CSS
      (.gb-element-{uniqueId}) in normal state.

   Don't merge tier 1 into tier 2 — that re-introduces the 3.4.1–3.4.3 bug
   where user customizations in Settings silently fell back to defaults.
   ========================================================================== */

/* Tier 1 — variable defaults at :where() so PHP inline overrides win */

:where(.bm-fade-in, .bm-slide-up, .bm-slide-down, .bm-slide-left, .bm-slide-right,
       .bm-scale-in, .bm-scale-out, .bm-blur-in, .bm-tilt-up, .bm-clip-reveal) {
	--motion-duration: 0.6s; /* 5.7.0 — was 0.5s */
	--motion-distance: 48px; /* 5.7.0 — was 40px */
	--motion-delay: 0s;
}

:where(.bm-scale-in)     { --motion-scale: 0.90; } /* 5.8.0 — was 0.96 */
:where(.bm-scale-out)    { --motion-scale: 1.1; }  /* 5.8.0 — was 1.04 */
:where(.bm-tilt-up)      { --motion-rotate: 4deg; }
:where(.bm-blur-in)      { --motion-blur: 8px; }
:where(.bm-clip-reveal)  { --motion-clip-from: inset(0 0 100% 0); }

/* Tier 2 — initial state at .X.X (0,2,0) to beat GB normal-state CSS */

.bm-fade-in.bm-fade-in,
.bm-slide-up.bm-slide-up,
.bm-slide-down.bm-slide-down,
.bm-slide-left.bm-slide-left,
.bm-slide-right.bm-slide-right,
.bm-scale-in.bm-scale-in,
.bm-scale-out.bm-scale-out,
.bm-blur-in.bm-blur-in,
.bm-tilt-up.bm-tilt-up,
.bm-clip-reveal.bm-clip-reveal {
	opacity: 0;
	transition:
		opacity var(--motion-duration) var(--motion-easing),
		transform var(--motion-duration) var(--motion-easing);
	transition-delay: var(--motion-delay);
}

.bm-slide-up.bm-slide-up       { transform: translateY(var(--motion-distance)); }
.bm-slide-down.bm-slide-down   { transform: translateY(calc(var(--motion-distance) * -1)); }
.bm-slide-left.bm-slide-left   { transform: translateX(calc(var(--motion-distance) * -1)); }
.bm-slide-right.bm-slide-right { transform: translateX(var(--motion-distance)); }
.bm-scale-in.bm-scale-in       { transform: scale(var(--motion-scale)); }
.bm-scale-out.bm-scale-out     { transform: scale(var(--motion-scale)); }

.bm-tilt-up.bm-tilt-up {
	transform: translateY(var(--motion-distance)) perspective(800px) rotateX(var(--motion-rotate));
	transform-origin: bottom center;
}

.bm-blur-in.bm-blur-in {
	filter: blur(var(--motion-blur));
	transition:
		opacity var(--motion-duration) var(--motion-easing),
		transform var(--motion-duration) var(--motion-easing),
		filter var(--motion-duration) var(--motion-easing);
	transition-delay: var(--motion-delay);
}

/*
 * --motion-clip-from holds the initial clip rectangle. JS sets this CSS
 * variable (not the clip-path property directly) when the user picks a
 * `data-motion-origin`, so the .is-visible rule's `clip-path: inset(0)`
 * wins on reveal without needing !important.
 */
.bm-clip-reveal.bm-clip-reveal {
	clip-path: var(--motion-clip-from);
	transition:
		opacity var(--motion-duration) var(--motion-easing),
		transform var(--motion-duration) var(--motion-easing),
		clip-path var(--motion-duration) var(--motion-easing);
	transition-delay: var(--motion-delay);
}

/* Visible states */

.bm-fade-in.is-visible,
.bm-slide-up.is-visible,
.bm-slide-down.is-visible,
.bm-slide-left.is-visible,
.bm-slide-right.is-visible,
.bm-scale-in.is-visible,
.bm-scale-out.is-visible,
.bm-tilt-up.is-visible {
	opacity: 1;
	transform: none;
}

.bm-blur-in.is-visible {
	opacity: 1;
	transform: none;
	filter: blur(0);
}

.bm-clip-reveal.is-visible {
	opacity: 1;
	transform: none;
	clip-path: inset(0 0 0 0);
}

/* ── Stagger inheritance FOUC mask ──
   ==========================================================================
   5.8.3 — Pre-runtime FOUC guard for the 5.3.0 stagger-inheritance feature.
   When the user activates stagger + an animation slug on a parent block,
   the render filter emits `data-motion-stagger-source="{slug}"` on the
   parent (without the slug class) and only `bm-stagger` as a class. The
   runtime's `expandStaggerSources()` then walks each parent and applies
   `bm-{slug}` to every direct child that doesn't already have its own
   reveal class. That step runs after DOMContentLoaded — there's a brief
   window between DOM parse and runtime init where children render in
   their natural state (visible, not yet animated), then jump to opacity:0
   + transform when the slug class lands, then transition to is-visible.
   The "jump" reads as a stutter / "lag", especially on above-the-fold
   stagger lists where many children flash at once (5.8.1 made this more
   visible by extending cascade to above-the-fold single-column lists).

   The fix: pre-apply opacity:0 to direct children of any stagger-source
   parent. The transform jump still happens when the runtime adds the
   slug class, but it happens while opacity:0 — invisible, no visible
   artifact. The slug's own initial-state rule (.bm-slide-up.bm-slide-up
   { opacity: 0; transform: ... }) has higher specificity (0,2,0 vs the
   mask's 0,1,1) so once the runtime applies the slug, the slug rule
   takes over with the correct transform. The is-visible rule
   (.bm-slide-up.bm-slide-up.is-visible) at 0,3,0 then beats both, with
   the cascade firing the transition to the final state.

   Children that already have their own reveal class (the pre-5.3 path,
   per-child config) are unaffected — their slug class is server-side
   from page parse, opacity:0 already applies, no FOUC window exists.
   The mask is redundant for them but harmless (same opacity:0).
   ========================================================================== */

[data-motion-stagger-source] > * {
	opacity: 0;
}

/* 5.8.4 — pass-through wrapper exception. When a stagger-source has a
   single child that's a known structural wrapper (GB Looper, etc.),
   the runtime descends through it and applies `bm-{slug}` to the
   wrapper's children (the actual loop iterations), and tags the
   wrapper itself with `bm-stagger-passthrough`. The wrapper does NOT
   get an animation slug, so we need to override the FOUC mask above
   to keep it visible. The wrapper's children get their own opacity:0
   via the rule below — the slug rules then take over once the runtime
   finishes adding classes. */
[data-motion-stagger-source] > .bm-stagger-passthrough {
	opacity: 1;
}
[data-motion-stagger-source] > .bm-stagger-passthrough > * {
	opacity: 0;
}

/* ── On-load ──
   ========================================================================== */

:where(.bm-animate-load) {
	--motion-duration: 0.6s; /* 5.7.0 — was 0.5s */
	--motion-delay: 0s;
}

.bm-animate-load.bm-animate-load {
	opacity: 0;
	transform: translateY(48px); /* 5.7.0 — matches site distance default; was 40px */
	transition:
		opacity var(--motion-duration) var(--motion-easing),
		transform var(--motion-duration) var(--motion-easing);
	transition-delay: var(--motion-delay);
}

.bm-animate-load.is-ready {
	opacity: 1;
	transform: none;
}

/* ── Delay utilities ──
   ========================================================================== */

.bm-delay-1 { --motion-delay: 0.1s; }
.bm-delay-2 { --motion-delay: 0.2s; }
.bm-delay-3 { --motion-delay: 0.3s; }
.bm-delay-4 { --motion-delay: 0.4s; }
.bm-delay-5 { --motion-delay: 0.5s; }

/* ── Counters ──
   ========================================================================== */

:where(.bm-counter) {
	--motion-duration: 0.6s; /* 5.7.0 — was 0.5s */
	--motion-delay: 0s;
}

.bm-counter.bm-counter {
	opacity: 0;
	transition: opacity var(--motion-duration) var(--motion-easing);
	transition-delay: var(--motion-delay);
}

.bm-counter.is-visible { opacity: 1; }

/* ── Text reveals ──
   ========================================================================== */

.bm-text-reveal-lines .bm-line {
	--motion-duration: 0.6s; /* 5.7.0 — was 0.5s */
	display: block;
	opacity: 0;
	transform: translateY(16px);
	transition:
		opacity var(--motion-duration) var(--motion-easing),
		transform var(--motion-duration) var(--motion-easing);
}

.bm-text-reveal-lines.is-visible .bm-line { opacity: 1; transform: none; }

.bm-text-reveal-words .bm-word {
	--motion-duration: 0.6s; /* 5.7.0 — was 0.5s */
	display: inline-block;
	opacity: 0;
	transform: translateY(16px);
	transition:
		opacity var(--motion-duration) var(--motion-easing),
		transform var(--motion-duration) var(--motion-easing);
}

.bm-text-reveal-words.is-visible .bm-word { opacity: 1; transform: none; }
.bm-text-reveal-words .bm-word + .bm-word { margin-left: 0.25em; }

.bm-text-reveal-chars .bm-char {
	--motion-duration: 0.6s; /* 5.7.0 — was 0.5s */
	display: inline-block;
	opacity: 0;
	transform: translateY(8px);
	transition:
		opacity var(--motion-duration) var(--motion-easing),
		transform var(--motion-duration) var(--motion-easing);
}

.bm-text-reveal-chars.is-visible .bm-char { opacity: 1; transform: none; }

/* ── Idle / loop animations ──
   ==========================================================================
   Continuous, infinite loops. Designed to be calm and ambient — never
   attention-grabbing. Override durations via CSS variables when needed:
     .my-cta { --motion-pulse-duration: 3s; }
   ========================================================================== */

@keyframes bm-float {
	0%, 100% { transform: translateY(0); }
	50%      { transform: translateY(calc(var(--motion-float-distance, 8px) * -1)); }
}

.bm-float {
	--motion-float-duration: 4s;
	--motion-float-distance: 8px;
	animation: bm-float var(--motion-float-duration) ease-in-out infinite;
}

@keyframes bm-pulse {
	0%, 100% { transform: scale(1); }
	50%      { transform: scale(1.04); }
}

.bm-pulse {
	--motion-pulse-duration: 3s;
	animation: bm-pulse var(--motion-pulse-duration) ease-in-out infinite;
}

@keyframes bm-glow {
	0%, 100% { box-shadow: 0 0 0 0 var(--motion-glow-color, rgba(0, 0, 0, 0.18)); }
	50%      { box-shadow: 0 0 24px 0 var(--motion-glow-color, rgba(0, 0, 0, 0.18)); }
}

.bm-glow {
	--motion-glow-duration: 3s;
	animation: bm-glow var(--motion-glow-duration) ease-in-out infinite;
}

/* ══════════════════════════════════════════════════════════════════════
   SCROLL-DRIVEN ANIMATIONS
   Base classes only. Dynamic @keyframes are generated per-block
   by PHP (block_motion_render_block_filter) based on user config.
   ══════════════════════════════════════════════════════════════════════ */

.bm-scroll-scale { overflow: hidden; }

/* Fallback: elements visible by default for unsupported browsers */
.bm-scroll-scale,
.bm-scroll-parallax,
.bm-scroll-opacity,
.bm-scroll-blur {
	/* Dynamic animation applied via inline <style> from PHP */
}

/* ══════════════════════════════════════════════════════════════════════
   SCROLL-TEXT — text reveals progressively as the element scrolls through
   the viewport. Apple/Stripe/Vercel/Salient-style read-along effect.

   Implementation (5.9.0+): per-span scroll-driven opacity reveal. The
   runtime's `prepareScrollText()` splits the element into
   <span class="bm-word"> (default) or <span class="bm-char"> based on
   the `data-motion-scroll-text-split` attribute and tags each span
   with --bm-st-i (index) and --bm-st-n (total). The container declares
   a named view-timeline (`view-timeline-name: --bm-scroll-text-tl`)
   and each span subscribes to it (`animation-timeline: --bm-scroll-
   text-tl`) with its own per-span animation-range slice computed via
   calc() — this gives a left-to-right reveal where each word/char
   becomes opaque as the user scrolls past it.

   Customizable via the only remaining CSS variable:
     --bm-scroll-text-dim-alpha   unrevealed-text opacity baseline (0–1)

   Pre-5.9.0 used a CSS mask-image gradient driven by a single timeline
   on the parent and exposed `--bm-scroll-text-soft` /
   `--bm-scroll-text-range`. Both were retired in the 5.9.0 refactor —
   the new technique has crisp per-word boundaries (no gradient zone
   to soften) and computes ranges internally from --bm-st-i/n. Saved
   `motion.scrollTextSoft` values on legacy posts persist as inert
   data; nothing reads them.

   Browser support: Chrome 115+, Edge 115+. Safari/Firefox without
   `animation-timeline: view()` fall through to the
   `@supports not (animation-timeline: view())` block below and see
   the text fully opaque (graceful no-op fallback).
   ══════════════════════════════════════════════════════════════════════ */

.bm-scroll-text {
	--bm-scroll-text-dim-alpha: 0.18;
	/* 5.9.2 — declare a named view-timeline on the container so all
	   per-span animations can subscribe to ONE shared scroll progress
	   (the section's, not their own). Pre-5.9.2 each span used
	   `animation-timeline: view()` which defaults to the subject
	   element's OWN view-timeline. For tiny inline-block spans, that
	   meant each word/char animated based on when IT individually
	   crossed the viewport — uncoordinated, no proper left-to-right
	   reveal. The named-timeline pattern syncs every span to the
	   section's progress. */
	view-timeline-name: --bm-scroll-text-tl;
	view-timeline-axis: block;
}

.bm-scroll-text .bm-word {
	display: inline-block;
	opacity: var(--bm-scroll-text-dim-alpha);
}

/* 5.9.6 — chars stay `display: inline` so adjacent glyphs share one
   text run and the browser keeps applying kerning + ligatures across
   the per-char span boundary. Pre-5.9.6 chars were `display: inline-
   block` (mirroring the .bm-word rule). User feedback after 5.9.5:
   *"En reveal by characters, parece que se rompe el formato del
   texto, en el sentido que el letter spacing parece aumentar."* —
   exactly the expected symptom of inline-block boxes severing kern
   pairs (Te / Wa / AV / fi-ligatures, etc.) and falling back to the
   font's default advance widths between every character. The per-
   span scroll-driven reveal only animates `opacity`, which works
   identically on `display: inline`, so dropping `inline-block` here
   has no functional cost and restores the original typography.
   (`.bm-word` keeps `inline-block` — there's no kerning to preserve
   between separate word boxes, and the inline-block here doesn't
   cost anything visually.) */
.bm-scroll-text .bm-char {
	display: inline;
	opacity: var(--bm-scroll-text-dim-alpha);
}

/* 5.9.1 — non-breaking word wrapper for chars mode. Each word's chars
   live inside this span so the browser doesn't wrap mid-word. The
   wrapper itself is inline-block to act as one indivisible line-
   breaking unit; the chars INSIDE it are now `display: inline`
   (5.9.6), so the browser still treats the wrapper's contents as a
   single text run for kerning purposes. */
.bm-scroll-text .bm-scroll-word-wrap {
	display: inline-block;
	white-space: nowrap;
}

@supports (animation-timeline: view()) {
	.bm-scroll-text .bm-word,
	.bm-scroll-text .bm-char {
		animation: bm-scroll-text-reveal linear both;
		/* 5.9.2 — subscribe to the container's named timeline so all
		   spans share the same scroll-progress source. Each span's
		   `animation-range` is its slice within the section's view. */
		animation-timeline: --bm-scroll-text-tl;
		/* 5.9.17 — slice expression uses the typed `--bm-st-rstart`
		   (default 0%) and `--bm-st-rend` (default 50%) custom
		   properties so the user can tune the reveal window via the
		   inspector. Each span's slice = rstart + (i/N)*(rend-rstart)
		   to rstart + ((i+1)/N)*(rend-rstart). The N=22 example
		   (heading "Plain Language is..." with 22 words) renders
		   span 0 at cover 0%–~2.27% and span 21 at cover ~47.73%–50%
		   under the default range; with rstart=0, rend=100 the
		   spans spread across the full cover progress instead. */
		animation-range:
			cover calc( var( --bm-st-rstart, 0% ) + var( --bm-st-i, 0 ) / max( var( --bm-st-n, 1 ), 1 ) * ( var( --bm-st-rend, 50% ) - var( --bm-st-rstart, 0% ) ) )
			cover calc( var( --bm-st-rstart, 0% ) + ( var( --bm-st-i, 0 ) + 1 ) / max( var( --bm-st-n, 1 ), 1 ) * ( var( --bm-st-rend, 50% ) - var( --bm-st-rstart, 0% ) ) );
	}
}

@keyframes bm-scroll-text-reveal {
	from { opacity: var(--bm-scroll-text-dim-alpha); }
	to   { opacity: 1; }
}

/* Browsers without scroll-driven animations: text stays at the dim
   opacity. Override that fallback so users on older browsers see
   readable text (no animation, full opacity — graceful degradation). */
@supports not (animation-timeline: view()) {
	.bm-scroll-text .bm-word,
	.bm-scroll-text .bm-char {
		opacity: 1;
	}
}

/* ══════════════════════════════════════════════════════════════════════
   MARQUEE
   Custom block (block-motion/marquee) registered in PHP. Renders as:

     .bm-marquee[ --modifiers]
       .bm-marquee__track
         .bm-marquee__group
           .bm-marquee__item × N

   Responsive strategy: render.php emits desktop + tablet + mobile values
   for every device-aware variable. The breakpoint media-queries below
   promote the matching tablet/mobile variant into the active variable
   used by the keyframe + gap rules. getComputedStyle() in the runtime JS
   reads the effective value, so the draggable controller adapts at any
   viewport without extra logic.
   ══════════════════════════════════════════════════════════════════════ */

.bm-marquee {
	/* 5.7.3 — base rule reads the desktop value via `var()` chain
	   from the per-breakpoint `--bm-marquee-{prop}-desktop` variant
	   that render.php emits inline. This is the desktop tier of the
	   responsive cascade; the @media rules below re-bind the same
	   `--bm-marquee-{prop}` to the *-tablet / *-mobile variants at
	   smaller viewports.
	   Pre-5.7.3 render.php emitted `--bm-marquee-{prop}` inline
	   directly — that won over any media query rule (inline-style
	   specificity beats selector specificity even inside a media
	   query), so responsive overrides silently fell back to desktop.
	   See render.php's own header comment for full diagnosis. */
	--bm-marquee-bg: var(--base-2, #fff);
	--bm-marquee-speed: var(--bm-marquee-speed-desktop, 40s);
	--bm-marquee-gap:   var(--bm-marquee-gap-desktop, 96px);
	--bm-marquee-fade:  var(--bm-marquee-fade-desktop, 0px);
	/* 5.9.18 — edge inset for the iOS/App-Store-style carousel
	   affordance. Default 0 keeps pre-5.9.18 flush-edge layouts
	   unchanged; values > 0 indent items from the wrapper edges,
	   which combined with the natural right-edge peek of an
	   overflow-clipped row signals "scrollable carousel" to the
	   user without text/icons. Applied as `padding-inline` on the
	   wrapper (not the track) so auto-scrolling marquees keep
	   their loop seamless — the track translates inside the
	   padded content area but the loop itself isn't broken. */
	--bm-marquee-edge-inset: var(--bm-marquee-edge-inset-desktop, 0px);
	--bm-marquee-item-opacity: 1;
	--bm-marquee-item-opacity-hover: 1;
	--bm-marquee-loop-width: 0px;
	--bm-marquee-loop-height: 0px;
	--bm-marquee-height: 400px;
	--bm-marquee-delay: 0s;

	position: relative;
	width: 100%;
	overflow: hidden;
	padding-inline: var(--bm-marquee-edge-inset);
	box-sizing: border-box;
}

.bm-marquee::before,
.bm-marquee::after {
	content: "";
	position: absolute;
	inset-block: 0;
	width: var(--bm-marquee-fade);
	z-index: 2;
	pointer-events: none;
}

.bm-marquee::before {
	left: 0;
	background: linear-gradient(to right, var(--bm-marquee-bg), transparent);
}

.bm-marquee::after {
	right: 0;
	background: linear-gradient(to left, var(--bm-marquee-bg), transparent);
}

/* Fade strength variants. Default (linear) uses the 2-stop gradient
   above. Soft extends the solid section so the bg dominates further into
   the marquee before fading; visually denser edges. Hard pulls the
   transition earlier so the bulk of the gradient is transparent; more
   abrupt edge. Same hex stops for both edges via inheritance — only the
   stop *position* changes. */
.bm-marquee--fade-soft::before {
	background: linear-gradient( to right, var(--bm-marquee-bg) 0%, var(--bm-marquee-bg) 35%, transparent 100% );
}
.bm-marquee--fade-soft::after {
	background: linear-gradient( to left, var(--bm-marquee-bg) 0%, var(--bm-marquee-bg) 35%, transparent 100% );
}
.bm-marquee--fade-hard::before {
	background: linear-gradient( to right, var(--bm-marquee-bg) 0%, transparent 60% );
}
.bm-marquee--fade-hard::after {
	background: linear-gradient( to left, var(--bm-marquee-bg) 0%, transparent 60% );
}

.bm-marquee__track {
	display: flex;
	align-items: center;
	width: max-content;
	will-change: transform;
}

/* Auto-scroll plays only after JS has cloned items to fill the viewport.
   The is-ready class is added by the runtime once setup completes.
   `animation-delay` reads --bm-marquee-delay (0s by default); the user
   sets it via the Initial delay control. The delay applies once on the
   first iteration; subsequent loops are continuous. */
.bm-marquee.is-ready:not(.bm-marquee--draggable) .bm-marquee__track {
	animation: bm-marquee-scroll var(--bm-marquee-speed) linear var(--bm-marquee-delay) infinite;
}

.bm-marquee.bm-marquee--reverse.is-ready:not(.bm-marquee--draggable) .bm-marquee__track {
	animation-direction: reverse;
}

/* Pause-on-hover — frontend feature so visitors can hover to pause and
   read marquee text. 5.7.4: explicitly excluded from the editor preview
   wrapper. Pre-5.7.4 the editor inherited the rule, so any cursor pass
   over the marquee block (e.g., on the way to the list view sidebar)
   paused the auto-scroll. The editor is for editing; if the user clicks
   Play, they want the animation running, not paused-on-cursor-proximity. */
.bm-marquee:not(.bm-marquee--editor-preview):not(.bm-marquee--draggable):not(.bm-marquee--no-pause):hover .bm-marquee__track {
	animation-play-state: paused;
}

/* Pause-on-click. The runtime toggles `is-paused` on click; when set,
   the auto-scroll animation pauses (draggable mode is exempt — it owns
   its own click semantics for momentum hand-off). */
.bm-marquee.is-paused:not(.bm-marquee--draggable) .bm-marquee__track {
	animation-play-state: paused;
}

/* 5.9.9 — Auto-scroll opt-out. When the user disables auto-scroll in
   the inspector, render.php (frontend) and edit.js (editor preview)
   emit this class. The CSS-driven non-draggable path drops the
   animation entirely (`animation: none` wins over the `is-ready` /
   `.is-previewing` rules because `none` resets all animation sub-
   properties). The draggable path is handled in `marquee-draggable.js`:
   the controller reads the same class at init and zeroes
   `baseVelocity` so the auto tick doesn't advance the transform. The
   selector list covers both contexts (frontend `is-ready`, editor
   preview `.is-previewing`) so the toggle behaves identically in
   inspector preview and on the published page. */
.bm-marquee--no-autoscroll.is-ready:not(.bm-marquee--draggable) .bm-marquee__track,
.bm-marquee--no-autoscroll.bm-marquee--editor-preview.is-previewing .bm-marquee__track {
	animation: none;
}

.bm-marquee__group {
	display: flex;
	align-items: center;
	gap: var(--bm-marquee-gap);
	padding-right: var(--bm-marquee-gap);
	flex-shrink: 0;
	min-width: max-content;
}

.bm-marquee__item {
	opacity: var(--bm-marquee-item-opacity);
	transition: opacity 0.2s ease;
}

.bm-marquee__item:hover {
	opacity: var(--bm-marquee-item-opacity-hover);
}

/* Draggable variant — JS drives the transform, no CSS animation. */
.bm-marquee--draggable .bm-marquee__track {
	cursor: grab;
	touch-action: pan-y;
	user-select: none;
	-webkit-user-select: none;
}

.bm-marquee--draggable.is-dragging .bm-marquee__track {
	cursor: grabbing;
}

.bm-marquee--draggable .bm-marquee__track img {
	-webkit-user-drag: none;
	user-drag: none;
}

@keyframes bm-marquee-scroll {
	from { transform: translateX(0); }
	to   { transform: translateX(calc(-1 * var(--bm-marquee-loop-width))); }
}

/* ── Vertical marquee (5.5.0) ──
   ==========================================================================
   When direction is `up` (bottom-to-top) or `down` (top-to-bottom), the
   marquee flips axis: track + group lay out as columns, fades sit on the
   top and bottom edges, the keyframe translates Y instead of X. Container
   needs an explicit height (no natural height when content is scrolling
   continuously); the user sets --bm-marquee-height via the Height control.
   Default 400px.
   ========================================================================== */

.bm-marquee--vertical {
	width: 100%;
	height: var(--bm-marquee-height);
}

/* Top + bottom fades replace left + right. */
.bm-marquee--vertical::before,
.bm-marquee--vertical::after {
	inset-block: auto;
	inset-inline: 0;
	width: auto;
	height: var(--bm-marquee-fade);
}
.bm-marquee--vertical::before {
	top: 0;
	left: 0;
	background: linear-gradient( to bottom, var(--bm-marquee-bg), transparent );
}
.bm-marquee--vertical::after {
	bottom: 0;
	right: 0;
	left: 0;
	background: linear-gradient( to top, var(--bm-marquee-bg), transparent );
}
/* Vertical fade-strength variants. */
.bm-marquee--vertical.bm-marquee--fade-soft::before {
	background: linear-gradient( to bottom, var(--bm-marquee-bg) 0%, var(--bm-marquee-bg) 35%, transparent 100% );
}
.bm-marquee--vertical.bm-marquee--fade-soft::after {
	background: linear-gradient( to top, var(--bm-marquee-bg) 0%, var(--bm-marquee-bg) 35%, transparent 100% );
}
.bm-marquee--vertical.bm-marquee--fade-hard::before {
	background: linear-gradient( to bottom, var(--bm-marquee-bg) 0%, transparent 60% );
}
.bm-marquee--vertical.bm-marquee--fade-hard::after {
	background: linear-gradient( to top, var(--bm-marquee-bg) 0%, transparent 60% );
}

.bm-marquee--vertical .bm-marquee__track {
	flex-direction: column;
	width: auto;
	height: max-content;
}

.bm-marquee--vertical .bm-marquee__group {
	flex-direction: column;
	padding-right: 0;
	padding-bottom: var(--bm-marquee-gap);
	min-width: 0;
	min-height: max-content;
}

/* Vertical keyframe — translateY by loopHeight. */
.bm-marquee--vertical.is-ready:not(.bm-marquee--draggable) .bm-marquee__track {
	animation-name: bm-marquee-scroll-vertical;
}

@keyframes bm-marquee-scroll-vertical {
	from { transform: translateY(0); }
	to   { transform: translateY(calc(-1 * var(--bm-marquee-loop-height))); }
}

/* Editor preview — the React edit component adds .bm-marquee--editor-preview
   to the wrapper. Two states (5.5.16):
   - When the user clicks "Play preview" (default OFF), React adds
     `is-previewing` to the wrapper. Cloned groups appear (DOM
     manipulation in React's useEffect, mirroring the runtime), the
     side fade gradients render, and the auto-scroll animation runs.
   - When the user toggles preview off (default state), React removes
     `is-previewing`. The editor falls back to the "static, scrollable"
     view: overflow:auto so all items are reachable, no animation, no
     clones, no fade gradients — clean editing surface.
   Pre-5.5.16 also had an `is-paused` state for auto-pause-on-selection
   (animation frozen but clones kept visible). 5.5.16 removed it: with
   the default flipped to OFF, auto-pause's "edit-easier" win was moot
   and it actively conflicted with the user's explicit Play click. */
.bm-marquee--editor-preview {
	overflow-x: auto;
	overflow-y: hidden;
}

/* Vertical preview uses Y scroll so the user can reach items below the
   fold; height stays bounded by --bm-marquee-height so the editor canvas
   doesn't stretch to fit every item. */
.bm-marquee--editor-preview.bm-marquee--vertical {
	overflow-x: hidden;
	overflow-y: auto;
}

/* When previewing, switch to the natural overflow:hidden + clone-and-loop
   visual so the editor matches the frontend rendering. */
.bm-marquee--editor-preview.is-previewing {
	overflow-x: hidden;
	overflow-y: hidden;
}

/* Editor preview animation — mirrors the frontend rule but scoped to
   `is-previewing` so it only fires when the user wants the preview live.
   No animation-delay applied in the editor (the frontend `delay` is for
   one-time stagger with parent reveals; in the editor we want immediate
   visual feedback). */
.bm-marquee--editor-preview.is-previewing .bm-marquee__track {
	animation: bm-marquee-scroll var(--bm-marquee-speed) linear infinite;
}
.bm-marquee--editor-preview.is-previewing.bm-marquee--vertical .bm-marquee__track {
	animation-name: bm-marquee-scroll-vertical;
}
.bm-marquee--editor-preview.is-previewing.bm-marquee--reverse .bm-marquee__track {
	animation-direction: reverse;
}

/* 5.5.16 — `is-previewing.is-paused` rule removed alongside the auto-
   pause-on-selection logic in src/marquee/index.js. Manual toggle is
   now the single source of truth: Play = animate, Pause = static. */

/* Fade gradients off when preview is OFF (keeps the items the user is
   editing at the edges visible). */
.bm-marquee--editor-preview:not(.is-previewing)::before,
.bm-marquee--editor-preview:not(.is-previewing)::after {
	display: none;
}

/* Reset transform when preview is OFF so the static view doesn't show
   the last frame of a paused animation. */
.bm-marquee--editor-preview:not(.is-previewing) .bm-marquee__track {
	animation: none;
	transform: none;
}

/* Cloned groups in the editor preview are decorative — block clicks so
   the user can't accidentally try to edit a clone (the click should land
   on the original, which is positioned earlier in the DOM). */
.bm-marquee--editor-preview .bm-marquee__group[data-bm-clone="true"] {
	pointer-events: none;
	user-select: none;
	-webkit-user-select: none;
}

/* Responsive cascade: re-bind --bm-marquee-* to the *-tablet / *-mobile
   variants below the matching breakpoint. Falls back to desktop value
   if the user didn't customize for that breakpoint. */
@media (max-width: 1024px) {
	.bm-marquee {
		--bm-marquee-speed: var(--bm-marquee-speed-tablet, 35s);
		--bm-marquee-gap:   var(--bm-marquee-gap-tablet, 64px);
		/* 5.5.19: fade fallback flipped 144px → 0px to match the
		   fade-opt-in model. Inert for plugin-rendered marquees
		   (render.php always emits *-tablet) but coherent for
		   hand-authored markup. */
		--bm-marquee-fade:  var(--bm-marquee-fade-tablet, 0px);
		/* 5.9.18 — tablet edge inset; falls through to desktop when
		   the user only set the desktop value (render.php cascades
		   undefined breakpoint values from desktop). */
		--bm-marquee-edge-inset: var(--bm-marquee-edge-inset-tablet, var(--bm-marquee-edge-inset-desktop, 0px));
	}
}

@media (max-width: 767px) {
	.bm-marquee {
		--bm-marquee-speed: var(--bm-marquee-speed-mobile, 30s);
		--bm-marquee-gap:   var(--bm-marquee-gap-mobile, 48px);
		/* 5.5.19: fade fallback flipped 128px → 0px (see tablet rule). */
		--bm-marquee-fade:  var(--bm-marquee-fade-mobile, 0px);
		/* 5.9.18 — mobile edge inset; cascades from tablet then desktop. */
		--bm-marquee-edge-inset: var(--bm-marquee-edge-inset-mobile, var(--bm-marquee-edge-inset-tablet, var(--bm-marquee-edge-inset-desktop, 0px)));
	}
}

/* ── Per-breakpoint disable (4.7.0) ──
   ==========================================================================
   When a block carries `bm-disable-mobile|tablet|desktop`, the matching
   media query forces the visible state and kills every kind of animation
   (transitions, keyframes, scroll-driven, transforms, filters). Same idea
   as the prefers-reduced-motion overrides below, just scoped by viewport.

   Breakpoints match the marquee responsive cascade:
     mobile  ≤ 767px
     tablet  768px – 1024px
     desktop ≥ 1025px
   ========================================================================== */

@media (max-width: 767px) {
	.bm-disable-mobile.bm-disable-mobile,
	.bm-disable-mobile.bm-disable-mobile .bm-line,
	.bm-disable-mobile.bm-disable-mobile .bm-word,
	.bm-disable-mobile.bm-disable-mobile .bm-char,
	.bm-disable-mobile.bm-disable-mobile .bm-marquee__track,
	.bm-disable-mobile.bm-disable-mobile.bm-marquee .bm-marquee__track {
		opacity: 1 !important;
		transform: none !important;
		filter: none !important;
		clip-path: none !important;
		transition: none !important;
		animation: none !important;
		mask-image: none !important;
		-webkit-mask-image: none !important;
	}
}

@media (min-width: 768px) and (max-width: 1024px) {
	.bm-disable-tablet.bm-disable-tablet,
	.bm-disable-tablet.bm-disable-tablet .bm-line,
	.bm-disable-tablet.bm-disable-tablet .bm-word,
	.bm-disable-tablet.bm-disable-tablet .bm-char,
	.bm-disable-tablet.bm-disable-tablet .bm-marquee__track,
	.bm-disable-tablet.bm-disable-tablet.bm-marquee .bm-marquee__track {
		opacity: 1 !important;
		transform: none !important;
		filter: none !important;
		clip-path: none !important;
		transition: none !important;
		animation: none !important;
		mask-image: none !important;
		-webkit-mask-image: none !important;
	}
}

@media (min-width: 1025px) {
	.bm-disable-desktop.bm-disable-desktop,
	.bm-disable-desktop.bm-disable-desktop .bm-line,
	.bm-disable-desktop.bm-disable-desktop .bm-word,
	.bm-disable-desktop.bm-disable-desktop .bm-char,
	.bm-disable-desktop.bm-disable-desktop .bm-marquee__track,
	.bm-disable-desktop.bm-disable-desktop.bm-marquee .bm-marquee__track {
		opacity: 1 !important;
		transform: none !important;
		filter: none !important;
		clip-path: none !important;
		transition: none !important;
		animation: none !important;
		mask-image: none !important;
		-webkit-mask-image: none !important;
	}
}

/* ── Reduced motion ──
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
	.bm-fade-in, .bm-slide-up, .bm-slide-down, .bm-slide-left, .bm-slide-right,
	.bm-scale-in, .bm-scale-out, .bm-blur-in, .bm-tilt-up, .bm-clip-reveal,
	.bm-animate-load, .bm-counter,
	.bm-text-reveal-lines .bm-line,
	.bm-text-reveal-words .bm-word,
	.bm-text-reveal-chars .bm-char {
		opacity: 1 !important;
		transform: none !important;
		filter: none !important;
		clip-path: none !important;
		transition: none !important;
	}

	.bm-scroll-scale, .bm-scroll-parallax, .bm-scroll-opacity, .bm-scroll-blur,
	.bm-float, .bm-pulse, .bm-glow {
		animation: none !important;
		transform: none !important;
		opacity: 1 !important;
		filter: none !important;
		box-shadow: none !important;
	}

	/* 5.9.7 — reduced-motion neutralization moved to the per-span
	   selectors. Pre-5.9.7 this rule targeted `.bm-scroll-text` (the
	   parent) and reset `animation` + `mask-image` there. Both were
	   no-ops post-5.9.0:
	     - The parent has no animation (animations run on .bm-word /
	       .bm-char children since the 5.9.0 per-span refactor).
	     - The mask-image fallback was retired in 5.9.0 too.
	   Net effect pre-5.9.7: reduced-motion users still saw the per-
	   span fade-in play out as they scrolled, AND because the spans'
	   baseline `opacity: var(--bm-scroll-text-dim-alpha)` (0.18) was
	   never overridden, the text was invisible at the start of the
	   reveal range. The fix targets the actual animated elements and
	   forces full opacity. */
	.bm-scroll-text .bm-word,
	.bm-scroll-text .bm-char {
		animation: none !important;
		opacity: 1 !important;
	}

	.bm-marquee .bm-marquee__track {
		animation: none !important;
		transform: none !important;
	}

	.bm-marquee .bm-marquee__item {
		opacity: 1 !important;
	}
}
