/* ============================================================
   The Codero — FX engine (effects.css)
   100+ distinct effects & animations. Numbered E001–E082 here;
   E101–E126 are interactive effects in effects.js.
   All are transform/opacity-based, mobile-safe, and fully
   disabled under prefers-reduced-motion.
   ============================================================ */

@property --ang { syntax: "<angle>"; initial-value: 0deg; inherits: false; }

:root {
  --fx-accent: 124, 92, 255;
  --fx-accent2: 0, 212, 255;
  --fx-speed: 1;
}

/* ============================================================
   A. AMBIENT BACKGROUND LAYERS
   ============================================================ */

/* E001 — Aurora: drifting light fields (transform-only: GPU composited,
   zero repaints — the old background-position+filter version caused lag) */
/* inset was -35%, which made this the largest always-on composited layer on
   the site: 170% x 170% of the viewport, i.e. 2.89x its area, held against it
   permanently. The drift animation only travels 4vw/4vh and scales to 1.08, so
   it needs 8% of headroom, not 35%. At -12% the layer is 1.54x the viewport —
   47% less texture and fill — and the gradient anchors below are recomputed so
   every light field sits at exactly the same absolute position as before. The
   gradient SIZES are already in vw/vh, so they are unaffected by the box. */
.fx-aurora {
  position: fixed; inset: -12%; z-index: -3; pointer-events: none;
  background:
    radial-gradient(38vw 44vh at 17.10% 22.58%, rgba(var(--fx-accent), 0.20), transparent 65%),
    radial-gradient(34vw 40vh at 82.90% 8.87%, rgba(var(--fx-accent2), 0.16), transparent 65%),
    radial-gradient(42vw 46vh at 60.97% 85.65%, rgba(255, 94, 190, 0.10), transparent 65%),
    radial-gradient(30vw 36vh at 3.39% 77.42%, rgba(80, 255, 200, 0.08), transparent 65%);
  animation: fx-aurora-drift 30s ease-in-out infinite alternate;
  will-change: transform;
}
@keyframes fx-aurora-drift {
  0%   { transform: translate3d(0, 0, 0) scale(1); }
  50%  { transform: translate3d(4vw, -3vh, 0) scale(1.08); }
  100% { transform: translate3d(-4vw, 4vh, 0) scale(1.04); }
}

/* E002/E003/E004 — Floating gradient orbs (three sizes, three paths) */
.fx-orb { position: fixed; border-radius: 50%; z-index: -2; pointer-events: none; will-change: transform; }
.fx-orb-1 {
  width: 46vw; height: 46vw; top: -12vw; left: -10vw;
  background: radial-gradient(circle at 35% 35%, rgba(var(--fx-accent), 0.22), transparent 62%);
  animation: fx-orb-a 30s ease-in-out infinite alternate;
}
.fx-orb-2 {
  width: 34vw; height: 34vw; bottom: -8vw; right: -6vw;
  background: radial-gradient(circle at 40% 40%, rgba(var(--fx-accent2), 0.16), transparent 62%);
  animation: fx-orb-b 24s ease-in-out infinite alternate;
}
.fx-orb-3 {
  width: 22vw; height: 22vw; top: 42%; left: 58%;
  background: radial-gradient(circle at 45% 45%, rgba(255, 94, 190, 0.12), transparent 60%);
  animation: fx-orb-c 36s ease-in-out infinite alternate;
}
@keyframes fx-orb-a { 0% { transform: translate3d(0, 0, 0) scale(1); } 100% { transform: translate3d(9vw, 7vh, 0) scale(1.18); } }
@keyframes fx-orb-b { 0% { transform: translate3d(0, 0, 0) scale(1.1); } 100% { transform: translate3d(-8vw, -9vh, 0) scale(0.92); } }
@keyframes fx-orb-c { 0% { transform: translate3d(0, 0, 0); } 50% { transform: translate3d(-12vw, 6vh, 0); } 100% { transform: translate3d(6vw, -8vh, 0); } }

/* E005 — Starfield with individual twinkle */
.fx-stars { position: fixed; inset: 0; z-index: -2; pointer-events: none; overflow: hidden; }
.fx-stars span {
  position: absolute; width: 2px; height: 2px; border-radius: 50%;
  background: #fff; opacity: 0.4;
  animation: fx-twinkle var(--tw, 3s) ease-in-out var(--d, 0s) infinite;
}
@keyframes fx-twinkle {
  0%, 100% { opacity: 0.15; transform: scale(0.7); }
  50% { opacity: 0.9; transform: scale(1.25); }
}

