/* ============================================================================
   VialPro · arena.css — v2 · capa de vida compartida por los juegos
   ----------------------------------------------------------------------------
   Se carga DESPUÉS del <style> propio de cada juego, así que solo añade; no
   pisa el tema de ninguno. Cada juego mantiene su color y su fondo.

   Qué arregla, y por qué importa más de lo que parece:

   · LAS PREGUNTAS APARECÍAN DE GOLPE. Enunciado y opciones se cambiaban en el
     mismo fotograma. Eso lee como "se ha recargado la página", no como "viene
     la siguiente". Ahora entran escalonadas, 45 ms entre ellas: el ojo las
     sigue en orden y el juego respira.

   · ACERTAR Y FALLAR SE SENTÍAN IGUAL. Tres de los cuatro juegos no importan
     juice.js: no tenían ni sacudida ni partículas, solo un borde de color.
     Ahora hay un fotograma de destello (verde o rojo) a pantalla completa —
     el recurso más barato que existe para que un fallo duela.

   · EL TIEMPO NO SE SENTÍA. Había un número que se ponía rojo. Ahora, cuando
     quedan pocos segundos, los bordes de la pantalla laten en rojo. La
     urgencia se percibe en la periferia, que es donde el ojo detecta el
     peligro, sin tapar el contenido que hay que leer.

   · PERDER UNA VIDA ERA UN scale(1.25). Ahora el corazón se hincha, se parte
     con un giro y cae desvaneciéndose.

   · NO HABÍA REACCIÓN AL TOCAR. Las opciones no se hundían al pulsarlas. Ese
     hundimiento de 2 px es la mitad de la sensación de "botón real".

   Todo se anima con transform/opacity, que es lo único que se compone en GPU.
   Ni un box-shadow ni un filter dentro de un bucle: en un móvil de gama media
   eso solo se paga en fotogramas perdidos. Y todo se apaga entero con
   prefers-reduced-motion.
   ========================================================================== */

/* ── Entrada escalonada de la pregunta ────────────────────────────────────── */
@keyframes vpRise {
  from { opacity: 0; transform: translate3d(0, 16px, 0) scale(.985); }
  to   { opacity: 1; transform: translate3d(0, 0, 0) scale(1); }
}
.vp-enter { animation: vpRise .42s cubic-bezier(.22, 1, .36, 1) both; }

.vp-enter-1 { animation-delay: .00s }
.vp-enter-2 { animation-delay: .05s }
.vp-enter-3 { animation-delay: .10s }
.vp-enter-4 { animation-delay: .15s }
.vp-enter-5 { animation-delay: .20s }

/* Salida hacia atrás, no hacia un lado: da profundidad y deja claro que la
   pregunta anterior "se archiva" en vez de simplemente irse. */
@keyframes vpLeave {
  from { opacity: 1; transform: translate3d(0, 0, 0) scale(1); }
  to   { opacity: 0; transform: translate3d(0, -14px, 0) scale(.96); }
}
.vp-leave { animation: vpLeave .23s cubic-bezier(.4, 0, 1, 1) both; }

/* ── La foto de la pregunta ───────────────────────────────────────────────── */
/* Se cambiaba con `style.backgroundImage = url(...)`: un fotograma con la foto
   vieja y al siguiente la nueva, a veces a medio descargar. El fundido corto
   con un empuje de escala tapa el salto y de paso da profundidad — el mismo
   truco que un documental usa para que una foto fija no parezca muerta. */
@keyframes vpImgIn {
  from { opacity: 0; transform: scale(1.06); }
  to   { opacity: 1; transform: scale(1); }
}
.vp-imgin { animation: vpImgIn .5s cubic-bezier(.22, 1, .36, 1) both; }

/* ── Destello de acierto / fallo ──────────────────────────────────────────── */
/* Un solo nodo reutilizado, fijo, sin eventos y sin repintar layout.
   Sube rápido (el golpe) y baja lento (el eco): al revés se siente barato. */
.vp-flash {
  position: fixed;
  inset: 0;
  z-index: 9990;
  pointer-events: none;
  opacity: 0;
}
.vp-flash-ok  { background: radial-gradient(ellipse 90% 70% at 50% 50%,
                rgba(64, 255, 160, .30) 0%, rgba(64, 255, 160, 0) 72%); }
