/* ==========================================================================
   LegiLearn — capa de efectos (EXPERIMENTAL)

   Todo lo visual de aquí es material de prueba. Vive en su propio fichero y
   nada de styles.css depende de ello: para tirar un efecto basta con borrar
   su bloque, y para tirarlos todos, con quitar el <link> de head.html.

   Cada efecto se activa por clase, siempre con prefijo fx-. Ninguno cambia
   el HTML que ya había: se cuelgan de elementos que existen o de un <div>
   decorativo con aria-hidden.

   Índice
     A. Tokens y utilidades de la capa
     B. Fondos          aurora, malla, rejilla, puntos, grano, haz, escáner
     C. Superficies     borde vivo, brillo, inclinación, foco, elevación
     D. Texto           degradado animado, entrada por palabras, subrayado
     E. Scroll          barra de progreso, parallax, revelados, apilado
     F. Micro           imán, anillos, medidores, cursor
     G. Reducción de movimiento
   ========================================================================== */

/* --------------------------------------------------------------------------
   A. Tokens y utilidades

   Todo lo que empieza escondido —lo que aparece al hacer scroll, las letras
   que suben, las barras que se llenan— se esconde sólo bajo html.fx, y esa
   clase la pone effects.js nada más arrancar. Si el fichero no carga, no
   llega, o se cae con un error antes de tiempo, lo que se ve es la página
   entera y quieta, nunca una sección en blanco esperando a un JavaScript que
   no va a venir.
   -------------------------------------------------------------------------- */
:root {
  --fx-violet: #7c3aed;
  --fx-cyan: #06b6d4;
  --fx-rose: #f43f5e;
  --fx-lime: #84cc16;

  --fx-slow: 24s;
  --fx-med: 12s;
  --fx-fast: 0.6s;

  /* Posición del puntero. app/effects.js las escribe; sin JS valen el centro
     y todo lo que las use se queda quieto en el sitio correcto. */
  --fx-mx: 50%;
  --fx-my: 50%;
  --fx-scroll: 0;
}

/* Un envoltorio que recorta los fondos y crea contexto de apilado propio,
   para que ninguna capa decorativa se cuele por encima del contenido. */
.fx-stage {
  position: relative;
  isolation: isolate;
  overflow: hidden;
}
.fx-layer {
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
}
.fx-stage > *:not(.fx-layer) { position: relative; }

/* --------------------------------------------------------------------------
   B. Fondos
   -------------------------------------------------------------------------- */

/* B1. Aurora: tres manchas de color desenfocadas que se pasean despacio.
   Es el fondo más llamativo del lote y el más caro: el desenfoque de 80px
   sobre un elemento grande obliga al navegador a repintar, así que va sólo
   en secciones altas y nunca dos a la vez en pantalla. */
.fx-aurora { overflow: hidden; }
.fx-aurora i {
  position: absolute;
  display: block;
  width: 46vw;
  height: 46vw;
  min-width: 380px;
  min-height: 380px;
  border-radius: 50%;
  filter: blur(80px);
  opacity: 0.5;
  will-change: transform;
}
.fx-aurora i:nth-child(1) {
  top: -14%; left: -8%;
  background: radial-gradient(circle at 50% 50%, var(--amber) 0%, transparent 68%);
  animation: fx-drift-a var(--fx-slow) ease-in-out infinite;
}
.fx-aurora i:nth-child(2) {
  top: 4%; right: -12%;
  background: radial-gradient(circle at 50% 50%, var(--fx-violet) 0%, transparent 68%);
  animation: fx-drift-b calc(var(--fx-slow) * 1.3) ease-in-out infinite;
}
.fx-aurora i:nth-child(3) {
  bottom: -22%; left: 26%;
  background: radial-gradient(circle at 50% 50%, var(--fx-cyan) 0%, transparent 68%);
  animation: fx-drift-c calc(var(--fx-slow) * 1.1) ease-in-out infinite;
}
/* Sobre el ink la aurora necesita menos opacidad: el negro la amplifica. */
.slab .fx-aurora i,
.fx-aurora--dark i { opacity: 0.34; }

