/* VS duel — split-screen layout + central animation. Two panels fill the
   viewport so two students can play side by side on one shared touchscreen. */

/* ── Container + Arena shell ─────────────────────────────────────────────────
   .vs-arena is always flex-column: scoreboard bar on top, .vs-main below.
   A skin class (vs-skin-classic / vs-skin-school) on .vs-arena overrides
   colours, panel shapes and activity elements — no JS branching needed.     */
.vs-wrap  { height: 100%; min-height: 0; container-type: size; container-name: vs; }
.vs-arena { display: flex; flex-direction: column; height: 100%; min-height: 0; }

/* ── 3-column game area ──────────────────────────────────────────────────── */
/* EL REPARTO DEL ANCHO: 1 / 2 / 1, DECIDIDO POR EL DUEÑO (2026-08-25).
   En v1.51.581 se cambió a 1.3 / 1.4 / 1.3 con este argumento medido: la
   animación —que es decorado— se llevaba la MITAD del ancho y a cada alumno le
   quedaba el 24 % (en un portátil, 224 px para jugar contra 448 px de cuerda).
   Con el reparto nuevo el panel subía al 31 % y la tecla de Operaciones de 58 a
   81 px. El dato es cierto, pero la decisión NO era técnica: cuánto pesa la
   cuerda frente a lo que juega el alumno es producto, y se tomó al vuelo dentro
   de una tanda de maquetación sin preguntar. El dueño vio el cambio, pidió los
   números de las tres opciones y eligió volver a 1 / 2 / 1.
   Queda escrito para que no se «arregle» otra vez desde la medida: el coste
   (panel 24 %, tecla 63 px, cifra 24 px en su ventana) está aceptado a sabiendas.
   Cambiarlo es decisión suya, no de quien mida. */
.vs-main {
  flex: 1; min-height: 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 2fr) minmax(0, 1fr);
  grid-template-areas: "s1 stage s2";
}
.vs-panel.vs-left  { grid-area: s1; }
.vs-panel.vs-right { grid-area: s2; }

/* Animación central desactivada: ocultar el escenario y repartir todo el ancho
   entre los dos paneles. `grid-template-rows: 1fr` es CLAVE: sin él, en vertical
   los paneles heredaban las DOS filas del layout apilado (stage+paneles) y solo
   ocupaban una fracción del alto → el texto se apretaba y se cortaba dejando el
   resto del panel vacío. Con una sola fila a 1fr, los paneles llenan el alto. */
.vs-arena.vs-no-stage .vs-main {
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr);
  grid-template-areas: "s1 s2";
}
.vs-arena.vs-no-stage .vs-stage { display: none; }
/* Editor: atenuar la lista de animaciones cuando la animación central está apagada. */
.vsanim-list-off { opacity: .4; pointer-events: none; filter: grayscale(1); }