.vp-flash-bad { background: radial-gradient(ellipse 90% 70% at 50% 50%,
                rgba(255, 48, 60, .42) 0%, rgba(255, 48, 60, 0) 72%); }
.vp-flash-on  { animation: vpFlash .38s ease-out both; }
@keyframes vpFlash {
  0%   { opacity: 0 }
  12%  { opacity: 1 }
  100% { opacity: 0 }
}

/* ── Urgencia de tiempo ───────────────────────────────────────────────────── */
/* Un velo que solo vive en los bordes: avisa sin tapar lo que hay que leer. */
.vp-urgency {
  position: fixed;
  inset: 0;
  z-index: 9988;
  pointer-events: none;
  opacity: 0;
  background: radial-gradient(ellipse 78% 62% at 50% 50%,
              transparent 55%, rgba(255, 45, 55, .55) 100%);
  transition: opacity .3s ease-out;
}
.vp-urgency.vp-on { animation: vpPulse 1s ease-in-out infinite; }
@keyframes vpPulse { 0%, 100% { opacity: .25 } 50% { opacity: .7 } }
.vp-urgency.vp-critical { animation-duration: .48s; }

/* Pocas vidas ≠ poco tiempo. Esto es fondo emocional, no alarma: entra despacio
   y se queda quieto. Si latiera, en dos partidas dejaría de significar nada y
   solo cansaría la vista. */
html.vp-lowlife body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 9987;
  pointer-events: none;
  background: radial-gradient(ellipse 82% 68% at 50% 50%,
              transparent 58%, rgba(180, 12, 22, .38) 100%);
  animation: vpFadeIn .9s ease-out both;
}
@keyframes vpFadeIn { from { opacity: 0 } to { opacity: 1 } }

/* ── Botones que responden al dedo ────────────────────────────────────────── */
.opt, .hbtn, .obtn, .ans, .option-btn, [data-answer] {
  transition: transform .12s cubic-bezier(.34, 1.4, .64, 1),
              box-shadow .2s ease-out,
              border-color .2s ease-out;
}
.opt:active, .hbtn:active, .obtn:active, .ans:active, .option-btn:active,
[data-answer]:active {
  transform: scale(.975) translateY(1px);
}
@media (hover: hover) {
  .opt:hover, .ans:hover, .option-btn:hover, [data-answer]:hover { transform: translateY(-2px); }
}
/* Una opción ya contestada no debe seguir respondiendo al dedo: si se hunde
   después de bloquearse, el juego parece que sigue aceptando respuestas. */
.opt.dis:active, .ans.dis:active, .option-btn:disabled:active { transform: none; }

/* ── Golpe de respaldo ────────────────────────────────────────────────────── */
/* Solo se usa si juice.js no llegara a cargar. Lleva anticipación (se encoge
   antes de crecer) porque sin ella un scale sube "de la nada" y no se lee como
   un impacto. Nombre propio, no `jx-pop`: no queremos pisar el de juice. */
@keyframes vpPop {
  0%   { transform: scale(1) }
  12%  { transform: scale(.965) }
  38%  { transform: scale(1.12) }
  62%  { transform: scale(.99) }
  100% { transform: scale(1) }
}
.vp-pop { animation: vpPop .38s cubic-bezier(.34, 1.4, .64, 1); }

/* ── Marcador que sube ────────────────────────────────────────────────────── */
@keyframes vpTick {
  0%   { transform: scale(1) }
  40%  { transform: scale(1.28) }
  100% { transform: scale(1) }
}
.vp-tick { animation: vpTick .34s cubic-bezier(.34, 1.56, .64, 1); }

/* ── Pass or Smash · el sello que cae ─────────────────────────────────────── */
/* Entra grande, girado y desenfocado de intención, y aterriza pasado de rosca.
   Un sello que aparece a su tamaño final no golpea: solo se enciende. */
