  :root{
    --bg:#EEF2F7; --surface:#FFFFFF; --surface-2:#E4EAF2; --accent-text:#125E80;
    --ink:#16283D; --ink-soft:#51637A; --line:#AEBFD3; --line-fuerte:#8FA3BD;
    --accent:#1C9FD7; --accent-border:#167DA8; --accent-hi:#36BDF7; --accent-ink:#0B2C4D; --accent-soft:#D6EEFB; --accent-soft-ink:#0B2C4D;
    /* --muted-fill NO puede ser igual a --surface: los botones secundarios
       (.btn-secondary — "Vaciar", "Cancelar", "Volver al mapa", etc.) casi
       siempre están parados sobre un .panel/.subpanel, que usa --surface de
       fondo. Si coinciden, el botón se funde con el fondo y solo queda el
       borde fino como pista — se ve "sin color", casi apagado/ya-presionado
       (reportado por el usuario). Un tono --surface-2 lo separa del panel
       en los dos temas sin inventar un color nuevo. */
    --muted-fill:#E4EAF2; --muted-border:#6B6E85;
    --danger-fill:#FBDAD3; --danger-ink:#8A2A18; --danger-border:#D98F7B;

    --cat-move:#FF6B5B;   --cat-move-ink:#5C160D;
    --cat-loop:#FFC93C;   --cat-loop-ink:#5A3E00;
    --cat-sound:#3FC98A;  --cat-sound-ink:#0B3F28;
    --cat-ai:#8B6FF0;     --cat-ai-ink:#241454;
    --tan-6:#E8663C;      --tan-7:#2FA8A0;   /* piezas 6 y 7 del tangram avanzado */
    --world-robo:#2F8FE0; --world-robo-ink:#0B2C4D;
    --world-math:#12B8A0; --world-math-ink:#063D35;
    --avatar-pink:#FF6FA5;
    --gold:#E0A62C;
    /* Verde de "esto ya lo tienes". Va como token y no como literal
       porque se usa sobre --surface, que cambia con el tema: el mismo
       verde legible en blanco queda apagado sobre el azul oscuro. */
    --ok:#2E9E5B;
    /* HACIA DÓNDE SE APAGA UN COLOR DE MUNDO.
       Apagar es acercarse al fondo, y el fondo cambia con el tema: al
       negro en oscuro, al blanco en claro. Se hace con un token y no
       mezclando con --line porque --line es AZUL (#33506F): mezclar el
       coral del mundo con azul lo empuja del tono 6° al 348°, o sea al
       lado rosa. El dueño lo vio antes que yo — "ese tono de rojo parece
       morado". Mezclar con negro o con blanco no gira el tono ni un
       grado: solo baja el brillo. */
    --apagar-hacia:#FFFFFF;

    --font-display: 'Fredoka', ui-rounded, "SF Pro Rounded", "Segoe UI Rounded", "Century Gothic", system-ui, sans-serif;
    --font-body: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    --font-mono: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;

    --shadow: 0 3px 0 rgba(22,40,61,.15);
    --radius-lg: 22px; --radius-md: 14px;
  }
  @media (prefers-color-scheme: dark){
    :root{ --bg:#0F1B2C; --surface:#16263D; --surface-2:#1E3350; --ink:#E8EFF7; --ink-soft:#9FB2C7; --line:#33506F; --line-fuerte:#4A5B73; --accent:#36BDF7; --accent-border:#167DA8; --accent-hi:#7FD4FA; --accent-text:#36BDF7; --accent-ink:#0B2C4D; --accent-soft:#123B55; --accent-soft-ink:#BEE7FB; --muted-fill:#1E3350; --muted-border:#7C879C; --danger-fill:#4A2420; --danger-ink:#FFD3C4; --danger-border:#8A4A3A; --shadow: 0 3px 0 rgba(0,0,0,.4); --ok:#5FD08A; --apagar-hacia:#000000; }
  }
  /* --line subió en los DOS temas el 2026-08-23, y por la misma razón:
     contra --surface-2 daba 1.22:1 en claro y 1.23:1 en oscuro, o sea un
     borde que a efectos prácticos no existe. El barrido de las 1062
     pantallas lo encontró 892 veces en oscuro —el tablero por donde camina
     Robi, las ranuras del circuito, el tangram, la vitrina, el mapa del
     cuerpo— y el claro tenía el mismo defecto aunque el barrido, que corre
     en oscuro, no lo estuviera mirando. Se midieron los dos.

       Claro:  #CBD6E3 → #AEBFD3   (1.22:1 → 1.55:1 contra --surface-2)
       Oscuro: #2A4160 → #33506F   (1.23:1 → 1.53:1 contra --surface-2)

     Es el mínimo para que una línea se lea sin convertir la aplicación en
     una cuadrícula. --line-fuerte sigue existiendo para donde hace falta
     separar de verdad: bordes de tablero y subnavegación del panel. */
  :root[data-theme="dark"]{ --apagar-hacia:#000000; --ok:#5FD08A; --bg:#0F1B2C; --surface:#16263D; --surface-2:#1E3350; --ink:#E8EFF7; --ink-soft:#9FB2C7; --line:#33506F; --line-fuerte:#4A5B73; --accent:#36BDF7; --accent-border:#167DA8; --accent-hi:#7FD4FA; --accent-text:#36BDF7; --accent-ink:#0B2C4D; --accent-soft:#123B55; --accent-soft-ink:#BEE7FB; --muted-fill:#1E3350; --muted-border:#7C879C; --danger-fill:#4A2420; --danger-ink:#FFD3C4; --danger-border:#8A4A3A; --shadow: 0 3px 0 rgba(0,0,0,.4); }
  :root[data-theme="light"]{ --apagar-hacia:#FFFFFF; --ok:#2E9E5B; --bg:#EEF2F7; --surface:#FFFFFF; --surface-2:#E4EAF2; --ink:#16283D; --ink-soft:#51637A; --line:#AEBFD3; --line-fuerte:#8FA3BD; --accent:#1C9FD7; --accent-border:#167DA8; --accent-hi:#36BDF7; --accent-text:#125E80; --accent-ink:#0B2C4D; --accent-soft:#D6EEFB; --accent-soft-ink:#0B2C4D; --muted-fill:#E4EAF2; --muted-border:#6B6E85; --danger-fill:#FBDAD3; --danger-ink:#8A2A18; --danger-border:#D98F7B; --shadow: 0 3px 0 rgba(22,40,61,.15); }

  *{box-sizing:border-box;}
  /* Estas reglas solo importan dentro del WebView nativo (Capacitor/iOS);
     en un navegador de escritorio no cambian nada visible. Evitan que la
     app "se sienta como una página web": sin rebote de scroll, sin flash
     azul ni menú contextual al tocar y mantener, sin selección de texto
     accidental, y respetando el notch/home indicator del iPhone. */
  html, body{overscroll-behavior:none;}
  /* touch-action:manipulation quita el doble-tap-para-zoom del WKWebView:
     el meta viewport (user-scalable=no) ya lo bloquea en la mayoría de
     versiones de iOS, pero esto es el respaldo por si acaso — una app que
     se siente nativa no debería hacer zoom al tocar dos veces. */
  html{touch-action:manipulation;}
  *{-webkit-tap-highlight-color:transparent;-webkit-touch-callout:none;}
  body, button, .avatar-card, .choice-chip, .tap-choice, .node-btn, .pill-btn{-webkit-user-select:none;user-select:none;}

  /* El atributo `hidden` de HTML es solo `display:none` de la hoja del
     navegador, así que CUALQUIER regla propia con `display` le gana — sin
     !important y sin que nadie lo note. Se descubrió en el peor sitio
     posible: el aviso de "Cámara encendida" de los retos con cámara está
     en `display:flex`, así que se veía SIEMPRE, también con la cámara
     apagada. Un indicador de cámara que miente es peor que no tenerlo, y
     es la regla 3 de docs/EVALUACION_REALIDAD_AUMENTADA.md.
     Una sola línea lo arregla para todo el proyecto — hay una docena de
     módulos que usan `hidden`. */
  [hidden]{display:none !important;}
  input, textarea{-webkit-user-select:text;user-select:text;}
  #app{padding-top:env(safe-area-inset-top);padding-bottom:env(safe-area-inset-bottom);padding-left:env(safe-area-inset-left);padding-right:env(safe-area-inset-right);}
  body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--font-body);-webkit-font-smoothing:antialiased;}
  h1,h2,h3{font-family:var(--font-display);text-wrap:balance;margin:0;}
  button{font-family:inherit;color:inherit;}
  input,select,textarea{font-family:inherit;}
  .visually-hidden{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);}
  .lucide{width:1.15em;height:1.15em;flex:none;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;vertical-align:-0.2em;}
  .theme-toggle{position:fixed;top:calc(14px + env(safe-area-inset-top));right:calc(14px + env(safe-area-inset-right));z-index:40;width:44px;height:44px;border-radius:50%;border:2.5px solid var(--line);background:var(--surface);color:var(--ink-soft);display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:var(--shadow);font-size:17px;flex:none;}
  .theme-toggle:hover{color:var(--accent-border);border-color:var(--accent-border);}
  /* Con sesión iniciada, topbar.js reubica este botón dentro de #topbar (al
     final, junto al chip de perfil) para que sea parte ordenada de la barra
     en vez de flotar encima de su contenido. Aquí anulamos el posicionamiento
     fijo solo en ese contexto; en las pantallas sin topbar (login, elección
     de perfil) el botón se queda en su posición flotante de siempre. */
  .topbar .theme-toggle{position:static;width:44px;height:44px;}
  /* Dentro de una pantalla (Mi perfil de aprendizaje) el botón fluye con
     el contenido en vez de quedar fijo en la esquina, que es donde vive
     mientras no hay barra. Sin esto se quedaba flotando encima del panel. */
  .theme-toggle.en-el-contenido{position:static;width:44px;height:44px;flex:none;}
  .info-toggle{position:fixed;bottom:calc(12px + env(safe-area-inset-bottom));left:calc(12px + env(safe-area-inset-left));z-index:40;width:26px;height:26px;border-radius:50%;border:1.5px solid var(--line);background:var(--surface);color:var(--ink-soft);display:flex;align-items:center;justify-content:center;cursor:pointer;opacity:.55;font-size:12px;}
  .info-toggle .lucide{width:14px;height:14px;}
  .info-toggle:hover{opacity:1;color:var(--accent-border);border-color:var(--accent-border);}
  /* Cuando la barra inferior está en pantalla (solo estudiantes, solo
     pantallas angostas — ver .has-bottom-nav en core/topbar.js), este
     botón caía justo encima del primer destino, "Mapa", y se lo tapaba.
     Sube por encima de la barra en vez de convivir con ella. Los 64px son
     la altura medida de la barra (62px) más un margen: la barra crece con
     el área segura del teléfono, que ya viene sumada aparte en el
     env() de abajo. */
  body.has-bottom-nav .info-toggle{
    bottom:calc(12px + 64px + env(safe-area-inset-bottom));
  }
  /* Dentro de un reto no se muestra, y esto se decidió midiendo.
     Muchos retos terminan con una fila de acciones pegada al borde
     inferior del panel ("Vaciar", "Ejecutar") que ocupa casi todo el
     ancho; el botón de ayuda, anclado abajo a la izquierda, se le montaba
     encima — medido: 5×20 px sobre "Vaciar", en 8 retos de bloques. Cinco
     píxeles bastan para robarle el toque a un botón del juego, y eso es
     justo lo que un botón decorativo no puede hacer nunca.

     Subirlo no sirve: la altura del panel depende del contenido, así que
     no hay un desplazamiento fijo que funcione en todos los retos.
     Moverlo tampoco: la fila de acciones ocupa el ancho entero.

     Así que dentro del reto se retira. Ahí el niño está jugando y tiene su
     "← Mapa" arriba; "Acerca de Robi Lab" es una pantalla para un adulto,
     y vuelve a estar abajo a la izquierda en todas las demás pantallas —
     que es donde el usuario pidió que estuviera. */
  body:has(#screenNode.screen.active) .info-toggle{ display:none; }
  .lucide.lucide-lg{width:1.6em;height:1.6em;}
  .lucide.lucide-xl{width:2.3em;height:2.3em;}

  #app{min-height:100vh;display:flex;flex-direction:column;}
  /* Con la barra inferior, el cuerpo añade su hueco DEBAJO de #app; si
     #app sigue midiendo 100vh, el documento acaba midiendo 100vh + ese
     hueco y la página se puede arrastrar aunque no haya nada que ver. Se
     le descuenta, y así el total vuelve a ser exactamente una pantalla. */
  body.has-bottom-nav #app{
    min-height:calc(100vh - 70px - env(safe-area-inset-bottom));
  }
  .topbar{display:flex;align-items:center;gap:12px;padding:10px 18px;border-bottom:1px solid var(--line);background:var(--surface);flex-wrap:wrap;}
  /* Cuando a la derecha del .spacer no queda nada visible (un estudiante
     en un teléfono: sus botones se mudaron a la barra de abajo), el
     spacer empujaría contra el vacío y dejaría la racha, el XP y el mundo
     pegados al borde izquierdo. topbar.js lo detecta y pone esta clase.
     Ver centrarSiNoQuedaNadaALaDerecha. */
  /* CENTRAR SIN QUE SE SALGA POR LA IZQUIERDA.
     `justify-content:center` centra, pero cuando el contenido es MÁS ANCHO
     que el hueco reparte el sobrante a los dos lados: el primer chip se
     va a coordenada negativa y queda cortado contra el borde, fuera del
     alcance del dedo. Medido en un iPhone: contenido 342 px, hueco 291 px,
     la racha empezaba en x=-15. El dueño lo vio: "están incluso más a la
     izquierda".
     Con dos separadores elásticos —uno delante y otro detrás, los dos
     `flex:1 1 0`— el centrado sale del reparto: si sobra sitio se lo
     reparten y el contenido queda en medio; si no sobra, los dos se
     encogen a cero y el contenido arranca pegado al borde izquierdo, que
     es lo peor que puede pasar. Nunca en negativo. */
  .topbar.topbar-centrada{justify-content:flex-start;}
  /* El separador de delante es un ELEMENTO de verdad, no un ::before.
     Lo intenté con ::before y no centraba nada: .topbar-sticky-map ya usa
     ese pseudoelemento para pintar la franja del notch/isla, y un elemento
     solo tiene uno — las dos reglas se mezclaban, ganaba el
     position:absolute de la franja y mi separador se quedaba en cero
     (medido: 18 px de aire a la izquierda contra 31 a la derecha). */
  /* Los huecos de los botones de panel están SIEMPRE en la barra y se
     rellenan según el rol. Vacíos miden cero, pero siguen siendo
     elementos del flex: cada uno se lleva su separación de 8 px. Con los
     dos vacíos eso son 16 px fantasma a la derecha del contenido, que es
     exactamente lo que impedía que la barra quedara centrada (medido:
     36 px de aire a la izquierda contra 52 a la derecha). */
  #teacherPanelBtnWrap:empty, #adminPanelBtnWrap:empty{display:none;}
  .topbar .spacer-inicio{display:none;}
  .topbar.topbar-centrada .spacer-inicio{display:block;flex:1 1 0;min-width:0;}
  .topbar.topbar-centrada .spacer{flex:1 1 0;min-width:0;}
  /* Fija (sticky) solo mientras se ve el mapa — topbar.js le agrega/quita
     esta clase en cada showScreen(). Adentro de un reto (screenNode) NO
     se aplica a propósito: ahí ocupar esa franja fija resta espacio de
     juego en pantallas chicas sin aportar nada, ya que el reto tiene su
     propia cabecera con "← Mapa" para volver. z-index por debajo de
     .profile-menu (60) para que su menú desplegable siga tapando todo lo
     demás correctamente. */
  /* top:0 a secas pega el header contra el borde REAL de la pantalla, no
     contra abajo del notch/Dynamic Island — #app no es un contenedor con
     scroll propio (no tiene overflow), así que el scroll ocurre en el
     viewport y "sticky" mide desde ahí, ignorando el padding-top de #app
     (que sí funciona para el layout normal, sin scroll). Reporte real con
     captura: el header quedaba tapado por la barra de estado de iOS. */
  .topbar.topbar-sticky-map{position:sticky;top:env(safe-area-inset-top);z-index:45;}
  /* LA BARRA TIENE QUE LLEGAR HASTA EL BORDE DE ARRIBA.
     #app reserva la franja del notch/isla con padding-top, así que la
     barra se pega DEBAJO de esa franja y arriba quedaba el color del
     fondo (--bg) en vez del de la barra (--surface). En un iPhone eso se
     lee como si la barra flotara en medio y por detrás transparentara
     hasta la isla — lo reportó el dueño mirándolo en su teléfono.
     Se pinta esa franja con el mismo color que la barra, en vez de mover
     el padding de #app: ese padding también protege el contenido de las
     otras pantallas, y quitarlo para arreglar una arreglaría la barra
     metiendo el resto debajo del notch. */
  .topbar.topbar-sticky-map::before{
    content:'';position:absolute;left:0;right:0;bottom:100%;
    height:env(safe-area-inset-top);
    background:var(--surface);
    pointer-events:none;
  }
  /* Barra de navegación inferior: solo aparece en pantallas angostas (ver
     media query más abajo) y solo para estudiantes (topbar.js la
     muestra/oculta igual que el resto de la barra superior). En escritorio
     no se usa — ahí la navegación arriba tiene espacio de sobra. */
  .bottom-nav{display:none;position:fixed;left:0;right:0;bottom:0;z-index:39;justify-content:space-around;align-items:stretch;gap:4px;background:var(--surface);border-top:1px solid var(--line);padding:6px 10px calc(6px + env(safe-area-inset-bottom));box-shadow:0 -3px 10px rgba(0,0,0,.08);}
  .bottom-nav-btn{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;background:none;border:none;color:var(--ink-soft);font-family:inherit;font-size:11px;font-weight:800;padding:6px 2px;cursor:pointer;border-radius:14px;-webkit-user-select:none;user-select:none;}
  .bottom-nav-btn svg{width:22px;height:22px;}
  .bottom-nav-btn.active{color:var(--accent-text);background:var(--accent-soft);}

  /* Barra inferior del panel de administración — mismo lenguaje visual
     que .bottom-nav de arriba (posición fija, íconos+etiqueta, estado
     .active con --accent-soft), pero un componente propio, NO el mismo:
     .bottom-nav está atado a refreshTopbar()/showScreen() en topbar.js y
     gateado a state.profile.role==='student', con su propio mapa de
     PANTALLAS completas (Inicio/Progreso/Insignias). Acoplar el cambio de
     SECCIÓN dentro del panel de admin a ese sistema de navegación de
     nivel superior mezclaría dos cosas separadas. Sin riesgo de que
     convivan las dos barras a la vez: un staff nunca tiene
     state.profile.role==='student' al mismo tiempo que una sesión de
     personal activa. Vive DENTRO de #screenAdmin (no suelto en <body>)
     para heredar gratis el mismo show/hide que ya usa showScreen() en
     cualquier pantalla — position:fixed no la saca de esa cascada de
     display, solo de la de posición. */
  .admin-bottom-nav{display:none;position:fixed;left:0;right:0;bottom:0;z-index:39;justify-content:space-around;align-items:stretch;gap:4px;background:var(--surface);border-top:1px solid var(--line);padding:6px 10px calc(6px + env(safe-area-inset-bottom));box-shadow:0 -3px 10px rgba(0,0,0,.08);}
  .admin-bottom-nav-btn{flex:1;position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;background:none;border:none;color:var(--ink-soft);font-family:inherit;font-size:11px;font-weight:800;padding:6px 2px;cursor:pointer;border-radius:14px;-webkit-user-select:none;user-select:none;}
  .admin-bottom-nav-btn svg{width:22px;height:22px;}
  .admin-bottom-nav-btn.active{color:var(--accent-text);background:var(--accent-soft);}
  .admin-tab-badge{position:absolute;top:2px;right:8px;min-width:16px;height:16px;padding:0 4px;border-radius:99px;background:var(--cat-move);color:#fff;font-size:10px;font-weight:800;display:flex;align-items:center;justify-content:center;}
  .nav-pills .admin-tab-badge{position:static;margin-left:4px;}
  /* Subnavegación dentro de una sección. Se ve igual en celular y en
     escritorio a propósito: es el mismo nivel de decisión en los dos, y
     dos aspectos distintos para lo mismo obligan a aprenderlo dos veces.
     Se desliza en horizontal cuando no cabe, en vez de partirse en dos
     filas y empujar el contenido hacia abajo. */
  .admin-subnav{display:flex;gap:6px;margin:0 0 14px;overflow-x:auto;-webkit-overflow-scrolling:touch;padding-bottom:2px;}
  .admin-subnav::-webkit-scrollbar{height:0;}
  .admin-subnav-btn{position:relative;flex:none;background:var(--surface-2);border:1px solid var(--line);
    color:var(--ink-soft);font-family:inherit;font-size:13px;font-weight:700;
    padding:8px 14px;border-radius:99px;cursor:pointer;white-space:nowrap;min-height:44px;}
  .admin-subnav-btn.active{background:var(--accent-soft);color:var(--accent-text);border-color:var(--accent-border);}
  .admin-subnav-btn .admin-tab-badge{position:static;margin-left:6px;}
  /* Estado y rol en la lista de personal — se leen de un vistazo, sin
     tener que interpretar un color solo. */
  .staff-estado{font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;padding:2px 7px;border-radius:99px;vertical-align:middle;margin-left:4px;}
  .staff-estado.activo{background:var(--ok-soft,rgba(45,180,120,.16));color:var(--ok-text,#2db478);}
  .staff-estado.espera{background:var(--surface-2);color:var(--ink-soft);border:1px solid var(--line);}
  .staff-rol{font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;padding:2px 7px;border-radius:99px;background:var(--accent-soft);color:var(--accent-text);vertical-align:middle;margin-left:4px;}
  @media (max-width:820px){
    .admin-bottom-nav{display:flex;}
    /* .admin-tab-nav-desktop NO se oculta aquí — .nav/.nav-pills de Tabler
       viven dentro de un @scope(.tabler-panel) que le gana a cualquier
       regla de acá por proximidad de scope, sin importar el orden ni
       !important (mismo problema que documenta applyTeacherSearch en
       admin.js). Se oculta a mano con estilo en línea desde JS —
       ver syncAdminTabNavForWidth en admin.js. */
    #screenAdmin.screen.active{padding-bottom:calc(70px + env(safe-area-inset-bottom));}
    /* El botón de ayuda vive fijo abajo a la izquierda y caía justo
       encima del primer destino de esta barra, tapándolo — el mismo
       choque que ya se había resuelto para la barra del estudiante
       (body.has-bottom-nav más arriba), pero esta barra es un componente
       aparte y no pone esa clase. Se sube por encima igual. Los 64 px son
       la altura de la barra (62) más un margen; el área segura del
       teléfono ya viene sumada en el env() de la propia regla. */
    body:has(#screenAdmin.screen.active) .info-toggle{
      bottom:calc(12px + 64px + env(safe-area-inset-bottom));
    }
  }

  @media (max-width:820px){
    /* Estos tres ya viven en la barra inferior en móvil — ocultarlos arriba
       (con !important porque topbar.js les pone display inline según el
       rol) es lo que evita el amontonamiento original. */
    #btnHome, #btnMastery, #btnBadges{display:none !important;}
    .bottom-nav{display:flex;}
    /* Espacio para que el contenido no quede tapado detrás de la barra fija.
       Va en body (no en cada .screen) para no pelear con el padding-bottom
       propio que ya tiene cada pantalla. Solo con .has-bottom-nav (la barra
       realmente visible, ver core/topbar.js) — sin esta condición, pantallas
       sin barra (login, verificación, paneles de profesor/admin...)
       quedaban con 70px de aire reservado de más, obligando a scrollear
       sin necesidad. */
    body.has-bottom-nav{padding-bottom:calc(70px + env(safe-area-inset-bottom));}

    /* El botón de tema NUNCA debe poder empujar la barra a una segunda línea
       (se veía feo y en algunos WebViews el margen para que todo cupiera en
       una sola fila era de apenas unos px, así que cualquier nombre un poco
       más largo o una métrica de fuente distinta lo rompía). En vez de
       competir por espacio dentro del flex, en móvil sale del flujo con
       position:absolute anclado a la propia barra (relative) — así el resto
       de chips solo compite por el ancho que le sobra, y el botón de tema
       siempre queda en su esquina sin poder causar un salto de línea. */
    /* El hueco que hay que reservar a la derecha se CALCULA de las medidas
       reales del botón, no se estima. Estaba en 54px fijos y hacían falta
       58 (44 de ancho + 14 de separación al borde): el chip del perfil
       terminaba 4px DENTRO del botón de tema y se veían encimados —
       reportado por el usuario, "el icono del perfil está muy pegado al
       icono para cambiar de tema", y medido después: chip 236→321, botón
       317→361.

       Cuatro píxeles es exactamente el tipo de error que produce escribir
       a mano la suma de dos medidas que ya están escritas en otra regla.
       Ahora las tres viven en variables y la suma la hace calc(): si
       mañana el botón crece a 48px, el hueco crece solo. Los 8px del final
       son el mismo gap del flex, para que el chip no llegue a rozarlo. */
    .topbar{
      --tt-size:44px; --tt-right:14px; --tt-aire:8px;
      flex-wrap:nowrap;gap:8px;position:relative;
    }
    /* El hueco de la derecha SOLO si el botón de tema está de verdad en la
       barra. Con un niño dentro el botón vive en "Mi perfil de
       aprendizaje" (ver colocarBotonDeTema en core/topbar.js), y la barra
       seguía reservándole 66 px: una cuarta parte del ancho del teléfono
       apartada para nada, empujando la racha, el XP y el mundo a la
       izquierda. La clase la pone la misma función que coloca el botón,
       para que no puedan desincronizarse. */
    .topbar.topbar-con-boton-de-tema{
      padding-right:calc(var(--tt-size) + var(--tt-right) + var(--tt-aire) + env(safe-area-inset-right));
    }
    .topbar .theme-toggle{
      position:absolute;top:50%;transform:translateY(-50%);
      right:calc(var(--tt-right) + env(safe-area-inset-right));
      width:var(--tt-size);height:var(--tt-size);
    }
    /* Chips más compactos para dejarle aire al nombre del perfil. */
    .stat-chip{padding:5px 9px;font-size:12.5px;gap:3px;}
    /* El relleno se recorta en pantallas angostas para dejarle sitio al
       nombre, pero el alto tocable no: 44px es el mínimo de Apple y de
       Material, y este chip es la única forma de cambiar de perfil. */
    .profile-chip{padding:4px 10px 4px 4px;gap:6px;flex:0 1 auto;min-width:0;min-height:44px;}
    .profile-chip .av{width:24px;height:24px;flex:none;}
    /* Un nombre largo trunca con "…" en vez de ensanchar el chip — el resto
       de la barra nunca debería depender de cuántas letras tenga el nombre. */
    #profileChipName{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:92px;display:inline-block;vertical-align:middle;}
  }
  .brand{display:flex;align-items:center;gap:9px;font-family:var(--font-display);font-weight:700;font-size:18px;}
  .brand svg{width:28px;height:28px;flex:none;}
  .spacer{flex:1;}
  /* min-height de 44: aquí vive "← Mapa", el botón con el que un niño
     sale de CADA reto, y medía 32 px de alto. Lo encontró la auditoría en
     vivo y no verificar-zonas-tactiles.js, porque este alto no está
     escrito en ninguna parte: sale del relleno más la línea de texto. */
  .pill-btn{border:none;background:var(--surface);color:var(--ink);border-radius:999px;padding:8px 15px;min-height:44px;font-size:13.5px;font-weight:600;cursor:pointer;display:inline-flex;align-items:center;gap:6px;box-shadow:0 0 0 1.5px var(--line), 0 2.5px 0 var(--line);transition:transform .12s ease, background .15s ease;}
  .pill-btn:hover{background:var(--surface-2);}
  .pill-btn:active{transform:translateY(2.5px);box-shadow:0 0 0 1.5px var(--line);}
  .pill-btn:focus-visible, button:focus-visible, .node-btn:focus-visible, .avatar-card:focus-visible, .tap-choice:focus-visible, input:focus-visible{outline:3px solid var(--accent);outline-offset:2px;}
  /* white-space:nowrap + flex:none: sin esto, un número de varias cifras
     (XP de miles, o incluso una racha de 2 dígitos en una barra angosta)
     podía quedarse sin espacio horizontal dentro del chip y partir el
     texto en dos líneas ("1536" arriba, "XP" abajo) — el chip se ve mal y
     encima crece de alto sin necesidad. Con nowrap, si no entra, el chip
     empuja al resto de la barra en vez de partirse; flex:none evita que
     el propio flujo flex lo comprima para "ayudar" a que quepa. */
  .stat-chip{display:inline-flex;align-items:center;gap:5px;background:var(--surface-2);border-radius:999px;padding:5px 12px;font-weight:800;font-size:13.5px;font-variant-numeric:tabular-nums;border:2px solid var(--line);white-space:nowrap;flex:none;}
  .stat-chip.xp{color:#8A6D00;background:#FFF3D0;border-color:#E8C666;}
  .stat-chip.streak{color:#B23B1E;background:#FFE7DE;border-color:#F0A183;}
  .profile-chip{display:inline-flex;align-items:center;gap:8px;background:var(--surface-2);border:none;border-radius:999px;padding:5px 13px 5px 5px;cursor:pointer;font-weight:700;font-size:13.5px;color:var(--ink);box-shadow:0 0 0 1.5px var(--line), 0 2.5px 0 var(--line);min-height:44px;}
  .profile-chip:active{transform:translateY(2.5px);box-shadow:0 0 0 1.5px var(--line);}
  .profile-chip .av{width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;}
  .profile-chip .av svg, .profile-chip .av img{width:18px;height:18px;object-fit:contain;}
  .role-badge{font-size:10.5px;font-weight:800;text-transform:uppercase;letter-spacing:.03em;background:var(--cat-ai);color:#fff;padding:2px 7px;border-radius:999px;}
  .role-badge.admin{background:#16283D;}

  /* Menú del chip de perfil (core/topbar.js) — solo para personal
     (profesor/a, admin). position:fixed porque se posiciona a mano con
     getBoundingClientRect() del chip, sin depender de que algún
     ancestro tenga position:relative (el topbar solo lo tiene en
     pantallas angostas). */
  .profile-menu{position:fixed;background:var(--surface);border:1.5px solid var(--line);border-radius:14px;box-shadow:var(--shadow);padding:6px;min-width:190px;z-index:60;}
  .profile-menu button{display:flex;align-items:center;gap:9px;width:100%;text-align:left;padding:10px 12px;border-radius:10px;border:none;background:none;color:var(--ink);font-weight:700;font-size:13.5px;cursor:pointer;}
  .profile-menu button:hover{background:var(--surface-2);}
  .profile-menu button.danger{color:var(--cat-move);}

  .screen{display:none;flex:1;flex-direction:column;}
  .screen.active{display:flex;}

  /* ---------- Generic centered screen ---------- */
  /* padding/gap más ajustados que antes (eran 40px/26px) — con formularios
     de varios campos (cuenta de padre/madre, staff) el total se pasaba de
     una pantalla de celular y obligaba a bajar y subir para ver el botón
     de enviar. Menos aire entre bloques, no menos aire alrededor del
     contenido en sí (los .text-input, botones, etc. no cambian). */
  .center-screen{align-items:center;justify-content:center;padding:22px 18px;gap:16px;text-align:center;}
  .kicker{color:var(--accent-soft-ink);background:var(--accent-soft);display:inline-block;padding:5px 14px;border-radius:999px;font-size:12.5px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;}
  .center-screen h1{font-size:clamp(24px,4vw,36px);max-width:20ch;}
  .center-screen p.lead{color:var(--ink-soft);max-width:50ch;font-size:16px;line-height:1.5;margin:0;}

  /* ---------- Auth / profile screens ---------- */
  .profile-row{display:flex;flex-wrap:wrap;gap:16px;justify-content:center;max-width:760px;}
  .avatar-card{width:120px;background:var(--surface);border:3px solid var(--line);border-radius:var(--radius-lg);padding:16px 10px;cursor:pointer;text-align:center;box-shadow:var(--shadow);transition:transform .15s ease, border-color .15s ease;}
  .avatar-card:hover{transform:translateY(-3px);border-color:var(--accent-border);}
  .avatar-card .av{width:56px;height:56px;border-radius:16px;display:flex;align-items:center;justify-content:center;margin:0 auto 10px;}
  .avatar-card .av svg, .avatar-card .av img{width:34px;height:34px;object-fit:contain;}
  .avatar-card .nm{font-weight:800;font-size:14px;}
  .avatar-card .ag{font-size:12px;color:var(--ink-soft);}
  .avatar-card.new{border-style:dashed;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;color:var(--ink-soft);font-weight:700;font-size:13px;}
  .avatar-card.new .plus{font-size:26px;color:var(--accent-text);}
  /* .avatar-card-wrap: envuelve la tarjeta de perfil + su botón de
     eliminar aparte (no puede ir DENTRO del <button> de la tarjeta, un
     botón dentro de otro botón es HTML inválido) — position:relative aquí
     es lo que deja anclar .avatar-del a la esquina de la tarjeta. */
  .avatar-card-wrap{position:relative;}
  .avatar-del, .avatar-dl, .avatar-edit{position:absolute;top:-6px;width:24px;height:24px;border-radius:50%;border:2px solid var(--line);background:var(--surface);color:var(--ink-soft);display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:var(--shadow);opacity:.7;transition:opacity .15s ease, color .15s ease, border-color .15s ease;}
  .avatar-del{right:-6px;}
  .avatar-del:hover{opacity:1;color:var(--cat-move);border-color:var(--cat-move);}
  .avatar-dl{right:22px;}
  .avatar-dl:hover{opacity:1;color:var(--accent-border);border-color:var(--accent-border);}
  /* .avatar-edit: "Editar nombre" — pedido real de un usuario que no
     encontraba dónde renombrar un perfil ya creado. Mismo patrón que
     .avatar-del/.avatar-dl (icono chico anclado a la esquina de la
     tarjeta), un slot más a la izquierda. */
  .avatar-edit{right:50px;}
  .avatar-edit:hover{opacity:1;color:var(--cat-sound);border-color:var(--cat-sound);}
  .avatar-del .lucide, .avatar-dl .lucide, .avatar-edit .lucide{width:12px;height:12px;}
  /* min-height, no más relleno: el subrayado tiene que seguir pegado al
     texto (si crece el relleno, el enlace se ve flotando en un hueco),
     pero el área que responde al dedo no puede bajar de 44. Con
     inline-flex el texto queda centrado dentro de esos 44 sin moverse. */
  .text-link{background:none;border:none;color:var(--accent-text);font-weight:700;font-size:13.5px;cursor:pointer;text-decoration:underline;padding:6px;min-height:44px;display:inline-flex;align-items:center;justify-content:center;}

  .creator-card{background:var(--surface);border:3px solid var(--line);border-radius:var(--radius-lg);padding:20px;max-width:520px;width:100%;text-align:left;box-shadow:var(--shadow);}
  .creator-card h2{font-size:19px;margin-bottom:14px;}
  .field-label{font-size:12.5px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--ink-soft);margin:10px 0 6px;}
  .text-input{width:100%;padding:12px 14px;border-radius:var(--radius-md);border:2.5px solid var(--line);font-size:16px;background:var(--bg);color:var(--ink);}
  /* Campo de varios correos (core/entradaDeCorreos.js): la caja es la de
     .text-input y adentro van pastillas y el cursor, como el «Para:» de
     un correo. Ancho completo y crece con las pastillas. */
  .correos-campo{display:flex;flex-wrap:wrap;align-items:center;gap:6px;width:100%;min-height:52px;padding:8px 10px;border-radius:var(--radius-md);border:2.5px solid var(--line);background:var(--bg);cursor:text;box-sizing:border-box;}
  .correos-campo:focus-within{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft);}
  .correos-pastillas{display:contents;}
  .correos-pastilla{display:inline-flex;align-items:center;gap:4px;max-width:100%;padding:4px 6px 4px 12px;border-radius:999px;background:var(--accent-soft);color:var(--accent-soft-ink);font-size:14px;font-weight:600;line-height:1.3;}
  .correos-pastilla .correos-texto{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
  .correos-pastilla.invalida{background:#FDE3DF;color:var(--cat-move-ink);text-decoration:underline wavy var(--cat-move);}
  .correos-quitar{position:relative;border:none;background:transparent;color:inherit;font-size:18px;line-height:1;width:24px;height:24px;border-radius:50%;cursor:pointer;padding:0;display:inline-flex;align-items:center;justify-content:center;}
  /* La equis se queda chica —dentro de una pastilla no cabe más— pero el
     toque es de 44: el ::after es un rectángulo invisible centrado en
     ella, el mismo recurso que ya usan .loop-chip-del y .modal-close-x.
     El halo sobresale 10 px por cada lado: a la izquierda cae sobre el
     final del correo y a la derecha sobre el borde del campo de escribir,
     que no son tocables (el texto no hace nada y el campo tiene todo el
     ancho restante). De otra equis no roba nada: entre dos centros hay
     más de 44 px aunque el correo sea cortísimo. */
  .correos-quitar::after{content:'';position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:44px;height:44px;border-radius:50%;}
  .correos-quitar:hover{background:rgba(0,0,0,.12);}
  .correos-entrada{flex:1 1 160px;min-width:120px;border:none;outline:none;background:transparent;color:var(--ink);font-size:16px;padding:6px 4px;}
  .correos-entrada::placeholder{color:var(--ink-soft);}
  /* UN <select> CON UNA OPCIÓN LARGA ENSANCHA LA PÁGINA ENTERA EN iOS.
     WebKit cuenta el texto de la opción elegida como desbordamiento del
     select aunque lo pinte recortado, y ese desbordamiento sube por cada
     contenedor con overflow visible hasta el documento: en el panel de
     administración «Libre — el estudiante elige el mundo (…)» dejaba el
     documento 30 px más ancho que la pantalla, la página se deslizaba de
     lado y la cabecera se separaba del borde. Chrome no lo hace, así que
     en el navegador de escritorio no se veía nunca; se midió dentro del
     WebKit del simulador (2026-09-13). Recortar el select cierra la fuga
     para todos, incluidos los que vengan. */
  select{overflow:hidden;text-overflow:ellipsis;max-width:100%;}
  .text-input:focus{outline:3px solid var(--accent);outline-offset:1px;border-color:var(--accent);}
  .choice-grid{display:flex;flex-wrap:wrap;gap:8px;}
  .choice-chip{border:none;background:var(--surface);border-radius:14px;padding:11px 15px;cursor:pointer;font-weight:700;font-size:13.5px;color:var(--ink);display:flex;flex-direction:column;align-items:center;gap:4px;min-width:78px;box-shadow:0 0 0 1.5px var(--line), 0 3px 0 var(--line);transition:transform .1s ease;}
  .choice-chip:active{transform:translateY(3px);box-shadow:0 0 0 1.5px var(--line);}
  .choice-chip .ic{font-size:22px;}
  .choice-chip.sel{background:var(--accent-soft);color:var(--accent-soft-ink);box-shadow:0 0 0 1.5px var(--accent-border), 0 3px 0 var(--accent-border);}
  /* ---------- Selector de banda de edad (auth.js, showCreator) ----------
     Antes eran chips en .choice-grid (flex-wrap) — con 3 textos de largo
     bien distinto, a veces entraban 2 por fila y la tercera quedaba sola,
     descentrada. Una fila por opción, ancho completo, no tiene ese
     problema a ningún ancho de pantalla. */
  .band-list{display:flex;flex-direction:column;gap:8px;}
  .band-row{display:flex;align-items:center;gap:12px;width:100%;border:none;background:var(--surface);border-radius:14px;padding:12px 15px;cursor:pointer;text-align:left;box-shadow:0 0 0 1.5px var(--line), 0 3px 0 var(--line);transition:transform .1s ease;}
  .band-row:active{transform:translateY(3px);box-shadow:0 0 0 1.5px var(--line);}
  .band-row.sel{background:var(--accent-soft);box-shadow:0 0 0 1.5px var(--accent-border), 0 3px 0 var(--accent-border);}
  .band-ic{font-size:26px;flex:none;}
  .band-text{display:flex;flex-direction:column;gap:1px;}
  .band-label{font-weight:800;font-size:14px;color:var(--ink);}
  .band-sub{font-weight:600;font-size:12px;color:var(--ink-soft);}
  .band-row.sel .band-label{color:var(--accent-soft-ink);}
  .band-row.sel .band-sub{color:var(--accent-soft-ink);opacity:.8;}
  .avatar-swatch{width:44px;height:44px;border-radius:50%;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 0 0 1.5px rgba(0,0,0,.15), 0 2px 0 rgba(0,0,0,.15);}
  .avatar-swatch svg, .avatar-swatch img{width:26px;height:26px;object-fit:contain;}
  .avatar-swatch.sel{border-color:var(--ink);box-shadow:0 0 0 3px var(--surface), 0 0 0 6px var(--ink);}
  .creator-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:14px;flex-wrap:wrap;}
  /* Antes: border de 3px + box-shadow de 4px del mismo color, apilados uno
     tras otro (7px en total abajo) — con una declaración de box-shadow
     duplicada de más (la primera nunca llegaba a pintarse). Se veía como un
     borde grueso "doblado" en vez de un único relieve limpio. Ahora: sin
     border; un contorno fino (mismo color, en los 4 lados) + el brillo
     superior + la sombra inferior viven juntos en un solo box-shadow, así
     que en la base se funden en una sola banda en vez de sumarse dos. */
  .btn-primary{background:var(--accent);color:var(--accent-ink);border:none;border-radius:var(--radius-md);padding:12px 21px;font-size:15.5px;font-weight:800;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:8px;box-shadow:0 0 0 1.5px var(--accent-border), inset 0 2px 0 var(--accent-hi), 0 4px 0 var(--accent-border);transition:transform .1s ease;}
  .btn-primary:active{transform:translateY(3px);box-shadow:0 0 0 1.5px var(--accent-border), inset 0 2px 0 var(--accent-hi);}
  .btn-primary:disabled{opacity:.45;cursor:not-allowed;}
  .btn-secondary{background:var(--muted-fill);color:var(--ink);border:none;border-radius:var(--radius-md);padding:12px 19px;font-weight:700;cursor:pointer;box-shadow:0 0 0 1.5px var(--muted-border), 0 4px 0 var(--muted-border);transition:transform .1s ease;}
  .btn-secondary:active{transform:translateY(3px);box-shadow:0 0 0 1.5px var(--muted-border);}
  .btn-danger{background:#FFE3DD;color:#7A1F0D;border:1px solid #F3B7A4;border-radius:var(--radius-md);padding:10px 16px;font-weight:700;cursor:pointer;}
  .privacy-note{font-size:12px;color:var(--ink-soft);max-width:440px;line-height:1.5;}

  /* ---------- Home / path map ---------- */
  #screenHome{padding:20px 16px 60px;align-items:center;position:relative;}
  .home-inner{max-width:640px;width:100%;}
  .hero-greet{background:var(--surface);border:3px solid var(--line);border-radius:var(--radius-lg);padding:18px 20px;display:flex;align-items:center;gap:14px;margin-bottom:22px;box-shadow:var(--shadow);}
  .hero-greet .av{width:52px;height:52px;border-radius:16px;display:flex;align-items:center;justify-content:center;flex:none;}
  .hero-greet .av svg, .hero-greet .av img{width:32px;height:32px;object-fit:contain;}
  .hero-greet .txt{flex:1;}
  .hero-greet h2{font-size:19px;}
  .xp-track{background:var(--muted-fill);border:2px solid var(--muted-border);border-radius:999px;height:12px;overflow:hidden;margin-top:6px;width:160px;}
  .xp-track > div{height:100%;background:var(--accent);border-right:2px solid var(--accent-border);box-shadow:inset 0 1.5px 0 var(--accent-hi);}

  /* Aviso de "nuevo mundo activo" (ver showHome, screens/home.js) — un
     botón entero (no solo texto) porque hace algo al tocarlo: desplaza
     el mapa hasta el mundo recién desbloqueado. Vive en su propio
     wrap (#worldUnlockBannerWrap) por encima de #pathContainer, así
     siempre es lo primero que se ve al entrar al mapa. */
  /* ---------- Aviso flotante de mundo desbloqueado ----------
     Reemplaza a .new-world-banner, que era una caja dentro del mapa: se
     comía sitio arriba del todo y dejaba de verse en cuanto el estudiante
     bajaba a su reto. Este va anclado a la esquina inferior derecha, por
     encima de la barra inferior, y se queda ahí mientras se navega el
     mapa. Es Robi quien habla —de ahí su cara a la izquierda— en vez de
     un cartel del sistema. */
  #worldUnlockToastWrap{position:fixed;z-index:41;right:0;bottom:0;pointer-events:none;}
  #worldUnlockToastWrap > *{pointer-events:auto;}
  .unlock-toast{
    position:fixed;z-index:41;
    right:calc(14px + env(safe-area-inset-right));
    bottom:calc(14px + env(safe-area-inset-bottom));
    display:flex;align-items:stretch;
    max-width:min(320px, calc(100vw - 28px));
    background:var(--surface);border:2px solid var(--world-color, var(--accent));
    border-radius:18px;box-shadow:0 8px 24px rgba(0,0,0,.28);
    animation:unlockToastIn .45s cubic-bezier(.34,1.56,.64,1);
    overflow:hidden;
  }
  body.has-bottom-nav .unlock-toast{bottom:calc(14px + 64px + env(safe-area-inset-bottom));}
  .unlock-toast-go{
    display:flex;align-items:center;gap:11px;flex:1;min-width:0;
    background:none;border:none;cursor:pointer;text-align:left;
    padding:12px 6px 12px 12px;font-family:inherit;
  }
  .unlock-toast-robi{width:42px;height:42px;flex:none;object-fit:contain;}
  .unlock-toast-txt{display:flex;flex-direction:column;gap:1px;min-width:0;}
  .unlock-toast-title{font-size:12.5px;font-weight:700;color:var(--ink-soft);}
  .unlock-toast-world{
    display:flex;align-items:center;gap:6px;
    font-family:var(--font-display);font-size:16px;color:var(--ink);
    white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
  }
  .unlock-toast-world .lucide{width:16px;height:16px;flex:none;color:var(--world-color, var(--accent));}
  .unlock-toast-cta{
    display:flex;align-items:center;gap:4px;
    font-size:12px;font-weight:700;color:var(--accent-text);
  }
  .unlock-toast-cta .lucide{width:13px;height:13px;}
  .unlock-toast-close{
    flex:none;background:none;border:none;cursor:pointer;
    padding:0 10px;color:var(--ink-soft);
    display:grid;place-items:center;align-self:stretch;
  }
  .unlock-toast-close:hover{color:var(--ink);}
  .unlock-toast-close .lucide{width:16px;height:16px;}
  @keyframes unlockToastIn{
    from{opacity:0;transform:translateY(14px) scale(.94);}
    to{opacity:1;transform:none;}
  }
  @media (prefers-reduced-motion: reduce){
    .unlock-toast{animation:none !important;}
  }

  /* Accesos directos a los mundos (home.js): fila horizontal deslizable
     arriba del mapa — reemplaza el scroll largo entre mundos. */
  /* Panel de métricas del admin (admin.js) */
  .metric-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:10px;margin-bottom:18px;}
  .metric-card{background:var(--surface-2);border-radius:12px;padding:12px 14px;box-shadow:inset 0 0 0 1.5px var(--line);}
  .metric-value{font-size:26px;font-weight:800;color:var(--accent-text);font-variant-numeric:tabular-nums;line-height:1.1;}
  .metric-label{font-size:12px;color:var(--ink-soft);margin-top:2px;}
  .metric-h{font-size:14px;font-weight:800;margin:18px 0 8px;color:var(--ink);}
  /* Caja de "campo opcional" (backendAuth.js, código de institución).
     Se ve deliberadamente distinta de los campos normales y de la casilla
     de consentimiento: borde punteado, fondo tenue y etiqueta "Opcional",
     para que nadie la marque creyendo que es un requisito. */
  .optional-box{
    margin-top:14px;padding:12px 14px;border-radius:12px;
    border:1.5px dashed var(--line);background:var(--surface-2);
  }
  .optional-head{display:flex;align-items:center;gap:8px;margin-bottom:8px;}
  .optional-tag{
    font-size:10.5px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;
    color:var(--ink-soft);background:var(--surface);
    padding:2px 8px;border-radius:999px;box-shadow:inset 0 0 0 1px var(--line);
  }
  .optional-hint{font-size:11.5px;color:var(--ink-soft);}
  .optional-check{display:flex;gap:9px;align-items:flex-start;font-size:12.5px;color:var(--ink-soft);cursor:pointer;line-height:1.5;}
  .optional-check input{margin-top:3px;flex:none;}
  .optional-check strong{color:var(--ink);font-weight:700;}
  /* ---------- Fila de mundos: fija y con flechas ----------
     Queda pegada bajo la barra superior mientras se recorre el mapa, para
     poder cambiar de mundo sin volver arriba del todo. El top se calcula
     con --topbar-h, que mide showHome() en vez de estar escrito a mano:
     la barra superior cambia de alto según qué botones muestre.

  /* Aquí estaban los estilos de la tira de mundos y de sus flechas.
     Se fueron con la tira: el mundo se elige ahora desde el botón de
     la barra de arriba (.world-pick) y su hoja (.mundo-fila). */

  .world-banner, .node-wrap{
    scroll-margin-top:calc(env(safe-area-inset-top) + var(--topbar-h, 0px) + var(--chips-h, 0px) + 14px);
  }

  .world-banner{display:flex;align-items:center;gap:10px;padding:13px 16px;border-radius:16px;margin:22px 0 16px;color:#fff;font-weight:800;font-size:15px;border:3px solid rgba(0,0,0,.18);box-shadow:0 3px 0 rgba(0,0,0,.22), inset 0 0 0 999px rgba(0,0,0,.4);}
  .world-banner .lock{margin-left:auto;font-size:13px;font-weight:700;opacity:.9;display:flex;align-items:center;gap:4px;}
  .world-banner .custom-tag{margin-left:auto;font-size:11px;font-weight:700;background:rgba(255,255,255,.25);padding:3px 9px;border-radius:999px;}
  .world-banner .recommended-tag{margin-left:auto;display:inline-flex;align-items:center;gap:4px;font-size:11px;font-weight:700;background:rgba(255,255,255,.25);padding:3px 9px;border-radius:999px;}
  .world-banner .recommended-tag .lucide{width:12px;height:12px;}
  /* Mundo todavía bloqueado: el banner es un <button> real (ver home.js)
     que despliega/contrae su recorrido — reset de los estilos por
     defecto del navegador para <button> (fuente, alineado, cursor) para
     que siga viéndose igual que la versión <div> de un mundo desbloqueado. */
  .world-banner-collapsed{width:100%;text-align:left;font:inherit;cursor:pointer;-webkit-appearance:none;appearance:none;}
  .world-banner-caret{width:14px;height:14px;transition:transform .15s ease;}
  .world-banner-open .world-banner-caret{transform:rotate(180deg);}
  .path-track{display:flex;flex-direction:column;align-items:center;gap:2px;position:relative;padding:4px 0 8px;}
  .path-track-collapsed{display:none;}
  /* Separador de nivel (Básico I / Básico II / ...) dentro del recorrido de
     un mundo — puramente informativo, ver home.js. */
  .tier-divider{display:flex;align-items:center;gap:6px;font-size:11.5px;font-weight:800;text-transform:uppercase;letter-spacing:.05em;color:var(--ink-soft);background:var(--surface-2);border-radius:999px;padding:5px 14px;margin:14px 0 8px;}
  .tier-divider.done{color:var(--cat-sound-ink,var(--ink));background:color-mix(in srgb, var(--cat-sound) 20%, var(--surface-2));}
  .tier-divider .lucide{width:13px;height:13px;}
  /* position:relative es el ANCLA de las insignias del nodo ("¡Nuevo!", la
     chispa del reto propio). Antes solo lo tenían los nodos con
     .has-extra-flag, y los desplazados funcionaban de casualidad: su
     transform crea contenedor de posición. Al PRIMER nodo de un mundo —sin
     desplazamiento y sin transform— la insignia se le iba a un ancestro
     lejano. Aquí se hace explícito para todos. */
  .node-wrap{display:flex;flex-direction:column;align-items:center;gap:4px;margin:6px 0;position:relative;}
  .node-wrap.off-l{transform:translateX(-54px);}
  .node-wrap.off-r{transform:translateX(54px);}
  .node-btn{width:72px;height:72px;border-radius:50%;border:none;display:flex;align-items:center;justify-content:center;font-size:28px;cursor:pointer;box-shadow:0 0 0 1.5px rgba(0,0,0,.2), 0 4px 0 rgba(0,0,0,.28);position:relative;background:var(--surface-2);color:var(--ink-soft);}
  .node-btn[data-state="available"]{animation:nodepulse 1.8s ease-in-out infinite;}
  @keyframes nodepulse{0%,100%{box-shadow:0 0 0 1.5px rgba(0,0,0,.2), 0 4px 0 rgba(0,0,0,.28);}50%{box-shadow:0 0 0 1.5px rgba(0,0,0,.2), 0 4px 0 rgba(0,0,0,.28), 0 0 0 8px rgba(28,159,215,.25);}}
  .node-btn[data-state="locked"]{cursor:not-allowed;filter:grayscale(.4);opacity:.6;}
  .node-btn[data-state="done"]{color:#fff;}
  .node-stars{display:flex;gap:2px;justify-content:center;}
  .node-stars img{width:14px;height:14px;}
  .node-label{font-size:12.5px;font-weight:700;text-align:center;max-width:120px;color:var(--ink-soft);}
  .node-btn.extra{width:56px;height:56px;font-size:20px;border:2.5px dashed var(--line);background:var(--surface);box-shadow:0 3px 0 var(--line);}
  /* Todo reto extra que llega a pintarse en el mapa está, por definición,
     pendiente (home.js ya filtra los que están done) — el pulso llama la
     atención sin depender de que el niño haya visto el aviso en el modal de
     "¡Reto completado!" (que ahora además lleva directo hasta aquí, ver
     router.js). */
  .node-btn.extra.reinforce{border-color:#E0824A;color:#E0824A;animation:nodepulseReinforce 1.8s ease-in-out infinite;}
  .node-btn.extra.bonus{border-color:#D4A72C;color:#D4A72C;animation:nodepulseBonus 1.8s ease-in-out infinite;}
  /* 'spaced' = repaso de repetición espaciada (ver core/state.js,
     surfaceDueSpacedReviews) — violeta, un tercer color propio para no
     confundirse con el naranja de "necesitas refuerzo" (reinforce, tono
     de alerta) ni el dorado de "ya lo dominaste" (bonus, tono de logro):
     este no es ninguno de los dos, es "repasemos algo que ya sabías
     bien, antes de que se te olvide". */
  .node-btn.extra.spaced{border-color:#8B6FD9;color:#8B6FD9;animation:nodepulseSpaced 1.8s ease-in-out infinite;}
  @keyframes nodepulseReinforce{0%,100%{box-shadow:0 3px 0 #E0824A;}50%{box-shadow:0 3px 0 #E0824A, 0 0 0 8px rgba(224,130,74,.3);}}
  @keyframes nodepulseBonus{0%,100%{box-shadow:0 3px 0 #D4A72C;}50%{box-shadow:0 3px 0 #D4A72C, 0 0 0 8px rgba(212,167,44,.3);}}
  @keyframes nodepulseSpaced{0%,100%{box-shadow:0 3px 0 #8B6FD9;}50%{box-shadow:0 3px 0 #8B6FD9, 0 0 0 8px rgba(139,111,217,.3);}}
  .node-wrap.has-extra-flag{position:relative;}
  .extra-flag{position:absolute;top:-6px;right:-2px;background:#D4A72C;color:#4A3400;font-size:9.5px;font-weight:800;text-transform:uppercase;letter-spacing:.02em;padding:2px 6px;border-radius:999px;box-shadow:0 1.5px 0 rgba(0,0,0,.2);white-space:nowrap;}
  .extra-flag.reinforce{background:#E0824A;color:#4A2400;}
  .extra-flag.spaced{background:#8B6FD9;color:#241A4A;}
  .node-btn.autogen{border:2px dotted var(--cat-ai);}

  /* ---------- Reto escrito por el propio colegio ----------
     Sutil por decisión: el niño no tiene que hacer nada distinto con él,
     así que la marca no compite con el candado ni con "¡Nuevo!", que sí le
     dicen qué hacer. Una chispa pequeña en la esquina, del color de la
     institución, y un borde apenas insinuado.
     El tamaño va en unidades relativas para que crezca con la tipografía
     de quien la tenga agrandada: una marca de 8 píxeles fijos desaparece
     para quien más necesita verla. */
  .node-btn.propio{box-shadow:0 3px 0 var(--line), 0 0 0 2px var(--accent-soft);}

  /* ---------- Pie de la vista previa de un reto ----------
     Lo ve el profesor al terminar de jugar su propio borrador. Dice qué
     habría pasado (estrellas, o que no se califica) y devuelve al editor.
     Deliberadamente sobrio: no celebra nada, porque no hay nada que
     celebrar — no es un logro de nadie. */
  /* La cinta que avisa de que se está previsualizando. Color de aviso, no
     de error: no ha pasado nada malo, solo hay que saber dónde se está. */
  .vista-previa-cinta{
    display:flex; align-items:center; justify-content:space-between; gap:12px;
    padding:8px 12px; margin-bottom:10px; border-radius:10px;
    background:var(--accent-soft); color:var(--ink);
    font-size:13px; font-weight:700;
  }
  .vista-previa-fin{display:flex;flex-direction:column;gap:2px;margin-bottom:10px;text-align:center;}
  .vista-previa-fin strong{font-size:15px;color:var(--ink);}
  .vista-previa-fin span{font-size:13px;color:var(--ink-soft);}
  /* Se cuelga del propio botón (.node-btn ya es position:relative). Del
     contenedor NO: es tan ancho como la etiqueta del reto y la chispa se
     iba lejos del círculo. */
  .nodo-propio{
    position:absolute; top:0; right:0;
    /* rem y no em: dentro del botón, 1em son 28px (el tamaño del ícono del
       reto) y la chispa salía de 32px sobre un círculo de 72 — cualquier
       cosa menos discreta. En rem sigue creciendo con la tipografía de
       quien la tenga agrandada, que es lo que se quería del tamaño
       relativo, sin heredar el del ícono. Medido en el navegador. */
    width:1.05rem; height:1.05rem; border-radius:50%;
    display:flex; align-items:center; justify-content:center;
    background:var(--surface); color:var(--accent);
    box-shadow:0 1px 3px rgba(0,0,0,.18);
    pointer-events:none;
  }
  .nodo-propio svg{width:.62rem;height:.62rem;}

  /* ---------- Node screen chrome ---------- */
  /* DENTRO DE UN RETO NO HAY BARRA DE ARRIBA (ver showScreen, topbar.js).
     La racha y el XP no cambian mientras dura el reto y el mundo ya está
     elegido: era una banda de pantalla gastada en datos que nadie mira
     hasta salir. Lleva !important a propósito — refreshTopbar le escribe
     display:flex en línea cada vez que corre, y sin esto la barra
     reaparecería sola al primer acierto. */
  body.en-reto .topbar{display:none !important;}
  #screenNode{padding:18px 16px 60px;align-items:center;}
  /* Con la barra inferior puesta, el cuerpo YA reserva 70px + área segura
     para que nada quede debajo de ella (body.has-bottom-nav). Los 60 de
     aquí se sumaban a esos y dejaban 130px de relleno vacío al final: el
     reto entero cabía en la pantalla y aun así se podía desplazar, que es
     justo la sensación de "no me cabe" que reportó el usuario. */
  body.has-bottom-nav #screenNode{padding-bottom:8px;}
  /* Y con la barra fuera, lo primero del reto sube hasta donde se puede.
     El tope no es el borde de la pantalla: #app ya lleva el área segura
     como relleno, así que esto se mide DESDE debajo de la isla y nunca
     mete nada dentro de ella. Los pocos píxeles que quedan son el aire
     mínimo para que la cinta no salga pegada al recorte. */
  body.en-reto #screenNode{padding-top:6px;}
  body.en-reto .leccion-cinta{padding-top:2px;}
  .node-inner{max-width:640px;width:100%;}
  /* Retos de tablero (block/board, ver router.js): .game-body necesita 3
     columnas con anchos mínimos (240+240+300+huecos ≈ 810px) que superan los
     640px de arriba. Sin este ancho propio, el grid se sale del contenedor
     igual (max-width no encoge por debajo del contenido mínimo), la columna
     central (1fr) se estira sin límite en pantallas anchas dejando sus
     botones descentrados, y el panel de "¡Reto completado!" que se agrega
     después —que sí respeta los 640px— termina con un ancho distinto al del
     juego de arriba. */
  .node-inner.wide{max-width:900px;}
  .node-head{display:flex;align-items:center;gap:10px;margin-bottom:16px;flex-wrap:wrap;}
  /* El min-width evita que el título se estruje hasta el ancho de su
     palabra más larga. Con solo `flex:1` (min-width:auto = min-content),
     un título de palabras cortas como "Robi ve el mundo real" se encogía a
     59 px y caía en cuatro renglones de una palabra cada uno, mientras la
     etiqueta del mundo se quedaba cómoda al lado. Con un ancho mínimo, es
     la etiqueta la que baja de línea — que es lo correcto: el título del
     reto importa más que el nombre del mundo. `min()` lo mantiene por
     debajo del contenedor en pantallas muy angostas. */
  .node-head h2{font-size:19px;flex:1 1 auto;min-width:min(100%, 11em);}
  /* Cuando se completa un reto, router.js agrega el panel de "¡Reto
     completado!" como hermano del panel/game-body del juego dentro de
     #nodeBody. Sin este margen quedan pegados uno contra el otro (mismo
     fondo y borde), como si fueran una sola caja partida. */
  #nodeBody > * + *{margin-top:18px;}
  .node-tagpill{font-size:11.5px;font-weight:800;text-transform:uppercase;letter-spacing:.03em;background:var(--surface-2);color:var(--ink-soft);padding:4px 10px;border-radius:999px;}
  .panel{background:var(--surface);border:3px solid var(--line);border-radius:var(--radius-lg);padding:18px;box-shadow:var(--shadow);}

  /* ---------- Mastery dashboard ---------- */
  #screenMastery{padding:20px 16px 60px;align-items:center;}
  .mastery-inner{max-width:640px;width:100%;}
  .mastery-row{display:flex;align-items:center;gap:12px;padding:10px 0;}
  .mastery-row .lbl{width:190px;font-weight:700;font-size:14px;flex:none;}
  .mastery-bar{flex:1;height:14px;border-radius:999px;background:var(--muted-fill);border:2px solid var(--muted-border);overflow:hidden;}
  .mastery-bar > div{height:100%;border-radius:999px;box-shadow:inset 0 -3px 0 rgba(0,0,0,.2);}
  .mastery-row .tag{width:120px;text-align:right;font-weight:800;font-size:12.5px;}
  /* A 375px la etiqueta de 190px y el porcentaje de 120px dejaban la
     barra en menos de 40px: la barra ES el dato, así que en pantalla
     estrecha el nombre pasa arriba y la barra ocupa el ancho. */
  @media (max-width:520px){
    .mastery-row{flex-wrap:wrap;gap:4px 10px;padding:9px 0;}
    .mastery-row .lbl{width:100%;}
    .mastery-bar{flex:1 1 auto;min-width:0;}
    .mastery-row .tag{width:auto;min-width:44px;}
  }

  /* ---------- Mi dominio: resumen, grupos y favoritos ----------
     Esta pantalla era UNA lista plana de hasta 147 destrezas, sin cortes
     ni resumen: el dueño la describió como scroll infinito, y para saber
     "¿cómo voy?" había que recorrerla entera. */
  .dominio-resumen{
    display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:16px;
  }
  .dominio-dato{
    -webkit-appearance:none;appearance:none;cursor:pointer;font-family:inherit;
    display:flex;flex-direction:column;align-items:center;gap:2px;
    padding:12px 8px;min-height:64px;
    background:var(--surface);border:2px solid var(--line);border-radius:var(--radius-lg);
    transition:border-color .15s ease, transform .15s ease;
  }
  .dominio-dato:hover{border-color:var(--line-fuerte);}
  .dominio-dato:active{transform:translateY(1px);}
  .dominio-num{font-size:26px;font-weight:800;line-height:1;font-variant-numeric:tabular-nums;}
  .dominio-etq{font-size:11.5px;font-weight:700;color:var(--ink-soft);text-align:center;line-height:1.25;}

  .dominio-grupo{
    margin-top:12px;background:var(--surface);
    border:2px solid var(--line);border-radius:var(--radius-lg);
    padding:0 14px;overflow:hidden;
  }
  .dominio-grupo > summary{
    display:flex;align-items:center;gap:9px;cursor:pointer;list-style:none;
    padding:13px 0;font-weight:800;font-size:15px;min-height:48px;
  }
  .dominio-grupo > summary::-webkit-details-marker{display:none;}
  .dominio-punto{width:11px;height:11px;border-radius:50%;flex:none;}
  .dominio-grupo-tit{flex:1;min-width:0;}
  .dominio-cuenta{
    flex:none;font-size:12.5px;font-weight:800;
    background:var(--surface-2);color:var(--ink-soft);
    border-radius:999px;padding:2px 9px;font-variant-numeric:tabular-nums;
  }
  .dominio-chev{flex:none;display:flex;color:var(--ink-soft);transition:transform .18s ease;}
  .dominio-chev .lucide{width:18px;height:18px;}
  .dominio-grupo[open] > summary .dominio-chev{transform:rotate(180deg);}
  .dominio-pie{margin:0 0 6px;font-size:13px;color:var(--ink-soft);line-height:1.45;}
  .dominio-grupo > .mastery-row:last-child{padding-bottom:14px;}

  .dominio-titulo{
    display:flex;align-items:center;gap:8px;
    margin:0 0 10px;font-size:16px;font-weight:800;
  }
  .dominio-titulo .lucide{width:19px;height:19px;color:var(--cat-loop);fill:currentColor;}
  .dominio-vacio{margin:0;font-size:13.5px;color:var(--ink-soft);line-height:1.5;}
  /* ---------- Mis favoritos: pastillas + lista plana ----------
     El diseño anterior anidaba cuatro bordes redondeados —panel, tarjeta
     del mundo, botón del reto y otra caja para la estrella— y el dueño lo
     describió como "feo y anticuado". Aquí NO hay ninguna caja dentro de
     otra: el mundo se filtra con pastillas, y la fila no lleva borde ni
     relleno propios. Lo que la hace tocable es la baldosa de color del
     icono, el triángulo de reproducir y el hundido al apretar. */
  /* UNA SOLA FILA QUE SE DESPLAZA, COMO EN SPOTIFY O YOUTUBE.
     Partirlas en filas se probó y no vale: con cinco mundos de nombres
     largos salían TRES filas —194px, un cuarto del panel— de filtro para
     una lista que cabe entera. El filtro no puede pesar como el contenido.

     Lo que hacía que el carrusel se viera roto no era el carrusel: era que
     al filtrar se desplazaba solo para traer la pastilla activa, y dejaba
     medio nombre asomando por la izquierda sin que nadie lo hubiera
     tocado. Ahora la franja se queda donde el niño la dejó, así que
     cualquier corte es de un gesto suyo — que es como se lee un carrusel
     en cualquier aplicación.

     El hueco de 40px al final es lo que permite que el desvanecido sea
     fijo y no haya que medir nada: al llegar al tope, lo que se desvanece
     es ese hueco y no la última pastilla. */
  .fav-filtros{
    display:flex;align-items:center;gap:8px;margin:12px 0 6px;
    overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none;
    padding-right:40px;
    -webkit-mask-image:linear-gradient(to right, #000 calc(100% - 40px), transparent);
    mask-image:linear-gradient(to right, #000 calc(100% - 40px), transparent);
  }
  .fav-filtros::-webkit-scrollbar{display:none;}
  .fav-pastillas{display:contents;}
  .fav-pastilla{
    -webkit-appearance:none;appearance:none;cursor:pointer;font-family:inherit;
    flex:none;display:inline-flex;align-items:center;gap:5px;
    /* 44px de alto aunque el dibujo pida menos: es un control que tiene
       pastillas pegadas arriba, abajo y al lado, así que no hay a quién
       robarle el toque con una zona ampliada. */
    padding:5px 12px;min-height:44px;
    background:transparent;border:1.5px solid var(--line);border-radius:999px;
    color:var(--ink-soft);font-size:12.5px;font-weight:700;white-space:nowrap;
    transition:background .15s ease, border-color .15s ease, color .15s ease;
  }
  .fav-pastilla:active{transform:translateY(1px);}
  .fav-pastilla-punto{flex:none;width:7px;height:7px;border-radius:50%;background:currentColor;}
  /* EL COLOR DEL MUNDO LLEGA COMO `color`, NUNCA COMO UN FONDO CALCULADO.
     Los mundos guardan su color en una variable de CSS, así que desde el
     JS no se puede hacer aritmética con él para sacar una versión
     translúcida. Aquí sí: currentColor con opacidad da ese tinte sin
     conocer el valor, y funciona igual con una variable que con un
     hexadecimal, sin color-mix() —que al WebView viejo de las tabletas no
     le llega—. La capa va detrás por z-index, y el `isolation` impide que
     se hunda por debajo de la fila entera. */
  .fav-color{position:relative;isolation:isolate;}
  .fav-color::before{
    content:'';position:absolute;inset:0;border-radius:inherit;
    background:currentColor;opacity:.14;z-index:-1;
  }
  /* Sin activar, el nombre se lee en la tinta normal: cinco pastillas de
     cinco colores distintos gritando a la vez es exactamente el ruido que
     se quería quitar. El color queda en el punto. */
  .fav-pastilla .fav-pastilla-tx{color:var(--ink-soft);}
  .fav-pastilla.activa .fav-pastilla-tx{color:var(--ink);}
  /* La pastilla se tiñe SOLO cuando está activa. Teñirlas todas —que es
     lo que hacía .fav-color por sí sola— pinta cinco pastillas de cinco
     colores y las cinco parecen seleccionadas: justo la señal contraria.
     Sin activar, el color vive únicamente en el punto. */
  .fav-pastilla.fav-color::before{opacity:0;}
  .fav-pastilla.activa{border-color:currentColor;}
  .fav-pastilla.fav-color.activa::before{opacity:.16;}
  .fav-pastilla:not(.fav-color).activa{color:var(--ink);background:var(--surface-2);border-color:var(--line-fuerte);}

  /* El número no puede competir con el nombre: va más chico y apagado, y
     con cifras de ancho fijo para que las pastillas no bailen al cambiar
     de 9 a 10. */
  .fav-pastilla-n{font-size:11.5px;font-weight:800;color:var(--ink-soft);font-variant-numeric:tabular-nums;}
  .fav-pastilla.activa .fav-pastilla-n{color:var(--ink);}

  .dominio-favlista{display:flex;flex-direction:column;gap:2px;margin-top:6px;}
  .dominio-fav-fila{display:flex;align-items:center;gap:4px;}
  .dominio-fav-fila .dominio-fav{flex:1 1 auto;min-width:0;}
  /* SIN BORDE Y SIN RELLENO. Es la regla del rediseño y no puede volver a
     ponerse una caja aquí sin recuperar el anidamiento que se quitó. */
  .dominio-fav{
    -webkit-appearance:none;appearance:none;cursor:pointer;font-family:inherit;text-align:left;
    display:flex;align-items:center;gap:12px;width:100%;
    padding:8px 8px;min-height:56px;
    background:transparent;border:none;border-radius:var(--radius-md);
    color:var(--ink);
    transition:background .15s ease, transform .12s ease;
  }
  .dominio-fav:hover{background:var(--surface-2);}
  .dominio-fav:active{background:var(--surface-2);transform:translateY(1px);}
  /* La baldosa hace de carátula: ancla la fila y dice de qué mundo es sin
     necesidad de encabezado. El color se lo pone el mundo desde el JS. */
  .dominio-fav-ic{
    flex:none;display:flex;align-items:center;justify-content:center;
    width:42px;height:42px;border-radius:12px;font-size:22px;
    background:var(--surface-2);color:var(--accent-text);
  }
  /* Con color de mundo, el fondo lo pone .fav-color::before y el gris
     de debajo estorbaría. */
  .dominio-fav-ic.fav-color{background:transparent;}
  .dominio-fav-ic .lucide{width:23px;height:23px;color:currentColor;}
  .dominio-fav-txt{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px;}
  .dominio-fav-txt strong{font-size:14.5px;font-weight:700;overflow-wrap:anywhere;line-height:1.25;}
  .dominio-fav-txt small{font-size:11.5px;color:var(--ink-soft);font-weight:600;}
  .dominio-fav-ir{flex:none;display:flex;color:var(--accent-text);}
  .dominio-fav-ir .lucide{width:20px;height:20px;}
  /* La estrella tampoco tiene caja: es un icono al final de la fila. El
     área que responde al dedo sigue siendo de 44px aunque no se dibuje. */
  .dominio-fav-quitar{
    -webkit-appearance:none;appearance:none;cursor:pointer;
    flex:0 0 44px;width:44px;height:44px;
    display:flex;align-items:center;justify-content:center;
    background:transparent;border:none;border-radius:50%;
    color:var(--cat-loop);
    transition:background .15s ease;
  }
  .dominio-fav-quitar .lucide{width:21px;height:21px;fill:currentColor;}
  .dominio-fav-quitar:active{background:var(--surface-2);}
  /* Apagada = ya no es favorito. La estrella sin relleno es la misma
     señal que en el pie del reto, para que no haya que aprenderla dos
     veces; la fila entera se atenúa porque está de salida. */
  .dominio-fav-quitar.apagada{color:var(--ink-soft);}
  .dominio-fav-quitar.apagada .lucide{fill:none;stroke:currentColor;stroke-width:2;}
  .dominio-fav-fila.fav-quitado .dominio-fav{opacity:.45;}

  /* ---------- Mis logros (insignias) ---------- */
  #screenBadges{padding:20px 16px 60px;align-items:center;}
  .badges-inner{max-width:680px;width:100%;}
  .badge-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:14px;margin-top:20px;}
  .badge-card{background:var(--surface);border:3px solid var(--line);border-radius:var(--radius-lg);padding:20px 16px;text-align:center;box-shadow:var(--shadow);}
  .badge-card .badge-ic{width:60px;height:60px;border-radius:50%;display:flex;align-items:center;justify-content:center;margin:0 auto 10px;background:var(--surface-2);font-size:28px;}
  .badge-card .badge-ic svg{width:30px;height:30px;color:#fff;}
  .badge-card h3{font-size:15px;margin-bottom:6px;}
  .badge-card .progress-note{font-size:12.5px;color:var(--ink-soft);}
  .badge-card.earned .stars-total{font-size:13px;font-weight:800;color:var(--gold);margin-bottom:10px;}
  /* Carril de niveles dentro de cada tarjeta de mundo — una píldora por
     nivel con contenido (ver tiersWithContent en state.js). */
  .tier-pill-row{display:flex;flex-direction:column;gap:6px;margin-top:8px;}
  .tier-pill{display:flex;align-items:center;gap:7px;padding:7px 10px;border-radius:10px;border:none;font-weight:700;font-size:12.5px;text-align:left;background:var(--surface-2);color:var(--ink-soft);cursor:not-allowed;}
  .tier-pill .lucide{width:15px;height:15px;flex:none;}
  .tier-pill span{flex:1;}
  .tier-pill small{font-weight:800;font-variant-numeric:tabular-nums;opacity:.8;}
  .tier-pill.earned{background:#FFF3D0;color:#8A6D00;cursor:pointer;box-shadow:0 0 0 1.5px #E8C666;}
  .tier-pill.earned:active{transform:translateY(1px);}

  /* ---------- Certificado (horizontal, tamaño A4) ----------
     La vista previa en pantalla usa la misma proporción 297:210 (A4
     horizontal) que los archivos que se descargan — lo que se ve aquí es,
     en forma, lo que sale en el PNG/PDF (que se dibujan aparte en un
     <canvas>, ver badges.js downloadCertificateCanvas). */
  .certificate-wrap{max-width:980px;width:100%;padding:24px 16px 60px;margin:0 auto;}
  .certificate-card{background:#FFFFFF;color:#16283D;border:8px solid #1C9FD7;border-radius:24px;box-shadow:var(--shadow);position:relative;aspect-ratio:297/210;display:flex;overflow:hidden;}
  .certificate-card::before{content:'';position:absolute;inset:12px;border:2px solid #D6EEFB;border-radius:14px;pointer-events:none;}
  .cert-col-icon{flex:0 0 30%;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;padding:20px;border-right:2px solid #D6EEFB;}
  .cert-col-text{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:20px 28px;min-width:0;}
  .certificate-card .cert-kicker{font-size:12.5px;letter-spacing:.08em;text-transform:uppercase;color:#51637A;font-weight:800;}
  .certificate-card .cert-world-ic{width:23%;aspect-ratio:1;max-width:90px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:2.6vw;color:#fff;}
  /* El ícono del mundo DENTRO de su círculo. .lucide-xl mide 2.3em y el
     círculo lleva font-size 2.6vw (26px en el celular): en una pantalla
     ancha el ícono medía 60px en un círculo de 52 y se salía por los
     cuatro lados — el «logo feo» de la izquierda que vio el dueño en el
     navegador. Mismo caso que .circuit-slot: se acota aquí, relativo al
     círculo, y no en .lucide-xl, que comparten otros trece retos. */
  .certificate-card .cert-world-ic .lucide{width:54%;height:auto;aspect-ratio:1;flex:none;}
  .certificate-card .cert-robi-img{width:26%;max-width:100px;object-fit:contain;}
  .certificate-card h1{font-family:var(--font-display);font-size:clamp(18px,2.6vw,30px);margin:0 0 8px;color:#16283D;}
  .certificate-card .cert-body{font-size:clamp(11px,1.35vw,15px);color:#33455C;line-height:1.5;max-width:46ch;margin:0 auto;}
  .certificate-card .cert-name{font-size:clamp(18px,2.8vw,32px);font-weight:800;margin:6px 0;font-family:var(--font-display);color:#16283D;}
  .certificate-card .cert-skills{display:flex;flex-wrap:wrap;gap:6px;justify-content:center;margin:12px 0 2px;}
  .certificate-card .cert-skill-chip{background:#EEF2F7;border:1.5px solid #CBD6E3;border-radius:999px;padding:4px 10px;font-size:clamp(9px,1vw,11.5px);font-weight:700;color:#16283D;}
  .certificate-card .cert-date{font-size:clamp(9px,1vw,11.5px);color:#667382;margin-top:14px;}
  .certificate-card .cert-code{font-size:clamp(9px,1vw,11.5px);color:#667382;margin-top:4px;letter-spacing:.03em;}
  .certificate-card .cert-code strong{color:#33455C;}
  /* Nota de la vista previa simplificada (badges.js) — aclara que el
     diploma real trae más contenido, para que no parezca que falta algo. */
  .certificate-card .cert-preview-note{font-size:10.5px;color:#8695A8;font-style:italic;line-height:1.4;margin-top:10px;max-width:40ch;}
  .certificate-actions{display:flex;gap:10px;justify-content:center;flex-wrap:wrap;margin-top:20px;}

  /* En un celular, ".certificate-card" mantenía SIEMPRE la proporción
     horizontal de una hoja A4 apaisada (297:210) — en una pantalla
     angosta eso la deja carísima de baja (una tira de ~280px de alto con
     dos columnas lado a lado adentro), forzando todo el texto al tamaño
     mínimo de sus clamp() y viéndose amontonado. Por debajo de 560px de
     ancho pasa a apilarse verticalmente (ícono/Robi arriba, texto abajo)
     con alto libre en vez de la proporción fija — el PNG/PDF descargado
     no se toca, sigue siendo la hoja A4 de siempre (renderCertificateCanvas
     en badges.js dibuja esa versión aparte, a mano, sin depender de este
     CSS en absoluto). */
  @media (max-width:560px){
    .certificate-card{aspect-ratio:auto;flex-direction:column;}
    .cert-col-icon{flex:none;flex-direction:row;justify-content:center;border-right:none;border-bottom:2px solid #D6EEFB;padding:16px;gap:10px;}
    .certificate-card .cert-world-ic{width:52px;height:52px;max-width:52px;font-size:26px;}
    .certificate-card .cert-robi-img{width:44px;max-width:44px;}
    .cert-col-text{padding:18px 20px 24px;}
    .certificate-card h1{font-size:20px;}
    .certificate-card .cert-body{font-size:13.5px;}
    .certificate-card .cert-name{font-size:22px;}
  }

  /* ---------- Star result ---------- */
  .profesor-img{width:76px;height:76px;object-fit:contain;filter:drop-shadow(0 3px 3px rgba(0,0,0,.2));animation:starPop .4s cubic-bezier(.34,1.56,.64,1);}
  .stars-big{display:flex;gap:10px;justify-content:center;}
  .stars-big img{width:52px;height:52px;filter:drop-shadow(0 2px 2px rgba(0,0,0,.15));}
  .stars-big img.pop{animation:starPop .45s cubic-bezier(.34,1.56,.64,1) backwards;}
  @keyframes starPop{0%{opacity:0;transform:scale(0);}100%{opacity:1;transform:scale(1);}}

  /* ---------- Block-programming node (ported) ---------- */
  .game-body{display:grid;grid-template-columns:minmax(200px,240px) minmax(240px,360px) minmax(240px,300px);gap:14px;}
  .subpanel h3{font-size:12px;text-transform:uppercase;letter-spacing:.06em;color:var(--ink-soft);font-family:var(--font-body);font-weight:700;margin-bottom:10px;}
  .stage-goal{margin:-4px 0 10px;font-size:13.5px;font-weight:700;color:var(--ink);text-align:center;}
  /* padding-top: en móvil .palette-list tiene overflow-x:auto (franja
     horizontal), y eso también recorta verticalmente — sin este margen, el
     "levante" de .block-card:hover (translateY(-2px)) quedaba cortado
     arriba por el propio contenedor. */
  .palette-list{display:flex;flex-direction:column;gap:9px;padding-top:4px;}
  /* .block-card ("Bloques"): solo ícono, sin la palabra al lado — boceto
     aprobado por el usuario. Cuadrado chico y parejo en vez de la pastilla
     alargada de antes. */
  .block-card{border:none;border-radius:14px;width:46px;height:46px;display:flex;align-items:center;justify-content:center;cursor:pointer;color:var(--cat-ink,#222);background:var(--cat-color,#ddd);box-shadow:0 0 0 1.5px rgba(0,0,0,.18), 0 3px 0 rgba(0,0,0,.2);flex:none;}
  .block-card:hover{transform:translateY(-2px);}
  .block-card:active{transform:translateY(1px);box-shadow:0 0 0 1.5px rgba(0,0,0,.18), 0 1px 0 rgba(0,0,0,.12);}
  .block-card .lucide{width:22px;height:22px;}
  /* .block-hint: qué hace el bloque que se acaba de tocar — aparece un
     momento y se esconde sola (block.js la dispara y le pone el timeout).
     max-height en vez de solo opacity: así en reposo no ocupa ALTO
     ninguno (no es un texto fijo debajo de "Bloques"), y al aparecer
     empuja el contenido de abajo suavemente en vez de superponerse. */
  .block-hint{max-height:0;overflow:hidden;opacity:0;font-size:12.5px;font-weight:700;color:var(--ink-soft);text-align:center;transition:opacity .15s ease, max-height .15s ease, margin-top .15s ease;margin-top:0;}
  .block-hint.show{max-height:24px;opacity:1;margin-top:8px;}
  /* ---------- Cajita Repetir (contenedor de bucle en "Tu programa") ---------- */
  /* El botón de la paleta y la cajita necesitan ancho/alto natural — sin
     esto heredan el cuadrado fijo de .block-card/.script-item y el
     contenido se encima (visto en la primera prueba visual). */
  .loop-add-card{width:auto !important;height:46px;padding:0 14px;display:inline-flex;align-items:center;gap:7px;background:var(--surface);color:var(--ink);box-shadow:0 0 0 1.5px var(--line), 0 3px 0 var(--line);}
  .loop-add-card .loop-add-label{font-weight:700;font-size:12.5px;white-space:nowrap;}
  .script-item.loop-box{width:auto;height:auto;min-height:48px;cursor:default;color:var(--ink);background:var(--surface-2);}
  .loop-box .loop-head button,.loop-box .loop-open{color:var(--ink);}
  .loop-box{display:inline-flex;align-items:center;gap:8px;background:var(--surface-2);border-radius:var(--radius-md);padding:7px 10px;box-shadow:inset 0 0 0 2px var(--accent-soft, var(--line));}
  .loop-box.open{box-shadow:inset 0 0 0 2.5px var(--accent);}
  .loop-box .loop-head{display:inline-flex;align-items:center;gap:5px;}
  .loop-box .loop-head .lucide{width:16px;height:16px;}
  .loop-box .loop-head button{width:44px;height:44px;border-radius:12px;border:none;background:var(--surface);font-size:15px;cursor:pointer;font-weight:700;box-shadow:0 0 0 1.5px var(--line), 0 2px 0 var(--line);}
  .loop-box .loop-head button:active{transform:translateY(2px);box-shadow:0 0 0 1.5px var(--line);}
  .loop-box .loop-count{font-weight:800;font-variant-numeric:tabular-nums;min-width:1.6em;text-align:center;}
  .loop-box .loop-children{display:inline-flex;align-items:center;gap:5px;flex-wrap:wrap;}
  .loop-box .loop-empty{font-size:11.5px;color:var(--ink-soft);font-style:italic;padding:0 4px;}
  .loop-chip{position:relative;display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;border-radius:10px;background:var(--cat-color, var(--surface));color:var(--cat-ink, inherit);}
  .loop-chip .lucide{width:17px;height:17px;}
  .loop-chip.active{outline:3px solid var(--accent);}
  .loop-chip-del{position:absolute;top:-7px;right:-7px;width:18px;height:18px;border-radius:50%;border:none;background:var(--cat-move);color:#fff;display:flex;align-items:center;justify-content:center;cursor:pointer;padding:0;}
  .loop-chip-del .lucide{width:11px;height:11px;}
  .loop-box .loop-close,.loop-box .loop-open{width:44px;height:44px;border-radius:12px;border:none;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;background:var(--surface);box-shadow:0 0 0 1.5px var(--line), 0 2px 0 var(--line);}
  .loop-box .loop-close{background:var(--cat-sound);color:#fff;box-shadow:none;}
  .loop-box .loop-close .lucide,.loop-box .loop-open .lucide{width:15px;height:15px;}

  /* .script-progress ("Paso X de N"): oculto por defecto (display:none
     puesto desde el HTML) — solo aparece mientras el programa CORRE,
     porque recién ahí hay un total fijo de pasos que tenga sentido
     mostrar. block.js lo muestra/actualiza en cada paso de executeAll()
     y lo vuelve a ocultar cuando termina la corrida. */
  .script-progress-label{color:var(--ink-soft);font-size:11.5px;font-weight:700;margin-bottom:6px;}
  .script-progress-bar{display:flex;gap:4px;margin-bottom:10px;}
  .script-progress-bar span{flex:1;height:4px;border-radius:2px;background:var(--line);}
  .script-progress-bar span.done{background:var(--cat-sound);}
  .script-progress-bar span.current{background:var(--cat-loop);}

  /* "Tu programa": chips chicos de solo ícono (34px) que se acomodan en
     fila y saltan de línea si no entran — ya no hace falta scroll propio
     para verlos todos. Tocar un chip lo abre mostrando ↑↓✕ en el lugar
     (sin empujar a los demás fuera de la fila); tocarlo de nuevo (o tocar
     otro paso) lo cierra. Boceto aprobado por el usuario antes de esto. */
  .script-list{display:flex;flex-direction:row;flex-wrap:wrap;gap:6px;margin-bottom:10px;min-height:34px;}
  .script-empty{color:var(--ink-soft);font-size:13.5px;text-align:center;padding:24px 10px;border:2px dashed var(--line);border-radius:var(--radius-md);width:100%;}
  .script-item{display:flex;align-items:center;justify-content:center;gap:0;border:none;border-radius:12px;width:44px;height:44px;padding:0;color:var(--cat-ink,#222);background:var(--cat-color,#ddd);cursor:pointer;flex:none;transition:transform .12s ease;}
  .script-item .ic{display:flex;flex:none;}
  .script-item .ic .lucide{width:16px;height:16px;}
  /* Al expandirse, el paso mostraba unas flechas de 18x11 px: el punto más
     pequeño de toda la aplicación. Ahora los tres controles —subir, bajar y
     quitar— son botones de 44 en una fila. El paso ocupa más ancho, pero
     .script-list ya envuelve, así que simplemente se lleva su renglón. */
  .script-item.expanded{width:auto;height:52px;justify-content:flex-start;gap:6px;padding:4px 6px 4px 10px;cursor:default;}
  .script-item.active{outline:2px solid var(--ink);outline-offset:2px;transform:scale(1.12);animation:scriptStepPulse .5s ease infinite alternate;}
  .script-item.done{opacity:.4;}
  @keyframes scriptStepPulse{from{box-shadow:0 0 0 0 rgba(0,0,0,.25);}to{box-shadow:0 0 0 6px rgba(0,0,0,0);}}
  /* Subir/bajar comparten un solo control angosto de dos mitades en vez de
     dos botones separados — el chip expandido ocupa mucho menos ancho. */
  .script-reorder{display:flex;flex-direction:row;gap:4px;flex:none;}
  .script-reorder button{width:44px;height:44px;flex:none;border:none;border-radius:11px;background:rgba(255,255,255,.5);padding:0;cursor:pointer;display:flex;align-items:center;justify-content:center;}
  .script-reorder button .lucide{width:18px;height:18px;}
  .script-reorder button:disabled{opacity:.35;cursor:not-allowed;}
  .script-del{width:44px;height:44px;border-radius:11px;border:none;background:rgba(255,90,70,.45);cursor:pointer;display:flex;align-items:center;justify-content:center;flex:none;}
  .script-del .lucide{width:12px;height:12px;}
  .run-row{display:flex;gap:8px;margin-top:4px;}
  .stage-wrap{position:relative;width:min(100%,300px);aspect-ratio:1/1;margin:0 auto;}
  /* --line-fuerte: este es el tablero por el que camina Robi, la
     superficie de juego más usada de toda la aplicación, y con --line su
     borde contrastaba 1.23:1 contra el fondo — el niño no veía dónde
     empieza y acaba el mapa. La auditoría lo encontró en 920 pantallas de
     1062, o sea prácticamente en todos los retos de bloques. */
  .stage-grid{position:absolute;inset:0;display:grid;border-radius:14px;overflow:hidden;border:3px solid var(--line-fuerte);background:var(--surface-2);}
  .cell{border:1px solid color-mix(in srgb, var(--line) 60%, transparent);display:flex;align-items:center;justify-content:center;font-size:1.5em;cursor:default;}
  .cell.obstacle{background:color-mix(in srgb, var(--ink) 12%, var(--surface-2));}
  .cell.goal{background:color-mix(in srgb, var(--cat-sound) 25%, var(--surface-2));}
  .cell.start{background:color-mix(in srgb, var(--world-robo) 25%, var(--surface-2));}
  .cell.editable{cursor:pointer;}
  .cell.editable:hover{outline:2px solid var(--accent);outline-offset:-2px;}
  /* "Predice antes de ejecutar" (node.predictBeforeRun, ver
     showPredictStep en block.js) — mientras se elige, TODAS las celdas
     sin obstáculo quedan tocables con un contorno propio, distinto del
     de edición (.editable), para no confundir "elegir dónde va a
     terminar Robi" con "editar el mapa". */
  .predict-banner{background:var(--accent-soft);color:var(--accent-soft-ink);border:2px solid var(--accent-border);border-radius:var(--radius-md);padding:8px 12px;font-size:12.5px;font-weight:700;text-align:center;margin-bottom:8px;}
  /* Se toca "Ejecutar" otra vez sin haber predicho: en vez de apilar
     carteles (el fallo que esto reemplaza), el mismo cartel late una vez
     para llamar la atención sobre él. */
  .predict-banner.insiste{animation:predictInsiste .9s ease-out 2;}
  @keyframes predictInsiste{0%,100%{transform:scale(1);}35%{transform:scale(1.04);box-shadow:0 0 0 4px var(--accent-soft);}}
  @media (prefers-reduced-motion:reduce){ .predict-banner.insiste{animation:none;outline:3px solid var(--accent-border);} }
  .cell.predict-candidate{cursor:pointer;box-shadow:inset 0 0 0 2px var(--accent);}
  .cell.predict-candidate:active{background:color-mix(in srgb, var(--accent) 25%, var(--surface-2));}
  /* ==================== Niveles de "misión" (block.js) ====================
     light/collect: p_light1 (Enciende las luces) y p_collect1 (Recolecta
     las baterías) — celda apagada vs. encendida/recogida. box/target:
     p_cargo1 (La grúa de Robi) — --box-color lo pone JS por celda según
     el color de esa caja/zona específica (drawBoxesAndTargets, block.js),
     así el mismo CSS sirve para cualquier color sin tocarlo de nuevo. */
  .cell.light-off{background:color-mix(in srgb, var(--ink) 10%, var(--surface-2));opacity:.55;}
  .cell.light-on{background:color-mix(in srgb, var(--cat-loop) 35%, var(--surface-2));opacity:1;}
  .cell.item-cell{background:color-mix(in srgb, var(--cat-ai) 22%, var(--surface-2));}
  .cell.box-cell{background:color-mix(in srgb, var(--box-color, var(--accent)) 22%, var(--surface-2));}
  .cell.target-cell{background:color-mix(in srgb, var(--box-color, var(--accent)) 12%, var(--surface-2));outline:3px dashed var(--box-color, var(--accent));outline-offset:-4px;}
  /* Insignia de "Robi está cargando una caja" — hija de .robot, así que
     rota/se mueve con él sin lógica de posición propia. */
  .carry-badge{position:absolute;top:-6px;right:-6px;width:20px;height:20px;border-radius:50%;background:var(--box-color, var(--accent));display:flex;align-items:center;justify-content:center;font-size:12px;box-shadow:var(--shadow);}
  .robot{position:absolute;top:0;left:0;display:flex;align-items:center;justify-content:center;transition:transform .38s cubic-bezier(.4,1.6,.4,1), opacity .3s ease;will-change:transform;}
  .robot svg, .robot img{width:78%;height:78%;object-fit:contain;filter:drop-shadow(0 3px 4px rgba(0,0,0,.25));}
  .robot.bump{animation:bump .28s ease;}
  @keyframes bump{0%,100%{filter:none;}50%{transform-origin:center;}}
  /* Cuando el programa no llega a la meta, Robi vuelve solo al inicio en
     vez de quedarse plantado a mitad de camino — este leve desvanecido
     (no un salto brusco) es la pista de que "está volviendo", antes de
     que la posición cambie. */
  .robot.returning{opacity:.5;}
  /* .speech ahora es hermano de .stage-grid dentro de .stage-wrap (ver
     renderStage/positionRobot en block.js) — left/top en px los pone JS
     según la celda del robot; el transform de aquí solo centra el globo
     sobre ese punto y lo sube un poco, ya no depende del padre. Con
     white-space:normal + max-width en vez de nowrap fijo, un texto más
     largo se acomoda en 2 líneas en vez de ensancharse sin límite. */
  .speech{position:absolute;transform:translate(-50%,calc(-100% - 10px));max-width:150px;width:max-content;text-align:center;background:var(--surface);border:2px solid var(--ink);border-radius:14px;padding:6px 11px;font-size:12.5px;line-height:1.3;font-weight:700;white-space:normal;box-shadow:var(--shadow);opacity:0;transition:opacity .2s ease, transform .2s ease;pointer-events:none;z-index:3;}
  .speech.show{opacity:1;transform:translate(-50%,calc(-100% - 18px));}
  /* .speech.below: cuando Robi está en la fila de arriba del tablero, ver
     positionRobot() en block.js — el globo se muestra hacia abajo en vez
     de hacia arriba, para que nunca quede pegado/cortado contra el borde
     de arriba de la pantalla en móvil (el mapa queda fijo ahí arriba). */
  .speech.below{transform:translate(-50%,10px);}
  .speech.below.show{transform:translate(-50%,18px);}
  .code-panel{background:#0d1b2a;color:#c7e6ff;border-radius:var(--radius-md);padding:11px 13px;font-family:var(--font-mono);font-size:12px;line-height:1.6;overflow:auto;white-space:pre;margin-top:12px;}
  .code-panel .kw{color:#ff9d7a;} .code-panel .fn{color:#7ad6ff;}

  @media (max-width:820px){
    /* minmax(0,1fr) en vez de 1fr: sin el 0, una pista de grid no encoge
       por debajo del min-content de lo que tenga adentro — y las franjas
       horizontales de bloques/programa de aquí abajo (overflow-x:auto)
       tienen un min-content enorme (la suma de todos los bloques sin
       recortar). Sin este mínimo explícito, en vez de scrollear adentro,
       toda la columna se ensancha y empuja el layout fuera de la pantalla. */
    .game-body{grid-template-columns:minmax(0,1fr);gap:8px;}
    /* En móvil, el robot (.stage-panel) queda fijo arriba de todo mientras
       se arman y ejecutan los pasos debajo — así nunca hace falta
       desplazarse para verlo moverse. order:-1 lo sube visualmente sin
       tocar el HTML (block.js sigue generando Bloques/Programa/Mapa en ese
       orden; a nadie más le importa el orden real). */
    .stage-panel{order:-1;position:sticky;top:0;z-index:5;padding:8px 14px calc(8px + env(safe-area-inset-top));}
    .stage-panel h3{margin-bottom:4px;}
    .stage-panel .stage-goal{margin:0 0 4px;font-size:12px;}
    .stage-panel .stage-wrap{width:min(48vw,190px);}
    .stage-panel .mascot-box{margin-top:6px;padding:7px 10px;gap:7px;}
    .stage-panel .mascot-box img, .stage-panel .mascot-box svg{width:24px;height:24px;}
    .stage-panel .mascot-box p{font-size:12px;line-height:1.3;}

    .game-body .subpanel{padding:11px 14px;}

    /* La paleta de bloques (elegir qué agregar) sí funciona bien como
       franja horizontal — es un menú de opciones para hojear, como una
       bandeja de stickers: el orden en que se ven no importa. */
    .palette-list{flex-direction:row;overflow-x:auto;-webkit-overflow-scrolling:touch;padding-bottom:2px;}
    .palette-list .block-card{flex:0 0 auto;}
    /* "Tu programa" ya no necesita franja horizontal en móvil: los chips
       son chicos (34px) y saltan de línea solos (ver .script-list base).
       BUG real reportado por un usuario probando con su hijo de 5 años
       ("los pasos no se ven bien de forma vertical"), reproducido y
       confirmado con la app corriendo: .script-list crecía sin límite,
       y .run-row (el de "Vaciar"/"Ejecutar", ver más abajo) era sticky
       clavado cerca del borde inferior de LA PANTALLA — no de este panel
       — así que en cuanto el programa pasaba de ~2 filas de chips (unos
       15 pasos), la fila de más quedaba geométricamente detrás del botón
       "Ejecutar", tapada, sin scroll propio ni ningún indicio de que
       faltaban pasos por ver; incluso llegar a verla requería adivinar
       que había que desplazar TODA la página unos pocos px de sobra.
       Antes tenía "sin scroll propio" como ventaja explícita — dejó de
       serlo en cuanto un programa creció lo suficiente. Ahora
       .script-list tiene su propio tope con scroll acotado: nunca puede
       quedar detrás de nada, sin importar cuántos pasos tenga el
       programa, y al no ser un múltiplo exacto de fila+separación,
       siempre asoma un pedacito de la fila siguiente cuando hay más
       para ver, como pista de que se puede seguir bajando. */
    .script-list{max-height:120px;overflow-y:auto;-webkit-overflow-scrolling:touch;scroll-behavior:smooth;}

    /* .run-row ya NO es sticky: al quedar .script-list acotado arriba
       (ver comentario anterior), "Vaciar"/"Ejecutar" quedan siempre en
       flujo normal justo debajo — nunca puede treparse sobre pasos que
       todavía no se vieron. En la enorme mayoría de retos (programas
       cortos) esto se sigue viendo, en la práctica, "pegado abajo sin
       hacer falta bajar" — la única diferencia real es en programas muy
       largos, donde ahora hace falta un scroll de página normal y
       predecible para alcanzarlo, en vez de un contenido escondido sin
       aviso. */
    .run-row{margin:8px -14px -11px;padding:10px 14px calc(11px + 4px);border-top:1.5px solid var(--line);border-radius:0 0 var(--radius-lg) var(--radius-lg);}
  }

  /* ---------- Concept-card node ---------- */
  .card-deck{position:relative;min-height:150px;}
  .concept-slide{background:var(--accent-soft);color:var(--accent-soft-ink);border:3px solid var(--accent-border);border-radius:var(--radius-md);padding:20px;font-size:16px;line-height:1.55;font-weight:600;text-align:center;}
  .dots{display:flex;justify-content:center;gap:6px;margin:14px 0;}
  .dots span{width:8px;height:8px;border-radius:50%;background:var(--line);}
  .dots span.on{background:var(--accent);}
  /* Pista puntual de concept.js banda A: oculta hasta el primer fallo,
     mismo patrón de max-height/opacity que .block-hint. */
  .concept-hint{max-height:0;overflow:hidden;opacity:0;font-size:13.5px;font-weight:700;color:var(--ink-soft);text-align:center;background:var(--accent-soft);border-radius:var(--radius-md);transition:opacity .2s ease, max-height .2s ease, margin-top .2s ease, padding .2s ease;margin-top:0;padding:0 12px;}
  .concept-hint.show{max-height:60px;opacity:1;margin-top:10px;padding:10px 12px;}

  /* ---------- Demo "variable" (concept.js banda A) ---------- */
  .var-demo{display:flex;justify-content:center;margin:18px 0 4px;}
  .var-box{display:flex;flex-direction:column;align-items:center;gap:6px;background:var(--accent-soft);border:3px solid var(--accent-border);border-radius:var(--radius-md);padding:18px 28px;min-width:130px;}
  .var-label{font-weight:800;font-size:13px;color:var(--ink-soft);text-transform:uppercase;letter-spacing:.03em;}
  .var-value{font-size:34px;font-weight:800;color:var(--accent-soft-ink);}
  .var-box.pop{animation:varBump .4s cubic-bezier(.34,1.56,.64,1);}
  @keyframes varBump{0%{transform:scale(1);}35%{transform:scale(1.12);}100%{transform:scale(1);}}
  /* ➕/➖ en vez de un solo botón — ver comentario en renderVariableDemo
     (concept.js) sobre por qué un solo botón repetido no requería
     ninguna decisión real. */
  .var-btn-row{display:flex;gap:14px;justify-content:center;margin-top:16px;}
  .var-btn{width:52px;height:52px;border-radius:50%;border:none;background:var(--surface);font-size:24px;cursor:pointer;box-shadow:0 0 0 1.5px var(--line), 0 3px 0 var(--line);}
  .var-btn:active{transform:translateY(3px);box-shadow:0 0 0 1.5px var(--line);}

  /* ---------- Demo "función" (concept.js banda A) ----------
     Rediseñada: los 4 botones están SIEMPRE habilitados (2 correctos +
     2 distractores), no uno solo por vez — ver comentario en
     renderFunctionDemo (concept.js) sobre por qué. */
  .func-steps{display:flex;gap:10px;flex-wrap:wrap;justify-content:center;margin-top:16px;}
  .func-step{border:none;background:var(--surface);border-radius:var(--radius-md);padding:14px 16px;font-weight:700;font-size:15px;cursor:pointer;box-shadow:0 0 0 1.5px var(--line), 0 3px 0 var(--line);transition:transform .1s ease;}
  .func-step:active{transform:translateY(3px);box-shadow:0 0 0 1.5px var(--line);}
  .func-step.wrong{animation:sortShake .4s ease;}
  .func-progress{font-size:26px;margin:6px 0 4px;letter-spacing:.15em;}

  /* ---------- Demo "batería/energía" (concept.js banda A, r1) ---------- */
  .battery-demo-stage{position:relative;display:flex;justify-content:center;align-items:center;margin:18px 0 4px;min-height:110px;}
  .battery-demo-robi{width:96px;filter:grayscale(1) brightness(.65);transition:filter .4s ease;}
  .battery-demo-robi.on{filter:none;}
  .battery-give-btn{position:absolute;right:calc(50% - 90px);bottom:0;width:52px;height:52px;border-radius:50%;border:none;background:var(--accent-soft);box-shadow:0 0 0 1.5px var(--accent-border), 0 3px 0 var(--accent-border);font-size:26px;cursor:pointer;display:flex;align-items:center;justify-content:center;animation:battPulse 1s ease infinite;}
  @keyframes battPulse{0%,100%{transform:scale(1);}50%{transform:scale(1.1);}}

  /* ---------- Demo "conductor/aislante" (concept.js banda A, r1c) ---------- */
  .sort-items{display:flex;gap:12px;justify-content:center;flex-wrap:wrap;margin:16px 0;min-height:52px;}
  .sort-item{width:52px;height:52px;border-radius:50%;border:none;background:var(--surface);font-size:26px;cursor:pointer;box-shadow:0 0 0 1.5px var(--line), 0 3px 0 var(--line);transition:transform .1s ease;}
  .sort-item.sel{box-shadow:0 0 0 2.5px var(--accent), 0 3px 0 var(--accent);transform:translateY(-3px);}
  .sort-buckets{display:flex;gap:12px;justify-content:center;}
  .sort-bucket{flex:1;max-width:150px;min-height:64px;border-radius:var(--radius-md);border:2.5px dashed var(--line);background:var(--surface-2);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;padding:10px;cursor:pointer;}
  .sort-bucket-ic{font-size:22px;}
  /* #176: texto del balde ("Deja pasar"/"No deja pasar") — ver comentario
     junto a renderMaterialSortDemo, concept.js. Sin esto el balde solo
     mostraba el ícono ⚡/🚫, insuficiente para que un niño de 5-6 años
     sepa qué significa "dónde va" sin haberlo adivinado antes. */
  .sort-bucket-label{font-size:11.5px;font-weight:700;color:var(--ink-soft);line-height:1.15;}
  .sort-bucket-items{font-size:20px;min-height:1.2em;}
  .sort-bucket.shake{animation:sortShake .4s ease;}
  @keyframes sortShake{0%,100%{transform:translateX(0);}25%{transform:translateX(-6px);}75%{transform:translateX(6px);}}

  /* ---------- Demo "estados de la materia" (concept.js banda A, s4) ---------- */
  .matter-demo{display:flex;flex-direction:column;align-items:center;gap:4px;margin:18px 0 4px;}
  .matter-ic{font-size:52px;transition:transform .25s cubic-bezier(.34,1.56,.64,1);}
  .matter-label{font-weight:800;font-size:14px;color:var(--ink-soft);}

  /* ---------- Demo "serie vs. paralelo" (concept.js banda A, r4) ---------- */
  .bulb-row{display:flex;gap:14px;justify-content:center;font-size:40px;margin:18px 0;}
  .bulb-ic{transition:opacity .3s ease;}

  /* ---------- Demo "sensores" (concept.js banda A, r6) ---------- */
  .sensor-stage{display:flex;align-items:center;justify-content:center;gap:24px;font-size:44px;margin:18px 0;min-height:60px;}

  /* ---------- Demo "depurar" (concept.js banda A, p6) ----------
     #173: antes el paso con el error venía con un anillo PULSANTE desde
     el primer render (.wrong, animation:debugPulse) y era el ÚNICO
     tocable de los tres — "la opción correcta viene pre-marcada, no
     tiene sentido" (reporte real). Ahora los 3 pasos son botones
     normales sin ninguna marca hasta que el niño toca uno: .wrong-flash
     es la respuesta transitoria a tocar un paso que SÍ está bien (mismo
     patrón que .maze-cell.wrong-flash), y .fixed solo aparece DESPUÉS de
     encontrar y corregir el paso correcto — nunca antes. */
  .debug-map{display:grid;gap:5px;margin:14px auto 4px;width:max-content;}
  .debug-map .dm-cell{width:46px;height:46px;border-radius:10px;background:var(--surface);box-shadow:inset 0 0 0 1.5px var(--line);display:flex;align-items:center;justify-content:center;font-size:24px;}
  .debug-map .dm-goal{box-shadow:inset 0 0 0 2px var(--cat-sound);}
  .debug-steps{display:flex;gap:10px;align-items:center;justify-content:center;font-size:32px;margin:18px 0;}
  .debug-step{background:none;border:none;padding:4px;line-height:1;border-radius:50%;}
  .debug-step[data-i]{cursor:pointer;}
  .debug-step.wrong-flash{box-shadow:0 0 0 3px var(--cat-move);animation:debugShake .35s ease;}
  .debug-step.fixed{box-shadow:0 0 0 3px var(--cat-sound);}
  @keyframes debugShake{0%,100%{transform:translateX(0);}25%{transform:translateX(-4px);}75%{transform:translateX(4px);}}

  /* ---------- Demo "IA aprende de ejemplos" (concept.js banda A, a1/a5) ---------- */
  .ai-examples{display:flex;gap:12px;justify-content:center;margin:18px 0;}
  .ai-example{width:56px;height:56px;border-radius:50%;border:none;background:var(--surface);font-size:28px;cursor:pointer;box-shadow:0 0 0 1.5px var(--line), 0 3px 0 var(--line);transition:opacity .2s ease;}
  .ai-example.done{opacity:.35;}
  .ai-guess-ic{font-size:48px;margin:14px 0;}
  .ai-accuracy-face{font-size:52px;margin:14px 0 4px;}

  /* ---------- Demo "flota/se hunde" (concept.js banda A, s5) ---------- */
  .float-pool{display:flex;align-items:flex-end;justify-content:center;gap:14px;min-height:90px;padding:10px;background:color-mix(in srgb, #1C9FD7 22%, var(--surface-2));border-radius:var(--radius-md);margin-top:14px;}
  .float-item{font-size:28px;}
  .float-item.floats{align-self:flex-start;}
  .float-item.sinks{align-self:flex-end;}

  /* margin-top 14, no 6. Con 6, en "Busca la luz y la sombra" la fila de
     botones ("Usar la cámara" / "Aquí es") quedaba pegada a la primera
     opción de la lista: el usuario lo vio en pantalla y lo reportó —"entre
     usar la cámara y el botón de la sombra de un armario no hay espacio...
     siempre debe haber espacios aunque sea mínimos". Se arregla aquí y no
     en lightcam porque esta lista la usan diez retos y en todos va después
     de algo. */
  .tap-choice-col{display:flex;flex-direction:column;gap:10px;margin-top:14px;}
  .tap-choice{border:none;background:var(--surface);border-radius:var(--radius-md);padding:14px 17px;font-weight:700;font-size:15px;cursor:pointer;text-align:left;color:var(--ink);box-shadow:0 0 0 1.5px var(--line), 0 3px 0 var(--line);transition:transform .1s ease;}
  .tap-choice:active{transform:translateY(3px);box-shadow:0 0 0 1.5px var(--line);}
  .tap-choice:hover{box-shadow:0 0 0 1.5px var(--accent-border), 0 3px 0 var(--line);}
  .tap-choice.right{background:color-mix(in srgb, var(--cat-sound) 18%, var(--surface));box-shadow:0 0 0 1.5px var(--cat-sound), 0 3px 0 var(--cat-sound);}
  .tap-choice.wrong{background:color-mix(in srgb, var(--cat-move) 15%, var(--surface));box-shadow:0 0 0 1.5px var(--cat-move), 0 3px 0 var(--cat-move);}

  /* ---------- Circuit node ---------- */
    /* Los retos de circuito con muchas piezas (r2b, r9, r13) se salían de
     la pantalla en su versión extra: hasta 77px por debajo de la barra.
     Ranuras y piezas más compactas, sin bajar de 60px, que sigue siendo
     holgado para el dedo de un niño. */
  .circuit-row{display:flex;align-items:center;justify-content:center;gap:10px;flex-wrap:wrap;margin:10px 0;}
  .circuit-slot{width:64px;height:64px;border-radius:16px;border:3px dashed var(--line);display:flex;align-items:center;justify-content:center;font-size:30px;background:var(--surface-2);}
  .circuit-slot.filled{border:none;background:var(--accent-soft);box-shadow:0 0 0 1.5px var(--accent-border), 0 3px 0 var(--accent-border);}
  .circuit-arrow{font-size:22px;color:var(--ink-soft);}
  .circuit-slot.glow{animation:glowpulse 1s ease infinite;}
  @keyframes glowpulse{0%,100%{box-shadow:0 0 0 0 rgba(28,159,215,.5);}50%{box-shadow:0 0 0 10px rgba(28,159,215,0);}}
  .tray{display:flex;gap:8px;justify-content:center;flex-wrap:wrap;margin-top:10px;}
  .tray-piece{width:60px;height:60px;border-radius:16px;border:none;background:var(--surface-2);font-size:28px;cursor:pointer;box-shadow:0 0 0 1.5px var(--line), 0 3px 0 rgba(0,0,0,.15);}
  .tray-piece:active{transform:translateY(2px);}

  /* El ícono dentro de la caja, no encima de ella.
   *
   * .lucide-xl mide 2.3em, y estas cajas traen font-size 30px/28px — o
   * sea 69px de ícono en una caja de 64 y 64 en una de 60. El número
   * venía de cuando las piezas eran EMOJIS: un emoji a 30px ocupa unos
   * 30px, pero un SVG a 2.3em ocupa 69. Al cambiar emojis por íconos, la
   * caja se quedó con la medida de antes y el dibujo se salió por los
   * cuatro lados.
   *
   * Se fija aquí y no bajando .lucide-xl porque esa clase la comparten
   * trece retos con cajas de tamaños distintos; tocarla arreglaría este
   * y descuadraría los otros doce. */
  .circuit-slot .lucide{width:34px;height:34px;}
  .tray-piece .lucide{width:32px;height:32px;}

  .tray-piece.used{opacity:.25;pointer-events:none;}

  /* ---------- Circuit simulator node (circuitsim) ---------- */
  /* .sim-loop es el "cable": un rectángulo con borde donde 5 puntitos
     (.sim-dot) recorren el perímetro en bucle — su animation-duration se
     ajusta en JS según la corriente calculada (I=V/R), así que más
     resistencia = flujo visiblemente más lento, no solo un número. */
  .sim-loop{position:relative;width:min(92%,320px);height:150px;margin:16px auto 10px;border:4px solid var(--line);border-radius:16px;background:var(--surface-2);}
  .sim-dot{position:absolute;width:9px;height:9px;margin:-4.5px;border-radius:50%;background:var(--accent);box-shadow:0 0 5px var(--accent);animation-name:simFlow;animation-timing-function:linear;animation-iteration-count:infinite;}
  @keyframes simFlow{
    0%{top:0%; left:0%;} 25%{top:0%; left:100%;} 50%{top:100%; left:100%;} 75%{top:100%; left:0%;} 100%{top:0%; left:0%;}
  }
  .sim-battery, .sim-led{position:absolute;width:40px;height:40px;border-radius:50%;background:var(--surface);box-shadow:0 0 0 3px var(--line);display:flex;align-items:center;justify-content:center;color:var(--ink-soft);}
  .sim-battery{top:50%;left:-4px;transform:translate(-50%,-50%);}
  .sim-led{top:50%;right:-4px;left:auto;transform:translate(50%,-50%);}
  .sim-led.lit{color:var(--gold);background:color-mix(in srgb, var(--gold) var(--glow, 35)%, var(--surface));box-shadow:0 0 0 3px var(--gold), 0 0 var(--glow-size,6px) var(--glow-size,6px) color-mix(in srgb, var(--gold) 70%, transparent);transition:background .25s ease, box-shadow .25s ease;}
  .sim-readout{text-align:center;font-weight:700;font-size:13.5px;color:var(--ink-soft);min-height:40px;margin-bottom:10px;font-variant-numeric:tabular-nums;}
  .sim-readout strong{color:var(--ink);}
  .sim-resistors{display:flex;gap:8px;justify-content:center;flex-wrap:wrap;}
  .resistor-chip{display:inline-flex;align-items:center;gap:5px;border:none;background:var(--surface-2);border-radius:12px;padding:9px 13px;font-weight:800;font-size:13px;cursor:pointer;box-shadow:0 0 0 1.5px var(--line), 0 3px 0 var(--line);}
  .resistor-chip:active{transform:translateY(2px);box-shadow:0 0 0 1.5px var(--line);}
  .resistor-chip.sel{background:var(--accent-soft);box-shadow:0 0 0 1.5px var(--accent-border), 0 3px 0 var(--accent-border);}

  /* ---------- Lab sim node (labsim) ---------- */
  /* Un solo emoji grande hace de "material" — .pop (mismo nombre de clase
     que .stars-big img.pop/.var-box.pop, cada uno con su propia animación
     — no es una keyframe compartida) le da un pequeño rebote cada vez que
     cambia de estado, para que el cambio se note aunque sea solo un
     carácter distinto. state-solid/liquid/gas cambian el fondo del
     recuadro nada más — no hace falta un sistema de partículas real para
     que "se sienta" sólido/líquido/gas. */
  .labsim-stage{width:min(100%,260px);height:150px;margin:16px auto 10px;border:4px solid var(--line);border-radius:16px;display:flex;align-items:center;justify-content:center;background:var(--surface-2);transition:background .3s ease;}
  .labsim-stage.state-solid{background:color-mix(in srgb, var(--world-robo) 18%, var(--surface-2));}
  .labsim-stage.state-liquid{background:color-mix(in srgb, var(--cat-move) 18%, var(--surface-2));}
  .labsim-stage.state-gas{background:color-mix(in srgb, var(--ink) 8%, var(--surface-2));}
  .labsim-emoji{font-size:64px;line-height:1;}
  .labsim-emoji.pop{animation:labsimPop .4s cubic-bezier(.4,1.6,.4,1);}
  @keyframes labsimPop{0%{transform:scale(.6);}60%{transform:scale(1.15);}100%{transform:scale(1);}}
  .labsim-readout{text-align:center;font-weight:700;font-size:13.5px;color:var(--ink-soft);min-height:40px;margin-bottom:10px;}
  .labsim-readout strong{color:var(--ink);}
  .labsim-actions{display:flex;gap:10px;justify-content:center;}

  /* ---------- Logic gate node (logicgate) ---------- */
  /* A propósito NO se parece al simulador de circuito: aquí no hay flujo
     animado — los interruptores son 0/1 y la salida reacciona al instante,
     para que se sienta como "lógica discreta" y no como "electricidad
     continua" (ver comentario al inicio de logicgate.js). */
  .gate-stage{display:flex;align-items:center;justify-content:center;gap:4px;margin:20px 0 8px;flex-wrap:wrap;}
  .gate-inputs{display:flex;flex-direction:column;gap:14px;}
  .gate-input-row{display:flex;align-items:center;}
  .gate-switch{width:50px;height:50px;border-radius:50%;font-size:20px;font-weight:900;border:none;background:var(--surface-2);color:var(--ink-soft);box-shadow:0 0 0 1.5px var(--line), 0 3px 0 var(--line);cursor:pointer;font-variant-numeric:tabular-nums;}
  .gate-switch:active{transform:translateY(2px);box-shadow:0 0 0 1.5px var(--line);}
  .gate-switch.on{background:var(--accent);color:#fff;box-shadow:0 0 0 1.5px var(--accent-border), 0 3px 0 var(--accent-border);}
  .gate-wire{width:24px;height:5px;border-radius:3px;background:var(--line);flex:none;}
  .gate-wire.on{background:var(--accent);box-shadow:0 0 6px var(--accent);}
  .gate-box{width:78px;height:78px;border-radius:20px;background:var(--surface-2);box-shadow:0 0 0 1.5px var(--line), 0 4px 0 var(--line);display:flex;align-items:center;justify-content:center;font-weight:900;font-size:14px;color:var(--ink);text-align:center;padding:4px;}
  .gate-output{width:58px;height:58px;border-radius:50%;background:var(--surface-2);box-shadow:0 0 0 1.5px var(--line), 0 3px 0 var(--line);display:flex;align-items:center;justify-content:center;color:var(--ink-soft);flex:none;}
  .gate-output.on{color:var(--gold);background:color-mix(in srgb, var(--gold) 30%, var(--surface));box-shadow:0 0 0 1.5px var(--gold), 0 0 16px 6px color-mix(in srgb, var(--gold) 55%, transparent);transition:background .2s ease, box-shadow .2s ease;}
  .gate-readout{text-align:center;font-weight:700;font-size:13.5px;color:var(--ink-soft);min-height:40px;}
  .gate-readout strong{color:var(--ink);}

  /* ---------- Showcase node ---------- */
  .showcase-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;}
  .showcase-card{background:var(--surface-2);border:2.5px solid var(--line);border-radius:var(--radius-md);padding:14px;text-align:center;}
  .showcase-card .ic{font-size:32px;margin-bottom:8px;}
  .showcase-card h4{font-size:13.5px;margin-bottom:5px;}
  .showcase-card p{font-size:12px;color:var(--ink-soft);margin:0;line-height:1.4;}

  /* ---------- Force node ---------- */
  .force-track{display:flex;gap:4px;justify-content:center;margin:22px 0;}
  .force-cell{width:44px;height:44px;border-radius:10px;background:var(--surface-2);display:flex;align-items:center;justify-content:center;font-size:20px;position:relative;}
  .force-cell.target{background:color-mix(in srgb, var(--cat-sound) 30%, var(--surface-2));}
  .force-btns{display:flex;justify-content:center;gap:14px;}
  .force-btn{width:64px;height:64px;border-radius:50%;border:none;background:var(--cat-move);color:var(--cat-move-ink);font-size:26px;cursor:pointer;box-shadow:0 0 0 1.5px rgba(0,0,0,.2), 0 3px 0 rgba(0,0,0,.25);}
  .force-btn:active{transform:translateY(2px);box-shadow:0 0 0 1.5px rgba(0,0,0,.2);}

  /* ---------- RuleSort node (Puente de las Reglas) ---------- */
  .rulesort-stage{display:flex;justify-content:center;margin:22px 0;}
  .rulesort-creature{display:flex;flex-direction:column;align-items:center;gap:4px;background:var(--surface-2);border-radius:var(--radius-md);padding:18px 26px;font-size:40px;transition:transform .3s, opacity .3s;}
  .rulesort-creature.rulesort-ok{transform:scale(1.15);opacity:.35;}
  .rulesort-trait{font-size:22px;}
  .rulesort-progress{display:flex;justify-content:center;gap:5px;margin-bottom:14px;}
  .rulesort-dot{width:9px;height:9px;border-radius:50%;background:var(--line);}
  .rulesort-dot.done{background:var(--cat-move);}
  .rulesort-bridges{display:flex;justify-content:center;gap:14px;}
  .rulesort-bridge{padding:12px 18px;}

  /* ---------- DataBelt node (Fábrica de Robi) ---------- */
  .databelt-rule{background:var(--surface-2);border-radius:var(--radius-md);padding:10px 14px;font-size:13px;margin:10px 0 6px;}
  .databelt-belt{display:flex;justify-content:center;margin:20px 0;}
  .databelt-package{background:var(--surface-2);border-radius:12px;padding:16px 28px;font-size:26px;font-weight:700;font-variant-numeric:tabular-nums;transition:transform .3s, opacity .3s;}
  .databelt-package.rulesort-ok{transform:scale(1.15);opacity:.35;}
  .databelt-bins{display:flex;justify-content:center;gap:14px;}
  .databelt-bin{padding:12px 18px;}

  /* ---------- BubbleSort node (Ordena la fila) ---------- */
  /* En pantalla ancha la fila va en horizontal, que es como se lee "una
     fila ordenada". */
  .bubblesort-row{display:flex;align-items:center;justify-content:center;gap:6px;margin:22px 0;flex-wrap:wrap;}
  .bubblesort-cell{display:flex;flex-direction:column;align-items:center;gap:2px;background:var(--surface-2);border-radius:10px;padding:10px 12px;min-width:44px;min-height:44px;transition:background .2s, border-color .2s;
    -webkit-appearance:none;appearance:none;font-family:inherit;color:inherit;cursor:pointer;border:2px solid transparent;}
  .bubblesort-cell.elegida{border-color:var(--accent);background:var(--accent-soft);}
  .bubblesort-ic{font-size:18px;}
  .bubblesort-val{font-size:14px;font-weight:700;font-variant-numeric:tabular-nums;color:var(--ink-soft);}
  /* EL ⇄ TIENE QUE PARECER UN BOTÓN.
     Era una flecha suelta, sin borde ni fondo: "esos iconos del medio que
     son para intercambiar, no parecen botones ni nada". El hijo del dueño
     no entendió que había que tocarlos, y él mismo tocó las piezas y no
     pasaba nada. Ahora tiene forma de botón —fondo, borde y relieve— y
     además tocar las piezas también funciona. */
  .bubblesort-swap{
    -webkit-appearance:none;appearance:none;cursor:pointer;
    display:flex;align-items:center;justify-content:center;
    background:var(--surface);color:var(--accent-text);
    border:1.5px solid var(--accent-border);border-radius:10px;
    padding:4px;box-shadow:0 2px 0 rgba(0,0,0,.14);
    transition:transform .12s ease, background .15s ease;
  }
  .bubblesort-swap:hover{background:var(--accent-soft);}
  .bubblesort-swap:active{transform:translateY(2px);box-shadow:none;}
  .bubblesort-swap .lucide{width:20px;height:20px;}
  .bubblesort-que-hacer{margin:0 0 6px;font-weight:700;font-size:14.5px;line-height:1.45;}
  .bubblesort-ej{
    display:inline-flex;align-items:center;justify-content:center;
    min-width:26px;padding:0 5px;border-radius:7px;
    background:var(--surface);border:1.5px solid var(--accent-border);color:var(--accent-text);
  }
  /* Dentro de la burbuja de Robi manda `.mascot-box svg{width:32px}`, que
     es el tamaño del propio Robi: el dibujito de ejemplo salía casi tan
     alto como la línea de texto. Aquí es una palabra más de la frase, no
     una ilustración. */
  .mascot-box .bubblesort-ej .lucide{width:17px;height:17px;}
  /* EN EL TELÉFONO, LA FILA SE PONE EN COLUMNA.
     En horizontal no caben cinco piezas más sus cuatro botones, así que
     la fila se partía en dos renglones y el último botón quedaba al final
     del primero con una sola pieza al lado: un botón que dice "cambia
     estas dos" señalando algo que no está ahí. Probé dejarla en una línea
     deslizable y salió peor (arrancaba por la mitad, con piezas
     cortadas), y encoger no vale porque el suelo de 44px de zona táctil
     manda. En columna cada botón queda entre sus dos piezas, se ve todo,
     y las zonas táctiles se respetan. Cuesta alto, que es lo barato aquí:
     la pantalla del reto se desplaza. */
  @media (max-width:430px){
    .bubblesort-row{flex-direction:column;flex-wrap:nowrap;gap:6px;}
    /* Cada pieza pasa a ser una pastilla ancha y baja: el dibujo y su
       número uno al lado del otro, para que la columna no se dispare de
       alto con los retos de siete piezas. */
    .bubblesort-cell{flex-direction:row;gap:10px;min-width:132px;padding:8px 16px;justify-content:center;}
    .bubblesort-ic{font-size:20px;}
    .bubblesort-val{font-size:15px;}
    /* Y las flechas apuntan a donde de verdad se van a mover las piezas.
       Un ⇄ entre dos piezas apiladas diría izquierda-derecha con el
       movimiento en vertical: el dibujo contradiciendo a lo que hace.
       Se giran las DOS —la del botón y la de la frase que lo explica—
       porque el niño busca en la pantalla el mismo dibujo que leyó. */
    .bubblesort-swap .lucide,
    .bubblesort-ej .lucide{transform:rotate(90deg);}
  }

  /* ---------- Algebra node (Caja mágica) ---------- */
  .algebra-eq{display:flex;align-items:center;justify-content:center;gap:10px;font-size:26px;font-weight:700;font-variant-numeric:tabular-nums;background:var(--surface-2);border-radius:var(--radius-md);padding:20px;margin:20px 0;transition:background .2s;}
  .algebra-const{color:var(--ink-soft);font-size:20px;}
  .algebra-eq-sign{color:var(--ink-soft);}
  .algebra-palette{display:flex;flex-wrap:wrap;justify-content:center;gap:10px;}
  .algebra-card{min-width:56px;padding:12px 14px;border-radius:12px;border:none;background:var(--cat-move);color:var(--cat-move-ink);font-size:17px;font-weight:700;cursor:pointer;box-shadow:0 3px 0 rgba(0,0,0,.25);}
  .algebra-card:active{transform:translateY(2px);box-shadow:none;}

  /* ---------- NumWheel node (Rueda de llaves) ---------- */
  .numwheel-ring{position:relative;width:220px;height:220px;margin:14px auto;border-radius:50%;background:var(--surface-2);}
  .numwheel-cell{position:absolute;width:32px;height:32px;margin-left:-16px;margin-top:-16px;border-radius:50%;background:var(--surface);border:2px solid var(--line);display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:700;font-variant-numeric:tabular-nums;}
  .numwheel-cell.target{border-color:var(--cat-sound);box-shadow:0 0 0 3px color-mix(in srgb, var(--cat-sound) 40%, transparent);}
  .numwheel-cell.current{background:var(--cat-move);color:var(--cat-move-ink);}
  .numwheel-keys{display:flex;flex-wrap:wrap;justify-content:center;gap:10px;}
  .numwheel-key{min-width:52px;padding:10px 12px;border-radius:12px;border:none;background:var(--cat-move);color:var(--cat-move-ink);font-size:15px;font-weight:700;cursor:pointer;box-shadow:0 3px 0 rgba(0,0,0,.25);}
  .numwheel-key:active{transform:translateY(2px);box-shadow:none;}

  /* ---------- Clockset node (Pon la hora) — lote medida 2026-08-18 ---------- */
  .clockset-target{font-size:16px;margin-bottom:10px;color:var(--ink-soft);}
  .clockset-face{position:relative;width:200px;height:200px;margin:8px auto;border-radius:50%;background:var(--surface-2);border:4px solid var(--line);transition:border-color .25s ease;}
  .clockset-face.right{border-color:var(--cat-sound);}
  .clockset-face.wrong{animation:clockShake .35s ease;}
  @keyframes clockShake{0%,100%{transform:translateX(0);}25%{transform:translateX(-6px);}75%{transform:translateX(6px);}}
  .clockset-num{position:absolute;transform:translate(-50%,-50%);font-size:14px;font-weight:700;font-variant-numeric:tabular-nums;color:var(--ink);}
  /* Las manecillas rotan desde su base (bottom center) — transform-origin
     ancla el giro al centro del reloj. */
  .clockset-hand{position:absolute;left:50%;bottom:50%;transform-origin:bottom center;border-radius:4px;background:var(--ink);}
  .clockset-hand.hour{width:7px;height:48px;}
  .clockset-hand.minute{width:4px;height:68px;background:var(--cat-move);}
  .clockset-pin{position:absolute;left:50%;top:50%;width:14px;height:14px;margin:-7px 0 0 -7px;border-radius:50%;background:var(--ink);}
  .clockset-controls{display:flex;justify-content:center;gap:12px;margin:12px 0 4px;}
  .clockset-btn{padding:10px 16px;border-radius:12px;border:none;background:var(--cat-move);color:var(--cat-move-ink);font-size:15px;font-weight:700;cursor:pointer;box-shadow:0 3px 0 rgba(0,0,0,.25);}
  .clockset-btn:active{transform:translateY(2px);box-shadow:none;}
  .clockset-reading{font-size:15px;font-weight:700;color:var(--ink-soft);font-variant-numeric:tabular-nums;}

  /* ---------- Shopcoin node (La tienda de Robi) ---------- */
  .shopcoin-item{display:flex;align-items:center;justify-content:center;gap:10px;margin-bottom:10px;}
  .shopcoin-item-ic{font-size:40px;}
  .shopcoin-item-lbl{font-size:16px;color:var(--ink-soft);}
  .shopcoin-bank{display:flex;flex-wrap:wrap;justify-content:center;gap:8px;margin-bottom:12px;}
  .shopcoin-slot{border:none;background:none;padding:0;cursor:pointer;}
  .shopcoin-slot:active .shopcoin-coin{transform:scale(.92);}
  .shopcoin-coin{display:flex;align-items:center;justify-content:center;width:52px;height:52px;border-radius:50%;background:#D9A93F;color:#4A3208;border:3px solid #B78A26;font-size:13px;font-weight:800;font-variant-numeric:tabular-nums;transition:transform .1s ease;}
  .shopcoin-coin.bill{width:74px;height:44px;border-radius:8px;background:#69A96E;color:#12300F;border-color:#4C8A52;}
  .shopcoin-tray-wrap{background:var(--surface-2);border:2px dashed var(--line);border-radius:14px;padding:10px;margin:0 auto;max-width:340px;}
  .shopcoin-tray-lbl{display:block;font-size:12.5px;color:var(--ink-faint,var(--ink-soft));margin-bottom:6px;}
  .shopcoin-tray{display:flex;flex-wrap:wrap;justify-content:center;gap:6px;min-height:52px;align-items:center;}
  .shopcoin-total{font-size:16px;font-weight:800;margin-top:8px;font-variant-numeric:tabular-nums;}
  .shopcoin-empty{font-size:13px;color:var(--ink-soft);opacity:.7;}

  /* ---------- Measure node (Mide con la regla) ---------- */
  .measure-stage{margin:10px auto;display:inline-block;text-align:left;}
  .measure-object{height:44px;border-radius:10px;background:color-mix(in srgb, var(--cat-move) 22%, var(--surface-2));border:2px solid var(--cat-move);display:flex;align-items:center;justify-content:center;overflow:hidden;}
  .measure-object-ic{font-size:26px;}
  .measure-object-lbl{font-size:13px;color:var(--ink-soft);margin:4px 0 8px;}
  .measure-row{display:flex;gap:0;min-height:38px;align-items:center;border-left:2px dashed var(--line);}
  .measure-row.right{outline:3px solid var(--cat-sound);border-radius:6px;}
  .measure-cube{display:inline-flex;width:34px;height:34px;align-items:center;justify-content:center;font-size:26px;line-height:1;}
  .measure-controls{display:flex;justify-content:center;gap:12px;margin:12px 0 4px;}
  .measure-count{font-size:15px;font-weight:700;color:var(--ink-soft);font-variant-numeric:tabular-nums;}

  /* ---------- Pictogram node (Cuenta y grafica) — lote datos 2026-08-18 ---------- */
  .pictogram-scene{display:flex;flex-wrap:wrap;justify-content:center;gap:4px;background:var(--surface-2);border:2px solid var(--line);border-radius:14px;padding:10px;max-width:340px;margin:0 auto 12px;}
  .pictogram-item{font-size:24px;line-height:1.2;}
  /* Con cuatro categorías, 18px de separación y columnas de 44 el
     pictograma medía 403px en una ventana de 375: el niño tenía que
     desplazar de lado un gráfico que debería verse entero. La separación
     cede antes que las columnas —los botones + y − no pueden encoger, son
     zona tocable— y si aun así no cabe, se envuelve. */
  .pictogram-chart{display:flex;flex-wrap:wrap;justify-content:center;align-items:flex-end;gap:10px clamp(6px, 3vw, 18px);margin:0 auto;max-width:100%;}
  .pictogram-chart.right{outline:3px solid var(--cat-sound);border-radius:12px;padding:4px;}
  .pictogram-col{display:flex;flex-direction:column;align-items:center;gap:4px;}
  .pictogram-stack{display:flex;flex-direction:column-reverse;gap:2px;min-height:30px;justify-content:flex-start;}
  .pictogram-cell{font-size:22px;line-height:1.1;}
  .pictogram-count{font-size:15px;font-weight:800;font-variant-numeric:tabular-nums;}
  .pictogram-btns{display:flex;gap:6px;}
  .pictogram-btn{width:44px;height:44px;border-radius:12px;border:none;background:var(--cat-move);color:var(--cat-move-ink);font-size:18px;font-weight:800;cursor:pointer;box-shadow:0 3px 0 rgba(0,0,0,.25);}
  .pictogram-btn:active{transform:translateY(2px);box-shadow:none;}
  .pictogram-lbl{font-size:12.5px;color:var(--ink-soft);max-width:76px;}

  /* ---------- Barra de arriba: más estrecha, con el mundo ---------- */
  /* Los chips de racha y XP se apretaron para hacerle sitio al mundo. El
     dueño lo pidió así: "estrechar un poco el icono de racha y el de XP,
     para poder al lado el selector de mundos... todo bien bonito nada
     amontonado". Se recorta el relleno y el hueco, no el tamaño de la
     letra: encoger el número es lo primero que deja de leerse de lejos, y
     esos dos números son justo lo que el niño mira. */
  .topbar .stat-chip{padding:5px 9px;gap:5px;}
  .topbar .stat-chip .lucide{width:15px;height:15px;}

  .world-pick{display:inline-flex;align-items:center;gap:7px;padding:5px 10px;
    border-radius:999px;border:1.5px solid var(--world-color, var(--line));
    background:var(--card);cursor:pointer;max-width:190px;
    font-size:13.5px;font-weight:700;color:var(--ink);}
  /* Aquí el ícono va DEL color del mundo sobre el fondo de la tarjeta, no
     al revés: no hay texto blanco sobre color, así que no aplica el mismo
     problema. El borde del botón lleva el color y eso es lo que identifica
     el mundo de un vistazo. */
  .world-pick-ic{display:inline-flex;width:20px;height:20px;align-items:center;justify-content:center;
    color:var(--world-color, var(--accent));}
  .world-pick-ic svg, .world-pick-ic img{width:18px;height:18px;}
  .world-pick-txt{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
  .world-pick-caret{width:14px;height:14px;flex:0 0 14px;color:var(--ink-soft);}

  /* La hoja para elegir mundo: sube desde abajo, donde alcanza el pulgar. */
  .mundo-hoja{max-width:420px;}
  .mundo-lista{display:flex;flex-direction:column;gap:8px;}
  .mundo-fila{display:flex;align-items:center;gap:12px;padding:12px 14px;border-radius:14px;
    border:1.5px solid var(--line);background:var(--card);cursor:pointer;text-align:left;
    font-size:15px;font-weight:700;color:var(--ink);}
  .mundo-fila.sel{border-color:var(--accent);background:var(--accent-soft);}
  .mundo-fila.bloqueado{opacity:.55;cursor:default;}
  /* El mismo velo del 40% que ya lleva el banner del mundo, y por el
     mismo motivo: el ícono va en blanco, y sobre el color puro el
     contraste queda entre 2,11 y 3,73 — por debajo del 3,0 que pide WCAG
     incluso para un gráfico que no es texto. Con el velo sube a más de 5
     sin tocar la paleta, que es lo que hay que conservar: los colores de
     mundo son la señal con la que el niño sabe dónde está.
     Medido con los cinco colores, no supuesto. */
  .mundo-fila-ic{display:inline-flex;width:32px;height:32px;align-items:center;justify-content:center;
    border-radius:50%;background:var(--world-color);color:#fff;flex:0 0 32px;
    box-shadow:inset 0 0 0 999px rgba(0,0,0,.4);}
  .mundo-fila-ic svg, .mundo-fila-ic img{width:20px;height:20px;}
  .mundo-fila-txt{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px;}
  .mundo-fila-reco{display:inline-flex;align-items:center;gap:4px;font-size:11.5px;font-weight:700;
    color:var(--accent-text, var(--accent));}
  .mundo-fila-reco .lucide{width:12px;height:12px;}

  /* El perfil, abajo a la derecha de Logros. */
  .bottom-nav-perfil .bn-av{width:22px;height:22px;border-radius:50%;overflow:hidden;
    display:inline-flex;align-items:center;justify-content:center;}
  .bottom-nav-perfil .bn-av img{width:18px;height:18px;}
  .bottom-nav-perfil span:last-child{max-width:72px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}

  /* El mundo elegido puede estar bloqueado en una institución que abre por
     etapas. Se dice en una línea, no en un banner: el nombre del mundo ya
     está arriba, y lo único que falta aquí es quién tiene la llave. */
  .mundo-bloqueado{display:flex;align-items:center;justify-content:center;gap:6px;
    max-width:520px;margin:0 auto 14px;padding:10px 14px;border-radius:12px;
    background:var(--card);border:1px dashed var(--line);
    color:var(--ink-soft);font-size:13px;font-weight:700;}
  .mundo-bloqueado .lucide{width:15px;height:15px;}

  /* ---------- Título de maestro ---------- */
  .maestria-caja{display:flex;align-items:center;gap:12px;max-width:520px;margin:0 auto 14px;
    padding:12px 16px;border-radius:16px;border:1px dashed var(--line);background:var(--card);}
  .maestria-caja.vigente{border-style:solid;border-color:var(--accent);background:var(--accent-soft);}
  .maestria-medalla{font-size:28px;line-height:1;}
  .maestria-texto{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px;}
  .maestria-texto strong{font-size:14.5px;}
  .maestria-texto small{font-size:12px;color:var(--ink-soft);}
  .maestria-btn{flex:0 0 auto;}

  /* "de hoy" en la tarjeta de la lección: más pequeño y en minúsculas
     para que el número siga mandando, pero visible. */
  .leccion-hoy-marca{font-size:.72em;font-weight:800;text-transform:uppercase;
    letter-spacing:.06em;opacity:.85;margin-left:2px;}

  /* ---------- Temas del mapa (círculos al estilo Duolingo) ---------- */
  /* El anillo son las lecciones del tema, repartidas alrededor del
     círculo: pintadas las que ya hizo. Se dibuja con elementos rotados y
     no con un SVG por tema porque el número de lecciones cambia por tema
     y por mundo, y generar un arco por cada uno costaría más de lo que
     vale — aquí basta con puntos alrededor. */
  /* Los círculos llevan el color de SU mundo, con algo de relieve.
   *
   * El dueño los vio y dijo lo que eran: "se ven muy serios, como que le
   * falta algo; primero el color de su respectivo mundo pero también algo
   * de brillo, algo que no se vea tan simple, bonito y moderno, no
   * simplón". Tenía razón: dieciocho tarjetas grises con un aro azul no
   * se distinguen entre sí ni dicen en qué mundo estás.
   *
   * Todo el color sale de --world-color, que pone el mapa. Con
   * color-mix se saca de ahí el borde, el fondo y el resplandor, así que
   * los cinco mundos quedan coherentes sin escribir cinco paletas — y si
   * mañana cambia un color de mundo, cambia solo aquí también. */
  .tema-mapa{
    position:relative;
    border:1px solid color-mix(in srgb, var(--world-color, var(--accent)) 28%, var(--line));
    border-radius:20px;
    background:
      radial-gradient(120% 100% at 0% 0%, color-mix(in srgb, var(--world-color, var(--accent)) 12%, transparent), transparent 60%),
      var(--card);
    box-shadow:0 1px 2px rgba(0,0,0,.12);
    margin:0 auto 12px;max-width:520px;overflow:hidden;
    transition:box-shadow .18s ease, border-color .18s ease, transform .12s ease;}
  .tema-mapa:active{transform:scale(.995);}
  /* Una línea de luz arriba, como la que deja un plástico brillante. Es
     lo que quita el aspecto de caja plana sin añadir ningún dibujo. */
  .tema-mapa::before{content:'';position:absolute;inset:0 0 auto 0;height:1px;
    background:linear-gradient(90deg, transparent, color-mix(in srgb, var(--world-color, var(--accent)) 55%, transparent), transparent);
    pointer-events:none;}
  .tema-mapa[open]{border-color:color-mix(in srgb, var(--world-color, var(--accent)) 55%, var(--line));}
  /* TERMINADO: EL COLOR DEL MUNDO, EN VOZ BAJA.
   *
   * Aquí hubo primero un VERDE de "hecho", y estaba mal por dos razones.
   * La de fondo la dijo el dueño —"no es el color del mundo"— y la
   * objetiva salió al medirla: el verde de completado, rgb(95,208,138),
   * es casi el color del mundo Ciencia, rgb(63,201,138). En Ciencia,
   * terminado y en curso habrían salido del mismo color y la distinción
   * desaparecía.
   *
   * Así que los tres estados se separan SIN colores ajenos:
   *   · terminado  — el color del mundo bajado de tono, sin resplandor,
   *                  y el anillo cerrado en una sola pieza;
   *   · en curso   — el color del mundo entero, barritas separadas que
   *                  cuentan lo que falta, y el resplandor;
   *   · sin abrir  — gris y con candado.
   * El color no va solo: el terminado lleva su visto y el bloqueado su
   * candado, para quien no distinga los tonos. */
  .tema-mapa.completo{
    border-color:color-mix(in srgb, var(--world-color, var(--accent)) 45%, var(--apagar-hacia));
    box-shadow:0 1px 2px rgba(0,0,0,.12);
  }
  /* El ícono y el anillo del terminado se pintan más abajo, junto a los
     del tema en curso: los dos estados comparten esas piezas y tenerlos
     en un solo sitio es lo que evita que se separen. */
  /* El tema en curso ya no se abre solo, así que tiene que distinguirse
     CERRADO: más color y un resplandor suave. Sin esto, el niño vuelve al
     mapa y ve dieciocho círculos iguales. */
  /* Cerrado: apagado pero no invisible. Tiene que verse que está ahí y
     que llegará — es parte de lo que motiva a seguir. */
  /* De pago es otra cosa que "todavía no llegaste": el mundo entero está
     ahí y se puede mirar, solo que lo abre un adulto. Se distingue del
     candado de avance para que el niño no crea que le falta jugar más. */
  /* La tarjeta del día, cuando su lección es de pago: se apaga y lleva su
     candado. Sigue tocándose —de ahí sale el aviso de compra— pero ya no
     se anuncia como algo que se puede jugar. */
  .leccion-hoy.de-pago{position:relative;filter:saturate(.45);}
  .leccion-hoy-candado{position:absolute;top:10px;right:12px;display:flex;align-items:center;justify-content:center;
    width:26px;height:26px;border-radius:50%;background:var(--accent);color:#fff;}
  .leccion-hoy-candado .lucide{width:13px;height:13px;}
  /* La salida de "ahora no puedo escuchar": discreta y al final. Es una
     salida, no una alternativa que compita con el reto. */
  .sin-oido{display:flex;flex-direction:column;align-items:center;gap:6px;margin-top:16px;}
  .sin-oido small{color:var(--ink-soft);font-size:12.5px;text-align:center;max-width:34ch;line-height:1.35;}
  .tema-mapa.de-pago{opacity:.72;}
  .tema-mapa.de-pago .tema-candado{background:var(--accent);color:#fff;}
  .mundo-fila.de-pago{opacity:.8;}
  .mundo-fila-candado{display:flex;align-items:center;gap:4px;font-size:11.5px;font-weight:700;color:var(--ink-soft);margin-top:2px;}
  .mundo-fila-candado .lucide{width:12px;height:12px;}
  .tema-mapa.bloqueado{opacity:.62;}
  /* El ícono del tema se conserva, solo pierde saturación: sigue
     diciendo de qué va el tema, que es su trabajo. */
  .tema-mapa.bloqueado .tema-icono{
    filter:saturate(.35);
    box-shadow:inset 0 0 0 999px rgba(0,0,0,.5), 0 0 0 2px var(--line);}
  /* El candado, pequeño y en la esquina del círculo. Encima del anillo,
     con el borde del color del fondo para que se recorte limpio. */
  .tema-candado{
    position:absolute;top:-2px;left:-2px;
    width:22px;height:22px;border-radius:50%;
    display:flex;align-items:center;justify-content:center;
    background:var(--surface-2, var(--card));
    border:2px solid var(--card);
    color:var(--ink-soft);
    box-shadow:0 1px 3px rgba(0,0,0,.25);}
  .tema-candado .lucide{width:11px;height:11px;}
  .tema-mapa.bloqueado .tema-barra{stroke:var(--line);}
  .tema-mapa.bloqueado .tema-jugar{cursor:default;}
  .tema-mapa.empezado{
    border-color:var(--world-color, var(--accent));
    box-shadow:0 0 0 1px color-mix(in srgb, var(--world-color, var(--accent)) 35%, transparent),
               0 6px 20px color-mix(in srgb, var(--world-color, var(--accent)) 22%, transparent);}
  .tema-cabeza{display:flex;align-items:center;gap:10px;padding:14px 16px;
    cursor:pointer;list-style:none;user-select:none;}
  .tema-cabeza::-webkit-details-marker{display:none;}
  .tema-jugar{flex:1;min-width:0;display:flex;align-items:center;gap:14px;
    background:none;border:0;padding:0;margin:0;font:inherit;color:inherit;
    cursor:pointer;text-align:left;}
  .tema-jugar:active{transform:scale(.985);}
  .tema-abrir{list-style:none;flex:0 0 auto;width:44px;height:44px;border-radius:50%;
    display:flex;align-items:center;justify-content:center;cursor:pointer;
    border:1.5px solid color-mix(in srgb, var(--world-color, var(--accent)) 30%, var(--line));
    background:var(--card);color:var(--ink-soft);
    transition:transform .18s ease, border-color .12s ease, color .12s ease, background .12s ease;}
  .tema-abrir:hover{border-color:var(--world-color, var(--accent));
    color:var(--world-color, var(--accent));
    background:color-mix(in srgb, var(--world-color, var(--accent)) 10%, var(--card));}
  .tema-abrir::-webkit-details-marker{display:none;}
  .tema-abrir .lucide{width:18px;height:18px;}
  .tema-circulo{position:relative;width:62px;height:62px;flex:0 0 62px;
    display:flex;align-items:center;justify-content:center;}
  /* El anillo: un arco por lección, con un hueco entre ellos. Pintado el
     que ya hizo, en el color del mundo. */
  .tema-anillo{position:absolute;inset:0;width:62px;height:62px;}
  /* Los arcos que faltan se tiñen del color del mundo, muy bajos: en gris
     azulado sobre un círculo coral se veían de otra familia, como si
     fueran de otra tarjeta. */
  .tema-barra{fill:none;stroke:color-mix(in srgb, var(--world-color, var(--accent)) 30%, var(--line));
    stroke-width:4;stroke-linecap:round;}
  .tema-barra.hecha{stroke:var(--world-color, var(--accent));}
  /* El ícono va en blanco sobre el color del mundo, así que lleva el mismo
     velo del 40% que el banner: sin él, el contraste queda entre 2,11 y
     3,73 y no se lee. Medido, no supuesto. */
  /* El velo del 40% se queda: es lo que hace legible el ícono blanco, y
     está medido con los cinco colores (verificar-contraste-de-mundos).
     Lo que se aclara es lo de FUERA — un aro del color puro y un
     resplandor— que da vida sin tocar el área donde va el dibujo. En tema
     claro, tres de los cinco colores no llegan ni al mínimo de un gráfico
     puestos como ícono, así que dejarlos "más vivos" por dentro no era
     una opción: sería bonito y no se vería. */
  .tema-icono{width:44px;height:44px;border-radius:50%;
    background:var(--world-color, var(--accent));color:#fff;
    box-shadow:inset 0 0 0 999px rgba(0,0,0,.4),
               0 0 0 2px color-mix(in srgb, var(--world-color, var(--accent)) 60%, transparent),
               0 3px 10px color-mix(in srgb, var(--world-color, var(--accent)) 30%, transparent);
    display:flex;align-items:center;justify-content:center;}
  /* EL TEMA EN CURSO: el color del mundo, entero, con su aro y su
     resplandor. Es el único que los lleva. */
  .tema-mapa.empezado .tema-icono{
    box-shadow:inset 0 0 0 999px rgba(0,0,0,.4),
               0 0 0 3px color-mix(in srgb, var(--world-color, var(--accent)) 85%, transparent),
               0 5px 18px color-mix(in srgb, var(--world-color, var(--accent)) 50%, transparent);}
  /* EL TEMA TERMINADO: todo el círculo baja de tono, y sin resplandor.
     Compartía estas dos reglas con el tema en curso, y el resultado era
     que los dos salían idénticos —mismo aro, mismo anillo, mismo color—
     mientras el único rasgo que los separaba era la saturación del borde
     exterior: medido, rgb(194,99,97) contra rgb(255,107,91). En un
     teléfono eso es el mismo círculo dos veces.
     Cuánto se apaga tiene un límite medido: con el aro al 45% queda a
     distancia 1,93 de lo bloqueado y 2,04 de lo que está en curso — o sea
     en medio, que es donde el dueño lo quiso. Por debajo de eso empieza a
     leerse como un tema cerrado con candado, que es otra cosa. */
  .tema-mapa.completo .tema-icono{
    background:color-mix(in srgb, var(--world-color, var(--accent)) 65%, var(--apagar-hacia));
    box-shadow:inset 0 0 0 999px rgba(0,0,0,.38),
               0 0 0 2px color-mix(in srgb, var(--world-color, var(--accent)) 45%, var(--apagar-hacia));}
  /* El aro del terminado es UNA circunferencia entera, dibujada así en
     home.js: aquí solo se le da color y grosor. Se engorda un poco
     respecto a las barritas porque ya no cuenta nada — es una sola pieza
     cerrada, y una pieza cerrada se lee mejor con cuerpo. */
  .tema-mapa.completo .tema-barra{
    stroke:color-mix(in srgb, var(--world-color, var(--accent)) 70%, var(--apagar-hacia));
    stroke-width:6;}
  .tema-texto{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px;text-align:left;}
  .tema-texto strong{font-size:15px;line-height:1.25;}
  .tema-texto small{font-size:12px;color:var(--ink-soft);}
  /* Abierto, el "+" se convierte en "×" girándolo: la misma pieza dice
     "abre" y "cierra" sin cambiar de dibujo. */
  .tema-mapa[open] .tema-abrir{transform:rotate(45deg);border-color:var(--accent);}
  /* Los retos de dentro son una LISTA, no un mapa.
     Heredaban el tamaño del mapa antiguo —círculos de 130 px pensados
     para verse desde lejos en una fila larga— y dentro de un tema abierto
     ocupaban una pantalla entera por reto. Aquí el niño ya sabe dónde
     está: lo que necesita es repasar de un vistazo qué hay dentro. */
  .tema-mapa > .node-wrap{margin:0 auto 8px;transform:none;}
  .tema-mapa > .node-wrap:first-of-type{margin-top:6px;}
  .tema-mapa > .node-wrap:last-child{margin-bottom:16px;}
  .tema-mapa > .node-wrap .node-btn{width:52px;height:52px;min-width:52px;min-height:52px;}
  .tema-mapa > .node-wrap .node-btn .lucide-lg{width:26px;height:26px;}
  .tema-mapa > .node-wrap .node-label{font-size:13px;max-width:230px;}
  .tema-mapa > .node-wrap .node-stars img{width:14px;height:14px;}

  /* ---------- Bitlamp node (Enciende el número) ---------- */
  /* Los focos van en fila y de mayor a menor, como se escribe un número.
     El que está encendido se distingue por color Y por el dibujo, no solo
     por color: un niño que no distingue bien los tonos tiene que poder
     jugar igual. */
  .bitlamp-pedido{font-size:17px;margin-bottom:12px;color:var(--ink-soft);}
  .bitlamp-focos{display:flex;justify-content:center;gap:10px;flex-wrap:wrap;}
  .bitlamp-foco{display:flex;flex-direction:column;align-items:center;gap:4px;
    padding:12px 14px;border-radius:14px;border:2px solid var(--line);
    background:var(--card);cursor:pointer;min-width:74px;
    transition:transform .12s ease, border-color .12s ease, background .12s ease;}
  .bitlamp-foco:hover{transform:translateY(-2px);}
  .bitlamp-foco.encendido{border-color:var(--accent);background:var(--accent-soft);}
  .bitlamp-bombilla{font-size:26px;line-height:1;}
  .bitlamp-valor{font-size:12px;font-weight:700;color:var(--ink-soft);font-variant-numeric:tabular-nums;}
  .bitlamp-lectura{margin-top:12px;font-size:15px;color:var(--ink-soft);letter-spacing:.14em;}
  .bitlamp-total{font-size:16px;font-weight:800;margin-top:6px;font-variant-numeric:tabular-nums;}

  /* ---------- Gate node (Enciende el foco) ---------- */
  .gate-pedido{font-size:16px;margin-bottom:12px;color:var(--ink-soft);}
  .gate-circuito{display:flex;flex-direction:column;align-items:center;gap:12px;}
  .gate-palancas{display:flex;justify-content:center;gap:12px;flex-wrap:wrap;}
  .gate-palanca{display:flex;flex-direction:column;align-items:center;gap:2px;
    padding:10px 16px;border-radius:12px;border:2px solid var(--line);
    background:var(--card);cursor:pointer;min-width:70px;
    transition:border-color .12s ease, background .12s ease;}
  .gate-palanca.arriba{border-color:var(--accent);background:var(--accent-soft);}
  .gate-palanca-nombre{font-size:19px;font-weight:800;}
  .gate-palanca-estado{font-size:11px;color:var(--ink-soft);}
  .gate-puertas{display:flex;justify-content:center;gap:10px;flex-wrap:wrap;}
  .gate-puerta{display:flex;flex-direction:column;align-items:center;gap:2px;
    padding:10px 14px;border-radius:12px;border:2px dashed var(--line);
    background:var(--card);max-width:210px;}
  .gate-puerta.pasa{border-style:solid;border-color:var(--accent);}
  .gate-puerta-nombre{font-size:17px;font-weight:800;letter-spacing:.04em;}
  .gate-puerta-de{font-size:11.5px;color:var(--ink-soft);font-weight:700;}
  .gate-foco{display:flex;align-items:center;justify-content:center;gap:8px;margin-top:14px;}
  .gate-bombilla{font-size:34px;line-height:1;}
  .gate-foco-txt{font-size:15px;font-weight:700;color:var(--ink-soft);}

  /* ---------- Numbuild node (Arma el número) ---------- */
  .numbuild-target{font-size:17px;margin-bottom:10px;color:var(--ink-soft);}
  .numbuild-cols{display:flex;justify-content:center;align-items:flex-end;gap:22px;}
  .numbuild-col{display:flex;flex-direction:column;align-items:center;gap:4px;}
  .numbuild-lbl{font-size:12.5px;font-weight:700;color:var(--ink-soft);}
  .note-mini{font-weight:400;font-size:11px;color:var(--ink-soft);opacity:.8;}
  .numbuild-stack{display:flex;flex-direction:column-reverse;gap:2px;min-height:30px;}
  .numbuild-block{font-size:20px;line-height:1.1;}
  .numbuild-total{font-size:16px;font-weight:800;margin-top:10px;font-variant-numeric:tabular-nums;}

  /* ---------- Fractionbar node (Reparte el chocolate) ---------- */
  .fractionbar-ask{display:flex;align-items:center;justify-content:center;gap:10px;font-size:16px;color:var(--ink-soft);margin-bottom:12px;}
  .fractionbar-frac{display:inline-flex;flex-direction:column;align-items:center;font-size:20px;font-weight:800;color:var(--ink);line-height:1.1;font-variant-numeric:tabular-nums;}
  .fractionbar-line{display:block;width:26px;height:3px;background:var(--ink);border-radius:2px;margin:2px 0;}
  .fractionbar-bar{display:flex;justify-content:center;gap:3px;margin:0 auto;}
  .fractionbar-bar.right{outline:3px solid var(--cat-sound);border-radius:10px;padding:3px;}
  .fractionbar-piece{width:44px;height:56px;border-radius:8px;border:2px solid var(--line);background:var(--surface-2);font-size:24px;cursor:pointer;opacity:.35;transition:opacity .15s ease, transform .1s ease;}
  .fractionbar-piece.shaded{opacity:1;background:color-mix(in srgb, var(--cat-move) 22%, var(--surface-2));border-color:var(--cat-move);}
  .fractionbar-piece:active{transform:scale(.94);}
  .fractionbar-reading{font-size:14px;color:var(--ink-soft);margin-top:8px;font-variant-numeric:tabular-nums;}

  /* ---------- Estimate node (Al ojo) ---------- */
  .estimate-stage{position:relative;height:180px;max-width:360px;margin:0 auto 10px;background:var(--surface-2);border:2px solid var(--line);border-radius:14px;overflow:hidden;}
  .estimate-item{position:absolute;transform:translate(-50%,-50%);font-size:26px;}
  .estimate-stage.covered{display:flex;align-items:center;justify-content:center;}
  .estimate-cover{font-size:17px;font-weight:700;color:var(--ink-soft);}
  .estimate-actions{margin-bottom:10px;}

  /* ---------- Bodymap node (Mapa del cuerpo) — lote cuerpo humano 2026-08-18 ---------- */
  .bodymap-wrap{display:flex;justify-content:center;gap:18px;flex-wrap:wrap;align-items:flex-start;}
  .bodymap-figure{display:flex;flex-direction:column;align-items:center;gap:4px;}
  .bodymap-zone{position:relative;border:2px dashed var(--line);background:var(--surface-2);cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;padding:6px;transition:border-color .2s ease;}
  /* La silueta se dibuja con los anchos/alto de cada zona apilada —
     cabeza chica y redonda, pecho y barriga anchos, piernas largas. */
  .bodymap-zone.zone-cabeza{width:76px;height:76px;border-radius:50%;}
  .bodymap-zone.zone-pecho{width:130px;height:74px;border-radius:16px 16px 6px 6px;}
  .bodymap-zone.zone-barriga{width:120px;height:66px;border-radius:6px 6px 14px 14px;}
  .bodymap-zone.zone-piernas{width:96px;height:88px;border-radius:8px 8px 16px 16px;}
  .bodymap-zone-lbl{font-size:11px;color:var(--ink-soft);}
  .bodymap-zone-parts{display:flex;gap:2px;flex-wrap:wrap;justify-content:center;}
  .bodymap-placed{font-size:20px;}
  /* Los íconos anatómicos tienen que medir lo mismo que los emojis que
     conviven con ellos: si un cerebro dibujado sale a la mitad del tamaño
     de unos pulmones en emoji, la bandeja se ve descuadrada. */
  .bodymap-placed .bodymap-svg{width:20px;height:20px;vertical-align:-3px;color:var(--accent-hi);}
  .bodymap-zone.right-flash{border-color:var(--cat-sound);border-style:solid;}
  .bodymap-zone.wrong-flash{border-color:#C0392B;border-style:solid;animation:clockShake .35s ease;}
  .bodymap-tray{display:flex;flex-direction:column;gap:8px;max-width:150px;}
  .bodymap-part{display:flex;align-items:center;gap:8px;border:2px solid var(--line);border-radius:12px;background:var(--surface);padding:6px 10px;cursor:pointer;}
  .bodymap-part.selected{border-color:var(--cat-move);box-shadow:0 0 0 3px color-mix(in srgb, var(--cat-move) 35%, transparent);}
  .bodymap-part-ic{font-size:22px;}
  .bodymap-part-ic .bodymap-svg{width:22px;height:22px;vertical-align:-3px;color:var(--accent);}
  /* Los de Healthicons son formas RELLENAS, no trazos: sin esto salen
     invisibles, porque .lucide fuerza fill:none y stroke. */
  .icono-salud{fill:currentColor;stroke:none;}
  .bodymap-part-lbl{font-size:13px;font-weight:700;color:var(--ink);}

  /* ---------- Plate node (Plato saludable) ---------- */
  .plate-needs{display:flex;flex-wrap:wrap;justify-content:center;gap:8px;margin-bottom:10px;}
  .plate-need{font-size:12.5px;font-weight:700;padding:4px 10px;border-radius:999px;background:var(--surface-2);border:1.5px solid var(--line);color:var(--ink-soft);font-variant-numeric:tabular-nums;}
  .plate-need.met{background:color-mix(in srgb, var(--cat-sound) 20%, var(--surface-2));border-color:var(--cat-sound);color:var(--ink);}
  .plate-dish-wrap{display:flex;justify-content:center;margin-bottom:12px;}
  .plate-dish{display:flex;flex-wrap:wrap;justify-content:center;align-items:center;gap:6px;width:190px;min-height:110px;border-radius:50%;background:var(--surface-2);border:6px solid var(--line);padding:16px;}
  .plate-dish.right{border-color:var(--cat-sound);}
  .plate-dish .plate-food{border:none;background:none;font-size:26px;padding:2px;cursor:pointer;}
  .plate-tray{display:flex;flex-wrap:wrap;justify-content:center;gap:8px;}
  .plate-tray .plate-food{display:flex;align-items:center;gap:6px;border:2px solid var(--line);border-radius:12px;background:var(--surface);padding:6px 10px;cursor:pointer;}
  .plate-tray .plate-food:active{transform:scale(.95);}

  /* ---------- Orbit node (Órbitas del sistema solar) — lote Tierra/Universo 2026-08-18 ---------- */
  .orbit-stage{position:relative;margin:4px auto;}
  .orbit-sun{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);font-size:30px;z-index:2;}
  .orbit-ring{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);border:2px dashed var(--line);border-radius:50%;}
  /* left/top los pone orbit.js: cada anillo tiene su propio ángulo. */
  .orbit-slot{position:absolute;transform:translate(-50%,-50%);min-width:44px;height:44px;border-radius:999px;border:2px solid var(--line);background:var(--surface);font-size:15px;font-weight:800;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:3px;padding:0 8px;font-variant-numeric:tabular-nums;z-index:3;}
  .orbit-slot.wrong-flash{border-color:#C0392B;animation:clockShake .35s ease;}
  .orbit-ring-bodies{font-size:18px;line-height:1;}
  .orbit-body{font-size:18px;}
  .orbit-tray{display:flex;flex-wrap:wrap;justify-content:center;gap:8px;margin-top:14px;}

  /* ---------- Earthday node (Día y noche) ---------- */
  .earthday-sky{position:relative;display:flex;align-items:center;justify-content:center;gap:36px;background:linear-gradient(90deg, color-mix(in srgb, #F5C64F 25%, var(--surface-2)), var(--surface-2) 55%, color-mix(in srgb, #16283D 35%, var(--surface-2)));border:2px solid var(--line);border-radius:14px;padding:22px;max-width:340px;margin:0 auto 6px;}
  .earthday-sun{font-size:44px;}
  /* Disco terrestre con el lado que mira al Sol iluminado (fijo — el Sol
     no se mueve; lo que cambia de lado es la casa). */
  .earthday-earth{position:relative;width:120px;height:120px;border-radius:50%;background:linear-gradient(90deg, #7EC8E3 0 50%, #274B6D 50% 100%);border:3px solid var(--line);}
  .earthday-house{position:absolute;transform:translate(-50%,-50%);font-size:22px;transition:left .35s ease, top .35s ease;}
  .earthday-progress{font-size:14.5px;font-weight:700;color:var(--ink-soft);margin:4px 0;font-variant-numeric:tabular-nums;}

  /* ---------- Tangram node (Tangram de Robi) ---------- */
  .tangram-grid{position:absolute;inset:0;display:grid;border-radius:14px;overflow:hidden;border:3px solid var(--line);background:var(--surface-2);}
  /* El tablero, la vista previa y los controles tienen que caber en una
     sola pantalla: si el niño tiene que subir y bajar para ver la pieza
     que eligió y el sitio donde va, pierde el hilo y se equivoca. Medido
     en un teléfono de 375x812: se pasaba 77px. Se recorta el tablero, se
     achica la vista previa y los controles pasan a una sola fila. */
  .tangram-board-wrap{max-width:100% !important;margin:10px auto !important;}
  .tangram-controls{display:flex;align-items:center;justify-content:center;gap:12px;flex-wrap:wrap;margin-top:6px;}
  .tangram-preview-box{width:74px;flex:none;}
  .tangram-preview-box .stage-wrap{width:74px;margin:0;}
  .tangram-preview-label{font-size:11.5px;color:var(--ink-soft);text-align:center;margin-bottom:3px;}
  .tangram-cell{border:1px solid color-mix(in srgb, var(--line) 60%, transparent);cursor:pointer;}
  .tangram-cell.silhouette{background:color-mix(in srgb, var(--cat-move) 30%, var(--surface-2));border-style:dashed;border-color:color-mix(in srgb, var(--cat-move) 55%, transparent);}
  .tangram-cell.filled{border-style:solid;}
  .tangram-palette{display:flex;flex-wrap:wrap;justify-content:center;gap:6px;margin:8px 0;}
  .tangram-buttons{display:flex;flex-direction:column;gap:6px;}
  .tangram-buttons .btn-secondary{font-size:13px;padding:7px 12px;white-space:nowrap;}
  .tangram-piece{padding:6px 10px;border-radius:10px;border:2px solid var(--line);background:var(--surface);cursor:pointer;font-size:12.5px;}
  .tangram-piece.sel{background:var(--surface-2);}

  /* ---------- ChainReact node (Reacción en cadena) ---------- */
  .chainreact-track{display:flex;align-items:center;justify-content:center;flex-wrap:wrap;gap:2px;margin:16px 0;}
  .chainreact-node{width:44px;height:44px;border-radius:10px;background:var(--surface-2);display:flex;align-items:center;justify-content:center;font-size:20px;}
  .chainreact-node.start,.chainreact-node.goal{background:color-mix(in srgb, var(--cat-sound) 30%, var(--surface-2));}
  .chainreact-node.filled{background:color-mix(in srgb, var(--cat-move) 30%, var(--surface-2));}
  .chainreact-arrow{color:var(--ink-soft);font-size:14px;}
  .chainreact-pool{display:flex;flex-wrap:wrap;justify-content:center;gap:10px;}
  .chainreact-piece{display:flex;flex-direction:column;align-items:center;gap:2px;padding:10px 12px;border-radius:12px;border:2px solid var(--line);background:var(--surface);cursor:pointer;}
  .chainreact-piece .ic{font-size:22px;}
  .chainreact-piece .hint{font-size:11px;color:var(--ink-soft);}

  /* ---------- MixLab node (Laboratorio de mezclas) ---------- */
  .mixlab-discovered{display:flex;flex-wrap:wrap;justify-content:center;gap:10px;margin-bottom:14px;min-height:56px;}
  .mixlab-result-card{display:flex;flex-direction:column;align-items:center;gap:2px;background:var(--surface-2);border-radius:10px;padding:8px 12px;}
  .mixlab-result-card .ic{font-size:22px;}
  .mixlab-result-card .lbl{font-size:11px;color:var(--ink-soft);}
  .mixlab-ingredients{display:flex;flex-wrap:wrap;justify-content:center;gap:10px;}
  .mixlab-ingredient{width:56px;height:56px;border-radius:50%;border:none;background:var(--cat-move);font-size:26px;cursor:pointer;box-shadow:0 3px 0 rgba(0,0,0,.25);}
  .mixlab-ingredient.sel{box-shadow:0 0 0 3px var(--ink), 0 3px 0 rgba(0,0,0,.25);transform:translateY(-2px);}

  /* ---------- FoodWeb node (Equilibra el ecosistema) ---------- */
  .foodweb-rows{display:flex;flex-direction:column;gap:10px;margin:16px 0;}
  .foodweb-row{display:flex;align-items:center;gap:10px;background:var(--surface-2);border-radius:12px;padding:10px 14px;}
  .foodweb-ic{font-size:22px;}
  .foodweb-label{flex:1;font-size:13.5px;}
  .foodweb-btn{width:44px;height:44px;border-radius:50%;border:none;background:var(--cat-move);color:var(--cat-move-ink);font-size:18px;font-weight:700;cursor:pointer;}
  .foodweb-btn:disabled{opacity:.35;cursor:not-allowed;}
  .foodweb-count{min-width:46px;text-align:center;font-size:16px;font-weight:700;font-variant-numeric:tabular-nums;}
  .foodweb-count.ok{color:var(--cat-sound);}
  .foodweb-star{margin-left:2px;}
  .foodweb-target{font-size:11px;color:var(--ink-soft);}

  /* ---------- AiAudit node (Jurado de la IA) ---------- */
  .aiaudit-case{display:flex;flex-direction:column;align-items:center;gap:8px;background:var(--surface-2);border-radius:var(--radius-md);padding:20px;margin:20px 0;transition:transform .3s, opacity .3s;}
  .aiaudit-case.rulesort-ok{transform:scale(1.06);opacity:.5;}
  .aiaudit-ic{font-size:40px;}
  .aiaudit-badge{font-size:20px;}
  .aiaudit-color{font-size:22px;}
  .aiaudit-decision{font-size:14px;color:var(--ink-soft);font-weight:600;}
  .aiaudit-verdicts{display:flex;justify-content:center;gap:14px;}
  .aiaudit-btn{padding:12px 18px;}

  /* ---------- MusicSeq node (DJ Robi) ---------- */
  /* La rejilla del ritmo pasó de casillas de 28 px a 44, el mínimo para un
     dedo. Ocho casillas de 44 no caben junto a una etiqueta de 96 px en un
     teléfono, así que el nombre del instrumento sube ENCIMA de su fila y
     las casillas se reparten en dos compases de cuatro cuando no hay
     ancho. Eso no es un apaño: ocho tiempos SON dos compases de cuatro, y
     verlos separados es más correcto musicalmente que una tira de ocho. */
  .musicseq-grid{display:flex;flex-direction:column;gap:14px;margin:16px 0;}
  .musicseq-row{display:flex;flex-direction:column;align-items:flex-start;gap:6px;}
  .musicseq-lbl{font-size:13px;font-weight:700;color:var(--ink-soft);}
  /* 4 casillas de 44 + 3 huecos de 6 = 194 px: el max-width fuerza que
     entren exactamente cuatro por fila, sin depender de cuánto mida la
     pantalla. En cuanto hay sitio se sueltan las ocho en una tira y el
     hueco tras la cuarta marca el cambio de compás. */
  .musicseq-cells{display:flex;flex-wrap:wrap;gap:6px;max-width:194px;}
  .musicseq-cell{width:44px;height:44px;border-radius:9px;border:2px solid var(--line);background:var(--surface);cursor:pointer;padding:0;flex:none;}
  /* El primer tiempo de cada compás va con el borde más marcado: es
     donde cae el acento, y ahora que van de cuatro en cuatro se ve. */
  .musicseq-cell:nth-child(4n+1){border-color:var(--ink-soft);}
  .musicseq-cell.on{background:var(--cat-move);border-color:var(--cat-move);}
  .musicseq-cell.playing{box-shadow:0 0 0 3px var(--cat-sound);}
  @media (min-width:560px){
    .musicseq-cells{max-width:none;flex-wrap:nowrap;}
    .musicseq-cells > .musicseq-cell:nth-child(4){margin-right:14px;}
    .musicseq-row{flex-direction:row;align-items:center;gap:12px;}
    .musicseq-lbl{min-width:104px;}
  }
  .musicseq-form-grid{display:flex;flex-direction:column;gap:12px;margin:8px 0;}
  /* Aquí (editor del profesor, teacher.js) las casillas son hermanas de la
     etiqueta, no van en su propio contenedor: por eso la fila envuelve y
     la etiqueta ocupa un renglón entero en vez de una columna aparte. */
  .musicseq-form-row{display:flex;flex-wrap:wrap;align-items:center;gap:6px;}
  .musicseq-form-lbl{flex:0 0 100%;font-size:12.5px;font-weight:700;color:var(--ink-soft);}
  .musicseq-form-cell{width:44px;height:44px;border-radius:9px;border:2px solid var(--line);background:var(--surface);cursor:pointer;padding:0;}
  .musicseq-form-cell.on{background:var(--cat-move);border-color:var(--cat-move);}


  /* ---------- Zona de toque de 44 px sin agrandar el dibujo ----------
     Un icono chico puede seguir viéndose chico y ser fácil de tocar: el
     ::after es un rectángulo invisible de 44x44 centrado en el botón.
     Solo se aplica donde NO hay otro elemento tocable al lado al que le
     robaría el toque; donde sí lo hay (la rejilla de emojis, los tres
     iconos del avatar) el botón se agranda de verdad o se deja como está.
     La .loop-chip que envuelve a .loop-chip-del es un <span> sin acción
     propia, así que ahí el halo tampoco pisa nada. */
  /* .info-toggle NO va en esta lista: ya es position:fixed, que también
     es un elemento posicionado y le sirve igual de ancla al ::after.
     Ponerle position:relative aquí le quitaba el fixed —esta regla va
     después y pesa lo mismo—, así que el botón de ayuda se caía del
     rincón inferior izquierdo al flujo normal, arriba del todo, y como
     conservaba su bottom:12px se desplazaba 12 px hacia ARRIBA y quedaba
     medio cortado fuera de la pantalla. Reportado por el usuario: "el
     icono de ayuda está escondido arriba a la izquierda". El verificador
     de zonas táctiles no lo vio porque mide el tamaño del área, y el área
     seguía midiendo 44 px: estaba bien, pero en el sitio equivocado. */
  /* Ninguno de los tres va aquí ya: .info-toggle es fixed y
     .loop-chip-del/.modal-close-x son absolute, y las tres cosas sirven
     igual de ancla al ::after. A .loop-chip-del este relative sí le hacía
     daño —su regla absolute está ANTES, así que perdía el top:-7px /
     right:-7px y la equis de borrar se caía de la esquina del bloque al
     flujo—; a .modal-close-x no, porque su absolute viene después. Se
     quitan los dos: dejar uno "porque no molesta" es dejar una trampa
     que se dispara el día que alguien mueva una regla de sitio. */
  .loop-chip-del::after, .modal-close-x::after, .info-toggle::after{
    content:'';position:absolute;top:50%;left:50%;
    transform:translate(-50%,-50%);width:44px;height:44px;border-radius:50%;
  }


  /* ---------- Alto mínimo de los botones que se tocan ----------
     Estos se miden por relleno, no por alto fijo, así que según la fuente
     del sistema caían en 42-43 px: a uno o dos píxeles del mínimo, que es
     igual de insuficiente que a diez. El min-height los ancla sin cambiar
     nada de su aspecto —el relleno ya los dejaba casi ahí—. */
  .btn-primary, .btn-secondary, .tap-choice, .bodymap-part,
  .numwheel-key, .clockset-btn, .decision-opt, .fz-num,
  .showcase-card{min-height:44px;}
  /* El redondeo de la caja flexible dejaba este en 43 px de ancho. */
  .foodweb-btn{min-width:44px;flex:none;}

  /* ---------- Tres mecánicas de tamaño real (2026-08-22) ----------
     La sombra que crece, la huella medida con el pie y el paso caminado.
     Comparten el mismo criterio que arscale: todo lo que representa una
     medida se expresa en % del alto de su escena, nunca en píxeles, que es
     lo único que hace que la proporción sea la misma en un teléfono y en
     una tablet. */

  /* --- La sombra que crece --- */
  .gs-prompt{color:var(--ink-soft);font-size:14.5px;line-height:1.5;margin:0 0 6px;}
  .gs-orden{font-size:14.5px;font-weight:700;color:var(--ink);margin:0 0 10px;}
  .gs-donde{color:var(--muted);font-size:12.5px;}
  .gs-escena{
    position:relative;width:100%;aspect-ratio:3/2;margin:6px auto 10px;
    border-radius:16px;overflow:hidden;border:3px solid var(--line);
    background:linear-gradient(to bottom, var(--accent-soft) 0%, var(--surface-2) 74%, var(--surface) 74%, var(--surface) 100%);
  }
  .gs-suelo{
    position:absolute;left:0;right:0;top:0;
    display:flex;align-items:flex-end;justify-content:center;gap:8%;
    padding:0 6%;pointer-events:none;
  }
  .gs-figura{display:flex;align-items:flex-end;justify-content:center;height:100%;flex:0 1 auto;min-width:0;}
  .gs-sil{display:block;width:auto;max-width:100%;overflow:visible;}
  /* Doble clase a propósito: siluetaSVG() les pone también .ars-sil, y la
     regla de arscale va después en esta hoja, así que con una sola clase
     ganaba ella y el niño salía del color del animal. */
  .gs-sil.gs-sil g{fill:var(--accent);}
  .gs-sil.gs-nino g{fill:color-mix(in srgb, var(--ink) 62%, transparent);}
  /* El tamaño correcto se revela como una sombra clara DETRÁS de la que
     paró el niño: ver por cuánto se pasó vale más que leerlo. Va dentro de
     la figura del animal y anclada a su mismo suelo — centrada sobre toda
     la escena se montaba encima del niño. */
  .gs-figura-bicho{position:relative;}
  /* top:0 además de bottom:0 no es decorativo: el alto de la sombra se da
     en PORCENTAJE, y sin una altura de referencia el porcentaje resolvía
     contra una caja de alto automático, o sea contra cero. Tiene que medir
     lo mismo que .gs-figura, que es la banda útil de la escena. */
  /* max-width:none es obligatorio, no cosmético: .gs-sil lleva
     max-width:100%, y el 100% es el ancho de .gs-figura-bicho, que lo fija
     el animal EN CRECIMIENTO. Si el niño para la sombra pronto, ese padre
     mide dos píxeles y recortaba la sombra correcta a dos píxeles con él. */
  /* Clase repetida a propósito: .ars-sil lleva max-width:100% y va DESPUÉS
     en esta hoja, así que con una sola clase ganaba ella. Y ese 100% es el
     ancho de .gs-figura-bicho, que lo fija el animal EN CRECIMIENTO: si el
     niño para pronto, ese padre mide dos píxeles y recortaba la sombra
     correcta a dos píxeles con él. */
  .gs-fantasma.gs-fantasma{
    position:absolute;bottom:0;left:50%;transform:translateX(-50%);
    display:none;max-width:none;pointer-events:none;
  }
  .gs-fantasma .gs-sil.gs-sil g{fill:color-mix(in srgb, var(--accent) 28%, transparent);}
  .gs-marcador{display:flex;justify-content:center;font-size:12px;font-weight:700;color:var(--muted);margin-bottom:8px;}
  .gs-parar{width:100%;}
  .gs-veredicto{
    margin:10px 0 0;font-size:13.5px;line-height:1.45;color:var(--ink);
    padding:9px 11px;border-radius:10px;border:1px solid var(--line);background:var(--bg);
  }
  .gs-veredicto.bien{border-color:var(--cat-sound);background:color-mix(in srgb, var(--cat-sound) 14%, var(--bg));}
  .gs-veredicto.lejos{border-color:var(--cat-loop);background:color-mix(in srgb, var(--cat-loop) 12%, var(--bg));}
  .gs-dato, .gs-fantasma-nota{display:block;margin-top:3px;color:var(--ink-soft);font-size:12.5px;}

  /* --- La huella a tamaño real --- */
  .fp-prompt{color:var(--ink-soft);font-size:14.5px;line-height:1.5;margin:0 0 10px;}
  .fp-orden{color:var(--ink-soft);font-size:13.5px;line-height:1.5;margin:0 0 12px;}
  .fp-escena{
    position:relative;display:flex;align-items:center;justify-content:center;
    min-height:210px;margin:8px 0 12px;
  }
  .fp-svg{display:block;width:auto;}
  .fp-grande g{fill:color-mix(in srgb, var(--accent) 34%, var(--surface-2));}
  /* Los pies del niño se dibujan encima de la huella, a la misma escala:
     ahí es donde la división se ve en vez de calcularse. */
  /* En COLUMNA, no en fila: la pregunta es cuántos pies caben a lo LARGO
     de la huella, y ponerlos uno al lado del otro mediría el ancho. El
     dibujo tiene que decir lo mismo que el enunciado. */
  .fp-pies{
    position:absolute;inset:0;display:flex;flex-direction:column;
    align-items:center;justify-content:center;gap:1px;pointer-events:none;
  }
  .fp-chico g{fill:var(--cat-sound);}
  .fp-pies.dentro{gap:1px;}
  .fp-pregunta{font-size:14.5px;font-weight:700;color:var(--ink);margin:6px 0 8px;}
  .fp-veredicto{
    margin:10px 0 0;font-size:13.5px;line-height:1.45;color:var(--ink);
    padding:9px 11px;border-radius:10px;border:1px solid var(--line);background:var(--bg);
  }
  .fp-veredicto.bien{border-color:var(--cat-sound);background:color-mix(in srgb, var(--cat-sound) 14%, var(--bg));}
  .fp-veredicto.lejos{border-color:var(--cat-loop);background:color-mix(in srgb, var(--cat-loop) 12%, var(--bg));}
  .fp-dato{display:block;margin-top:3px;color:var(--ink-soft);font-size:12.5px;}

  /* --- Camina como él --- */
  .st-prompt{color:var(--ink-soft);font-size:14.5px;line-height:1.5;margin:0 0 10px;}
  .st-comparacion{display:flex;flex-direction:column;gap:6px;margin:12px 0;}
  .st-barra{
    height:34px;border-radius:9px;display:flex;align-items:center;padding:0 10px;
    font-size:12.5px;font-weight:800;color:var(--ink);
  }
  .st-suya{width:100%;background:color-mix(in srgb, var(--accent) 45%, var(--surface));}
  .st-tuya{background:color-mix(in srgb, var(--cat-sound) 55%, var(--surface));min-width:44px;}
  .st-nota{font-size:12.5px;color:var(--ink-soft);line-height:1.5;margin:8px 0 0;}
  .st-pregunta{font-size:14.5px;font-weight:700;color:var(--ink);margin:12px 0 8px;}
  .st-contador{
    display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;
    margin:16px auto;width:150px;height:150px;border-radius:50%;
    border:4px solid var(--line);background:var(--surface-2);
    transition:transform .12s ease, border-color .12s ease;
  }
  .st-contador.late{transform:scale(1.06);border-color:var(--cat-sound);}
  .st-contador span{font-size:46px;font-weight:900;color:var(--ink);line-height:1;font-variant-numeric:tabular-nums;}
  .st-contador small{font-size:12.5px;color:var(--muted);font-weight:700;}
  .st-pista{text-align:center;font-size:13px;color:var(--ink-soft);margin-bottom:10px;line-height:1.5;}
  .st-pista.listo{color:var(--ink);}
  .st-paso{width:100%;min-height:64px;font-size:17px;}
  .st-auto{width:100%;min-height:48px;margin-top:8px;font-size:14px;}
  .st-dato{display:block;margin-top:4px;color:var(--ink-soft);font-size:12.5px;}

  @media (prefers-reduced-motion:reduce){
    .st-contador{transition:none;}
  }

  /* Aviso de nombre de perfil repetido. Sale mientras el padre escribe,
     no después de tocar el botón: un error que aparece al final se lee
     como "la aplicación falló", y este es algo que él puede arreglar. */
  .aviso-nombre{
    margin:6px 0 0;font-size:12.5px;line-height:1.4;color:var(--cat-move);
    background:color-mix(in srgb, var(--cat-move) 12%, var(--bg));
    border:1px solid var(--cat-move);border-radius:9px;padding:7px 10px;
  }

  /* ---------- "Todavía estoy buscando tus perfiles" ----------
     Una pantalla de perfiles vacía y una pantalla de perfiles que aún no
     ha cargado se ven exactamente igual, y la primera lectura de un padre
     es que se perdieron. Estas dos piezas las distinguen: tarjetas
     fantasma donde van a aparecer los perfiles, y una línea que lo dice
     con palabras. */
  .avatar-card.cargando{
    background:var(--surface-2);
    border:2px dashed var(--line);
    cursor:default;
    /* Del alto de una tarjeta de perfil de verdad, para que la pantalla no
       pegue un salto en el momento en que llegan los perfiles. Una tarjeta
       real mide 148 px con la etiqueta de banda en una línea y unos 175 si
       se parte en dos, así que 160 deja el salto en menos de 15 px vaya
       como vaya. */
    min-height:160px;
    animation:cargaLatido 1.4s ease-in-out infinite;
  }
  .avatar-card.cargando:nth-of-type(2){ animation-delay:.35s; }
  @keyframes cargaLatido{
    0%,100%{opacity:.45;}
    50%{opacity:.85;}
  }
  .auth-cargando{
    margin:10px 0 0;font-size:13px;color:var(--ink-soft);
    display:flex;align-items:center;justify-content:center;gap:8px;
  }
  .auth-cargando::before{
    content:'';width:14px;height:14px;border-radius:50%;flex:none;
    border:2px solid var(--line);border-top-color:var(--accent);
    animation:cargaGiro .8s linear infinite;
  }
  @keyframes cargaGiro{ to{ transform:rotate(360deg); } }
  /* Quien pidió menos movimiento no debería ver dos animaciones a la vez
     solo por abrir la aplicación. El aviso sigue ahí, quieto. */
  @media (prefers-reduced-motion:reduce){
    .avatar-card.cargando{animation:none;opacity:.6;}
    .auth-cargando::before{animation:none;border-top-color:var(--line);}
  }

  /* ---------- Gears node ---------- */
  .gears-stage{display:flex;justify-content:center;align-items:center;gap:26px;margin:20px 0;}
  /* Antes: círculo con borde punteado y un emoji ⚙️ adentro — se veía como
     un dibujo genérico, no como el objeto real. Ahora es un botón "3D"
     igual al resto de la app (mismo anillo+sombra que .node-btn) con el
     ícono SVG real (engranaje/llave/polea) adentro, a color, del tamaño
     de todo el botón. */
  .gear{width:96px;height:96px;border-radius:50%;border:none;background:var(--surface-2);color:var(--ink-soft);display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 0 0 1.5px rgba(0,0,0,.2), 0 4px 0 rgba(0,0,0,.28);}
  .gear .lucide{width:60%;height:60%;}
  .gear:active{transform:translateY(3px);box-shadow:0 0 0 1.5px rgba(0,0,0,.2);}
  .gear.spin{animation:spinL 2.2s linear infinite;}
  .gear.spin2{animation:spinR 2.2s linear infinite;}
  @keyframes spinL{from{transform:rotate(0);}to{transform:rotate(360deg);}}
  @keyframes spinR{from{transform:rotate(0);}to{transform:rotate(-360deg);}}

  /* ---------- AI train node ---------- */
  .ai-pool{display:flex;flex-wrap:wrap;gap:10px;justify-content:center;margin:14px 0;}
  .ai-item{width:52px;height:52px;border-radius:14px;background:var(--surface-2);font-size:26px;display:flex;align-items:center;justify-content:center;cursor:pointer;border:none;box-shadow:0 0 0 1.5px var(--line), 0 3px 0 var(--line);}
  .ai-item.selected{box-shadow:0 0 0 1.5px var(--accent-border), 0 3px 0 var(--accent-border);}
  .ai-item.placed{opacity:.3;pointer-events:none;}
  .ai-bins{display:flex;gap:14px;justify-content:center;}
  .ai-bin{flex:1;max-width:180px;min-height:90px;border:2px dashed var(--line);border-radius:var(--radius-md);padding:10px;text-align:center;font-weight:700;font-size:13px;color:var(--ink-soft);cursor:pointer;}
  .ai-bin .items{display:flex;flex-wrap:wrap;gap:5px;justify-content:center;margin-top:8px;}
  .ai-bin .items span{font-size:20px;}
  /* El cajón se sacude cuando el toque no se puede atender —no hay dibujo
     elegido todavía—. El mensaje de Robi dice qué falta; esto dice DÓNDE
     ha llegado el toque, para que no parezca que la pantalla no responde. */
  .ai-bin.wrong-flash{border-color:#C0392B;border-style:solid;animation:clockShake .35s ease;}
  .ai-test-row{display:flex;justify-content:center;gap:20px;margin-top:16px;}
  .ai-test-card{background:var(--surface-2);border:2.5px solid var(--line);border-radius:var(--radius-md);padding:14px;text-align:center;min-width:100px;}
  .ai-test-card .ic{font-size:30px;}
  .ai-test-card .guess{margin-top:8px;font-weight:800;font-size:13px;}
  .ai-test-card .guess.ok{color:var(--cat-sound);}
  .ai-test-card .guess.no{color:var(--cat-move);}

  /* ---------- Match pairs node (juego "unir pares") ---------- */
  .match-grid{display:grid;grid-template-columns:1fr 1fr;gap:18px;margin-top:14px;}
  .match-col{display:flex;flex-direction:column;gap:10px;}
  .match-item{display:flex;align-items:center;gap:9px;border:none;background:var(--surface-2);border-radius:var(--radius-md);padding:12px 14px;font-weight:700;font-size:14px;color:var(--ink);cursor:pointer;box-shadow:0 0 0 1.5px var(--line), 0 3px 0 var(--line);transition:transform .1s ease;}
  .match-item:active{transform:translateY(2px);}
  .match-item .lucide{width:22px;height:22px;flex:none;}
  /* `flex:none` obligaba a la casilla a ser tan ancha como su contenido, y
     un montón de seis huevos 🥚🥚🥚🥚🥚🥚 se salía por el borde derecho
     (reportado en "El número y cuántos hay"). La columna del mapa mide
     1fr, así que lo que sobra no se recorta: se desborda.
     Con `min-width:0` la casilla puede encoger y el montón pasa a la línea
     de abajo — que para contar hasta es mejor que una fila larga. */
  .match-item .match-ic{font-size:22px;line-height:1.15;flex:0 1 auto;min-width:0;overflow-wrap:anywhere;}
  .match-item{min-width:0;}
  .match-item .lbl{flex:1;}
  .match-item.sel{box-shadow:0 0 0 1.5px var(--accent-border), 0 3px 0 var(--accent-border);}
  .match-item.matched{background:color-mix(in srgb, var(--cat-sound) 20%, var(--surface-2));box-shadow:0 0 0 1.5px var(--cat-sound), 0 3px 0 var(--cat-sound);opacity:.65;cursor:default;}
  .match-item.wrong{background:color-mix(in srgb, var(--cat-move) 18%, var(--surface-2));box-shadow:0 0 0 1.5px var(--cat-move), 0 3px 0 var(--cat-move);}

  /* ---------- Memory node (juego "memoria") ---------- */
  .memory-grid{display:grid;gap:9px;margin-top:14px;}
  .memory-card{aspect-ratio:1/1;border:none;border-radius:var(--radius-md);background:var(--accent);color:var(--accent-ink);display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 0 0 1.5px var(--accent-border), 0 3px 0 var(--accent-border);transition:transform .12s ease;overflow:hidden;}
  /* Respaldo si algún contenido a medida (ej. muchos puntitos seguidos)
     igual quedara más ancho que la tarjeta pese al font-size ya achicado
     en JS (memory.js, memoryIcFontPx) — nunca deja que el texto se salga
     ni tape la tarjeta vecina; el tamaño de la tarjeta en sí no cambia. */
  .memory-ic{max-width:100%;white-space:nowrap;}
  .memory-card .lucide{width:26px;height:26px;}
  .memory-card:active{transform:scale(.94);}
  .memory-card.up{background:var(--surface-2);color:var(--ink);box-shadow:0 0 0 1.5px var(--line), 0 2px 0 var(--line);cursor:default;}
  .memory-card.up .lucide{width:30px;height:30px;}
  .memory-card.up .memory-ic{font-size:30px;line-height:1;}
  .memory-card.matched{opacity:.45;}

  /* ---------- Odd one out node (juego "¿cuál no pertenece?") ---------- */
  .odd-grid{display:flex;flex-wrap:wrap;gap:14px;justify-content:center;margin-top:16px;}
  .odd-item{display:flex;flex-direction:column;align-items:center;gap:6px;width:92px;border:none;background:var(--surface-2);border-radius:var(--radius-md);padding:16px 10px;font-weight:700;font-size:12.5px;color:var(--ink);cursor:pointer;box-shadow:0 0 0 1.5px var(--line), 0 3px 0 var(--line);transition:transform .1s ease;}
  .odd-item:active{transform:translateY(2px);}
  .odd-item .lucide{width:30px;height:30px;}
  /* Mismo caso que .match-ic: la casilla mide 92 px fijos y un montón de
     cuatro estrellas ⭐⭐⭐⭐ a 30 px se salía por el borde (medido: 14 px
     de más, en "¿Cuál sobra?"). Con max-width y el corte permitido, el
     montón pasa a la línea de abajo en vez de desbordarse. */
  .odd-item .odd-ic{font-size:30px;line-height:1.15;max-width:100%;overflow-wrap:anywhere;}
  .odd-item.right{background:color-mix(in srgb, var(--cat-sound) 20%, var(--surface-2));box-shadow:0 0 0 1.5px var(--cat-sound), 0 3px 0 var(--cat-sound);}
  .odd-item.wrong{background:color-mix(in srgb, var(--cat-move) 18%, var(--surface-2));box-shadow:0 0 0 1.5px var(--cat-move), 0 3px 0 var(--cat-move);}

  /* ---------- Pattern node (predicción de patrones) ---------- */
  .pattern-seq{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;font-size:34px;margin:18px 0;}
  .pattern-tile{width:48px;height:48px;display:flex;align-items:center;justify-content:center;background:var(--surface-2);border-radius:var(--radius-md);}
  .pattern-blank{border:2.5px dashed var(--line);font-size:22px;}
  .pattern-tile.solved{border:none;animation:varBump .4s cubic-bezier(.34,1.56,.64,1);}
  .pattern-options{display:flex;gap:14px;justify-content:center;}
  .pattern-opt{width:60px;height:60px;border-radius:50%;border:none;background:var(--surface);font-size:28px;cursor:pointer;box-shadow:0 0 0 1.5px var(--line), 0 3px 0 var(--line);transition:transform .1s ease;}
  .pattern-opt:active{transform:translateY(2px);}
  .pattern-opt.right{background:color-mix(in srgb, var(--cat-sound) 20%, var(--surface));box-shadow:0 0 0 1.5px var(--cat-sound), 0 3px 0 var(--cat-sound);}
  .pattern-opt.wrong{animation:sortShake .4s ease;}

  /* ---------- Realdebug node (depurar de verdad) ---------- */
  .debug-track{display:flex;gap:6px;justify-content:center;margin:18px 0 14px;}
  .debug-cell{width:44px;height:44px;display:flex;align-items:center;justify-content:center;font-size:22px;background:var(--surface-2);border-radius:var(--radius-md);border:2px solid transparent;}
  .debug-cell.has-robot{border-color:var(--accent);}
  /* Robi dentro de una casilla del carril: el dibujo manda sobre el alto
     de línea del emoji al que sustituye. */
  .debug-cell .robi-mascot-img{width:30px;height:30px;display:block;}
  .robust-ic .robi-mascot-img{width:28px;height:28px;display:block;}
  .debug-program{display:flex;gap:10px;justify-content:center;flex-wrap:wrap;}
  .debug-block{width:52px;height:52px;border-radius:var(--radius-md);border:none;background:var(--surface);font-size:24px;cursor:pointer;box-shadow:0 0 0 1.5px var(--line), 0 3px 0 var(--line);}
  .debug-block:disabled{opacity:.5;cursor:default;}
  .debug-block:not(:disabled):active{transform:translateY(2px);}
  .debug-picker{display:flex;gap:6px;background:var(--surface-2);border-radius:var(--radius-md);padding:6px;}
  .debug-picker-opt{width:44px;height:44px;border-radius:var(--radius-sm,8px);border:none;background:var(--surface);font-size:20px;cursor:pointer;box-shadow:0 0 0 1.5px var(--line);}

  /* ---------- Grupos que se juntan (demo "¿qué es sumar?", concept.js) ---------- */
  .sum-groups{display:flex;align-items:center;justify-content:center;gap:12px;flex-wrap:wrap;margin:18px 0;min-height:52px;}
  .sum-group{display:flex;flex-wrap:wrap;gap:4px;justify-content:center;max-width:180px;}
  .sum-ic{font-size:26px;line-height:1;}
  .sum-plus{font-size:22px;font-weight:800;color:var(--ink-soft);}
  .sum-group.sum-merged{animation:varBump .4s cubic-bezier(.34,1.56,.64,1);}

  /* ---------- Demo "circuito abierto/cerrado" (concept.js, r1b) ---------- */
  .circuit-open-stage{display:flex;align-items:center;justify-content:center;gap:14px;font-size:38px;margin:20px 0;}
  .circuit-open-switch-ic{font-size:30px;}

  /* ---------- Demo "analógico vs. digital" (concept.js, r8) ---------- */
  .analog-dial-track{width:min(80%,260px);height:22px;border-radius:11px;background:var(--surface-2);margin:18px auto;overflow:hidden;box-shadow:inset 0 0 0 1.5px var(--line);}
  .analog-dial-fill{height:100%;background:var(--accent);transition:width .25s ease;}
  .digital-switch-ic{font-size:44px;margin:18px 0;}

  /* ---------- Demo "el sonido viaja en ondas" (concept.js, s7) ---------- */
  .soundwave-stage{display:flex;align-items:center;justify-content:center;gap:16px;font-size:38px;margin:20px 0;min-height:52px;}
  .soundwave-bell{background:none;border:none;font-size:44px;cursor:pointer;}
  .soundwave-bell:disabled{opacity:.6;cursor:default;}
  .soundwave-ic{font-size:30px;min-width:34px;text-align:center;}

  /* ---------- Demo "vemos porque la luz rebota" (concept.js, s8) ---------- */
  .lightsee-stage{display:flex;align-items:center;justify-content:center;gap:14px;font-size:48px;margin:20px 0;min-height:60px;}
  .lightsee-ic{font-size:48px;}

  /* ---------- Demo "¿lo hizo una IA?" (concept.js, a8) ---------- */
  .aicreate-choices{display:flex;gap:14px;justify-content:center;flex-wrap:wrap;margin-top:16px;}

  /* ---------- Demo "algoritmo = pasos en orden" (concept.js, p12) ---------- */
  .algo-steps{display:flex;flex-wrap:wrap;gap:10px;justify-content:center;margin-top:14px;}

  /* ---------- Demo "programa robusto" (concept.js, p13) ---------- */
  .robust-stage{display:flex;align-items:center;justify-content:center;gap:14px;font-size:38px;margin:20px 0;min-height:52px;}

  /* ---------- Demo "lógica Y/O" (concept.js, r11) ---------- */
  .logic-stage{display:flex;align-items:center;justify-content:center;gap:18px;margin:20px 0;}
  .logic-switch{background:none;border:none;padding:6px;cursor:pointer;min-width:44px;min-height:44px;display:inline-flex;align-items:center;justify-content:center;}
  /* El punto que reemplazó a 🟢/⚪ (ver puntoDeInterruptor en concept.js).
     Apagado lleva borde: sin él, un círculo del color de la superficie es
     invisible, que era el defecto del ⚪ sobre fondo claro. */
  .logic-dot{display:block;width:36px;height:36px;border-radius:50%;background:var(--surface-2);border:2px solid var(--line-fuerte);transition:background .15s,border-color .15s,box-shadow .15s;}
  .logic-dot.on{background:#3FBF6A;border-color:#2E9A52;box-shadow:0 0 0 4px rgba(63,191,106,.22);}
  .logic-bulb{font-size:44px;}

  /* ---------- Demo "genes heredados" (concept.js, s10) ---------- */
  .genetics-stage{display:flex;align-items:center;justify-content:center;gap:16px;margin:20px 0;flex-wrap:wrap;}
  .genetics-parent{background:var(--surface-2);border:none;border-radius:var(--radius-md);padding:10px 14px;font-size:16px;cursor:pointer;box-shadow:0 0 0 1.5px var(--line), 0 3px 0 var(--line);}
  .genetics-parent:disabled{opacity:.6;cursor:default;}
  .genetics-baby{font-size:40px;}

  /* ---------- Demo "visión por computadora" (concept.js, a11) ---------- */
  .vision-photo{font-size:56px;margin:16px 0;}

  /* ---------- Demo "binario = 0 y 1" (concept.js, p14) ---------- */
  .binary-switches{display:flex;gap:12px;justify-content:center;margin:18px 0 10px;}
  .binary-switch{width:52px;height:52px;border-radius:var(--radius-md);border:none;background:var(--surface-2);font-size:24px;font-weight:800;cursor:pointer;box-shadow:0 0 0 1.5px var(--line), 0 3px 0 var(--line);}
  .binary-count{font-size:20px;min-height:26px;}

  /* ---------- Demo "optimizar" (concept.js, p16) ---------- */
  .optimize-progress{font-size:26px;min-height:32px;margin:14px 0;}

  /* ---------- Paint node (pintar resolviendo) ---------- */
  .paint-grid{display:inline-flex;flex-direction:column;gap:3px;background:var(--surface-2);padding:8px;border-radius:var(--radius-md);margin:16px auto;}
  .paint-row{display:flex;gap:3px;}
  .paint-cell{width:26px;height:26px;border-radius:5px;flex:none;}
  .paint-cell.bg{background:transparent;}
  .paint-cell.has-zone{background:var(--surface);box-shadow:inset 0 0 0 2px var(--line);}
  .paint-cell.has-zone.active{box-shadow:inset 0 0 0 2px var(--accent);animation:debugPulse 1.2s ease infinite;}
  .paint-cell.filled{box-shadow:none;animation:varBump .35s cubic-bezier(.34,1.56,.64,1);}
  .paint-sum-text{font-size:22px;font-weight:800;color:var(--ink);margin:14px 0 6px;}
  /* Vitrina de 5-6 años, parte de PRESENTAR. Una pieza a la vez, grande
     y centrada: si el niño va a tener que reconocerla en diez segundos,
     tiene que haberla mirado bien primero. */
  .showcase-ensena{text-align:center;}
  .showcase-ensena-ic{display:flex;justify-content:center;margin:14px 0 10px;color:var(--accent);}
  .showcase-ensena-ic .lucide{width:72px;height:72px;}
  .showcase-ensena-nombre{font-size:20px;margin:0 0 8px;}
  .showcase-ensena-txt{color:var(--ink-soft);font-size:15px;line-height:1.55;max-width:30ch;margin:0 auto 18px;}
  .showcase-ensena .btn-primary{width:100%;max-width:280px;}

  .paint-choices{display:flex;gap:12px;justify-content:center;flex-wrap:wrap;margin-top:10px;}
  .paint-choice{min-width:56px;min-height:44px;padding:10px 14px;border-radius:var(--radius-md);border:none;background:var(--surface);font-size:19px;font-weight:800;color:var(--ink);cursor:pointer;box-shadow:0 0 0 1.5px var(--line), 0 3px 0 var(--line);}
  .paint-choice:active{transform:translateY(2px);}
  .paint-choice .paint-dots{display:flex;flex-wrap:wrap;gap:3px;max-width:70px;justify-content:center;}
  .paint-dot{width:9px;height:9px;border-radius:50%;background:var(--accent);flex:none;}
  .paint-choice.wrong{animation:sortShake .4s ease;}
  /* El aviso de "terminaste" usaba --cat-sound-ink (#0B3F28), un verde
     muy oscuro definido UNA sola vez, para tema claro. Sobre la
     superficie oscura daba 1,27:1 de contraste — el mínimo para leer un
     texto es 4,5:1 — y el usuario lo reportó como "apenas se ve". --ok
     está definido en los cuatro estados de tema y da 7,9:1 en oscuro y
     3,4:1 en claro (suficiente: 19px en negrita cuenta como texto
     grande, umbral 3:1). */
  .paint-done-msg{font-size:19px;font-weight:800;color:var(--ok);margin-top:14px;}

  /* Confirmación al acertar.
     Antes no había ninguna: al tocar la opción correcta el botón no
     cambiaba y la pantalla se repintaba 450 ms después, así que el niño
     no sabía si su toque había entrado. :active no sirve para esto — en
     una pantalla táctil dura lo que dura el dedo encima. */
  .paint-choice.correct{
    outline:3px solid var(--ok);
    background:color-mix(in srgb, var(--ok) 18%, var(--surface));
  }

  /* ---------- Balance node (juego "balanza") ---------- */
  .balance-stage{position:relative;height:210px;margin:22px auto 0;display:flex;flex-direction:column;align-items:center;justify-content:flex-end;}
  .balance-beam{display:flex;justify-content:space-between;align-items:center;width:min(90%,300px);gap:20px;position:relative;z-index:2;transform-origin:center center;transition:transform .55s cubic-bezier(.34,1.56,.64,1);}
  .balance-beam::before{content:'';position:absolute;left:50%;top:50%;width:6px;height:70px;background:var(--line);transform:translate(-50%,-100%);z-index:-1;}
  .balance-beam.tilt-l{transform:rotate(-11deg);}
  .balance-beam.tilt-r{transform:rotate(11deg);}
  .balance-pan{flex:1;display:flex;flex-direction:column;align-items:center;gap:6px;border:none;background:var(--surface-2);border-radius:16px;padding:16px 10px;cursor:pointer;box-shadow:0 0 0 1.5px var(--line), 0 3px 0 var(--line);transition:transform .1s ease;}
  .balance-pan:active{transform:translateY(2px);}
  .balance-pan .lbl{font-size:11.5px;font-weight:700;text-align:center;line-height:1.2;}
  .balance-pan .balance-ic{font-size:36px;line-height:1;}
  /* "Montón" de varios emoji apilados (ver balance.js, pic()) — 3
     columnas: 2-3 ítems entran en una fila, más se acomodan en varias
     filas, siempre dentro del plato en vez de desbordarlo en línea recta. */
  .balance-pan .balance-ic-pile{display:grid;grid-template-columns:repeat(3,1fr);gap:1px 4px;justify-items:center;align-items:center;max-width:100px;}
  .balance-pan .balance-ic-piece{font-size:22px;line-height:1.15;}
  .balance-pan .lucide{width:32px;height:32px;}
  .balance-pan.right{background:color-mix(in srgb, var(--cat-sound) 20%, var(--surface-2));box-shadow:0 0 0 1.5px var(--cat-sound), 0 3px 0 var(--cat-sound);}
  .balance-pan.wrong{background:color-mix(in srgb, var(--cat-move) 18%, var(--surface-2));box-shadow:0 0 0 1.5px var(--cat-move), 0 3px 0 var(--cat-move);}
  .balance-stand{width:10px;height:90px;background:var(--line);border-radius:4px;position:relative;}
  .balance-stand::after{content:'';position:absolute;bottom:-8px;left:50%;transform:translateX(-50%);width:70px;height:16px;background:var(--line);border-radius:8px;}

  /* ---------- Dress-up node (juego "viste a tu robot", sin fallar) ---------- */
  /* Compactado para que el muñeco, las tres categorías y sus opciones
     entren en una pantalla de teléfono sin desplazarse: medido, se pasaba
     99px en 375x812. */
  .dressup-preview{display:flex;justify-content:center;gap:12px;margin:10px 0 4px;}
  .dressup-slot{width:54px;height:54px;border-radius:50%;background:var(--surface-2);border:2px dashed var(--line);display:flex;align-items:center;justify-content:center;font-size:30px;transition:transform .2s cubic-bezier(.34,1.56,.64,1);}
  .dressup-slot.filled{border-style:solid;border-color:var(--accent-border);background:var(--accent-soft);animation:dressupPop .35s cubic-bezier(.34,1.56,.64,1);}
  @keyframes dressupPop{0%{transform:scale(.6);}100%{transform:scale(1);}}
  .dressup-cats{display:flex;flex-direction:column;gap:9px;margin-top:10px;}
  .dressup-cat-label{font-weight:800;font-size:11.5px;color:var(--ink-soft);text-transform:uppercase;letter-spacing:.03em;margin-bottom:4px;}
  .dressup-opts{display:flex;gap:8px;flex-wrap:wrap;}
  /* 76px de ancho hacían que la cuarta opción bajara a un segundo
     renglón, y con tres categorías eso sumaba dos filas de más: el reto
     se salía de la pantalla y el niño tenía que desplazarse para ver que
     todavía le faltaban opciones. A 62px caben las cuatro en una fila. */
  .dressup-opt{display:flex;flex-direction:column;align-items:center;gap:3px;width:62px;border:none;background:var(--surface);border-radius:var(--radius-md);padding:7px 4px;font-weight:700;font-size:10.5px;cursor:pointer;box-shadow:0 0 0 1.5px var(--line), 0 3px 0 var(--line);transition:transform .1s ease;}
  .dressup-opt:active{transform:translateY(2px);}
  .dressup-opt .dressup-ic{font-size:26px;}
  .dressup-opt.sel{background:color-mix(in srgb, var(--accent) 18%, var(--surface));box-shadow:0 0 0 1.5px var(--accent), 0 3px 0 var(--accent);}

  /* ---------- Maze node (juego "traza el camino") ---------- */
  .maze-grid{display:grid;gap:4px;width:min(100%,300px);aspect-ratio:1/1;margin:16px auto 0;padding:6px;border-radius:14px;background:var(--surface-2);border:3px solid var(--line);user-select:none;-webkit-user-select:none;}
  /* aspect-ratio:1 en la celda (no solo en el grid contenedor) es lo que
     de verdad le da alto a cada fila — sin esto, con grid-auto-rows sin
     definir y celdas sin contenido intrínseco, las filas colapsaban a 0px
     de alto: document.elementFromPoint() en el centro "visual" de una fila
     encontraba el contenedor .maze-grid en vez de la celda (altura real
     0), y el trazo nunca avanzaba más allá de la celda de inicio. */
  .maze-cell{background:var(--surface);border-radius:8px;display:flex;align-items:center;justify-content:center;position:relative;aspect-ratio:1;}
  .maze-cell .lucide{width:60%;height:60%;color:var(--ink-soft);}
  /* .wall: la celda NO forma parte del camino válido (ver renderMazeNode,
     maze.js) — antes se veía igual que el piso libre, así que el
     laberinto era invisible y solo se podía "resolver" tocando a ciegas.
     El rayado diagonal lee claramente como "bloqueado" sin depender de
     un ícono ni de texto. Va ANTES que .wrong-flash en el archivo a
     propósito: si se toca por error, el flash de aviso (más abajo) tiene
     que ganarle visualmente a este patrón, y en CSS con la misma
     especificidad gana la regla que está después. */
  .maze-cell.wall{background:repeating-linear-gradient(45deg,
    color-mix(in srgb, var(--ink) 16%, var(--surface-2)) 0 7px,
    color-mix(in srgb, var(--ink) 7%, var(--surface-2)) 7px 14px);}
  .maze-cell.start-cell{background:color-mix(in srgb, var(--accent) 20%, var(--surface));}
  .maze-cell.start-cell .lucide{color:var(--accent);}
  .maze-cell.goal-cell{background:color-mix(in srgb, var(--gold) 25%, var(--surface));}
  .maze-cell.goal-cell .lucide{color:var(--gold);}
  .maze-cell.traced{background:color-mix(in srgb, var(--cat-sound) 35%, var(--surface));}
  .maze-cell.wrong-flash{background:color-mix(in srgb, var(--cat-move) 55%, var(--surface));}
  /* #172 (obstaclepath): piedras sueltas sobre piso transitable, no
     "pared" fija como .wall — visualmente más liviano (sin la textura a
     rayas) porque conceptualmente sí se puede tocar sin querer al
     planear la ruta, a diferencia de las paredes del laberinto de
     corredor único. Mismo tono que .cell.obstacle (block.js, styles.css
     línea ~531) para que "obstáculo" se vea igual en toda la app. */
  .maze-cell.obstacle{background:color-mix(in srgb, var(--ink) 12%, var(--surface-2));font-size:22px;}

  /* ---------- Ramp node (juego "rampa y bola") ---------- */
  .ramp-stage{position:relative;height:100px;margin:24px 0 10px;}
  .ramp-track{position:absolute;bottom:20px;left:4%;right:4%;height:4px;background:var(--line);border-radius:2px;}
  .ramp-marker{position:absolute;bottom:2px;transform:translateX(-50%);width:44px;height:44px;display:flex;align-items:center;justify-content:center;}
  .ramp-marker .lucide{width:30px;height:30px;color:var(--ink-soft);}
  .ramp-slot{position:absolute;bottom:4px;transform:translateX(-50%);width:40px;height:40px;border-radius:10px;border:2px dashed var(--line);background:var(--surface-2);display:flex;align-items:center;justify-content:center;font-size:18px;color:var(--ink-soft);font-weight:800;}
  .ramp-slot .lucide{width:22px;height:22px;}
  .ramp-slot.filled{border-style:solid;border-color:var(--cat-sound);background:color-mix(in srgb, var(--cat-sound) 18%, var(--surface-2));}
  .ramp-ball{position:absolute;bottom:18px;width:22px;height:22px;border-radius:50%;background:var(--gold);box-shadow:0 2px 5px rgba(0,0,0,.25), inset -3px -3px 0 rgba(0,0,0,.12);transform:translateX(-50%);transition:left 1.6s cubic-bezier(.42,0,.58,1);}

  /* ---------- Carrera node (juego "carrera de reflejos") ---------- */
  .race-stage{position:relative;height:130px;margin:18px 0 6px;overflow:hidden;border-radius:14px;background:var(--surface-2);border:2px solid var(--line);}
  .race-ground{position:absolute;left:0;right:0;bottom:22px;height:3px;background:var(--line);}
  .race-robi{position:absolute;left:50%;bottom:22px;transform:translate(-50%,0);width:48px;height:48px;display:flex;align-items:center;justify-content:center;z-index:2;}
  .race-robi .lucide{width:38px;height:38px;color:var(--accent);}
  .race-robi.jumping{animation:raceJump .48s ease;}
  @keyframes raceJump{0%{transform:translate(-50%,0);}40%{transform:translate(-50%,-46px);}100%{transform:translate(-50%,0);}}
  .race-robi.stumble{animation:raceStumble .45s ease;}
  @keyframes raceStumble{0%,100%{transform:translate(-50%,0) rotate(0);}30%{transform:translate(-50%,4px) rotate(-8deg);}60%{transform:translate(-50%,4px) rotate(6deg);}}
  .race-lane{position:absolute;inset:0;}
  .race-obstacle{position:absolute;bottom:20px;left:104%;transform:translate(-50%,0);width:40px;height:40px;display:flex;align-items:center;justify-content:center;animation-name:raceApproach;animation-timing-function:linear;animation-fill-mode:forwards;}
  .race-obstacle .lucide{width:32px;height:32px;color:var(--cat-move);}
  .race-obstacle .race-ic{font-size:30px;line-height:1;}
  /* El obstáculo AVANZA hasta la posición de Robi (ahora 50%, centrada —
     antes 12%, pegada al borde izquierdo, ver .race-robi/.race-zone).
     Si NO se esquiva, se queda ahí (fill-mode:forwards + .hit lo pausa
     tal cual, "chocó con Robi"). Si SÍ se esquiva, carrera.js reemplaza
     esta animación por una propia que sigue deslizándolo MÁS ALLÁ de
     Robi y afuera de pantalla — antes simplemente se pausaba y se
     desvanecía en el punto exacto donde estaba al tocar "Salta", que en
     un toque tardío podía caer justo encima/detrás de Robi (mismo
     z-index más bajo que .race-robi) y se veía como si el obstáculo
     hubiera pasado "por debajo" de ella de golpe, sin ningún salto real
     de por medio — bug real reportado. Ahora, esquivarlo de verdad lo
     hace deslizarse (ver .race-obstacle.dodged en carrera.js) por
     DEBAJO de Robi mientras ella está en el aire — el mismo texto del
     reporte, pero como el efecto correcto en vez del bug. */
  @keyframes raceApproach{from{left:104%;}to{left:50%;}}
  .race-obstacle.hit{animation-play-state:paused;}
  .race-zone{position:absolute;left:50%;bottom:14px;transform:translateX(-50%);width:56px;height:56px;border-radius:50%;border:2px dashed color-mix(in srgb, var(--ink-soft) 40%, transparent);}
  .race-status{text-align:center;font-weight:800;color:var(--ink-soft);font-size:13px;margin-top:6px;}

  /* ---------- Story node (juego "aventura", sin fallar) ---------- */
  .story-scene{text-align:center;font-size:64px;line-height:1.1;margin:10px 0 4px;}
  .story-text{text-align:center;color:var(--ink-soft);font-size:15px;line-height:1.5;max-width:40ch;margin:0 auto 14px;}

  /* ---------- Puzzle node (juego "rompecabezas temático") ---------- */
  /* --line-fuerte y no --line: este borde de 3px es lo que delimita el
     tablero donde el niño arrastra las piezas, y sobre el fondo del panel
     --line contrasta 1.23:1 — se ve como si no hubiera tablero. */
  .puzzle-grid{display:grid;gap:4px;width:min(100%,260px);aspect-ratio:1/1;margin:16px auto 0;border-radius:14px;overflow:hidden;border:3px solid var(--line-fuerte);background:var(--surface-2);}
  .puzzle-tile{border:none;padding:0;cursor:pointer;background-repeat:no-repeat;outline-offset:-3px;}
  .puzzle-tile.sel{outline:3px solid var(--accent);}
  .puzzle-tile.correct{outline:3px solid var(--cat-sound);}

  /* ---------- Sequence node (juego "ordena la secuencia") ---------- */
  .sequence-chosen{display:flex;gap:8px;justify-content:center;margin:14px 0 10px;flex-wrap:wrap;}
  /* Las fichas dejan de ser cuadraditos de 56px: ahora llevan su texto
     debajo del dibujo, que es lo que hace el reto entendible (ver
     picConTexto en sequence.js). Ancho fijo para que la fila quede
     pareja, y alto libre para que quepa el texto sin recortarlo. */
  .sequence-slot{width:96px;min-height:76px;border-radius:var(--radius-md);border:2px dashed var(--line);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;padding:6px 5px;position:relative;color:var(--ink-soft);}
  .sequence-slot .num{position:absolute;top:-9px;left:-6px;background:var(--surface-2);border-radius:999px;width:18px;height:18px;font-size:10.5px;font-weight:800;display:flex;align-items:center;justify-content:center;color:var(--ink-soft);}
  .sequence-slot.filled{border-style:solid;border-color:var(--cat-sound);background:color-mix(in srgb, var(--cat-sound) 15%, var(--surface-2));}
  .sequence-slot .lucide{width:26px;height:26px;}
  .sequence-slot .sequence-ic{font-size:26px;line-height:1;}
  .sequence-pool{display:flex;flex-wrap:wrap;gap:10px;justify-content:center;}
  .sequence-item{width:96px;min-height:76px;border:none;border-radius:var(--radius-md);background:var(--surface-2);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;padding:6px 5px;cursor:pointer;font-family:inherit;color:inherit;box-shadow:0 0 0 1.5px var(--line), 0 3px 0 var(--line);transition:transform .1s ease;}
  .sequence-item:active{transform:translateY(2px);}
  .sequence-item .lucide{width:26px;height:26px;}
  .sequence-item .sequence-ic{font-size:26px;line-height:1;}
  .sequence-lbl{font-size:10.5px;font-weight:700;line-height:1.2;text-align:center;color:var(--ink);
    display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;}
  .sequence-slot .sequence-lbl{color:var(--ink-soft);}
  .sequence-slot.filled .sequence-lbl{color:var(--ink);}
  .sequence-item.wrong{background:color-mix(in srgb, var(--cat-move) 18%, var(--surface-2));box-shadow:0 0 0 1.5px var(--cat-move), 0 3px 0 var(--cat-move);}

  /* ---------- Drag & drop node (juego "arrastrar y soltar") ---------- */
  .dragdrop-targets{display:flex;gap:12px;justify-content:center;flex-wrap:wrap;margin:16px 0 18px;}
  .dragdrop-slot{width:76px;height:76px;border-radius:var(--radius-md);border:2px dashed var(--line);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;color:var(--ink-soft);padding:4px;}
  .dragdrop-slot .slot-label{font-size:10.5px;font-weight:700;text-align:center;line-height:1.15;}
  .dragdrop-slot.filled{border-style:solid;border-color:var(--cat-sound);background:color-mix(in srgb, var(--cat-sound) 15%, var(--surface-2));color:var(--ink);}
  .dragdrop-slot .lucide{width:24px;height:24px;}
  .dragdrop-slot .dragdrop-ic{font-size:24px;line-height:1;}
  .dragdrop-tray{display:flex;gap:12px;justify-content:center;flex-wrap:wrap;min-height:64px;}
  .dragdrop-piece{width:64px;height:64px;border-radius:var(--radius-md);background:var(--surface-2);display:flex;align-items:center;justify-content:center;cursor:grab;box-shadow:0 0 0 1.5px var(--line), 0 3px 0 var(--line);touch-action:none;-webkit-user-select:none;user-select:none;}
  .dragdrop-piece.dragging{cursor:grabbing;box-shadow:0 8px 18px rgba(0,0,0,.32);z-index:60;}
  .dragdrop-piece .lucide{width:28px;height:28px;}
  .dragdrop-piece .dragdrop-ic{font-size:28px;line-height:1;}

  /* ---------- Catch node (juego "atrápalo") ---------- */
  .catch-stage{position:relative;width:100%;height:230px;background:var(--surface-2);border-radius:var(--radius-md);border:2px solid var(--line);overflow:hidden;margin:14px 0 8px;}
  .catch-item{position:absolute;width:50px;height:50px;border:none;border-radius:50%;background:var(--surface);box-shadow:0 0 0 1.5px var(--line), 0 3px 0 var(--line);display:flex;align-items:center;justify-content:center;cursor:pointer;transform:translate(-50%,-50%);animation:catchPop .18s ease;}
  .catch-item .lucide{width:24px;height:24px;}
  .catch-item .catch-ic{font-size:24px;line-height:1;}
  .catch-item.good{background:color-mix(in srgb, var(--cat-sound) 30%, var(--surface));}
  .catch-item.bad{background:color-mix(in srgb, var(--cat-move) 25%, var(--surface));}
  /* Arranca en .92 y no en .5: el efecto de aparición se conserva, pero
     la casilla nunca baja de 46 px, ni siquiera durante la animación. */
  @keyframes catchPop{from{transform:translate(-50%,-50%) scale(.92);opacity:0;}to{transform:translate(-50%,-50%) scale(1);opacity:1;}}
  .catch-status{text-align:center;font-weight:800;color:var(--ink-soft);font-size:13px;}

  /* ---------- Cuenta regresiva de los retos con reloj ---------- */
  /* Tapa el escenario entero mientras cuenta, así el niño no puede tocar
     nada por accidente antes de que empiece de verdad. */
  .cuenta-regresiva{position:absolute;inset:0;z-index:5;border-radius:inherit;
    display:flex;align-items:center;justify-content:center;
    background:color-mix(in srgb, var(--surface-2) 86%, transparent);}
  /* --accent y --ok a secas quedaban en 2.48 y 2.82 de contraste sobre el
     velo en modo claro (medido en el navegador) — por debajo del mínimo
     hasta para texto grande. Mezclados con --ink conservan el color y
     suben a 4.97 y 5.43 en claro, 7.74 y 8.26 en oscuro. */
  .cuenta-regresiva-num{font-size:64px;font-weight:900;line-height:1;
    color:color-mix(in srgb, var(--accent) 55%, var(--ink));
    animation:cuentaLatido .55s ease-out;}
  .cuenta-regresiva-num.cuenta-regresiva-ya{font-size:42px;
    color:color-mix(in srgb, var(--ok) 55%, var(--ink));}
  @keyframes cuentaLatido{from{transform:scale(.45);opacity:0;}
    45%{transform:scale(1.12);opacity:1;} to{transform:scale(1);opacity:1;}}
  /* Con movimiento reducido igual se cuenta: lo que se quita es el rebote,
     no el aviso de que el reto está por empezar. */
  @media (prefers-reduced-motion: reduce){ .cuenta-regresiva-num{animation:none;} }

  /* ---------- Decision builder node ---------- */
  .decision-row{display:flex;flex-direction:column;gap:8px;margin:14px 0;}
  .decision-label{font-weight:800;font-size:13.5px;color:var(--ink-soft);text-transform:uppercase;letter-spacing:.03em;}
  .decision-opts{display:flex;gap:8px;flex-wrap:wrap;}
  .decision-opt{border:none;background:var(--surface);border-radius:12px;padding:11px 15px;font-weight:700;font-size:14px;cursor:pointer;box-shadow:0 0 0 1.5px var(--line), 0 3px 0 var(--line);transition:transform .1s ease;}
  .decision-opt:active{transform:translateY(3px);box-shadow:0 0 0 1.5px var(--line);}
  .decision-opt.sel{background:color-mix(in srgb, var(--cat-ai) 16%, var(--surface));box-shadow:0 0 0 1.5px var(--cat-ai), 0 3px 0 var(--cat-ai);}
  .decision-opt.right{background:color-mix(in srgb, var(--cat-sound) 20%, var(--surface));box-shadow:0 0 0 1.5px var(--cat-sound), 0 3px 0 var(--cat-sound);}
  .decision-opt.wrong{background:color-mix(in srgb, var(--cat-move) 18%, var(--surface));box-shadow:0 0 0 1.5px var(--cat-move), 0 3px 0 var(--cat-move);}
  /* Escena banda A: la condición ya resuelta, mostrada como dato (no como
     opción para elegir) — texto grande porque suele ser emoji + pocas
     palabras ("🪨 Ve un obstáculo"), fácil de leer o simplemente mirar. */
  .decision-scene{background:var(--accent-soft);border:2.5px solid var(--accent-border);border-radius:var(--radius-md);padding:16px;text-align:center;font-size:20px;font-weight:800;margin-top:6px;}

  /* ---------- Toast / XP popup ---------- */
  /* top usa env(safe-area-inset-top) — mismo patrón que .theme-toggle/
     .topbar-sticky-map más arriba — sin esto, en un celular con notch/isla
     dinámica (reportado por un usuario probando en su iPhone) el toast
     queda tapado detrás de esa zona en vez de aparecer debajo. */
  .toast-wrap{position:fixed;top:calc(16px + env(safe-area-inset-top));left:50%;transform:translateX(-50%);z-index:50;display:flex;flex-direction:column;gap:8px;align-items:center;pointer-events:none;}
  /* El aviso flotante, con los colores del tema.
  
     Antes era una píldora con el fondo y el texto ESCRITOS A MANO
     (#16283D y blanco). En tema claro pasaba; en oscuro era un rectángulo
     casi del mismo color que el fondo, y el usuario lo describió tal cual:
     "un cuadro todo feo que ni se alcanza a ver". Ese es el problema de
     un color fijo — solo puede estar bien en un tema.
  
     Ahora usa las mismas piezas que las tarjetas y los diálogos de la
     aplicación: superficie, borde y tinta del tema. Así el aviso se
     reconoce como parte de la misma casa, se lea donde se lea. */
  .toast{
    display:flex;align-items:center;gap:9px;
    background:var(--surface);color:var(--ink);
    border:1.5px solid var(--line);
    padding:12px 18px;border-radius:14px;
    font-weight:600;font-size:14px;line-height:1.45;
    max-width:min(90vw, 420px);text-align:left;
    box-shadow:0 6px 18px rgba(0,0,0,.22);
    opacity:0;transform:translateY(-8px);
    transition:opacity .25s ease, transform .25s ease;
  }
  .toast.show{opacity:1;transform:translateY(0);}
  /* Una franja de color a la izquierda dice de qué va el aviso sin gritar,
     y sin depender de que alguien lea la primera palabra. */
  .toast::before{content:'';width:4px;align-self:stretch;border-radius:999px;background:var(--accent);flex:none;}
  .toast.ok::before{background:var(--ok);}
  .toast.aviso::before{background:var(--gold);}
  .toast.error::before{background:var(--danger-border);}

  /* La ganancia de experiencia sí es una píldora: es una celebración
     pequeña, no un aviso que haya que leer con calma. */
  .xp-pop{
    background:var(--gold);color:#3B2A00;border-color:transparent;
    border-radius:999px;font-weight:800;
  }
  .xp-pop::before{display:none;}

  .confetti{position:absolute;font-size:22px;opacity:0;top:40%;left:50%;}
  .confetti.go{animation:pop .9s ease forwards;}
  @keyframes pop{0%{opacity:1;transform:translate(-50%,-50%) scale(.4);}100%{opacity:0;transform:translate(calc(-50% + var(--dx)), calc(-50% + var(--dy))) scale(1.1) rotate(var(--rot));}}

  .mascot-box{display:flex;gap:10px;align-items:flex-start;background:var(--accent-soft);border:2.5px solid var(--accent-border);border-radius:var(--radius-md);padding:11px;margin-top:12px;}
  .mascot-box svg, .mascot-box img{width:32px;height:32px;flex:none;object-fit:contain;}
  .mascot-box p{margin:0;font-size:13.5px;line-height:1.4;color:var(--accent-soft-ink);font-weight:600;}

  /* ---------- Staff / admin / teacher ---------- */
  .staff-choice-row{display:flex;gap:16px;flex-wrap:wrap;justify-content:center;max-width:640px;}
  .staff-choice-card{width:220px;background:var(--surface);border:3px solid var(--line);border-radius:var(--radius-lg);padding:20px;cursor:pointer;text-align:left;box-shadow:var(--shadow);}
  .staff-choice-card:hover{border-color:var(--accent-border);}
  .staff-choice-card .ic{font-size:30px;margin-bottom:8px;}
  .staff-choice-card h3{font-size:16px;margin-bottom:4px;}
  .staff-choice-card p{font-size:12.5px;color:var(--ink-soft);margin:0;line-height:1.4;}

  .panel-header{display:flex;align-items:center;gap:12px;margin-bottom:18px;flex-wrap:wrap;}
  .panel-header h1{font-size:22px;}
  .section-block{margin-bottom:26px;}
  .section-block h2{font-size:14px;text-transform:uppercase;letter-spacing:.05em;color:var(--ink-soft);margin-bottom:12px;}
  .list-card{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius-md);padding:14px 16px;display:flex;align-items:center;gap:12px;margin-bottom:10px;flex-wrap:wrap;}
  .list-card .grow{flex:1;min-width:160px;}
  .list-card .ttl{font-weight:800;font-size:14.5px;}
  .list-card .sub{font-size:12.5px;color:var(--ink-soft);}
  .empty-note{color:var(--ink-soft);font-size:14px;padding:16px;text-align:center;border:2px dashed var(--line);border-radius:var(--radius-md);}

  .switch-row{display:flex;align-items:center;gap:12px;padding:12px 0;border-bottom:1px solid var(--line);}
  .switch-row:last-child{border-bottom:none;}
  .switch-row .grow{flex:1;}
  .switch-row .ttl{font-weight:700;font-size:14px;}
  .switch-row .sub{font-size:12.5px;color:var(--ink-soft);}
  .switch{position:relative;display:inline-block;width:46px;height:26px;flex:none;}
  .switch input{opacity:0;width:0;height:0;}
  .slider{position:absolute;cursor:pointer;inset:0;background:var(--line);border-radius:999px;transition:.2s;}
  .slider:before{content:"";position:absolute;height:20px;width:20px;left:3px;top:3px;background:white;border-radius:50%;transition:.2s;}
  input:checked + .slider{background:var(--accent);}
  input:checked + .slider:before{transform:translateX(20px);}
  .num-input{width:80px;padding:8px 10px;border-radius:10px;border:1.5px solid var(--line);font-size:14px;text-align:center;}

  .grid-editor-modes{display:flex;gap:8px;flex-wrap:wrap;margin:12px 0;}
  .mode-btn{border:none;background:var(--surface);border-radius:12px;padding:10px 14px;font-weight:700;font-size:13px;cursor:pointer;box-shadow:0 0 0 1.5px var(--line), 0 3px 0 var(--line);transition:transform .1s ease;}
  .mode-btn:active{transform:translateY(3px);box-shadow:0 0 0 1.5px var(--line);}
  .mode-btn.sel{background:var(--accent-soft);box-shadow:0 0 0 1.5px var(--accent-border), 0 3px 0 var(--accent-border);}

  /* Acordeón de niveles del panel de profesor/admin (screens/teacher.js,
     renderTeacherWorldsRemote) — <details>/<summary> nativo, sin JS propio
     para abrir/cerrar. Mismo borde/espaciado que ya usa .list-group para
     que se sienta parte del mismo mundo visual, no un componente aparte. */
  /* applyTeacherSearch (screens/teacher.js) — cada fila de reto también
     trae .d-flex (Tabler: display:flex!important), así que ocultarla
     necesita su propio !important para ganar el empate. */
  /* Listado de instituciones del panel (admin.js → cargarInstituciones).

     En pantalla de teléfono la fila se parte en dos líneas: arriba el
     nombre, abajo el código y los botones. Con las filas sin ninguna
     separación entre sí, el hueco DENTRO de una fila acababa siendo mayor
     que el hueco ENTRE filas, y el ojo agrupaba el código con el colegio
     de abajo — visto en una captura a 375 px, no deducido. Una línea por
     fila y menos aire por dentro que por fuera lo resuelve sin depender
     de que quepan en una sola línea. */
  .institucion-fila{display:flex;align-items:center;gap:10px;flex-wrap:wrap;
    justify-content:space-between;padding:10px 0;border-bottom:1px solid var(--line);}
  .institucion-fila:last-child{border-bottom:none;}
  .institucion-codigo{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
  .institucion-codigo strong{font-size:17px;letter-spacing:.12em;font-variant-numeric:tabular-nums;}
  /* Estado de la institución (lo que la app y la puerta leen) y el
     detalle que se abre debajo de la fila para editar o ver el contrato. */
  .institucion-estado{display:inline-block;font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.04em;
    padding:2px 8px;border-radius:99px;background:var(--surface-2);color:var(--ink-soft);vertical-align:middle;}
  .institucion-estado.ok{background:var(--accent-soft);color:var(--accent-soft-ink);}
  .institucion-estado.aviso{background:#FBEFC7;color:#6A4B00;}
  .institucion-estado.mal{background:var(--danger-fill);color:var(--danger-ink);}
  .institucion-detalle{flex-basis:100%;border-top:1px dashed var(--line);margin-top:6px;padding-top:10px;}
  /* minmax(0,1fr) y no 1fr a secas: un 1fr no baja del ancho mínimo de
     su contenido, y tres campos numéricos —que traen ancho propio— en un
     celular sumaban 39 px más que la fila y la página entera se ensanchaba.
     Medido a 375 px antes de publicar. */
  .ct-dos{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:0 12px;}
  .ct-tres{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:0 12px;}
  @media (max-width:560px){ .ct-tres{grid-template-columns:repeat(2,minmax(0,1fr));} }
  .institucion-form input,.institucion-form select,.institucion-form textarea{min-width:0;width:100%;}
  .contrato-cabecera{display:flex;justify-content:space-between;gap:10px;flex-wrap:wrap;align-items:flex-start;margin-bottom:8px;}
  .contrato-acciones{display:flex;gap:6px;flex-wrap:wrap;}
  .huella-corta{font-family:ui-monospace,Menlo,monospace;}
  .contrato-concluido{border:1.5px solid var(--danger-border);background:var(--danger-fill);color:var(--danger-ink);border-radius:10px;padding:10px 12px;margin-bottom:12px;}
  .contrato-concluido .text-secondary{color:inherit;opacity:.85;}
  @media (max-width:520px){
    /* Apilado: el código pega con su nombre y la línea separa de la
       siguiente. El margen de arriba es menor que el padding de la fila,
       así que dentro siempre hay menos aire que fuera. */
    .institucion-fila{flex-direction:column;align-items:stretch;gap:4px;padding:12px 0;}
    .institucion-codigo{margin-top:2px;}
  }

  .node-row-hidden{display:none!important;}
  .tier-accordion{border-top:1px solid var(--line);}
  .tier-accordion:first-of-type{border-top:none;}
  .tier-accordion summary{cursor:pointer;padding:12px 20px;font-weight:700;font-size:13.5px;list-style:none;display:flex;align-items:center;gap:6px;user-select:none;}
  .tier-accordion summary::-webkit-details-marker{display:none;}
  .tier-accordion summary::before{content:'▸';display:inline-block;transition:transform .15s ease;color:var(--ink-soft);}
  .tier-accordion[open] summary::before{transform:rotate(90deg);}

  /* Mismo acordeón, aplicado al selector de TIPO de reto al crear uno
     nuevo (buildTypeChoices, screens/teacher.js) — 30 tipos en 8 familias
     ya no caben como un listado vertical sin fin; plegado por familia,
     una sola abierta a la vez si el profesor así lo prefiere. */
  .type-accordion summary{padding:10px 4px;font-size:12.5px;text-transform:uppercase;letter-spacing:.04em;color:var(--ink-soft);}
  .type-accordion .choice-grid{padding:2px 4px 12px;}
  .type-accordion-count{font-weight:400;text-transform:none;letter-spacing:normal;color:var(--ink-soft);}

  /* Un nivel más de plegado sobre el mismo <details>/<summary> nativo:
     el mundo ENTERO (renderTeacherWorldsRemote) — antes solo los niveles
     de adentro estaban plegados y los 5 mundos se pintaban siempre
     abiertos, uno atrás del otro. El <summary> ES el .card-header (ya
     trae su propio padding/flex de Tabler); solo hace falta ocultar el
     triangulito nativo del navegador y sumar la misma flechita ▾ ya
     usada en .tier-accordion, aplicada aquí a un ícono real en vez de un
     carácter de texto (mismo <svg> que el resto de la app). */
  .world-accordion > summary{list-style:none;user-select:none;}
  .world-accordion > summary::-webkit-details-marker{display:none;}
  .world-accordion-caret{display:inline-flex;transition:transform .15s ease;}
  .world-accordion-caret svg{width:16px;height:16px;}
  .world-accordion[open] .world-accordion-caret{transform:rotate(180deg);}

  /* Selector de emoji (screens/teacher.js, EMOJI_PICKER_GROUPS) — popover
     flotante que se abre al tocar cualquier campo marcado
     data-emoji-field, position:fixed fijado aquí (no inline) así el JS
     solo calcula left/top/bottom según el espacio disponible. */
  .emoji-picker-pop{position:fixed;z-index:80;width:300px;max-height:280px;overflow-y:auto;background:var(--surface);border-radius:14px;box-shadow:var(--shadow),0 12px 28px rgba(0,0,0,.18);padding:10px;border:1.5px solid var(--line);scrollbar-width:thin;scrollbar-color:var(--line-fuerte) transparent;}
  /* La barra por omisión sale como una franja blanca gruesa sobre el
     fondo oscuro del selector: se ve más que el contenido. */
  .emoji-picker-pop::-webkit-scrollbar{width:8px;}
  .emoji-picker-pop::-webkit-scrollbar-track{background:transparent;}
  .emoji-picker-pop::-webkit-scrollbar-thumb{background:var(--line-fuerte);border-radius:8px;border:2px solid var(--surface);}
  .emoji-picker-group{margin-bottom:8px;}
  .emoji-picker-buscar{display:flex;align-items:center;gap:6px;margin:0 0 8px;padding:6px 10px;border:1.5px solid var(--line);border-radius:10px;background:var(--bg);}
  .emoji-picker-buscar .lucide{width:16px;height:16px;color:var(--ink-soft);flex:none;}
  .emoji-picker-buscar input{flex:1;min-width:0;border:none;background:transparent;outline:none;font-size:14px;color:var(--ink);}
  .emoji-picker-buscar input::placeholder{color:var(--ink-soft);}
  .emoji-picker-group-label{font-size:10.5px;text-transform:uppercase;letter-spacing:.04em;color:var(--ink-soft);padding:2px 4px 4px;}
  .emoji-picker-grid{display:flex;flex-wrap:wrap;gap:2px;}
  /* font-size fijo en 22px servía mientras todo fuera un emoji de un
     carácter. El grupo "Ya en este reto" puede traer un texto corto
     ("8 ÷ 2", "2 y 2" — valores que el contenido usa de verdad), y eso
     se partía en dos líneas y se salía del botón. Ahora el texto se
     encoge y se recorta dentro del botón. */
  .emoji-picker-btn{border:none;background:none;font-size:22px;line-height:1;padding:6px;border-radius:10px;cursor:pointer;width:44px;height:44px;display:flex;align-items:center;justify-content:center;overflow:hidden;white-space:nowrap;}
  .emoji-picker-group .emoji-picker-btn{font-size:min(22px, 2.6em);}
  /* Un botón cuyo valor no es un solo carácter: se le baja el tamaño para
     que quepa la palabra completa en vez de recortarla a la mitad. */
  .emoji-picker-btn.texto-mode{font-size:11px;line-height:1.15;white-space:normal;font-variant-numeric:tabular-nums;}
  .emoji-picker-btn:hover,.emoji-picker-btn:focus-visible{background:var(--surface-2);outline:none;}
  .emoji-picker-btn.icon-mode svg{width:20px;height:20px;color:var(--ink);}
  /* Pestañas Emoji/Íconos — solo aparecen en campos marcados data-icon-ok
     (ver openEmojiPickerFor, teacher.js); los demás siguen mostrando solo
     la grilla de emoji, sin pestañas, comportamiento sin cambios. */
  .emoji-picker-tabs{display:flex;gap:4px;margin-bottom:8px;border-bottom:1px solid var(--line);padding-bottom:8px;}
  .emoji-picker-tab{flex:1;border:none;background:var(--surface-2);border-radius:8px;padding:6px 8px;font-size:12.5px;font-weight:600;color:var(--ink-soft);cursor:pointer;}
  .emoji-picker-tab.sel{background:var(--cat-move);color:var(--cat-move-ink);}
  /* Vista previa en vivo (wireEmojiPreviews, teacher.js) — siempre junto
     al campo, así el profesor nunca tiene que leer código para saber qué
     hay guardado (un emoji se ve tal cual; un ícono se dibuja de verdad,
     nunca su nombre en texto). */
  /* El ícono ES el control, no una decoración al lado de una caja de
     texto. Es un <button>, así que hay que apagarle el aspecto de botón
     del navegador y darle el propio. Alto mínimo 44px por la regla de
     zonas táctiles (ver verificar-zonas-tactiles): el cuadro visible
     mide 34, el resto lo pone el padding. */
  /* Ancho FIJO, no min-width: con min-width, "Seleccione…" estiraba su
     botón a 109px mientras el de al lado con un emoji medía 62, y las
     filas de una misma lista quedaban desalineadas — se veía como un
     error de maquetación. 78px entra "Seleccione…" a 10.5px y cualquier
     emoji o ícono con aire. Un valor más largo se recorta con puntos
     suspensivos en vez de estirar la fila. */
  .ic-emoji-preview{display:inline-flex;align-items:center;justify-content:center;width:78px;min-height:44px;padding:5px 6px;border:1px solid var(--line);border-radius:10px;background:var(--surface-2);color:var(--ink);font-size:18px;font-family:inherit;line-height:1.15;margin-left:0;vertical-align:middle;cursor:pointer;text-align:center;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;transition:border-color .12s,background .12s;}
  .ic-emoji-preview:hover{border-color:var(--accent);background:var(--surface);}
  .ic-emoji-preview:focus-visible{outline:2px solid var(--accent);outline-offset:2px;}
  .ic-emoji-preview-svg{width:18px;height:18px;color:var(--ink);}
  /* Estado vacío: el texto que pidió el usuario, no un cuadrito gris que
     no se distingue de un campo roto. */
  .ic-emoji-vacio{color:var(--ink-soft);font-size:10.5px;white-space:nowrap;letter-spacing:.01em;}
  /* Envuelve cada input[data-emoji-field] + su .ic-emoji-preview
     (wireEmojiPreviews, teacher.js) para que SIEMPRE queden pegados uno
     al otro, sin importar el text-align/layout del contenedor que los
     rodea — antes, el input (.form-control, display:block) y la vista
     previa (hermano suelto) podían terminar en líneas separadas, con la
     vista previa centrada sola en la suya dentro de .center-screen
     (bug real: "el selector aparece a un lado, como si no hubiera un
     orden", reporte de usuario al crear un mundo nuevo). */
  .ic-emoji-field-wrap{display:inline-flex;align-items:center;gap:4px;}
  /* Variante "swatch grande" (data-big-swatch="1" en el input — hoy solo
     el ícono de "Crear mundo"): mostrar el input de TEXTO Y la vista
     previa al lado era mostrar el mismo valor dos veces junto al
     selector de Color, que es un solo círculo clickeable — "por qué
     generas un textbox... deberia ser como el color, estas duplicando",
     reporte real de usuario. El input sigue existiendo (guarda el valor
     de verdad, sigue siendo tabulable con teclado) pero se reduce a un
     punto invisible; la vista previa pasa a ser el único control visible,
     mismo tamaño/forma que .avatar-swatch (44px, círculo) para que quede
     a la par del selector de Color. */
  /* El input se esconde SIEMPRE, no solo en la variante grande. Sigue en
     el DOM: guarda el valor, es tabulable y todo el código que lee
     .value o inp.nextElementSibling sigue funcionando. Lo que se va es la
     caja de texto, que prometía que ahí se escribe. */
  .ic-emoji-field-wrap{position:relative;}
  .ic-emoji-field-wrap input[data-emoji-field]{position:absolute;width:1px;height:1px;padding:0;margin:0;border:0;opacity:0;overflow:hidden;pointer-events:none;}
  .ic-emoji-preview-big{width:44px;height:44px;padding:0;border:0;border-radius:50%;font-size:24px;box-shadow:0 0 0 1.5px rgba(0,0,0,.15), 0 2px 0 rgba(0,0,0,.15);}
  .ic-emoji-preview-big .ic-emoji-vacio{font-size:9.5px;line-height:1.05;white-space:normal;padding:0 3px;}
  .ic-emoji-preview-big .ic-emoji-preview-svg{width:26px;height:26px;}


  /* Buscadores del panel. La lupa iba DENTRO del placeholder como emoji
     (🔎 Buscar...), que es lo que da sensación de aplicación pobre: un
     emoji haciendo de ícono, y encima desapareciendo en cuanto el usuario
     escribe la primera letra. Ahora es un ícono de verdad, dibujado como
     fondo del campo, que se queda mientras se escribe. Va como data URI
     porque un <use> del sprite no se puede usar de background-image; el
     trazo es el mismo lucide-search del sprite. currentColor no funciona
     dentro de un data URI, así que el color va escrito en cada tema. */
  input[type="search"].form-control{
    background-repeat:no-repeat;
    background-position:12px 50%;
    background-size:17px 17px;
    padding-left:37px;
    background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2351637A' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m21 21-4.34-4.34'/%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3C/svg%3E");
  }
  :root:not([data-theme="light"]) input[type="search"].form-control{
    background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239FB2C7' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m21 21-4.34-4.34'/%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3C/svg%3E");
  }
  :root[data-theme="dark"] input[type="search"].form-control{
    background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239FB2C7' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m21 21-4.34-4.34'/%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3C/svg%3E");
  }

  /* Campo señalado cuando las dos contraseñas no calzan. El aviso de texto
     vive al final del formulario, junto al botón, así que en un formulario
     largo —el de crear cuenta lo es: correo, contraseña, repetir, código de
     colegio, PIN, consentimiento— queda fuera de la vista del campo que
     falla. Esto lo marca donde ocurrió. Se apaga en cuanto se escribe. */
  .text-input.no-calza{border-color:var(--danger-border);background:var(--danger-fill);}
  .text-input.no-calza:focus{border-color:var(--danger-border);}

  /* Ver la contraseña escrita (cablearCampoDeContrasena, core/state.js).
     El wrap es inline-block y hereda el ancho del campo: los campos de
     contraseña de esta aplicación son .text-input, que ocupan todo el
     ancho disponible, así que el wrap tiene que ocuparlo también o el
     campo se encogería al meterlo dentro. */
  .pw-field{position:relative;display:block;width:100%;}
  .pw-field input{width:100%;padding-right:48px;}
  .pw-toggle{position:absolute;top:0;bottom:0;right:2px;margin:auto 0;width:44px;height:44px;display:flex;align-items:center;justify-content:center;background:none;border:0;border-radius:10px;color:var(--ink-soft);cursor:pointer;padding:0;}
  .pw-toggle:hover{color:var(--ink);background:var(--surface-2);}
  .pw-toggle:focus-visible{outline:2px solid var(--accent);outline-offset:-2px;}
  .pw-toggle .lucide{width:19px;height:19px;}

  /* Recordatorio de descanso. Robi grande y centrado, sin nada rojo ni
     de alarma: no está regañando, está proponiendo. */
  .descanso-card{text-align:center;max-width:340px;}
  .descanso-robi{width:78px;height:78px;object-fit:contain;margin:0 auto 6px;display:block;}
  .descanso-card h3{margin:4px 0 8px;font-size:18px;}
  .descanso-card p{color:var(--ink-soft);font-size:14.5px;line-height:1.55;margin:0 0 16px;}
  .descanso-botones{display:flex;flex-direction:column;gap:8px;}
  .descanso-card .btn-primary,.descanso-card .btn-secondary{width:100%;}

  /* Cabecera del modal de tutorial: antes era un emoji a 46px escrito
     como texto; ahora es el ícono del tipo de reto, del mismo tamaño. */
  /* El saludo por nombre del modal de instrucciones: se distingue de las
     líneas de instrucción sin gritar — es Robi hablándole al niño, no un
     paso que haya que seguir. */
  /* Las tres caritas del piloto. Grandes y separadas: las toca un niño de
     cinco años con el dedo, no un adulto con el ratón. La zona táctil
     mínima que usa el resto de la aplicación son 44 px; estas pasan de 90
     para que no haya forma de darle a la de al lado sin querer. */
  .piloto-caritas{display:flex;gap:10px;justify-content:center;}
  .piloto-carita{flex:1;min-width:90px;min-height:96px;border:none;cursor:pointer;
    background:var(--surface);border-radius:var(--radius-md);padding:12px 6px;
    box-shadow:0 0 0 1.5px var(--line), 0 3px 0 var(--line);
    display:flex;flex-direction:column;align-items:center;gap:6px;transition:transform .1s ease;}
  .piloto-carita:active{transform:translateY(3px);box-shadow:0 0 0 1.5px var(--line);}
  .piloto-cara{font-size:38px;line-height:1;}
  .piloto-texto{font-size:12px;font-weight:700;color:var(--ink-soft);text-align:center;}

  .tutorial-saludo{margin:0 0 12px;text-align:center;font-weight:700;color:var(--accent);font-size:15px;}
  .tutorial-icono{text-align:center;margin-bottom:6px;color:var(--accent);}
  .tutorial-icono .lucide{width:44px;height:44px;vertical-align:middle;}

  /* Selector de color compacto (Crear mundo, teacher.js): un solo botón
     .avatar-swatch que muestra el color elegido y abre un popover con
     las opciones — mismo patrón visual que el selector de ícono/emoji,
     para poder poner Ícono y Color lado a lado en vez de que el color
     ocupe siempre su propia fila de 6 círculos. */
  /* Pestaña "Escribir" del selector de ícono/emoji. */
  .emoji-picker-nota{font-size:11.5px;color:var(--ink-soft);margin:2px 0 8px;line-height:1.45;}
  .emoji-picker-escribir{display:flex;gap:6px;align-items:center;}
  .emoji-picker-escribir input{flex:1 1 auto;min-width:0;}
  .emoji-picker-vista{display:flex;align-items:center;gap:8px;margin-top:10px;font-size:11.5px;color:var(--ink-soft);}

  .color-picker-pop{position:fixed;z-index:70;background:var(--surface);border-radius:14px;box-shadow:0 8px 24px rgba(0,0,0,.25);padding:10px;display:flex;flex-wrap:wrap;gap:8px;max-width:220px;}

  /* El diálogo se centra sobre la parte VISIBLE de la pantalla, no sobre
     la pantalla entera.
  
     Con `inset:0` a secas, un elemento fijo mide el alto de la ventana —
     y en Android ese alto NO cambia cuando se abre el teclado: sigue
     siendo la pantalla completa. El resultado es que la tarjeta se centra
     respecto a un espacio que ya no existe y termina pegada al teclado,
     con los botones al borde. Lo reportó el usuario con una captura al
     escribir el código de 4 dígitos.
  
     `dvh` es la unidad que sí encoge con el teclado. Se declara DESPUÉS de
     la versión en `vh` a propósito: una WebView antigua que no la entienda
     descarta esa línea y se queda con el comportamiento de siempre, en vez
     de quedarse sin alto. */
  /* ---------- Capa de espera (Robi + barra) ----------
  
     Aparece mientras la app se pone al día con el servidor al iniciar
     sesión. Antes ese rato era una pantalla quieta: el usuario lo
     describió como "se demoró demasiado", y parte de "demasiado" es no
     saber si está pasando algo.
  
     La barra NO lleva porcentaje escrito. Un número invita a mirarlo
     avanzar, y eso alarga la espera; lo que hace falta es la señal de que
     algo se mueve. Aquí además el avance es REAL —son cuatro pasos
     contables— así que la barra no miente. */
  .capa-carga{
    position:fixed;inset:0;height:100vh;height:100dvh;z-index:80;
    background:var(--bg);
    display:flex;align-items:center;justify-content:center;padding:24px;
  }
  .capa-carga[hidden]{display:none;}
  .capa-carga-caja{text-align:center;max-width:300px;width:100%;}

  /* Robi no gira: un robot dando vueltas se lee como error. Flota, que es
     lo que hace en el resto de la aplicación. */
  .capa-carga-robi{
    width:84px;height:84px;object-fit:contain;
    filter:drop-shadow(0 4px 6px rgba(0,0,0,.25));
    animation:robiFlota 1.8s ease-in-out infinite;
  }
  @keyframes robiFlota{
    0%,100%{transform:translateY(0);}
    50%{transform:translateY(-9px);}
  }
  .capa-carga-titulo{font-size:16.5px;font-weight:700;margin:16px 0 4px;color:var(--ink);}
  .capa-carga-detalle{color:var(--ink-soft);font-size:13px;margin:0;min-height:1.3em;}

  .capa-carga-barra{
    height:5px;margin-top:18px;border-radius:999px;
    background:var(--muted-fill);overflow:hidden;
  }
  .capa-carga-barra > div{
    height:100%;width:0%;border-radius:999px;
    background:linear-gradient(90deg, var(--accent), var(--accent-hi));
    transition:width .4s ease;
  }
  /* Sin pasos que contar, la barra se pasea sola: quedarse clavada en cero
     parece un atasco. */
  .capa-carga-barra.indeterminada > div{
    width:40%;animation:barraPasea 1.1s ease-in-out infinite;transition:none;
  }
  @keyframes barraPasea{
    0%{margin-left:-40%;}
    100%{margin-left:100%;}
  }

  @media (prefers-reduced-motion: reduce){
    .capa-carga-robi{animation:robiLate 2.4s ease-in-out infinite;}
    @keyframes robiLate{50%{opacity:.5;}}
    .capa-carga-barra.indeterminada > div{animation:none;width:100%;opacity:.5;}
  }

  /* El alto lo manda la VENTANA VISUAL, no la pantalla: con el teclado
     arriba, iOS no encoge el layout —100dvh sigue valiendo la pantalla
     entera— y un cuadro centrado se centraba en un espacio que ya estaba
     tapado. Ver js/core/teclado.js, que publica estas dos variables. El
     100dvh queda de respaldo para un navegador sin visualViewport. */
  .survey-overlay{position:fixed;left:0;right:0;top:var(--tope-visible, 0px);
    height:var(--alto-visible, 100dvh);
    background:rgba(15,27,44,.55);display:flex;align-items:center;justify-content:center;
    z-index:60;padding:20px;animation:overlayFade .18s ease;overflow-y:auto;}
  /* Con el teclado arriba el hueco es bajo: el cuadro se ancla arriba y
     puede desplazarse. Centrado, un cuadro más alto que el hueco se corta
     por ARRIBA —por el título, que es lo que dice de qué se trata— y ese
     trozo no se puede recuperar desplazándose. */
  html.hay-teclado .survey-overlay{align-items:flex-start;
    /* La zona segura NO es opcional aquí. Anclar arriba con 10 px a secas
       metía el cuadro debajo de la isla dinámica del iPhone: el título
       quedaba tapado por la hora y la batería. Lo reportó el usuario con
       una captura, y con la observación correcta —"un cambio no puede
       dañar otra cosa"—: esto lo introduje al resolver que el teclado
       tapara el campo en el iPad, y de paso rompí el iPhone.
       max() y no una suma: en un aparato sin muesca la zona segura vale 0
       y aun así hace falta el margen de respiro. */
    padding-top:max(10px, env(safe-area-inset-top, 0px));
    padding-bottom:max(10px, env(safe-area-inset-bottom, 0px));}
  .survey-card{position:relative;background:var(--surface);border:3px solid var(--line);border-radius:var(--radius-lg);padding:26px;padding-bottom:0;max-width:400px;width:100%;text-align:center;box-shadow:var(--shadow);max-height:calc(100vh - 40px);max-height:calc(100dvh - 40px);max-height:calc(var(--alto-visible, 100dvh) - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px) - 20px);overflow-y:auto;animation:cardPop .25s cubic-bezier(.34,1.56,.64,1);}
  /* El aire de abajo lo pone un bloque, no el relleno de la tarjeta.

     Origen: el usuario reportó dos veces, con captura, que con el teclado
     abierto los botones del diálogo del código quedaban pegados al filo
     inferior. En su Redmi el hueco de abajo se veía como la cuarta parte
     del de arriba.

     HONESTIDAD SOBRE LA CAUSA: la primera explicación que escribí aquí
     —que la WebView de Android ignora el padding-bottom de un contenedor
     desplazable— es FALSA. Se comprobó después en un emulador ajustado a
     la resolución y densidad de esa tableta, en tema oscuro y con el
     teclado abierto: con el CSS anterior el hueco medía 29 px, igual que
     con este. El defecto no se reproduce ahí.

     Este bloque se conserva igualmente porque no cuesta nada y es más
     robusto que confiar en el relleno de un contenedor que a veces se
     desplaza: un bloque vacío ocupa sitio en cualquier motor y en los dos
     estados. Pero NO está demostrado que sea lo que arregla el síntoma
     del usuario. Si vuelve a verse, hay que medir dentro de esa tableta
     concreta (adb + CDP), no en el emulador. */
  .survey-card::after{content:'';display:block;height:26px;}

  /* Pantalla baja: el diálogo se aprieta para CABER.
  
     Ésta es la causa de verdad del "botones pegados al filo", medida
     dentro de la tableta del usuario con el teclado abierto: el teclado le
     deja 318 px de alto, el diálogo necesita 302 y en la tarjeta caben
     278. Se desborda por 24 px, se desplaza, y lo que se ve es la parte de
     arriba con los botones al borde — el aire de abajo queda fuera de
     pantalla.
  
     No se reproducía en el emulador porque allí el teclado dejaba 500 px.
     La diferencia es la densidad: esa tableta va a 280 ppp (dpr 1,75), así
     que su espacio lógico es bastante menor que el de un emulador a 240.
  
     Por eso el arreglo no es tocar el relleno, es recuperar los 24 px que
     faltan: menos margen alrededor y dentro de la tarjeta cuando la
     pantalla es baja. El umbral son 420 px, por encima del caso real (318)
     y por debajo de cualquier pantalla sin teclado. */
  @media (max-height: 420px){
    .survey-overlay{padding:8px;}
    /* El tope de la tarjeta también baja: seguía restando los 40 px del
       relleno grande y por eso el sitio que se ganaba arriba no llegaba a
       usarse. */
    .survey-card{padding:16px;padding-bottom:0;max-height:calc(100dvh - 16px);}
    .survey-card::after{height:16px;}
    .dialog-title{margin-bottom:6px;}
  }

  /* Botón "cerrar" en forma de X, esquina superior derecha — reemplaza al
     botón "Cerrar" de ancho completo al final de la tarjeta en los modales
     que son solo de lectura (Acerca de, Créditos): mismo cierre, sin
     gastar una fila entera de alto. Los modales que sí tienen una acción
     real (Cancelar/Enviar, por ejemplo el formulario de solicitud de
     datos) conservan su botón de abajo tal cual — ahí "Cancelar" no sobra,
     es la mitad de un par con sentido. */
  .modal-close-x{position:absolute;top:14px;right:14px;width:30px;height:30px;border-radius:50%;border:2px solid var(--line);background:var(--surface);color:var(--ink-soft);display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:var(--shadow);opacity:.75;transition:opacity .15s ease, color .15s ease, border-color .15s ease;}
  .modal-close-x:hover{opacity:1;color:var(--ink);border-color:var(--ink-soft);}
  .modal-close-x .lucide{width:16px;height:16px;}
  .survey-card.complete-card{max-width:420px;}

  /* ---------- EL RESULTADO DE UN RETO, COMO PIE ----------
     Antes el reto terminaba en dos pasos: una barra dentro del reto con
     su botón, y al tocarla una ventana centrada con las estrellas y OTRO
     botón. El dueño lo pidió en uno: "no un botón y luego otro botón".

     El pie va anclado abajo y sin velo, así que el reto se sigue viendo
     detrás — que es lo que hacía falta para que el desenlace de la
     aventura no quedara tapado, justo el trabajo que hacía la barra. */
  .survey-overlay.overlay-pie{
    align-items:flex-end;
    padding:0;
    background:transparent;
    /* El fondo no se traga los toques; solo la tarjeta responde. Lo que
       impide tocar el reto de atrás es `reto-resuelto`, que ya estaba. */
    pointer-events:none;
    overflow:visible;
  }
  .survey-overlay.overlay-pie > .survey-card{pointer-events:auto;}
  /* COMPACTO A PROPÓSITO. El pie sale solo, sin que nadie lo pida, así
     que lo que tape es lo que el niño pierde de vista de su propio
     trabajo. Ya estaba decidido —"un niño que arma algo tiene derecho a
     mirarlo antes de que se lo tapen"— y sigue valiendo: con el pie
     grande, la mitad de abajo del reto desaparecía en cuanto terminaba.
     De ahí los tamaños de esta tarjeta, más pequeños que los de la
     ventana centrada de la que viene. */
  .survey-card.resultado-pie{
    max-width:560px;width:100%;
    margin:0;
    /* SIN FILO Y SIN CURVA. Heredaba el borde de 3px de la ventana
       centrada de la que salió, y se veía por arriba y por los costados:
       "el footer tiene un filo, no debe tenerlo". Lo que lo separa del
       reto es su sombra y su fondo, no una línea dibujada. */
    border:none;
    border-radius:0;
    /* Un fondo propio, un punto por encima de la superficie normal, para
       que se lea como una capa distinta del reto que queda detrás sin
       necesidad de una raya. */
    background:linear-gradient(180deg, var(--surface-2) 0%, var(--surface) 60%);
    padding:12px 14px calc(12px + env(safe-area-inset-bottom));
    box-shadow:0 -8px 26px rgba(0,0,0,.34);
    animation:pieEntra .26s cubic-bezier(.34,1.4,.64,1);
    max-height:52vh;
    overflow-y:auto;
    text-align:left;
  }
  .survey-card.resultado-pie::after{display:none;}
  /* ROBI LLEGANDO A SALUDAR, al terminar la lección del día.
     Ocupa lo que ocupaba el 🎉 que había antes más un poco: la tarjeta no
     puede crecer tanto que el botón "Siguiente" se salga de pantalla en
     un teléfono, que es lo que pasa si se le da el tamaño natural de 512.
     Sin borde y sin fondo: el archivo se dibuja sobre transparente y así
     Robi aparece sobre la tarjeta, no dentro de una caja. */
  .robi-saluda{
    display:block;
    width:min(190px, 52vw);
    height:min(190px, 52vw);
    margin:0 auto 2px;
    border:0;
    background:transparent;
    /* No es un control: no se toca, no recibe el foco y no se puede
       desplazar por dentro. */
    pointer-events:none;
  }
  /* Con la pantalla baja —un teléfono en horizontal— manda que quepa el
     botón, así que Robi se encoge en vez de empujarlo fuera. */
  @media (max-height:560px){
    .robi-saluda{width:110px;height:110px;}
  }
  /* CON EL PIE ABIERTO, LA PANTALLA DE ATRÁS SE SIGUE DESPLAZANDO.
     Aquí vivían dos candados (overflow:hidden en el cuerpo y en la
     pantalla). Los puse como "la causa más probable" del doble toque en
     el botón de repetir, sin haber reproducido el fallo. La causa real
     era otra —touch-action:none en el cuerpo, que hace que iOS se coma el
     PRIMER toque de los botones del pie aunque el pie esté en
     touch-action:auto; medido tocando con el dedo en un iPhone 17: con la
     línea, dos toques; sin ella, uno—. Así que los candados nunca
     arreglaron nada, y sí quitaban algo que el dueño quería: poder
     deslizarse por el reto que queda debajo del pie para ver cómo le
     quedó. Se fueron, y que el primer toque sigue bastando se comprobó
     otra vez con el dedo.

     NI AQUÍ NI EN NINGÚN SITIO VUELVE touch-action:none sobre el cuerpo
     con el pie abierto: eso sí rompe el primer toque, y hay un
     verificador que lo impide. */
  /* ROBI HABLA, Y HABLA UNA SOLA VEZ.
     Antes el pie decía tres veces lo mismo —título, frase de ánimo y
     aviso— y ocupaba 358px, el 44% de la pantalla. Ahora Robi lo dice en
     su burbuja, igual que dentro del reto: una línea de título con las
     estrellas y los XP al lado, y una sola línea debajo. */
  .pie-cabecera{display:flex;align-items:flex-start;gap:10px;}
  .pie-cabecera .profesor-img{width:42px;height:42px;flex:none;margin:2px 0 0;object-fit:contain;}
  .pie-burbuja{
    flex:1;min-width:0;
    background:var(--accent-soft);
    border:1.5px solid var(--accent-border);
    /* La esquina de arriba a la izquierda sin curva: es la punta que
       apunta a quien habla, como cualquier bocadillo. */
    border-radius:4px 14px 14px 14px;
    padding:9px 12px;
  }
  .pie-titulo{
    display:flex;align-items:center;flex-wrap:wrap;gap:8px;
    margin:0 !important;font-size:15.5px;font-weight:800;line-height:1.2;
    color:var(--accent-soft-ink);
  }
  .pie-estrellas{display:inline-flex;gap:4px;}
  .pie-estrellas img{width:16px;height:16px;}
  /* En el pie el sello es del tamaño de una estrella: ahí acompaña al
     título, no preside una ventana como en su tamaño original. */
  .pie-estrellas .hecho-sello{width:20px;height:20px;margin:0;box-shadow:0 0 0 1.5px var(--accent-border);}
  .pie-estrellas .hecho-sello svg{width:13px;height:13px;}
  .pie-linea{margin:4px 0 0;font-size:12.5px;line-height:1.35;color:var(--accent-soft-ink);opacity:.92;}
  .pie-linea + .pie-linea{margin-top:3px;}
  .pie-linea svg{width:14px;height:14px;vertical-align:-2px;}
  .resultado-pie .xp-earned-pill{margin:0;padding:2px 9px;font-size:12px;}
  /* Una fila y una sola forma de botón: el principal con texto y los
     secundarios como cuadrados del mismo alto. Antes eran tres anchos
     distintos, uno partido en dos renglones y la estrella con forma de
     óvalo. */
  .resultado-acciones{display:flex;gap:8px;align-items:stretch;margin-top:11px;flex-wrap:nowrap;}
  .resultado-acciones .btn-primary{flex:1 1 auto;min-width:0;padding:11px 14px;font-size:15px;min-height:46px;}
  .resultado-acciones .btn-primary svg{flex:none;}
  .pie-icono{
    -webkit-appearance:none;appearance:none;font-family:inherit;cursor:pointer;
    flex:0 0 46px;width:46px;min-height:46px;
    display:inline-flex;align-items:center;justify-content:center;
    background:var(--muted-fill);color:var(--ink);
    border:none;border-radius:var(--radius-md);
    box-shadow:0 0 0 1.5px var(--muted-border), 0 3px 0 var(--muted-border);
    transition:transform .1s ease;
  }
  .pie-icono:active{transform:translateY(3px);box-shadow:0 0 0 1.5px var(--muted-border);}
  .pie-icono svg{width:20px;height:20px;}
  /* Sin marcar, la estrella se anuncia con el color de acción; marcada,
     manda el amarillo de .logro-favorito.marcado, que es el mismo que la
     aplicación usa ya para las estrellas. */
  .pie-icono.logro-favorito{color:var(--accent-text);}
  .pie-icono.logro-favorito.marcado{
    box-shadow:0 0 0 1.5px var(--cat-loop), 0 3px 0 var(--cat-loop);}
  @keyframes pieEntra{from{opacity:0;transform:translateY(18px);}to{opacity:1;transform:none;}}
  @media (prefers-reduced-motion:reduce){ .survey-card.resultado-pie{animation:none;} }
  /* CON EL PIE ABIERTO, LA BARRA DE DESTINOS SE VA.
     Antes el pie se subía 64px para no quedar debajo de "Mapa / Mi
     dominio". Pero 64 era un número escrito a mano, no el alto real de
     esa barra: en un iPhone con isla mide unos 96 —lleva el área segura
     dentro—, así que el pie quedaba flotando, con un trozo del reto
     asomando entre los dos. El dueño: "el footer sale súper despegado de
     abajo e incluso destapada la parte de abajo... hace ver la
     aplicación barata".
     Quitar la barra mientras se lee el resultado resuelve las dos cosas
     a la vez: el pie se apoya en el borde con el mismo margen SIEMPRE
     —el área segura y nada más— y no hay nada debajo que asome. Y la
     barra no hace falta ahí: el propio pie lleva su "Volver al mapa".
     Vuelve sola al cerrarse el pie (body.pie-abierto, ver router.js). */
  body.pie-abierto .bottom-nav{display:none;}
  body.pie-abierto{padding-bottom:0;}
  /* En pantallas muy estrechas el botón principal manda: los iconos se
     encogen antes que él, y su texto nunca se parte en dos renglones. */
  @media (max-width:360px){
    .resultado-acciones{gap:6px;}
    .pie-icono{flex:0 0 42px;width:42px;}
    .resultado-acciones .btn-primary{padding:11px 10px;font-size:14px;}
  }
  @keyframes overlayFade{from{opacity:0;}to{opacity:1;}}
  @keyframes cardPop{from{opacity:0;transform:scale(.9);}to{opacity:1;transform:scale(1);}}
  .survey-stars{display:flex;justify-content:center;gap:8px;margin:18px 0;}
  .survey-star{font-size:34px;cursor:pointer;color:var(--line);background:none;border:none;}
  .survey-star.on{color:var(--gold);}

  /* ---------- Avisos dentro del modal de "¡Reto completado!" ---------- */
  .xp-earned-pill{display:inline-flex;align-items:center;gap:5px;background:#FFF3D0;color:#8A6D00;font-weight:800;font-size:13.5px;padding:5px 14px;border-radius:999px;margin-top:10px;}
  .xp-earned-pill .lucide{width:15px;height:15px;}
  .complete-notices{margin-top:10px;text-align:left;background:var(--accent-soft);border:2px solid var(--accent-border);border-radius:var(--radius-md);padding:10px 12px;}
  .complete-notices p{margin:0;font-size:12.5px;color:var(--accent-soft-ink);line-height:1.4;}
  .complete-notices p + p{margin-top:6px;}

  /* Resalte temporal de la fila de una solicitud de datos, cuando un
     admin llega desde el botón "Revisar y responder" del correo (ver
     admin.js, revealDataRequestRow) — sin esto, la lista completa de
     solicitudes (pendientes + resueltas) no deja claro cuál de todas es
     la que motivó el correo. */
  .highlight-pulse{animation:highlightPulse 2.2s ease;}
  @keyframes highlightPulse{
    0%{box-shadow:0 0 0 3px var(--accent-hi);}
    70%{box-shadow:0 0 0 3px var(--accent-hi);}
    100%{box-shadow:0 0 0 0 transparent;}
  }

  @media (prefers-reduced-motion: reduce){
    .robot,.confetti,.speech,.avatar-card,.node-btn,.gear,.highlight-pulse{transition:none !important;animation:none !important;}
  }

  /* Tabler dark-mode patch: the vendored @scope stylesheet resolves --tblr-* custom
     properties correctly (verified via getComputedStyle) but a handful of Tabler
     component rules that read those properties through var() don't repaint with them
     in this browser's @scope implementation. Patched with the same dark-palette values
     Tabler itself defines, scoped to plain/uncolored buttons and form borders only —
     colored variants (.btn-teal, .btn-outline-danger, etc.) already render correctly. */
  .tabler-panel[data-bs-theme="dark"] .btn:not(.btn-teal):not(.btn-outline-danger):not(.btn-primary):not(.btn-secondary){background-color:#1f2937;color:#e5e7eb;border-color:#374151;}
  .tabler-panel[data-bs-theme="dark"] .btn:not(.btn-teal):not(.btn-outline-danger):not(.btn-primary):not(.btn-secondary):hover{background-color:#28374a;}
  .tabler-panel[data-bs-theme="dark"] .form-control{border-color:#374151;}

  /* Los bordes del panel eran invisibles en modo oscuro, y no por un
     descuido de un componente suelto: .card, .list-group-item, .table y
     el resto NO leen --tblr-border-color sino
     --tblr-border-color-translucent, que Tabler define en oscuro como
     rgba(72,110,149,.14). Ese valor está calculado para el gris de fondo
     de Tabler; sobre el azul mucho más oscuro de Robi Lab da un borde a
     un paso de no existir. Resultado: tarjetas sin contorno, imposible
     ver dónde empieza una y acaba otra.

     Se arregla en el TOKEN, no componente por componente: así lo hereda
     todo lo que ya usa Tabler y todo lo que se agregue mañana, sin que
     nadie tenga que acordarse. La opacidad sube de .14 a .55 sobre un
     tono más claro — suficiente para leerse contra #1f2937 (fondo de
     tarjeta) y contra el fondo de pantalla, sin convertir el panel en una
     cuadrícula de cajas.

     La auditoría de interfaz mide el contraste de verdad y falla si algún
     borde vuelve a quedar por debajo del mínimo.
     Va dentro de su PROPIO @scope, y esto no es adorno: la regla del
     paquete es `:scope[data-bs-theme=dark]` dentro de
     `@scope (.tabler-panel)`. Empata en especificidad con
     `.tabler-panel[data-bs-theme="dark"]` (las dos son 0,2,0), pero en la
     cascada la PROXIMIDAD DE ÁMBITO se compara antes que el orden de
     aparición: una regla con ámbito le gana a una sin ámbito aunque la
     nuestra venga después y en otro archivo. Escrita fuera del @scope no
     hacía absolutamente nada — comprobado midiendo la variable en vivo,
     seguía valiendo rgba(72,110,149,.14) después de recargar. Dentro del
     mismo ámbito las dos tienen proximidad 0, y ahí sí decide el orden. */
  @scope (.tabler-panel) {
    :scope[data-bs-theme=dark]{
      --tblr-border-color-translucent:rgba(148,175,208,.34);
      --tblr-border-color:#4a5b73;
    }
  }
  /* El mismo problema y el mismo motivo para la subnavegación propia: su
     borde salía de --line, que sobre el fondo del panel contrasta 1.23:1.
     Aquí no hay @scope de por medio, es una clase nuestra. */
  /* --line es el borde discreto de toda la aplicación y funciona bien
     dentro de una tarjeta; sobre el fondo del panel se pierde (1.23:1
     medido). --line-fuerte existe para eso: el mismo papel, con contraste
     suficiente para separar una cosa de otra en los dos temas. */
  .admin-subnav-btn{border-color:var(--line-fuerte);}
  /* Tabler's default white-on-teal .btn-teal text sits under WCAG AA (2.98:1) — darken the text for the same reason the app's own teal accent text was darkened earlier. */
  .tabler-panel .btn-teal{color:#04241F;}

  /* ==================== Reto "Robi ve el mundo real" (colorhunt) ====================
     El único reto que puede encender la cámara, así que su estilo tiene un
     trabajo extra además de verse bien: dejar clarísimo cuándo la cámara
     está activa. De ahí .ch-live-badge, que es un aviso permanente sobre el
     video y no un detalle decorativo — se ve incluso si el niño levanta el
     teléfono y solo mira de reojo.

     El "ojo" de Robi (.ch-robi-eye) es el mismo objeto en las dos puntas
     del reto: presenta el juego y lo cierra. Es una pupila que parpadea con
     una animación de dos tiempos; el cierre le suma un anillo de color que
     late (.ch-robi-happy), no un cartel de "ganaste". */
  .ch-intro{text-align:center;padding:26px 20px;}
  .ch-robi-eye{
    width:88px;height:88px;margin:0 auto 14px;border-radius:50%;
    background:radial-gradient(circle at 50% 45%, var(--surface) 0 46%, var(--accent-soft) 47% 100%);
    border:3px solid var(--accent-border);
    display:flex;align-items:center;justify-content:center;position:relative;
    animation:chBlink 4.2s infinite;
  }
  .ch-robi-pupil{
    width:30px;height:30px;border-radius:50%;
    background:var(--accent-border);
    box-shadow:inset 0 0 0 6px var(--accent);
  }
  @keyframes chBlink{
    0%,92%,100%{transform:scaleY(1);}
    95%{transform:scaleY(.12);}
  }
  .ch-robi-happy{animation:chBlink 4.2s infinite, chPulseRing 1.6s ease-out infinite;}
  @keyframes chPulseRing{
    0%{box-shadow:0 0 0 0 var(--accent-hi);}
    100%{box-shadow:0 0 0 18px transparent;}
  }
  .ch-intro-title{font-family:var(--font-display);font-size:21px;margin:0 0 8px;color:var(--ink);}
  .ch-intro-text{color:var(--ink-soft);font-size:15px;line-height:1.55;max-width:34em;margin:0 auto 12px;}
  /* inline-flex y no block: el botón lleva un ícono al lado del texto y
     con `block` el SVG se apoya en la línea base y queda descolgado. */
  .ch-big-btn{
    display:inline-flex;align-items:center;justify-content:center;gap:9px;
    width:100%;max-width:280px;margin:0 auto 10px;font-size:17px;padding:14px 18px;
  }
  .ch-big-btn .lucide{width:20px;height:20px;flex:none;}

  /* ---------- Aviso de permiso de cámara (primera vez) ---------- */
  .ch-permiso{text-align:left;}
  .ch-permiso .ch-intro-title{text-align:center;margin-bottom:4px;}
  .ch-permiso .ch-intro-text{text-align:center;}
  /* La pantalla de permiso se salía 11px del alto de un teléfono. Se
     aprieta el espaciado y NADA más: es un aviso de privacidad, y bajarle
     el tamaño de letra para ganar píxeles sería justo el atajo que no hay
     que tomar. */
  /* 12, no 6. Con 6, "Permitir la cámara" y "Ahora no" quedaban pegados —
     y aquí importa más que en ningún otro sitio: es una decisión de
     permiso, y tocar el que no era por falta de aire es justo lo que no
     puede pasar. Lo encontró la regla "sin-aire" de la auditoría, barriendo
     las 104 pantallas de reto después de arreglar el mismo defecto en
     "Busca la luz y la sombra". */
  .ch-permiso .ch-big-btn{margin-bottom:12px;}
  .ch-permiso .ch-reglas{margin:8px 0;}
  /* Etiqueta que declara de entrada a quién le habla la pantalla. Va
     arriba del título, no debajo, porque su trabajo es que un niño la lea
     y sepa que esto no es para él. */
  .ch-adulto{
    display:inline-flex;align-items:center;gap:6px;
    font-size:11.5px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;
    color:var(--accent-text);background:var(--accent-soft);
    padding:5px 11px;border-radius:999px;margin-bottom:12px;
  }
  .ch-adulto .lucide{width:13px;height:13px;}
  .ch-reglas{display:grid;gap:8px;margin:10px 0;}
  .ch-regla{
    display:flex;gap:10px;align-items:flex-start;
    font-size:13px;line-height:1.55;color:var(--ink-soft);
  }
  .ch-regla .lucide{width:17px;height:17px;flex:none;margin-top:2px;color:var(--accent-text);}
  .ch-alt-btn{display:block;margin:0 auto;}
  .ch-privacy-note{
    color:var(--ink-soft);font-size:12.5px;line-height:1.55;
    max-width:32em;margin:6px auto 0;
  }

  .ch-stage{padding:16px;}
  /* La consigna es lo primero que tiene que leerse: un niño de 5 años que
     todavía no lee de corrido se guía por el cuadrito de color (.ch-swatch)
     y por la palabra en ese mismo color. Por eso el color va DOS veces —
     como texto y como mancha— y no solo escrito. */
  .ch-ask{
    display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:8px;
    text-align:center;margin-bottom:12px;
  }
  .ch-ask-num{
    font-size:11.5px;letter-spacing:.06em;text-transform:uppercase;
    color:var(--ink-soft);width:100%;
  }
  .ch-ask-text{font-family:var(--font-display);font-size:20px;color:var(--ink);}
  .ch-swatch{
    width:26px;height:26px;border-radius:8px;display:inline-block;
    border:2px solid var(--line);box-shadow:var(--shadow);
  }
  .ch-ask-hint{width:100%;font-size:13px;color:var(--ink-soft);}

  .ch-viewport{
    position:relative;width:100%;max-width:420px;margin:0 auto;
    aspect-ratio:4/3;border-radius:var(--radius-lg);overflow:hidden;
    background:#0d1622;border:3px solid var(--line);
  }
  .ch-viewport video{width:100%;height:100%;object-fit:cover;display:block;}
  /* display:block le gana al atributo hidden (que solo trae display:none
     desde la hoja del navegador). Sin esta línea, ocultar el vídeo desde
     el código no oculta nada. */
  .ch-viewport video[hidden]{display:none;}
  /* El anillo marca EXACTAMENTE el cuadrado que el motor analiza (el 50%
     central del cuadro, ver renderCameraStage). Que coincidan importa: si
     el niño apunta a algo fuera del anillo y Robi "no lo ve", el juego
     parecería roto. */
  .ch-target-ring{
    position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
    width:50%;aspect-ratio:1;border-radius:50%;
    border:4px dashed rgba(255,255,255,.75);
    overflow:hidden;transition:border-color .2s ease;
  }
  .ch-ring-hot{border-color:var(--accent-hi);border-style:solid;}
  /* Se llena de abajo hacia arriba mientras el color se sostiene: el niño
     ve que "algo está pasando" antes de que el reto se dé por acertado, en
     vez de un salto sin aviso. */
  .ch-ring-fill{
    position:absolute;left:0;bottom:0;width:100%;height:0%;
    background:rgba(255,255,255,.32);transition:height .18s linear;
  }
  .ch-live-badge{
    position:absolute;left:10px;top:10px;
    display:inline-flex;align-items:center;gap:6px;
    background:rgba(12,20,32,.82);color:#fff;
    font-size:12px;font-weight:700;padding:5px 10px;border-radius:999px;
  }
  .ch-live-badge svg{width:14px;height:14px;}
  .ch-live-badge::after{
    content:"";width:7px;height:7px;border-radius:50%;background:#ff5b4a;
    animation:chRec 1.4s ease-in-out infinite;
  }
  @keyframes chRec{0%,100%{opacity:1;}50%{opacity:.25;}}

  .ch-sees{
    display:flex;align-items:center;justify-content:center;gap:9px;
    margin-top:12px;font-size:15px;color:var(--ink);
  }
  .ch-sees-dot{
    width:20px;height:20px;border-radius:50%;background:var(--line);
    border:2px solid var(--line);transition:background .2s ease;
  }
  .ch-stage-actions{display:flex;justify-content:center;margin-top:14px;}

  .ch-nocam-note{
    text-align:center;color:var(--ink-soft);font-size:13px;
    max-width:32em;margin:0 auto 14px;
  }
  .ch-objects{
    display:grid;grid-template-columns:repeat(3,1fr);gap:10px;
    max-width:340px;margin:0 auto;
  }
  .ch-object{
    font-size:40px;line-height:1;padding:14px 0;cursor:pointer;
    background:var(--surface-2);border:2px solid var(--line);
    border-radius:var(--radius-md);box-shadow:var(--shadow);
    transition:transform .12s ease;
  }
  .ch-object:hover{transform:translateY(-2px);}
  .ch-object.right{background:var(--accent-soft);border-color:var(--accent-border);}
  .ch-object.wrong{background:var(--danger-fill);border-color:var(--danger-border);}

  @media (max-width:420px){
    .ch-ask-text{font-size:18px;}
    .ch-objects{grid-template-columns:repeat(3,1fr);gap:8px;}
    .ch-object{font-size:34px;padding:12px 0;}
  }
  @media (prefers-reduced-motion: reduce){
    .ch-robi-eye,.ch-robi-happy,.ch-live-badge::after{animation:none !important;}
    .ch-ring-fill,.ch-object{transition:none !important;}
  }

  /* ==================== Ajustes de la cuenta (screens/settings.js) ====================
     Sustituye a un menú desplegable de seis filas que ya no daba más. Lo
     que un menú no podía hacer y estas reglas sí: separar los ajustes en
     grupos con título, mostrar el estado de cada uno en su propia fila, y
     dejar lo destructivo abajo con un marco que avisa antes de tocarlo. */
  /* ==================== Campos de código (PIN) ====================
     Reemplaza los estilos en línea que se repetían en cada diálogo, y de
     paso arregla algo que se veía mal: el letter-spacing que separa los
     puntos del código se aplicaba también al TEXTO DE AYUDA, así que
     "Repite el código" salía con las letras desparramadas y cortado a la
     mitad. El espaciado es formato del DATO; la ayuda es una frase normal
     y debe leerse como tal.

     La regla del ::placeholder va sobre .text-input entero, no solo sobre
     .pin-input, porque el mismo problema lo tenían el campo del código de
     institución y el de verificación de certificados — cualquier campo
     con espaciado propio lo habría heredado, incluidos los que se
     escriban después. */
  .text-input::placeholder{letter-spacing:normal;}
  .pin-input{
    text-align:center;letter-spacing:.5em;font-size:22px;
    /* El espaciado agrega un hueco después del ÚLTIMO carácter, que
       descentra visualmente lo escrito; se compensa con el mismo valor. */
    text-indent:.5em;
  }

  .settings-inner{max-width:620px;margin:0 auto;padding:22px 16px 60px;}
  /* El botón de tema es position:fixed en la esquina cuando no hay topbar
     (esta pantalla es una de esas), así que la cabecera se reserva su
     ancho a la derecha: sin eso el título "Ajustes de la cuenta" pasaba
     por debajo del botón en pantallas angostas. */
  .settings-head{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:22px;padding-right:52px;}
  .settings-head h1{font-size:23px;flex:1 1 auto;min-width:min(100%, 9em);}
  .settings-group{margin-bottom:26px;}

  /* La tarjeta del plan, arriba del todo en Ajustes. Es lo único de esta
     pantalla que cuesta dinero, y por eso es lo único que no es una fila:
     una fila más entre "Correo" y "Contraseña" diría que el plan es un
     ajuste como los demás.

     Ojo con el nombre: `.plan-card` YA existe y es el diálogo del muro
     (paywall.js). Esta es `.plan-resumen` para que las dos no se pisen. */
  .plan-resumen{
    background:var(--surface);border:1px solid var(--line);
    border-radius:14px;padding:16px;
  }
  /* Con plan, el borde y la corona se tiñen de dorado: la tarjeta entera
     se siente distinta, que es justo lo que se está pagando. */
  .plan-resumen-activo{border-color:color-mix(in srgb, var(--gold) 55%, transparent);}
  .plan-resumen-activo .plan-resumen-ic{color:var(--gold);}

  .plan-resumen-head{display:flex;align-items:flex-start;gap:13px;}
  .plan-resumen-ic{color:var(--accent);flex:none;line-height:0;margin-top:2px;}
  .plan-resumen-ic .lucide{width:22px;height:22px;}
  .plan-resumen-txt{display:flex;flex-direction:column;gap:4px;min-width:0;}
  .plan-resumen-txt b{font-size:15.5px;line-height:1.3;}
  .plan-resumen-txt small{color:var(--ink-soft);font-size:13px;line-height:1.5;}

  .plan-resumen-ventajas{list-style:none;margin:14px 0 0;padding:0;
    display:flex;flex-direction:column;gap:2px;}
  .plan-resumen-ventajas li{
    display:flex;align-items:flex-start;gap:9px;
    color:var(--ink-soft);font-size:13px;line-height:1.5;padding:4px 0;
  }
  .plan-resumen-ventajas li span{color:var(--accent);flex:none;line-height:0;margin-top:3px;}
  .plan-resumen-ventajas li .lucide{width:14px;height:14px;}
  /* Con plan las marcas pasan a verde: son cosas que YA tienes, no cosas
     que podrías tener. */
  .plan-resumen-activo .plan-resumen-ventajas li span{color:var(--ok);}

  .plan-resumen-cta{width:100%;margin-top:14px;}

  .settings-group-title{
    font-size:11.5px;font-weight:700;letter-spacing:.09em;text-transform:uppercase;
    color:var(--ink-soft);margin:0 0 8px 2px;
  }
  /* Cada fila es un botón de ancho completo: en un teléfono, el área
     tocable de un ajuste no puede ser solo su texto. */
  .settings-row{
    display:flex;align-items:center;gap:13px;width:100%;
    text-align:left;cursor:pointer;
    background:var(--surface);border:1px solid var(--line);
    border-radius:var(--radius-md);padding:14px 15px;margin-bottom:9px;
    transition:border-color .15s ease, transform .1s ease;
  }
  .settings-row:hover{border-color:var(--accent);}
  .settings-row:active{transform:translateY(1px);}
  .settings-row-ic{
    display:grid;place-items:center;flex:none;
    width:36px;height:36px;border-radius:11px;
    background:var(--accent-soft);color:var(--accent-soft-ink);
  }
  .settings-row-ic .lucide{width:18px;height:18px;}
  .settings-row-txt{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1 1 auto;}
  .settings-row-title{font-size:15px;font-weight:700;color:var(--ink);}
  .settings-row-desc{font-size:12.5px;color:var(--ink-soft);line-height:1.4;}
  /* El estado se trunca en vez de romper la fila: un correo largo no
     puede empujar el resto ni partirse en tres renglones. */
  .settings-row-state{
    font-size:12.5px;color:var(--ink-soft);flex:0 1 auto;
    max-width:38%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
    text-align:right;
  }
  /* Chevron reutilizando el ícono de "desplegar" girado 270°: apunta a la
     derecha, que es a donde lleva la fila. */
  .settings-row-chev{display:grid;place-items:center;flex:none;color:var(--ink-soft);}
  /* El interruptor de "Solo para probar": enseña el estado sin leer.
     Es el único de la aplicación, así que vive aquí y no en un archivo de
     componentes que tendría un solo inquilino. */
  /* Los detalles de la suscripción: pares dato/valor, uno por línea, con
     el valor a la derecha para que se lean en vertical de un vistazo. */
  .plan-detalles{margin:12px 0 0;padding:0;display:flex;flex-direction:column;gap:1px;
    background:var(--line);border-radius:10px;overflow:hidden;}
  .plan-detalles > div{display:flex;justify-content:space-between;align-items:baseline;gap:12px;
    background:var(--surface);padding:9px 11px;}
  .plan-detalles dt{margin:0;font-size:12.5px;color:var(--ink-soft);flex:none;}
  .plan-detalles dd{margin:0;font-size:13px;font-weight:700;text-align:right;}
  .plan-gestionar{width:100%;margin-top:10px;min-height:44px;display:inline-flex;align-items:center;justify-content:center;gap:8px;}
  .plan-gestionar .lucide{width:16px;height:16px;}
  .plan-detalles-nota{margin:8px 0 0;font-size:12px;line-height:1.4;color:var(--ink-soft);}
  .settings-interruptor{
    flex:none;width:46px;height:27px;border-radius:999px;
    background:var(--muted-fill);box-shadow:inset 0 0 0 1.5px var(--muted-border);
    display:flex;align-items:center;padding:3px;
    transition:background .18s ease, box-shadow .18s ease;
  }
  .settings-interruptor > span{
    width:21px;height:21px;border-radius:50%;background:var(--ink-soft);
    transition:transform .18s cubic-bezier(.34,1.4,.64,1), background .18s ease;
  }
  .settings-interruptor.on{background:var(--accent);box-shadow:inset 0 0 0 1.5px var(--accent-border);}
  .settings-interruptor.on > span{transform:translateX(19px);background:var(--accent-ink);}
  /* ACTIVO / INACTIVO en la propia línea: el estado se lee, no se deduce
     del verbo de un botón. */
  .plan-estado-chip{
    font-size:11.5px;font-weight:800;letter-spacing:.04em;
    padding:1px 7px;border-radius:999px;
    background:var(--muted-fill);color:var(--ink-soft);
    box-shadow:inset 0 0 0 1.5px var(--muted-border);
  }
  .plan-estado-chip.activo{background:var(--accent-soft);color:var(--accent-soft-ink);box-shadow:inset 0 0 0 1.5px var(--accent-border);}
  .settings-row-chev .lucide{width:16px;height:16px;transform:rotate(-90deg);}

  .settings-danger .settings-group-title{color:var(--danger-ink);}
  .settings-danger .settings-row{border-color:var(--danger-border);background:transparent;}
  .settings-danger .settings-row:hover{border-color:var(--danger-ink);}
  .settings-danger .settings-row-ic{background:var(--danger-fill);color:var(--danger-ink);}
  .settings-danger .settings-row-title{color:var(--danger-ink);}

  @media (max-width:480px){
    .settings-row-desc{display:none;}   /* en pantallas angostas manda el título y el estado */
    /* El estado baja DEBAJO del título en vez de pelearle el ancho: al
       lado, un correo largo obligaba a cortar los dos ("Correo
       electrónico" en dos renglones y "padre1@local.c…" truncado). El
       ícono ocupa las dos filas del área para no descolgarse. */
    .settings-row{
      display:grid;align-items:center;
      grid-template-columns:auto 1fr auto;
      grid-template-areas:"ic txt chev" "ic state chev";
      column-gap:13px;
    }
    .settings-row-ic{grid-area:ic;}
    .settings-row-txt{grid-area:txt;}
    .settings-row-chev{grid-area:chev;}
    .settings-row-state{grid-area:state;max-width:100%;text-align:left;margin-top:2px;}
  }
  @media (prefers-reduced-motion: reduce){
    .settings-row{transition:none !important;}
  }

  /* Título de diálogo con ícono: el ícono es un SVG y con `display:block`
     del h2 se apoyaría en la línea base, quedando descolgado respecto al
     texto. Reemplaza a los emojis 🔒/🔑 que llevaban estos títulos —
     misma información, sin el registro informal que desentona en una
     pantalla de seguridad. */
  .dialog-title{
    display:flex;align-items:center;gap:8px;
    font-size:17px;margin-bottom:8px;
  }
  .dialog-title .lucide{width:18px;height:18px;flex:none;color:var(--accent-text);}

  /* ---------- Vitrina en modo juego (5-6 años) ----------
     Ver renderShowcaseBandA en screens/node/showcase.js: a esta edad la
     vitrina deja de ser una lámina que se lee y pasa a ser un "busca la
     pieza". Las tarjetas se vuelven botones y pierden la descripción
     larga, así que caben más grandes y con el dibujo protagonista. */
  .showcase-ask{
    display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:6px;
    text-align:center;margin-bottom:14px;
  }
  .showcase-ask-num{
    width:100%;font-size:11.5px;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-soft);
  }
  .showcase-ask-text{font-family:var(--font-display);font-size:19px;color:var(--ink);}
  /* Tarjetas más chicas: son diez, y con el tamaño de la vitrina de
     lectura se iban 216px fuera de pantalla en un teléfono. La versión
     EXTRA seguía pasándose 24px después de ese primer ajuste — la medición
     de entonces solo miraba el reto principal, no su extra. */
  .showcase-grid.banda-a{grid-template-columns:repeat(auto-fill,minmax(84px,1fr));gap:6px;}
  .showcase-card.pick{padding:7px 5px;}
  .showcase-card.pick{
    cursor:pointer;border:2px solid var(--line);background:var(--surface);
    font-family:inherit;text-align:center;
    transition:transform .12s ease, border-color .15s ease, background .15s ease;
  }
  .showcase-card.pick:hover{transform:translateY(-2px);border-color:var(--accent);}
  .showcase-card.pick .ic{font-size:24px;margin-bottom:2px;}
  .showcase-card.pick h4{font-size:11px;margin:0;line-height:1.2;}
  .showcase-card.pick.right{background:var(--accent-soft);border-color:var(--accent-border);}
  .showcase-card.pick.wrong{background:var(--danger-fill);border-color:var(--danger-border);}
  @media (prefers-reduced-motion: reduce){ .showcase-card.pick{transition:none !important;} }

  /* ---------- Demos de masa/peso y volumen (5-6 años) ----------
     Ver renderMassWeightDemo y renderVolumeDemo en screens/node/concept.js:
     estos dos conceptos eran los últimos que a esa edad se resolvían con
     una pregunta de opción múltiple en vez de tocando algo. */
  .mw-stage{
    display:flex;align-items:center;justify-content:center;gap:22px;
    margin:18px 0;min-height:96px;
  }
  .mw-caja{
    font-size:30px;letter-spacing:2px;
    background:var(--surface-2);border:2px solid var(--line);
    border-radius:var(--radius-md);padding:14px 16px;
  }
  .mw-balanza{
    font-family:var(--font-display);font-size:26px;color:var(--ink);
    background:var(--accent-soft);color:var(--accent-soft-ink);
    border-radius:var(--radius-md);padding:14px 18px;min-width:96px;
    font-variant-numeric:tabular-nums;
  }
  .mw-cambia{animation:mwCambia .4s ease;}
  @keyframes mwCambia{
    0%{transform:scale(1);} 45%{transform:scale(1.18);} 100%{transform:scale(1);}
  }
  .mw-lugares{display:flex;gap:10px;justify-content:center;flex-wrap:wrap;}
  .mw-lugares button:disabled{opacity:.45;cursor:default;}
  .mw-nota{margin-top:14px;font-size:14.5px;color:var(--ink-soft);min-height:1.4em;}

  .vol-stage{display:flex;gap:26px;justify-content:center;margin:18px 0;}
  /* Los dos frascos son idénticos a propósito: si tuvieran tamaños
     distintos, la comparación la daría el dibujo y no lo que se echa. */
  .vol-frasco{
    position:relative;width:76px;height:110px;
    border:3px solid var(--line);border-top:none;
    border-radius:6px 6px 14px 14px;
    background:var(--surface-2);overflow:hidden;
    display:flex;flex-direction:column;justify-content:flex-end;
  }
  .vol-relleno{
    width:100%;height:0;background:var(--accent);
    transition:height .7s cubic-bezier(.4,1.2,.5,1);
  }
  .vol-etq{
    position:absolute;left:0;right:0;bottom:-26px;
    font-size:13px;font-weight:700;color:var(--ink-soft);text-align:center;
  }
  .vol-stage{padding-bottom:26px;}
  @media (prefers-reduced-motion: reduce){
    .mw-cambia{animation:none !important;}
    .vol-relleno{transition:none !important;}
  }

  /* ---------- Selector de edad al editar un perfil ----------
     Tres filas en vez de un desplegable: a diferencia de un <select>, deja
     ver las tres opciones a la vez con su nombre y su rango, que es
     exactamente lo que un padre viene a comprobar. */
  .edad-opts{display:grid;gap:8px;}
  .edad-opt{
    display:flex;align-items:center;gap:11px;width:100%;text-align:left;
    background:var(--surface);border:2px solid var(--line);
    border-radius:var(--radius-md);padding:10px 12px;cursor:pointer;
    font-family:inherit;transition:border-color .15s ease, background .15s ease;
  }
  .edad-opt:hover{border-color:var(--accent);}
  .edad-opt.sel{border-color:var(--accent-border);background:var(--accent-soft);}
  .edad-ic{font-size:22px;flex:none;}
  .edad-txt{display:flex;flex-direction:column;line-height:1.3;}
  .edad-txt strong{font-size:14.5px;color:var(--ink);}
  .edad-txt span{font-size:12.5px;color:var(--ink-soft);}
  .edad-opt.sel .edad-txt strong{color:var(--accent-soft-ink);}
  @media (prefers-reduced-motion: reduce){ .edad-opt{transition:none !important;} }

  /* Aviso del tangram: por qué una pieza no entró. Ocupa su sitio SIEMPRE
     (min-height) aunque esté vacío, para que al aparecer no empuje el
     tablero hacia abajo — un salto de la pantalla justo después de un
     intento fallido se siente como si algo se hubiera roto. */
  .tangram-aviso{
    min-height:1.5em;margin:6px 0 2px;text-align:center;
    font-size:13px;font-weight:700;color:var(--cat-move);
    opacity:0;transition:opacity .18s ease;
  }
  .tangram-aviso.visible{opacity:1;}
  @media (prefers-reduced-motion: reduce){ .tangram-aviso{transition:none !important;} }
  .tangram-tip{margin:0 0 6px;text-align:center;font-size:11px;color:var(--muted);}

  /* ---------- Muro de plan (js/screens/paywall.js) ----------
     Dos pantallas con tono deliberadamente distinto: la del niño es corta
     y sin una sola palabra de dinero; la del adulto lo dice todo. Ver el
     comentario de cabecera de paywall.js. */
  .node-btn.de-pago{
    background:var(--surface-2) !important;
    border:2px dashed var(--accent-border);
    color:var(--accent-text);
    opacity:.85;
  }
  .node-btn.de-pago .lucide{width:22px;height:22px;}
  .tier-divider.de-pago{color:var(--accent-text);}
  .tier-divider.de-pago .lucide{width:14px;height:14px;}

  .paywall-card{max-width:340px;text-align:center;}
  .plan-card{max-height:86vh;overflow-y:auto;}
  .paywall-robi img{display:block;margin:0 auto 8px;}
  .paywall-kid{color:var(--ink-soft);font-size:15px;line-height:1.55;margin:0 0 14px;}
  .paywall-adult-link{margin-top:8px;font-size:13px;}
  .paywall-adult-link .lucide{width:14px;height:14px;}

  .plan-card{max-width:460px;text-align:left;}
  .plan-estado{font-size:14px;margin:6px 0 12px;color:var(--ink-soft);display:flex;gap:6px;align-items:flex-start;}
  .plan-estado .lucide{width:16px;height:16px;flex:none;margin-top:2px;}
  .plan-estado.activo{color:var(--cat-sound);font-weight:700;}
  .plan-estado.aviso{color:var(--cat-loop);font-weight:700;}
  .plan-cols{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:10px 0;}
  .plan-col{border:2px solid var(--line);border-radius:12px;padding:10px 12px;background:var(--surface-2);}
  .plan-col.destacado{border-color:var(--accent-border);background:var(--accent-soft);color:var(--accent-soft-ink);}
  .plan-col h4{margin:0 0 6px;font-size:13.5px;}
  .plan-col ul{margin:0;padding-left:16px;}
  .plan-col li{font-size:12.5px;line-height:1.45;margin-bottom:3px;}
  /* La promesa de que nada se pierde va en el muro y no en la letra chica:
     es la duda concreta que frena a un padre —"si no pago, ¿le borran lo
     que hizo?"— y dejarla sin responder ahí mismo cuesta la conversión y,
     peor, la confianza. */
  .plan-nada-se-pierde{
    display:flex;gap:7px;align-items:flex-start;
    font-size:12.5px;line-height:1.5;color:var(--ink-soft);
    background:var(--surface-2);border-radius:10px;padding:9px 11px;margin:10px 0;
  }
  .plan-nada-se-pierde .lucide{width:15px;height:15px;flex:none;margin-top:2px;color:var(--cat-sound);}
  /* ---------- El campo del código de promoción ----------
     La clase .codigo-input existía en el HTML desde el principio y NO
     tenía ni una regla en toda la hoja: el navegador pintaba su caja
     blanca de fábrica, con su borde gris de sistema, en medio de una
     pantalla que por lo demás está tematizada. En modo oscuro era una
     mancha blanca. Esto es esa regla que faltaba.

     El campo se trata como lo que es: un código corto que se dicta y se
     teclea. Monoespaciada y con separación entre letras para que no se
     confundan O y 0 al copiarlo de un papel, en mayúscula, y con el
     grupo entero sobre su propia tarjeta para que se lea como una acción
     aparte de la compra y no como un renglón perdido debajo. */
  /* El "Cerrar" quedaba pegado a la caja del código, como si fuera parte
     de ella: "el botón de cerrar está muy pegado a la cajita de canjear".
     Separado y a lo ancho, que es lo que se espera de la salida de una
     ventana. */
  .plan-card > .btn-secondary{
    display:block;width:100%;margin-top:22px;min-height:46px;
  }
  .plan-promo{
    margin-top:14px;
    background:var(--surface-2);
    border:1px solid var(--line);
    border-radius:14px;
    padding:12px 13px 11px;
  }
  .plan-promo-label{
    display:flex;align-items:center;gap:6px;
    font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;
    color:var(--ink-soft);margin-bottom:8px;
  }
  .plan-promo-label .lucide{width:15px;height:15px;flex:none;color:var(--accent-text);}
  /* Apilados de salida, y en una fila solo cuando de verdad sobra ancho.
     Los códigos son largos de verdad —ROBI-3M-XXXXX-XXXXX-XXXXX-XXXXX,
     31 caracteres— y con el botón al lado el campo se quedaba en 145px:
     el propio ejemplo del recuadro salía cortado a media palabra. */
  .plan-promo-row{display:flex;flex-direction:column;gap:8px;align-items:stretch;}
  .plan-promo-campo{flex:1;min-width:0;display:flex;}
  .codigo-input{
    flex:1;min-width:0;width:100%;
    /* appearance:none quita el relieve interior que iOS le pone sola a
       todo input de texto, que es la mitad del aire "anticuado". */
    -webkit-appearance:none;appearance:none;
    background:var(--surface);
    color:var(--ink);
    border:1.5px solid var(--line);
    border-radius:11px;
    padding:10px 12px;
    /* 16px o más: por debajo de eso Safari en iPhone hace zoom solo al
       enfocar el campo y deja la pantalla torcida. */
    font-size:16px;
    font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
    letter-spacing:.06em;
    text-transform:uppercase;
    transition:border-color .15s ease, box-shadow .15s ease, background .15s ease;
  }
  .codigo-input::placeholder{
    color:var(--ink-soft);opacity:.55;letter-spacing:.06em;text-transform:none;
  }
  .codigo-input:hover{border-color:var(--line-fuerte);}
  .codigo-input:focus{
    outline:none;
    border-color:var(--accent);
    box-shadow:0 0 0 3px var(--accent-soft);
  }
  .codigo-input.tiene-texto{border-color:var(--accent-border);}
  .plan-promo-btn{flex:none;white-space:nowrap;border-radius:11px;}
  /* El botón apagado no se esconde: se apaga. Que esté ahí desde el
     principio le dice al padre qué va a pasar cuando termine de
     escribir, y no lo sorprende apareciendo debajo del dedo. */
  .plan-promo-btn:disabled{opacity:.45;cursor:default;box-shadow:none;transform:none;}
  @media (min-width:480px){
    .plan-promo-row{flex-direction:row;}
    .plan-promo-btn{align-self:stretch;}
  }
  .plan-promo-msg{min-height:1.2em;margin:8px 0 0;font-size:12.5px;}
  .plan-promo-msg.ok{color:var(--cat-sound);font-weight:700;}
  .plan-promo-msg.error{color:var(--cat-move);font-weight:700;}
  @media (max-width:420px){ .plan-cols{grid-template-columns:1fr;} }

  /* Marca los ajustes que existen solo para probar y que hay que
     acordarse de apagar. Color de peligro y no de acento: no es una
     novedad que promocionar, es algo encendido que no debería quedarse
     encendido. */
  .admin-aviso-chip{
    display:inline-block;margin-left:6px;vertical-align:1px;
    font-size:10.5px;font-weight:800;text-transform:uppercase;letter-spacing:.05em;
    color:var(--danger-ink);background:var(--danger-fill);
    border:1px solid var(--danger-border);border-radius:999px;padding:1px 7px;
  }

  /* ---------- Códigos de promoción (panel de admin) ---------- */
  .promo-form{display:grid;grid-template-columns:auto 1fr;gap:8px 10px;align-items:center;max-width:520px;margin-bottom:10px;}
  .promo-form label{font-size:12.5px;font-weight:700;color:var(--ink-soft);}
  .promo-form button{grid-column:1 / -1;justify-self:start;}
  .promo-msg{min-height:1.2em;margin:4px 0 10px;font-size:13px;font-family:var(--font-mono,ui-monospace,monospace);}
  .promo-msg.ok{color:var(--cat-sound);font-weight:700;}
  .promo-msg.error{color:var(--cat-move);font-weight:700;}
  .promo-table{font-size:12.5px;}
  .promo-code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11.5px;user-select:all;}
  .promo-copy{border:none;background:transparent;cursor:pointer;color:var(--ink-soft);padding:2px 4px;vertical-align:middle;}
  .promo-copy .lucide{width:13px;height:13px;}
  .promo-copy.copiado{color:var(--cat-sound);}
  /* Un código que ya no sirve se ve apagado, no escondido: la lista tiene
     que poder responder "¿este que le mandé sigue vivo?" sin filtrar. */
  .promo-row.anulado, .promo-row.agotado, .promo-row.caducado{opacity:.45;}
  /* «En espera» NO es lo mismo que muerto: el código está bien y va a
     servir, solo que todavía no. Por eso no lleva el opacity .45 de los
     otros tres — se distingue por su etiqueta, no por parecer apagado. */
  .promo-row.espera .promo-code{opacity:.7;}
  .promo-row.espera .promo-estado{color:var(--accent);font-weight:700;}
  .promo-row.anulado .promo-code{text-decoration:line-through;}
  .promo-estado{font-size:11.5px;color:var(--ink-soft);}
  @media (max-width:600px){ .promo-form{grid-template-columns:1fr;} }

  /* ---------- Reto "de tamaño real" (screens/node/arscale.js) ----------
     El visor imita lo que un niño espera de una aplicación de cámara: una
     mira en el centro y el número grande. La cámara es el fondo; el reto
     funciona igual sin ella (regla 4 de EVALUACION_REALIDAD_AUMENTADA). */
  .ars-prompt{color:var(--ink-soft);font-size:14.5px;line-height:1.5;margin:0 0 10px;}
  .ars-altura{text-align:center;}
  .ars-altura img{display:block;margin:0 auto 6px;}
  .ars-altura p{color:var(--ink-soft);font-size:14px;line-height:1.5;margin:0 0 10px;}
  .ars-altura-row{display:flex;align-items:center;justify-content:center;gap:10px;margin:10px 0;}
  .ars-altura-val{font-family:var(--font-display);font-size:34px;color:var(--ink);font-variant-numeric:tabular-nums;}
  .ars-altura-cm{font-size:15px;color:var(--ink-soft);}
  .ars-altura-row .btn-secondary{min-width:46px;font-size:20px;padding:6px 10px;}
  .ars-pista{font-size:12.5px;color:var(--muted);margin:6px 0 10px;}
  .ars-para-que{font-size:12px;color:var(--ink-soft);line-height:1.45;margin:0 0 10px;}

  /* 240x320 dejaba el reto 1px fuera de pantalla en el caso con la
     consigna más larga. El visor es lo único que se puede recortar sin
     que la mira deje de verse. */
  .ars-visor{
    position:relative;width:min(100%,222px);aspect-ratio:3/4;margin:6px auto;
    border-radius:16px;overflow:hidden;background:var(--surface-2);
    border:3px solid var(--line);
  }
  .ars-visor video{width:100%;height:100%;object-fit:cover;display:block;}
  /* El display:block de arriba le GANA al atributo hidden (que solo
     pone display:none desde la hoja del navegador, con menos
     especificidad). Sin esta línea, ocultar el vídeo desde el código
     no oculta nada — y el vídeo vacío es lo que hace que la WebView de
     Android componga mal el ojo de Robi. */
  .ars-visor video[hidden]{display:none;}
  .ars-mira{position:absolute;inset:0;pointer-events:none;
    background:
      linear-gradient(to right, transparent 49%, color-mix(in srgb, var(--accent) 70%, transparent) 49% 51%, transparent 51%),
      linear-gradient(to bottom, transparent 49%, color-mix(in srgb, var(--accent) 70%, transparent) 49% 51%, transparent 51%);}
  /* Cámara apagada: el mismo ojo de Robi que usa colorhunt, no un icono
     gris. Es el estado que ve TODO el que no dio permiso de cámara, así
     que es la primera impresión del reto y tiene que verse bien. */
  .ars-camoff{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;color:var(--ink-soft);font-size:12.5px;text-align:center;padding:10px;
    background:radial-gradient(120% 90% at 50% 22%, var(--accent-soft) 0%, var(--surface-2) 70%);}
  .ars-camoff .ch-robi-eye{width:64px;height:64px;margin:0;}
  .ars-camoff .ch-robi-pupil{width:22px;height:22px;box-shadow:inset 0 0 0 5px var(--accent);}
  .ars-visor.con-camara .ars-mira{opacity:.9;}
  /* Aviso de cámara encendida: regla 3, y por eso es fijo y de alto
     contraste, no un detalle decorativo que se pueda perder de vista. */
  .ars-camon{
    position:absolute;top:6px;left:6px;display:flex;align-items:center;gap:5px;
    background:var(--cat-move);color:#fff;font-size:11px;font-weight:800;
    padding:3px 8px;border-radius:99px;
  }
  .ars-camon .lucide{width:12px;height:12px;}
  .ars-lectura{
    position:absolute;bottom:6px;left:50%;transform:translateX(-50%);
    background:rgba(0,0,0,.55);color:#fff;font-family:var(--font-display);
    font-size:24px;padding:2px 14px;border-radius:99px;font-variant-numeric:tabular-nums;
  }
  .ars-slider{width:100%;margin-top:6px;}
  .ars-controles{display:flex;gap:8px;justify-content:center;flex-wrap:wrap;margin-top:8px;}

  /* ---------- Escena a tamaño real ----------
     El niño ve la silueta del animal sobre su propio cuarto, con la
     línea del techo donde le toca. Todo se posiciona en % del alto de
     la escena, que es lo que hace que la proporción sea la de verdad:
     si se usaran píxeles, la misma escena mentiría en cada pantalla. */
  .ars-escena{
    position:relative;width:100%;aspect-ratio:3/2;margin:8px auto 10px;  /* lo fija ASPECTO_ESCENA en arscale.js */
    border-radius:16px;overflow:hidden;background:var(--surface-2);
    border:3px solid var(--line);
  }
  .ars-escena video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;}
  /* display:block le gana al atributo hidden (que solo trae display:none
     desde la hoja del navegador). Sin esta línea, ocultar el vídeo desde
     el código no oculta nada. */
  .ars-escena video[hidden]{display:none;}
  /* Sin cámara la escena no queda vacía: un suelo y un cielo dibujados.
     Con cámara, este mismo fondo se atenúa y hace de velo para que la
     silueta se lea sobre cualquier pared. */
  .ars-escena-fondo{
    position:absolute;inset:0;
    background:linear-gradient(to bottom, var(--accent-soft) 0%, var(--surface-2) 72%, var(--surface) 72%, var(--surface) 100%);
  }
  .ars-escena.con-camara .ars-escena-fondo{
    background:linear-gradient(to bottom, rgba(0,0,0,.34), rgba(0,0,0,.12) 60%, rgba(0,0,0,.42));
  }
  .ars-techo{
    position:absolute;left:0;right:0;height:0;
    border-top:2px dashed var(--cat-loop);
    display:flex;justify-content:flex-start;align-items:flex-start;pointer-events:none;
  }
  /* La etiqueta cuelga POR DEBAJO de la línea: cuando el techo queda
     alto —que es lo normal— una etiqueta por encima se sale de la
     escena y overflow:hidden se la come. */
  .ars-techo span{
    background:var(--cat-loop);color:var(--surface);
    font-size:10.5px;font-weight:800;padding:2px 7px;
    white-space:nowrap;margin-left:8px;
  }
  .ars-techo.abajo span{border-radius:0 0 6px 6px;}
  .ars-techo.arriba span{transform:translateY(-100%);border-radius:6px 6px 0 0;}
  .ars-suelo{
    position:absolute;left:0;right:0;top:0;
    display:flex;align-items:flex-end;justify-content:flex-end;gap:7%;
    padding:0 6%;pointer-events:none;
  }
  /* position:relative + etiqueta absoluta: si la etiqueta viviera dentro
     del flujo, empujaría la silueta hacia arriba tantos píxeles como
     midiera, y dos etiquetas de distinto número de líneas dejarían a las
     dos figuras apoyadas a distinta altura. La escala dejaría de ser
     real, que es lo único que este reto tiene que garantizar. */
  .ars-figura{display:flex;flex-direction:column;align-items:center;justify-content:flex-end;height:100%;flex:0 1 auto;min-width:0;position:relative;}
  .ars-sil{display:block;width:auto;max-width:100%;overflow:visible;}
  .ars-sil g{fill:var(--accent);}
  .ars-sil-nino g{fill:color-mix(in srgb, var(--ink) 62%, transparent);}
  .ars-escena.con-camara .ars-sil g{fill:var(--accent);}
  .ars-escena.con-camara .ars-sil-nino g{fill:rgba(255,255,255,.72);}
  /* Los rótulos van DEBAJO de la escena, no dentro: dentro se recortaban
     contra el borde cada vez que la figura era más estrecha que su propio
     texto, que es siempre que el niño está al lado de algo grande. */
  .ars-pies{display:flex;flex-wrap:wrap;gap:6px;justify-content:center;margin:6px 0 2px;}
  .ars-pie{
    font-size:11px;font-weight:800;line-height:1.3;
    padding:3px 9px;border-radius:999px;border:1px solid var(--line);
  }
  .ars-pie.nino{color:var(--ink-soft);background:var(--bg);}
  .ars-pie.bicho{
    color:var(--ink);
    background:color-mix(in srgb, var(--accent) 20%, var(--bg));
    border-color:color-mix(in srgb, var(--accent) 45%, var(--line));
  }
  .ars-veredicto{
    margin:8px 0 0;font-size:13.5px;line-height:1.45;color:var(--ink);
    padding:9px 11px;border-radius:10px;border:1px solid var(--line);background:var(--bg);
  }
  /* El veredicto dice si CABE, no si el niño acertó —eso ya lo dice el
     botón—. Por eso no usa el verde/rojo de acierto y error: un "No
     cabe" en rojo junto a un botón verde se lee como una contradicción.
     Ambos van en el ámbar de la medida, el mismo de la línea del techo. */
  .ars-veredicto.si-cabe,
  .ars-veredicto.no-cabe{border-color:var(--cat-loop);background:color-mix(in srgb, var(--cat-loop) 12%, var(--bg));}
  .ars-dato{display:block;margin-top:3px;color:var(--ink-soft);font-size:12.5px;}
  .ars-resumen{font-size:14px;font-weight:700;color:var(--ink);margin:10px 0 8px;}

  .ars-barras{display:flex;flex-direction:column;gap:6px;margin:10px 0;}
  .ars-barra{position:relative;height:24px;border-radius:8px;background:var(--bg);border:1px solid var(--line);overflow:hidden;}
  /* El relleno tiene que CONTRASTAR con la pista, o las barras se ven
     todas iguales y la comparación —que es el reto entero— desaparece.
     --muted-fill es casi el mismo color que el fondo de la barra. */
  .ars-barra-fill{position:absolute;inset:0 auto 0 0;background:color-mix(in srgb, var(--accent) 52%, var(--surface));}
  .ars-barra.yo .ars-barra-fill{background:color-mix(in srgb, var(--accent) 62%, var(--surface));}
  .ars-barra.medido .ars-barra-fill{background:color-mix(in srgb, var(--cat-loop) 55%, var(--surface));}
  .ars-barra.cabe .ars-barra-fill{background:color-mix(in srgb, var(--cat-sound) 55%, var(--surface));}
  .ars-barra-txt{position:relative;display:block;line-height:24px;padding-left:8px;font-size:12px;font-weight:700;color:var(--ink);}
  .ars-pregunta{font-size:14.5px;font-weight:700;color:var(--ink);margin:10px 0 6px;}

  /* ---------- Medidor de luz (screens/node/lightcam.js) ----------
     La barra con la zona objetivo pintada encima es el reto: el número
     solo no dice si vas bien, y ver hacia dónde hay que llegar mientras
     mueves el teléfono es la diferencia entre buscar y adivinar. */
  .lc-prompt{color:var(--ink);font-size:14.5px;font-weight:700;line-height:1.4;margin:0 0 7px;}
  /* Compactado tras medir: con la lista de lugares de la versión sin
     cámara (cuatro botones), el reto se salía hasta 41px de la pantalla. */
  .lc-visor{position:relative;width:min(100%,164px);aspect-ratio:4/3;margin:0 auto 7px;
    border-radius:14px;overflow:hidden;background:var(--surface-2);border:3px solid var(--line);}
  .lc-visor video{width:100%;height:100%;object-fit:cover;display:block;}
  /* El display:block de arriba le GANA al atributo hidden (que solo
     pone display:none desde la hoja del navegador, con menos
     especificidad). Sin esta línea, ocultar el vídeo desde el código
     no oculta nada — y el vídeo vacío es lo que hace que la WebView de
     Android componga mal el ojo de Robi. */
  .lc-visor video[hidden]{display:none;}
  /* Mismo ojo que en arscale, escalado a este visor, que es más pequeño
     (164px de ancho frente a los de arscale). Sin achicarlo, los 88px de
     .ch-robi-eye no caben y se comerían el texto de debajo. */
  .lc-camoff{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:7px;color:var(--ink-soft);font-size:11.5px;text-align:center;padding:0 8px;}
  .lc-camoff .ch-robi-eye{width:52px;height:52px;margin:0;border-width:2.5px;}
  .lc-camoff .ch-robi-pupil{width:18px;height:18px;box-shadow:inset 0 0 0 4px var(--accent);}
  .lc-camon{position:absolute;top:5px;left:5px;display:flex;align-items:center;gap:4px;
    background:var(--cat-move);color:#fff;font-size:10.5px;font-weight:800;padding:2px 7px;border-radius:99px;}
  .lc-camon .lucide{width:11px;height:11px;}

  .lc-barra{position:relative;height:22px;border-radius:99px;overflow:hidden;
    background:linear-gradient(to right, #10151F, #6B7A8F, #FFF3C4);
    border:2px solid var(--line);margin:2px 0 4px;}
  /* Lo que la cámara ya ha llegado a ver en este cuarto. Va DEBAJO de la
     zona objetivo y sin borde: es contexto ("de aquí a aquí llega Robi"),
     no una meta, y con borde competiría con la zona verde. */
  .lc-explorado{position:absolute;top:0;bottom:0;background:rgba(255,255,255,.14);}
  .lc-zona{position:absolute;top:0;bottom:0;background:color-mix(in srgb, var(--cat-sound) 55%, transparent);
    border-left:2px solid var(--cat-sound);border-right:2px solid var(--cat-sound);}
  .lc-aguja{position:absolute;top:-3px;bottom:-3px;width:4px;left:0;background:var(--ink);
    box-shadow:0 0 0 2px var(--surface);transition:left .18s ease;}
  @media (prefers-reduced-motion: reduce){ .lc-aguja{transition:none !important;} }
  .lc-lectura{text-align:center;margin:0 0 4px;}
  .lc-lectura #lcValor{font-family:var(--font-display);font-size:22px;color:var(--ink);font-variant-numeric:tabular-nums;}
  .lc-unidad{font-size:12px;color:var(--ink-soft);margin-left:5px;}
  .lc-estado{min-height:1.3em;margin:0 0 6px;text-align:center;font-size:13px;font-weight:700;}
  .lc-estado.bien{color:var(--cat-sound);}
  .lc-estado.mal{color:var(--cat-loop);}
  .lc-controles{display:flex;gap:8px;justify-content:center;flex-wrap:wrap;margin-bottom:10px;}
  /* Los lugares son cuatro y van uno debajo de otro: más apretados que un
     .tap-choice normal, o no caben con el visor encima. */
  #lcSinCamara{gap:6px;margin-top:0;}
  #lcSinCamara .tap-choice{padding:9px 13px;font-size:13.5px;}
  /* Nivel terminado pero sin las estrellas que pide el diploma. Se ve
     distinto de "todavía no lo terminas": está a un paso, no bloqueado. */
  .tier-pill.casi{border-color:var(--cat-loop);color:var(--cat-loop);}
  .tier-pill.casi small{color:var(--cat-loop);font-weight:800;}

  /* Compra del plan dentro de la aplicación (js/core/compra.js). El botón
     del plan anual va primero y en primario: es el que conviene a las dos
     partes —caja por adelantado para el negocio, 37% de ahorro para la
     familia— y esconderlo debajo sería fingir neutralidad. */
  .plan-comprar{margin-top:10px;}
  .plan-comprar-btn{display:block;width:100%;text-align:center;margin-bottom:7px;line-height:1.25;}
  .plan-comprar-btn small{display:block;font-size:11.5px;font-weight:600;opacity:.85;margin-top:2px;}
  .plan-tienda-nota{font-size:11.5px;color:var(--muted);margin:2px 0 0;text-align:center;}
  .plan-solo-app{
    display:flex;gap:7px;align-items:flex-start;
    font-size:12.5px;line-height:1.5;color:var(--ink-soft);
    background:var(--surface-2);border-radius:10px;padding:9px 11px;margin:0;
  }
  .plan-solo-app .lucide{width:15px;height:15px;flex:none;margin-top:2px;color:var(--accent-text);}
  .plan-compra-msg{min-height:1.2em;margin:5px 0 0;font-size:12.5px;text-align:center;}
  .plan-compra-msg.ok{color:var(--cat-sound);font-weight:700;}
  .plan-compra-msg.error{color:var(--cat-move);font-weight:700;}
  /* Reto publicado después de que el niño terminó su nivel. Se marca en
     el mapa porque un nivel con ✓ y retos pendientes dentro parece un
     error de cuentas; con la marca es una buena noticia. */
  .nodo-nuevo{
    position:absolute;top:-6px;left:50%;transform:translateX(-50%);
    background:var(--cat-sound);color:#06301D;
    font-size:10px;font-weight:800;letter-spacing:.02em;
    padding:2px 7px;border-radius:99px;white-space:nowrap;z-index:2;
    box-shadow:0 2px 0 rgba(0,0,0,.25);
  }

  /* ---------- Perímetro y área (screens/node/geoarea.js) ----------
     Los lados del borde son botones finos SOBRE la cuadrícula: tocar "el
     lado" y no "la celda" es lo que enseña que el perímetro se mide en los
     bordes sin vecino, no en el interior. */
  .ga-prompt{color:var(--ink);font-size:14.5px;font-weight:700;line-height:1.4;margin:0 0 10px;}
  .ga-wrap{position:relative;margin:0 auto 8px;}
  .ga-grid{display:grid;width:100%;height:100%;}
  .ga-cell{border:1px solid color-mix(in srgb, var(--line) 55%, transparent);}
  .ga-cell.dentro{background:var(--surface-2);cursor:pointer;}
  .ga-cell.dentro.contada{background:color-mix(in srgb, var(--accent) 42%, var(--surface));}
  .ga-lado{
    position:absolute;border:none;padding:0;cursor:pointer;
    background:color-mix(in srgb, var(--cat-loop) 70%, transparent);
    z-index:2;border-radius:2px;
  }
  .ga-lado-arriba,.ga-lado-abajo{height:7px;}
  .ga-lado-abajo{transform:translateY(-7px);margin-top:100%;}
  .ga-lado-izq,.ga-lado-der{width:7px;}
  .ga-lado.contado{background:var(--cat-sound);}
  .ga-lado:focus-visible{outline:3px solid var(--accent);outline-offset:1px;}
  .ga-cuenta{text-align:center;font-size:14px;color:var(--ink-soft);margin:4px 0 8px;}
  .ga-cuenta strong{font-family:var(--font-display);font-size:22px;color:var(--ink);font-variant-numeric:tabular-nums;}
  .ga-pregunta{font-size:14.5px;font-weight:700;color:var(--ink);margin:6px 0;}

  /* Editor del profesor: la misma cuadrícula, para dibujar la figura. */
  .geo-editor{display:grid;gap:2px;max-width:220px;margin:6px 0;}
  .geo-editor-cell{aspect-ratio:1;border:1px solid var(--line);background:var(--surface);cursor:pointer;padding:0;border-radius:3px;}
  .geo-editor-cell.on{background:var(--accent);border-color:var(--accent-border);}
  .geo-editor-info{font-size:12.5px;font-weight:700;color:var(--ink-soft);margin:2px 0;}

  /* ---------- Fábrica de números (screens/node/factorize.js) ----------
     El árbol se dibuja igual que en el cuaderno: el número arriba y sus
     dos partes colgando. Que se parezca al papel es el punto — así lo que
     aprende aquí lo puede repetir en clase sin traducir nada. */
  .fz-prompt{color:var(--ink);font-size:14.5px;font-weight:700;line-height:1.4;margin:0 0 10px;}
  .fz-arbol{display:flex;justify-content:center;overflow-x:auto;padding:4px 0 8px;}
  .fz-nodo{display:flex;flex-direction:column;align-items:center;}
  .fz-num{
    border:2px solid var(--line);background:var(--surface);color:var(--ink);
    font-family:var(--font-display);font-size:17px;min-width:44px;padding:5px 9px;
    border-radius:10px;cursor:pointer;font-variant-numeric:tabular-nums;
  }
  .fz-num.partido{opacity:.55;cursor:default;}
  /* Un primo se marca en verde y deja de ser tocable: "ya no se puede
     partir" se ve, en vez de descubrirse tocando y que no pase nada. */
  .fz-num.primo{background:color-mix(in srgb, var(--cat-sound) 24%, var(--surface));border-color:var(--cat-sound);cursor:default;}
  .fz-hijos{display:flex;gap:12px;margin-top:14px;position:relative;}
  .fz-hijos::before{content:'';position:absolute;top:-9px;left:25%;right:25%;height:2px;background:var(--line);}
  .fz-hijos > .fz-nodo::before{content:'';display:block;width:2px;height:9px;background:var(--line);margin-bottom:-1px;}
  .fz-estado{text-align:center;font-size:13px;font-weight:700;color:var(--cat-sound);min-height:1.3em;margin:4px 0;}
  .fz-partir{text-align:center;margin:6px 0;}
  .fz-elige{font-size:13.5px;color:var(--ink-soft);margin:0 0 6px;}
  .fz-par{margin:3px;font-variant-numeric:tabular-nums;}
  .fz-pregunta{font-size:14.5px;font-weight:700;color:var(--ink);margin:8px 0 6px;}
  .fz-info{font-size:12.5px;font-weight:700;color:var(--ink-soft);margin:4px 0;font-variant-numeric:tabular-nums;}
  .fz-info.malo{color:var(--cat-move);}
  .fz-info.aviso{color:var(--cat-loop);}

  /* ---------- Zonas tocables: el mínimo para un dedo de cinco años ----------

     Una auditoría automática sobre las 990 pantallas (los 495 retos en
     versión normal y extra) en iPhone, iPad, teléfono Android y tablet
     Android encontró botones de 23x23 px. Apple pide 44pt y Android 48dp;
     en una aplicación que usan niños de cinco años, 23 px son unos 6 mm:
     menos de la mitad de la yema de un dedo infantil.

     Aquí solo se tocan los botones cuyo tamaño sale del RELLENO, no de una
     rejilla: crecer no mueve nada más de la pantalla, solo agranda la zona
     donde se puede tocar. Los que dependen de geometría —la rejilla del
     secuenciador musical, los anillos de órbitas y los botones de la cadena
     alimenticia— se dejan como están: agrandarlos cambia el tamaño de la
     rejilla y puede desbordar en teléfono, así que es una decisión de
     diseño, no un arreglo. Los objetos que caen en el juego de atrapar son
     pequeños a propósito: ahí está la dificultad.

     La comprobación vive en la propia auditoría: si alguno de estos vuelve a
     bajar de 36 px, sale en el informe. */
  .bubblesort-swap{min-width:44px;min-height:44px;}
  .tangram-piece{min-height:44px;}
  .tangram-buttons .btn-secondary{min-height:44px;}
  .resistor-chip{min-height:44px;}
  .tap-choice{min-height:44px;}

  /* Pista y regla revelada del puente (rulesort.js). Se ve distinta de la
     consigna: es información que aparece DESPUÉS, por algo que pasó. */
  .rulesort-pista{margin:10px auto 0;max-width:34ch;text-align:center;font-size:13.5px;font-weight:700;color:var(--ink-soft);background:var(--surface-2);border-radius:var(--radius-md);padding:9px 12px;}
  .rulesort-pista.revelada{color:var(--ink);box-shadow:inset 0 0 0 2px var(--accent-soft, var(--line));}

  /* ==================== Retos de las brechas del IB ====================
     Seis mecánicas para comunicación, habilidades sociales y expresión.
     Comparten tres criterios con el resto del catálogo: nada por debajo de
     44px de zona tocable, el estado se ve por FORMA además de por color
     (un borde, una marca) para quien no distingue bien los colores, y todo
     lo que crece con el contenido usa grid/flex con gap, nunca márgenes
     sueltos que se dupliquen. */

  /* --- El código de Robi (codemsg) --- */
  .codemsg-tabla{display:flex;flex-wrap:wrap;justify-content:center;gap:8px;margin:10px 0 14px;padding:10px;background:var(--surface-2);border-radius:var(--radius-md);}
  .codemsg-fila{display:flex;align-items:center;gap:6px;background:var(--surface);border:1.5px solid var(--line);border-radius:99px;padding:5px 12px;}
  .codemsg-sim{font-size:17px;letter-spacing:.06em;}
  .codemsg-igual{color:var(--ink-soft);font-size:12px;}
  .codemsg-pal{font-size:13.5px;font-weight:700;color:var(--ink);}
  .codemsg-label{font-size:11.5px;text-transform:uppercase;letter-spacing:.05em;color:var(--ink-soft);margin:10px 0 5px;}
  .codemsg-origen,.codemsg-armado{display:flex;flex-wrap:wrap;justify-content:center;gap:8px;min-height:52px;align-items:center;
    padding:8px;border-radius:var(--radius-md);border:2px dashed var(--line);}
  .codemsg-armado{border-style:solid;background:var(--surface-2);}
  .codemsg-armado.right{border-color:var(--ok-border,#3BAE7F);box-shadow:0 0 0 3px var(--accent-soft);}
  .codemsg-celda{min-width:46px;min-height:44px;display:flex;align-items:center;justify-content:center;
    font-size:17px;font-weight:700;padding:4px 10px;border-radius:10px;background:var(--surface);
    border:2px solid var(--line);color:var(--ink);font-family:inherit;}
  .codemsg-celda.puesta{cursor:pointer;border-color:var(--accent-border);}
  .codemsg-piezas{display:flex;flex-wrap:wrap;justify-content:center;gap:8px;margin-top:12px;}
  .codemsg-pieza{display:flex;flex-direction:column;align-items:center;gap:2px;min-width:56px;min-height:52px;
    padding:6px 10px;border-radius:12px;background:var(--surface);border:2px solid var(--line);cursor:pointer;font-family:inherit;}
  .codemsg-pieza:hover{border-color:var(--accent-border);}
  .codemsg-pieza-cara{font-size:18px;font-weight:700;color:var(--ink);letter-spacing:.06em;}

  /* --- ¿Qué dice el gráfico? (datatell) --- */
  .datatell-titulo{text-align:center;font-size:14px;font-weight:700;color:var(--ink);margin:8px 0 6px;}
  .datatell-grafico{display:flex;justify-content:center;align-items:flex-end;gap:12px;height:150px;padding:0 8px;}
  .datatell-col{display:flex;flex-direction:column;align-items:center;justify-content:flex-end;height:100%;flex:0 1 72px;}
  .datatell-valor{font-size:13px;font-weight:700;color:var(--ink);margin-bottom:3px;font-variant-numeric:tabular-nums;}
  .datatell-barra-hueco{width:100%;flex:1;display:flex;align-items:flex-end;}
  .datatell-barra{width:100%;background:var(--accent);border-radius:6px 6px 0 0;min-height:4px;}
  .datatell-et{font-size:11.5px;color:var(--ink-soft);margin-top:5px;text-align:center;line-height:1.25;}
  .datatell-total{text-align:center;font-size:12px;color:var(--ink-soft);margin-top:6px;font-variant-numeric:tabular-nums;}
  .datatell-frases{display:flex;flex-direction:column;gap:8px;margin-top:14px;}
  .datatell-frase{display:flex;align-items:flex-start;gap:10px;text-align:left;min-height:44px;
    padding:10px 12px;border-radius:var(--radius-md);background:var(--surface);border:2px solid var(--line);
    cursor:pointer;font-family:inherit;font-size:13.5px;line-height:1.45;color:var(--ink);}
  /* La marca ✓ además del color: el estado no puede depender solo del tono. */
  .datatell-tilde{flex:none;width:22px;height:22px;border-radius:6px;border:2px solid var(--line);
    display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:700;color:var(--accent-text);}
  .datatell-frase.marcada{border-color:var(--accent-border);background:var(--accent-soft);color:var(--accent-soft-ink);}
  .datatell-frase.marcada .datatell-tilde{border-color:var(--accent-border);background:var(--surface);}
  .datatell-frases.right .datatell-frase{border-color:var(--ok-border,#3BAE7F);}

  /* --- Turnos (sharefair) --- */
  .sharefair-robots{display:flex;flex-wrap:wrap;justify-content:center;gap:8px;margin:10px 0 14px;}
  .sharefair-robot{display:flex;flex-direction:column;align-items:center;gap:1px;min-width:88px;min-height:76px;
    padding:8px 10px;border-radius:14px;background:var(--surface);border:2px solid var(--line);cursor:pointer;font-family:inherit;}
  .sharefair-robot.elegido{border-color:var(--accent-border);background:var(--accent-soft);box-shadow:0 0 0 3px var(--accent-soft);}
  /* "Listo" se marca con un borde entero, no con un verde: se distingue
     igual en una pantalla en blanco y negro. */
  .sharefair-robot.listo{border-color:var(--ok-border,#3BAE7F);border-width:3px;}
  .sharefair-robot-ic{font-size:22px;}
  .sharefair-robot-nom{font-size:12px;font-weight:700;color:var(--ink);}
  .sharefair-robot-cnt{font-size:12.5px;color:var(--ink-soft);font-variant-numeric:tabular-nums;}
  .sharefair-robot-regla{font-size:10px;color:var(--accent-text);text-align:center;line-height:1.2;max-width:96px;}
  .sharefair-recurso{font-size:12.5px;font-weight:700;color:var(--ink-soft);margin-bottom:6px;}
  .sharefair-turnos{display:flex;flex-wrap:wrap;justify-content:center;gap:6px;padding:10px;
    background:var(--surface-2);border-radius:var(--radius-md);border:2px solid transparent;}
  .sharefair-turnos.right{border-color:var(--ok-border,#3BAE7F);}
  .sharefair-turno{display:flex;flex-direction:column;align-items:center;justify-content:center;
    width:52px;height:52px;border-radius:12px;background:var(--surface);border:2px dashed var(--line);
    cursor:pointer;font-family:inherit;gap:0;}
  .sharefair-turno.ocupado{border-style:solid;border-color:var(--accent-border);}
  .sharefair-turno-num{font-size:10px;color:var(--ink-soft);font-variant-numeric:tabular-nums;}
  .sharefair-turno-ic{font-size:20px;line-height:1;min-height:20px;}

  /* --- ¿Qué ve desde ahí? (viewpoint) --- */
  .viewpoint-escena{position:relative;display:grid;place-items:center;gap:10px;margin:14px auto;padding:18px;
    max-width:340px;border-radius:18px;background:var(--surface-2);border:2px solid var(--line);}
  .viewpoint-fila{display:flex;gap:10px;align-items:flex-end;}
  .viewpoint-obj{font-size:34px;line-height:1;}
  .viewpoint-obj.chico{font-size:26px;}
  .viewpoint-robi{display:flex;flex-direction:column;align-items:center;gap:0;}
  .viewpoint-robi .robi-mascot-img{width:38px;height:38px;}
  .viewpoint-flecha{font-size:15px;color:var(--accent-text);line-height:1;}
  /* Robi se coloca donde de verdad está mirando: si la escena lo dibuja
     siempre debajo, el reto pide imaginar algo que la propia imagen
     contradice. */
  .viewpoint-frente .viewpoint-robi{order:2;}
  .viewpoint-atras  .viewpoint-robi{order:-1;}
  .viewpoint-izquierda,.viewpoint-derecha{grid-auto-flow:column;}
  .viewpoint-izquierda .viewpoint-robi{order:-1;}
  .viewpoint-derecha  .viewpoint-robi{order:2;}
  .viewpoint-izquierda .viewpoint-flecha,.viewpoint-derecha .viewpoint-flecha{margin-top:2px;}
  .viewpoint-donde{font-size:13.5px;color:var(--ink);margin:0 0 10px;}
  .viewpoint-opciones{display:flex;flex-wrap:wrap;justify-content:center;gap:10px;}
  .viewpoint-opcion{display:flex;align-items:center;gap:6px;min-height:56px;padding:10px 14px;
    border-radius:14px;background:var(--surface);border:2px solid var(--line);cursor:pointer;font-family:inherit;}
  .viewpoint-opcion:hover{border-color:var(--accent-border);}
  .viewpoint-opcion.right{border-color:var(--ok-border,#3BAE7F);border-width:3px;}
  .viewpoint-opcion.wrong{border-color:var(--danger-border);animation:vpNo .3s ease;}
  @keyframes vpNo{25%{transform:translateX(-5px);}75%{transform:translateX(5px);}}
  @media (prefers-reduced-motion:reduce){ .viewpoint-opcion.wrong{animation:none;} }

  /* --- Reparto del trabajo (teamplan) --- */
  .teamplan-equipo{display:flex;flex-wrap:wrap;justify-content:center;gap:8px;margin:10px 0 14px;}
  .teamplan-miembro{display:flex;flex-direction:column;align-items:center;gap:1px;min-width:104px;min-height:82px;
    padding:8px 10px;border-radius:14px;background:var(--surface);border:2px solid var(--line);cursor:pointer;font-family:inherit;}
  .teamplan-miembro.elegido{border-color:var(--accent-border);background:var(--accent-soft);box-shadow:0 0 0 3px var(--accent-soft);}
  .teamplan-miembro-ic{font-size:22px;}
  .teamplan-miembro-nom{font-size:12.5px;font-weight:700;color:var(--ink);}
  .teamplan-miembro-sabe{font-size:10px;color:var(--ink-soft);text-align:center;line-height:1.25;max-width:110px;}
  .teamplan-miembro-cnt{font-size:12px;color:var(--ink-soft);font-variant-numeric:tabular-nums;}
  .teamplan-label{font-size:11.5px;text-transform:uppercase;letter-spacing:.05em;color:var(--ink-soft);margin-bottom:6px;}
  .teamplan-tareas{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:8px;
    border:2px solid transparent;border-radius:var(--radius-md);padding:2px;}
  .teamplan-tareas.right{border-color:var(--ok-border,#3BAE7F);}
  .teamplan-tarea{display:flex;flex-direction:column;align-items:center;gap:2px;min-height:88px;padding:10px;
    border-radius:12px;background:var(--surface);border:2px dashed var(--line);cursor:pointer;font-family:inherit;}
  .teamplan-tarea.asignada{border-style:solid;border-color:var(--accent-border);}
  .teamplan-tarea-ic{font-size:20px;}
  .teamplan-tarea-nom{font-size:12.5px;font-weight:700;color:var(--ink);text-align:center;line-height:1.25;}
  .teamplan-tarea-destreza{font-size:10px;color:var(--ink-soft);}
  .teamplan-tarea-dueno{font-size:11.5px;color:var(--accent-text);font-weight:700;margin-top:2px;}

  /* --- El espejo de Robi (mirrorart) --- */
  /* La rejilla ocupa el ancho disponible y las celdas se reparten lo que
     haya: en un teléfono con 6 columnas cada celda pasa de 50px, y con 10
     columnas queda en ~32. Un tamaño fijo de 30px las dejaba chicas
     incluso donde sobraba sitio. El tope de 54px evita que en una tablet
     la figura se convierta en un cartel. */
  /* SIN overflow: la rejilla cabe siempre o no cabe nunca. Lo puse yo
     pensando que desplazar de lado era mejor que encoger las celdas, y es
     al revés — lo dijo el usuario mirándolo: "los cuadros deben llenar el
     ancho de la pantalla, nunca más que eso, si no se pierde el sentido
     del juego". Y tenía razón por partida doble: con la rejilla desplazada
     se veía media figura y el reto parecía empezar con el lado derecho ya
     lleno. Un espejo que no se ve entero no es un espejo.

     Lo que se acota es el número de COLUMNAS (máximo 8, ver
     verificar-retos-resolubles.js), no el ancho de la rejilla. */
  .mirrorart-rejilla-hueco{position:relative;display:block;margin:12px auto;width:100%;}
  /* El ancho de la REJILLA se acota al que de verdad ocupan sus celdas, y
     ahí está la clave de que el espejo quede centrado.

     Las celdas topan en 54px. Cuando hay pocas columnas, el carril de la
     rejilla sale más ancho que la celda, la celda se queda pegada al borde
     izquierdo de su carril, y el hueco que se VE entre la columna 3 y la 4
     deja de coincidir con la frontera de carriles — que es donde se dibuja
     el eje (left:50%). Medido: con 6 columnas el eje salía 9px a la
     derecha del hueco real; con 8, donde la celda llena el carril, salía
     exacto. Por eso fallaba en un reto de los seis y en los demás no se
     notaba. Lo reportó el usuario con una captura: "mira dónde está la
     línea central, a un lado".

     El primer intento fue justify-items:center, y fue PEOR: las celdas no
     tienen ancho propio (viven de estirarse en su carril), así que
     centrarlas las dejó en cero — la rejilla se convirtió en una fila de
     puntitos. Lo vi en la captura; la medición del eje seguía diciendo
     "desfase 0", porque el punto medio entre dos celdas de ancho cero
     también es un punto. Una medida puede estar contenta mientras la
     pantalla está rota.

     Lo correcto es limitar el ancho de la rejilla al que ocupan sus
     celdas: 54px por columna más los huecos y el relleno. Así el carril
     nunca es más ancho que la celda, las celdas siguen estirándose, y el
     centro del carril es el centro visible. --cols lo pone mirrorart.js.
     Vigilado por verificar-eje-del-espejo.js. */
  .mirrorart-rejilla{display:grid;gap:3px;border:2px solid transparent;border-radius:10px;padding:3px;
    width:100%;margin:0 auto;
    max-width:min(100%, calc(var(--cols, 8) * 54px + (var(--cols, 8) - 1) * 3px + 10px));}
  .mirrorart-rejilla.right{border-color:var(--ok-border,#3BAE7F);}
  /* Al acertar, las dos mitades se igualan de color. Mientras se juega
     conviene distinguir lo que puso Robi de lo que pone el niño —así ve
     qué lleva hecho—, pero el premio es ver la figura ENTERA, y una figura
     de dos colores no se lee como una sola cosa. */
  .mirrorart-rejilla.right .mirrorart-celda.on{
    background:var(--accent);border-color:var(--accent-border);border-style:solid;
    transition:background .3s ease;
  }
  /* Sin min-width: la celda vale lo que le toque del ancho disponible, y
     punto. El mínimo de 36px que había aquí es lo que hacía que una
     rejilla de 10 columnas midiera 392px dentro de un hueco de 316 y
     apareciera la barra de desplazamiento. El tamaño tocable se protege
     limitando las COLUMNAS, no forzando el ancho de la celda: con 8
     columnas en el teléfono más angosto la celda sale en 36px. */
  .mirrorart-celda{aspect-ratio:1;min-width:0;max-width:54px;border-radius:6px;background:var(--surface-2);
    border:1.5px solid var(--line);cursor:pointer;padding:0;font-family:inherit;}
  .mirrorart-celda.on{background:var(--accent);border-color:var(--accent-border);}
  /* Las celdas del modelo se ven distintas de las que pinta el niño: es el
     enunciado, no su respuesta, y no se pueden borrar. */
  .mirrorart-celda.dada{background:var(--accent-soft);border-color:var(--accent-border);border-style:dashed;cursor:default;}
  /* Fondo y borde no pueden ser el mismo color: con background y
     border-color los dos en --accent-border la celda del modelo se veía
     como un bloque plano sin contorno (contraste 1.00:1, medido en 101
     pantallas). El borde más claro la sigue distinguiendo de las celdas
     que pinta el niño. */
  .mirrorart-celda.dada.on{background:var(--accent-border);border-color:var(--accent-hi);}
  /* El espejo tiene que VERSE. La primera versión era una línea de 2px
     punteada al 55% de opacidad, metida en el hueco de 3px entre celdas y
     del mismo azul que ellas: estaba en el sitio matemáticamente exacto
     (medido: x=195, justo entre la columna 2 y la 3) y era invisible. Y
     un reto que dice "la línea del medio es el espejo" sobre una línea
     que no se ve no explica nada. Ahora es una banda sólida, clara, por
     encima de las celdas y con un halo que la despega del fondo. */
  .mirrorart-eje{
    position:absolute;top:-6px;bottom:-6px;width:3px;margin-left:-1.5px;
    background:var(--accent-hi);border-radius:99px;z-index:2;pointer-events:none;
    box-shadow:0 0 0 2px var(--bg), 0 0 10px var(--accent-hi);
  }
  /* Los dos lados se distinguen: el del modelo es territorio de Robi (no
     se toca) y el otro es el del niño. Es lo que responde, sin una sola
     palabra, a "¿dónde tengo que pintar yo?". */
  /* El :not(.on) es imprescindible, no cosmético. Sin él, estas dos reglas
     tienen la misma especificidad que .mirrorart-celda.on y vienen
     DESPUÉS, así que le ganaban: el niño pintaba una celda de su lado y
     seguía viéndose vacía. O sea que el reto se resolvía y no se veía
     resuelto — visto en pantalla, con la montaña completa mostrando la
     mitad izquierda llena y la derecha en huecos. Aquí el tinte de cada
     lado solo aplica a lo que TODAVÍA no está pintado. */
  .mirrorart-celda.lado-modelo:not(.on){background:var(--surface);opacity:.75;cursor:default;}
  .mirrorart-celda.lado-tuyo:not(.on){background:var(--surface-2);box-shadow:inset 0 0 0 1px var(--accent-soft);}

  /* El cuadro de Robi mientras no tiene nada que decir: sin alto, sin
     borde, sin sitio. Vuelve en cuanto habla (robiDice, node/block.js). */
  .mascot-box.callado{display:none;}

  /* ---------- La barra de "ya está" ----------
     Aparece dentro del propio reto cuando se resuelve, en vez de saltar
     derecho al modal de estrellas. El reto se queda a la vista —el
     rompecabezas armado, el circuito encendido, la figura simétrica— y el
     niño decide cuándo pasar. Ver mostrarBarraDeLogro en node/router.js
     para por qué existe. */
  /* Pegada al borde inferior de la pantalla, no al final del contenido.
     El motivo es concreto: en los retos de bloques la barra caía DEBAJO
     del botón "Ejecutar", fuera de la vista, así que el niño terminaba el
     reto y no veía por dónde seguir — lo reportó el usuario. Con sticky se
     queda a la vista mientras haya contenido por debajo y se asienta en su
     sitio al llegar al final, sin taparle nada de forma permanente. */
  .logro-barra{
    position:sticky;
    bottom:calc(12px + env(safe-area-inset-bottom));
    z-index:20;
    display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;
    margin:16px 0 4px;padding:12px 14px;border-radius:var(--radius-lg);
    background:var(--accent-soft);color:var(--accent-soft-ink);
    box-shadow:0 0 0 2px var(--accent-border), 0 6px 18px rgba(0,0,0,.28);
    animation:logroEntra .28s cubic-bezier(.34,1.4,.64,1);
  }
  /* Por encima de la barra inferior de destinos cuando la hay. */
  body.has-bottom-nav .logro-barra{ bottom:calc(12px + 64px + env(safe-area-inset-bottom)); }
  .logro-texto{display:flex;align-items:center;gap:10px;font-size:16px;font-weight:800;}
  .logro-robi{width:34px;height:34px;flex:none;}
  /* El botón no compite con el texto: ocupa lo que le sobre y nunca baja
     de la zona tocable mínima. */
  .logro-seguir{flex:1 1 auto;min-width:140px;min-height:48px;font-size:15.5px;}
  /* La estrella de "me gustó". No compite con Seguir —ese es el camino
     principal— así que va sin relleno de color hasta que se marca, y
     nunca baja de los 48px de zona tocable: es un botón para un dedo de
     seis años. */
  /* La FORMA ya no se decide aquí: el favorito vive en el pie del reto y
     comparte cuadrado, radio y relieve con los otros dos iconos
     (.pie-icono). Antes ponía su propio tamaño y su propio radio, y por
     eso salía más ancho y más ovalado que sus vecinos — el dueño lo
     señaló: "el de Me gusta tiene incluso un botón más ovalado". Aquí
     queda solo lo que es SUYO: el amarillo al marcarlo. */
  .logro-favorito{
    transition:transform .16s cubic-bezier(.34,1.4,.64,1), background .16s ease, color .16s ease;
  }
  /* --cat-loop es el amarillo que la aplicación ya usa para las estrellas,
     y viene con su tinta (--cat-loop-ink) para leer encima. Los dos son
     el mismo valor en modo claro y oscuro, y aquí eso no estorba porque
     el botón pinta su propio fondo: el par amarillo/marrón se lee igual
     esté donde esté. Inventar un dorado suelto sí habría dado un color
     que solo funciona en uno de los dos temas. */
  .logro-favorito.marcado{
    background:var(--cat-loop);
    border-color:var(--cat-loop);
    color:var(--cat-loop-ink);
  }
  /* Rellena la estrella al marcarla: el contorno y el relleno se
     distinguen de un vistazo, y el color solo no basta para quien no lo
     ve igual. */
  .logro-favorito.marcado .lucide{fill:currentColor;}
  .logro-favorito.marcado{animation:favoritoLatido .32s cubic-bezier(.34,1.4,.64,1);}
  @keyframes favoritoLatido{0%{transform:scale(1);}45%{transform:scale(1.22);}100%{transform:scale(1);}}
  @media (prefers-reduced-motion:reduce){ .logro-favorito.marcado{animation:none;} }
  @keyframes logroEntra{from{opacity:0;transform:translateY(10px);}to{opacity:1;transform:none;}}
  @media (prefers-reduced-motion:reduce){ .logro-barra{animation:none;} }
  /* Con el reto ya resuelto, nada dentro responde a los toques: lo
     conseguido no se puede deshacer sin querer mientras se mira. La barra
     misma queda fuera de esa regla, que para eso está. */
  #nodeBody.reto-resuelto > *:not(.logro-barra){pointer-events:none;}
  /* Con el reto resuelto se reserva sitio para la barra al final del
     contenido: como está pegada al borde inferior, sin este hueco se
     apoyaba justo encima de la fila de "Vaciar / Ejecutar" y se veía
     amontonada. Los botones ya no sirven —el reto terminó— pero taparlos
     a medias se ve peor que dejarlos ver enteros. */
  /* El hueco es EXACTAMENTE lo que mide el pie (--pie-alto, lo escribe
     reservarSitioBajoElPie en router.js), más un respiro. Un número fijo
     no vale: el pie crece con los avisos y con el reto extra, y con el
     pie alto el final del reto no llegaba a asomar por mucho que el niño
     se desplazara. Los 92px quedan de reserva por si el pie no llegó a
     medirse. */
  #nodeBody.reto-resuelto{padding-bottom:calc(var(--pie-alto, 92px) + 16px);}

  /* El sello de "hecho" que sustituye a las estrellas en los retos que no
     se pueden fallar (vitrina, aventura, dibujo libre). Ver el comentario
     junto a sinNota en node/router.js: tres estrellas sobre algo que no se
     puede fallar no informan de nada. */
  .hecho-sello{
    display:inline-flex;align-items:center;justify-content:center;
    width:72px;height:72px;margin:6px auto 0;border-radius:50%;
    background:var(--accent-soft);color:var(--accent-text);
    box-shadow:0 0 0 3px var(--accent-border);
    animation:pop .4s cubic-bezier(.34,1.56,.64,1);
  }
  @media (prefers-reduced-motion:reduce){ .hecho-sello{animation:none;} }

  /* Marca "sin nota" en el selector de tipos del profesor: los tipos donde
     no hay respuesta correcta (vitrina, aventura, vestir a Robi) no dan
     estrellas ni entran en el promedio del diploma. Se avisa en el chip y
     otra vez, más largo, dentro del formulario — ver renderNodeEditorFields
     en screens/teacher.js. */
  .choice-chip .chip-sin-nota{
    display:block;margin-top:3px;font-size:9.5px;font-weight:800;
    text-transform:uppercase;letter-spacing:.05em;
    color:var(--ink-soft);background:var(--surface-2);
    border-radius:99px;padding:1px 7px;
  }
  .choice-chip.es-sin-nota{border-style:dashed;}


/* ── Dispositivos conectados (Ajustes) ─────────────────────────────────
   Misma caja y mismo ritmo que las filas de Ajustes, pero con la acción
   al lado en vez del galón: aquí no se entra a ningún sitio, se cierra
   algo, y una fila que parece navegable invita a tocarla por error. */
.sesion-fila{
  display:flex;align-items:center;gap:11px;
  border:1.5px solid var(--line);border-radius:14px;
  background:var(--surface);padding:11px 13px;margin-bottom:8px;
}
.sesion-ic{flex:none;display:flex;color:var(--ink-soft);}
.sesion-ic .lucide{width:20px;height:20px;}
.sesion-txt{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px;}
.sesion-nombre{font-weight:700;font-size:14px;color:var(--ink);}
.sesion-esta{
  font-size:11px;font-weight:700;letter-spacing:.4px;text-transform:uppercase;
  color:var(--ok);border:1px solid var(--ok);border-radius:999px;
  padding:1px 7px;margin-left:5px;vertical-align:middle;
}
.sesion-cuando{font-size:12px;color:var(--ink-soft);}
.sesion-cerrar{flex:none;padding:7px 14px;font-size:13px;}
.settings-nota{font-size:12.5px;color:var(--ink-soft);margin:6px 2px 0;line-height:1.5;}


/* La píldora a la que se acaba de volver desde su certificado. Un halo
   breve, no un color nuevo: la cuadrícula se redibuja entera al volver, y
   sin una señal hay que buscar con la vista cuál de todas era la de uno.
   Se apaga sola — dejarlo fijo convertiría un "estás aquí" en un estado
   que no significa nada un minuto después. */
.tier-pill.recien-visto{
  animation: recienVisto 1.6s ease-out 1;
}
@keyframes recienVisto{
  0%   { box-shadow:0 0 0 0 var(--accent); }
  15%  { box-shadow:0 0 0 4px var(--accent); }
  100% { box-shadow:0 0 0 0 rgba(0,0,0,0); }
}
@media (prefers-reduced-motion: reduce){
  .tier-pill.recien-visto{animation:none;box-shadow:0 0 0 3px var(--accent);}
}

/* ---------- Piloto (panel de administración) ---------- */
.piloto-fila{display:flex;align-items:center;justify-content:space-between;gap:12px;
  padding:10px 12px;border:1px solid var(--border,#dfe3e8);border-radius:10px;margin-bottom:8px;}
.piloto-etiqueta{font-size:11px;padding:1px 7px;border-radius:999px;
  background:var(--muted-fill,#eee);color:var(--text-secondary,#667);vertical-align:middle;}
.piloto-etapa{border:1px solid var(--border,#dfe3e8);border-radius:10px;padding:10px 12px;margin-bottom:10px;}
.piloto-etapa-cab{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;}
.piloto-etapa-titulo{font-size:12px;text-transform:uppercase;letter-spacing:.05em;
  color:var(--text-secondary,#667);margin-bottom:6px;}
.piloto-etapa-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px;}
@media (max-width:520px){ .piloto-etapa-grid{grid-template-columns:1fr;} }
/* Casilla + etiqueta en una sola zona pulsable: en tablet se marcan con el
   dedo, y un cuadradito de 13 px no se acierta. 44 px es el mínimo que
   exige verificar-zonas-tactiles, y aquí sobra sitio para dárselo. */
.piloto-chip{display:flex;align-items:center;gap:8px;min-height:44px;cursor:pointer;font-size:13.5px;}
.piloto-etapa-cuenta{font-size:12px;margin-top:8px;}
.piloto-barra{display:flex;height:10px;border-radius:999px;overflow:hidden;background:var(--muted-fill,#eee);}
.piloto-barra-parte{display:block;height:100%;}
.piloto-barra-parte.mal{background:#e05555;}
.piloto-barra-parte.asi{background:#e0a94a;}
.piloto-barra-parte.bien{background:#4aa96c;}
.piloto-fila-botones{display:flex;gap:6px;flex-wrap:wrap;}
/* Una persona por fila, con la casilla y el nombre en la misma zona
   pulsable: se marca con el dedo en una tablet, no con el ratón. */
.piloto-persona{display:flex;align-items:center;gap:10px;min-height:48px;
  padding:4px 6px;border-bottom:1px solid var(--line,#e6e8eb);cursor:pointer;font-size:13.5px;}
.piloto-persona:last-child{border-bottom:0;}

/* ---------- Zonas del mapa: la lección del día ---------- */
/* La banda de nivel deja de ser un rótulo y pasa a ser el sitio donde el
   niño pide su lección. Se apoya en .tier-divider, que ya existía y ya
   marcaba dónde empieza cada nivel: no se inventa un componente nuevo
   para algo que el niño ya sabía mirar. */
.tier-divider.con-leccion{
  /* El recorrido centra a sus hijos (.path-track tiene align-items:center),
     así que una zona con lección tiene que salirse de ese centrado para
     ocupar el ancho: si no, queda una pastilla estrecha en medio del mapa
     y no parece el sitio donde se toca para empezar. */
  align-self:stretch;
  flex-direction:column;align-items:stretch;gap:8px;
  width:100%;max-width:420px;margin-left:auto;margin-right:auto;
  border-radius:18px;padding:10px 12px 12px;
}
.tier-divider.con-leccion .zona-titulo{display:flex;align-items:center;gap:6px;padding-left:2px;}
.zona-leccion{
  width:100%;
  display:flex;align-items:center;justify-content:space-between;gap:10px;
  min-height:56px;padding:10px 14px;margin-top:2px;
  border:0;border-radius:14px;cursor:pointer;text-align:left;
  background:var(--accent);color:var(--accent-ink,#04121f);
  box-shadow:0 2px 0 rgba(0,0,0,.18);
  font-family:inherit;
}
.zona-leccion:active{transform:translateY(1px);box-shadow:0 1px 0 rgba(0,0,0,.18);}
.zona-leccion[disabled]{
  background:var(--muted-fill,#e6e8eb);color:var(--ink-soft,#667);
  cursor:default;box-shadow:none;
}
.zona-leccion-texto{display:flex;flex-direction:column;gap:2px;font-weight:700;font-size:15px;}
.zona-leccion-texto small{font-weight:500;font-size:12px;opacity:.85;}
/* El aro de avance: cuántas lecciones de la zona lleva hechas. Va aquí y
   no en un número suelto porque lo que el niño quiere saber de un
   vistazo es "cuánto me falta", no "cuántas van". */
.zona-leccion-aro{
  flex:0 0 64px;height:8px;border-radius:999px;
  background:rgba(0,0,0,.18);overflow:hidden;
}
.zona-leccion-aro i{display:block;height:100%;background:currentColor;border-radius:999px;}

/* ---------- Dentro de la lección ---------- */
.leccion-progreso{
  display:flex;flex-direction:column;gap:6px;align-items:center;
  font-size:12.5px;color:var(--ink-soft);margin-bottom:2px;
}
.leccion-barra{display:block;width:min(220px,70%);height:8px;border-radius:999px;
  background:var(--muted-fill,#e6e8eb);overflow:hidden;}
.leccion-barra i{display:block;height:100%;background:var(--accent);border-radius:999px;
  transition:width .35s ease;}
.leccion-aprendido{
  margin:12px auto 0;padding:10px 14px;max-width:320px;border-radius:12px;
  background:var(--muted-fill,#eef1f4);display:flex;flex-direction:column;gap:2px;
}
.leccion-aprendido span{font-size:12px;color:var(--ink-soft);text-transform:uppercase;letter-spacing:.05em;}
.leccion-aprendido strong{font-size:14.5px;line-height:1.4;}

/* ---------- La cinta de la lección, arriba del reto ----------
   Va fija y siempre visible mientras dura la lección. Es lo que evita que
   cuatro retos seguidos se sientan interminables: el niño sabe cuánto le
   falta sin tener que terminar uno para enterarse. */
.leccion-cinta{
  display:flex;align-items:center;gap:12px;
  padding:10px 4px 12px;
}
.leccion-salir{
  flex:0 0 auto;width:44px;height:44px;border:0;border-radius:50%;
  background:transparent;color:var(--ink-soft);font-size:19px;line-height:1;
  cursor:pointer;font-family:inherit;
}
.leccion-salir:active{background:var(--muted-fill,#e6e8eb);}
.leccion-cinta-barra{
  flex:1 1 auto;height:14px;border-radius:999px;
  background:var(--muted-fill,#e6e8eb);overflow:hidden;
}
.leccion-cinta-barra i{
  display:block;height:100%;border-radius:999px;background:var(--accent);
  transition:width .4s cubic-bezier(.2,.8,.3,1);
}
.leccion-cinta-cuenta{
  flex:0 0 auto;font-size:13px;font-weight:700;color:var(--ink-soft);
  font-variant-numeric:tabular-nums;min-width:52px;text-align:right;
}

/* ---------- Metas del mapa ----------
   Contra uno mismo y la de la clase. Nunca una tabla de posiciones: ver
   el comentario en core/leccion.js sobre por qué con niños no se copia
   esa parte de Duolingo. */
.metas{display:flex;flex-direction:column;gap:8px;margin:10px 0 2px;}
.meta{
  display:flex;align-items:center;gap:12px;
  padding:10px 14px;border-radius:14px;
  background:var(--surface-2);
}
.meta-txt{display:flex;flex-direction:column;gap:1px;flex:1 1 auto;min-width:0;}
.meta-txt strong{font-size:13.5px;}
.meta-txt small{font-size:12px;color:var(--ink-soft);}
/* La pista de la barra va en --line, no en --muted-fill: ese es el
   MISMO color que el fondo de la tarjeta (en claro y en oscuro), así que
   la pista no se veía y de «1 de 32» solo quedaba una astilla azul
   suelta a la derecha, «algo medio dibujado que parece tapado». El
   relleno tiene un mínimo del ancho de la barra para que un 3 % sea un
   punto redondo y no una media luna. */
.meta-barra{flex:0 0 76px;height:9px;border-radius:999px;background:var(--line);overflow:hidden;}
.meta-barra i{display:block;height:100%;min-width:9px;border-radius:999px;background:var(--accent);}
/* La de la clase se distingue de la personal: son dos cosas distintas y
   mezclarlas haría pensar que el niño va detrás de sus compañeros. */
.meta-clase{background:color-mix(in srgb, var(--accent) 12%, var(--surface-2));}
.meta-clase .meta-barra i{background:var(--cat-sound,var(--accent));}

/* ---------- La lección del día: una tarjeta, arriba del mapa ----------
   Antes había un botón en cada banda de nivel y salían cuatro a la vez.
   Ahora es una sola cosa que hacer hoy, y por eso ocupa el sitio de
   honor: lo primero que se ve al abrir la aplicación. */
.leccion-hoy{
  display:flex;align-items:center;justify-content:space-between;gap:14px;
  width:100%;min-height:72px;padding:12px 16px;margin:10px 0 2px;
  border:0;border-radius:16px;cursor:pointer;text-align:left;font-family:inherit;
  background:var(--accent);color:var(--accent-ink,#04121f);
  box-shadow:0 3px 0 rgba(0,0,0,.2);
}
.leccion-hoy:active{transform:translateY(1px);box-shadow:0 2px 0 rgba(0,0,0,.2);}
.leccion-hoy.terminado{background:var(--surface-2);color:var(--ink);cursor:default;box-shadow:none;}
.leccion-hoy-txt{display:flex;flex-direction:column;gap:1px;min-width:0;flex:1 1 auto;}
.leccion-hoy-txt strong{font-size:17px;line-height:1.2;}
.leccion-hoy-txt small{font-size:12.5px;opacity:.9;}
.leccion-hoy-mundo{font-size:11.5px;opacity:.72;text-transform:uppercase;letter-spacing:.04em;}
/* Cuánto lleva del trimestre. Va sin número a propósito: al niño le sirve
   ver que la barra crece, no saber que va por la 12 de 60. */
.leccion-hoy-aro{flex:0 0 70px;height:9px;border-radius:999px;background:rgba(0,0,0,.2);overflow:hidden;}
.leccion-hoy-aro i{display:block;height:100%;border-radius:999px;background:currentColor;}

/* Cuando ya hizo la lección del día. Deja de ser un botón grande que
   invita a seguir y pasa a ser un sello de "cumplido": el día está hecho,
   y hacer otra es una oferta, no lo que toca. */
.leccion-hoy.hecha{
  background:color-mix(in srgb, var(--cat-sound,#4aa96c) 22%, var(--surface-2));
  color:var(--ink);cursor:default;box-shadow:none;min-height:60px;
}
.leccion-hoy.hecha .lucide{width:18px;height:18px;vertical-align:-3px;margin-right:4px;}
.leccion-otra{
  display:block;width:100%;margin:6px 0 2px;padding:10px 14px;min-height:44px;
  border:1.5px dashed var(--line,#d7dbe0);border-radius:12px;background:transparent;
  color:var(--ink-soft);font-family:inherit;font-size:13px;cursor:pointer;text-align:center;
}
.leccion-otra:active{background:var(--muted-fill,#eef1f4);}

/* La tarjeta de hoy, pegada bajo la barra superior.
   Antes vivía suelta arriba del mapa y, en cuanto el niño bajaba, dejaba
   de verse — y cada día quedaba más abajo. Es lo único que necesita tener
   siempre delante: qué le toca hoy y si ya lo hizo. */
.leccion-hoy-fija{
  position:sticky;
  /* Debajo de la barra superior, no del borde de la pantalla. Con 8px
     fijos la caja se quedaba pegada PERO tapada por la barra: al bajar se
     veía media frase y el botón de abajo entero, que es justo lo que
     reportó el dueño ("se ve medio raro un mensaje y el otro a medias").
     --topbar-h lo mide medirBarrasFijas en cada dibujado, porque la barra
     cambia de alto según qué botones muestre. */
  /* Pegado a la barra, sin holgura: con 8 px de separación se veía por la
     rendija el borde de la tarjeta que pasaba por detrás. El aire va como
     relleno DENTRO, que sí lleva fondo. */
  /* HAY QUE SUMAR LA FRANJA DE LA ISLA. La barra no se pega al borde de la
     pantalla sino a env(safe-area-inset-top) (ver .topbar-sticky-map), así
     que su BORDE DE ABAJO está a inset + alto, no a alto. Con solo
     --topbar-h esta tarjeta se pegaba unos 59 px demasiado arriba en un
     iPhone con isla y la barra le tapaba la primera línea: se leía "No
     tienes pendientes por hoy" sin el "Ya hiciste tu lección de hoy" de
     encima. En un navegador de escritorio el inset vale 0, las dos
     cuentas dan igual y el fallo NO se reproduce — por eso sobrevivió a
     dos intentos de arreglo mirándolo aquí. El scroll-margin-top de los
     nodos del mapa ya sumaba el inset; a esta regla le faltaba. */
  top:calc(var(--topbar-h, 0px) + env(safe-area-inset-top));
  z-index:5;
  /* Fondo propio: sin él, los círculos del mapa se ven POR DEBAJO de la
     caja mientras pasan, y el texto queda ilegible sobre ellos. */
  background:var(--bg);
  padding:6px 0 8px;
  margin-bottom:6px;
}
/* El aspa para cerrar la felicitación del día. 44 px como todo lo que se
   toca, y discreta: cierra un aviso, no cancela nada. */
.leccion-hoy-cerrar{
  flex:0 0 auto;width:44px;height:44px;border:0;background:none;
  color:inherit;opacity:.7;cursor:pointer;
  display:flex;align-items:center;justify-content:center;border-radius:50%;
}
.leccion-hoy-cerrar:hover{opacity:1;background:rgba(0,0,0,.08);}
.leccion-hoy-cerrar .lucide{width:18px;height:18px;}
.leccion-hoy.hecha{display:flex;align-items:center;gap:8px;}
.leccion-hoy.hecha .leccion-hoy-txt{flex:1;min-width:0;}

/* "Hacer otra": un botón de verdad, con su ícono. Antes era un recuadro
   de línea discontinua que no parecía tocable. */
.leccion-otra{
  display:flex;align-items:center;justify-content:center;gap:8px;
  width:100%;margin:6px 0 2px;padding:11px 14px;min-height:46px;
  border:0;border-radius:12px;background:var(--surface-2);
  color:var(--ink);font-family:inherit;font-size:13.5px;font-weight:600;
  cursor:pointer;box-shadow:0 2px 0 rgba(0,0,0,.14);
}
.leccion-otra .lucide{width:17px;height:17px;flex:0 0 auto;}
.leccion-otra:active{transform:translateY(1px);box-shadow:0 1px 0 rgba(0,0,0,.14);}
/* Cuando el plan gratuito ya gastó sus lecciones del día. No se pinta
   apagado ni deshabilitado: sí lleva a algún sitio —a la pantalla del
   plan— y un botón apagado diría que no. Lleva el color del acento en el
   candado y en el borde para que se lea como una puerta, no como un
   error. */
.leccion-otra.de-pago{
  background:var(--accent-soft);color:var(--accent-soft-ink);
  border:1.5px solid var(--accent-border);
}
.leccion-otra.de-pago .lucide{color:var(--accent-text);}
/* Las metas, ahora dentro del resumen del final de la lección. */
.complete-card .metas{margin:12px 0 0;}
.complete-card .meta{padding:8px 12px;}

/* ---------- LA PALABRA TAPADA (método de la hoja roja) ----------
   El 赤シート japonés: lo importante se subraya con marcador verde y una
   lámina roja lo hace desaparecer. Aquí la lámina es un rectángulo rojo
   sobre el hueco, y al acertar se LEVANTA —no se desvanece— para que se
   vea que debajo estaba la palabra, en su sitio dentro de la frase. */
.tapapalabra{max-width:560px;margin:0 auto;}
.tapa-intro{color:var(--ink-soft);text-align:center;margin:0 0 6px;font-size:14px;line-height:1.45;}
.tapa-cuenta{
  text-align:center;font-size:12px;font-weight:800;letter-spacing:.04em;
  text-transform:uppercase;color:var(--ink-soft);margin-bottom:10px;
}
.tapa-frase{
  font-size:19px;line-height:1.75;text-align:center;
  background:var(--surface-2);border:1.5px solid var(--line);border-radius:14px;
  padding:16px 14px;margin-bottom:12px;
}
/* El hueco ocupa el sitio de la palabra, no un ancho fijo: con un ancho
   fijo, el largo del rectángulo delataría cuál es la respuesta. */
.tapa-hueco{position:relative;display:inline-block;vertical-align:baseline;}
.tapa-palabra{font-weight:800;color:var(--accent-text);}
.tapa-lamina{
  position:absolute;left:-3px;right:-3px;top:-1px;bottom:-1px;
  background:#D94A3D;
  border-radius:5px;
  box-shadow:inset 0 -2px 0 rgba(0,0,0,.18);
  transform-origin:left center;
  transition:transform .34s cubic-bezier(.4,0,.2,1), opacity .34s ease;
}
.tapa-hueco.abierto .tapa-lamina{transform:rotateY(-96deg);opacity:0;}
@media (prefers-reduced-motion:reduce){
  .tapa-lamina{transition:opacity .2s ease;}
  .tapa-hueco.abierto .tapa-lamina{transform:none;opacity:0;}
}
.tapa-opciones{display:flex;flex-wrap:wrap;gap:9px;justify-content:center;margin-bottom:10px;}
.tapa-opcion{
  -webkit-appearance:none;appearance:none;cursor:pointer;font-family:inherit;
  padding:11px 16px;min-height:48px;
  font-size:15.5px;font-weight:700;
  background:var(--surface);color:var(--ink);
  border:2px solid var(--line);border-radius:12px;
  transition:border-color .15s ease, transform .12s ease, background .15s ease;
}
.tapa-opcion:hover{border-color:var(--line-fuerte);}
.tapa-opcion:active{transform:translateY(1px);}
.tapa-opcion.bien{background:var(--ok);border-color:var(--ok);color:#08301C;}
.tapa-opcion.mal{border-color:var(--cat-move);animation:tapaNo .32s ease;}
@keyframes tapaNo{0%,100%{transform:translateX(0);}25%{transform:translateX(-5px);}75%{transform:translateX(5px);}}
@media (prefers-reduced-motion:reduce){ .tapa-opcion.mal{animation:none;} }

/* ==================== PANEL DE IDIOMAS ====================
   La lista de textos a traducir es larga por definición —miles de
   entradas— así que cada fila tiene que leerse de un vistazo: el
   original arriba, con su contexto pequeño debajo, y la casilla para
   escribir al lado. En pantalla estrecha se apilan. */
.idioma-barra{background:var(--line);border-radius:999px;height:6px;overflow:hidden;max-width:160px;}
.idioma-barra i{display:block;height:100%;background:var(--accent);border-radius:999px;transition:width .3s ease;}

.idioma-fila{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:10px;
  padding:10px 0;border-bottom:1px solid var(--line);align-items:start;}
.idioma-original{font-size:13px;line-height:1.5;word-break:break-word;}
.idioma-original small{display:block;color:var(--ink-soft);font-size:11.5px;margin-top:3px;}
.idioma-valor{font-size:13px;resize:vertical;min-height:52px;}
/* Un destello corto al guardar: dice "quedó" sin robar el sitio a nada
   ni obligar a leer un aviso. Se apaga solo. */
.idioma-valor.guardado{animation:idiomaGuardado 1.2s ease;}
@keyframes idiomaGuardado{
  0%{box-shadow:0 0 0 2px var(--accent);}
  100%{box-shadow:0 0 0 0 transparent;}
}
@media (max-width:640px){
  .idioma-fila{grid-template-columns:1fr;}
}
