/*
 * Room Teachers — una sola hoja, sin framework.
 *
 * Sustituye a la hoja terracota manteniendo todos los nombres de tokens y de
 * componentes: las plantillas Jinja no cambian una línea.
 *
 * Dirección: noche tranquila. Fondo azul-gris profundo, un solo acento
 * blurple usado como línea y como halo, nunca como relleno grande. Los
 * botones primarios son un contorno del acento, no un bloque de color: nada
 * en la pantalla grita antes de que el alumno hable.
 *
 * Tema claro incluido: `prefers-color-scheme` y, si algún día hay un
 * conmutador, `[data-theme="light"]` / `[data-theme="dark"]` en <html>.
 */

/* --- tokens: noche -------------------------------------------------------- */
:root {
  --ink: #e9e9ed;
  --ink-soft: #b2b6ca;
  --ink-faint: #75798c;
  --paper: #161826;
  --card: #232532;
  --line: #3f424d;
  --line-soft: #2b2d38;

  /* Blurple. Suficiente cromatismo para ser un acento sobre rampas
     desaturadas, sin la urgencia del rojo o del naranja. */
  --accent: #9184d9;
  --accent-dark: #b5abfc;   /* el paso más claro: hover y texto sobre el wash */
  --accent-wash: #2b2741;
  --good: #7ec8a2;
  --good-wash: #1d2e2a;
  --warn: #d8b46a;
  --warn-wash: #302b1f;
  --bad: #e08a86;
  --bad-wash: #332229;

  --radius: 8px;
  --radius-sm: 4px;
  --shadow: 0 0 0 1px var(--line), 0 6px 18px rgba(0, 0, 0, .45);
  --font: "Inter", ui-sans-serif, system-ui, sans-serif;
  --font-display: "Inter", ui-sans-serif, system-ui, sans-serif;
  --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;

  --shell: 1080px;
  color-scheme: dark;
}

/* --- tokens: día ---------------------------------------------------------- */
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) {
    --ink: #23252e;
    --ink-soft: #595d6c;
    --ink-faint: #75798c;
    --paper: #f3f5fe;
    --card: #fdfdff;
    --line: #cfd3e5;
    --line-soft: #e4e7f5;

    --accent: #5d5294;
    --accent-dark: #423a6a;
    --accent-wash: #f5f4ff;
    --good: #2f6b50;
    --good-wash: #e9f3ee;
    --warn: #7d5e12;
    --warn-wash: #f7f0dd;
    --bad: #97403c;
    --bad-wash: #f9ebea;

    --shadow: 0 1px 2px rgba(35, 37, 46, .05), 0 8px 24px -14px rgba(35, 37, 46, .22);
    color-scheme: light;
  }
}

:root[data-theme="light"] {
  --ink: #23252e;
  --ink-soft: #595d6c;
  --ink-faint: #75798c;
  --paper: #f3f5fe;
  --card: #fdfdff;
  --line: #cfd3e5;
  --line-soft: #e4e7f5;
  --accent: #5d5294;
  --accent-dark: #423a6a;
  --accent-wash: #f5f4ff;
  --good: #2f6b50;
  --good-wash: #e9f3ee;
  --warn: #7d5e12;
  --warn-wash: #f7f0dd;
  --bad: #97403c;
  --bad-wash: #f9ebea;
  --shadow: 0 1px 2px rgba(35, 37, 46, .05), 0 8px 24px -14px rgba(35, 37, 46, .22);
  color-scheme: light;
}

/* Inter, alojada en el propio servidor. SIL Open Font License. */
@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("/static/fonts/inter-var.woff2") format("woff2");
}

* { box-sizing: border-box; }

/* El atributo `hidden` gana a cualquier clase.
   La regla del navegador es `[hidden] { display: none }`, que pierde en
   especificidad contra `.btn { display: inline-flex }` — así que el botón de
   silenciar el micrófono, marcado como oculto hasta que hay clase, se veía
   igualmente desde el primer momento. */
[hidden] { display: none !important; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font);
  font-size: 16px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  text-wrap: pretty;
}

a { color: var(--accent-dark); text-decoration-thickness: 1px; text-underline-offset: 2px; }
a:hover { color: var(--accent); }

:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
::selection { background: var(--accent-wash); color: var(--ink); }

h1, h2, h3 { font-family: var(--font-display); font-weight: 500; line-height: 1.18; margin: 0; }
h1 { font-size: 2rem; letter-spacing: -.02em; }
h2 { font-size: 1.3rem; letter-spacing: -.01em; }
h3 { font-size: 1rem; font-weight: 600; }

p { margin: 0 0 .8em; }
p:last-child { margin-bottom: 0; }

/* --- shell ---------------------------------------------------------------- */
/* Opaque by default, translucent only where the blur behind it really works.
   The other way round — translucent always, blurred if supported — is what it
   used to be, and it meant that anywhere `backdrop-filter` does not apply the
   header was simply 85% opaque over a scrolling page. On `/admin/usuarios`
   and `/admin/mensajes` the row of pills sits exactly where the bar ends up
   after a small scroll, so it showed through and read as a second row of tabs
   riding on the first.

   Not a rare configuration: Safari before 18 needs the `-webkit-` prefix,
   which was missing, and Firefox drops the effect whenever hardware
   acceleration is off. */
.topbar {
  background: var(--paper);
  border-bottom: 1px solid var(--line-soft);
  position: sticky;
  top: 0;
  z-index: 20;
}
@supports ((backdrop-filter: blur(10px)) or (-webkit-backdrop-filter: blur(10px))) {
  .topbar {
    background: color-mix(in srgb, var(--paper) 85%, transparent);
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
  }
}
.topbar__inner {
  max-width: var(--shell);
  margin: 0 auto;
  padding: .7rem 1.25rem;
  display: flex;
  align-items: center;
  gap: 1.5rem;
}
/* La marca. El símbolo va sin `fill` en el marcado y se pinta desde aquí, con
   los tokens que ya existen: así el tema claro no necesita otro dibujo y la
   marca cambia sola cuando cambia el tema. El color va por `nth-child`, de
   modo que **el orden de los cuatro `rect` en el parcial importa**. */
.brand {
  display: flex;
  align-items: center;
  gap: .55rem;
  white-space: nowrap;
  font-family: var(--font-display);
  font-size: 1.05rem;
  font-weight: 500;
  letter-spacing: -.02em;
  color: var(--ink);
  text-decoration: none;
}
.brand i { font-style: normal; font-weight: 300; color: var(--ink-soft); }
/* `flex: none` porque en la barra el símbolo comparte fila con una navegación
   que sí puede encogerse, y sin esto se aplasta antes que ella. */
.brand__mark { height: 20px; width: auto; flex: none; }
.brand__mark rect { fill: var(--line); }
.brand__mark rect:nth-child(2) { fill: var(--accent); }
.brand__mark rect:nth-child(3) { fill: var(--accent-dark); }

/* Apilada: entrar, crear cuenta, pantallas de espera. El margen porque debajo
   va el título de la pantalla, y sin él la marca y el titular se tocan. */
.brand--stack { flex-direction: column; gap: .75rem; font-size: 1.3rem; }
.brand--stack .brand__mark { height: 35px; }
.page-head .brand--stack { margin-bottom: 1.1rem; }

/* En la clase preparada el símbolo pierde el acento. Es la única pantalla con
   un solo botón que importa, y dos cosas moradas compitiendo son una de más.
   A cambio va algo más grande: en gris y a 20 px desaparecía del todo. */
.live-hero .brand__mark { height: 26px; margin-bottom: 1rem; }
.live-hero .brand__mark rect { fill: var(--line); }
.live-hero .brand__mark rect:nth-child(2) { fill: var(--ink-faint); }
.live-hero .brand__mark rect:nth-child(3) { fill: var(--ink-soft); }
.nav { display: flex; gap: 1.1rem; flex: 1; flex-wrap: wrap; }
.nav a {
  color: var(--ink-soft);
  text-decoration: none;
  font-size: .92rem;
  font-weight: 500;
  padding: .2rem 0;
  border-bottom: 1px solid transparent;
}
.nav a:hover { color: var(--ink); }
.nav a.is-active { color: var(--ink); border-bottom-color: var(--accent); }

.wallet {
  display: inline-flex;
  align-items: baseline;
  gap: .35rem;
  border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent);
  color: var(--accent-dark);
  background: transparent;
  border-radius: 999px;
  padding: .28rem .8rem;
  font-size: .87rem;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  text-decoration: none;
  white-space: nowrap;
}
.wallet:hover { background: var(--accent-wash); color: var(--accent-dark); }

