/* ============================================================================
 * Syna Motion — Core Stylesheet (framework-free)
 * ============================================================================
 * The agnostic CSS surface for the suite. Works in PHP/Bootstrap sites, the
 * Unity→React Bridge-HUD, plain HTML, and React alike — no framework required.
 * Pair with core/runtime.ts (the vanilla scanner that wires pointer/scroll
 * variables and toggles reveal state).
 *
 * Class contract: every effect's class is `syna-fx-<id>` (asserted by
 * scripts/validate-manifest.mjs). Per-effect tunables are CSS custom properties
 * (the `cssVar` field in the registry); set them inline or via runtime.
 *
 * Token dependency: reads --motion-* / --ease-* (see tokens/motion-tokens.ts;
 * call applyMotionTokens() if your app doesn't already define them). Falls back
 * to sensible literals so the stylesheet is usable standalone.
 *
 * Absorbs (now the SSOT for): www-theme-kit _sparkle-effects / _scroll-reveal /
 * _card-hover / _animated-gradient-bg / _spotlight-follow, and
 * syna-theme-kit effects/spotlight.css.
 * ============================================================================ */

/* Timing tokens are NOT redeclared on effect elements. A declaration such as
 * `--motion-slow: var(--motion-slow, 400ms)` is a self-reference cycle: CSS makes
 * the property guaranteed-invalid on that element AND every descendant, so any
 * host rule reading it without a fallback silently computes to `none`. Every
 * rule below reads the tokens with a literal fallback instead, and inherits a
 * host's --motion-* / --ease-* values when the host defines them.
 * Guarded by test/css-contract.test.ts. */

/* ── Tier 1: Physics ───────────────────────────────────────────────────────*/

.syna-fx-spring-in {
  /* backwards, not both: the last keyframe equals the element's own style, and a
     `both` fill would pin transform after the entrance, disabling hover/press. */
  animation: syna-spring-in var(--motion-slow, 400ms) var(--ease-emphasized, cubic-bezier(0.34, 1.56, 0.64, 1)) backwards;
}
@keyframes syna-spring-in {
  0% { opacity: 0; transform: scale(0.92) translateY(8px); }
  60% { opacity: 1; transform: scale(1.02) translateY(0); }
  100% { opacity: 1; transform: scale(1) translateY(0); }
}

/* `stagger` is an orchestration modifier: runtime sets --fx-i (child index) and
 * --fx-stagger (ms step); we offset animation-delay by their product. */
.syna-fx-stagger > * {
  animation-delay: calc(var(--fx-i, 0) * var(--fx-stagger, 80ms));
}

/* spring-in + stagger: the entrance must play on each indexed CHILD, not the
 * parent — animating the parent moves the whole group as one block and no
 * stagger is visible. The per-child delay lives inside the shorthand (second
 * time value) because a shorthand would otherwise reset animation-delay. */
.syna-fx-stagger.syna-fx-spring-in {
  animation: none;
}
.syna-fx-stagger.syna-fx-spring-in > * {
  animation: syna-spring-in
    var(--motion-slow, 400ms)
    var(--ease-emphasized, cubic-bezier(0.34, 1.56, 0.64, 1))
    calc(var(--fx-i, 0) * var(--fx-stagger, 80ms))
    backwards;
}

/* ── Tier 2: Cursor & ambient ──────────────────────────────────────────────*/

/* --mx/--my registered as typed <length-percentage> so they INTERPOLATE — the
   spotlight then eases toward the cursor (smooth follow) instead of snapping to
   each pointer sample. --spot-prox drives the fade (0 far → 1 near) and also
   interpolates so a card lights/dims smoothly as the cursor sweeps the row.
   Guarded in @supports so browsers without @property still get an instant
   (but working) follow. */
@supports (background: paint(x)) {
  @property --mx { syntax: '<length-percentage>'; inherits: true; initial-value: 50%; }
  @property --my { syntax: '<length-percentage>'; inherits: true; initial-value: 50%; }
  @property --spot-prox { syntax: '<number>'; inherits: true; initial-value: 0; }
}

/* Spotlight — a PERIMETER EDGE-LIGHT. A bright glow rides the card's border
   nearest the cursor and spills ADDITIVELY onto the surface and into the gutter
   between cards. The runtime writes --mx/--my (cursor position relative to THIS
   card — may fall outside 0–100% when the cursor is in the gutter/over a
   sibling, which is what pulls the light toward that edge) and --spot-prox
   (0 far → 1 near) so a shared row of cards hands the light off card-to-card as
   the cursor travels. Colored by the active --accent-* palette.

   Three additive layers, all mix-blend-mode:screen (light ADDS, never darkens):
     ::before  the bright edge line — a radial at (mx,my) masked to the border
               band, so only the perimeter lights and it's hottest nearest the
               cursor. Plus a hot near-white core at the closest point.
     ::after   the SPILL — a big blurred colored radial that bleeds off the edge
               onto the dark surface + into the gutter (the reference bloom). */