/* E006 — Tech grid with a traveling sweep (transform-only) */
.fx-gridlines {
  position: fixed; inset: 0; z-index: -2; pointer-events: none; overflow: hidden;
  -webkit-mask-image: radial-gradient(720px 460px at 50% 0%, #000 25%, transparent 78%);
          mask-image: radial-gradient(720px 460px at 50% 0%, #000 25%, transparent 78%);
}
.fx-gridlines::before {
  content: ""; position: absolute; inset: -100px;
  background-image:
    linear-gradient(rgba(255, 255, 255, 0.045) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255, 255, 255, 0.045) 1px, transparent 1px);
  background-size: 46px 46px;
  animation: fx-grid-sweep 14s linear infinite;
  will-change: transform;
}
@keyframes fx-grid-sweep {
  0% { transform: translate3d(0, 0, 0); }
  100% { transform: translate3d(46px, 92px, 0); }
}

/* E007 — Scanlines overlay (transform drift) */
.fx-scan { position: fixed; inset: 0; z-index: 40; pointer-events: none; opacity: 0.5; overflow: hidden; }
.fx-scan::before {
  content: ""; position: absolute; inset: -100px;
  background: repeating-linear-gradient(0deg, rgba(255, 255, 255, 0.022) 0 1px, transparent 1px 4px);
  animation: fx-scan-drift 9s linear infinite;
  will-change: transform;
}
@keyframes fx-scan-drift { 0% { transform: translate3d(0, 0, 0); } 100% { transform: translate3d(0, 84px, 0); } }

/* E008 — Film grain (transform jiggle, no repaints) */
.fx-noise { position: fixed; inset: 0; z-index: 39; pointer-events: none; opacity: 0.05; overflow: hidden; }
.fx-noise::before {
  /* the jiggle travels at most 52px, so 60px of bleed is ample (was 120px
     for no reason — and it is a repeating texture, so there is no anchoring
     to preserve) */
  content: ""; position: absolute; inset: -60px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='0.6'/%3E%3C/svg%3E");
  animation: fx-noise-jiggle 0.9s steps(4) infinite;
  will-change: transform;
}
@keyframes fx-noise-jiggle {
  0% { transform: translate3d(0, 0, 0); }
  25% { transform: translate3d(-34px, 46px, 0); }
  50% { transform: translate3d(52px, -28px, 0); }
  75% { transform: translate3d(-24px, -52px, 0); }
  100% { transform: translate3d(0, 0, 0); }
}

/* E009 — Cursor glow that follows the pointer */
.fx-glow {
  position: fixed; z-index: -1; pointer-events: none;
  width: 520px; height: 520px; border-radius: 50%;
  background: radial-gradient(circle, rgba(var(--fx-accent), 0.10), transparent 62%);
  transform: translate(-50%, -50%);
}

/* ============================================================
   B. TYPOGRAPHY EFFECTS
   ============================================================ */

/* E010 — Shimmering gradient text */
h1 .grad-text, .error-code, .marquee-track span b {
  background: var(--grad);
  -webkit-background-clip: text; background-clip: text; color: transparent;
  background-size: 220% 100%;
  animation: fx-text-shimmer 6s linear infinite;
}
@keyframes fx-text-shimmer { 0% { background-position: 0% 50%; } 100% { background-position: 220% 50%; } }

/* E011 — Soft glow breathing on the main heading */
.hero-title { animation: fx-glow-breathe 5s ease-in-out infinite; }
@keyframes fx-glow-breathe {
  0%, 100% { text-shadow: 0 0 0 rgba(var(--fx-accent), 0); }
  50% { text-shadow: 0 0 34px rgba(var(--fx-accent), 0.35); }
}

/* E012 — Neon flicker on the brand wordmark */
.brand-name { animation: fx-neon 7s linear infinite; }
@keyframes fx-neon {
  0%, 91%, 94%, 100% { opacity: 1; }
  92% { opacity: 0.55; }
  93% { opacity: 0.85; }
  96% { opacity: 0.7; }
}

/* E013 — Rainbow hue cycle on the logo mark */
.brand-mark { animation: fx-rainbow 9s linear infinite; }
@keyframes fx-rainbow {
  0% { filter: hue-rotate(0deg); }
  100% { filter: hue-rotate(360deg); }
}

/* E014 — Jello wobble on brand hover */
.brand:hover .brand-mark { animation: fx-jello 0.9s ease both; }
@keyframes fx-jello {
  0% { transform: scale(1); }
  25% { transform: scale(1.15, 0.85); }
  40% { transform: scale(0.9, 1.1); }
  60% { transform: scale(1.06, 0.94); }
  80% { transform: scale(0.98, 1.02); }
  100% { transform: scale(1); }
}

/* E015 — Letter-drop entrance for hero / 404 headings (JS splits letters) */
.fx-letter { display: inline-block; animation: fx-letter-drop 0.6s cubic-bezier(0.2, 0.7, 0.3, 1.35) both; animation-delay: calc(var(--i, 0) * 26ms); }
.grad-text.fx-letter { background: var(--grad); -webkit-background-clip: text; background-clip: text; }
@keyframes fx-letter-drop {
  0% { opacity: 0; transform: translateY(-0.7em) rotate(-6deg) scale(0.8); filter: blur(4px); }
  100% { opacity: 1; transform: none; filter: blur(0); }
}

/* E016 — Typewriter caret */
.fx-typing::after { content: "▌"; margin-left: 2px; color: var(--accent-2); animation: fx-caret 0.9s steps(1) infinite; }
@keyframes fx-caret { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }

/* E018 — Glitch layers (404 code; JS sets data-text) */
.fx-glitch { position: relative; }
.fx-glitch::before, .fx-glitch::after {
  content: attr(data-text); position: absolute; inset: 0;
  background: var(--bg); overflow: hidden; pointer-events: none;
}
.fx-glitch::before { color: #ff5ea6; animation: fx-glitch-a 2.8s steps(2) infinite; }
.fx-glitch::after { color: var(--accent-2); animation: fx-glitch-b 3.1s steps(2) infinite; }
@keyframes fx-glitch-a {
  0%, 88%, 100% { clip-path: inset(0 0 100% 0); transform: none; opacity: 0; }
  90% { clip-path: inset(12% 0 58% 0); transform: translate(-5px, -2px); opacity: 0.85; }
  93% { clip-path: inset(52% 0 22% 0); transform: translate(4px, 2px); opacity: 0.85; }
  96% { clip-path: inset(30% 0 44% 0); transform: translate(-3px, 1px); opacity: 0.85; }
}
@keyframes fx-glitch-b {
  0%, 86%, 100% { clip-path: inset(0 0 100% 0); transform: none; opacity: 0; }
  89% { clip-path: inset(60% 0 8% 0); transform: translate(5px, 2px); opacity: 0.8; }
  94% { clip-path: inset(8% 0 74% 0); transform: translate(-4px, -1px); opacity: 0.8; }
  97% { clip-path: inset(40% 0 38% 0); transform: translate(3px, -2px); opacity: 0.8; }
}

/* E019 — Eyebrow: calm entrance only. No text mutation, no sweeping
   overlays — the pill keeps normal spacing and legibility. */
.eyebrow { position: relative; animation: fx-fade-up 0.55s ease both; }

/* ============================================================
   C. CARDS
   ============================================================ */

.card { position: relative; }

/* E020 — Card lift + deep shadow */
.card:hover { transform: translateY(-5px); transition: transform 0.25s ease, border-color 0.2s, box-shadow 0.25s; }

/* E021 — Spotlight that follows the cursor inside cards */
.card::after {
  content: ""; position: absolute; inset: 0; border-radius: inherit;
  background: radial-gradient(250px circle at var(--mx, 50%) var(--my, 50%), rgba(var(--fx-accent), 0.16), transparent 65%);
  opacity: 0; transition: opacity 0.3s; pointer-events: none;
}
.card:hover::after { opacity: 1; }

/* E022 — Rotating conic border-runner on hover */
.card::before {
  content: ""; position: absolute; inset: -1px; border-radius: inherit; padding: 1px;
  background: conic-gradient(from var(--ang), transparent 0 68%, rgba(var(--fx-accent), 0.9) 84%, rgba(var(--fx-accent2), 0.9) 100%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor; mask-composite: exclude;
  opacity: 0; transition: opacity 0.35s; pointer-events: none;
  animation: fx-border-spin 3.2s linear infinite;
}
.card:hover::before { opacity: 1; }
@keyframes fx-border-spin { to { --ang: 360deg; } }

/* E023 — Light sweep across cards once on hover */
.project-card::after, .cat-card::after { }
.card .fx-sweep {
  position: absolute; top: 0; bottom: 0; width: 45%; left: -60%;
  background: linear-gradient(105deg, transparent, rgba(255, 255, 255, 0.09), transparent);
  transform: skewX(-18deg); pointer-events: none;
}
.card:hover .fx-sweep { animation: fx-sweep 0.8s ease; }
/* Transform-only sweep (was animating `left`, which forced a full relayout of
   the card every frame). --sweep-d is the travel distance as a % of the sweep
   element's own width: a 45%-wide bar crossing 190% of its parent = 422% of
   itself; the narrower ad-CTA bar needs 475%. Visually identical, but it now
   runs on the compositor. */
@keyframes fx-sweep {
  0%   { transform: translateX(0) skewX(-18deg); }
  100% { transform: translateX(var(--sweep-d, 422%)) skewX(-18deg); }
}

/* E024 — Staggered entrance for grid children (--i set by JS/build) */
.grid > * { animation: fx-fade-up 0.55s ease both; animation-delay: calc(var(--i, 0) * 70ms); }
@keyframes fx-fade-up {
  0% { opacity: 0; transform: translateY(22px) scale(0.985); }
  100% { opacity: 1; transform: none; }
}

/* E025 — Category card 3D flip-in */
.cat-card { animation-name: fx-flip-in; transform-style: preserve-3d; perspective: 600px; }
@keyframes fx-flip-in {
  0% { opacity: 0; transform: rotateY(70deg) translateZ(-40px); }
  100% { opacity: 1; transform: none; }
}

/* E026 — Category icon spin-bounce on card hover */
.cat-card:hover .cat-icon { animation: fx-icon-pop 0.6s cubic-bezier(0.3, 1.6, 0.4, 1) both; }
@keyframes fx-icon-pop {
  0% { transform: rotate(0) scale(1); }
  40% { transform: rotate(-12deg) scale(1.25); }
  70% { transform: rotate(8deg) scale(0.95); }
  100% { transform: rotate(0) scale(1); }
}

/* E027 — Language dot ping */
.meta-lang:hover .lang-dot { animation: fx-ping 1s ease infinite; }
@keyframes fx-ping {
  0% { box-shadow: 0 0 0 0 rgba(var(--fx-accent2), 0.55); }
  100% { box-shadow: 0 0 0 10px rgba(var(--fx-accent2), 0); }
}

/* E028 — NEW badge pulse + shine */
.badge-new { animation: fx-badge-pulse 2s ease-in-out infinite; position: relative; overflow: hidden; }
@keyframes fx-badge-pulse {
  0%, 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(var(--fx-accent), 0.5); }
  50% { transform: scale(1.08); box-shadow: 0 0 0 7px rgba(var(--fx-accent), 0); }
}

/* E029 — Source badge hover grow */
.src-badge { transition: transform 0.2s cubic-bezier(0.3, 1.6, 0.4, 1); }
.src-badge:hover { transform: scale(1.12); }

/* ============================================================
   D. BUTTONS, CHIPS & INTERACTIVE BITS
   ============================================================ */

.btn { position: relative; overflow: hidden; }

/* E030 — Gradient pan on primary buttons */
.btn-primary { background-size: 180% 100%; transition: background-position 0.5s ease, box-shadow 0.25s; }
.btn-primary:hover { background-position: 100% 50%; box-shadow: 0 8px 26px rgba(var(--fx-accent), 0.45); }

/* E031 — Glow breathe on primary idle */
.btn-primary { animation: fx-btn-breathe 3.6s ease-in-out infinite; }
@keyframes fx-btn-breathe {
  0%, 100% { box-shadow: 0 5px 18px rgba(var(--fx-accent), 0.28); }
  50% { box-shadow: 0 8px 30px rgba(var(--fx-accent), 0.5); }
}

/* E032 — Click ripple (JS injects .fx-ripple span) */
.fx-ripple {
  position: absolute; border-radius: 50%; pointer-events: none;
  background: rgba(255, 255, 255, 0.45); transform: scale(0);
  animation: fx-ripple-go 0.65s ease-out forwards;
}
@keyframes fx-ripple-go { to { transform: scale(3.4); opacity: 0; } }

/* E033 — Press squish */
.btn:active { animation: fx-squish 0.3s ease; }
@keyframes fx-squish { 0% { transform: scale(1); } 50% { transform: scale(0.94); } 100% { transform: scale(1); } }

/* E034 — Arrow nudge */
.btn:hover .arrow { animation: fx-arrow-nudge 0.7s ease infinite; }
@keyframes fx-arrow-nudge { 0%, 100% { transform: translateX(0); } 50% { transform: translateX(4px); } }

/* E035 — Icon wiggle on ghost buttons */
.btn-ghost:hover { animation: fx-wiggle 0.5s ease; }
@keyframes fx-wiggle {
  0%, 100% { transform: rotate(0); }
  25% { transform: rotate(-1.6deg); }
  75% { transform: rotate(1.6deg); }
}

/* E036 — Search focus expand + halo */
.search-box { transition: border-color 0.2s, box-shadow 0.25s, transform 0.25s; }
.search-box:focus-within { transform: scale(1.015); box-shadow: 0 0 0 4px rgba(var(--fx-accent), 0.2), 0 10px 30px rgba(var(--fx-accent), 0.15); border-color: var(--accent); }

/* E037 — Chip pop when activated */
.chip-filter.active { animation: fx-pop 0.35s cubic-bezier(0.3, 1.8, 0.5, 1) both; }
@keyframes fx-pop { 0% { transform: scale(0.85); } 100% { transform: scale(1); } }

/* E038 — Chip icon bounce on hover */
.chip-filter:hover { animation: fx-chip-bounce 0.5s ease; }
@keyframes fx-chip-bounce {
  0%, 100% { transform: translateY(0); }
  40% { transform: translateY(-3px); }
}

/* E039 — Select focus ring expand */
select:focus, input:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(var(--fx-accent), 0.25); transition: box-shadow 0.2s; }

/* ============================================================
   E. HEADER & NAVIGATION
   ============================================================ */

/* E040 — Header shrink on scroll (JS toggles .fx-scrolled) */
.site-header { transition: box-shadow 0.3s, background 0.3s; }
.nav-wrap { transition: height 0.3s ease; }
.site-header.fx-scrolled { box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35); }
.site-header.fx-scrolled .nav-wrap { height: 54px; }

/* E041 — Nav link sliding underline */
.site-nav a { position: relative; }
.site-nav a::after {
  content: ""; position: absolute; left: 50%; right: 50%; bottom: 4px; height: 2px;
  background: var(--grad); border-radius: 2px; transition: left 0.25s, right 0.25s;
}
.site-nav a:hover::after { left: 20%; right: 20%; }

/* E042 — Active nav pill sheen */
.site-nav a.active { position: relative; overflow: hidden; }
.site-nav a.active::before {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(110deg, transparent 20%, rgba(var(--fx-accent2), 0.18) 50%, transparent 80%);
  animation: fx-pill-sheen 4.5s ease-in-out infinite;
}