@keyframes vpStamp {
  0%   { transform: scale(2.6) rotate(-22deg); opacity: 0 }
  35%  { transform: scale(.88) rotate(4deg);   opacity: 1 }
  55%  { transform: scale(1.08) rotate(-2deg) }
  100% { transform: scale(1) rotate(0);        opacity: 1 }
}
.vp-stamp { animation: vpStamp .46s cubic-bezier(.2, 1.5, .4, 1) both; }

/* La carta que llega. Se anima `.card-inner`, nunca `.card`: el arrastre
   escribe transform inline sobre `.card` y una animación CSS lo pisaría —
   adiós al gesto de deslizar, que es el juego entero. Entra desde abajo y
   ligeramente rotada, como si se levantara del mazo. */
@keyframes vpCardIn {
  from { opacity: 0; transform: translate3d(0, 26px, 0) scale(.94) rotate(-2.5deg); }
  to   { opacity: 1; transform: translate3d(0, 0, 0) scale(1) rotate(0); }
}
.vp-card-in { animation: vpCardIn .38s cubic-bezier(.22, 1, .36, 1) both; }

/* ── Supervivencia · la barra de tiempo ───────────────────────────────────── */
/* El juego escribe `style.width = "43%"` diez veces por segundo. Animar width
   es animar layout: rehace la maquetación de toda la pantalla en cada tick, en
   un móvil, mientras corren las animaciones de las opciones.

   Aquí se fija el ancho al 100% con !important y arena.js lee el porcentaje que
   el juego SIGUE escribiendo en el inline (el inline se queda ahí aunque no
   gane la cascada) para aplicarlo como scaleX, que se compone en GPU. El juego
   no se entera; el móvil sí. */
#tfill, .tfill {
  width: 100% !important;
  transform-origin: left center;
  transition: transform .12s linear !important;
  will-change: transform;
}

/* ── Boss · la rejilla de 30 casillas ─────────────────────────────────────── */
/* Era el elemento más informativo de la pantalla y el más muerto. */
@keyframes vpCellHit {
  0%   { transform: scale(1) }
  35%  { transform: scale(1.5) }
  100% { transform: scale(1) }
}
.vp-cell-hit { animation: vpCellHit .32s cubic-bezier(.34, 1.56, .64, 1); }

/* ── Boss · la sala aprieta ───────────────────────────────────────────────── */
/* Cuando el juego entra en .stress no cambiamos el fondo (eso repintaría toda
   la pantalla): acercamos la cámara un pelo. El encuadre se cierra y la
   sensación es de agobio, a coste de una sola propiedad compuesta en GPU. */
html.vp-stress .screen,
html.vp-stress #app {
  animation: vpTense 3.4s ease-in-out infinite alternate;
}
@keyframes vpTense {
  from { transform: scale(1) }
  to   { transform: scale(1.014) }
}

/* ══════════════════════════════════════════════════════════════════════════
   LAS PANTALLAS DE CLÍMAX
   --------------------------------------------------------------------------
   El fin de ronda, el récord y el veredicto son el momento en que el jugador
   decide si vuelve a jugar o cierra. Y eran las tres pantallas más planas de
   toda la app: los números aparecían ya escritos, el veredicto se encendía sin
   peso y las listas se volcaban de golpe.

   Un número que sube contando se lee como un logro. El mismo número puesto ahí
   no se lee como nada. Esa es toda la diferencia, y cuesta 20 líneas.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── El arte de cada pantalla ─────────────────────────────────────────────── */
/* Un lienzo vacío detrás de la pantalla, con la plancha generada para ELLA.
   Va a `z-index:0` y sin eventos: se cuela debajo de todo lo que el juego ya
   pinta sin tocar una sola de sus reglas. La deriva es lentísima y solo mueve
   `transform`, así que la imagen no se re-rasteriza nunca. */