.syna-fx-spotlight {
  position: relative;
  isolation: isolate;
  --spotlight-size: 460px;         /* spill radius */
  --spotlight-edge: 190px;         /* edge-light reach along the border */
  --spotlight-intensity: 1;        /* master multiplier for the glow */
  --spotlight-color: var(--accent-cyan, #4de8ff);
  /* screen ADDS light, which vanishes on a near-white surface. Light themes set
     `normal` so the edge-light reads as a colored rim instead. */
  --spotlight-blend: screen;
  --spot-prox: 0;
  --mx: 50%;
  --my: 50%;
}
/* Edge line — bright accent along the border nearest the cursor, hottest at the
   closest point (a near-white core), masked to the border band so the interior
   stays clear. */
.syna-fx-spotlight::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  z-index: 2;
  opacity: var(--spot-prox, 0);
  transition: opacity var(--motion-normal, 300ms) var(--ease-standard, ease);
  mix-blend-mode: var(--spotlight-blend, screen);
  padding: 2px; /* border band thickness for the xor mask */
  background:
    /* hot near-white core — tight + bright so the lit border reads as a crisp
       stroke right at the closest point on the edge */
    radial-gradient(calc(var(--spotlight-edge) * 0.32) circle at var(--mx) var(--my),
      #ffffff 0%,
      color-mix(in srgb, #fff 88%, var(--spotlight-color)) 18%,
      color-mix(in srgb, var(--spotlight-color) 82%, transparent) 42%,
      transparent 72%),
    /* wider saturated accent wash along the border */
    radial-gradient(var(--spotlight-edge) circle at var(--mx) var(--my),
      color-mix(in srgb, var(--spotlight-color) calc(100% * var(--spotlight-intensity)), transparent) 0%,
      color-mix(in srgb, var(--spotlight-color) calc(55% * var(--spotlight-intensity)), transparent) 40%,
      transparent 78%);
  -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  mask-composite: exclude;
}
/* Spill — the big blurred colored bloom that bleeds off the edge onto the
   surface and into the gutter. Overflows the card (no clip) so it fills the gap
   between siblings; additive so overlapping spills from two cards sum. */
.syna-fx-spotlight::after {
  content: '';
  position: absolute;
  inset: -40%;                     /* extend past the card so spill reaches the gutter */
  border-radius: inherit;
  pointer-events: none;
  z-index: 0;
  opacity: calc(var(--spot-prox, 0) * 0.9);
  transition: opacity var(--motion-normal, 300ms) var(--ease-standard, ease);
  mix-blend-mode: var(--spotlight-blend, screen);
  background:
    radial-gradient(var(--spotlight-size) circle at var(--mx) var(--my),
      color-mix(in srgb, var(--spotlight-color) calc(60% * var(--spotlight-intensity)), transparent) 0%,
      color-mix(in srgb, var(--spotlight-color) calc(26% * var(--spotlight-intensity)), transparent) 32%,
      transparent 60%);
  filter: blur(16px);
}
.syna-fx-spotlight > * { position: relative; z-index: 1; }

/* Spotlight group — the shared-light container. Its children (the .syna-fx-
   spotlight cards) are tracked from ONE pointer listener at the group, so the
   light travels across the whole row and through the gutters as one sweep. The
   group establishes stacking + lets sibling spills overlap additively. */
.syna-fx-spotlight-group {
  position: relative;
  isolation: isolate;
}
/* Cards inside a group must let their spill overflow into the gutter. */
.syna-fx-spotlight-group .syna-fx-spotlight { overflow: visible; }

/* 3D tilt — the runtime drives it with a SPRING (rAF-integrated), writing the
   settled --rx/--ry each frame, so the surface has weight: it accelerates toward
   the cursor, overshoots slightly, and settles. No CSS transition on transform
   (the spring already smooths it; a transition would double-smooth into lag). */
.syna-fx-tilt-3d {
  --fx-tilt-max: 12;
  position: relative;
  transform: perspective(900px) rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
  transform-style: preserve-3d;
  will-change: transform;
}
/* PBR specular highlight — a physically-based Blinn-Phong lobe, NOT a soft
   reflection blotch. The runtime computes the half-vector between the fixed key
   light and the view direction against the tilted surface normal and writes:
     --spec-x / --spec-y  the lobe center (where N·H peaks on the surface)
     --spec-i             lobe intensity  = (N·H)^shininess   (tight, hard)
     --fresnel            Schlick rim term = F0 + (1-F0)(1-N·V)^5  (edge glint)
   The lobe is a SMALL, high-exponent radial (tight falloff = high shininess);
   Fresnel adds a thin grazing-edge brighten. Reads as polished coated plastic,
   not a smeared gradient. */
.syna-fx-tilt-3d::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  z-index: 2;
  opacity: 0;
  transition: opacity var(--motion-normal, 300ms) var(--ease-standard, ease);
  mix-blend-mode: screen;
  background:
    /* Specular lobe — tight (11% radius) with a hard core→transparent ramp so
       the highlight reads as a crisp glint that slides with the surface, not a
       soft blob. */
    radial-gradient(11% 14% at var(--spec-x, 50%) var(--spec-y, 40%),
      rgba(255, 255, 255, calc(0.95 * var(--spec-i, 0))) 0%,
      rgba(255, 255, 255, calc(0.28 * var(--spec-i, 0))) 40%,
      transparent 66%),
    /* Fresnel edge — a thin bright rim that intensifies at grazing view angles
       (a coated-surface hallmark), masked to the border band. */
    linear-gradient(var(--fresnel-angle, 145deg),
      rgba(214, 232, 255, calc(0.5 * var(--fresnel, 0))) 0%,
      transparent 18%);
}
.syna-fx-tilt-3d:hover::after { opacity: 1; }