/* E043 — Theme toggle spin on click (JS adds .fx-spin) */
.fx-spin { animation: fx-spin-once 0.55s cubic-bezier(0.3, 1.4, 0.4, 1) both; }
@keyframes fx-spin-once { 0% { transform: rotate(0) scale(1); } 60% { transform: rotate(200deg) scale(1.18); } 100% { transform: rotate(360deg) scale(1); } }

/* E044 — Icon button hover ring */
.icon-btn { position: relative; transition: transform 0.2s, border-color 0.2s; }
.icon-btn:hover { transform: translateY(-2px); }
.icon-btn::after {
  content: ""; position: absolute; inset: -4px; border-radius: 14px;
  border: 1px solid rgba(var(--fx-accent), 0.55); opacity: 0; transform: scale(0.9);
  transition: opacity 0.25s, transform 0.25s; pointer-events: none;
}
.icon-btn:hover::after { opacity: 1; transform: scale(1); }

/* E045 — Mobile drawer slide + backdrop */
.site-nav { transition: transform 0.3s cubic-bezier(0.3, 1.2, 0.4, 1), opacity 0.25s; }
@media (max-width: 640px) {
  .site-nav { transform: translateY(-14px); opacity: 0; display: flex; visibility: hidden; }
  .site-nav.open { transform: none; opacity: 1; visibility: visible; }
  .site-nav.open a { animation: fx-drawer-item 0.4s ease both; animation-delay: calc(var(--i, 0) * 55ms); }
}
@keyframes fx-drawer-item {
  0% { opacity: 0; transform: translateX(-14px); }
  100% { opacity: 1; transform: none; }
}

/* ============================================================
   F. SCROLL & PAGE-LEVEL
   ============================================================ */

/* E046 — Scroll progress bar (transform: scaleX, no layout thrash) */
.fx-progress {
  position: fixed; top: 0; left: 0; height: 3px; width: 100%; z-index: 90;
  transform: scaleX(0); transform-origin: 0 50%;
  background: linear-gradient(90deg, var(--accent), var(--accent-2), #ff5ea6, var(--accent));
  background-size: 220% 100%;
  animation: fx-progress-hue 4s linear infinite;
  box-shadow: 0 0 12px rgba(var(--fx-accent), 0.6);
  will-change: transform;
}
@keyframes fx-progress-hue { 0% { background-position: 0% 50%; } 100% { background-position: 220% 50%; } }

/* E047 — Page enter fade */
body { animation: fx-page-in 0.5s ease both; }
@keyframes fx-page-in { 0% { opacity: 0; } 100% { opacity: 1; } }

/* E048 — Page leave fade (JS adds .fx-leave before navigating) */
html.fx-leave body { animation: fx-page-out 0.22s ease both; }
@keyframes fx-page-out { 0% { opacity: 1; transform: none; } 100% { opacity: 0; transform: translateY(-8px); } }

/* E049 — Preloader (JS injects; auto-removes) */
#fx-pre {
  position: fixed; inset: 0; z-index: 999; display: grid; place-items: center;
  background: var(--bg); transition: opacity 0.45s ease, visibility 0.45s;
}
#fx-pre.done { opacity: 0; visibility: hidden; }
#fx-pre .fx-pre-logo {
  width: 64px; height: 64px; border-radius: 18px; display: grid; place-items: center;
  background: var(--grad); color: #fff; font-family: var(--font-display);
  font-size: 1.8rem; font-weight: 700;
  animation: fx-pre-spin 1.1s cubic-bezier(0.6, 0.1, 0.4, 0.9) infinite;
}
@keyframes fx-pre-spin {
  0% { transform: rotate(0) scale(1); border-radius: 18px; }
  50% { transform: rotate(180deg) scale(0.82); border-radius: 50%; }
  100% { transform: rotate(360deg) scale(1); border-radius: 18px; }
}
#fx-pre .fx-pre-bar {
  position: absolute; bottom: 18%; left: 50%; transform: translateX(-50%);
  width: 150px; height: 3px; border-radius: 3px; overflow: hidden; background: var(--card-2);
}
#fx-pre .fx-pre-bar::after {
  content: ""; display: block; height: 100%; width: 40%; border-radius: 3px;
  background: var(--grad); animation: fx-pre-slide 1.1s ease-in-out infinite;
}
@keyframes fx-pre-slide { 0% { transform: translateX(-110%); } 100% { transform: translateX(400%); } }

/* E050 — Back-to-top floating button */
.fx-top {
  position: fixed; right: 20px; bottom: 20px; z-index: 80;
  width: 46px; height: 46px; border-radius: 14px; border: 1px solid var(--line-strong);
  background: var(--card); color: var(--text); cursor: pointer;
  display: grid; place-items: center; box-shadow: var(--shadow);
  opacity: 0; transform: translateY(16px) scale(0.9); pointer-events: none;
  transition: opacity 0.3s, transform 0.3s;
}
.fx-top.show { opacity: 1; transform: none; pointer-events: auto; animation: fx-top-in 0.4s cubic-bezier(0.3, 1.6, 0.4, 1); }
.fx-top:hover { border-color: var(--accent); animation: fx-top-bob 1.2s ease-in-out infinite; }
@keyframes fx-top-in { 0% { transform: translateY(16px) scale(0.9); } 100% { transform: none; } }
@keyframes fx-top-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-4px); } }

/* E051 — Scroll-down hint in hero */
.scroll-down {
  display: inline-flex; align-items: center; gap: 0.5em; margin-top: 2.2rem;
  color: var(--muted); font-size: 0.82rem; font-weight: 600;
  letter-spacing: 0.08em; text-transform: uppercase;
}
.scroll-down svg { animation: fx-bob-arrow 1.8s ease-in-out infinite; }
.scroll-down:hover { color: var(--accent-2); text-decoration: none; }
@keyframes fx-bob-arrow { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(6px); } }

/* E052 — Parallax targets get smooth transform transitions */
[data-parallax] { will-change: transform; }

/* ============================================================
   G. MARQUEE
   ============================================================ */

/* E053 — Infinite marquee strip */
.marquee-strip {
  overflow: hidden; border-block: 1px solid var(--line);
  padding: 0.85rem 0; margin: 1.6rem 0 0;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}
.marquee-track { display: flex; width: max-content; animation: fx-marquee 30s linear infinite; will-change: transform; }
.marquee-track:hover { animation-play-state: paused; }
.marquee-track > span {
  display: inline-block; white-space: nowrap; padding-right: 3rem;
  font-family: var(--font-display); font-weight: 700; font-size: 0.92rem;
  letter-spacing: 0.14em; text-transform: uppercase; opacity: 0.7;
}
@keyframes fx-marquee { 0% { transform: translateX(0); } 100% { transform: translateX(-50%); } }

/* E054 — Second, slower reverse marquee row */
.marquee-track.rev { animation: fx-marquee-rev 42s linear infinite; }
@keyframes fx-marquee-rev { 0% { transform: translateX(-50%); } 100% { transform: translateX(0); } }

/* ============================================================
   H. DATA & STATUS BITS
   ============================================================ */

/* E055 — Live pill radar sweep on the dot */
.live-dot.ok { position: relative; }
.live-dot.ok::after {
  content: ""; position: absolute; inset: -3px; border-radius: 50%;
  border: 1px solid rgba(52, 211, 153, 0.7);
  animation: fx-radar 1.6s ease-out infinite;
}
@keyframes fx-radar { 0% { transform: scale(0.6); opacity: 1; } 100% { transform: scale(2.1); opacity: 0; } }

/* E056 — Connecting pill shimmer */
.live-pill.connecting { position: relative; overflow: hidden; }
.live-pill.connecting::before {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(100deg, transparent 30%, rgba(255, 255, 255, 0.12) 50%, transparent 70%);
  animation: fx-pill-sheen 1.4s linear infinite;
}

/* E057 — Skeleton shimmer utility */
.fx-skeleton {
  background: linear-gradient(100deg, var(--card-2) 40%, rgba(255, 255, 255, 0.09) 50%, var(--card-2) 60%);
  background-size: 220% 100%;
  animation: fx-skeleton 1.3s linear infinite;
}
@keyframes fx-skeleton { 0% { background-position: 120% 0; } 100% { background-position: -120% 0; } }

/* E058 — Avatar ring rotates its gradient */
.avatar-ring { background-size: 220% 220%; animation: fx-avatar-ring 12s ease infinite; }
@keyframes fx-avatar-ring {
  0%, 100% { background-position: 0% 50%; }
  50% { background-position: 100% 50%; }
}

/* E059 — Avatar hover tilt-spin */
.avatar-ring { transition: transform 0.4s cubic-bezier(0.3, 1.4, 0.4, 1); }
.avatar-ring:hover { transform: rotate(8deg) scale(1.05); }

/* E060 — Pulsing ring behind avatar */
.avatar-ring::before {
  content: ""; position: absolute; inset: -7px; border-radius: 50%;
  border: 1px solid rgba(var(--fx-accent), 0.5);
  animation: fx-radar 2.4s ease-out infinite;
}

/* E061 — Pulse dot (connecting status) */
.pulse-dot { animation: fx-pulse-dot 2s ease infinite; }
@keyframes fx-pulse-dot {
  0% { box-shadow: 0 0 0 0 rgba(var(--fx-accent2), 0.55); }
  70% { box-shadow: 0 0 0 9px rgba(var(--fx-accent2), 0); }
  100% { box-shadow: 0 0 0 0 rgba(var(--fx-accent2), 0); }
}

/* E062 — License note border beam */
.license-note { position: relative; overflow: hidden; }
.license-note::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px;
  background: linear-gradient(180deg, var(--accent), var(--accent-2), #ff5ea6, var(--accent));
  background-size: 100% 300%; animation: fx-beam 3s linear infinite;
}
@keyframes fx-beam { 0% { background-position: 50% 0%; } 100% { background-position: 50% 300%; } }

/* E063 — CTA band gradient pan */
.cta-band { background-size: 200% 200%; animation: fx-cta-pan 16s ease infinite; }
@keyframes fx-cta-pan { 0%, 100% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } }

/* ============================================================
   I. PROJECT DETAIL / ABOUT
   ============================================================ */