/* El nav va vacío en la cabecera pública; sin esto se queda ocupando el hueco
   flexible y empuja los enlaces de acceso contra el borde. */
.nav:empty { display: none; }
.topbar__link {
  color: var(--ink-soft);
  text-decoration: none;
  font-size: .92rem;
  font-weight: 500;
  white-space: nowrap;
}
.topbar__link:hover { color: var(--ink); }
.wallet small { font-weight: 400; color: var(--ink-faint); }

main { max-width: var(--shell); margin: 0 auto; padding: 2.2rem 1.25rem 5rem; }
.page-head { margin-bottom: 1.5rem; }
.page-head p { color: var(--ink-soft); max-width: 62ch; }
/* Entregado como `.page-head .kicker`; generalizado porque el hero de la
   clase en vivo lleva el mismo sobretítulo y no está dentro de `.page-head`.
   El descendiente era un subconjunto de esto, así que nada pierde estilo. */
.kicker {
  font-size: .76rem;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: .35rem;
}

/* --- cards ---------------------------------------------------------------- */
.card {
  background: var(--card);
  border: 1px solid var(--line-soft);
  border-radius: var(--radius);
  padding: 1.25rem;
  box-shadow: var(--shadow);
}
/* Separación entre bloques apilados uno tras otro.

   Una rejilla y una tarjeta son la misma cosa en el flujo de la página: un
   bloque con una caja alrededor. `.card + .card` a secas se dejaba fuera
   todos los pares en los que uno de los dos era una rejilla, y una rejilla
   seguida de una tarjeta salía a cero: dos cajas sin hueco se leen como una
   sola con una raya en medio. Pasaba en las fichas de `/admin/usuarios/{id}`
   y `/admin/mensajes/{id}`, que son las pantallas que alternan las dos. */
/* …y un `<form>` que envuelve tarjetas o rejillas cuenta igual. La cadena de
   arriba mira al hermano inmediato, y un formulario alrededor la parte: en
   `/clases/nueva` la tarjeta del micrófono va seguida del `<form>` de la
   reserva, así que `.card + .grid` no llegaba a verse y las dos cajas salían
   pegadas a cero mientras el resto de la página separaba a 16 px. */
.card + .card,
.card + .grid,
.card + form,
.grid + .card,
.grid + .grid,
.grid + form,
form + .card,
form + .grid { margin-top: 1rem; }
/* …pero no dentro de una rejilla, que ya separa con `gap`. Ahí el margen
   empujaba hacia abajo a toda tarjeta que no fuera la primera y, como la
   fila se estira a la misma altura, la dejaba además 16 px más baja: dos
   cajas contiguas con distinto alto y desalineadas. */
.grid > .card + .card { margin-top: 0; }
.card__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: .9rem;
  flex-wrap: wrap;
}
/* Lo de la derecha se queda a la derecha aunque se caiga de línea.

   `space-between` solo reparte lo que comparte línea. En cuanto el título se
   alarga —el objetivo del curso, en el panel— el bloque de la derecha envuelve
   a una línea propia y, siendo el único que hay en ella, `space-between` lo
   deja pegado a la izquierda con medio ancho de tarjeta en blanco al lado. En
   el panel eso salía a 550 px de hueco por debajo de 860 px de ventana, y
   como solo pasa cuando hay objetivo, dos cursos seguidos alineaban sus
   botones en lados distintos.

   `margin-left:auto` empuja igual en los dos casos, así que la posición deja
   de depender de si el texto de al lado cabe. */
.card__head > :last-child:not(:only-child) { margin-left: auto; }
.card--flat { box-shadow: none; background: transparent; }
.card--accent {
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 22%, transparent),
              0 10px 30px -18px var(--accent);
}

.grid { display: grid; gap: 1rem; }
/* `min(Xpx, 100%)` y no `Xpx` a secas: un `minmax` con mínimo fijo no baja
   de él, así que en una pantalla más estrecha que el mínimo la rejilla se
   sale del viewport en lugar de pasar a una columna. */
.grid--2 { grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr)); }
.grid--3 { grid-template-columns: repeat(auto-fit, minmax(min(230px, 100%), 1fr)); }
.stack > * + * { margin-top: 1rem; }

.muted { color: var(--ink-soft); }
.faint { color: var(--ink-faint); font-size: .86rem; }
.tiny { font-size: .8rem; }

/* --- disclosures ---------------------------------------------------------- */
/* `<details>` sale del navegador con la flecha y poco más, y en un formulario
   eso no se lee como algo en lo que se pueda pulsar. Global y no de una
   pantalla: lo usan la transcripción del informe y dos pantallas del panel, y
   las tres querían esto. */
/* Sin tocar el `display`: en cuanto deja de ser `list-item` Chrome y Safari
   se llevan por delante el triángulo, que es toda la señal de que aquí se
   puede pulsar. El área de toque se gana con relleno. */
details > summary {
  cursor: pointer;
  font-weight: 500;
  font-size: .89rem;
  color: var(--ink);
  padding: .5rem 0;
}
details[open] > summary { margin-bottom: .6rem; }
details > summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* --- flags ---------------------------------------------------------------- */
/* Dibujadas, no escritas: Windows no tiene ninguna fuente capaz de pintar un
   emoji de bandera y las sacaba como dos letras en cajitas. Ver `flags.py`.

   Se dimensiona en `em` para que siga al texto del que va delante — la misma
   bandera aparece dentro de un `h1` de la ficha del curso y dentro de una
   celda de tabla del panel. */
.flag {
  width: 1.35em;
  height: .9em;
  flex: none;
  border-radius: 2px;
  vertical-align: -.06em;
  /* Japón y Corea son casi todo blanco y se disolvían en la tarjeta que hay
     detrás. El filo va aquí y no en el SVG porque depende del tema, no de la
     bandera. */
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--ink) 14%, transparent);
}

/* --- buttons -------------------------------------------------------------- */
/* El primario es un contorno del acento. Un bloque de color saturado en la
   pantalla previa a hablar añade presión donde no hace falta. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .45rem;
  min-height: 44px;
  border: 1px solid var(--accent);
  border-radius: var(--radius-sm);
  padding: .5rem 1.05rem;
  font: inherit;
  font-weight: 500;
  font-size: .93rem;
  cursor: pointer;
  text-decoration: none;
  background: transparent;
  color: var(--accent-dark);
  transition: background .14s ease, border-color .14s ease, color .14s ease;
}
.btn:hover {
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  color: var(--accent-dark);
  border-color: var(--accent-dark);
}
.btn:active { background: color-mix(in srgb, var(--accent) 26%, transparent); }
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.btn[disabled], .btn[aria-disabled="true"] {
  opacity: .45;
  cursor: not-allowed;
  background: transparent;
  border-color: var(--line);
  color: var(--ink-faint);
}
.btn--ghost { border-color: var(--line); color: var(--ink); }
.btn--ghost:hover { background: var(--line-soft); border-color: var(--ink-faint); color: var(--ink); }
.btn--danger { border-color: color-mix(in srgb, var(--bad) 60%, transparent); color: var(--bad); }
.btn--danger:hover { background: var(--bad-wash); border-color: var(--bad); color: var(--bad); }
.btn--big { min-height: 56px; padding: .9rem 1.7rem; font-size: 1.05rem; }
.btn--block { width: 100%; }

/* Entrar con Google. Neutro a propósito: el botón de un tercero no compite
   con el primario de la pantalla, que es el formulario de al lado. El
   logotipo lleva sus cuatro colores porque las normas de marca de Google no
   permiten repintarlo, así que el fondo se queda en el papel de la tarjeta
   para que se vea igual de bien en claro y en oscuro. */
.btn--google {
  min-height: 48px;
  border-color: var(--line);
  color: var(--ink);
  background: var(--paper);
  gap: .6rem;
}
.btn--google:hover { background: var(--line-soft); border-color: var(--ink-faint); color: var(--ink); }
.btn--google svg { flex: none; }

/* La raya con la «o» en medio. Marca que son dos caminos al mismo sitio y no
   dos pasos del mismo. */
.or {
  display: flex;
  align-items: center;
  gap: .8rem;
  margin: 1.1rem 0;
  color: var(--ink-faint);
  font-size: .84rem;
}
.or::before, .or::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--line-soft);
}

.actions { display: flex; gap: .6rem; flex-wrap: wrap; align-items: center; }