/* ── Centre stage ────────────────────────────────────────────────────────── */
.vs-stage {
  grid-area: stage; display: flex; flex-direction: column; min-width: 0; min-height: 0;
  padding: .4rem .6rem; text-align: center;
}
.vs-stage-canvas { flex: 1; min-height: 0; position: relative; display: flex; align-items: center; justify-content: center; }
.vs-stage-canvas > * { width: 100%; height: 100%; }
.vs-anim-fallback { margin: auto; max-width: 18rem; padding: 1rem; color: #6c757d; font-size: .95rem; border: 2px dashed #ced4da; border-radius: .6rem; }
.vs-tug-label { font-weight: 800; font-size: clamp(1rem, 2cqw, 1.6rem); margin: 0 0 .1rem; transition: color .2s; }

/* ── Animación COMPACTA (dueño, 2026-08-18, con foto de referencia) ──────────
   Por defecto la animación LLENA el carril central (flex:1). Con esta opción
   de Presentación (`vsAnimCompact`) se encoge a la franja de arriba —donde el
   dueño marcó el hueco en su foto— y la mitad de abajo del carril queda LIBRE,
   con el fondo del skin, para lo que se quiera poner ahí más adelante (no lo
   decide este cambio). `cqh` porque `.vs-wrap` ya es contenedor de consulta
   (container-name: vs): escala con el ALTO real del duelo, no con el viewport. */
.vs-arena.vs-anim-compact .vs-stage-canvas {
  flex: 0 0 auto;
  height: clamp(90px, 32cqh, 280px);
}
/* «Más arriba please esa animación» (dueño, 2026-08-18, tras ver la v1.51.520):
   compacta ya encogía el CANVAS, pero antes de él seguía el hueco de SIEMPRE
   —el padding-top de `.vs-stage` y el margen/alto de `.vs-tug-label`— pensado
   para cuando la animación llena todo el carril. Medido con Playwright
   (`.vs-stage-canvas svg` contra el borde de la barra): 33px de hueco muerto
   antes de que empezara el dibujo. En compacta ese hueco no cumple ningún
   propósito (no hay nada más arriba que respirar), así que se quita. */
.vs-arena.vs-anim-compact .vs-stage { padding-top: 0; }
.vs-arena.vs-anim-compact .vs-tug-label { margin: 0; font-size: clamp(.8rem, 1.6cqw, 1.15rem); }

/* ── Activity panels ─────────────────────────────────────────────────────── */
/* QUIÉN MIDE ES EL PANEL, QUIEN SE VE ES EL CUERPO — y por eso están separados.
   La caja de medida (`container-type: size`) vivía en `.vs-body`, que es también
   la que los skins PINTAN (borde, fondo, esquinas). Eso ataba las dos cosas: un
   contenedor de tamaño se mide por FUERA —su contenido no cuenta, si no
   colapsaría a cero—, así que el marco no podía nunca encoger hasta su bloque y
   el aire sobrante quedaba DENTRO del marco de colores («el aire debe estar por
   fuera de las actividades, no por dentro», dueño con captura).
   El panel es un hijo del grid: su tamaño ya viene dado desde fuera, así que
   medir ahí no es circular, y mide prácticamente la misma caja (el cuerpo es el
   panel menos su relleno) — las plantillas siguen escalando igual. Ya libre, el
   cuerpo puede abrazar a su actividad cuando la plantilla lo declara (abajo,
   `ww-fit-block`). */
.vs-panel { display: flex; flex-direction: column; min-width: 0; min-height: 0;
  container-type: size; }
.vs-body  { flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; justify-content: center; overflow: auto; padding: .5rem .75rem;
  box-sizing: border-box; }
/* ESTÁNDAR: el panel de actividad y todo su contenido usan border-box, para que
   width:100% + padding NO desborde el panel (el bug que partía las tarjetas en
   móvil). Aplica a CUALQUIER plantilla embebida en VS, en todos los skins. */
.vs-body, .vs-body * { box-sizing: border-box; }
.vs-q     { font-size: clamp(1.1rem, 2.4cqw, 1.8rem); font-weight: 600; text-align: center; margin-bottom: 1rem; }
.vs-done  { margin: auto; text-align: center; }
/* ESTÁNDAR: los skins NO repiten esta regla — solo definen los tokens
   --vs-done-fg / --vs-done-muted en su .vs-body. El !important vive UNA vez
   aquí (hace falta para ganar a .text-muted de Bootstrap). */
.vs-body .vs-done, .vs-body .vs-done .h5 { color: var(--vs-done-fg, inherit) !important; }
.vs-body .vs-done .text-muted { color: var(--vs-done-muted, #6c757d) !important; }

/* Panel-level answer feedback */
.vs-body.vs-flash-ok { animation: vs-flash-ok .7s ease-out; }
.vs-body.vs-flash-no { animation: vs-flash-no .7s ease-out; }
@keyframes vs-flash-ok { from { background: rgba(25,135,84,.28); } to { background: transparent; } }
@keyframes vs-flash-no { from { background: rgba(220,53,69,.28); } to { background: transparent; } }
.rq-opt.rq-picked { outline: 4px solid #ffc107; outline-offset: -2px; }

/* ══════════════════════════════════════════════════════════════════════════
   SCOREBOARD BAR  —  always present, skins override colours below
   ══════════════════════════════════════════════════════════════════════════ */
/* EL MARCADOR CRECE CON LA ARENA (§3). Aquí ponía `height: 68px`: en la pizarra
   4K del aula, a pantalla completa, la barra que dice de quién es cada lado y
   cómo va el duelo se quedaba en 72 px — el 3 % de la pantalla— con la etiqueta
   del equipo a 8 px de letra. Un tope en px no escala, y este es el peor sitio
   para tenerlo: es lo único que la clase mira para saber quién gana.
   Todo el marcador se pide por TOKEN para que un tema pueda recolorearlo y
   ajustarlo sin volver a escribir estas reglas (ley del skin). El piso protege
   el móvil; el techo evita que en una pizarra gigantesca la barra se coma el
   juego. Se escala con `cqh` —el ALTO de la arena— porque es una franja
   horizontal: lo que decide si se lee es cuánto alto se le da. */
.vss-bar {
  display: flex; flex-shrink: 0;
  height: var(--vss-alto, clamp(46px, 7.5cqh, 132px));
  z-index: 10;
}
/* Left + right team halves */
.vss-team {
  flex: 1; display: flex; align-items: center; gap: .5rem;
  padding: .4rem .75rem; overflow: hidden;
}
/* EL ESPEJO YA ESTÁ EN EL DOM (vsView: la mitad izquierda es ficha·nombre·puntos
   y la derecha puntos·nombre·ficha). Aquí ponía `row-reverse`, que lo volvía a
   invertir: la ficha del alumno de la derecha acababa pegada al centro, junto al
   rombo VS, mientras la del alumno de la izquierda estaba en el borde. Medido:
   52 px del borde a la izquierda contra 338 a la derecha. tv-show lo tapaba con
   su propio `flex-direction: row` —por eso solo se veía sin tema—, y la red del
   espejo de `matrix-smoke` lo cazó a la primera en las cinco plantillas de duelo.
   Sin la doble inversión, `.vss-info-r { text-align: right }` (abajo) queda del
   lado correcto: el nombre pegado a su ficha, como en la mitad izquierda. */
.vss-info   { flex: 1; min-width: 0; }
.vss-info-r { text-align: right; }
.vss-label  { font-size: var(--vss-label-size, clamp(.5rem, 1.3cqh, 1.3rem)); font-weight: 700; letter-spacing: .18em; color: rgba(255,255,255,.8); text-transform: uppercase; text-shadow: 0 1px 2px rgba(0,0,0,.45); }
/* Names sit on skin gradients (blue/red, sometimes mid-tone) — without a shadow
   the white washes out and is unreadable. A dark halo guarantees contrast on any
   skin, matching how .vss-score is already treated. */
.vss-name   { font-size: var(--vss-name-size, clamp(.78rem, 2.2cqh, 2.4rem)); font-weight: 900; color: #fff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-shadow: 0 1px 4px rgba(0,0,0,.6), 0 0 2px rgba(0,0,0,.4); }
.vss-score  { font-size: var(--vss-score-size, clamp(1.5rem, 3.8cqh, 4.2rem)); font-weight: 900; color: #fff; font-variant-numeric: tabular-nums; flex-shrink: 0; text-shadow: 0 2px 8px rgba(0,0,0,.3); }
/* Progress bar inside bar */
.vs-prog     { height: 4px; border-radius: 999px; background: rgba(255,255,255,.22); margin-top: .3rem; overflow: hidden; }
.vs-prog-bar { height: 100%; width: 0; background: rgba(255,255,255,.75); transition: width .4s ease; }
/* Centred VS badge — lives in .vss-mid, a fixed-width flex child (not absolute) */
.vss-mid {
  width: var(--vss-mid, 68px); flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
}
.vss-badge {
  width: var(--vss-badge, 50px); height: var(--vss-badge, 50px);
  border-radius: 50%; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  font-weight: 900; font-size: var(--vss-badge-size, .9rem); letter-spacing: .04em;
  border: 3px solid rgba(255,255,255,.9);
}
/* Avatar in bar */
.vs-avatar { width: 2.2rem; height: 2.2rem; border-radius: 50%; object-fit: cover; border: 2px solid rgba(255,255,255,.5); flex-shrink: 0; }
.vs-avatar-init { display: flex; align-items: center; justify-content: center; background: rgba(255,255,255,.22); font-size: 1rem; font-weight: 800; color: #fff; }
/* La FOTO del alumno: era 2.6rem clavados — un punto de 42 px en una pizarra de
   2160. Escala con la barra que la contiene. */
.vss-av { width: var(--vss-av, clamp(2rem, 5cqh, 5.5rem)); height: var(--vss-av, clamp(2rem, 5cqh, 5.5rem)); }

/* ══════════════════════════════════════════════════════════════════════════
   SKIN: CLÁSICO
   ══════════════════════════════════════════════════════════════════════════ */
.vs-skin-classic .vss-left  { background: #1565c0; }
.vs-skin-classic .vss-right { background: #c62828; }
.vs-skin-classic .vss-mid   { background: #1565c0; background: linear-gradient(90deg, #1565c0, #c62828); }
.vs-skin-classic .vss-badge { background: #fff; color: #374151; box-shadow: 0 2px 8px rgba(0,0,0,.2); border-color: #dee2e6; }
.vs-skin-classic .vs-stage  { border-left: 1px solid #dee2e6; border-right: 1px solid #dee2e6; }

/* SKIN: COLEGIOS — rules live in themes/colegios/skin.css, loaded by applySkin() */

/* ══════════════════════════════════════════════════════════════════════════
   REJILLA DE OPCIONES DENTRO DE LOS PANELES DEL DUELO
   Override live.css projector sizes (padding:2rem, font-size:1.5rem) so
   choice options fit inside the narrow side panels at any orientation.
   ══════════════════════════════════════════════════════════════════════════ */
.vs-body .ww-opt-grid { gap: .4rem; }
/* CORRECCIÓN (2026-08-25): ayer añadí aquí `.vs-body .ww-opt` diciendo que era
   «la otra manera de pintar la misma opción». No lo comprobé, y es FALSO: en el
   duelo no existe ningún `.ww-opt` — esa clase la emite solo el player Individual
   de Quiz, y la ronda compartida emite `.vs-opt.rq-opt`. Era un selector muerto,
   justo lo que la ley §30 prohíbe. Se queda solo el que sí pinta. */
.vs-body .ww-opt-grid .btn {
  font-size: clamp(.7rem, 2.4cqw, 1rem);
  padding: .4em .45em;
  font-weight: 600;
}

/* ESTÁNDAR: el contenido de ronda de CADA plantilla embebida en VS se dimensiona
   con unidades de CONTENEDOR (cqw), nunca de viewport (vmin/vw) — si no, en el
   panel estrecho se ve gigante y desborda. Cubre Tildes/Comas (Quiz y
   Operaciones ya arriba / en el query de ajuste). */
.vs-body .tc-passage    { font-size: clamp(.85rem, 9cqw, 1.4rem); }
.vs-body .tc-ch, .vs-body .tc-gap { line-height: 1.5; }

/* ══════════════════════════════════════════════════════════════════════════
   PORTRAIT / NARROW
   ══════════════════════════════════════════════════════════════════════════ */
/* PORTRAIT (vertical): barra compacta + apilar — animación arriba (a lo ancho),
   los dos paneles abajo. El stage es 1fr (no 90px) → la animación no queda
   minúscula. minmax(0,…) deja que las columnas/filas encojan (si no, el
   contenido las fuerza a crecer y los paneles se solapan). */
@container vs (max-aspect-ratio: 5/4) {
  /* En vertical el marcador se COMPRIME, y se hace declarando los MISMOS tokens
     en la propia `.vss-bar`: una declaración en el elemento gana a la del tema
     (que vive en el ancestro `.vs-skin-*`), así la compresión vale para TODOS los
     temas sin que ninguno tenga que repetirla. Un tema que quiera otra medida en
     vertical redeclara el token en `.vs-skin-X .vss-bar` (0,2,0). */
  .vss-bar {
    --vss-alto: 46px;
    --vss-badge: 36px;
    --vss-badge-size: .72rem;
    --vss-mid: 50px;
    --vss-av: 1.9rem;
    --vss-score-size: clamp(1rem, 2.2cqw, 1.5rem);
    --vss-name-size: clamp(.62rem, 1.2cqw, .88rem);
  }
  .vss-label { display: none; }

  .vs-main {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    grid-template-areas: "stage stage" "s1 s2";
    grid-template-rows: minmax(0, 1fr) minmax(0, 1.8fr);
    align-content: stretch; column-gap: .4rem;
  }
  .vs-skin-classic .vs-stage { border: none; border-bottom: 1px solid #dee2e6; }
}

/* AJUSTE ESTÁNDAR DE LA ACTIVIDAD EN EL PANEL — se aplica en vertical O en
   cualquier contenedor BAJO (móvil apaisado, laptop pequeña). PRIMERO el layout
   reparte el espacio (celda del grid → altura definida); LUEGO el contenido se
   dibuja dentro y se ESCALA para caber con unidades de CONTENEDOR (cqw), nunca
   de viewport. Vale para todas las plantillas VS y todos los skins. */
@container vs (max-aspect-ratio: 5/4) or (max-height: 700px) {
  .vs-panel { justify-content: stretch; align-items: stretch; }
  .vs-body  { padding: .45rem .5rem; justify-content: flex-start; min-width: 0; min-height: 0; overflow: hidden; }
  .vs-q     { font-size: clamp(.85rem, 4cqw, 1.3rem); margin-bottom: .3rem; }

  /* Teclado numérico (Operaciones): la ESTRUCTURA (que quepa) vive UNA vez en
     styles/scaffold.css; aquí solo los TAMAÑOS de este panel, en cqw porque en VS
     manda el ancho del panel. */
  /* EL VALOR DEL MODO ES EL FALLBACK DEL TOKEN, no un muro. Aquí ponía
     `gap: 2cqw` a secas, y como esta regla (0,2,0) gana a la de la plantilla
     (0,1,0), el token del tema quedaba MUERTO: tv-show tenía que escribir su
     propio `.vs-body .ww-keypad-round { gap: … }` para colarse por encima —
     justo la invasión que la migración quiere retirar. Consumiendo el token
     aquí, el modo pone su valor por defecto Y el tema puede cambiarlo sin
     conocer la anatomía. Lo destapó `tools/shots.mjs`: sin esto el teclado se
     movía 2 px (migración de temas, 2026-08-22). */
  /* UN SOLO TAMAÑO PARA EL MODO. Aquí había TRES fórmulas —enunciado, visor y
     tecla, cada una con sus propios topes— y otras tres en Equipos y otras tres
     en la plantilla: nueve para tres textos que en una calculadora miden lo
     mismo. El modo ya solo dice cuánto vale UNA CIFRA en este panel; el
     enunciado y el visor salen de ahí (ver `--math-cifra` en styles/math.css).
     En VS manda el ancho del panel (`cqw`), con `cqh` de tope para que la
     cabecera no se coma el teclado. */
  .vs-body .ww-keypad-round   { gap: var(--math-gap, 2cqw); --math-cifra: clamp(.6rem, 11cqw, 12cqh); }
  .vs-body .ww-keypad         { gap: var(--key-gap, 1.8cqw); }
}

/* LA CABECERA, A LA ALTURA DE UNA FILA DE TECLAS, Y SIEMPRE. Una fila mide ~28 %
   del ancho del panel (el teclado ocupa ~85 % y tiene 3 columnas). Sin esto, la
   chapa medía su texto más un relleno mínimo —29 px contra teclas de 71— y el
   visor parecía de otra actividad. El segundo tope (`18cqh`) es para el panel
   ANCHO Y BAJO que queda cuando el profe apaga la animación central: allí el
   28 % del ancho son 117 px de cabecera, que se comían el alto del teclado.
   ESTABA DENTRO del container query de vertical/bajo, así que en la ventana de
   escritorio con la animación APAGADA no existía: el bloque calculaba el hueco
   del teclado restando CERO de cabecera, el teclado no se topaba y el flex lo
   aplastaba — teclas de 128×101 (proporción 1,27). Es el reparto del alto del
   bloque, no un ajuste de pantalla estrecha: va fuera, para las dos. */
.vs-body { --math-head-min: min(28cqw, 18cqh); }

/* MARCADOR EN VERTICAL — RESUELTO, y cómo (§3, medido)
   Este comentario explicaba una deuda que ya no existe, y encima se quedó SIN
   CERRAR (le faltaba el `*​/`), así que se tragaba el banner siguiente.
   La deuda decía: «el andamio portrait del marcador está copiado en los skins».
   Traerlo aquí como PROPIEDADES no funcionaba —cada skin define su propia
   `.vs-skin-X .vss-bar` (0,2,0) y una regla compartida `.vss-bar` (0,1,0) pierde:
   la barra volvía a su alto de escritorio y tv-show se descuadraba, 256k píxeles
   de diferencia según `tools/shots.mjs`—. La salida no era ganar la guerra de
   especificidad, era dejar de pelearla: en vertical se redeclaran los TOKENS
   (`--vss-alto` y compañía) en la propia `.vss-bar`, y una declaración en el
   elemento gana a la del tema, que cuelga del ancestro `.vs-skin-*`. Una sola
   compresión para todos los temas, y el que quiera otra la pide en
   `.vs-skin-X .vss-bar` (0,2,0). Ver el bloque PORTRAIT / NARROW de arriba. */

/* ══════════════════════════════════════════════════════════════════════════
   POLÍTICA DE MAQUETACIÓN DEL PANEL (declarada por meta.panelFit de la plantilla;
   vsView pone la clase ww-fit-<modo> en .vs-body). UN solo sitio decide el caso:

     ww-fit-fill   (defecto) → el contenido LLENA el panel y se escala para caber
                    (Tildes/Comas vía fitPassage, Quiz vía rejilla). Es lo que ya
                    hacen las reglas base de arriba; no necesita nada extra.
     ww-fit-block  → BLOQUE único indivisible (la calculadora): a su tamaño
                    natural, centrado y CON TOPE — no se estira a toda la vertical
                    (teclas larguísimas); en panel bajo encoge para caber.
     ww-fit-center → tamaño natural, centrado, sin llenar (contenido pequeño).
   ══════════════════════════════════════════════════════════════════════════ */
/* EL MARCO ABRAZA A LA ACTIVIDAD y el aire se va FUERA. `flex: 0 1 auto` +
   `margin: auto`: el cuerpo mide lo que mide su bloque y se centra en el panel;
   `max-height: 100%` es lo que impide que se salga cuando el panel es bajo (el
   teclado encoge, ver más abajo). Antes el cuerpo llenaba la columna (`flex: 1`)
   y el bloque se centraba DENTRO: el borde de colores quedaba larguísimo con dos
   bandas vacías dentro de él. Solo aplica al caso BLOQUE que declara la
   plantilla (`meta.panelFit`): en `fill` el contenido SÍ llena y el marco debe
   seguir llenando la columna. */
.vs-body.ww-fit-block  { justify-content: center; overflow: hidden;
  /* SOLO EN VERTICAL. Aquí ponía `margin: auto` para centrar el bloque, y en
     una columna flex los márgenes automáticos ganan a `align-items: stretch`
     TAMBIÉN en horizontal: el cuerpo dejaba de estirarse a lo ancho del panel
     y se quedaba con el ancho de su contenido. Medido con el duelo montado:
     panel 209 px, cuerpo 142 — 33 px de margen a cada lado. Como el tope de
     alto de las filas se calcula sobre el ancho del PANEL, las teclas salían
     de 39 de ancho por 71 de alto (proporción 0,55: casi el doble de altas
     que anchas). Centrar en vertical era lo que se quería; estrechar el panel,
     no. */
  flex: 0 1 auto; height: auto; max-height: 100%; margin-block: auto; }
.vs-body.ww-fit-center { justify-content: center; overflow: auto; }
.vs-body.ww-fit-center > * { flex: 0 0 auto; margin: auto; }
/* EL BLOQUE LLENA SU CAJA. Aquí había un `max-width` que restaba la cabecera,
   sabía que la rejilla del teclado es 3×4 y llevaba el hueco escrito en `1.5rem`:
   un MODO haciendo cuentas con la anatomía de UNA plantilla, que es justo lo que
   §0 prohíbe. La cuenta se mudó a `styles/math.css` —su dueña— y aquí queda solo
   lo que sí es del modo: que el bloque no herede un alto fijo del cuerpo. */
.vs-body.ww-fit-block .ww-keypad-round {
  height: auto;   /* el cuerpo ya no tiene alto fijo: el bloque LO DA, no lo hereda */
  margin: auto;
}
/* Las teclas no se hacen más altas que ~1 ancho y pico (cqw = ancho del panel):
   en un panel muy alto las filas se topan ahí y el teclado se centra en su hueco,
   en uno bajo encogen (min 0) para caber. Así quedan casi cuadradas, un punto más
   altas — no barras verticales. */
.vs-body .ww-keypad {
  /* LA FORMA DEL TECLADO LA DECIDE SU REJILLA, no el hueco (dueño, 2026-08-22:
     «la forma de la pieza manda sobre llenar el hueco»).
     Aquí había un tope de alto de fila en `34cqw` — el 34 % del ancho del
     PANEL— calibrado suponiendo que el teclado llena el panel. No lo llena
     (relleno del cuerpo, del bloque y hueco entre teclas), así que el tope
     quedaba ~1,8× por encima del ancho real de una tecla y las filas se
     estiraban hasta él: teclas casi el doble de altas que anchas.
     Declarando la proporción de la rejilla (3 columnas × 4 filas) las teclas
     salen cuadradas SOLAS, sea cual sea la forma de la ventana, y el sobrante
     queda como aire — que es lo que pide la ley. Medido en seis formas de
     ventana: proporción de la tecla entre 1,01 y 1,06 (antes, 0,55 a 0,86).
     El tope en `cqh` es el que impide que un panel muy alto haga un teclado
     gigante: el ancho no pasa del 62 % del ALTO del panel. */
  aspect-ratio: var(--key-cols, 3) / var(--key-rows, 4);
  width: 100%;
  align-self: center;
  grid-auto-rows: minmax(0, 1fr); align-content: center;
  /* EL TECLADO MIDE LO QUE MIDEN SUS FILAS, no todo el hueco. Con `flex: 1` se
     estiraba a lo alto del panel y, como sus filas están topadas para que las
     teclas no se conviertan en barras, el sobrante se quedaba DENTRO: una banda
     de aire encima y otra debajo de las teclas, dentro del propio teclado. Así
     el bloque —enunciado + teclado— queda junto y el aire se va a los bordes,
     que es donde no molesta. Puede encoger si el panel es bajo (`0 1 auto` +
     filas `minmax(0, …)`), que es lo que impide que se salga. */
  flex: 0 1 auto; height: auto;
}

/* ══════════════════════════════════════════════════════════════════════════
   CARRERA TERMINADA — se cierran las calculadoras y la animación llena el centro
   ══════════════════════════════════════════════════════════════════════════ */
.vs-arena.vs-race-finished .vs-panel { display: none; }
.vs-arena.vs-race-finished .vs-main {
  grid-template-columns: 1fr !important;
  grid-template-rows: 1fr !important;
  grid-template-areas: "stage" !important;
}
.vs-arena.vs-race-finished .vs-stage { animation: vs-stage-pop .45s ease-out; }
@keyframes vs-stage-pop { from { transform: scale(.85); opacity: .4; } to { transform: scale(1); opacity: 1; } }

/* ══════════════════════════════════════════════════════════════════════════
   TUG-OF-WAR SVG ANIMATION
   ══════════════════════════════════════════════════════════════════════════ */
.vs-tug-scene { width: 100%; height: 100%; }
.vs-tug-svg   { width: 100%; height: 100%; display: block; overflow: visible; }

.tug-ground    { stroke: rgba(0,0,0,.18); stroke-width: 3; stroke-linecap: round; }
.tug-pit       { fill: rgba(120,72,28,.28); }
.tug-centerline{ stroke: rgba(0,0,0,.22); stroke-width: 2; stroke-dasharray: 6 7; }
.tug-mark      { fill: none; stroke: rgba(0,0,0,.25); stroke-width: 2; stroke-linejoin: round; }

.tug-fig-left  { color: #0d6efd; }
.tug-fig-right { color: #dc3545; }
.tug-fig       { transform-box: fill-box; transform-origin: 50% 100%; animation: tug-strain 2.2s ease-in-out infinite; }
.tug-fig-right { animation-delay: -1.1s; }
.tug-limb  { stroke: currentColor; stroke-width: 13; stroke-linecap: round; fill: none; }
.tug-torso { stroke: currentColor; stroke-width: 26; stroke-linecap: round; }
.tug-head  { fill: currentColor; }
.tug-eye   { fill: #fff; }
.tug-hand  { fill: currentColor; }
.tug-shadow{ fill: rgba(0,0,0,.13); }

.tug-rope    { fill: none; stroke: #b9863b; stroke-width: 11; stroke-linecap: round; filter: drop-shadow(0 2px 1px rgba(0,0,0,.25)); }
.tug-knotball{ fill: #7a4e16; }
.tug-pole    { stroke: #555; stroke-width: 3; stroke-linecap: round; }
.tug-flag    { fill: #ffc107; stroke: #c79100; stroke-width: 1; }

@keyframes tug-strain { 0%,100% { transform: rotate(-3deg); } 50% { transform: rotate(-7deg); } }

.vs-tug-scene.lead-left  .tug-fig-left,
.vs-tug-scene.lead-right .tug-fig-right { animation: none; transform: rotate(-12deg); }
.vs-tug-scene.lead-left  .tug-fig-right,
.vs-tug-scene.lead-right .tug-fig-left  { animation: none; transform: rotate(9deg); }

@keyframes tug-yank-l { 0%,100% { transform: translateX(0); } 45% { transform: translateX(-30px); } }
@keyframes tug-yank-r { 0%,100% { transform: translateX(0); } 45% { transform: translateX(30px); } }
.vs-tug-scene.pull-left  .tug-dynamic { animation: tug-yank-l .45s ease; }
.vs-tug-scene.pull-right .tug-dynamic { animation: tug-yank-r .45s ease; }

.tug-dust { opacity: 0; transform-box: fill-box; transform-origin: center; }
.tug-dust circle { fill: rgba(140,110,70,.55); }
@keyframes tug-puff { 0% { opacity: .7; transform: translateY(0) scale(.4); } 100% { opacity: 0; transform: translateY(-20px) scale(1.6); } }
.vs-tug-scene.pull-left  .tug-dust-left  { animation: tug-puff .5s ease-out; }
.vs-tug-scene.pull-right .tug-dust-right { animation: tug-puff .5s ease-out; }

@media (prefers-reduced-motion: reduce) {
  .tug-fig, .tug-dynamic, .tug-dust { animation: none !important; }
}

/* ══════════════════════════════════════════════════════════════════════════
   SETUP SCREEN — avatar upload + effect toggles
   ══════════════════════════════════════════════════════════════════════════ */
.vs-av-upload-wrap { display: flex; align-items: center; gap: .5rem; }
.vs-av-preview {
  width: 52px; height: 52px; border-radius: 50%; overflow: hidden;
  border: 2px dashed #ced4da; background: #f8f9fa;
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.vs-av-thumb { width: 100%; height: 100%; object-fit: cover; }
.vs-av-btn   { flex-shrink: 0; }

.vs-fx-panel { max-width: 420px; text-align: left; border: 1px solid #e9ecef; border-radius: .6rem; background: #f8f9fa; }
.vs-fx-panel > summary { cursor: pointer; padding: .55rem .9rem; font-weight: 600; list-style: none; }
.vs-fx-panel > summary::-webkit-details-marker { display: none; }
.vs-fx-grid  { padding: .2rem .9rem .7rem; display: flex; flex-direction: column; gap: .35rem; }
.vs-fx-row   { display: flex; align-items: center; gap: .7rem; cursor: pointer; margin: 0; }
.vs-fx-label { font-weight: 600; line-height: 1.15; }
.vs-fx-label small { font-weight: 400; }

/* ══════════════════════════════════════════════════════════════════════════
   VS ANIMATION PICKER (Presentación panel)
   ══════════════════════════════════════════════════════════════════════════ */
.vsanim-pick      { display: flex; align-items: center; justify-content: center; }
.vsanim-tile-body { padding: .55rem; text-align: center; line-height: 1.1; }
.vsanim-tile-body .bi { font-size: 1.7rem; }
.vsanim-preview   { height: 76px; width: 100%; overflow: hidden; pointer-events: none; }
.vsanim-preview svg { width: 100% !important; height: 100% !important; }

/* ══════════════════════════════════════════════════════════════════════════
   RESULT SCREEN
   ══════════════════════════════════════════════════════════════════════════ */
.vs-result-screen {
  display: flex; align-items: center; justify-content: center;
  width: 100%; min-height: 100%; height: 100%; padding: 1rem;
}
.vs-result-screen .vs-result { width: 100%; max-width: 640px; }

/* ══════════════════════════════════════════════════════════════════════════
   VS WINNER CELEBRATION — base (works on any skin; the TV-Show skin amps it).
   Staggered entrance: rays + spotlight, crown drops, trophy pops, name scales.
   ══════════════════════════════════════════════════════════════════════════ */
.vs-celebration {
  position: relative; text-align: center;
  width: 100%; max-width: 560px; padding: 2rem 1rem; overflow: hidden;
}
.vs-celeb-rays {
  position: absolute; left: 50%; top: -25%; width: 130%; height: 110%;
  transform: translateX(-50%); z-index: -2; pointer-events: none;
  background: repeating-conic-gradient(from 0deg at 50% 38%,
    rgba(255,196,0,.10) 0deg 7deg, transparent 7deg 19deg);
  animation: vsc-spin 22s linear infinite;
}
.vs-celeb-spotlight {
  position: absolute; left: 50%; top: -12%; width: 120%; height: 64%;
  transform: translateX(-50%); z-index: -1; pointer-events: none;
  background: radial-gradient(ellipse at center top, rgba(255,255,255,.16), transparent 70%);
}
.vs-celeb-crown  { font-size: 2.6rem; line-height: 1; animation: vsc-drop .7s cubic-bezier(.2,1.4,.5,1) both; }
.vs-celeb-trophy { font-size: 4.6rem; color: #f9c700; line-height: 1; margin-top: -.2rem;
  filter: drop-shadow(0 6px 16px rgba(0,0,0,.4)); animation: vsc-pop .6s .15s cubic-bezier(.2,1.6,.5,1) both; }
.vs-celeb-label  { font-size: 1.05rem; font-weight: 900; letter-spacing: .3em;
  opacity: .9; margin-top: .5rem; animation: vsc-fade .5s .35s both; }
.vs-celeb-name   { font-size: clamp(2rem, 7vw, 3.6rem); font-weight: 900; line-height: 1.05;
  margin: .15rem 0; animation: vsc-pop .55s .3s cubic-bezier(.2,1.5,.5,1) both; }
.vs-celeb-score  { font-size: 1.35rem; font-weight: 800; opacity: .92; animation: vsc-fade .5s .5s both; }
.vs-celeb-podium { margin: 1.1rem auto 1.3rem; max-width: 420px; animation: vsc-fade .5s .6s both; }

/* RESUMEN DEL DUELO — qué puso cada uno, para que "ganó Ana" tenga un porqué
   delante de la clase (core/duelSummary.js). Colores por TOKEN: los skins
   recolorean el duelo entero y esto no puede quedarse fuera. */
.duel-sum {
  display: flex; gap: 2cqmin; justify-content: center; flex-wrap: wrap;
  margin: 0 auto 1.2rem; max-width: 640px; animation: vsc-fade .5s .7s both;
}
.duel-sum__side {
  flex: 1 1 40%; min-width: 40%;
  padding: 1.6cqmin 2cqmin; border-radius: 12px;
  background: var(--vs-done-bg, rgba(255,255,255,.08));
  border-top: 3px solid var(--ww-accent);
}
.duel-sum__side--left  { border-top-color: var(--vs-left,  var(--ww-accent)); }
.duel-sum__side--right { border-top-color: var(--vs-right, var(--ww-accent)); }
.duel-sum__name  { font-weight: 800; font-size: max(13px, 2.2cqmin); color: var(--vs-done-fg); }
.duel-sum__score { font-weight: 800; font-size: max(15px, 3cqmin);   color: var(--vs-done-fg); }
.duel-sum__list  { list-style: none; margin: .4rem 0 0; padding: 0; }
.duel-sum__list li {
  font-size: max(11px, 1.9cqmin); color: var(--vs-done-muted); line-height: 1.5;
}
.vs-celeb-actions{ animation: vsc-fade .5s .72s both; }
/* Base colour cue per winning side (skins override with richer tones). */
.vs-win-left  .vs-celeb-name { color: #2563eb; }
.vs-win-right .vs-celeb-name { color: #dc2626; }

@keyframes vsc-drop { from { opacity: 0; transform: translateY(-44px) scale(.6) rotate(-12deg); } to { opacity: 1; transform: none; } }
@keyframes vsc-pop  { from { opacity: 0; transform: scale(.4); } to { opacity: 1; transform: scale(1); } }
@keyframes vsc-fade { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
@keyframes vsc-spin { to { transform: translateX(-50%) rotate(360deg); } }

@media (prefers-reduced-motion: reduce) {
  .vs-celeb-rays { animation: none; }
  .vs-celeb-crown, .vs-celeb-trophy, .vs-celeb-label,
  .vs-celeb-name, .vs-celeb-score, .vs-celeb-podium, .vs-celeb-actions { animation: none; }
}