/* E064 — Facts row sweep highlight */
.facts div { position: relative; transition: padding-left 0.25s ease; }
.facts div::before {
  content: ""; position: absolute; left: -8px; top: 20%; bottom: 20%; width: 3px;
  border-radius: 3px; background: var(--grad);
  transform: scaleY(0); transition: transform 0.25s ease;
}
.facts div:hover { padding-left: 6px; }
.facts div:hover::before { transform: scaleY(1); }

/* E065 — Timeline line draws downward */
.timeline { position: relative; }
.timeline::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 2px;
  background: var(--grad); transform-origin: top;
  animation: fx-line-grow 1.6s ease both;
}
@keyframes fx-line-grow { 0% { transform: scaleY(0); } 100% { transform: scaleY(1); } }

/* E066 — Timeline dot ping */
.tl-dot { animation: fx-tl-ping 2.6s ease infinite; }
@keyframes fx-tl-ping {
  0%, 100% { box-shadow: 0 0 0 0 rgba(var(--fx-accent), 0.45); }
  50% { box-shadow: 0 0 0 6px rgba(var(--fx-accent), 0); }
}

/* E067 — Timeline month cascade */
.tl-month { animation: fx-slide-right 0.6s ease both; }
@keyframes fx-slide-right { 0% { opacity: 0; transform: translateX(-18px); } 100% { opacity: 1; transform: none; } }

/* ============================================================
   J. README & CONTENT
   ============================================================ */

/* E068 — Code blocks glow on hover */
.readme-card pre { transition: border-color 0.25s, box-shadow 0.25s; position: relative; }
.readme-card pre:hover { border-color: rgba(var(--fx-accent), 0.5); box-shadow: 0 8px 26px rgba(var(--fx-accent), 0.14); }

/* E069 — Copy button pop-in + success check (JS injects) */
.fx-copy {
  position: absolute; top: 8px; right: 8px; z-index: 2;
  border: 1px solid var(--line-strong); background: var(--card); color: var(--muted);
  border-radius: 8px; padding: 0.3em 0.6em; font-size: 0.72rem; font-weight: 700;
  cursor: pointer; opacity: 0; transform: translateY(-4px);
  transition: opacity 0.2s, transform 0.2s;
}
.readme-card pre:hover .fx-copy, .fx-copy:focus-visible { opacity: 1; transform: none; }
.fx-copy.ok { color: #34d399; border-color: rgba(52, 211, 153, 0.6); animation: fx-pop 0.35s cubic-bezier(0.3, 1.8, 0.5, 1) both; }

/* E070 — Fancy link underline draw inside README */
.readme-card p a, .contact-links a {
  background-image: linear-gradient(var(--accent-2), var(--accent-2));
  background-size: 0% 2px; background-repeat: no-repeat;
  background-position: 0 100%; transition: background-size 0.3s ease;
  padding-bottom: 2px;
}
.readme-card p a:hover, .contact-links a:hover { background-size: 100% 2px; text-decoration: none; }

/* E071 — Images lazy fade + zoom settle (JS marks .fx-img-pend) */
.fx-img-pend { opacity: 0; transform: scale(0.97); }
.fx-img-in { opacity: 1; transform: none; transition: opacity 0.5s ease, transform 0.5s ease; }
.readme-card img { transition: transform 0.35s ease, box-shadow 0.3s; }
.readme-card img:hover { transform: scale(1.02); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35); }

/* E072 — Blockquote-ish hr shimmer in readme */
.readme-card hr {
  height: 2px; border: none; border-radius: 2px;
  background: linear-gradient(90deg, transparent, rgba(var(--fx-accent), 0.6), transparent);
  background-size: 200% 100%; animation: fx-hr-slide 8s linear infinite;
}
@keyframes fx-hr-slide { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }

/* ============================================================
   K. 404 PAGE
   ============================================================ */

/* E073 — Floating 404 code */
.error-code { display: inline-block; animation: fx-floaty 4.5s ease-in-out infinite; }
@keyframes fx-floaty { 0%, 100% { transform: translateY(0) rotate(-1deg); } 50% { transform: translateY(-12px) rotate(1.5deg); } }

/* E074 — Shake on empty search submit (JS adds .fx-shake) */
.fx-shake { animation: fx-shake 0.45s ease; }
@keyframes fx-shake {
  0%, 100% { transform: translateX(0); }
  20% { transform: translateX(-8px); }
  40% { transform: translateX(7px); }
  60% { transform: translateX(-5px); }
  80% { transform: translateX(3px); }
}

/* E075 — Suggested cards stagger-pulse on 404 hover */
.error-suggest .project-card:hover { animation: fx-suggest-lift 0.5s cubic-bezier(0.3, 1.4, 0.4, 1); }
@keyframes fx-suggest-lift {
  0% { transform: scale(1); }
  45% { transform: scale(1.04) rotate(-0.6deg); }
  100% { transform: scale(1); }
}

/* ============================================================
   L. FOOTER
   ============================================================ */

/* E076 — Animated scallop wave above footer (transform slide) */
.site-footer { position: relative; }
.site-footer::before {
  content: ""; position: absolute; top: -20px; left: -44px; right: -44px; height: 21px;
  background-image: radial-gradient(circle at 11px -5px, transparent 12px, color-mix(in srgb, var(--bg-2) 82%, transparent) 13px);
  background-size: 22px 21px; background-repeat: repeat-x;
  animation: fx-wave-slide 8s linear infinite;
  opacity: 0.9; will-change: transform;
}
@keyframes fx-wave-slide { 0% { transform: translate3d(0, 0, 0); } 100% { transform: translate3d(22px, 0, 0); } }

/* E077 — Credit underline draw */
.f-credit a { background-image: linear-gradient(var(--accent-2), var(--accent-2)); background-size: 0% 2px; background-repeat: no-repeat; background-position: 0 100%; padding-bottom: 2px; transition: background-size 0.3s; }
.f-credit a:hover { background-size: 100% 2px; }

/* ============================================================
   M. CURSOR & PARTY
   ============================================================ */

/* E078 — Custom cursor ring + dot (JS drives position) */
#fx-cursor-ring, #fx-cursor-dot {
  position: fixed; top: 0; left: 0; z-index: 98; pointer-events: none;
  border-radius: 50%; transform: translate(-50%, -50%);
}
#fx-cursor-ring {
  width: 34px; height: 34px; border: 1.5px solid rgba(var(--fx-accent), 0.65);
  transition: width 0.2s, height 0.2s, border-color 0.2s;
}
#fx-cursor-ring.big { width: 54px; height: 54px; border-color: rgba(var(--fx-accent2), 0.8); }
#fx-cursor-dot { width: 6px; height: 6px; background: var(--accent-2); }

/* E079 — Confetti/burst canvas layer */
/* A full-viewport canvas at DPR 2 is a ~33 MB backing store at 1080p (133 MB
   at 4K) and stays composited every frame even with nothing drawn on it. It is
   display:none until there is actually a particle to show, and cleared on the
   way out. */
#fx-burst { position: fixed; inset: 0; z-index: 97; pointer-events: none; display: none; }
#fx-burst.on { display: block; }

/* E080 — Party overlay (konami easter egg) */
#fx-party {
  position: fixed; inset: 0; z-index: 96; pointer-events: none; opacity: 0;
  background: linear-gradient(120deg, rgba(255, 0, 128, 0.14), rgba(255, 220, 0, 0.12), rgba(0, 255, 200, 0.14), rgba(120, 0, 255, 0.14));
  background-size: 320% 320%;
}
#fx-party.on { animation: fx-party-flash 6s ease both; }
@keyframes fx-party-flash {
  0% { opacity: 0; background-position: 0% 50%; }
  15% { opacity: 1; }
  85% { opacity: 1; background-position: 100% 50%; }
  100% { opacity: 0; background-position: 100% 50%; }
}

/* ============================================================
   E082 — Off-screen animation pausing (performance)
   ------------------------------------------------------------
   effects.js puts .fx-qs on a section once it scrolls out of view. Endless
   animations inside it stop being ticked until it comes back. This is purely
   a cost saving — anything the visitor can actually see is untouched, so no
   effect is lost, changed or slowed. It matters most on the long pages and
   the ad grid, where 20 slots otherwise each keep several loops running.
   Only infinite/ambient animations are listed: one-shot entrance and hover
   animations are deliberately left alone so reveals keep their timing.
   ============================================================ */
.fx-qs .fx-sweep,
.fx-qs .badge-new,
.fx-qs .pulse-dot,
.fx-qs .tl-dot,
.fx-qs .avatar-ring,
.fx-qs .cta-band,
.fx-qs .marquee-track,
.fx-qs .error-code,
.fx-qs .fx-typing::after,
.fx-qs .hero-card-body code::after,
.fx-qs .section-head h2::after,
.fx-qs .page-head::before,
.fx-qs .section-head::before,
.fx-qs .marquee-strip::before,
.fx-qs .marquee-strip::after,
.fx-qs .hero-card::before,
.fx-qs .side-card::before,
.fx-qs .f-credit a,
.fx-qs .ad-crazy::before,
.fx-qs .ad-crazy .ad-cta::after,
.fx-qs .ad-crazy .ad-emoji,
.fx-qs .chaos-text,
.fx-qs .fx-preloader,
.fx-qs .fx-skeleton,
.fx-qs .fx-radar,
.fx-qs .fx-beam,
.fx-qs [class*="fx-glitch"]::before,
.fx-qs [class*="fx-glitch"]::after,
/* The rest of the paint-scope loops. Box-shadow, text-shadow,
   background-position, filter and clip-path cannot be composited, so these
   repaint their element on every frame — they are the expensive kind and
   they were simply missing from this list. All of them live inside blocks
   that already receive .fx-qs, so pausing them costs nothing visually: they
   are identical when on screen and silent when they are not.
   Deliberately NOT listed: .brand-mark (site header, always on screen),
   .fx-progress (fixed scroll bar) and .fx-disco (a deliberate full-screen
   takeover) — none of them is ever off screen, so pausing is meaningless. */
