/* Sin esto, [hidden] pierde contra reglas de autor con display:flex (mismo
   bug ya encontrado y corregido en game.css). */
[hidden]{ display:none !important; }

:root{
  --bg-black: oklch(0.14 0.01 40);
  --card-black: oklch(0.16 0.012 45);
  --panel-black: oklch(0.185 0.014 45);
  --coral: oklch(0.74 0.19 55);
  --coral-dark: oklch(0.45 0.13 45);
  --glow: oklch(0.78 0.2 60);
  --ink: oklch(0.97 0.005 90);
  --ink-dim: oklch(0.78 0.01 90);
  --ink-faint: oklch(0.55 0.01 90);
  --good: oklch(0.72 0.19 145);
  --hair: oklch(1 0 0 / 0.08);
}
*{ box-sizing:border-box; margin:0; padding:0; }
html,body{
  height:100%;
  /* Pantalla fija tipo app -- no debe scrollear nunca (mismo fix que portada.css). */
  overflow:hidden;
  overscroll-behavior:none;
}
body{
  font-family:"Inter","Helvetica Neue",Helvetica,Arial,sans-serif;
  background: oklch(0.08 0.005 40);
  color: var(--ink);
  display:flex;
  align-items:center;
  justify-content:center;
  min-height:100vh;
  /* 100dvh (alto visible real, sin la barra del navegador) pisa el 100vh de
     arriba solo donde el navegador lo soporta -- si no lo soporta, ignora la
     línea y se queda con el vh de respaldo. */
  min-height:100dvh;
  -webkit-font-smoothing:antialiased;
}
a{ color: var(--coral); }
a:hover{ color: oklch(0.8 0.18 58); }

.stage{
  position:relative;
  width:100%;
  min-height:100vh;
  min-height:100dvh;
  display:flex;
  align-items:center;
  justify-content:center;
  background: var(--bg-black);
}

#screen-prepay{
  position:relative;
  width:100%;
  max-width:430px;
  height:100vh;
  /* Fix real bug: 100vh en móvil cuenta el alto de la barra del navegador como
     visible, aunque no lo sea -- con overflow:hidden abajo, eso empujaba los
     botones fuera de la pantalla real. 100dvh es el alto visible verdadero. */
  height:100dvh;
  max-height:932px;
  background: var(--bg-black);
  /* SIN lienzo fijo/canvas-fit.js (2026-08-31) -- ver 01 Memory/
     42_BUG_FRANJAS_LIENZO_FIJO. #screen-prepay vuelve a ser el contenedor
     REAL del contenido (flex-column), no un host que centra un lienzo
     escalado. El carrusel es el bloque flexible que absorbe la diferencia
     entre 934px (alto natural del contenido) y el alto real del telefono. */
  display:flex;
  flex-direction:column;
  overflow:hidden;
  box-shadow: 0 40px 100px -20px oklch(0 0 0 / 0.6);
}
@media (min-width:561px){
  /* dvh acá también -- esta regla, al ser más específica, pisaba el fix de
     100dvh de la regla base con vh puro otra vez, reintroduciendo el mismo
     bug (alto mal calculado) para cualquier viewport >=561px. */
  #screen-prepay{ border-radius:44px; height:min(94dvh,932px); }
}

.brand-mini{
  display:flex; align-items:center; gap:7px;
  font-family:"Space Grotesk",sans-serif;
  font-size:13px; font-weight:700; letter-spacing:0.02em;
  color: var(--ink);
  /* Antes lo tenía .status-bar (borrada -- era un status bar falso de iOS,
     no algo real). Sin esto el logo quedaría pegado al notch real. */
  padding: calc(env(safe-area-inset-top,0px) + 8px) 22px 8px;
}
.brand-mini .mark{
  width:22px; height:23px;
  display:block;
  object-fit:contain;
}

