@charset "utf-8";
/* ============================================================
   panel.css — el área de control.

   Se carga DESPUÉS de base.css y solo en las tres pantallas de panel. Hereda
   de allí la paleta, la tipografía y los temas; aquí solo va lo que un panel
   necesita y una web de presentación no.

   UN PANEL NO SE DISEÑA COMO UNA PORTADA, y esa es la única idea de este
   archivo. La portada tiene que convencer a alguien que pasaba por ahí: aire,
   titulares grandes, una cosa por pantalla. El panel lo abre cada día quien ya
   está dentro, para buscar un dato concreto y salir. Lo que allí es virtud
   —el aire, el radio de 22 px, la sombra— aquí es un estorbo: obliga a
   desplazarse para ver lo que cabría de un vistazo.

   Así que aquí todo se aprieta: la letra baja a 14 px, el radio de 22 px pasa
   a 8, las sombras desaparecen y las separaciones las hacen filetes de 1 px.
   Cabe cuatro veces más información en la misma pantalla, que es exactamente
   lo que se le pide.

   LAS CIFRAS SIEMPRE EN TABULAR. Un panel es una columna de números que se
   comparan entre sí; con cifras proporcionales el 1 ocupa menos que el 8 y las
   unidades dejan de alinearse, así que comparar dos filas obliga a leerlas en
   vez de mirarlas.
   ============================================================ */

:root {
  --panel-lateral: 15rem;
  --panel-fila: 2.5rem;        /* alto de fila de tabla: 40 px */
  --panel-radio: 8px;

  /* La zona pulsable mínima, que NO es el alto de la fila. Son dos medidas
     distintas y confundirlas es lo que engorda los paneles: la fila mide lo
     que necesita el texto (40 px), y el botón de dentro se agranda con un
     pseudo-elemento transparente hasta los 44x44 que pide la WCAG 2.5.5 sin
     empujar nada. A 44 px de fila caben 16 filas en un portátil; a 40, 18 —y
     ésas dos filas de más son justo las que evitan un desplazamiento. */
  --panel-toque: 2.75rem;      /* 44 px */

  /* Lo que mide de verdad la cabecera pegajosa de la tabla: 11 px de letra
     más el relleno de --e-2 arriba y abajo. Se escribe una vez porque hay que
     restárselo al `scroll-margin` de las filas, y dos números que tienen que
     coincidir y viven separados acaban no coincidiendo. */
  --panel-cabecera: 2.25rem;   /* 36 px */

  /* El panel tiene su propia escala, más corta y más abajo que la de la web. */
  --p-xs: .6875rem;   /* 11 px — etiquetas de columna */
  --p-sm: .8125rem;   /* 13 px — texto secundario */
  --p-base: .875rem;  /* 14 px — el cuerpo del panel */
  --p-md: 1rem;
  --p-lg: 1.25rem;
  --p-xl: 1.75rem;    /* las cifras grandes, y no más */

  /* EL ESCALÓN QUE LE FALTA A LA ESCALA DE BASE. La de base.css va 4, 8, 12,
     16, 24, 32, 48, 72: entre --e-4 y --e-5 hay un salto de 1,5x, el mayor de
     toda la parte baja de la escala (12→16 es 1,33x). En la web no se nota
     porque allí se separa de --e-5 para arriba; en el panel sí, porque aquí se
     vive entre 12 y 24 y hay que elegir entre apretado y suelto sin nada en
     medio. InstUI —el sistema de Canvas— resuelve lo mismo con 16 y 24 pero
     mete un 12 y un 36 alrededor; aquí el hueco que hacía falta era el de 20,
     y sirve para UNA cosa concreta: separar bloques entre sí más de lo que
     cada bloque se separa por dentro. Con los dos a 16, el hueco entre dos
     bloques se lee como parte del bloque de arriba y la página parece una
     lista continua en vez de cajas. */
  --p-e-45: 1.25rem;  /* 20 px */
}

/* ============================================================
   0. Lo que base.css viste para la web y aquí estorba

   Las tres pantallas cargan base.css entera, y base.css está escrita para una
   web de presentación. Tres de sus reglas caen encima del panel por el nombre
   —no por intención— y lo desmontan. Medido en el navegador, no deducido:

   1. `section { padding-block: clamp(2.75rem, 6vw, 5.5rem) }`. Cada `.bloque` y
      cada `.peligro` son un <section>, así que TODOS llevaban entre 44 y 88 px
      de aire muerto arriba y abajo. En la portada ese aire es la mitad del
      diseño; aquí es un panel que dice ser denso y deja 77 px en blanco encima
      de cada título. Con esto puesto, la pantalla de administración medía
      2.976 px de alto sin un solo dato dentro.

   2. `.pie` es el PIE DE LA WEB —fondo, filete y `padding-block: 48px 32px`— y
      en el panel `.pie` es el subtítulo de una cifra («Máxima: 0»). Cada una de
      las cinco cifras del alumno se llevaba 48 px de relleno y una raya
      horizontal que parecía separar dos cosas que son una sola.

   3. `.barra-fila` es la FILA DE LA BARRA SUPERIOR de la web, con
      `min-height: 4.25rem`. En el panel es una barra de habilidad de 8 px:
      las ocho destrezas ocupaban 628 px en vez de 230.

   No se arregla en base.css a propósito: quien renombre allí rompe doce
   páginas. Se devuelven aquí las cuatro propiedades que sobran y nada más.
   ============================================================ */
.panel-cuerpo section { padding-block: 0; }
.metrica .pie { background: none; border-top: 0; padding: 0; }

/* `[hidden]` TIENE QUE GANAR SIEMPRE, Y AQUÍ NO GANABA.

   `hidden` no es más que `display: none` en la hoja del navegador, y cualquier
   regla de autor con `display` se lo come. En este panel se lo comían tres:

     · `.fallo { display: grid }` — y los avisos nacen `hidden`. Resultado: la
       pantalla de administración enseñaba A LA VEZ «aquí no hay sesión
       abierta», «tu cuenta no es de administrador» y «no se pudo leer el
       panel», que se contradicen entre sí; y la del profesor, los tres suyos.
     · `.metricas { display: grid }` — la fila de cifras del profesor se
       escondía con `dl.hidden` y no se escondía: quedaban tres columnas
       rotuladas «CARGANDO».
     · `.ajuste .que span { display: block }` (0,2,1) le ganaba a
       `.guardado[hidden]` (0,2,0), así que los cinco ajustes del alumno
       enseñaban «Lo has cambiado tú» y «Guardado» desde el primer cuadro.

   Una regla con `!important` y se acaba la clase entera de fallo, también la
   del próximo `display` que alguien añada. Es el mismo remedio que lleva
   cualquier reinicio moderno, y `[hidden]` no tiene otro uso que este. */
body.panel [hidden] { display: none !important; }

/* ============================================================
   1. El armazón
   ============================================================ */
body.panel {
  font-size: var(--p-base);
  background: var(--papel-2);
}

.panel-marco {
  display: grid;
  grid-template-columns: var(--panel-lateral) minmax(0, 1fr);
  min-height: 100dvh;
}

/* ---------- la columna lateral ----------
   CON COLOR, Y SIN CONVERTIRLA EN UNA PORTADA. Un panel es denso a propósito y
   no lleva degradados ni halos; pero la columna en --papel sobre un cuerpo en
   --papel-2 se distinguían por un filete de 1 px y nada más, y eso hace que la
   navegación y el contenido se lean como la misma superficie. Un 5 % de la
   marca es un tinte que no se ve como color —se ve como «esto es otra cosa»— y
   separa las dos zonas sin un solo píxel de más. Va con `color-mix` sobre
   --papel para que el tema oscuro salga solo: en claro sube un azul muy
   lavado, en oscuro lo baja. */
.panel-lateral {
  background: color-mix(in srgb, var(--marca) 5%, var(--papel));
  border-right: 1px solid var(--linea);
  padding: var(--e-4) var(--e-3);
  display: flex; flex-direction: column; gap: var(--e-5);
  position: sticky; top: 0; height: 100dvh; overflow-y: auto;
}

.panel-rotulo { display: flex; align-items: center; gap: .5rem; padding: var(--e-2) var(--e-2) 0; text-decoration: none; }
.panel-rotulo svg { width: 1.75rem; height: 1.75rem; flex: none; }
.panel-rotulo b { font-family: var(--display); font-size: 1.0625rem; font-weight: 600; letter-spacing: -.03em; }

.panel-grupo { display: grid; gap: 1px; }
.panel-grupo > h2 {
  font-family: var(--texto);
  font-size: var(--p-xs); font-weight: 700; letter-spacing: .11em; text-transform: uppercase;
  color: var(--tinta-3); padding: var(--e-2) var(--e-2) var(--e-1);
}
.panel-grupo a {
  display: flex; align-items: center; gap: .625rem;
  padding: .5rem var(--e-2); border-radius: var(--panel-radio);
  color: var(--tinta-2); text-decoration: none; font-weight: 500;
  font-size: var(--p-base);
}
.panel-grupo a svg { width: 1.0625rem; height: 1.0625rem; flex: none; color: var(--tinta-3); }
.panel-grupo a:hover { background: var(--papel-2); color: var(--tinta); }
.panel-grupo a:hover svg { color: var(--tinta-2); }
/* La sección en la que se está lleva además un filete de marca a la izquierda.
   El lavado solo se pierde: sobre una columna que ya va tintada, la diferencia
   entre «encima está el ratón» y «aquí estás» era un gris y un azul muy
   parecidos, y en una pantalla mala eran el mismo. El filete es color de
   verdad, se ve de reojo y no ocupa una línea. */