.fx-qs h1 .grad-text,
.fx-qs .grad-text,
.fx-qs .hero-title,
.fx-qs .error-title,
.fx-qs .error-hero::before,
.fx-qs .btn-primary,
.fx-qs .meta-lang .lang-dot,
.fx-qs .license-note::before,
.fx-qs .readme-card hr,
.fx-qs .f-col ul li a,
.fx-qs .chaos-fill {
  animation-play-state: paused !important;
}

/* E080b — Combo-code toast (which code just fired) */
#fx-code-toast {
  position: fixed; left: 50%; bottom: 26px; z-index: 99;
  transform: translateX(-50%) translateY(14px) scale(0.96);
  padding: 0.55rem 1.1rem; border-radius: 999px;
  border: 1px solid rgba(var(--fx-accent), 0.55);
  background: color-mix(in srgb, var(--card) 88%, transparent);
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
  color: var(--text); font-size: 0.78rem; font-weight: 800;
  letter-spacing: 0.1em; text-transform: uppercase; white-space: nowrap;
  pointer-events: none; opacity: 0;
  transition: opacity 0.25s ease, transform 0.25s cubic-bezier(0.2, 1.4, 0.4, 1);
}
#fx-code-toast.on { opacity: 1; transform: translateX(-50%) translateY(0) scale(1); }
#fx-code-toast small {
  display: block; font-size: 0.62rem; font-weight: 600;
  letter-spacing: 0.04em; text-transform: none; color: var(--muted); margin-top: 2px;
}

/* E080c — Emoji rain (high-tier codes). Keyframe name is deliberately distinct
   from E127's injected fx-emoji-fall: two @keyframes with one name would
   silently override each other. */
#fx-emoji-rain { position: fixed; inset: 0; z-index: 94; pointer-events: none; overflow: hidden; }
#fx-emoji-rain span {
  position: absolute; top: -8%; font-size: 1.6rem; will-change: transform;
  animation: fx-code-rain var(--dur, 3.2s) linear var(--del, 0s) forwards;
}
@keyframes fx-code-rain {
  0%   { transform: translateY(0) rotate(0deg); opacity: 0; }
  10%  { opacity: 1; }
  100% { transform: translateY(112vh) rotate(var(--rot, 320deg)); opacity: 0.9; }
}

/* E081 — Global smooth theme cross-fade */
body, .card, .site-header, .site-footer, .search-box, .icon-btn, .btn-ghost {
  transition-property: background-color, color, border-color, box-shadow, background-position, transform;
  transition-duration: 0.25s;
}

/* E082 — Reduced motion: kill every effect globally */
@media (prefers-reduced-motion: reduce) {
  .fx-aurora, .fx-orb, .fx-stars, .fx-gridlines, .fx-scan, .fx-noise, .fx-glow,
  #fx-cursor-ring, #fx-cursor-dot, #fx-party { display: none !important; }
  *, *::before, *::after {
    animation-duration: 0.001s !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001s !important;
  }
}

/* ============================================================
   WAVE 2 — E083 to E100
   ============================================================ */

/* E083 — Animated gradient beam border on the side card */
.side-card { position: relative; }
.side-card::before {
  content: ""; position: absolute; inset: -1px; border-radius: inherit; padding: 1.5px;
  background: linear-gradient(120deg, rgba(var(--fx-accent), 0.9), rgba(var(--fx-accent2), 0.9), #ff5ea6, rgba(var(--fx-accent), 0.9));
  background-size: 300% 300%;
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor; mask-composite: exclude;
  animation: fx-side-beam 5s linear infinite; pointer-events: none;
}
@keyframes fx-side-beam { 0% { background-position: 0% 50%; } 100% { background-position: 300% 50%; } }

/* E084 — Neon flicker on the 404 title */
.error-title { animation: fx-title-neon 6s linear infinite; }
@keyframes fx-title-neon {
  0%, 88%, 92%, 100% { text-shadow: 0 0 22px rgba(var(--fx-accent2), 0.35); }
  90% { text-shadow: 0 0 4px rgba(var(--fx-accent2), 0.1); }
  94% { text-shadow: 0 0 30px rgba(255, 94, 166, 0.5); }
}

/* E085 — Pager links slide in from opposite sides */
.pager-link:first-child { animation: fx-from-left 0.6s cubic-bezier(0.2, 0.8, 0.3, 1.1) both; }
.pager-link.right { animation: fx-from-right 0.6s cubic-bezier(0.2, 0.8, 0.3, 1.1) both; animation-delay: 0.08s; }
@keyframes fx-from-left { 0% { opacity: 0; transform: translateX(-34px); } 100% { opacity: 1; transform: none; } }
@keyframes fx-from-right { 0% { opacity: 0; transform: translateX(34px); } 100% { opacity: 1; transform: none; } }

/* E086 — Section headings get an animated gradient underline bar */
.section-head h2, .page-head h1 { position: relative; padding-bottom: 10px; }
.section-head h2::after, .page-head h1::after {
  content: ""; position: absolute; left: 0; bottom: 0; height: 3px; width: 64px;
  border-radius: 3px; background: linear-gradient(90deg, var(--accent), var(--accent-2), #ff5ea6);
  background-size: 200% 100%;
  animation: fx-underline-pan 4s linear infinite;
}
@keyframes fx-underline-pan { 0% { background-position: 0% 50%; } 100% { background-position: 200% 50%; } }

/* E087 — Chips row: soft rainbow aura drift */
.chips-row { position: relative; }
.chips-row::before {
  content: ""; position: absolute; inset: -14px -20px; z-index: -1; border-radius: 20px;
  background: conic-gradient(from var(--ang), rgba(var(--fx-accent), 0.08), rgba(var(--fx-accent2), 0.08), rgba(255, 94, 166, 0.08), rgba(var(--fx-accent), 0.08));
  filter: blur(14px);
  animation: fx-border-spin 9s linear infinite;
}

/* E088 — Facts rows: hover gradient wash */
.facts div:hover { background: linear-gradient(90deg, rgba(var(--fx-accent), 0.08), transparent 70%); border-radius: 6px; }

/* E089 — Blinking terminal caret after the GitHub handle */
.hero-card-body code::after { content: "▌"; margin-left: 3px; color: var(--accent-2); animation: fx-caret 1s steps(1) infinite; }

/* E090 — README h2 slides an accent bar in on hover */
.readme-card h2, .readme-card h3 { transition: padding-left 0.25s ease; }
.readme-card h2:hover, .readme-card h3:hover { padding-left: 10px; }
.readme-card h2::before, .readme-card h3::before {
  content: ""; display: inline-block; width: 0; height: 0.7em; margin-right: 0; vertical-align: middle;
  border-radius: 3px; background: var(--grad);
  transition: width 0.25s ease, margin-right 0.25s ease;
}
.readme-card h2:hover::before, .readme-card h3:hover::before { width: 6px; margin-right: 10px; }

/* E091 — Scroll-down arrow leaves echo trails */
.scroll-down svg { position: relative; }
.scroll-down::after {
  content: ""; position: absolute; left: 50%; bottom: -8px; width: 5px; height: 5px; border-radius: 50%;
  background: var(--accent-2); transform: translateX(-50%); opacity: 0;
  animation: fx-echo-drop 1.8s ease-out infinite;
}
.scroll-down { position: relative; }
@keyframes fx-echo-drop { 0% { opacity: 0.9; transform: translateX(-50%) scale(1); } 100% { opacity: 0; transform: translateX(-50%) translateY(16px) scale(0.4); } }

/* E092 — Category card: dotted texture zoom on hover */
.cat-card { background-image: radial-gradient(rgba(var(--fx-accent), 0.10) 1px, transparent 1px); background-size: 14px 14px; transition: background-size 0.4s ease, border-color 0.2s, box-shadow 0.25s; }
.cat-card:hover { background-size: 5px 5px; }

/* E093 — Pager arrows slide forever on hover */
.pager-link .muted { display: inline-block; }
.pager-link:first-child:hover .muted { animation: fx-arrow-l 0.9s ease infinite; }
.pager-link.right:hover .muted { animation: fx-arrow-r 0.9s ease infinite; }
@keyframes fx-arrow-l { 0%, 100% { transform: translateX(0); } 50% { transform: translateX(-4px); } }
@keyframes fx-arrow-r { 0%, 100% { transform: translateX(0); } 50% { transform: translateX(4px); } }

/* E094 — Footer links hue-cycle on hover */
.f-col ul li a { transition: color 0.2s, filter 0.3s; }
.f-col ul li a:hover { animation: fx-link-hue 1.6s linear infinite; }
@keyframes fx-link-hue { 0% { filter: hue-rotate(0deg); } 100% { filter: hue-rotate(360deg); } }

/* E095 — Chip emojis spin on card hover */
.project-card:hover .chip, .cat-card:hover .cat-icon { animation: fx-emoji-spin 0.8s cubic-bezier(0.3, 1.4, 0.4, 1); display: inline-flex; }
@keyframes fx-emoji-spin { 0% { transform: rotate(0) scale(1); } 60% { transform: rotate(360deg) scale(1.2); } 100% { transform: rotate(360deg) scale(1); } }

/* E096 — Page-head rainbow ribbon sweep behind the title */
.page-head { position: relative; }
.page-head::before {
  content: ""; position: absolute; top: 0; left: 4%; right: 4%; height: 2px; border-radius: 2px;
  background: linear-gradient(90deg, transparent, rgba(var(--fx-accent), 0.7), rgba(var(--fx-accent2), 0.7), transparent);
  background-size: 220% 100%;
  animation: fx-hr-slide 5s linear infinite;
}

/* E097 — cards-4 grids: alternate 3D flip-up entrance */
.cards-4 > *:nth-child(even) { animation-name: fx-flip-up; }
@keyframes fx-flip-up {
  0% { opacity: 0; transform: perspective(700px) rotateX(38deg) translateY(26px); }
  100% { opacity: 1; transform: none; }
}

/* E098 — LIVE corner ribbon on the hero profile card */
.hero-card::after {
  content: "LIVE"; position: absolute; top: 14px; right: -30px; z-index: 1;
  transform: rotate(38deg); padding: 3px 34px;
  background: var(--grad); color: #fff; font-size: 0.62rem; font-weight: 800; letter-spacing: 0.16em;
  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.35);
  animation: fx-ribbon-blink 2.6s ease-in-out infinite;
}
@keyframes fx-ribbon-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.55; } }