/* Dos botones grandes que son dos salidas distintas y no un botón con su
   matiz. `.btn` es inline-flex, así que lo único que los separaba era el
   espacio entre dos palabras: pegados en ancho y, al partirse la línea en el
   móvil, sin nada de aire entre ellos. El hueco de arriba se pensó para los
   botones normales y entre dos de 56px se sigue leyendo como uno solo. */
.actions--center {
  justify-content: center;
  gap: 1rem;
  margin-bottom: 1.1rem;
}

/* Las tarjetas de paquete. El número grande son las clases y no el precio:
   quien mira esta rejilla está comprando clases, y el precio solo tiene
   sentido cuando ya sabe cuántas se lleva.

   El precio por clase es lo que hace comparables los tres: sin esa línea,
   60 € es «el caro» en vez de «el barato», que es justo lo contrario de lo
   que dice la aritmética. Va al otro extremo de la fila y no pegado al
   total, porque juntos —«30 € 6 € la clase»— se leen como un solo número
   mal escrito; separados, además, los tres quedan en columna y la escalera
   de 7, 6 y 5 se ve de arriba abajo sin leer nada. */
.pack__classes {
  font-size: 1.95rem;
  font-weight: 400;
  letter-spacing: -.025em;
  line-height: 1.1;
}
.pack__of { color: var(--ink-soft); font-size: .9rem; margin-top: .1rem; }
.pack__price {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: .5rem;
  margin: .85rem 0 .5rem;
  padding-top: .75rem;
  border-top: 1px solid var(--line-soft);
}
.pack__amount { font-size: 1.45rem; font-weight: 500; letter-spacing: -.02em; }
.pack__each { font-size: .88rem; color: var(--ink-soft); }

/* Una salida para quien no sabe contestar la pregunta de al lado.
   Objeto propio y no una línea de ayuda más: debajo del selector de nivel ya
   hay un párrafo gris —la descripción del nivel elegido— y dos textos grises
   seguidos se leen como el mismo texto, con el enlace de dentro pasando por
   una nota al pie. */
.nudge {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: .7rem 1rem;
  margin-top: .9rem;
  padding: .8rem 1rem;
  border: 1px solid color-mix(in srgb, var(--accent) 32%, transparent);
  border-radius: var(--radius-sm);
  background: var(--accent-wash);
}
/* `18rem` de base para que en una columna estrecha el texto se quede con la
   fila entera y el botón caiga debajo, en vez de partirse en tres palabras
   por línea. */
.nudge__text { display: flex; flex-direction: column; gap: .1rem; flex: 1 1 18rem; }
.nudge__title { font-weight: 600; color: var(--ink); }
.nudge .btn { flex: none; }
@media (max-width: 480px) { .nudge .btn { width: 100%; } }

/* --- forms ---------------------------------------------------------------- */
label { display: block; font-weight: 500; font-size: .89rem; margin-bottom: .32rem; color: var(--ink); }
.field { margin-bottom: 1.05rem; }
.field .hint { font-size: .84rem; color: var(--ink-faint); margin-top: .35rem; }
.field--error label { color: var(--bad); }
.field--error input, .field--error select, .field--error textarea { border-color: var(--bad); }
.field--error .hint { color: var(--bad); }

/* `input:not([type])` incluido a propósito: un input sin atributo `type` es
   de texto para el navegador pero no lo casa `input[type="text"]`, así que se
   quedaba sin ancho ni borde. Pasaba en cuatro sitios, uno de ellos el nombre
   en el formulario de registro. Y `tel` por lo mismo, un tipo más tarde: el
   teléfono del formulario de presupuesto salía a la mitad de ancho que los
   campos de encima. */
input:not([type]),
input[type="text"], input[type="email"], input[type="password"],
input[type="number"], input[type="search"], input[type="tel"], select, textarea {
  width: 100%;
  min-height: 44px;
  padding: .6rem .75rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--paper) 60%, var(--card));
  font: inherit;
  color: var(--ink);
}
::placeholder { color: var(--ink-faint); }
textarea { min-height: 84px; resize: vertical; }
input:hover, select:hover, textarea:hover { border-color: var(--ink-faint); }
input:focus, select:focus, textarea:focus {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: var(--accent);
}
input[disabled], select[disabled], textarea[disabled] { opacity: .45; cursor: not-allowed; }

/* Radios como tarjetas: una fila de radios de verdad se lee como un
   formulario que rellenar, no como una elección que tomar. */
.choices { display: flex; gap: .5rem; flex-wrap: wrap; }
.choice { position: relative; }
.choice input { position: absolute; opacity: 0; inset: 0; cursor: pointer; }
.choice span {
  display: flex;
  align-items: center;
  gap: .4rem;
  min-height: 44px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: .5rem .9rem;
  font-size: .92rem;
  background: transparent;
  color: var(--ink-soft);
  cursor: pointer;
  transition: border-color .14s ease, background .14s ease, color .14s ease;
}
.choice input:hover + span { border-color: var(--ink-faint); color: var(--ink); }
.choice input:checked + span {
  border-color: var(--accent);
  background: var(--accent-wash);
  color: var(--accent-dark);
}
.choice input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 2px; }
.choice input[disabled] + span { opacity: .45; cursor: not-allowed; }
/* La única opción del selector de nivel que no es un nivel: la prueba, que
   lleva a otra pantalla en vez de matricular. El borde discontinuo lo dice
   antes de pulsarla, que es cuando sirve de algo saberlo. */
.choice--other span { border-style: dashed; }

/* --- messages ------------------------------------------------------------- */
.alert {
  border-radius: var(--radius-sm);
  padding: .75rem 1rem;
  font-size: .92rem;
  margin-bottom: 1.1rem;
  border: 1px solid transparent;
  border-left-width: 2px;
}
.alert--bad { background: var(--bad-wash); border-color: color-mix(in srgb, var(--bad) 35%, transparent); border-left-color: var(--bad); color: var(--ink); }
.alert--good { background: var(--good-wash); border-color: color-mix(in srgb, var(--good) 32%, transparent); border-left-color: var(--good); color: var(--ink); }
.alert--warn { background: var(--warn-wash); border-color: color-mix(in srgb, var(--warn) 32%, transparent); border-left-color: var(--warn); color: var(--ink); }
.alert--info { background: var(--accent-wash); border-color: color-mix(in srgb, var(--accent) 32%, transparent); border-left-color: var(--accent); color: var(--ink); }

/* --- pills and stats ------------------------------------------------------ */
.pill {
  display: inline-flex;
  align-items: center;
  gap: .3rem;
  border-radius: 999px;
  padding: .16rem .62rem;
  font-size: .76rem;
  font-weight: 500;
  letter-spacing: .01em;
  border: 1px solid var(--line);
  background: transparent;
  color: var(--ink-soft);
  white-space: nowrap;
}
.pill--live { border-color: color-mix(in srgb, var(--good) 50%, transparent); color: var(--good); }
.pill--pending { border-color: color-mix(in srgb, var(--warn) 45%, transparent); color: var(--warn); }
.pill--bad { border-color: color-mix(in srgb, var(--bad) 45%, transparent); color: var(--bad); }
.pill--level { border-color: color-mix(in srgb, var(--accent) 50%, transparent); color: var(--accent-dark); }

.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(112px, 1fr)); gap: .7rem; }

/* Barras de uso por semana. Barras y no una línea porque son semanas contadas
   y no una serie continua — y sobre todo porque una semana sin clases tiene
   que verse vacía, que es justo lo que una línea que une los puntos que
   existen se traga. */
.bars { display: flex; align-items: flex-end; gap: .35rem; height: 9rem; }
.bars__col { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column;
             justify-content: flex-end; height: 100%; text-align: center; }
.bars__bar {
  background: color-mix(in srgb, var(--accent) 55%, transparent);
  border-radius: 3px 3px 0 0;
  min-height: 2px;
}
.bars__label {
  font-size: .68rem; color: var(--ink-faint); margin-top: .35rem;
  white-space: nowrap; overflow: hidden; text-overflow: clip;
}
/* Con muchas semanas las fechas se pisan: se queda una de cada tres, y esa
   puede salirse de su columna porque las vecinas se han quedado en blanco.
   Sin el overflow visible la fecha se recorta a lo ancho de su barra, que a
   partir de unas veinte semanas es «18/» y no una fecha. */