.vp-screen-art {
  position: absolute;
  inset: -5%;
  z-index: 0;
  display: block;
  pointer-events: none;
  background-position: center;
  background-size: cover;
  opacity: 0;
  animation: vpDrift 28s ease-in-out infinite alternate;
  will-change: transform;
  /* Vaciada por el centro: el marcador va justo ahí y tiene que leerse sin
     pelearse con el fondo. Máscara estática, se hornea una vez. */
  -webkit-mask-image: radial-gradient(ellipse 58% 42% at 50% 48%,
                      rgba(0,0,0,.12) 0%, rgba(0,0,0,.58) 54%, #000 100%);
          mask-image: radial-gradient(ellipse 58% 42% at 50% 48%,
                      rgba(0,0,0,.12) 0%, rgba(0,0,0,.58) 54%, #000 100%);
}

/* La plancha de cada pantalla. Se asigna aquí y no en JavaScript: cambiar el
   arte de una pantalla no debería obligar a tocar lógica.

   Todas locales, ninguna colgada del CDN donde se generaron. Un fondo servido
   desde un dominio de terceros es una bomba de relojería: el día que caduque,
   la pantalla se queda desnuda y nadie sabrá por qué. */

/* Pass or Smash · fin de ronda. Arena de neón, ámbar a un lado y violeta al
   otro, centro negro para que el marcador se lea. Era la pantalla de
   recompensa del juego y no tenía un solo píxel de arte: cuatro degradados
   dorados y unas estrellas simuladas con CSS. */
html[data-vp-game="pos"] .final > .vp-screen-art {
  background-image: url("/juegos/assets/arena/pos-final.webp");
  opacity: .44;
}

/* ── Ranuras a la espera de su plancha ────────────────────────────────────
   Cada una está declarada con `opacity:0` a propósito: el lienzo ya está
   montado y es inerte, y el día que la imagen exista basta con descomentar la
   línea y subir la opacidad. Ni una línea de JavaScript que tocar.

   Cada pantalla lleva la suya y no una genérica, porque cada una cuenta una
   cosa distinta: la calma tensa de antes de un examen no se parece en nada al
   estallido de un récord.                                                  */

/* La splash. Se ve en los CUATRO juegos y es lo PRIMERO que aparece — hoy es
   un `#07060a` plano. Es la pantalla con mejor relación entre lo que cuesta y
   lo que se nota. */
#vp-splash > .vp-screen-art,
.vp-splash-root > .vp-screen-art {
  /* background-image: url("/juegos/assets/arena/splash.webp"); opacity: .5; */
  animation-duration: 18s;
}

/* Survival · la cinemática de RÉCORD MUNDIAL. El mejor momento del juego,
   resuelto hasta hoy con un degradado marrón. Ahora es un podio de luz con
   laureles y ceniza dorada, y el centro sigue negro para que la cifra del
   récord se lea sin pelearse con nada. */
html[data-vp-game="survival"] .cine > .vp-screen-art {
  background-image: url("/juegos/assets/arena/survival-record.webp");
  opacity: .62;
}

/* Duelo · el KO y el cofre. El clímax del duelo, hoy sobre el degradado del
   body. */
html[data-vp-game="duelo"] #screenS8 > .vp-screen-art {
  /* background-image: url("/juegos/assets/arena/duelo-ko.webp"); opacity: .5; */
}

/* Duelo · buscando rival. Hoy son dos anillos de CSS. */
html[data-vp-game="duelo"] #screenS2 > .vp-screen-art {
  /* background-image: url("/juegos/assets/arena/duelo-buscando.webp"); opacity: .45; */
}

/* ══════════════════════════════════════════════════════════════════════════
   EL EXAMEN · pantalla por pantalla
   ══════════════════════════════════════════════════════════════════════════ */

/* ── La entrada al aula, en movimiento ────────────────────────────────────── */
/* La pantalla cuenta que estás ENTRANDO, y lo contaba con una foto quieta del
   aula. Ahora corre por debajo el plano de la puerta abriéndose con la cámara
   avanzando, y la examinadora aparece encima. Lo que se ve es lo que pasa.

   `object-fit: cover` para que llene sin bandas. Va detrás de `.bg` y de su
   velo, así que el texto sigue teniendo exactamente el mismo contraste que
   antes: no se toca ni una regla de las suyas. */