/* E099 — Marquee edge glow pulses where text enters/exits */
.marquee-strip { position: relative; }
.marquee-strip::before, .marquee-strip::after {
  content: ""; position: absolute; top: 0; bottom: 0; width: 60px; z-index: 1; pointer-events: none;
  animation: fx-edge-glow 3s ease-in-out infinite;
}
.marquee-strip::before { left: 0; background: radial-gradient(60px 30px at 0% 50%, rgba(var(--fx-accent), 0.28), transparent 70%); }
.marquee-strip::after { right: 0; background: radial-gradient(60px 30px at 100% 50%, rgba(var(--fx-accent2), 0.28), transparent 70%); animation-delay: 1.5s; }
@keyframes fx-edge-glow { 0%, 100% { opacity: 0.4; } 50% { opacity: 1; } }

/* E100 — Light-speed diagonal streaks behind the 404 hero */
.error-hero::before {
  content: ""; position: absolute; inset: -40%; z-index: -1; pointer-events: none;
  background:
    linear-gradient(115deg, transparent 42%, rgba(var(--fx-accent2), 0.10) 50%, transparent 58%),
    linear-gradient(115deg, transparent 30%, rgba(var(--fx-accent), 0.10) 38%, transparent 46%),
    linear-gradient(115deg, transparent 58%, rgba(255, 94, 166, 0.09) 66%, transparent 74%);
  background-size: 300% 300%;
  animation: fx-lightspeed 7s linear infinite;
}
@keyframes fx-lightspeed {
  0% { background-position: 0% 0%, 0% 0%, 0% 0%; }
  100% { background-position: 300% 200%, 260% 160%, 340% 240%; }
}

/* ---- helpers used by JS wave-2 effects (E127-E150) ---- */
.fx-disco { animation: fx-disco-spin 0.6s linear infinite; }
@keyframes fx-disco-spin { 0% { filter: hue-rotate(0deg); } 100% { filter: hue-rotate(360deg); } }
.fx-target-flash { animation: fx-target-flash 1.2s ease; border-radius: 12px; }
@keyframes fx-target-flash { 0% { box-shadow: 0 0 0 0 rgba(var(--fx-accent), 0.55); } 60% { box-shadow: 0 0 0 18px rgba(var(--fx-accent), 0); } 100% { box-shadow: none; } }
.fx-head-in > h2, .fx-head-in > h1 { animation: fx-head-slide 0.55s cubic-bezier(0.2, 0.8, 0.3, 1.15) both; }
@keyframes fx-head-slide { 0% { opacity: 0; transform: translateX(-26px); } 100% { opacity: 1; transform: none; } }
.fx-ring {
  position: absolute; border: 2px solid rgba(var(--fx-accent), 0.7); border-radius: 50%;
  pointer-events: none; transform: translate(-50%, -50%) scale(0); animation: fx-ring-go 0.6s ease-out forwards;
}
@keyframes fx-ring-go { to { transform: translate(-50%, -50%) scale(3); opacity: 0; } }
.fx-trail-dot {
  position: fixed; z-index: 95; width: 7px; height: 7px; border-radius: 50%; pointer-events: none;
  animation: fx-trail-fade 0.55s ease-out forwards;
}
@keyframes fx-trail-fade { 0% { opacity: 0.85; transform: scale(1); } 100% { opacity: 0; transform: scale(0.2); } }
.fx-dots { position: fixed; right: 14px; top: 50%; transform: translateY(-50%); z-index: 85; display: flex; flex-direction: column; gap: 10px; }
.fx-dots button { width: 9px; height: 9px; border-radius: 50%; border: none; cursor: pointer; background: var(--line-strong); padding: 0; transition: background 0.25s, transform 0.25s; }
.fx-dots button.on { background: var(--accent); transform: scale(1.35); }
.fx-scatter { display: inline-block; transition: transform 0.45s cubic-bezier(0.2, 0.8, 0.3, 1.2), opacity 0.45s; }
.fx-rotate-role { transition: opacity 0.3s ease; }
.fx-clock { font-variant-numeric: tabular-nums; }
@media (max-width: 640px) { .fx-dots { display: none; } }



@keyframes fx-pill-sheen { 0%, 55% { transform: translateX(-120%); } 85%, 100% { transform: translateX(120%); } }

/* ============================================================
   WAVE 3 — Ad-zone chaos (heavy-ads.html), E151+
   ============================================================ */

/* E151 — Ad grid: tighter columns when many ads rain down */
.ad-zone.ads-many { grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); }
.ad-zone.ads-many .ad-unit { min-height: 200px; }

/* E152 — Cascading slot entrances, 12 different personalities */
.ad-creative.in-drop    { animation: fx-ad-drop 0.55s cubic-bezier(0.2, 0.9, 0.3, 1.3) both; }
@keyframes fx-ad-drop { 0% { opacity: 0; transform: translateY(-70px) scale(0.85); } 70% { transform: translateY(8px); } 100% { opacity: 1; transform: none; } }
.ad-creative.in-spin    { animation: fx-ad-spin 0.6s cubic-bezier(0.2, 0.8, 0.3, 1.2) both; }
@keyframes fx-ad-spin { 0% { opacity: 0; transform: rotate(-360deg) scale(0.4); } 100% { opacity: 1; transform: none; } }
.ad-creative.in-flip    { animation: fx-ad-flip 0.6s cubic-bezier(0.2, 0.8, 0.3, 1.15) both; backface-visibility: hidden; }
@keyframes fx-ad-flip { 0% { opacity: 0; transform: perspective(700px) rotateY(540deg); } 100% { opacity: 1; transform: perspective(700px) rotateY(0); } }
.ad-creative.in-swing   { animation: fx-ad-swing 0.7s cubic-bezier(0.3, 1.4, 0.4, 1) both; transform-origin: top center; }
@keyframes fx-ad-swing { 0% { opacity: 0; transform: rotate(24deg) translateY(-50px); } 60% { transform: rotate(-9deg); } 80% { transform: rotate(4deg); } 100% { opacity: 1; transform: none; } }
.ad-creative.in-zoom    { animation: fx-ad-zoom 0.5s cubic-bezier(0.2, 1.4, 0.4, 1) both; }
@keyframes fx-ad-zoom { 0% { opacity: 0; transform: scale(0.15) rotate(10deg); } 100% { opacity: 1; transform: scale(1) rotate(0); } }
.ad-creative.in-slide-l { animation: fx-ad-slide-l 0.55s cubic-bezier(0.2, 0.9, 0.3, 1.2) both; }
@keyframes fx-ad-slide-l { 0% { opacity: 0; transform: translateX(-110%) rotate(-6deg); } 100% { opacity: 1; transform: none; } }
.ad-creative.in-slide-r { animation: fx-ad-slide-r 0.55s cubic-bezier(0.2, 0.9, 0.3, 1.2) both; }
@keyframes fx-ad-slide-r { 0% { opacity: 0; transform: translateX(110%) rotate(6deg); } 100% { opacity: 1; transform: none; } }
.ad-creative.in-tumble  { animation: fx-ad-tumble 0.7s cubic-bezier(0.2, 0.8, 0.3, 1.1) both; }
@keyframes fx-ad-tumble { 0% { opacity: 0; transform: translate(60px, -90px) rotate(300deg) scale(0.4); } 100% { opacity: 1; transform: none; } }
.ad-creative.in-bounce  { animation: fx-ad-bounce 0.75s cubic-bezier(0.3, 1.6, 0.5, 1) both; }
@keyframes fx-ad-bounce { 0% { opacity: 0; transform: scaleY(0.1); transform-origin: bottom; } 55% { opacity: 1; transform: scaleY(1.12); } 80% { transform: scaleY(0.95); } 100% { transform: scaleY(1); } }
.ad-creative.in-jello   { animation: fx-ad-jello 0.8s ease both; }
@keyframes fx-ad-jello {
  0% { opacity: 0; transform: skewX(18deg) scaleX(0.6); }
  40% { opacity: 1; transform: skewX(-10deg) scaleX(1.12); }
  65% { transform: skewX(6deg) scaleX(0.96); }
  85% { transform: skewX(-2deg); }
  100% { transform: none; }
}
.ad-creative.in-flicker { animation: fx-ad-flicker 0.65s steps(6) both; }
@keyframes fx-ad-flicker { 0% { opacity: 0; } 30% { opacity: 0.7; } 45% { opacity: 0.2; } 70% { opacity: 0.9; } 100% { opacity: 1; } }
.ad-creative.in-zoom-blur { animation: fx-ad-zoom-blur 0.55s cubic-bezier(0.2, 1.2, 0.3, 1) both; }
@keyframes fx-ad-zoom-blur { 0% { opacity: 0; transform: scale(1.9); filter: blur(14px); } 100% { opacity: 1; transform: scale(1); filter: blur(0); } }

/* E153 — Every 5th ad is a BIG one (drama) */
.ad-unit.ad-big { grid-column: span 2; min-height: 260px; }
.ad-unit.ad-big .ad-emoji { font-size: 3.6rem; }
.ad-unit.ad-big .ad-title { font-size: 1.7rem; }
@media (max-width: 700px) { .ad-unit.ad-big { grid-column: span 1; } }

/* E154 — Hue-cycling ad border, unique per slot via --hue */
.ad-crazy::before {
  content: ""; position: absolute; inset: -1px; border-radius: inherit; padding: 2px;
  background: linear-gradient(120deg,
    hsl(var(--hue, 260), 95%, 62%),
    hsl(calc(var(--hue, 260) + 90), 95%, 60%),
    hsl(calc(var(--hue, 260) + 200), 95%, 62%),
    hsl(var(--hue, 260), 95%, 62%));
  background-size: 300% 300%;
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor; mask-composite: exclude;
  animation: fx-side-beam 4s linear infinite;
  pointer-events: none;
}

