/* =============================================================================
 *  main.css — Sistema de Asistencias "Visum"
 *  Rediseño visual v2. Reemplaza por completo la hoja anterior.
 *
 *  Qué se mantuvo y qué cambió
 *  ---------------------------
 *  Se conservan TODOS los nombres de clase de la hoja anterior, incluidos los
 *  del navbar horizontal. Eso hace que la hoja sea un reemplazo directo: una
 *  pantalla que no se migre sigue renderizando, sólo con el aspecto nuevo.
 *
 *  Lo que cambió de fondo:
 *
 *  1. Una escala de medidas única. Antes cada pantalla resolvía su propio alto
 *     de control y su propio padding, así que dos cajas que debían medir igual
 *     medían distinto. Ahora hay --h-ctl (alto de TODO control: input, select,
 *     botón, botón de icono), --card-pad, --gap y --r*. Nada declara un alto o
 *     un radio suelto.
 *
 *  2. Navegación lateral. El navbar horizontal se quedaba sin ancho con doce
 *     enlaces y escondía la mitad detrás de desplegables. La barra lateral los
 *     muestra todos, agrupados, y colapsa a iconos o a cajón según el ancho.
 *     Las clases .navbar* siguen definidas para las pantallas sin migrar.
 *
 *  3. Un solo alto de fila. Hubo un conmutador de densidad con tres niveles;
 *     se sacó. Elegir entre tres altos de fila no es una decisión que el
 *     listado le tenga que pedir a nadie, y la que se usaba era siempre la
 *     misma. Quedaron los valores del nivel amplio, que es el que se lee.
 *
 *  4. Tema claro y oscuro recalibrados. Los pares texto/fondo se volvieron a
 *     medir; ninguno baja de 4.5:1, y los tintes de estado tienen su propio
 *     color de texto por tema, que es donde la hoja anterior perdía contraste.
 *
 *  El tema lo decide data-tema en <html>, puesto por el script inline del
 *  layout antes del primer paint.
 * ========================================================================== */

/* ============================================================================
 *  TIPOGRAFÍA
 *
 *  IBM Plex Sans para texto y IBM Plex Mono para todo lo que es dato de
 *  identidad o de reloj: DNI, legajo, horarios, CUIT, códigos de materia. Un
 *  DNI en fuente proporcional se lee peor y se compara peor entre filas.
 *
 *  Self-hosted: los .woff2 van en src/main/resources/static/fonts/. Si todavía
 *  no están, la pila de respaldo entra sola y la app se ve correcta con las
 *  fuentes del sistema — no hay que apurar la descarga para poder desplegar.
 * ========================================================================== */
@font-face {
    font-family: 'IBM Plex Sans';
    src: url('/fonts/IBMPlexSans-Regular.woff2') format('woff2');
    font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
    font-family: 'IBM Plex Sans';
    src: url('/fonts/IBMPlexSans-Medium.woff2') format('woff2');
    font-weight: 500; font-style: normal; font-display: swap;
}
@font-face {
    font-family: 'IBM Plex Sans';
    src: url('/fonts/IBMPlexSans-SemiBold.woff2') format('woff2');
    font-weight: 600; font-style: normal; font-display: swap;
}
@font-face {
    font-family: 'IBM Plex Mono';
    src: url('/fonts/IBMPlexMono-Regular.woff2') format('woff2');
    font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
    font-family: 'IBM Plex Mono';
    src: url('/fonts/IBMPlexMono-Medium.woff2') format('woff2');
    font-weight: 500; font-style: normal; font-display: swap;
}

/* ============================================================================
 *  TOKENS — TEMA OSCURO (por defecto)
 * ========================================================================== */
:root {
    --font-sans: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI',
                 'Helvetica Neue', sans-serif;
    --font-mono: 'IBM Plex Mono', 'Cascadia Code', 'Fira Code', monospace;

    /* Superficies, de la página hacia arriba. Cuatro escalones y no dos: con
       sólo bg y bg-elev, una tarjeta dentro de una tarjeta quedaba indistinta. */
    --bg:            #0f1216;
    --surface:       #161a20;
    --surface-2:     #1d232b;
    --surface-3:     #252d37;
    --field:         #11151a;   /* fondo de input: más hundido que la tarjeta */

    --border:        #282f39;
    --border-strong: #3a444f;

    --text:          #e6ecf3;
    --text-muted:    #98a2b0;
    --text-dim:      #6c7683;   /* etiquetas, migas, metadatos */

    /* Dos azules y no uno: --primary es para TEXTO e iconos sobre fondo oscuro;
       --primary-solid es fondo de botón, con blanco encima. El azul de texto
       usado como fondo daba 2.7:1 contra el blanco. */
    --primary:        #5fa8ff;
    --primary-solid:  #2563eb;
    --primary-solid-h:#1d4ed8;
    --primary-tint:   rgba(95, 168, 255, .13);
    --primary-ring:   rgba(95, 168, 255, .24);

    /* Marca. Fondo del logo y del panel de la pantalla de acceso. */
    --marca:    #1f5fa8;
    --marca-2:  #12406f;

    --success: #3fb27f;
    --warning: #e0a83a;
    --danger:  #f0685c;
    --accent:  #e87722;

    /* Estado sobre fondo teñido. El texto NUNCA es el color pleno: sobre su
       propio tinte se empasta. Cada tema define su par tinte/texto. */
    --t-success: rgba(63, 178, 127, .16);
    --t-warning: rgba(224, 168, 58, .16);
    --t-danger:  rgba(240, 104, 92, .15);
    --t-info:    rgba(95, 168, 255, .14);
    --t-neutral: rgba(152, 162, 176, .14);
    --t-accent:  rgba(232, 119, 34, .16);

    --on-success: #6ddfaa;
    --on-warning: #f2c775;
    --on-danger:  #ff9c92;
    --on-info:    #8fc3ff;
    --on-neutral: #adb6c2;
    --on-accent:  #ffb37a;

    --borde-success: rgba(63, 178, 127, .38);
    --borde-danger:  rgba(240, 104, 92, .38);

    /* Un color por día para la grilla. Llevan texto blanco encima, así que se
       calibran como FONDO, no como texto. */
    --dia-lun: #3b82f6;
    --dia-mar: #0f9d76;
    --dia-mie: #d97706;
    --dia-jue: #8b5cf6;
    --dia-vie: #db2777;
    --dia-sab: #64748b;
    --dia-dom: #4d4d6b;

    /* Sombras por tema: en oscuro son un velo negro suave, en claro tienen que
       ser más visibles para que una tarjeta se separe del fondo. */
    --shadow-sm: 0 1px 2px rgba(0, 0, 0, .32);
    --shadow-md: 0 6px 18px -4px rgba(0, 0, 0, .45);
    --shadow-lg: 0 24px 56px -12px rgba(0, 0, 0, .62);
    --scrim:     rgba(6, 9, 13, .66);

    /* ---- Medidas ----
       Un solo alto para todo control interactivo. Es lo que hace que un input,
       un select, un botón y un botón de icono puestos en la misma fila queden
       alineados sin corregir nada a mano. */
    --h-ctl:   38px;
    --h-ctl-sm:28px;

    --r-sm: 6px;
    --r:    9px;
    --r-lg: 13px;
    --r-xl: 18px;

    --space-xs: 4px;
    --space-sm: 8px;
    --space:    16px;
    --space-lg: 24px;
    --space-xl: 32px;

    --lateral-w:  266px;   /* barra lateral abierta */
    --lateral-wc: 70px;    /* colapsada a iconos */
    --barra-h:    58px;    /* barra superior */
    --content-max:1320px;
    --form-max:   780px;

    /* ---- Alto de fila y aire de las tarjetas ----
       Van juntos a propósito: una tabla apretada dentro de una tarjeta con
       mucho aire se ve desbalanceada. Son los valores del nivel "amplio" del
       conmutador de densidad que hubo hasta acá; el conmutador se sacó y estos
       quedaron como los únicos. */
    --row-h:    58px;
    --cell-y:   16px;
    --card-pad: 24px;
    --gap:      20px;

    /* ---- Movimiento ----
       Tres escalones. El easing es siempre el mismo: salida rápida y frenada
       larga, que se lee como "el sistema ya respondió" sin pedir atención. */
    --t-fast: 120ms;
    --t-base: 200ms;
    --t-slow: 300ms;
    --ease:   cubic-bezier(.22, .68, .28, 1);

    /* El cambio de tema no es un escalón más. No responde a un clic: cambia la
       luz de toda la pantalla, y de oscuro a claro, de golpe, encandila. Por
       eso dura más y no usa --ease (ver "Cambio de tema"). Va en ms porque
       tema.js la lee para saber cuándo terminó. */
    --t-tema: 500ms;

    /* Columnas que cambian con el ancho. Se resuelven con media queries sobre
       la variable y no repitiendo el layout: así una sola regla define la
       estructura y la media query sólo dice cuántas columnas entran. */
    --shell-cols: var(--lateral-w) minmax(0, 1fr);
    --auth-cols:  1.02fr 1fr;
    --panel-cols: repeat(auto-fit, minmax(320px, 1fr));
    --form-cols:  1fr 1fr;
}

/* ============================================================================
 *  TOKENS — TEMA CLARO
 *  No son los oscuros invertidos: en fondo claro el mismo azul y el mismo verde
 *  pierden contraste contra el blanco, así que cada uno se bajó hasta que su
 *  par texto/fondo pasara 4.5:1.
 * ========================================================================== */
:root[data-tema="claro"] {
    --bg:            #f5f6f8;
    --surface:       #ffffff;
    --surface-2:     #f2f4f7;
    --surface-3:     #e8ecf1;
    --field:         #fbfcfd;

    --border:        #dde2e9;
    --border-strong: #c3cad4;

    --text:          #151a20;
    --text-muted:    #5b6675;
    --text-dim:      #7d8794;

    --primary:        #1257b8;
    --primary-solid:  #1a5fd0;
    --primary-solid-h:#12489f;
    --primary-tint:   rgba(18, 87, 184, .09);
    --primary-ring:   rgba(18, 87, 184, .20);

    --marca:   #14497f;
    --marca-2: #0d3159;

    --success: #12744a;
    --warning: #8a5a08;
    --danger:  #b3261e;
    --accent:  #b85c10;

    --t-success: rgba(18, 116, 74, .12);
    --t-warning: rgba(138, 90, 8, .13);
    --t-danger:  rgba(179, 38, 30, .11);
    --t-info:    rgba(18, 87, 184, .10);
    --t-neutral: rgba(91, 102, 117, .12);
    --t-accent:  rgba(184, 92, 16, .13);

    /* Sobre fondo claro el tinte es casi blanco, así que el texto va oscuro.
       El acento no puede reusar --accent: sobre su propio tinte da 3.7:1. */
    --on-success: #0f6440;
    --on-warning: #79500a;
    --on-danger:  #a3221b;
    --on-info:    #12519f;
    --on-neutral: #525d6b;
    --on-accent:  #8f4409;

    --borde-success: rgba(18, 116, 74, .32);
    --borde-danger:  rgba(179, 38, 30, .30);

    --dia-lun: #1a5fd0;
    --dia-mar: #0b7355;
    --dia-mie: #a85c07;
    --dia-jue: #6d34c4;
    --dia-vie: #b31d5f;
    --dia-sab: #4d5866;
    --dia-dom: #3d3d55;

    --shadow-sm: 0 1px 2px rgba(16, 24, 40, .06);
    --shadow-md: 0 8px 20px -6px rgba(16, 24, 40, .14);
    --shadow-lg: 0 24px 56px -14px rgba(16, 24, 40, .22);
    --scrim:     rgba(20, 28, 38, .42);
}

/* ---- Barra lateral colapsada ---- */
:root[data-lateral="cerrado"] { --lateral-w: var(--lateral-wc); }

@media (max-width: 1000px) { :root { --shell-cols: minmax(0, 1fr); --form-cols: 1fr; } }
@media (max-width: 860px)  { :root { --auth-cols: minmax(0, 1fr); } }

/* ============================================================================
 *  MOVIMIENTO REDUCIDO (WCAG 2.3.3)
 *  No se apaga con display ni quitando reglas: se reduce a 1ms. Así las
 *  transiciones siguen EXISTIENDO —el código que espera transitionend sigue
 *  recibiéndolo— pero terminan antes de que el ojo las registre.
 * ========================================================================== */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 1ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 1ms !important;
        scroll-behavior: auto !important;
    }
}

/* ============================================================================
 *  RESET Y BASE
 * ========================================================================== */
*, *::before, *::after { box-sizing: border-box; }

/* hidden lo pone el navegador con display:none, pero esa regla viene de la hoja
   del navegador y pierde contra cualquier display que declaremos nosotros. */
[hidden] { display: none !important; }

html {
    background: var(--bg);
    /* La barra de scroll y los controles nativos (date, select, checkbox) toman
       de acá si son claros u oscuros. Sin esto, en tema claro el calendario del
       input date seguía abriéndose negro. */
    color-scheme: dark;
}
:root[data-tema="claro"] { color-scheme: light; }

body {
    margin: 0;
    padding: 0;
    background: var(--bg);
    color: var(--text);
    font-family: var(--font-sans);
    font-size: 14px;
    line-height: 1.55;
    min-height: 100vh;
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
}

a { color: var(--primary); text-decoration: none; }
a:hover { color: var(--primary-solid); text-decoration: underline; }

h1, h2, h3 { margin-top: 0; line-height: 1.25; font-weight: 600; letter-spacing: -.015em; }
h1 { font-size: 1.5rem;  margin-bottom: var(--space-sm); }
h2 { font-size: .98rem;  margin-bottom: var(--space-sm); }
h3 { font-size: .92rem;  margin-bottom: var(--space-xs); }

code, kbd, samp {
    font-family: var(--font-mono);
    font-size: .86em;
    padding: 2px 6px;
    background: var(--surface-2);
    border-radius: var(--r-sm);
}

/* Todo lo numérico con cifras de ancho fijo: una columna de horas o de minutos
   no se puede comparar de un vistazo si los dígitos bailan. */
.mono, code, .table td time { font-variant-numeric: tabular-nums; }

input, select, button, textarea { font-family: inherit; }

/* Foco visible sólo al tabular, no al hacer click (WCAG 2.4.7). */
:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; border-radius: var(--r-sm); }

::selection { background: var(--primary-ring); }

/* Barra de scroll fina y del color del borde, para las cajas que scrollean
   dentro de la página (tablas, cajón lateral, listas largas). */
.scroll-fino { scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; }
.scroll-fino::-webkit-scrollbar       { width: 9px; height: 9px; }
.scroll-fino::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 9px;
                                        border: 2px solid transparent; background-clip: content-box; }
.scroll-fino::-webkit-scrollbar-track { background: transparent; }

/* Enlace para saltar al contenido. Invisible hasta que se lo tabula. */
.skip-link {
    position: absolute; left: -9999px; top: 0; z-index: 200;
    padding: var(--space-sm) var(--space);
    background: var(--primary-solid); color: #fff;
    border-radius: 0 0 var(--r) 0;
}
.skip-link:focus { left: 0; text-decoration: none; }

.text-muted { color: var(--text-muted); }

/* La cantidad que acompaña a un título: "(5 materias)" al lado de "1° año". */
.titulo-cantidad { font-weight: normal; font-size: .9rem; }
.text-dim   { color: var(--text-dim); }

/* ============================================================================
 *  ANIMACIONES
 *  Todas cortas y todas de entrada. No hay ninguna en bucle salvo el disco de
 *  "enviando" y el escáner del pase, que informan que algo está ocurriendo.
 * ========================================================================== */