@keyframes fx-drift-a {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
  33%      { transform: translate3d(12vw, 6vh, 0) scale(1.15); }
  66%      { transform: translate3d(-4vw, 12vh, 0) scale(0.92); }
}
@keyframes fx-drift-b {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1.05); }
  40%      { transform: translate3d(-14vw, 10vh, 0) scale(0.88); }
  70%      { transform: translate3d(-6vw, -6vh, 0) scale(1.2); }
}
@keyframes fx-drift-c {
  0%, 100% { transform: translate3d(0, 0, 0) scale(0.95); }
  50%      { transform: translate3d(10vw, -14vh, 0) scale(1.18); }
}

/* B2. Malla cónica: un degradado cónico enorme girando muy despacio detrás
   de un desenfoque. Más barato que la aurora —una sola capa— y da un fondo
   que nunca se repite del todo. */
.fx-mesh::before {
  content: "";
  position: absolute;
  inset: -40%;
  background: conic-gradient(
    from 0deg,
    rgba(241, 200, 15, 0.5),
    rgba(124, 58, 237, 0.42),
    rgba(6, 182, 212, 0.42),
    rgba(22, 163, 74, 0.38),
    rgba(241, 200, 15, 0.5)
  );
  filter: blur(90px);
  opacity: 0.4;
  animation: fx-spin calc(var(--fx-slow) * 2.5) linear infinite;
}
@keyframes fx-spin {
  to { transform: rotate(1turn); }
}

/* B3. Rejilla en fuga: dos degradados repetidos que se desplazan. Con la
   máscara radial la rejilla no llega nunca a los bordes, así que no hay que
   dibujar un corte: se desvanece sola. */
.fx-grid::before {
  content: "";
  position: absolute;
  inset: -50% 0 0;
  background-image:
    linear-gradient(to right, rgba(10, 10, 11, 0.07) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(10, 10, 11, 0.07) 1px, transparent 1px);
  background-size: 64px 64px;
  mask-image: radial-gradient(ellipse 80% 60% at 50% 40%, #000 30%, transparent 78%);
  animation: fx-grid-pan 18s linear infinite;
}
.slab .fx-grid::before,
.fx-grid--dark::before {
  background-image:
    linear-gradient(to right, rgba(255, 255, 255, 0.08) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(255, 255, 255, 0.08) 1px, transparent 1px);
}
@keyframes fx-grid-pan {
  to { background-position: 64px 64px; }
}

/* B4. Rejilla en perspectiva: la misma idea inclinada hacia el horizonte.
   Queda a medio camino entre cuadrícula técnica y suelo de sintetizador. */
.fx-floor {
  perspective: 340px;
  perspective-origin: 50% 0%;
}
.fx-floor::before {
  content: "";
  position: absolute;
  inset: 40% -60% -40%;
  background-image:
    linear-gradient(to right, rgba(241, 200, 15, 0.28) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(241, 200, 15, 0.28) 1px, transparent 1px);
  background-size: 70px 70px;
  transform: rotateX(72deg);
  mask-image: linear-gradient(to bottom, transparent, #000 45%, transparent);
  animation: fx-floor-run 4s linear infinite;
}
@keyframes fx-floor-run {
  to { background-position: 0 70px; }
}

/* B5. Matriz de puntos: la versión sobria de la rejilla. Un punto cada 24px,
   con un halo que sigue al puntero recortando la máscara. */
.fx-dots::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: radial-gradient(circle at 1px 1px, rgba(10, 10, 11, 0.16) 1px, transparent 0);
  background-size: 24px 24px;
  mask-image: radial-gradient(260px circle at var(--fx-mx) var(--fx-my), #000 0%, transparent 75%);
}
.slab .fx-dots::before,
.fx-dots--dark::before {
  background-image: radial-gradient(circle at 1px 1px, rgba(255, 255, 255, 0.22) 1px, transparent 0);
}
/* Sin puntero (móvil, sin JS) el halo no tiene sentido: se enseña entera y
   apagada, que es mejor que una mancha fija en el centro. */
.fx-dots--flat::before {
  mask-image: radial-gradient(ellipse 70% 70% at 50% 50%, #000 20%, transparent 80%);
}

/* B6. Grano: ruido de turbulencia SVG por encima de todo. Le quita el
   plástico a los degradados grandes; a tamaño 1 es casi invisible y es justo
   lo que hace falta. */
.fx-grain::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  opacity: 0.4;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/><feColorMatrix type='saturate' values='0'/></filter><rect width='160' height='160' filter='url(%23n)' opacity='0.5'/></svg>");
  background-size: 160px 160px;
}
/* El grano que además tiembla. Cuatro fotogramas bastan: más ya no se ve. */
.fx-grain--live::after { animation: fx-grain-jump 0.5s steps(1) infinite; }
@keyframes fx-grain-jump {
  0%   { transform: translate(0, 0); }
  25%  { transform: translate(-3%, 2%); }
  50%  { transform: translate(2%, -3%); }
  75%  { transform: translate(-2%, -2%); }
  100% { transform: translate(0, 0); }
}

/* B7. Haz de luz: una franja clara que cruza la losa oscura cada pocos
   segundos, como el reflejo de un cristal. */
.fx-beam::before {
  content: "";
  position: absolute;
  top: -50%;
  bottom: -50%;
  width: 46%;
  left: -60%;
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, 0.12), transparent);
  transform: skewX(-14deg);
  animation: fx-beam-pass 7s ease-in-out infinite;
}
@keyframes fx-beam-pass {
  0%, 62% { left: -60%; }
  100%    { left: 130%; }
}