.panel-grupo a[aria-current="page"] {
  background: var(--marca-lavado); color: var(--marca-tinta); font-weight: 600;
  box-shadow: inset 3px 0 0 var(--marca);
}
.panel-grupo a[aria-current="page"] svg { color: var(--marca); }
/* El contador de pendientes va a la derecha del enlace, no en un punto de
   color: «3» dice cuántos, un punto solo dice «hay algo». */
.panel-grupo a .cuenta {
  margin-left: auto; font-size: var(--p-xs); font-weight: 700;
  font-variant-numeric: tabular-nums;
  background: var(--racha-lavado); color: var(--racha-tinta);
  padding: .1rem .4rem; border-radius: var(--r-full);
}

.panel-pie { margin-top: auto; padding: var(--e-3) var(--e-2) 0; border-top: 1px solid var(--linea-suave); }
.panel-yo { display: flex; align-items: center; gap: .625rem; }
.panel-yo .avatar {
  width: 2rem; height: 2rem; border-radius: var(--r-full); flex: none;
  background: var(--marca); color: var(--sobre-marca);
  display: grid; place-items: center; font-weight: 700; font-size: var(--p-sm);
}
.panel-yo .quien { min-width: 0; }
.panel-yo .quien b { display: block; font-size: var(--p-sm); font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.panel-yo .quien span { display: block; font-size: var(--p-xs); color: var(--tinta-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* ---------- el cuerpo ---------- */
.panel-cuerpo { padding: var(--e-5) var(--e-6) var(--e-8); min-width: 0; }
@media (max-width: 60rem) { .panel-cuerpo { padding: var(--e-4) var(--e-4) var(--e-7); } }

.panel-cabeza {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: var(--e-4); flex-wrap: wrap;
  padding-bottom: var(--e-4); margin-bottom: var(--e-5);
  border-bottom: 1px solid var(--linea);
}
.panel-cabeza h1 { font-size: var(--p-xl); letter-spacing: -.02em; }
.panel-cabeza p { color: var(--tinta-2); font-size: var(--p-sm); margin-top: .25rem; }

/* En móvil la columna lateral se convierte en una tira horizontal arriba.
   No en un menú desplegable: en un panel se salta entre secciones cada pocos
   segundos, y abrir un menú cada vez para eso cansa enseguida. */
@media (max-width: 60rem) {
  /* `1fr` es `minmax(auto, 1fr)`, y ese `auto` es el ancho mínimo del
     CONTENIDO: cualquier cosa ancha que se cuele dentro —una tabla sin
     envoltorio, una tira de columnas— infla la pista por encima de la
     ventana y se va de lado la página entera. Es el mismo fallo que ya
     documenta el bloque del final para `.panel-rejilla-ancha`, donde llegó a
     medir 513 px dentro de una caja de 288. Aquí hoy no desborda porque la
     única tabla va en un envoltorio que desplaza, pero el suelo sobra igual:
     una columna de panel nunca quiere crecer con su contenido. */
  .panel-marco { grid-template-columns: minmax(0, 1fr); }
  .panel-lateral {
    position: static; height: auto; flex-direction: row; align-items: center;
    gap: var(--e-3); overflow-x: auto; overflow-y: hidden;
    border-right: 0; border-bottom: 1px solid var(--linea);
    padding: var(--e-3) var(--e-4);
  }
  .panel-lateral .panel-grupo { display: flex; gap: var(--e-1); }
  .panel-lateral .panel-grupo > h2 { display: none; }
  .panel-grupo a { white-space: nowrap; }
  .panel-grupo a svg { display: none; }
  .panel-yo .quien { display: none; }

  /* «TALKADEMY» SE COMÍA CASI LA MITAD DE LA TIRA. Medido a 320 px de ancho
     útil (305 reales con barra de desplazamiento): el rótulo ocupaba 138 px
     de los 305 que se ven, el 45 %, y la tira entera mide 916 px en
     administración y 1.036 en el expediente. Con el rótulo entero solo
     entraba UNO de los siete enlaces de administración, y el de la sección en
     la que se está —«Administración», que empieza en el píxel 333— quedaba
     fuera de la pantalla: se abría el panel y no se veía dónde se estaba.
     Quitando la palabra quedan 28 px de emblema y los 110 que se liberan son
     dos enlaces más a la vista. No se pierde la salida a la web: el emblema
     sigue enlazando a la portada, y el raíl de tableta ya hacía esto mismo. */
  .panel-rotulo { padding: 0; }
  .panel-rotulo b { display: none; }

  /* La cuenta se queda clavada al borde derecho en vez de viajar al final de
     la tira. Con `margin-left: auto` a secas el avatar caía en el píxel 868 de
     916: no se veía nunca sin arrastrar la tira hasta el tope, que es tanto
     como no estar. `sticky` sobre el eje que desplaza lo deja siempre a la
     vista, y el fondo —el mismo tinte de la columna— es lo que impide que los
     enlaces se lean por debajo al pasar. */
  .panel-pie {
    margin-top: 0; margin-left: auto; border: 0;
    position: sticky; right: 0;
    padding: 0 0 0 var(--e-3);
    background: linear-gradient(to right,
      transparent, color-mix(in srgb, var(--marca) 5%, var(--papel)) var(--e-3));
  }
}

/* ---------- el raíl de tableta: entre 60rem y 72rem ----------
   Comprobado en los dos anchos que importan.

   A 744 px (tableta en vertical) la tira horizontal es lo correcto y se queda:
   ahí el ancho es el recurso escaso —una tabla de cinco columnas ya va justa—
   y regalarle 84 px a la navegación se nota en cada fila.

   A 1024 px (tableta en horizontal, y el portátil pequeño) no lo era. Esa
   anchura cae hoy del lado de la columna de 15rem, y 240 px de navegación
   sobre 1024 son casi la cuarta parte de la pantalla dedicada a nueve enlaces
   que ya se saben de memoria. Pero la tira horizontal tampoco servía: a 1024
   hay sitio de sobra en vertical y ninguno que perder en horizontal.

   La respuesta es la de Canvas: un raíl de 84 px con el icono y su etiqueta
   debajo. Devuelve 156 px al contenido —una columna entera de tabla— sin
   quitar ni un nombre de sección, que es lo que pasa con los raíles de solo
   iconos: se ahorran 30 px y se paga con un menú que hay que adivinar. Los
   54 px del raíl minimizado de Canvas NO están aquí a propósito: minimizar es
   un botón, un estado que recordar y una preferencia que guardar, y eso no se
   decide desde una hoja de estilos. */
@media (min-width: 60.0625rem) and (max-width: 72rem) {
  :root { --panel-lateral: 5.25rem; }   /* 84 px, la medida de Canvas */

  .panel-lateral { padding: var(--e-4) var(--e-2); gap: var(--e-3); }

  /* Solo el emblema: «Talkademy» no entra en 68 px de hueco útil, y partido en
     dos líneas se lee peor que no estar. */
  .panel-rotulo { justify-content: center; padding: 0 0 var(--e-2); }
  .panel-rotulo b { display: none; }

  /* Los títulos de grupo se van —«EN ESTA PANTALLA» a 11 px ocuparía tres
     líneas— y lo que separaba se lo queda un filete, que es lo que hacían de
     verdad. */
  .panel-grupo > h2 { display: none; }
  .panel-grupo + .panel-grupo { border-top: 1px solid var(--linea-suave); padding-top: var(--e-3); }

  .panel-grupo a {
    position: relative;
    flex-direction: column; gap: .25rem;
    padding: .4375rem .25rem; text-align: center;
    font-size: var(--p-xs); font-weight: 600; line-height: 1.2;
  }
  /* La etiqueta parte en dos líneas en vez de cortarse con puntos suspensivos:
     «Administración» recortada a «Administra…» obliga a pensar, y pensar en un
     menú es justo lo que no puede costar nada. */
  .panel-grupo a > span:not(.cuenta) { overflow-wrap: anywhere; hyphens: auto; }
  .panel-grupo a svg { display: block; width: 1.25rem; height: 1.25rem; }

  /* El contador sale del flujo: en columna se colocaría como una tercera línea
     debajo de la etiqueta y parecería otro enlace. */
  .panel-grupo a .cuenta { position: absolute; top: .1875rem; right: .1875rem; margin: 0; }

  /* Icono, etiqueta y relleno suman 55 px de alto: por encima de los 44 de la
     zona pulsable mínima sin tener que forzar nada. */
  .panel-pie { padding-inline: 0; }
  .panel-yo { justify-content: center; }
  .panel-yo .quien { display: none; }
}

/* ============================================================
   2. Bloques
   ============================================================ */
.bloque {
  background: var(--papel);
  border: 1px solid var(--linea);
  border-radius: var(--panel-radio);
  /* 20 y no 16: el hueco ENTRE bloques tiene que ser mayor que el relleno de
     dentro de un bloque, o los dos se leen igual y el ojo no sabe dónde
     termina uno. 24 ya separaba demasiado para un panel. */
  margin-bottom: var(--p-e-45);
}
/* `.peligro` va en la misma lista y no es un añadido: la zona de peligro es un
   bloque más, con su cabecera y su cuerpo, y solo se diferencia en el color del
   marco. Escrito solo para `.bloque`, su cabecera se quedaba sin relleno, sin
   filete y sin retícula, y su <h2> caía en la tipografía de titular de
   base.css: «Lo que no puedes hacer desde aquí» salía en serif a 30 px, pegado
   al borde, dentro de un panel donde ningún otro título pasa de 16. Se veía en
   las tres pantallas. La regla de impresión de más abajo ya trataba a los dos
   juntos, que es la prueba de que siempre fue un olvido. */
:is(.bloque, .peligro) > header {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--e-3); flex-wrap: wrap;
  padding: var(--e-3) var(--e-4);
  border-bottom: 1px solid var(--linea);
}
:is(.bloque, .peligro) > header h2 { font-family: var(--texto); font-size: var(--p-md); font-weight: 700; letter-spacing: -.01em; }
:is(.bloque, .peligro) > header p { font-size: var(--p-sm); color: var(--tinta-3); width: 100%; }
.bloque-cuerpo { padding: var(--e-4); }
.bloque-cuerpo.sin-aire { padding: 0; }

/* La rejilla del panel: se apila sola y no necesita una clase por reparto. */
.panel-rejilla { display: grid; gap: var(--p-e-45); grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr)); }
.panel-rejilla-ancha { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); }
@media (max-width: 68rem) { .panel-rejilla-ancha { grid-template-columns: 1fr; } }