.media-block{
  position:relative;
  /* flex:0 0 (sin encoger) para que coincida con el alto fijo de
     .media-primary (259px) -- si se encogía, la imagen se desbordaba
     sobre el bloque de "Fastest Engage" de abajo. */
  flex: 0 0 260px;
  min-height:0;
  /* margin-bottom negativo -- ajustado por el usuario en el canvas de
     /design (Prepay Spacing Editor, 2026-08-27) para cerrar el hueco
     vacío que quedaba antes de "Join Us". Verificado visualmente, no es
     un bug: no genera overlap real con el contenido de .engage-block. */
  margin: 0 20px -36px;
  display:flex;
  justify-content:flex-start;
}
.media-primary{
  /* Medidas exactas ajustadas por el usuario en el canvas de /design
     (04 Assets/design_prepay_spacing), ver 01 Memory. */
  position:relative;
  width:316px;
  height:219px;
  border-radius:22px;
  overflow:hidden;
  border:1px solid var(--hair);
}
.media-primary img{
  position:absolute;
  inset:0;
  width:100%;
  height:100%;
  object-fit:scale-down;
}
.media-primary-tag{
  position:absolute;
  top:10px; left:12px;
  font-size:10px;
  font-weight:800;
  letter-spacing:0.08em;
  text-transform:uppercase;
  color: var(--ink);
  background: oklch(0.1 0.01 40 / 0.65);
  padding:5px 10px;
  border-radius:999px;
  z-index:1;
}
.media-secondary{
  /* Fix real (2026-08-29): este left:286px era un pixel literal de un
     export de Figma pensado para un lienzo de EXACTAMENTE 430px de ancho.
     En cualquier pantalla real más angosta (375, 414...) empujaba el
     elemento fuera del borde derecho (medido: 36px de desborde real a
     375px) -- el contenedor lo recortaba, viéndose "cortado". Este
     elemento está diseñado para pegarse al borde DERECHO (3px de gap,
     medido en el render sin desborde a 430px) -- con right: en vez de
     left:, ese mismo gap se mantiene sin importar el ancho real de la
     pantalla, para siempre. */
  position:absolute;
  top:50px;
  right:3px;
  width:101px;
  height:117px;
  border-radius:18px;
  overflow:hidden;
  border:2.5px solid var(--bg-black);
  box-shadow: 0 12px 30px -8px oklch(0 0 0 / 0.6);
  z-index:2;
}
.media-secondary img{
  position:absolute;
  inset:0;
  width:100%;
  height:100%;
  object-fit:contain;
}
.live-tag{
  /* Afuera de .media-secondary (que tiene overflow:hidden) -- posicionado relativo
     a .media-block, arriba/afuera de la esquina superior derecha de esa tarjeta
     chica, sin que nada lo recorte. Ver comparación visual del usuario, 01 Memory.
     Fix real (2026-08-29): mismo bug que .media-secondary -- left:322px era un
     pixel literal de Figma para un lienzo de 430px exactos, se desbordaba en
     pantallas más angostas. right:32px (gap real medido sin desborde) lo
     mantiene pegado al borde derecho sin importar el ancho real. */
  position:absolute;
  top:30px; right:32px;
  display:flex; align-items:center; gap:3px;
  font-size:7.2px;
  font-weight:800;
  letter-spacing:0.04em;
  color: var(--ink);
  background: oklch(0.55 0.2 25 / 0.9);
  padding:2.4px 6.4px;
  border-radius:999px;
  z-index:3;
}
.live-tag .dot{ width:4px; height:4px; border-radius:50%; background:var(--ink); animation: pulse-dot 1.4s ease-in-out infinite; }
@keyframes pulse-dot{ 0%,100%{ opacity:1; } 50%{ opacity:0.35; } }

.engage-block{
  flex:none;
  padding: 8px 20px;
}
.engage-label{
  display:flex;
  justify-content:space-between;
  align-items:baseline;
  margin-bottom:8px;
}
.engage-label .title{
  font-family:"Space Grotesk",sans-serif;
  font-size:11.5px;
  font-weight:700;
  letter-spacing:0.06em;
  text-transform:uppercase;
  color: var(--ink-dim);
}
.engage-label .pct{
  font-size:12px;
  font-weight:800;
  color: var(--coral);
}
.progress-track{
  position:relative;
  width:100%;
  height:9px;
  border-radius:999px;
  background: oklch(0.28 0.02 45);
  overflow:hidden;
  border:1px solid var(--hair);
}
.progress-fill{
  position:absolute;
  inset:0;
  width:68%;
  border-radius:999px;
  background: linear-gradient(90deg, var(--coral-dark), var(--coral), var(--glow));
  box-shadow: 0 0 10px oklch(0.75 0.19 55 / 0.6);
}
.engage-actions{
  display:flex;
  justify-content:space-between;
  margin-top:10px;
}
.engage-item{
  display:flex;
  flex-direction:column;
  align-items:center;
  gap:3px;
  flex:1;
}
.engage-item svg{ width:18px; height:18px; color: var(--ink-dim); }
.engage-item .count{ font-size:11px; font-weight:700; color: var(--ink); }
.engage-item .label{ font-size:8.5px; font-weight:700; letter-spacing:0.05em; text-transform:uppercase; color: var(--ink-faint); }