/* B8. Escáner: una línea que baja en bucle, con un rastro. Sobre la losa
   oscura recuerda a un panel de vigilancia, que es justo el tema. */
.fx-scan::before {
  content: "";
  position: absolute;
  left: 0; right: 0;
  height: 180px;
  background: linear-gradient(to bottom, transparent, rgba(241, 200, 15, 0.16), transparent);
  animation: fx-scan-down 6s linear infinite;
}
.fx-scan::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image: repeating-linear-gradient(
    to bottom,
    rgba(255, 255, 255, 0.035) 0 1px,
    transparent 1px 3px
  );
}
@keyframes fx-scan-down {
  from { top: -180px; }
  to   { top: 100%; }
}

/* B9. Foco que sigue al puntero. La variable la escribe effects.js; el
   degradado no se recalcula, sólo se mueve, así que sale barato. */
.fx-spot::before {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(
    420px circle at var(--fx-mx) var(--fx-my),
    rgba(241, 200, 15, 0.16),
    transparent 70%
  );
  transition: opacity 0.3s var(--ease);
}

/* B10. Estrellas: puntos minúsculos que parpadean. Tres capas con tamaños y
   velocidades distintas para que no se vea el patrón. */
.fx-stars::before,
.fx-stars::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image:
    radial-gradient(1px 1px at 20% 30%, rgba(255, 255, 255, 0.8) 50%, transparent),
    radial-gradient(1px 1px at 70% 15%, rgba(255, 255, 255, 0.6) 50%, transparent),
    radial-gradient(1.5px 1.5px at 45% 70%, rgba(241, 200, 15, 0.7) 50%, transparent),
    radial-gradient(1px 1px at 85% 55%, rgba(255, 255, 255, 0.5) 50%, transparent),
    radial-gradient(1px 1px at 10% 80%, rgba(6, 182, 212, 0.6) 50%, transparent);
  background-size: 340px 340px;
  animation: fx-twinkle 4s ease-in-out infinite;
}
.fx-stars::after {
  background-size: 210px 210px;
  animation-delay: -2s;
  animation-duration: 6s;
  opacity: 0.6;
}
@keyframes fx-twinkle {
  0%, 100% { opacity: 0.85; }
  50%      { opacity: 0.35; }
}

/* --------------------------------------------------------------------------
   C. Superficies: lo que le pasa a una tarjeta cuando te acercas
   -------------------------------------------------------------------------- */

/* C1. Borde vivo: un cónico girando debajo de la tarjeta, recortado a un
   marco de 1px con dos máscaras. Es el truco del degradado como borde sin
   pintar cuatro pseudoelementos. */
.fx-border {
  position: relative;
  border-radius: var(--radius-m);
  isolation: isolate;
}
.fx-border::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  padding: 1px;
  background: conic-gradient(
    from var(--fx-angle, 0deg),
    transparent 0turn,
    var(--amber) 0.12turn,
    var(--fx-violet) 0.25turn,
    transparent 0.4turn,
    transparent 1turn
  );
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  opacity: 0;
  transition: opacity 0.35s var(--ease);
}
.fx-border:hover::before,
.fx-border:focus-within::before { opacity: 1; }
/* El giro necesita una propiedad animable: un ángulo registrado. Sin
   @property el navegador interpola entre dos cadenas, es decir, no interpola. */
.fx-border--always::before {
  opacity: 1;
  animation: fx-turn 4s linear infinite;
}
@property --fx-angle {
  syntax: "<angle>";
  initial-value: 0deg;
  inherits: false;
}
@keyframes fx-turn {
  to { --fx-angle: 1turn; }
}