/* E155 — Ad emoji party: idle wiggle, hover explosion scale */
.ad-crazy .ad-emoji { animation: fx-ad-idle 2.6s ease-in-out var(--ad-i, 0) infinite; display: inline-block; }
@keyframes fx-ad-idle { 0%, 100% { transform: translateY(0) rotate(0); } 50% { transform: translateY(-5px) rotate(7deg); } }
.ad-crazy:hover .ad-emoji { animation: fx-ad-emoji-go 0.6s cubic-bezier(0.2, 1.6, 0.4, 1) both; }
@keyframes fx-ad-emoji-go { 0% { transform: scale(1); } 55% { transform: scale(1.9) rotate(22deg); } 100% { transform: scale(1.25) rotate(0); } }

/* E156 — Ad CTA shimmer line */
.ad-crazy .ad-cta { position: relative; overflow: hidden; }
.ad-crazy .ad-cta::after {
  --sweep-d: 475%; /* 40%-wide bar crossing 190% of its parent */
  content: ""; position: absolute; top: 0; bottom: 0; width: 40%; left: -60%;
  background: linear-gradient(105deg, transparent, rgba(255, 255, 255, 0.35), transparent);
  transform: skewX(-18deg); animation: fx-sweep 2.4s ease var(--ad-i, 0) infinite;
}

/* E157 — CHAOS METER */
.chaos-meter {
  height: 10px; border-radius: 999px; overflow: hidden; margin-top: 1rem;
  background: var(--card-2); border: 1px solid var(--line);
}
.chaos-fill {
  height: 100%; border-radius: 999px; width: 0%;
  background: linear-gradient(90deg, #34d399, #fbbf24, #f87171, #ff5ea6, var(--accent));
  background-size: 300% 100%;
  animation: fx-chaos-hue 2s linear infinite;
  transition: width 0.8s cubic-bezier(0.2, 0.9, 0.3, 1.1);
}
@keyframes fx-chaos-hue { 0% { background-position: 0% 50%; } 100% { background-position: 300% 50%; } }
.chaos-text { font-size: 0.82rem; margin: 0.45rem 0 0; font-weight: 600; }

/* E158 — STORM MODE (12+ ads): page-wide lightning flicker + shake-in */
body.ads-storm { animation: fx-storm-shake 0.5s ease; }
@keyframes fx-storm-shake {
  0%, 100% { transform: translate(0, 0); }
  20% { transform: translate(-6px, 3px); }
  40% { transform: translate(5px, -4px); }
  60% { transform: translate(-4px, 2px); }
  80% { transform: translate(3px, -2px); }
}
body.ads-storm::after {
  content: ""; position: fixed; inset: 0; z-index: 60; pointer-events: none;
  background: rgba(255, 255, 255, 0.06);
  animation: fx-lightning 3.2s steps(1) infinite;
}
@keyframes fx-lightning {
  0%, 92%, 100% { opacity: 0; }
  93% { opacity: 1; }
  94% { opacity: 0; }
  96% { opacity: 0.8; }
  97% { opacity: 0; }
}

/* E159 — ADPOCALYPSE (16+ ads): the world tilts */
body.ads-tilt-world main { animation: fx-world-tilt 1.4s ease both; transform-origin: 50% 0; }
@keyframes fx-world-tilt {
  0% { transform: rotate(0); }
  40% { transform: rotate(-0.7deg); }
  70% { transform: rotate(0.4deg); }
  100% { transform: rotate(0deg) scale(1); }
}
body.ads-tilt-world .ad-crazy .ad-emoji { animation: fx-ad-idle 1.1s ease-in-out infinite; }

/* E160 — Slot badge glows in storm mode */
body.ads-storm .ad-slug {
  color: #ff5ea6;
  animation: fx-slot-blink 1.1s steps(2) infinite;
}
@keyframes fx-slot-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.45; } }

@media (prefers-reduced-motion: reduce) {
  .ad-creative.in-drop, .ad-creative.in-spin, .ad-creative.in-flip, .ad-creative.in-swing,
  .ad-creative.in-zoom, .ad-creative.in-slide-l, .ad-creative.in-slide-r, .ad-creative.in-tumble,
  .ad-creative.in-bounce, .ad-creative.in-jello, .ad-creative.in-flicker, .ad-creative.in-zoom-blur {
    animation: none; opacity: 1;
  }
  body.ads-storm, body.ads-storm::after, body.ads-tilt-world main, .chaos-fill { animation: none !important; }
  body.ads-tilt-world main { transform: none; }
}

/* ============================================================
   PERF GOVERNORS — keep the effects, cut the cost
   ============================================================ */

/* E161 — Ad slots: rainbow border + CTA shimmer run on hover only.
   Idle slots keep their colorful static border (hue per slot) but no
   repaint loop — with 20 ads this was the single biggest lag source. */
.ad-crazy::before { animation: fx-side-beam 4s linear infinite; animation-play-state: paused; }
.ad-crazy:hover::before { animation-play-state: running; }
.ad-crazy .ad-cta::after { animation-play-state: paused; }
.ad-crazy:hover .ad-cta::after { animation-play-state: running; }
.ad-crazy .ad-emoji { animation: fx-ad-idle 2.6s ease-in-out var(--ad-i, 0) infinite; }
/* cap the wiggle party: only the first 8 slots idle-animate */
.ad-zone.ads-many .ad-unit:nth-child(n+9) .ad-emoji { animation: none; }
.ad-zone.ads-many .ad-unit:nth-child(n+9):hover .ad-emoji { animation: fx-ad-emoji-go 0.6s cubic-bezier(0.2, 1.6, 0.4, 1) both; }

/* E162 — .fx-lite: FPS governor / low-end devices. The fun stays
   (ripples, confetti, tilt, marquees, badges) but the expensive
   full-screen ambient loops are dropped first. */
.fx-lite .fx-noise, .fx-lite .fx-scan, .fx-lite .fx-gridlines,
.fx-lite .fx-orb, .fx-lite #fx-cursor-ring, .fx-lite #fx-cursor-dot,
.fx-lite .fx-glow, .fx-lite .fx-trail-dot { display: none !important; }
.fx-lite .fx-aurora { animation: none; }
.fx-lite .fx-stars span { animation: none; opacity: 0.35; }
.fx-lite .marquee-track { animation-duration: 60s; }
.fx-lite .fx-disco { animation: none; }

/* E163 — .fx-hidden: tab in background -> pause every ambient loop */
.fx-hidden .fx-aurora, .fx-hidden .fx-orb, .fx-hidden .fx-stars span,
.fx-hidden .fx-gridlines::before, .fx-hidden .fx-scan::before, .fx-hidden .fx-noise::before,
.fx-hidden .marquee-track, .fx-hidden .live-dot.ok::after, .fx-hidden .pulse-dot,
.fx-hidden .badge-new, .fx-hidden .avatar-ring, .fx-hidden .fx-scan::after { animation-play-state: paused !important; }

/* E164 — Mobile trims: tiny screens skip the full-screen grain/scanline
   overlays entirely (battery + composite cost), keep aurora/orbs. */
@media (max-width: 640px) {
  .fx-noise, .fx-scan, .fx-gridlines { display: none; }
  .fx-glow { display: none; }
  .fx-stars { opacity: 0.6; }
}

/* ============================================================
   E200+ — CINEMATIC LAYER (cinematic.js)
   Whole-screen set pieces. Ground rules for everything below:
   - every layer is position:fixed, pointer-events:none, and
     display:none until JS switches it on, so nothing here costs
     a single frame at page load;
   - only transform and opacity are animated, so the compositor
     does all the work and the page never repaints;
   - no filter:, box-shadow: or backdrop-filter: anywhere — those
     re-rasterise the layer on every frame.
   ============================================================ */

/* E200 — Shared canvas for every pixel effect (matrix rain, warp,
   lightning, shockwave rings). One canvas, DPR capped at 1.5 in JS,
   display:none while idle so its backing store is released. */
#fx-cine {
  position: fixed; inset: 0; z-index: 93;
  pointer-events: none; display: none;
}
#fx-cine.on { display: block; }

/* E209 — Flash burn. Opacity only, animated through WAAPI. Kept out of the
   render tree while idle (display:none) so an invisible full-screen layer is
   never left sitting on top of the page. */
#fx-flash {
  position: fixed; inset: 0; z-index: 98;
  pointer-events: none; display: none; opacity: 0;
}
#fx-flash.on { display: block; }

/* E204 — Solar flare. A soft radial gradient (softness comes from the
   gradient itself, not a blur filter) scaled and spun by the compositor. */
#fx-flare {
  position: fixed; inset: -8%; z-index: 90;
  pointer-events: none; display: none; opacity: 0;
  background:
    radial-gradient(circle at 50% 118%, rgba(255,196,64,0.95), rgba(255,96,0,0.6) 30%, rgba(255,0,88,0.3) 54%, transparent 72%),
    radial-gradient(circle at 18% 6%, rgba(255,255,214,0.5), transparent 42%);
}
#fx-flare.on { display: block; animation: fx-flare-burst 1.7s ease-out both; }
@keyframes fx-flare-burst {
  0%   { opacity: 0;   transform: scale(0.62) rotate(-7deg); }
  12%  { opacity: 1;   transform: scale(0.86) rotate(-4deg); }
  58%  { opacity: 0.8; transform: scale(1.08) rotate(3deg); }
  100% { opacity: 0;   transform: scale(1.34) rotate(8deg); }
}

/* E205 — Hologram glitch: scanline veil sweeping the viewport, plus a
   chromatic ghost on the three known headings. The ghost is a real
   pseudo-element layer moved with transform, so the glitch never
   repaints text. Scoped deliberately: .readme-card h2::before is
   already in use by E090, so readme headings are excluded. */