.bars--dense .bars__label { display: none; overflow: visible; }
.bars--dense .bars__col:nth-child(3n + 1) .bars__label { display: block; }
@media (max-width: 640px) {
  /* En una pantalla de móvil no caben ni catorce. */
  .bars .bars__label { display: none; overflow: visible; }
  .bars .bars__col:nth-child(3n + 1) .bars__label { display: block; }
  .bars--dense .bars__col:nth-child(3n + 1) .bars__label { display: none; }
  .bars--dense .bars__col:nth-child(6n + 1) .bars__label { display: block; }
}
.stat {
  background: transparent;
  border: 0;
  border-left: 1px solid var(--line-soft);
  border-radius: 0;
  padding: .1rem .85rem;
}
.stat__label { font-size: .74rem; color: var(--ink-faint); font-weight: 500; text-transform: uppercase; letter-spacing: .07em; }
.stat__value { font-size: 1.9rem; font-weight: 400; font-variant-numeric: tabular-nums; letter-spacing: -.02em; line-height: 1.1; }
.stat__delta { font-size: .8rem; font-weight: 500; font-variant-numeric: tabular-nums; }
.up { color: var(--good); }
.down { color: var(--bad); }
.flat { color: var(--ink-faint); }

/* Medidores. El número solo no dice nada; la barra es lo que hace que 62 y 84
   se sientan distintos de un vistazo. */
.meter { margin-bottom: .8rem; }
.meter__top { display: flex; justify-content: space-between; font-size: .88rem; margin-bottom: .3rem; }
.meter__top strong { font-weight: 500; font-variant-numeric: tabular-nums; }
.meter__track { height: 4px; border-radius: 999px; background: var(--line-soft); overflow: hidden; }
.meter__fill {
  height: 100%;
  border-radius: 999px;
  background: var(--accent);
  box-shadow: 0 0 8px color-mix(in srgb, var(--accent) 60%, transparent);
  transition: width .4s ease;
}

/* The microphone level. Not a `.meter`: this one is redrawn every frame while
   somebody talks, and `.meter__fill`'s .4s transition would turn a level into
   a lag. Taller too — it is the thing being watched, not a figure's footnote. */
.mic-level { height: 10px; border-radius: 999px; background: var(--line-soft); overflow: hidden; }
.mic-level__bar { display: block; height: 100%; width: 0; border-radius: 999px; background: var(--good); }
/* La versión de una línea, para la pantalla de reservar. Ahí esto es lo
   primero que hay que hacer y sale bien a la primera casi siempre, así que en
   reposo no debe pesar más que el aviso que es. Solo se encogen los estados
   tranquilos: un fallo conserva el sitio de siempre, porque es el único
   momento en que las palabras son el contenido. */
.mic-check--compact { padding: .8rem 1.1rem; }
.mic-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .9rem;
  flex-wrap: wrap;
}
/* Título y motivo en la misma línea mientras quepan, y el motivo debajo
   cuando no — nunca el botón encima del título. */
.mic-row__text {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: .1rem .55rem;
  min-width: 0;
}
.mic-row h2 { font-size: 1.02rem; }
.mic-ok { color: var(--good); }

.mic-check ul { margin: .45rem 0 0; padding-left: 1.15rem; }
.mic-check li + li { margin-top: .3rem; }
.mic-check .alert :last-child { margin-bottom: 0; }
.mic-check .alert p { margin: .5rem 0 0; }

/* --- sparkline ------------------------------------------------------------ */
.spark { display: block; width: 100%; height: 56px; overflow: visible; }
.spark__line { fill: none; stroke: var(--accent); stroke-width: 1.5; stroke-linejoin: round; stroke-linecap: round; vector-effect: non-scaling-stroke; }
.spark__area { fill: color-mix(in srgb, var(--accent) 14%, transparent); }
.spark__dot { fill: var(--accent-dark); }

/* --- lists and tables ----------------------------------------------------- */
.list { list-style: none; margin: 0; padding: 0; }
.list > li { padding: .7rem 0; border-bottom: 1px solid var(--line-soft); }
.list > li:last-child { border-bottom: 0; padding-bottom: 0; }
.list > li:first-child { padding-top: 0; }

.row { display: flex; gap: 1rem; align-items: center; justify-content: space-between; }

/* --- las fichas de curso --------------------------------------------------- */
/* «Tus cursos», en `/cursos`. Eran filas de `.list`: un flex sin `wrap`, con el
   título y el objetivo a la izquierda y el botón a la derecha. El objetivo es
   texto libre y casi siempre una frase entera, así que en un móvil empujaba al
   botón contra el borde y lo dejaba partido en dos líneas — el bug que se ve, y
   el síntoma de que ahí había una ficha y no una fila.

   Rejilla y no columnas fijas: dos cursos ocupan media pantalla cada uno, uno
   solo no se estira a lo ancho de un portátil, y por debajo de 15 rem pasan a
   una columna sin necesidad de una media query. */
.courses {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
  gap: .8rem;
}

/* Borde y nada de fondo: la ficha vive dentro de una tarjeta que ya tiene el
   suyo, y un segundo relleno la convierte en una caja dentro de otra caja. */
.course {
  display: flex;
  flex-direction: column;
  gap: .45rem;
  border: 1px solid var(--line-soft);
  border-radius: var(--radius);
  padding: .9rem 1rem 1rem;
  min-width: 0;
}
.course__head { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
/* La bandera es un SVG en línea; sin esto se encoge cuando el título es largo. */
.course__flag { flex: none; display: inline-flex; }
.course__title { font-size: 1rem; font-weight: 600; margin: 0; min-width: 0; }
.course__meta { margin: 0; color: var(--ink-faint); font-size: .8rem; }
/* Dos líneas y luego puntos suspensivos. El recorte es de aquí y no del
   servidor: en una pantalla ancha la frase entera cabe, y cortarla en Python
   la dejaría cortada para todo el mundo. */
.course__goal {
  margin: .1rem 0 0;
  color: var(--ink-soft);
  font-size: .86rem;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}
/* El informe de la última clase, dentro de la ficha del curso.

   Enlace entero y no un renglón con un enlace dentro: todo lo que hay aquí
   —la nota, la fecha y el resumen— es un adelanto de la misma página, y un
   objetivo del tamaño de un dedo importa más que uno del tamaño de «ver».

   Fondo y no borde: la ficha ya está dentro de una tarjeta con borde, y un
   tercer marco la convierte en una caja dentro de otra caja dentro de otra. */
.course__last {
  display: flex;
  gap: .6rem;
  align-items: flex-start;
  margin-top: .15rem;
  padding: .5rem .6rem;
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--ink) 4%, transparent);
  color: inherit;
  text-decoration: none;
}
.course__last:hover { background: color-mix(in srgb, var(--ink) 8%, transparent); }
/* La nota, en su columna: es el número que se busca al abrir esto, y con el
   resumen al lado se lee sin leerlo. */
.course__score {
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  min-width: 2.3rem;
  line-height: 1.15;
}
.course__score strong { font-size: 1.05rem; font-variant-numeric: tabular-nums; }
.course__score small {
  font-size: .62rem;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--ink-faint);
}
.course__last-text { min-width: 0; display: flex; flex-direction: column; gap: .1rem; }
.course__last-when {
  font-size: .72rem;
  text-transform: uppercase;
  letter-spacing: .05em;
  color: var(--ink-faint);
}
/* Dos líneas, por lo mismo que el objetivo de arriba: el resumen es un
   párrafo entero y aquí solo cabe su principio. */
.course__last-summary {
  font-size: .84rem;
  color: var(--ink-soft);
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}

/* Su propia línea, con `margin-top:auto` para que dos fichas de distinto alto
   —una con objetivo y otra sin él— tengan los botones a la misma altura. */
.course__actions {
  display: flex;
  gap: .5rem;
  flex-wrap: wrap;
  align-items: center;
  margin-top: auto;
  padding-top: .55rem;
}
/* Un curso en pausa sigue estando, con su historial: se apaga, no se esconde. */
.course--paused { opacity: .72; }

@media (max-width: 480px) {
  /* A un dedo de ancho las dos siguen cabiendo en la misma línea —apilarlas
     alargaba cada ficha noventa píxeles para no ganar nada—, pero el reparto
     deja de ser el natural: «Nueva clase» se queda con todo el hueco que sobra
     y «Ver curso» con lo justo. Es la acción de la pantalla, y aquí se lee
     cansado un martes por la noche.

     Si en algún idioma los dos rótulos no caben, `flex-wrap` los pasa a dos
     líneas y cada uno ocupa la suya: nada se aprieta ni se corta. */
  .course__actions .btn:first-child { flex: 1 1 8rem; }
}
.row__main { min-width: 0; }
.row__title { font-weight: 500; }