/* Magnetic — runtime sets --tx/--ty px pull. */
.syna-fx-magnetic {
  --fx-magnetic-strength: 0.3;
  transform: translate(var(--tx, 0px), var(--ty, 0px));
  transition: transform var(--motion-normal, 300ms) var(--ease-emphasized, ease);
  will-change: transform;
}

/* Rim glow — a pixel-thin rim light with bloom that orbits the element
   border, tracking the pointer angle from the element center (runtime sets
   --fx-rim-angle / --fx-rim-prox). The ring is a conic-gradient arc masked to
   the border band (xor mask); ::after is the same arc blurred = the bloom. */
.syna-fx-rim-glow {
  position: relative;
  --fx-rim-color: var(--accent-cyan, #4de8ff);
  --fx-rim-width: 2px;
}
.syna-fx-rim-glow::before,
.syna-fx-rim-glow::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  z-index: 2;
  padding: var(--fx-rim-width, 2px);
  background: conic-gradient(from calc(var(--fx-rim-angle, 0deg) - 35deg),
    transparent 0deg,
    color-mix(in srgb, var(--fx-rim-color) 85%, transparent) 25deg,
    #fff 35deg,
    color-mix(in srgb, var(--fx-rim-color) 85%, transparent) 45deg,
    transparent 70deg);
  -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  mask-composite: exclude;
  opacity: var(--fx-rim-prox, 0.6);
  transition: opacity var(--motion-fast, 150ms) linear;
}
/* Bloom — the same rim arc, fattened and blurred, layered under the crisp rim
   so the light spills outward (a hint of bloom). A second, wider+softer pass
   (::after has its own duplicate via the ::before/::after share) reads as the
   halo; the drop-shadow adds the colored spill beyond the border. */
.syna-fx-rim-glow::after {
  padding: calc(var(--fx-rim-width, 2px) * 3.2);
  filter: blur(9px)
    drop-shadow(0 0 6px color-mix(in srgb, var(--fx-rim-color) 70%, transparent));
  opacity: calc(var(--fx-rim-prox, 0.6) * 0.85);
}

/* Cursor ripple — runtime appends a span.syna-ripple at click point AND fires
   a true screen-space displacement wave (core/waterRipple.ts: an SVG
   feDisplacementMap ring warps the element's actual pixels outward like a tap
   on water). The span is the specular crest: a thin ring with bloom, not a
   filled 2D disc. */