@keyframes v-in       { from { opacity: 0; transform: translateY(9px); } to { opacity: 1; transform: none; } }
@keyframes v-fade     { from { opacity: 0; } to { opacity: 1; } }
@keyframes v-pop      { from { opacity: 0; transform: scale(.96) translateY(6px); } to { opacity: 1; transform: none; } }
@keyframes v-slide-in { from { opacity: 0; transform: translateX(22px); } to { opacity: 1; transform: none; } }
@keyframes v-shimmer  { from { background-position: -460px 0; } to { background-position: 460px 0; } }
@keyframes v-spin     { to { transform: rotate(360deg); } }
@keyframes v-grow     { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@keyframes v-scan     { 0% { transform: translateY(-46%); opacity: 0; } 12% { opacity: 1; }
                        88% { opacity: 1; } 100% { transform: translateY(46%); opacity: 0; } }
@keyframes v-pulse    { 0% { box-shadow: 0 0 0 0 var(--primary-ring); }
                        70% { box-shadow: 0 0 0 12px rgba(0,0,0,0); }
                        100% { box-shadow: 0 0 0 0 rgba(0,0,0,0); } }

/* Entrada escalonada de los bloques de una pantalla. El retardo lo pone la
   posición, no el contenido: cualquier grupo de tarjetas entra en cascada sin
   que la plantilla tenga que numerarlas.

   Va sobre :nth-child y no con una clase por tarjeta porque las pantallas
   listan cantidades variables y no se puede saber de antemano cuántas hay. */
.entra > *          { animation: v-in 420ms var(--ease) both; }
.entra > *:nth-child(1) { animation-delay: 40ms; }
.entra > *:nth-child(2) { animation-delay: 110ms; }
.entra > *:nth-child(3) { animation-delay: 180ms; }
.entra > *:nth-child(4) { animation-delay: 240ms; }
.entra > *:nth-child(n+5) { animation-delay: 290ms; }

/* ============================================================================
 *  ESQUELETO DE LA APLICACIÓN
 *  Barra lateral fija + columna de contenido con barra superior pegada arriba.
 * ========================================================================== */
.app {
    display: grid;
    grid-template-columns: var(--shell-cols);
    min-height: 100vh;
}

.app__col {
    display: flex;
    flex-direction: column;
    min-width: 0;   /* sin esto una tabla ancha estira toda la grilla */
}

/* ---- Barra lateral ----
   Queda pegada al alto de la ventana y no se desplaza con la página: la marca
   arriba y el botón de colapsar abajo son puntos de referencia, y si se van con
   el scroll hay que volver al principio del listado para llegar a ellos.

   El que se desplaza es .lateral__nav, adentro. Antes desbordaba la barra
   entera, así que en una pantalla baja --o con los doce destinos a la vista--
   el pie se iba fuera de cuadro y el botón de colapsar no aparecía nunca.

   sticky y no fixed: como ítem de la grilla sigue ocupando su columna, así que
   el contenido no necesita ningún margen izquierdo que compensar. */
.lateral {
    position: sticky;
    top: 0;
    height: 100vh;
    display: flex;
    flex-direction: column;
    background: var(--surface);
    border-right: 1px solid var(--border);
    overflow: hidden;
}

.lateral__marca {
    display: flex; align-items: center; gap: 11px;
    height: var(--barra-h); padding: 0 16px;
    flex-shrink: 0;
    border-bottom: 1px solid var(--border);
    color: var(--text); text-decoration: none;
    font-weight: 600; letter-spacing: -.01em;
}
.lateral__marca:hover { text-decoration: none; color: var(--text); }

.lateral__logo {
    display: inline-flex; align-items: center; justify-content: center;
    width: 30px; height: 30px; flex-shrink: 0;
    border-radius: 9px;
    background: var(--marca); color: #fff;
    font-weight: 700; font-size: .95rem;
}

/* min-height 0 además de flex 1: sin eso un ítem flex no se encoge por debajo de
   su contenido, el nav crece hasta el alto de sus enlaces y empuja el pie afuera
   en vez de aparecer una barra de desplazamiento. */
.lateral__nav {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    padding: 12px 10px;
    display: flex; flex-direction: column; gap: 2px;
}

/* Los enlaces no se encogen. Un ítem flex achica antes que desbordar, así que en
   una ventana baja los quince destinos se aplastaban para entrar en el alto
   disponible en vez de aparecer la barra de desplazamiento: quedaban de 24px en
   lugar de 36, apretados y con el texto rozando el borde. El height de cada uno
   es una medida, no una sugerencia. */
.lateral__nav > * { flex-shrink: 0; }

/* Título de grupo. En el cajón y en la barra abierta se muestran todos los
   enlaces del grupo, sin desplegable: hay alto de sobra y esconder los enlaces
   detrás de un segundo clic sólo agrega un paso. */
.lateral__grupo {
    display: block;
    height: 18px; margin: 14px 0 4px; padding: 0 12px;
    font-size: .76rem; line-height: 18px; font-weight: 600;
    text-transform: uppercase; letter-spacing: .08em;
    color: var(--text-dim);
    text-decoration: underline; text-decoration-color: var(--border-strong);
    text-underline-offset: 4px;
    transition: color var(--t-fast) var(--ease), text-decoration-color var(--t-fast) var(--ease),
                height var(--t-slow) var(--ease), margin var(--t-slow) var(--ease),
                background-color var(--t-slow) var(--ease);
}
/* El título del grupo lleva a su pantalla intermedia, y tiene que parecerlo: sigue
   apagado en reposo --si compitiera con los destinos de abajo la lista tendría cuatro
   niveles de énfasis en vez de dos-- pero un poco más grande y con un subrayado tenue,
   que se enciende con el hover. Solo con el hover pasaba desapercibido. */
.lateral__grupo:hover { color: var(--text); text-decoration-color: currentColor; }

.lateral__link {
    display: flex; align-items: center; gap: 11px;
    height: 36px; padding: 0 12px;
    border-radius: var(--r);
    color: var(--text-muted); font-size: .88rem;
    text-decoration: none; white-space: nowrap; overflow: hidden;
    transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.lateral__link:hover { background: var(--surface-2); color: var(--text); text-decoration: none; }
.lateral__link svg   { flex-shrink: 0; }

/* La pantalla en la que uno está parado. Píldora teñida y texto en el azul de
   la marca: no lleva barra lateral de color porque, con la barra ya al borde
   izquierdo de la pantalla, un acento en el mismo borde se lee como ruido. */
.lateral__link--activo {
    background: var(--primary-tint);
    color: var(--primary);
    font-weight: 600;
}

.lateral__pie {
    padding: 12px 10px;
    border-top: 1px solid var(--border);
    flex-shrink: 0;
}

.lateral__colapsar {
    display: flex; align-items: center; gap: 11px;
    width: 100%; height: 36px; padding: 0 12px;
    border: none; border-radius: var(--r);
    background: none; color: var(--text-dim);
    font-size: .85rem; cursor: pointer;
    transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.lateral__colapsar:hover { background: var(--surface-2); color: var(--text); }
.lateral__colapsar svg   { flex-shrink: 0; }

/* ---- Estado colapsado: sólo iconos ----
   Los títulos de grupo se convierten en un separador: sin texto no dicen nada,
   pero sin nada en su lugar los tres grupos quedan pegados en una sola lista. */
/* Colapsar anima el ancho de la columna y los textos se desvanecen en vez de desaparecer
   de golpe. Por eso los iconos no se centran con justify-content: se quedan donde
   estaban, con el relleno justo para caer en el medio de la columna angosta (70px, 10px
   de margen del nav y del pie, iconos de 18px: 16px a cada lado). Centrados, arrancaban
   saltando al medio de la columna ancha y recién después viajaban a su lugar. El texto
   sigue en el HTML: el lector de pantalla lo lee aunque no se vea, cosa que con
   display:none se perdía. */
.app { transition: grid-template-columns var(--t-slow) var(--ease); }
.lateral__texto { transition: opacity var(--t-fast) var(--ease); }
.lateral__link, .lateral__colapsar, .lateral__marca {
    transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease),
                padding var(--t-slow) var(--ease);
}
:root[data-lateral="cerrado"] .lateral__texto   { opacity: 0; }
:root[data-lateral="cerrado"] .lateral__link,
:root[data-lateral="cerrado"] .lateral__colapsar { padding: 0 16px; }
/* Colapsada, el título se vuelve un separador. pointer-events none porque sigue
   siendo un enlace: sin esto queda una franja de 1px apretable entre dos iconos,
   que solo se puede acertar de casualidad y lleva a una pantalla que no se
   pidió. El acceso al grupo no se pierde: esta a un clic de expandir el menú. */
:root[data-lateral="cerrado"] .lateral__grupo {
    height: 1px; margin: 10px 12px; padding: 0;
    background: var(--border); overflow: hidden; color: transparent;
    text-decoration-color: transparent;
    pointer-events: none;
}
:root[data-lateral="cerrado"] .lateral__marca { padding: 0 20px; }

/* ---- Barra superior ---- */
/* sticky ya alcanza como referencia de la marca centrada: cualquier position
   distinta de static crea el bloque contenedor de un hijo absoluto. */
.barra-sup {
    position: sticky; top: 0; z-index: 60;
    display: flex; align-items: center; gap: 12px;
    height: var(--barra-h);
    padding: 0 clamp(16px, 2.2vw, 28px);
    background: var(--surface);
    border-bottom: 1px solid var(--border);
}
.barra-sup__espacio { flex: 1; }

/* ---- Marca de la barra superior (sólo en modo cajón) ----
   Con la barra lateral fuera de pantalla, la marca de la aplicación no se ve por
   ningún lado. Va centrada respecto de la barra completa y no repartida entre dos
   espaciadores: a la izquierda hay una hamburguesa y a la derecha el tema más el
   usuario, así que con espaciadores el centro queda visiblemente corrido.

   Sale del flujo, de modo que no le pelea el ancho a nada, y no recibe el puntero
   salvo en el enlace mismo: si tomara todo el ancho taparía los botones de al lado. */
.barra-sup__marca {
    position: absolute; left: 50%; top: 50%;
    transform: translate(-50%, -50%);
    display: none;                  /* la enciende .solo-movil en el cajón */
    align-items: center; gap: 9px;
    color: var(--text); text-decoration: none;
    font-weight: 600; font-size: .95rem; letter-spacing: -.01em;
    white-space: nowrap;
}
.barra-sup__marca:hover { color: var(--text); text-decoration: none; }
.barra-sup__logo {
    display: inline-flex; align-items: center; justify-content: center;
    width: 26px; height: 26px; flex-shrink: 0;
    border-radius: 8px;
    background: var(--marca); color: #fff;
    font-weight: 700; font-size: .85rem;
}

.barra-sup__buscar { position: relative; flex: 1; max-width: 340px; }
.barra-sup__buscar svg {
    position: absolute; left: 11px; top: 50%; transform: translateY(-50%);
    color: var(--text-dim); pointer-events: none;
}
.barra-sup__buscar input {
    width: 100%; height: 34px; padding: 0 12px 0 34px;
    background: var(--field); color: var(--text);
    border: 1px solid var(--border); border-radius: var(--r);
    font-size: .86rem;
    transition: border-color var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
.barra-sup__buscar input:focus {
    border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-ring); outline: none;
}

/* Reloj de la barra. Es el dato que más se mira en una app de asistencia: si la
   clase empieza 8:00 y el reloj del sistema no está a la vista, no hay forma de
   saber si una marca entró tarde. */
.barra-sup__reloj {
    display: inline-flex; align-items: center; gap: 7px;
    height: 28px; padding: 0 10px;
    border-radius: 999px; background: var(--t-neutral);
    font-family: var(--font-mono); font-size: .78rem; color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}
.barra-sup__reloj::before {
    content: ''; width: 6px; height: 6px; border-radius: 50%;
    background: var(--success);
}

.barra-sup__usuario {
    display: inline-flex; align-items: center; gap: 9px;
    padding: 3px 10px 3px 3px;
    border: 1px solid var(--border); border-radius: 999px;
    color: var(--text); text-decoration: none;
    transition: border-color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}
.barra-sup__usuario:hover { border-color: var(--border-strong); background: var(--surface-2);
                            color: var(--text); text-decoration: none; }

.avatar {
    display: inline-flex; align-items: center; justify-content: center;
    width: 26px; height: 26px; flex-shrink: 0;
    border-radius: 50%;
    background: var(--primary-tint); color: var(--primary);
    font-size: .72rem; font-weight: 600;
}
.avatar--lg { width: 54px; height: 54px; border-radius: 16px; font-size: 1.15rem; }
.avatar--tabla { width: 28px; height: 28px; background: var(--surface-3); color: var(--text-muted);
                 font-size: .68rem; }

/* Botón cuadrado de icono: tema, cerrar sesión, hamburguesa. Comparte el alto
   con los demás controles para que la fila de la barra quede alineada. */
.btn-icono {
    display: inline-flex; align-items: center; justify-content: center;
    width: 34px; height: 34px; flex-shrink: 0; padding: 0;
    border: 1px solid var(--border); border-radius: var(--r);
    background: none; color: var(--text-muted); cursor: pointer;
    transition: color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease),
                background var(--t-fast) var(--ease);
}
.btn-icono:hover { color: var(--text); border-color: var(--border-strong); }
.btn-icono--peligro:hover { color: var(--on-danger); border-color: var(--danger); }
.btn-icono--plano { border-color: transparent; }
.btn-icono--plano:hover { background: var(--surface-2); border-color: var(--border); }

/* Conmutador de tema: se muestra el icono del tema al que se va a cambiar, no
   el actual. El botón dice qué hace, no dónde estás. */
.tema-toggle__luna { display: none; }
:root[data-tema="claro"] .tema-toggle__luna { display: block; }
:root[data-tema="claro"] .tema-toggle__sol  { display: none; }

/* ---- Cambio de tema ----
   Un tema se funde en el otro en vez de saltar. Lo dispara tema.js; acá vive
   cuánto dura y cómo se ve.

   Camino principal: el navegador saca una foto de la pantalla, aplica el tema
   nuevo y funde una en la otra. Cambia todo a la vez —degradados e iconos
   incluidos, que una transición de color no sabe interpolar— y una tabla de
   cientos de filas cuesta lo mismo que una pantalla vacía.

   No se acorta con movimiento reducido, y no es un descuido: un fundido no
   mueve nada de lugar, y a quien le molesta el movimiento le molesta más un
   destello de pantalla completa. La regla de MOVIMIENTO REDUCIDO no alcanza a
   estos pseudoelementos, y la del camino alternativo le gana por especificidad. */
::view-transition-old(root),
::view-transition-new(root) {
    animation-duration: var(--t-tema);
    animation-timing-function: ease-in-out;
}

/* Camino alternativo, para navegadores sin esa función (Chrome 109, el último
   que corre en Windows 7, entre otros): cada elemento funde sus colores
   mientras dura el cambio, y solo entonces. El resto del tiempo el hover
   necesita sus 120ms, no medio segundo. */
:root.tema-cambiando,
:root.tema-cambiando *,
:root.tema-cambiando *::before,
:root.tema-cambiando *::after {
    transition-property: background-color, color, border-color, outline-color,
                         fill, stroke, box-shadow, text-decoration-color !important;
    transition-duration: var(--t-tema) !important;
    transition-timing-function: ease-in-out !important;
    transition-delay: 0s !important;
}

/* ---- Cajón en pantallas chicas ----
   Debajo de 1000px la barra lateral sale del flujo y entra como cajón. La
   hamburguesa y el velo aparecen sólo en ese modo. */
.lateral__velo { display: none; }
.solo-movil    { display: none !important; }

@media (max-width: 1000px) {
    .lateral {
        position: fixed; z-index: 120; top: 0; bottom: 0; left: 0;
        width: 274px; --lateral-w: 274px;
        transform: translateX(-102%);
        transition: transform var(--t-slow) var(--ease);
        box-shadow: var(--shadow-lg);
    }
    :root[data-cajon="abierto"] .lateral { transform: translateX(0); }
    /* Colapsar a iconos no tiene sentido dentro del cajón. */
    :root[data-lateral="cerrado"] .lateral__texto { opacity: 1; }
    :root[data-lateral="cerrado"] .lateral__link  { justify-content: flex-start; padding: 0 12px; }
    :root[data-lateral="cerrado"] .lateral__grupo {
        height: auto; margin: 14px 0 4px; padding: 0 12px;
        background: none; color: var(--text-dim);
        text-decoration-color: var(--border-strong);
    }
    :root[data-lateral="cerrado"] .lateral__marca { justify-content: flex-start; padding: 0 16px; }

    .lateral__velo {
        display: block; position: fixed; inset: 0; z-index: 110;
        background: var(--scrim);
        opacity: 0; pointer-events: none;
        transition: opacity var(--t-base) var(--ease);
    }
    :root[data-cajon="abierto"] .lateral__velo { opacity: 1; pointer-events: auto; }
    :root[data-cajon="abierto"] body { overflow: hidden; }

    .solo-movil       { display: inline-flex !important; }
    .solo-escritorio  { display: none !important; }
    .lateral__pie     { display: none; }
}

@media (max-width: 700px) {
    .ocultar-sm { display: none !important; }
    :root { --card-pad: 16px; }

    /* Acá .ocultar-sm se lleva el nombre de usuario y en la píldora queda sólo el
       avatar. El relleno asimétrico --3px a la izquierda, 10 a la derecha-- está
       calculado para el par avatar+texto: con el texto afuera sobra aire de un
       solo lado y el círculo se ve como un óvalo con la inicial descentrada.
       Relleno parejo lo devuelve a un círculo de 34px, el mismo alto y ancho que
       los botones de icono que tiene al lado. */
    .barra-sup__usuario { padding: 3px; }
}

/* ---- Contenido ---- */
.container {
    flex: 1;
    width: 100%;
    max-width: var(--content-max);
    margin: 0 auto;
    padding: clamp(18px, 2.4vw, 30px) clamp(16px, 2.2vw, 28px) 56px;
}

/* ============================================================================
 *  NAVBAR HORIZONTAL (compatibilidad)
 *  Se conserva para las pantallas que todavía no usen layout/base.html nuevo.
 *  Toma los tokens nuevos, así que se ve igual de actualizado.
 * ========================================================================== */
.navbar {
    display: flex; align-items: center; justify-content: space-between;
    padding: 0 clamp(16px, 2.2vw, 28px); height: var(--barra-h);
    background: var(--surface); border-bottom: 1px solid var(--border);
}
.navbar__brand a { display: flex; align-items: center; gap: var(--space-sm);
                   color: var(--text); text-decoration: none; font-weight: 600; }
.navbar__brand a:hover { text-decoration: none; color: var(--text); }
.navbar__logo {
    display: inline-flex; align-items: center; justify-content: center;
    width: 30px; height: 30px; border-radius: 9px;
    background: var(--marca); color: #fff; font-weight: 700; font-size: .95rem;
}
.navbar__title { font-size: 1rem; }
.navbar__menu  { display: flex; gap: var(--space-lg); flex: 1; margin-left: var(--space-xl); }
.navbar__link {
    display: inline-flex; align-items: center; gap: 6px;
    color: var(--text-muted); padding: var(--space-xs) 0;
    border-bottom: 2px solid transparent; white-space: nowrap;
    transition: color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
}
.navbar__link:hover { color: var(--text); border-bottom-color: var(--primary); text-decoration: none; }
.navbar__link--activo { color: var(--text); border-bottom-color: var(--primary); }
.navbar__grupo { position: relative; }
.navbar__grupo-boton {
    display: inline-flex; align-items: center; gap: 6px;
    padding: var(--space-xs) 0; background: none; border: none;
    border-bottom: 2px solid transparent; color: var(--text-muted);
    font: inherit; cursor: pointer; white-space: nowrap;
    transition: color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
}
.navbar__grupo-boton:hover { color: var(--text); }
.navbar__grupo--activo > .navbar__grupo-boton { color: var(--text); border-bottom-color: var(--primary); }
.nav-icono { flex-shrink: 0; }
.nav-flecha { flex-shrink: 0; opacity: .6; transition: transform var(--t-fast) var(--ease); }
.navbar__grupo--abierto .nav-flecha { transform: rotate(180deg); }
.navbar__submenu {
    display: none; position: absolute; top: calc(100% + var(--space-sm)); left: 0;
    min-width: 200px; padding: var(--space-xs);
    background: var(--surface-2); border: 1px solid var(--border);
    border-radius: var(--r); box-shadow: var(--shadow-md); z-index: 60;
}
.navbar__grupo--abierto .navbar__submenu { display: block; animation: v-pop 160ms var(--ease) both; }
.navbar__sublink { display: block; padding: var(--space-sm) var(--space);
                   border-radius: var(--r-sm); color: var(--text-muted); white-space: nowrap; }
.navbar__sublink:hover { background: var(--surface); color: var(--text); text-decoration: none; }
.navbar__sublink.navbar__link--activo { color: var(--primary); border-bottom: none; }
.navbar__user { display: flex; align-items: center; gap: var(--space-sm); }
.navbar__username { color: var(--text-muted); font-size: .86rem; text-decoration: none; }
.navbar__username:hover { color: var(--primary); text-decoration: underline; }
.navbar__logout-form { margin: 0; }
.navbar__toggle, .navbar__backdrop { display: none; }

/* ============================================================================
 *  MIGAS DE PAN
 *  Franja fina arriba del título. Los grupos van apagados porque no tienen
 *  pantalla propia: mostrarlos como enlace prometería un destino que no existe.
 * ========================================================================== */
.migas { margin-bottom: 14px; }
.migas__lista {
    display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-xs);
    margin: 0; padding: 0; list-style: none; font-size: .8rem;
}
.migas__item { color: var(--text-dim); }
.migas__item + .migas__item::before { content: "/"; margin-right: var(--space-xs); opacity: .45; }
.migas__item[aria-current="page"] { color: var(--text-muted); }
.migas__item--grupo { opacity: .75; }
.migas__item a { color: var(--text-muted); }