table { width: 100%; border-collapse: collapse; font-size: .91rem; }
th, td { text-align: left; padding: .6rem .6rem; border-bottom: 1px solid var(--line-soft); vertical-align: top; }
th { font-size: .74rem; text-transform: uppercase; letter-spacing: .07em; color: var(--ink-faint); font-weight: 500; }
tbody tr:hover { background: color-mix(in srgb, var(--ink) 4%, transparent); }
td.num { text-align: right; font-variant-numeric: tabular-nums; }
.table-wrap { overflow-x: auto; }

/* 360 px: la tabla de correcciones deja de ser tabla y pasa a ser una lista
   de fichas, con el nombre de la columna delante de cada valor. */
@media (max-width: 560px) {
  .table--cards thead { display: none; }
  .table--cards tr {
    display: block;
    border: 1px solid var(--line-soft);
    border-radius: var(--radius-sm);
    padding: .6rem .7rem;
    margin-bottom: .6rem;
  }
  .table--cards td { display: flex; gap: .6rem; border: 0; padding: .18rem 0; }
  .table--cards td::before {
    content: attr(data-label);
    flex: 0 0 6.5rem;
    font-size: .72rem;
    text-transform: uppercase;
    letter-spacing: .07em;
    color: var(--ink-faint);
    padding-top: .2rem;
  }
  .table--cards td:empty { display: none; }
}

/* --- badges --------------------------------------------------------------- */
.badges { display: flex; flex-wrap: wrap; gap: .6rem; }
.badge {
  display: flex;
  gap: .55rem;
  align-items: flex-start;
  background: transparent;
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-sm);
  padding: .6rem .8rem;
  max-width: 260px;
}
.badge__icon { font-size: 1.25rem; line-height: 1.2; filter: saturate(.75); }
.badge__title { font-weight: 500; font-size: .9rem; }

/* --- lesson-specific ------------------------------------------------------ */
/* La pantalla más emocional del producto: mucho aire, un halo del acento
   detrás del botón, ningún borde duro, ninguna cuenta atrás. */
.live-hero {
  position: relative;
  overflow: hidden;
  background:
    radial-gradient(120% 90% at 50% 0%, color-mix(in srgb, var(--accent) 22%, transparent), transparent 70%),
    linear-gradient(180deg, var(--card), var(--paper));
  border: 1px solid var(--line-soft);
  border-radius: var(--radius);
  padding: 3.2rem 2rem;
  text-align: center;
}
.live-hero h1 { font-size: 2.1rem; margin-bottom: .4rem; }
.live-hero p { color: var(--ink-soft); }
/* `:not(.btn--ghost)` porque el hero lleva dos botones grandes — «Entrar» y
   «Terminar» — y sin esto el descendiente gana en especificidad al modificador
   ghost: los dos salían con el halo del acento y con el mismo peso visual,
   que es justo lo contrario de lo que la jerarquía quiere decir. */
.live-hero .btn--big:not(.btn--ghost) {
  border-color: var(--accent);
  color: var(--accent-dark);
  box-shadow: 0 0 0 6px color-mix(in srgb, var(--accent) 10%, transparent);
}
.live-hero .btn--big:not(.btn--ghost):hover {
  box-shadow: 0 0 0 10px color-mix(in srgb, var(--accent) 14%, transparent);
}
/* Una tarjeta dentro del hero — lo que la clase preparada cuenta antes del
   botón. Alineada a la izquierda porque es texto que se lee, no un titular,
   y con aire por los dos lados porque el botón que va debajo lleva un halo
   de 6 px que crece a 10 al pasar por encima: sin margen el halo se montaba
   encima del borde de la tarjeta y las dos cajas se leían como una sola. */
.live-hero > .card {
  margin: 1.6rem 0;
  text-align: left;
}
/* Y lo mismo por fuera: el hero no es `.card`, así que la comprobación del
   micrófono que va justo detrás se quedaba fuera de la regla que separa un
   bloque del siguiente. */
.live-hero + .card,
.live-hero + .grid { margin-top: 1rem; }

/* La clase dada en esta misma página, sobre una conexión directa con el
   worker. Sin Jitsi no hay ventana de videollamada, así que lo único que se
   ve del profesor es su voz: el orbe late con ella. */
.live-room { margin-top: 1.4rem; }

/* El reloj de la clase: 3 px en el borde de arriba de la sala.
   Va aquí y no en la tarjeta de estado de abajo porque «¿cuánto me queda?» es
   la pregunta de mitad de clase, y hasta ahora había que bajar a buscarla. Una
   barra no se lee, se ve: el número exacto está al lado del estado para quien
   lo busque. */
.timebar {
  position: absolute; top: 0; left: 0; right: 0; height: 3px;
  background: color-mix(in srgb, var(--accent) 14%, transparent);
}
.timebar span {
  display: block; height: 100%; background: var(--accent);
  transition: width .6s linear;
}
.room-clock { color: var(--ink-faint); font-size: .86rem; margin-left: .5rem;
              font-variant-numeric: tabular-nums; white-space: nowrap; }

/* Los mandos de la clase.

   Una sola fila, centrada, con los botones en un orden que no cambia: pausar,
   silenciar, y —antes de haber entrado— entrar. Lo que cambia con el estado es
   cuáles se ven, nunca dónde están, porque lo que se busca a mitad de clase se
   busca con prisa y con la vista en otra parte.

   `min-height` para que la fila no se encoja al ocultarse un botón: la sala
   entera daba un salto hacia arriba en cuanto la conexión se establecía, justo
   en el segundo en que el alumno está mirando. */
.room-controls {
  display: flex; gap: .6rem; flex-wrap: wrap;
  align-items: center; justify-content: center;
  min-height: 44px;
}

/* Lo que se le puede pedir al profesor sin hablar.

   Debajo de los mandos y en voz más baja que ellos: pausar y silenciar son
   cosas que le haces a la clase, y esto son cosas que le pides a una persona.
   Tres botones callados en una fila que envuelve, porque en un móvil estrecho
   caben dos y medio. */
.room-asks {
  display: flex; gap: .5rem; flex-wrap: wrap; justify-content: center;
  margin-top: .8rem;
}

/* «No te oímos».

   Ámbar y no rojo, y una línea y no una tarjeta: es algo que se puede
   arreglar en diez segundos, casi siempre un interruptor del casco, y quien lo
   lee está en mitad de una clase que ya le pone nervioso. El brief lo dice con
   todas las letras — nada de rojo en esta pantalla. */
.room-quiet {
  max-width: 40ch;
  margin: .9rem auto 0;
  padding: .55rem .8rem;
  border: 1px solid color-mix(in srgb, var(--warn) 45%, transparent);
  border-radius: var(--radius-sm);
  background: var(--warn-wash);
  color: var(--ink);
  font-size: .86rem;
}

/* El mismo aviso, cuando lo que hay que hacer es pulsar.

   El navegador que se niega a reproducir al profesor solo cede ante un gesto
   del usuario, así que el aviso tiene que llevar dentro el gesto: contarle a
   alguien que su clase está muda y dejarle buscar el remedio es contarle el
   problema y quedarse la solución. */
.room-quiet--act {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: .45rem .7rem;
  text-align: center;
}

/* La clase parada.

   Ocupa el sitio de los mandos en vez de sumarse a ellos: mientras está
   pausada solo hay una cosa que hacer, y dejar «silenciar» al lado es ofrecer
   maneras de hablarle a alguien que no está escuchando. El fondo la separa de
   una sala que por lo demás no ha cambiado — sin él, una clase en pausa y una
   clase en marcha se ven igual, y esa es la confusión que cuesta minutos. */
.room-paused {
  background: color-mix(in srgb, var(--accent) 7%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent);
  border-radius: var(--radius);
  padding: 1rem;
  max-width: 32rem;
  margin: 0 auto;
}
.room-paused__title { font-weight: 600; margin: 0 0 .2rem; }
.room-paused .actions { margin-top: .9rem; }

/* La salida de la clase. Separada por una línea y sin peso de botón: es la
   única acción que cierra algo que se está pagando, y estaba compartiendo fila
   y tamaño con la que lo abre. Visible siempre — esconder la salida de una
   llamada es peor que tenerla cerca — pero en voz baja. */