.syna-fx-cursor-ripple {
  position: relative;
  overflow: hidden;
  --fx-ripple-color: currentColor;
}
.syna-fx-cursor-ripple .syna-ripple {
  position: absolute;
  border-radius: 50%;
  pointer-events: none;
  transform: translate(-50%, -50%) scale(0);
  background: radial-gradient(circle,
    transparent 52%,
    color-mix(in srgb, #fff 40%, transparent) 66%,
    color-mix(in srgb, var(--fx-ripple-color, currentColor) 35%, transparent) 74%,
    transparent 82%);
  filter: drop-shadow(0 0 8px color-mix(in srgb, var(--fx-ripple-color, currentColor) 55%, transparent));
  animation: syna-ripple 700ms var(--ease-decelerate, cubic-bezier(0, 0, 0.2, 1)) forwards;
}
@keyframes syna-ripple {
  to { transform: translate(-50%, -50%) scale(1.6); opacity: 0; }
}

/* Particle burst — a confetti blast (React surface; keyframes live here so the
   sprite-sheet twirl + two-stage ballistics are GPU-composited, not JS-ticked).
   The React component sizes/colors/masks each piece and sets the per-piece vars
   (--bx/--by blast vector, --fall gravity depth, --sway flutter, --frame-* the
   sprite-sheet walk); these three tracks drive them. Outer element = ballistics
   + fade; inner element = flutter + sprite twirl (kept apart so no two tracks
   fight over `transform`). */
@keyframes syna-confetti-fly {
  0% { transform: translate3d(0, 0, 0); }
  /* Explosive outward impulse — reaches the blast apex fast (eased-out). */
  32% { transform: translate3d(var(--bx, 0), var(--by, 0), 0); }
  /* Gravity takes over: continue the horizontal drift a little further while
     falling --fall px past the apex (the rain-down). */
  100% {
    transform: translate3d(calc(var(--bx, 0) * 1.25), calc(var(--by, 0) + var(--fall, 160px)), 0);
  }
}
/* Flutter — the paper-in-air horizontal wobble (inner element). alternate +
   phase desync (per-piece delay) so the cloud shimmers, not sways as a block. */
@keyframes syna-confetti-sway {
  from { transform: translateX(var(--sway-neg, -20px)); }
  to { transform: translateX(var(--sway, 20px)); }
}
/* Sprite-sheet twirl — steps() walk across the 8-frame confetti sheet's
   mask-position, so the flip is crisp frame-by-frame like an engine flipbook,
   not a smoothly-interpolated rotate. */
@keyframes syna-confetti-twirl {
  from { -webkit-mask-position: var(--frame-start, 0%) 0; mask-position: var(--frame-start, 0%) 0; }
  to { -webkit-mask-position: var(--frame-end, -100%) 0; mask-position: var(--frame-end, -100%) 0; }
}
/* Fade — hold full opacity through the blast, fade during the fall. */
@keyframes syna-confetti-fade {
  0%, 55% { opacity: 1; }
  100% { opacity: 0; }
}

/* Toggle — a themed on/off switch whose knob squash-and-stretches as it crosses
   the track. React drives the squash via motion v12 keyframes; the vanilla
   runtime toggles data-checked and this CSS runs the squash keyframe + slides
   the knob. The knob is a glossy 3D accent orb (radial sphere + top specular +
   inner shadow); the track fills accent when ON, neutral when OFF. Geometry is
   PARAMETRIC: the knob diameter and travel derive from the track box, so the
   knob always conforms exactly to the capsule — resize by overriding
   --fx-toggle-width/height/pad and everything follows. Capsule identity:
   knob = inner height ⇒ travel = inner width − knob = width − height. */
.syna-fx-toggle {
  --fx-toggle-accent: var(--accent-cyan, #4de8ff);
  --fx-toggle-width: 60px;
  --fx-toggle-height: 28px;
  --fx-toggle-pad: 3px;    /* gap between knob and capsule wall */
  --fx-toggle-border: 1px;
  /* 28 − 2×(3+1) = 20px knob; 60 − 28 = 32px travel (defaults). */
  --fx-toggle-knob: calc(var(--fx-toggle-height) - 2 * (var(--fx-toggle-pad) + var(--fx-toggle-border)));
  --fx-toggle-travel: calc(var(--fx-toggle-width) - var(--fx-toggle-height));
  display: inline-flex;
  align-items: center;
  gap: 0.75rem;
  cursor: pointer;
  user-select: none;
  /* Reset so a bare <button data-motion="toggle"> has no UA chrome. */
  appearance: none;
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: inherit;
}
.syna-fx-toggle[data-disabled] { cursor: not-allowed; opacity: 0.5; }
.syna-fx-toggle:focus-visible { outline: none; }
.syna-fx-toggle:focus-visible .syna-fx-toggle-track {
  outline: 2px solid color-mix(in srgb, var(--fx-toggle-accent) 70%, transparent);
  outline-offset: 2px;
}
.syna-fx-toggle-track {
  position: relative;
  display: inline-flex;
  align-items: center;
  /* border-box + flex:none: the capsule must hold its exact size regardless of
     the host page's box-sizing reset or a flexing/wrapping sibling label —
     a shrunken track with fixed travel is how the knob escapes the capsule. */
  box-sizing: border-box;
  flex: none;
  width: var(--fx-toggle-width, 60px);
  height: var(--fx-toggle-height, 28px);
  padding: var(--fx-toggle-pad, 3px);
  border-radius: 999px;
  /* OFF: recessed neutral slot. */
  background: var(--bg-tertiary, #33334a);
  border: var(--fx-toggle-border, 1px) solid var(--glass-border, rgba(255, 255, 255, 0.12));
  box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.45);
  transition:
    background var(--motion-normal, 300ms) var(--ease-standard, ease),
    border-color var(--motion-normal, 300ms) var(--ease-standard, ease),
    box-shadow var(--motion-normal, 300ms) var(--ease-standard, ease);
}
/* ON: the track lights to the accent, with an inner accent glow. */
.syna-fx-toggle[data-checked='true'] .syna-fx-toggle-track {
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--fx-toggle-accent) 92%, #fff 8%),
    var(--fx-toggle-accent));
  border-color: color-mix(in srgb, var(--fx-toggle-accent) 60%, transparent);
  box-shadow:
    inset 0 1px 3px rgba(0, 0, 0, 0.3),
    0 0 12px color-mix(in srgb, var(--fx-toggle-accent) 45%, transparent);
}
/* The glossy 3D knob orb — a shaded sphere: a soft top-lit radial base, a bright
   specular cap (::before), and an inner bottom shadow for roundness. */
.syna-fx-toggle-knob {
  position: relative;
  display: block;
  box-sizing: border-box;
  flex: none;
  width: var(--fx-toggle-knob, 20px);
  height: var(--fx-toggle-knob, 20px);
  border-radius: 50%;
  transform-origin: center;
  /* Sphere shading: highlight upper-left, falloff to a darker lower edge. */
  background:
    radial-gradient(circle at 35% 28%,
      #ffffff 0%,
      #eef2f6 26%,
      #c3ccd6 62%,
      #9aa6b4 100%);
  box-shadow:
    0 2px 4px rgba(0, 0, 0, 0.45),          /* cast shadow on the track */
    inset 0 -2px 3px rgba(0, 0, 0, 0.28),   /* bottom inner shadow (roundness) */
    inset 0 2px 2px rgba(255, 255, 255, 0.9); /* top inner rim light */
}
/* Specular cap — a small bright glint pinned to the upper-left of the orb. */
.syna-fx-toggle-knob::before {
  content: '';
  position: absolute;
  top: 12%;
  left: 20%;
  width: 42%;
  height: 32%;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(255, 255, 255, 0.95) 0%, transparent 70%);
  pointer-events: none;
}
/* ON: the orb picks up the accent — a colored sphere with the same shading and
   a soft accent bloom so it reads as a lit bead, not a flat dot. */