/* ============================================================
   3. Cifras

   Una fila de cifras arriba del panel es el elemento más copiado y el peor
   usado: cuatro números enormes que nadie mira porque no dicen si están bien o
   mal. Aquí cada una lleva su comparación debajo, que es lo que convierte un
   número en una noticia.
   ============================================================ */
.metricas {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 9rem), 1fr));
  background: var(--papel); border: 1px solid var(--linea); border-radius: var(--panel-radio);
  margin-bottom: var(--p-e-45);
  overflow: hidden;
}
.metrica { padding: var(--e-4); }
/* LOS FILETES LOS DIBUJA CADA CIFRA A SU DERECHA Y DEBAJO, NO A LA IZQUIERDA.

   La rejilla es `auto-fit`: cuántas columnas hay lo decide el ancho, y ninguna
   regla sabe cuál es la primera de cada fila. Antes se ponía el filete a la
   izquierda —y en el móvil arriba— dando por hecho una fila en el escritorio
   y una columna en el móvil. A 390 px caben dos por fila: salía una raya
   encima de la segunda cifra y ninguna entre las dos, y cada pantalla que
   usaba la pieza tenía que parchearla.

   Con una línea de 1 px a la derecha y otra debajo de cada cifra, las de
   dentro separan tenga la rejilla las columnas que tenga; las del borde caen
   fuera y `overflow: hidden` las recorta contra el marco. Si la última fila
   queda incompleta, el hueco sigue cerrado: la cifra de encima le pone el
   filete de arriba y la de al lado el de su izquierda. Va en `box-shadow` y
   no en borde porque un borde ocupa sitio dentro de la celda y descuadraría
   la columna del borde; la tercera sombra tapa el píxel donde se cruzan. */
.metricas > .metrica {
  box-shadow: 1px 0 0 var(--linea), 0 1px 0 var(--linea), 1px 1px 0 var(--linea);
}
.metrica dt { font-size: var(--p-xs); font-weight: 600; letter-spacing: .09em; text-transform: uppercase; color: var(--tinta-3); }
.metrica dd {
  margin: .375rem 0 0;
  font-family: var(--display); font-size: var(--p-xl); font-weight: 600;
  letter-spacing: -.025em; line-height: 1; font-variant-numeric: tabular-nums;
  color: var(--tono, var(--tinta));
}
.metrica .pie { display: block; font-family: var(--texto); font-size: var(--p-xs); font-weight: 400; color: var(--tinta-3); margin-top: .375rem; letter-spacing: 0; }

/* ============================================================
   4. Tablas

   40 px de fila, no 44. Los 44 venían de la regla del dedo, y la regla del
   dedo no es sobre la fila: es sobre lo que se pulsa DENTRO de la fila. Un
   panel de datos que se abre en un portátil gana una fila entera por cada
   cuatro al bajar de 44 a 40, y las tablas serias van por ahí —Stripe y Linear
   rondan los 40, InstUI de Canvas deriva a 36-37 en sus listas densas—. El
   texto sigue centrado porque la altura la pone `height` con
   `vertical-align: middle`, no un relleno que haya que recalcular; y lo
   pulsable se agranda aparte, más abajo, hasta los 44x44.

   Cabecera pegada arriba para que al bajar por doscientas filas no haya que
   recordar qué columna era cuál.
   ============================================================ */
.tabla-panel { width: 100%; border-collapse: collapse; font-size: var(--p-base); }
.tabla-panel thead th {
  position: sticky; top: 0; z-index: 2;
  background: var(--papel-2);
  text-align: left; font-size: var(--p-xs); font-weight: 700;
  letter-spacing: .09em; text-transform: uppercase; color: var(--tinta-3);
  padding: var(--e-2) var(--e-4); white-space: nowrap;
  border-bottom: 1px solid var(--linea);
}
.tabla-panel td { padding: 0 var(--e-4); height: var(--panel-fila); border-bottom: 1px solid var(--linea-suave); vertical-align: middle; }
.tabla-panel tbody tr:hover { background: var(--papel-2); }
.tabla-panel tbody tr:last-child td { border-bottom: 0; }
.tabla-panel .num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.tabla-panel .principal { font-weight: 600; }
.tabla-panel .secundario { color: var(--tinta-3); font-size: var(--p-sm); }
/* Una fecha no se parte. `2025-01-14` lleva dos guiones dentro y el navegador
   los toma por puntos de corte válidos: con la columna de al lado ancha, la
   celda salía en dos líneas («2025-01-» y «14»), que rompe los 40 px de la
   fila y obliga a leer la fecha dos veces. Tabular, además, para que la
   columna de altas se compare de un vistazo como cualquier otra cifra. */
.tabla-panel td.fecha { white-space: nowrap; font-variant-numeric: tabular-nums; }

/* LA CABECERA PEGAJOSA NO SE PEGABA. `overflow-x: auto` a secas no deja el
   otro eje en `visible`: el CSS lo sube a `auto` en cuanto uno de los dos
   desplaza, así que este envoltorio era un contenedor de desplazamiento
   vertical —de cero píxeles de recorrido— y el `position: sticky` del `th` se
   anclaba a ÉL en vez de a la ventana. Medido en panel-admin con 60 filas: al
   bajar 300 px la cabecera se iba 300 px con la página. De paso, ese mismo
   contenedor recortaba por arriba y por abajo cualquier contorno de foco.

   A partir de 60rem el envoltorio deja de desplazar: la tabla del panel está
   dimensionada para caber ahí, y sin contenedor de por medio la cabecera se
   pega a la ventana —que es lo que de verdad se desplaza— y el foco no se
   recorta. Comprobado: a 1024 y a 1440 px la cabecera no se mueve ni un píxel
   al bajar 260 px de página.

   POR DEBAJO DE 60rem LA RENUNCIA ERA INNECESARIA. Aquí la cabecera se
   declaraba `static` —«prefiero que no prometa lo que no puede cumplir»— y el
   diagnóstico era exacto pero la conclusión, la corta: el `sticky` no se
   anclaba a la ventana porque el envoltorio ya era un contenedor de
   desplazamiento, sí; lo que faltaba es que ese contenedor TUVIERA recorrido
   vertical. Con una altura máxima lo tiene, y entonces el ancla es justo la
   correcta: la tabla se desplaza dentro de su caja —en los dos ejes— y su
   cabecera se queda arriba de esa caja, que es donde está mirando quien la
   recorre. Medido en administración a 320 px: la tabla mide 735 px dentro de
   una caja de 266, y con 12 filas la caja recorre 60 px en vertical; antes, al
   desplazar, la cabecera se iba con las filas.

   El tope es `min(70dvh, 30rem)` y no una altura fija: en un móvil apaisado de
   360 px de alto, 480 px de caja dejarían la tabla sin nada alrededor y no se
   vería que la página sigue. Y no lleva `overscroll-behavior: contain` a
   propósito: al llegar al final de la tabla el gesto tiene que seguir moviendo
   la página, o la caja se convierte en una trampa. */
.tabla-envoltorio-panel { overflow-x: auto; }
@media (min-width: 60rem) { .tabla-envoltorio-panel { overflow: visible; } }
@media (max-width: 59.9375rem) {
  .tabla-envoltorio-panel { max-height: min(70dvh, 30rem); overflow: auto; }
  /* El `th` hereda `position: sticky` y `top: 0` de arriba; aquí solo se le
     recuerda el fondo, porque una cabecera translúcida sobre filas que pasan
     por debajo se lee como dos filas superpuestas. */
  .tabla-panel thead th { background: var(--papel-2); }
}