.way-out { border-top: 1px solid var(--line-soft); margin-top: 1.6rem; padding-top: 1rem; }
.btn--quiet {
  /* Discreto no es pequeño ni invisible: lleva su borde como cualquier otro
     botón —sin él se lee como un enlace y nadie sabe que se puede pulsar— y
     conserva los 44 px de alto, porque es un objetivo táctil como los demás.
     Lo que lo baja de rango es el color y el tamaño de letra, no quitarle la
     forma de botón. */
  border-color: var(--line); color: var(--ink-soft);
  padding: .35rem .9rem; font-size: .86rem; font-weight: 400;
}
.btn--quiet:hover { border-color: var(--ink-faint); color: var(--ink); }

/* Lo que se ha pedido —la última frase— aterriza aquí: alineado a la
   izquierda y con medida de lectura, dentro de un hero centrado. */
.asked { max-width: 52ch; margin: .9rem auto 0; text-align: left; }

/* El panel de lectura dentro de la sala.
   Vive dentro del hero, que está centrado y apretado contra los botones de
   arriba, así que aquí se le devuelven las dos cosas que perdió al mudarse:
   aire por encima —estaba pegado a «Silenciar mi micrófono»— y el texto
   alineado a la izquierda, porque una conversación centrada no se lee como
   una conversación. */
.room-subtitles { margin-top: 2.6rem; }
.room-subtitles .actions { position: relative; gap: 1rem; margin-bottom: 1.1rem; }
/* El spinner reserva sitio siempre (ver la sección de HTMX), y en una fila
   centrada eso descentra el botón: media anchura de «Buscando…» más el hueco,
   de forma permanente, para evitar un salto que dura un parpadeo. Aquí sale
   del flujo y aparece centrado debajo, así que el botón queda donde debe y el
   aviso sigue estando cuando el worker tarda. */
.room-subtitles .spinner {
  position: absolute; top: calc(100% + .35rem); left: 0; right: 0;
  text-align: center;
}
/* La medida en `rem` y no en `ch`: la transcripción y su pie tienen tamaños de
   letra distintos, así que dos `60ch` daban dos anchos y las dos columnas no
   se alineaban entre sí. */
.room-subtitles .transcript,
.room-subtitles > p { max-width: 32rem; margin-left: auto; margin-right: auto; }
/* El pie va en la misma columna que la transcripción, no centrado bajo ella:
   suelto en el centro parecía de otra cosa. */
.room-subtitles > p { margin-top: .8rem; text-align: left; }
.live-room__status { min-height: 1.4em; margin: .9rem 0 1.1rem; color: var(--ink-soft); }
.live-room audio { display: none; }

/* `--level` lo escribe el analizador de audio de la página, 0 a 1. Sin JS se
   queda en 0 y el orbe es un círculo quieto, que es exactamente lo que debe
   verse mientras el profesor calla. */
.orb {
  --level: 0;
  width: 96px; height: 96px;
  margin: 0 auto;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%,
    color-mix(in srgb, var(--accent) 85%, white),
    var(--accent) 60%,
    color-mix(in srgb, var(--accent) 70%, black));
  transform: scale(calc(1 + var(--level) * 0.18));
  box-shadow: 0 0 calc(18px + var(--level) * 40px)
              color-mix(in srgb, var(--accent) calc(25% + var(--level) * 45%), transparent);
  /* Corto: por debajo de la duración de una sílaba, o el orbe va por detrás
     de la voz y parece otra cosa. */
  transition: transform .08s linear, box-shadow .08s linear;
}
@media (prefers-reduced-motion: reduce) {
  .orb { transform: none; transition: none; }
}

.dot {
  display: inline-block;
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--good);
  box-shadow: 0 0 0 0 color-mix(in srgb, var(--good) 55%, transparent);
  margin-right: .45rem;
  animation: pulse 2.4s ease-out infinite;
}
@keyframes pulse {
  0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--good) 50%, transparent); }
  70% { box-shadow: 0 0 0 9px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}
@media (prefers-reduced-motion: reduce) {
  .dot { animation: none; }
  .meter__fill { transition: none; }
}

/* La frase «¿qué acaba de decir?». Se lee como una cita, porque es lo que es. */
.repeat {
  margin: 0;
  padding: .85rem 1.1rem;
  border-left: 2px solid var(--accent);
  background: var(--accent-wash);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}
.repeat__who {
  display: block;
  font-size: .72rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: .09em;
  color: var(--accent-dark);
  margin-bottom: .25rem;
}
.repeat p { font-size: 1.1rem; line-height: 1.5; }

.correction { font-family: var(--mono); font-size: .87rem; }
.correction del { color: var(--bad); text-decoration-thickness: 1px; }
.correction ins { color: var(--good); text-decoration: none; }

.exercise { border: 1px solid var(--line-soft); border-radius: var(--radius); padding: 1.1rem; background: var(--card); }
.exercise + .exercise { margin-top: .9rem; }
.exercise__prompt { font-size: 1.05rem; margin-bottom: .8rem; }
.exercise__prompt code { background: var(--line-soft); padding: .1rem .35rem; border-radius: var(--radius-sm); font-family: var(--mono); }

.chunks { display: flex; flex-wrap: wrap; gap: .4rem; margin-bottom: .8rem; }
.chunk {
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: .4rem .7rem;
  background: transparent;
  color: var(--ink);
  cursor: grab;
  font-size: .92rem;
  user-select: none;
}
.chunk.is-picked { background: var(--accent-wash); border-color: var(--accent); color: var(--accent-dark); }

/* --- misc ----------------------------------------------------------------- */
.auth-shell { max-width: 400px; margin: 4rem auto; }
.center { text-align: center; }
.empty { text-align: center; padding: 2.6rem 1rem; color: var(--ink-soft); }
.empty h3 { margin-bottom: .4rem; color: var(--ink); }

hr { border: 0; border-top: 1px solid var(--line-soft); margin: 1.2rem 0; }

/* --- HTMX ----------------------------------------------------------------- */
/* Los fragmentos que se cambian solos no deben saltar. Reservan su altura y
   entran con un fundido corto; el spinner ocupa sitio siempre. */
.spinner { display: inline-block; visibility: hidden; }
.htmx-request .spinner, .htmx-request.spinner { visibility: visible; }
[hx-get], [hx-post] { transition: opacity .12s ease; }
.htmx-request[hx-get], .htmx-request[hx-post] { opacity: .6; }
.htmx-settling { opacity: 0; }
.htmx-added { opacity: 0; }
[hx-swap] > * { animation: fade-in .18s ease both; }
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
#repeat { min-height: 5.2rem; }

@media (max-width: 640px) {
  h1 { font-size: 1.55rem; }
  main { padding: 1.25rem 1rem 4rem; }
  /* Dos filas: marca y saldo arriba, navegación y salida abajo.
     `flex-wrap` es lo que permite que el nav baje de línea — sin él su
     `order` no tiene a dónde ir y los enlaces se apilan cortados contra el
     borde. El nav no envuelve, se desplaza: cinco enlaces en tres filas
     convierten la cabecera en media pantalla. */
  .topbar__inner { gap: .6rem .8rem; flex-wrap: wrap; }
  /* Salto de línea forzado entre el saldo y la navegación. `order` sola no
     basta: ordena, pero no parte, y un nav que puede encogerse acaba
     apretujado al final de la primera fila en lugar de abrir la segunda. */
  .topbar__inner::after {
    content: "";
    flex-basis: 100%;
    height: 0;
    order: 2;
  }
  .nav {
    gap: .9rem;
    order: 3;
    /* `1 1 0` con `min-width: 0`: sin los dos, un elemento flex no baja de
       su ancho de contenido, el nav se queda con la línea entera y «Salir»
       cae a una tercera fila. Con ellos, el nav toma lo que sobra y hace
       scroll por dentro. */
    flex: 1 1 0;
    min-width: 0;
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
  }
  .nav::-webkit-scrollbar { display: none; }
  .nav a { white-space: nowrap; }
  .topbar__inner form { order: 4; }
  .live-hero { padding: 2.2rem 1.1rem; }
  .stats { grid-template-columns: repeat(2, 1fr); }
  .btn--block-mobile { width: 100%; }
}

/* --- landing -------------------------------------------------------------- */
/* La única página que ve alguien sin cuenta. Construida con los componentes
   del producto: enseñar el sitio al que vas es más honesto que una foto de
   archivo, y además no puede prometer una pantalla que no existe. */
