/* ═══════════════════════════════════════════════════════════════
   TEXTO QUE SE ESCRIBE SOLO — typing.js: palabras que se revelan,
   neón, glitch y sus variantes por calidad y dispositivo. El tamaño y
   el ancho del texto los pone cada página.
   ═══════════════════════════════════════════════════════════════ */
.typing {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 650;
  line-height: 1.52;
  letter-spacing: 0;
  white-space: pre-wrap;
  text-wrap: pretty;
  text-shadow: 0 0 14px rgba(0, 255, 156, 0.18), 0 0 28px rgba(0, 212, 255, 0.08);
}

/* Sin will-change (PLAN-04 F3): forzaba una capa por palabra (~30), cada una con su filtro, que
   la GPU volvía a aplicar en cada frame. Una palabra pasa al compositor solo mientras algo suyo
   se anima. */
.typing .word {
  display: inline-block;
  position: relative;
  color: rgba(232, 255, 244, 0.9);
  filter: blur(5px);
  opacity: 0.28;
  transform: translateY(0.1em);
  text-shadow: 0 0 2px rgba(0, 255, 156, 0.18);
}

/* El revelado: de desenfocada y tenue a nítida, con una transición en el compositor.
   typing.js solo le pone la duración (--revelado, según las letras de la palabra) y le cambia
   las letras; ya no escribe su filtro, su opacidad ni su posición en cada frame. */
.typing .word--active {
  color: rgba(238, 255, 247, 0.98);
  filter: blur(0);
  opacity: 1;
  transform: none;
  /* El foco termina antes (60 % del revelado): blur() no va al compositor, y si su transición
     siguiera corriendo cuando la palabra se resuelve, Chrome dejaría fuera del compositor
     también su aparición y su respiración ("otra animación incompatible"). */
  transition:
    filter calc(var(--revelado, 350ms) * 0.6) cubic-bezier(0.33, 1, 0.68, 1),
    opacity calc(var(--revelado, 350ms) * 0.85) cubic-bezier(0.33, 1, 0.68, 1),
    transform calc(var(--revelado, 350ms) * 0.85) cubic-bezier(0.33, 1, 0.68, 1);
  text-shadow:
    0 0 5px rgba(230, 255, 242, 0.4),
    0 0 12px rgba(0, 255, 156, 0.52),
    0 0 26px rgba(0, 255, 156, 0.24),
    0 0 42px rgba(0, 212, 255, 0.12);
}

/* Un solo brillo, pintado una vez: el halo de 3 px que antes daba un drop-shadow aparte (un
   filtro que la GPU reaplicaba en cada frame a cada palabra) va como la primera capa. */
.typing .word--resolved {
  color: rgba(239, 255, 248, 0.98);
  filter: none;
  opacity: 1;
  transform: none;
  /* Cancela lo que quede del revelado: una transición en curso cuyo final coincide con el
     estado nuevo sigue corriendo (así lo pide la especificación) y, junto a la aparición,
     dejaba a las dos fuera del compositor. Sin transition-property, se cancela. */
  transition: none;
  text-shadow:
    0 0 3px rgba(0, 255, 156, var(--word-glow-a, 0.3)),
    0 0 3px rgba(232, 255, 244, 0.44),
    0 0 10px rgba(0, 255, 156, 0.5),
    0 0 24px rgba(0, 255, 156, var(--word-glow-a, 0.3)),
    0 0 46px rgba(0, 212, 255, 0.12);
}

/* La aparición, sin `fill` que la deje puesta para siempre (`backwards`: solo antes de
   empezar); al terminar manda el estado resuelto, que es igual a su último cuadro. */
.quality-medium .typing .word--resolved,
.quality-high .typing .word--resolved {
  animation: word-neon-pop 680ms cubic-bezier(0.16, 1, 0.3, 1) backwards;
}

/* La respiración entra cuando la aparición termina (typing.js pone .word--respira en su
   animationend). Si arrancan juntas, Chrome no lleva ninguna al compositor: las dos animan la
   opacidad de la misma palabra (compositor.mjs, 2026-09-29). Una detrás de la otra, sí. */
.quality-high .typing .word--respira {
  animation: word-neon-breathe 5.4s ease-in-out infinite;
  animation-delay: var(--word-glow-delay, 0ms);
}