.carousel-block{
  /* Vuelve a ser flexible (2026-08-31, ver 01 Memory/42_BUG_FRANJAS_LIENZO_
     FIJO) -- el alto fijo de 320px asumia que el lienzo fijo (#41) siempre
     daba 932px completos, pero ese lienzo dejaba franjas negras a los lados
     en pantallas cortas. Sin el lienzo, este es el UNICO bloque que se achica
     para que el resto (media/engage/cta/telegram-join, con sus medidas ya
     afinadas por el usuario, intactas) siga entrando sin cortarse ni hacer
     scroll. En pantallas altas ocupa ~297px (practicamente igual a los
     320px fijos de antes); min-height 90px es el piso real, igual al usado
     en /posttournament. */
  flex: 1 1 auto;
  /* Piso real medido (2026-08-31): con el contenido de adentro ya en su
     tamano minimo (ver .carousel-slide/.icon/h3/p mas abajo), 128px es lo
     que necesita el carrusel completo (slide + flechas) para no cortar
     texto en 741px de alto real (iPhone del usuario) -- medido directo en
     Chrome, no calculado a ojo. */
  min-height:124px;
  display:flex;
  flex-direction:column;
  /* clamp de dos puntos reales (741px -> valor chico, 932px -> valor
     original) via calc(), no vh puro -- vh puro no pasa por los dos puntos
     que hacen falta. Ver 01 Memory/42_BUG_FRANJAS_LIENZO_FIJO. */
  padding: clamp(4px, calc(-27.05px + 4.19vh), 12px) 0 clamp(14px, calc(-106.26px + 16.23vh), 45px);
  border-top:1px solid var(--hair);
  background:
    radial-gradient(ellipse 90% 60% at 50% 0%, oklch(0.3 0.08 55 / 0.14), transparent 65%),
    var(--card-black);
}
.carousel-viewport{
  position:relative;
  flex:1;
  min-height:0;
  overflow:hidden;
  padding: 0 20px;
}
.carousel-track{
  display:flex;
  height:100%;
  transition: transform 0.35s cubic-bezier(.4,0,.2,1);
}
.carousel-slide{
  flex: 0 0 100%;
  display:flex;
  flex-direction:column;
  align-items:center;
  justify-content:center;
  text-align:center;
  /* clamp(vh) en vez de valores fijos (2026-08-31) -- ahora que el carrusel
     puede achicarse en pantallas cortas (ver 01 Memory/42_BUG_FRANJAS_
     LIENZO_FIJO), el contenido de adentro tiene que encogerse CON el
     espacio real disponible, no quedarse fijo y cortarse contra el
     overflow:hidden de .carousel-viewport. Mismo patron que .timer-dial de
     game.css. */
  gap: clamp(3px, calc(-16.41px + 2.62vh), 8px);
  padding: clamp(1px, calc(-10.64px + 1.57vh), 4px) 6px;
  border-radius:16px;
  /* Preparado para imagen de fondo por slide: setear --slide-bg inline
     (style="--slide-bg:url(...)") sin tocar esta hoja de estilos. */
  background-image: var(--slide-bg, none);
  background-size: cover;
  background-position: center;
}
.slide-head{
  display:flex;
  align-items:center;
  justify-content:center;
  gap: clamp(4px, calc(-11.49px + 2.09vh), 8px);
}
.carousel-slide .icon{
  width:clamp(18px, calc(-20.83px + 5.24vh), 28px); height:clamp(18px, calc(-20.83px + 5.24vh), 28px);
  border-radius:9px;
  background: linear-gradient(155deg, var(--coral), var(--coral-dark));
  display:flex; align-items:center; justify-content:center;
  flex:none;
}
.carousel-slide .icon svg{ width:clamp(10px, calc(-9.41px + 2.62vh), 15px); height:clamp(10px, calc(-9.41px + 2.62vh), 15px); }
.carousel-slide h3{
  font-family:"Space Grotesk",sans-serif;
  font-size:clamp(11.5px, calc(-2.06px + 1.83vh), 15px);
  font-weight:700;
  color: var(--ink);
}
.carousel-slide p{
  font-size:clamp(9.5px, calc(-2.14px + 1.57vh), 12.5px);
  line-height:1.28;
  color: var(--ink-faint);
  max-width:280px;
}

.carousel-nav{
  display:flex;
  align-items:center;
  justify-content:space-between;
  padding: clamp(2px, calc(-13.49px + 2.09vh), 6px) 14px 0;
}
.carousel-arrow{
  width:clamp(26px, calc(2.73px + 3.14vh), 32px); height:clamp(26px, calc(2.73px + 3.14vh), 32px);
  border-radius:50%;
  border:1.5px solid var(--hair);
  background: var(--panel-black);
  color: var(--ink-dim);
  display:flex; align-items:center; justify-content:center;
  cursor:pointer;
  flex:none;
}
.carousel-arrow:hover{ border-color: oklch(0.55 0.1 55 / 0.6); color: var(--ink); }
.carousel-arrow svg{ width:15px; height:15px; }
.carousel-dots{
  display:flex;
  gap:6px;
}
.carousel-dots .dot{
  width:6px; height:6px;
  border-radius:50%;
  background: oklch(0.4 0.02 45);
  transition: background 0.2s ease, transform 0.2s ease;
}
.carousel-dots .dot.is-active{
  background: var(--coral);
  transform: scale(1.3);
}

.cta-block{
  flex:none;
  /* Sin safe-area-inset-bottom acá a propósito -- este bloque no toca el
     borde físico de abajo (.telegram-join-block va después y ya reserva esa
     franja), así que sumarla acá era doble conteo: le robaba ~34px reales
     en iPhone al presupuesto vertical del carrusel de arriba. Bottom subido
     a 58px (2026-08-30, canvas de diseño final): el nuevo asset PNG del
     botón (boton-on-large.png) trae el glow ya incrustado en la imagen y se
     derrama debajo del botón -- necesita ese aire para no cortarse. Piso
     bajado a 38px SOLO en pantallas cortas (clamp, 2026-08-31, ver 01
     Memory/42_BUG_FRANJAS_LIENZO_FIJO) -- overflow:visible en este bloque
     ya deja que el glow se pinte afuera de su padding igual, asi que
     bajar el aire no lo corta, solo se superpone un poco mas con el
     bloque de Telegram de abajo en el peor caso; verificado visualmente
     sin verse roto. */
  padding: 12px 20px clamp(38px, calc(-39.58px + 10.47vh), 58px);
  background: var(--card-black);
  border-top:1px solid var(--hair);
  /* overflow:visible a propósito -- el glow del PNG del botón se pasa por
     fuera del box-model normal del bloque (ver .cta-btn-bg, top negativo). */
  overflow:visible;
}
.glow-btn{
  position:relative;
  width:100%;
  padding:16px 24px;
  border:1.5px solid oklch(0.78 0.19 58 / 0.9);
  border-radius:999px;
  background: radial-gradient(120% 160% at 50% 120%, oklch(0.32 0.1 45 / 0.55), transparent 60%), oklch(0.1 0.01 40);
  color: var(--ink);
  font-family:inherit;
  font-size:16px;
  font-weight:700;
  /* cursor:pointer en la clase base (2026-08-31, reportado por el usuario:
     "sale el icono de prohibido" en escritorio) -- estaba en not-allowed
     SIEMPRE, sin importar si el boton de verdad estaba deshabilitado.
     .login-pin-submit:disabled (mas abajo) ya pone not-allowed cuando
     corresponde de verdad. */
  cursor:pointer;
  display:flex;
  align-items:center;
  justify-content:center;
  gap:8px;
  isolation:isolate;
  box-shadow:
    0 0 1px oklch(0.85 0.18 58 / 0.9),
    inset 0 0 12px oklch(0.7 0.19 55 / 0.35),
    0 0 18px oklch(0.72 0.19 55 / 0.55),
    0 0 40px oklch(0.65 0.18 50 / 0.35);
  overflow:visible;
}
.glow-btn::before{
  content:"";
  position:absolute;
  inset:-14px;
  z-index:-1;
  border-radius:999px;
  background: conic-gradient(from 200deg,
    transparent 0deg, oklch(0.75 0.2 60 / 0.55) 60deg, transparent 140deg,
    transparent 200deg, oklch(0.7 0.18 40 / 0.4) 260deg, transparent 340deg);
  filter: blur(14px);
  opacity:0.9;
  pointer-events:none;
}
.glow-btn svg{ width:16px; height:16px; flex:none; }