.syna-fx-toggle[data-checked='true'] .syna-fx-toggle-knob {
  background:
    radial-gradient(circle at 35% 28%,
      color-mix(in srgb, var(--fx-toggle-accent) 8%, #fff) 0%,
      color-mix(in srgb, var(--fx-toggle-accent) 55%, #fff) 40%,
      var(--fx-toggle-accent) 78%,
      color-mix(in srgb, var(--fx-toggle-accent) 70%, #000 30%) 100%);
  box-shadow:
    0 2px 5px rgba(0, 0, 0, 0.4),
    0 0 10px color-mix(in srgb, var(--fx-toggle-accent) 65%, transparent),
    inset 0 -2px 3px color-mix(in srgb, var(--fx-toggle-accent) 60%, #000 40%),
    inset 0 2px 2px rgba(255, 255, 255, 0.85);
}
/* Vanilla-surface knob position + squash. React drives these via motion; for the
   Core runtime we key off data-checked and run the squash keyframe on flip. */
.syna-fx-toggle-track .syna-fx-toggle-knob { transform: translateX(0); }
.syna-fx-toggle[data-checked='true'] .syna-fx-toggle-knob {
  transform: translateX(var(--fx-toggle-travel, 32px));
}
/* Only the vanilla runtime tags the element with data-fx-animate; that scopes
   the squash keyframe to it so React's motion-driven squash never double-runs. */
.syna-fx-toggle[data-fx-animate] .syna-fx-toggle-knob {
  animation: syna-toggle-squash 0.32s var(--ease-standard, ease);
}
@keyframes syna-toggle-squash {
  0% { scale: 1 1; }
  45% { scale: 1.5 0.5; }   /* thin along travel at the apex */
  80% { scale: 1 1; }
  100% { scale: 1 1; }
}
.syna-fx-toggle-input {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
.syna-fx-toggle-label {
  font-weight: 500;
  color: var(--text-secondary, #b4b4c8);
}

/* ── Lighting: soft/hard shadows, glows, bloom ─────────────────────────────*/
/* A standalone lighting utility — put `syna-fx-light` on any surface and select
   the treatment with data-fx-lighting="soft-shadow|hard-shadow|glow|bloom".
   This is the SSOT for the FX_LIGHTING_PRESETS vocabulary; shimmer reuses the
   same --fx-light-shadow / --fx-light-filter tokens. Kept as box-shadow (not a
   pseudo) so it composes with any effect and honors border-radius for free. */
.syna-fx-light {
  --fx-light-shadow: none;
  --fx-light-filter: none;
  box-shadow: var(--fx-light-shadow);
  filter: var(--fx-light-filter);
  transition:
    box-shadow var(--motion-normal, 300ms) var(--ease-standard, ease),
    filter var(--motion-normal, 300ms) var(--ease-standard, ease);
}
/* Soft shadow — a wide, diffuse elevation shadow (ambient occlusion feel). */
.syna-fx-light[data-fx-lighting='soft-shadow'] {
  --fx-light-shadow: 0 14px 34px rgba(0, 0, 0, 0.32), 0 4px 10px rgba(0, 0, 0, 0.22);
}
/* Hard shadow — a crisp, offset, zero-blur shadow for graphic contrast. */
.syna-fx-light[data-fx-lighting='hard-shadow'] {
  --fx-light-shadow: 7px 7px 0 rgba(0, 0, 0, 0.46);
}
/* Glow — a tight accent-colored outer light (a rim + a small halo). */
.syna-fx-light[data-fx-lighting='glow'] {
  --fx-light-shadow:
    0 0 0 1px color-mix(in srgb, var(--accent-cyan, #4de8ff) 36%, transparent),
    0 0 22px color-mix(in srgb, var(--accent-cyan, #4de8ff) 48%, transparent);
}
/* Bloom — a wide, saturated, multi-color light spill for showcase moments. */
.syna-fx-light[data-fx-lighting='bloom'] {
  --fx-light-shadow:
    0 0 18px color-mix(in srgb, var(--accent-cyan, #4de8ff) 54%, transparent),
    0 0 56px color-mix(in srgb, var(--accent-pink, #fb7185) 42%, transparent),
    0 18px 42px rgba(0, 0, 0, 0.34);
  --fx-light-filter: saturate(1.18);
}

/* ── Tier 3: Text ──────────────────────────────────────────────────────────*/

/* Scramble — runtime swaps glyphs; CSS just sets the monospace cadence. */
.syna-fx-scramble { --fx-scramble-ms: 1200; font-variant-ligatures: none; }

/* Gradient text — flowing multi-color fill. Doubled class = specificity 0,2,0
   so a consumer's own `background:` shorthand (which would reset background-
   clip and leave the transparent text-fill with nothing behind it → invisible
   text) can't win the cascade. background-clip:text NEEDS the element
   background, so the pseudo-layer trick used by shimmer can't apply. */
.syna-fx-gradient-text.syna-fx-gradient-text {
  --fx-gradient-secs: 4;
  background: linear-gradient(90deg,
    var(--accent-cyan, #4de8ff), var(--accent-purple, #a855f7),
    var(--accent-pink, #fb7185), var(--accent-cyan, #4de8ff));
  background-size: 200% auto;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
  animation: syna-gradient-flow calc(var(--fx-gradient-secs, 4) * 1s) linear infinite;
}
@keyframes syna-gradient-flow {
  to { background-position: 200% center; }
}

/* Shimmer — horizontal light sweep. The sweep paints on ::after, NOT the
   element background — a consumer's own `background:` shorthand later in the
   cascade would reset background-image and silently kill it. */
.syna-fx-shimmer {
  --fx-shimmer-secs: 3;
  --fx-blend-mode: screen;
  --fx-shimmer-opacity: 0.95;
  --fx-shimmer-core: color-mix(in srgb, #fff 78%, transparent);
  --fx-shimmer-edge: color-mix(in srgb, var(--accent-cyan, #4de8ff) 42%, transparent);
  --fx-light-shadow: none;
  --fx-light-filter: none;
  position: relative;
  overflow: hidden;
  box-shadow: var(--fx-light-shadow);
  filter: var(--fx-light-filter);
}
.syna-fx-shimmer::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  z-index: 2;
  opacity: var(--fx-shimmer-opacity);
  mix-blend-mode: var(--fx-blend-mode, screen);
  background-image: linear-gradient(100deg,
    transparent 18%,
    var(--fx-shimmer-edge) 38%,
    var(--fx-shimmer-core) 50%,
    var(--fx-shimmer-edge) 62%,
    transparent 82%);
  background-size: 240% 100%;
  background-repeat: no-repeat;
  animation: syna-shimmer calc(var(--fx-shimmer-secs, 3) * 1s) linear infinite;
}
.syna-fx-shimmer[data-fx-blend='additive'] {
  --fx-blend-mode: screen;
  --fx-shimmer-opacity: 0.95;
  --fx-shimmer-core: color-mix(in srgb, #fff 82%, transparent);
  --fx-shimmer-edge: color-mix(in srgb, var(--accent-cyan, #4de8ff) 48%, transparent);
}
.syna-fx-shimmer[data-fx-blend='multiply'] {
  --fx-blend-mode: multiply;
  --fx-shimmer-opacity: 0.86;
  --fx-shimmer-core: color-mix(in srgb, #11061f 54%, transparent);
  --fx-shimmer-edge: color-mix(in srgb, var(--accent-purple, #a855f7) 32%, transparent);
}
.syna-fx-shimmer[data-fx-blend='color-dodge'] {
  --fx-blend-mode: color-dodge;
  --fx-shimmer-opacity: 1;
  --fx-shimmer-core: color-mix(in srgb, #fff 92%, transparent);
  --fx-shimmer-edge: color-mix(in srgb, var(--accent-pink, #fb7185) 58%, transparent);
}
.syna-fx-shimmer[data-fx-blend='tint'] {
  --fx-blend-mode: normal;
  --fx-shimmer-opacity: 0.72;
  --fx-shimmer-core: color-mix(in srgb, var(--accent-cyan, #4de8ff) 38%, transparent);
  --fx-shimmer-edge: color-mix(in srgb, var(--accent-purple, #a855f7) 26%, transparent);
}
.syna-fx-shimmer[data-fx-lighting='soft-shadow'] {
  --fx-light-shadow: 0 14px 34px rgba(0, 0, 0, 0.32);
}
.syna-fx-shimmer[data-fx-lighting='hard-shadow'] {
  --fx-light-shadow: 7px 7px 0 rgba(0, 0, 0, 0.46);
}
.syna-fx-shimmer[data-fx-lighting='glow'] {
  --fx-light-shadow:
    0 0 0 1px color-mix(in srgb, var(--accent-cyan, #4de8ff) 36%, transparent),
    0 0 22px color-mix(in srgb, var(--accent-cyan, #4de8ff) 46%, transparent);
}
.syna-fx-shimmer[data-fx-lighting='bloom'] {
  --fx-light-shadow:
    0 0 18px color-mix(in srgb, var(--accent-cyan, #4de8ff) 54%, transparent),
    0 0 56px color-mix(in srgb, var(--accent-pink, #fb7185) 42%, transparent),
    0 18px 42px rgba(0, 0, 0, 0.34);
  --fx-light-filter: saturate(1.18);
}
@keyframes syna-shimmer {
  0% { background-position: -150% 0; }
  100% { background-position: 250% 0; }
}

/* ── Tier 4: Scroll cinematics ─────────────────────────────────────────────*/

/* Reveal-up / reveal-scale — initial hidden state; runtime adds .is-revealed. */
.syna-fx-reveal-up,
.syna-fx-reveal-scale {
  opacity: 0;
  transition:
    opacity var(--motion-slow, 400ms) var(--ease-decelerate, ease),
    transform var(--motion-slow, 400ms) var(--ease-decelerate, ease);
  transition-delay: calc(var(--fx-i, 0) * var(--fx-stagger, 80ms));
}
.syna-fx-reveal-up { transform: translateY(24px); }
.syna-fx-reveal-scale { transform: scale(0.92); }
.syna-fx-reveal-up.is-revealed,
.syna-fx-reveal-scale.is-revealed {
  opacity: 1;
  transform: none;
}

/* count-up — purely runtime-driven; class is a marker for the scanner. */
.syna-fx-count-up { font-variant-numeric: tabular-nums; }

/* ── Protocol: No Snapping appear / layout ─────────────────────────────────*/

.syna-fx-fade-in,
.syna-fx-fade-out,
.syna-fx-slide-in-up,
.syna-fx-slide-in-down,
.syna-fx-slide-in-left,
.syna-fx-slide-in-right,
.syna-fx-zoom-bounce {
  opacity: 0;
  transition:
    opacity var(--motion-normal, 300ms) var(--ease-decelerate, ease),
    transform var(--motion-normal, 300ms) var(--ease-emphasized, cubic-bezier(0.34, 1.56, 0.64, 1));
  transition-delay: calc(var(--fx-i, 0) * var(--fx-stagger, 80ms));
}
.syna-fx-fade-in { transform: none; }
.syna-fx-fade-out { opacity: 1; }
.syna-fx-fade-out.is-revealed { opacity: 0; }
.syna-fx-slide-in-up { transform: translateY(24px); }
.syna-fx-slide-in-down { transform: translateY(-24px); }
.syna-fx-slide-in-left { transform: translateX(-28px); }
.syna-fx-slide-in-right { transform: translateX(28px); }
.syna-fx-zoom-bounce {
  transform: scale(0.01);
  animation: syna-zoom-bounce var(--motion-slow, 400ms) var(--ease-emphasized, cubic-bezier(0.34, 1.56, 0.64, 1)) both;
  animation-delay: calc(var(--fx-i, 0) * var(--fx-stagger, 80ms));
}
@keyframes syna-zoom-bounce {
  0% { opacity: 0; transform: scale(0.01); }
  60% { opacity: 1; transform: scale(1.06); }
  100% { opacity: 1; transform: scale(1); }
}
.syna-fx-fade-in.is-revealed,
.syna-fx-slide-in-up.is-revealed,
.syna-fx-slide-in-down.is-revealed,
.syna-fx-slide-in-left.is-revealed,
.syna-fx-slide-in-right.is-revealed {
  opacity: 1;
  transform: none;
}
.syna-fx-zoom-bounce.is-revealed {
  opacity: 1;
  transform: none;
  animation: none;
}

.syna-fx-stable-field {
  display: inline-block;
  font-variant-numeric: tabular-nums;
  min-width: calc(var(--fx-stable-ch, 4) * 1ch);
  text-align: inherit;
  white-space: nowrap;
}

.syna-fx-fade-ellipsis {
  --fx-ellipsis-lines: 1;
  display: block;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  mask-image: linear-gradient(to right, #000 85%, transparent);
  -webkit-mask-image: linear-gradient(to right, #000 85%, transparent);
}

.syna-fx-shrink-fit {
  display: block;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  mask-image: linear-gradient(to right, #000 85%, transparent);
  -webkit-mask-image: linear-gradient(to right, #000 85%, transparent);
  transition: font-size var(--motion-fast, 150ms) var(--ease-standard, cubic-bezier(0.2, 0, 0, 1));
}
.syna-fx-fade-ellipsis[data-fx-lines]:not([data-fx-lines='1']) {
  white-space: normal;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: var(--fx-ellipsis-lines, 2);
  line-clamp: var(--fx-ellipsis-lines, 2);
  mask-image: linear-gradient(to bottom, #000 70%, transparent);
  -webkit-mask-image: linear-gradient(to bottom, #000 70%, transparent);
}

.syna-fx-digit-slide,
.syna-fx-digit-flip {
  display: inline-flex;
  font-variant-numeric: tabular-nums;
  overflow: hidden;
  vertical-align: baseline;
}
.syna-fx-digit-slide .syna-digit,
.syna-fx-digit-flip .syna-digit {
  display: inline-block;
  width: 1ch;
  text-align: center;
  transition: transform var(--motion-normal, 300ms) var(--ease-emphasized, cubic-bezier(0.34, 1.56, 0.64, 1));
}
@keyframes syna-digit-flip {
  0% { transform: rotateX(90deg); opacity: 0; }
  100% { transform: rotateX(0deg); opacity: 1; }
}
@keyframes syna-digit-slide {
  0% { transform: translateY(0.6em); opacity: 0; }
  100% { transform: translateY(0); opacity: 1; }
}

/* ── Tier: Wispa (glass control motion) ────────────────────────────────────*/

@property --angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}
@property --lightColor {
  syntax: "<color>";
  inherits: false;
  initial-value: #ffffff4d;
}

.syna-fx-press-scale {
  transition: transform 0.2s ease;
  display: inline-block;
}
.syna-fx-press-scale:hover { transform: scale(1.05); }
.syna-fx-press-scale:active { transform: scale(0.95); }

.syna-fx-squash-pulse.is-squashing,
.syna-fx-squash-pulse[data-squashing] {
  animation: syna-squash-pulse 0.25s ease 1 forwards;
}
@keyframes syna-squash-pulse {
  0% { scale: 1; }
  40% { scale: 1.03; }
  70% { scale: 0.97; }
  100% { scale: 1; }
}

.syna-fx-conic-border {
  position: relative;
  --angle: 0deg;
  --lightColor: #ffffff4d;
}
.syna-fx-conic-border::after {
  content: "";
  display: block;
  position: absolute;
  left: 50%;
  top: 50%;
  width: calc(100% + 20px);
  height: calc(100% + 20px);
  transform: translate(-50%, -50%);
  border-radius: inherit;
  box-sizing: initial;
  padding: 2px;
  background: conic-gradient(
    from var(--angle),
    #ffffff0d 0% 30%,
    var(--lightColor) 40%,
    #ffffff0d 50% 75%,
    var(--lightColor) 85%,
    #ffffff0d 95%
  );
  -webkit-mask-image: linear-gradient(red, red), linear-gradient(red, red);
  mask-image: linear-gradient(red, red), linear-gradient(red, red);
  -webkit-mask-clip: padding-box, content-box;
  mask-clip: padding-box, content-box;
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  opacity: 0;
  transition: --angle 0.3s ease, opacity 0.3s ease;
  pointer-events: none;
}
.syna-fx-conic-border:hover::after,
.syna-fx-conic-border[data-fx-state="hover"]::after {
  opacity: 1;
  --angle: 100deg;
}
.syna-fx-conic-border[data-fx-state="spin"]::after {
  opacity: 1;
  animation: syna-hover-rotate 2s linear infinite;
}
@keyframes syna-hover-rotate {
  0% { --angle: 0deg; --lightColor: #ffffff4d; }
  50% { --lightColor: #fff9; }
  100% { --angle: 360deg; --lightColor: #ffffff4d; }
}

.syna-fx-shimmer-bar {
  position: relative;
  width: 100%;
  height: 10px;
  border-radius: 999px;
  overflow: hidden;
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.08);
}
.syna-fx-shimmer-bar::before {
  content: "";
  display: block;
  height: 100%;
  width: var(--fx-shimmer-p, 35%);
  border-radius: inherit;
  background: linear-gradient(90deg, var(--wispa-glow-a, #ca6650), var(--wispa-glow-b, #b98acf));
  transition: width 0.35s ease;
  position: relative;
  overflow: hidden;
}
.syna-fx-shimmer-bar::after {
  content: "";
  position: absolute;
  inset: 0;
  width: var(--fx-shimmer-p, 35%);
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.55), transparent);
  transform: translateX(-100%);
  animation: syna-shimmer-sweep 1.4s ease-in-out infinite;
  pointer-events: none;
}
@keyframes syna-shimmer-sweep {
  to { transform: translateX(100%); }
}

.syna-fx-hover-label-swap {
  position: relative;
  display: inline-block;
  min-width: 9rem;
  min-height: 1.35rem;
}
.syna-fx-hover-label-swap > [data-idle],
.syna-fx-hover-label-swap > [data-hover] {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  white-space: nowrap;
  transition: opacity 0.3s;
}
.syna-fx-hover-label-swap > [data-idle] { opacity: 1; }
.syna-fx-hover-label-swap > [data-hover] { opacity: 0; }
.syna-fx-hover-label-swap:hover > [data-idle],
.syna-fx-hover-label-swap:focus-within > [data-idle] { opacity: 0; }
.syna-fx-hover-label-swap:hover > [data-hover],
.syna-fx-hover-label-swap:focus-within > [data-hover] { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  :root:not([data-syna-force-motion]) .syna-fx-squash-pulse.is-squashing,
  :root:not([data-syna-force-motion]) .syna-fx-squash-pulse[data-squashing] {
    animation: none !important;
  }
  :root:not([data-syna-force-motion]) .syna-fx-conic-border[data-fx-state="spin"]::after {
    animation: none !important;
    --angle: 100deg;
  }
  :root:not([data-syna-force-motion]) .syna-fx-shimmer-bar::after {
    animation: none !important;
  }
}

/* ── Accessibility: honor reduced-motion across the whole suite ────────────*/
/* The :not([data-syna-force-motion]) guard lets a motion SHOWCASE opt back in
 * via setForceMotion(true) — see tokens/motion-tokens.ts. Production apps leave
 * force-motion off and this fully honors the user's OS preference. */
@media (prefers-reduced-motion: reduce) {
  :root:not([data-syna-force-motion]) :is([class*='syna-fx-'], [class*='syna-fx-'] *) {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
  :root:not([data-syna-force-motion]) :is(
    .syna-fx-reveal-up, .syna-fx-reveal-scale,
    .syna-fx-fade-in, .syna-fx-fade-out,
    .syna-fx-slide-in-up, .syna-fx-slide-in-down, .syna-fx-slide-in-left, .syna-fx-slide-in-right,
    .syna-fx-zoom-bounce
  ) {
    opacity: 1 !important;
    transform: none !important;
    animation: none !important;
  }
  :root:not([data-syna-force-motion]) :is(.syna-fx-tilt-3d, .syna-fx-magnetic) {
    transform: none !important;
  }
}