/* ---------- la celda sin dato ----------
   Un hueco en blanco y un dato que no ha llegado se ven igual, y no son lo
   mismo: uno significa «esta persona no tiene teléfono» y el otro «la API aún
   no ha contestado». El esqueleto (.cargando) dice lo segundo; el guion largo
   dice lo primero. En --tinta-3 sobre --papel da 4,91:1, y baja a 4,37:1 sobre
   la fila con el ratón encima —el mismo par que ya usan los rótulos de
   columna—, así que el guion se queda al tamaño del texto y no se encoge.

   Dos formas de llegar aquí: una celda VACÍA de verdad se pinta sola, y si el
   JavaScript ya escribe el guion (panel-admin.js lo hace) basta con ponerle
   `class="sin-dato"` a ese <td> para que salga en gris en vez de en negro. */
.tabla-panel td:empty::before,
.tabla-panel td.sin-dato { color: var(--tinta-3); }
.tabla-panel td:empty::before { content: "\2014"; }

/* ---------- la columna por la que está ordenado ----------
   Sin esto, una tabla ordenada por «Última vez» se ve idéntica a una ordenada
   por «Alta», y quien llega a mitad de página no sabe qué está mirando. La
   cabecera activa sube de --tinta-3 a --tinta (16,8:1 sobre --papel-2, frente
   a los 4,37:1 del resto) y lleva su flecha; el filete de --marca debajo marca
   la columna sin teñirla entera, que a 40 px de fila sería un muro de color.

   El triángulo se dibuja con bordes y `currentColor`: un SVG incrustado
   llevaría el color escrito dentro y en tema oscuro saldría negro sobre
   negro. `[aria-sort]` va en el <th> —es el atributo que ya existe para esto—
   y el <button> de dentro es lo que se pulsa para reordenar. */
.tabla-panel thead th[aria-sort] { padding: 0; }
.tabla-panel thead th[aria-sort] > button {
  display: inline-flex; align-items: center; gap: .375rem;
  font: inherit; letter-spacing: inherit; text-transform: inherit;
  color: inherit; background: none; border: 0; cursor: pointer;
  padding: var(--e-2) var(--e-4); width: 100%;
  position: relative;
}
.tabla-panel thead th[aria-sort].num > button { justify-content: flex-end; }
/* La flecha va DETRÁS del rótulo, que es donde se lee («Última vez, de más
   reciente a menos»), y ocupa su sitio siempre aunque esté invisible: si
   apareciera solo en la columna activa, ordenar por otra movería de golpe
   todos los rótulos de la cabecera. */
.tabla-panel thead th[aria-sort] > button::after {
  content: ""; width: 0; height: 0; flex: none;
  border-inline: .25rem solid transparent;
  border-block-end: .3125rem solid currentColor;
  opacity: 0;
}
.tabla-panel thead th[aria-sort]:hover > button::after { opacity: .45; }
.tabla-panel thead th[aria-sort="ascending"],
.tabla-panel thead th[aria-sort="descending"] {
  color: var(--tinta);
  box-shadow: inset 0 -2px 0 var(--marca);   /* filete bajo la columna activa:
                                                --marca sobre --papel-2 da
                                                4,34:1 en claro y 5,75:1 en
                                                oscuro, por encima del 3:1 que
                                                pide un elemento gráfico */
}
.tabla-panel thead th[aria-sort="ascending"] > button::after,
.tabla-panel thead th[aria-sort="descending"] > button::after { opacity: 1; }
.tabla-panel thead th[aria-sort="descending"] > button::after { rotate: 180deg; }

/* ---------- la fila seleccionada, y muchas a la vez ----------
   `aria-selected` para una tabla que se recorre con el teclado y la casilla
   marcada para la selección múltiple: son dos caminos al mismo aspecto, y con
   `:has()` el segundo no necesita que nadie añada una clase desde JavaScript.
   El fondo lavado solo no basta —en una pantalla mala se pierde—, así que la
   fila lleva además un filete de --marca a la izquierda: 4,87:1 sobre --papel
   en claro y 6,67:1 en oscuro, cuando a un elemento gráfico se le piden 3:1. */
.tabla-panel tbody tr[aria-selected="true"],
.tabla-panel tbody tr:has(.selector input:checked) { background: var(--marca-lavado); }
.tabla-panel tbody tr[aria-selected="true"] > td:first-child,
.tabla-panel tbody tr:has(.selector input:checked) > td:first-child {
  box-shadow: inset 3px 0 0 var(--marca);
}
/* Con el ratón encima, una fila seleccionada tiene que seguir pareciendo
   seleccionada: sin esto el `:hover` de arriba le devolvía el gris y parecía
   que se había deseleccionado sola. */
.tabla-panel tbody tr[aria-selected="true"]:hover,
.tabla-panel tbody tr:has(.selector input:checked):hover {
  background: color-mix(in srgb, var(--tinta) 5%, var(--marca-lavado));
}

/* La columna de casillas: estrecha, y la casilla con su zona de 44x44. */
.tabla-panel :is(td, th).selector { width: 1px; padding-inline: var(--e-4) 0; }
.tabla-panel .selector input[type="checkbox"] {
  width: 1rem; height: 1rem; margin: 0; display: block;
  accent-color: var(--marca); cursor: pointer; position: relative;
}

/* ---------- la fila que acaba de cambiar ----------
   Cuando algo se guarda y la tabla se repinta, la fila que cambió es la única
   que importa y es exactamente la que no se distingue. Un destello de un
   segundo la señala y se apaga solo: ni deja un rastro que haya que limpiar ni
   obliga a buscarla. Verde porque es el color del acuse de guardado del resto
   del panel, no un cuarto idioma.
   Con `prefers-reduced-motion` no se anima: se deja el lavado puesto y quieto,
   porque quien apaga el movimiento quiere ver el cambio, no perdérselo. */
@keyframes destello-fila {
  from { background-color: var(--acierto-lavado); }
  to   { background-color: transparent; }
}
.tabla-panel tbody tr.recien { animation: destello-fila 1.1s ease-out 1; }
@media (prefers-reduced-motion: reduce) {
  .tabla-panel tbody tr.recien { animation: none; background-color: var(--acierto-lavado); }
}

/* ---------- la fila deshabilitada ----------
   Se apaga con color de texto, NO con `opacity`: bajar la opacidad de la fila
   entera arrastraría también el contraste del texto por debajo de 4,5:1 y la
   fila pasaría de «no se puede tocar» a «no se puede leer». --tinta-3 sobre
   --papel da 4,91:1, que sigue siendo texto legible y a la vez se ve apagado
   al lado de las filas vivas. */
.tabla-panel tbody tr[aria-disabled="true"] { color: var(--tinta-3); }
.tabla-panel tbody tr[aria-disabled="true"]:hover { background: none; }
.tabla-panel tbody tr[aria-disabled="true"] .principal { font-weight: 500; }
.tabla-panel tbody tr[aria-disabled="true"] :is(.estado, .etiqueta) { opacity: .7; }

/* ---------- el foco del teclado ----------
   Un panel se recorre con el tabulador y con las flechas, y hay tres cosas que
   se rompen siempre:

   1. El contorno recortado. El de base.css sale 2 px HACIA FUERA, y en una
      fila de 40 px dentro de un envoltorio que desplaza, esos 2 px se comen.
      Aquí el contorno de la fila va hacia DENTRO (offset negativo), que además
      es como se lee un cursor de rejilla, y el de los controles a ras (0).
   2. La cabecera pegajosa tapando la fila a la que se acaba de bajar. Lo
      arregla `scroll-margin-block-start`: el navegador deja ese hueco al
      desplazar hacia un elemento enfocado, así que la fila aterriza DEBAJO de
      la cabecera en vez de detrás. Al subir hace falta el mismo hueco abajo
      para la barra de selección.
   3. La fila que no se entera de que el foco está dentro de ella. `:has()`
      la enciende cuando el foco cae en un botón de una de sus celdas. */
.tabla-panel tbody tr { scroll-margin-block: var(--panel-cabecera) var(--panel-toque); }
.tabla-panel tbody tr:focus-visible {
  outline: 2px solid var(--marca); outline-offset: -2px;
  background: var(--papel-2);
}
.tabla-panel tbody tr:has(:focus-visible) { background: var(--papel-2); }
.tabla-panel :is(td, th) :is(a, button, input, select):focus-visible { outline-offset: 0; }

/* ---------- lo pulsable dentro de una fila de 40 px ----------
   La WCAG 2.5.5 pide 44x44 y la fila mide 40: se resuelve agrandando la zona
   de pulsación con un pseudo-elemento transparente centrado en el control. Con
   relleno no se puede —el relleno haría crecer la celda y con ella la fila, y
   volveríamos a los 44 de los que veníamos—. Los 2 px que el área se sale por
   arriba y por abajo se solapan con los de la fila vecina; en ese carril de
   4 px gana la fila de abajo, que es la que se pinta después, y es un precio
   muy pequeño por no tener que pulsar un blanco de 32 px. */
/* EL ENSANCHE ESTABA ESCRITO SOLO PARA LOS BOTONES DE DENTRO DE UNA FILA, Y
   EL MISMO BOTÓN FUERA DE LA TABLA SEGUÍA MIDIENDO 32. `.btn-panel` declara
   `min-height: 2rem` y `.btn-peligro` sale en 35 px de relleno más renglón:
   los dos por debajo de los 44 de la WCAG 2.5.5 en la cabecera de la página,
   en la zona de peligro y en la barra de selección, que es donde están los
   botones que más se pulsan con el pulgar. El truco no tenía por qué ser de
   la tabla: se sube a los dos selectores de base y la fila de 40 px se
   conserva igual, porque lo que crece es el objetivo, no la caja.

   `.palanca` entra aquí por lo mismo. Mide 44x24 —lo ancho ya estaba, lo alto
   faltaban 20 px— y como es un <label>, pulsar cualquier parte suya, el
   pseudo-elemento incluido, marca el interruptor de dentro. */