/* CTA final (canvas de diseño, 2026-08-30) -- reemplaza las 5 capas CSS a mano
   del glass-cta viejo (frost/face/edge/glow + filtro SVG feDisplacementMap) por
   un solo PNG exportado con el vidrio/glow ya incrustado en la imagen
   (boton-on-large.png). Modificador propio (.cta-svg-btn) para no tocar
   .glow-btn base, que comparten los botones de los modales de login/join. No
   toca prepay.js -- JS solo hace classList.toggle('is-waiting'/'is-confirming'/
   'is-locked') sobre el botón, nunca referencia estas capas internas. */
.glow-btn.cta-svg-btn{
  border:none;
  background:none;
  box-shadow:none;
  isolation:isolate;
}
.glow-btn.cta-svg-btn::before{
  /* El glow ya viene incrustado en boton-on-large.png -- el halo de ::before
     (conic-gradient + blur) del .glow-btn base quedaría duplicado. El halo del
     estado "Waiting to Start…" (glow-pulse de abajo) queda pendiente de un
     asset propio -- por ahora ese estado se ve con el PNG estático "on", sin
     animación (decisión explícita del usuario, 2026-08-30: "si hace falta un
     poco de glow se lo agregamos en un siguiente paso"). */
  content:none;
}
.cta-btn-bg{
  position:absolute; left:0; top:-20px;
  width:100%; height:auto;
  z-index:0;
  pointer-events:none;
}
/* Estado bloqueado (identidad sin resolver, Etapa 2) -- dos PNG/SVG completos
   (ON/OFF) en vez del filtro opacity+grayscale que usan Join Us/Log In, porque
   el usuario diseñó el botón "apagado" como un asset propio, no como un dimming
   genérico. prepay.js sigue siendo el único que decide cuál se ve: solo hace
   classList.toggle('is-locked', ...), nunca referencia estas dos capas.
   Fix real (2026-08-30): el atributo style="" inline que traía el canvas de
   diseño NUNCA se aplicaba -- el CSP del servidor bloquea estilos inline
   igual que bloquea <script>/<style> (mismo bug de siempre, ver 01 Memory).
   Se pasan a reglas CSS reales acá. Valores finales ajustados a ojo por el
   usuario con una herramienta de alineación (artifact interactivo con los
   assets reales superpuestos, comparando ON/OFF en overlay) -- no son iguales
   entre sí a propósito, cada asset tiene su propio recorte/proporción real. */
.cta-btn-bg--on{ top:5px; height:117px; object-fit:fill; }
.cta-btn-bg--off{ top:-15px; height:131px; object-fit:fill; display:none; }
#btn-reserve-spot.is-locked .cta-btn-bg--on{ display:none; }
#btn-reserve-spot.is-locked .cta-btn-bg--off{ display:block; }

/* "Waiting to Start…" -- fade in/fade out real entre ON y OFF (pedido
   explícito del usuario, 2026-09-01: "el fade in fade out del boton de ON a
   OFF no lo hace" -- la primera versión solo atenuaba la opacidad de --on,
   sin mostrar nunca --off, así que no se veía como un cruce real entre las
   dos capas). Mismos valores ya aprobados de prepay-sandbox.css (0.4/1,
   pico en 35%, ciclo 3.2s, "encendido moderado apagado lento") -- --off
   ahora se muestra (display:block) y anima con la curva inversa, así cuando
   --on baja a 0.4 --off sube y se ve la capa "apagada" por debajo, y
   viceversa. */
/* top/height propios acá (no los de la regla base de arriba, pensada para
   is-locked donde --off se ve sola) -- verificado con overlay real (los dos
   assets superpuestos a la misma opacidad, sin canal alpha) que el pill de
   --off con top:-15px/height:131px NO coincide con el de --on (top:5px/
   height:117px): --off es un SVG ya recortado casi al pill exacto (sin
   bleed de glow como el PNG de --on), así que con esos valores queda mucho
   más grande y desplazado. top:21px/height:56px sí calza pill-con-pill
   (reportado por el usuario: "si tienen los mismos parametros no tienen
   pq no machar", 2026-09-01 -- el problema real era que NO tenían los
   mismos parámetros de destino, aunque debieran). */