/* C2. Brillo: una banda diagonal que barre la tarjeta al pasar por encima.
   El clásico de las tarjetas de producto, y sigue funcionando. */
.fx-shine {
  position: relative;
  overflow: hidden;
}
.fx-shine::after {
  content: "";
  position: absolute;
  top: -60%;
  bottom: -60%;
  width: 40%;
  left: -70%;
  pointer-events: none;
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, 0.5), transparent);
  transform: skewX(-18deg);
  transition: left 0.75s var(--ease);
}
.fx-shine:hover::after { left: 130%; }
.slab .fx-shine::after { background: linear-gradient(100deg, transparent, rgba(255, 255, 255, 0.14), transparent); }

/* C3. Foco dentro de la tarjeta: el mismo halo del fondo, pero recortado a
   una sola tarjeta y encendiéndose sólo cuando el puntero está dentro. */
.fx-card-spot {
  position: relative;
  overflow: hidden;
}
.fx-card-spot::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  background: radial-gradient(
    240px circle at var(--fx-mx) var(--fx-my),
    rgba(241, 200, 15, 0.22),
    transparent 70%
  );
  transition: opacity 0.3s var(--ease);
}
.fx-card-spot:hover::before { opacity: 1; }
.slab .fx-card-spot::before {
  background: radial-gradient(
    240px circle at var(--fx-mx) var(--fx-my),
    rgba(241, 200, 15, 0.16),
    transparent 70%
  );
}

/* C4. Inclinación: la tarjeta gira un par de grados siguiendo al puntero.
   Los ángulos los pone effects.js; aquí sólo se declara la perspectiva y la
   vuelta al reposo. */
.fx-tilt {
  transform-style: preserve-3d;
  transition: transform 0.4s var(--ease);
  will-change: transform;
}
.fx-tilt.is-tilting { transition: transform 0.08s linear; }
.fx-tilt > * { transform: translateZ(0); }
/* Un hijo puede despegarse del plano de la tarjeta: es lo que hace que la
   inclinación se lea como profundidad y no como una tarjeta torcida. */
.fx-tilt .fx-pop { transform: translateZ(38px); }

/* C5. Elevación con floración: sombra que crece y color que se cuela por
   debajo. Menos aparatoso que la inclinación y sienta bien a cualquier lista. */
.fx-lift {
  transition: transform 0.35s var(--ease), box-shadow 0.35s var(--ease);
}
.fx-lift:hover {
  transform: translateY(-6px);
  box-shadow: 0 30px 60px -32px rgba(10, 10, 11, 0.45), 0 0 0 1px var(--line-2);
}
.fx-lift--amber:hover {
  box-shadow: 0 30px 60px -30px rgba(241, 200, 15, 0.55), 0 0 0 1px rgba(241, 200, 15, 0.35);
}

/* C6. Cristal: fondo translúcido con desenfoque de lo que hay detrás. Sólo
   luce encima de algo con color; sobre blanco no se ve nada. */
.fx-glass {
  background: rgba(255, 255, 255, 0.62);
  backdrop-filter: blur(14px) saturate(1.4);
  -webkit-backdrop-filter: blur(14px) saturate(1.4);
  border: 1px solid rgba(255, 255, 255, 0.5);
}
.fx-glass--dark {
  background: rgba(10, 10, 11, 0.45);
  border-color: rgba(255, 255, 255, 0.12);
}

/* C7. Recorte al pasar: la tarjeta revela una segunda capa de color desde la
   esquina. Sin JS y sin mover nada de sitio. */
.fx-wipe { position: relative; overflow: hidden; }
.fx-wipe::before {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(135deg, var(--amber), rgba(241, 200, 15, 0.15));
  clip-path: circle(0% at 8% 92%);
  transition: clip-path 0.55s var(--ease);
}
.fx-wipe:hover::before { clip-path: circle(140% at 8% 92%); }
.fx-wipe > * { position: relative; }

/* --------------------------------------------------------------------------
   D. Texto
   -------------------------------------------------------------------------- */

/* D1. Degradado animado dentro de las letras. El recorte al texto obliga a
   pintar el fondo a tamaño doble y moverlo: si no, no hay nada que animar. */