:is(.btn-panel, .btn-peligro, .palanca),
.tabla-panel .selector input[type="checkbox"] { position: relative; }
.btn-panel::after,
.btn-peligro::after,
.palanca::after,
.tabla-panel .selector input[type="checkbox"]::after {
  content: ""; position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: 100%; height: 100%;
  min-width: var(--panel-toque); min-height: var(--panel-toque);
}
/* El botón de ordenar de la cabecera NO lleva este ensanche, y es a propósito:
   ya ocupa el ancho entero de su columna por 34 px de alto, que es un blanco
   grande de sobra; estirarlo a 44 lo metería cinco píxeles dentro de la
   primera fila y ordenar la tabla al intentar pulsar el primer registro es
   peor fallo que el que se venía a arreglar. */
/* Las acciones de una fila van juntas y a la derecha, y la celda no se estira:
   una columna de acciones ancha empuja los datos fuera de la pantalla. */
.tabla-panel td.acciones-fila { width: 1px; white-space: nowrap; text-align: right; }
.tabla-panel td.acciones-fila > * + * { margin-left: var(--e-2); }

/* ---------- la barra de la selección múltiple ----------
   Pegada abajo y no arriba: las casillas se marcan bajando por la tabla, y el
   botón que actúa sobre lo marcado tiene que estar donde está la mano, no a
   dos pantallas de vuelta. Aparece solo cuando hay algo marcado —de ahí el
   `[hidden]`, que hace falta explícito porque `display: flex` le ganaría—. */
.barra-seleccion {
  position: sticky; bottom: var(--e-4); z-index: 3;
  display: flex; align-items: center; gap: var(--e-3) var(--p-e-45); flex-wrap: wrap;
  margin-top: var(--p-e-45);
  padding: var(--e-3) var(--p-e-45);
  background: var(--papel);
  border: 1px solid var(--linea); border-radius: var(--panel-radio);
  box-shadow: var(--sombra-alta);
  font-size: var(--p-sm);
}
.barra-seleccion[hidden] { display: none; }
.barra-seleccion .cuantos { font-weight: 700; font-variant-numeric: tabular-nums; }
.barra-seleccion .lote { display: flex; flex-wrap: wrap; gap: var(--e-2); margin-left: auto; }

/* El estado vacío no es un hueco: dice qué falta y qué hacer. */
.vacio { padding: var(--e-7) var(--e-5); text-align: center; }
.vacio h3 { font-family: var(--texto); font-size: var(--p-md); font-weight: 700; }
.vacio p { color: var(--tinta-2); font-size: var(--p-sm); margin-top: var(--e-2); max-width: 38ch; margin-inline: auto; }
.vacio .btn { margin-top: var(--e-4); }

/* ============================================================
   5. Estado

   Píldora con punto, no solo color: quien no distingue rojo de verde tiene que
   poder leer «Bloqueado» igual que lo lee todo el mundo.
   ============================================================ */
.estado {
  display: inline-flex; align-items: center; gap: .375rem;
  font-size: var(--p-xs); font-weight: 700; letter-spacing: .02em;
  padding: .1875rem .5rem; border-radius: var(--r-full);
  background: var(--papel-2); color: var(--tinta-2);
  white-space: nowrap;
}
.estado::before { content: ""; width: .4375rem; height: .4375rem; border-radius: 50%; background: currentColor; flex: none; }
/* El contorno del color de la propia píldora, con `box-shadow` para que no
   cambie de tamaño. Los lavados son tan claros que sobre --papel la píldora no
   tenía silueta: se leía como una palabra de color suelta, no como una
   etiqueta, y dos seguidas se juntaban en una sola mancha. Sale de
   `currentColor`, así que cada variante se dibuja su propio borde y ninguna
   necesita una regla. */
.estado { box-shadow: inset 0 0 0 1px color-mix(in srgb, currentColor 22%, transparent); }
.estado.bien   { background: var(--acierto-lavado); color: var(--acierto-tinta); }
.estado.ojo    { background: var(--xp-lavado); color: var(--xp-tinta); }
.estado.mal    { background: var(--racha-lavado); color: var(--racha-tinta); }
/* Se retiró `.estado.info` (fondo --marca-lavado, texto --marca-tinta): el
   JavaScript solo emite cuatro variantes —bien, ojo, mal y quieto— y ni un
   `estado info` sale de los paneles ni del marcado construido. */
.estado.quieto { background: var(--papel-2); color: var(--tinta-3); }

/* Etiqueta sin punto, para clasificar en vez de para avisar. */
.etiqueta {
  display: inline-flex; align-items: center;
  font-size: var(--p-xs); font-weight: 600;
  padding: .125rem .4375rem; border-radius: 4px;
  background: var(--papel-2); color: var(--tinta-2);
  border: 1px solid var(--linea);
  white-space: nowrap;
}

/* ============================================================
   6. Ajustes

   Un interruptor se guarda solo y se confirma en el sitio; una casilla va con
   un botón de guardar. Mezclar los dos en la misma pantalla es lo que hace que
   nadie sepa si se guardó.
   ============================================================ */
.ajuste {
  display: grid;
  /* 11rem DE SUELO PARA EL RÓTULO, y no `minmax(0, 1fr)`. La segunda pista es
     `auto`, o sea que se mide por su contenido y cobra ANTES que la parte
     flexible: cuando el valor es texto largo se lo queda todo y al rótulo no
     le queda nada. Medido en la ficha del profesor a 545 px, la fila «Titular»
     —«Conversación sin miedo para quien ya entiende pero no arranca»— dejaba
     la columna del rótulo en 40 px; a 320 y a 390, en CERO. El rótulo no se
     recortaba: desaparecía, y con él la única palabra que dice qué es ese
     valor. Con el suelo puesto, quien parte en varias líneas es el valor, que
     es lo correcto: «Titular» ocupa lo que ocupa y la frase se acomoda. */
  grid-template-columns: minmax(11rem, 1fr) auto;
  gap: var(--e-3) var(--e-4); align-items: center;
  padding: var(--e-3) var(--e-4);
  border-bottom: 1px solid var(--linea-suave);
}
.ajuste:last-child { border-bottom: 0; }
.ajuste .que { min-width: 0; }
.ajuste .que b { display: block; font-weight: 600; }
.ajuste .que span { display: block; font-size: var(--p-sm); color: var(--tinta-3); margin-top: .125rem; }
/* La marca de «esto ya no es el valor de fábrica». Sin ella, mirando el panel
   no se distingue lo que alguien decidió de lo que vino puesto. */
.ajuste .tocado { font-size: var(--p-xs); color: var(--marca-tinta); font-weight: 600; margin-top: .25rem; display: inline-block; }

.palanca { position: relative; display: inline-block; width: 2.75rem; height: 1.5rem; flex: none; cursor: pointer; }
.palanca input { position: absolute; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; }
.palanca .pista {
  position: absolute; inset: 0; border-radius: var(--r-full);
  /* 1,27:1 con --linea: el interruptor apagado no se distinguia de su fondo,
     y el encendido en jade vivo daba 2,54. El estado de un control tiene que
     verse (1.4.11), asi que las dos posiciones usan tinta. */
  background: var(--tinta-3); transition: background .15s ease;
  pointer-events: none;
}
.palanca .pista::after {
  content: ""; position: absolute; top: .1875rem; left: .1875rem;
  width: 1.125rem; height: 1.125rem; border-radius: 50%;
  background: var(--papel); transition: transform .15s ease;
  /* La sombra del botón salía de un negro escrito a mano, y en tema oscuro un
     negro sobre una pista oscura no es una sombra: es nada. Tirando de
     --tinta, el mismo 25 % es negro en claro y casi blanco en oscuro, que es
     donde de verdad despega el botón del fondo. */
  box-shadow: 0 1px 2px color-mix(in srgb, var(--tinta) 25%, transparent);
}
.palanca input:checked ~ .pista { background: var(--acierto-tinta); }
.palanca input:checked ~ .pista::after { transform: translateX(1.25rem); }
.palanca input:focus-visible ~ .pista { outline: 3px solid var(--marca); outline-offset: 2px; }

/* LOS TRES ESTADOS QUE LE FALTABAN.

   El JavaScript ya vive los tres —`control.disabled = true` mientras viaja la
   petición, el `.catch` que devuelve el control a su sitio, y el `disabled`
   permanente de un ajuste que no se puede tocar—, pero el CSS solo tenía un
   `opacity: .5` para todos, así que «apagado a propósito», «ahora mismo está
   guardando» y «no se pudo guardar» se veían exactamente igual. Y el único que
   se explicaba era el error, con un texto al lado que quien está mirando el
   interruptor no lee.

   Van por `data-estado` en el <label class="palanca"> y no por clase para no
   chocar con `.fallo`, que en esta misma hoja ya es un bloque de aviso con su
   `display: grid`. Quien conecte esto pone y quita
   `palanca.dataset.estado = 'guardando' | 'error'`. */