#btn-reserve-spot.is-waiting .cta-btn-bg--off{
  display:block;
  top:21px;
  height:56px;
}
#btn-reserve-spot.is-waiting .cta-btn-bg--on{
  animation: cta-fade-on 3.2s ease-in-out infinite;
}
#btn-reserve-spot.is-waiting .cta-btn-bg--off{
  animation: cta-fade-off 3.2s ease-in-out infinite;
}
@keyframes cta-fade-on{
  0%   { opacity: 0.4; }
  35%  { opacity: 1; }
  100% { opacity: 0.4; }
}
@keyframes cta-fade-off{
  0%   { opacity: 0.6; }
  35%  { opacity: 0; }
  100% { opacity: 0.6; }
}
/* Texto + ícono -- fade chico, no completo (pedido explícito del usuario,
   2026-09-01: "no completo como el color del boton pero si algo pq es parte
   del estilo"). Mismo timing que el glow de fondo (3.2s, pico en 35%) para
   que todo respire en sincronía, rango de opacidad mucho más angosto
   (0.78-1) para que el texto siga siendo legible en todo momento. */
#btn-reserve-spot.is-waiting #btn-reserve-label{
  animation: cta-label-fade 3.2s ease-in-out infinite;
}
@keyframes cta-label-fade{
  0%   { opacity: 0.78; }
  35%  { opacity: 1; }
  100% { opacity: 0.78; }
}
/* Ripple de "esperando" -- mismo patron ya aprobado y en uso en
   .lobby-ripple (game.css/client.html), reusado tal cual (radio del anillo,
   timing, delays entre anillos) en vez de inventar valores nuevos, solo
   adaptado de circulo (logo) a pildora (boton). Oculto por defecto (solo
   .cta-ripple sin mas), visible unicamente en is-waiting. */
.cta-ripple{
  position:absolute;
  inset:-6px;
  /* SIN z-index:-1 (2026-09-01) -- #btn-reserve-spot tiene isolation:isolate
     (.cta-svg-btn), asi que un z-index negativo lo mandaba detras de TODO
     dentro de ese contexto, incluidas .cta-btn-bg--on/--off (fondo opaco,
     cubre el boton entero) -- el ripple animaba pero quedaba tapado, nunca
     se veia. .lobby-ripple (game.css), el patron original que se copio
     ("reusado tal cual" decia el comentario viejo, pero este z-index no
     estaba ahi) no lleva z-index -- queda en el flujo normal. */
  border-radius:999px;
  border:2px solid oklch(0.74 0.19 55 / 0.6);
  pointer-events:none;
  display:none;
}
#btn-reserve-spot.is-waiting .cta-ripple{
  display:block;
  animation: cta-ripple 2.4s ease-out infinite;
}
#btn-reserve-spot.is-waiting .cta-ripple.r2{ animation-delay:0.8s; }
#btn-reserve-spot.is-waiting .cta-ripple.r3{ animation-delay:1.6s; }
@keyframes cta-ripple{
  0%   { transform:scale(1); opacity:0.6; }
  100% { transform:scale(1.3); opacity:0; }
}
/* Ícono girando de "Waiting to Start…" -- prepay.js ya le pone la clase
   .spin (ver applyIdentityUI), pero la animación nunca existió en ningún
   CSS del proyecto -- el ícono se quedaba quieto. */
#btn-reserve-icon.spin{
  animation: cta-spin 1s linear infinite, cta-label-fade 3.2s ease-in-out infinite;
  transform-origin: center;
}
@keyframes cta-spin{ to{ transform: rotate(360deg); } }
/* Texto/ícono grises mientras está bloqueado (identidad sin resolver), blancos
   cuando se desbloquea -- pedido explícito del usuario, 2026-08-30. Blanco
   (var(--ink)) ya es el color por defecto heredado de .glow-btn, así que solo
   hace falta la excepción para el estado bloqueado. var(--ink-dim) medido
   (oklch 0.78 vs 0.97 del blanco) resultó muy sutil para leerse como
   "apagado" a simple vista -- var(--ink-faint) (oklch 0.55) es el mismo gris
   marcado que ya usa el resto del proyecto para texto secundario/inactivo. */
#btn-reserve-spot.is-locked #btn-reserve-label,
#btn-reserve-spot.is-locked #btn-reserve-icon{
  color: var(--ink-faint);
}
.glow-btn.cta-svg-btn span,
.glow-btn.cta-svg-btn svg{
  position:relative;
  z-index:1;
  /* top:24px (ajustado a ojo por el usuario con la misma herramienta de
     alineación, 2026-08-30) -- el texto/ícono bajan para centrarse en el
     pill real de boton-on-large.png (top:5px; height:117px de arriba). */
  top:24px;
  text-shadow: 1px 1px 1px rgba(0,0,0,0.35);
}

/* Vincula tu Telegram — secundario, no compite con .glow-btn (Opcion A del
   canvas de diseño, aprobada 2026-08-19/20). Badge separado del boton para que
   se lea como explicacion ("como funciona"), no como otra forma de entrar. */