#fx-holo {
  position: fixed; inset: 0; z-index: 91;
  pointer-events: none; display: none; opacity: 0;
  background:
    linear-gradient(105deg, rgba(0,255,214,0.10), rgba(255,0,140,0.10)),
    repeating-linear-gradient(to bottom, rgba(180,255,240,0.10) 0 1px, transparent 1px 3px);
}
#fx-holo.on { display: block; animation: fx-holo-sweep 1.15s linear both; }
@keyframes fx-holo-sweep {
  0%   { opacity: 0; transform: translate3d(0,-13%,0) scaleY(1.06); }
  12%  { opacity: 1; }
  55%  { transform: translate3d(0,0,0) scaleY(1); }
  88%  { opacity: 0.75; }
  100% { opacity: 0; transform: translate3d(0,5%,0) scaleY(1.02); }
}
.fx-holo .hero-title { position: relative; }
.fx-holo .hero-title::before,
.fx-holo .page-head h1::before,
.fx-holo .section-head h2::before {
  content: attr(data-text);
  position: absolute; left: 0; top: 0; width: 100%;
  color: #ff2d78; opacity: 0.5; pointer-events: none;
  animation: fx-rgb-ghost 1.05s steps(1) both;
}
@keyframes fx-rgb-ghost {
  0%   { transform: translate3d(-5px,0,0); }
  25%  { transform: translate3d(4px,-1px,0); }
  50%  { transform: translate3d(-2px,1px,0); }
  75%  { transform: translate3d(6px,0,0); }
  100% { transform: translate3d(0,0,0); opacity: 0; }
}

/* ============================================================
   E220+ — EFFECTS LEVEL SETTINGS (settings.html) and the FX catalog gallery.

   The gallery can render 325 effects at once, so it is built the same way as
   the rest of the site: tiles are paused off-screen via .fx-qs, and every
   catalog animation is transform/opacity only.
   ============================================================ */

/* E220 — level picker */
.fxl-grid { display: grid; gap: 0.75rem; grid-template-columns: repeat(auto-fit, minmax(168px, 1fr)); margin-top: 1.25rem; }
.fxl-card {
  position: relative; text-align: left; cursor: pointer;
  padding: 0.9rem 1rem; border-radius: 14px;
  background: color-mix(in srgb, var(--panel) 88%, transparent);
  border: 1px solid var(--line); color: var(--text);
  transition: transform .18s ease, border-color .18s ease;
}
.fxl-card:hover { transform: translateY(-3px); border-color: color-mix(in srgb, var(--accent) 55%, var(--line)); }
.fxl-card.on { border-color: var(--accent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 60%, transparent); }
.fxl-card .fxl-name { font-family: var(--font-display); font-weight: 700; font-size: 1.02rem; display: block; }
.fxl-card .fxl-tier { font-size: 0.72rem; letter-spacing: .06em; text-transform: uppercase; opacity: .72; display: block; margin-top: .15rem; }
.fxl-card .fxl-note { display: block; font-size: 0.82rem; opacity: .8; margin-top: .45rem; line-height: 1.45; }
.fxl-card .fxl-badge {
  position: absolute; top: .6rem; right: .6rem; font-size: .62rem; letter-spacing: .06em;
  text-transform: uppercase; padding: .16rem .4rem; border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 22%, transparent); border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent);
}

/* E221 — category toggles */
.fxl-cats { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: 1rem; }
.fxl-cat {
  display: inline-flex; align-items: center; gap: .45rem; cursor: pointer;
  padding: .4rem .75rem; border-radius: 999px; font-size: .84rem;
  background: color-mix(in srgb, var(--panel) 80%, transparent); border: 1px solid var(--line);
  transition: border-color .16s ease, opacity .16s ease;
}
.fxl-cat.off { opacity: .45; }
.fxl-cat input { accent-color: var(--accent); }

/* E222 — stats readout */
.fxl-stats { display: flex; flex-wrap: wrap; gap: 1.5rem; margin-top: 1.1rem; }
.fxl-stat { display: flex; flex-direction: column; }
.fxl-stat b { font-family: var(--font-display); font-size: 1.5rem; line-height: 1.1; }
.fxl-stat span { font-size: .76rem; letter-spacing: .05em; text-transform: uppercase; opacity: .7; }

/* E223 — the gallery */
.fxg-bar { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; margin: 1.1rem 0 1rem; }
.fxg-bar input[type="search"] {
  flex: 1 1 200px; min-width: 0; padding: .5rem .8rem; border-radius: 10px;
  background: color-mix(in srgb, var(--panel) 80%, transparent);
  border: 1px solid var(--line); color: var(--text); font: inherit; font-size: .9rem;
}
.fxg-chip {
  cursor: pointer; padding: .35rem .7rem; border-radius: 999px; font-size: .8rem;
  background: color-mix(in srgb, var(--panel) 78%, transparent); border: 1px solid var(--line); color: var(--text);
}
.fxg-chip.on { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 18%, transparent); }

.fxg-grid { display: grid; gap: .6rem; grid-template-columns: repeat(auto-fill, minmax(152px, 1fr)); }
.fxg-tile {
  /* The catalog is 325 tiles, so this page renders 650 boxes (tile + its
     ::before shape) if nothing intervenes. content-visibility:auto lets the
     browser skip style, layout and paint for tiles that are off screen, and
     `auto` in contain-intrinsic-size means it remembers each tile's real size
     after its first render, so the scrollbar does not jump. Tiles are
     self-contained and already clip, so this is safe to apply here — unlike
     on <section>, which hosts overflow decorations and would clip them.
     IntersectionObserver still sees skipped tiles, so the .fx-qs pausing
     keeps working. */
  content-visibility: auto;
  contain-intrinsic-size: auto 96px;
  position: relative; overflow: hidden; min-height: 96px;
  border-radius: 12px; border: 1px solid var(--line);
  background: color-mix(in srgb, var(--panel) 70%, transparent);
  display: flex; flex-direction: column; justify-content: flex-end;
}
.fxg-tile .fxg-meta { position: relative; z-index: 1; padding: .45rem .55rem; background: linear-gradient(transparent, color-mix(in srgb, var(--bg) 88%, transparent)); }
.fxg-tile .fxg-name { display: block; font-size: .74rem; font-weight: 600; line-height: 1.25; }
.fxg-tile .fxg-id { display: block; font-size: .62rem; opacity: .6; letter-spacing: .04em; }
.fxg-tile.locked { opacity: .38; }
.fxg-tile.locked .fxg-name::after { content: " · locked"; opacity: .7; }

/* The gallery tiles are animated by the generated catalog rules, which are
   gated on the level and out-specify anything here. The one thing they cannot
   express is "this particular tile is above your level", so that is the single
   !important in this block: a locked tile must not move. */
.fxg-tile.locked { animation: none !important; }

/* E224 — catalog decorations applied across the site by fx-catalog.js.

   These are ZERO-SIZE and in normal flow on purpose. `position:absolute` would
   need a positioned host, and most hosts here (a plain <section>, a chip, a
   badge) are not positioned — making them so would risk moving any other
   absolutely-positioned child they own. A 0x0 in-flow box contributes nothing
   to layout, creates its own positioning context for the shape, and moves with
   the host. The shape itself overflows it, which is why overflow stays visible. */
.fx-cat-item {
  position: relative; display: block;
  width: 0; height: 0; overflow: visible;
  pointer-events: none; z-index: 0;
}
/* FIX: the generated shapes size themselves in PERCENTAGES of their host
   (--fxc-w: 38% etc). These hosts are 0x0, so 38% of 0 is 0 and every
   decoration rendered as nothing. Give each shape an absolute size here.
   Specificity is .fx-cat-item.fxcs-x::before (0,2,1) which beats the
   generated .fxcs-x::before (0,1,1) whatever order the two stylesheets load
   in, so this cannot be undone by the catalog file arriving later. */
.fx-cat-item.fxcs-dot::before   { width: 46px; height: 46px; }
.fx-cat-item.fxcs-blob::before  { width: 120px; height: 120px; }
.fx-cat-item.fxcs-ring::before  { width: 54px; height: 54px; }
.fx-cat-item.fxcs-bar::before   { width: 104px; height: 24px; }
.fx-cat-item.fxcs-line::before  { width: 88px; height: 4px; }
.fx-cat-item.fxcs-card::before  { width: 104px; height: 68px; }
.fx-cat-item.fxcs-grid::before  { width: 110px; height: 74px; }

.fx-cat-ambient { opacity: .5; }
.fx-cat-particle { opacity: .85; }
.fx-cat-text { opacity: .9; }
.fx-cat-card { opacity: .55; }
.fx-cat-ui { opacity: .7; }
.fx-cat-scroll { opacity: .35; }
.fx-cat-overlay { position: fixed; left: 50%; top: 50%; z-index: 88; opacity: .35; }
.fx-cat-cursor { position: fixed; left: 0; top: 0; z-index: 92; opacity: .8; }

/* gallery tiles are real sized boxes, so they do want clipping */
.fxg-tile { position: relative; overflow: hidden; }

/* E225 — reduced motion: the level picker still works, nothing animates. */
@media (prefers-reduced-motion: reduce) {
  .fx-catalog [data-fx], .fx-catalog [data-fx]::before { animation: none !important; }
  .fx-cat-item { display: none !important; }
  .fxl-card:hover { transform: none; }
}

/* E226 — effects level OFF. Same technique the reduced-motion block uses:
   collapse durations rather than pausing. Pausing would strand anything that
   waits on animationend (the preloader is the obvious one) and leave it on
   screen forever. */
html.fx-off .fx-aurora, html.fx-off .fx-orb, html.fx-off .fx-stars,
html.fx-off .fx-gridlines, html.fx-off .fx-scan, html.fx-off .fx-noise,
html.fx-off .fx-glow, html.fx-off #fx-cursor-ring, html.fx-off #fx-cursor-dot,
html.fx-off #fx-party, html.fx-off #fx-burst, html.fx-off #fx-emoji-rain,
html.fx-off #fx-cine, html.fx-off .fx-dots, html.fx-off .fx-progress,
html.fx-off .fx-cat-item { display: none !important; }
/* the settings gallery keeps its tiles (they are previews, not effects) —
   they just hold still, which the animation:none rule below does */
html.fx-off *, html.fx-off *::before, html.fx-off *::after {
  animation-duration: 0.001s !important;
  animation-iteration-count: 1 !important;
  transition-duration: 0.001s !important;
}