/* 1. Deshabilitada. Sin `opacity`, que apaga por igual la pista y el botón y
      deja el conjunto en un gris sucio donde no se distingue si está encendida
      o apagada. Aquí se apaga con tokens: pista de --linea-suave, botón de
      --papel-2 y un borde que mantiene el contorno visible. Encendida y
      deshabilitada conserva el verde, rebajado, porque «está puesto pero no
      lo puedes cambiar» tiene que seguir diciendo que está puesto. */
.palanca:has(input:disabled) { cursor: not-allowed; }
.palanca input:disabled { cursor: not-allowed; }
.palanca input:disabled ~ .pista {
  background: var(--linea-suave);
  box-shadow: inset 0 0 0 1px var(--linea);
}
.palanca input:disabled ~ .pista::after { background: var(--papel-2); box-shadow: none; }
.palanca input:checked:disabled ~ .pista {
  background: color-mix(in srgb, var(--acierto) 45%, var(--papel));
}

/* 2. Guardando. El control ya enseña el valor nuevo —es lo que decidió
      panel-admin.js y está bien: devolverlo atrás mientras viaja la petición
      se siente roto—, así que lo que falta no es el valor sino el «espera».
      Un latido lento de la pista, `cursor: progress` y nada más: una rueda
      girando de 24 px al lado de un interruptor de 44 es más ruido que aviso.
      Sin movimiento, el latido se queda a media luz y quieto, que dice lo
      mismo sin parpadear. */
@keyframes latido-palanca { 50% { opacity: .55; } }
.palanca[data-estado="guardando"] { cursor: progress; }
.palanca[data-estado="guardando"] .pista { animation: latido-palanca 1.1s ease-in-out infinite; }

/* Mientras guarda, el interruptor recupera SUS COLORES aunque esté
   `disabled`. Es el detalle que costó verlo: como el JavaScript bloquea el
   control durante la petición, sin esto se pintaba con el gris de «apagado a
   propósito» y los dos estados eran el mismo dibujo. Y lo que enseña es el
   valor NUEVO, en verde y a plena luz, porque es el que se está guardando; lo
   que dice que aún no ha terminado es el latido, no un color apagado. */
.palanca[data-estado="guardando"] input:disabled ~ .pista { background: var(--linea); box-shadow: none; }
.palanca[data-estado="guardando"] input:checked:disabled ~ .pista { background: var(--acierto); }
.palanca[data-estado="guardando"] input:disabled ~ .pista::after { background: var(--papel); }

@media (prefers-reduced-motion: reduce) {
  /* Sin latido hay que decirlo con algo quieto, y tiene que distinguirse del
     aro del error: éste es discontinuo y en gris de texto secundario —«en
     marcha»—, y el del fallo es continuo y rojo —«ha terminado mal»—. */
  .palanca[data-estado="guardando"] .pista {
    animation: none;
    outline: 2px dashed var(--tinta-3); outline-offset: 2px;
  }
}

/* 3. No se pudo guardar. El interruptor ya ha vuelto solo a donde estaba —eso
      lo hace el JavaScript—, y el aro rojo es lo que explica por qué se ha
      movido dos veces delante de los ojos de quien lo tocó. Va por fuera, sin
      tocar el color de la pista, para que se siga viendo si quedó encendido o
      apagado. --racha-tinta sobre --papel da 5,08:1 en claro y 7,02:1 en
      oscuro, cuando a un elemento gráfico se le piden 3:1. */
.palanca[data-estado="error"] .pista {
  outline: 2px solid var(--racha-tinta); outline-offset: 2px;
}

.ajuste input[type="number"], .ajuste input[type="text"], .ajuste select {
  font: inherit; font-size: var(--p-base);
  padding: .375rem .5rem; width: 9rem;
  /* 12 px de relleno más 22 de renglón daban 34: diez por debajo de los 44 de
     la WCAG 2.5.5. Y a estos tres no les vale el truco del pseudo-elemento que
     usan los botones —un <input> y un <select> son elementos reemplazados y no
     generan `::after`—, ni les vale que su etiqueta sea pulsable: en un campo
     donde se ESCRIBE hay que acertar con el dedo en el campo, no en su rótulo.
     La fila crece de 58 a 68 px y es el único sitio del panel donde engordar
     está bien: se tocan cuatro veces al año, no cuarenta por pantalla. */
  min-height: var(--panel-toque);
  border: 1px solid var(--linea); border-radius: 6px;
  background: var(--papel); color: var(--tinta);
  font-variant-numeric: tabular-nums;
}
.ajuste input[type="text"] { width: 16rem; max-width: 100%; }

/* EL `max-width: 100%` DE ARRIBA NO SUJETA NADA, Y ÉSTE ES EL MOTIVO. La fila
   es `grid-template-columns: minmax(0, 1fr) auto`, y una pista `auto` se mide
   por lo que pide su contenido: como el campo lleva un `width` definido de
   16rem, su aportación a la pista son 256 px fijos —ni `min-width: 0` los
   baja, porque para medir la pista cuenta el ancho declarado y no el
   intrínseco—. Así que la pista mide 256 y el `100%` se calcula sobre esos
   mismos 256: se está limitando a sí mismo.

   Medido en administración a 320 px de ventana y 300 de ancho útil: la fila
   deja 233,8 px para la retícula y el campo pide 256, de modo que la página
   entera necesitaba 305 px y se desplazaba 5 de lado. Solo pasa en las dos
   filas de texto —«dominioCanonico» y «aviso»—; las de interruptor y las de
   número (9rem) caben.

   Y el mismo `auto` aplasta el rótulo en la otra dirección. La segunda pista
   se mide por su contenido, así que cuando el contenido es texto largo se lo
   queda todo: en la ficha del profesor, la fila «Titular» —«Conversación sin
   miedo para quien ya entiende pero no arranca»— dejaba la columna del rótulo
   en CERO píxeles a 320 y a 390, con el valor ocupando 218 y 288. El rótulo no
   se cortaba: desaparecía. Y con un desplegable de 9rem quedaban 74 px, donde
   «Cuánto te pides cada día. Puedes bajarla una semana mala…» sale a una
   palabra por renglón: doce líneas para una frase de dos.

   Así que por debajo de 34rem se apila TODO salvo lo que lleva interruptor. Un
   interruptor mide 44 px de ancho y deja 174 para el rótulo, que es sitio de
   sobra; ponerlo debajo gastaría una línea por ajuste y en administración hay
   catorce. Medido después: la columna del rótulo pasa de 0 y de 74 px a entre
   174 y 234, y la ayuda más larga baja de doce renglones a cuatro. */
@media (max-width: 34rem) {
  .ajuste:not(:has(.palanca)) { grid-template-columns: minmax(0, 1fr); }
  .ajuste input[type="text"] { width: 100%; }
}

/* El acuse de recibo del guardado. Aparece al lado del control que se tocó, no
   arriba del todo: a quien acaba de mover un interruptor abajo del panel, un
   mensaje en la cabecera no le llega. */
.guardado { font-size: var(--p-xs); font-weight: 600; color: var(--acierto-tinta); }
.guardado[hidden] { display: none; }

/* ============================================================
   7. Zona de peligro

   Al final, separada, y con su propio color. Lo que borra no se pone al lado
   de lo que guarda.
   ============================================================ */
.peligro {
  border: 1px solid color-mix(in srgb, var(--racha) 45%, transparent);
  border-radius: var(--panel-radio);
  background: var(--papel);
  margin-top: var(--e-6);
}
.peligro > header { border-bottom-color: color-mix(in srgb, var(--racha) 25%, transparent); }
.peligro > header h2 { color: var(--racha-tinta); }
/* Relleno, no de contorno. Lo tenía de contorno por prudencia —que no llamara
   la atencion— y es justo al reves: un boton que borra tiene que verse a la
   primera para que nadie lo pulse creyendo que hace otra cosa. Stripe, Linear
   y Shopify lo rellenan; el de contorno se confunde con un boton secundario. */
.btn-peligro {
  background: var(--racha); color: var(--sobre-racha);
  border: 1px solid var(--racha);
  border-radius: 6px; padding: .4375rem .875rem;
  font: inherit; font-size: var(--p-sm); font-weight: 700; cursor: pointer;
}
/* El hover era `--racha-tinta` con `#fff`, y estaba mal por los dos lados. El
   `#fff` es el único color literal que quedaba en la hoja, y en tema oscuro
   `--racha-tinta` es el coral claro (#FF7A63): blanco encima da 2,56:1, la
   mitad de lo que pide un texto. Y además no se veía nada, porque de noche
   `--racha-tinta` y `--racha` son el MISMO color y el fondo no cambiaba: el
   hover consistía en romper el contraste del texto y en nada más.
   `brightness` no necesita ningún color: oscurece el relleno que ya hay, sea
   el que sea el tema, y la pareja relleno/tinta sigue siendo la que base.css
   midió. Queda en 5,17:1 en claro y 6,48:1 en oscuro. */
/* Ni literal ni ilegible. Era blanco sobre --racha-tinta, y de noche esa
   variable ES --racha: el fondo no cambiaba y el texto daba 2,56:1. Hundiendo
   el propio color se oscurece en los dos temas y la tinta sale de su ficha:
   4,96:1 de dia, 6,20:1 de noche. */
