/*! effects-lab v1.0.0 — effects.css
 *  Generated by scripts/build.mjs. Do not edit dist/ by hand.
 *  Sources live in src/core/ and src/effects/.
 *  Animation engine: Motion (MIT) — vendored as dist/motion.js
 */
/* ══ src/core/00-tokens.css ══ */
/* =========================================================================
 * effects-lab — design tokens
 * Every value is a CSS custom property. Override any of them on :root (or on
 * any container) to re-theme the whole library. No brand colours are baked in:
 * the defaults are neutral greys plus a single hue you control with --fx-hue.
 * ========================================================================= */
:root {
  /* --- brand hooks: set these and everything follows ---------------------- */
  --fx-hue: 250;
  --fx-accent: hsl(var(--fx-hue) 90% 62%);
  --fx-accent-2: hsl(calc(var(--fx-hue) + 55) 88% 60%);
  --fx-accent-3: hsl(calc(var(--fx-hue) - 65) 85% 58%);
  --fx-accent-contrast: hsl(var(--fx-hue) 90% 8%);

  /* --- surfaces ---------------------------------------------------------- */
  --fx-bg: hsl(var(--fx-hue) 12% 7%);
  --fx-surface: hsl(var(--fx-hue) 10% 11%);
  --fx-surface-2: hsl(var(--fx-hue) 10% 15%);
  --fx-border: hsl(var(--fx-hue) 12% 22%);
  --fx-fg: hsl(var(--fx-hue) 15% 96%);
  --fx-muted: hsl(var(--fx-hue) 10% 62%);

  /* --- motion: durations -------------------------------------------------- */
  --fx-dur-1: 0.16s;   /* micro feedback */
  --fx-dur-2: 0.28s;   /* state change */
  --fx-dur-3: 0.5s;    /* entrance */
  --fx-dur-4: 0.9s;    /* large / cinematic */
  --fx-stagger: 0.055s;

  /* --- motion: easing ----------------------------------------------------- */
  --fx-ease: cubic-bezier(0.32, 0.72, 0, 1);         /* the workhorse */
  --fx-ease-out: cubic-bezier(0.22, 1, 0.36, 1);      /* decelerate */
  --fx-ease-in: cubic-bezier(0.55, 0, 1, 0.45);
  --fx-ease-spring: linear(0, 0.0072 1.4%, 0.0297 2.9%, 0.1197 5.9%, 0.4441 12.3%, 0.5723 14.9%, 0.6842, 0.7813 20.3%, 0.8638 23.2%, 0.9327 26.4%, 0.9881 30.1%, 1.0292 34%, 1.0559 38.4%, 1.0694 43.5%, 1.0673 49.6%, 1.0431 61.1%, 1.0067 80.2%, 1);
  --fx-ease-snap: cubic-bezier(0.16, 1.2, 0.3, 1);

  /* --- geometry ----------------------------------------------------------- */
  --fx-radius: 14px;
  --fx-radius-sm: 8px;
  --fx-radius-lg: 24px;
  --fx-ring: 0 0 0 1px var(--fx-border);
  --fx-shadow: 0 1px 2px hsl(0 0% 0% / 0.3), 0 8px 24px -12px hsl(0 0% 0% / 0.5);
  --fx-blur: 14px;
}

/* Opt-in light palette: <html data-fx-theme="light"> */
:root[data-fx-theme="light"] {
  --fx-bg: hsl(var(--fx-hue) 20% 98%);
  --fx-surface: hsl(var(--fx-hue) 24% 100%);
  --fx-surface-2: hsl(var(--fx-hue) 20% 95%);
  --fx-border: hsl(var(--fx-hue) 16% 87%);
  --fx-fg: hsl(var(--fx-hue) 20% 10%);
  --fx-muted: hsl(var(--fx-hue) 10% 42%);
  --fx-accent-contrast: hsl(0 0% 100%);
  --fx-shadow: 0 1px 2px hsl(var(--fx-hue) 20% 40% / 0.08), 0 10px 30px -14px hsl(var(--fx-hue) 30% 30% / 0.25);
}

/* ══ src/core/01-base.css ══ */
/* =========================================================================
 * effects-lab — base + reduced-motion contract
 *
 * REDUCED MOTION works two ways and both must keep content readable:
 *   1. the OS setting  -> @media (prefers-reduced-motion: reduce)
 *   2. a manual switch -> :root[data-fx-reduced]  (set by FX.setReducedMotion)
 * Use the shared selector list below in every effect stylesheet.
 * ========================================================================= */

[data-fx] { --fx-i: 0; }

/* Shared text-splitting primitives (produced by FX.util.splitText) --------- */
.fx-split { display: inline; }
.fx-word-wrap,
.fx-line-wrap { display: inline-block; overflow: hidden; vertical-align: top; padding-block: 0.12em; margin-block: -0.12em; }
.fx-word,
.fx-line { display: inline-block; will-change: transform, opacity; }
.fx-char { display: inline-block; will-change: transform, opacity; }

/* Screen-reader text kept intact when an effect rewrites the DOM ----------- */
.fx-sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* Global safety net -------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  [data-fx] *,
  [data-fx] { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; }
}
:root[data-fx-reduced] [data-fx],
:root[data-fx-reduced] [data-fx] * {
  animation-duration: 0.01ms !important;
  animation-iteration-count: 1 !important;
}
/* Content must never stay invisible because motion was switched off.        */
@media (prefers-reduced-motion: reduce) {
  [data-fx][data-fx-ready] { opacity: 1 !important; }
}
:root[data-fx-reduced] [data-fx][data-fx-ready] { opacity: 1 !important; }

/* Utility container every demo/effect can rely on -------------------------- */
.fx-stage {
  position: relative;
  display: grid;
  place-items: center;
  min-height: 220px;
  padding: 2rem;
  border-radius: var(--fx-radius);
  background: var(--fx-surface);
  color: var(--fx-fg);
  overflow: hidden;
}

/* ══ src/effects/bg.css ══ */
/* =========================================================================
 * effects-lab — category: bg (Backgrounds)
 *
 * Ambient, decorative background layers. Rules for this file:
 *   · every class and keyframe is namespaced `fx-bg-`
 *   · every colour comes from the tokens in src/core/00-tokens.css
 *   · every loop is seamless (frame at 0% === frame at 100%, or the layer
 *     fades through zero at both ends)
 *   · every decorative layer is inert: absolute, inset 0, pointer-events none
 *   · reduced motion freezes each loop on a deliberately chosen frame via
 *     `--fx-bg-play` + a negative `animation-delay` — never blank, never
 *     mid-transition
 * ========================================================================= */

/* Every effect below answers to BOTH `data-fx="bg-<name>"` (the library-wide
   switch) and the legacy `.fx-bg-<name>` class, so background markup reads the
   same as every other category. Child parts keep their own `__part` classes. */

/* ---------------------------------------------------------------- switches */
@property --fx-bg-angle { syntax: "<angle>"; inherits: false; initial-value: 0deg; }

:root { --fx-bg-play: running; }
@media (prefers-reduced-motion: reduce) { :root { --fx-bg-play: paused; } }
:root[data-fx-reduced] { --fx-bg-play: paused; }

/* ------------------------------------------------------------- scaffolding */

/* The section a background lives in. Creates the stacking context so blend
   modes stay trapped inside and can never touch the rest of the page. */
.fx-bg-scene {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  display: grid;
  place-items: center;
  place-self: stretch;
  width: 100%;
  min-height: 260px;
  padding: clamp(1.5rem, 6%, 3rem);
  border-radius: var(--fx-radius-lg);
  background: var(--fx-bg);
  color: var(--fx-fg);
}

/* Per-instance colour hooks. Override these, never the tokens themselves.
   They are declared on every element that paints, not only on .fx-bg-layer:
   bg-border-beam draws straight onto its container and needs them too. */
.fx-bg-layer,
:is(.fx-bg-border-beam,[data-fx~="bg-border-beam"]) {
  --fx-bg-c1: var(--fx-accent);
  --fx-bg-c2: var(--fx-accent-2);
  --fx-bg-c3: var(--fx-accent-3);
  --fx-bg-line: color-mix(in srgb, var(--fx-fg) 14%, transparent);
  /* generated film grain, used as an alpha mask so its colour follows --fx-fg */
  --fx-bg-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0.36 0.36 0.36 0 0'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23g)'/%3E%3C/svg%3E");
}

/* Any decorative layer. Never interactive, never in the a11y tree. */
.fx-bg-layer {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
  user-select: none;
  border-radius: inherit;
}

/* JS effects mount their layer into whatever element carries data-fx. */
.fx-bg-host { isolation: isolate; overflow: hidden; }
.fx-bg-canvas { position: absolute; inset: 0; display: block; width: 100%; height: 100%; }

/* Foreground content always wins. */
.fx-bg-content { position: relative; z-index: 1; max-width: 36ch; }

.fx-bg-eyebrow {
  display: inline-block;
  padding: 0.32em 0.85em;
  border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--fx-border) 80%, transparent);
  background: color-mix(in srgb, var(--fx-surface) 55%, transparent);
  color: var(--fx-muted);
  font-size: 0.625rem;
  font-weight: 500;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  backdrop-filter: blur(8px);
}
.fx-bg-title {
  margin: 0.85rem 0 0.45rem;
  font-size: clamp(1.35rem, 4.4vw, 1.95rem);
  font-weight: 560;
  line-height: 1.08;
  letter-spacing: -0.032em;
  text-wrap: balance;
}
.fx-bg-sub {
  margin: 0;
  color: var(--fx-muted);
  font-size: 0.8125rem;
  line-height: 1.6;
  max-width: 34ch;
}

/* Nested-enclosure card, for the effects that sit behind a surface. */
.fx-bg-card {
  position: relative;
  z-index: 1;
  color: var(--fx-fg);
  padding: 6px;
  border-radius: calc(var(--fx-radius-lg) + 6px);
  background: color-mix(in srgb, var(--fx-surface) 45%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fx-fg) 8%, transparent);
  backdrop-filter: blur(10px);
}
.fx-bg-card__inner {
  padding: 1.25rem 1.4rem 1.4rem;
  border-radius: var(--fx-radius-lg);
  background: color-mix(in srgb, var(--fx-surface) 88%, transparent);
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, var(--fx-fg) 10%, transparent),
    var(--fx-shadow);
}

/* =========================================================================
 * bg-aurora — three soft colour fields drifting on unrelated clocks
 * ========================================================================= */
.fx-bg-aurora__blob {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 62%;
  height: 78%;
  margin: -39% 0 0 -31%;
  border-radius: 50%;
  filter: blur(42px);
  mix-blend-mode: screen;
  will-change: transform;
  opacity: var(--fx-bg-opacity, 0.6);
}
.fx-bg-aurora__blob:nth-child(1) {
  background: radial-gradient(circle at 50% 50%,
    color-mix(in srgb, var(--fx-bg-c1) 58%, transparent) 0%,
    color-mix(in srgb, var(--fx-bg-c1) 28%, transparent) 42%,
    transparent 70%);
  animation: fx-bg-aurora-a 26s cubic-bezier(0.45, 0, 0.55, 1) -8s infinite;
  animation-play-state: var(--fx-bg-play, running);
}
.fx-bg-aurora__blob:nth-child(2) {
  background: radial-gradient(circle at 50% 50%,
    color-mix(in srgb, var(--fx-bg-c2) 52%, transparent) 0%,
    color-mix(in srgb, var(--fx-bg-c2) 24%, transparent) 44%,
    transparent 72%);
  animation: fx-bg-aurora-b 33s cubic-bezier(0.45, 0, 0.55, 1) -14s infinite;
  animation-play-state: var(--fx-bg-play, running);
}
.fx-bg-aurora__blob:nth-child(3) {
  background: radial-gradient(circle at 50% 50%,
    color-mix(in srgb, var(--fx-bg-c3) 48%, transparent) 0%,
    color-mix(in srgb, var(--fx-bg-c3) 22%, transparent) 46%,
    transparent 74%);
  animation: fx-bg-aurora-c 41s cubic-bezier(0.45, 0, 0.55, 1) -3s infinite;
  animation-play-state: var(--fx-bg-play, running);
}
@keyframes fx-bg-aurora-a {
  0%   { transform: translate3d(-26%, -14%, 0) scale(1);    }
  33%  { transform: translate3d(16%, 6%, 0)    scale(1.22); }
  66%  { transform: translate3d(-8%, 20%, 0)   scale(0.9);  }
  100% { transform: translate3d(-26%, -14%, 0) scale(1);    }
}
@keyframes fx-bg-aurora-b {
  0%   { transform: translate3d(24%, 10%, 0)  scale(1.1);  }
  33%  { transform: translate3d(-18%, 22%, 0) scale(0.88); }
  66%  { transform: translate3d(6%, -18%, 0)  scale(1.24); }
  100% { transform: translate3d(24%, 10%, 0)  scale(1.1);  }
}
@keyframes fx-bg-aurora-c {
  0%   { transform: translate3d(2%, 26%, 0)   scale(0.96); }
  33%  { transform: translate3d(28%, -12%, 0) scale(1.18); }
  66%  { transform: translate3d(-24%, -2%, 0) scale(1.04); }
  100% { transform: translate3d(2%, 26%, 0)   scale(0.96); }
}

/* =========================================================================
 * bg-gradient-mesh — four anchored points morph into each other
 * ========================================================================= */
:is(.fx-bg-gradient-mesh,[data-fx~="bg-gradient-mesh"]) { --fx-bg-opacity: 0.52; }
.fx-bg-gradient-mesh__node {
  position: absolute;
  width: 86%;
  height: 92%;
  border-radius: 50%;
  filter: blur(48px);
  mix-blend-mode: screen;
  will-change: transform;
  opacity: var(--fx-bg-opacity, 0.52);
}
.fx-bg-gradient-mesh__node:nth-child(1) {
  top: -26%; left: -22%;
  background: radial-gradient(circle at 50% 50%,
    color-mix(in srgb, var(--fx-bg-c1) 58%, transparent) 0%, transparent 66%);
  animation: fx-bg-gradient-mesh-a 24s cubic-bezier(0.45, 0, 0.55, 1) -5s infinite;
  animation-play-state: var(--fx-bg-play, running);
}
.fx-bg-gradient-mesh__node:nth-child(2) {
  top: -30%; right: -24%;
  background: radial-gradient(circle at 50% 50%,
    color-mix(in srgb, var(--fx-bg-c2) 52%, transparent) 0%, transparent 66%);
  animation: fx-bg-gradient-mesh-b 29s cubic-bezier(0.45, 0, 0.55, 1) -11s infinite;
  animation-play-state: var(--fx-bg-play, running);
}
.fx-bg-gradient-mesh__node:nth-child(3) {
  bottom: -32%; left: -18%;
  background: radial-gradient(circle at 50% 50%,
    color-mix(in srgb, var(--fx-bg-c3) 50%, transparent) 0%, transparent 66%);
  animation: fx-bg-gradient-mesh-c 35s cubic-bezier(0.45, 0, 0.55, 1) -19s infinite;
  animation-play-state: var(--fx-bg-play, running);
}
.fx-bg-gradient-mesh__node:nth-child(4) {
  bottom: -28%; right: -20%;
  background: radial-gradient(circle at 50% 50%,
    color-mix(in srgb, var(--fx-bg-c1) 42%, transparent) 0%, transparent 64%);
  animation: fx-bg-gradient-mesh-d 43s cubic-bezier(0.45, 0, 0.55, 1) -2s infinite;
  animation-play-state: var(--fx-bg-play, running);
}
@keyframes fx-bg-gradient-mesh-a {
  0%, 100% { transform: translate3d(0, 0, 0)     scale(1);    }
  50%      { transform: translate3d(18%, 14%, 0) scale(1.18); }
}
@keyframes fx-bg-gradient-mesh-b {
  0%, 100% { transform: translate3d(0, 0, 0)       scale(1.12); }
  50%      { transform: translate3d(-16%, 18%, 0)  scale(0.92); }
}
@keyframes fx-bg-gradient-mesh-c {
  0%, 100% { transform: translate3d(0, 0, 0)       scale(0.95); }
  50%      { transform: translate3d(20%, -16%, 0)  scale(1.2);  }
}
@keyframes fx-bg-gradient-mesh-d {
  0%, 100% { transform: translate3d(0, 0, 0)       scale(1.08); }
  50%      { transform: translate3d(-22%, -12%, 0) scale(0.9);  }
}

/* =========================================================================
 * bg-beams — light shafts drifting across on a diagonal
 * ========================================================================= */
:is(.fx-bg-beams,[data-fx~="bg-beams"])::before,
:is(.fx-bg-beams,[data-fx~="bg-beams"])::after {
  content: "";
  position: absolute;
  left: -70%;
  top: -70%;
  width: 240%;
  height: 240%;
  transform-origin: 50% 50%;
  mask-image: radial-gradient(ellipse 52% 52% at 50% 50%, #000 12%, transparent 70%);
}
:is(.fx-bg-beams,[data-fx~="bg-beams"])::before {
  --fx-bg-tilt: -24deg;
  --fx-bg-period: 460px;
  background: repeating-linear-gradient(90deg,
    transparent 0 170px,
    color-mix(in srgb, var(--fx-bg-c1) 30%, transparent) 230px,
    transparent 290px 460px);
  animation: fx-bg-beams-drift 22s linear -6s infinite;
  animation-play-state: var(--fx-bg-play, running);
}
:is(.fx-bg-beams,[data-fx~="bg-beams"])::after {
  --fx-bg-tilt: -16deg;
  --fx-bg-period: 730px;
  background: repeating-linear-gradient(90deg,
    transparent 0 250px,
    color-mix(in srgb, var(--fx-bg-c2) 22%, transparent) 365px,
    transparent 480px 730px);
  animation: fx-bg-beams-drift 31s linear -17s infinite;
  animation-play-state: var(--fx-bg-play, running);
}
@keyframes fx-bg-beams-drift {
  from { transform: rotate(var(--fx-bg-tilt)) translate3d(0, 0, 0); }
  to   { transform: rotate(var(--fx-bg-tilt)) translate3d(calc(var(--fx-bg-period) * -1), 0, 0); }
}

/* =========================================================================
 * bg-border-beam — a light travels the perimeter of a container
 * ========================================================================= */
:is(.fx-bg-border-beam,[data-fx~="bg-border-beam"]) {
  position: relative;
  isolation: isolate;
  color: var(--fx-fg);
  max-width: 30rem;
  padding: 6px;                        /* the outer tray of the nested enclosure */
  border-radius: calc(var(--fx-radius-lg) + 6px);
  background: color-mix(in srgb, var(--fx-surface) 55%, transparent);
  backdrop-filter: blur(10px);
}
.fx-bg-border-beam__inner {
  position: relative;
  z-index: 1;
  padding: 1.25rem 1.4rem 1.4rem;
  border-radius: var(--fx-radius-lg);  /* concentric with the tray */
  background: color-mix(in srgb, var(--fx-surface) 92%, transparent);
  box-shadow: inset 0 1px 0 color-mix(in srgb, var(--fx-fg) 9%, transparent);
}
:is(.fx-bg-border-beam,[data-fx~="bg-border-beam"])::before,
:is(.fx-bg-border-beam,[data-fx~="bg-border-beam"])::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1px;
  pointer-events: none;
  -webkit-mask-image: linear-gradient(#000 0 0), linear-gradient(#000 0 0);
  -webkit-mask-clip: content-box, border-box;
  -webkit-mask-composite: xor;
  mask-image: linear-gradient(#000 0 0), linear-gradient(#000 0 0);
  mask-clip: content-box, border-box;
  mask-composite: exclude;
}
/* the resting hairline */
:is(.fx-bg-border-beam,[data-fx~="bg-border-beam"])::before {
  background: linear-gradient(to bottom,
    color-mix(in srgb, var(--fx-fg) 16%, transparent),
    color-mix(in srgb, var(--fx-fg) 6%, transparent));
}
/* the travelling light */
:is(.fx-bg-border-beam,[data-fx~="bg-border-beam"])::after {
  background: conic-gradient(from var(--fx-bg-angle),
    transparent 0deg,
    transparent 262deg,
    color-mix(in srgb, var(--fx-bg-c2) 45%, transparent) 322deg,
    var(--fx-bg-c2) 348deg,
    var(--fx-bg-c1) 358deg,
    transparent 360deg);
  animation: fx-bg-border-beam-orbit var(--fx-bg-duration, 8s) linear infinite;
  animation-play-state: var(--fx-bg-play, running);
}
@keyframes fx-bg-border-beam-orbit { to { --fx-bg-angle: 360deg; } }

/* =========================================================================
 * bg-grid — technical grid under a fading radial mask, lit by a drifting glow
 * ========================================================================= */
:is(.fx-bg-grid,[data-fx~="bg-grid"]) {
  --fx-bg-cell: 44px;
  mask-image: radial-gradient(ellipse 78% 72% at 50% 44%, #000 16%, transparent 78%);
}
:is(.fx-bg-grid,[data-fx~="bg-grid"])::before {
  content: "";
  position: absolute;
  inset: -1px;
  background-image:
    linear-gradient(to right,  var(--fx-bg-line) 1px, transparent 1px),
    linear-gradient(to bottom, var(--fx-bg-line) 1px, transparent 1px);
  background-size: var(--fx-bg-cell) var(--fx-bg-cell);
  opacity: var(--fx-bg-opacity, 0.7);
}
:is(.fx-bg-grid,[data-fx~="bg-grid"])::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 78%;
  height: 96%;
  margin: -48% 0 0 -39%;
  border-radius: 50%;
  background: radial-gradient(circle at 50% 50%,
    color-mix(in srgb, var(--fx-bg-c1) 44%, transparent) 0%,
    color-mix(in srgb, var(--fx-bg-c2) 16%, transparent) 46%,
    transparent 72%);
  filter: blur(34px);
  mix-blend-mode: screen;
  will-change: transform;
  animation: fx-bg-grid-wander 28s cubic-bezier(0.45, 0, 0.55, 1) -7s infinite;
  animation-play-state: var(--fx-bg-play, running);
}
@keyframes fx-bg-grid-wander {
  0%   { transform: translate3d(-18%, -10%, 0) scale(0.95); }
  33%  { transform: translate3d(16%, 8%, 0)    scale(1.16); }
  66%  { transform: translate3d(-4%, 16%, 0)   scale(0.88); }
  100% { transform: translate3d(-18%, -10%, 0) scale(0.95); }
}

/* =========================================================================
 * bg-grid-pulse — grid cells light up at random (JS builds the cells)
 * ========================================================================= */
.fx-bg-grid-pulse__layer {
  mask-image: radial-gradient(ellipse 74% 70% at 50% 46%, #000 6%, transparent 72%);
}
.fx-bg-grid-pulse__lines {
  position: absolute;
  inset: -1px;
  background-image:
    linear-gradient(to right,  var(--fx-bg-line) 1px, transparent 1px),
    linear-gradient(to bottom, var(--fx-bg-line) 1px, transparent 1px);
  background-size: var(--fx-bg-cell, 44px) var(--fx-bg-cell, 44px);
  opacity: 0.62;
}
.fx-bg-grid-pulse__cells {
  position: absolute;
  top: 0;
  left: 0;
  display: grid;
  grid-auto-rows: var(--fx-bg-cell, 44px);
  grid-template-columns: repeat(var(--fx-bg-cols, 8), var(--fx-bg-cell, 44px));
}
.fx-bg-grid-pulse__cell {
  opacity: 0;
  background: radial-gradient(circle at 35% 30%,
    color-mix(in srgb, var(--fx-bg-c1) 55%, transparent) 0%,
    color-mix(in srgb, var(--fx-bg-c2) 16%, transparent) 70%,
    transparent 100%);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fx-bg-c1) 26%, transparent);
}
.fx-bg-grid-pulse__cell.is-lit {
  animation: fx-bg-grid-pulse-glow var(--fx-bg-fade, 3.4s) cubic-bezier(0.45, 0, 0.55, 1) both;
}
/* the static frame: a hand-placed scatter, not a random snapshot */
.fx-bg-grid-pulse__cell.is-static { opacity: 0.55; }
@keyframes fx-bg-grid-pulse-glow {
  0%   { opacity: 0;    }
  38%  { opacity: 0.85; }
  100% { opacity: 0;    }
}

/* =========================================================================
 * bg-dots — dot matrix drifting one cell per loop under a radial mask
 * ========================================================================= */
:is(.fx-bg-dots,[data-fx~="bg-dots"]) {
  --fx-bg-cell: 22px;
  mask-image: radial-gradient(ellipse 76% 74% at 50% 46%, #000 12%, transparent 76%);
}
:is(.fx-bg-dots,[data-fx~="bg-dots"])::before {
  content: "";
  position: absolute;
  inset: calc(var(--fx-bg-cell) * -2);
  background-image: radial-gradient(circle at center,
    color-mix(in srgb, var(--fx-fg) 42%, transparent) 1.1px, transparent 1.2px);
  background-size: var(--fx-bg-cell) var(--fx-bg-cell);
  opacity: var(--fx-bg-opacity, 0.5);
  animation:
    fx-bg-dots-drift 44s linear -12s infinite,
    fx-bg-dots-breathe 14s cubic-bezier(0.45, 0, 0.55, 1) -4s infinite;
  animation-play-state: var(--fx-bg-play, running);
}
:is(.fx-bg-dots,[data-fx~="bg-dots"])::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 72%;
  height: 92%;
  margin: -46% 0 0 -36%;
  border-radius: 50%;
  background: radial-gradient(circle at 50% 50%,
    color-mix(in srgb, var(--fx-bg-c1) 38%, transparent) 0%, transparent 70%);
  filter: blur(30px);
  mix-blend-mode: screen;
  will-change: transform;
  animation: fx-bg-dots-wander 26s cubic-bezier(0.45, 0, 0.55, 1) -9s infinite;
  animation-play-state: var(--fx-bg-play, running);
}
@keyframes fx-bg-dots-drift {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(calc(var(--fx-bg-cell) * -1), calc(var(--fx-bg-cell) * -1), 0); }
}
@keyframes fx-bg-dots-breathe {
  0%, 100% { opacity: var(--fx-bg-opacity, 0.5); }
  50%      { opacity: calc(var(--fx-bg-opacity, 0.5) * 1.5); }
}
@keyframes fx-bg-dots-wander {
  0%   { transform: translate3d(-14%, 8%, 0)  scale(1);    }
  50%  { transform: translate3d(14%, -10%, 0) scale(1.14); }
  100% { transform: translate3d(-14%, 8%, 0)  scale(1);    }
}

/* =========================================================================
 * bg-dot-spotlight — the dot grid brightens around the pointer (JS)
 * ========================================================================= */
.fx-bg-dot-spotlight__layer {
  --fx-bg-cell: 20px;
  --fx-bg-r: 300;
  --fx-bg-x: 0;
  --fx-bg-y: 0;
}
.fx-bg-dot-spotlight__base {
  position: absolute;
  inset: 0;
  background-image: radial-gradient(circle at center,
    color-mix(in srgb, var(--fx-fg) 30%, transparent) 1.1px, transparent 1.2px);
  background-size: var(--fx-bg-cell) var(--fx-bg-cell);
  background-position: 0 0;
  opacity: 0.4;
  mask-image: radial-gradient(ellipse 80% 78% at 50% 50%, #000 18%, transparent 82%);
}
.fx-bg-dot-spotlight__orb {
  position: absolute;
  top: 0;
  left: 0;
  width: calc(var(--fx-bg-r) * 1px);
  height: calc(var(--fx-bg-r) * 1px);
  /* the dot phase is cancelled out so the bright dots stay locked to the grid */
  background-image: radial-gradient(circle at center,
    var(--fx-bg-c1) 1.6px, transparent 1.7px);
  background-size: var(--fx-bg-cell) var(--fx-bg-cell);
  background-position:
    calc((var(--fx-bg-r) * 0.5 - var(--fx-bg-x)) * 1px)
    calc((var(--fx-bg-r) * 0.5 - var(--fx-bg-y)) * 1px);
  mask-image: radial-gradient(circle at 50% 50%, #000 0%, transparent 66%);
  transform: translate3d(
    calc(var(--fx-bg-x) * 1px - 50%),
    calc(var(--fx-bg-y) * 1px - 50%), 0);
  opacity: var(--fx-bg-opacity, 0.9);
}
.fx-bg-dot-spotlight__halo {
  position: absolute;
  top: 0;
  left: 0;
  width: calc(var(--fx-bg-r) * 1.6px);
  height: calc(var(--fx-bg-r) * 1.6px);
  border-radius: 50%;
  background: radial-gradient(circle at 50% 50%,
    color-mix(in srgb, var(--fx-bg-c1) 26%, transparent) 0%, transparent 68%);
  mix-blend-mode: screen;
  transform: translate3d(
    calc(var(--fx-bg-x) * 1px - 50%),
    calc(var(--fx-bg-y) * 1px - 50%), 0);
}

/* =========================================================================
 * bg-noise — generated film grain, achromatic mask tinted by --fx-fg
 * ========================================================================= */
:is(.fx-bg-noise,[data-fx~="bg-noise"]) {
  overflow: hidden;
  mix-blend-mode: overlay;
  opacity: var(--fx-bg-opacity, 0.085);
}
:is(.fx-bg-noise,[data-fx~="bg-noise"])::before {
  content: "";
  position: absolute;
  inset: -8%;
  background: var(--fx-fg);
  -webkit-mask-image: var(--fx-bg-grain);
  mask-image: var(--fx-bg-grain);
  -webkit-mask-size: 180px 180px;
  mask-size: 180px 180px;
  animation: fx-bg-noise-jitter 1.2s steps(1, end) infinite;
  animation-play-state: var(--fx-bg-play, running);
}
@keyframes fx-bg-noise-jitter {
  0%    { transform: translate3d(0, 0, 0);       }
  12.5% { transform: translate3d(-3%, 2%, 0);    }
  25%   { transform: translate3d(2%, -3%, 0);    }
  37.5% { transform: translate3d(-4%, -2%, 0);   }
  50%   { transform: translate3d(3%, 3%, 0);     }
  62.5% { transform: translate3d(-2%, 4%, 0);    }
  75%   { transform: translate3d(4%, -1%, 0);    }
  87.5% { transform: translate3d(-1%, -4%, 0);   }
  100%  { transform: translate3d(0, 0, 0);       }
}

/* =========================================================================
 * bg-conic-glow — a slow conic sweep blurred behind a surface
 * ========================================================================= */
:is(.fx-bg-conic-glow,[data-fx~="bg-conic-glow"])::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: clamp(260px, 120%, 720px);
  aspect-ratio: 1;
  translate: -50% -50%;
  background: conic-gradient(from 0deg,
    transparent 0deg,
    color-mix(in srgb, var(--fx-bg-c1) 62%, transparent) 52deg,
    transparent 128deg,
    color-mix(in srgb, var(--fx-bg-c2) 56%, transparent) 206deg,
    transparent 276deg,
    color-mix(in srgb, var(--fx-bg-c3) 48%, transparent) 322deg,
    transparent 360deg);
  filter: blur(44px);
  mask-image: radial-gradient(circle at 50% 50%, #000 34%, transparent 70%);
  opacity: var(--fx-bg-opacity, 0.78);
  will-change: transform;
  animation: fx-bg-conic-glow-spin 18s linear infinite;
  animation-play-state: var(--fx-bg-play, running);
}
@keyframes fx-bg-conic-glow-spin { to { transform: rotate(360deg); } }

/* =========================================================================
 * bg-stars — canvas particle field (JS), sitting in a faint nebula
 * ========================================================================= */
.fx-bg-stars__layer::before {
  content: "";
  position: absolute;
  inset: -12%;
  background:
    radial-gradient(ellipse 52% 44% at 24% 22%,
      color-mix(in srgb, var(--fx-bg-c1) 22%, transparent) 0%, transparent 68%),
    radial-gradient(ellipse 56% 46% at 78% 76%,
      color-mix(in srgb, var(--fx-bg-c3) 18%, transparent) 0%, transparent 70%);
  filter: blur(26px);
}

/* =========================================================================
 * bg-scanlines — CRT raster with a soft band travelling down
 * ========================================================================= */
:is(.fx-bg-scanlines,[data-fx~="bg-scanlines"]) {
  background-image: repeating-linear-gradient(to bottom,
    color-mix(in srgb, var(--fx-fg) 22%, transparent) 0 1px,
    transparent 1px 4px);
  opacity: var(--fx-bg-opacity, 0.42);
}
:is(.fx-bg-scanlines,[data-fx~="bg-scanlines"])::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 38%;
  background: linear-gradient(to bottom,
    transparent 0%,
    color-mix(in srgb, var(--fx-bg-c1) 30%, transparent) 50%,
    transparent 100%);
  animation: fx-bg-scanlines-sweep 12s cubic-bezier(0.45, 0, 0.55, 1) -4.6s infinite;
  animation-play-state: var(--fx-bg-play, running);
}
:is(.fx-bg-scanlines,[data-fx~="bg-scanlines"])::after {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(ellipse 92% 86% at 50% 50%,
    transparent 38%, color-mix(in srgb, var(--fx-bg) 78%, transparent) 100%);
}
@keyframes fx-bg-scanlines-sweep {
  0%   { transform: translate3d(0, -110%, 0); opacity: 0; }
  14%  { opacity: 1; }
  86%  { opacity: 1; }
  100% { transform: translate3d(0, 380%, 0);  opacity: 0; }
}

/* =========================================================================
 * bg-wave-lines — two contour fields rotating against each other
 * ========================================================================= */
:is(.fx-bg-wave-lines,[data-fx~="bg-wave-lines"]) {
  mask-image: radial-gradient(ellipse 72% 70% at 50% 50%, #000 8%, transparent 74%);
}
:is(.fx-bg-wave-lines,[data-fx~="bg-wave-lines"])::before,
:is(.fx-bg-wave-lines,[data-fx~="bg-wave-lines"])::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 190%;
  height: 190%;
  translate: -50% -50%;
  transform-origin: 50% 50%;
  will-change: transform;
}
:is(.fx-bg-wave-lines,[data-fx~="bg-wave-lines"])::before {
  background: repeating-radial-gradient(ellipse 44% 17% at 50% 50%,
    transparent 0 9.9%,
    color-mix(in srgb, var(--fx-bg-c1) 34%, transparent) 9.9% 11%,
    transparent 11% 11.1%);
  opacity: var(--fx-bg-opacity, 0.6);
  animation: fx-bg-wave-lines-cw 46s linear infinite;
  animation-play-state: var(--fx-bg-play, running);
}
:is(.fx-bg-wave-lines,[data-fx~="bg-wave-lines"])::after {
  background: repeating-radial-gradient(ellipse 39% 21% at 50% 50%,
    transparent 0 11.6%,
    color-mix(in srgb, var(--fx-bg-c3) 28%, transparent) 11.6% 12.8%,
    transparent 12.8% 12.9%);
  opacity: calc(var(--fx-bg-opacity, 0.6) * 0.8);
  animation: fx-bg-wave-lines-ccw 58s linear -20s infinite;
  animation-play-state: var(--fx-bg-play, running);
}
@keyframes fx-bg-wave-lines-cw  { to { transform: rotate(360deg);  } }
@keyframes fx-bg-wave-lines-ccw { to { transform: rotate(-360deg); } }

/* =========================================================================
 * bg-spotlight-follow — a large soft light trailing the pointer (JS)
 * ========================================================================= */
.fx-bg-spotlight-follow__layer {
  --fx-bg-r: 560;
  --fx-bg-x: 0;
  --fx-bg-y: 0;
}
.fx-bg-spotlight-follow__orb {
  position: absolute;
  top: 0;
  left: 0;
  width: calc(var(--fx-bg-r) * 1px);
  height: calc(var(--fx-bg-r) * 1px);
  border-radius: 50%;
  background: radial-gradient(circle at 50% 50%,
    color-mix(in srgb, var(--fx-bg-c1) 40%, transparent) 0%,
    color-mix(in srgb, var(--fx-bg-c2) 16%, transparent) 38%,
    transparent 68%);
  mix-blend-mode: screen;
  opacity: var(--fx-bg-opacity, 0.95);
  transform: translate3d(
    calc(var(--fx-bg-x) * 1px - 50%),
    calc(var(--fx-bg-y) * 1px - 50%), 0);
}
.fx-bg-spotlight-follow__rim {
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(to right,  var(--fx-bg-line) 1px, transparent 1px),
    linear-gradient(to bottom, var(--fx-bg-line) 1px, transparent 1px);
  background-size: 56px 56px;
  opacity: 0.2;
  mask-image: radial-gradient(ellipse 84% 82% at 50% 50%, #000 12%, transparent 84%);
}

/* =========================================================================
 * bg-torn-edge — CSS only. A torn paper edge as a section divider: a strip
 * of ink masked by feTurbulence fractal noise that is fully dense at one
 * edge and ragged at the other, so a surface reads as ripped off whatever
 * sits behind it. No image asset, no dependency — the tear is one data URI,
 * intersected with a density gradient exactly like a grunge poster edge.
 *
 * Place the strip directly against the section it belongs to and give
 * --fx-bg-tear-fill that section's own background, so the dense edge is
 * seamless and only the ragged edge shows. The noise is stretched more
 * vertically than horizontally on purpose: the tears read as drips and
 * flecks rather than as an even speckle.
 * ========================================================================= */
:is(.fx-bg-torn-edge, [data-fx~="bg-torn-edge"]) {
  /* seed 11 — the classic cut */
  --fx-bg-tear-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='900' height='120'><filter id='t' x='0' y='0' width='100%25' height='100%25'><feTurbulence type='fractalNoise' baseFrequency='0.014 0.09' numOctaves='4' seed='11'/><feColorMatrix type='matrix' values='0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  1.9 0 0 0 -0.34'/></filter><rect width='900' height='120' filter='url(%23t)'/></svg>");
  --fx-bg-tear-h: clamp(40px, 7vh, 96px);
  --fx-bg-tear-fill: var(--fx-surface);
  display: block;
  width: 100%;
  height: var(--fx-bg-tear-h);
  background: var(--fx-bg-tear-fill);
  pointer-events: none;
  /* Dense at the bottom, torn at the top: a strip that sits ON TOP of the
     section below it and rips upward across whatever is above. */
  -webkit-mask-image:
    var(--fx-bg-tear-mask),
    linear-gradient(to top, #000 0%, rgba(0, 0, 0, 0.72) 38%, transparent 100%);
  mask-image:
    var(--fx-bg-tear-mask),
    linear-gradient(to top, #000 0%, rgba(0, 0, 0, 0.72) 38%, transparent 100%);
  -webkit-mask-size: 100% 100%, 100% 100%;
  mask-size: 100% 100%, 100% 100%;
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}

/* The mirrored cut: dense at the top, ragged at the bottom — for a surface
   ending above whatever comes next. The noise mask is direction-free; only
   the density gradient flips. */
:is(.fx-bg-torn-edge, [data-fx~="bg-torn-edge"])[data-fx-tear-edge="bottom"] {
  -webkit-mask-image:
    var(--fx-bg-tear-mask),
    linear-gradient(to bottom, #000 0%, rgba(0, 0, 0, 0.72) 38%, transparent 100%);
  mask-image:
    var(--fx-bg-tear-mask),
    linear-gradient(to bottom, #000 0%, rgba(0, 0, 0, 0.72) 38%, transparent 100%);
}

/* A second cut for variety: different seed, coarser horizontal grain, so two
   tears on one page never read as the same rip repeated. */
:is(.fx-bg-torn-edge, [data-fx~="bg-torn-edge"])[data-fx-tear-cut="rough"] {
  --fx-bg-tear-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='900' height='120'><filter id='t' x='0' y='0' width='100%25' height='100%25'><feTurbulence type='fractalNoise' baseFrequency='0.022 0.07' numOctaves='4' seed='4'/><feColorMatrix type='matrix' values='0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  2.1 0 0 0 -0.4'/></filter><rect width='900' height='120' filter='url(%23t)'/></svg>");
}

/* Demo scaffolding: a live ground above, a surface below, the tear between.
   Only the gallery preview uses these. */
.fx-bg-tear-demo { display: flex; flex-direction: column; justify-content: flex-end; padding: 0; }
.fx-bg-tear-demo .fx-bg-tear-demo-surface {
  position: relative;
  z-index: 1;
  background: var(--fx-surface);
  padding: 1.1rem 1.4rem 1.4rem;
}
.fx-bg-tear-demo :is(.fx-bg-torn-edge, [data-fx~="bg-torn-edge"]) {
  position: relative;
  z-index: 1;
  margin-bottom: -1px;   /* no seam line at the dense edge */
}

/* =========================================================================
 * bg-ascii-field — WebGL canvas (JS). The shader draws everything; CSS only
 * carries the opacity option and softens the box edges so the weave reads
 * as weather inside the section, not as a pasted rectangle.
 * ========================================================================= */
.fx-bg-ascii-field__layer {
  opacity: var(--fx-bg-opacity, 1);
  mask-image: radial-gradient(ellipse 92% 90% at 50% 46%, #000 55%, transparent 98%);
}

/* =========================================================================
 * Light theme: screen keeps highlights readable on dark, multiply on light.
 * ========================================================================= */
:root[data-fx-theme="light"] .fx-bg-aurora__blob,
:root[data-fx-theme="light"] .fx-bg-gradient-mesh__node,
:root[data-fx-theme="light"] :is(.fx-bg-grid,[data-fx~="bg-grid"])::after,
:root[data-fx-theme="light"] :is(.fx-bg-dots,[data-fx~="bg-dots"])::after,
:root[data-fx-theme="light"] .fx-bg-dot-spotlight__halo,
:root[data-fx-theme="light"] .fx-bg-spotlight-follow__orb {
  mix-blend-mode: multiply;
  opacity: 0.55;
}
:root[data-fx-theme="light"] :is(.fx-bg-noise,[data-fx~="bg-noise"]) { mix-blend-mode: multiply; opacity: 0.06; }

/* =========================================================================
 * Reduced motion — explicit static frames for the two loops that fade
 * through zero, where a paused frame could land on "invisible".
 * (Everything else freezes on a chosen frame via --fx-bg-play.)
 * ========================================================================= */
@media (prefers-reduced-motion: reduce) {
  :is(.fx-bg-scanlines,[data-fx~="bg-scanlines"])::before {
    animation: none;
    transform: translate3d(0, 92%, 0);
    opacity: 1;
  }
  /* !important is needed to beat the core safety net, which crushes every
     animation inside [data-fx] to 0.01ms and would leave a cell at opacity 0. */
  .fx-bg-grid-pulse__cell.is-lit { animation: none !important; opacity: 0.55; }
}
:root[data-fx-reduced] :is(.fx-bg-scanlines,[data-fx~="bg-scanlines"])::before {
  animation: none;
  transform: translate3d(0, 92%, 0);
  opacity: 1;
}
:root[data-fx-reduced] .fx-bg-grid-pulse__cell.is-lit { animation: none !important; opacity: 0.55; }

/* ══ src/effects/card.css ══ */
/* =========================================================================
 * effects-lab — Cards & bento  (card-*)
 * Multi-card / grid / card-structure behaviour: bento emphasis, shared
 * spotlights, pointer-angle borders, flipping, expanding, stacking.
 *
 * Every class starts .fx-card- and every keyframe starts fx-card-.
 * No hardcoded colours: everything derives from src/core/00-tokens.css.
 * ========================================================================= */

/* ---------------------------------------------------------------- shared -- */

.fx-card-demo {
  width: 100%;
  max-width: 520px;
  margin-inline: auto;
}

/* generic equal-width row for effects that need no grid of their own */
.fx-card-row {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}

/* The "tray": outer shell of the double-bezel. Cards sit inside it. */
.fx-card-tray {
  padding: 8px;
  border-radius: var(--fx-radius-lg);
  background: color-mix(in srgb, var(--fx-fg) 4%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fx-fg) 8%, transparent);
}

/* The "face": inner core. Concentric radius, own hairline + inset highlight. */
.fx-card-surface {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 12px 13px;
  border-radius: calc(var(--fx-radius-lg) - 8px);
  background: var(--fx-surface);
  color: var(--fx-fg);
  font: inherit;
  text-align: left;
  text-decoration: none;
  overflow: hidden;
  box-shadow:
    inset 0 0 0 1px color-mix(in srgb, var(--fx-fg) 9%, transparent),
    inset 0 1px 0 color-mix(in srgb, var(--fx-fg) 7%, transparent);
}

.fx-card-eyebrow {
  display: block;
  margin: 0;
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--fx-muted);
}

.fx-card-title {
  display: block;
  margin: 0;
  font-size: 14px;
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: -0.012em;
  color: var(--fx-fg);
}

.fx-card-sub {
  display: block;
  margin: 0;
  font-size: 11px;
  line-height: 1.4;
  color: var(--fx-muted);
}

.fx-card-figure {
  display: block;
  font-size: 22px;
  font-weight: 600;
  line-height: 1;
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
  color: var(--fx-fg);
}

.fx-card-hint {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin: 0 0 10px;
  padding: 3px 10px;
  border-radius: 999px;
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--fx-muted);
  background: color-mix(in srgb, var(--fx-fg) 5%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fx-fg) 8%, transparent);
}
.fx-card-hint::before {
  content: "";
  width: 5px;
  height: 5px;
  border-radius: 999px;
  background: var(--fx-accent);
}

/* Focus rings stay intact everywhere in this category. */
.fx-card-surface:focus-visible,
.fx-card-tray :focus-visible {
  outline: 2px solid var(--fx-accent);
  outline-offset: 2px;
}

/* ======================================================= card-bento-hover == */

.fx-card-bento {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-auto-rows: minmax(64px, 1fr);
  gap: 8px;
}

.fx-card-bento-cell {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 2px;
  min-width: 0;
  transform: scale(1);
  transition:
    transform 400ms var(--fx-ease),
    opacity 400ms var(--fx-ease),
    filter 400ms var(--fx-ease),
    box-shadow 400ms var(--fx-ease);
  will-change: transform;
}

.fx-card-bento-cell.is-wide { grid-column: span 2; }
.fx-card-bento-cell.is-tallcell { grid-row: span 2; }

/* Neighbours recede while one cell is engaged. */
.fx-card-bento.is-engaged .fx-card-bento-cell {
  opacity: var(--fx-card-dim, 0.45);
  transform: scale(0.982);
  filter: saturate(0.6);
}

.fx-card-bento .fx-card-bento-cell.is-active {
  opacity: 1;
  filter: none;
  transform: scale(var(--fx-card-grow, 1.03));
  z-index: 2;
  transition-duration: 200ms;
  box-shadow:
    inset 0 0 0 1px color-mix(in srgb, var(--fx-accent) 42%, transparent),
    inset 0 1px 0 color-mix(in srgb, var(--fx-fg) 10%, transparent),
    0 14px 34px -18px color-mix(in srgb, var(--fx-accent) 70%, transparent);
}

.fx-card-bento-cell .fx-card-figure {
  transition: color 200ms var(--fx-ease);
}
.fx-card-bento-cell.is-active .fx-card-figure { color: var(--fx-accent); }

@media (prefers-reduced-motion: reduce) {
  .fx-card-bento-cell,
  .fx-card-bento.is-engaged .fx-card-bento-cell,
  .fx-card-bento .fx-card-bento-cell.is-active {
    transform: none;
    filter: none;
    transition: opacity 120ms linear, box-shadow 120ms linear;
  }
  .fx-card-bento.is-engaged .fx-card-bento-cell { opacity: 0.7; }
}
:root[data-fx-reduced] .fx-card-bento-cell,
:root[data-fx-reduced] .fx-card-bento.is-engaged .fx-card-bento-cell,
:root[data-fx-reduced] .fx-card-bento .fx-card-bento-cell.is-active {
  transform: none;
  filter: none;
  transition: opacity 120ms linear, box-shadow 120ms linear;
}
:root[data-fx-reduced] .fx-card-bento.is-engaged .fx-card-bento-cell { opacity: 0.7; }

/* ===================================================== card-spotlight-grid = */

.fx-card-spot {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}

.fx-card-spot-item {
  --fx-card-sx: 50%;
  --fx-card-sy: 0%;
  isolation: isolate;
  min-width: 0;
}

/* the wash inside the card */
.fx-card-spot-item::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  background: radial-gradient(
    var(--fx-card-spot-r, 240px) circle at var(--fx-card-sx) var(--fx-card-sy),
    color-mix(in srgb, var(--fx-accent) 26%, transparent),
    transparent 68%
  );
  opacity: 0;
  transition: opacity 340ms var(--fx-ease);
}

/* the lit hairline */
.fx-card-spot-item::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1px;
  background: radial-gradient(
    calc(var(--fx-card-spot-r, 240px) * 0.75) circle at var(--fx-card-sx) var(--fx-card-sy),
    var(--fx-accent),
    transparent 62%
  );
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  opacity: 0;
  pointer-events: none;
  transition: opacity 340ms var(--fx-ease);
}

.fx-card-spot.is-lit .fx-card-spot-item::before,
.fx-card-spot.is-lit .fx-card-spot-item::after,
.fx-card-spot .fx-card-spot-item.is-lit::before,
.fx-card-spot .fx-card-spot-item.is-lit::after {
  opacity: 1;
  transition-duration: 180ms;
}

/* touch + reduced: one calm static light, no tracking */
.fx-card-spot.is-static .fx-card-spot-item::before,
.fx-card-spot.is-static .fx-card-spot-item::after {
  opacity: 0.55;
  transition: none;
}

@media (prefers-reduced-motion: reduce) {
  .fx-card-spot-item::before,
  .fx-card-spot-item::after { transition: opacity 120ms linear; }
}
:root[data-fx-reduced] .fx-card-spot-item::before,
:root[data-fx-reduced] .fx-card-spot-item::after { transition: opacity 120ms linear; }

/* ======================================================= card-glow-border == */

.fx-card-glow {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}

.fx-card-glow-item {
  --fx-card-ang: 0deg;
  --fx-card-lit: 0;
  min-width: 0;
}

.fx-card-glow-item::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1.5px;
  background: conic-gradient(
    from var(--fx-card-ang) at 50% 50%,
    var(--fx-card-accent, var(--fx-accent)),
    color-mix(in srgb, var(--fx-accent-2) 70%, transparent) 9%,
    transparent 22%,
    transparent 78%,
    color-mix(in srgb, var(--fx-accent-2) 70%, transparent) 91%,
    var(--fx-card-accent, var(--fx-accent))
  );
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  opacity: var(--fx-card-lit, 0);
  pointer-events: none;
  transition: opacity 260ms var(--fx-ease);
}

.fx-card-glow.is-static .fx-card-glow-item::after,
.fx-card-glow-item.is-lit::after {
  opacity: 0.75;
}

@media (prefers-reduced-motion: reduce) {
  .fx-card-glow-item::after { transition: opacity 120ms linear; }
}
:root[data-fx-reduced] .fx-card-glow-item::after { transition: opacity 120ms linear; }

/* ================================================================ card-3d == */

.fx-card-3d {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
  perspective: 900px;
}

.fx-card-3d-item {
  --fx-card-rx: 0deg;
  --fx-card-ry: 0deg;
  --fx-card-gx: 50%;
  --fx-card-gy: 0%;
  min-width: 0;
  min-height: 130px;
  justify-content: flex-end;
  transform-style: preserve-3d;
  transform: rotateX(var(--fx-card-rx)) rotateY(var(--fx-card-ry));
  transition: transform 480ms var(--fx-ease), box-shadow 480ms var(--fx-ease);
  will-change: transform;
}

.fx-card-3d-item.is-active {
  transition-duration: 180ms;
  box-shadow:
    inset 0 0 0 1px color-mix(in srgb, var(--fx-fg) 14%, transparent),
    0 22px 40px -26px hsl(0 0% 0% / 0.85);
}

/* parallax layers: real z-depth, the perspective does the work */
.fx-card-3d-layer {
  transform: translateZ(var(--fx-card-z, 0px));
  transition: transform 480ms var(--fx-ease);
}
.fx-card-3d-item.is-active .fx-card-3d-layer { transition-duration: 180ms; }
.fx-card-3d-layer.is-near { --fx-card-z: 0px; }
.fx-card-3d-item.is-active .fx-card-3d-layer.is-near { --fx-card-z: 34px; }
.fx-card-3d-layer.is-mid { --fx-card-z: 0px; }
.fx-card-3d-item.is-active .fx-card-3d-layer.is-mid { --fx-card-z: 16px; }

/* glare sheen */
.fx-card-3d-item::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: radial-gradient(
    64px circle at var(--fx-card-gx) var(--fx-card-gy),
    color-mix(in srgb, var(--fx-fg) 20%, transparent),
    color-mix(in srgb, var(--fx-fg) 5%, transparent) 46%,
    transparent 100%
  );
  opacity: 0;
  pointer-events: none;
  mix-blend-mode: screen;
  transition: opacity 320ms var(--fx-ease);
}
.fx-card-3d-item.is-active::after { opacity: 1; transition-duration: 160ms; }

.fx-card-3d.is-static .fx-card-3d-item,
.fx-card-3d.is-static .fx-card-3d-layer { transform: none; transition: none; }

@media (prefers-reduced-motion: reduce) {
  .fx-card-3d-item,
  .fx-card-3d-item.is-active,
  .fx-card-3d-layer { transform: none; transition: box-shadow 120ms linear; }
  .fx-card-3d-item::after { display: none; }
}
:root[data-fx-reduced] .fx-card-3d-item,
:root[data-fx-reduced] .fx-card-3d-item.is-active,
:root[data-fx-reduced] .fx-card-3d-layer { transform: none; transition: box-shadow 120ms linear; }
:root[data-fx-reduced] .fx-card-3d-item::after { display: none; }

/* ==================================================== card-reveal-content == */

.fx-card-reveal-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}

.fx-card-reveal {
  min-height: 132px;
  justify-content: flex-end;
  min-width: 0;
}

.fx-card-reveal-body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  transform: translateY(0);
  transition: transform 380ms var(--fx-ease);
}

.fx-card-reveal-more {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 9px 13px 11px;
  background: color-mix(in srgb, var(--fx-surface-2) 92%, var(--fx-accent));
  box-shadow: inset 0 1px 0 color-mix(in srgb, var(--fx-fg) 12%, transparent);
  transform: translateY(101%);
  transition: transform 380ms var(--fx-ease);
  will-change: transform;
}

.fx-card-reveal-more .fx-card-sub { color: var(--fx-fg); }

.fx-card-reveal-cta {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 999px;
  font-size: 11px;
  color: var(--fx-accent-contrast);
  background: var(--fx-accent);
  transition: transform 200ms var(--fx-ease);
}

@media (hover: hover) and (pointer: fine) {
  .fx-card-reveal:hover .fx-card-reveal-more { transform: translateY(0); transition-duration: 210ms; }
  .fx-card-reveal:hover .fx-card-reveal-body { transform: translateY(-14px); transition-duration: 210ms; }
  .fx-card-reveal:hover .fx-card-reveal-cta { transform: translate(2px, -1px) scale(1.06); }
}

/* keyboard mirrors hover for links/buttons inside the card */
.fx-card-reveal:focus-within .fx-card-reveal-more { transform: translateY(0); transition-duration: 210ms; }
.fx-card-reveal:focus-within .fx-card-reveal-body { transform: translateY(-14px); transition-duration: 210ms; }

/* touch: the panel is simply always there */
@media (hover: none) {
  .fx-card-reveal-more { position: static; transform: none; padding-inline: 0; background: none; box-shadow: none; margin-top: 8px; }
}

@media (prefers-reduced-motion: reduce) {
  .fx-card-reveal-body,
  .fx-card-reveal-more,
  .fx-card-reveal-cta { transition: none; }
  .fx-card-reveal:hover .fx-card-reveal-body,
  .fx-card-reveal:focus-within .fx-card-reveal-body { transform: none; }
}
:root[data-fx-reduced] .fx-card-reveal-body,
:root[data-fx-reduced] .fx-card-reveal-more,
:root[data-fx-reduced] .fx-card-reveal-cta { transition: none; }
:root[data-fx-reduced] .fx-card-reveal:hover .fx-card-reveal-body,
:root[data-fx-reduced] .fx-card-reveal:focus-within .fx-card-reveal-body { transform: none; }

/* =========================================================== card-expand == */

.fx-card-expand {
  position: relative;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  grid-auto-rows: 1fr;
  gap: 8px;
  min-height: 210px;
}

.fx-card-expand-item {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 3px;
  min-width: 0;
  border: 0;
  cursor: pointer;
  transition: opacity 320ms var(--fx-ease), transform 320ms var(--fx-ease);
}

.fx-card-expand.is-open .fx-card-expand-item:not(.is-expanded) {
  opacity: 0.3;
  transform: scale(0.985);
  transition-duration: 200ms;
}

.fx-card-expand-item.is-animating { z-index: 4; }

.fx-card-expand-item.is-expanded {
  position: absolute;
  inset: 0;
  z-index: 5;
  justify-content: flex-start;
  opacity: 1;
  box-shadow:
    inset 0 0 0 1px color-mix(in srgb, var(--fx-accent) 34%, transparent),
    0 30px 60px -30px hsl(0 0% 0% / 0.8);
}

.fx-card-expand-inner {
  display: flex;
  flex-direction: column;
  gap: 4px;
  width: 100%;
}

.fx-card-expand-detail {
  display: none;
  margin-top: 6px;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 240ms var(--fx-ease-out) 90ms, transform 240ms var(--fx-ease-out) 90ms;
}
.fx-card-expand-item.is-expanded .fx-card-expand-detail { display: block; }
.fx-card-expand-item.is-expanded.is-shown .fx-card-expand-detail { opacity: 1; transform: none; }

.fx-card-expand-close {
  position: absolute;
  top: 10px;
  right: 10px;
  display: none;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 999px;
  font-size: 12px;
  line-height: 1;
  color: var(--fx-muted);
  background: color-mix(in srgb, var(--fx-fg) 8%, transparent);
}
.fx-card-expand-item.is-expanded .fx-card-expand-close { display: inline-flex; }

@media (prefers-reduced-motion: reduce) {
  .fx-card-expand-item,
  .fx-card-expand-detail { transition: opacity 120ms linear; transform: none; }
  .fx-card-expand.is-open .fx-card-expand-item:not(.is-expanded) { transform: none; }
  .fx-card-expand-item.is-expanded .fx-card-expand-detail { opacity: 1; transform: none; }
}
:root[data-fx-reduced] .fx-card-expand-item,
:root[data-fx-reduced] .fx-card-expand-detail { transition: opacity 120ms linear; transform: none; }
:root[data-fx-reduced] .fx-card-expand.is-open .fx-card-expand-item:not(.is-expanded) { transform: none; }
:root[data-fx-reduced] .fx-card-expand-item.is-expanded .fx-card-expand-detail { opacity: 1; transform: none; }

/* ============================================================ card-stack == */

.fx-card-stack {
  position: relative;
  width: 100%;
  max-width: 320px;
  min-height: 168px;
  margin-inline: auto;
  padding-bottom: 28px;
  cursor: pointer;
}
.fx-card-stack:focus-visible {
  outline: 2px solid var(--fx-accent);
  outline-offset: 6px;
  border-radius: var(--fx-radius-lg);
}

.fx-card-stack-item {
  position: absolute;
  inset: 0 0 28px;
  justify-content: space-between;
  transform-origin: 50% 50%;
  will-change: transform;
  backface-visibility: hidden;
}

.fx-card-stack-item.is-top {
  box-shadow:
    inset 0 0 0 1px color-mix(in srgb, var(--fx-accent) 30%, transparent),
    0 22px 44px -26px hsl(0 0% 0% / 0.8);
}

.fx-card-stack-count {
  position: absolute;
  left: 50%;
  bottom: 0;
  transform: translateX(-50%);
  font-size: 9px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--fx-muted);
}

/* =========================================================== card-flip ==== */

.fx-card-flip {
  position: relative;
  display: block;
  width: 100%;
  min-height: 150px;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  perspective: 1000px;
  font: inherit;
  color: inherit;
  text-align: left;
}
.fx-card-flip:focus-visible {
  outline: 2px solid var(--fx-accent);
  outline-offset: 4px;
  border-radius: var(--fx-radius-lg);
}

.fx-card-flip-inner {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  transform: rotateY(0deg);
  transition: transform 440ms var(--fx-ease);
  will-change: transform;
}
.fx-card-flip.is-flipped .fx-card-flip-inner { transform: rotateY(180deg); transition-duration: 380ms; }
.fx-card-flip.is-axis-x .fx-card-flip-inner { transform: rotateX(0deg); }
.fx-card-flip.is-axis-x.is-flipped .fx-card-flip-inner { transform: rotateX(180deg); }

.fx-card-flip-face {
  position: absolute;
  inset: 0;
  justify-content: space-between;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}
.fx-card-flip-face.is-back { transform: rotateY(180deg); }
.fx-card-flip.is-axis-x .fx-card-flip-face.is-back { transform: rotateX(180deg); }
.fx-card-flip-face.is-back {
  background: color-mix(in srgb, var(--fx-surface-2) 80%, var(--fx-accent));
}

/* reduced motion + no-3d fallback: a 120ms crossfade, no rotation */
.fx-card-flip.is-crossfade .fx-card-flip-inner { transform: none !important; transition: none; }
.fx-card-flip.is-crossfade .fx-card-flip-face {
  backface-visibility: visible;
  -webkit-backface-visibility: visible;
  transform: none !important;
  transition: opacity 120ms linear;
}
.fx-card-flip.is-crossfade .fx-card-flip-face.is-back { opacity: 0; }
.fx-card-flip.is-crossfade.is-flipped .fx-card-flip-face.is-front { opacity: 0; }
.fx-card-flip.is-crossfade.is-flipped .fx-card-flip-face.is-back { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .fx-card-flip-inner { transition: none; }
}
:root[data-fx-reduced] .fx-card-flip-inner { transition: none; }

/* ============================================================= card-peel == */

.fx-card-peel {
  position: relative;
  min-width: 0;
}

.fx-card-peel-face {
  clip-path: polygon(0 0, 100% 0, 100% calc(100% - 0px), calc(100% - 0px) 100%, 0 100%);
  transition: clip-path 380ms var(--fx-ease);
  min-height: 118px;
  justify-content: flex-end;
}

/* what the lifted corner exposes */
.fx-card-peel-under {
  position: absolute;
  right: 0;
  bottom: 0;
  display: grid;
  align-items: end;
  justify-items: end;
  width: 52px;
  height: 52px;
  padding: 0 8px 6px 0;
  border-bottom-right-radius: calc(var(--fx-radius-lg) - 8px);
  font-size: 11px;
  line-height: 1;
  color: var(--fx-accent-contrast);
  background: var(--fx-accent);
  opacity: 0;
  transition: opacity 380ms var(--fx-ease);
  pointer-events: none;
}

/* the flap itself: the folded-back sheet, lying on top of the card */
.fx-card-peel-flap {
  position: absolute;
  right: 0;
  bottom: 0;
  width: 52px;
  height: 52px;
  background: linear-gradient(
    to top left,
    color-mix(in srgb, var(--fx-surface-2) 62%, var(--fx-fg)) 0%,
    var(--fx-surface-2) 38%,
    var(--fx-surface) 100%
  );
  clip-path: polygon(0 0, 100% 0, 0 100%);
  transform: scale(0);
  transform-origin: 50% 50%;
  filter: drop-shadow(-5px -5px 11px hsl(0 0% 0% / 0.55));
  transition: transform 380ms var(--fx-ease);
  pointer-events: none;
  will-change: transform;
}

@media (hover: hover) and (pointer: fine) {
  .fx-card-peel:hover .fx-card-peel-face,
  .fx-card-peel:focus-within .fx-card-peel-face {
    clip-path: polygon(0 0, 100% 0, 100% calc(100% - 52px), calc(100% - 52px) 100%, 0 100%);
    transition-duration: 220ms;
  }
  .fx-card-peel:hover .fx-card-peel-flap,
  .fx-card-peel:focus-within .fx-card-peel-flap { transform: scale(1); transition-duration: 220ms; }
  .fx-card-peel:hover .fx-card-peel-under,
  .fx-card-peel:focus-within .fx-card-peel-under { opacity: 1; transition-duration: 220ms; }
}

/* touch: a small permanent dog-ear so the affordance still reads */
@media (hover: none) {
  .fx-card-peel-face { clip-path: polygon(0 0, 100% 0, 100% calc(100% - 20px), calc(100% - 20px) 100%, 0 100%); }
  .fx-card-peel-flap { width: 20px; height: 20px; transform: scale(1); filter: none; }
  .fx-card-peel-under { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .fx-card-peel-face,
  .fx-card-peel-flap,
  .fx-card-peel-under { transition: opacity 120ms linear; }
  .fx-card-peel:hover .fx-card-peel-face,
  .fx-card-peel:focus-within .fx-card-peel-face { transition: none; }
}
:root[data-fx-reduced] .fx-card-peel-face,
:root[data-fx-reduced] .fx-card-peel-flap,
:root[data-fx-reduced] .fx-card-peel-under { transition: opacity 120ms linear; }

/* ====================================================== card-shine-sweep == */

.fx-card-shine {
  position: relative;
  overflow: hidden;
  min-width: 0;
  transform: translateY(0);
  transition: transform 340ms var(--fx-ease), box-shadow 340ms var(--fx-ease);
}

.fx-card-shine::after {
  content: "";
  position: absolute;
  top: -30%;
  bottom: -30%;
  left: 0;
  width: 42%;
  background: linear-gradient(
    100deg,
    transparent 0%,
    color-mix(in srgb, var(--fx-fg) 20%, transparent) 42%,
    color-mix(in srgb, var(--fx-fg) 34%, transparent) 50%,
    color-mix(in srgb, var(--fx-fg) 20%, transparent) 58%,
    transparent 100%
  );
  transform: translateX(-160%) skewX(-14deg);
  opacity: 0;
  pointer-events: none;
  /* at rest only opacity animates — the band snaps back off-stage invisibly */
  transition: opacity 90ms linear;
  will-change: transform;
}

@media (hover: hover) and (pointer: fine) {
  .fx-card-shine:hover { transform: translateY(-3px); }
  .fx-card-shine:hover::after,
  .fx-card-shine:focus-within::after {
    transform: translateX(300%) skewX(-14deg);
    opacity: 1;
    transition: transform 720ms var(--fx-ease-out), opacity 120ms linear;
  }
}
.fx-card-shine:focus-within::after {
  transform: translateX(300%) skewX(-14deg);
  opacity: 1;
  transition: transform 720ms var(--fx-ease-out), opacity 120ms linear;
}

@media (prefers-reduced-motion: reduce) {
  .fx-card-shine,
  .fx-card-shine:hover { transform: none; transition: box-shadow 120ms linear; }
  .fx-card-shine::after,
  .fx-card-shine:hover::after,
  .fx-card-shine:focus-within::after { display: none; }
}
:root[data-fx-reduced] .fx-card-shine,
:root[data-fx-reduced] .fx-card-shine:hover { transform: none; transition: box-shadow 120ms linear; }
:root[data-fx-reduced] .fx-card-shine::after,
:root[data-fx-reduced] .fx-card-shine:hover::after,
:root[data-fx-reduced] .fx-card-shine:focus-within::after { display: none; }

/* ======================================================== card-lift-group = */

.fx-card-lift {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}

.fx-card-lift-item {
  min-width: 0;
  justify-content: flex-end;
  transform: translateY(0) scale(1);
  transition:
    transform 340ms var(--fx-ease),
    opacity 340ms var(--fx-ease),
    filter 340ms var(--fx-ease),
    box-shadow 340ms var(--fx-ease);
  will-change: transform;
}

@media (hover: hover) and (pointer: fine) {
  .fx-card-lift:has(.fx-card-lift-item:hover) .fx-card-lift-item:not(:hover) {
    opacity: 0.4;
    filter: saturate(0.55);
    transform: scale(0.97);
    transition-duration: 190ms;
  }
  .fx-card-lift-item:hover {
    transform: translateY(-5px) scale(1.012);
    transition-duration: 190ms;
    box-shadow:
      inset 0 0 0 1px color-mix(in srgb, var(--fx-accent) 38%, transparent),
      0 18px 34px -22px hsl(0 0% 0% / 0.85);
  }
}

.fx-card-lift:has(.fx-card-lift-item:focus-visible) .fx-card-lift-item:not(:focus-visible) {
  opacity: 0.4;
  filter: saturate(0.55);
  transform: scale(0.97);
  transition-duration: 190ms;
}
.fx-card-lift-item:focus-visible {
  transform: translateY(-5px) scale(1.012);
  transition-duration: 190ms;
}

@media (prefers-reduced-motion: reduce) {
  .fx-card-lift-item,
  .fx-card-lift-item:hover,
  .fx-card-lift-item:focus-visible,
  .fx-card-lift:has(.fx-card-lift-item:hover) .fx-card-lift-item:not(:hover),
  .fx-card-lift:has(.fx-card-lift-item:focus-visible) .fx-card-lift-item:not(:focus-visible) {
    transform: none;
    filter: none;
    transition: opacity 120ms linear, box-shadow 120ms linear;
  }
}
:root[data-fx-reduced] .fx-card-lift-item,
:root[data-fx-reduced] .fx-card-lift-item:hover,
:root[data-fx-reduced] .fx-card-lift-item:focus-visible,
:root[data-fx-reduced] .fx-card-lift:has(.fx-card-lift-item:hover) .fx-card-lift-item:not(:hover),
:root[data-fx-reduced] .fx-card-lift:has(.fx-card-lift-item:focus-visible) .fx-card-lift-item:not(:focus-visible) {
  transform: none;
  filter: none;
  transition: opacity 120ms linear, box-shadow 120ms linear;
}

/* ==================================================== card-proximity-grid = */

.fx-card-prox {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}

.fx-card-prox-item {
  --fx-card-p: 0;
  min-width: 0;
  justify-content: flex-end;
  transform: translateY(0) scale(1);
  transition: transform 420ms var(--fx-ease), box-shadow 420ms var(--fx-ease);
  will-change: transform;
}

.fx-card-prox.is-tracking .fx-card-prox-item {
  transition: transform 130ms linear, box-shadow 220ms var(--fx-ease);
}

.fx-card-prox-item::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow: inset 0 0 0 1px var(--fx-accent);
  opacity: calc(var(--fx-card-p, 0) * 0.55);
  pointer-events: none;
  transition: opacity 220ms var(--fx-ease);
}

.fx-card-prox-item .fx-card-eyebrow {
  color: color-mix(in srgb, var(--fx-accent) calc(var(--fx-card-p, 0) * 100%), var(--fx-muted));
  transition: color 220ms var(--fx-ease);
}

@media (prefers-reduced-motion: reduce) {
  .fx-card-prox-item,
  .fx-card-prox.is-tracking .fx-card-prox-item { transform: none !important; transition: box-shadow 120ms linear; }
}
:root[data-fx-reduced] .fx-card-prox-item,
:root[data-fx-reduced] .fx-card-prox.is-tracking .fx-card-prox-item { transform: none !important; transition: box-shadow 120ms linear; }

/* ------------------------------------------------- small-stage guardrails - */
@media (max-width: 520px) {
  .fx-card-bento,
  .fx-card-spot,
  .fx-card-glow,
  .fx-card-3d,
  .fx-card-lift,
  .fx-card-prox,
  .fx-card-reveal-grid { grid-template-columns: repeat(2, 1fr); }
  .fx-card-bento-cell.is-wide { grid-column: span 2; }
}

/* =========================================================================
 * card-expand-carousel — "Widening card carousel"
 *
 * The JS owns every animating number: each card's real width, the track's
 * translate, the per-card weight --fx-card-expcar-u (0 = idle, 1 = active)
 * and the viewport's background tint. There are DELIBERATELY no CSS
 * transitions on width, transform or the headline: a transition would fight
 * the rAF loop and make an interrupted advance snap. The only transitions
 * here are the ones the loop does not touch.
 * ========================================================================= */

.fx-card-expcar {
  --fx-card-expcar-h: 406px;
  --fx-card-expcar-artw: 610px;
  --fx-card-expcar-gap: 16px;
  --fx-card-expcar-pad: 24px;
  --fx-card-expcar-meta-fs: 13px;
  --fx-card-expcar-pi: 21.5;   /* headline line pitch, idle   (unitless px) */
  --fx-card-expcar-pa: 34;     /* headline line pitch, active (unitless px) */
  display: block;
  width: 100%;
}

.fx-card-expcar__viewport {
  position: relative;
  overflow: hidden;
  width: 100%;
  height: var(--fx-card-expcar-h);
  border-radius: var(--fx-radius-lg);
  background-color: var(--fx-surface-2);
  box-shadow: inset 0 0 0 1px var(--fx-border);
  touch-action: pan-y;          /* vertical page scroll stays the page's */
  cursor: grab;
}
.fx-card-expcar.is-dragging .fx-card-expcar__viewport { cursor: grabbing; }

.fx-card-expcar__track {
  display: flex;
  align-items: stretch;
  gap: var(--fx-card-expcar-gap);
  height: 100%;
  width: max-content;
  will-change: transform;
}

/* --------------------------------------------------------------- a card -- */
/* A clipping window, nothing more. Bottom-aligned flow so the headline grows
   UPWARD from a fixed baseline while the meta row stays exactly where it is. */
.fx-card-expcar__card {
  --fx-card-expcar-u: 0;
  position: relative;
  flex: 0 0 auto;
  height: 100%;
  min-width: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  align-items: flex-start;
  gap: calc(var(--fx-card-expcar-pad) * 0.28);
  padding: var(--fx-card-expcar-pad);
  margin: 0;
  border: 0;
  border-radius: calc(var(--fx-radius-lg) - 6px);
  background: var(--fx-card-expcar-tint, var(--fx-surface-2));
  /* Text sits on photography, so it is white and the scrim is neutral black —
     both are hue-independent and stay correct under any --fx-hue or theme. */
  color: hsl(0 0% 100%);
  text-align: left;
  font: inherit;
  cursor: pointer;
  -webkit-appearance: none;
          appearance: none;
  -webkit-tap-highlight-color: transparent;
}

/* The picture: sized by the card's FIXED HEIGHT and horizontally centred.
   Its box never changes and it is never transformed by the animation — the
   card simply reveals more of the same picture, symmetrically, as it widens.
   translateX(-50%) is a constant centring offset, not an animated value. */
.fx-card-expcar__art {
  position: absolute;
  top: 0;
  left: 50%;
  z-index: 0;
  width: var(--fx-card-expcar-artw);
  min-width: var(--fx-card-expcar-artw);
  max-width: none;
  height: var(--fx-card-expcar-h);
  object-fit: cover;
  transform: translateX(-50%);
  pointer-events: none;
  user-select: none;
}

/* Legibility scrim. Neutral, and a touch heavier once the card is open. */
.fx-card-expcar__card::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background-image: linear-gradient(
    to top,
    hsl(0 0% 0% / calc(0.56 + 0.22 * var(--fx-card-expcar-u))) 0%,
    hsl(0 0% 0% / 0.3) 44%,
    hsl(0 0% 0% / 0.06) 78%
  );
  pointer-events: none;
}

.fx-card-expcar__title,
.fx-card-expcar__meta {
  position: relative;
  z-index: 2;
  margin: 0;
}

/* One clock, one curve: the pitch is a straight read of --fx-card-expcar-u.
   Line breaks are hard-coded in the markup and the text never re-wraps, so
   the block changes size without a single word moving between lines. */
.fx-card-expcar__title {
  font-size: calc((var(--fx-card-expcar-pi) + (var(--fx-card-expcar-pa) - var(--fx-card-expcar-pi)) * var(--fx-card-expcar-u)) * 0.78px);
  line-height: calc((var(--fx-card-expcar-pi) + (var(--fx-card-expcar-pa) - var(--fx-card-expcar-pi)) * var(--fx-card-expcar-u)) * 1px);
  font-weight: 620;
  letter-spacing: calc(-0.004em - 0.014em * var(--fx-card-expcar-u)); /* tighter as it grows */
  white-space: nowrap;
  text-wrap: nowrap;
  text-shadow: 0 1px 12px hsl(0 0% 0% / 0.45);
}

/* Lags a little behind the headline, and fades only — it must never move. */
.fx-card-expcar__meta {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: var(--fx-card-expcar-meta-fs);
  line-height: 1.35;
  font-weight: 500;
  white-space: nowrap;
  color: hsl(0 0% 100% / 0.82);
  opacity: clamp(0, calc((var(--fx-card-expcar-u) - 0.32) / 0.58), 1);
}
.fx-card-expcar__meta > * { margin: 0; }
.fx-card-expcar__meta > * + *::before {
  content: "";
  display: inline-block;
  width: 3px; height: 3px;
  margin-right: 8px;
  vertical-align: 0.24em;
  border-radius: 50%;
  background: currentColor;
  opacity: 0.6;
}

.fx-card-expcar__card:focus-visible {
  outline: 2px solid var(--fx-accent);
  outline-offset: -4px;
}

/* --------------------------------------------------- generated demo art -- */
/* Token-derived gradients plus a geometric motif — no image files, no
   network. Real usage puts an <img> here instead. */
.fx-card-expcar-art {
  --fx-card-expcar-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.86' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23g)'/%3E%3C/svg%3E");
  display: block;
  overflow: hidden;
  background-color: var(--fx-surface-2);
  background-image:
    radial-gradient(88% 120% at 16% 6%, var(--fx-accent) 0%, color-mix(in oklab, var(--fx-accent) 28%, transparent) 40%, transparent 72%),
    radial-gradient(76% 92% at 86% 20%, var(--fx-accent-2) 0%, color-mix(in oklab, var(--fx-accent-2) 22%, transparent) 38%, transparent 70%),
    linear-gradient(196deg, var(--fx-surface-2) 0%, var(--fx-surface) 100%);
}
.fx-card-expcar-art::before {
  content: "";
  position: absolute;
  inset: 0;
  opacity: 0.42;
  background-image: repeating-linear-gradient(112deg, color-mix(in oklab, var(--fx-fg) 16%, transparent) 0 1px, transparent 1px 14px);
  -webkit-mask-image: radial-gradient(120% 100% at 22% 0%, #000 8%, transparent 74%);
          mask-image: radial-gradient(120% 100% at 22% 0%, #000 8%, transparent 74%);
}
.fx-card-expcar-art::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image: var(--fx-card-expcar-grain);
  background-size: 160px 160px;
  opacity: 0.15;
  mix-blend-mode: overlay;
}
.fx-card-expcar-art[data-ph="b"] {
  background-image:
    radial-gradient(72% 92% at 80% 14%, var(--fx-accent-2) 0%, color-mix(in oklab, var(--fx-accent-2) 26%, transparent) 36%, transparent 68%),
    radial-gradient(100% 110% at 8% 90%, var(--fx-accent-3) 0%, color-mix(in oklab, var(--fx-accent-3) 24%, transparent) 44%, transparent 76%),
    linear-gradient(158deg, var(--fx-surface) 0%, var(--fx-surface-2) 100%);
}
.fx-card-expcar-art[data-ph="b"]::before {
  opacity: 0.5;
  background-image: repeating-radial-gradient(circle at 70% 30%, transparent 0 18px, color-mix(in oklab, var(--fx-fg) 20%, transparent) 18px 19px);
  -webkit-mask-image: radial-gradient(90% 90% at 70% 30%, #000 0%, transparent 82%);
          mask-image: radial-gradient(90% 90% at 70% 30%, #000 0%, transparent 82%);
}
.fx-card-expcar-art[data-ph="c"] {
  background-image:
    radial-gradient(92% 120% at 48% 0%, var(--fx-accent-3) 0%, color-mix(in oklab, var(--fx-accent-3) 24%, transparent) 40%, transparent 74%),
    linear-gradient(22deg, var(--fx-surface-2) 0%, var(--fx-surface) 58%, var(--fx-surface-2) 100%);
}
.fx-card-expcar-art[data-ph="c"]::before {
  opacity: 0.46;
  background-image: repeating-linear-gradient(64deg, color-mix(in oklab, var(--fx-accent) 32%, transparent) 0 6px, transparent 6px 32px);
  -webkit-mask-image: linear-gradient(64deg, #000 0%, transparent 86%);
          mask-image: linear-gradient(64deg, #000 0%, transparent 86%);
}
.fx-card-expcar-art[data-ph="d"] {
  background-image:
    radial-gradient(80% 100% at 90% 94%, var(--fx-accent) 0%, color-mix(in oklab, var(--fx-accent) 24%, transparent) 40%, transparent 72%),
    linear-gradient(302deg, var(--fx-surface) 0%, var(--fx-surface-2) 100%);
}
.fx-card-expcar-art[data-ph="d"]::before {
  opacity: 0.55;
  background-image: radial-gradient(color-mix(in oklab, var(--fx-fg) 24%, transparent) 1.2px, transparent 1.3px);
  background-size: 15px 15px;
  -webkit-mask-image: linear-gradient(212deg, #000 0%, transparent 76%);
          mask-image: linear-gradient(212deg, #000 0%, transparent 76%);
}
.fx-card-expcar-art[data-ph="e"] {
  background-image:
    radial-gradient(90% 110% at 30% 100%, var(--fx-accent-2) 0%, color-mix(in oklab, var(--fx-accent-2) 26%, transparent) 42%, transparent 74%),
    linear-gradient(120deg, var(--fx-surface-2) 0%, var(--fx-surface) 100%);
}
.fx-card-expcar-art[data-ph="e"]::before {
  opacity: 0.5;
  background-image: repeating-linear-gradient(0deg, color-mix(in oklab, var(--fx-fg) 18%, transparent) 0 1px, transparent 1px 11px);
  -webkit-mask-image: radial-gradient(100% 90% at 50% 100%, #000 0%, transparent 80%);
          mask-image: radial-gradient(100% 90% at 50% 100%, #000 0%, transparent 80%);
}

/* Demo chrome only. The width is definite on purpose: the track's intrinsic
   size is the whole row, so an auto-sized parent would otherwise stretch to
   it and the preview would sit off-centre. */
.fx-card-expcar-demo {
  display: block;
  width: 516px;
  max-width: 100%;
  margin-inline: auto;
}

.fx-card-expcar-hint {
  margin: 10px 0 0;
  font-size: 12px;
  line-height: 1.4;
  color: var(--fx-muted);
  text-align: center;
}

/* ------------------------------------------------------- reduced motion -- */
/* No width tween, no translate animation, no blur and no headline scaling:
   the JS runs with duration 0, so the state simply swaps. The only thing
   left moving is the meta row, and only its opacity, capped at 120ms.
   Every card stays readable and the carousel stays fully usable. */
@media (prefers-reduced-motion: reduce) {
  .fx-card-expcar__card { filter: none !important; }
  .fx-card-expcar__meta { transition: opacity 120ms linear; }
  .fx-card-expcar__viewport { transition: background-color 120ms linear; }
}
:root[data-fx-reduced] .fx-card-expcar__card { filter: none !important; }
:root[data-fx-reduced] .fx-card-expcar__meta { transition: opacity 120ms linear; }
:root[data-fx-reduced] .fx-card-expcar__viewport { transition: background-color 120ms linear; }

/* ══ src/effects/count.css ══ */
/* =========================================================================
 * effects-lab — Numbers & data  (category slug: "count")
 *
 * Every colour comes from the tokens in src/core/00-tokens.css. Every class
 * and keyframe is namespaced fx-count-*.
 *
 * The shared rule of this category: a number must never move the layout while
 * it changes. `font-variant-numeric: tabular-nums` fixes the glyph advance,
 * `display: inline-block` makes the reserved min-width (set by JS from the
 * measured final string) actually apply.
 * ========================================================================= */

/* ---------------------------------------------------------------- base --- */
.fx-count-num {
  display: inline-block;
  font-variant-numeric: tabular-nums lining-nums;
  font-feature-settings: "tnum" 1, "lnum" 1;
  white-space: nowrap;
}

/* Shared stat typography — used by the copy/paste snippets and the demos. */
.fx-count-block {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  color: var(--fx-fg);
  font: inherit;
}

.fx-count-value {
  font-size: clamp(2rem, 8vw, 3.1rem);
  font-weight: 650;
  line-height: 1.05;
  letter-spacing: -0.03em;
  color: var(--fx-fg);
}

.fx-count-label {
  font-size: 0.8125rem;
  font-weight: 500;
  letter-spacing: 0.02em;
  color: var(--fx-muted);
}

.fx-count-eyebrow {
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--fx-muted);
}

.fx-count-delta {
  display: inline-flex;
  align-items: center;
  gap: 0.3em;
  align-self: flex-start;
  padding: 0.18em 0.5em;
  border-radius: 999px;
  font-size: 0.75rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--fx-accent);
  background: color-mix(in srgb, var(--fx-accent) 14%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fx-accent) 24%, transparent);
}

/* ----------------------------------------------------------- odometer --- */
.fx-count-odo {
  --fx-count-cell: 1.16em;
  display: inline-flex;
  /* centre, not baseline: an overflow:hidden column has no usable baseline */
  align-items: center;
  line-height: var(--fx-count-cell);
}

.fx-count-odo-fixed,
.fx-count-odo-sep {
  display: inline-block;
  height: var(--fx-count-cell);
  line-height: var(--fx-count-cell);
  color: inherit;
  /* the suffix often starts with a space (" km") — keep it */
  white-space: pre;
}

.fx-count-odo-sep { opacity: 0.7; }

.fx-count-odo-col {
  display: inline-block;
  width: 1ch;
  height: var(--fx-count-cell);
  overflow: hidden;
}

.fx-count-odo-strip {
  display: block;
  will-change: transform;
  transition-property: transform;
  transition-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
  transition-duration: 0s;
}

.fx-count-odo-cell {
  display: block;
  height: var(--fx-count-cell);
  line-height: var(--fx-count-cell);
  text-align: center;
}

/* --------------------------------------------------------------- flip --- */
.fx-count-flip {
  display: inline-flex;
  align-items: stretch;
  gap: 0.09em;
  perspective: 620px;
}

.fx-count-flip-fixed,
.fx-count-flip-sep {
  display: inline-flex;
  align-items: center;
  padding-inline: 0.04em;
  white-space: pre;
}

.fx-count-flip-sep { opacity: 0.7; }

.fx-count-flip-cell {
  position: relative;
  display: inline-block;
  width: 1ch;
  padding: 0.1em 0.16em;
  border-radius: 0.14em;
  background: var(--fx-surface-2);
  box-shadow: var(--fx-ring);
  text-align: center;
  /* the hairline split that makes a split-flap read as two leaves */
  background-image: linear-gradient(
    to bottom,
    color-mix(in srgb, var(--fx-fg) 5%, transparent) 0 calc(50% - 0.5px),
    color-mix(in srgb, var(--fx-bg) 70%, transparent) calc(50% - 0.5px) calc(50% + 0.5px),
    transparent calc(50% + 0.5px) 100%
  );
}

.fx-count-flip-face {
  display: block;
  transform-style: preserve-3d;
  backface-visibility: hidden;
  will-change: transform;
}

/* --------------------------------------------------------------- ring --- */
.fx-count-ring {
  position: relative;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.15rem;
  width: var(--fx-count-size, 8.5rem);
  aspect-ratio: 1;
  text-align: center;
}

.fx-count-ring-art {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.fx-count-ring-track {
  stroke: var(--fx-border);
}

.fx-count-ring-arc {
  stroke: var(--fx-accent);
  transition-property: stroke-dashoffset;
  transition-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
  transition-duration: 0s;
}

.fx-count-ring-num {
  position: relative;
  font-size: clamp(1.25rem, 5vw, 1.75rem);
  font-weight: 650;
  letter-spacing: -0.02em;
  color: var(--fx-fg);
}

.fx-count-ring-label {
  position: relative;
  max-width: 76%;
  font-size: 0.6875rem;
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--fx-muted);
}

/* ---------------------------------------------------------------- bar --- */
.fx-count-bar {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  width: 100%;
}

.fx-count-bar-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
}

.fx-count-bar-name {
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--fx-muted);
}

.fx-count-bar-num {
  font-size: clamp(1.1rem, 4vw, 1.5rem);
  font-weight: 650;
  letter-spacing: -0.02em;
  color: var(--fx-fg);
}

.fx-count-bar-track {
  position: relative;
  height: 0.5rem;
  border-radius: 999px;
  background: var(--fx-surface-2);
  box-shadow: var(--fx-ring);
  overflow: hidden;
}

.fx-count-bar-fill {
  display: block;
  height: 100%;
  border-radius: inherit;
  transform: scaleX(0);
  transform-origin: left center;
  background: linear-gradient(90deg, var(--fx-accent), var(--fx-accent-2));
  will-change: transform;
  transition-property: transform;
  transition-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
  transition-duration: 0s;
}

/* --------------------------------------------------------- bars group --- */
.fx-count-bars {
  display: flex;
  align-items: flex-end;
  gap: 0.55rem;
  width: 100%;
}

.fx-count-bars-item {
  display: flex;
  flex: 1 1 0;
  flex-direction: column;
  align-items: center;
  gap: 0.35rem;
  min-width: 0;
}

.fx-count-bars-cap {
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--fx-fg);
}

.fx-count-bars-col {
  position: relative;
  display: block;
  width: 100%;
  height: var(--fx-count-bars-h, 5.5rem);
  border-radius: var(--fx-radius-sm);
  background: var(--fx-surface-2);
  box-shadow: var(--fx-ring);
  overflow: hidden;
}

.fx-count-bars-fill {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  transform: scaleY(0);
  transform-origin: bottom center;
  background: linear-gradient(180deg, var(--fx-accent), var(--fx-accent-3));
  will-change: transform;
  transition-property: transform;
  transition-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
  transition-duration: 0s;
}

.fx-count-bars-name {
  font-size: 0.6875rem;
  font-weight: 500;
  letter-spacing: 0.04em;
  color: var(--fx-muted);
}

/* ----------------------------------------------------------- sparkline --- */
.fx-count-spark {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  width: 100%;
}

.fx-count-spark-art {
  display: block;
  order: 3;
  width: 100%;
  height: auto;
  margin-top: 0.4rem;
  overflow: visible;
}

.fx-count-spark-line {
  stroke: var(--fx-accent);
  stroke-width: 2.25;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}

.fx-count-spark-area {
  fill: color-mix(in srgb, var(--fx-accent) 16%, transparent);
  stroke: none;
}

.fx-count-spark-dot {
  fill: var(--fx-accent);
  stroke: var(--fx-surface);
  stroke-width: 2;
  opacity: 0;
}

.fx-count-spark-num {
  order: 1;
  font-size: clamp(1.6rem, 6vw, 2.4rem);
  font-weight: 650;
  line-height: 1.05;
  letter-spacing: -0.03em;
  color: var(--fx-fg);
}

.fx-count-spark-label {
  order: 2;
  font-size: 0.8125rem;
  color: var(--fx-muted);
}

/* --------------------------------------------------------- percent sweep -- */
.fx-count-sweep {
  position: relative;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.1rem;
  width: var(--fx-count-size, 8.5rem);
  aspect-ratio: 1;
  text-align: center;
}

.fx-count-sweep-disc {
  --fx-count-p: 0;
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background:
    conic-gradient(
      from -90deg,
      var(--fx-accent) 0,
      var(--fx-accent-2) calc(var(--fx-count-p) * 1%),
      var(--fx-surface-2) 0 100%
    );
  /* black here is pure alpha for the mask, not a colour in the design system */
  -webkit-mask: radial-gradient(farthest-side,
    rgb(0 0 0 / 0) calc(100% - var(--fx-count-thick, 13px) - 1px),
    rgb(0 0 0 / 1) calc(100% - var(--fx-count-thick, 13px)));
  mask: radial-gradient(farthest-side,
    rgb(0 0 0 / 0) calc(100% - var(--fx-count-thick, 13px) - 1px),
    rgb(0 0 0 / 1) calc(100% - var(--fx-count-thick, 13px)));
}

.fx-count-sweep-num {
  position: relative;
  font-size: clamp(1.25rem, 5vw, 1.8rem);
  font-weight: 650;
  letter-spacing: -0.02em;
  color: var(--fx-fg);
}

.fx-count-sweep-label {
  position: relative;
  max-width: 74%;
  font-size: 0.6875rem;
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--fx-muted);
}

/* -------------------------------------------------------------- ticker --- */
.fx-count-ticker {
  position: relative;
}

.fx-count-live {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--fx-muted);
}

.fx-count-live::before {
  content: "";
  width: 0.45em;
  height: 0.45em;
  border-radius: 50%;
  background: var(--fx-accent);
  box-shadow: 0 0 0 0 color-mix(in srgb, var(--fx-accent) 60%, transparent);
  animation: fx-count-pulse 2.4s var(--fx-ease-out, cubic-bezier(0.22, 1, 0.36, 1)) infinite;
}

@keyframes fx-count-pulse {
  0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--fx-accent) 55%, transparent); }
  70%  { box-shadow: 0 0 0 0.55em color-mix(in srgb, var(--fx-accent) 0%, transparent); }
  100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--fx-accent) 0%, transparent); }
}

/* ---------------------------------------------------------- stat block --- */
.fx-count-stat {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.45rem;
  width: 100%;
}

.fx-count-stat-value {
  font-size: clamp(2rem, 8vw, 3.1rem);
  font-weight: 650;
  line-height: 1.02;
  letter-spacing: -0.035em;
  color: var(--fx-fg);
}

.fx-count-stat-rule {
  display: block;
  width: 100%;
  height: 1px;
  margin: 0.15rem 0 0.25rem;
  background: linear-gradient(90deg, var(--fx-accent), var(--fx-border) 70%, transparent);
  transform-origin: left center;
}

.fx-count-stat-reveal {
  transition-property: opacity, transform;
  transition-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
  transition-duration: 0s;
}

.fx-count-stat[data-fx-count-state="out"] .fx-count-stat-reveal {
  opacity: 0;
  transform: translateY(9px);
}

.fx-count-stat[data-fx-count-state="out"] .fx-count-stat-rule {
  opacity: 1;
  transform: scaleX(0);
}

.fx-count-stat[data-fx-count-state="in"] .fx-count-stat-reveal {
  opacity: 1;
  transform: none;
}

/* -------------------------------------------------------------- rating --- */
.fx-count-rating {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.3rem;
}

.fx-count-rating-row {
  display: inline-flex;
  gap: 0.28rem;
  margin-bottom: 0.15rem;
}

.fx-count-rating-item {
  position: relative;
  display: inline-block;
  width: var(--fx-count-star, 1.65rem);
  height: var(--fx-count-star, 1.65rem);
  transition-property: opacity, transform;
  transition-timing-function: cubic-bezier(0.16, 1.2, 0.3, 1);
  transition-duration: 0s;
}

.fx-count-rating-item.is-dot {
  width: var(--fx-count-star, 1rem);
  height: var(--fx-count-star, 1rem);
}

/* the empty shape has to stay visible against the card it sits on, so it is
   derived from the foreground colour rather than from a surface token */
.fx-count-rating-shape {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  fill: color-mix(in srgb, var(--fx-fg) 16%, transparent);
}

.fx-count-rating-item.is-dot .fx-count-rating-shape {
  border-radius: 50%;
  background: color-mix(in srgb, var(--fx-fg) 16%, transparent);
  box-shadow: var(--fx-ring);
}

.fx-count-rating-shape.is-fill {
  --fx-count-f: 0;
  fill: var(--fx-accent);
  clip-path: inset(0 calc((1 - var(--fx-count-f)) * 100%) 0 0);
  transition-property: clip-path;
  transition-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
  transition-duration: 0s;
}

.fx-count-rating-item.is-dot .fx-count-rating-shape.is-fill {
  background: var(--fx-accent);
  box-shadow: none;
}

.fx-count-rating[data-fx-count-state="out"] .fx-count-rating-item {
  opacity: 0;
  transform: scale(0.78);
}

.fx-count-rating[data-fx-count-state="in"] .fx-count-rating-item {
  opacity: 1;
  transform: none;
}

.fx-count-rating-num {
  font-size: clamp(1.5rem, 6vw, 2.1rem);
  font-weight: 650;
  letter-spacing: -0.03em;
  color: var(--fx-fg);
}

.fx-count-rating-label {
  font-size: 0.8125rem;
  color: var(--fx-muted);
}

/* ------------------------------------------------------- demo scaffold --- */
/* Only used by the gallery previews in catalog/count.json. */
.fx-count-demo {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  width: min(100%, 22rem);
  padding: 1.25rem 1.4rem;
  border-radius: var(--fx-radius);
  background: var(--fx-surface);
  box-shadow: var(--fx-ring), var(--fx-shadow);
  color: var(--fx-fg);
  font-family: inherit;
  text-align: left;
}

.fx-count-demo.is-wide { width: min(100%, 26rem); }
.fx-count-demo.is-center { align-items: center; text-align: center; }

.fx-count-demo-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.25rem;
}

.fx-count-demo-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin-top: 0.15rem;
  padding-top: 0.7rem;
  border-top: 1px solid var(--fx-border);
}

/* ------------------------------------------------------- reduced motion --- */
/* The JS path already renders the final state directly; these rules are the
   belt-and-braces version for the CSS-driven parts. Values stay correct and
   fully readable — only the movement is removed. */
@media (prefers-reduced-motion: reduce) {
  .fx-count-odo-strip,
  .fx-count-ring-arc,
  .fx-count-bar-fill,
  .fx-count-bars-fill,
  .fx-count-stat-reveal,
  .fx-count-rating-item,
  .fx-count-rating-shape.is-fill {
    transition: none !important;
  }
  .fx-count-flip-face { transform: none !important; }
  .fx-count-live::before { animation: none; }
  .fx-count-stat[data-fx-count-state="out"] .fx-count-stat-reveal,
  .fx-count-stat[data-fx-count-state="out"] .fx-count-stat-rule,
  .fx-count-rating[data-fx-count-state="out"] .fx-count-rating-item {
    opacity: 1;
    transform: none;
  }
}

:root[data-fx-reduced] .fx-count-odo-strip,
:root[data-fx-reduced] .fx-count-ring-arc,
:root[data-fx-reduced] .fx-count-bar-fill,
:root[data-fx-reduced] .fx-count-bars-fill,
:root[data-fx-reduced] .fx-count-stat-reveal,
:root[data-fx-reduced] .fx-count-rating-item,
:root[data-fx-reduced] .fx-count-rating-shape.is-fill {
  transition: none !important;
}
:root[data-fx-reduced] .fx-count-flip-face { transform: none !important; }
:root[data-fx-reduced] .fx-count-live::before { animation: none; }
:root[data-fx-reduced] .fx-count-stat[data-fx-count-state="out"] .fx-count-stat-reveal,
:root[data-fx-reduced] .fx-count-stat[data-fx-count-state="out"] .fx-count-stat-rule,
:root[data-fx-reduced] .fx-count-rating[data-fx-count-state="out"] .fx-count-rating-item {
  opacity: 1;
  transform: none;
}

/* ══ src/effects/cursor.css ══ */
/* =========================================================================
 * effects-lab — cursor
 * Every cursor node lives inside a `.fx-cursor-layer` that is a child of the
 * host element, so it is clipped to the host and removed on cleanup.
 * `cursor: none` is only ever applied to the host (or, with
 * data-fx-scope="page", to <body>) — never to <html>.
 * ========================================================================= */

/* ---------------------------------------------------------------- layer -- */
.fx-cursor-layer {
  position: absolute;
  inset: 0;
  z-index: 60;
  overflow: hidden;
  border-radius: inherit;
  pointer-events: none;
  contain: layout paint;
  opacity: 0;
  visibility: hidden;
}

.fx-cursor-layer.is-page {
  position: fixed;
  inset: 0;
  z-index: 2147483000;
  border-radius: 0;
}

/* Blend-mode cursors must not sit inside an extra stacking context. */
.fx-cursor-layer.is-raw {
  z-index: auto;
  contain: none;
  opacity: 1;
}

.fx-cursor-layer > * {
  position: absolute;
  top: 0;
  left: 0;
  will-change: transform;
}

/* The layer is `position: absolute; inset: 0`, so the host must be a containing
   block. Adding `position: relative` to a static element does not move it — it
   only stops the layer escaping to some far-away positioned ancestor. */
.fx-cursor-host { position: relative; }

/* Native cursor: hidden only inside the host, and only when a custom cursor
   is actually drawn in its place. */
.fx-cursor-host.fx-cursor-hide,
.fx-cursor-host.fx-cursor-hide * { cursor: none; }

body.fx-cursor-hide-page,
body.fx-cursor-hide-page * { cursor: none; }

/* ------------------------------------------------------------ cursor-dot -- */
.fx-cursor-dot {
  width: var(--fx-cursor-size, 13px);
  height: var(--fx-cursor-size, 13px);
  border-radius: 50%;
  background: var(--fx-accent);
}

/* ----------------------------------------------------------- cursor-ring -- */
.fx-cursor-ring {
  width: var(--fx-cursor-ring, 34px);
  height: var(--fx-cursor-ring, 34px);
  border-radius: 50%;
  border: 1.5px solid var(--fx-accent);
}

.fx-cursor-ring-dot {
  width: var(--fx-cursor-size, 7px);
  height: var(--fx-cursor-size, 7px);
  border-radius: 50%;
  background: var(--fx-accent);
}

/* ---------------------------------------------------------- cursor-blend -- */
.fx-cursor-blend {
  width: var(--fx-cursor-size, 44px);
  height: var(--fx-cursor-size, 44px);
  border-radius: 50%;
  background: var(--fx-fg);
  mix-blend-mode: difference;
}

/* ---------------------------------------------------------- cursor-trail -- */
.fx-cursor-trail-dot {
  width: var(--fx-cursor-size, 14px);
  height: var(--fx-cursor-size, 14px);
  border-radius: 50%;
  background: var(--fx-accent);
}

/* -------------------------------------------------------- cursor-stretch -- */
.fx-cursor-stretch {
  width: var(--fx-cursor-size, 26px);
  height: var(--fx-cursor-size, 26px);
  border-radius: 50%;
  background: var(--fx-accent);
}

/* -------------------------------------------------- cursor-magnetic-snap -- */
.fx-cursor-snap {
  width: var(--fx-cursor-size, 30px);
  height: var(--fx-cursor-size, 30px);
  border-radius: 50%;
  border: 1.5px solid var(--fx-accent);
  background: color-mix(in srgb, var(--fx-accent) 10%, transparent);
  transition:
    width 0.34s var(--fx-ease-snap),
    height 0.34s var(--fx-ease-snap),
    border-radius 0.34s var(--fx-ease-snap);
}

.fx-cursor-snap-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--fx-accent);
}

/* ---------------------------------------------------------- cursor-label -- */
.fx-cursor-label-dot {
  width: var(--fx-cursor-size, 10px);
  height: var(--fx-cursor-size, 10px);
  border-radius: 50%;
  background: var(--fx-accent);
}

.fx-cursor-label-pill {
  transform-origin: 0 0;
  padding: 0.36em 0.8em;
  border-radius: 999px;
  background: var(--fx-accent);
  color: var(--fx-accent-contrast);
  font: inherit;
  font-size: 0.78rem;
  font-weight: 600;
  line-height: 1.25;
  letter-spacing: 0.01em;
  white-space: nowrap;
  box-shadow: var(--fx-shadow);
  opacity: 0;
}

/* ------------------------------------------------------ cursor-spotlight -- */
/* A circular hole: the soft falloff is a radial gradient, everything beyond it
   is one enormous box-shadow. Both ride along on transform — zero repaint. */
.fx-cursor-spot {
  --fx-cursor-dim: color-mix(in srgb, var(--fx-bg) 88%, transparent);
  width: var(--fx-cursor-size, 190px);
  height: var(--fx-cursor-size, 190px);
  border-radius: 50%;
  background: radial-gradient(
    circle closest-side,
    transparent 0 52%,
    var(--fx-cursor-dim) 100%
  );
  box-shadow: 0 0 0 9999px var(--fx-cursor-dim);
}

/* ----------------------------------------------------------- cursor-glow -- */
.fx-cursor-glow {
  width: var(--fx-cursor-size, 280px);
  height: var(--fx-cursor-size, 280px);
  border-radius: 50%;
  background: radial-gradient(
    circle closest-side,
    color-mix(in srgb, var(--fx-accent) 70%, transparent) 0%,
    color-mix(in srgb, var(--fx-accent-2) 26%, transparent) 46%,
    transparent 100%
  );
}

/* ------------------------------------------------------ cursor-crosshair -- */
.fx-cursor-cross-h {
  width: 100%;
  height: 1px;
  background: color-mix(in srgb, var(--fx-accent) 55%, transparent);
}

.fx-cursor-cross-v {
  width: 1px;
  height: 100%;
  background: color-mix(in srgb, var(--fx-accent) 55%, transparent);
}

.fx-cursor-cross-dot {
  width: var(--fx-cursor-size, 7px);
  height: var(--fx-cursor-size, 7px);
  border-radius: 50%;
  background: var(--fx-accent);
}

/* ------------------------------------------------- reduced motion (CSS) --- */
/* JS handles the real fallback (most cursors attach nothing at all). These
   rules only make sure no size/position transition can still run. */
@media (prefers-reduced-motion: reduce) {
  .fx-cursor-snap { transition: none; }
  .fx-cursor-layer,
  .fx-cursor-layer * { animation: none; }
}
:root[data-fx-reduced] .fx-cursor-snap { transition: none; }
:root[data-fx-reduced] .fx-cursor-layer,
:root[data-fx-reduced] .fx-cursor-layer * { animation: none; }

/* ============================================================ demo shell == */
/* Used by the gallery previews (and perfectly usable on a real page). */
.fx-cursor-demo,
.fx-cursor-demo *,
.fx-cursor-demo *::before,
.fx-cursor-demo *::after { box-sizing: border-box; }

.fx-cursor-demo {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 0.75rem;
  width: 100%;
  height: 100%;
  min-height: 260px;
  align-self: stretch;
  justify-self: stretch;
  padding: clamp(1rem, 4%, 2rem);
  border-radius: var(--fx-radius);
  background: var(--fx-surface);
  color: var(--fx-fg);
  overflow: hidden;
}

.fx-cursor-demo-eyebrow {
  margin: 0;
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--fx-muted);
}

.fx-cursor-demo-title {
  margin: 0;
  font-size: clamp(1.4rem, 5vw, 2rem);
  line-height: 1.1;
  letter-spacing: -0.02em;
}

.fx-cursor-demo-copy {
  margin: 0;
  max-width: 34ch;
  font-size: 0.9rem;
  line-height: 1.5;
  color: var(--fx-muted);
}

.fx-cursor-demo-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
  margin-top: 0.35rem;
}

.fx-cursor-demo-btn {
  padding: 0.6em 1.1em;
  border: 0;
  border-radius: var(--fx-radius-sm);
  background: var(--fx-accent);
  color: var(--fx-accent-contrast);
  font: inherit;
  font-size: 0.85rem;
  font-weight: 600;
}

.fx-cursor-demo-btn.is-ghost {
  background: transparent;
  color: var(--fx-fg);
  box-shadow: var(--fx-ring);
}

.fx-cursor-demo-link {
  color: var(--fx-fg);
  font-size: 0.85rem;
  font-weight: 500;
  text-decoration: none;
  border-bottom: 1px solid var(--fx-border);
  padding-bottom: 2px;
}

.fx-cursor-demo-tiles {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0.6rem;
  margin-top: 0.35rem;
}

.fx-cursor-demo-tile {
  display: grid;
  place-items: center;
  min-height: 74px;
  border-radius: var(--fx-radius-sm);
  background: var(--fx-surface-2);
  box-shadow: var(--fx-ring);
  color: var(--fx-muted);
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-decoration: none;
}

.fx-cursor-demo :focus-visible {
  outline: 2px solid var(--fx-accent-2);
  outline-offset: 3px;
}

/* ══ src/effects/gesture.css ══ */
/* =========================================================================
 * effects-lab — category: gesture  ("Gestures & interactions")
 * Everything here is namespaced .fx-gesture-* / @keyframes fx-gesture-*.
 * No hardcoded colours: tokens from src/core/00-tokens.css only.
 * Motion for anything the user released; CSS only for static skin + focus.
 * ========================================================================= */

/* ---------------------------------------------------------------- shared */
.fx-gesture {
  color: var(--fx-fg);
  font: 500 14px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif;
  -webkit-tap-highlight-color: transparent;
}
.fx-gesture *,
.fx-gesture *::before,
.fx-gesture *::after { box-sizing: border-box; }

.fx-gesture :focus-visible,
.fx-gesture:focus-visible {
  outline: 2px solid var(--fx-accent);
  outline-offset: 3px;
  border-radius: var(--fx-radius-sm);
}

.fx-gesture-grab {
  touch-action: none;
  cursor: grab;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  will-change: transform;
}
.fx-gesture-grab.is-dragging { cursor: grabbing; }

.fx-gesture-hint {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fx-muted);
  pointer-events: none;
}
.fx-gesture-hint::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--fx-accent);
}

/* Demo-only wrapper: centres a control in the gallery stage and lets the
   hint sit under it. Namespaced, so it is safe to copy or ignore. */
.fx-gesture-demo {
  display: grid;
  gap: 14px;
  width: 100%;
  justify-items: center;
  align-content: center;
  color: var(--fx-fg);
  font: 500 14px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif;
}
.fx-gesture-demo__caption {
  font-size: 12px;
  color: var(--fx-muted);
  text-align: center;
  max-width: 34ch;
}

.fx-gesture-sr {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ------------------------------------------------------- 1. gesture-drag */
.fx-gesture-drag {
  position: relative;
  width: min(100%, 440px);
  height: 250px;
  margin-inline: auto;
  border-radius: var(--fx-radius-lg);
  border: 1px dashed var(--fx-border);
  background:
    radial-gradient(circle at 1px 1px, var(--fx-border) 1px, transparent 0) 0 0 / 22px 22px,
    var(--fx-surface);
  overflow: hidden;
  touch-action: none;
}
.fx-gesture-drag__puck {
  position: absolute;
  inset: 0;
  margin: auto;
  width: 108px;
  height: 108px;
  display: grid;
  place-content: center;
  gap: 4px;
  text-align: center;
  border-radius: 28px;
  background: linear-gradient(160deg, var(--fx-accent), var(--fx-accent-3));
  color: var(--fx-accent-contrast);
  box-shadow: var(--fx-shadow);
  font-weight: 700;
  font-size: 13px;
  touch-action: none;
  cursor: grab;
  user-select: none;
  -webkit-user-select: none;
  will-change: transform;
}
.fx-gesture-drag__puck.is-dragging { cursor: grabbing; }
.fx-gesture-drag__puck small {
  display: block;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  opacity: 0.66;
}
.fx-gesture-drag__hint {
  position: absolute;
  left: 14px;
  bottom: 12px;
}

/* -------------------------------------------------- 2. gesture-drag-axis */
.fx-gesture-axis {
  width: min(100%, 430px);
  margin-inline: auto;
  display: grid;
  gap: 18px;
  padding: 26px 24px 22px;
  border-radius: var(--fx-radius-lg);
  background: var(--fx-surface);
  box-shadow: var(--fx-ring);
}
.fx-gesture-axis__track {
  position: relative;
  height: 56px;
  border-radius: 999px;
  background: var(--fx-surface-2);
  box-shadow: inset 0 0 0 1px var(--fx-border);
  touch-action: pan-y;
}
.fx-gesture-axis__marks {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 28px;
  pointer-events: none;
}
.fx-gesture-axis__mark {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--fx-border);
}
.fx-gesture-axis__handle {
  position: absolute;
  top: 4px;
  left: 4px;
  width: 48px;
  height: 48px;
  display: grid;
  place-content: center;
  border-radius: 50%;
  background: var(--fx-accent);
  color: var(--fx-accent-contrast);
  box-shadow: var(--fx-shadow);
  font-weight: 800;
  font-size: 15px;
  touch-action: pan-y;
  cursor: grab;
  user-select: none;
  -webkit-user-select: none;
  will-change: transform;
}
.fx-gesture-axis__handle.is-dragging { cursor: grabbing; }
.fx-gesture-axis__foot {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}
.fx-gesture-axis__value {
  font-size: 15px;
  font-weight: 700;
  color: var(--fx-fg);
}

/* ------------------------------------------------ 3. gesture-swipe-card */
.fx-gesture-swipe {
  position: relative;
  width: min(100%, 300px);
  height: 290px;
  margin-inline: auto;
  touch-action: pan-y;
}
.fx-gesture-swipe__card {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 4px;
  padding: 20px;
  border-radius: var(--fx-radius-lg);
  background: linear-gradient(170deg, var(--fx-surface-2), var(--fx-surface));
  box-shadow: var(--fx-ring), var(--fx-shadow);
  touch-action: pan-y;
  user-select: none;
  -webkit-user-select: none;
  will-change: transform;
  overflow: hidden;
}
.fx-gesture-swipe__card::before {
  content: "";
  position: absolute;
  inset: 0 0 42% 0;
  background: linear-gradient(140deg, var(--fx-accent), var(--fx-accent-2));
  opacity: 0.85;
}
.fx-gesture-swipe__card > * { position: relative; }
.fx-gesture-swipe__card strong { font-size: 18px; font-weight: 700; }
.fx-gesture-swipe__card span { font-size: 13px; color: var(--fx-muted); }
.fx-gesture-swipe__card.is-top { cursor: grab; }
.fx-gesture-swipe__card.is-top.is-dragging { cursor: grabbing; }
.fx-gesture-swipe__badge {
  position: absolute;
  top: 18px;
  padding: 6px 12px;
  border-radius: 999px;
  border: 2px solid currentColor;
  font-size: 12px;
  font-weight: 800;
  letter-spacing: 0.12em;
  opacity: 0;
  pointer-events: none;
}
.fx-gesture-swipe__badge--like { left: 18px; color: var(--fx-accent-2); }
.fx-gesture-swipe__badge--nope { right: 18px; color: var(--fx-accent-3); }
.fx-gesture-swipe__status {
  position: absolute;
  z-index: 10;
  margin: 0;
  left: 0; right: 0;
  bottom: -38px;
  text-align: center;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fx-muted);
}

/* --------------------------------------------------- 4. gesture-carousel */
.fx-gesture-carousel {
  width: min(100%, 470px);
  margin-inline: auto;
  display: grid;
  gap: 16px;
}
.fx-gesture-carousel__viewport {
  overflow: hidden;
  border-radius: var(--fx-radius-lg);
  touch-action: pan-y;
  cursor: grab;
}
.fx-gesture-carousel__viewport.is-dragging { cursor: grabbing; }
.fx-gesture-carousel__track {
  display: flex;
  gap: 14px;
  will-change: transform;
  user-select: none;
  -webkit-user-select: none;
}
.fx-gesture-carousel__slide {
  flex: 0 0 76%;
  height: 190px;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 2px;
  padding: 18px;
  border-radius: var(--fx-radius-lg);
  background: linear-gradient(155deg, var(--fx-surface-2), var(--fx-surface));
  box-shadow: var(--fx-ring);
  will-change: transform, opacity;
}
.fx-gesture-carousel__slide strong { font-size: 17px; }
.fx-gesture-carousel__slide span { font-size: 12px; color: var(--fx-muted); }
.fx-gesture-carousel__dots {
  display: flex;
  justify-content: center;
  gap: 9px;
}
.fx-gesture-carousel__dot {
  width: 9px;
  height: 9px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: var(--fx-border);
  cursor: pointer;
  transition: background var(--fx-dur-2) var(--fx-ease-out), width var(--fx-dur-2) var(--fx-ease-out);
}
.fx-gesture-carousel__dot[aria-current="true"] {
  width: 26px;
  background: var(--fx-accent);
}

/* ---------------------------------------------------- 5. gesture-reorder */
.fx-gesture-reorder {
  width: min(100%, 400px);
  margin-inline: auto;
  position: relative;
  list-style: none;
  margin-block: 0;
  padding: 0;
  display: block;
}
.fx-gesture-reorder__item {
  position: relative;
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 8px;
  padding: 13px 16px;
  border-radius: var(--fx-radius);
  background: var(--fx-surface);
  box-shadow: var(--fx-ring);
  font-size: 14px;
  will-change: transform;
  touch-action: pan-x;
  user-select: none;
  -webkit-user-select: none;
}
.fx-gesture-reorder__item.is-lifted {
  z-index: 3;
  background: var(--fx-surface-2);
  box-shadow: var(--fx-ring), var(--fx-shadow);
}
.fx-gesture-reorder__handle {
  flex: none;
  width: 26px;
  height: 26px;
  display: grid;
  place-content: center;
  padding: 0;
  border: 0;
  border-radius: var(--fx-radius-sm);
  background: transparent;
  color: var(--fx-muted);
  cursor: grab;
  touch-action: none;
  letter-spacing: -2px;
  font-size: 15px;
}
.fx-gesture-reorder__handle[aria-pressed="true"] {
  background: var(--fx-accent);
  color: var(--fx-accent-contrast);
  cursor: grabbing;
}
.fx-gesture-reorder__num {
  margin-left: auto;
  font-variant-numeric: tabular-nums;
  font-size: 12px;
  color: var(--fx-muted);
}

/* ----------------------------------------------------- 6. gesture-toggle */
.fx-gesture-toggle {
  display: inline-flex;
  align-items: center;
  gap: 14px;
  padding: 10px 18px 10px 10px;
  border: 0;
  border-radius: 999px;
  background: var(--fx-surface);
  box-shadow: var(--fx-ring);
  color: var(--fx-fg);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
}
.fx-gesture-toggle__track {
  position: relative;
  flex: none;
  width: 64px;
  height: 36px;
  border-radius: 999px;
  background: var(--fx-surface-2);
  box-shadow: inset 0 0 0 1px var(--fx-border);
  transition: background var(--fx-dur-2) var(--fx-ease-out);
}
.fx-gesture-toggle[aria-checked="true"] .fx-gesture-toggle__track { background: var(--fx-accent); }
.fx-gesture-toggle__thumb {
  position: absolute;
  top: 4px;
  left: 4px;
  width: 28px;
  height: 28px;
  border-radius: 999px;
  background: var(--fx-fg);
  box-shadow: 0 1px 3px hsl(0 0% 0% / 0.35);
  will-change: transform;
}
.fx-gesture-toggle[aria-checked="true"] .fx-gesture-toggle__thumb { background: var(--fx-accent-contrast); }
.fx-gesture-toggle__label { font-size: 14px; min-width: 4.5ch; text-align: left; }

/* ------------------------------------------------------- 7. gesture-like */
.fx-gesture-like {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 12px 20px 12px 16px;
  border: 0;
  border-radius: 999px;
  background: var(--fx-surface);
  box-shadow: var(--fx-ring);
  color: var(--fx-fg);
  font: inherit;
  font-weight: 700;
  cursor: pointer;
  touch-action: manipulation;
  will-change: transform;
}
.fx-gesture-like__icon {
  display: block;
  width: 26px;
  height: 26px;
  color: var(--fx-muted);
  will-change: transform;
  transition: color var(--fx-dur-2) var(--fx-ease-out);
}
.fx-gesture-like[aria-pressed="true"] .fx-gesture-like__icon { color: var(--fx-accent); }
.fx-gesture-like__icon path {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  transition: fill var(--fx-dur-2) var(--fx-ease-out);
}
.fx-gesture-like[aria-pressed="true"] .fx-gesture-like__icon path { fill: currentColor; }
.fx-gesture-like__count { font-variant-numeric: tabular-nums; min-width: 3ch; text-align: left; }
.fx-gesture-like__spark {
  position: absolute;
  top: 50%;
  left: 27px;
  width: 7px;
  height: 7px;
  margin: -3.5px 0 0 -3.5px;
  border-radius: 999px;
  pointer-events: none;
  will-change: transform, opacity;
}

/* --------------------------------------------------- 8. gesture-confetti */
.fx-gesture-confetti {
  position: relative;
  width: min(100%, 460px);
  height: 250px;
  margin-inline: auto;
  display: grid;
  place-content: center;
  gap: 12px;
  justify-items: center;
  border-radius: var(--fx-radius-lg);
  background: var(--fx-surface);
  box-shadow: var(--fx-ring);
  overflow: hidden;
}
.fx-gesture-confetti__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}
.fx-gesture-confetti__btn {
  position: relative;
  padding: 14px 26px;
  border: 0;
  border-radius: 999px;
  background: var(--fx-accent);
  color: var(--fx-accent-contrast);
  font: inherit;
  font-weight: 700;
  font-size: 15px;
  cursor: pointer;
  touch-action: manipulation;
  will-change: transform;
}
.fx-gesture-confetti__note {
  position: relative;
  font-size: 12px;
  color: var(--fx-muted);
}
.fx-gesture-confetti.is-celebrated .fx-gesture-confetti__note { color: var(--fx-accent); }

/* ----------------------------------------------------- 9. gesture-slider */
.fx-gesture-slider {
  width: min(100%, 440px);
  margin-inline: auto;
  display: grid;
  gap: 10px;
  padding: 44px 26px 24px;
  border-radius: var(--fx-radius-lg);
  background: var(--fx-surface);
  box-shadow: var(--fx-ring);
}
.fx-gesture-slider__track {
  position: relative;
  height: 10px;
  border-radius: 999px;
  background: var(--fx-surface-2);
  box-shadow: inset 0 0 0 1px var(--fx-border);
  touch-action: pan-y;
  cursor: pointer;
}
.fx-gesture-slider__fill {
  position: absolute;
  top: 0; left: 0; bottom: 0;
  width: 100%;
  border-radius: 999px;
  background: var(--fx-accent);
  transform-origin: left center;
  transform: scaleX(0);
  will-change: transform;
}
.fx-gesture-slider__thumb {
  position: absolute;
  top: 50%;
  left: 0;
  width: 26px;
  height: 26px;
  margin: -13px 0 0 -13px;
  border-radius: 999px;
  background: var(--fx-fg);
  box-shadow: 0 1px 4px hsl(0 0% 0% / 0.4);
  touch-action: pan-y;
  cursor: grab;
  will-change: transform;
}
.fx-gesture-slider__thumb.is-dragging { cursor: grabbing; }
.fx-gesture-slider__bubble {
  position: absolute;
  top: -40px;
  left: 0;
  padding: 4px 10px;
  border-radius: 999px;
  background: var(--fx-accent);
  color: var(--fx-accent-contrast);
  font-size: 12px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  transform-origin: 50% 120%;
  pointer-events: none;
  will-change: transform, opacity;
}
.fx-gesture-slider__foot {
  display: flex;
  justify-content: space-between;
  font-size: 12px;
  color: var(--fx-muted);
}

/* --------------------------------------------- 10. gesture-pull-refresh */
.fx-gesture-pull {
  position: relative;
  width: min(100%, 400px);
  height: 270px;
  margin-inline: auto;
  border-radius: var(--fx-radius-lg);
  background: var(--fx-surface);
  box-shadow: var(--fx-ring);
  overflow: hidden;
  touch-action: pan-x;
}
.fx-gesture-pull__head {
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 66px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  font-size: 12px;
  font-weight: 600;
  color: var(--fx-muted);
  will-change: transform, opacity;
}
.fx-gesture-pull__spinner {
  width: 17px;
  height: 17px;
  border-radius: 50%;
  border: 2px solid var(--fx-border);
  border-top-color: var(--fx-accent);
  will-change: transform;
}
.fx-gesture-pull__body {
  position: relative;
  padding: 14px;
  display: grid;
  gap: 8px;
  background: var(--fx-surface);
  will-change: transform;
}
.fx-gesture-pull__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding-bottom: 6px;
}
.fx-gesture-pull__btn {
  padding: 6px 12px;
  border: 0;
  border-radius: 999px;
  background: var(--fx-surface-2);
  box-shadow: var(--fx-ring);
  color: var(--fx-fg);
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
}
.fx-gesture-pull__row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 11px 14px;
  border-radius: var(--fx-radius);
  background: var(--fx-surface-2);
  font-size: 13px;
  will-change: transform, opacity;
}
.fx-gesture-pull__row i {
  flex: none;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--fx-accent);
}
.fx-gesture-pull__row time {
  margin-left: auto;
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  color: var(--fx-muted);
}

/* -------------------------------------------------- 11. gesture-long-press */
.fx-gesture-longpress {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 14px;
  padding: 12px 24px 12px 12px;
  border: 0;
  border-radius: 999px;
  background: var(--fx-surface);
  box-shadow: var(--fx-ring);
  color: var(--fx-fg);
  font: inherit;
  font-weight: 700;
  cursor: pointer;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  will-change: transform;
}
.fx-gesture-longpress__ring {
  flex: none;
  width: 40px;
  height: 40px;
  transform: rotate(-90deg);
  overflow: visible;
}
.fx-gesture-longpress__ring circle {
  fill: none;
  stroke-width: 3.5;
  stroke-linecap: round;
}
.fx-gesture-longpress__bg { stroke: var(--fx-border); }
.fx-gesture-longpress__fg { stroke: var(--fx-accent); }
.fx-gesture-longpress__check {
  position: absolute;
  left: 12px;
  top: 50%;
  width: 40px;
  height: 40px;
  margin-top: -20px;
  display: grid;
  place-content: center;
  color: var(--fx-accent);
  font-size: 19px;
  opacity: 0;
  pointer-events: none;
}
.fx-gesture-longpress__label { min-width: 12ch; text-align: left; font-size: 14px; }
.fx-gesture-longpress.is-done { color: var(--fx-accent); }

/* ---------------------------------------------------- 12. gesture-stepper */
.fx-gesture-stepper {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px;
  border-radius: 999px;
  background: var(--fx-surface);
  box-shadow: var(--fx-ring);
}
.fx-gesture-stepper__btn {
  width: 42px;
  height: 42px;
  display: grid;
  place-content: center;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: var(--fx-surface-2);
  color: var(--fx-fg);
  font: inherit;
  font-size: 21px;
  font-weight: 600;
  line-height: 1;
  cursor: pointer;
  touch-action: manipulation;
  will-change: transform;
}
.fx-gesture-stepper__btn:disabled { opacity: 0.38; cursor: not-allowed; }
.fx-gesture-stepper__value {
  position: relative;
  width: 82px;
  height: 42px;
  overflow: hidden;
  display: grid;
  place-items: center;
}
.fx-gesture-stepper__digit {
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  font-size: 22px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  will-change: transform, opacity;
}

/* ====================================================== reduced motion ====
 * Springs become short eased moves or instant snaps (JS side). CSS side:
 * kill every transition + transform-based decoration. Dragging still works.
 * ========================================================================= */
/* Colour and fill transitions stay — they carry meaning and do not move.
   Width/position transitions and every particle flourish are removed. */
@media (prefers-reduced-motion: reduce) {
  .fx-gesture-carousel__dot { transition-property: background; }
  .fx-gesture-like__spark { display: none; }
  .fx-gesture-pull__spinner { border-top-color: var(--fx-border); }
}
:root[data-fx-reduced] .fx-gesture-carousel__dot { transition-property: background; }
:root[data-fx-reduced] .fx-gesture-like__spark { display: none; }
:root[data-fx-reduced] .fx-gesture-pull__spinner { border-top-color: var(--fx-border); }

/* =========================================================================
 * gesture-carousel-native — drag-to-scroll on a REAL overflow container.
 * The classes only steer the native scroller: is-dragging / is-gliding
 * suspend CSS scroll-snap while the effect owns scrollLeft (snap would
 * yank the position out of its hands mid-write), and smooth scrolling is
 * forced off for the same reason. The overshoot transform is painted by
 * the effect itself.
 * ========================================================================= */
.fx-gesture-carousel-native { cursor: grab; }
.fx-gesture-carousel-native.is-dragging {
  cursor: grabbing;
  user-select: none;
  -webkit-user-select: none;
}
.fx-gesture-carousel-native.is-dragging,
.fx-gesture-carousel-native.is-gliding {
  scroll-snap-type: none;
  scroll-behavior: auto;
}

/* Demo scaffolding: an honest scroll row — snap, gap, edge padding — that
   the effect then enhances. Only the gallery preview uses these. */
.fx-gesture-cnative-row {
  display: flex;
  gap: 12px;
  width: 100%;
  max-width: 34rem;
  overflow-x: auto;
  padding: 4px 4px 10px;
  scroll-snap-type: x mandatory;
  scrollbar-width: thin;
}
.fx-gesture-cnative-row > * {
  flex: 0 0 46%;
  scroll-snap-align: start;
  display: grid;
  gap: 4px;
  align-content: center;
  min-height: 108px;
  padding: 14px 16px;
  border: 1px solid var(--fx-border);
  border-radius: var(--fx-radius);
  background: var(--fx-surface);
}
.fx-gesture-cnative-row strong { font-size: 15px; }
.fx-gesture-cnative-row span { color: var(--fx-muted); font-size: 12.5px; }

/* ------------------------------------------- 14. gesture-layered-pager
 * Three decoupled layers: gradient (never moves) · photo (the only thing
 * that tracks the finger / scales on pull) · text + controls (never
 * translate, only fade). Item colours come from data-fx-from / data-fx-to;
 * the chrome's own light ink and dark ink are derived from --fx-hue.
 * Geometry fractions (20% photo top, 4% chevron top) match the JS constants.
 * --------------------------------------------------------------------- */
.fx-gesture-lpager {
  --fx-gesture-lpager-fg: hsl(var(--fx-hue) 20% 97%);
  --fx-gesture-lpager-ink: hsl(var(--fx-hue) 35% 5%);
  --fx-gesture-lpager-from: var(--fx-accent);
  --fx-gesture-lpager-to: var(--fx-gesture-lpager-ink);
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 9 / 17;
  overflow: hidden;
  isolation: isolate;
  container-type: inline-size;
  border-radius: var(--fx-radius-lg);
  background: var(--fx-gesture-lpager-ink);
  color: var(--fx-gesture-lpager-fg);
  touch-action: pan-y;               /* horizontal is ours; vertical is claimed per gesture in JS */
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  cursor: grab;
}
.fx-gesture-lpager.is-dragging { cursor: grabbing; }
.fx-gesture-lpager:focus-visible {
  outline: 2px solid var(--fx-gesture-lpager-fg);
  outline-offset: -4px;
}
.fx-gesture-lpager :focus-visible {
  outline: 2px solid var(--fx-gesture-lpager-fg);
  outline-offset: 3px;
  border-radius: var(--fx-radius-sm);
}

/* layer 1 — background */
.fx-gesture-lpager-bgs,
.fx-gesture-lpager-photos {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.fx-gesture-lpager-bgs { z-index: 0; }
.fx-gesture-lpager-bg {
  position: absolute;
  inset: 0;
  opacity: 0;
  background: linear-gradient(to bottom, var(--fx-gesture-lpager-from), var(--fx-gesture-lpager-to));
}
/* Top-heavy: top of the screen ends at ~0.29 of its brightness, bottom ~0.6. */
.fx-gesture-lpager-scrim {
  position: absolute;
  inset: 0;
  z-index: 5;
  opacity: 0;
  background: linear-gradient(to bottom,
    color-mix(in srgb, var(--fx-gesture-lpager-ink) 71%, transparent),
    color-mix(in srgb, var(--fx-gesture-lpager-ink) 40%, transparent));
}

/* layer 2 — photo */
.fx-gesture-lpager-photos { z-index: 1; }
.fx-gesture-lpager-photo {
  position: absolute;
  left: 0;
  right: 0;
  top: 20%;
  bottom: 0;
  opacity: 0;
  transform-origin: 50% 0;           /* the pull-down shrinks it from the top centre */
  will-change: transform, opacity;
}
.fx-gesture-lpager-img,
.fx-gesture-lpager-art {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: 50% 100%;
  pointer-events: none;
  -webkit-user-drag: none;
}
/* data-fx-tone: grayscale the photo, then multiply the item's gradient over
   it, masked by the cut-out's own alpha so only the person is coloured. */
.fx-gesture-lpager.is-toned .fx-gesture-lpager-img { filter: grayscale(1) contrast(1.08); }
.fx-gesture-lpager-tint {
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom,
    color-mix(in oklab, var(--fx-gesture-lpager-from) 60%, var(--fx-gesture-lpager-fg)),
    var(--fx-gesture-lpager-from) 45%,
    var(--fx-gesture-lpager-to));
  mix-blend-mode: multiply;
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-position: 50% 100%;
  mask-position: 50% 100%;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
}

/* layer 3 — text (fixed slots, only opacity changes) */
.fx-gesture-lpager-item {
  position: absolute;
  inset: 0;
  z-index: 2;
  margin: 0;
  padding: 0;
  pointer-events: none;
}
.fx-gesture-lpager-item.is-current .fx-gesture-lpager-detail { pointer-events: auto; }
.fx-gesture-lpager-name {
  position: absolute;
  left: 8cqw;
  right: 28cqw;
  bottom: 21%;
  margin: 0;
  font: 400 13cqw/0.98 system-ui, -apple-system, "Segoe UI", sans-serif;
  letter-spacing: -0.03em;
  color: var(--fx-gesture-lpager-fg);
  opacity: 0;
}
.fx-gesture-lpager-name-top {
  top: 7%;
  bottom: auto;
  right: 8cqw;
}
.fx-gesture-lpager-sub {
  position: absolute;
  left: 8cqw;
  right: 8cqw;
  top: 81.5%;
  margin: 0;
  font-size: max(11px, 3.9cqw);
  line-height: 1.35;
  letter-spacing: 0.01em;
  color: color-mix(in srgb, var(--fx-gesture-lpager-fg) 62%, transparent);
  opacity: 0;
}
.fx-gesture-lpager-detail {
  position: absolute;
  left: 8cqw;
  right: 8cqw;
  top: 21%;
  bottom: 25%;
  display: grid;
  align-content: start;
  gap: 3.4cqw;
  overflow: hidden;
  font-size: max(11px, 4.1cqw);
  line-height: 1.45;
  color: color-mix(in srgb, var(--fx-gesture-lpager-fg) 86%, transparent);
  pointer-events: none;
}
.fx-gesture-lpager-detail > * { margin: 0; opacity: 0; }
.fx-gesture-lpager-detail ul,
.fx-gesture-lpager-detail ol {
  display: grid;
  gap: 2.6cqw;
  margin: 0;
  padding: 0;
  list-style: none;
  opacity: 1;
}
.fx-gesture-lpager-detail li {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 3cqw;
  opacity: 0;
}
.fx-gesture-lpager-detail li > img,
.fx-gesture-lpager-thumb {
  display: block;
  width: 11cqw;
  height: 11cqw;
  border-radius: var(--fx-radius-sm);
  object-fit: cover;
  background: linear-gradient(145deg, var(--fx-gesture-lpager-from), var(--fx-gesture-lpager-to));
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fx-gesture-lpager-fg) 18%, transparent);
}
.fx-gesture-lpager-detail li:nth-child(2) .fx-gesture-lpager-thumb {
  background: linear-gradient(200deg, var(--fx-gesture-lpager-from), var(--fx-gesture-lpager-to) 70%);
}
.fx-gesture-lpager-detail li:nth-child(3) .fx-gesture-lpager-thumb {
  background: radial-gradient(circle at 30% 30%, var(--fx-gesture-lpager-from), var(--fx-gesture-lpager-to) 75%);
}
.fx-gesture-lpager-meta {
  font-size: max(10px, 3.3cqw);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--fx-gesture-lpager-fg) 58%, transparent);
}
.fx-gesture-lpager-detail li small { color: color-mix(in srgb, var(--fx-gesture-lpager-fg) 55%, transparent); }

/* controls — fixed; the chevron is the one element that rides down */
.fx-gesture-lpager-play,
.fx-gesture-lpager-chev {
  position: absolute;
  z-index: 6;
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  border-radius: 50%;
  cursor: pointer;
  font: inherit;
}
.fx-gesture-lpager-play {
  right: 8cqw;
  bottom: 19.5%;
  width: max(40px, 15cqw);
  height: max(40px, 15cqw);
  background: var(--fx-gesture-lpager-fg);
  color: var(--fx-gesture-lpager-ink);
  transition: transform var(--fx-dur-1) var(--fx-ease-out);
}
.fx-gesture-lpager-play:active { transform: scale(0.94); }
.fx-gesture-lpager-play svg { width: 42%; height: 42%; }
.fx-gesture-lpager-chev {
  left: calc(50% - max(18px, 6cqw));
  top: 4%;
  width: max(36px, 12cqw);
  height: max(36px, 12cqw);
  background: transparent;
  color: var(--fx-gesture-lpager-fg);
  transform-origin: 50% 50%;
}
.fx-gesture-lpager-chev svg { width: 62%; height: 62%; }

/* Demo scaffolding: a portrait phone-sized stage centred in the preview. */
.fx-gesture-lpager-demo > [data-fx] { width: 224px; }

/* Reduced motion: no scrub, no scale animation, no travel. State changes
   are instant; opacity alone crossfades, capped at 100ms. */
@media (prefers-reduced-motion: reduce) {
  .fx-gesture-lpager-bg,
  .fx-gesture-lpager-scrim,
  .fx-gesture-lpager-photo,
  .fx-gesture-lpager-name,
  .fx-gesture-lpager-sub,
  .fx-gesture-lpager-detail > *,
  .fx-gesture-lpager-detail li,
  .fx-gesture-lpager-play { transition: opacity 100ms var(--fx-ease-out); }
  .fx-gesture-lpager-play:active { transform: none; }
}
:root[data-fx-reduced] .fx-gesture-lpager-bg,
:root[data-fx-reduced] .fx-gesture-lpager-scrim,
:root[data-fx-reduced] .fx-gesture-lpager-photo,
:root[data-fx-reduced] .fx-gesture-lpager-name,
:root[data-fx-reduced] .fx-gesture-lpager-sub,
:root[data-fx-reduced] .fx-gesture-lpager-detail > *,
:root[data-fx-reduced] .fx-gesture-lpager-detail li,
:root[data-fx-reduced] .fx-gesture-lpager-play { transition: opacity 100ms var(--fx-ease-out); }
:root[data-fx-reduced] .fx-gesture-lpager-play:active { transform: none; }

/* ══ src/effects/hover.css ══ */
/* =========================================================================
 * effects-lab — category: hover  ("Hover & pointer")
 * Pointer-driven micro-interactions. Every hover state is gated on
 * `(hover: hover) and (pointer: fine)` so a tap never triggers a phantom
 * hover, and every interactive effect is mirrored on :focus-visible so the
 * keyboard sees the same thing the mouse does.
 *
 * Timing house style:  in 120–220ms · out slightly slower than in.
 * Springs live in JS (hover.js); CSS handles the state-toggle cases.
 * ========================================================================= */

/* ---------------------------------------------------------------------------
 * 0. Shared furniture — buttons, cards, lists used by the copy-paste snippets
 * ------------------------------------------------------------------------ */
.fx-hover-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.55em;
  padding: 0.85em 1.5em;
  border: 1px solid var(--fx-border);
  border-radius: var(--fx-radius);
  background: var(--fx-surface-2);
  color: var(--fx-fg);
  font: 600 0.95rem/1 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  letter-spacing: -0.01em;
  text-decoration: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.fx-hover-btn--accent {
  background: var(--fx-accent);
  border-color: transparent;
  color: var(--fx-accent-contrast);
}
.fx-hover-btn--ghost { background: transparent; }

.fx-hover-card {
  box-sizing: border-box;
  width: min(300px, 100%);
  padding: 1.15rem 1.25rem 1.25rem;
  border: 1px solid var(--fx-border);
  border-radius: var(--fx-radius-lg);
  background: var(--fx-surface-2);
  color: var(--fx-fg);
  font: 400 0.9rem/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  text-decoration: none;
}
.fx-hover-card-kicker {
  margin: 0 0 0.5rem;
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--fx-accent);
}
.fx-hover-card-title { margin: 0 0 0.35rem; font-size: 1.05rem; font-weight: 650; letter-spacing: -0.015em; }
.fx-hover-card-body { margin: 0; color: var(--fx-muted); }

.fx-hover-hint {
  font: 600 0.68rem/1 ui-sans-serif, system-ui, -apple-system, sans-serif;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--fx-muted);
}
.fx-hover-stack { display: flex; flex-direction: column; align-items: center; gap: 1rem; }
.fx-hover-row { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 0.75rem; }

.fx-hover-list {
  list-style: none;
  margin: 0;
  padding: 0;
  width: min(360px, 100%);
  font: 400 1rem/1 ui-sans-serif, system-ui, -apple-system, sans-serif;
}
.fx-hover-list li + li { border-top: 1px solid var(--fx-border); }
.fx-hover-list a {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.8rem 0.15rem;
  color: var(--fx-fg);
  text-decoration: none;
  font-weight: 550;
  letter-spacing: -0.01em;
}
.fx-hover-list a span { color: var(--fx-muted); font-size: 0.78rem; font-weight: 500; }

/* ---------------------------------------------------------------------------
 * 1. hover-lift — raise + shadow + a hair of scale
 * ------------------------------------------------------------------------ */
.fx-hover-lift,
[data-fx~="hover-lift"] {
  --fx-hover-lift-y: -5px;
  --fx-hover-lift-scale: 1.012;
  transition:
    transform 240ms var(--fx-ease-out),
    box-shadow 240ms var(--fx-ease-out),
    border-color 240ms linear;
  will-change: transform;
}
@media (hover: hover) and (pointer: fine) {
  .fx-hover-lift:hover,
  [data-fx~="hover-lift"]:hover {
    transform: translate3d(0, var(--fx-hover-lift-y), 0) scale(var(--fx-hover-lift-scale));
    box-shadow: 0 2px 4px hsl(var(--fx-hue) 30% 4% / 0.28), 0 20px 42px -20px hsl(var(--fx-hue) 40% 3% / 0.8);
    border-color: color-mix(in oklab, var(--fx-accent) 42%, var(--fx-border));
    transition-duration: 170ms;
  }
}
.fx-hover-lift:focus-visible,
[data-fx~="hover-lift"]:focus-visible {
  transform: translate3d(0, var(--fx-hover-lift-y), 0) scale(var(--fx-hover-lift-scale));
  box-shadow: 0 2px 4px hsl(var(--fx-hue) 30% 4% / 0.28), 0 20px 42px -20px hsl(var(--fx-hue) 40% 3% / 0.8);
  border-color: color-mix(in oklab, var(--fx-accent) 42%, var(--fx-border));
  transition-duration: 170ms;
}

/* ---------------------------------------------------------------------------
 * 2. hover-press — press feedback, pointer + keyboard
 *    Deliberately NOT hover-gated: a press is real on touch too.
 * ------------------------------------------------------------------------ */
.fx-hover-press {
  --fx-hover-press-scale: 0.965;
  transition: transform 260ms var(--fx-ease-snap), background-color 200ms linear;
  will-change: transform;
  touch-action: manipulation;
}
@media (hover: hover) and (pointer: fine) {
  .fx-hover-press:hover { background-color: color-mix(in oklab, var(--fx-fg) 8%, var(--fx-surface-2)); }
  .fx-hover-press.fx-hover-btn--accent:hover { background-color: color-mix(in oklab, var(--fx-fg) 12%, var(--fx-accent)); }
}
.fx-hover-press:active,
.fx-hover-press.fx-hover-press-down {
  transform: scale(var(--fx-hover-press-scale));
  transition-duration: 110ms;
  transition-timing-function: var(--fx-ease-out);
}

/* ---------------------------------------------------------------------------
 * 3. hover-shine — diagonal highlight sweeps across
 * ------------------------------------------------------------------------ */
.fx-hover-shine,
[data-fx~="hover-shine"] {
  position: relative;
  overflow: hidden;
  isolation: isolate;
}
.fx-hover-shine::after,
[data-fx~="hover-shine"]::after {
  content: "";
  position: absolute;
  top: -60%;
  bottom: -60%;
  left: -40%;
  width: 34%;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(
    90deg,
    transparent,
    color-mix(in oklab, var(--fx-fg) 42%, transparent),
    transparent
  );
  transform: translate3d(-160%, 0, 0) skewX(-18deg);
  opacity: 0;
  transition: transform 380ms var(--fx-ease-out), opacity 200ms linear;
}
@media (hover: hover) and (pointer: fine) {
  .fx-hover-shine:hover::after,
  [data-fx~="hover-shine"]:hover::after {
    transform: translate3d(420%, 0, 0) skewX(-18deg);
    opacity: 1;
    transition: transform 620ms var(--fx-ease-out), opacity 120ms linear;
  }
}
.fx-hover-shine:focus-visible::after,
[data-fx~="hover-shine"]:focus-visible::after {
  transform: translate3d(420%, 0, 0) skewX(-18deg);
  opacity: 1;
  transition: transform 620ms var(--fx-ease-out), opacity 120ms linear;
}

/* ---------------------------------------------------------------------------
 * 4. hover-underline-grow — underline grows from the side you entered
 * ------------------------------------------------------------------------ */
.fx-hover-underline {
  --fx-hover-underline-h: 2px;
  --fx-hover-origin: 0%;
  position: relative;
  text-decoration: none;
  padding-bottom: 0.12em;
}
.fx-hover-underline::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: var(--fx-hover-underline-h);
  border-radius: 2px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: var(--fx-hover-origin) 50%;
  transition: transform 260ms var(--fx-ease-out);
  pointer-events: none;
}
@media (hover: hover) and (pointer: fine) {
  .fx-hover-underline:hover::after {
    transform: scaleX(1);
    transition-duration: 200ms;
  }
}
.fx-hover-underline:focus-visible::after {
  transform: scaleX(1);
  transition-duration: 200ms;
}

/* ---------------------------------------------------------------------------
 * 5. hover-text-swap — label slides up, its twin slides in from below
 * ------------------------------------------------------------------------ */
.fx-hover-swap {
  position: relative;
  display: inline-block;
  overflow: hidden;
  line-height: 1.3;
}
.fx-hover-swap-a,
.fx-hover-swap-b {
  display: block;
  white-space: nowrap;
  line-height: inherit;
  transition: transform 260ms var(--fx-ease-out);
}
.fx-hover-swap-b {
  position: absolute;
  top: 0;
  left: 0;
  transform: translate3d(0, 100%, 0);
}
@media (hover: hover) and (pointer: fine) {
  .fx-hover-swap-host:hover .fx-hover-swap-a { transform: translate3d(0, -100%, 0); transition-duration: 200ms; }
  .fx-hover-swap-host:hover .fx-hover-swap-b { transform: translate3d(0, 0, 0); transition-duration: 200ms; }
}
.fx-hover-swap-host:focus-visible .fx-hover-swap-a { transform: translate3d(0, -100%, 0); transition-duration: 200ms; }
.fx-hover-swap-host:focus-visible .fx-hover-swap-b { transform: translate3d(0, 0, 0); transition-duration: 200ms; }

/* ---------------------------------------------------------------------------
 * 6. hover-icon-nudge — the arrow moves, the label stays put
 * ------------------------------------------------------------------------ */
.fx-hover-nudge,
[data-fx~="hover-icon-nudge"] {
  --fx-hover-nudge-x: 4px;
  --fx-hover-nudge-y: 0px;
}
.fx-hover-nudge-icon {
  display: inline-block;
  transition: transform 230ms var(--fx-ease-out);
  will-change: transform;
}
@media (hover: hover) and (pointer: fine) {
  .fx-hover-nudge:hover .fx-hover-nudge-icon,
  [data-fx~="hover-icon-nudge"]:hover .fx-hover-nudge-icon {
    transform: translate3d(var(--fx-hover-nudge-x), var(--fx-hover-nudge-y), 0);
    transition-duration: 160ms;
  }
}
.fx-hover-nudge:focus-visible .fx-hover-nudge-icon,
[data-fx~="hover-icon-nudge"]:focus-visible .fx-hover-nudge-icon {
  transform: translate3d(var(--fx-hover-nudge-x), var(--fx-hover-nudge-y), 0);
  transition-duration: 160ms;
}

/* ---------------------------------------------------------------------------
 * 7. hover-ripple — circle expands from the press point
 * ------------------------------------------------------------------------ */
.fx-hover-ripple-host {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  touch-action: manipulation;
  transition: background-color 180ms linear;
}
.fx-hover-ripple-dot {
  position: absolute;
  z-index: 0;
  border-radius: 50%;
  pointer-events: none;
  background: radial-gradient(circle, currentColor 0%, color-mix(in oklab, currentColor 20%, transparent) 62%, transparent 72%);
  transform: translate(-50%, -50%) scale(0.05);
  opacity: 0;
}

/* ---------------------------------------------------------------------------
 * 8. hover-spotlight — radial light tracks the cursor inside the element
 * ------------------------------------------------------------------------ */
.fx-hover-spotlight {
  --fx-hover-x: 50%;
  --fx-hover-y: 50%;
  --fx-hover-spot: 180px;
  position: relative;
  overflow: hidden;
  isolation: isolate;
}
.fx-hover-spotlight::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  border-radius: inherit;
  pointer-events: none;
  background: radial-gradient(
    var(--fx-hover-spot) circle at var(--fx-hover-x) var(--fx-hover-y),
    color-mix(in oklab, var(--fx-accent) 34%, transparent),
    transparent 62%
  );
  opacity: 0;
  transition: opacity 260ms linear;
}
.fx-hover-spotlight > * { position: relative; z-index: 1; }
@media (hover: hover) and (pointer: fine) {
  .fx-hover-spotlight:hover::before { opacity: 1; transition-duration: 150ms; }
}
.fx-hover-spotlight:focus-visible::before,
.fx-hover-spotlight:focus-within::before { opacity: 1; transition-duration: 150ms; }

/* ---------------------------------------------------------------------------
 * 9. hover-border-glow — the light rides the border edge near the cursor
 * ------------------------------------------------------------------------ */
.fx-hover-borderglow {
  --fx-hover-x: 50%;
  --fx-hover-y: 50%;
  --fx-hover-glow: 150px;
  position: relative;
  isolation: isolate;
}
.fx-hover-borderglow::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  border-radius: inherit;
  padding: 1.5px;
  pointer-events: none;
  background: radial-gradient(
    var(--fx-hover-glow) circle at var(--fx-hover-x) var(--fx-hover-y),
    var(--fx-accent),
    color-mix(in oklab, var(--fx-accent-2) 70%, transparent) 40%,
    transparent 72%
  );
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  opacity: 0;
  transition: opacity 280ms linear;
}
@media (hover: hover) and (pointer: fine) {
  .fx-hover-borderglow:hover::after { opacity: 1; transition-duration: 160ms; }
}
.fx-hover-borderglow:focus-visible::after,
.fx-hover-borderglow:focus-within::after { opacity: 1; transition-duration: 160ms; }

/* ---------------------------------------------------------------------------
 * 10. hover-tilt-3d — perspective tilt with an optional glare sheet
 * ------------------------------------------------------------------------ */
.fx-hover-tilt-on {
  position: relative;
  transform-style: preserve-3d;
  will-change: transform;
}
.fx-hover-tilt-glare {
  --fx-hover-gx: 50%;
  --fx-hover-gy: 50%;
  position: absolute;
  inset: 0;
  z-index: 3;
  border-radius: inherit;
  pointer-events: none;
  background: radial-gradient(
    60% 60% at var(--fx-hover-gx) var(--fx-hover-gy),
    color-mix(in oklab, var(--fx-fg) 26%, transparent),
    transparent 70%
  );
  opacity: 0;
  transition: opacity 260ms linear;
}
@media (hover: hover) and (pointer: fine) {
  .fx-hover-tilt-on:hover .fx-hover-tilt-glare { opacity: 1; transition-duration: 150ms; }
}

/* ---------------------------------------------------------------------------
 * 11. hover-magnetic / hover-cursor-repel — JS springs, CSS only preps layers
 * ------------------------------------------------------------------------ */
.fx-hover-magnetic-on,
.fx-hover-repel-on { will-change: transform; }

/* ---------------------------------------------------------------------------
 * 12. hover-image-follow — a preview card rides the cursor over a link list
 * ------------------------------------------------------------------------ */
.fx-hover-follow-host { position: relative; }
.fx-hover-follow-card {
  --fx-hover-tone: 0;
  position: absolute;
  top: 0;
  left: 0;
  z-index: 5;
  display: grid;
  place-items: center;
  overflow: hidden;
  border-radius: var(--fx-radius);
  border: 1px solid var(--fx-border);
  background:
    linear-gradient(
      140deg,
      hsl(calc(var(--fx-hue) + var(--fx-hover-tone)) 80% 58%),
      hsl(calc(var(--fx-hue) + var(--fx-hover-tone) - 50) 70% 42%)
    );
  box-shadow: var(--fx-shadow);
  color: var(--fx-accent-contrast);
  font: 700 0.8rem/1 ui-sans-serif, system-ui, -apple-system, sans-serif;
  letter-spacing: 0.02em;
  pointer-events: none;
  opacity: 0;
  scale: 0.86;
  transition: opacity 240ms linear, scale 240ms var(--fx-ease-out);
}
.fx-hover-follow-card.fx-hover-is-on {
  opacity: 1;
  scale: 1;
  transition-duration: 160ms;
}
.fx-hover-follow-card img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* ---------------------------------------------------------------------------
 * 13. hover-depth-parallax — children drift by their data-fx-depth
 * ------------------------------------------------------------------------ */
.fx-hover-depth-host { position: relative; }
.fx-hover-depth-host [data-fx-depth] { will-change: transform; }
.fx-hover-scene {
  position: relative;
  display: grid;
  place-items: center;
  width: min(340px, 100%);
  height: 190px;
  border: 1px solid var(--fx-border);
  border-radius: var(--fx-radius-lg);
  background: radial-gradient(120% 120% at 50% 110%, var(--fx-surface-2), var(--fx-surface));
  overflow: hidden;
}
.fx-hover-scene-layer {
  position: absolute;
  display: grid;
  place-items: center;
  border-radius: var(--fx-radius);
  font: 600 0.72rem/1 ui-sans-serif, system-ui, -apple-system, sans-serif;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.fx-hover-scene-back {
  inset: 22% 14% auto auto;
  width: 96px; height: 60px;
  background: color-mix(in oklab, var(--fx-accent-3) 34%, var(--fx-surface-2));
  color: var(--fx-muted);
  border: 1px solid var(--fx-border);
}
.fx-hover-scene-mid {
  inset: auto auto 20% 12%;
  width: 104px; height: 64px;
  background: color-mix(in oklab, var(--fx-accent-2) 38%, var(--fx-surface-2));
  color: var(--fx-fg);
  border: 1px solid var(--fx-border);
}
.fx-hover-scene-front {
  width: 150px; height: 82px;
  background: var(--fx-accent);
  color: var(--fx-accent-contrast);
  box-shadow: var(--fx-shadow);
}

/* =========================================================================
 * REDUCED MOTION
 * Hover still reads as a state change — colour/opacity only. No travel,
 * no tilt, no sweep, no spring. Content stays complete and usable.
 * (JS effects bail out in init(); these rules cover the CSS-only ones.)
 * ========================================================================= */
@media (prefers-reduced-motion: reduce) {
  .fx-hover-lift, [data-fx~="hover-lift"],
  .fx-hover-press,
  .fx-hover-nudge-icon,
  .fx-hover-swap-a, .fx-hover-swap-b,
  .fx-hover-underline::after,
  .fx-hover-shine::after, [data-fx~="hover-shine"]::after,
  .fx-hover-spotlight::before,
  .fx-hover-borderglow::after,
  .fx-hover-tilt-glare,
  .fx-hover-follow-card { transition-duration: 1ms !important; }

  .fx-hover-lift:hover, [data-fx~="hover-lift"]:hover,
  .fx-hover-lift:focus-visible, [data-fx~="hover-lift"]:focus-visible {
    transform: none;
    box-shadow: none;
    border-color: var(--fx-accent);
  }
  .fx-hover-press:active, .fx-hover-press.fx-hover-press-down { transform: none; }
  .fx-hover-shine::after, [data-fx~="hover-shine"]::after { display: none; }
  .fx-hover-nudge:hover .fx-hover-nudge-icon,
  [data-fx~="hover-icon-nudge"]:hover .fx-hover-nudge-icon,
  .fx-hover-nudge:focus-visible .fx-hover-nudge-icon,
  [data-fx~="hover-icon-nudge"]:focus-visible .fx-hover-nudge-icon { transform: none; }
  .fx-hover-underline::after { transform: scaleX(1); opacity: 0; transition: opacity 1ms linear; }
  .fx-hover-underline:hover::after,
  .fx-hover-underline:focus-visible::after { transform: scaleX(1); opacity: 1; }
  .fx-hover-follow-card { scale: 1; }
}

:root[data-fx-reduced] .fx-hover-lift,
:root[data-fx-reduced] [data-fx~="hover-lift"],
:root[data-fx-reduced] .fx-hover-press,
:root[data-fx-reduced] .fx-hover-nudge-icon,
:root[data-fx-reduced] .fx-hover-swap-a,
:root[data-fx-reduced] .fx-hover-swap-b,
:root[data-fx-reduced] .fx-hover-underline::after,
:root[data-fx-reduced] .fx-hover-shine::after,
:root[data-fx-reduced] [data-fx~="hover-shine"]::after,
:root[data-fx-reduced] .fx-hover-spotlight::before,
:root[data-fx-reduced] .fx-hover-borderglow::after,
:root[data-fx-reduced] .fx-hover-tilt-glare,
:root[data-fx-reduced] .fx-hover-follow-card { transition-duration: 1ms !important; }

:root[data-fx-reduced] .fx-hover-lift:hover,
:root[data-fx-reduced] [data-fx~="hover-lift"]:hover,
:root[data-fx-reduced] .fx-hover-lift:focus-visible,
:root[data-fx-reduced] [data-fx~="hover-lift"]:focus-visible {
  transform: none;
  box-shadow: none;
  border-color: var(--fx-accent);
}
:root[data-fx-reduced] .fx-hover-press:active,
:root[data-fx-reduced] .fx-hover-press.fx-hover-press-down { transform: none; }
:root[data-fx-reduced] .fx-hover-shine::after,
:root[data-fx-reduced] [data-fx~="hover-shine"]::after { display: none; }
:root[data-fx-reduced] .fx-hover-nudge:hover .fx-hover-nudge-icon,
:root[data-fx-reduced] [data-fx~="hover-icon-nudge"]:hover .fx-hover-nudge-icon,
:root[data-fx-reduced] .fx-hover-nudge:focus-visible .fx-hover-nudge-icon,
:root[data-fx-reduced] [data-fx~="hover-icon-nudge"]:focus-visible .fx-hover-nudge-icon { transform: none; }
:root[data-fx-reduced] .fx-hover-underline::after { transform: scaleX(1); opacity: 0; transition: opacity 1ms linear; }
:root[data-fx-reduced] .fx-hover-underline:hover::after,
:root[data-fx-reduced] .fx-hover-underline:focus-visible::after { transform: scaleX(1); opacity: 1; }
:root[data-fx-reduced] .fx-hover-follow-card { scale: 1; }

/* Reduced motion still needs a legible *state* — swap travel for colour. ---- */
@media (prefers-reduced-motion: reduce) {
  .fx-hover-shine:hover, [data-fx~="hover-shine"]:hover,
  .fx-hover-shine:focus-visible, [data-fx~="hover-shine"]:focus-visible,
  .fx-hover-swap-host:hover, .fx-hover-swap-host:focus-visible,
  .fx-hover-ripple-host:hover {
    border-color: var(--fx-accent);
    background-color: color-mix(in oklab, var(--fx-accent) 16%, var(--fx-surface-2));
  }
  .fx-hover-ripple-host:active {
    background-color: color-mix(in oklab, var(--fx-accent) 34%, var(--fx-surface-2));
  }
  .fx-hover-swap-host:hover .fx-hover-swap-a,
  .fx-hover-swap-host:focus-visible .fx-hover-swap-a { transform: none; }
  .fx-hover-swap-host:hover .fx-hover-swap-b,
  .fx-hover-swap-host:focus-visible .fx-hover-swap-b { transform: translate3d(0, 100%, 0); }
}
:root[data-fx-reduced] .fx-hover-shine:hover,
:root[data-fx-reduced] [data-fx~="hover-shine"]:hover,
:root[data-fx-reduced] .fx-hover-shine:focus-visible,
:root[data-fx-reduced] [data-fx~="hover-shine"]:focus-visible,
:root[data-fx-reduced] .fx-hover-swap-host:hover,
:root[data-fx-reduced] .fx-hover-swap-host:focus-visible,
:root[data-fx-reduced] .fx-hover-ripple-host:hover {
  border-color: var(--fx-accent);
  background-color: color-mix(in oklab, var(--fx-accent) 16%, var(--fx-surface-2));
}
:root[data-fx-reduced] .fx-hover-ripple-host:active {
  background-color: color-mix(in oklab, var(--fx-accent) 34%, var(--fx-surface-2));
}
:root[data-fx-reduced] .fx-hover-swap-host:hover .fx-hover-swap-a,
:root[data-fx-reduced] .fx-hover-swap-host:focus-visible .fx-hover-swap-a { transform: none; }
:root[data-fx-reduced] .fx-hover-swap-host:hover .fx-hover-swap-b,
:root[data-fx-reduced] .fx-hover-swap-host:focus-visible .fx-hover-swap-b { transform: translate3d(0, 100%, 0); }

/* ══ src/effects/ios.css ══ */
/* =========================================================================
 * effects-lab — category: ios  ("iOS")
 * Everything here is namespaced .fx-ios-* / @keyframes fx-ios-*.
 * Colours come from src/core/00-tokens.css. The two semantic colours iOS
 * keeps fixed regardless of brand hue (destructive red, warning orange)
 * follow load.css's precedent: overridable hooks with a fallback.
 * ========================================================================= */

/* ---------------------------------------------------------------- shared */
.fx-ios,
.fx-ios-demo {
  --fx-ios-font: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --fx-ios-hair: color-mix(in srgb, var(--fx-fg) 14%, transparent);
  --fx-ios-fill: color-mix(in srgb, var(--fx-fg) 9%, transparent);
  --fx-ios-fill-2: color-mix(in srgb, var(--fx-fg) 16%, transparent);
  --fx-ios-thumb: color-mix(in srgb, var(--fx-fg) 24%, var(--fx-surface));
  --fx-ios-material-bg: color-mix(in srgb, var(--fx-surface-2) 70%, transparent);
  --fx-ios-material-fx: saturate(180%) blur(22px);
  --fx-ios-destructive: var(--fx-danger, hsl(4 80% 58%));
  --fx-ios-warn: var(--fx-warning, hsl(36 94% 54%));
  --fx-ios-ink: hsl(var(--fx-hue) 14% 4%);
  --fx-ios-on-ink: hsl(var(--fx-hue) 15% 97%);
  --fx-ios-shadow: hsl(var(--fx-hue) 30% 2% / 0.5);
  --fx-ios-wall: radial-gradient(120% 80% at 15% 0%, color-mix(in srgb, var(--fx-accent) 60%, var(--fx-bg)) 0%, transparent 60%),
                 radial-gradient(90% 70% at 100% 100%, color-mix(in srgb, var(--fx-accent-2) 45%, var(--fx-bg)) 0%, transparent 65%),
                 var(--fx-bg);
}
:root[data-fx-theme="light"] .fx-ios,
:root[data-fx-theme="light"] .fx-ios-demo {
  --fx-ios-thumb: var(--fx-surface);
  --fx-ios-shadow: hsl(var(--fx-hue) 30% 20% / 0.22);
}

.fx-ios {
  color: var(--fx-fg);
  font: 400 15px/1.35 var(--fx-ios-font);
  -webkit-tap-highlight-color: transparent;
}
.fx-ios *,
.fx-ios *::before,
.fx-ios *::after { box-sizing: border-box; }
.fx-ios :focus-visible { outline: 2px solid var(--fx-accent); outline-offset: 2px; }

.fx-ios-material {
  background: var(--fx-ios-material-bg);
  -webkit-backdrop-filter: var(--fx-ios-material-fx);
  backdrop-filter: var(--fx-ios-material-fx);
}
@media (prefers-reduced-transparency: reduce) {
  .fx-ios-material { background: var(--fx-surface-2); -webkit-backdrop-filter: none; backdrop-filter: none; }
  .fx-ios-cm-backdrop { background: color-mix(in srgb, var(--fx-bg) 88%, transparent) !important; -webkit-backdrop-filter: none !important; backdrop-filter: none !important; }
}
@media (prefers-contrast: more) {
  .fx-ios-material { background: var(--fx-surface-2); box-shadow: inset 0 0 0 1px var(--fx-border); }
}

.fx-ios-sr {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ------------------------------------------------ demo scaffolding (gallery) */
.fx-ios-demo {
  display: grid;
  justify-items: center;
  align-content: center;
  gap: 10px;
  width: 100%;
  color: var(--fx-fg);
  font: 400 15px/1.35 var(--fx-ios-font);
}
.fx-ios-device {
  position: relative;
  width: 252px;
  height: 420px;
  border-radius: 38px;
  overflow: hidden;
  isolation: isolate;
  background: var(--fx-bg);
  color: var(--fx-fg);
  box-shadow: 0 0 0 1px var(--fx-border), 0 0 0 6px var(--fx-surface-2), 0 0 0 7px var(--fx-border), var(--fx-shadow);
}
.fx-ios-device.is-short { height: 262px; width: 280px; border-radius: 34px; }
.fx-ios-hint {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin: 0;
  font: 600 11px/1 var(--fx-ios-font);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fx-muted);
}
.fx-ios-hint::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--fx-accent); }

/* grouped-list vocabulary shared by several demos */
.fx-ios-caption {
  margin: 16px 30px 6px;
  font-size: 12px;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--fx-muted);
}
.fx-ios-group {
  margin: 0 14px 12px;
  padding: 0;
  list-style: none;
  border-radius: 12px;
  overflow: hidden;
  background: var(--fx-surface);
}
.fx-ios-row {
  position: relative;
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  min-height: 44px;
  padding: 10px 14px;
  border: 0;
  background: none;
  color: inherit;
  font: 400 15px/1.25 var(--fx-ios-font);
  text-align: left;
}
.fx-ios-row + .fx-ios-row::before,
.fx-ios-group > li + li > .fx-ios-row::before {
  content: "";
  position: absolute;
  top: 0; left: 14px; right: 0;
  border-top: 0.5px solid var(--fx-ios-hair);
}
.fx-ios-row small { flex: none; margin-left: auto; font-size: 14px; color: var(--fx-muted); white-space: nowrap; }
button.fx-ios-row { cursor: pointer; transition: background-color 0.16s ease; }
button.fx-ios-row:active { background: var(--fx-ios-fill); transition: none; }
.fx-ios-glyph {
  flex: none;
  display: grid;
  place-items: center;
  width: 28px; height: 28px;
  border-radius: 7px;
  color: var(--fx-ios-on-ink);
  background: linear-gradient(160deg, hsl(calc(var(--fx-hue) + var(--fx-ios-app, 0)) 72% 60%), hsl(calc(var(--fx-hue) + var(--fx-ios-app, 0) + 26) 66% 42%));
}
.fx-ios-glyph svg { width: 16px; height: 16px; }

/* =============================================== 1 · ios-large-title ==== */
.fx-ios-lt {
  position: relative;
  overflow-y: auto;
  overscroll-behavior-y: contain;
  scrollbar-width: none;
  background: var(--fx-bg);
  outline-offset: -2px;
}
.fx-ios-lt::-webkit-scrollbar { display: none; }
.fx-ios-lt-bar {
  position: sticky;
  top: 0;
  z-index: 3;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  padding: 0 14px;
}
.fx-ios-lt-mat {
  position: absolute;
  inset: 0;
  opacity: 0;
  border-bottom: 0.5px solid var(--fx-ios-hair);
  pointer-events: none;
}
.fx-ios-lt-small {
  position: absolute;
  left: 50%;
  top: 50%;
  max-width: 55%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 600;
  font-size: 16px;
  letter-spacing: -0.01em;
  opacity: 0;
  transform: translate(-50%, -50%) translateY(5px);
  transition: opacity 0.18s var(--fx-ease-out), transform 0.26s var(--fx-ease-out);
}
.fx-ios-lt.is-compact .fx-ios-lt-small { opacity: 1; transform: translate(-50%, -50%); }
.fx-ios-lt-actions { position: relative; display: flex; gap: 16px; }
.fx-ios-lt-actions button {
  border: 0; padding: 0; background: none; cursor: pointer;
  color: var(--fx-accent); font: 400 16px/1 var(--fx-ios-font);
}
.fx-ios-lt-large {
  margin: 0 16px 10px;
  font-size: 32px;
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: -0.022em;
  transform-origin: left center;
}

/* =============================================== 2 · ios-context-menu === */
.fx-ios-cm { position: relative; overflow: hidden; isolation: isolate; }
.fx-ios-cm-trigger {
  touch-action: pan-y;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  cursor: default;
}
.fx-ios-cm-layer { position: absolute; inset: 0; z-index: 20; pointer-events: none; }
.fx-ios-scope-page .fx-ios-cm-layer { position: fixed; }
.fx-ios-cm-layer.is-open { pointer-events: auto; }
.fx-ios-cm-backdrop {
  position: absolute;
  inset: 0;
  opacity: 0;
  background: color-mix(in srgb, var(--fx-bg) 38%, transparent);
  -webkit-backdrop-filter: blur(14px) saturate(120%);
  backdrop-filter: blur(14px) saturate(120%);
  transition: opacity 0.2s var(--fx-ease-out);
}
.fx-ios-cm-layer.is-open > .fx-ios-cm-backdrop { opacity: 1; transition-duration: 0.28s; }
.fx-ios-cm-lift {
  position: absolute;
  z-index: 1;
  pointer-events: none;
  box-shadow: 0 18px 40px -14px var(--fx-ios-shadow);
  will-change: transform;
}
.fx-ios-cm-menu {
  position: absolute;
  z-index: 2;
  min-width: 188px;
  max-width: calc(100% - 20px);
  margin: 0;
  padding: 0;
  list-style: none;
  border-radius: 14px;
  overflow: hidden;
  opacity: 0;
  box-shadow: 0 14px 40px -12px var(--fx-ios-shadow);
  will-change: transform, opacity;
  outline: none;
}
.fx-ios-cm-layer:not(.is-open) .fx-ios-cm-menu { pointer-events: none; }
.fx-ios-cm-menu li { margin: 0; }
.fx-ios-cm-menu li + li { border-top: 0.5px solid var(--fx-ios-hair); }
.fx-ios-cm-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 18px;
  width: 100%;
  min-height: 42px;
  padding: 0 14px 0 16px;
  border: 0;
  background: none;
  color: var(--fx-fg);
  font: 400 15px/1.2 var(--fx-ios-font);
  text-align: left;
  cursor: pointer;
}
.fx-ios-cm-item svg { flex: none; width: 17px; height: 17px; }
.fx-ios-cm-item.is-hot,
.fx-ios-cm-item:focus-visible { background: var(--fx-ios-fill-2); outline: none; }
.fx-ios-cm-item.is-destructive { color: var(--fx-ios-destructive); }

/* demo: message thread */
.fx-ios-chat {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 6px;
  height: 100%;
  padding: 18px 12px 14px;
  background: var(--fx-bg);
}
.fx-ios-chat-day { align-self: center; margin-bottom: 6px; font-size: 11px; color: var(--fx-muted); }
.fx-ios-bubble {
  align-self: flex-start;
  max-width: 78%;
  padding: 8px 12px;
  border-radius: 18px;
  font-size: 14px;
  line-height: 1.3;
  background: var(--fx-surface-2);
  color: var(--fx-fg);
}
.fx-ios-bubble.is-me { align-self: flex-end; background: var(--fx-accent); color: var(--fx-accent-contrast); }
.fx-ios-chat-status { min-height: 16px; margin-top: 6px; align-self: center; font-size: 11px; color: var(--fx-muted); }

/* =============================================== 3 · ios-swipe-actions == */
.fx-ios-sw {
  width: 100%;
  margin: 0;
  padding: 0;
  list-style: none;
  border-radius: 12px;
  overflow: hidden;
  background: var(--fx-surface-2);
}
.fx-ios-sw-row { position: relative; overflow: hidden; touch-action: pan-y; }
.fx-ios-sw-row[hidden] { display: none; }
.fx-ios-sw-face {
  position: relative;
  z-index: 5;
  background: var(--fx-surface-2);
  touch-action: pan-y;
  user-select: none;
  -webkit-user-select: none;
  cursor: grab;
  will-change: transform;
  outline-offset: -2px;
}
.fx-ios-sw-face.is-dragging { cursor: grabbing; }
.fx-ios-sw-row + .fx-ios-sw-row .fx-ios-sw-face::before {
  content: "";
  position: absolute;
  top: 0; left: 30px; right: 0;
  border-top: 0.5px solid var(--fx-ios-hair);
}
.fx-ios-sw-side { position: absolute; inset: 0; visibility: hidden; }
.fx-ios-sw-btn {
  position: absolute;
  top: 0; bottom: 0; left: 0;
  width: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  color: var(--fx-ios-on-ink);
  background: var(--fx-accent);
  cursor: pointer;
  will-change: transform;
}
.fx-ios-sw-btn:focus-visible { outline: 2px solid var(--fx-ios-on-ink); outline-offset: -4px; }
.fx-ios-sw-btn.is-warn { background: var(--fx-ios-warn); }
.fx-ios-sw-btn.is-destructive { background: var(--fx-ios-destructive); }
.fx-ios-sw-btn.is-neutral { background: color-mix(in srgb, var(--fx-muted) 70%, var(--fx-surface)); }
.fx-ios-sw-label {
  position: absolute;
  top: 0; bottom: 0; left: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  padding: 0 14px;
  font: 500 12.5px/1 var(--fx-ios-font);
  white-space: nowrap;
}
.fx-ios-sw-label svg { width: 18px; height: 18px; }

/* demo: mailbox row */
.fx-ios-mail {
  display: grid;
  grid-template-columns: 10px 1fr auto;
  column-gap: 10px;
  row-gap: 2px;
  padding: 10px 14px 10px 10px;
}
.fx-ios-mail .fx-ios-dot {
  grid-row: span 2;
  align-self: start;
  width: 9px; height: 9px;
  margin-top: 5px;
  border-radius: 50%;
  background: var(--fx-accent);
  visibility: hidden;
}
.fx-ios-sw-row.is-unread .fx-ios-dot { visibility: visible; }
.fx-ios-mail strong { font-size: 15px; font-weight: 600; }
.fx-ios-mail time { font-size: 13px; color: var(--fx-muted); }
.fx-ios-mail span { grid-column: 2 / -1; font-size: 13.5px; color: var(--fx-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.fx-ios-sw-demo { width: 300px; }

/* =============================================== 4 · ios-segmented ====== */
.fx-ios-seg {
  position: relative;
  display: grid;
  grid-template-columns: repeat(var(--fx-ios-seg-n, 3), minmax(0, 1fr));
  min-width: 200px;
  padding: 2px;
  border-radius: 9px;
  background: var(--fx-ios-fill);
  isolation: isolate;
  user-select: none;
  -webkit-user-select: none;
  touch-action: pan-y;
}
.fx-ios-seg-thumb {
  position: absolute;
  top: 2px; bottom: 2px; left: 2px;
  width: calc((100% - 4px) / var(--fx-ios-seg-n, 3));
  border-radius: 7px;
  background: var(--fx-ios-thumb);
  box-shadow: 0 3px 8px var(--fx-ios-shadow), 0 0 0 0.5px var(--fx-ios-hair);
  pointer-events: none;
  will-change: transform;
}
.fx-ios-seg-sep {
  position: absolute;
  top: 9px; bottom: 9px;
  width: 1px;
  margin-left: -0.5px;
  background: var(--fx-ios-hair);
  pointer-events: none;
}
.fx-ios-seg-item {
  position: relative;
  z-index: 1;
  min-height: 30px;
  padding: 7px 10px;
  border: 0;
  border-radius: 7px;
  background: none;
  color: var(--fx-fg);
  font: 500 13px/1 var(--fx-ios-font);
  cursor: pointer;
  transition: opacity 0.16s ease;
}
.fx-ios-seg-item.is-selected { font-weight: 600; }
.fx-ios-seg-item.is-pressed { opacity: 0.45; transition: none; }
.fx-ios-seg-item:focus-visible { outline-offset: -2px; }
.fx-ios-seg-demo .fx-ios-seg { width: 300px; }

/* =============================================== 5 · ios-island ========= */
.fx-ios-isl { position: relative; width: 100%; height: 150px; pointer-events: none; }
.fx-ios-isl-surface {
  position: absolute;
  inset: 0;
  background: var(--fx-ios-ink);
  color: var(--fx-ios-on-ink);
  clip-path: inset(0 calc(50% - 59px) calc(100% - 34px) calc(50% - 59px) round 17px);
  will-change: clip-path;
}
.fx-ios-isl-toggle {
  position: absolute;
  z-index: 1;
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: pointer;
  pointer-events: auto;
}
.fx-ios-isl-toggle:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--fx-accent); }
.fx-ios-isl-compact {
  position: absolute;
  top: 0; left: 50%;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 12px 0 7px;
  transform: translateX(-50%);
  font: 600 13px/1 var(--fx-ios-font);
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}
.fx-ios-isl-expanded {
  position: absolute;
  inset: 4px;
  z-index: 2;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 12px;
  padding: 14px 18px;
  opacity: 0;
  pointer-events: none;
}
.fx-ios-isl-expanded button { pointer-events: auto; }
.fx-ios-isl-dot {
  display: grid; place-items: center;
  width: 22px; height: 22px;
  border-radius: 50%;
  background: var(--fx-accent);
  color: var(--fx-accent-contrast);
}
.fx-ios-isl-dot svg { width: 13px; height: 13px; }
.fx-ios-isl-time { color: var(--fx-accent); }
.fx-ios-isl-row { display: flex; align-items: center; gap: 10px; }
.fx-ios-isl-row .fx-ios-isl-dot { width: 40px; height: 40px; }
.fx-ios-isl-row .fx-ios-isl-dot svg { width: 20px; height: 20px; }
.fx-ios-isl-meta { display: grid; gap: 3px; font-size: 13px; }
.fx-ios-isl-meta small { font-size: 12px; opacity: 0.6; }
.fx-ios-isl-big {
  margin-left: auto;
  color: var(--fx-accent);
  font: 600 32px/1 var(--fx-ios-font);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
}
.fx-ios-isl-bar {
  height: 4px;
  border-radius: 2px;
  overflow: hidden;
  background: color-mix(in srgb, var(--fx-ios-on-ink) 18%, transparent);
}
.fx-ios-isl-bar i { display: block; height: 100%; background: var(--fx-accent); transform-origin: left center; }
.fx-ios-isl-btn {
  align-self: flex-start;
  padding: 7px 14px;
  border: 0;
  border-radius: 99px;
  background: color-mix(in srgb, var(--fx-ios-on-ink) 16%, transparent);
  color: var(--fx-ios-on-ink);
  font: 600 12px/1 var(--fx-ios-font);
  cursor: pointer;
}
/* demo: a phone top with wallpaper and status bar */
.fx-ios-screen { background: var(--fx-ios-wall); }
.fx-ios-screen > .fx-ios-isl { position: absolute; top: 8px; left: 8px; right: 8px; width: auto; }
.fx-ios-status {
  position: absolute;
  top: 16px; left: 26px; right: 22px;
  display: flex;
  justify-content: space-between;
  font: 600 13px/1 var(--fx-ios-font);
}
.fx-ios-status i { display: inline-block; width: 22px; height: 10px; border-radius: 3px; box-shadow: inset 0 0 0 1.5px currentColor; position: relative; }
.fx-ios-status i::after { content: ""; position: absolute; inset: 2.5px 6px 2.5px 2.5px; border-radius: 1.5px; background: currentColor; }
.fx-ios-clock {
  position: absolute;
  left: 0; right: 0; bottom: 34px;
  text-align: center;
}
.fx-ios-clock b { display: block; font: 300 58px/1 var(--fx-ios-font); letter-spacing: -0.02em; }
.fx-ios-clock span { font-size: 14px; font-weight: 500; opacity: 0.8; }

/* =============================================== 6 · ios-card-open ====== */
.fx-ios-co { position: relative; overflow: hidden; isolation: isolate; }
.fx-ios-co-card {
  position: relative;
  display: block;
  border-radius: 20px;
  overflow: hidden;
  background: var(--fx-surface);
  box-shadow: 0 10px 24px -14px var(--fx-ios-shadow);
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
  transition: transform 0.2s var(--fx-ease-out);
}
.fx-ios-co-card:active { transform: scale(0.97); transition-duration: 0.12s; }
.fx-ios-co-card [data-fx-body] { display: none; }
.fx-ios-co-cover {
  position: relative;
  height: var(--fx-ios-co-h, 200px);
  color: var(--fx-ios-on-ink);
  background: linear-gradient(160deg, hsl(calc(var(--fx-hue) + var(--fx-ios-app, 20)) 70% 58%), hsl(calc(var(--fx-hue) + var(--fx-ios-app, 20) - 60) 62% 26%));
}
.fx-ios-co-cover-in { position: absolute; left: 18px; right: 18px; bottom: 18px; }
.fx-ios-co-cover-in small {
  display: block;
  margin-bottom: 4px;
  font: 600 11px/1.2 var(--fx-ios-font);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  opacity: 0.78;
}
.fx-ios-co-cover-in h3 { margin: 0; font: 700 23px/1.1 var(--fx-ios-font); letter-spacing: -0.02em; }
.fx-ios-co-layer { position: absolute; inset: 0; z-index: 20; pointer-events: none; visibility: hidden; }
.fx-ios-scope-page .fx-ios-co-layer { position: fixed; }
.fx-ios-co.is-card-open .fx-ios-co-layer { visibility: visible; pointer-events: auto; }
.fx-ios-co-scrim { position: absolute; inset: 0; opacity: 0; background: color-mix(in srgb, var(--fx-bg) 55%, transparent); }
.fx-ios-co-panel {
  position: absolute;
  inset: 0;
  background: var(--fx-bg);
  transform-origin: 50% 0;
  will-change: transform, clip-path;
  outline: none;
}
.fx-ios-co-scroll {
  position: absolute;
  inset: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: none;
  outline: none;
}
.fx-ios-co-scroll::-webkit-scrollbar { display: none; }
.fx-ios-co-hero { touch-action: none; cursor: grab; }
.fx-ios-co-panel.is-dragging { user-select: none; -webkit-user-select: none; cursor: grabbing; }
.fx-ios-co-body { display: block; padding: 20px 20px 36px; font-size: 15px; line-height: 1.5; opacity: 0; }
.fx-ios-co-body p { margin: 0 0 12px; }
.fx-ios-co-body p:first-child { font-size: 17px; font-weight: 600; line-height: 1.35; }
.fx-ios-co-close {
  position: absolute;
  top: 12px; right: 12px;
  z-index: 2;
  display: grid;
  place-items: center;
  width: 30px; height: 30px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  color: var(--fx-fg);
  opacity: 0;
  cursor: pointer;
}
/* demo: Today feed */
.fx-ios-today { display: flex; flex-direction: column; gap: 14px; height: 100%; padding: 22px 16px 16px; background: var(--fx-bg); }
.fx-ios-today header small { display: block; font: 600 11px/1.2 var(--fx-ios-font); letter-spacing: 0.04em; text-transform: uppercase; color: var(--fx-muted); }
.fx-ios-today header h2 { margin: 2px 0 0; font: 700 30px/1.1 var(--fx-ios-font); letter-spacing: -0.022em; }

/* =============================================== 7 · ios-picker-wheel === */
.fx-ios-pk {
  --fx-ios-pk-row: 32px;
  --fx-ios-pk-rows: 7;
  position: relative;
  display: flex;
  padding: 0 8px;
  user-select: none;
  -webkit-user-select: none;
}
.fx-ios-pk-band {
  position: absolute;
  top: 50%; left: 8px; right: 8px;
  height: var(--fx-ios-pk-row);
  border-radius: 8px;
  background: var(--fx-ios-fill);
  transform: translateY(-50%);
  pointer-events: none;
}
.fx-ios-pk-col { position: relative; flex: 1 1 0; min-width: 0; }
.fx-ios-pk-scroll {
  position: relative;
  height: calc(var(--fx-ios-pk-row) * var(--fx-ios-pk-rows));
  overflow-y: scroll;
  overscroll-behavior: contain;
  scroll-snap-type: y mandatory;
  scrollbar-width: none;
  cursor: grab;
  outline: none;
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 22%, #000 78%, transparent);
  mask-image: linear-gradient(to bottom, transparent, #000 22%, #000 78%, transparent);
}
.fx-ios-pk-scroll::-webkit-scrollbar { display: none; }
.fx-ios-pk-scroll.is-free { scroll-snap-type: none; }
.fx-ios-pk-scroll.is-dragging { cursor: grabbing; }
.fx-ios-pk-scroll:focus-visible { -webkit-mask-image: none; mask-image: none; box-shadow: inset 0 0 0 2px var(--fx-accent); border-radius: 8px; }
.fx-ios-pk-list { margin: 0; padding: calc(var(--fx-ios-pk-row) * (var(--fx-ios-pk-rows) - 1) / 2) 0; list-style: none; }
.fx-ios-pk-item {
  height: var(--fx-ios-pk-row);
  line-height: var(--fx-ios-pk-row);
  text-align: center;
  scroll-snap-align: center;
  font-size: 20px;
  font-variant-numeric: tabular-nums;
  color: var(--fx-muted);
  backface-visibility: hidden;
  will-change: transform, opacity;
}
.fx-ios-pk-item.is-center { color: var(--fx-fg); }
.fx-ios-pk-col.has-unit .fx-ios-pk-item { padding-right: 50%; text-align: right; }
.fx-ios-pk-unit {
  position: absolute;
  top: 50%; left: calc(50% + 8px);
  transform: translateY(-50%);
  font: 600 14px/1 var(--fx-ios-font);
  pointer-events: none;
}
.fx-ios-pk-card { width: 300px; padding: 12px 0; border-radius: 16px; background: var(--fx-surface); }

/* =============================================== 8 · ios-push-nav ======= */
.fx-ios-pn { position: relative; overflow: hidden; isolation: isolate; background: var(--fx-bg); }
.fx-ios-pn-stack { position: absolute; inset: 0; touch-action: pan-y; }
.fx-ios-pn.is-swiping { user-select: none; -webkit-user-select: none; }
.fx-ios-pn-page {
  position: absolute;
  inset: 0;
  background: var(--fx-bg);
  box-shadow: -12px 0 28px -14px var(--fx-ios-shadow);
  will-change: transform;
}
.fx-ios-pn-page[hidden] { display: none; }
.fx-ios-pn-screen {
  position: absolute;
  inset: 0;
  padding: 50px 0 16px;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: none;
  outline: none;
}
.fx-ios-pn-screen::-webkit-scrollbar { display: none; }
.fx-ios-pn-dim {
  position: absolute;
  inset: 0;
  opacity: 0;
  background: color-mix(in srgb, var(--fx-ios-ink) 35%, transparent);
  pointer-events: none;
}
.fx-ios-pn-bar {
  position: absolute;
  top: 0; left: 0; right: 0;
  z-index: 5;
  height: 44px;
  display: flex;
  align-items: center;
  border-bottom: 0.5px solid var(--fx-ios-hair);
}
.fx-ios-pn-back {
  position: relative;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  gap: 3px;
  max-width: 42%;
  height: 44px;
  padding: 0 10px 0 8px;
  border: 0;
  background: none;
  color: var(--fx-accent);
  font: 400 16px/1 var(--fx-ios-font);
  cursor: pointer;
}
.fx-ios-pn-back[hidden] { display: none; }
.fx-ios-pn-chev { display: inline-grid; }
.fx-ios-pn-bls { display: inline-grid; min-width: 0; }
.fx-ios-pn-bl { grid-area: 1 / 1; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.fx-ios-pn-titles { position: absolute; inset: 0; display: grid; place-items: center; pointer-events: none; }
.fx-ios-pn-title {
  grid-area: 1 / 1;
  max-width: 48%;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font: 600 16px/1 var(--fx-ios-font);
  letter-spacing: -0.01em;
}
.fx-ios-pn [data-fx-push]::after {
  content: "";
  flex: none;
  width: 7px; height: 7px;
  margin-left: auto;
  border-top: 2px solid var(--fx-muted);
  border-right: 2px solid var(--fx-muted);
  transform: rotate(45deg);
  opacity: 0.7;
}
.fx-ios-pn [data-fx-push]:has(small)::after { margin-left: 8px; }
.fx-ios-note { margin: 0 30px 12px; font-size: 13px; line-height: 1.4; color: var(--fx-muted); }
.fx-ios-hero-art {
  display: grid;
  place-items: center;
  height: 96px;
  margin: 0 14px 14px;
  border-radius: 14px;
  color: var(--fx-ios-on-ink);
  background: linear-gradient(150deg, hsl(calc(var(--fx-hue) - 110) 55% 48%), hsl(calc(var(--fx-hue) - 70) 50% 26%));
}
.fx-ios-hero-art svg { width: 44px; height: 44px; }

/* =============================================== 9 · ios-banner ========= */
.fx-ios-bn { position: relative; overflow: hidden; isolation: isolate; }
.fx-ios-bn-layer {
  position: absolute;
  top: 0; left: 0; right: 0;
  z-index: 20;
  padding: 8px 8px 0;
  pointer-events: none;
}
.fx-ios-scope-page .fx-ios-bn-layer { position: fixed; max-width: 440px; margin: 0 auto; }
.fx-ios-bn-card {
  position: relative;
  border-radius: 22px;
  box-shadow: 0 12px 30px -12px var(--fx-ios-shadow);
  pointer-events: auto;
  touch-action: none;
  cursor: grab;
  user-select: none;
  -webkit-user-select: none;
  will-change: transform;
}
.fx-ios-bn-card.is-dragging { cursor: grabbing; }
.fx-ios-bn-main {
  display: grid;
  grid-template-columns: 38px 1fr;
  column-gap: 10px;
  row-gap: 1px;
  align-items: center;
  width: 100%;
  padding: 11px 40px 11px 11px;
  border: 0;
  border-radius: 22px;
  background: none;
  color: var(--fx-fg);
  font: inherit;
  text-align: left;
  cursor: inherit;
}
.fx-ios-bn-main:focus-visible { outline-offset: -2px; }
.fx-ios-bn-icon {
  grid-row: span 2;
  display: grid;
  place-items: center;
  width: 38px; height: 38px;
  border-radius: 9px;
  color: var(--fx-ios-on-ink);
  background: linear-gradient(160deg, hsl(calc(var(--fx-hue) + 150) 64% 56%), hsl(calc(var(--fx-hue) + 120) 60% 36%));
}
.fx-ios-bn-icon svg { width: 20px; height: 20px; }
.fx-ios-bn-head { display: flex; align-items: baseline; gap: 8px; font-size: 14px; }
.fx-ios-bn-head strong { font-weight: 600; }
.fx-ios-bn-head time { margin-left: auto; font-size: 12px; color: var(--fx-muted); }
.fx-ios-bn-text { grid-column: 2; font-size: 14px; line-height: 1.3; }
.fx-ios-bn-more { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 0.3s var(--fx-ease); }
.fx-ios-bn-card.is-expanded .fx-ios-bn-more { grid-template-rows: 1fr; }
.fx-ios-bn-more-in { min-height: 0; overflow: hidden; }
.fx-ios-bn-more-in > * { padding: 0 12px 12px 59px; font-size: 14px; line-height: 1.35; }
.fx-ios-bn-more-in p { margin: 0 0 10px; }
.fx-ios-bn-actions { display: flex; gap: 8px; }
.fx-ios-bn-actions button {
  flex: 1;
  padding: 9px 6px;
  border: 0;
  border-radius: 10px;
  background: var(--fx-ios-fill-2);
  color: var(--fx-fg);
  font: 600 13px/1 var(--fx-ios-font);
  cursor: pointer;
}
.fx-ios-bn-close {
  position: absolute;
  top: 9px; right: 9px;
  display: grid;
  place-items: center;
  width: 24px; height: 24px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--fx-ios-fill);
  color: var(--fx-muted);
  cursor: pointer;
}
.fx-ios-bn-close svg { width: 9px; height: 9px; }
/* demo: lock screen */
.fx-ios-lock {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding-top: 92px;
  background: var(--fx-ios-wall);
}
.fx-ios-lock b { font: 300 64px/1 var(--fx-ios-font); letter-spacing: -0.02em; }
.fx-ios-lock span { margin-top: 6px; font-size: 14px; font-weight: 500; opacity: 0.8; }
.fx-ios-pill-btn {
  position: absolute;
  left: 50%; bottom: 26px;
  padding: 10px 18px;
  border: 0;
  border-radius: 99px;
  color: var(--fx-fg);
  font: 600 13px/1 var(--fx-ios-font);
  white-space: nowrap;
  cursor: pointer;
  transform: translateX(-50%);
  transition: transform 0.16s var(--fx-ease-out);
}
.fx-ios-pill-btn:active { transform: translateX(-50%) scale(0.96); }

/* =============================================== 10 · ios-jiggle ======== */
.fx-ios-jg {
  position: relative;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  align-content: start;
  row-gap: 16px;
  min-height: 100%;
  padding: 48px 10px 110px;
  background: var(--fx-ios-wall);
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  touch-action: manipulation;
}
.fx-ios-jg-edit {
  position: absolute;
  top: 14px; right: 14px;
  padding: 6px 12px;
  border: 0;
  border-radius: 99px;
  background: var(--fx-ios-fill-2);
  color: var(--fx-fg);
  font: 600 12px/1 var(--fx-ios-font);
  cursor: pointer;
}
.fx-ios-jg-cell { position: relative; display: flex; justify-content: center; }
.fx-ios-jg-cell[hidden] { display: none; }
.fx-ios-jg-wob { position: relative; }
.fx-ios-jg.is-editing .fx-ios-jg-wob {
  animation: fx-ios-jiggle var(--fx-ios-jt, 0.26s) ease-in-out var(--fx-ios-jd, 0s) infinite alternate;
}
@keyframes fx-ios-jiggle {
  from { transform: rotate(calc(var(--fx-ios-jr, 1.4deg) * -1)) translateY(0.5px); }
  to   { transform: rotate(var(--fx-ios-jr, 1.4deg)) translateY(-0.5px); }
}
.fx-ios-jg-app {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
  width: 56px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--fx-fg);
  font: 500 10.5px/1.1 var(--fx-ios-font);
  cursor: pointer;
}
.fx-ios-jg-icon {
  display: grid;
  place-items: center;
  width: 46px; height: 46px;
  border-radius: 11px;
  color: var(--fx-ios-on-ink);
  background: linear-gradient(160deg, hsl(calc(var(--fx-hue) + var(--fx-ios-app, 0)) 76% 62%), hsl(calc(var(--fx-hue) + var(--fx-ios-app, 0) + 28) 68% 40%));
  box-shadow: 0 4px 10px -6px var(--fx-ios-shadow);
  transition: filter 0.16s ease, transform 0.16s var(--fx-ease-out);
}
.fx-ios-jg-icon svg { width: 24px; height: 24px; }
.fx-ios-jg-app:active .fx-ios-jg-icon { filter: brightness(0.75); transform: scale(0.94); transition-duration: 0.08s; }
.fx-ios-jg-label { max-width: 100%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.fx-ios-jg-badge {
  position: absolute;
  top: -6px; left: -2px;
  z-index: 2;
  display: grid;
  place-items: center;
  width: 20px; height: 20px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: color-mix(in srgb, var(--fx-surface-2) 70%, var(--fx-fg) 30%);
  color: var(--fx-fg);
  box-shadow: 0 2px 6px var(--fx-ios-shadow);
  opacity: 0;
  transform: scale(0.3);
  pointer-events: none;
  cursor: pointer;
  transition: transform 0.16s var(--fx-ease-out), opacity 0.12s linear;
}
.fx-ios-jg.is-editing .fx-ios-jg-badge {
  opacity: 1;
  transform: scale(1);
  pointer-events: auto;
  transition: transform 0.42s var(--fx-ease-spring) calc(var(--fx-i, 0) * 14ms), opacity 0.14s linear calc(var(--fx-i, 0) * 14ms);
}
.fx-ios-jg-dock {
  position: absolute;
  left: 10px; right: 10px; bottom: 10px;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  padding: 10px 4px;
  border-radius: 26px;
}
.fx-ios-jg-dock .fx-ios-jg-label { display: none; }

/* ================================================= reduced motion ======= */
@media (prefers-reduced-motion: reduce) {
  .fx-ios-lt-small { transform: translate(-50%, -50%); transition: opacity 0.1s linear; }
  .fx-ios-cm-trigger { transform: none !important; }
  .fx-ios-cm-backdrop { transition: opacity 0.1s linear; }
  .fx-ios-co-card:active { transform: none; }
  .fx-ios-co-layer { opacity: 0; transition: opacity 0.12s linear; }
  .fx-ios-co-layer.is-open { opacity: 1; }
  .fx-ios-co-body { opacity: 1 !important; }
  .fx-ios-isl-compact, .fx-ios-isl-expanded { transition: opacity 0.1s linear; }
  .fx-ios-isl.is-expanded .fx-ios-isl-compact { opacity: 0; }
  .fx-ios-isl.is-expanded .fx-ios-isl-expanded { opacity: 1; }
  .fx-ios-bn-card { opacity: 0; visibility: hidden; transform: none; transition: opacity 0.12s linear, visibility 0s linear 0.12s; }
  .fx-ios-bn.is-shown .fx-ios-bn-card { opacity: 1; visibility: visible; transition: opacity 0.12s linear; }
  .fx-ios-bn-more { transition: none; }
  .fx-ios-jg.is-editing .fx-ios-jg-wob { animation: none; }
  .fx-ios-jg-badge, .fx-ios-jg.is-editing .fx-ios-jg-badge { transform: none; transition: opacity 0.1s linear; }
  .fx-ios-jg-app:active .fx-ios-jg-icon { transform: none; }
  .fx-ios-pill-btn:active { transform: translateX(-50%); }
}
:root[data-fx-reduced] .fx-ios-lt-small { transform: translate(-50%, -50%); transition: opacity 0.1s linear; }
:root[data-fx-reduced] .fx-ios-cm-trigger { transform: none !important; }
:root[data-fx-reduced] .fx-ios-cm-backdrop { transition: opacity 0.1s linear; }
:root[data-fx-reduced] .fx-ios-co-card:active { transform: none; }
:root[data-fx-reduced] .fx-ios-co-layer { opacity: 0; transition: opacity 0.12s linear; }
:root[data-fx-reduced] .fx-ios-co-layer.is-open { opacity: 1; }
:root[data-fx-reduced] .fx-ios-co-body { opacity: 1 !important; }
:root[data-fx-reduced] .fx-ios-isl-compact,
:root[data-fx-reduced] .fx-ios-isl-expanded { transition: opacity 0.1s linear; }
:root[data-fx-reduced] .fx-ios-isl.is-expanded .fx-ios-isl-compact { opacity: 0; }
:root[data-fx-reduced] .fx-ios-isl.is-expanded .fx-ios-isl-expanded { opacity: 1; }
:root[data-fx-reduced] .fx-ios-bn-card { opacity: 0; visibility: hidden; transform: none; transition: opacity 0.12s linear, visibility 0s linear 0.12s; }
:root[data-fx-reduced] .fx-ios-bn.is-shown .fx-ios-bn-card { opacity: 1; visibility: visible; transition: opacity 0.12s linear; }
:root[data-fx-reduced] .fx-ios-bn-more { transition: none; }
:root[data-fx-reduced] .fx-ios-jg.is-editing .fx-ios-jg-wob { animation: none; }
:root[data-fx-reduced] .fx-ios-jg-badge,
:root[data-fx-reduced] .fx-ios-jg.is-editing .fx-ios-jg-badge { transform: none; transition: opacity 0.1s linear; }
:root[data-fx-reduced] .fx-ios-jg-app:active .fx-ios-jg-icon { transform: none; }
:root[data-fx-reduced] .fx-ios-pill-btn:active { transform: translateX(-50%); }

/* ══ src/effects/layout.css ══ */
/* =========================================================================
 * effects-lab — Layout & lists
 * Namespace: .fx-layout-*  /  keyframes fx-layout-*
 * Only tokens from src/core/00-tokens.css are used — no literal colours.
 * ========================================================================= */

/* ------------------------------------------------------------- shell ----- */
.fx-layout {
  --fx-layout-gap: 8px;
  box-sizing: border-box;
  width: 100%;
  max-width: 470px;
  margin-inline: auto;
  display: flex;
  flex-direction: column;
  gap: 10px;
  color: var(--fx-fg);
  font-size: 12.5px;
  line-height: 1.35;
  text-align: left;
}
.fx-layout *,
.fx-layout *::before,
.fx-layout *::after { box-sizing: border-box; }

.fx-layout-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
}
.fx-layout-bar-end { margin-inline-start: auto; }

.fx-layout-legend {
  color: var(--fx-muted);
  font-size: 11px;
  letter-spacing: 0.02em;
  margin: 0;
}

/* ------------------------------------------------------------ controls --- */
.fx-layout-btn {
  appearance: none;
  font: inherit;
  font-weight: 550;
  line-height: 1;
  padding: 6px 10px;
  border-radius: var(--fx-radius-sm);
  border: 1px solid var(--fx-border);
  background: var(--fx-surface-2);
  color: var(--fx-muted);
  cursor: pointer;
  transition: background-color var(--fx-dur-1) var(--fx-ease),
              color var(--fx-dur-1) var(--fx-ease),
              border-color var(--fx-dur-1) var(--fx-ease),
              transform var(--fx-dur-1) var(--fx-ease);
}
@media (hover: hover) and (pointer: fine) {
  .fx-layout-btn:hover { color: var(--fx-fg); border-color: var(--fx-muted); }
}
.fx-layout-btn:active { transform: scale(0.97); }
.fx-layout-btn[aria-pressed="true"],
.fx-layout-btn[aria-selected="true"] {
  background: var(--fx-accent);
  border-color: transparent;
  color: var(--fx-accent-contrast);
}
.fx-layout-btn[disabled] { opacity: 0.4; cursor: default; }
.fx-layout-btn[data-dir]::after {
  content: "\2191";
  display: inline-block;
  margin-inline-start: 5px;
  font-size: 0.9em;
  transition: transform var(--fx-dur-2) var(--fx-ease);
}
.fx-layout-btn[data-dir="desc"]::after { transform: rotate(180deg); }

.fx-layout-x {
  appearance: none;
  flex: none;
  width: 20px;
  height: 20px;
  display: grid;
  place-items: center;
  padding: 0;
  font: inherit;
  font-size: 14px;
  line-height: 1;
  border-radius: 999px;
  border: 1px solid transparent;
  background: transparent;
  color: var(--fx-muted);
  cursor: pointer;
  transition: background-color var(--fx-dur-1) var(--fx-ease), color var(--fx-dur-1) var(--fx-ease);
}
@media (hover: hover) and (pointer: fine) {
  .fx-layout-x:hover { background: var(--fx-surface-2); color: var(--fx-fg); }
}
.fx-layout-x[disabled] { opacity: 0; pointer-events: none; }

.fx-layout :focus-visible {
  outline: 2px solid var(--fx-accent);
  outline-offset: 2px;
  border-radius: var(--fx-radius-sm);
}

/* ---------------------------------------------------------- list shell --- */
.fx-layout-list {
  position: relative;          /* exiting items are pinned inside this box */
  margin: 0;
  padding: 0;
  list-style: none;
}
.fx-layout [data-fx-item] { box-sizing: border-box; }
.fx-layout [data-fx-item][hidden] { display: none !important; }
.fx-layout .fx-layout-exiting { z-index: 1; }

/* -------------------------------------------------------------- pieces --- */
.fx-layout-chip,
.fx-layout-tile,
.fx-layout-card,
.fx-layout-row {
  border-radius: var(--fx-radius-sm);
  border: 1px solid var(--fx-border);
  background: var(--fx-surface-2);
  color: var(--fx-fg);
}

.fx-layout-chip {
  display: flex;
  align-items: center;
  min-height: 34px;
  overflow: hidden;            /* the counter-scaled label is clipped, never stretched */
  font-weight: 550;
  will-change: transform;
}
.fx-layout-chip > [data-fx-inner] {
  display: block;
  padding: 8px 12px;
  white-space: nowrap;
  will-change: transform;
}

.fx-layout-rows { display: flex; flex-direction: column; gap: 6px; position: relative; }
.fx-layout-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 8px 7px 10px;
  will-change: transform;
}
.fx-layout-dot {
  flex: none;
  width: 7px;
  height: 7px;
  border-radius: 999px;
  background: var(--fx-accent);
}
.fx-layout-label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fx-layout-num {
  flex: none;
  color: var(--fx-muted);
  font-variant-numeric: tabular-nums;
  font-size: 11.5px;
}

/* ------------------------------------------------ 1 · layout-flip -------- */
.fx-layout-flip-list { position: relative; display: grid; gap: var(--fx-layout-gap); }
.fx-layout-flip-list[data-mode="grid"] { grid-template-columns: repeat(3, 1fr); }
.fx-layout-flip-list[data-mode="row"] {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-start;
}
.fx-layout-flip-list[data-mode="row"] .fx-layout-chip { flex: 0 0 auto; }
.fx-layout-flip-list[data-mode="stack"] {
  display: flex;
  flex-direction: column;
  gap: 5px;
}
.fx-layout-flip-list[data-mode="stack"] .fx-layout-chip { min-height: 30px; }

/* ----------------------------------------- 2 · layout-filter-grid -------- */
.fx-layout-filter-grid {
  position: relative;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--fx-layout-gap);
}
.fx-layout-tile {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 2px;
  min-height: 62px;
  padding: 8px 9px;
  will-change: transform, opacity;
}
.fx-layout-tile strong { font-weight: 600; }
.fx-layout-tag { color: var(--fx-muted); font-size: 10.5px; letter-spacing: 0.04em; text-transform: uppercase; }

/* ------------------------------------------- 5 · layout-expand-row ------- */
.fx-layout-erows { display: flex; flex-direction: column; gap: 6px; position: relative; }
.fx-layout-erow {
  border-radius: var(--fx-radius-sm);
  border: 1px solid var(--fx-border);
  background: var(--fx-surface-2);
  overflow: hidden;
}
.fx-layout-erow-head {
  appearance: none;
  width: 100%;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  font: inherit;
  font-weight: 550;
  text-align: left;
  color: var(--fx-fg);
  background: transparent;
  border: 0;
  cursor: pointer;
}
.fx-layout-caret {
  flex: none;
  color: var(--fx-muted);
  transition: transform var(--fx-dur-2) var(--fx-ease);
}
.fx-layout-caret-end { margin-inline-start: auto; }
.fx-layout-erow.is-open .fx-layout-caret { transform: rotate(90deg); }

/* grid-template-rows 0fr -> 1fr: the rows below are carried by layout */
.fx-layout-collapse {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--fx-dur-2) var(--fx-ease);
}
.is-open > .fx-layout-collapse { grid-template-rows: 1fr; }
.fx-layout-collapse-clip { overflow: hidden; min-height: 0; }
.fx-layout-collapse-body {
  padding: 0 10px 10px 28px;
  color: var(--fx-muted);
  opacity: 0;
  transform: translateY(-4px);
  transition: opacity 0.14s var(--fx-ease-in), transform 0.14s var(--fx-ease-in);
}
.is-open > .fx-layout-collapse .fx-layout-collapse-body {
  opacity: 1;
  transform: none;
  transition: opacity 0.26s 0.07s var(--fx-ease-out), transform 0.26s 0.07s var(--fx-ease-out);
}

/* --------------------------------------- 6 · layout-masonry-settle ------- */
.fx-layout-masonry {
  position: relative;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-auto-rows: 16px;
  grid-auto-flow: row dense;
  gap: 6px;
}
.fx-layout-masonry .fx-layout-tile {
  grid-row: span var(--fx-span, 3);
  min-height: 0;
  will-change: transform, opacity;
}

/* ------------------------------------------ 7 · layout-tab-panels -------- */
.fx-layout-tablist { display: flex; gap: 6px; }
.fx-layout-panels {
  position: relative;
  overflow: hidden;            /* the clip box that is scaled on Y */
  border-radius: var(--fx-radius-sm);
  border: 1px solid var(--fx-border);
  background: var(--fx-surface-2);
  will-change: transform;
}
.fx-layout-panels-inner { position: relative; will-change: transform; }
.fx-layout-panel { position: relative; padding: 11px 12px; }
.fx-layout-panel.is-leaving { position: absolute; inset-block-start: 0; inset-inline: 0; }
.fx-layout-panel[hidden] { display: none; }
.fx-layout-panel h4 { margin: 0 0 5px; font-size: 12.5px; font-weight: 600; }
.fx-layout-panel p { margin: 0 0 6px; color: var(--fx-muted); }
.fx-layout-panel p:last-child { margin-bottom: 0; }

/* ------------------------------ 8 · layout-notification-stack ------------ */
.fx-layout-notes {
  --fx-note-h: 62px;
  --fx-note-peek: 12px;        /* only the empty bottom edge of the card below shows */
  position: relative;
  margin: 0;
  padding: 0 0 6px;
  list-style: none;
  min-height: calc(var(--fx-note-h) + 48px);
}
.fx-layout-note-slot { position: relative; }
.fx-layout-note-slot:not(.fx-layout-exiting) + .fx-layout-note-slot { margin-top: calc(var(--fx-note-peek) - var(--fx-note-h)); }
.fx-layout-note {
  position: relative;
  display: flex;
  align-items: center;
  gap: 8px;
  height: var(--fx-note-h);
  padding: 0 8px 0 12px;
  border-radius: var(--fx-radius);
  border: 1px solid var(--fx-border);
  background: var(--fx-surface-2);
  box-shadow: var(--fx-shadow);
  transform-origin: top center;
  transform: scale(calc(1 - var(--fx-depth, 0) * 0.045));
  transition: transform var(--fx-dur-2) var(--fx-ease);
  will-change: transform;
}
/* depth is dimmed with an opaque overlay, never with opacity — a translucent
   card would let the cards stacked behind it show through */
.fx-layout-note::after {
  content: "";
  position: absolute;
  inset: -1px;
  border-radius: inherit;
  background: var(--fx-bg);
  opacity: calc(var(--fx-depth, 0) * 0.22);
  pointer-events: none;
  transition: opacity var(--fx-dur-2) var(--fx-ease);
}
.fx-layout-note-slot:not(.is-top) .fx-layout-note { pointer-events: none; }
.fx-layout-note-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1 1 auto; }
.fx-layout-note-title { font-weight: 600; font-size: 12.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fx-layout-note-meta { color: var(--fx-muted); font-size: 11px; }

/* ------------------------------------------ 9 · layout-split-view -------- */
.fx-layout-split {
  --fx-split: 46%;
  display: grid;
  grid-template-columns: var(--fx-split) 10px 1fr;
  gap: 0;
  height: 176px;
  border-radius: var(--fx-radius-sm);
  border: 1px solid var(--fx-border);
  background: var(--fx-surface);
  overflow: hidden;
  transition: grid-template-columns var(--fx-dur-3) var(--fx-ease);
}
.fx-layout-split.is-dragging { transition: none; }
.fx-layout-pane {
  min-width: 0;
  padding: 10px 11px;
  overflow: hidden;
  background: var(--fx-surface-2);
}
.fx-layout-pane h4 { margin: 0 0 6px; font-size: 12px; font-weight: 600; }
.fx-layout-pane p { margin: 0; color: var(--fx-muted); font-size: 11.5px; }
.fx-layout-pane-list { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 5px; }
.fx-layout-pane-list li {
  padding: 5px 7px;
  border-radius: 6px;
  background: var(--fx-surface);
  color: var(--fx-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.fx-layout-divider {
  position: relative;
  cursor: col-resize;
  background: var(--fx-border);
  touch-action: none;
  display: grid;
  place-items: center;
}
.fx-layout-divider::before {
  content: "";
  width: 2px;
  height: 26px;
  border-radius: 999px;
  background: var(--fx-muted);
  opacity: 0.5;
  transition: opacity var(--fx-dur-1) var(--fx-ease), height var(--fx-dur-1) var(--fx-ease);
}
@media (hover: hover) and (pointer: fine) {
  .fx-layout-divider:hover::before { opacity: 1; height: 36px; }
}
.fx-layout-split.is-dragging .fx-layout-divider::before { opacity: 1; height: 36px; }

/* --------------------------------------- 10 · layout-grid-density -------- */
.fx-layout-dgrid {
  position: relative;
  display: grid;
  grid-template-columns: repeat(var(--fx-cols, 3), 1fr);
  gap: var(--fx-layout-gap);
}
.fx-layout-card {
  overflow: hidden;            /* the counter-scaled child is clipped */
  min-height: 0;
  will-change: transform;
}
.fx-layout-card-inner {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 9px 10px;
  will-change: transform;
}
.fx-layout-card-inner strong { font-weight: 600; font-size: 12px; }
.fx-layout-card-inner span { color: var(--fx-muted); font-size: 11px; }
.fx-layout-swatch {
  display: block;
  height: 18px;
  border-radius: 5px;
  background: linear-gradient(120deg, var(--fx-accent), var(--fx-accent-2));
  margin-bottom: 3px;
}

/* ------------------------------------ 11 · layout-accordion-group -------- */
.fx-layout-acc { display: flex; flex-direction: column; gap: 6px; }
.fx-layout-acc-item {
  border-radius: var(--fx-radius-sm);
  border: 1px solid var(--fx-border);
  background: var(--fx-surface-2);
  overflow: hidden;
}
.fx-layout-acc-h { margin: 0; font-size: inherit; font-weight: inherit; }
.fx-layout-acc-trigger {
  appearance: none;
  width: 100%;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 9px 10px;
  font: inherit;
  font-weight: 550;
  text-align: left;
  color: var(--fx-fg);
  background: transparent;
  border: 0;
  cursor: pointer;
}
.fx-layout-acc-trigger .fx-layout-caret { transition: transform var(--fx-dur-2) var(--fx-ease); }
.fx-layout-acc-item.is-open .fx-layout-caret { transform: rotate(90deg); }

/* =========================================================================
 * Reduced motion — the layout change happens instantly and correctly.
 * No sliding, no scaling, no stagger. (JS paths are handled in layout.js.)
 * ======================================================================= */
@media (prefers-reduced-motion: reduce) {
  .fx-layout-collapse,
  .fx-layout-collapse-body,
  .fx-layout-caret,
  .fx-layout-note,
  .fx-layout-note::after,
  .fx-layout-split,
  .fx-layout-btn,
  .fx-layout-x,
  .fx-layout-btn[data-dir]::after,
  .fx-layout-divider::before { transition: none !important; }
  .fx-layout-btn:active { transform: none; }
  .is-open > .fx-layout-collapse .fx-layout-collapse-body { transform: none; }
}
:root[data-fx-reduced] .fx-layout-collapse,
:root[data-fx-reduced] .fx-layout-collapse-body,
:root[data-fx-reduced] .fx-layout-caret,
:root[data-fx-reduced] .fx-layout-note,
:root[data-fx-reduced] .fx-layout-note::after,
:root[data-fx-reduced] .fx-layout-split,
:root[data-fx-reduced] .fx-layout-btn,
:root[data-fx-reduced] .fx-layout-x,
:root[data-fx-reduced] .fx-layout-btn[data-dir]::after,
:root[data-fx-reduced] .fx-layout-divider::before { transition: none !important; }
:root[data-fx-reduced] .fx-layout-btn:active { transform: none; }
:root[data-fx-reduced] .is-open > .fx-layout-collapse .fx-layout-collapse-body { transform: none; }

/* ══ src/effects/load.css ══ */
/* =========================================================================
 * effects-lab — category: load  ("Loading & feedback")
 *
 * These run while the user is already waiting. Nothing in here is loud.
 *   - spinners turn at a CONSTANT angular speed (linear, no easing)
 *   - shimmer sweeps travel in ONE direction, slowly, at low contrast
 *   - dots stagger forward, they never reverse into a wave
 *   - shake decays to dead still
 *
 * REDUCED MOTION
 *   A loading indicator carries information: "something is happening".
 *   Freezing it would be a lie, so continuous motion is replaced by a slow,
 *   low-contrast opacity pulse driven from JS (see `breathe()` in load.js).
 *   It has to be JS because src/core/01-base.css clamps every CSS
 *   animation-duration to 0.01ms under reduced motion, and because
 *   `[data-fx][data-fx-ready] { opacity: 1 !important }` outranks a CSS
 *   animation on the effect root — hence every pulse targets an inner node.
 * ========================================================================= */

/* -------------------------------------------------------------- tokens --- */
/* Local, derived from the global tokens. Override any of them per instance. */
.fx-load-root {
  --fx-load-track: color-mix(in oklab, var(--fx-fg) 11%, transparent);
  --fx-load-sheen: color-mix(in oklab, var(--fx-fg) 10%, transparent);
  --fx-load-ok: var(--fx-success, var(--fx-accent));
  /* An error must read as an error whatever --fx-hue is, so this is the one
     colour not derived from the hue. Set --fx-danger to override it. */
  --fx-load-danger: var(--fx-danger, hsl(6 72% 57%));
}

.fx-load-root { color: var(--fx-fg); }

/* Every generated visual is decorative; the accessible name lives on the
   role="status" / role="progressbar" host. */
.fx-load-sr {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ========================================================== 1. shimmer === */
/* The sweep. One direction, 1.6s, linear, ~10% contrast. It is invisible at
   both ends of its travel, so the loop has a calm pause instead of a snap. */
.fx-load-sheen-host { position: relative; overflow: hidden; }

.fx-load-sheen {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(
    90deg,
    transparent 0%,
    var(--fx-load-sheen) 48%,
    var(--fx-load-sheen) 52%,
    transparent 100%
  );
  transform: translateX(-100%);
  animation: fx-load-sweep var(--fx-load-sweep-dur, 1.6s) linear infinite;
  will-change: transform;
}

@keyframes fx-load-sweep {
  from { transform: translateX(-100%); }
  to   { transform: translateX(100%); }
}

/* Applied to an arbitrary block by `load-shimmer`. */
.fx-load-shimmer { position: relative; overflow: hidden; }

/* Reduced-motion stand-in for the sweep: it does not travel, it breathes.
   JS drives the opacity so the global reduced-motion clamp cannot flatten it. */
.fx-load-still {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(
    90deg,
    transparent 0%,
    var(--fx-load-sheen) 50%,
    transparent 100%
  );
}

/* ========================================================= 2. skeleton === */
.fx-load-skel {
  display: grid;
  gap: 12px;
  width: 100%;
}

.fx-load-skel-head {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 4px;
}

.fx-load-skel-avatar {
  flex: 0 0 auto;
  width: 40px; height: 40px;
  border-radius: 50%;
  background: var(--fx-load-track);
}

.fx-load-skel-headlines { display: grid; gap: 8px; flex: 1 1 auto; }

.fx-load-skel-line {
  height: var(--fx-load-line-h, 11px);
  border-radius: 999px;
  background: var(--fx-load-track);
}

.fx-load-skel-line.is-short { width: 58%; }
.fx-load-skel-line.is-mid   { width: 78%; }
.fx-load-skel-line.is-title { height: 13px; width: 45%; }

/* ========================================================== 3. spinner === */
.fx-load-spinner {
  display: inline-grid;
  place-items: center;
  gap: 10px;
  grid-auto-flow: column;
}

.fx-load-spinner-glyph {
  display: block;
  width: var(--fx-load-size, 30px);
  height: var(--fx-load-size, 30px);
  /* Constant angular speed. No easing, ever — an eased spinner reads as a
     stutter and makes the wait feel longer than it is. */
  animation: fx-load-rotate var(--fx-load-turn, 0.78s) linear infinite;
  will-change: transform;
}

.fx-load-spinner-glyph svg { display: block; width: 100%; height: 100%; }

@keyframes fx-load-rotate {
  to { transform: rotate(360deg); }
}

.fx-load-arc-track {
  fill: none;
  stroke: var(--fx-load-track);
  stroke-width: 3;
}

.fx-load-arc {
  fill: none;
  stroke: var(--fx-accent);
  stroke-width: 3;
  stroke-linecap: round;
  /* r = 10.5 -> C = 65.97. 26% of the circle reads as an arc, not a dot. */
  stroke-dasharray: 17.2 48.8;
}

/* Reduced motion closes the arc into a full ring. A stopped arc pointing
   nowhere is meaningless; a ring that breathes still reads as "busy". */
.fx-load-arc--ring { stroke-dasharray: none; }

.fx-load-spinner-label {
  font-size: 0.85rem;
  color: var(--fx-muted);
}

/* ============================================================= 4. dots === */
.fx-load-dots {
  display: inline-flex;
  align-items: center;
  gap: var(--fx-load-gap, 7px);
}

.fx-load-dot {
  width: var(--fx-load-dot, 8px);
  height: var(--fx-load-dot, 8px);
  border-radius: 50%;
  background: var(--fx-accent);
  opacity: 0.3;
  /* Forward stagger: each dot is the same cycle, shifted later in time.
     Never an `alternate` loop — that reads as a wave bouncing back. */
  animation: fx-load-hop var(--fx-load-hop-dur, 1.05s) infinite;
  animation-delay: calc(var(--fx-i, 0) * var(--fx-load-hop-stagger, 0.14s));
  will-change: transform, opacity;
}

@keyframes fx-load-hop {
  0%   { transform: translateY(0);     opacity: 0.3; animation-timing-function: var(--fx-ease-out); }
  26%  { transform: translateY(-5px);  opacity: 1;   animation-timing-function: var(--fx-ease-in); }
  54%  { transform: translateY(0);     opacity: 0.3; }
  100% { transform: translateY(0);     opacity: 0.3; }
}

/* ============================================================== 5. bars === */
.fx-load-bar {
  position: relative;
  overflow: hidden;
  width: 100%;
  height: var(--fx-load-bar-h, 6px);
  border-radius: 999px;
  background: var(--fx-load-track);
}

/* --- indeterminate ------------------------------------------------------- */
.fx-load-bar-seg {
  position: absolute;
  inset-block: 0;
  left: 0;
  width: 38%;
  border-radius: inherit;
  background: var(--fx-accent);
  transform: translateX(-100%);
  /* 100% here is the segment's own width (38% of the track), so a full
     off-left -> off-right travel is 100/38 = 263%. Linear: a progress
     indicator that speeds up and slows down is telling a story it can't back. */
  animation: fx-load-bar-sweep var(--fx-load-sweep-dur, 1.5s) linear infinite;
  will-change: transform;
}

@keyframes fx-load-bar-sweep {
  from { transform: translateX(-100%); }
  to   { transform: translateX(263%); }
}

/* --- determinate --------------------------------------------------------- */
.fx-load-bar-fill {
  position: absolute;
  inset-block: 0;
  left: 0;
  width: 100%;
  border-radius: inherit;
  background: var(--fx-accent);
  transform: scaleX(0);
  transform-origin: left center;
  /* A transition, not keyframes: values arrive at unpredictable times and a
     transition retargets from wherever the fill currently is. */
  transition: transform var(--fx-load-fill-dur, 0.45s) var(--fx-ease-out);
  will-change: transform;
}

.fx-load-meter {
  display: grid;
  gap: 9px;
  width: 100%;
}

.fx-load-meter-top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  font-size: 0.85rem;
  color: var(--fx-muted);
}

.fx-load-meter-value {
  color: var(--fx-fg);
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
}

.fx-load-steps {
  display: flex;
  gap: 6px;
  margin-top: 4px;
}

.fx-load-step {
  appearance: none;
  border: 1px solid var(--fx-border);
  background: var(--fx-surface-2);
  color: var(--fx-fg);
  font: inherit;
  font-size: 0.78rem;
  padding: 5px 11px;
  border-radius: var(--fx-radius-sm);
  cursor: pointer;
  font-variant-numeric: tabular-nums;
  transition: transform var(--fx-dur-1) var(--fx-ease-out),
              background-color var(--fx-dur-1) ease,
              border-color var(--fx-dur-1) ease;
}

@media (hover: hover) and (pointer: fine) {
  .fx-load-step:hover { background: var(--fx-surface); border-color: var(--fx-accent); }
}

.fx-load-step:active { transform: scale(0.97); }

/* ==================================================== 6. ring progress === */
.fx-load-ring {
  position: relative;
  display: inline-grid;
  place-items: center;
  width: var(--fx-load-ring-size, 128px);
  height: var(--fx-load-ring-size, 128px);
}

.fx-load-ring svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  /* Start the arc at 12 o'clock. */
  transform: rotate(-90deg);
}

.fx-load-ring-track {
  fill: none;
  stroke: var(--fx-load-track);
  stroke-width: 8;
}

.fx-load-ring-arc {
  fill: none;
  stroke: var(--fx-accent);
  stroke-width: 8;
  stroke-linecap: round;
  transition: stroke-dashoffset var(--fx-load-fill-dur, 0.55s) var(--fx-ease-out);
  will-change: stroke-dashoffset;
}

.fx-load-ring-readout {
  position: relative;
  display: grid;
  justify-items: center;
  gap: 2px;
  line-height: 1;
}

.fx-load-ring-num {
  font-size: 1.6rem;
  font-weight: 600;
  color: var(--fx-fg);
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
}

.fx-load-ring-unit {
  font-size: 0.7rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--fx-muted);
}

/* ==================================================== 7. pulse holder ==== */
.fx-load-pulse-inner {
  /* Calm, slow, symmetric. This is "refreshing", not "loading" — it must not
     compete with the content it sits on top of. */
  animation: fx-load-breathe var(--fx-load-breathe-dur, 2.2s) ease-in-out infinite;
  will-change: opacity;
}

@keyframes fx-load-breathe {
  0%, 100% { opacity: 1; }
  50%      { opacity: var(--fx-load-breathe-min, 0.46); }
}

/* ========================================================== 8. button === */
.fx-load-btn {
  position: relative;
  display: inline-grid;
  place-items: center;
  min-width: var(--fx-load-btn-w, 168px);
  min-height: 42px;
  padding: 0 20px;
  border: 1px solid transparent;
  border-radius: var(--fx-radius-sm);
  background: var(--fx-accent);
  color: var(--fx-accent-contrast);
  font: inherit;
  font-size: 0.92rem;
  font-weight: 560;
  cursor: pointer;
  overflow: hidden;
  transition: transform var(--fx-dur-1) var(--fx-ease-out),
              background-color var(--fx-dur-2) ease,
              filter var(--fx-dur-2) ease;
}

@media (hover: hover) and (pointer: fine) {
  .fx-load-btn:hover { filter: brightness(1.07); }
}

.fx-load-btn:active { transform: scale(0.97); }
.fx-load-btn:disabled { cursor: default; }
.fx-load-btn[data-fx-state='success'] { background: var(--fx-load-ok); }

/* The pop lives on an inner node so it composes with :active on the button
   instead of fighting it over the same inline transform. */
.fx-load-btn-pop {
  display: grid;
  place-items: center;
  grid-template-areas: 'face';
  width: 100%;
}

.fx-load-btn-face {
  grid-area: face;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  white-space: nowrap;
  /* Blur bridges the crossfade: without it you see two labels overlapping,
     with it you see one thing changing. */
  transition: opacity 170ms var(--fx-ease-out),
              filter 170ms var(--fx-ease-out),
              transform 170ms var(--fx-ease-out);
}

.fx-load-btn-face[data-fx-off] {
  opacity: 0;
  filter: blur(3px);
  transform: scale(0.94);
  pointer-events: none;
}

.fx-load-btn-face .fx-load-spinner-glyph {
  --fx-load-size: 18px;
}

.fx-load-btn-face .fx-load-arc-track { stroke: color-mix(in oklab, currentColor 28%, transparent); }
.fx-load-btn-face .fx-load-arc { stroke: currentColor; }

/* ===================================================== 9. success check === */
.fx-load-check {
  display: inline-grid;
  place-items: center;
  gap: 12px;
  justify-items: center;
}

.fx-load-check-glyph {
  display: block;
  width: var(--fx-load-check-size, 64px);
  height: var(--fx-load-check-size, 64px);
  will-change: transform;
}

.fx-load-check-glyph svg { display: block; width: 100%; height: 100%; overflow: visible; }

.fx-load-check-ring {
  fill: none;
  stroke: var(--fx-load-ok);
  stroke-width: 3;
  opacity: 0.32;
}

.fx-load-check-path {
  fill: none;
  stroke: var(--fx-load-ok);
  stroke-width: 4;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.fx-load-check-label {
  font-size: 0.9rem;
  color: var(--fx-muted);
}

.fx-load-btn-face .fx-load-check-glyph { --fx-load-check-size: 20px; }
.fx-load-btn-face .fx-load-check-ring { display: none; }
.fx-load-btn-face .fx-load-check-path { stroke: currentColor; stroke-width: 5; }

/* ====================================================== 10. error shake === */
.fx-load-shake {
  display: grid;
  gap: 12px;
  justify-items: stretch;
  width: 100%;
}

.fx-load-shake-box {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 14px;
  border: 1px solid var(--fx-border);
  border-radius: var(--fx-radius-sm);
  background: var(--fx-surface-2);
  color: var(--fx-fg);
  font-size: 0.9rem;
  transition: border-color var(--fx-dur-2) ease,
              background-color var(--fx-dur-2) ease,
              color var(--fx-dur-2) ease;
  will-change: transform;
}

.fx-load-shake-box[data-fx-invalid] {
  border-color: var(--fx-load-danger);
  background: color-mix(in oklab, var(--fx-load-danger) 12%, var(--fx-surface-2));
}

.fx-load-shake-msg {
  font-size: 0.82rem;
  color: var(--fx-muted);
  min-height: 1.2em;
  opacity: 0;
  transition: opacity var(--fx-dur-2) var(--fx-ease-out),
              color var(--fx-dur-2) ease;
}

.fx-load-shake-msg[data-fx-on] {
  opacity: 1;
  color: var(--fx-load-danger);
}

.fx-load-shake-dot {
  flex: 0 0 auto;
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--fx-muted);
  transition: background-color var(--fx-dur-2) ease;
}

.fx-load-shake-box[data-fx-invalid] .fx-load-shake-dot { background: var(--fx-load-danger); }

.fx-load-shake .fx-load-trigger { justify-self: start; }

/* ============================================================ 11. toast === */
.fx-load-toast-scope {
  position: relative;
  display: grid;
  place-items: center;
  width: min(380px, 100%);
  min-height: 150px;
  padding-bottom: 8px;
}

.fx-load-toast-dock {
  position: absolute;
  left: 50%;
  bottom: 0;
  translate: -50% 0;
  width: min(320px, 100%);
  pointer-events: none;
}

.fx-load-toast-dock[data-fx-edge='top'] { bottom: auto; top: 0; }

.fx-load-toast {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 14px;
  border-radius: var(--fx-radius-sm);
  border: 1px solid var(--fx-border);
  background: var(--fx-surface-2);
  color: var(--fx-fg);
  box-shadow: var(--fx-shadow);
  font-size: 0.88rem;
  pointer-events: auto;
  will-change: transform, opacity;
}

.fx-load-toast-icon {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 20px; height: 20px;
  border-radius: 50%;
  background: color-mix(in oklab, var(--fx-load-ok) 22%, transparent);
  color: var(--fx-load-ok);
}

.fx-load-toast-icon svg { display: block; width: 12px; height: 12px; }

.fx-load-toast-body { flex: 1 1 auto; }

/* ====================================================== shared trigger === */
.fx-load-trigger {
  appearance: none;
  border: 1px solid var(--fx-border);
  border-radius: var(--fx-radius-sm);
  background: var(--fx-surface-2);
  color: var(--fx-fg);
  font: inherit;
  font-size: 0.86rem;
  padding: 8px 15px;
  cursor: pointer;
  transition: transform var(--fx-dur-1) var(--fx-ease-out),
              background-color var(--fx-dur-1) ease,
              border-color var(--fx-dur-1) ease;
}

@media (hover: hover) and (pointer: fine) {
  .fx-load-trigger:hover { border-color: var(--fx-accent); background: var(--fx-surface); }
}

.fx-load-trigger:active { transform: scale(0.97); }

/* ======================================================= demo scaffolds === */
.fx-load-card {
  width: min(400px, 100%);
  padding: 18px;
  border-radius: var(--fx-radius);
  border: 1px solid var(--fx-border);
  background: var(--fx-surface);
  box-shadow: var(--fx-ring);
}

.fx-load-stackless { display: grid; gap: 14px; justify-items: center; }

.fx-load-row {
  display: flex;
  align-items: center;
  gap: 12px;
}

.fx-load-copy { display: grid; gap: 6px; }
.fx-load-copy h4 { margin: 0; font-size: 0.95rem; font-weight: 600; color: var(--fx-fg); }
.fx-load-copy p  { margin: 0; font-size: 0.82rem; color: var(--fx-muted); }

/* ================================================== reduced motion ======= */
/* Continuous motion off. The JS `breathe()` pulse takes over on an inner node
   so these indicators keep saying "still working" instead of going dead. */
@media (prefers-reduced-motion: reduce) {
  .fx-load-sheen        { animation: none; opacity: 0; }
  .fx-load-spinner-glyph,
  .fx-load-dot,
  .fx-load-bar-seg,
  .fx-load-pulse-inner  { animation: none; }
  .fx-load-dot          { opacity: 0.55; transform: none; }
  .fx-load-bar-seg      { transform: translateX(0); width: 100%; opacity: 0.55; }
  .fx-load-bar-fill,
  .fx-load-ring-arc     { transition-duration: 0.16s; transition-timing-function: linear; }
  .fx-load-btn-face     { transition-property: opacity; transition-duration: 0.12s; }
  .fx-load-btn-face[data-fx-off] { filter: none; transform: none; }
  .fx-load-shake-box    { transform: none; }
}

:root[data-fx-reduced] .fx-load-sheen        { animation: none; opacity: 0; }
:root[data-fx-reduced] .fx-load-spinner-glyph,
:root[data-fx-reduced] .fx-load-dot,
:root[data-fx-reduced] .fx-load-bar-seg,
:root[data-fx-reduced] .fx-load-pulse-inner  { animation: none; }
:root[data-fx-reduced] .fx-load-dot          { opacity: 0.55; transform: none; }
:root[data-fx-reduced] .fx-load-bar-seg      { transform: translateX(0); width: 100%; opacity: 0.55; }
:root[data-fx-reduced] .fx-load-bar-fill,
:root[data-fx-reduced] .fx-load-ring-arc     { transition-duration: 0.16s; transition-timing-function: linear; }
:root[data-fx-reduced] .fx-load-btn-face     { transition-property: opacity; transition-duration: 0.12s; }
:root[data-fx-reduced] .fx-load-btn-face[data-fx-off] { filter: none; transform: none; }
:root[data-fx-reduced] .fx-load-shake-box    { transform: none; }

/* ══ src/effects/loop.css ══ */
/* =========================================================================
 * effects-lab — category: loop  ("Loops & marquees")
 *
 * Every motion here is an infinite CSS keyframe animation so it runs on the
 * compositor and never touches the main thread. JS is used only for the three
 * things CSS cannot do: measuring (seamless marquee period), coupling to
 * scroll velocity, and pausing the loop when it is off-screen or the tab is
 * hidden (see .fx-loop-is-paused below).
 *
 * Reduced motion: NO infinite motion at all. Marquees turn into a static,
 * horizontally scrollable row; orbits / floats / pulses / spins freeze on a
 * deliberately composed frame.
 * ========================================================================= */

/* ---------------------------------------------------------- shared tokens */
[data-fx^="loop-"],
.fx-loop-marquee,
.fx-loop-orbit,
.fx-loop-wave {
  --fx-loop-gap: 2rem;
  --fx-loop-fade: 48px;
  --fx-loop-dur: 24s;
}

/* ------------------------------------------------- off-screen pause gate */
/* Set by JS via IntersectionObserver + document.hidden. Compositor-friendly:
   a paused animation costs nothing. */
.fx-loop-is-paused,
.fx-loop-is-paused * { animation-play-state: paused !important; }

/* =========================================================================
 * MARQUEE ENGINE — shared by loop-marquee, -vertical, -hover-pause,
 * -scroll-speed, loop-logo-belt and loop-ticker.
 *
 * <host class="fx-loop-marquee">
 *   <div class="fx-loop-track">
 *     <div class="fx-loop-group">…real content…</div>
 *     <div class="fx-loop-group fx-loop-clone" aria-hidden inert>…</div> × N
 *
 * The track is translated by exactly ONE group period (measured in JS,
 * including the trailing gap) with a linear curve, so frame 100% is pixel
 * identical to frame 0%. N is ceil(container / period) + 1, which guarantees
 * the container stays covered for narrow content in a wide box.
 * ========================================================================= */
.fx-loop-marquee {
  position: relative;
  display: block;
  inline-size: 100%;
  max-inline-size: 100%;
  overflow: hidden;
  color: var(--fx-fg);
}

.fx-loop-track {
  display: flex;
  flex-wrap: nowrap;
  inline-size: max-content;
  will-change: transform;
}

.fx-loop-group {
  display: flex;
  flex: 0 0 auto;
  flex-wrap: nowrap;
  align-items: center;
  gap: var(--fx-loop-gap);
  padding-inline-end: var(--fx-loop-gap);
}

.fx-loop-clone { pointer-events: none; }

/* --- horizontal ---------------------------------------------------------- */
.fx-loop-horizontal .fx-loop-track { flex-direction: row; }
.fx-loop-horizontal .fx-loop-track.fx-loop-run {
  animation: fx-loop-shift-x var(--fx-loop-dur) linear infinite;
}

/* --- vertical ------------------------------------------------------------ */
.fx-loop-vertical {
  block-size: var(--fx-loop-height, 240px);
}
.fx-loop-vertical .fx-loop-track {
  flex-direction: column;
  inline-size: 100%;
  block-size: max-content;
}
.fx-loop-vertical .fx-loop-group {
  flex-direction: column;
  align-items: stretch;
  padding-inline-end: 0;
  padding-block-end: var(--fx-loop-gap);
}
.fx-loop-vertical .fx-loop-track.fx-loop-run {
  animation: fx-loop-shift-y var(--fx-loop-dur) linear infinite;
}

/* --- direction ----------------------------------------------------------- */
.fx-loop-reverse .fx-loop-track.fx-loop-run { animation-direction: reverse; }

@keyframes fx-loop-shift-x {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(var(--fx-loop-shift, -100%), 0, 0); }
}
@keyframes fx-loop-shift-y {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(0, var(--fx-loop-shift, -100%), 0); }
}

/* --- edge fade ----------------------------------------------------------- */
/* currentColor keeps the mask theme-neutral — only its alpha is read. */
.fx-loop-fade-x {
  -webkit-mask-image: linear-gradient(to right, transparent 0, currentColor var(--fx-loop-fade), currentColor calc(100% - var(--fx-loop-fade)), transparent 100%);
          mask-image: linear-gradient(to right, transparent 0, currentColor var(--fx-loop-fade), currentColor calc(100% - var(--fx-loop-fade)), transparent 100%);
}
.fx-loop-fade-y {
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, currentColor var(--fx-loop-fade), currentColor calc(100% - var(--fx-loop-fade)), transparent 100%);
          mask-image: linear-gradient(to bottom, transparent 0, currentColor var(--fx-loop-fade), currentColor calc(100% - var(--fx-loop-fade)), transparent 100%);
}

/* --- static / reduced-motion marquee ------------------------------------- */
/* No clones, no animation, and the row becomes reachable by scrolling. */
.fx-loop-marquee.fx-loop-static {
  overflow-x: auto;
  overflow-y: hidden;
  overscroll-behavior-x: contain;
  scrollbar-width: thin;
  -webkit-mask-image: none;
          mask-image: none;
}
.fx-loop-vertical.fx-loop-static {
  overflow-x: hidden;
  overflow-y: auto;
  overscroll-behavior-y: contain;
}
.fx-loop-static .fx-loop-track {
  animation: none !important;
  transform: none !important;
  will-change: auto;
}
.fx-loop-static .fx-loop-group { padding-inline-end: 0; padding-block-end: 0; }
.fx-loop-marquee.fx-loop-static:focus-visible {
  outline: 2px solid var(--fx-accent);
  outline-offset: 3px;
}

/* --- hover-pause variant -------------------------------------------------- */
/* The stop itself is a playbackRate ramp in JS (a CSS animation cannot ease
   its own play-state). This is only the visual confirmation. */
.fx-loop-hover-pause .fx-loop-group {
  opacity: 0.88;
  transition: opacity var(--fx-dur-2) var(--fx-ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .fx-loop-hover-pause:hover .fx-loop-group { opacity: 1; }
}
.fx-loop-hover-pause:focus-within .fx-loop-group { opacity: 1; }

/* --- logo belt ------------------------------------------------------------ */
.fx-loop-logo-belt .fx-loop-group > * {
  opacity: 0.6;
  filter: grayscale(1);
  transition: opacity var(--fx-dur-2) var(--fx-ease-out), filter var(--fx-dur-2) var(--fx-ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .fx-loop-logo-belt:hover .fx-loop-group > * { opacity: 1; filter: grayscale(0); }
}

/* --- ticker --------------------------------------------------------------- */
/* Every item carries its own trailing separator, including the last one in a
   group, so the dot rhythm is unbroken across the loop seam. */
.fx-loop-ticker { font-size: 0.85rem; }
.fx-loop-ticker .fx-loop-group > * { position: relative; white-space: nowrap; }
.fx-loop-ticker .fx-loop-group > *::after {
  content: '';
  position: absolute;
  inset-inline-end: calc(var(--fx-loop-gap) / -2);
  inset-block-start: 50%;
  inline-size: 4px;
  block-size: 4px;
  border-radius: 50%;
  background: currentColor;
  opacity: 0.35;
  translate: 50% -50%;
}

/* =========================================================================
 * ORBIT
 * Satellites ride a ring with rotate(θ) translateX(r) rotate(−θ) so the
 * payload stays upright while the position circles the centre.
 * ========================================================================= */
.fx-loop-orbit {
  position: relative;
  inline-size: min(100%, var(--fx-loop-size, 300px));
  aspect-ratio: 1;
  margin-inline: auto;
  color: var(--fx-fg);
}
.fx-loop-orbit-ring {
  position: absolute;
  inset-inline-start: 50%;
  inset-block-start: 50%;
  inline-size: calc(var(--fx-loop-r) * 2);
  block-size: calc(var(--fx-loop-r) * 2);
  translate: -50% -50%;
  border: 1px dashed var(--fx-border);
  border-radius: 50%;
  opacity: 0.7;
  pointer-events: none;
}
.fx-loop-orbit-core {
  position: absolute;
  inset-inline-start: 50%;
  inset-block-start: 50%;
  translate: -50% -50%;
  z-index: 2;
}
.fx-loop-orbit-item {
  position: absolute;
  inset-inline-start: 50%;
  inset-block-start: 50%;
  translate: -50% -50%;
  will-change: transform;
  animation: fx-loop-orbit-run var(--fx-loop-dur) linear infinite;
  animation-delay: calc(-1 * var(--fx-loop-phase, 0) * var(--fx-loop-dur));
}
.fx-loop-orbit-item.fx-loop-ccw { animation-direction: reverse; }

@keyframes fx-loop-orbit-run {
  from { transform: rotate(0deg)   translateX(var(--fx-loop-r)) rotate(0deg); }
  to   { transform: rotate(360deg) translateX(var(--fx-loop-r)) rotate(-360deg); }
}

/* =========================================================================
 * FLOAT — ambient bobbing. Every child gets its own duration, amplitude and
 * a negative delay, so nothing ever ends up in phase with its neighbour.
 * ========================================================================= */
.fx-loop-float-item {
  will-change: transform;
  animation: fx-loop-float var(--fx-loop-dur, 4s) ease-in-out var(--fx-loop-delay, 0s) infinite alternate;
}
@keyframes fx-loop-float {
  from {
    transform: translate3d(0, calc(var(--fx-loop-amp, 10px) * -0.5), 0)
               rotate(calc(var(--fx-loop-tilt, 0deg) * -1));
  }
  to {
    transform: translate3d(0, calc(var(--fx-loop-amp, 10px) * 0.5), 0)
               rotate(var(--fx-loop-tilt, 0deg));
  }
}

/* =========================================================================
 * PULSE — a breathing halo for live / recording / online indicators.
 * ========================================================================= */
.fx-loop-pulse {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
}
.fx-loop-pulse-host { position: relative; }
.fx-loop-pulse-ring {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: currentColor;
  opacity: 0;
  pointer-events: none;
  z-index: 0;
  will-change: transform, opacity;
  animation: fx-loop-pulse-ring var(--fx-loop-dur, 2.4s) cubic-bezier(0.22, 1, 0.36, 1) var(--fx-loop-delay, 0s) infinite;
}
@keyframes fx-loop-pulse-ring {
  0%        { transform: scale(1); opacity: var(--fx-loop-peak, 0.3); }
  70%, 100% { transform: scale(var(--fx-loop-scale, 2.4)); opacity: 0; }
}
.fx-loop-pulse-dot {
  position: relative;
  display: inline-block;
  inline-size: 0.6em;
  block-size: 0.6em;
  border-radius: 50%;
  background: currentColor;
  flex: 0 0 auto;
}

/* =========================================================================
 * SPIN — continuous rotation for badges and circular lockups.
 * ========================================================================= */
.fx-loop-spin-target {
  display: inline-block;
  will-change: transform;
  animation: fx-loop-spin var(--fx-loop-dur, 14s) linear infinite;
}
.fx-loop-spin-target.fx-loop-ccw { animation-direction: reverse; }
@keyframes fx-loop-spin {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}

/* =========================================================================
 * CIRCULAR TEXT
 * ========================================================================= */
.fx-loop-circular {
  position: relative;
  display: inline-grid;
  place-items: center;
  aspect-ratio: 1;
  color: var(--fx-fg);
  line-height: 1;
}
.fx-loop-circular-ring {
  position: absolute;
  inset: 0;
  will-change: transform;
  animation: fx-loop-spin var(--fx-loop-dur, 22s) linear infinite;
}
.fx-loop-circular-ring.fx-loop-ccw { animation-direction: reverse; }
.fx-loop-circular-char {
  position: absolute;
  inset-inline-start: 50%;
  inset-block-start: 50%;
  /* Pivot at the ring centre, NOT at the glyph's own centre — with the default
     origin the -50% centring shift gets rotated too and the ring smears open. */
  transform-origin: 0 0;
  display: block;
  font-size: var(--fx-loop-size, 0.95rem);
  letter-spacing: 0;
  white-space: pre;
}
.fx-loop-circular-center {
  position: relative;
  z-index: 1;
  display: grid;
  place-items: center;
}

/* =========================================================================
 * WAVE — a travelling sine across a row of bars. The stagger is a negative
 * animation-delay per bar, so the crest walks along the row forever.
 * ========================================================================= */
.fx-loop-wave {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--fx-loop-bar-gap, 5px);
  block-size: var(--fx-loop-height, 92px);
  color: var(--fx-accent);
}
.fx-loop-wave-bar {
  display: block;
  flex: 0 1 auto;
  inline-size: var(--fx-loop-bar-w, 5px);
  block-size: 100%;
  border-radius: 999px;
  background: currentColor;
  transform-origin: center;
  will-change: transform;
  animation: fx-loop-wave var(--fx-loop-dur, 1.8s) ease-in-out infinite;
  animation-delay: calc(var(--fx-i, 0) * var(--fx-loop-step, -0.07s));
}
@keyframes fx-loop-wave {
  0%, 100% { transform: scaleY(var(--fx-loop-min, 0.18)); opacity: 0.55; }
  50%      { transform: scaleY(1); opacity: 1; }
}

/* =========================================================================
 * REDUCED MOTION — both the OS setting and the manual switch.
 * Nothing loops. Everything stays legible and reachable.
 * ========================================================================= */
@media (prefers-reduced-motion: reduce) {
  .fx-loop-track,
  .fx-loop-orbit-item,
  .fx-loop-float-item,
  .fx-loop-pulse-ring,
  .fx-loop-spin-target,
  .fx-loop-circular-ring,
  .fx-loop-wave-bar { animation: none !important; }

  .fx-loop-float-item,
  .fx-loop-spin-target { transform: none !important; }

  .fx-loop-marquee {
    overflow-x: auto;
    overflow-y: hidden;
    -webkit-mask-image: none;
            mask-image: none;
  }
  .fx-loop-vertical { overflow-x: hidden; overflow-y: auto; }
  .fx-loop-pulse-ring { opacity: var(--fx-loop-peak, 0.3); transform: scale(1.4); }
}
:root[data-fx-reduced] .fx-loop-track,
:root[data-fx-reduced] .fx-loop-orbit-item,
:root[data-fx-reduced] .fx-loop-float-item,
:root[data-fx-reduced] .fx-loop-pulse-ring,
:root[data-fx-reduced] .fx-loop-spin-target,
:root[data-fx-reduced] .fx-loop-circular-ring,
:root[data-fx-reduced] .fx-loop-wave-bar { animation: none !important; }

:root[data-fx-reduced] .fx-loop-float-item,
:root[data-fx-reduced] .fx-loop-spin-target { transform: none !important; }

:root[data-fx-reduced] .fx-loop-marquee {
  overflow-x: auto;
  overflow-y: hidden;
  -webkit-mask-image: none;
          mask-image: none;
}
:root[data-fx-reduced] .fx-loop-vertical { overflow-x: hidden; overflow-y: auto; }
:root[data-fx-reduced] .fx-loop-pulse-ring { opacity: var(--fx-loop-peak, 0.3); transform: scale(1.4); }

/* =========================================================================
 * DEMO FURNITURE — small neutral pieces used by the gallery previews.
 * Tokens only, no hardcoded colours.
 * ========================================================================= */
.fx-loop-demo {
  inline-size: 100%;
  max-inline-size: 100%;
  min-inline-size: 0;
  display: grid;
  gap: 0.85rem;
  align-content: center;
  color: var(--fx-fg);
  font: 500 0.9rem/1.35 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
}
.fx-loop-eyebrow {
  font-size: 0.66rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--fx-muted);
}
.fx-loop-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.5rem 0.9rem;
  border: 1px solid var(--fx-border);
  border-radius: 999px;
  background: var(--fx-surface-2);
  color: var(--fx-fg);
  font-size: 0.82rem;
  white-space: nowrap;
}
.fx-loop-chip[data-accent] { border-color: var(--fx-accent); color: var(--fx-accent); }
.fx-loop-logo {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 1rem;
  font-weight: 650;
  letter-spacing: -0.02em;
  white-space: nowrap;
  color: var(--fx-fg);
}
.fx-loop-logo i {
  inline-size: 20px;
  block-size: 20px;
  border-radius: 6px;
  background: linear-gradient(135deg, var(--fx-accent), var(--fx-accent-2));
  flex: 0 0 auto;
}
.fx-loop-logo i[data-shape="round"] { border-radius: 50%; }
.fx-loop-logo i[data-shape="tri"] { border-radius: 4px; rotate: 45deg; scale: 0.82; }
.fx-loop-stat { display: inline-flex; align-items: baseline; gap: 0.4rem; white-space: nowrap; }
.fx-loop-stat b { font-variant-numeric: tabular-nums; font-weight: 650; }
.fx-loop-stat u { text-decoration: none; color: var(--fx-muted); font-size: 0.78rem; }
.fx-loop-stat em { font-style: normal; color: var(--fx-accent); font-size: 0.78rem; }
.fx-loop-card {
  display: grid;
  gap: 0.3rem;
  padding: 0.7rem 0.9rem;
  border: 1px solid var(--fx-border);
  border-radius: var(--fx-radius-sm);
  background: var(--fx-surface-2);
  box-shadow: var(--fx-shadow);
}
.fx-loop-card b { font-size: 0.86rem; font-weight: 600; }
.fx-loop-card span { font-size: 0.74rem; color: var(--fx-muted); }
.fx-loop-badge {
  display: grid;
  place-items: center;
  inline-size: 44px;
  block-size: 44px;
  border-radius: 12px;
  background: var(--fx-surface-2);
  border: 1px solid var(--fx-border);
  font-size: 0.78rem;
  font-weight: 650;
  color: var(--fx-fg);
}
.fx-loop-live {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.35rem 0.8rem 0.35rem 0.6rem;
  border-radius: 999px;
  border: 1px solid var(--fx-border);
  background: var(--fx-surface-2);
  color: var(--fx-accent);
  font-size: 0.72rem;
  font-weight: 650;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  flex: 0 0 auto;
}
.fx-loop-row { display: flex; align-items: center; gap: 0.75rem; min-inline-size: 0; }
.fx-loop-row > .fx-loop-marquee { min-inline-size: 0; }
.fx-loop-seal {
  display: grid;
  place-items: center;
  inline-size: 46px;
  block-size: 46px;
  border-radius: 50%;
  background: var(--fx-accent);
  color: var(--fx-accent-contrast);
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.02em;
}

/* ══ src/effects/media.css ══ */
/* =========================================================================
 * effects-lab — category: media  ("Image & media")
 * Every class is .fx-media-*, every keyframe is fx-media-*.
 * No colours are hardcoded: everything derives from src/core/00-tokens.css.
 * ========================================================================= */

/* ---------------------------------------------------------------- frame ---- */
/* The frame is the clipping box. It never changes size, so nothing reflows. */
.fx-media-frame,
[data-fx~="media-clip-reveal"],
[data-fx~="media-mask-scale"],
[data-fx~="media-blur-up"],
[data-fx~="media-ken-burns"],
[data-fx~="media-zoom-hover"],
[data-fx~="media-duotone-hover"],
[data-fx~="media-compare"],
[data-fx~="media-lightbox-zoom"],
[data-fx~="media-tilt-parallax"],
[data-fx~="media-caption-slide"],
[data-fx~="media-shutter"] {
  position: relative;
  display: block;
  overflow: hidden;
  margin: 0;
  aspect-ratio: 3 / 2;
  border-radius: var(--fx-radius);
  background-color: var(--fx-surface);
  box-shadow: var(--fx-ring);
  isolation: isolate;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
}

/* The grid container of media-grid-cascade is a layout, not a frame. */
[data-fx~="media-grid-cascade"] {
  position: relative;
  aspect-ratio: auto;
  overflow: visible;
  background: none;
  box-shadow: none;
  border-radius: 0;
}

.fx-media-frame > img,
.fx-media-frame > .fx-media-img,
.fx-media-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: inherit;
}

/* ---------------------------------------------------- generated artwork ---- */
/* Self-contained placeholder art for demos. No network, no image files:
   layered gradients + a geometric motif + an inline feTurbulence grain. */
.fx-media-placeholder {
  --fx-media-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23g)'/%3E%3C/svg%3E");
  position: relative;
  display: block;
  width: 100%;
  height: 100%;
  border-radius: inherit;
  overflow: hidden;
  background-color: var(--fx-surface-2);
  background-image:
    radial-gradient(90% 120% at 14% 8%, var(--fx-accent) 0%, color-mix(in oklab, var(--fx-accent) 30%, transparent) 42%, transparent 72%),
    radial-gradient(80% 90% at 88% 22%, var(--fx-accent-2) 0%, color-mix(in oklab, var(--fx-accent-2) 24%, transparent) 40%, transparent 70%),
    radial-gradient(110% 120% at 62% 108%, var(--fx-accent-3) 0%, color-mix(in oklab, var(--fx-accent-3) 26%, transparent) 46%, transparent 78%),
    linear-gradient(200deg, var(--fx-surface-2) 0%, var(--fx-surface) 100%);
}

/* geometric motif */
.fx-media-placeholder::before {
  content: "";
  position: absolute;
  inset: 0;
  opacity: 0.4;
  background-image:
    repeating-linear-gradient(115deg, color-mix(in oklab, var(--fx-fg) 14%, transparent) 0 1px, transparent 1px 13px);
  -webkit-mask-image: radial-gradient(120% 100% at 20% 0%, #000 10%, transparent 76%);
          mask-image: radial-gradient(120% 100% at 20% 0%, #000 10%, transparent 76%);
}

/* film grain */
.fx-media-placeholder::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image: var(--fx-media-grain);
  background-size: 180px 180px;
  opacity: 0.17;
  mix-blend-mode: overlay;
  pointer-events: none;
}

.fx-media-placeholder[data-ph="b"] {
  background-image:
    radial-gradient(70% 90% at 78% 12%, var(--fx-accent-2) 0%, color-mix(in oklab, var(--fx-accent-2) 28%, transparent) 38%, transparent 68%),
    radial-gradient(100% 110% at 6% 92%, var(--fx-accent) 0%, color-mix(in oklab, var(--fx-accent) 26%, transparent) 44%, transparent 76%),
    linear-gradient(160deg, var(--fx-surface) 0%, var(--fx-surface-2) 100%);
}
.fx-media-placeholder[data-ph="b"]::before {
  opacity: 0.55;
  background-image:
    repeating-radial-gradient(circle at 74% 26%, transparent 0 16px, color-mix(in oklab, var(--fx-fg) 20%, transparent) 16px 17px);
  -webkit-mask-image: radial-gradient(90% 90% at 74% 26%, #000 0%, transparent 80%);
          mask-image: radial-gradient(90% 90% at 74% 26%, #000 0%, transparent 80%);
}

.fx-media-placeholder[data-ph="c"] {
  background-image:
    radial-gradient(90% 120% at 50% 0%, var(--fx-accent-3) 0%, color-mix(in oklab, var(--fx-accent-3) 26%, transparent) 40%, transparent 74%),
    linear-gradient(20deg, var(--fx-surface-2) 0%, var(--fx-surface) 60%, var(--fx-surface-2) 100%);
}
.fx-media-placeholder[data-ph="c"]::before {
  opacity: 0.5;
  background-image:
    repeating-linear-gradient(60deg, color-mix(in oklab, var(--fx-accent) 34%, transparent) 0 6px, transparent 6px 30px);
  -webkit-mask-image: linear-gradient(60deg, #000 0%, transparent 85%);
          mask-image: linear-gradient(60deg, #000 0%, transparent 85%);
}

.fx-media-placeholder[data-ph="d"] {
  background-image:
    radial-gradient(80% 100% at 92% 96%, var(--fx-accent) 0%, color-mix(in oklab, var(--fx-accent) 26%, transparent) 40%, transparent 72%),
    linear-gradient(300deg, var(--fx-surface) 0%, var(--fx-surface-2) 100%);
}
.fx-media-placeholder[data-ph="d"]::before {
  opacity: 0.6;
  background-image:
    radial-gradient(color-mix(in oklab, var(--fx-fg) 26%, transparent) 1.2px, transparent 1.3px);
  background-size: 14px 14px;
  -webkit-mask-image: linear-gradient(210deg, #000 0%, transparent 78%);
          mask-image: linear-gradient(210deg, #000 0%, transparent 78%);
}

/* A deliberately coarse, low-detail twin used as the blur-up placeholder. */
.fx-media-lqip {
  position: absolute;
  inset: 0;
  z-index: 2;
}
.fx-media-lqip::before,
.fx-media-lqip::after { display: none; }

/* demo scaffolding ---------------------------------------------------------- */
.fx-media-demo {
  width: 100%;
  max-width: 420px;
  margin: 0 auto;
  color: var(--fx-fg);
  font: 500 13px/1.4 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
}
.fx-media-demo.is-wide { max-width: 490px; }
.fx-media-hint {
  margin: 10px 0 0;
  text-align: center;
  color: var(--fx-muted);
  font: 600 10px/1.5 ui-sans-serif, system-ui, -apple-system, sans-serif;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

/* ================================================== 1. media-clip-reveal ==== */
/* A mask, not clip-path: an element clipped to nothing never reports as
   intersecting, so a clip-path reveal would never fire on scroll. */
.fx-media-clip {
  --fx-media-dur: 0.9s;
  --fx-media-delay: 0s;
  -webkit-mask-image: var(--fx-media-mask-img);
          mask-image: var(--fx-media-mask-img);
  -webkit-mask-size: var(--fx-media-mask-size);
          mask-size: var(--fx-media-mask-size);
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-position: var(--fx-media-mask-from);
          mask-position: var(--fx-media-mask-from);
  transition:
    -webkit-mask-position var(--fx-media-dur) var(--fx-ease-out) var(--fx-media-delay),
    mask-position var(--fx-media-dur) var(--fx-ease-out) var(--fx-media-delay);
}
.fx-media-clip.fx-media-in {
  -webkit-mask-position: var(--fx-media-mask-to);
          mask-position: var(--fx-media-mask-to);
}

.fx-media-clip > .fx-media-inner {
  transform: var(--fx-media-from, none);
  transition: transform var(--fx-media-dur) var(--fx-ease-out) var(--fx-media-delay);
  will-change: transform;
}
.fx-media-clip.fx-media-in > .fx-media-inner { transform: none; }

/* ================================================== 2. media-mask-scale ===== */
.fx-media-mask {
  --fx-media-dur: 1s;
  --fx-media-delay: 0s;
  -webkit-mask-image: var(--fx-media-mask-img);
          mask-image: var(--fx-media-mask-img);
  -webkit-mask-size: var(--fx-media-mask-size);
          mask-size: var(--fx-media-mask-size);
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-position: var(--fx-media-mask-from);
          mask-position: var(--fx-media-mask-from);
  transition:
    -webkit-mask-position var(--fx-media-dur) var(--fx-ease-out) var(--fx-media-delay),
    mask-position var(--fx-media-dur) var(--fx-ease-out) var(--fx-media-delay);
}
.fx-media-mask.fx-media-in {
  -webkit-mask-position: var(--fx-media-mask-to);
          mask-position: var(--fx-media-mask-to);
}
.fx-media-mask > .fx-media-inner {
  transform: scale(var(--fx-media-scale, 1.15));
  transform-origin: center;
  transition: transform var(--fx-media-dur) var(--fx-ease-out) var(--fx-media-delay);
  will-change: transform;
}
.fx-media-mask.fx-media-in > .fx-media-inner { transform: scale(1); }

/* ================================================== 3. media-blur-up ======== */
.fx-media-blurup { --fx-media-dur: 0.7s; }
.fx-media-blurup > .fx-media-lqip {
  filter: blur(18px) saturate(1.25);
  transform: scale(1.1);
  opacity: 1;
  transition: opacity calc(var(--fx-media-dur) * 0.8) var(--fx-ease-out);
}
.fx-media-blurup > .fx-media-inner {
  opacity: 0;
  filter: blur(14px);
  transform: scale(1.05);
  transition:
    opacity calc(var(--fx-media-dur) * 0.7) var(--fx-ease-out),
    filter var(--fx-media-dur) var(--fx-ease-out),
    transform var(--fx-media-dur) var(--fx-ease-out);
  will-change: filter, transform, opacity;
}
.fx-media-blurup.fx-media-in > .fx-media-lqip { opacity: 0; }
.fx-media-blurup.fx-media-in > .fx-media-inner {
  opacity: 1;
  filter: blur(0);
  transform: scale(1);
}

/* ================================================== 4. media-ken-burns ====== */
.fx-media-kenburns > .fx-media-inner {
  transform-origin: var(--fx-media-origin, 50% 50%);
  animation: fx-media-ken var(--fx-media-dur, 20s) var(--fx-ease) infinite alternate both;
  will-change: transform;
}
@keyframes fx-media-ken {
  from { transform: scale(1) translate3d(0, 0, 0); }
  to   { transform: scale(var(--fx-media-scale, 1.14)) translate3d(var(--fx-media-tx, 0%), var(--fx-media-ty, 0%), 0); }
}
/* static frame when motion is off */
.fx-media-kenburns.fx-media-still > .fx-media-inner {
  animation: none;
  transform: scale(calc(1 + (var(--fx-media-scale, 1.14) - 1) * 0.5));
}

/* ================================================== 5. media-zoom-hover ===== */
[data-fx~="media-zoom-hover"] > img,
[data-fx~="media-zoom-hover"] > .fx-media-img {
  transform: scale(1);
  transition: transform 420ms var(--fx-ease-out);
  will-change: transform;
}
@media (hover: hover) and (pointer: fine) {
  [data-fx~="media-zoom-hover"]:hover > img,
  [data-fx~="media-zoom-hover"]:hover > .fx-media-img,
  [data-fx~="media-zoom-hover"]:focus-within > img,
  [data-fx~="media-zoom-hover"]:focus-within > .fx-media-img {
    transform: scale(var(--fx-media-zoom, 1.06));
  }
}

/* ================================================== 6. media-duotone-hover == */
[data-fx~="media-duotone-hover"] > img,
[data-fx~="media-duotone-hover"] > .fx-media-img {
  filter: grayscale(1) contrast(1.06) brightness(0.96);
  transform: scale(1);
  transition:
    filter 440ms var(--fx-ease-out),
    transform 440ms var(--fx-ease-out);
}
[data-fx~="media-duotone-hover"]::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  border-radius: inherit;
  background-image: linear-gradient(135deg, var(--fx-accent-3) 0%, var(--fx-accent) 100%);
  mix-blend-mode: color;
  opacity: var(--fx-media-duotone, 0.92);
  transition: opacity 440ms var(--fx-ease-out);
}
@media (hover: hover) and (pointer: fine) {
  [data-fx~="media-duotone-hover"]:hover > img,
  [data-fx~="media-duotone-hover"]:hover > .fx-media-img,
  [data-fx~="media-duotone-hover"]:focus-within > img,
  [data-fx~="media-duotone-hover"]:focus-within > .fx-media-img {
    filter: grayscale(0) contrast(1) brightness(1);
    transform: scale(1.04);
  }
  [data-fx~="media-duotone-hover"]:hover::after,
  [data-fx~="media-duotone-hover"]:focus-within::after { opacity: 0; }
}

/* ================================================== 7. media-compare ======== */
.fx-media-compare {
  touch-action: none;
  cursor: ew-resize;
  -webkit-user-select: none;
          user-select: none;
}
.fx-media-compare > img,
.fx-media-compare > .fx-media-img { position: absolute; inset: 0; z-index: 1; }

.fx-media-compare-clip {
  position: absolute;
  inset: 0;
  z-index: 2;
  border-radius: inherit;
  clip-path: inset(0 calc(100% - var(--fx-media-p, 50%)) 0 0);
  transition: clip-path var(--fx-dur-2) var(--fx-ease-out);
}
.fx-media-compare-clip > img,
.fx-media-compare-clip > .fx-media-img,
.fx-media-compare-clip > .fx-media-placeholder {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.fx-media-compare-handle {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  transform: translate3d(var(--fx-media-p, 50%), 0, 0);
  transition: transform var(--fx-dur-2) var(--fx-ease-out);
}
.fx-media-compare-handle::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: -1px;
  width: 2px;
  background: var(--fx-fg);
  opacity: 0.9;
}
.fx-media-compare-grip {
  position: absolute;
  top: 50%;
  left: 0;
  width: 40px;
  height: 40px;
  margin: 0;
  padding: 0;
  display: grid;
  place-items: center;
  transform: translate(-50%, -50%);
  pointer-events: auto;
  cursor: ew-resize;
  border: 1px solid var(--fx-border);
  border-radius: 999px;
  background: var(--fx-surface);
  color: var(--fx-fg);
  box-shadow: var(--fx-shadow);
  transition: transform var(--fx-dur-1) var(--fx-ease-out);
}
.fx-media-compare-grip svg { width: 20px; height: 20px; display: block; }
.fx-media-compare-grip:focus-visible {
  outline: 2px solid var(--fx-accent);
  outline-offset: 3px;
}
.fx-media-compare.is-dragging .fx-media-compare-clip,
.fx-media-compare.is-dragging .fx-media-compare-handle { transition: none; }
.fx-media-compare.is-dragging .fx-media-compare-grip { transform: translate(-50%, -50%) scale(1.06); }

.fx-media-compare-label {
  position: absolute;
  bottom: 10px;
  z-index: 4;
  padding: 3px 9px;
  border-radius: 999px;
  border: 1px solid var(--fx-border);
  background: var(--fx-surface);
  color: var(--fx-muted);
  font: 600 10px/1.5 ui-sans-serif, system-ui, sans-serif;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  pointer-events: none;
}
.fx-media-compare-label.is-start { left: 10px; }
.fx-media-compare-label.is-end { right: 10px; }

/* ================================================== 8. media-lightbox-zoom == */
[data-fx~="media-lightbox-zoom"] { cursor: zoom-in; }
[data-fx~="media-lightbox-zoom"]:focus-visible {
  outline: 2px solid var(--fx-accent);
  outline-offset: 4px;
}
.fx-media-lightbox {
  position: fixed;
  inset: 0;
  z-index: 2147483000;
  display: block;
}
.fx-media-lightbox-backdrop {
  position: absolute;
  inset: 0;
  background: var(--fx-bg);
  opacity: 0;
  -webkit-backdrop-filter: blur(var(--fx-blur));
          backdrop-filter: blur(var(--fx-blur));
  cursor: zoom-out;
}
.fx-media-lightbox-stage {
  position: absolute;
  top: 0;
  left: 0;
  overflow: hidden;
  border-radius: var(--fx-radius-lg);
  box-shadow: var(--fx-shadow);
  transform-origin: 0 0;
  will-change: transform;
}
.fx-media-lightbox-stage > img,
.fx-media-lightbox-stage > .fx-media-img,
.fx-media-lightbox-stage > .fx-media-placeholder {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.fx-media-lightbox-close {
  position: absolute;
  top: 16px;
  right: 16px;
  width: 40px;
  height: 40px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 1px solid var(--fx-border);
  border-radius: 999px;
  background: var(--fx-surface);
  color: var(--fx-fg);
  cursor: pointer;
  opacity: 0;
  transition: opacity var(--fx-dur-2) var(--fx-ease-out);
}
.fx-media-lightbox-close svg { width: 18px; height: 18px; display: block; }
.fx-media-lightbox-close:focus-visible {
  outline: 2px solid var(--fx-accent);
  outline-offset: 3px;
}
.fx-media-lightbox.is-open .fx-media-lightbox-close { opacity: 1; }

/* ================================================== 9. media-tilt-parallax == */
.fx-media-tilt {
  perspective: var(--fx-media-perspective, 900px);
  transform-style: preserve-3d;
}
.fx-media-tilt > .fx-media-inner {
  transform:
    rotateX(var(--fx-media-rx, 0deg))
    rotateY(var(--fx-media-ry, 0deg))
    translate3d(var(--fx-media-tx, 0px), var(--fx-media-ty, 0px), 0)
    scale(var(--fx-media-scale, 1.08));
  will-change: transform;
}

/* ================================================= 10. media-caption-slide == */
.fx-media-caption {
  position: absolute;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 3;
  display: grid;
  gap: 4px;
  padding: 46px 18px 16px;
  color: var(--fx-fg);
  text-align: left;
  background-image: linear-gradient(
    to top,
    color-mix(in oklab, var(--fx-bg) 92%, transparent) 0%,
    color-mix(in oklab, var(--fx-bg) 62%, transparent) 55%,
    transparent 100%
  );
  transform: translate3d(0, 100%, 0);
  opacity: 0;
  transition:
    transform 420ms var(--fx-ease-out),
    opacity 300ms var(--fx-ease-out);
  will-change: transform, opacity;
}
.fx-media-caption-title {
  font: 600 15px/1.25 ui-sans-serif, system-ui, -apple-system, sans-serif;
  transform: translate3d(0, 10px, 0);
  transition: transform 420ms var(--fx-ease-out) 40ms;
}
.fx-media-caption-text {
  font: 400 12px/1.45 ui-sans-serif, system-ui, -apple-system, sans-serif;
  color: var(--fx-muted);
  transform: translate3d(0, 10px, 0);
  transition: transform 420ms var(--fx-ease-out) 90ms;
}
@media (hover: hover) and (pointer: fine) {
  [data-fx~="media-caption-slide"]:hover .fx-media-caption,
  [data-fx~="media-caption-slide"]:focus-within .fx-media-caption {
    transform: translate3d(0, 0, 0);
    opacity: 1;
  }
  [data-fx~="media-caption-slide"]:hover .fx-media-caption-title,
  [data-fx~="media-caption-slide"]:hover .fx-media-caption-text,
  [data-fx~="media-caption-slide"]:focus-within .fx-media-caption-title,
  [data-fx~="media-caption-slide"]:focus-within .fx-media-caption-text {
    transform: translate3d(0, 0, 0);
  }
}
/* Touch / coarse pointers have no hover: the caption simply stays visible. */
@media not all and (hover: hover) {
  .fx-media-caption { transform: none; opacity: 1; }
  .fx-media-caption-title,
  .fx-media-caption-text { transform: none; }
}

/* ================================================= 11. media-grid-cascade === */
.fx-media-grid {
  display: grid;
  grid-template-columns: repeat(var(--fx-media-cols, 3), 1fr);
  gap: var(--fx-media-gap, 10px);
}
.fx-media-grid > * { aspect-ratio: 4 / 3; }

/* ================================================= 12. media-shutter ======== */
.fx-media-shutter > .fx-media-inner {
  transform: scale(var(--fx-media-scale, 1.06));
  transition: transform var(--fx-media-dur, 0.8s) var(--fx-ease-out);
  will-change: transform;
}
.fx-media-shutter.fx-media-in > .fx-media-inner { transform: scale(1); }

.fx-media-slats {
  position: absolute;
  inset: 0;
  z-index: 4;
  pointer-events: none;
  border-radius: inherit;
  overflow: hidden;
}
.fx-media-slat {
  position: absolute;
  background: var(--fx-media-slat-color, var(--fx-surface));
  transform-origin: var(--fx-media-slat-origin, center top);
  transition: transform var(--fx-media-dur, 0.8s) var(--fx-ease-out) var(--fx-media-slat-delay, 0s);
  will-change: transform;
}

/* ========================================================================= */
/* Reduced motion — the image is simply there: sharp, unmasked, in place.     */
/* ========================================================================= */
@media (prefers-reduced-motion: reduce) {
  .fx-media-clip,
  .fx-media-mask,
  .fx-media-blurup,
  .fx-media-shutter {
    clip-path: none;
    -webkit-mask-image: none;
            mask-image: none;
    transition: none;
  }
  .fx-media-clip > .fx-media-inner,
  .fx-media-mask > .fx-media-inner,
  .fx-media-blurup > .fx-media-inner,
  .fx-media-shutter > .fx-media-inner,
  .fx-media-kenburns > .fx-media-inner,
  .fx-media-tilt > .fx-media-inner {
    animation: none;
    transform: none;
    filter: none;
    opacity: 1;
    transition: none;
    will-change: auto;
  }
  .fx-media-blurup > .fx-media-lqip,
  .fx-media-slats { display: none; }

  [data-fx~="media-zoom-hover"] > img,
  [data-fx~="media-zoom-hover"] > .fx-media-img,
  [data-fx~="media-duotone-hover"] > img,
  [data-fx~="media-duotone-hover"] > .fx-media-img { transition: none; transform: none; }
  [data-fx~="media-zoom-hover"]:hover > img,
  [data-fx~="media-zoom-hover"]:hover > .fx-media-img,
  [data-fx~="media-zoom-hover"]:focus-within > img,
  [data-fx~="media-zoom-hover"]:focus-within > .fx-media-img { transform: none; }
  [data-fx~="media-duotone-hover"]:hover > img,
  [data-fx~="media-duotone-hover"]:hover > .fx-media-img,
  [data-fx~="media-duotone-hover"]:focus-within > img,
  [data-fx~="media-duotone-hover"]:focus-within > .fx-media-img { transform: none; }

  .fx-media-caption {
    transform: none;
    opacity: 1;
    transition: none;
  }
  .fx-media-caption-title,
  .fx-media-caption-text { transform: none; transition: none; }

  .fx-media-compare-clip,
  .fx-media-compare-handle,
  .fx-media-compare-grip { transition: none; }
  .fx-media-lightbox-close { transition: none; opacity: 1; }
}

:root[data-fx-reduced] .fx-media-clip,
:root[data-fx-reduced] .fx-media-mask,
:root[data-fx-reduced] .fx-media-blurup,
:root[data-fx-reduced] .fx-media-shutter {
  clip-path: none;
  -webkit-mask-image: none;
          mask-image: none;
  transition: none;
}
:root[data-fx-reduced] .fx-media-clip > .fx-media-inner,
:root[data-fx-reduced] .fx-media-mask > .fx-media-inner,
:root[data-fx-reduced] .fx-media-blurup > .fx-media-inner,
:root[data-fx-reduced] .fx-media-shutter > .fx-media-inner,
:root[data-fx-reduced] .fx-media-kenburns > .fx-media-inner,
:root[data-fx-reduced] .fx-media-tilt > .fx-media-inner {
  animation: none;
  transform: none;
  filter: none;
  opacity: 1;
  transition: none;
  will-change: auto;
}
:root[data-fx-reduced] .fx-media-blurup > .fx-media-lqip,
:root[data-fx-reduced] .fx-media-slats { display: none; }

:root[data-fx-reduced] [data-fx~="media-zoom-hover"] > img,
:root[data-fx-reduced] [data-fx~="media-zoom-hover"] > .fx-media-img,
:root[data-fx-reduced] [data-fx~="media-duotone-hover"] > img,
:root[data-fx-reduced] [data-fx~="media-duotone-hover"] > .fx-media-img { transition: none; transform: none; }
:root[data-fx-reduced] [data-fx~="media-zoom-hover"]:hover > img,
:root[data-fx-reduced] [data-fx~="media-zoom-hover"]:hover > .fx-media-img,
:root[data-fx-reduced] [data-fx~="media-zoom-hover"]:focus-within > img,
:root[data-fx-reduced] [data-fx~="media-zoom-hover"]:focus-within > .fx-media-img { transform: none; }
:root[data-fx-reduced] [data-fx~="media-duotone-hover"]:hover > img,
:root[data-fx-reduced] [data-fx~="media-duotone-hover"]:hover > .fx-media-img,
:root[data-fx-reduced] [data-fx~="media-duotone-hover"]:focus-within > img,
:root[data-fx-reduced] [data-fx~="media-duotone-hover"]:focus-within > .fx-media-img { transform: none; }

:root[data-fx-reduced] .fx-media-caption {
  transform: none;
  opacity: 1;
  transition: none;
}
:root[data-fx-reduced] .fx-media-caption-title,
:root[data-fx-reduced] .fx-media-caption-text { transform: none; transition: none; }

:root[data-fx-reduced] .fx-media-compare-clip,
:root[data-fx-reduced] .fx-media-compare-handle,
:root[data-fx-reduced] .fx-media-compare-grip { transition: none; }
:root[data-fx-reduced] .fx-media-lightbox-close { transition: none; opacity: 1; }

/* ══ src/effects/nav.css ══ */
/* =========================================================================
 * effects-lab — category: nav ("Navigation & chrome")
 *
 * Headers, tab bars, docks, sidebars and the chrome around a page.
 * House rules for this category:
 *   · state changes land in 200-300ms (--fx-dur-2), reduced motion <=120ms
 *   · every threshold has hysteresis, so nothing flickers on the boundary
 *   · indicators move with a spring and animate position *and* size
 *   · only transform / opacity / filter / backdrop-filter run per frame;
 *     the two layout properties that do animate (header padding, sidebar
 *     width) are one-shot state changes on a single contained element
 *
 * All classes start `.fx-nav-`, all keyframes `fx-nav-`.
 * ========================================================================= */

/* ============================================================== primitives */

/* The library cannot assume the host page ships a box-sizing reset, and the
   sidebar's width animation and the dock's geometry both depend on it. */
[class*="fx-nav-"] { box-sizing: border-box; }

/* A column so demo panes (sidebar shell, dock stage) can fill the stage. */
.fx-nav-demo {
  display: flex;
  flex-direction: column;
  color: var(--fx-fg);
  font: 500 14px/1.45 inherit;
  background: var(--fx-bg);
  min-height: 100%;
}

.fx-nav-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  padding: 14px 16px;
  background: var(--fx-surface);
  border-bottom: 1px solid var(--fx-border);
}

.fx-nav-sticky {
  position: sticky;
  top: 0;
  z-index: 6;
}

.fx-nav-brand {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  font-weight: 640;
  font-size: 14px;
  letter-spacing: -0.012em;
  white-space: nowrap;
}

.fx-nav-mark {
  width: 22px;
  height: 22px;
  flex: none;
  border-radius: 7px;
  background: linear-gradient(140deg, var(--fx-accent), var(--fx-accent-2));
  box-shadow: inset 0 1px 0 color-mix(in srgb, var(--fx-fg) 22%, transparent);
}

.fx-nav-links {
  display: flex;
  align-items: center;
  gap: 2px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.fx-nav-link {
  position: relative;
  z-index: 1;
  display: inline-block;
  padding: 7px 12px;
  border-radius: 999px;
  color: var(--fx-muted);
  font-size: 13px;
  font-weight: 560;
  text-decoration: none;
  white-space: nowrap;
  background: none;
  border: 0;
  cursor: pointer;
  font-family: inherit;
  transition: color var(--fx-dur-2) var(--fx-ease);
}

.fx-nav-link:focus-visible {
  outline: 2px solid var(--fx-accent);
  outline-offset: 2px;
}

.fx-nav-link.is-active,
.fx-nav-link[aria-current] {
  color: var(--fx-fg);
}

.fx-nav-pillbtn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 13px;
  border-radius: 999px;
  border: 1px solid var(--fx-border);
  background: var(--fx-surface-2);
  color: var(--fx-fg);
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
}

/* ---- page filler, so scroll demos have something to scroll ------------- */

.fx-nav-body {
  display: grid;
  gap: 18px;
  padding: 18px 16px 28px;
}

.fx-nav-block > h4 {
  margin: 0 0 10px;
  font-size: 13px;
  font-weight: 640;
  letter-spacing: -0.01em;
  color: var(--fx-fg);
}

.fx-nav-lines {
  display: grid;
  gap: 7px;
}

.fx-nav-lines i {
  display: block;
  height: 7px;
  border-radius: 4px;
  background: var(--fx-border);
}

.fx-nav-lines i:nth-child(2) { width: 88%; }
.fx-nav-lines i:nth-child(3) { width: 64%; }
.fx-nav-lines i:nth-child(4) { width: 79%; }

/* ============================================ shared sliding indicator ---- */

.fx-nav-tabs {
  position: relative;
  display: inline-flex;
  align-items: center;
  padding: 3px;
  border-radius: 999px;
}

.fx-nav-indicator {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 0;
  width: 0;
  height: 0;
  border-radius: 999px;
  pointer-events: none;
  background: color-mix(in srgb, var(--fx-accent) 20%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fx-accent) 38%, transparent);
  will-change: transform, width, height;
}

.fx-nav-indicator.is-underline {
  border-radius: 2px;
  background: var(--fx-accent);
  box-shadow: none;
}

/* ================================================= nav-shrink ------------ */

.fx-nav-shrink {
  border-bottom-color: transparent;
  transition:
    padding var(--fx-dur-2) var(--fx-ease),
    border-color var(--fx-dur-2) var(--fx-ease),
    box-shadow var(--fx-dur-2) var(--fx-ease),
    background-color var(--fx-dur-2) var(--fx-ease);
}

.fx-nav-shrink .fx-nav-brand {
  transform-origin: left center;
  transition: transform var(--fx-dur-2) var(--fx-ease);
}

.fx-nav-shrink.is-condensed {
  padding-block: 8px;
  border-bottom-color: var(--fx-border);
  box-shadow: 0 1px 0 color-mix(in srgb, var(--fx-fg) 6%, transparent);
}

.fx-nav-shrink.is-condensed .fx-nav-brand { transform: scale(0.88); }

/* =========================================== nav-hide-on-scroll ---------- */

.fx-nav-hide {
  will-change: transform;
  transition: transform var(--fx-dur-2) var(--fx-ease);
}

.fx-nav-hide.is-hidden { transform: translate3d(0, -101%, 0); }

/* ============================================ nav-blur-on-scroll --------- */

.fx-nav-blur {
  background: transparent;
  border-bottom-color: transparent;
  transition:
    background-color var(--fx-dur-2) var(--fx-ease),
    backdrop-filter var(--fx-dur-2) var(--fx-ease),
    border-color var(--fx-dur-2) var(--fx-ease);
}

.fx-nav-blur.is-floating {
  background: color-mix(in srgb, var(--fx-surface) 68%, transparent);
  backdrop-filter: blur(var(--fx-blur)) saturate(180%);
  -webkit-backdrop-filter: blur(var(--fx-blur)) saturate(180%);
  border-bottom-color: color-mix(in srgb, var(--fx-border) 70%, transparent);
}

/* the scroll edge: a soft fade where content meets the chrome, not a rule */
.fx-nav-blur::after {
  content: "";
  position: absolute;
  inset: 100% 0 auto;
  height: 14px;
  pointer-events: none;
  opacity: 0;
  background: linear-gradient(to bottom, color-mix(in srgb, var(--fx-bg) 60%, transparent), transparent);
  transition: opacity var(--fx-dur-2) var(--fx-ease);
}

.fx-nav-blur.is-floating::after { opacity: 1; }

@media (prefers-reduced-transparency: reduce) {
  .fx-nav-blur.is-floating {
    background: var(--fx-surface);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
}

/* ================================================ nav-scroll-spy --------- */

.fx-nav-rail {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 2px;
  padding: 4px;
  border-radius: var(--fx-radius);
}

.fx-nav-rail .fx-nav-link {
  display: block;
  border-radius: var(--fx-radius-sm);
  text-align: left;
}

.fx-nav-rail .fx-nav-indicator { border-radius: var(--fx-radius-sm); }

.fx-nav-spy-layout {
  display: grid;
  grid-template-columns: 148px minmax(0, 1fr);
  gap: 14px;
  align-items: start;
  padding: 14px 16px 28px;
}

.fx-nav-spy-layout { flex: 1 1 auto; }

/* clears the sticky header above it */
.fx-nav-spy-aside {
  position: sticky;
  top: 62px;
}

.fx-nav-section {
  scroll-margin-top: 16px;
  padding-block: 10px 26px;
}

/* lets the last section scroll past the reading line */
.fx-nav-tail { height: 190px; }

/* ====================================================== nav-dock --------- */

.fx-nav-dock {
  position: relative;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  gap: 10px;
  padding: 10px 14px;
  border-radius: var(--fx-radius-lg);
  border: 1px solid color-mix(in srgb, var(--fx-border) 80%, transparent);
  background: color-mix(in srgb, var(--fx-surface) 76%, transparent);
  backdrop-filter: blur(var(--fx-blur)) saturate(180%);
  -webkit-backdrop-filter: blur(var(--fx-blur)) saturate(180%);
  box-shadow: var(--fx-shadow);
}

.fx-nav-dock-item {
  position: relative;
  flex: none;
  width: 42px;
  height: 42px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  border-radius: 12px;
  background: linear-gradient(150deg, var(--fx-surface-2), var(--fx-surface));
  box-shadow: inset 0 0 0 1px var(--fx-border);
  color: var(--fx-fg);
  font: inherit;
  font-size: 15px;
  font-weight: 640;
  cursor: pointer;
  transform-origin: bottom center;
  will-change: transform;
}

.fx-nav-dock-item:nth-child(2) { background: linear-gradient(150deg, var(--fx-accent), var(--fx-accent-2)); color: var(--fx-accent-contrast); }
.fx-nav-dock-item:nth-child(4) { background: linear-gradient(150deg, var(--fx-accent-3), var(--fx-accent)); color: var(--fx-accent-contrast); }

.fx-nav-dock-item:focus-visible { outline: 2px solid var(--fx-accent); outline-offset: 3px; }

.fx-nav-dock-tip {
  position: absolute;
  bottom: calc(100% + 9px);
  left: 50%;
  padding: 3px 8px;
  border-radius: 7px;
  background: var(--fx-surface-2);
  box-shadow: var(--fx-ring), var(--fx-shadow);
  color: var(--fx-fg);
  font-size: 11px;
  font-weight: 560;
  white-space: nowrap;
  opacity: 0;
  transform: translate(-50%, 4px);
  pointer-events: none;
}

.fx-nav-dock-stage {
  flex: 1 1 auto;
  display: grid;
  place-items: end center;
  min-height: 200px;
  padding: 16px 16px 24px;
}

/* fallback presentation when magnification is off (touch / reduced motion) */
.fx-nav-dock.is-static .fx-nav-dock-item {
  transition: transform var(--fx-dur-1) var(--fx-ease-out);
}

@media (hover: hover) and (pointer: fine) {
  .fx-nav-dock.is-static .fx-nav-dock-item:hover { transform: translateY(-3px); }
}

/* ================================================= nav-mega-menu --------- */

.fx-nav-mega { position: relative; }

.fx-nav-mega-trigger {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 7px 12px;
  border: 0;
  border-radius: 999px;
  background: none;
  color: var(--fx-muted);
  font: inherit;
  font-size: 13px;
  font-weight: 560;
  cursor: pointer;
  transition: color var(--fx-dur-1) var(--fx-ease), background-color var(--fx-dur-1) var(--fx-ease);
}

.fx-nav-mega-trigger::after {
  content: "";
  width: 5px;
  height: 5px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: translateY(-1px) rotate(45deg);
  transition: transform var(--fx-dur-2) var(--fx-ease);
}

.fx-nav-mega-trigger[aria-expanded="true"] {
  color: var(--fx-fg);
  background: var(--fx-surface-2);
}

.fx-nav-mega-trigger[aria-expanded="true"]::after { transform: translateY(1px) rotate(-135deg); }

.fx-nav-mega-viewport {
  position: relative;
  height: 0;
  overflow: hidden;
  visibility: hidden;
  border-top: 1px solid transparent;
  background: var(--fx-surface);
}

.fx-nav-mega-viewport.is-open {
  visibility: visible;
  border-top-color: var(--fx-border);
}

.fx-nav-mega-panel {
  position: absolute;
  inset: 0 0 auto 0;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px 14px;
  padding: 14px 16px 16px;
  opacity: 0;
  pointer-events: none;
}

.fx-nav-mega-panel.is-active { pointer-events: auto; }

.fx-nav-mega-item {
  display: block;
  padding: 7px 9px;
  border-radius: var(--fx-radius-sm);
  color: var(--fx-fg);
  font-size: 12.5px;
  font-weight: 560;
  text-decoration: none;
  transition: background-color var(--fx-dur-1) var(--fx-ease);
}

.fx-nav-mega-item span {
  display: block;
  margin-top: 2px;
  color: var(--fx-muted);
  font-size: 11px;
  font-weight: 450;
}

@media (hover: hover) and (pointer: fine) {
  .fx-nav-mega-item:hover { background: var(--fx-surface-2); }
  .fx-nav-mega-trigger:hover { color: var(--fx-fg); }
}

.fx-nav-mega-item:focus-visible { outline: 2px solid var(--fx-accent); outline-offset: 1px; }

/* =========================================== nav-breadcrumb-reveal ------- */

.fx-nav-crumbs {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 2px;
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: 12.5px;
}

.fx-nav-crumb {
  display: inline-block;
  padding: 4px 8px;
  border-radius: var(--fx-radius-sm);
  color: var(--fx-muted);
  text-decoration: none;
  white-space: nowrap;
}

.fx-nav-crumb[aria-current] { color: var(--fx-fg); font-weight: 600; }

.fx-nav-crumb-sep {
  flex: none;
  color: color-mix(in srgb, var(--fx-muted) 60%, transparent);
  user-select: none;
}

@media (hover: hover) and (pointer: fine) {
  .fx-nav-crumb:hover { color: var(--fx-fg); background: var(--fx-surface-2); }
}

/* ================================================ nav-back-to-top -------- */

.fx-nav-totop {
  display: inline-grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border: 1px solid var(--fx-border);
  border-radius: 999px;
  background: var(--fx-surface-2);
  color: var(--fx-fg);
  font: inherit;
  cursor: pointer;
  box-shadow: var(--fx-shadow);
  opacity: 0;
  transform: translate3d(0, 10px, 0) scale(0.84);
  pointer-events: none;
  will-change: transform, opacity;
  transition:
    opacity var(--fx-dur-2) var(--fx-ease),
    transform var(--fx-dur-2) var(--fx-ease);
}

.fx-nav-totop.is-visible {
  opacity: 1;
  transform: translate3d(0, 0, 0) scale(1);
  pointer-events: auto;
  transition:
    opacity var(--fx-dur-2) var(--fx-ease-out),
    transform 0.46s var(--fx-ease-spring);
}

.fx-nav-totop svg { width: 16px; height: 16px; display: block; }

/* a zero-height sticky rail: keeps the button pinned inside a scroll
   container, where position:fixed would escape to the viewport */
.fx-nav-totop-slot {
  position: sticky;
  bottom: 14px;
  z-index: 7;
  display: flex;
  align-items: flex-end;
  justify-content: flex-end;
  height: 0;
  margin-right: 16px;
}

/* ============================================= nav-progress-crumb -------- */

.fx-nav-progress {
  position: absolute;
  inset: auto 0 0 0;
  height: 2px;
  overflow: hidden;
  background: color-mix(in srgb, var(--fx-border) 55%, transparent);
}

.fx-nav-progress-fill {
  display: block;
  height: 100%;
  width: 100%;
  transform: scaleX(0);
  transform-origin: left center;
  background: linear-gradient(90deg, var(--fx-accent), var(--fx-accent-2));
  will-change: transform;
}

.fx-nav-progress.is-smooth .fx-nav-progress-fill { will-change: transform; }

/* =========================================== nav-sidebar-collapse -------- */

.fx-nav-shell {
  flex: 1 1 auto;
  display: flex;
  align-items: stretch;
  gap: 0;
}

.fx-nav-side {
  position: relative;
  flex: none;
  width: 216px;
  overflow: hidden;
  padding: 12px 10px;
  border-right: 1px solid var(--fx-border);
  background: var(--fx-surface);
  will-change: width;
}

/* Everything in the head is out of flow, so narrowing the rail cannot
   reposition the toggle or re-wrap the title. */
.fx-nav-side-head {
  position: relative;
  height: 30px;
  margin-bottom: 12px;
}

.fx-nav-side-toggle {
  position: absolute;
  top: 1px;
  left: 6px;
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  border: 1px solid var(--fx-border);
  border-radius: var(--fx-radius-sm);
  background: var(--fx-surface-2);
  color: var(--fx-fg);
  cursor: pointer;
  font: inherit;
}

.fx-nav-side-toggle:focus-visible { outline: 2px solid var(--fx-accent); outline-offset: 2px; }

.fx-nav-side-toggle svg {
  width: 14px;
  height: 14px;
  transition: transform var(--fx-dur-2) var(--fx-ease);
}

.fx-nav-side.is-collapsed .fx-nav-side-toggle svg { transform: rotate(180deg); }

.fx-nav-side-list {
  display: grid;
  gap: 3px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.fx-nav-side-link {
  position: relative;
  display: block;
  height: 34px;
  border-radius: var(--fx-radius-sm);
  color: var(--fx-muted);
  text-decoration: none;
  transition: background-color var(--fx-dur-1) var(--fx-ease), color var(--fx-dur-1) var(--fx-ease);
}

.fx-nav-side-link[aria-current] { background: var(--fx-surface-2); color: var(--fx-fg); }

.fx-nav-side-link:focus-visible { outline: 2px solid var(--fx-accent); outline-offset: 1px; }

@media (hover: hover) and (pointer: fine) {
  .fx-nav-side-link:hover { color: var(--fx-fg); background: var(--fx-surface-2); }
}

/* the icon is the anchor point: it never moves, so nothing jumps */
.fx-nav-side-icon {
  position: absolute;
  top: 50%;
  left: 10px;
  width: 20px;
  height: 20px;
  margin-top: -10px;
  display: grid;
  place-items: center;
  border-radius: 6px;
  background: color-mix(in srgb, var(--fx-accent) 16%, transparent);
  color: var(--fx-fg);
  font-size: 11px;
  font-weight: 700;
}

/* absolutely positioned + nowrap => the label can never re-wrap mid-collapse */
.fx-nav-side-label {
  position: absolute;
  top: 50%;
  left: 40px;
  transform: translateY(-50%);
  white-space: nowrap;
  font-size: 12.5px;
  font-weight: 560;
  transition:
    opacity 0.16s var(--fx-ease) 0.09s,
    transform 0.16s var(--fx-ease) 0.09s;
}

.fx-nav-side.is-collapsed .fx-nav-side-label {
  opacity: 0;
  transform: translateY(-50%) translateX(-6px);
  transition-delay: 0s;
  transition-duration: 0.12s;
}

.fx-nav-side-title {
  position: absolute;
  top: 50%;
  left: 42px;
  transform: translateY(-50%);
  font-weight: 640;
  font-size: 13px;
  letter-spacing: -0.01em;
  white-space: nowrap;
  transition: opacity 0.16s var(--fx-ease) 0.09s;
}

.fx-nav-side.is-collapsed .fx-nav-side-title { opacity: 0; transition-delay: 0s; transition-duration: 0.12s; }

.fx-nav-main {
  flex: 1 1 auto;
  min-width: 0;
  padding: 16px;
  display: grid;
  gap: 14px;
  align-content: start;
}

/* ========================================================= reduced motion */

@media (prefers-reduced-motion: reduce) {
  .fx-nav-shrink,
  .fx-nav-shrink .fx-nav-brand,
  .fx-nav-blur,
  .fx-nav-blur::after,
  .fx-nav-mega-trigger::after,
  .fx-nav-side-toggle svg,
  .fx-nav-dock.is-static .fx-nav-dock-item { transition-duration: 0.12s; }

  .fx-nav-side-label,
  .fx-nav-side-title,
  .fx-nav-side.is-collapsed .fx-nav-side-label,
  .fx-nav-side.is-collapsed .fx-nav-side-title { transition: opacity 0.12s linear; transition-delay: 0s; }

  .fx-nav-side.is-collapsed .fx-nav-side-label { transform: translateY(-50%); }

  .fx-nav-hide { transition: none; }
  .fx-nav-hide.is-hidden { transform: none; }

  .fx-nav-totop,
  .fx-nav-totop.is-visible { transition: opacity 0.12s linear; transform: none; }

  .fx-nav-dock.is-static .fx-nav-dock-item:hover { transform: none; }
}

:root[data-fx-reduced] .fx-nav-shrink,
:root[data-fx-reduced] .fx-nav-shrink .fx-nav-brand,
:root[data-fx-reduced] .fx-nav-blur,
:root[data-fx-reduced] .fx-nav-blur::after,
:root[data-fx-reduced] .fx-nav-mega-trigger::after,
:root[data-fx-reduced] .fx-nav-side-toggle svg,
:root[data-fx-reduced] .fx-nav-dock.is-static .fx-nav-dock-item { transition-duration: 0.12s; }

:root[data-fx-reduced] .fx-nav-side-label,
:root[data-fx-reduced] .fx-nav-side-title,
:root[data-fx-reduced] .fx-nav-side.is-collapsed .fx-nav-side-label,
:root[data-fx-reduced] .fx-nav-side.is-collapsed .fx-nav-side-title { transition: opacity 0.12s linear; transition-delay: 0s; }

:root[data-fx-reduced] .fx-nav-side.is-collapsed .fx-nav-side-label { transform: translateY(-50%); }

:root[data-fx-reduced] .fx-nav-hide { transition: none; }
:root[data-fx-reduced] .fx-nav-hide.is-hidden { transform: none; }

:root[data-fx-reduced] .fx-nav-totop,
:root[data-fx-reduced] .fx-nav-totop.is-visible { transition: opacity 0.12s linear; transform: none; }

:root[data-fx-reduced] .fx-nav-dock.is-static .fx-nav-dock-item:hover { transform: none; }

/* ================================================ nav-glass-hero -------- */
/*
 * A split photo hero that collapses into the glass nav. The JS sets the
 * geometry custom properties (--fx-ghero-*) from the hero width; the values
 * below are the no-JS fallbacks, so the markup still reads as a hero.
 * Everything in the chrome is "lit from below": gradients run from the brand
 * hue at the top to near-white at the bottom, hairlines are brightest on the
 * bottom edge, and there are no drop shadows.
 */

.fx-nav-ghero {
  --fx-ghero-pill: clamp(28px, 2.4cqw, 36px);
  --fx-ghero-lit: linear-gradient(180deg,
    color-mix(in oklab, var(--fx-accent) 92%, var(--fx-fg)) 0%,
    color-mix(in oklab, var(--fx-accent) 46%, var(--fx-fg)) 58%,
    color-mix(in oklab, var(--fx-accent) 10%, var(--fx-fg)) 100%);
  --fx-ghero-hair: color-mix(in srgb, var(--fx-fg) 10%, transparent);
  --fx-ghero-hair-lit: color-mix(in srgb, var(--fx-fg) 30%, transparent);
  container-type: inline-size;
  position: relative;
  display: block;
  width: 100%;
  overflow-x: clip;
  color: var(--fx-fg);
  background: var(--fx-bg);
  font-size: 14px;
  line-height: 1.45;
}

/* ---- the bar: sticky, glass, out of flow ------------------------------- */

.fx-nav-ghero-bar {
  position: sticky;
  top: 0;
  z-index: 10;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: clamp(16px, 2.2cqw, 36px);
  height: var(--fx-ghero-nav, 64px);
  margin-bottom: calc(-1 * var(--fx-ghero-nav, 64px));
  padding-inline: var(--fx-ghero-margin, 9.3cqw);
}

/* the glass: a real backdrop blur, lighter than the frost so the copy that
   scrolls under it stays visibly there */
.fx-nav-ghero-bar::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: color-mix(in srgb, var(--fx-bg) 44%, transparent);
  -webkit-backdrop-filter: blur(var(--fx-ghero-glass-blur, 16px)) saturate(1.3);
          backdrop-filter: blur(var(--fx-ghero-glass-blur, 16px)) saturate(1.3);
  opacity: var(--fx-ghero-glass, 0);
}

/* the finished bar's bottom edge — the same 1px, 70% line as the divider,
   centred on the same pixel row so the hand-over is invisible */
.fx-nav-ghero-bar::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -0.5px;
  height: 1px;
  pointer-events: none;
  background: color-mix(in srgb, var(--fx-fg) 70%, transparent);
  opacity: var(--fx-ghero-edge, 0);
}

.fx-nav-ghero-brand {
  justify-self: start;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
  /* --fx-ghero-name: the JS fades the name out when the bar is too narrow
     for name + row; the mark stays, the accessible name stays */
  color: color-mix(in srgb, var(--fx-fg) calc(var(--fx-ghero-name, 1) * 100%), transparent);
  font-size: clamp(14px, 1.1cqw, 17px);
  font-weight: 600;
  letter-spacing: -0.01em;
  text-decoration: none;
  white-space: nowrap;
  border-radius: 8px;
}

/* an arch, lit from below */
.fx-nav-ghero-mark {
  flex: none;
  width: 18px;
  height: 24px;
  border-radius: 999px 999px 3px 3px;
  background: var(--fx-ghero-lit);
  box-shadow: inset 0 -1px 0 color-mix(in srgb, var(--fx-fg) 70%, transparent);
}

.fx-nav-ghero-nav { justify-self: start; min-width: 0; }

.fx-nav-ghero-links {
  display: flex;
  align-items: center;
  gap: clamp(6px, 0.7cqw, 10px);
  margin: 0;
  padding: 0;
  list-style: none;
}

.fx-nav-ghero-pill {
  position: relative;
  display: inline-flex;
  align-items: center;
  height: var(--fx-ghero-pill);
  padding: 0 clamp(12px, 1.3cqw, 19px);
  border-radius: 999px;
  color: color-mix(in srgb, var(--fx-fg) 88%, transparent);
  background-color: color-mix(in srgb, var(--fx-fg) 4%, transparent);
  box-shadow:
    inset 0 0 0 1px var(--fx-ghero-hair),
    inset 0 -1px 0 var(--fx-ghero-hair-lit);
  /* invisible over the frost and the bar; in flight it frosts what it crosses */
  -webkit-backdrop-filter: blur(10px);
          backdrop-filter: blur(10px);
  font-size: clamp(12px, 0.95cqw, 14px);
  font-weight: 500;
  letter-spacing: 0.005em;
  text-decoration: none;
  white-space: nowrap;
  will-change: transform;
  transition: background-color var(--fx-dur-1) var(--fx-ease), color var(--fx-dur-1) var(--fx-ease);
}

/* in flight the inactive chips pick up a glass body, so sharp copy they
   cross never shows through a bare hairline */
.fx-nav-ghero-pill.is-flying:not([aria-current]) {
  background-color: color-mix(in srgb, var(--fx-bg) 42%, transparent);
}

.fx-nav-ghero-pill[aria-current] {
  color: var(--fx-accent-contrast);
  background: var(--fx-ghero-lit);
  box-shadow:
    inset 0 0 0 1px color-mix(in srgb, var(--fx-fg) 16%, transparent),
    inset 0 -1px 0 color-mix(in srgb, var(--fx-fg) 75%, transparent);
  font-weight: 560;
}

/* pill + nested circle: sign-in and the CTA share it */
.fx-nav-ghero-signin,
.fx-nav-ghero-cta {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  border-radius: 999px;
  text-decoration: none;
  white-space: nowrap;
  font-weight: 500;
}

.fx-nav-ghero-signin {
  justify-self: end;
  height: var(--fx-ghero-pill);
  padding: 0 3px 0 clamp(12px, 1.2cqw, 17px);
  color: var(--fx-fg);
  font-size: clamp(12px, 0.95cqw, 14px);
  background-color: color-mix(in srgb, var(--fx-fg) 5%, transparent);
  box-shadow:
    inset 0 0 0 1px var(--fx-ghero-hair),
    inset 0 -1px 0 var(--fx-ghero-hair-lit);
  transition: background-color var(--fx-dur-1) var(--fx-ease);
}

.fx-nav-ghero-knob {
  position: relative;
  flex: none;
  height: calc(100% - 6px);
  aspect-ratio: 1;
  border-radius: 50%;
  background: var(--fx-ghero-lit);
  box-shadow: inset 0 -1px 0 color-mix(in srgb, var(--fx-fg) 70%, transparent);
  color: var(--fx-accent-contrast);
}

.fx-nav-ghero-knob::before {
  content: "";
  position: absolute;
  inset: 0;
  margin: auto;
  width: 44%;
  height: 44%;
  background: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 8h10M9 4l4 4-4 4'/%3E%3C/svg%3E") center / contain no-repeat;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 8h10M9 4l4 4-4 4'/%3E%3C/svg%3E") center / contain no-repeat;
  transition: transform var(--fx-dur-2) var(--fx-ease-out);
}

/* ---- the hero: a pinned photo stage + copy that scrolls ----------------- */

.fx-nav-ghero-hero {
  position: relative;
  height: var(--fx-ghero-track, 62.5cqw);
}

.fx-nav-ghero-stage {
  position: sticky;
  top: 0;
  z-index: 0;
  height: var(--fx-ghero-h, 62.5cqw);
  overflow: clip;
  background: var(--fx-bg);
}

/* layer A (the author's photo) and layer B (its clone) share this rule, so
   box, object-fit and object-position are identical by construction */
.fx-nav-ghero-photo {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  max-width: none;
  object-fit: cover;
}

/* Layer B. Black/white here are compositing constants, not colours: the
   dodge layer divides the blurred photo by the blur's own coverage, which
   removes the dark halo a CSS blur leaves at the frame edges. */
.fx-nav-ghero-frost {
  position: absolute;
  inset: 0;
  overflow: hidden;
  isolation: isolate;
  background: black;
  pointer-events: none;
}

.fx-nav-ghero-twin { filter: blur(var(--fx-ghero-blur, 2.7cqw)); }

.fx-nav-ghero-norm {
  position: absolute;
  inset: 0;
  overflow: hidden;
  background: white;
  mix-blend-mode: color-dodge;
}

.fx-nav-ghero-norm > i {
  position: absolute;
  inset: 0;
  background: black;
  filter: blur(var(--fx-ghero-blur, 2.7cqw));
}

/* the grade over the frost: a dark multiply, heaviest bottom-left, and a
   brand-hue band across the upper third */
.fx-nav-ghero-grade {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.fx-nav-ghero-grade::before,
.fx-nav-ghero-grade::after {
  content: "";
  position: absolute;
  inset: 0;
}

.fx-nav-ghero-grade::before {
  background:
    linear-gradient(to top right, hsl(var(--fx-hue) 40% 6% / 0.82) 0%, hsl(var(--fx-hue) 40% 8% / 0.4) 38%, transparent 70%),
    linear-gradient(180deg, hsl(var(--fx-hue) 30% 10% / 0.35), transparent 30%);
  mix-blend-mode: multiply;
}

.fx-nav-ghero-grade::after {
  background: linear-gradient(180deg,
    transparent 0%,
    color-mix(in oklab, var(--fx-accent) 30%, transparent) 14%,
    color-mix(in oklab, var(--fx-accent) 12%, transparent) 26%,
    transparent 36%);
  mix-blend-mode: screen;
}

.fx-nav-ghero-line {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
}

.fx-nav-ghero-line path {
  fill: none;
  stroke: color-mix(in srgb, var(--fx-fg) 70%, transparent);
  stroke-width: 1;
}

/* the scroll cue: three chevrons, stroke graded grey-white → accent */
.fx-nav-ghero-cue {
  position: absolute;
  top: 0;
  left: 50%;
  z-index: 2;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border-radius: 999px;
  transform: translate3d(-50%, calc(var(--fx-ghero-h, 62.5cqw) * 0.575), 0) translateY(-50%);
}

.fx-nav-ghero-cue::before {
  content: "";
  width: clamp(18px, 2.3cqw, 34px);
  aspect-ratio: 34 / 28;
  background: linear-gradient(180deg, color-mix(in srgb, var(--fx-fg) 82%, transparent) 10%, var(--fx-accent) 100%);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 34 28' fill='none' stroke='%23000' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 3l13 6.5L30 3M4 11l13 6.5L30 11M4 19l13 6.5L30 19'/%3E%3C/svg%3E") center / contain no-repeat;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 34 28' fill='none' stroke='%23000' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 3l13 6.5L30 3M4 11l13 6.5L30 11M4 19l13 6.5L30 19'/%3E%3C/svg%3E") center / contain no-repeat;
}

/* ---- copy: in the hero, NOT pinned — it scrolls under the glass -------- */

.fx-nav-ghero-copy {
  position: absolute;
  inset: 0 0 auto 0;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  height: var(--fx-ghero-h, 62.5cqw);
  padding: var(--fx-ghero-copy-top, 8cqw) var(--fx-ghero-margin, 9.3cqw) 0 44%;
  pointer-events: none;
}

.fx-nav-ghero-copy > * { pointer-events: auto; }

.fx-nav-ghero-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 clamp(8px, 1.3cqw, 20px);
  color: color-mix(in oklab, var(--fx-accent) 38%, var(--fx-fg));
  font-size: clamp(11px, 0.85cqw, 13px);
  font-weight: 500;
  letter-spacing: 0.02em;
}

.fx-nav-ghero-spark {
  flex: none;
  width: 1.15em;
  height: 1.15em;
  background: var(--fx-ghero-lit);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8 0c.5 4.2 3.8 7.5 8 8-4.2.5-7.5 3.8-8 8-.5-4.2-3.8-7.5-8-8 4.2-.5 7.5-3.8 8-8z'/%3E%3C/svg%3E") center / contain no-repeat;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8 0c.5 4.2 3.8 7.5 8 8-4.2.5-7.5 3.8-8 8-.5-4.2-3.8-7.5-8-8 4.2-.5 7.5-3.8 8-8z'/%3E%3C/svg%3E") center / contain no-repeat;
}

.fx-nav-ghero-title {
  margin: 0;
  max-width: 17ch;
  color: var(--fx-fg);
  font-size: clamp(21px, 3.9cqw, 72px);
  font-weight: 300;
  line-height: 1.07;
  letter-spacing: -0.028em;
  text-wrap: balance;
}

/* emphasis by colour only */
.fx-nav-ghero-title em { font-style: normal; color: color-mix(in oklab, var(--fx-accent) 48%, var(--fx-fg)); }
.fx-nav-ghero-title em + em,
.fx-nav-ghero-title em:nth-of-type(2) { color: color-mix(in oklab, var(--fx-muted) 55%, var(--fx-fg)); }

.fx-nav-ghero-lede {
  max-width: 36em;
  margin: clamp(8px, 1.4cqw, 22px) 0 0;
  color: color-mix(in srgb, var(--fx-fg) 80%, transparent);
  font-size: clamp(12px, 1.15cqw, 17px);
  line-height: 1.47;
  text-wrap: pretty;
}

.fx-nav-ghero-cta {
  height: clamp(30px, 2.8cqw, 42px);
  margin-top: clamp(12px, 2cqw, 30px);
  padding: 0 4px 0 clamp(14px, 1.4cqw, 20px);
  gap: 12px;
  color: var(--fx-accent-contrast);
  background: var(--fx-ghero-lit);
  box-shadow:
    inset 0 0 0 1px color-mix(in srgb, var(--fx-fg) 16%, transparent),
    inset 0 -1px 0 color-mix(in srgb, var(--fx-fg) 80%, transparent);
  font-size: clamp(12px, 0.95cqw, 14.5px);
  font-weight: 560;
  transition: transform var(--fx-dur-1) var(--fx-ease-out);
}

.fx-nav-ghero-cta:active { transform: scale(0.98); }

.fx-nav-ghero-cta .fx-nav-ghero-knob {
  height: calc(100% - 8px);
  background: var(--fx-accent-contrast);
  box-shadow: inset 0 -1px 0 color-mix(in srgb, var(--fx-fg) 22%, transparent);
  color: var(--fx-fg);
}

.fx-nav-ghero-brand:focus-visible,
.fx-nav-ghero-pill:focus-visible,
.fx-nav-ghero-signin:focus-visible,
.fx-nav-ghero-cta:focus-visible,
.fx-nav-ghero-cue:focus-visible {
  outline: 2px solid var(--fx-accent);
  outline-offset: 3px;
}

@media (hover: hover) and (pointer: fine) {
  .fx-nav-ghero-pill:not([aria-current]):hover { background-color: color-mix(in srgb, var(--fx-fg) 10%, transparent); color: var(--fx-fg); }
  .fx-nav-ghero-signin:hover { background-color: color-mix(in srgb, var(--fx-fg) 10%, transparent); }
  .fx-nav-ghero-signin:hover .fx-nav-ghero-knob::before,
  .fx-nav-ghero-cta:hover .fx-nav-ghero-knob::before { transform: translateX(2px); }
}

/* ---- compact: below the `compact` width (JS adds .is-compact) ---------- */

.fx-nav-ghero.is-compact { --fx-ghero-pill: 30px; }

.fx-nav-ghero.is-compact .fx-nav-ghero-bar {
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: 8px;
}

.fx-nav-ghero.is-compact .fx-nav-ghero-nav { justify-self: end; }
.fx-nav-ghero.is-compact .fx-nav-ghero-links { gap: 4px; }
.fx-nav-ghero.is-compact .fx-nav-ghero-pill { padding: 0 10px; font-size: 12px; }

/* circle only; the label stays in the accessible name */
.fx-nav-ghero.is-compact .fx-nav-ghero-signin { padding: 0; gap: 0; width: var(--fx-ghero-pill); justify-content: center; }
.fx-nav-ghero.is-compact .fx-nav-ghero-signin .fx-nav-ghero-knob { height: calc(100% - 4px); }
.fx-nav-ghero.is-compact .fx-nav-ghero-label {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.fx-nav-ghero.is-compact .fx-nav-ghero-copy { padding-left: var(--fx-ghero-margin, 16px); }
.fx-nav-ghero.is-compact .fx-nav-ghero-title { max-width: 14ch; font-size: clamp(26px, 7.6cqw, 42px); }
.fx-nav-ghero.is-compact .fx-nav-ghero-lede { font-size: clamp(14px, 3.9cqw, 16px); }
.fx-nav-ghero.is-compact .fx-nav-ghero-cta { height: 40px; font-size: 14px; }

/* ---- reduced motion: rest state, then a plain sticky glass bar --------- */

.fx-nav-ghero.is-static .fx-nav-ghero-bar { position: relative; }
.fx-nav-ghero.is-static.is-docked .fx-nav-ghero-bar {
  position: sticky;
  animation: fx-nav-ghero-in 0.12s linear;
}

@keyframes fx-nav-ghero-in { from { opacity: 0; } }

@media (prefers-reduced-motion: reduce) {
  .fx-nav-ghero-cta:active { transform: none; }
  .fx-nav-ghero-knob::before { transition: none; }
}
:root[data-fx-reduced] .fx-nav-ghero-cta:active { transform: none; }
:root[data-fx-reduced] .fx-nav-ghero-knob::before { transition: none; }

@media (prefers-reduced-transparency: reduce) {
  .fx-nav-ghero-bar::before {
    background: var(--fx-surface);
    -webkit-backdrop-filter: none;
            backdrop-filter: none;
  }
}

/* ---- demo art: a dark hall lit by a tall doorway, built from tokens ---- */
/* Stand-in for a photograph. It needs what a good hero photo needs: dark
   masses, and a clearly bright zone low in the frame that the divider cuts. */

.fx-nav-ghero-art {
  --fx-ghero-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23g)'/%3E%3C/svg%3E");
  overflow: hidden;
  background:
    radial-gradient(46% 30% at 36% 78%, hsl(var(--fx-hue) 60% 40% / 0.45), transparent 70%),
    linear-gradient(180deg,
      hsl(var(--fx-hue) 34% 5%) 0%,
      hsl(var(--fx-hue) 30% 8%) 40%,
      hsl(var(--fx-hue) 26% 13%) 70%,
      hsl(var(--fx-hue) 30% 6%) 100%);
}

.fx-nav-ghero-art > span { position: absolute; display: block; }

/* coffered ceiling and a slit of light */
.fx-nav-ghero-art::before {
  content: "";
  position: absolute;
  inset: 0 0 76% 0;
  background:
    linear-gradient(180deg, transparent 58%, hsl(var(--fx-hue) 60% 72% / 0.55) 60%, transparent 63%),
    repeating-linear-gradient(90deg, hsl(var(--fx-hue) 30% 26% / 0.55) 0 1px, transparent 1px 5.5%);
  -webkit-mask: linear-gradient(180deg, #000, transparent);
          mask: linear-gradient(180deg, #000, transparent);
}

.fx-nav-ghero-art::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image: var(--fx-ghero-grain);
  background-size: 160px 160px;
  opacity: 0.12;
  mix-blend-mode: overlay;
}

/* back wall: vertical fins */
.fx-nav-ghero-art-fins {
  inset: 14% 6% 27% 6%;
  background:
    linear-gradient(180deg, transparent 96%, hsl(var(--fx-hue) 40% 30% / 0.6) 99%, transparent),
    repeating-linear-gradient(90deg, hsl(var(--fx-hue) 24% 15%) 0 0.4%, hsl(var(--fx-hue) 30% 8%) 0.4% 2.6%);
  -webkit-mask: linear-gradient(180deg, transparent, #000 30%, #000);
          mask: linear-gradient(180deg, transparent, #000 30%, #000);
  opacity: 0.7;
}

/* the doorway: dim at the lintel, near-white at the threshold */
.fx-nav-ghero-art-arch {
  left: 28%;
  width: 14.5%;
  top: 21%;
  bottom: 27%;
  border-radius: 50% 50% 0 0 / 22% 22% 0 0;
  background:
    radial-gradient(70% 40% at 50% 100%, hsl(var(--fx-hue) 30% 99%), transparent 80%),
    linear-gradient(180deg,
      hsl(var(--fx-hue) 60% 20%) 0%,
      hsl(var(--fx-hue) 74% 42%) 34%,
      hsl(var(--fx-hue) 76% 68%) 64%,
      hsl(var(--fx-hue) 50% 90%) 88%,
      hsl(var(--fx-hue) 30% 98%) 100%);
  box-shadow: 0 0 6cqw 0.8cqw hsl(var(--fx-hue) 90% 62% / 0.28);
}

.fx-nav-ghero-art-arch.is-far {
  left: 71%;
  width: 7%;
  top: 40%;
  background: linear-gradient(180deg, hsl(var(--fx-hue) 40% 12%), hsl(var(--fx-hue) 56% 34%) 78%, hsl(var(--fx-hue) 50% 52%));
  box-shadow: none;
  opacity: 0.8;
}

/* floor: polished, carrying the doorway's reflection */
.fx-nav-ghero-art-floor {
  inset: 73% 0 0 0;
  background:
    radial-gradient(9% 80% at 35.25% 0%, hsl(var(--fx-hue) 50% 94% / 0.9), transparent 72%),
    radial-gradient(34% 70% at 35.25% 0%, hsl(var(--fx-hue) 75% 62% / 0.42), transparent 72%),
    repeating-linear-gradient(180deg, transparent 0 22%, hsl(var(--fx-hue) 30% 45% / 0.07) 22% 23%),
    linear-gradient(180deg, hsl(var(--fx-hue) 26% 16%), hsl(var(--fx-hue) 32% 4%));
}

/* the light the doorway throws across the floor */
.fx-nav-ghero-art-beam {
  inset: 73% 0 0 0;
  background: linear-gradient(180deg, hsl(var(--fx-hue) 60% 88% / 0.55), hsl(var(--fx-hue) 70% 60% / 0.12) 70%, transparent);
  clip-path: polygon(28% 0, 42.5% 0, 62% 100%, 14% 100%);
  mix-blend-mode: screen;
}

/* a figure for scale, dark against the light */
.fx-nav-ghero-art-figure {
  left: 38.2%;
  width: 1.3%;
  bottom: 26%;
  height: 10.5%;
  border-radius: 40% 40% 12% 12%;
  background: hsl(var(--fx-hue) 40% 4%);
}

.fx-nav-ghero-art-figure::before {
  content: "";
  position: absolute;
  left: 50%;
  bottom: 100%;
  width: 85%;
  aspect-ratio: 1;
  margin-bottom: 4%;
  border-radius: 50%;
  background: inherit;
  transform: translateX(-50%);
}

/* foreground pillars, lit on their inner faces */
.fx-nav-ghero-art-pillar {
  inset: 0 auto 0 0;
  width: 6.5%;
  background: linear-gradient(90deg, hsl(var(--fx-hue) 30% 3%), hsl(var(--fx-hue) 26% 7%) 82%, hsl(var(--fx-hue) 50% 34% / 0.8) 100%);
}

.fx-nav-ghero-art-pillar.is-right {
  inset: 0 0 0 auto;
  background: linear-gradient(270deg, hsl(var(--fx-hue) 30% 3%), hsl(var(--fx-hue) 26% 7%) 82%, hsl(var(--fx-hue) 50% 28% / 0.7) 100%);
}

/* ---- demo page below the hero ------------------------------------------ */

.fx-nav-ghero-page {
  display: grid;
  gap: 34px;
  padding: 34px var(--fx-ghero-margin, 24px) 60px;
  background: var(--fx-bg);
}

.fx-nav-ghero-sec { scroll-margin-top: 12px; }

.fx-nav-ghero-sec > h2 {
  margin: 0 0 8px;
  font-size: clamp(18px, 2.2cqw, 30px);
  font-weight: 400;
  letter-spacing: -0.02em;
  line-height: 1.15;
}

.fx-nav-ghero-sec > p {
  max-width: 44em;
  margin: 0 0 10px;
  color: color-mix(in srgb, var(--fx-fg) 72%, transparent);
  font-size: 13px;
  line-height: 1.55;
}

.fx-nav-ghero-kicker {
  margin: 0 0 6px;
  color: color-mix(in oklab, var(--fx-accent) 50%, var(--fx-fg));
  font-size: 11px;
  letter-spacing: 0.04em;
}

.fx-nav-ghero-tiles {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 10px;
  margin-top: 14px;
}

.fx-nav-ghero-tiles > span {
  display: flex;
  align-items: flex-end;
  min-height: 92px;
  padding: 10px 12px;
  border-radius: var(--fx-radius);
  color: var(--fx-accent-contrast);
  font-size: 12px;
  font-weight: 600;
  background: linear-gradient(160deg, var(--fx-accent), color-mix(in oklab, var(--fx-accent) 20%, var(--fx-fg)));
}

.fx-nav-ghero-tiles > span:nth-child(2) { background: linear-gradient(160deg, var(--fx-accent-2), color-mix(in oklab, var(--fx-accent-2) 20%, var(--fx-fg))); }
.fx-nav-ghero-tiles > span:nth-child(3) { background: linear-gradient(160deg, var(--fx-accent-3), color-mix(in oklab, var(--fx-accent-3) 20%, var(--fx-fg))); }

/* ══ src/effects/reveal.css ══ */
/* =========================================================================
 * effects-lab — category: reveal ("Scroll reveal")
 *
 * The motion itself lives in src/effects/reveal.js (Motion drives transform /
 * opacity / filter / clip-path). This file holds:
 *   1. the structural bits an effect needs (masks, stroke setup, digit track)
 *   2. the shared demo furniture the gallery previews use
 *   3. the reduced-motion safety net, in both required forms
 *
 * Everything here is namespaced `fx-reveal-`. Colours are tokens only.
 * ========================================================================= */

/* ────────────────────────────────────────────── 1. effect structure ───── */

/* reveal-mask-slide: one block per split line, so two short lines can never
   collapse onto the same row once they become inline-blocks. */
.fx-reveal-mask-lines .fx-line-wrap { display: block; }

/* reveal-line-draw: strokes animate via stroke-dashoffset, so give the SVG a
   sane default and keep the geometry crisp while it draws. */
[data-fx*="reveal-line-draw"] svg { display: block; width: 100%; height: auto; overflow: visible; }
[data-fx*="reveal-line-draw"] svg [stroke] { vector-effect: non-scaling-stroke; }

/* reveal-line-draw fallback: an underline rule drawn beneath the element. */
.fx-reveal-line-host { position: relative; }
.fx-reveal-rule {
  position: absolute;
  left: 0;
  right: 0;
  bottom: -0.28em;
  height: 2px;
  border-radius: 2px;
  background: currentColor;
  transform-origin: 0 50%;
  pointer-events: none;
}

/* reveal-counter: digits must not jiggle while they tick. */
.fx-reveal-counter {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

/* ────────────────────────────────────────── 2. demo / preview furniture ── */

.fx-reveal-demo {
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  width: 100%;
  max-width: 24rem;
  text-align: left;
  color: var(--fx-fg);
}
.fx-reveal-demo.is-wide { max-width: 26rem; }

.fx-reveal-eyebrow {
  margin: 0;
  font-size: 0.66rem;
  font-weight: 650;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--fx-accent);
}

.fx-reveal-title {
  margin: 0;
  font-size: 1.5rem;
  line-height: 1.16;
  font-weight: 650;
  letter-spacing: -0.022em;
  color: var(--fx-fg);
}
.fx-reveal-title.is-big { font-size: 1.85rem; }

.fx-reveal-body {
  margin: 0;
  font-size: 0.875rem;
  line-height: 1.55;
  color: var(--fx-muted);
}

.fx-reveal-card {
  background: var(--fx-surface-2);
  border: 1px solid var(--fx-border);
  border-radius: var(--fx-radius);
  box-shadow: var(--fx-shadow);
  padding: 1.05rem 1.15rem;
  display: flex;
  flex-direction: column;
  gap: 0.45rem;
  color: var(--fx-fg);
}

.fx-reveal-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.5rem;
  width: 100%;
  max-width: 24rem;
}

.fx-reveal-row {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  padding: 0.62rem 0.8rem;
  font-size: 0.85rem;
  color: var(--fx-fg);
  background: var(--fx-surface-2);
  border: 1px solid var(--fx-border);
  border-radius: var(--fx-radius-sm);
}
.fx-reveal-row span:last-child { margin-left: auto; color: var(--fx-muted); font-size: 0.75rem; }

.fx-reveal-dot {
  flex: none;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--fx-accent);
}

.fx-reveal-grid {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 0.4rem;
  width: 100%;
  max-width: 22rem;
}

.fx-reveal-tile {
  aspect-ratio: 1 / 1;
  border-radius: var(--fx-radius-sm);
  background: var(--fx-surface-2);
  border: 1px solid var(--fx-border);
  display: grid;
  place-items: center;
  font-size: 0.66rem;
  color: var(--fx-muted);
}
.fx-reveal-tile.is-accent { background: var(--fx-accent); border-color: transparent; color: var(--fx-accent-contrast); }

.fx-reveal-stats {
  display: flex;
  gap: 1.9rem;
  flex-wrap: wrap;
}
.fx-reveal-stat { display: flex; flex-direction: column; gap: 0.15rem; }
.fx-reveal-stat b {
  font-size: 1.9rem;
  font-weight: 650;
  letter-spacing: -0.03em;
  line-height: 1;
  color: var(--fx-fg);
  font-variant-numeric: tabular-nums;
}
.fx-reveal-stat small {
  font-size: 0.66rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--fx-muted);
}

.fx-reveal-media {
  width: 100%;
  max-width: 24rem;
  aspect-ratio: 16 / 9;
  border-radius: var(--fx-radius);
  border: 1px solid var(--fx-border);
  background:
    radial-gradient(120% 110% at 12% 0%, var(--fx-accent) 0%, transparent 58%),
    radial-gradient(120% 120% at 100% 100%, var(--fx-accent-2) 0%, transparent 62%),
    var(--fx-surface-2);
  display: grid;
  place-items: center;
  overflow: hidden;
}
.fx-reveal-media span {
  font-size: 0.7rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  font-weight: 650;
  color: var(--fx-fg);
  mix-blend-mode: normal;
}

.fx-reveal-chart {
  width: 100%;
  max-width: 24rem;
  color: var(--fx-accent);
}
.fx-reveal-chart .fx-reveal-axis { color: var(--fx-border); }

/* ───────────────────────────────────────────── 3. reduced motion ──────── */
/* Both conditions, always. Nothing may stay hidden, clipped or half-drawn.  */

@media (prefers-reduced-motion: reduce) {
  [data-fx^="reveal-"],
  [data-fx^="reveal-"] > *,
  [data-fx*=" reveal-"],
  [data-fx*=" reveal-"] > * {
    animation: none !important;
    transform: none !important;
    filter: none !important;
    clip-path: none !important;
    opacity: 1 !important;
  }
  .fx-reveal-rule { display: none; }
  [data-fx^="reveal-"] svg [stroke] {
    stroke-dasharray: none !important;
    stroke-dashoffset: 0 !important;
  }
}

:root[data-fx-reduced] [data-fx^="reveal-"],
:root[data-fx-reduced] [data-fx^="reveal-"] > *,
:root[data-fx-reduced] [data-fx*=" reveal-"],
:root[data-fx-reduced] [data-fx*=" reveal-"] > * {
  animation: none !important;
  transform: none !important;
  filter: none !important;
  clip-path: none !important;
  opacity: 1 !important;
}
:root[data-fx-reduced] .fx-reveal-rule { display: none; }
:root[data-fx-reduced] [data-fx^="reveal-"] svg [stroke] {
  stroke-dasharray: none !important;
  stroke-dashoffset: 0 !important;
}

/* line-draw demo: a secondary series in the muted tone. */
.fx-reveal-chart .fx-reveal-ghost { color: var(--fx-muted); }

/* ══ src/effects/scroll.css ══ */
/* =========================================================================
 * effects-lab — category: scroll  ("Scroll motion")
 * Structure + neutral states for the scroll-linked effects.
 * Every class is .fx-scroll-…  •  every keyframe is fx-scroll-…
 *
 * Length knobs (override inline or in your own CSS):
 *   --fx-scroll-run    how far the page scrolls through a pinned section
 *   --fx-scroll-stage  height of the pinned panel / horizontal viewport
 *   --fx-scroll-top    sticky offset for stacked cards
 * ========================================================================= */

/* ------------------------------------------------------------- shared ---- */
/* Wrapper used by the gallery previews: gives the injected demo its own
   padding so it reads correctly inside a bare scroll stage. */
.fx-scroll-demo {
  display: block;
  padding: 16px;
  box-sizing: border-box;
}

.fx-scroll-frame {
  position: relative;
  overflow: hidden;
  border-radius: var(--fx-radius);
  background: var(--fx-surface);
  box-shadow: var(--fx-ring);
}

.fx-scroll-spacer {
  display: block;
  min-height: var(--fx-scroll-spacer-h, 220px);
}

.fx-scroll-note {
  margin: 0;
  color: var(--fx-muted);
  font-size: 0.8125rem;
  line-height: 1.5;
  letter-spacing: 0.01em;
}

.fx-scroll-card {
  padding: 18px 20px;
  border-radius: var(--fx-radius);
  background: var(--fx-surface);
  box-shadow: var(--fx-ring);
  color: var(--fx-fg);
}

.fx-scroll-card h3,
.fx-scroll-card h4 { margin: 0 0 6px; font-size: 1rem; line-height: 1.25; }
.fx-scroll-card p { margin: 0; color: var(--fx-muted); font-size: 0.875rem; line-height: 1.5; }

/* ----------------------------------------------------------- parallax ---- */
.fx-scroll-parallax {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  border-radius: var(--fx-radius);
  background: var(--fx-surface);
  box-shadow: var(--fx-ring);
  min-height: 180px;
}

/* Placeholder backdrop. Replace the `background` with your own image in real
   use — but keep the negative inset: the layer has to bleed past the frame or
   the drift will expose an edge. The grid is deliberate: a parallax over a
   featureless gradient moves just as much but reads as completely static. */
.fx-scroll-parallax-layer {
  position: absolute;
  inset: -25% 0;
  z-index: 0;
  will-change: transform;
  backface-visibility: hidden;
  background:
    repeating-linear-gradient(0deg, color-mix(in srgb, var(--fx-fg) 9%, transparent) 0 1px, transparent 1px 34px),
    repeating-linear-gradient(90deg, color-mix(in srgb, var(--fx-fg) 9%, transparent) 0 1px, transparent 1px 34px),
    radial-gradient(120% 80% at 18% 12%, var(--fx-accent) 0%, transparent 58%),
    radial-gradient(90% 70% at 88% 82%, var(--fx-accent-3) 0%, transparent 62%),
    linear-gradient(160deg, var(--fx-surface-2) 0%, var(--fx-surface) 100%);
}

.fx-scroll-parallax-content {
  position: relative;
  z-index: 1;
  padding: 22px 24px;
  color: var(--fx-fg);
}
.fx-scroll-parallax-content h3 { margin: 0 0 4px; font-size: 1.0625rem; }
.fx-scroll-parallax-content p { margin: 0; font-size: 0.8125rem; color: var(--fx-fg); opacity: 0.78; }

/* ------------------------------------------------------ parallax-layers -- */
.fx-scroll-layers {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  min-height: 220px;
  border-radius: var(--fx-radius);
  background: linear-gradient(180deg, var(--fx-surface-2) 0%, var(--fx-surface) 100%);
  box-shadow: var(--fx-ring);
}

.fx-scroll-layer {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  will-change: transform;
  backface-visibility: hidden;
  pointer-events: none;
}

.fx-scroll-layer > * { pointer-events: auto; }

.fx-scroll-blob {
  width: var(--fx-scroll-blob-size, 120px);
  aspect-ratio: 1;
  border-radius: 50%;
  background: radial-gradient(circle at 34% 30%, var(--fx-accent-2), var(--fx-accent) 62%, transparent 72%);
  opacity: 0.55;
  filter: blur(2px);
}

.fx-scroll-layer-title {
  margin: 0;
  color: var(--fx-fg);
  font-size: 1.375rem;
  letter-spacing: -0.015em;
  text-align: center;
}

/* ------------------------------------------------------- progress bar ---- */
.fx-scroll-bar {
  position: sticky;
  top: 0;
  z-index: 5;
  height: var(--fx-scroll-bar-h, 4px);
  border-radius: 999px;
  background: var(--fx-border);
  overflow: hidden;
}

.fx-scroll-bar-fill {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: inherit;
  transform: scaleX(0);
  transform-origin: 0 50%;
  will-change: transform;
  background: linear-gradient(90deg, var(--fx-accent) 0%, var(--fx-accent-2) 100%);
}

.fx-scroll-bar[data-fx-axis='x'] .fx-scroll-bar-fill,
.fx-scroll-bar.is-vertical .fx-scroll-bar-fill {
  transform: scaleY(0);
  transform-origin: 50% 0;
}

/* ------------------------------------------------------ progress ring ---- */
.fx-scroll-ring {
  position: sticky;
  top: 12px;
  z-index: 5;
  display: grid;
  place-items: center;
  width: var(--fx-scroll-ring-size, 84px);
  height: var(--fx-scroll-ring-size, 84px);
  margin-inline-start: auto;
  color: var(--fx-fg);
}

.fx-scroll-ring svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  transform: rotate(-90deg);
  overflow: visible;
}

.fx-scroll-ring-track {
  fill: none;
  stroke: var(--fx-border);
  stroke-width: 7;
}

.fx-scroll-ring-arc {
  fill: none;
  stroke: var(--fx-accent);
  stroke-width: 7;
  stroke-linecap: round;
}

.fx-scroll-ring-label {
  position: relative;
  font-size: 0.8125rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}

/* ------------------------------------------------------- sticky stack ---- */
.fx-scroll-stack {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--fx-scroll-stack-gap, 90px);
}

.fx-scroll-stack-card {
  position: sticky;
  top: var(--fx-scroll-top, 16px);
  transform-origin: 50% 0;
  will-change: transform, opacity;
  padding: 20px 22px;
  min-height: var(--fx-scroll-stack-card-h, 120px);
  border-radius: var(--fx-radius-lg);
  background: var(--fx-surface);
  box-shadow: var(--fx-ring), var(--fx-shadow);
  color: var(--fx-fg);
}

.fx-scroll-stack-card h3 { margin: 0 0 6px; font-size: 1.0625rem; }
.fx-scroll-stack-card p { margin: 0; color: var(--fx-muted); font-size: 0.875rem; line-height: 1.5; }

.fx-scroll-stack.fx-scroll-stack-static { gap: 16px; }
.fx-scroll-stack.fx-scroll-stack-static .fx-scroll-stack-card {
  position: static;
  transform: none;
  opacity: 1;
  min-height: 0;
}

/* --------------------------------------------------------- pin section --- */
.fx-scroll-pin {
  position: relative;
  min-height: var(--fx-scroll-run, 260vh);
}

.fx-scroll-pin-panel {
  position: sticky;
  top: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 18px;
  height: var(--fx-scroll-stage, 100vh);
  padding: 24px;
  border-radius: var(--fx-radius);
  background: var(--fx-surface);
  box-shadow: var(--fx-ring);
  overflow: hidden;
}

.fx-scroll-pin-track {
  height: 3px;
  border-radius: 999px;
  background: var(--fx-border);
  overflow: hidden;
}

.fx-scroll-pin-fill {
  display: block;
  height: 100%;
  border-radius: inherit;
  background: var(--fx-accent);
  transform: scaleX(0);
  transform-origin: 0 50%;
  will-change: transform;
}

.fx-scroll-pin-steps {
  position: relative;
  flex: 1;
  min-height: var(--fx-scroll-pin-steps-h, 120px);
}

.fx-scroll-pin-step {
  position: absolute;
  inset: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 8px;
  opacity: 0;
  will-change: transform, opacity;
  transition: opacity var(--fx-dur-2) var(--fx-ease-out), transform var(--fx-dur-2) var(--fx-ease-out);
  color: var(--fx-fg);
}

.fx-scroll-pin-step:first-of-type { opacity: 1; }
.fx-scroll-pin-step b { font-size: 1.25rem; letter-spacing: -0.015em; }
.fx-scroll-pin-step span { color: var(--fx-muted); font-size: 0.875rem; line-height: 1.5; }

.fx-scroll-pin.fx-scroll-pin-static { min-height: 0; }
.fx-scroll-pin.fx-scroll-pin-static .fx-scroll-pin-panel {
  position: static;
  height: auto;
}
.fx-scroll-pin.fx-scroll-pin-static .fx-scroll-pin-track { display: none; }
.fx-scroll-pin.fx-scroll-pin-static .fx-scroll-pin-steps {
  display: flex;
  flex-direction: column;
  gap: 16px;
  min-height: 0;
}
.fx-scroll-pin.fx-scroll-pin-static .fx-scroll-pin-step {
  position: static;
  opacity: 1;
  transform: none;
  transition: none;
}

/* ---------------------------------------------------------- horizontal --- */
.fx-scroll-hz {
  position: relative;
  min-height: var(--fx-scroll-run, 300vh);
}

.fx-scroll-hz-viewport {
  position: sticky;
  top: 0;
  display: flex;
  align-items: center;
  height: var(--fx-scroll-stage, 100vh);
  overflow: hidden;
  border-radius: var(--fx-radius);
  background: var(--fx-surface);
  box-shadow: var(--fx-ring);
}

.fx-scroll-hz-track {
  display: flex;
  gap: 16px;
  padding: 0 20px;
  will-change: transform;
  backface-visibility: hidden;
}

.fx-scroll-hz-item {
  flex: 0 0 auto;
  width: var(--fx-scroll-hz-item-w, 200px);
  padding: 18px;
  border-radius: var(--fx-radius);
  background: var(--fx-surface-2);
  box-shadow: var(--fx-ring);
  color: var(--fx-fg);
}
.fx-scroll-hz-item h4 { margin: 0 0 6px; font-size: 0.9375rem; }
.fx-scroll-hz-item p { margin: 0; color: var(--fx-muted); font-size: 0.8125rem; line-height: 1.45; }

.fx-scroll-hz.fx-scroll-hz-static { min-height: 0; }
.fx-scroll-hz.fx-scroll-hz-static .fx-scroll-hz-viewport {
  position: static;
  height: auto;
  padding-block: 16px;
  overflow-x: auto;
  overscroll-behavior-x: contain;
}
.fx-scroll-hz.fx-scroll-hz-static .fx-scroll-hz-track { transform: none; }

/* --------------------------------------------------------------- zoom ---- */
.fx-scroll-zoom {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  min-height: 200px;
  border-radius: var(--fx-radius);
  background: var(--fx-surface);
  box-shadow: var(--fx-ring);
}

/* Placeholder backdrop — swap the `background` for your own image. The grid
   matters: scaling a smooth gradient by 30% looks almost identical to not
   scaling it at all, so the zoom needs edges to read against. */
.fx-scroll-zoom-layer {
  position: absolute;
  inset: 0;
  z-index: 0;
  will-change: transform;
  backface-visibility: hidden;
  background:
    repeating-linear-gradient(0deg, hsl(0 0% 100% / 0.14) 0 1px, transparent 1px 38px),
    repeating-linear-gradient(90deg, hsl(0 0% 100% / 0.14) 0 1px, transparent 1px 38px),
    conic-gradient(from 210deg at 62% 38%, var(--fx-accent-3), var(--fx-accent), var(--fx-accent-2), var(--fx-accent-3)),
    var(--fx-surface-2);
  opacity: 0.85;
}

.fx-scroll-zoom-content {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: flex-end;
  min-height: inherit;
  padding: 20px 22px;
  color: var(--fx-fg);
  text-shadow: 0 1px 12px hsl(0 0% 0% / 0.35);
}
.fx-scroll-zoom-content h3 { margin: 0; font-size: 1.125rem; letter-spacing: -0.015em; }

/* ------------------------------------------------------------- rotate ---- */
.fx-scroll-rotate {
  display: grid;
  place-items: center;
  padding: 24px;
}

.fx-scroll-rotate-target {
  display: grid;
  place-items: center;
  width: var(--fx-scroll-rotate-size, 108px);
  aspect-ratio: 1;
  border-radius: var(--fx-radius-lg);
  background: conic-gradient(from 0deg, var(--fx-accent), var(--fx-accent-2), var(--fx-accent-3), var(--fx-accent));
  color: var(--fx-accent-contrast);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  will-change: transform;
  backface-visibility: hidden;
}

/* ----------------------------------------------------- word highlight ---- */
.fx-scroll-words {
  margin: 0;
  color: var(--fx-fg);
  font-size: 1.125rem;
  line-height: 1.55;
  letter-spacing: -0.005em;
  text-wrap: balance;
}

.fx-scroll-words .fx-word { will-change: opacity; }

/* ------------------------------------------------------ velocity skew ---- */
.fx-scroll-skew {
  transform-origin: 50% 50%;
  will-change: transform;
  backface-visibility: hidden;
}

/* =======================================================================
 * Reduced motion — the effects already branch in JS; this covers the
 * CSS-side transitions and the neutral resting state.
 * ===================================================================== */
@media (prefers-reduced-motion: reduce) {
  .fx-scroll-parallax-layer,
  .fx-scroll-layer,
  .fx-scroll-zoom-layer,
  .fx-scroll-rotate-target,
  .fx-scroll-hz-track,
  .fx-scroll-skew {
    transform: none;
    will-change: auto;
  }
  .fx-scroll-pin-step {
    transition: none;
  }
  .fx-scroll-bar-fill,
  .fx-scroll-pin-fill {
    will-change: auto;
  }
}

:root[data-fx-reduced] .fx-scroll-parallax-layer,
:root[data-fx-reduced] .fx-scroll-layer,
:root[data-fx-reduced] .fx-scroll-zoom-layer,
:root[data-fx-reduced] .fx-scroll-rotate-target,
:root[data-fx-reduced] .fx-scroll-hz-track,
:root[data-fx-reduced] .fx-scroll-skew {
  transform: none;
  will-change: auto;
}
:root[data-fx-reduced] .fx-scroll-pin-step { transition: none; }
:root[data-fx-reduced] .fx-scroll-bar-fill,
:root[data-fx-reduced] .fx-scroll-pin-fill { will-change: auto; }

/* ══ src/effects/svg.css ══ */
/* =========================================================================
 * effects-lab — category: svg ("SVG & paths")
 *
 * Structural CSS only: the motion lives in svg.js. Everything here exists so
 * the inline SVG in the copy-paste snippets has somewhere sensible to sit and
 * picks its colour up from the tokens in src/core/00-tokens.css.
 *
 * Every class starts .fx-svg- and every keyframe fx-svg-.
 * ========================================================================= */

/* ------------------------------------------------------------- containers */

.fx-svg-stage {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 18px;
  width: 100%;
  min-height: 100%;
  padding: 20px;
  box-sizing: border-box;
  color: var(--fx-fg);
}

.fx-svg-figure {
  display: block;
  width: 100%;
  max-width: 420px;
  margin-inline: auto;
  color: var(--fx-accent);
}

.fx-svg-figure > svg {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}

/* Token-driven paint. Presentation attributes cannot hold var(), so the
   secondary strokes/fills are set from here instead. */
.fx-svg-axis { stroke: var(--fx-border); }
.fx-svg-soft { stroke: var(--fx-muted); }
.fx-svg-bar  { fill: var(--fx-muted); }
.fx-svg-bar--accent { fill: currentColor; }

.fx-svg-caption {
  margin: 0;
  font-size: 12px;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--fx-muted);
}

.fx-svg-copy {
  margin: 0;
  max-width: 24ch;
  font-size: clamp(22px, 5vw, 34px);
  line-height: 1.35;
  font-weight: 600;
  text-align: center;
  color: var(--fx-fg);
  text-wrap: balance;
}

/* --------------------------------------------- inline word decorations --- */

.fx-svg-underline,
.fx-svg-highlight {
  position: relative;
  display: inline-block;
  color: inherit;
}

.fx-svg-underline > svg {
  position: absolute;
  left: -2%;
  bottom: -0.3em;
  width: 104%;
  height: 0.46em;
  overflow: visible;
  color: var(--fx-accent);
  pointer-events: none;
}

.fx-svg-highlight {
  padding-inline: 0.18em;
}

.fx-svg-highlight > svg {
  position: absolute;
  left: -14%;
  top: -34%;
  width: 128%;
  height: 176%;
  overflow: visible;
  color: var(--fx-accent);
  pointer-events: none;
}

/* ---------------------------------------------------------- connector ---- */

.fx-svg-flow {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  width: 100%;
  max-width: 460px;
  min-height: 160px;
  margin-inline: auto;
  color: var(--fx-accent);
}

.fx-svg-connector-layer {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
  z-index: 0;
}

.fx-svg-node {
  position: relative;
  z-index: 1;
  padding: 12px 14px;
  border-radius: var(--fx-radius-sm);
  background: var(--fx-surface);
  box-shadow: var(--fx-ring);
  color: var(--fx-fg);
  font-size: 13px;
  font-weight: 600;
  white-space: nowrap;
}

.fx-svg-node--accent {
  background: var(--fx-surface-2);
  color: var(--fx-accent);
}

/* Offsetting a node is what makes the connector curve, rather than draw a
   straight line between two boxes that happen to share a centre line.
   align-self, not a transform, so the row still reserves the space. */
.fx-svg-node--low  { align-self: flex-end; }
.fx-svg-node--high { align-self: flex-start; }

/* ------------------------------------------------------------- icon btn -- */

.fx-svg-iconbtn {
  display: inline-grid;
  place-items: center;
  width: 52px;
  height: 52px;
  padding: 0;
  border: 0;
  border-radius: var(--fx-radius-sm);
  background: var(--fx-surface);
  box-shadow: var(--fx-ring);
  color: var(--fx-accent);
  cursor: pointer;
  transition: background-color var(--fx-dur-1) var(--fx-ease-out),
              transform var(--fx-dur-1) var(--fx-ease-out);
}

.fx-svg-iconbtn > svg {
  width: 26px;
  height: 26px;
  overflow: visible;
}

@media (hover: hover) and (pointer: fine) {
  .fx-svg-iconbtn:hover { background: var(--fx-surface-2); }
}

.fx-svg-iconbtn:active { transform: scale(0.94); }

.fx-svg-iconbtn:focus-visible {
  outline: 2px solid var(--fx-accent);
  outline-offset: 3px;
}

/* ------------------------------------------------------------ scroll box - */

.fx-svg-scrollbox {
  width: 100%;
  height: 100%;
  min-height: 260px;
  max-height: 100%;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
}

.fx-svg-scrollpad {
  display: grid;
  place-items: center;
  min-height: 190px;
  padding: 16px;
  box-sizing: border-box;
}

/* A short lead-in, so the drawing is already partly on screen before the
   first scroll and the panel never reads as empty. */
.fx-svg-scrollpad--lead { min-height: 110px; }

.fx-svg-scrollhint {
  margin: 0;
  font-size: 12px;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--fx-muted);
  animation: fx-svg-nudge 2.2s var(--fx-ease) infinite;
}

@keyframes fx-svg-nudge {
  0%, 62%, 100% { transform: translateY(0); opacity: 0.75; }
  80%           { transform: translateY(5px); opacity: 1; }
}

/* ------------------------------------------------------------------ logo - */

.fx-svg-logo { max-width: 340px; }

/* ----------------------------------------------------------------- blobs - */

.fx-svg-blobs { max-width: 360px; }

.fx-svg-blobs > svg { overflow: visible; }

/* ---------------------------------------------------- reduced motion ------
 * The JS effects branch on ctx.reduced themselves. This only has to stop the
 * decorative CSS loop above — and it has to answer to both the OS setting and
 * the gallery's manual switch.
 * ------------------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
  .fx-svg-scrollhint { animation: none; transform: none; opacity: 1; }
  .fx-svg-iconbtn { transition: none; }
  .fx-svg-iconbtn:active { transform: none; }
}

:root[data-fx-reduced] .fx-svg-scrollhint { animation: none; transform: none; opacity: 1; }
:root[data-fx-reduced] .fx-svg-iconbtn { transition: none; }
:root[data-fx-reduced] .fx-svg-iconbtn:active { transform: none; }

/* ══ src/effects/text.css ══ */
/* =========================================================================
 * effects-lab — Text
 * Category slug: text  ·  classes: .fx-text-*  ·  keyframes: fx-text-*
 *
 * Every effect here is built on FX.util.splitText() (core) or on plain CSS.
 * Reduced motion is written twice, always:
 *   @media (prefers-reduced-motion: reduce)  +  :root[data-fx-reduced]
 * ========================================================================= */

/* ------------------------------------------------------------------ demo -- */
/* Scaffolding used only by the gallery previews in catalog/text.json.        */
.fx-text-demo {
  width: 100%;
  max-width: 32rem;
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  text-align: left;
}
.fx-text-demo-eyebrow {
  margin: 0;
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--fx-muted);
}
.fx-text-demo-title {
  margin: 0;
  font-size: clamp(1.5rem, 4.4vw, 2.15rem);
  line-height: 1.12;
  letter-spacing: -0.025em;
  font-weight: 650;
  color: var(--fx-fg);
  text-wrap: balance;
}
.fx-text-demo-stat {
  margin: 0;
  font-size: clamp(2.4rem, 9vw, 3.5rem);
  line-height: 1;
  letter-spacing: -0.04em;
  font-weight: 700;
  color: var(--fx-fg);
}
.fx-text-demo-sub {
  margin: 0;
  font-size: 0.95rem;
  line-height: 1.55;
  color: var(--fx-muted);
}
.fx-text-demo-mono {
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  letter-spacing: -0.01em;
}

/* ============================================================ split-rise == */
.fx-text-rise .fx-word,
.fx-text-rise .fx-char,
.fx-text-rise .fx-line { backface-visibility: hidden; }

/* ============================================================ split-fade == */
.fx-text-fade .fx-word-wrap { overflow: visible; }

/* ============================================== scramble / decrypt ======== */
.fx-text-scramble .fx-char,
.fx-text-decrypt .fx-char { text-align: center; }

.fx-text-scramble .fx-char.is-scrambling {
  opacity: 0.68;
}
.fx-text-decrypt .fx-char.is-scrambling {
  color: var(--fx-accent);
  opacity: 0.85;
}
.fx-text-decrypt .fx-char.is-front {
  color: var(--fx-accent-2);
  opacity: 1;
}

/* ============================================================ typewriter == */
.fx-text-typewriter { position: relative; }
.fx-text-typewriter .fx-char { visibility: hidden; }
.fx-text-typewriter .fx-char.is-typed { visibility: visible; }
.fx-text-typewriter .fx-word-wrap { overflow: visible; }

/* Zero-width absolutely positioned caret: it never affects layout. */
.fx-text-caret {
  position: absolute;
  display: inline-block;
  width: 0;
  height: 0;
  overflow: visible;
  pointer-events: none;
}
.fx-text-caret::before {
  content: "";
  position: absolute;
  left: 0.04em;
  top: 0.06em;
  height: 1em;
  width: 0.075em;
  min-width: 1px;
  border-radius: 1px;
  background: var(--fx-accent);
  animation: fx-text-caret-blink 1.06s steps(1, end) infinite;
}
@keyframes fx-text-caret-blink {
  0%, 50%   { opacity: 1; }
  50.01%, 100% { opacity: 0; }
}

/* ================================================================= shine == */
.fx-text-shine,
[data-fx~="text-shine"] {
  --fx-text-shine-base: var(--fx-fg);
  --fx-text-shine-band: var(--fx-accent);
  --fx-text-shine-dur: 3.6s;
  color: var(--fx-text-shine-base);
  /* The band has to be wide enough to actually cross a glyph. At 6% of a 260%
     gradient it was ~2% of the element and flashed past unseen. */
  background-image: linear-gradient(
    100deg,
    var(--fx-text-shine-base) 0%,
    var(--fx-text-shine-base) 32%,
    var(--fx-text-shine-band) 50%,
    var(--fx-text-shine-base) 68%,
    var(--fx-text-shine-base) 100%
  );
  background-size: 260% 100%;
  background-repeat: repeat;
  background-position: 100% 0;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  animation: fx-text-shine-sweep var(--fx-text-shine-dur) infinite;
  will-change: background-position;
}
/* A decelerating sweep spends almost all its travel in the first few hundred
   milliseconds, which reads as a glitch rather than a pass. Keep it near-linear
   and give it most of the cycle; the rest is the deliberate pause. */
@keyframes fx-text-shine-sweep {
  0%   { background-position: 100% 0; animation-timing-function: cubic-bezier(0.4, 0.05, 0.5, 0.95); }
  62%  { background-position: 0% 0; }
  100% { background-position: 0% 0; }
}

/* ========================================================= gradient-flow == */
.fx-text-gradient,
[data-fx~="text-gradient-flow"] {
  --fx-text-gradient-dur: 7s;
  background-image: linear-gradient(
    92deg,
    var(--fx-accent) 0%,
    var(--fx-accent-2) 33%,
    var(--fx-accent-3) 66%,
    var(--fx-accent) 100%
  );
  background-size: 300% 100%;
  background-position: 0 0;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  animation: fx-text-gradient-pan var(--fx-text-gradient-dur) linear infinite;
  will-change: background-position;
}
@keyframes fx-text-gradient-pan {
  from { background-position: 0 0; }
  to   { background-position: -300% 0; }
}

/* ======================================================== highlight-sweep = */
.fx-text-highlight {
  --fx-text-highlight-color: var(--fx-accent);
  --fx-text-highlight-color-soft: var(--fx-accent);
  --fx-text-highlight-dur: 0.52s;
  --fx-text-highlight-step: 0.05s;
}
.fx-text-highlight .fx-word-wrap { overflow: visible; }
.fx-text-highlight .fx-word {
  position: relative;
  isolation: isolate;
}
.fx-text-highlight .fx-word::before {
  content: "";
  position: absolute;
  z-index: -1;
  left: -0.1em;
  right: -0.1em;
  top: 0.14em;
  bottom: 0.08em;
  border-radius: 0.1em;
  background: var(--fx-text-highlight-color-soft);
  background: color-mix(in oklab, var(--fx-text-highlight-color) 34%, transparent);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--fx-text-highlight-dur) var(--fx-ease-out);
  transition-delay: calc(var(--fx-i, 0) * var(--fx-text-highlight-step));
}
.fx-text-highlight.is-swept .fx-word::before { transform: scaleX(1); }

/* ======================================================== underline-draw == */
.fx-text-underline {
  --fx-text-underline-color: var(--fx-accent);
  --fx-text-underline-thickness: 2px;
  --fx-text-underline-dur: 0.48s;
  position: relative;
  display: inline-block;
}
.fx-text-underline::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -0.1em;
  height: var(--fx-text-underline-thickness);
  border-radius: 999px;
  background: var(--fx-text-underline-color);
  transform: scaleX(0);
  transform-origin: right center;
  transition: transform var(--fx-text-underline-dur) var(--fx-ease-out);
}
.fx-text-underline.is-drawn::after {
  transform: scaleX(1);
  transform-origin: left center;
}
@media (hover: hover) and (pointer: fine) {
  .fx-text-underline[data-fx-trigger="hover"]:hover::after,
  .fx-text-underline[data-fx-trigger="hover"]:focus-visible::after {
    transform: scaleX(1);
    transform-origin: left center;
  }
}

/* ================================================================== wave == */
.fx-text-wave {
  --fx-text-wave-amp: 0.13em;
  --fx-text-wave-dur: 2.6s;
  --fx-text-wave-step: 0.06s;
}
.fx-text-wave .fx-word-wrap { overflow: visible; }
.fx-text-wave .fx-char {
  animation: fx-text-wave-bob var(--fx-text-wave-dur) ease-in-out infinite;
  animation-delay: calc(var(--fx-i, 0) * var(--fx-text-wave-step) * -1);
}
@keyframes fx-text-wave-bob {
  0%, 100% { transform: translate3d(0, var(--fx-text-wave-amp), 0); }
  50%      { transform: translate3d(0, calc(var(--fx-text-wave-amp) * -1), 0); }
}

/* ================================================================ glitch == */
.fx-text-glitch {
  position: relative;
  display: inline-block;
  isolation: isolate;
}
.fx-text-glitch-layer {
  position: absolute;
  inset: 0;
  z-index: -1;
  opacity: 0;
  pointer-events: none;
  user-select: none;
  white-space: inherit;
}
.fx-text-glitch-layer.is-a { color: var(--fx-accent); }
.fx-text-glitch-layer.is-b { color: var(--fx-accent-2); }

.fx-text-glitch.is-glitching {
  animation: fx-text-glitch-main var(--fx-text-glitch-dur, 0.5s) steps(1, end) 1;
}
.fx-text-glitch.is-glitching .fx-text-glitch-layer.is-a {
  animation: fx-text-glitch-a var(--fx-text-glitch-dur, 0.5s) steps(1, end) 1;
}
.fx-text-glitch.is-glitching .fx-text-glitch-layer.is-b {
  animation: fx-text-glitch-b var(--fx-text-glitch-dur, 0.5s) steps(1, end) 1;
}
@keyframes fx-text-glitch-main {
  0%, 100% { transform: translate3d(0, 0, 0); }
  14%      { transform: translate3d(1px, 0, 0); }
  32%      { transform: translate3d(-1px, 0, 0); }
  56%      { transform: translate3d(1px, 0, 0); }
  78%      { transform: translate3d(0, 0, 0); }
}
@keyframes fx-text-glitch-a {
  0%       { opacity: 0; clip-path: inset(0 0 100% 0); transform: translate3d(0, 0, 0); }
  8%       { opacity: 1; clip-path: inset(10% 0 64% 0); transform: translate3d(-3px, 0, 0); }
  26%      { opacity: 1; clip-path: inset(54% 0 22% 0); transform: translate3d(3px, 0, 0); }
  44%      { opacity: 1; clip-path: inset(26% 0 50% 0); transform: translate3d(-2px, 0, 0); }
  62%      { opacity: 1; clip-path: inset(72% 0 6% 0);  transform: translate3d(2px, 0, 0); }
  80%      { opacity: 1; clip-path: inset(6% 0 80% 0);  transform: translate3d(-1px, 0, 0); }
  92%, 100% { opacity: 0; clip-path: inset(0 0 100% 0); transform: translate3d(0, 0, 0); }
}
@keyframes fx-text-glitch-b {
  0%       { opacity: 0; clip-path: inset(0 0 100% 0); transform: translate3d(0, 0, 0); }
  8%       { opacity: 1; clip-path: inset(62% 0 12% 0); transform: translate3d(3px, 0, 0); }
  26%      { opacity: 1; clip-path: inset(18% 0 58% 0); transform: translate3d(-3px, 0, 0); }
  44%      { opacity: 1; clip-path: inset(46% 0 28% 0); transform: translate3d(2px, 0, 0); }
  62%      { opacity: 1; clip-path: inset(4% 0 74% 0);  transform: translate3d(-2px, 0, 0); }
  80%      { opacity: 1; clip-path: inset(76% 0 4% 0);  transform: translate3d(1px, 0, 0); }
  92%, 100% { opacity: 0; clip-path: inset(0 0 100% 0); transform: translate3d(0, 0, 0); }
}

/* =========================================================== rotate-words = */
.fx-text-rotate {
  position: relative;
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
  text-align: left;
  padding-block: 0.16em;
  margin-block: -0.16em;
}
.fx-text-rotate-item {
  display: inline-block;
  white-space: nowrap;
  will-change: transform, opacity;
}
.fx-text-rotate-item.is-leaving {
  position: absolute;
  left: 0;
  top: 0.16em;
}
.fx-text-rotate-accent { color: var(--fx-accent); }

/* =============================================================== counter == */
.fx-text-counter {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

/* ======================================================= variable-weight == */
.fx-text-vweight .fx-word-wrap { overflow: visible; }
.fx-text-vweight .fx-char { text-align: center; }

/* =========================================================================
 * REDUCED MOTION — every effect ends at its final, complete, readable state.
 * Ambient loops (shine, gradient-flow, wave, glitch, caret) freeze on one
 * static frame.
 * ========================================================================= */
@media (prefers-reduced-motion: reduce) {
  .fx-text-shine,
  [data-fx~="text-shine"] {
    animation: none;
    background-image: none;
    color: var(--fx-text-shine-base);
    -webkit-text-fill-color: currentColor;
  }
  .fx-text-gradient,
  [data-fx~="text-gradient-flow"] {
    animation: none;
    background-position: 0 0;
  }
  .fx-text-highlight .fx-word::before {
    transition: none;
    transition-delay: 0s;
    transform: scaleX(1);
  }
  .fx-text-underline::after {
    transition: none;
    transform: scaleX(1);
    transform-origin: left center;
  }
  .fx-text-wave .fx-char {
    animation: none;
    transform: none;
  }
  .fx-text-glitch.is-glitching,
  .fx-text-glitch.is-glitching .fx-text-glitch-layer {
    animation: none;
    opacity: 0;
    transform: none;
  }
  .fx-text-glitch-layer { display: none; }
  .fx-text-caret { display: none; }
  .fx-text-caret::before { animation: none; opacity: 1; }
  .fx-text-typewriter .fx-char { visibility: visible; }
  .fx-text-rotate { overflow: visible; }
  .fx-text-rotate-item { transform: none; opacity: 1; }
}

:root[data-fx-reduced] .fx-text-shine,
:root[data-fx-reduced] [data-fx~="text-shine"] {
  animation: none;
  background-image: none;
  color: var(--fx-text-shine-base);
  -webkit-text-fill-color: currentColor;
}
:root[data-fx-reduced] .fx-text-gradient,
:root[data-fx-reduced] [data-fx~="text-gradient-flow"] {
  animation: none;
  background-position: 0 0;
}
:root[data-fx-reduced] .fx-text-highlight .fx-word::before {
  transition: none;
  transition-delay: 0s;
  transform: scaleX(1);
}
:root[data-fx-reduced] .fx-text-underline::after {
  transition: none;
  transform: scaleX(1);
  transform-origin: left center;
}
:root[data-fx-reduced] .fx-text-wave .fx-char {
  animation: none;
  transform: none;
}
:root[data-fx-reduced] .fx-text-glitch.is-glitching,
:root[data-fx-reduced] .fx-text-glitch.is-glitching .fx-text-glitch-layer {
  animation: none;
  opacity: 0;
  transform: none;
}
:root[data-fx-reduced] .fx-text-glitch-layer { display: none; }
:root[data-fx-reduced] .fx-text-caret { display: none; }
:root[data-fx-reduced] .fx-text-caret::before { animation: none; opacity: 1; }
:root[data-fx-reduced] .fx-text-typewriter .fx-char { visibility: visible; }
:root[data-fx-reduced] .fx-text-rotate { overflow: visible; }
:root[data-fx-reduced] .fx-text-rotate-item { transform: none; opacity: 1; }

/* =========================================================================
 * text-knockout-window — CSS only. Text as a WINDOW onto one image: the
 * letters are cut out with background-clip:text and the image is pinned to
 * the viewport with background-attachment:fixed, so scrolling moves the
 * letterforms across a stationary picture — a knockout layer, not a fill.
 *
 * Two tricks carried over from production use, both load-bearing:
 *
 *   REGISTRATION. Any number of elements declaring this effect with the
 *   same --fx-text-knockout-src are windows onto ONE image: fixed
 *   attachment aligns every background to the viewport, so a headline and
 *   a stat three sections apart stay in perfect register with zero
 *   measuring and zero JavaScript.
 *
 *   THE LUMINANCE FLOOR. A real image runs the photograph's full range,
 *   and its darkest region can swallow the letters against a dark page.
 *   background-blend-mode composites the image against the floor colour
 *   per channel, so no in-letter pixel can fall below it while every
 *   brighter part shows through untouched. The default floor is measured,
 *   not felt: rgb(134 134 142) is exactly 5:1 on a near-black page. The
 *   light theme flips the same trick into a ceiling (darken), so letters
 *   can never wash out against a light page either.
 *
 * KNOWN LIMIT, by design: background-attachment:fixed is broken on iOS and
 * most touch browsers. On any coarse pointer — and anywhere clip-to-text
 * is unsupported — the text falls back to solid --fx-fg ink automatically.
 * ========================================================================= */
:is(.fx-text-knockout, [data-fx~="text-knockout-window"]) {
  --fx-text-knockout-src:
    radial-gradient(90% 130% at 16% 12%, var(--fx-accent) 0%, transparent 62%),
    radial-gradient(90% 110% at 82% 26%, var(--fx-accent-2) 0%, transparent 58%),
    radial-gradient(120% 130% at 55% 100%, var(--fx-accent-3) 0%, transparent 66%),
    linear-gradient(160deg, var(--fx-surface-2), var(--fx-surface));
  --fx-text-knockout-floor: rgb(134 134 142);
  --fx-text-knockout-pos: center 36%;
  background-color: var(--fx-text-knockout-floor);
  background-blend-mode: lighten;
  background-image: var(--fx-text-knockout-src);
  background-attachment: fixed;
  background-position: var(--fx-text-knockout-pos);
  background-size: cover;
  background-repeat: no-repeat;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
}

/* Light theme: the floor becomes a ceiling. darken guarantees no in-letter
   pixel is LIGHTER than this grey, which is the failure mode on paper. */
:root[data-fx-theme="light"] :is(.fx-text-knockout, [data-fx~="text-knockout-window"]) {
  --fx-text-knockout-floor: rgb(112 112 120);
  background-blend-mode: darken;
}

/* The two fallbacks, one rule each: no clip-to-text, and any coarse pointer
   (where a fixed background is a slideshow, not an effect). Solid ink. */
@supports not ((-webkit-background-clip: text) or (background-clip: text)) {
  :is(.fx-text-knockout, [data-fx~="text-knockout-window"]) {
    background: none;
    color: var(--fx-fg);
    -webkit-text-fill-color: var(--fx-fg);
  }
}
@media (hover: none), (pointer: coarse) {
  :is(.fx-text-knockout, [data-fx~="text-knockout-window"]) {
    background: none;
    color: var(--fx-fg);
    -webkit-text-fill-color: var(--fx-fg);
  }
}

/* Demo scaffolding: the gallery stage cannot scroll the page, so the preview
   pans the shared image slowly instead — every window shifts in register,
   which is exactly the property scrolling shows on a real page. */
.fx-text-knockout-demo-pan {
  /* The oversize matters: percentage positions offset by (area - image),
     and a gradient under `cover` fills the area exactly, which makes any
     position change a rendering no-op. Blown up past the box, the shared
     image actually travels. */
  background-size: 175% 175%;
  animation: fx-text-knockout-pan 9s var(--fx-ease) infinite alternate;
}
@keyframes fx-text-knockout-pan {
  from { background-position: 22% 24%; }
  to   { background-position: 78% 62%; }
}
@media (prefers-reduced-motion: reduce) {
  .fx-text-knockout-demo-pan { animation: none; }
}
:root[data-fx-reduced] .fx-text-knockout-demo-pan { animation: none; }

/* ══ src/effects/transition.css ══ */
/* =========================================================================
 * effects-lab — category: transition ("Transitions & overlays")
 *
 * Everything here is namespaced .fx-transition-… / @keyframes fx-transition-…
 *
 * MOTION MODEL
 *   Overlays are driven by CSS *transitions* keyed off a [data-state] attribute
 *   that the JS controller flips. Transitions retarget from the current computed
 *   value, so re-opening mid-close never stacks or restarts — it just reverses.
 *   Keyframes are deliberately avoided for anything a user can fire twice.
 *
 * TIMING CONTRACT
 *   enter  : 200–420ms, decelerating (--fx-ease-out) or spring (--fx-ease-spring)
 *   exit   : always shorter than the enter
 *   scrim  : fades faster than the panel travels (180ms in / 140ms out)
 *
 * SCOPE
 *   .fx-transition-host clips its overlays (position:relative; overflow:hidden)
 *   so full-screen effects stay inside a gallery preview box. Adding
 *   data-fx-scope="page" makes the JS add .fx-transition-scope-page, which
 *   promotes the overlay layer to position:fixed for real-site use.
 * ========================================================================= */

/* ------------------------------------------------------------- variables -- */
.fx-transition-host {
  /* Local, overridable. All colour comes from the global token set. */
  --fx-transition-enter: 320ms;
  --fx-transition-exit: 200ms;
  --fx-transition-scrim-in: 180ms;
  --fx-transition-scrim-out: 140ms;
  --fx-transition-scrim: hsl(var(--fx-hue) 30% 5% / 0.58);
  --fx-transition-blur: 8px;
  --fx-transition-z: 20;

  position: relative;
  overflow: hidden;
  isolation: isolate;
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
  width: 100%;
  min-height: 210px;
  padding: 1.05rem;
  border-radius: var(--fx-radius);
  background: var(--fx-surface);
  color: var(--fx-fg);
  box-shadow: var(--fx-ring);
}

:root[data-fx-theme="light"] .fx-transition-host {
  --fx-transition-scrim: hsl(var(--fx-hue) 25% 18% / 0.42);
}

/* Real-site scope: overlays go fixed, the host stops clipping. */
.fx-transition-host.fx-transition-scope-page {
  overflow: visible;
  isolation: auto;
  --fx-transition-z: 9999;
}

/* ------------------------------------------------------ shared primitives -- */
.fx-transition-layer {
  position: absolute;
  inset: 0;
  z-index: var(--fx-transition-z);
  display: grid;
  visibility: hidden;
  transition: visibility 0s linear var(--fx-transition-exit);
}
.fx-transition-scope-page > .fx-transition-layer { position: fixed; }
.fx-transition-layer[data-state="open"] {
  visibility: visible;
  transition-delay: 0s;
}

.fx-transition-scrim {
  position: absolute;
  inset: 0;
  background: var(--fx-transition-scrim);
  opacity: 0;
  cursor: pointer;
  transition: opacity var(--fx-transition-scrim-out) var(--fx-ease-out);
  -webkit-backdrop-filter: blur(var(--fx-transition-blur));
  backdrop-filter: blur(var(--fx-transition-blur));
}
[data-state="open"] > .fx-transition-scrim {
  opacity: 1;
  transition-duration: var(--fx-transition-scrim-in);
}

/* A panel is always a real surface: token colours only. */
.fx-transition-surface {
  position: relative;
  background: var(--fx-surface-2);
  color: var(--fx-fg);
  border: 1px solid var(--fx-border);
  border-radius: var(--fx-radius);
  box-shadow: var(--fx-shadow);
  will-change: transform, opacity;
}
.fx-transition-surface:focus-visible,
.fx-transition-host :focus-visible {
  outline: 2px solid var(--fx-accent);
  outline-offset: 2px;
}

/* -------------------------------------------------------- demo furniture -- */
.fx-transition-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
}
.fx-transition-btn {
  appearance: none;
  font: inherit;
  font-size: 0.82rem;
  font-weight: 560;
  letter-spacing: 0.005em;
  line-height: 1;
  padding: 0.6em 0.95em;
  border-radius: var(--fx-radius-sm);
  border: 1px solid var(--fx-border);
  background: var(--fx-surface-2);
  color: var(--fx-fg);
  cursor: pointer;
  transition: background 160ms var(--fx-ease-out), border-color 160ms var(--fx-ease-out), transform 110ms var(--fx-ease-out);
}
.fx-transition-btn[data-variant="accent"] {
  background: var(--fx-accent);
  border-color: transparent;
  color: var(--fx-accent-contrast);
}
@media (hover: hover) and (pointer: fine) {
  .fx-transition-btn:hover { border-color: var(--fx-accent); }
}
.fx-transition-btn:active { transform: scale(0.97); }
.fx-transition-btn[aria-selected="true"],
.fx-transition-btn[aria-current="page"] { color: var(--fx-accent); }

.fx-transition-title {
  margin: 0 0 0.35rem;
  font-size: 1.02rem;
  line-height: 1.15;
  letter-spacing: -0.015em;
  font-weight: 620;
}
.fx-transition-copy {
  margin: 0;
  font-size: 0.82rem;
  line-height: 1.5;
  color: var(--fx-muted);
}
.fx-transition-eyebrow {
  margin: 0 0 0.3rem;
  font-size: 0.66rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--fx-accent);
}
.fx-transition-body { flex: 1 1 auto; min-height: 0; }
.fx-transition-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.45rem;
  margin-top: 0.85rem;
}
.fx-transition-note {
  margin: 0;
  font-size: 0.72rem;
  line-height: 1.45;
  color: var(--fx-muted);
}

/* Stacked panels: every panel occupies the same grid cell, so swapping one
   for another can never move the surrounding layout. */
.fx-transition-stack {
  display: grid;
  flex: 1 1 auto;
  min-height: 0;
}
.fx-transition-stack > [data-fx-panel] {
  grid-area: 1 / 1;
  min-width: 0;
}
.fx-transition-stack > [data-fx-panel][hidden] { display: none; }

.fx-transition-pane {
  padding: 0.95rem 1rem;
  border-radius: var(--fx-radius-sm);
  background: var(--fx-surface-2);
  border: 1px solid var(--fx-border);
}

/* ============================================================ page fade === */
.fx-transition-stack > [data-fx-panel].fx-transition-pagefade {
  opacity: 0;
  transform: translate3d(0, 10px, 0);
  transition: opacity 170ms var(--fx-ease-out), transform 170ms var(--fx-ease-out);
}
.fx-transition-stack > [data-fx-panel].fx-transition-pagefade[data-state="open"] {
  opacity: 1;
  transform: none;
  transition-duration: 300ms;
}
.fx-transition-stack > [data-fx-panel].fx-transition-pagefade[data-state="leaving"] {
  opacity: 0;
  transform: translate3d(0, -6px, 0);
}

/* =============================================================== curtain === */
.fx-transition-curtain-sheet {
  position: absolute;
  inset: 0;
  z-index: var(--fx-transition-z);
  background: var(--fx-accent);
  color: var(--fx-accent-contrast);
  display: grid;
  place-items: center;
  transform: translate3d(-101%, 0, 0);
  pointer-events: none;
  will-change: transform;
  transition: transform var(--fx-transition-enter) var(--fx-ease);
}
.fx-transition-scope-page > .fx-transition-curtain-sheet { position: fixed; }
.fx-transition-curtain-sheet[data-state="in"] { transform: translate3d(0, 0, 0); pointer-events: auto; }
.fx-transition-curtain-sheet[data-state="out"] { transform: translate3d(101%, 0, 0); pointer-events: auto; }
.fx-transition-curtain-sheet.fx-transition-instant { transition: none !important; }
.fx-transition-curtain-word {
  font-size: 0.7rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  font-weight: 620;
  opacity: 0.85;
}

/* ========================================================= view transition == */
.fx-transition-view-pane { position: relative; }

/* Consumed by ::view-transition-old()/::view-transition-new() rules that
   transition-view injects at runtime under a per-host transition name. */
@keyframes fx-transition-view-out {
  to { opacity: 0; transform: translate3d(0, -6px, 0); }
}
@keyframes fx-transition-view-in {
  from { opacity: 0; transform: translate3d(0, 10px, 0); }
}

/* ================================================================= modal === */
.fx-transition-modal-layer { place-items: center; padding: 1.25rem; }
.fx-transition-modal-panel {
  position: relative;
  z-index: 1;
  width: min(100%, 22rem);
  padding: 1.1rem 1.15rem 1.15rem;
  opacity: 0;
  transform: translate3d(0, 8px, 0) scale(0.96);
  transition: opacity var(--fx-transition-exit) var(--fx-ease-out),
              transform var(--fx-transition-exit) var(--fx-ease-out);
}
[data-state="open"] > .fx-transition-modal-panel {
  opacity: 1;
  transform: none;
  transition: opacity 200ms var(--fx-ease-out),
              transform 420ms var(--fx-ease-spring);
}

/* ================================================================= sheet === */
.fx-transition-sheet-layer { align-items: end; }
.fx-transition-sheet-panel {
  position: relative;
  z-index: 1;
  width: 100%;
  padding: 0.55rem 1.05rem 1.15rem;
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
  border-bottom: 0;
  transform: translate3d(0, 100%, 0);
  transition: transform var(--fx-transition-exit) var(--fx-ease);
}
[data-state="open"] > .fx-transition-sheet-panel {
  transform: translate3d(0, 0, 0);
  transition: transform 440ms var(--fx-ease-spring);
}
.fx-transition-sheet-panel.fx-transition-dragging { transition: none; }
.fx-transition-grip {
  display: block;
  width: 2.25rem;
  height: 0.28rem;
  margin: 0 auto 0.75rem;
  border-radius: 999px;
  background: var(--fx-border);
  cursor: grab;
}
.fx-transition-grip:active { cursor: grabbing; }
.fx-transition-sheet-grab {
  padding-top: 0.45rem;
  cursor: grab;
  touch-action: none;          /* the handle owns the vertical gesture */
  -webkit-user-select: none;
  user-select: none;
}
.fx-transition-sheet-grab:active { cursor: grabbing; }
.fx-transition-grip { touch-action: none; }

/* ================================================================ drawer === */
.fx-transition-drawer-layer { justify-items: end; }
.fx-transition-drawer-layer[data-side="left"] { justify-items: start; }
.fx-transition-drawer-panel {
  position: relative;
  z-index: 1;
  height: 100%;
  width: min(78%, 16.5rem);
  padding: 1rem 1.05rem;
  border-radius: 0;
  border-top: 0;
  border-bottom: 0;
  border-right: 0;
  transform: translate3d(101%, 0, 0);
  transition: transform var(--fx-transition-exit) var(--fx-ease);
}
[data-side="left"] > .fx-transition-drawer-panel {
  border-right: 1px solid var(--fx-border);
  border-left: 0;
  transform: translate3d(-101%, 0, 0);
}
[data-state="open"] > .fx-transition-drawer-panel {
  transform: translate3d(0, 0, 0);
  transition: transform 380ms var(--fx-ease);
}

/* =============================================================== popover === */
.fx-transition-anchor { position: relative; display: inline-block; }
.fx-transition-popover-panel {
  position: absolute;
  top: calc(100% + 0.5rem);
  left: 0;
  z-index: 40;
  width: max(11rem, 100%);
  padding: 0.7rem 0.8rem;
  text-align: left;
  visibility: hidden;
  opacity: 0;
  transform: translate3d(0, -6px, 0) scale(0.94);
  transform-origin: var(--fx-transition-origin, 50% 0%);
  transition: opacity 130ms var(--fx-ease-out),
              transform 130ms var(--fx-ease-out),
              visibility 0s linear 130ms;
}
.fx-transition-anchor[data-placement="top"] > .fx-transition-popover-panel {
  top: auto;
  bottom: calc(100% + 0.5rem);
  transform: translate3d(0, 6px, 0) scale(0.94);
}
[data-state="open"] > .fx-transition-popover-panel {
  visibility: visible;
  opacity: 1;
  transform: none;
  transition: opacity 160ms var(--fx-ease-out),
              transform 300ms var(--fx-ease-spring),
              visibility 0s;
}

/* ============================================================= accordion === */
.fx-transition-accordion { display: grid; gap: 0.4rem; }
.fx-transition-accordion-item {
  border: 1px solid var(--fx-border);
  border-radius: var(--fx-radius-sm);
  background: var(--fx-surface-2);
  overflow: hidden;
}
.fx-transition-accordion-trigger {
  appearance: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  width: 100%;
  font: inherit;
  font-size: 0.84rem;
  font-weight: 560;
  text-align: left;
  padding: 0.7rem 0.85rem;
  border: 0;
  background: none;
  color: var(--fx-fg);
  cursor: pointer;
}
.fx-transition-accordion-chevron {
  flex: none;
  width: 0.62rem;
  height: 0.62rem;
  border-right: 2px solid var(--fx-muted);
  border-bottom: 2px solid var(--fx-muted);
  transform: rotate(45deg);
  transform-origin: 65% 65%;
  transition: transform 300ms var(--fx-ease-out), border-color 200ms linear;
}
[aria-expanded="true"] > .fx-transition-accordion-chevron {
  transform: rotate(-135deg);
  border-color: var(--fx-accent);
}
.fx-transition-accordion-body {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 320ms var(--fx-ease-out);
}
.fx-transition-accordion-item[data-state="open"] > .fx-transition-accordion-body {
  grid-template-rows: 1fr;
}
.fx-transition-accordion-inner {
  overflow: hidden;
  min-height: 0;
  opacity: 0;
  transform: translate3d(0, -4px, 0);
  transition: opacity 140ms var(--fx-ease-out), transform 140ms var(--fx-ease-out);
}
.fx-transition-accordion-item[data-state="open"] .fx-transition-accordion-inner {
  opacity: 1;
  transform: none;
  transition-duration: 240ms;
  transition-delay: 90ms;
}
.fx-transition-accordion-inner > * { margin: 0; padding: 0 0.85rem 0.8rem; }

/* ========================================================= tabs indicator === */
.fx-transition-tablist {
  position: relative;
  display: flex;
  gap: 0.15rem;
  padding-bottom: 0.4rem;
  border-bottom: 1px solid var(--fx-border);
}
.fx-transition-tab {
  appearance: none;
  font: inherit;
  font-size: 0.82rem;
  font-weight: 540;
  padding: 0.4em 0.7em;
  border: 0;
  border-radius: var(--fx-radius-sm);
  background: none;
  color: var(--fx-muted);
  cursor: pointer;
  white-space: nowrap;
  transition: color 200ms var(--fx-ease-out);
}
.fx-transition-tab[aria-selected="true"] { color: var(--fx-fg); }
.fx-transition-tabs-indicator {
  position: absolute;
  left: 0;
  bottom: -1px;
  width: 100px;          /* base width — scaleX maps it onto the active tab */
  height: 2px;
  border-radius: 2px;
  background: var(--fx-accent);
  transform-origin: 0 50%;
  transform: translate3d(0, 0, 0) scaleX(0);
  will-change: transform;
  transition: transform 340ms var(--fx-ease);
}
.fx-transition-tabs-indicator.fx-transition-instant { transition: none; }
.fx-transition-tabpanel { padding-top: 0.75rem; }

/* ============================================================= crossfade === */
.fx-transition-stack > [data-fx-panel].fx-transition-crossfade {
  opacity: 0;
  transition: opacity 150ms var(--fx-ease-out);
}
.fx-transition-stack > [data-fx-panel].fx-transition-crossfade[data-state="open"] {
  opacity: 1;
  transition-duration: 260ms;
}

/* =========================================================== stagger menu === */
.fx-transition-menu-layer { align-content: stretch; }
.fx-transition-menu-panel {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 0.15rem;
  padding: 1.1rem 1.25rem;
  border-radius: 0;
  border: 0;
  background: var(--fx-bg);
  opacity: 0;
  transition: opacity var(--fx-transition-exit) var(--fx-ease-out);
}
[data-state="open"] > .fx-transition-menu-panel {
  opacity: 1;
  transition-duration: 240ms;
}
.fx-transition-menu-close {
  position: absolute;
  top: 0.7rem;
  right: 0.8rem;
}
.fx-transition-menu-link {
  display: block;
  width: fit-content;
  font-size: 1.35rem;
  font-weight: 620;
  line-height: 1.25;
  letter-spacing: -0.025em;
  color: var(--fx-fg);
  text-decoration: none;
  background: none;
  border: 0;
  padding: 0.1em 0;
  font-family: inherit;
  cursor: pointer;
  opacity: 0;
  transform: translate3d(0, 16px, 0);
  transition: opacity 160ms var(--fx-ease-out), transform 160ms var(--fx-ease-out);
}
[data-state="open"] .fx-transition-menu-link {
  opacity: 1;
  transform: none;
  transition: opacity 260ms var(--fx-ease-out),
              transform 460ms var(--fx-ease-spring);
  transition-delay: calc(var(--fx-i, 0) * var(--fx-transition-menu-stagger, 55ms) + 70ms);
}
@media (hover: hover) and (pointer: fine) {
  .fx-transition-menu-link:hover { color: var(--fx-accent); }
}

/* =========================================================== FLIP shared === */
.fx-transition-flip-track {
  display: grid;
  /* Deliberately unequal: the card changes size as well as position, which is
     what the inner counter-scale exists to keep readable. */
  grid-template-columns: 1.55fr 1fr;
  gap: 0.6rem;
  flex: 1 1 auto;
  min-height: 0;
}
.fx-transition-flip-slot {
  display: grid;
  align-content: start;
  padding: 0.5rem;
  border: 1px dashed var(--fx-border);
  border-radius: var(--fx-radius-sm);
  min-height: 0;
}
.fx-transition-flip-slot[data-size="narrow"] { align-content: center; }
.fx-transition-flip-item {
  padding: 0.7rem 0.8rem;
  border-radius: var(--fx-radius-sm);
  background: var(--fx-accent);
  color: var(--fx-accent-contrast);
  transform-origin: 0 0;
  will-change: transform;
  /* Duration comes through a custom property, never an inline transition
     shorthand — an inline declaration would outrank .fx-transition-instant
     and the INVERT step would animate instead of being committed silently. */
  transition: transform var(--fx-transition-flip-dur, 460ms) var(--fx-ease);
}
.fx-transition-flip-inner {
  transform-origin: 0 0;
  transition: transform var(--fx-transition-flip-dur, 460ms) var(--fx-ease);
}
.fx-transition-flip-item.fx-transition-instant,
.fx-transition-flip-item.fx-transition-instant .fx-transition-flip-inner { transition: none; }
.fx-transition-flip-label {
  margin: 0;
  font-size: 0.8rem;
  font-weight: 620;
  letter-spacing: -0.01em;
  white-space: nowrap;
}

/* ==========================================================================
 * REDUCED MOTION — both the OS setting and the manual switch.
 * Every transition becomes a <=120ms crossfade or an instant state change.
 * No travel, no scaling, no wipes. Open/close still works exactly the same.
 * ========================================================================== */
@media (prefers-reduced-motion: reduce) {
  .fx-transition-host * { transition-duration: 120ms !important; transition-delay: 0s !important; }
  .fx-transition-layer { transition: visibility 0s linear 120ms !important; }
  .fx-transition-layer[data-state="open"] { transition-delay: 0s !important; }
  .fx-transition-modal-panel,
  [data-state="open"] > .fx-transition-modal-panel,
  .fx-transition-sheet-panel,
  [data-state="open"] > .fx-transition-sheet-panel,
  .fx-transition-drawer-panel,
  [data-side="left"] > .fx-transition-drawer-panel,
  [data-state="open"] > .fx-transition-drawer-panel,
  .fx-transition-popover-panel,
  [data-state="open"] > .fx-transition-popover-panel,
  .fx-transition-menu-link,
  [data-state="open"] .fx-transition-menu-link,
  .fx-transition-stack > [data-fx-panel].fx-transition-pagefade,
  .fx-transition-stack > [data-fx-panel].fx-transition-pagefade[data-state="open"],
  .fx-transition-stack > [data-fx-panel].fx-transition-pagefade[data-state="leaving"],
  .fx-transition-accordion-inner,
  .fx-transition-accordion-item[data-state="open"] .fx-transition-accordion-inner { transform: none !important; }
  .fx-transition-popover-panel { transition: opacity 120ms linear, visibility 0s linear 120ms !important; }
  [data-state="open"] > .fx-transition-popover-panel { transition: opacity 120ms linear, visibility 0s !important; }
  .fx-transition-sheet-panel,
  .fx-transition-drawer-panel { opacity: 0; transition: opacity 120ms linear !important; }
  [data-state="open"] > .fx-transition-sheet-panel,
  [data-state="open"] > .fx-transition-drawer-panel { opacity: 1; }
  .fx-transition-curtain-sheet { display: none !important; }
  .fx-transition-accordion-body { transition-duration: 0s !important; }
  .fx-transition-accordion-chevron { transition: border-color 120ms linear !important; }
  .fx-transition-tabs-indicator { transition: none !important; }
  .fx-transition-flip-item,
  .fx-transition-flip-inner { transition: none !important; }
  .fx-transition-scrim { -webkit-backdrop-filter: none; backdrop-filter: none; }
}
:root[data-fx-reduced] .fx-transition-host * { transition-duration: 120ms !important; transition-delay: 0s !important; }
:root[data-fx-reduced] .fx-transition-layer { transition: visibility 0s linear 120ms !important; }
:root[data-fx-reduced] .fx-transition-layer[data-state="open"] { transition-delay: 0s !important; }
:root[data-fx-reduced] .fx-transition-modal-panel,
:root[data-fx-reduced] [data-state="open"] > .fx-transition-modal-panel,
:root[data-fx-reduced] .fx-transition-sheet-panel,
:root[data-fx-reduced] [data-state="open"] > .fx-transition-sheet-panel,
:root[data-fx-reduced] .fx-transition-drawer-panel,
:root[data-fx-reduced] [data-side="left"] > .fx-transition-drawer-panel,
:root[data-fx-reduced] [data-state="open"] > .fx-transition-drawer-panel,
:root[data-fx-reduced] .fx-transition-popover-panel,
:root[data-fx-reduced] [data-state="open"] > .fx-transition-popover-panel,
:root[data-fx-reduced] .fx-transition-menu-link,
:root[data-fx-reduced] [data-state="open"] .fx-transition-menu-link,
:root[data-fx-reduced] .fx-transition-stack > [data-fx-panel].fx-transition-pagefade,
:root[data-fx-reduced] .fx-transition-stack > [data-fx-panel].fx-transition-pagefade[data-state="open"],
:root[data-fx-reduced] .fx-transition-stack > [data-fx-panel].fx-transition-pagefade[data-state="leaving"],
:root[data-fx-reduced] .fx-transition-accordion-inner,
:root[data-fx-reduced] .fx-transition-accordion-item[data-state="open"] .fx-transition-accordion-inner { transform: none !important; }
:root[data-fx-reduced] .fx-transition-popover-panel { transition: opacity 120ms linear, visibility 0s linear 120ms !important; }
:root[data-fx-reduced] [data-state="open"] > .fx-transition-popover-panel { transition: opacity 120ms linear, visibility 0s !important; }
:root[data-fx-reduced] .fx-transition-sheet-panel,
:root[data-fx-reduced] .fx-transition-drawer-panel { opacity: 0; transition: opacity 120ms linear !important; }
:root[data-fx-reduced] [data-state="open"] > .fx-transition-sheet-panel,
:root[data-fx-reduced] [data-state="open"] > .fx-transition-drawer-panel { opacity: 1; }
:root[data-fx-reduced] .fx-transition-curtain-sheet { display: none !important; }
:root[data-fx-reduced] .fx-transition-accordion-body { transition-duration: 0s !important; }
:root[data-fx-reduced] .fx-transition-accordion-chevron { transition: border-color 120ms linear !important; }
:root[data-fx-reduced] .fx-transition-tabs-indicator { transition: none !important; }
:root[data-fx-reduced] .fx-transition-flip-item,
:root[data-fx-reduced] .fx-transition-flip-inner { transition: none !important; }
:root[data-fx-reduced] .fx-transition-scrim { -webkit-backdrop-filter: none; backdrop-filter: none; }

/* Translucency / contrast preferences ------------------------------------- */
@media (prefers-reduced-transparency: reduce) {
  .fx-transition-scrim {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    background: hsl(var(--fx-hue) 30% 5% / 0.82);
  }
}
@media (prefers-contrast: more) {
  .fx-transition-surface { border-color: var(--fx-fg); }
}

/* =========================================================================
 * transition-page-curtain — cross-document view-transition wipe.
 *
 * These rules are inert until BOTH are true: the effect's JS injected the
 * @view-transition opt-in, and one of the fx-wipe-* types was set on the
 * transition by pageswap/pagereveal. An untyped navigation matches nothing
 * here and gets the browser's own crossfade; a browser without the
 * :active-view-transition-type() selector drops these rules whole.
 *
 * The custom properties are written by the effect from its options; the
 * defaults here only cover a page where CSS loaded and JS did not.
 * ========================================================================= */
:root {
  --fx-page-curtain-skew: 15.84vh;    /* tan(9deg) x 100vh */
  --fx-page-curtain-dur: 0.5s;
  --fx-page-curtain-ease: cubic-bezier(0.76, 0, 0.24, 1);
}

/* The overlay's background IS the curtain: it is what the cover phase
   uncovers and the reveal phase covers again. Accent gradient by default,
   any paint via --fx-page-curtain-fill. */
:root:active-view-transition-type(fx-wipe-forward)::view-transition,
:root:active-view-transition-type(fx-wipe-back)::view-transition {
  background: var(--fx-page-curtain-fill,
    linear-gradient(135deg, var(--fx-accent-2), var(--fx-accent)));
}

/* Forward: the old page is drawn off to the left, the new one follows the
   curtain in from the right, delayed by one phase. */
:root:active-view-transition-type(fx-wipe-forward)::view-transition-old(root) {
  animation: fx-page-curtain-cover var(--fx-page-curtain-dur) var(--fx-page-curtain-ease) both;
}
:root:active-view-transition-type(fx-wipe-forward)::view-transition-new(root) {
  animation: fx-page-curtain-reveal var(--fx-page-curtain-dur) var(--fx-page-curtain-ease)
    var(--fx-page-curtain-dur) both;
}

/* Back is the mirror, and mirroring the geometry is exactly running the
   other phase backwards: the shape the cover ends on is the shape the
   reveal starts from. The symmetric easing makes `reverse` cost nothing in
   feel and keeps one set of keyframes as the single source of truth. */
:root:active-view-transition-type(fx-wipe-back)::view-transition-old(root) {
  animation: fx-page-curtain-reveal var(--fx-page-curtain-dur) var(--fx-page-curtain-ease) both reverse;
}
:root:active-view-transition-type(fx-wipe-back)::view-transition-new(root) {
  animation: fx-page-curtain-cover var(--fx-page-curtain-dur) var(--fx-page-curtain-ease)
    var(--fx-page-curtain-dur) both reverse;
}

/* The old page's visible area collapses toward the left edge, uncovering
   the curtain behind it. The slanted edge is the skew offset applied to
   opposite corners. */
@keyframes fx-page-curtain-cover {
  from {
    clip-path: polygon(
      0% 0%,
      calc(100% + var(--fx-page-curtain-skew)) 0%,
      100% 100%,
      0% 100%
    );
  }
  to {
    clip-path: polygon(
      0% 0%,
      0% 0%,
      calc(0% - var(--fx-page-curtain-skew)) 100%,
      0% 100%
    );
  }
}

/* The new page continues past in the same direction rather than meeting
   the old one — that is what makes it read as one sheet travelling, not
   two doors closing. */
@keyframes fx-page-curtain-reveal {
  from {
    clip-path: polygon(
      calc(100% + var(--fx-page-curtain-skew)) 0%,
      100% 0%,
      100% 100%,
      100% 100%
    );
  }
  to {
    clip-path: polygon(
      0% 0%,
      100% 0%,
      100% 100%,
      calc(0% - var(--fx-page-curtain-skew)) 100%
    );
  }
}

/* ---- the gallery's same-document simulation ----------------------------- */
/* Two stacked "pages" over the curtain layer; the JS plays the same
   keyframe geometry on them with the Web Animations API. Preview only. */
.fx-transition-page-curtain-sim {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  width: 100%;
  min-height: 250px;
  border-radius: var(--fx-radius-lg);
  border: 1px solid var(--fx-border);
}
.fx-transition-page-curtain-sim > [data-fx-panel] {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: grid;
  place-content: center;
  gap: 6px;
  text-align: center;
  padding: 1.4rem;
  background: var(--fx-bg);
  color: var(--fx-fg);
}
.fx-transition-page-curtain-fill {
  position: absolute;
  inset: 0;
  z-index: 0;
  background: var(--fx-page-curtain-fill,
    linear-gradient(135deg, var(--fx-accent-2), var(--fx-accent)));
}
.fx-transition-page-curtain-sim .fx-transition-demo-note {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 10px;
  z-index: 2;
  margin: 0;
  text-align: center;
  font-size: 10.5px;
  letter-spacing: 0.04em;
  color: var(--fx-muted);
}