.telegram-join-block{
  display:flex;
  flex-direction:column;
  align-items:center;
  gap:10px;
  /* Top bajado de 15px a 2px (canvas de diseño, 2026-08-30) para subir la
     fila un poco. Bottom sigue con el safe-area-inset -- espacio extra para
     no quedar tapado por el gesture bar/home indicator del sistema (TikTok,
     Instagram, etc. dejan este margen), eso no lo vio el canvas de diseño. */
  padding: 2px 20px calc(40px + env(safe-area-inset-bottom,0px));
  background: var(--card-black);
}
.telegram-join-btn{
  /* Fondo/borde/box-shadow removidos (canvas de diseño, 2026-08-30) -- ambos
     botones (Join Us, Log In) ahora son un PNG/SVG completo de fondo
     (.cta-btn-bg / .join-us-glow-bg de abajo), no un pill de CSS. */
  position:relative;
  width:100%;
  padding:14px 20px;
  border:none;
  border-radius:999px;
  background:none;
  color: var(--ink);
  font-family:"Space Grotesk",sans-serif;
  font-size:15px;
  font-weight:700;
  cursor:pointer;
  display:flex;
  align-items:center;
  justify-content:center;
  gap:8px;
  isolation:isolate;
}
.telegram-join-btn .cta-btn-bg{
  position:absolute; inset:0;
  width:100%; height:100%;
  object-fit:fill;
  z-index:0;
  pointer-events:none;
}
/* Par Left/Right dedicado (2026-09-01) -- reemplaza el hack de glow-bleed
   manual de arriba (join-us-glow-bg + mirror right:-20px), que causaba el
   overlap/overflow real reportado varias veces en la sesion. Los 4 assets
   (boton-on/off-left.png, boton-on/off-right.png) tienen el MISMO canvas
   (414x274) y el MISMO pillbox (17,77)-(396,196) verificado con Python/PIL
   -- el glow ya viene incrustado hacia el lado correcto adentro del propio
   canvas (el glow suave no toca ningun borde), asi que alcanza con centrar
   la imagen completa verticalmente al 100% del ancho del boton, sin ningun
   top/height calibrado a mano ni bleed fuera de la caja. Como ON y OFF
   comparten canvas/pillbox exactos, esta unica regla sirve para ambos
   estados de ambos lados -- ya no hace falta una excepcion por estado. */
.join-btn-bg,
.login-btn-bg{
  position:absolute;
  left:0; top:50%;
  width:100%; height:auto;
  transform:translateY(-50%);
  z-index:0;
  pointer-events:none;
}
/* Log In arranca ON (par Right ON) -- pedido explicito del usuario
   2026-09-01: antes arrancaba "apagado" (par Right OFF) pero eso era un
   remanente de cuando el hack de glow-bleed viejo rompia en telefonos
   reales (Etapa 3.42); con el par Left/Right dedicado ese riesgo ya no
   existe, asi que Log In se ve activo desde el inicio, igual que Join Us.
   El par OFF queda armado en el DOM por si hace falta un estado inactivo
   real mas adelante, pero hoy no se usa. */
.login-btn-bg--on{ display:block; }
.login-btn-bg--off{ display:none; }
.telegram-join-btn span,
.telegram-join-btn svg{ position:relative; z-index:1; }
.telegram-join-btn svg{ width:17px; height:17px; flex:none; }
.telegram-join-btn:disabled{ opacity:0.6; cursor:wait; }
.telegram-badge{
  display:flex;
  align-items:center;
  gap:5px;
  padding:4px 10px;
  border-radius:999px;
  background: var(--panel-black);
  border:1px solid var(--hair);
  /* top:14px (canvas de diseño, 2026-08-30) -- movido acá desde un style=""
     inline que el CSP bloqueaba en silencio (ver scripts/check-inline-styles.cjs,
     encontrado justo por ese script). */
  position:relative;
  top:14px;
}
.telegram-badge svg{ width:11px; height:11px; flex:none; }
.telegram-badge span{
  font-size:10.5px;
  font-weight:600;
  color: var(--ink-faint);
  letter-spacing:.02em;
}
.telegram-join-note{
  margin:0;
  font-size:11.5px;
  line-height:1.5;
  color: var(--ink-faint);
  text-align:center;
}

/* Join Us y Log In lado a lado (no apilados) -- mismo ancho cada uno. Gap
   bajado de 40px a 12px (2026-09-01, Etapa 3.43): los 40px eran para que el
   glow-bleed manual de start-boton.png (se derramaba ~20px a cada lado) no
   se pisara -- con el par Left/Right dedicado el glow ya viene incrustado
   adentro del propio canvas de cada asset, no hace falta margen extra. */
.telegram-join-row{
  display:flex;
  width:100%;
  gap:12px;
}
.telegram-join-item{
  flex:1;
  display:flex;
  flex-direction:column;
  align-items:center;
  gap:2px;
}
.telegram-join-item .telegram-join-btn{
  width:100%;
}
/* Leyenda debajo del boton en modo chat ("Talk to AI") -- letra regular a
   proposito (pedido del usuario 2026-08-31), no bold como el resto de los
   labels de esta fila. margin-top (2026-09-01): medido con canvas real
   (pixeles del glow renderizado, umbral 5% alpha, no la caja del <img>)
   -- el glow visible del par Left/Right termina 18px debajo del borde
   real del boton (no 39px, esa cifra era la caja completa de la imagen
   con su aire transparente, no el pixel visible). El gap base de 2px de
   .telegram-join-item ya cubre parte de eso, asi que acá solo hacen
   falta 16px mas (18-2) para que el caption arranque justo debajo del
   glow visible, sin aire de mas. */