/* ============================================================================
 *  ENCABEZADO DE PÁGINA
 * ========================================================================== */
.page-header { margin-bottom: var(--space-lg); animation: v-in 380ms var(--ease) both; }
.page-header h1 { margin-bottom: 6px; }
.page-header__subtitle {
    margin: 0; color: var(--text-muted); font-size: .9rem;
    max-width: 68ch; text-wrap: pretty;
}
.page-header--with-action {
    display: flex; justify-content: space-between; align-items: flex-start;
    gap: var(--gap); flex-wrap: wrap;
}
.page-header__fecha {
    margin: 0 0 6px; font-size: .78rem; font-weight: 500;
    text-transform: uppercase; letter-spacing: .08em; color: var(--text-dim);
}

/* Encabezado con avatar: ficha del docente, mi cuenta. */
.page-header--perfil { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.page-header--perfil > div { flex: 1; min-width: 12rem; }
.page-header__meta { display: flex; align-items: center; gap: var(--space-sm); flex-wrap: wrap; }

/* ============================================================================
 *  TARJETAS
 * ========================================================================== */
.card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--r-lg);
    box-shadow: var(--shadow-sm);
    padding: var(--card-pad);
    margin-bottom: var(--gap);
}
.card:last-child { margin-bottom: 0; }
.card--no-padding { padding: 0; overflow: hidden; }

/* Ancho único de los formularios de edición y de su bloque de datos del
   sistema. Antes cada pantalla traía el suyo en un style inline —640, 720 y
   760— y el ancho terminaba dependiendo de en cuál estabas parado. */
.card--form, .datos-sistema, .form-max { max-width: var(--form-max); }

/* Mi cuenta apila tres tarjetas: los datos, la verificación del correo y el cambio
   de contraseña. Sólo la última era .card--form, así que quedaba más angosta que
   las otras dos y la columna se veía escalonada. Se limita la sección entera en vez
   de sacarle el tope a esa tarjeta: los campos de contraseña a 1300px de ancho no
   ayudan a nadie, y el ancho de lectura del formulario sirve igual para el resto.
   El .card--form de adentro queda sin efecto, pero se deja porque describe lo que
   la tarjeta es. */
.mi-cuenta { max-width: var(--form-max); }

/* Bloque de sólo lectura: se distingue por la superficie, no por opacidad. Con
   opacity el texto perdía contraste y dejaba de cumplir 4.5:1. */
.card--readonly { background: var(--surface-2); box-shadow: none; }

/* Título de sección dentro de una tarjeta, en versalitas. Se usa cuando la
   tarjeta ES un bloque del panel y no un formulario. */
.card__titulo {
    margin: 0 0 var(--space); font-size: .76rem; font-weight: 600;
    text-transform: uppercase; letter-spacing: .08em; color: var(--text-dim);
}

/* Bajada: el texto que explica un título, pegado a él. Anula el margen de abajo del h2,
   en vez de que cada pantalla corriera el párrafo con su propio style. */
.bajada { margin-top: calc(-1 * var(--space-sm)); }

/* Pila de tarjetas con el gap de la densidad. Reemplaza los margin-bottom
   sueltos que cada pantalla resolvía por su cuenta. */
.pila { display: flex; flex-direction: column; gap: var(--gap); }

/* ============================================================================
 *  BOTONES
 *  Todos comparten --h-ctl, así que un botón al lado de un input o de un select
 *  queda alineado sin corregir nada.
 * ========================================================================== */
.btn {
    display: inline-flex; align-items: center; justify-content: center; gap: 8px;
    height: var(--h-ctl); padding: 0 16px;
    border: 1px solid transparent; border-radius: var(--r);
    font-size: .88rem; font-weight: 500;
    cursor: pointer; text-decoration: none; white-space: nowrap;
    transition: background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease),
                color var(--t-fast) var(--ease), transform var(--t-fast) var(--ease),
                box-shadow var(--t-fast) var(--ease);
}
.btn:hover  { text-decoration: none; transform: translateY(-1px); }
.btn:active { transform: translateY(0); transition-duration: 60ms; }
.btn svg    { flex-shrink: 0; }

/* Un botón deshabilitado no responde: si se levantara estaría mintiendo. */
.btn:disabled, .btn[aria-disabled="true"] {
    transform: none; cursor: not-allowed; opacity: .55;
}