.typing .word--glitch {
  color: rgba(230, 255, 242, 0.98);
  text-shadow:
    0 0 6px rgba(230, 255, 242, 0.52),
    0 0 18px rgba(0, 255, 156, 0.68),
    0 0 34px rgba(0, 255, 156, 0.38),
    0 0 56px rgba(0, 212, 255, 0.2);
  animation: word-glitch 160ms steps(2, end) infinite;
}

/* El destello de la aparición: antes animaba text-shadow y drop-shadow, que no van al
   compositor y además arrastraban a la respiración (compositeFailed 64). Ahora el pico de
   brillo lo da brightness(): aviva la palabra y su halo juntos. Sin blur(): Chrome no lleva al
   compositor un filtro que mueve píxeles (medido con compositor.mjs el 2026-09-29), y la
   palabra ya llega enfocada del revelado. Así todo son transform, opacity y brightness(). */
@keyframes word-neon-pop {
  0% {
    opacity: 0.35;
    transform: translateY(0.18em) scale(0.97);
    filter: brightness(1);
  }
  52% {
    opacity: 1;
    transform: translateY(-0.03em) scale(1.025);
    filter: brightness(1.55);
  }
  100% {
    opacity: 1;
    transform: translateY(0) scale(1);
    filter: brightness(1);
  }
}

/*
 * Respiración del neón SOLO por opacidad: animar text-shadow/drop-shadow
 * por palabra forzaba un repaint eterno de toda la sección (medido: la
 * página entera caía de 60 a ~41 fps en desktop). La opacidad se compone
 * en el compositor (GPU) sin repintar; el glow estático del estado
 * .word--resolved sigue ahí y el pulso se percibe igual.
 */
@keyframes word-neon-breathe {
  0%,
  100% {
    opacity: 0.86;
  }
  45% {
    opacity: 1;
  }
}

@keyframes word-glitch {
  0% {
    opacity: 1;
  }
  50% {
    opacity: 0.55;
  }
  100% {
    opacity: 1;
  }
}

/* En pantallas angostas, las que faltan se desenfocan menos. Solo las que esperan: si la
   activa entrara aquí, esta regla (más específica) le ganaría al revelado y la dejaría borrosa. */
@media (max-width: 720px) {
  .typing .word:not(.word--resolved, .word--active) {
    filter: blur(3px);
  }
}

@media (prefers-reduced-motion: reduce) {
  html:not(.svf-motion-override) .typing .word--glitch {
    animation: none;
  }

  html:not(.svf-motion-override) .typing .word--resolved,
  html:not(.svf-motion-override) .typing .word--respira {
    animation: none !important;
  }
}

/* ─────────────────────────────────────────────────────────────────
   MOBILE - Efectos mejorados para animación de escritura
   ───────────────────────────────────────────────────────────────── */

/* Mobile Medium/High: Glow sutil en palabras activas */
.is-mobile.quality-medium .typing .word--active,
.is-mobile.quality-high .typing .word--active {
  text-shadow:
    0 0 6px rgba(230, 255, 242, 0.38),
    0 0 13px rgba(0, 255, 156, 0.48),
    0 0 24px rgba(0, 212, 255, 0.16);
}

/* Mobile Medium/High: palabras resueltas con brillo suave, también en una sola capa. */
.is-mobile.quality-medium .typing .word--resolved,
.is-mobile.quality-high .typing .word--resolved {
  text-shadow:
    0 0 3px rgba(0, 255, 156, var(--word-glow-a, 0.3)),
    0 0 3px rgba(232, 255, 244, 0.42),
    0 0 10px rgba(0, 255, 156, 0.42),
    0 0 22px rgba(0, 255, 156, 0.22),
    0 0 34px rgba(0, 212, 255, 0.1);
}

/* Mobile High: entrada más corta, con su destello por brightness() (en el compositor), y
   después su respiración, más lenta (la misma regla de turnos que en escritorio). */
.is-mobile.quality-high .typing .word--resolved {
  animation: word-appear-mobile 520ms ease-out backwards;
}

.is-mobile.quality-high .typing .word--respira {
  animation: word-neon-breathe 6.2s ease-in-out infinite;
  animation-delay: var(--word-glow-delay, 0ms);
}

@keyframes word-appear-mobile {
  0% {
    opacity: 0.5;
    transform: translateY(0.08em) scale(0.98);
    filter: brightness(1);
  }
  50% {
    filter: brightness(1.35);
  }
  100% {
    opacity: 1;
    transform: translateY(0) scale(1);
    filter: brightness(1);
  }
}