.telegram-join-caption{
  font-family:"Inter",sans-serif;
  font-size:11px;
  font-weight:400;
  line-height:1;
  margin-top:16px;
  color: var(--ink-dim);
}
/* Log In -- se mudó acá desde la portada (misma pantalla que "Join Us", donde
   vive la identidad del jugador). Ya clickeable -- abre el modal de PIN.
   Blanco fijo (2026-09-01, Etapa 3.43+): el boton arranca ON (par Right
   dedicado, ver .login-btn-bg--on) igual que Join Us, asi que el texto va
   blanco siempre, no gris -- el gris era del estado OFF viejo que ya no
   existe por defecto. */
#btn-login-label{ color: var(--ink); }

/* Modal de Log In con PIN -- backend de verificación pendiente, ver
   01 Memory/15_PLAN_INTEGRACION_VISUALES. */
.login-modal-overlay{
  position:absolute;
  inset:0;
  z-index:40;
  background: oklch(0 0 0 / 0.6);
  display:flex;
  align-items:center;
  justify-content:center;
  padding:24px;
}
.login-modal-card{
  position:relative;
  width:100%;
  max-width:320px;
  /* max-height + overflow-y:auto -- sin esto, si los hijos de acá abajo
     necesitan más espacio del que hay, se encogían (flex-shrink:1 por
     default) en vez de que el modal scrolleara -- eso se veía como inputs
     comprimidos/overlapeados. Relativo al padre (.login-modal-overlay,
     inset:0 sobre #screen-prepay, el alto REAL del telefono) -- ya no hay
     un lienzo fijo de 932px del cual depender (2026-08-31, ver 01 Memory/
     42_BUG_FRANJAS_LIENZO_FIJO). */
  max-height:calc(100% - 48px);
  overflow-y:auto;
  background: var(--card-black);
  border:1px solid var(--hair, oklch(1 0 0 / 0.08));
  border-radius:24px;
  padding:28px 22px 24px;
  display:flex;
  flex-direction:column;
  align-items:center;
  text-align:center;
  gap:14px;
  box-shadow: 0 24px 60px -12px oklch(0 0 0 / 0.7);
}
/* #join-form-step y #join-success-step son subdivs de .login-modal-card (para el
   paso de transición "You're in!") -- el gap:14px del padre solo separa a estos
   dos divs ENTRE SÍ, no a lo que hay adentro de cada uno. Sin esto, título,
   subtítulo, inputs y botón quedaban con 0px de separación real entre sí (medido
   con getBoundingClientRect -- se veían pegados/superpuestos). */
#join-form-step,
#join-success-step{
  display:flex;
  flex-direction:column;
  align-items:center;
  text-align:center;
  gap:14px;
  width:100%;
}
.login-modal-close{
  position:absolute;
  top:14px; right:14px;
  width:28px; height:28px;
  border-radius:50%;
  border:none;
  background: var(--panel-black);
  display:flex; align-items:center; justify-content:center;
  cursor:pointer;
}
.login-modal-close svg{ width:14px; height:14px; }
.login-modal-title{
  font-family:"Space Grotesk",sans-serif;
  font-size:18px;
  font-weight:700;
  color: var(--ink);
  margin:6px 0 0;
  flex-shrink:0;
}
.login-modal-subtitle{
  margin:0;
  font-size:12.5px;
  line-height:1.5;
  color: var(--ink-faint);
  flex-shrink:0;
}
.login-pin-input{
  width:100%;
  padding:14px;
  border-radius:16px;
  border:1px solid var(--hair, oklch(1 0 0 / 0.08));
  background: var(--panel-black);
  color: var(--ink);
  font-family:"Space Grotesk",sans-serif;
  font-size:22px;
  font-weight:700;
  letter-spacing:0.4em;
  text-align:center;
  outline:none;
  flex-shrink:0;
}
.login-pin-submit{ width:100%; flex-shrink:0; }
/* opacity sola no alcanzaba -- el glow (box-shadow + ::before conic-gradient
   blureado) es tan intenso que un boton deshabilitado al 55% seguia leyendose
   "prendido", por lo que el cursor not-allowed parecia un bug en vez de una
   senal real de "llena el formulario primero" (reportado 2026-08-31). */
.login-pin-submit:disabled{
  opacity:0.45;
  cursor:not-allowed;
  filter:grayscale(0.8);
  box-shadow:none;
}
.login-pin-submit:disabled::before{ opacity:0.15; }
/* El placeholder largo ("Set a PIN...") con el letter-spacing/font-size grandes del
   input se salía de la caja -- achicado solo en el placeholder, el PIN ya tipeado
   sigue viéndose grande y espaciado. */
.login-pin-input::placeholder{ font-size:13px; letter-spacing:0.02em; }
.join-text-input{
  font-family:inherit;
  font-size:15px;
  font-weight:500;
  letter-spacing:normal;
  text-align:left;
}
.login-pin-error{
  margin:0;
  font-size:12px;
  flex-shrink:0;
  color: var(--bad, oklch(0.65 0.19 25));
}