.hero {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, .95fr);
  gap: 2.5rem;
  align-items: center;
  padding: 1rem 0 2.5rem;
}
.hero h1 { font-size: clamp(2rem, 4.4vw, 2.9rem); line-height: 1.1; margin-bottom: .9rem; }
.hero__lead { color: var(--ink-soft); font-size: 1.1rem; max-width: 46ch; margin-bottom: 1.4rem; }
.hero__demo .live-hero { padding: 2.2rem 1.5rem; }
/* La maqueta es decorativa: no debe poder recibir el foco al tabular. */
.hero__demo .btn { pointer-events: none; }

/* Dos columnas anchas. `grid--2` reparte en tres a este ancho y deja los
   cuatro pasos en 3 + 1, que se lee como si el último sobrara. */
.grid--wide { grid-template-columns: repeat(auto-fit, minmax(min(420px, 100%), 1fr)); }

main > section + section { margin-top: 3.5rem; }
main > section > .page-head h2 { font-size: 1.6rem; }

@media (max-width: 860px) {
  .hero { grid-template-columns: 1fr; gap: 1.8rem; padding-bottom: 1.5rem; }
  .hero__demo { order: -1; }
  main > section + section { margin-top: 2.5rem; }
}

/* --- legal pages and the account screen ---------------------------------- */

/* A card that is about to do something irreversible. Only the border and the
   heading are tinted: a whole red panel reads as an error that has already
   happened rather than a choice still being offered. */
.card--danger { border-color: color-mix(in srgb, var(--bad) 40%, var(--line)); }
.card--danger .card__head h2 { color: var(--bad); }

/* Two-column key/value, for the identity blocks on the legal pages and the
   account summary. Deliberately not `.table--cards`: these are two or three
   rows of plain facts, not a data table that needs to collapse. */
.table--plain th {
  width: 34%;
  text-align: left;
  font-weight: 600;
  color: var(--ink-soft);
  vertical-align: top;
  padding: .42rem .8rem .42rem 0;
}
.table--plain td { padding: .42rem 0; vertical-align: top; }
@media (max-width: 520px) {
  /* At this width a 34% label column leaves the value four characters wide. */
  .table--plain th, .table--plain td { display: block; width: auto; padding: .1rem 0; }
  .table--plain th { padding-top: .55rem; }
}

/* Un nombre de cookie es un literal: conserva sus minúsculas y su ancho. La
   columna de etiquetas la pone en mayúsculas la regla global de `th`, que está
   bien para «Nombre» y mal para `teachers_session`. */
.table--plain th code { text-transform: none; letter-spacing: 0; font-size: .82rem; }

/* Long-form legal text. The rest of the app is dashboards and forms, so
   nothing existed for a page that is simply read top to bottom. */
.prose { max-width: 68ch; }
.prose h2 { font-size: 1.05rem; margin: 1.9rem 0 .6rem; }
.prose h2:first-child { margin-top: 0; }
.prose p, .prose ul, .prose table { margin-bottom: .9rem; }
.prose ul { padding-left: 1.15rem; }
.prose li { margin-bottom: .4rem; }
.prose li::marker { color: var(--ink-faint); }
/* Los dos botones de consentimiento cuando salen dentro del texto legal y no
   en el banner: ahí flotan sobre el fondo y aquí tienen párrafos pegados
   arriba y abajo. */
.prose .consent__actions { margin: 1rem 0 1.2rem; }

/* The footer. Muted on purpose — it is a legal requirement and a way out, not
   part of the pitch. */
.footer {
  border-top: 1px solid var(--line);
  margin-top: 3rem;
  padding: 1.4rem 0 2rem;
}
.footer__inner {
  max-width: var(--shell);
  margin: 0 auto;
  padding: 0 1.25rem;
  display: flex;
  flex-wrap: wrap;
  gap: .6rem 1.4rem;
  align-items: center;
  justify-content: space-between;
  font-size: .85rem;
  color: var(--ink-faint);
}
.footer__brand { font-weight: 600; }
.footer__links { display: flex; flex-wrap: wrap; gap: .5rem 1.1rem; }
.footer__links a { color: var(--ink-faint); text-decoration: none; }
.footer__links a:hover { color: var(--ink); text-decoration: underline; }

/* The beta mark, next to the brand.

   An `<em>` inside the brand link rather than a sibling, so it travels with
   the logo and cannot end up on its own line when the topbar wraps — which is
   exactly what the nav did on a 390px screen before it was pinned down. */
.brand .beta {
  font-style: normal;
  font-size: .62rem;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  vertical-align: .35em;
  margin-left: .4rem;
  padding: .1rem .34rem;
  border-radius: 5px;
  color: var(--accent);
  background: var(--accent-wash);
  border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
}

/* --- the live transcript -------------------------------------------------- */

/* A `.pill` used as a control rather than a label. The element has to be a
   button — it performs an action and belongs in the tab order — and buttons
   arrive with a font and a background of their own. */
button.pill { cursor: pointer; font: inherit; font-size: .8rem; background: transparent; }
button.pill:hover { border-color: var(--accent); color: var(--accent-dark); }

/* Fixed height and its own scroll: an hour of conversation cannot be allowed
   to push the "Terminar" button off the bottom of the page. */
.transcript {
  /* Una conversación se lee alineada a la izquierda, como un chat. Heredaba
     el centrado del hero desde que el panel vive dentro de la sala. */
  text-align: left;
  max-height: 20rem;
  overflow-y: auto;
  padding: .9rem 1rem;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--paper);
  font-size: .92rem;
}
.transcript__line { margin: 0 0 .55rem; }
.transcript__line:last-child { margin-bottom: 0; }
.transcript__who {
  display: inline-block;
  min-width: 4.5rem;
  margin-right: .4rem;
  font-size: .78rem;
  font-weight: 600;
  color: var(--ink-faint);
}
.transcript__line--teacher .transcript__who { color: var(--accent); }
@media (max-width: 520px) {
  /* Two words of label and four of speech is not a readable line. */
  .transcript__who { display: block; min-width: 0; margin-bottom: .1rem; }
}

/* El titular de la maqueta del hero. Era un `h2` de verdad y entraba en el
   esquema de encabezados como si fuese una sección de la página. */
.demo__title { font-size: 1.5rem; font-weight: 600; margin: 0 0 .3rem; }

/* --- the help button ------------------------------------------------------ */

/* Bottom right, over everything, on every signed-in screen.

   A link and not a button: it goes to a page, so middle-click, "open in new
   tab" and a right-click menu all have to work. Styled as a circle with the
   label beside it on wide screens and the icon alone on narrow ones, where
   50px of thumb matters more than a word.

   `position: fixed` puts it outside the flow, which means nothing below it
   knows it is there — hence the footer padding further down. */
.help-fab {
  position: fixed;
  right: 1.1rem;
  bottom: 1.1rem;
  z-index: 40;
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  padding: .7rem 1.05rem .7rem .85rem;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--card);
  color: var(--ink);
  font-size: .85rem;
  font-weight: 600;
  text-decoration: none;
  box-shadow: 0 6px 20px rgba(0, 0, 0, .35);
  transition: transform .12s ease, border-color .12s ease, color .12s ease;
}
.help-fab:hover {
  border-color: var(--accent);
  color: var(--accent-dark);
  transform: translateY(-1px);
}
/* Keyboard focus has to be obvious on something floating over the page: the
   ring is the only thing telling somebody where they are. */
.help-fab:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.help-fab svg { width: 18px; height: 18px; flex: none; }

/* Room for it. Fixed elements do not reserve space, so without this the
   button sits on top of the last line of the footer on a short page. */
.footer { padding-bottom: 5rem; }

@media (max-width: 640px) {
  /* Icon only. The label is the first thing to go: on a phone this competes
     with the content for the same corner, and a circle reads as help without
     needing to say so. */
  .help-fab { padding: 0; width: 48px; height: 48px; justify-content: center; }
  .help-fab span { display: none; }
  .help-fab svg { width: 20px; height: 20px; }
}

@media (prefers-reduced-motion: reduce) {
  .help-fab { transition: none; }
  .help-fab:hover { transform: none; }
}

/* Printing a report or an invoice should not print a button. */
@media print {
  .help-fab { display: none; }
}

/* --- the consent banner --------------------------------------------------- */
/*
 * Abajo y a lo ancho, no un modal. Nadie viene a esta web a contestar una
 * pregunta sobre cookies: bloquear la página detrás de una capa oscura para
 * cobrar el peaje es justo lo que la guía de la AEPD llama muro de cookies. Se
 * puede seguir leyendo y navegando sin contestar; simplemente no se mide.
 *
 * Encima del botón de ayuda (`z-index: 40`) porque le tapa la esquina mientras
 * está, y se va en cuanto se contesta.
 */