.btn--primary { background: var(--primary-solid); color: #fff; }
.btn--primary:hover { background: var(--primary-solid-h); color: #fff;
                      box-shadow: 0 4px 14px -2px var(--primary-ring); }

.btn--ghost { background: transparent; color: var(--text-muted); border-color: var(--border); }
.btn--ghost:hover { color: var(--text); border-color: var(--border-strong); background: var(--surface-2); }

/* Botones fantasma con intención. Rojo tenue: las acciones que quitan, cierran o
   apagan (dar de baja, revocar, cerrar sesión, apagar la cámara). Verde tenue: las que
   agregan, reactivan o habilitan, y las descargas. El tinte en reposo es lo que avisa
   antes del clic: sin él, "Cerrar sesión" o "Descargar CSV" se veían iguales a un
   "Cancelar", y el CSV parecía algo para descartar. */
.btn--ghost.btn--danger { color: var(--on-danger); border-color: var(--borde-danger);
                          background: var(--t-danger); }
.btn--ghost.btn--danger:hover { color: var(--on-danger); border-color: var(--danger);
                                background: var(--t-danger); }
.btn--ghost.btn--success { color: var(--on-success); border-color: var(--borde-success);
                           background: var(--t-success); }
.btn--ghost.btn--success:hover { color: var(--on-success); border-color: var(--success);
                                 background: var(--t-success); }

.btn--danger-solid { background: var(--danger); color: #fff; }
.btn--danger-solid:hover { background: var(--danger); color: #fff; filter: brightness(.9); }

.btn--sm   { height: var(--h-ctl-sm); padding: 0 11px; font-size: .79rem; border-radius: var(--r-sm); }
.btn--lg   { height: 42px; padding: 0 18px; font-size: .93rem; }
.btn--full { width: 100%; }

/* ---- Envío en curso ----
   El botón queda bloqueado con un disco girando mientras el formulario viaja.
   Conserva el color pleno en vez de bajar a .55: si se apagara se leería como
   que el click no entró, que es justo lo contrario de lo que pasó. */
.btn--enviando, .btn--enviando:disabled { opacity: 1; cursor: progress; }
.btn--enviando::before {
    content: ''; display: inline-block;
    width: .85em; height: .85em;
    border: 2px solid currentColor; border-top-color: transparent; border-radius: 50%;
    animation: v-spin 620ms linear infinite;
}
/* Con movimiento reducido el disco no gira —un giro infinito es lo más molesto
   para quien pidió menos movimiento— pero el botón igual se ve ocupado. */
@media (prefers-reduced-motion: reduce) {
    .btn--enviando::before { animation: none; opacity: .6; }
}

.form-inline, .inline-form { display: inline-flex; margin: 0; }

/* Confirmar una salida pendiente. Son cuatro controles en una celda: en una sola fila
   empujaban la tabla más allá de la pantalla, incluso en escritorio. */
.cierre-salida { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-sm); margin: 0; }
.cierre-salida input, .cierre-salida select { flex: 1 1 9rem; width: auto; min-width: 0; }
.cierre-salida .btn { flex: 0 0 auto; }
/* Sin el ancho mínimo de .table-wrap: con la barra lateral no hay 760px, y el botón quedaba
   cortado adentro de la tabla. Lo que sostiene el formulario es el mínimo de su celda. */
@media (min-width: 761px) {
    .bloques-pendientes .table-wrap .table { min-width: 0; }
    .bloques-pendientes .table--tarjetas td:last-child { min-width: 18rem; }
}

/* Grupo de botones al pie de un formulario. */
.form-actions {
    display: flex; gap: 10px; flex-wrap: wrap;
    margin-top: var(--space-lg); padding-top: var(--space-lg);
    border-top: 1px solid var(--border);
}

/* En una barra de filtros los botones son un campo más de la fila, no el pie del
   formulario: el borde que los separa del último campo de un formulario vertical
   queda ahí como un guion suelto arriba de "Aplicar", del ancho de los dos
   botones y sin separar nada. */
.form-actions--inline { margin-top: 0; padding-top: 0; border-top: none; }

/* ============================================================================
 *  FORMULARIOS
 * ========================================================================== */
.form-group { margin-bottom: var(--space); }
.form-group:last-child { margin-bottom: 0; }

.form-group label, .label {
    display: block; margin-bottom: 6px;
    color: var(--text-muted); font-size: .82rem; font-weight: 500;
}

.form-group input[type="text"],
.form-group input[type="password"],
.form-group input[type="email"],
.form-group input[type="number"],
.form-group input[type="time"],
.form-group input[type="date"],
.form-group input[type="datetime-local"],
.form-group input[type="tel"],
.form-group input[type="url"],
.form-group input[type="search"],
.form-group select,
.form-group textarea,
.input {
    width: 100%; height: var(--h-ctl); padding: 0 12px;
    background: var(--field); color: var(--text);
    border: 1px solid var(--border); border-radius: var(--r);
    font-size: .9rem;
    transition: border-color var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
.form-group textarea, textarea.input { height: auto; min-height: 86px; padding: 10px 12px;
                                       line-height: 1.5; resize: vertical; }

/* Anillo de foco: el borde azul solo no alcanza —de gris a azul hay muy poco
   contraste entre estados y con daltonismo no se distingue— así que se suma un
   halo del color de la marca. */
.form-group input:focus, .form-group select:focus, .form-group textarea:focus,
.input:focus, .filtro__texto:focus, .filtro__estado:focus, .modal__campo-input:focus {
    border-color: var(--primary);
    box-shadow: 0 0 0 3px var(--primary-ring);
    outline: none;
}
.form-group input:focus-visible, .form-group select:focus-visible,
.form-group textarea:focus-visible, .input:focus-visible { outline: none; }

.form-group input:disabled, .form-group select:disabled, .form-group textarea:disabled {
    background: var(--surface-2); color: var(--text-dim); cursor: not-allowed;
}

/* Los spinners del input number sobran: el dato es opcional y no se ajusta de a uno. */
.form-group input[type="number"]::-webkit-outer-spin-button,
.form-group input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.form-group input[type="number"] { -moz-appearance: textfield; appearance: textfield; }

.form-row {
    display: grid; grid-template-columns: var(--form-cols); gap: var(--gap);
}
/* Tres campos que van juntos (año, desde, hasta; nombre, desde, hasta). En la grilla de
   dos columnas el tercero caía solo en una fila nueva y la fila se leía rota. */
.form-row--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); margin-bottom: var(--space); }
/* Cada campo traía su margen y el último de la fila no: al pasar a una columna quedaban
   separados de más entre sí y pegados al campo que sigue. El espacio lo pone el gap. */
.form-row--3 > .form-group { margin-bottom: 0; }
/* El menú propio (select-menu.js) mide lo que su texto: en una columna de la grilla ocupa el
   ancho, como las fechas de al lado. */
.form-row > .form-group > .menu { display: block; }
@media (max-width: 1000px) { .form-row--3 { grid-template-columns: 1fr; } }

/* Formulario de una sola fila: los campos y los botones uno al lado del otro, alineados al
   pie para que las etiquetas no desparejen los controles. La separación es el gap, así que
   los grupos no traen margen propio. Los botones van en .form-actions--inline. Los filtros
   de asistencias y del reporte, y el tope de equipos. */
.form-en-fila { display: flex; gap: .75rem; flex-wrap: wrap; align-items: flex-end; }
.form-en-fila > .form-group { margin: 0; }

/* Un dato que se copia carácter por carácter --CUIT, clave de recuperación-- se escribe en
   monoespaciada: los dígitos quedan alineados y un guion de más se ve. */
.input-mono { font-family: var(--font-mono); }
/* Agregar otra fila: separado de la última fila y del botón que envía el formulario. */
.periodo-agregar { margin: var(--space-sm) 0 var(--space); }

.form-hint, .hint {
    display: block; margin-top: 6px;
    color: var(--text-dim); font-size: .78rem; line-height: 1.45;
}

/* Dato que ocupa el lugar de un campo pero no se edita: el rol de una cuenta,
   el estado de una institución. Toma la misma altura que un input para que la
   fila no se desalinee cuando al lado hay uno de verdad, y va con borde
   punteado para que nadie intente escribir adentro. */
.form-static, .campo-fijo {
    display: flex; align-items: center;
    height: var(--h-ctl); margin: 0; padding: 0 12px;
    background: var(--surface-2);
    border: 1px dashed var(--border); border-radius: var(--r);
    color: var(--text-muted); font-size: .9rem;
}

.form-error {
    display: block; margin-top: 6px;
    color: var(--on-danger); font-size: .8rem; font-weight: 500;
}
.form-group input.is-invalid, .form-group select.is-invalid, .form-group textarea.is-invalid {
    border-color: var(--danger);
}
.form-group input.is-invalid:focus { box-shadow: 0 0 0 3px var(--t-danger); }
/* El aviso marca el select, que el menú propio (select-menu.js) guarda oculto adentro:
   el borde rojo va al botón, que es lo que se ve. */
.menu:has(> select.is-invalid) > .menu__boton { border-color: var(--danger); }

.required { color: var(--danger); margin-left: 2px; }

.form-seccion {
    margin: 0 0 var(--space); font-size: .76rem; font-weight: 600;
    color: var(--text-dim); text-transform: uppercase; letter-spacing: .08em;
}
.separador { border: 0; border-top: 1px solid var(--border); margin: var(--space-lg) 0 var(--space); }

.checkbox-label {
    display: inline-flex; align-items: center; gap: var(--space-sm);
    cursor: pointer; color: var(--text);
}
.checkbox-label input[type="checkbox"] {
    width: 18px; height: 18px; accent-color: var(--primary-solid); cursor: pointer;
}
/* Casilla con un texto largo al lado (las del consentimiento): va a la altura de la
   primera línea y no centrada contra un párrafo de tres. */
.checkbox-label--texto { align-items: flex-start; }
.checkbox-label--texto input[type="checkbox"] { margin-top: 2px; flex-shrink: 0; }

/* Texto legal del consentimiento. Se lee entero antes de firmar, así que va con la letra
   del cuerpo y no con la de código que un <pre> trae por defecto; conserva los saltos de
   línea del texto versionado. Antes el borde salía de un token que ya no existe y caía
   en un gris fijo que en tema claro se veía negro. */
.consentimiento__texto {
    white-space: pre-wrap; max-height: 360px; overflow-y: auto; margin: 0;
    padding: var(--space); border: 1px solid var(--border); border-radius: var(--r);
    background: var(--surface-2); font-family: inherit; font-size: .9rem; line-height: 1.55;
}

/* Derechos ARCO de la ficha: cada uno con su explicación a la izquierda y su acción a la
   derecha, separados por una línea como las filas de una tabla. Sin esto se veían como
   una lista con viñetas y el botón debajo del texto. */
.arco__derechos { list-style: none; margin: var(--space) 0 0; padding: 0; }
.arco__derechos li {
    display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
    gap: var(--space-sm) var(--space); padding: var(--space-sm) 0;
    border-top: 1px solid var(--border);
}
.arco__derechos li > div { display: flex; flex-direction: column; gap: 2px; flex: 1 1 280px; min-width: 0; }

/* ---- Campo de contraseña con botón ojo ---- */
.password-input { position: relative; display: flex; align-items: stretch; }
.password-input input { flex: 1; padding-right: 42px !important; }

/* Los botones nativos que el navegador agrega encima de type=password duplican
   el nuestro y confunden. */
input[type="password"]::-ms-reveal,
input[type="password"]::-ms-clear { display: none; width: 0; height: 0; }
input[type="password"]::-webkit-credentials-auto-fill-button,
input[type="password"]::-webkit-strong-password-auto-fill-button {
    display: none !important; visibility: hidden; pointer-events: none; position: absolute; right: 0;
}
.password-toggle {
    position: absolute; right: 4px; top: 50%; transform: translateY(-50%);
    width: 32px; height: 32px; padding: 0;
    display: inline-flex; align-items: center; justify-content: center;
    border: none; background: transparent; color: var(--text-dim);
    border-radius: var(--r-sm); cursor: pointer;
    transition: color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}
.password-toggle svg { width: 18px; height: 18px; display: block; }
.password-toggle:hover { color: var(--text); background: var(--surface-2); }

/* Medidor de fuerza: cuatro tramos que se van pintando. Es la única señal de
   que una contraseña "válida" puede seguir siendo mala. */
.password-fuerza { display: flex; gap: 4px; margin-top: 8px; }
.password-fuerza span { flex: 1; height: 3px; border-radius: 999px; background: var(--surface-3); }
.password-fuerza--1 span:nth-child(1) { background: var(--danger); }
.password-fuerza--2 span:nth-child(-n+2) { background: var(--warning); }
.password-fuerza--3 span:nth-child(-n+3) { background: var(--success); }
.password-fuerza--4 span { background: var(--success); }

/* ---- Selector de hora (dos listas) ---- */
.hora-picker { display: flex; align-items: center; gap: var(--space-xs); }
.hora-picker__campo {
    width: auto; min-width: 4.5rem;
    font-family: var(--font-mono); text-align: center;
}
.hora-picker__sep { color: var(--text-muted); font-weight: 600; }

/* ---- Select con búsqueda ----
   El <select> real queda oculto detrás; esto es sólo la capa visible. */
.buscable { position: relative; }
.buscable__input { width: 100%; }
.buscable__lista {
    position: absolute; top: calc(100% + 4px); left: 0; right: 0; z-index: 40;
    max-height: 15rem; overflow-y: auto;
    margin: 0; padding: var(--space-xs); list-style: none;
    background: var(--surface-2); border: 1px solid var(--border);
    border-radius: var(--r); box-shadow: var(--shadow-md);
    animation: v-pop 160ms var(--ease) both;
}
.buscable__opcion {
    padding: var(--space-sm) var(--space); border-radius: var(--r-sm);
    cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Activa = la que se elige con Enter. Se mueve con las flechas y también al
   pasar el mouse, para que teclado y mouse no discutan sobre cuál está elegida. */
.buscable__opcion--activa, .buscable__opcion:hover { background: var(--surface); }
.buscable__opcion--actual { color: var(--primary); font-weight: 600; }
.buscable__vacio { padding: var(--space-sm) var(--space); color: var(--text-muted); font-size: .88rem; }

/* ---- Menú: el mismo desplegable, disparado por un botón ----
   Para los <select> de pocas opciones, donde no hay nada que buscar. Comparte
   .buscable__lista y .buscable__opcion con el buscable a propósito: los dos
   desplegables de la aplicación tienen que verse iguales, y si se les cambia el
   radio o la sombra a uno, se le cambia al otro.

   Lo único propio es el botón, que imita a un campo --mismo alto, mismo borde,
   mismo radio-- porque ocupa el lugar de uno y va en la misma fila. */
.menu { display: inline-block; }
.menu__boton {
    display: inline-flex; align-items: center; justify-content: space-between; gap: var(--space-sm);
    width: 100%; height: var(--h-ctl); padding: 0 10px;
    background: var(--surface); color: var(--text);
    border: 1px solid var(--border); border-radius: var(--r);
    font: inherit; font-size: .88rem; text-align: left; cursor: pointer;
    transition: border-color var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
.menu__boton:hover { border-color: var(--border-strong); }
.menu__boton:focus-visible {
    border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-ring); outline: none;
}
.menu__texto  { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.menu__flecha { flex-shrink: 0; opacity: .6; transition: transform var(--t-fast) var(--ease); }
.menu__boton[aria-expanded="true"] .menu__flecha { transform: rotate(180deg); }

/* ============================================================================
 *  INSIGNIAS
 * ========================================================================== */
.badge {
    display: inline-flex; align-items: center;
    height: 21px; padding: 0 9px;
    border-radius: 999px;
    font-size: .69rem; font-weight: 600;
    text-transform: uppercase; letter-spacing: .05em; white-space: nowrap;
}
.badge--success { background: var(--t-success); color: var(--on-success); }
.badge--danger  { background: var(--t-danger);  color: var(--on-danger); }
.badge--warning { background: var(--t-warning); color: var(--on-warning); }
.badge--info    { background: var(--t-info);    color: var(--on-info); }
.badge--accent  { background: var(--t-accent);  color: var(--on-accent); }
.badge--neutral { background: var(--t-neutral); color: var(--on-neutral); }

/* Insignia que sigue al texto de una celda: "Materia inactiva" después del código. */
.badge--al-lado { margin-left: 6px; }

/* ============================================================================
 *  ALERTAS
 * ========================================================================== */
.alert {
    display: flex; gap: 10px; align-items: flex-start;
    padding: 11px 14px; margin-bottom: var(--space);
    border: 1px solid transparent; border-radius: var(--r);
    font-size: .89rem;
}
.alert p { margin: 0; }
/* Más de un párrafo: van envueltos, porque la alerta es flex y los pondría uno al lado del otro. */
.alert__cuerpo p + p { margin-top: 6px; }
.alert--error   { background: var(--t-danger);  border-color: var(--borde-danger);  color: var(--on-danger); }
.alert--success { background: var(--t-success); border-color: var(--borde-success); color: var(--on-success); }
.alert--warning { background: var(--t-warning); border-color: rgba(224,168,58,.35); color: var(--on-warning); }
.alert--info    { background: var(--t-info);    border-color: var(--primary-ring);  color: var(--on-info); }

/* ============================================================================
 *  AYUDA DE PANTALLA (ayuda.js)
 *  Como funciona una pantalla: se lee una vez, se cierra con la cruz y se vuelve
 *  a abrir con la "i" del título. Del azul de un aviso informativo, pero con el
 *  texto en el color de lectura: son dos o tres líneas, y en azul costaban más
 *  que un aviso de una sola.
 * ========================================================================== */
.ayuda {
    position: relative; max-width: 78ch;
    margin: 0 0 var(--space-lg); padding: 12px 44px 12px 16px;
    background: var(--t-info); border: 1px solid var(--primary-ring); border-radius: var(--r);
    color: var(--text); font-size: .88rem; line-height: 1.55; text-wrap: pretty;
}
.ayuda p { margin: 0; }
.ayuda p + p { margin-top: 6px; }
.ayuda--abriendo { animation: v-in 260ms var(--ease) both; }
.ayuda__cerrar {
    position: absolute; top: 8px; right: 8px;
    width: 26px; height: 26px; padding: 0;
    display: inline-flex; align-items: center; justify-content: center;
    border: none; border-radius: 50%; background: transparent;
    color: var(--text-dim); font-size: 1.1rem; line-height: 1; cursor: pointer;
    transition: color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}
.ayuda__cerrar:hover { color: var(--text); background: var(--surface-2); }

/* La "i" junto al título. Discreta a propósito: la explicación ya se leyó, y está
   para cuando haga falta releerla. La letra va por CSS (ver ayuda.js). */
.ayuda-boton {
    display: inline-flex; align-items: center; justify-content: center;
    width: 20px; height: 20px; margin-left: 10px; padding: 0;
    position: relative; top: -3px; vertical-align: middle;
    border: 1px solid var(--border-strong); border-radius: 50%;
    background: transparent; color: var(--text-dim);
    font: italic 600 .78rem/1 Georgia, 'Times New Roman', serif;
    cursor: pointer;
    transition: color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease),
                background var(--t-fast) var(--ease);
}
.ayuda-boton::before { content: 'i'; }
.ayuda-boton:hover,
.ayuda-boton[aria-expanded="true"] { color: var(--on-info); border-color: var(--primary-ring); background: var(--t-info); }
@media (prefers-reduced-motion: reduce) { .ayuda--abriendo { animation: none; } }

/* ============================================================================
 *  TOASTS
 *  Arriba a la derecha, no abajo: es donde queda la vista después de apretar un
 *  botón o guardar un formulario, así que el aviso aparece donde ya se está
 *  mirando.
 * ========================================================================== */
/* ---- Aviso de sesión (sesion.js) ----
   Abajo al centro: los avisos pasajeros van arriba a la derecha, y este no es pasajero,
   se queda hasta que la persona hace algo. Por encima del contenido y del cajón, por
   debajo del modal de confirmación. El fondo teñido de .alert es translúcido y acá flota
   sobre la pantalla: se apoya en la superficie para que el texto no se mezcle con lo de
   abajo. */
.sesion-aviso {
    position: fixed; left: 50%; bottom: 20px; transform: translateX(-50%); z-index: 9000;
    width: max-content; max-width: calc(100vw - 32px); margin: 0;
    align-items: center; box-shadow: var(--shadow-lg);
}
.sesion-aviso.alert--warning { background: linear-gradient(var(--t-warning), var(--t-warning)), var(--surface); }
.sesion-aviso.alert--error   { background: linear-gradient(var(--t-danger), var(--t-danger)), var(--surface); }
.sesion-aviso__texto { margin: 0; }
.sesion-aviso__reloj { font-weight: 600; font-variant-numeric: tabular-nums; }
.sesion-aviso__reloj:empty { display: none; }
.sesion-aviso .btn { flex-shrink: 0; }

#toast-container {
    position: fixed; top: 20px; right: 20px; z-index: 9999;
    display: flex; flex-direction: column; gap: 10px;
    max-width: calc(100vw - 40px); pointer-events: none;
}
.toast {
    pointer-events: auto;
    display: flex; align-items: flex-start; gap: 10px;
    min-width: 280px; max-width: 400px; padding: 12px 13px;
    background: var(--surface); color: var(--text);
    border: 1px solid var(--border); border-left: 3px solid var(--text-dim);
    border-radius: var(--r); box-shadow: var(--shadow-md);
    font-size: .88rem; line-height: 1.45;
    opacity: 0; transform: translateX(22px);
    transition: opacity var(--t-base) var(--ease), transform var(--t-base) var(--ease);
}
.toast--in  { opacity: 1; transform: translateX(0); }
.toast--out { opacity: 0; transform: translateX(22px); }
.toast__icon {
    flex-shrink: 0; width: 20px; height: 20px;
    display: inline-flex; align-items: center; justify-content: center;
    border-radius: 50%; font-size: .72rem; font-weight: 700;
}
.toast__msg   { flex: 1; word-break: break-word; }
.toast__close {
    flex-shrink: 0; width: 22px; height: 22px; padding: 0;
    display: inline-flex; align-items: center; justify-content: center;
    border: none; background: transparent; color: var(--text-dim);
    border-radius: 50%; cursor: pointer;
}
.toast__close:hover { color: var(--text); background: var(--surface-2); }

.toast--success { border-left-color: var(--success); }
.toast--success .toast__icon { background: var(--t-success); color: var(--on-success); }
.toast--warning { border-left-color: var(--warning); }
.toast--warning .toast__icon { background: var(--t-warning); color: var(--on-warning); }
.toast--error   { border-left-color: var(--danger); }
.toast--error   .toast__icon { background: var(--t-danger); color: var(--on-danger); }
.toast--info    { border-left-color: var(--primary); }
.toast--info    .toast__icon { background: var(--t-info); color: var(--on-info); }

/* ============================================================================
 *  MODAL DE CONFIRMACIÓN
 * ========================================================================== */
.modal-overlay {
    position: fixed; inset: 0; z-index: 10000;
    display: flex; align-items: center; justify-content: center;
    padding: var(--space-lg);
    background: var(--scrim);
    backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
    opacity: 0; pointer-events: none;
    transition: opacity var(--t-base) var(--ease);
}
.modal-overlay--in { opacity: 1; pointer-events: auto; }
.modal {
    width: 100%; max-width: 440px;
    padding: 28px 24px 22px;
    background: var(--surface); border: 1px solid var(--border);
    border-radius: var(--r-xl); box-shadow: var(--shadow-lg);
    transform: scale(.96) translateY(6px);
    transition: transform var(--t-base) var(--ease);
}
.modal-overlay--in .modal { transform: none; }
.modal__icon {
    width: 52px; height: 52px; margin: 0 auto 16px;
    display: flex; align-items: center; justify-content: center;
    border-radius: 50%; background: var(--t-info); color: var(--primary);
}
.modal__icon svg { display: block; }
.modal--danger .modal__icon { background: var(--t-danger); color: var(--danger); }
.modal__title  { margin: 0 0 8px; text-align: center; font-size: 1.1rem; }
.modal__detail {
    margin: 0 0 20px; text-align: center;
    color: var(--text-muted); font-size: .9rem; line-height: 1.5; text-wrap: pretty;
}
.modal__campo { margin: 0 0 20px; }
.modal__campo[hidden] { display: none; }
.modal__campo-label { display: block; margin-bottom: 6px; font-size: .82rem;
                      font-weight: 500; color: var(--text-muted); }
.modal__campo-input {
    width: 100%; height: var(--h-ctl); padding: 0 12px;
    background: var(--field); color: var(--text);
    border: 1px solid var(--border); border-radius: var(--r); font-size: .9rem;
}
/* Un campo de texto libre no entra en el alto fijo de los demás: con height los
   dos renglones del detalle quedan en uno solo y recortado. */
.modal__campo-input--texto { height: auto; padding: 8px 12px; resize: vertical; }
.modal__actions { display: flex; gap: 9px; justify-content: flex-end; }
.modal__actions .btn { min-width: 112px; }

/* ============================================================================
 *  TABLAS
 * ========================================================================== */
.table {
    width: 100%; border-collapse: separate; border-spacing: 0;
    font-size: .88rem;
}
.table thead th {
    position: sticky; top: 0; z-index: 2;
    padding: 9px 14px; text-align: left;
    background: var(--surface-2); color: var(--text-dim);
    font-size: .7rem; font-weight: 600;
    text-transform: uppercase; letter-spacing: .07em; white-space: nowrap;
    /* El borde va como box-shadow: el de una celda sticky no viaja con ella. */
    box-shadow: inset 0 -1px 0 var(--border);
}
.table tbody td {
    height: var(--row-h);
    padding: var(--cell-y) 14px;
    border-bottom: 1px solid var(--border);
    vertical-align: middle;
}
.table tbody tr:last-child td { border-bottom: none; }
.table tbody tr { transition: background var(--t-fast) var(--ease); }
.table tbody tr:hover { background: var(--surface-2); }

/* Fila de un registro dado de baja. Se marca con el color del texto y no con
   opacity: bajar la opacidad de toda la fila arrastraba también a las insignias
   y las dejaba por debajo del contraste mínimo. */
.table .row--inactive td { color: var(--text-dim); }
.table .row--inactive code { text-decoration: line-through; }

/* Columna de acciones. El selector nombra las dos cosas porque
   ".table thead th" (0,2,1) le gana por especificidad a ".table__actions"
   (0,1,0) y el text-align nunca llegaba al <th>. */
.table thead th.table__actions,
.table__actions { text-align: right; white-space: nowrap; }
.table__actions .btn--sm { margin: 0; }
.table__actions > * + * { margin-left: 6px; }

/* ---- Fila de detalle ----
   La letra chica de la fila de arriba, desplegada por fila-detalle.js. Se distingue
   por la superficie y por el borde de la izquierda, que la ata visualmente a su fila:
   sin eso, en una tabla de filas alternadas se lee como un registro más. */
.fila-detalle > td {
    background: var(--surface-2);
    box-shadow: inset 3px 0 0 var(--border-strong);
}
.fila-detalle__cuerpo {
    display: flex; flex-wrap: wrap;
    gap: var(--space) var(--space-xl);
    padding: 2px 0;
}
/* Ancho de lectura: un motivo escrito a mano puede ser un renglón o un párrafo, y a
   todo el ancho de la tabla el ojo pierde el renglón al volver. */
.fila-detalle__bloque { min-width: 13rem; max-width: 44rem; }
.fila-detalle__titulo {
    margin: 0 0 4px; font-size: .7rem; font-weight: 600;
    text-transform: uppercase; letter-spacing: .07em; color: var(--text-dim);
}
.fila-detalle__dato { margin: 0 0 2px; font-size: .86rem; text-wrap: pretty; }
.fila-detalle__dato > span:first-child { color: var(--text-muted); }
/* El botón no empuja la insignia: va al lado y en la misma línea. */
.fila-detalle__boton { margin-left: 8px; }

.table__empty { text-align: center; color: var(--text-muted); padding: 44px 20px !important; }
.table__empty svg { color: var(--text-dim); margin-bottom: 12px; }
.table__empty strong { display: block; color: var(--text); font-weight: 500; margin-bottom: 4px; }
/* La acción del estado vacío ("Cargar el primero"), separada del texto que la explica. */
.table__empty-accion { margin: var(--space) 0 0; }

/* ---- Orden por columna ----
   El encabezado ordenable es un botón: se llega con teclado y anuncia su
   estado. La flecha está siempre, apagada, para que se vea que la columna se
   puede ordenar antes de tocarla. */
.table th[data-orden] { cursor: pointer; user-select: none; }
.table th[data-orden]:hover { color: var(--text-muted); }
.table th[data-orden]::after {
    content: '↕'; display: inline-block; margin-left: 5px;
    font-size: .7rem; opacity: .28;
    transition: opacity var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.table th[aria-sort="ascending"]::after  { content: '↑'; opacity: 1; color: var(--primary); }
.table th[aria-sort="descending"]::after { content: '↓'; opacity: 1; color: var(--primary); }

/* ---- Marco de la tabla ----
   Scroll horizontal si es ancha y vertical si es larga, con el encabezado a la
   vista.

   El alto lo decide la pantalla que la contiene, no esta regla. En un listado
   (.pantalla-listado, más abajo) la tabla se queda con todo el alto que sobra;
   en una tarjeta suelta, con el de su contenido. Antes acá había un
   max-height: calc(100vh - 22rem): un número adivinado sobre cuánto ocupaba lo
   de arriba, que erraba en cuanto una pantalla tenía una fila más de filtros. */
.table-wrap {
    position: relative;
    overflow: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent;
}
.table-wrap::-webkit-scrollbar       { width: 9px; height: 9px; }
.table-wrap::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 9px;
                                       border: 2px solid transparent; background-clip: content-box; }
.table-wrap::-webkit-scrollbar-track { background: transparent; }
.table-wrap .table { min-width: 760px; }

/* Se conserva por las pantallas que la traen escrita. Desde que .table-wrap no fija
   alto, no cambia nada: describe lo que ya es el comportamiento por defecto. */
.table-wrap--libre { max-height: none; min-height: 0; }

/* ============================================================================
 *  PANTALLA DE LISTADO
 *  Los catálogos —docentes, usuarios, carreras, materias, comisiones, horarios,
 *  ciclos, días sin clase— ocupan exactamente la ventana, y el único que se
 *  desplaza es el cuerpo de la tabla. El encabezado, los filtros y las columnas
 *  quedan quietos.
 *
 *  Por qué. La tabla tenía un alto máximo calculado a ojo contra la ventana, y
 *  la página entera se desplazaba. Con pocas filas la tarjeta quedaba flotando
 *  arriba con la mitad de la pantalla vacía abajo; con muchas, al bajar se iban
 *  los filtros y el encabezado de la tabla, y había que volver arriba para
 *  acordarse de qué columna era cada una.
 *
 *  Se activa con :has() desde el <section> de la pantalla, y no con una clase
 *  en <body>: la pantalla es la que sabe lo que es, y el layout no tiene que
 *  enterarse de cuál se está mostrando.
 *
 *  Sólo de 761px para arriba. Abajo de eso la tabla se vuelve tarjetas
 *  (ver el bloque responsive) y encerrarlas en una caja del alto de la ventana
 *  da dos desplazamientos anidados en una pantalla que apenas tiene alto.
 * ========================================================================== */
@media (min-width: 761px) {
    .app:has(.pantalla-listado) { height: 100vh; min-height: 0; }

    /* overflow: hidden y no auto: acá no tiene que aparecer NINGUNA barra. La
       única que se ve es la del cuerpo de la tabla. */
    .app:has(.pantalla-listado) .app__col { min-height: 0; overflow: hidden; }

    /* auto y no hidden: es la válvula. Si la ventana es tan baja que ni con la
       tabla en su mínimo entra todo --una notebook chica con el navegador a
       media pantalla--, esta columna se desplaza y nada queda inalcanzable. En
       una ventana normal no se desplaza nunca. */
    .app:has(.pantalla-listado) .container {
        display: flex; flex-direction: column;
        min-height: 0; overflow: auto;
        /* El respiro de abajo lo da el padding de la ventana, no los 56px que
           dejaba la página larga para que el último renglón no quedara pegado. */
        padding-bottom: clamp(16px, 2.2vw, 28px);
    }

    .pantalla-listado {
        display: flex; flex-direction: column;
        flex: 1; min-height: 0;
    }

    /* Nada de lo de arriba ni de lo de abajo se achica para que entre la tabla:
       un filtro a medio alto o una tarjeta de nota recortada es peor que
       desplazar. El único que cede es el cuerpo de la tabla. */
    .pantalla-listado > * { flex-shrink: 0; }

    /* La tarjeta de la tabla es lo que crece, y lo dice ella con su clase. Lo de
       arriba --título, filtros-- mide lo que mide, y lo que sobra es de la
       tabla. Va marcada y no por posición: hay listados con una tarjeta de nota
       debajo de la tabla, y un :last-child estiraría esa. */
    .pantalla-listado > .listado-caja {
        flex: 1 1 auto;
        /* El mínimo es lo que hace que la válvula de arriba sirva: sin él, la
           caja se encogería hasta dejar la tabla en nada antes de desplazar. Da
           para el encabezado y tres filas. */
        min-height: 16rem;
        display: flex; flex-direction: column;
        margin-bottom: 0;
    }
    .pantalla-listado .table-wrap { flex: 1; min-height: 0; }

    /* Sin filas, el aviso se lleva la caja entera y queda centrado, en vez de
       ser un renglón perdido arriba de un rectángulo vacío. La condición mira
       la FILA y no la celda: la celda nunca lleva [hidden], lo lleva el <tr>
       que filtro-tabla.js muestra y esconde. */
    .pantalla-listado .table-wrap:has(tr:not([hidden]) > .table__empty) > .table {
        height: 100%;
    }
    .pantalla-listado .table__empty { vertical-align: middle; }
}

/* Aviso de que la tabla sigue hacia un costado: el borde que corta se desvanece. Las
   clases las pone table-scroll.js según dónde esté el desplazamiento; hasta ahora no
   tenían estilo y el aviso nunca aparecía. Es una máscara y no una sombra porque una
   sombra interna queda tapada por el fondo de las celdas. El negro no es un color que
   se vea: en una máscara solo dice qué parte queda visible. */
.table-wrap--mas-der {
    -webkit-mask-image: linear-gradient(to left, transparent, #000 32px);
            mask-image: linear-gradient(to left, transparent, #000 32px);
}
.table-wrap--mas-izq {
    -webkit-mask-image: linear-gradient(to right, transparent, #000 32px);
            mask-image: linear-gradient(to right, transparent, #000 32px);
}
.table-wrap--mas-izq.table-wrap--mas-der {
    -webkit-mask-image: linear-gradient(to right, transparent, #000 32px, #000 calc(100% - 32px), transparent);
            mask-image: linear-gradient(to right, transparent, #000 32px, #000 calc(100% - 32px), transparent);
}

/* ---- Columna de acciones fija ----
   En una tabla ancha, la columna de acciones se iba de la vista justo cuando
   uno terminaba de leer la fila. Queda pegada a la derecha, con un borde propio
   que la separa de lo que pasa por debajo.

   Sólo en escritorio: abajo de 760px la tabla se convierte en tarjetas y no hay
   nada de qué fijarse. */
@media (min-width: 761px) {
    .table--acciones-fijas th.table__actions,
    .table--acciones-fijas td.table__actions {
        position: sticky; right: 0; z-index: 1;
        background: var(--surface);
        box-shadow: inset 1px 0 0 var(--border);
    }
    .table--acciones-fijas th.table__actions {
        z-index: 3; background: var(--surface-2);
        box-shadow: inset 1px 0 0 var(--border), inset 0 -1px 0 var(--border);
    }
    .table--acciones-fijas tbody tr:hover td.table__actions { background: var(--surface-2); }
}

/* Nombre largo dentro de una celda: se recorta con puntos suspensivos en vez de
   ensanchar la columna. El texto completo queda en el title. */
.celda-recortada {
    display: block; max-width: 18rem;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.celda-nombre { display: inline-flex; align-items: center; gap: 10px; white-space: nowrap; }

/* Barra de progreso dentro de una celda: minutos dictados sobre programados. Un
   "94 / 120 min" obliga a hacer la división mentalmente en cada fila. */
.celda-progreso { display: flex; flex-direction: column; gap: 5px; min-width: 84px; }
.celda-progreso__valor { font-family: var(--font-mono); font-size: .79rem; color: var(--text-muted); }
.celda-progreso__barra { height: 4px; background: var(--surface-3); border-radius: 999px; overflow: hidden; }
.celda-progreso__barra span { display: block; height: 100%; border-radius: 999px; background: var(--success); }
.celda-progreso--parcial .celda-progreso__barra span { background: var(--warning); }
.celda-progreso--nulo    .celda-progreso__barra span { background: var(--danger); }

/* ============================================================================
 *  FILTRO DE LISTADOS
 * ========================================================================== */
.filtro {
    display: flex; gap: 9px; align-items: center; flex-wrap: wrap;
    margin-bottom: 14px;
}
.filtro__buscador { position: relative; flex: 1 1 260px; min-width: 0; }
.filtro__buscador svg {
    position: absolute; left: 11px; top: 50%; transform: translateY(-50%);
    color: var(--text-dim); pointer-events: none;
}
.filtro__buscador .filtro__texto { padding-left: 34px; }

.filtro__texto {
    flex: 1 1 260px; min-width: 0; width: 100%;
    height: var(--h-ctl); padding: 0 12px;
    background: var(--surface); color: var(--text);
    border: 1px solid var(--border); border-radius: var(--r);
    font-size: .88rem;
    transition: border-color var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
/* Los dos son lo mismo para el ojo: un desplegable que acota el listado. El de estado
   escribe sus opciones en la plantilla; los de columna las reciben de las filas. */
.filtro__estado, .filtro__campo {
    height: var(--h-ctl); padding: 0 10px;
    background: var(--surface); color: var(--text);
    border: 1px solid var(--border); border-radius: var(--r);
    font-size: .88rem; cursor: pointer;
}
/* El botón de un filtro por columna está topeado (data-menu-ancho-max), pero su lista no:
   para elegir hay que poder leer el nombre entero de la carrera o del docente. */
.filtro .menu:has(> select.filtro__campo) > .buscable__lista {
    right: auto; min-width: 100%; width: max-content; max-width: min(88vw, 24rem);
}
.filtro__resultado {
    margin-left: auto; color: var(--text-muted); font-size: .83rem;
    white-space: nowrap; font-variant-numeric: tabular-nums;
}

/* ============================================================================
 *  ESQUELETOS DE CARGA
 *  Se pintan mientras el servidor responde. Reservan el mismo alto que el
 *  contenido real, así la página no salta cuando llega.
 * ========================================================================== */
.skeleton {
    background: linear-gradient(90deg, var(--surface-2) 8%, var(--surface-3) 22%, var(--surface-2) 36%);
    background-size: 920px 100%;
    animation: v-shimmer 1250ms linear infinite;
    border-radius: var(--r-sm);
}
.skeleton--linea  { height: 11px; margin-bottom: 14px; }
.skeleton--titulo { height: 13px; width: 44%; margin-bottom: 18px; }
.skeleton--fila   { height: var(--row-h); margin-bottom: 6px; border-radius: var(--r-sm); }
.skeleton--tarjeta { height: 148px; }

/* ============================================================================
 *  PANEL DE INICIO
 *  Tres bloques: qué corre ahora, cómo viene el día y qué falta cargar. No hay
 *  accesos repetidos del menú; el único botón es el del pase, que es la acción.
 * ========================================================================== */
.home__header { display: flex; align-items: flex-start; justify-content: space-between;
                gap: var(--gap); flex-wrap: wrap; }
.home__pase { flex-shrink: 0; }

/* auto-fit en vez de una media query con número fijo: la grilla mete tantas
   columnas de al menos 320px como entren y baja sola. Con un corte fijo en
   1024px había un salto feo —tres columnas apretadas y 24px más abajo UNA sola
   con cuatro números flotando— porque faltaba el escalón de dos columnas. */
.home__paneles {
    display: grid; grid-template-columns: var(--panel-cols);
    gap: var(--gap); align-items: stretch;
}
.home__bloque { margin-bottom: 0; height: 100%; display: flex; flex-direction: column; }
.home__titulo {
    margin: 0 0 var(--space); font-size: .76rem; font-weight: 600;
    text-transform: uppercase; letter-spacing: .08em; color: var(--text-dim);
}
/* Los estados vacíos son buenas noticias, no errores: van en tono normal. */
.home__vacio { margin: 0; color: var(--text-muted); }
/* Un día sin clases por algo que hay que resolver --un ciclo sin activar, clases sin docente--
   se destaca y lleva a donde se resuelve; un feriado o un día que ya terminó queda apagado. */
.home__vacio--problema { color: var(--warning); }
/* En su propio renglón: pegado al texto, en una columna angosta bajaba solo y quedaba corrido. */
.home__vacio-enlace { display: block; width: fit-content; margin-top: 6px; font-weight: 500; }

.home__clases { list-style: none; margin: 0; padding: 0; }
/* Flex y no grid: dentro de una columna angosta el estado tiene que poder bajar
   solo a la línea siguiente. */
.home__clase {
    display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px;
    padding: 12px 0 12px 14px;
    border-left: 3px solid transparent;
    border-top: 1px solid var(--border);
}
.home__clase:first-child { border-top: none; }
/* La barra lateral dice de un vistazo cuál de las clases en curso falta marcar. */
.home__clase--ok      { border-left-color: var(--success); }
.home__clase--falta   { border-left-color: var(--warning); }
.home__clase--proxima { border-left-color: var(--border-strong); }
.home__clase-hora {
    font-family: var(--font-mono); font-size: .83rem; color: var(--text-muted);
    white-space: nowrap; font-variant-numeric: tabular-nums;
}
.home__clase-datos { display: flex; flex-direction: column; flex: 1 1 10rem; min-width: 0; }
.home__clase-datos span   { font-size: .83rem; }
.home__clase-datos strong { overflow-wrap: anywhere; font-weight: 600; }
.home__clase-estado { white-space: nowrap; }
.home__proximas-titulo { margin: 0 0 var(--space-sm); }
.home__sigue {
    margin: var(--space) 0 0; padding-top: 14px;
    border-top: 1px solid var(--border);
    font-size: .83rem; color: var(--text-dim);
}

/* ---- El día en números ----
   Dos columnas fijas y no auto-fit: son cuatro contadores y con auto-fit
   quedaban tres arriba y uno solo abajo, que se lee como si faltara algo. */
.home__numeros { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
/* Los números y la cobertura, en una columna que llena el bloque: así .home__cobertura
   (margin-top: auto) baja al pie y las dos tarjetas del panel terminan a la misma altura. */
.home__resumen { display: flex; flex-direction: column; flex: 1; }
.home__numero {
    display: flex; flex-direction: column; align-items: center; gap: 5px;
    padding: 15px 10px;
    border: 1px solid var(--border); border-radius: var(--r);
    color: var(--text); text-align: center; text-decoration: none;
    transition: border-color var(--t-fast) var(--ease), background var(--t-fast) var(--ease),
                transform var(--t-fast) var(--ease);
}
.home__numero:hover {
    border-color: var(--border-strong); background: var(--surface-2);
    transform: translateY(-1px); text-decoration: none; color: var(--text);
}
.home__numero:active { transform: translateY(0); transition-duration: 60ms; }
.home__numero-valor {
    font-size: 1.85rem; font-weight: 600; line-height: 1;
    font-variant-numeric: tabular-nums;
}
.home__numero-label { font-size: .79rem; color: var(--text-muted); }
.home__numero--ok      .home__numero-valor { color: var(--success); }
.home__numero--tarde   .home__numero-valor { color: var(--warning); }
.home__numero--ausente .home__numero-valor { color: var(--danger); }

.home__cobertura { margin-top: auto; padding-top: 20px; }
.home__cobertura-texto { margin: 0 0 9px; font-size: .86rem; color: var(--text-muted); }
.home__cobertura-texto strong { color: var(--text); }
.home__barra { height: 8px; background: var(--surface-3); border-radius: 999px; overflow: hidden; }
.home__barra-relleno {
    height: 100%; border-radius: 999px; background: var(--success);
    transform-origin: left;
    animation: v-grow 720ms var(--ease) both; animation-delay: 260ms;
}

/* ---- Requiere atención ---- */
.home__pendientes { list-style: none; margin: 0; padding: 0; }
.home__pendientes li { border-top: 1px solid var(--border); }
.home__pendiente {
    display: flex; align-items: center; gap: 14px;
    padding: 13px 0; color: var(--text); text-decoration: none;
    border-radius: var(--r-sm);
    transition: background var(--t-fast) var(--ease);
}
.home__pendiente:hover { text-decoration: none; background: var(--surface-2); color: var(--text); }
.home__pendiente-cantidad {
    flex-shrink: 0; min-width: 2.4rem; text-align: right;
    font-size: 1.4rem; font-weight: 600; color: var(--warning);
    font-variant-numeric: tabular-nums;
}
.home__pendiente-texto { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.home__pendiente-texto strong { font-weight: 500; }
.home__pendiente-texto span   { font-size: .82rem; color: var(--text-muted); }
.home__pendiente svg { flex-shrink: 0; color: var(--text-dim); }
/* Sin cantidad que contar --un ciclo sin activar es uno solo-- va el signo en su lugar. */
.home__pendiente-cantidad--alerta { font-weight: 700; }
/* Los casos por nombre: es lo que se va a buscar, así que no va tan apagado como el detalle. */
.home__pendiente-texto .home__pendiente-cuales { color: var(--text); }
/* Para un rol que no puede abrir esa pantalla: el mismo renglón, sin aspecto de enlace. */
.home__pendiente--sin-enlace:hover { background: none; }

@media (max-width: 620px) { .home__pase { width: 100%; } }

/* ============================================================================
 *  ACCESO — LOGIN, RECUPERACIÓN Y ALTA
 *
 *  Dos columnas: a la izquierda la marca y qué hace el sistema, a la derecha el
 *  formulario. La tarjeta centrada anterior dejaba la pantalla más usada del
 *  sistema como un recuadro de 380px en medio de un fondo vacío, sin decir en
 *  ningún momento a qué se está entrando.
 *
 *  Debajo de 860px el panel de marca desaparece —en un teléfono la mitad de la
 *  pantalla no puede ser decorativa— y el logo pasa arriba del formulario.
 * ========================================================================== */
/* Alto fijo de ventana, no min-height. Con min-height el alto de la grilla lo fija
   la columna más larga, así que un formulario de diez campos --el alta de
   institución-- estiraba también el panel azul: la trama y el círculo quedaban
   repartidos en una franja de dos pantallas de alto y el pie legal caía fuera de
   cuadro hasta que uno bajaba. El panel es un cartel, no un contenedor de datos:
   mide una pantalla y ahí se queda.

   dvh después de vh: en el celular, 100vh no descuenta la barra de direcciones que
   se repliega, así que el último campo del formulario queda debajo del borde. Los
   navegadores que no conocen dvh se quedan con la línea anterior. */
.auth {
    display: grid;
    grid-template-columns: var(--auth-cols);
    height: 100vh;
    height: 100dvh;
    overflow: hidden;
}

.auth__marca {
    position: relative; overflow: hidden;
    padding: 44px 48px;
    background: var(--marca); color: #fff;
    display: flex; flex-direction: column; justify-content: space-between;
}
/* Trama de puntos y un círculo desenfocado. Es lo único decorativo de la app y
   está acá porque es la única pantalla sin datos que mostrar. */
.auth__marca::before {
    content: ''; position: absolute; inset: 0;
    background-image: radial-gradient(circle at 1px 1px, rgba(255,255,255,.16) 1px, transparent 0);
    background-size: 22px 22px; opacity: .5;
}
.auth__marca::after {
    content: ''; position: absolute; right: -140px; top: -120px;
    width: 420px; height: 420px; border-radius: 50%;
    background: var(--marca-2); opacity: .55;
}
.auth__marca > * { position: relative; }

.auth__logo { display: flex; align-items: center; gap: 12px; }
.auth__logo-marca {
    display: inline-flex; align-items: center; justify-content: center;
    width: 38px; height: 38px; border-radius: 11px;
    background: rgba(255,255,255,.14); border: 1px solid rgba(255,255,255,.24);
    font-weight: 700; font-size: 1.1rem;
}
.auth__logo-texto { font-weight: 600; font-size: 1.05rem; letter-spacing: -.01em; }

.auth__pitch { max-width: 26rem; animation: v-in 500ms var(--ease) both; }
.auth__pitch h2 {
    margin: 0 0 14px; font-size: 2rem; line-height: 1.18;
    font-weight: 600; letter-spacing: -.02em; text-wrap: pretty;
}
.auth__pitch p { margin: 0 0 28px; font-size: .98rem; line-height: 1.6;
                 color: rgba(255,255,255,.78); text-wrap: pretty; }
.auth__lista { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 13px; }
.auth__lista li { display: flex; align-items: flex-start; gap: 11px;
                  font-size: .92rem; color: rgba(255,255,255,.9); }
.auth__lista svg { flex-shrink: 0; margin-top: 2px; opacity: .8; }


/* El que se desplaza es este, no la página: la barra aparece dentro de la columna
   del formulario y el panel de marca no se mueve.

   min-height 0 porque es un item de grilla: sin eso no puede achicarse por debajo
   del alto de su contenido, se desborda igual y la barra nunca aparece. */
.auth__panel {
    display: flex; flex-direction: column;
    min-height: 0;
    overflow-y: auto;
    padding: 24px;
    background: var(--bg);
    /* Misma barra fina que la del menú lateral. */
    scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent;
}
.auth__panel::-webkit-scrollbar       { width: 9px; }
.auth__panel::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 9px;
                                        border: 2px solid transparent; background-clip: content-box; }
.auth__panel::-webkit-scrollbar-track { background: transparent; }
.auth__tema  { display: flex; justify-content: flex-end; }
.auth__centro {
    flex: 1; display: flex; align-items: center; justify-content: center;
    padding: 16px 0 40px;
}
.auth__form { width: 100%; max-width: 376px; animation: v-in 420ms var(--ease) both; }
.auth__form--ancha { max-width: 460px; }
.auth__form h1 { margin: 0 0 7px; font-size: 1.6rem; }
.auth__form > p:first-of-type { margin: 0 0 28px; color: var(--text-muted); font-size: .93rem; }

.auth__campos { display: flex; flex-direction: column; gap: 15px; }
/* La separación de la columna es el gap: los grupos no llevan margen propio, y el botón se
   despega un poco más de los campos de lo que los campos se separan entre sí. */
.auth__campos > .form-group { margin: 0; }
.auth__campos > .btn { margin-top: 5px; }
/* Un dato dentro del texto --el correo al que se mandó el código--, en el color de
   lectura para que se encuentre de un vistazo. */
.auth__dato { color: var(--text); font-weight: 500; }
/* La otra salida, debajo del botón principal ("Salir y dejar la otra abierta"). */
.auth__alternativa { margin-top: 10px; }

/* ---- Clave de recuperación ----
   Se lee y se copia a mano: monoespaciada, espaciada y cortada donde haga falta en vez de
   desbordar. En la instalación va grande y sola en su tarjeta; en "Mi institución",
   dentro del aviso. */
.clave-recuperacion { font-family: var(--font-mono); font-size: 1.3rem; letter-spacing: .08em;
                      word-break: break-all; }
.clave-recuperacion--grande { font-size: 1.6rem; margin: 0; }
.clave-recuperacion-caja { margin: var(--space) 0; text-align: center; }

.auth__campo-fila { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 6px; }
.auth__campo-fila label { margin: 0; }
.auth__campo-fila a { font-size: .8rem; }

.auth__volver {
    display: inline-flex; align-items: center; gap: 6px;
    margin-bottom: 22px; padding: 0;
    border: none; background: none;
    color: var(--text-muted); font-size: .85rem; cursor: pointer;
}
.auth__volver:hover { color: var(--text); text-decoration: none; }

.auth__pie {
    margin-top: 26px; padding-top: 20px;
    border-top: 1px solid var(--border); text-align: center;
}
.auth__pie p { margin: 0; font-size: .86rem; color: var(--text-muted); }

/* La vuelta al kiosco (volver-al-kiosco.js): arriba del formulario, a la vista de quien
   llegó tocando la marca sin querer. */
.auth__kiosco {
    display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
    margin: 0 0 22px; padding: 10px 12px;
    background: var(--surface); border: 1px solid var(--border); border-radius: var(--r);
    font-size: .86rem; color: var(--text-muted);
}
.auth__kiosco strong { color: var(--text); font-variant-numeric: tabular-nums; }

.auth__nota {
    margin: 22px 0 0; padding: 12px 14px;
    background: var(--surface); border: 1px solid var(--border); border-radius: var(--r);
    font-size: .82rem; color: var(--text-muted); line-height: 1.55;
}

/* Logo sobre el formulario: sólo cuando el panel de marca no está. */
.auth__logo--movil { display: none; margin-bottom: 26px; }
.auth__logo--movil .auth__logo-marca {
    width: 34px; height: 34px; border-radius: 10px;
    background: var(--marca); border: none; font-size: 1rem;
}
.auth__logo--movil .auth__logo-texto { color: var(--text); font-size: 1rem; }

@media (max-width: 860px) {
    .auth__marca { display: none; }
    .auth__logo--movil { display: flex; }
}

/* ---- Código de un solo uso ----
   Seis casillas y no un input de seis dígitos: el código llega por mail y se
   copia a mano, y con casillas se ve cuántos faltan sin contar caracteres. */
.codigo-input { display: flex; gap: 9px; margin-bottom: 20px; }
.codigo-input input {
    width: 100%; height: 56px; padding: 0;
    text-align: center;
    font-family: var(--font-mono); font-size: 1.5rem;
    background: var(--field); color: var(--text);
    border: 1px solid var(--border); border-radius: var(--r);
    transition: border-color var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
.codigo-input input:focus {
    border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-ring); outline: none;
}
.codigo-input input:not(:placeholder-shown) { border-color: var(--border-strong); }

/* ---- Progreso de un alta en pasos ---- */
.pasos { display: flex; align-items: center; gap: 9px; margin-bottom: 24px; }
.pasos__tramo { flex: 1; height: 4px; border-radius: 999px; background: var(--surface-3); }
.pasos__tramo--hecho { background: var(--primary-solid); }
.pasos__texto { font-size: .75rem; color: var(--text-muted);
                font-family: var(--font-mono); white-space: nowrap; }

/* ---- Compatibilidad: tarjeta de login anterior ---- */
.login-body {
    display: flex; align-items: center; justify-content: center;
    min-height: 100vh; padding: var(--space-lg);
}
.login-card {
    width: 100%; max-width: 400px; padding: var(--space-xl);
    background: var(--surface); border: 1px solid var(--border);
    border-radius: var(--r-lg); box-shadow: var(--shadow-md);
}
.login-card--ancha { max-width: 560px; }
.login-card__header { text-align: center; margin-bottom: var(--space-lg); }
.login-card__logo {
    display: inline-flex; align-items: center; justify-content: center;
    width: 52px; height: 52px; margin-bottom: var(--space);
    border-radius: 15px; background: var(--marca); color: #fff;
    font-weight: 700; font-size: 1.5rem;
}
.login-card__header h1 { font-size: 1.3rem; margin: 0 0 var(--space-xs); }
.login-card__subtitle  { margin: 0; color: var(--text-muted); font-size: .92rem; }
.login-card__ayuda     { margin: var(--space) 0 0; text-align: center; font-size: .88rem; }
.login-card__hint      { margin: var(--space-lg) 0 0; text-align: center; color: var(--text-muted); }

/* ============================================================================
 *  LISTAS CLAVE-VALOR
 * ========================================================================== */
.kv { display: grid; grid-template-columns: max-content 1fr; gap: 11px 22px; margin: 0; font-size: .9rem; }
.kv dt { color: var(--text-muted); font-weight: 400; }
.kv dd { margin: 0; color: var(--text); }
.kv dd code { background: none; padding: 0; }

@media (max-width: 520px) {
    /* A un ancho de teléfono, "Última actualización" y su valor no entran en la
       misma línea: la etiqueta se parte en tres y el valor queda colgando. */
    .kv { grid-template-columns: 1fr; gap: 2px; }
    .kv dt { font-size: .78rem; text-transform: uppercase; letter-spacing: .05em; color: var(--text-dim); }
    .kv dd { margin-bottom: 10px; }
}

/* ============================================================================
 *  CHECKLIST
 * ========================================================================== */
.checklist { list-style: none; padding: 0; margin: 0; }
.checklist__item { position: relative; padding: var(--space-xs) 0 var(--space-xs) 28px; color: var(--text); }
.checklist__item::before {
    position: absolute; left: 0; top: 50%; transform: translateY(-50%);
    width: 18px; height: 18px; border-radius: 50%;
    text-align: center; line-height: 18px; font-size: 11px; font-weight: 700;
}
.checklist__item--done::before { content: '\2713'; background: var(--success); color: #fff; }
.checklist__item--pending::before { content: ''; border: 2px solid var(--border-strong);
                                    width: 14px; height: 14px; }
.checklist__item--pending { color: var(--text-muted); }

/* ============================================================================
 *  PANTALLA INTERMEDIA DE GRUPO
 * ========================================================================== */
.seccion__tarjetas { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: var(--gap); }
.seccion__tarjeta {
    margin-bottom: 0; color: inherit; text-decoration: none;
    transition: border-color var(--t-fast) var(--ease), transform var(--t-fast) var(--ease),
                box-shadow var(--t-fast) var(--ease);
}
/* Se levanta 2px y no 1 como los botones: es una superficie mucho más grande y
   con un solo píxel el movimiento no se lee. */
.seccion__tarjeta:hover {
    border-color: var(--primary); transform: translateY(-2px);
    box-shadow: var(--shadow-md); text-decoration: none;
}
.seccion__tarjeta:active { transform: translateY(0); transition-duration: 60ms; }
.seccion__tarjeta h2 { color: var(--primary); font-size: 1rem; margin-bottom: var(--space-xs); }
.seccion__tarjeta p  { margin: 0; font-size: .88rem; color: var(--text-muted); }

/* El icono de la tarjeta es el mismo que ese destino tiene en la barra lateral, y
   ese es el punto: se reconoce el destino sin leer el título. Va en una pastilla
   teñida en vez de suelto sobre la tarjeta --a 18px un trazo fino se pierde contra
   el fondo-- y arriba, alineado con la primera línea del título. */
.seccion__tarjeta { display: flex; align-items: flex-start; gap: var(--space); }
.seccion__icono {
    display: inline-flex; align-items: center; justify-content: center;
    width: 38px; height: 38px; flex-shrink: 0;
    border-radius: var(--r);
    background: var(--primary-tint); color: var(--primary);
    transition: background var(--t-fast) var(--ease);
}
.seccion__icono svg { width: 19px; height: 19px; }
.seccion__tarjeta:hover .seccion__icono { background: var(--primary-solid); color: #fff; }
/* min-width 0: sin esto una descripción larga no puede achicarse por debajo de su
   ancho de contenido y estira la tarjeta fuera de la grilla. */
.seccion__texto { min-width: 0; }

/* ============================================================================
 *  GRILLA HORARIA SEMANAL
 *  Grid de 30 minutos por fila. Los bloques se posicionan con grid-row desde la
 *  plantilla, así que acá sólo va el aspecto.
 * ========================================================================== */
.grilla-selector {
    display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap;
}
.grilla-selector label { color: var(--text-muted); font-size: .82rem; font-weight: 500;
                         display: block; margin-bottom: 6px; }
.grilla-selector select {
    height: var(--h-ctl); padding: 0 10px;
    background: var(--field); color: var(--text);
    border: 1px solid var(--border); border-radius: var(--r); font-size: .9rem;
}
.grilla-selector__carrera { flex: 1 1 240px; min-width: 0; }
.grilla-selector__carrera select { width: 100%; }
.grilla-selector__anio { width: 132px; }
/* La tarjeta del selector: menos relleno que una de contenido, porque adentro hay una sola
   fila de controles, y más aire abajo antes de la grilla. */
.grilla-selector-caja { padding: var(--space); margin-bottom: var(--space-lg); }
.grilla-selector__anio select { width: 100%; }
.grilla-selector__conteo { margin-left: auto; font-size: .82rem; color: var(--text-muted);
                           padding-bottom: 10px; }

.grilla {
    display: grid;
    /* Ancla de la marca de la hora actual, que se posiciona sobre las filas. */
    position: relative;
    grid-template-columns: 58px repeat(7, minmax(96px, 1fr));
    grid-template-rows: 34px repeat(32, 18px);
    gap: 3px;
    background: var(--surface);
    border: 1px solid var(--border); border-radius: var(--r-lg);
    box-shadow: var(--shadow-sm);
    padding: var(--card-pad);
    overflow: auto;
    scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent;
}
.grilla__corner { grid-row: 1; grid-column: 1; }
.grilla__day-header {
    grid-row: 1;
    display: flex; align-items: center; justify-content: center;
    background: var(--surface-2); border-radius: var(--r-sm);
    color: var(--text-dim); font-size: .74rem; font-weight: 600;
    text-transform: uppercase; letter-spacing: .06em;
}
.grilla__hour-label {
    grid-column: 1; grid-row-end: span 2;
    padding-right: 8px;
    color: var(--text-dim); font-family: var(--font-mono); font-size: .74rem;
    text-align: right;
}

/* El bloque es un <button>: hay que anular lo que el navegador le pone y que no
   le ponía al <a> anterior. Fondo teñido con el color del día y una barra del
   color pleno a la izquierda: el bloque relleno de color saturado, con siete
   colores conviviendo, convertía la grilla en un tablero de manchas. */
.grilla__item {
    display: flex; flex-direction: column; justify-content: center; gap: 2px;
    padding: 6px 9px;
    border: none; border-left: 3px solid var(--dia-lun);
    border-radius: var(--r-sm);
    background: color-mix(in oklab, var(--dia-lun) 16%, var(--surface));
    color: var(--text);
    font-family: inherit; text-align: left; line-height: 1.25;
    overflow: hidden; cursor: pointer;
    transition: filter var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.grilla__item:hover { filter: brightness(1.12); transform: translateY(-1px); color: var(--text); }
.grilla__item strong {
    display: block; font-size: .8rem; font-weight: 600;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.grilla__item small {
    display: block; font-size: .7rem; color: var(--text-muted); font-family: var(--font-mono);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* Un color por día. El selector de atributo replica el criterio de la hoja
   anterior: la plantilla escribe grid-column en un style inline, así que es de
   donde se puede deducir el día sin agregar una clase por bloque. */
.grilla__item[style*="grid-column: 2"] { border-left-color: var(--dia-lun);
    background: color-mix(in oklab, var(--dia-lun) 16%, var(--surface)); }
.grilla__item[style*="grid-column: 3"] { border-left-color: var(--dia-mar);
    background: color-mix(in oklab, var(--dia-mar) 16%, var(--surface)); }
.grilla__item[style*="grid-column: 4"] { border-left-color: var(--dia-mie);
    background: color-mix(in oklab, var(--dia-mie) 16%, var(--surface)); }
.grilla__item[style*="grid-column: 5"] { border-left-color: var(--dia-jue);
    background: color-mix(in oklab, var(--dia-jue) 16%, var(--surface)); }
.grilla__item[style*="grid-column: 6"] { border-left-color: var(--dia-vie);
    background: color-mix(in oklab, var(--dia-vie) 16%, var(--surface)); }
.grilla__item[style*="grid-column: 7"] { border-left-color: var(--dia-sab);
    background: color-mix(in oklab, var(--dia-sab) 16%, var(--surface)); }
.grilla__item[style*="grid-column: 8"] { border-left-color: var(--dia-dom);
    background: color-mix(in oklab, var(--dia-dom) 16%, var(--surface)); }

/* La hora actual: un guion a lo ancho de los días, con un punto en el borde izquierdo.
   Punteado y en rojo tenue, que es como lo dibuja cualquier calendario: tiene que leerse
   como una referencia y no como un bloque más de la grilla. La ubica grilla-ahora.js; sin
   JavaScript no aparece, y no se pierde nada porque no dice nada. */
.grilla__ahora {
    position: absolute; height: 0; z-index: 2; pointer-events: none;
    border-top: 1px dashed var(--danger); opacity: .6;
}
.grilla__ahora::before {
    content: ""; position: absolute; top: -3px; left: -3px;
    width: 6px; height: 6px; border-radius: 50%; background: var(--danger);
}

.grilla__footer { margin: var(--space) 0 0; font-size: .8rem; color: var(--text-dim); }

/* ---- Detalle flotante ---- */
.grilla-detalle {
    position: absolute; z-index: 70;
    min-width: 15rem; max-width: 20rem; padding: var(--space);
    background: var(--surface-2); border: 1px solid var(--border);
    border-radius: var(--r); box-shadow: var(--shadow-lg);
    font-size: .85rem;
    animation: v-pop 160ms var(--ease) both;
}
.grilla-detalle__titulo { margin: 0 0 var(--space-sm); font-weight: 600; color: var(--text); }
.grilla-detalle__datos {
    display: grid; grid-template-columns: auto minmax(0, 1fr);
    gap: 3px var(--space-sm); margin: 0 0 var(--space);
}
.grilla-detalle__datos dt { color: var(--text-muted); }
.grilla-detalle__datos dd { margin: 0; color: var(--text); }

/* ============================================================================
 *  PANTALLAS CON CÁMARA
 *  El pase y el registro del rostro comparten esqueleto: la cámara a la
 *  izquierda y todo lo demás a la derecha. La cámara se lleva algo más del 60%
 *  porque es lo único que hay que mirar con detalle; el resultado va al costado
 *  y a la altura de los ojos, no debajo del video.
 * ========================================================================== */
.camara-layout {
    display: grid;
    grid-template-columns: minmax(0, 1.55fr) minmax(280px, .9fr);
    gap: var(--gap); align-items: start;
}
.camara-layout__principal { min-width: 0; }
.camara-layout__lateral   { display: flex; flex-direction: column; gap: var(--gap); min-width: 0; }
/* Las clases de ahora y lo que falta cargar se refrescan juntas, en un contenedor que no dibuja
   caja propia: así las dos tarjetas quedan separadas por el mismo gap que las demás. */
.pase__panel { display: contents; }

/* El panel lateral acompaña a la cámara al hacer scroll en pantallas altas: si
   el video queda arriba y el mensaje se va de vista, no sirve de nada. */
@media (min-height: 800px) { .camara-layout__lateral { position: sticky; top: var(--space-lg); } }

/* ============================================================================
 *  PANTALLA CON CÁMARA, A ALTO DE VENTANA
 *  El pase ocupa exactamente la ventana: la cámara se lleva todo el alto que
 *  sobra y la columna de la derecha se desplaza sola, sin mover el video ni el
 *  encabezado.
 *
 *  Por qué. El video tenía aspect-ratio: 4/3 y un max-height en vh calculado a
 *  ojo. Entre los dos la caja terminaba más chata que la cámara, y como el video
 *  va con object-fit: cover, lo que no entraba se recortaba: la imagen quedaba
 *  cortada a la altura del cuello. Y la página entera se desplazaba, así que
 *  bajar a leer los avisos se llevaba el video fuera de vista.
 *
 *  Acá el video va con object-fit: contain y ocupa la caja entera: no se recorta
 *  nada, entre a lo ancho o a lo alto. Lo que queda desalineado con eso es el
 *  recuadro del rostro --que se dibuja en coordenadas de la cámara-- y de eso se
 *  encarga ajustarOverlay en pase-asistencia.js, que le da al canvas el tamaño y
 *  la posición exactos de la imagen visible. Hasta ahora ese recuadro caía bien
 *  sólo si la ventana tenía el alto justo para que la caja quedara en la misma
 *  proporción que la cámara.
 *
 *  De 901px para arriba, que es donde la cámara y el panel van lado a lado.
 *  Apilados, abajo de eso, la página se desplaza como siempre.
 * ========================================================================== */
@media (min-width: 901px) {
    .app:has(.pantalla-camara) { height: 100vh; min-height: 0; }
    .app:has(.pantalla-camara) .app__col { min-height: 0; overflow: hidden; }
    /* auto y no hidden, por lo mismo que en los listados: si la ventana es tan
       baja que ni con la cámara en su mínimo entra todo, se desplaza. */
    .app:has(.pantalla-camara) .container {
        display: flex; flex-direction: column;
        min-height: 0; overflow: auto;
        padding-bottom: clamp(16px, 2.2vw, 28px);
    }

    .pantalla-camara { display: flex; flex-direction: column; flex: 1; min-height: 0; }
    .pantalla-camara > * { flex-shrink: 0; }

    .pantalla-camara .camara-layout {
        flex: 1 1 auto; min-height: 22rem;
        align-items: stretch;
        /* La columna de la derecha tiene un ancho útil y no más: son un mensaje, tres
           botones y una lista corta, y estirarlos no los hace más legibles. Todo lo que
           sobra va a la cámara, que es lo único que se mira de lejos, y mientras más
           ancha sea su columna más alto puede usar la imagen sin recortarse. */
        grid-template-columns: minmax(0, 1fr) clamp(300px, 26%, 24rem);
    }

    /* La columna de la cámara: la tarjeta y la caja crecen hasta el borde. */
    .pantalla-camara .camara-layout__principal { display: flex; min-height: 0; }
    .pantalla-camara .camara-layout__principal > .card {
        flex: 1; min-height: 0; display: flex; margin-bottom: 0;
        /* La caja queda centrada cuando la proporción de la cámara deja aire a
           los costados: pegada a la izquierda se lee como un error de armado. */
        justify-content: center;
    }
    .pantalla-camara .camara-caja { flex: 1; min-width: 0; height: 100%; }
    .pantalla-camara .camara-video {
        height: 100%; width: 100%;
        /* Sin proporción fija y sin tope de alto: los dos juntos eran los que
           dejaban la caja más chata que la cámara y obligaban a recortar. */
        aspect-ratio: auto; max-height: none;
    }

    /* La columna de la derecha se desplaza sola. Sin sticky: ya no hay página
       que acompañar, y con el alto de la ventana el panel entero está a la vista. */
    .pantalla-camara .camara-layout__lateral {
        position: static;
        min-height: 0; overflow-y: auto;
        scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent;
        /* Aire para que la barra no quede encima del borde de las tarjetas. */
        padding-right: 4px;
    }
}

@media (max-width: 900px) {
    .camara-layout { grid-template-columns: 1fr; }
    .camara-layout__lateral { position: static; }
    /* Apilado, el video crece tanto que el estado y los botones quedan debajo
       del pliegue. Acá el video cede: de nada sirve verlo enorme si hay que
       scrollear para saber si marcó o no. */
    .camara-video { max-height: 45vh; }
}
@media (max-height: 760px) { .camara-video { max-height: 56vh; } }
@media (max-height: 620px) { .camara-video { max-height: 46vh; } }

.camara-caja { position: relative; width: 100%; line-height: 0; background: #0a0d11; }
.camara-video {
    width: 100%; aspect-ratio: 4 / 3; object-fit: cover;
    background: #0a0d11; display: block;
}

/* Las dos pantallas del pase --la completa y la ventana chica-- muestran el cuadro
   ENTERO: con `cover` lo que no entra se recorta, y lo primero que se va es el mentón
   de quien está parado frente a la cámara. El fondo de la caja tapa lo que sobre a los
   costados. Las otras pantallas con cámara siguen con `cover`: el registro del rostro y
   el kiosco encuadran de cerca y ahí recortar es lo que corresponde. */
.pantalla-camara .camara-video,
.pase-ventana .camara-video { object-fit: contain; }

/* El canvas del recuadro: la posición y el tamaño se los da ajustarOverlay() en el
   navegador, porque tiene que caer sobre la IMAGEN y no sobre la caja, que con `contain`
   no son lo mismo. Estos valores son los de antes de que corra, y los que valen en las
   pantallas cuyo script no lo reposiciona. */
.camara-overlay { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }

/* Guía sobre el video. El recuadro cambia de color según el estado, que es la
   señal que se ve estando parado a dos metros de la pantalla. */
.camara-guia {
    position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
    width: min(56%, 300px); aspect-ratio: 3 / 4;
    border: 2px solid var(--border-strong);
    border-radius: 26% / 32%;
    pointer-events: none;
    transition: border-color var(--t-base) var(--ease);
}
.camara-guia--buscando { border-color: var(--primary); }
.camara-guia--ok       { border-color: var(--success); }
.camara-guia--error    { border-color: var(--danger); }
/* Línea de barrido: dice que el sistema está mirando. Sólo mientras busca. */
.camara-guia--buscando::after {
    content: ''; position: absolute; left: 6%; right: 6%; top: 50%; height: 2px;
    background: var(--primary); box-shadow: 0 0 14px 2px var(--primary-ring);
    animation: v-scan 1500ms var(--ease) infinite;
}

/* El encabezado lleva el título a la izquierda y el paso al modo sin supervisión a la
   derecha: son dos modos de la misma pantalla, y el salto de uno al otro no es un control
   de la cámara. Envuelve en pantallas angostas en vez de apretar el título. */
.pase__header {
    margin-bottom: var(--space);
    display: flex; align-items: flex-start; justify-content: space-between;
    gap: var(--space); flex-wrap: wrap;
}
.pase__titulo { flex: 1 1 22rem; }

/* El pase al kiosco, en el encabezado y a la derecha. Estuvo al pie y a media
   opacidad: ahí no se encontraba. Lo que lo cuida no es esconderlo sino el cuadro
   de confirmación, que dice qué pasa --se cierra la sesión-- antes de que pase.
   Sigue siendo un botón fantasma: está a la vista sin competir con "Iniciar pase",
   que es a lo que se viene a esta pantalla. */
.pase__ir-kiosco { white-space: normal; text-align: center; }

/* El resultado es lo segundo más importante de la pantalla: ocupa el tope de la
   columna y con cuerpo grande, para poder leerse de reojo. */
/* ---- Barra de confirmación (pase y kiosco) ----
   Cuánto falta para que la identidad quede confirmada. Es el dato del servidor --cuánto
   lleva sostenida y cuánto hace falta--, no una animación de relleno: si la persona se
   corre, la barra baja o se va, que es lo que tiene que ver. Llega un dato por cuadro, uno
   por segundo, y la transición dura eso mismo: une un valor con el siguiente para que la
   barra avance en continuo y no a saltos. Al registrar se llena de golpe y queda verde. */
.progreso {
    --progreso-paso: 1s;   /* = INTERVALO_MS de pase-asistencia.js y kiosco.js */
    height: 8px; border-radius: 999px; overflow: hidden;
    background: var(--surface-3);
}
.progreso__barra {
    width: 0; height: 100%; border-radius: inherit;
    background: var(--primary-solid);
    transition: width var(--progreso-paso) linear, background-color var(--t-base) var(--ease);
}
.progreso--ok .progreso__barra { background: var(--success); transition-duration: 0s, var(--t-base); }
.progreso--grande { width: min(520px, 100%); height: 14px; margin: 0 auto; }
.pase__estado .progreso { margin: 2px 0 8px; }

.pase__estado {
    min-height: 5.5rem;
    display: flex; flex-direction: column; justify-content: center;
    line-height: normal;
}
.pase__etiqueta {
    display: flex; align-items: center; gap: 9px; margin: 0 0 12px;
    font-size: .74rem; font-weight: 600;
    text-transform: uppercase; letter-spacing: .08em; color: var(--text-dim);
}
.pase__punto {
    width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0;
    background: var(--text-dim);
}
.pase__punto--buscando { background: var(--primary); animation: v-pulse 1400ms var(--ease) infinite; }
.pase__punto--ok       { background: var(--success); }
.pase__mensaje {
    margin: 0 0 6px; font-size: 1.12rem; font-weight: 600; line-height: 1.3;
    transition: color var(--t-base) var(--ease);
}
.pase__mensaje--success { color: var(--success); }
.pase__mensaje--error   { color: var(--danger); }
.pase__mensaje--warn    { color: var(--warning); }
.pase__mensaje--info    { color: var(--text); font-weight: 400; }

/* Confirmación de marca. La única animación de la pantalla, y dura menos de
   medio segundo: quien toma asistencia está mirando la cámara y al docente, no
   el panel, así que el aviso tiene que alcanzar con verlo de reojo. Escala
   apenas 1.04: más empuja el texto de abajo y se lee como un salto. */
@keyframes pase-confirma {
    0% { transform: scale(1); } 35% { transform: scale(1.04); } 100% { transform: scale(1); }
}
.pase__mensaje--confirma { animation: pase-confirma 420ms var(--ease); transform-origin: left center; }

@keyframes pase-destello {
    0% { background: transparent; } 30% { background: var(--t-success); } 100% { background: transparent; }
}
.pase__estado:has(.pase__mensaje--confirma) {
    animation: pase-destello 700ms var(--ease); border-radius: var(--r);
}

.pase__clase { margin: 0; font-size: .86rem; color: var(--text-muted); min-height: 2.4em; }

.pase__acciones { display: flex; flex-direction: column; gap: 9px; }
.pase__acciones .btn { width: 100%; }
.pase__ayuda { margin: 0; color: var(--text-dim); font-size: .82rem;
               line-height: 1.55; text-wrap: pretty; }

/* ---- El pase en su propia ventana ----
   La ventana chica: la misma pantalla sin el sistema alrededor. No lleva barra lateral ni
   migas porque no es para navegar, es para dejarla en un costado mientras se trabaja. */
.pase-ventana {
    max-width: 460px; margin: 0 auto; padding: 14px;
    display: flex; flex-direction: column; gap: 12px;
}
.pase-ventana__barra { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.pase-ventana__marca { display: inline-flex; align-items: center; gap: 8px; font-weight: 600; }
.pase-ventana .camara-caja { border-radius: var(--r); overflow: hidden; }
.pase-ventana__estado { min-height: 4.5rem; }

/* El punto del menú: si hay un pase andando en otra ventana. Va posicionado y no al final
   de la fila porque con el menú colapsado el texto se esconde y el punto se iría con él;
   así queda a la vista sobre el ícono. El texto vive adentro para el lector de pantalla y
   se oculta con font-size, que --a diferencia de display:none-- no lo calla. */
.lateral__link:has(> .lateral__punto) { position: relative; }
.lateral__punto {
    position: absolute; right: 9px; top: 50%; transform: translateY(-50%);
    width: 8px; height: 8px; border-radius: 50%;
    font-size: 0; background: var(--text-dim);
}
.lateral__punto--andando { background: var(--success); animation: v-pulse 1400ms var(--ease) infinite; }
.lateral__punto--quieto  { background: var(--danger); }

/* El avance del reconocimiento, con forma de aviso: es el mismo toast del resto del
   sistema, con la barra adentro. Las dos partes van en bloque porque adentro del texto del
   aviso son <span>: en línea, el relleno con height 100% no mide nada y la barra se veía
   gris entera, sin avance. */
.toast--pase .progreso,
.toast--pase .progreso__barra { display: block; }
.toast--pase .progreso { margin-top: 7px; }

/* Últimas marcas: confirma que el sistema viene registrando y permite ver de un
   vistazo si alguien quedó afuera. */
.pase__marcas { list-style: none; margin: 0; padding: 0;
                display: flex; flex-direction: column; gap: 10px; }
.pase__marca { display: flex; align-items: center; gap: 10px; font-size: .85rem; }
.pase__marca-hora { font-family: var(--font-mono); font-size: .79rem; color: var(--text-dim);
                    font-variant-numeric: tabular-nums; }
.pase__marca-nombre { flex: 1; min-width: 0; overflow: hidden;
                      text-overflow: ellipsis; white-space: nowrap; }

@media (max-height: 700px) {
    .pase__header .page-header__subtitle { display: none; }
    .pase__header { margin-bottom: var(--space-sm); }
    .pase__estado { min-height: 4rem; }
    .pase__ayuda  { display: none; }
}
/* Apilado, los botones pasan a una fila: a lo ancho de la pantalla se ven
   desproporcionados y empujan otros 50px hacia abajo, justo lo que hace que
   queden fuera de la vista. Va al final para ganarle por cascada a las reglas
   de una sola columna de más arriba. */
@media (max-width: 900px) {
    /* flex-wrap: con los tres botones visibles, en un teléfono no entran en una fila. */
    .pase__acciones, .captura__acciones { flex-direction: row; flex-wrap: wrap; }
    .pase__acciones .btn, .captura__acciones .btn { flex: 1 1 auto; width: auto; white-space: normal; }
}

/* ---- Captura guiada del rostro ---- */
.captura__card { text-align: center; }
.captura__guia { margin-bottom: var(--space); }
.captura__paso { margin: 0; color: var(--text-dim); font-size: .74rem;
                 text-transform: uppercase; letter-spacing: .08em; font-weight: 600; }
.captura__instruccion { margin: 6px 0 0; font-size: 1.2rem; font-weight: 600; }
.captura__detalle { margin: 6px 0 0; color: var(--text-muted); font-size: .88rem; }
.captura__feedback {
    min-height: 2.6rem; margin: var(--space) 0 0;
    display: flex; align-items: center; justify-content: center;
    color: var(--text-muted); font-size: .93rem;
}
.captura__feedback--ok       { color: var(--on-success); font-weight: 600; }
.captura__feedback--corregir { color: var(--on-warning); font-weight: 600; }
.captura__titulo-lateral {
    margin: 0 0 var(--space-sm); font-size: .74rem; color: var(--text-dim);
    text-transform: uppercase; letter-spacing: .08em; font-weight: 600;
}
.captura__etapas { margin: 0; padding: 0; list-style: none; text-align: left; }
.captura__etapa {
    display: flex; align-items: center; gap: var(--space-sm);
    padding: var(--space-sm); border-radius: var(--r-sm);
    color: var(--text-muted); font-size: .87rem;
}
.captura__etapa-num {
    display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0;
    width: 22px; height: 22px;
    border: 1px solid var(--border-strong); border-radius: 50%; font-size: .75rem;
}
.captura__etapa-nombre { flex: 1; }
.captura__etapa-marca  { font-variant-numeric: tabular-nums; font-size: .8rem; }
.captura__etapa--activa { background: var(--surface-2); color: var(--text); }
.captura__etapa--activa .captura__etapa-num { border-color: var(--primary); color: var(--primary); }
.captura__etapa--lista { color: var(--on-success); }
.captura__etapa--lista .captura__etapa-num { background: var(--success); border-color: var(--success); color: #fff; }
.captura__acciones { display: flex; flex-direction: column; gap: 9px; }
.captura__acciones .btn { width: 100%; }
.captura__ayuda { margin: 0; color: var(--text-dim); font-size: .82rem; line-height: 1.5; }

/* En pantallas bajas cede primero el texto explicativo, no la lista de poses:
   sin ver en qué paso va, la guía no sirve de nada. */
@media (max-height: 760px) {
    .captura__detalle, .captura__ayuda { display: none; }
}

/* ============================================================================
 *  REPORTES
 * ========================================================================== */
.reporte__filtros {
    display: grid; grid-template-columns: repeat(auto-fit, minmax(158px, 1fr)); gap: 12px;
}
/* El recuento y las descargas son un ítem más de la fila de filtros, empujado al
   extremo derecho: ocupan el hueco que deja la última fila en vez de una franja
   propia entre la tarjeta y la tabla, que le comía alto al listado.

   Sin ancho propio y con wrap: si los filtros llenan la fila, este bloque baja
   entero en vez de apretar a los de al lado. */
.reporte__acciones {
    display: flex; align-items: center; flex-wrap: wrap;
    gap: 10px var(--gap);
    margin-left: auto;
}
.reporte__recuento { color: var(--text-muted); font-size: .88rem; white-space: nowrap; }
/* Los dos botones son la misma decisión —en qué formato me lo llevó— así que
   van pegados y no repartidos a lo ancho de la fila. */
.reporte__descargas { display: flex; align-items: center; gap: 9px; }

/* ============================================================================
 *  DERECHOS ARCO
 * ========================================================================== */
.arco__nota { margin: 0 0 4px; font-size: .86rem; color: var(--text-muted); text-wrap: pretty; }
.arco__historial {
    margin: var(--space-xs) 0 0; padding-left: var(--space);
    font-size: .85rem; color: var(--text-muted);
}
.arco__derechos { list-style: none; margin: 0; padding: 0; }
.arco__derechos li {
    display: flex; align-items: center; justify-content: space-between;
    gap: 16px; padding: 16px 0; flex-wrap: wrap;
    border-top: 1px solid var(--border);
}
.arco__derechos li > div { display: flex; flex-direction: column; min-width: 15rem; flex: 1; }
.arco__derechos li strong { font-weight: 500; }
.arco__derechos li span { font-size: .85rem; color: var(--text-muted); text-wrap: pretty; }

/* ============================================================================
 *  PUESTO DE CAPTURA NO AUTORIZADO
 *  No se parece a la página de error a propósito: un error dice que algo se
 *  rompió, esto dice que el sistema funciona y que falta estar en otra máquina.
 * ========================================================================== */
.puesto__nota { color: var(--text-muted); font-size: .89rem; }
.puesto__acciones { display: flex; flex-wrap: wrap; gap: var(--space-sm); margin-top: var(--space-lg); }
.puesto__subtitulo {
    margin: var(--space-xl) 0 var(--space); font-size: .76rem; color: var(--text-dim);
    text-transform: uppercase; letter-spacing: .08em; font-weight: 600;
}
/* Uno al lado del otro quedan por debajo del mínimo de 44px de un objetivo táctil. */
@media (max-width: 480px) { .puesto__acciones .btn { width: 100%; } }

/* ---- Tope de equipos ----
   Cerrado detrás de un botón (<details>): abrirlo es el paso deliberado. El campo es corto
   porque el número también lo es (1 a 50). */
.puesto__tope-campo { max-width: 8rem; }
.puesto__tope-nota  { margin-bottom: 0; }

/* ============================================================================
 *  PÁGINA DE ERROR
 * ========================================================================== */
.error-body {
    display: flex; align-items: center; justify-content: center;
    min-height: 100vh; padding: var(--space-lg);
}
.error-card {
    width: 100%; max-width: 480px; padding: var(--space-xl); text-align: center;
    background: var(--surface); border: 1px solid var(--border);
    border-radius: var(--r-lg); box-shadow: var(--shadow-md);
}
.error-card__code {
    font-size: 4rem; font-weight: 600; line-height: 1;
    color: var(--primary); margin-bottom: var(--space-sm); letter-spacing: -.04em;
    font-variant-numeric: tabular-nums;
}
.error-card__title { font-size: 1.3rem; margin: 0 0 var(--space); }
.error-card__msg   { margin: 0 0 var(--space-lg); color: var(--text-muted);
                     font-size: .93rem; line-height: 1.55; }
.error-card__actions { display: flex; gap: var(--space-sm); justify-content: center; flex-wrap: wrap; }
.error-card__detail {
    margin-top: var(--space-lg); padding-top: var(--space);
    border-top: 1px solid var(--border); text-align: left;
}
.error-card__detail summary { cursor: pointer; color: var(--text-muted);
                              font-size: .85rem; user-select: none; }
.error-card__detail summary:hover { color: var(--text); }
.error-card__detail .kv { margin-top: var(--space); font-size: .82rem; }
.error-card__detail .kv dd { word-break: break-word; font-family: var(--font-mono); color: var(--text-muted); }

/* ============================================================================
 *  TABLAS COMO TARJETAS EN PANTALLA ANGOSTA
 *
 *  Las tablas declaran min-width: 760px, así que en un teléfono de 360px había
 *  que desplazarse el doble del ancho de la pantalla para leer una fila entera.
 *
 *  Debajo de 760px cada fila pasa a ser una tarjeta y cada celda una línea
 *  "etiqueta: valor". La etiqueta sale de data-label, que las plantillas ponen
 *  en cada <td>, porque el <thead> deja de estar a la vista.
 *
 *  Los roles ARIA del HTML son los que reponen la semántica: cambiar el display
 *  de los elementos de una tabla hace que Chrome y Safari le saquen la
 *  semántica de tabla al árbol de accesibilidad.
 * ========================================================================== */
@media (max-width: 760px) {
    .table-wrap:has(.table--tarjetas) { overflow: visible; max-height: none; min-height: 0; }

    .table--tarjetas, .table--tarjetas tbody,
    .table--tarjetas tr, .table--tarjetas td { display: block; width: auto; }

    /* El selector repite .table-wrap porque así lo declara la regla de
       escritorio y una media query no suma especificidad: con .table a secas el
       ancho mínimo seguía ganando y la tarjeta se desbordaba 760px igual. */
    .table-wrap .table--tarjetas { min-width: 0; font-size: .93rem; }

    /* El encabezado se oculta sin sacarlo del árbol de accesibilidad:
       display:none lo haría desaparecer también para un lector de pantalla, y
       las etiquetas de columna son justamente lo que da sentido a cada valor. */
    .table--tarjetas thead {
        position: absolute; width: 1px; height: 1px;
        overflow: hidden; clip-path: inset(50%); white-space: nowrap;
    }

    .table--tarjetas tbody tr {
        margin-bottom: var(--space-sm); padding: var(--space-sm) var(--space);
        background: var(--surface); border: 1px solid var(--border); border-radius: var(--r);
    }
    .table--tarjetas tbody tr:last-child { margin-bottom: 0; }
    /* En tarjetas el resaltado al pasar el mouse no aporta —se toca, no se
       apunta— y en un teléfono queda pegado después del toque. */
    .table--tarjetas tbody tr:hover { background: var(--surface); }

    .table--tarjetas tbody td {
        display: flex; flex-wrap: wrap; justify-content: space-between; align-items: baseline;
        gap: var(--space); height: auto; padding: 6px 0;
        border-bottom: 1px solid var(--border); text-align: right;
    }
    .table--tarjetas tbody tr td:last-child { border-bottom: none; }

    .table--tarjetas tbody td::before {
        content: attr(data-label);
        flex: 0 0 auto; margin-right: var(--space);
        color: var(--text-dim); font-size: .72rem; font-weight: 600;
        text-transform: uppercase; letter-spacing: .06em; text-align: left;
    }
    .table--tarjetas tbody td:not([data-label])::before { content: none; }

    /* Celdas de dos partes: un valor principal y un dato secundario en <small>.
       Los tres elementos compiten por el mismo renglón y el del medio es el que
       se comprime: TUP-004 se partía en TUP- / 004. Con shrink en 0 el
       secundario se mantiene entero y, cuando no entra, baja completo. */
    .table--tarjetas tbody td > code,
    .table--tarjetas tbody td > .badge { flex: 0 0 auto; white-space: nowrap; }
    .table--tarjetas tbody td > small {
        flex: 0 0 auto; max-width: 100%; margin-left: auto; text-align: right;
    }
    .table--tarjetas .celda-nombre { white-space: normal; }

    /* Las acciones van abajo, a lo ancho y con altura de dedo: 44px es el
       mínimo de un objetivo táctil y los botones de tabla miden 28. */
    .table--tarjetas tbody td.table__actions {
        display: flex; flex-wrap: wrap; gap: var(--space-sm);
        justify-content: flex-start; padding-top: var(--space-sm); text-align: left;
    }
    .table--tarjetas tbody td.table__actions::before { content: none; }
    .table--tarjetas tbody td.table__actions > * + * { margin-left: 0; }
    .table--tarjetas tbody td.table__actions .btn--sm {
        flex: 1 1 auto; min-height: 44px; height: auto;
    }
    .table--tarjetas tbody td.table__actions .inline-form { display: contents; }

    /* El estado vacío no es una fila de datos: no lleva recuadro ni etiquetas. */
    .table--tarjetas tbody tr:has(.table__empty) { border: none; background: none; padding: 0; }
    .table--tarjetas td.table__empty { display: block; text-align: center; border-bottom: none; }
    .table--tarjetas td.table__empty::before { content: none; }

    /* El detalle es la continuación de la tarjeta de arriba, no otra tarjeta: apilado
       se pega a ella y le saca el margen que separa un registro del siguiente. */
    .table--tarjetas tbody tr.fila-detalle {
        margin-top: calc(var(--gap) * -1);
        border-top-left-radius: 0; border-top-right-radius: 0;
    }
    /* Alineado a la izquierda: en modo tarjeta los valores van a la derecha porque
       enfrentan a su etiqueta, pero acá no hay etiqueta enfrente --hay un párrafo-- y
       a la derecha se pierde el renglón al volver. */
    .table--tarjetas tr.fila-detalle td { border-bottom: none; text-align: left; }
}

/* ============================================================================
   Kiosco: la pantalla desatendida (ADR-0019)

   No hereda el layout con barra lateral a propósito, así que define acá el poco
   marco que necesita. Todo lo que hay en pantalla es la cámara y una línea de
   texto: cualquier cosa de más es un lugar donde alguien podría hacer clic en
   una máquina que nadie está mirando.

   Los tamaños son grandes porque quien marca lee de pie y a un metro, no
   sentado frente al monitor.
   ============================================================================ */

.kiosco {
    margin: 0;
    min-height: 100vh;
    background: var(--bg);
    color: var(--text);
    display: flex;
    align-items: center;
    justify-content: center;
    position: relative;   /* referencia de la barra */
}

/* La barra va posicionada y no como una fila más del flujo: así la cámara sigue
   centrada en la pantalla completa. Metida en el flujo, el video se corría hacia
   abajo el alto de la barra, y en una pantalla que se mira de pie y a un metro
   ese corrimiento se nota. */
.kiosco__barra {
    position: absolute; top: 0; left: 0; right: 0;
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--space);
    padding: var(--space) clamp(16px, 2.2vw, 28px);
}

.kiosco__marca {
    display: inline-flex; align-items: center; gap: 11px;
    color: var(--text); font-weight: 600; letter-spacing: -.01em;
}
.kiosco__logo {
    display: inline-flex; align-items: center; justify-content: center;
    width: 30px; height: 30px; flex-shrink: 0;
    border-radius: 9px;
    background: var(--marca); color: #fff;
    font-weight: 700; font-size: .95rem;
}

/* Discreto a propósito: es la salida de una pantalla cuyo trabajo es quedarse
   quieta. Tiene que estar para quien lo busca, sin invitar a nadie a apretarlo. */
.kiosco__entrar { opacity: .55; transition: opacity var(--t-fast) var(--ease); }
.kiosco__entrar:hover,
.kiosco__entrar:focus-visible { opacity: 1; }
/* La marca es un enlace al login (kiosco.html): sin subrayado ni azul, que en un mostrador
   la harían parecer un botón puesto para tocar. El foco se ve igual. */
a.kiosco__marca { text-decoration: none; border-radius: var(--r); }
a.kiosco__marca:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--primary-ring); }

.kiosco__marco {
    width: min(1100px, 96vw);
    display: grid;
    gap: var(--space-lg);
    padding: var(--space-lg);
}

.kiosco__camara {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius, 12px);
    overflow: hidden;
    box-shadow: var(--shadow-md);
}

.kiosco__panel {
    text-align: center;
    display: grid;
    gap: var(--space-sm);
    min-height: 8rem;      /* reservado: sin esto la pantalla salta al aparecer el resultado */
}

.kiosco__mensaje {
    margin: 0;
    font-size: clamp(1.4rem, 3vw, 2.2rem);
    font-weight: 600;
    color: var(--text);
}

.kiosco__mensaje--success { color: var(--success); }
.kiosco__mensaje--info    { color: var(--primary); }
.kiosco__mensaje--warn    { color: var(--warning); }
.kiosco__mensaje--error   { color: var(--danger); }

/* El apellido es lo que el docente mira para confirmar que lo reconoció a él, así
   que va más grande que el resto. Nunca lleva el nombre completo: el servidor no
   lo manda (RF-87). */
.kiosco__apellido {
    margin: 0;
    font-size: clamp(2rem, 6vw, 4rem);
    font-weight: 700;
    line-height: 1.1;
}

.kiosco__detalle {
    margin: 0;
    font-size: clamp(1rem, 2vw, 1.4rem);
    color: var(--text-muted);
}

.kiosco__iniciar {
    justify-self: center;
}

/* En pantallas anchas la cámara y el texto van lado a lado; en la PC de secretaría,
   que suele ser 1366x768, apilados dejan el texto abajo del pliegue. */
@media (min-width: 900px) {
    .kiosco__marco {
        grid-template-columns: 1.4fr 1fr;
        align-items: center;
    }
}

/* ---- Cámara del puesto (V026) ---------------------------------------------------------- */
/* La vista previa sirve para reconocer cuál cámara es, no para trabajar: chica alcanza. */
.camara-caja--vista { max-width: 360px; margin: 12px 0; }
/* El aviso de "la elegida no está conectada" va pegado a la cámara en las tres pantallas. */
.camara-aviso { margin-top: 8px; }

/* ---- Detalle del ciclo lectivo (V027) ------------------------------------------------ */
/* Una fila por período, cada una con su formulario. Flex y no la grilla de .form-row: la
   fila lleva además el conteo de comisiones y dos botones, y en una pantalla angosta tiene
   que poder partirse sin que los campos queden de un dedo de ancho. */
.periodo-item {
    display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--space-sm) var(--space);
    padding: var(--space) 0; border-top: 1px solid var(--border);
}
.periodo-item:first-of-type { border-top: none; }
.periodo-item__form {
    flex: 1 1 34rem; display: flex; flex-wrap: wrap; align-items: flex-end;
    gap: var(--space-sm) var(--space);
}
.periodo-item__form .form-group { flex: 1 1 9rem; margin: 0; }
.periodo-item__lectura { flex: 1 1 20rem; display: flex; flex-wrap: wrap; gap: var(--space-sm); }
/* Alineado con la base de los botones, no con la etiqueta de arriba. */
.periodo-item__comisiones { font-size: .85rem; white-space: nowrap; padding-bottom: 7px; }
.ciclo-detalle .page-header__subtitle {
    display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-sm);
}
.ciclo-detalle__registro { margin-top: var(--space-lg); }
.ciclo-detalle__pie { margin: var(--space) 0 0; }
/* El filtro por año de Comisiones va en su propio form GET; display: contents lo deja ser un
   control más de la barra de filtros en vez de una caja aparte. */
.filtro__ciclo { display: contents; }
/* El año de los días sin clase: va al servidor, al lado de los filtros del navegador. */
.filtro__anio { display: flex; align-items: center; gap: 8px; margin: 0; }
.filtro__anio label { margin: 0; color: var(--text-muted); font-size: .85rem; }
.filtro__anio input {
    width: 6.5rem; height: var(--h-ctl); padding: 0 10px;
    background: var(--surface); color: var(--text);
    border: 1px solid var(--border); border-radius: var(--r);
    font-size: .88rem; -moz-appearance: textfield; appearance: textfield;
}
.filtro__anio input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.filtro__anio input:focus-visible {
    border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-ring); outline: none;
}
/* Lista sin viñetas: los períodos en el listado de ciclos, los días sin clase en el detalle. */
.lista-plana { list-style: none; margin: 0; padding: 0; }
.lista-plana li + li { margin-top: 4px; }

/* ============================================================================
   Gráficos del reporte (RF-33)

   Dibujados a mano, sin librería: el proyecto no tiene ninguna dependencia de
   terceros en el front y esto no justifica la primera. Las barras que llevan
   texto van en HTML --se seleccionan, se leen con lector de pantalla y no se
   deforman-- y solo el gráfico semanal usa SVG, que es el que necesita escala.
   ============================================================================ */

.grafico { margin-top: 12px; }

/* El ancho del eje se reserva con la misma sangría arriba y abajo, para que cada
   etiqueta de semana caiga justo debajo de su columna. */
.grafico__area { position: relative; padding-left: var(--grafico-eje, 40px); }
.grafico__svg { width: 100%; height: 190px; display: block; }

/* Los valores del eje van sobre las líneas que marcan, no en una nota al pie:
   una guía sin número obliga a leer un renglón para saber qué marca. Van en HTML
   y no dentro del SVG porque el lienzo se estira --preserveAspectRatio none-- y
   el texto de adentro saldría deformado. */
.grafico__escala { position: absolute; left: 0; top: 0; bottom: 0; width: calc(var(--grafico-eje, 40px) - 8px); }
.grafico__escala-marca {
    position: absolute; right: 0; transform: translateY(-50%);
    font-size: .7rem; color: var(--text-muted); white-space: nowrap;
}
/* Las tres marcas del eje: arriba el 100%, al medio el 50%, abajo el 0%. */
.grafico__escala-marca--tope  { top: 0; }
.grafico__escala-marca--medio { top: 50%; }
.grafico__escala-marca--base  { top: 100%; }

.grafico__guia { stroke: var(--border); stroke-width: 1; stroke-dasharray: 3 4; vector-effect: non-scaling-stroke; }
.grafico__eje  { stroke: var(--border); stroke-width: 1; vector-effect: non-scaling-stroke; }

.grafico__barra { fill: var(--success); }
/* Sin dato no es cero: se dibuja al ras y apagado, para que no se lea como una
   semana en la que no se dictó nada. */
.grafico__barra--sin-dato { fill: var(--text-muted); opacity: .45; }

.grafico__etiquetas { display: flex; margin-top: 6px; padding-left: var(--grafico-eje, 40px); }
.grafico__etiqueta {
    flex: 1 1 0; min-width: 0; text-align: center;
    display: flex; flex-direction: column; gap: 1px;
}
.grafico__etiqueta-valor { font-size: .78rem; font-weight: 600; }
.grafico__etiqueta-texto { font-size: .7rem; color: var(--text-muted); }

/* ---- Barra apilada de estados ---- */
.grafico__apilada {
    display: flex; height: 22px; border-radius: 999px; overflow: hidden;
    background: var(--surface-3); margin-top: 12px;
}
.grafico__tramo { height: 100%; }
.grafico__tramo--ok      { background: var(--success); }
.grafico__tramo--tarde   { background: var(--warning); }
.grafico__tramo--ausente { background: var(--danger); }

.grafico__referencias { display: flex; flex-wrap: wrap; gap: 18px; margin-top: 12px; }
.grafico__referencia {
    display: inline-flex; align-items: center; gap: 7px;
    color: var(--text); text-decoration: none; font-size: .9rem;
    border-radius: var(--r-sm); padding: 2px 4px;
}
.grafico__referencia:hover { background: var(--surface-3); }
.grafico__punto { width: 10px; height: 10px; border-radius: 999px; flex: none; }
.grafico__punto--ok      { background: var(--success); }
.grafico__punto--tarde   { background: var(--warning); }
.grafico__punto--ausente { background: var(--danger); }

/* ---- Rankings ---- */
.grafico__rankings {
    display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: 24px; margin-top: 12px;
}
.grafico__ranking h3 { font-size: .95rem; margin: 0 0 10px; }
.grafico__fila {
    display: grid; grid-template-columns: minmax(0, 1fr) 90px auto;
    align-items: center; gap: 10px; padding: 5px 0;
}
.grafico__fila-etiqueta {
    font-size: .85rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.grafico__fila-barra {
    height: 8px; border-radius: 999px; background: var(--surface-3); overflow: hidden;
}
.grafico__fila-barra > span { display: block; height: 100%; background: var(--danger); border-radius: 999px; }
.grafico__fila-valor { font-size: .8rem; color: var(--text-muted); white-space: nowrap; }

@media (max-width: 640px) {
    .grafico__etiqueta-texto { display: none; }
    .grafico__fila { grid-template-columns: minmax(0, 1fr) 60px auto; }
}

/* ============================================================================
 *  UTILIDADES
 *  Separaciones sueltas que no son de ningún componente; antes cada una era un
 *  style="" en la plantilla. Van al final a propósito: con la misma especificidad
 *  gana la última regla, y estas tienen que ganarle al margen propio del componente
 *  (.form-actions, .kv, .home__vacio) cuando se las pone encima.
 * ========================================================================== */
.aire-arriba    { margin-top: var(--space); }
.aire-izquierda { margin-left: var(--space); }