.vp-screen-clip {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
  /* Visible DESDE EL PRIMER INSTANTE, no en cuanto cargue el vídeo. El elemento
     lleva `poster`, así que aunque el mp4 tarde o no llegue a arrancar, en
     pantalla ya está el aula de ese nivel. Antes esto empezaba en `opacity:0` y
     esperaba al vídeo: si el vídeo no arrancaba —conexión mala, ahorro de
     datos, autoplay denegado— los tres niveles se veían exactamente iguales,
     que es justo el problema que había que resolver. */
  opacity: 1;
  transition: opacity 1.2s ease-out;
}
/* La clase la pone el JS cuando hay un fotograma real; sin esa condición se ve
   un parpadeo a negro entre la foto y el vídeo. Y la foto se retira despacio
   por debajo, así que el relevo no se nota. */
/* La foto original del juego se retira en cuanto hay clip montado — con póster
   o con vídeo, da igual: lo que hay encima ya es el aula de ESE nivel. */
#scrEntrada:has(> .vp-screen-clip) > .bg { opacity: 0; transition: opacity .8s ease-out }
.vp-clip-live > .bg { opacity: 0 }

/* Cuando el plano termina se queda congelado en su último fotograma — el
   interior del aula. Un fotograma congelado es una foto, y una foto quieta es
   justo lo que veníamos a quitar, así que se le da una deriva de treinta
   segundos: no se percibe como movimiento, se percibe como que la escena sigue
   viva. Solo transform. */
.vp-clip-held > .vp-screen-clip {
  animation: vpHeld 30s ease-in-out infinite alternate;
  will-change: transform;
}
@keyframes vpHeld {
  from { transform: scale(1.02) translate3d(0, 0, 0) }
  to   { transform: scale(1.07) translate3d(0, -1.2%, 0) }
}

/* ── Los personajes recortados ────────────────────────────────────────────── */
/* Los tres sprites están recortados con fondo transparente. Un busto siempre
   termina en corte por la cintura — eso no es un defecto, es lo que es — pero
   un canto perfectamente recto delata el recorte. Un desvanecido de los
   últimos píxeles por abajo lo funde con la penumbra de la sala y desaparece
   el problema.

   Solo por abajo. La primera versión difuminaba también los lados porque el
   arte del nivel 3 traía los brazos cortados por el borde de la propia imagen;
   una vez redibujado con los dos brazos completos y margen por los cuatro
   lados, difuminar los costados solo serviría para comerse los puños.

   Máscara estática sobre un elemento que no se anima: se hornea una vez. */
/* #entradaSprite (el aula) ya NO lleva máscara: una máscara recorta también la
   drop-shadow del filtro por la caja del <img>, y eso dibujaba un rectángulo
   oscuro alrededor del profesor. Su desvanecido va horneado en
   assets/boss/aula/examinador-n*.webp. */
html[data-vp-game="boss"] #bossImg,
html[data-vp-game="boss"] .fin-boss {
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 90%, transparent 100%);
          mask-image: linear-gradient(to bottom, #000 0%, #000 90%, transparent 100%);
  /* Máscara y sombra no se combinan: la máscara corta la sombra por la caja
     del <img> y sale un rectángulo. Hoy estas dos imágenes no se ven (las
     tapan escena.js y escena-fin.js); esto es para cuando esas escenas no
     carguen. */
  filter: none;
}

/* ── Los avatares de la lista de niveles ──────────────────────────────────── */
/* Son cuadrados de 46 px con `object-fit: cover` sobre imágenes de cuerpo
   entero de proporción 3:4. Cover recorta por el CENTRO, y el centro de una
   figura de cuerpo entero es la corbata: en esos 46 px salía el traje, no la
   cara. Anclando el recorte arriba salen las tres caras, que es lo único que
   se puede reconocer a ese tamaño.

   Y un fondo propio: los recortes son transparentes, y una silueta transparente
   sobre la fila deja huecos que parecen un error de carga. */