.fx-gradient-text {
  background: linear-gradient(100deg, var(--fg), var(--amber-deep), var(--fx-violet), var(--fg));
  background-size: 300% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: fx-slide-bg 9s linear infinite;
}
.slab .fx-gradient-text,
.fx-gradient-text--dark {
  background-image: linear-gradient(100deg, #fff, var(--amber), var(--fx-cyan), #fff);
}
@keyframes fx-slide-bg {
  to { background-position: 300% 0; }
}

/* D2. Resaltado que se dibuja: una barra de color que crece por detrás de
   una palabra cuando la sección entra. Es el subrayado a rotulador. */
.fx-mark {
  position: relative;
  display: inline-block;
}
.fx-mark::before {
  content: "";
  position: absolute;
  left: -2px; right: -2px;
  bottom: 0.08em;
  height: 0.42em;
  z-index: -1;
  border-radius: 3px;
  background: var(--amber);
}
/* Sin la capa de efectos viva, el rotulador ya está pintado: no se dibuja,
   pero tampoco desaparece. */
html.fx .fx-mark::before {
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.7s var(--ease);
}
html.fx .fx-mark.is-in::before,
html.fx .is-in .fx-mark::before { transform: scaleX(1); }

/* D3. Entrada por palabras. effects.js parte el texto en <span> y aquí se
   escalonan con una variable de índice. El desenfoque es lo que lo separa de
   un fundido normal. */
html.fx .fx-words .fx-word {
  display: inline-block;
  opacity: 0;
  transform: translateY(0.5em) rotateX(45deg);
  filter: blur(6px);
  transition:
    opacity 0.7s var(--ease) calc(var(--i, 0) * 45ms),
    transform 0.7s var(--ease) calc(var(--i, 0) * 45ms),
    filter 0.7s var(--ease) calc(var(--i, 0) * 45ms);
}
html.fx .fx-words.is-in .fx-word {
  opacity: 1;
  transform: none;
  filter: none;
}

/* D4. Entrada por letras con máscara: cada letra sube desde debajo de una
   línea invisible. Más limpio que el desenfoque para titulares cortos. */
.fx-chars .fx-line { display: inline-block; overflow: hidden; vertical-align: bottom; }
html.fx .fx-chars .fx-char {
  display: inline-block;
  transform: translateY(105%);
  transition: transform 0.75s var(--ease) calc(var(--i, 0) * 22ms);
}
html.fx .fx-chars.is-in .fx-char { transform: none; }

/* D5. Subrayado que se dibuja al pasar, de izquierda a derecha, y se retira
   por el mismo lado al salir. El truco está en cambiar el origen. */
.fx-underline { position: relative; }
.fx-underline::after {
  content: "";
  position: absolute;
  left: 0; right: 0;
  bottom: -3px;
  height: 1.5px;
  background: var(--amber);
  transform: scaleX(0);
  transform-origin: right;
  transition: transform 0.35s var(--ease);
}
.fx-underline:hover::after,
.fx-underline:focus-visible::after {
  transform: scaleX(1);
  transform-origin: left;
}

/* D6. Contador: el número sube hasta su valor al entrar en pantalla. El
   valor final está en el HTML —así se lee sin JS— y effects.js lo recorre.
   La anchura tabular evita que el bloque baile mientras cuenta. */
.fx-count { font-variant-numeric: tabular-nums; }

/* D7. Texto que se descifra: caracteres aleatorios que se van fijando. Muy
   temático para una web de ciberseguridad, y muy fácil de que canse. */
.fx-scramble { font-family: var(--mono); }

/* --------------------------------------------------------------------------
   E. Scroll
   -------------------------------------------------------------------------- */

/* E1. Barra de progreso de lectura, pegada bajo la navegación. Con
   animation-timeline el navegador la mueve solo, sin escuchar el scroll;
   donde no exista, effects.js escribe --fx-scroll y sale igual. */
.fx-progress {
  position: fixed;
  top: 0; left: 0; right: 0;
  height: 3px;
  z-index: 60;
  background: linear-gradient(90deg, var(--amber), var(--fx-violet), var(--fx-cyan));
  transform-origin: left;
  transform: scaleX(var(--fx-scroll, 0));
  pointer-events: none;
}
@supports (animation-timeline: scroll()) {
  .fx-progress {
    animation: fx-grow linear;
    animation-timeline: scroll(root block);
  }
}
@keyframes fx-grow {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

/* E2. Revelados. Variantes de .reveal de styles.css, que sólo hace un
   desplazamiento hacia arriba. Todas se encienden con la misma clase .is-in
   que ya pone app.js, así que no hacen falta dos observadores. */
html.fx [class*="fx-in-"] { transition: opacity 0.9s var(--ease), transform 0.9s var(--ease), filter 0.9s var(--ease), clip-path 0.9s var(--ease); }
html.fx .fx-in-left  { opacity: 0; transform: translateX(-48px); }
html.fx .fx-in-right { opacity: 0; transform: translateX(48px); }
html.fx .fx-in-scale { opacity: 0; transform: scale(0.9); }
html.fx .fx-in-blur  { opacity: 0; filter: blur(14px); transform: translateY(14px); }
html.fx .fx-in-clip  { clip-path: inset(0 100% 0 0); }
html.fx .fx-in-rise  { opacity: 0; transform: translateY(60px) scale(0.97); }
html.fx .fx-in-flip  { opacity: 0; transform: perspective(900px) rotateX(-22deg); transform-origin: top; }
html.fx .fx-in-swing { opacity: 0; transform: rotate(-4deg) translateY(36px); }

html.fx [class*="fx-in-"].is-in,
html.fx .is-in > [class*="fx-in-"] {
  opacity: 1;
  transform: none;
  filter: none;
  clip-path: inset(0 0 0 0);
}

/* E3. Escalonado: los hijos entran uno detrás de otro. El índice lo pone
   effects.js con --i; sin él todos comparten retardo cero y entran juntos,
   que es un fallo aceptable. */
html.fx .fx-stagger > * {
  opacity: 0;
  transform: translateY(14px);
}
html.fx .fx-stagger > * {
  transition:
    opacity 0.45s var(--ease) calc(var(--i, 0) * 40ms),
    transform 0.45s var(--ease) calc(var(--i, 0) * 40ms);
}
html.fx .fx-stagger.is-in > * { opacity: 1; transform: none; }

/* E3b. Texto que se enciende al leerlo: cada palabra pasa de apagada a
   encendida según cuánto has bajado por el párrafo. La opacidad no se anima
   con una transición, se calcula: --p es el avance del bloque (0 a 1) y --n
   cuántas palabras hay, así que --p * --n es la palabra por la que va la
   lectura y cada una se compara con la suya.

   Es el efecto que más se ve en una página larga y el que peor sienta a un
   párrafo corto: por debajo de dos líneas no da tiempo a que se encienda. */
.fx-text-fill { --p: 0; }
html.fx .fx-text-fill .fx-word {
  opacity: clamp(0.16, calc((var(--p, 0) * var(--n, 1) - var(--i, 0)) * 0.55 + 0.16), 1);
  transform: none;
  filter: none;
  transition: none;
}

/* E4. Parallax: el elemento se mueve más despacio que la página. El
   desplazamiento lo escribe effects.js en --fx-shift a partir de
   data-parallax; aquí sólo se aplica. */
.fx-parallax {
  transform: translate3d(0, var(--fx-shift, 0px), 0);
  will-change: transform;
}

/* E5. Apilado: las tarjetas se quedan pegadas y la siguiente se les monta
   encima. Todo position: sticky, ni una línea de JS. */
.fx-stack { display: grid; gap: 0; }
.fx-stack > * {
  position: sticky;
  top: 120px;
}
.fx-stack > *:nth-child(1) { top: 104px; }
.fx-stack > *:nth-child(2) { top: 124px; }
.fx-stack > *:nth-child(3) { top: 144px; }
.fx-stack > *:nth-child(4) { top: 164px; }

/* E6. Escalado por scroll con animation-timeline: la tarjeta crece al entrar
   en pantalla y el navegador lo calcula sin JS. Donde no existe no pasa nada
   —el elemento se queda a su tamaño—, así que es mejora pura. */
@supports (animation-timeline: view()) {
  .fx-scroll-scale {
    animation: fx-pop-in linear both;
    animation-timeline: view();
    animation-range: entry 8% cover 34%;
  }
  .fx-scroll-fade {
    animation: fx-fade-edges linear both;
    animation-timeline: view();
    animation-range: entry 0% cover 30%;
  }
  /* La imagen del panel se endereza conforme sube. */
  .fx-scroll-straighten {
    animation: fx-straighten linear both;
    animation-timeline: view();
    animation-range: entry 20% cover 60%;
  }
}
@keyframes fx-pop-in {
  from { opacity: 0; transform: translateY(70px) scale(0.86); }
  to   { opacity: 1; transform: none; }
}
@keyframes fx-fade-edges {
  from { opacity: 0.15; filter: blur(8px); }
  to   { opacity: 1; filter: none; }
}
@keyframes fx-straighten {
  from { transform: perspective(1400px) rotateX(16deg) scale(0.93); }
  to   { transform: perspective(1400px) rotateX(0deg) scale(1); }
}

/* E7. La imagen del hero, inclinada, que se endereza con el scroll. Versión
   con JS para los navegadores sin animation-timeline: --fx-tilt la escribe
   effects.js. */
.fx-hero-shot {
  transform: perspective(1600px) rotateX(var(--fx-tilt, 0deg)) scale(var(--fx-zoom, 1));
  transform-origin: 50% 0;
  will-change: transform;
}

/* --------------------------------------------------------------------------
   F. Micro: cosas pequeñas que sólo se notan cuando faltan
   -------------------------------------------------------------------------- */

/* F1. Imán: el botón se acerca al puntero antes de que llegues. El
   desplazamiento lo calcula effects.js y no pasa de 8px: más ya es pegajoso. */
.fx-magnet {
  transform: translate3d(var(--fx-dx, 0px), var(--fx-dy, 0px), 0);
  transition: transform 0.35s var(--ease);
}
.fx-magnet.is-pulled { transition: transform 0.12s linear; }

/* F2. Anillos: dos ondas que salen del icono, como un radar. Van en el punto
   verde de "phishing activo" y en las fichas de icono. */
.fx-ring { position: relative; }
.fx-ring::before,
.fx-ring::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  border: 1.5px solid currentColor;
  opacity: 0;
  animation: fx-ripple 2.6s var(--ease) infinite;
}
.fx-ring::after { animation-delay: 1.3s; }
@keyframes fx-ripple {
  0%   { transform: scale(1); opacity: 0.55; }
  100% { transform: scale(2.1); opacity: 0; }
}

/* F3. Medidores que se llenan al entrar en pantalla. El ancho final lo lleva
   el HTML en --fx-fill; sin JS ya está lleno y no se pierde el dato. */
html.fx .fx-meter > i {
  width: 0;
  transition: width 1.2s var(--ease);
}
html.fx .fx-meter.is-in > i { width: var(--fx-fill, 68%); }

/* F4. Las barras del panel crecen desde abajo, una detrás de otra. */
html.fx .fx-bars i {
  transform: scaleY(0);
  transform-origin: bottom;
  transition: transform 0.45s var(--ease) calc(var(--i, 0) * 40ms);
}
html.fx .fx-bars.is-in i { transform: scaleY(1); }

/* F5. Cursor: un círculo que persigue al puntero con retraso y crece encima
   de lo que se puede pulsar. Sólo con ratón: en táctil no hay puntero al que
   seguir y en un portátil sin ratón tampoco aporta. */
.fx-cursor {
  position: fixed;
  top: 0; left: 0;
  width: 26px;
  height: 26px;
  margin: -13px 0 0 -13px;
  border-radius: 50%;
  border: 1.5px solid var(--amber-deep);
  pointer-events: none;
  z-index: 90;
  opacity: 0;
  transform: translate3d(var(--fx-cx, 0px), var(--fx-cy, 0px), 0) scale(var(--fx-cs, 1));
  transition: opacity 0.25s var(--ease), width 0.25s var(--ease), height 0.25s var(--ease);
  mix-blend-mode: difference;
}
.fx-cursor.is-on { opacity: 1; }
.fx-cursor.is-over { width: 54px; height: 54px; margin: -27px 0 0 -27px; background: rgba(241, 200, 15, 0.16); }
@media (pointer: coarse) {
  .fx-cursor { display: none; }
}

/* F6. Marquesina con bordes difuminados y que acelera al pasar por encima.
   styles.css ya la pausa al hover; esto es la alternativa: en vez de parar,
   corre. */
.fx-marquee {
  mask-image: linear-gradient(to right, transparent, #000 12%, #000 88%, transparent);
  -webkit-mask-image: linear-gradient(to right, transparent, #000 12%, #000 88%, transparent);
}
.fx-marquee .logos__track { animation-duration: 42s; transition: none; }
.fx-marquee:hover .logos__track { animation-play-state: running; animation-duration: 14s; }

/* F7. Botón con pulso de color: el fondo late muy despacio para llamar la
   atención sin moverse de sitio. */
.fx-breathe {
  animation: fx-breathe 3.4s ease-in-out infinite;
}
@keyframes fx-breathe {
  0%, 100% { box-shadow: 0 0 0 0 rgba(241, 200, 15, 0.5); }
  70%      { box-shadow: 0 0 0 16px rgba(241, 200, 15, 0); }
}

/* F8. Icono que gira 360º al pasar por la tarjeta entera, no sólo por él. */
.fx-spin-icon svg { transition: transform 0.6s var(--ease); }
.fx-spin-icon:hover svg,
:hover > .fx-spin-icon svg { transform: rotate(360deg); }

/* F9. Flecha que se escapa hacia la derecha. */
.fx-arrow svg { transition: transform 0.3s var(--ease); }
.fx-arrow:hover svg { transform: translateX(5px); }

/* F10. Subrayado de la navegación, con la barra creciendo desde el centro. */
.fx-nav-line { position: relative; }
.fx-nav-line::after {
  content: "";
  position: absolute;
  left: 0; right: 0;
  bottom: -6px;
  height: 2px;
  border-radius: 2px;
  background: var(--amber);
  transform: scaleX(0);
  transition: transform 0.3s var(--ease);
}
.fx-nav-line:hover::after,
.fx-nav-line[aria-current="page"]::after { transform: scaleX(1); }

/* --------------------------------------------------------------------------
   G. Reducción de movimiento

   Quien pide menos movimiento no pide "casi nada": pide nada. Lo que se
   apaga aquí es el bucle infinito y el desplazamiento; el color y el
   contraste se quedan, porque son información.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  .fx-aurora i,
  .fx-mesh::before,
  .fx-grid::before,
  .fx-floor::before,
  .fx-grain--live::after,
  .fx-beam::before,
  .fx-scan::before,
  .fx-stars::before,
  .fx-stars::after,
  .fx-gradient-text,
  .fx-border--always::before,
  .fx-ring::before,
  .fx-ring::after,
  .fx-breathe,
  .fx-progress { animation: none !important; }

  .fx-words .fx-word,
  .fx-chars .fx-char,
  .fx-stagger > *,
  [class*="fx-in-"] {
    opacity: 1 !important;
    transform: none !important;
    filter: none !important;
    clip-path: none !important;
    transition: none !important;
  }

  .fx-text-fill .fx-word { opacity: 1 !important; }

  .fx-parallax,
  .fx-hero-shot,
  .fx-magnet { transform: none !important; }

  .fx-cursor { display: none; }
  .fx-stack > * { position: static; }
  .fx-meter > i { width: var(--fx-fill, 68%); transition: none; }
  .fx-bars i { transform: none; transition: none; }
  .fx-mark::before { transform: scaleX(1); transition: none; }
}

/* --------------------------------------------------------------------------
   H. Catálogo (/efectos)

   Sólo lo usa la página de muestra. Cuando se decida qué efectos se quedan,
   este bloque y esa página se van juntos.
   -------------------------------------------------------------------------- */
.fx-demo-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  gap: 22px;
}
.fx-demo {
  position: relative;
  min-height: 230px;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 8px;
  padding: 22px;
  border: 1px solid var(--line);
  border-radius: var(--radius-m);
  background: var(--surface);
  overflow: hidden;
}
.fx-demo--ink {
  background: var(--ink);
  color: var(--ink-fg);
  border-color: rgba(255, 255, 255, 0.12);
}
.fx-demo--tall { min-height: 320px; }
.fx-demo__name {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.06em;
  color: var(--amber-deep);
}
.fx-demo--ink .fx-demo__name { color: var(--amber); }
.fx-demo__what { font-size: 14px; line-height: 1.5; color: var(--fg-body); }
.fx-demo--ink .fx-demo__what { color: var(--ink-text); }
.fx-demo__big {
  font-size: 30px;
  font-weight: 600;
  letter-spacing: -0.03em;
}
.fx-demo__hint {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--fg-faint);
}
.fx-demo--ink .fx-demo__hint { color: rgba(245, 245, 245, 0.45); }

/* Un espaciador alto para poder probar de verdad lo que depende del scroll. */
.fx-demo-gap {
  display: grid;
  place-items: center;
  height: 70vh;
  color: var(--fg-faint);
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
}