.consent {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 50;
  background: var(--card);
  border-top: 1px solid var(--line);
  box-shadow: 0 -8px 24px rgba(0, 0, 0, .35);
}
.consent__inner {
  max-width: var(--shell);
  margin: 0 auto;
  padding: .9rem 1.1rem;
  display: flex;
  align-items: center;
  gap: 1.2rem;
}
.consent__text {
  margin: 0;
  font-size: .85rem;
  line-height: 1.5;
  color: var(--ink-soft);
}
.consent__text strong { color: var(--ink); }
/* Los dos botones, del mismo tamaño y uno al lado del otro. `flex: none` para
   que «Rechazar» no encoja antes que «Aceptar» cuando el texto aprieta: dos
   botones de anchos distintos es exactamente la asimetría que hay que evitar. */
.consent__actions {
  display: flex;
  gap: .6rem;
  flex: none;
}
.consent__actions .btn { flex: none; }

@media (max-width: 640px) {
  /* En vertical, y los botones a lo ancho: en un móvil la fila de arriba deja
     dos botones de dos centímetros que se pulsan mal. */
  .consent__inner { flex-direction: column; align-items: stretch; gap: .8rem; }
  .consent__actions .btn { flex: 1; text-align: center; }
}

/* Sitio para el banner, igual que se lo hace el pie al botón de ayuda. Solo
   cuando está: la clase la pone la plantilla, así que una página sin banner no
   arrastra ocho centímetros de hueco en blanco al final. */
body.has-consent .footer { padding-bottom: 9rem; }
@media (max-width: 640px) {
  body.has-consent .footer { padding-bottom: 13rem; }
}

@media print {
  .consent { display: none; }
}

/* --- el banco de pruebas del avatar --------------------------------------- */
/*
 * Solo lo usa /admin/avatares. Vive aquí y no en la plantilla porque la sala
 * va a acabar heredando `.avatar` y sus capas: lo que se ajusta ahí son los
 * números, no la forma de montarlo.
 */
.avatar-lab {
  display: grid;
  grid-template-columns: minmax(0, 420px) minmax(0, 1fr);
  gap: 1.4rem;
  align-items: start;
}
/* La columna de mandos se queda quieta mientras se mira la cara: ajustar un
   deslizador con el avatar fuera de la pantalla no sirve de nada. */
.avatar-lab__stage { position: sticky; top: 1rem; }

.avatar-stage {
  border-radius: var(--radius);
  border: 1px solid var(--line);
  padding: 1rem;
  display: grid;
  place-items: center;
  background: var(--paper);
}
/* Los dos fondos del producto, para comprobar que la silueta se sostiene en
   los dos. Con valores literales y no con los tokens: el tema de quien está
   ajustando no tiene nada que ver con el tema en el que se verá. */
.avatar-stage[data-ground="dark"] { background: #161826; }
.avatar-stage[data-ground="light"] { background: #f3f5fe; }

.avatar {
  position: relative;
  width: min(360px, 70vw);
  aspect-ratio: 1;
  /* El transform del balanceo entra aquí en cada frame. */
  will-change: transform;
}
.avatar__layer {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  user-select: none;
  -webkit-user-drag: none;
}
/* Las capas recortadas. `opacity` la escribe el bucle; el `0` de aquí es lo
   que evita que la cara aparezca con la boca abierta durante el primer frame,
   antes de que el bucle llegue a correr. */
.avatar__layer--patch,
.avatar__layer--mouth,
.avatar__layer--eyes { opacity: 0; }

/* En la sala las máscaras vienen ya calculadas en el `style` del elemento,
   porque las decide el servidor: son las mismas constantes que lee el banco.
   En el banco las escribe el JavaScript en cada movimiento de un deslizador. */
.avatar__layer--mouth {
  -webkit-mask-image: var(--mouth-mask);
  mask-image: var(--mouth-mask);
  transform: var(--mouth-nudge);
}
.avatar__layer--eyes {
  -webkit-mask-image: var(--eye-mask);
  mask-image: var(--eye-mask);
}

/* La cara en el aula.

   Los tres topes están medidos, no elegidos: con `60vw` la cabeza empujaba
   «Entrar en la clase» hasta el borde inferior de un móvil en vertical, y en
   apaisado lo sacaba de la pantalla. El botón que empieza la clase —y el que
   la termina— tiene que verse sin desplazar; la cara puede permitirse ser más
   pequeña, el botón no puede permitirse no estar. `vh` es el tope que manda en
   apaisado y `vw` el que manda en vertical. */
.avatar--room {
  width: min(300px, 46vw, 30vh);
  margin: 0 auto .3rem;
}

/* El selector de cara y el campo de la frase. */
.avatar-lab__pick {
  display: grid;
  grid-template-columns: 5.5rem 1fr;
  align-items: center;
  gap: .7rem;
  font-size: .82rem;
  color: var(--ink-soft);
}
.avatar-lab__voice { max-width: 15rem; font-size: .8rem; }

/* Dos barras: lo que entra y lo que sale. Con una sola no se distingue «no
   llega audio» de «llega y no abre la boca», que son dos averías distintas. */
.avatar-lab__levels {
  display: grid;
  grid-template-columns: 3.6rem 1fr;
  align-items: center;
  gap: .35rem .6rem;
  margin: .8rem 0 .5rem;
}
.avatar-lab__meter {
  height: 4px;
  border-radius: 999px;
  background: var(--line-soft);
  overflow: hidden;
}
.avatar-lab__meter span {
  display: block;
  height: 100%;
  width: 0;
  background: var(--accent);
}

.avatar-lab__controls { display: grid; gap: 1rem; }
.avatar-lab__sliders { display: grid; gap: .45rem; }
.avatar-lab__slider {
  display: grid;
  grid-template-columns: 1fr 180px 3.5rem;
  align-items: center;
  gap: .7rem;
  font-size: .82rem;
  color: var(--ink-soft);
}
.avatar-lab__slider output {
  font-family: var(--mono);
  font-size: .78rem;
  text-align: right;
  color: var(--ink-faint);
}
.avatar-lab__slider input[type="range"] { width: 100%; accent-color: var(--accent); }

.avatar-lab__json {
  font-family: var(--mono);
  font-size: .74rem;
  line-height: 1.45;
  max-height: 15rem;
  overflow: auto;
  padding: .7rem .8rem;
  border-radius: var(--radius-sm);
  border: 1px solid var(--line-soft);
  background: var(--paper);
  color: var(--ink-soft);
  margin-bottom: .8rem;
}

/* El estado de los dos botones que congelan una capa para poder mirarla. */
.avatar-lab .btn.is-active { border-color: var(--accent); color: var(--accent-dark); }

@media (max-width: 900px) {
  .avatar-lab { grid-template-columns: 1fr; }
  .avatar-lab__stage { position: static; }
  .avatar-lab__slider { grid-template-columns: 1fr 120px 3rem; }
}

/* --- reservar una clase --------------------------------------------------- */
/* La pantalla de nueva clase es un curso y un botón, y esto es lo que hace que
   se lea así: la tarjeta de arriba centrada y con aire, y las cinco decisiones
   de antes plegadas debajo, en voz baja.

   El ancho está limitado porque una tarjeta con dos campos estirada a mil
   píxeles no parece una decisión sencilla, parece un formulario al que le
   faltan campos. */
.booking { max-width: 34rem; margin-left: auto; margin-right: auto; text-align: center; }
.booking .field { text-align: left; }
/* Un párrafo de cuatro líneas centrado se lee peor, y los avisos de esta
   tarjeta son párrafos. Solo el texto: los botones siguen en su fila. */
.booking .alert { text-align: left; }
.booking .actions { margin-top: 1.2rem; }
/* Lo que va a pasar al entrar, en una línea. Ni aviso ni error: es la
   descripción de la clase que se está a punto de empezar, así que va en el
   tono del resto de la tarjeta y no en el de una alerta. */
.booking__note {
  color: var(--ink-soft); font-size: .89rem;
  margin: .9rem 0 0; text-align: left;
}

/* Lo que ya no hace falta decidir, plegado.
   El resumen lleva las dos cosas a los lados —qué hay dentro y de qué va— para
   que se pueda decidir si abrirlo sin abrirlo. */
.options > summary { display: flex; justify-content: space-between; gap: 1rem; }
.options .way-out { text-align: center; }