html[data-vp-game="boss"] .lvl-thumb {
  object-position: top center;
  background: radial-gradient(ellipse 90% 70% at 50% 30%, #2a2333, #0d0a12);
}

/* El nivel elegido se nota. Antes cambiar de nivel no animaba nada: el borde
   saltaba de una fila a otra y punto. */
html[data-vp-game="boss"] .lvl-row {
  transition: transform .18s cubic-bezier(.34,1.4,.64,1), box-shadow .22s ease-out;
}
html[data-vp-game="boss"] .lvl-row.sel { transform: translateX(4px) scale(1.012) }
/* Y el bloqueado se siente bloqueado: al tocarlo no se hunde, se resiste. */
html[data-vp-game="boss"] .lvl-row.locked:active { animation: vpLocked .3s ease-out }
@keyframes vpLocked {
  0%,100% { transform: translateX(0) }
  25%     { transform: translateX(-4px) }
  60%     { transform: translateX(3px) }
}

/* ── El examen · la hoja respira ──────────────────────────────────────────── */
/* Una hoja de papel completamente inmóvil durante 30 preguntas se lee como una
   captura de pantalla. Un balanceo mínimo y lentísimo la convierte en un
   objeto que está ahí. Solo transform, y de una amplitud que no se nota
   conscientemente — que es justo el objetivo. */
html[data-vp-game="boss"] .exam-sheet {
  animation: vpSheet 9s ease-in-out infinite alternate;
}
@keyframes vpSheet {
  from { transform: rotate(-.12deg) translate3d(0, 0, 0) }
  to   { transform: rotate(.12deg) translate3d(0, -2px, 0) }
}

/* ── El veredicto aterriza ────────────────────────────────────────────────── */
/* Entra grande y desenfocado de intención, y se asienta pasándose de rosca.
   Un titular que aparece a su tamaño final no golpea: se enciende. */
@keyframes vpVerdict {
  0%   { opacity: 0; transform: scale(1.75); letter-spacing: .22em }
  45%  { opacity: 1; transform: scale(.94);  letter-spacing: .01em }
  70%  { transform: scale(1.03) }
  100% { opacity: 1; transform: scale(1); letter-spacing: .02em }
}
.final.show .verdict,
.jx-results .verdict {
  animation: vpVerdict .72s cubic-bezier(.2, 1.3, .4, 1) both;
}

/* La onda que sale del veredicto. Un solo pseudo-elemento, sin nodos nuevos, y
   se anima solo `transform`/`opacity` — el anillo se dibuja una vez. */
.final.show .verdict::after {
  content: "";
  position: absolute;
  left: 50%; top: 50%;
  width: 40px; height: 40px;
  margin: -20px 0 0 -20px;
  border-radius: 50%;
  border: 2px solid currentColor;
  pointer-events: none;
  animation: vpShock .9s .18s cubic-bezier(.2, .8, .3, 1) both;
}
.final.show .verdict { position: relative }
@keyframes vpShock {
  from { opacity: .55; transform: scale(.3) }
  to   { opacity: 0;   transform: scale(9) }
}

/* ── Los diales respiran ──────────────────────────────────────────────────── */
/* El dial grande lleva un brillo que lo recorre una vez, justo después de
   aparecer. Es lo que hace que un círculo parezca un marcador y no un borde. */
.final.show .dial.big { position: relative; overflow: hidden }
.final.show .dial.big::after {
  content: "";
  position: absolute;
  inset: -50%;
  pointer-events: none;
  background: linear-gradient(104deg, transparent 42%,
              rgba(255,255,255,.28) 50%, transparent 58%);
  transform: translate3d(-60%, 0, 0);
  animation: vpDialSweep 1.1s .55s cubic-bezier(.3,.7,.3,1) both;
}
@keyframes vpDialSweep {
  0%   { opacity: 0;  transform: translate3d(-60%, 0, 0) }
  20%  { opacity: 1 }
  100% { opacity: 0;  transform: translate3d(60%, 0, 0) }
}

/* ── El bocadillo de Santi escribe ────────────────────────────────────────── */
/* No es un efecto de máquina de escribir letra a letra (eso obliga a tocar el
   texto y encima se lee peor): el bocadillo se despliega desde su esquina, que
   es de donde saldría si lo estuviera diciendo. */
@keyframes vpBubbleIn {
  from { opacity: 0; transform: translate3d(-10px, 8px, 0) scale(.9) }
  to   { opacity: 1; transform: translate3d(0, 0, 0) scale(1) }
}
.final.show .final-bubble,
.fail.show .fail-bubble {
  transform-origin: 0 100%;
  animation: vpBubbleIn .42s .34s cubic-bezier(.34, 1.5, .64, 1) both;
}

/* ── Survival · la cinemática de récord ───────────────────────────────────── */
/* Era el clímax del juego resuelto con un degradado marrón. Ahora la escena
   entra empujando y late una vez, como un flash de cámara. */
@keyframes vpCineIn {
  0%   { opacity: 0; transform: scale(1.14) }
  40%  { opacity: 1 }
  100% { opacity: 1; transform: scale(1) }
}
.cine.show { animation: vpCineIn .9s cubic-bezier(.22, 1, .36, 1) both }

/* ── Fondo de arena generado ──────────────────────────────────────────────── */
/* Deriva lentísima para que la escena no esté muerta pero nada distraiga.
   Solo transform: la imagen nunca se re-rasteriza. */
.vp-arena-bg {
  position: absolute;
  inset: -5%;
  z-index: 0;
  background-position: center;
  background-size: cover;
  pointer-events: none;
  opacity: 0;
  animation: vpDrift 26s ease-in-out infinite alternate;
  will-change: transform;

  /* La plancha se vacía por el centro con una máscara. Sin esto, aun siendo una
     imagen oscura, la textura de la calle competía con el enunciado justo donde
     está el texto. Con la máscara el fondo solo vive en la periferia: se nota
     que hay un mundo detrás y se sigue leyendo sin esfuerzo.

     Es una máscara ESTÁTICA sobre una capa que ya tiene will-change:transform,
     así que se hornea una vez en la textura de la capa. No se anima nunca: una
     máscara animada sí obligaría a re-rasterizar en cada frame. */
  -webkit-mask-image: radial-gradient(ellipse 62% 46% at 50% 48%,
                      rgba(0,0,0,.18) 0%, rgba(0,0,0,.6) 55%, #000 100%);
          mask-image: radial-gradient(ellipse 62% 46% at 50% 48%,
                      rgba(0,0,0,.18) 0%, rgba(0,0,0,.6) 55%, #000 100%);
}
@keyframes vpDrift {
  from { transform: scale(1.05) translate3d(0, 0, 0) }
  to   { transform: scale(1.11) translate3d(0, -1.6%, 0) }
}

/* La imagen se asigna por juego, no en JavaScript: cambiar un fondo no debería
   obligar a tocar lógica. `opacity` sube solo cuando hay imagen que poner, así
   el layer es inerte si un día falta la plancha.

   La plancha está dibujada con el centro casi negro a propósito: el texto de
   las preguntas va encima y tiene que leerse sin pelear con el fondo. Todo el
   detalle (fuego, humo, brasas) vive en los bordes, que es donde el ojo lo
   percibe sin que estorbe.

   Fichero local, 720×1290 WebP, 65 KB. Se generó en Higgsfield y se bajó al
   repo: NO se sirve desde su CDN. Un fondo colgado de un dominio de terceros
   es una bomba de relojería — el día que caduque, el juego se queda desnudo. */
html[data-vp-game="survival"] .vp-arena-bg {
  background-image: url("/juegos/assets/arena/survival.webp");
  opacity: .55;
}
/* Pass or Smash NO lleva plancha: ya tiene su ciudad de noche con semáforos y,
   probada en pantalla, la diferencia era casi imperceptible. La imagen está
   generada y guardada por si el arte de ese juego cambia algún día:
   hf_20260731_102233_ab254a69 — arena neón, ámbar a la izquierda, violeta a la
   derecha, centro negro. */

/* Velo sobre el fondo: sin él, un fondo bonito se come el texto. */
.vp-arena-veil {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background: radial-gradient(ellipse 70% 55% at 50% 45%,
              rgba(5, 4, 6, .55) 0%, rgba(5, 4, 6, .82) 60%, rgba(5, 4, 6, .95) 100%);
}

@media (prefers-reduced-motion: reduce) {
  .vp-enter, .vp-leave, .vp-tick, .vp-pop,
  .vp-stamp, .vp-card-in, .vp-imgin, .vp-cell-hit, .vp-flash-on,
  .vp-urgency.vp-on, .vp-arena-bg,
  html.vp-stress .screen, html.vp-stress #app { animation: none !important; }
  .vp-enter, .vp-card-in, .vp-imgin { opacity: 1; transform: none; }
  .vp-leave { opacity: 1; }
}