/* Etapa 2 (identidad) -- estado gris/inactivo visual para Join Us / Log In
   mientras la identidad todavía no se resolvió o no aplica. Igual que
   .glow-btn:disabled, sin depender del atributo disabled porque estos botones
   necesitan seguir recibiendo el click para mostrar el aviso. Reserve My Spot
   ya NO usa este filtro genérico -- su estado bloqueado tiene su propio asset
   (boton-off-large.svg, ver .cta-btn-bg--off arriba), diseñado a propósito en
   vez de un dimming genérico. */
.telegram-join-btn.is-inactive{
  opacity:0.45;
  filter: grayscale(0.6);
}
.telegram-join-btn.is-hidden{ display:none; }

/* Widget de chat AI-PAL — mockup aprobado en /design (canvas "AI-PAL Chat
   Widget"), integrado como componente real dentro de #screen-prepay (mismo
   frame vertical de 430px). Colapsado por default, solo aparece si el
   participante ya está vinculado (chat:history llega, chat:not_linked no). */
.chat-bubble{
  display:none;
  position:absolute;
  right:16px;
  bottom:20px;
  z-index:20;
  width:56px;
  height:56px;
  border-radius:50%;
  border:1.5px solid oklch(0.78 0.19 58 / 0.6);
  background: radial-gradient(120% 160% at 50% 120%, oklch(0.32 0.1 45 / 0.5), transparent 60%), oklch(0.1 0.01 40);
  align-items:center;
  justify-content:center;
  box-shadow:
    0 0 1px oklch(0.85 0.18 58 / 0.8),
    inset 0 0 10px oklch(0.7 0.19 55 / 0.3),
    0 0 18px oklch(0.72 0.19 55 / 0.4),
    0 4px 16px oklch(0.1 0 0 / 0.5);
  cursor:pointer;
}
.chat-bubble.is-visible{ display:flex; }
.chat-bubble svg{ width:24px; height:24px; }
.chat-bubble .unread-dot{
  position:absolute;
  top:-2px; right:-2px;
  width:12px; height:12px;
  border-radius:50%;
  background: var(--coral);
  border:2px solid var(--bg-black);
  display:none;
}
.chat-bubble .unread-dot.is-visible{ display:block; }

.chat-panel{
  display:none;
  position:absolute;
  inset:0;
  z-index:30;
  background: var(--bg-black);
  flex-direction:column;
}
.chat-panel.is-open{ display:flex; }
.chat-header{
  flex:none;
  display:flex;
  align-items:center;
  gap:10px;
  padding:14px 16px;
  background: var(--card-black);
  border-bottom:1px solid var(--hair);
}
.chat-header .avatar{
  width:36px; height:36px; border-radius:50%; flex:none;
  background: linear-gradient(155deg, var(--coral), var(--coral-dark));
  display:flex; align-items:center; justify-content:center;
}
.chat-header .avatar svg{ width:18px; height:18px; }
.chat-header .titles{ flex:1; display:flex; flex-direction:column; gap:1px; min-width:0; }
.chat-header .titles .name{ font-family:"Space Grotesk",sans-serif; font-size:14.5px; font-weight:700; color:var(--ink); }
.chat-header .titles .subtitle{ font-size:10.5px; color:var(--ink-faint); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.chat-header .close-btn{
  flex:none; width:28px; height:28px; border-radius:50%; border:none;
  background: var(--panel-black); display:flex; align-items:center; justify-content:center; cursor:pointer;
}
.chat-header .close-btn svg{ width:14px; height:14px; }

.chat-messages{
  flex:1;
  overflow-y:auto;
  padding:16px;
  display:flex;
  flex-direction:column;
  gap:12px;
}
.chat-msg{ max-width:78%; display:flex; flex-direction:column; gap:3px; }
.chat-msg.from-ai{ align-self:flex-start; }
.chat-msg.from-user{ align-self:flex-end; align-items:flex-end; }
.chat-msg .bubble{ padding:10px 14px; font-size:13.5px; line-height:1.45; }
.chat-msg.from-ai .bubble{
  background: var(--panel-black);
  border:1px solid var(--hair);
  border-radius:16px 16px 16px 4px;
  color: var(--ink);
}
.chat-msg.from-user .bubble{
  background: linear-gradient(155deg, var(--coral), var(--coral-dark));
  border-radius:16px 16px 4px 16px;
  color: oklch(0.16 0.02 30);
  font-weight:500;
}
.chat-msg .timestamp{ font-size:10px; color:var(--ink-faint); margin:0 4px; }

.chat-input-row{
  flex:none;
  display:flex;
  align-items:flex-end;
  gap:8px;
  padding: 12px 14px calc(12px + env(safe-area-inset-bottom,0px));
  background: var(--card-black);
  border-top:1px solid var(--hair);
}
.chat-input-row textarea{
  flex:1;
  resize:none;
  max-height:120px;
  background: var(--panel-black);
  border:1px solid var(--hair);
  border-radius:20px;
  padding:11px 16px;
  font-family:inherit;
  font-size:13.5px;
  line-height:1.4;
  color: var(--ink);
  outline:none;
}
.chat-send-btn{
  flex:none;
  width:38px; height:38px;
  border-radius:50%;
  background: radial-gradient(120% 160% at 50% 120%, oklch(0.32 0.1 45 / 0.5), transparent 60%), oklch(0.1 0.01 40);
  border:1.5px solid oklch(0.78 0.19 58 / 0.6);
  display:flex; align-items:center; justify-content:center;
  cursor:pointer;
}
.chat-send-btn svg{ width:16px; height:16px; }