.btn-peligro:hover {
  background: color-mix(in oklab, var(--racha) 92%, #000);
  color: var(--sobre-racha);
}

/* En una pareja de botones donde uno destruye, el foco de partida va en el que
   no destruye. Quien pulsa Intro sin leer no puede borrarse la cuenta. */
.peligro .btn-panel + .btn-peligro { margin-left: var(--e-2); }

/* ============================================================
   8. Botones del panel

   Más pequeños y sin relieve: el botón con labio de la portada es para una
   llamada a la acción cada pantalla, no para catorce acciones por fila.
   ============================================================ */
.btn-panel {
  display: inline-flex; align-items: center; justify-content: center; gap: .375rem;
  font: inherit; font-size: var(--p-sm); font-weight: 600;
  padding: .4375rem .875rem; min-height: 2rem;
  border-radius: 6px; border: 1px solid var(--linea);
  background: var(--papel); color: var(--tinta); cursor: pointer;
  text-decoration: none; white-space: nowrap;
}
.btn-panel:hover { border-color: var(--tinta-3); background: var(--papel-2); }
.btn-panel.primario { background: var(--marca); border-color: var(--marca); color: var(--sobre-marca); }
.btn-panel.primario:hover { background: var(--marca-tinta); border-color: var(--marca-tinta); }
.btn-panel[disabled] { opacity: .55; cursor: not-allowed; }
.btn-panel svg { width: .9375rem; height: .9375rem; }

/* ============================================================
   9. Barras y gráficas

   Sin biblioteca: son barras, y una barra es un div con un ancho. Cargar
   200 KB de JavaScript para dibujar ocho rectángulos es el tipo de decisión
   que hace que un panel tarde dos segundos en abrir.
   ============================================================ */
.barras { display: grid; gap: var(--e-3); }
/* `min-height` y `padding-block` a cero: los hereda de la `.barra-fila` de
   base.css, que es la fila de la barra superior de la web y mide 68 px. Aquí
   una fila de habilidad son 21 px. */
.barra-fila {
  display: grid; grid-template-columns: 8.5rem minmax(0, 1fr) 3.5rem;
  gap: var(--e-3); align-items: center; font-size: var(--p-sm);
  min-height: 0; padding-block: 0;
}
.barra-fila > .nombre { color: var(--tinta-2); }
.barra-fila > .valor { text-align: right; font-variant-numeric: tabular-nums; font-weight: 600; }
.barra-pista { height: .5rem; border-radius: var(--r-full); background: var(--papel-2); overflow: hidden; }
.barra-pista > i { display: block; height: 100%; border-radius: var(--r-full); background: var(--tono, var(--marca)); }
@media (max-width: 34rem) {
  .barra-fila { grid-template-columns: minmax(0, 1fr) 3rem; }
  .barra-fila > .nombre { grid-column: 1 / -1; }
}

/* Las columnas de actividad: 60 días de XP. Se dibujan con flex y altura en
   porcentaje; el día de hoy va marcado porque es el único que se busca. */
.columnas { display: flex; align-items: flex-end; gap: 2px; height: 5rem; }
.columnas > i {
  flex: 1 1 0; min-width: 2px; border-radius: 2px 2px 0 0;
  /* .55 daba 2,29:1 en claro y 2,86:1 en oscuro sobre --papel, y esto no es
     un adorno: son los sesenta días de estudio, el dato entero de la gráfica.
     A .8 sube a 3,50:1 y 4,60:1, por encima del 3:1 que la WCAG 1.4.11 pide a
     un elemento gráfico. Lo que distingue «hoy» sigue siendo el color
     —--racha frente a --marca—, no lo apagados que estén los demás. */
  background: var(--marca); opacity: .8;
}
.columnas > i.hoy { opacity: 1; background: var(--racha); }
/* UN DÍA PARADO ES UN DATO, Y EN --linea NO SE VEÍA: 1,27:1 en claro y 1,33:1
   en oscuro sobre --papel. Y es la marca que más contraste necesita de toda la
   gráfica, porque el JavaScript la dibuja con un 2 % de altura —1,6 px en una
   tira de 80— justo para que no se confunda con un día flojo. En --tinta-3 da
   5,38:1 y 6,53:1, y sigue siendo gris al lado del azul de los días con XP. */
.columnas > i.vacio { background: var(--tinta-3); opacity: 1; }
/* A 320 px NO CABÍAN LOS SESENTA DÍAS. Medido: la caja de la gráfica queda en
   234 px y sesenta columnas de 2 px de mínimo más cincuenta y nueve huecos de
   2 suman 238, así que los últimos días se salían 4 px del bloque y pisaban su
   propio marco. Con el hueco a 1 px caben de sobra —59 px de huecos y 2,9 px
   por día— y además las columnas se ven más gruesas, que a este ancho es justo
   lo que hacía falta. */
@media (max-width: 25rem) { .columnas { gap: 1px; } }

/* La escalera de niveles: seis columnas, la del nivel actual a medio llenar. */
.escalera { display: grid; grid-template-columns: repeat(6, 1fr); gap: var(--e-2); align-items: end; height: 7rem; }
.escalon { display: flex; flex-direction: column; justify-content: flex-end; height: 100%; gap: var(--e-2); }
/* El escalón que aún no se ha subido iba a .3, y eso son entre 1,53:1 y 1,63:1
   en claro y entre 1,60:1 y 1,98:1 en oscuro sobre --papel: la mitad de la
   escalera no se veía. Y se ve o no se ve TODO: es la parte que dice cuánto
   queda hasta C2, que es la mitad de lo que se viene a mirar aquí. Se midieron
   los doce casos —seis tonos por dos temas— y .7 se quedaba a dos centésimas:
   --n-a1-tinta en claro daba 2,98:1. A .75 el peor sube a 3,26:1, por encima
   del 3:1 de 1.4.11, y el nivel alcanzado se sigue distinguiendo por lo que
   siempre lo distinguió: los 100 px de torre frente a los 4,5 del muñón. */
.escalon .torre { border-radius: 4px 4px 0 0; background: var(--tono); opacity: .75; }
.escalon.hecho .torre { opacity: 1; }
.escalon.actual .torre { opacity: 1; }
.escalon .clave { text-align: center; font-size: var(--p-xs); font-weight: 700; color: var(--tinta-3); }
/* EL RÓTULO NO PUEDE LLEVAR EL TONO CRUDO. A 11 px hace falta 4,5:1, y los
   vivos de los niveles no llegan: --n-a2 da 2,43:1 sobre --papel, --n-a1 2,54
   y --n-c2 2,80. La torre de al lado sí puede llevarlo —es un elemento
   gráfico— y además distingue por ALTURA, que es donde está el dato.

   Y no se cambia el tono por su variante `-tinta`. Cuando se escribió esto,
   `--n-a1-tinta` y `--n-a2-tinta` no existían en base.css y una `var()` a un
   token inexistente deja la propiedad en «no válido en el valor calculado»,
   así que las torres de A1 y A2 salían transparentes. Hoy las seis existen
   —base.css, líneas 52 a 57— y el marcado del expediente ya las usa para el
   --tono de cada escalón; aun así el rótulo no se pinta con ellas, porque
   sobre --papel dan entre 5,05:1 y 5,70:1 ahí, pero lo que hace falta es un
   color que funcione a 11 px en los dos temas sin depender de la ficha, y eso
   es exactamente lo que da la mezcla de abajo.

   La mezcla con --tinta hace el mismo trabajo sin inventar tokens y sale sola
   en los dos temas: en claro oscurece el color hasta pasar de 6:1, y en oscuro
   —donde --tinta es casi blanco— lo aclara. El nivel sigue teniendo su color;
   lo que cambia es cuánto. */
.escalon.actual .clave, .escalon.hecho .clave { color: color-mix(in srgb, var(--tono) 55%, var(--tinta)); }

/* ============================================================
   10. Mientras carga

   Un panel que llama a la API empieza vacío. Sin esto se ve un salto: primero
   la nada, luego todo de golpe. El esqueleto ocupa el sitio exacto que va a
   ocupar el dato.
   ============================================================ */
.cargando {
  background: linear-gradient(90deg, var(--papel-2) 25%, var(--linea-suave) 37%, var(--papel-2) 63%);
  background-size: 400% 100%;
  animation: brillo 1.4s ease infinite;
  border-radius: 4px; color: transparent !important;
  display: inline-block; min-width: 3rem;
}
@keyframes brillo { 0% { background-position: 100% 0; } 100% { background-position: -100% 0; } }
@media (prefers-reduced-motion: reduce) { .cargando { animation: none; } }

/* Cuando la llamada falla hay que decir qué pasó y qué hacer, no dejar el
   esqueleto girando para siempre. */
.fallo {
  display: grid; grid-template-columns: auto 1fr; gap: var(--e-3);
  padding: var(--e-3) var(--e-4);
  background: var(--racha-lavado); color: var(--racha-tinta);
  border-radius: var(--panel-radio); font-size: var(--p-sm);
}
.fallo svg { width: 1.125rem; height: 1.125rem; }
.fallo b { display: block; }
.fallo button { margin-top: var(--e-2); }

/* ============================================================
   11. En papel

   Un panel de administración se imprime bastante más de lo que se admite: la
   lista de alumnos de un grupo para pasar lista, el cuadre de un mes para
   llevárselo a una reunión, el volcado de ajustes antes de tocarlos. Y hasta
   ahora salía tal cual: la columna de navegación comiéndose el primer tercio
   de cada folio, botones impresos que invitan a pulsar el papel, tarjetas con
   sombra y —a quien tuviera el tema oscuro puesto— letra gris claro sobre un
   fondo que la impresora no imprime.

   NO SE REESCRIBE REGLA POR REGLA: SE REESCRIBEN LAS FICHAS. Todo el panel ya
   está pintado con los tokens de base.css, así que basta con apuntarlos a
   blanco y negro aquí dentro y las ciento y pico reglas de arriba se imprimen
   solas, sin un `!important` y sin duplicar nada.

   Y se apuntan a COLORES DE SISTEMA, no a un blanco escrito a mano. El motivo
   es que `prefers-color-scheme: dark` sigue valiendo mientras se imprime: el
   tema oscuro no se apaga al mandar a la impresora, y con él los tokens
   seguirían siendo los de noche. `color-scheme: light` obliga al navegador a
   resolver Canvas como blanco y CanvasText como negro pase lo que pase, que es
   justo el «negro sobre blanco» que se pide, y sin escribir un solo color
   literal que mañana haya que mantener a mano en dos temas.
   ============================================================ */
@media print {
  /* LOS DOS SELECTORES SON OBLIGATORIOS, y la primera versión de esto se
     imprimió en negro sobre negro por no tenerlos. Un `:root` a secas pesa
     (0,0,1) y las fichas de noche de base.css viven en `:root[data-theme="dark"]`
     y en `:root:not([data-theme="light"])`, que pesan (0,1,1): ganan ellas y el
     folio sale con la paleta oscura. `:root:not([data-theme="light"])` empata a
     peso con las dos, y como panel.css se carga después, gana la última —que es
     ésta—. El `:root` suelto de delante cubre el caso contrario, el del tema
     claro explícito. Entre los dos no queda ninguna combinación fuera. */
  :root,
  :root:not([data-theme="light"]) {
    color-scheme: light;

    --fondo: Canvas; --papel: Canvas; --papel-2: Canvas;
    --tinta: CanvasText; --tinta-2: CanvasText; --tinta-3: CanvasText;
    --linea: CanvasText; --linea-suave: CanvasText;

    /* Los seis del producto y la escalera de niveles: en pantalla son lo que
       distingue una racha de un acierto; en un folio en blanco y negro son
       seis grises que no se distinguen entre sí, y el significado ya lo lleva
       escrito al lado cada píldora. */
    --marca: CanvasText;   --marca-tinta: CanvasText;   --marca-lavado: Canvas;   --sobre-marca: Canvas;
    --racha: CanvasText;   --racha-tinta: CanvasText;   --racha-lavado: Canvas;   --sobre-racha: Canvas;
    --xp: CanvasText;      --xp-tinta: CanvasText;      --xp-lavado: Canvas;      --sobre-xp: Canvas;
    --acierto: CanvasText; --acierto-tinta: CanvasText; --acierto-lavado: Canvas; --sobre-acierto: Canvas;
    --premium: CanvasText; --premium-tinta: CanvasText; --premium-lavado: Canvas; --sobre-premium: Canvas;
    --mirar: CanvasText;   --mirar-tinta: CanvasText;   --mirar-lavado: Canvas;   --sobre-mirar: Canvas;

    --n-a1: CanvasText; --n-a1-tinta: CanvasText; --n-a1-lavado: Canvas;
    --n-a2: CanvasText; --n-a2-tinta: CanvasText; --n-a2-lavado: Canvas;
    --n-b1: CanvasText; --n-b1-tinta: CanvasText; --n-b1-lavado: Canvas;
    --n-b2: CanvasText; --n-b2-tinta: CanvasText; --n-b2-lavado: Canvas;
    --n-c1: CanvasText; --n-c1-tinta: CanvasText; --n-c1-lavado: Canvas;
    --n-c2: CanvasText; --n-c2-tinta: CanvasText; --n-c2-lavado: Canvas;

    --sombra: none; --sombra-alta: none;
  }

  /* El margen lo pone la página, no el cuerpo: así la cabecera de la tabla
     repetida cae siempre a la misma altura en todos los folios. */
  @page { margin: 1.4cm; }

  body.panel { background: Canvas; color: CanvasText; font-size: 10pt; }

  /* La rejilla de dos columnas deja de serlo: sin columna lateral no hay nada
     que repartir, y el contenido se queda con el folio entero. */
  .panel-marco { display: block; min-height: 0; }
  .panel-cuerpo { padding: 0; }

  /* Lo que no es información se va. Un enlace de navegación impreso no lleva a
     ningún sitio, un botón impreso no se pulsa, un interruptor impreso miente
     —parece que se puede mover—, y un aviso de «no se pudo cargar» con su
     botón de reintentar no tiene ningún sentido en un papel. */
  .panel-lateral,
  .barra-seleccion,
  .btn-panel, .btn-peligro,
  .palanca,
  .fallo,
  .guardado,
  .vacio .btn,
  /* El buscador sí se va, aunque los demás campos se queden: un <input> de
     ajuste impreso enseña el valor que hay puesto, que es información, y una
     caja de búsqueda vacía impresa no enseña nada. Además, lo que se imprime
     es la tabla YA filtrada, así que el buscador ni siquiera describe lo que
     hay debajo. */
  input[type="search"] { display: none !important; }

  /* Los bloques dejan de ser tarjetas y pasan a ser secciones: fuera el marco,
     la esquina redonda y la sombra, y queda el filete del título, que es lo
     único de todo eso que en papel sigue haciendo algo. */
  .bloque, .metricas, .peligro {
    border: 0; border-radius: 0; box-shadow: none;
    margin-bottom: var(--e-5);
  }
  .bloque > header, .peligro > header { padding-inline: 0; border-bottom: 1px solid CanvasText; }
  .bloque-cuerpo, .bloque-cuerpo.sin-aire { padding-inline: 0; }
  .bloque > header { break-after: avoid; }   /* un título no se queda solo al
                                                final del folio */

  /* LA CABECERA SE REPITE EN CADA FOLIO. Es lo que separa una lista impresa
     usable de tres páginas de columnas sin nombre: `table-header-group` hace
     que el navegador vuelva a dibujar el <thead> arriba de cada folio. Y
     `position: sticky` hay que quitarlo, porque en paginado no significa nada
     y algunos motores lo resuelven dejando la cabecera solo en el primero. */
  .tabla-envoltorio-panel { overflow: visible; }
  .tabla-panel thead { display: table-header-group; }
  .tabla-panel thead th { position: static; background: Canvas; border-bottom: 1px solid CanvasText; }
  .tabla-panel tr { break-inside: avoid; }
  /* Altura automática y relleno vertical: en papel no hay dedos que acertar, y
     una fila de 40 px fijos desperdicia folio cuando un nombre parte en dos
     líneas. */
  .tabla-panel td { height: auto; padding-block: .1cm; }
  .tabla-panel tbody tr:hover,
  .tabla-panel tbody tr[aria-selected="true"],
  .tabla-panel tbody tr.recien { background: none; }
  .tabla-panel tbody tr[aria-selected="true"] > td:first-child { box-shadow: none; }

  .metrica { break-inside: avoid; border-left: 1px solid CanvasText; }
  .metricas > .metrica { box-shadow: none; }

  /* La píldora de estado pierde el fondo de color —que la impresora no manda al
     papel de todos modos— y se queda con su borde y su palabra dentro, que era
     lo que de verdad decía el estado. */
  .estado, .etiqueta { background: none; border: 1px solid CanvasText; box-shadow: none; }

  /* Las barras y las columnas SÍ se imprimen: son el dato, no el adorno, y sin
     `print-color-adjust: exact` el navegador las deja en blanco y el folio sale
     con una rejilla de huecos donde estaba la gráfica. */
  .barra-pista > i, .columnas > i, .escalon .torre {
    print-color-adjust: exact; -webkit-print-color-adjust: exact;
  }
  .barra-pista { border: 1px solid CanvasText; }

  /* El esqueleto no se imprime con su texto de mentira dentro: si el dato no
     llegó, el folio tiene que enseñar un hueco, no un nombre inventado. */
  .cargando { animation: none; background: none; visibility: hidden; }

  /* Un subrayado azul en papel es una mancha; el enlace no lleva a ningún sitio
     desde un folio, así que se imprime como lo que queda de él: su texto. */
  .panel-cuerpo a { text-decoration: none; color: CanvasText; }
}

/* ============================================================
   El panel del alumno desbordaba 209 px a 320 px

   `grid-template-columns: 1fr` es en realidad `minmax(auto, 1fr)`, y ese
   `auto` es el tamaño minimo del CONTENIDO: una tabla o una tira de columnas
   ancha infla la pista por encima de su contenedor. Aqui llegaba a 513 px
   dentro de una caja de 288, y la pagina entera se iba de lado.

   `minmax(0, 1fr)` le quita ese suelo y la pista obedece al contenedor. Es el
   mismo remedio que ya lleva `.panel-rejilla`, que por eso nunca desbordo.
   ============================================================ */
@media (max-width: 68rem) {
  .panel-rejilla-ancha { grid-template-columns: minmax(0, 1fr); }
}
