/* =====================================================================
   Orden de capas de la cascada

   Hoy conviven dos generaciones visuales: una base y una capa de
   retematizado prefijada con body.app-body, que gana por especificidad.
   Ordenar eso con @layer permite que la precedencia la fije la capa y no
   la longitud del selector.

   La migración es incremental. Todo el CSS está de momento en `legacy`,
   que va la última y por tanto sigue mandando. Sacar un componente a su
   capa no cambia nada hasta que se borra su gemela de `legacy`: ese es el
   momento de comprobarlo con bin/ui_reference.php.

   CUIDADO: una regla normal SIN capa vence a todas las reglas normales
   dentro de capas. Si añades CSS nuevo, tiene que ir dentro de una capa.
   ===================================================================== */
@layer reset, tokens, base, componentes, modulos, tema, utilidades, legacy, migrado, accesibilidad;

/* =====================================================================
   PP Neue Montreal — autoalojada (licencia comercial de Pangram Pangram)

   Va fuera de @layer a propósito: @font-face no es una regla de estilo,
   no participa en la cascada y por tanto la advertencia sobre reglas sin
   capa no le aplica.

   SOLO EXISTEN TRES PESOS EN ESTILO NORMAL: 400, 500 y 700. La familia no
   tiene 600 ni 800. Comprobado en navegador: font-weight 600, 700, 800 y
   900 renderizan idénticos porque todos caen en el Bold real y el
   navegador no sintetiza pesos intermedios. Por eso el CSS de la
   aplicación usa únicamente --fw-regular / --fw-medium / --fw-bold.

   TAMPOCO HAY EJE DE ANCHURA: font-stretch y font-variation-settings
   'wdth' no hacen nada sobre esta familia (medido: 390,40px idéntico de
   75% a 125%). La jerarquía de los títulos se construye con tamaño, peso
   y los tokens --tracking-*, nunca con anchura.

   Los archivos thin (100) y semibold italic (600i) están en el repositorio
   pero no se declaran: no hay ningún uso real que los justifique y cada
   cara declarada es una descarga potencial.

   Las URL de aquí deben coincidir EXACTAMENTE con las del preload de los
   layouts. El helper asset() añade ?v=mtime; si el preload lo usara y esto
   no, el navegador descargaría cada fuente dos veces.
   ===================================================================== */
@font-face {
    font-family: "PP Neue Montreal";
    src: url("../fonts/neue-montreal/ppneuemontreal-book.woff2") format("woff2");
    font-weight: 400;
    font-style: normal;
    font-display: swap;
}
@font-face {
    font-family: "PP Neue Montreal";
    src: url("../fonts/neue-montreal/ppneuemontreal-medium.woff2") format("woff2");
    font-weight: 500;
    font-style: normal;
    font-display: swap;
}
@font-face {
    font-family: "PP Neue Montreal";
    src: url("../fonts/neue-montreal/ppneuemontreal-bold.woff2") format("woff2");
    font-weight: 700;
    font-style: normal;
    font-display: swap;
}
@font-face {
    font-family: "PP Neue Montreal";
    src: url("../fonts/neue-montreal/ppneuemontreal-italic.woff2") format("woff2");
    font-weight: 400;
    font-style: italic;
    font-display: swap;
}

/* =====================================================================
   Transiciones de navegación entre páginas

   Nexo Azul es una aplicación de páginas: cada clic recarga el documento
   entero y sin esto el usuario ve un parpadeo en blanco. `@view-transition`
   funde una página con la siguiente sin una línea de JavaScript y sin ninguna
   dependencia. Donde no esté soportado no pasa nada: se navega como siempre.

   EL ARMAZÓN NO PARTICIPA, y hubo que aprenderlo por las malas.

   Darle a `.sidebar` un `view-transition-name` no basta para dejarla quieta:
   el nombre sólo la saca del grupo raíz, y el navegador sigue animando SU
   grupo. Como la barra cambia de anchura —108 px con el panel contextual
   cerrado, 424 px con él abierto— el navegador interpolaba entre las dos
   instantáneas, es decir, la deformaba.

   El caso que lo disparaba es el más común de todos: pulsar un enlace DENTRO
   del submenú abierto. La instantánea vieja lleva el panel desplegado y la
   nueva lo lleva cerrado, así que el raíl aparecía duplicado a dos tamaños,
   el logotipo fantasmeaba y las dos páginas se veían superpuestas.

   La exclusión de verdad son las tres reglas de abajo: anular la animación
   del GRUPO (que es la que interpola caja y posición) y la de las dos
   instantáneas, dejando visible sólo la nueva. Con eso la barra y la cabecera
   se comportan como si la transición no existiera, que es justo lo que se
   quiere de un armazón persistente.

   Lo que sí se funde es el contenido, que es lo que de verdad cambia.
   ===================================================================== */
@view-transition { navigation: auto; }

@layer base {
    /* Nombre propio: los saca del grupo raíz, que es el que se funde. */
    .sidebar { view-transition-name: nexo-sidebar; }
    .topbar  { view-transition-name: nexo-topbar; }

    /* Y aquí se les quita toda animación. Las tres reglas hacen falta:
       - el GRUPO anima caja y posición (es la que deformaba),
       - `old` es la instantánea saliente (la que fantasmeaba),
       - `new` es la entrante, que debe estar visible desde el primer frame. */
    ::view-transition-group(nexo-sidebar),
    ::view-transition-group(nexo-topbar) {
        animation: none;
    }
    ::view-transition-old(nexo-sidebar),
    ::view-transition-old(nexo-topbar) {
        animation: none;
        opacity: 0;
    }
    ::view-transition-new(nexo-sidebar),
    ::view-transition-new(nexo-topbar) {
        animation: none;
        opacity: 1;
    }

    /* El contenido: fundido con un desplazamiento corto hacia arriba. La
       entrada dura algo más que la salida para que la página nueva se sienta
       como el destino y no como un relevo. */
    ::view-transition-old(root) {
        animation: nexo-page-out var(--dur-2) var(--ease) both;
    }
    ::view-transition-new(root) {
        animation: nexo-page-in var(--dur-3) var(--ease-enter) both;
    }
    @keyframes nexo-page-out {
        to { opacity: 0; }
    }
    @keyframes nexo-page-in {
        from { opacity: 0; transform: translateY(var(--sp-3)); }
        to   { opacity: 1; transform: none; }
    }

    /* Respeto de la preferencia del sistema. Se apaga entera: quien pide menos
       movimiento no está pidiendo movimiento más rápido. */
    @media (prefers-reduced-motion: reduce) {
        ::view-transition-group(*),
        ::view-transition-old(*),
        ::view-transition-new(*) {
            animation: none !important;
        }
    }

    /* Captura determinista del arnés visual (E-4).

       Sólo la activa bin/ui_reference.php con ?captura=1, y esa página no es
       una pantalla de la aplicación. Una animación a medio correr en el
       instante de medir mueve la geometría y produce diferencias que no son
       regresiones. */
    .nexo-sin-motion *,
    .nexo-sin-motion *::before,
    .nexo-sin-motion *::after {
        animation-duration: 0s !important;
        animation-delay: 0s !important;
        transition-duration: 0s !important;
        transition-delay: 0s !important;
    }
}
@layer legacy {
    /* =====================================================================
       Nexus — app.css
       Hoja común a los tres armazones: aplicación (body.app-body), acceso
       (body.auth-body) y páginas de error (body.error-body).

       El sistema original era "glass": tarjetas blancas translúcidas con
       desenfoque sobre un degradado azul PP→Asturias. La aplicación ya no se
       pinta así —body.app-body pasa las superficies a opacas y apaga el
       desenfoque—, y sobrevive solo en el acceso y en las páginas de error, que
       siguen sobre el azul. Los tokens --on-blue* son de ese sistema.

       Sin fuentes externas: la familia va autoalojada por CSP.
       ===================================================================== */

    /* ---------- Reset ---------- */
    *, *::before, *::after { box-sizing: border-box; }
    html, body, h1, h2, h3, h4, p, figure, blockquote, dl, dd { margin: 0; }
    body { min-height: 100vh; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; }
    img, picture, svg { max-width: 100%; display: block; }

    /* N-25. Lo que era constante en los sesenta SVG de cada pagina —relleno,
       trazo, ancho y remates— vive aqui una vez en vez de sesenta veces en el
       HTML. `.ico` la lleva solo el icono servido desde la hoja de simbolos; el
       que se emite entero conserva sus atributos de presentacion, y los siete
       con trazo propio lo declaran encima. */
    .ico {
        fill: none;
        stroke: currentColor;
        stroke-width: 1.9;
        stroke-linecap: round;
        stroke-linejoin: round;
    }
    /* Los dos unicos trazos propios del juego. Van en clase y no en `style=`
       porque la CSP (`style-src 'self'`) descarta los atributos de estilo. */
    .ico--t2 { stroke-width: 2; }
    .ico--t22 { stroke-width: 2.2; }

    /* La hoja de simbolos no se ve: sirve de origen para los `<use>`. Sin esto
       ocupaba 300x150 px al principio del documento —el tamano por defecto de
       un `<svg>` sin dimensiones— y empujaba el primer dato bajo el pliegue. */
    .nexo-hoja-iconos { position: absolute; width: 0; height: 0; overflow: hidden; }
    input, button, textarea, select { font: inherit; color: inherit; }
    button { cursor: pointer; }
    a { color: inherit; }
    ul[class], ol[class] { list-style: none; margin: 0; padding: 0; }

    /* ---------- Tokens ----------
       Toda la paleta sale de aquí. Antes había 407 colores sueltos repartidos por
       las cuatro hojas; ahora son estos peldaños y todo lo demás los referencia.
       El azul de marca (--b-500) y la tinta (--b-900) son exactamente los de
       siempre: lo que se ha corregido es la dispersión y el contraste. */
    :root {
        /* Rampa azul. Un solo tono (H215); de él salen marca, tinta y bordes. */
        --b-50:  #f3f7fc;
        --b-100: #e6eef9;
        --b-200: #c4d7f3;
        --b-300: #8ab2ea;
        --b-400: #538fe4;
        --b-500: #1f6fe0;   /* azul de marca, sin tocar */
        --b-600: #1a5ebc;
        --b-700: #154e9d;
        --b-800: #133b6c;
        --b-900: #14304e;   /* tinta, sin tocar */
        /* Extremos profundos de la rampa, para fondos a sangre. */
        --b-950: #0d1730;
        --b-1000: #020b1d;

        /* Portada de acceso. Va deliberadamente más oscura y más saturada que la
           rampa: es un fondo a sangre, no un color de texto, y la red neuronal que
           se dibuja encima solo se lee sobre este azul. */
        --auth-deep: #020d20;
        --auth-1: #020b1d;
        --auth-2: #031a3d;
        --auth-3: #052b5e;
        --auth-alt: #03162f;
        --auth-alt-1: #02132c;
        --auth-alt-2: #052c63;
        --auth-alt-3: #0a579f;

        /* Versiones claras de los estados: la única forma legible de cada uno
           sobre fondo oscuro. Son a los estados lo que --gold-ink es al dorado,
           pero al revés: --x sobre fondo claro, --x-soft sobre fondo oscuro. */
        --ok-soft: #c7f4dd;
        --warn-soft: #ffe2a8;
        --bad-soft: #ffdbe1;

        /* Neutros del mismo tono. --n-500 sustituye al antiguo #8094b3, que se
           quedaba en 3.09 sobre blanco y no llegaba al mínimo legible (AA 4.5). */
        --n-50:  #f8fafc;
        --n-100: #eff2f6;
        --n-200: #dde2e9;
        --n-300: #bdc6d1;
        --n-400: #7e90a9;   /* iconos y trazos, no texto */
        /* P-12. Era #617798, elegido midiendo sobre BLANCO PURO, donde da
           4,56. Sobre las superficies tintadas en las que se usa de verdad
           daba 4,24 en las cabeceras de tabla y 3,70 sobre el fondo rosa de
           una fila crítica: por debajo de AA en 287 apariciones medidas.
           El nuevo se ha elegido contra el PEOR de sus ocho fondos reales,
           con margen (4,73 y no 4,51), para que un ajuste posterior de un
           tinte no lo devuelva al rojo el mismo día. */
        --n-500: #536683;   /* texto terciario */
        --n-600: #485a7a;   /* texto secundario */

        /* Tinta */
        --ink: var(--b-900);
        --ink-2: var(--n-600);
        --ink-3: var(--n-500);

        /* Superficies.

           Estos cinco tokens se llamaban --glass* porque nacieron para el
           sistema translúcido original: tarjetas frosted sobre un degradado
           azul. Ese sistema ya no se pinta —body.app-body los redefine opacos y
           apaga el desenfoque con --blur: none— pero el nombre sobrevivió al
           cambio, de modo que el CSS decía «cristal» donde había blanco sólido.

           Lo que hacen HOY es nombrar superficies: el fondo de una tarjeta, su
           versión elevada, su borde y la línea divisoria neutra. Los valores
           translúcidos de aquí abajo solo los ven las páginas de error, que
           siguen sobre el fondo azul.

           --line es la neutra de la familia de líneas de estado, junto a
           --ok-line, --warn-line, --bad-line, --info-line, --muted-line y
           --gold-line. */
        --surface: rgba(255, 255, 255, .72);
        --surface-2: rgba(255, 255, 255, .54);
        --surface-raised: rgba(255, 255, 255, .86);
        --surface-border: rgba(255, 255, 255, .6);
        --line: rgba(120, 150, 190, .2);
        --blur: blur(20px) saturate(150%);

        /* Texto sobre el fondo azul (fuera de tarjetas) */
        --on-blue: #ffffff;
        --on-blue-2: rgba(255, 255, 255, .84);
        --on-blue-3: rgba(255, 255, 255, .6);

        /* Marca */
        --brand: var(--b-500);
        /* El azul de marca COMO TEXTO. Es un escalón más oscuro de la misma
           rampa, y existe porque #1f6fe0 da 4,06 : 1 sobre los fondos teñidos
           de los paneles (#e4eefb) y 4,76 : 1 sobre blanco: pasa raspando en
           uno y no pasa en el otro, y los enlaces «Ver todas» de las cabeceras
           de panel viven precisamente sobre el teñido. Con --b-600 son 5,32 y
           6,23.

           Se separa en vez de oscurecer --brand porque --brand también es
           FONDO en veinte reglas, y ahí el azul de marca está marcado «sin
           tocar». La norma que se incumplía es 1.4.3, que habla de texto; los
           rellenos y bordes los gobierna 1.4.11 con 3 : 1, y eso ya lo
           cumplían. Se cambia lo que falla, no todo lo que se parece. */
        --brand-texto: var(--b-600);
        --brand-600: var(--b-600);
        --brand-700: var(--b-700);
        --brand-grad: linear-gradient(135deg, var(--b-400), var(--b-500));
        --brand-soft: rgba(31, 111, 224, .12);

        /* Fondo de la aplicación. Es el único degradado que se sale de la rampa a
           propósito: el salto de tono es lo que le da profundidad al fondo. */
        --app-bg: #0a3f86;
        --app-bg-1: #0a4ea0;
        --app-bg-2: #1f6fe0;
        --app-bg-3: #2aa0e0;

        /* Estados. Los tonos son los de siempre; se ha bajado la luminosidad hasta
           que cada color es legible sobre su propio tinte, que es como se usan en
           los badges. El verde y el ámbar no llegaban al mínimo. */
        /* P-12. #167c45 daba 4,39 sobre su propio tinte y 3,61 sobre la fila
           resaltada de un concejo. Al llevar el chip de concejo a los cuatro
           listados (O-1) ese fallo pasó de anecdótico a estar en cada fila. */
        --ok:    #126739; --ok-bg:    rgba(18, 103, 57, .13);  --ok-line:    rgba(18, 103, 57, .30);
        /* P-12. #9b600d daba 4,33 sobre su propio tinte: el caso que la
           auditoría no había medido y el arnés sí. */
        --warn:  #935b0c; --warn-bg:  rgba(147, 91, 12, .13);  --warn-line:  rgba(147, 91, 12, .30);
        /* Tonos oscuros para texto sobre una superficie del MISMO tono. Un
           `--bad` sobre `--bad-bg` da 4,25 y no llega a AA: el tinte del 13 %
           se compone sobre la superficie del panel, que ya no es blanca. Lo
           canto el invariante A-3 en cuanto la galeria pinto la pildora en
           estado «bad», que el banco de pruebas nunca produce. */
        --bad-900: #5e111c;
        --bad-800: #a81c2e;
        --warn-800: #7a4b0a;
        --bad:   #c72339; --bad-bg:   rgba(199, 35, 57, .13);  --bad-line:   rgba(199, 35, 57, .30);
        /* --info es un escalón más oscuro que el azul de marca por lo mismo que
           --ok y --warn lo son desde P-12: se usa como TEXTO sobre --info-bg, que
           es ese mismo azul al 12 % sobre blanco (#e4eefb). Con --b-500 daba
           4,06 : 1 —la etiqueta «Sin fecha» del próximo movimiento, a 14 px y en
           negrita— y con --b-600 da 5,32. El tinte y la línea no cambian: siguen
           derivando del azul de marca, que es lo que se ve como color. */
        --info:  var(--b-600); --info-bg: rgba(31, 111, 224, .12); --info-line: rgba(31, 111, 224, .30);
        --muted: var(--n-500); --muted-bg: rgba(83, 102, 131, .12); --muted-line: rgba(83, 102, 131, .26);

        /* Superficies opacas para avisos persistentes. Los tintes --x-bg de arriba
           sirven para chips y badges, pero dejan pasar fotografías y degradados. */
        --alert-success-bg: #edf8f2;
        --alert-error-bg: #fff1f3;
        --alert-warning-bg: #fff8e8;
        --alert-info-bg: #eef5ff;

        /* Dorado asturiano: acento institucional, nunca un estado. Va aparte del
           ámbar de «atención» para que un dorado no se lea como una alerta.
           --gold es relleno o marca; --gold-ink es el único que vale como texto. */
        --gold: #f0b319;
        /* #8d6809 daba 4,32:1 al componerse sobre --gold-bg. */
        --gold-ink: #765600;
        --gold-bg: rgba(240, 179, 25, .16);
        --gold-line: rgba(240, 179, 25, .38);

        /* Categóricos: series de gráfica, circunscripciones y roles. Separados en
           tono y con luminosidad pareja para que ninguna serie pese más que otra. */
        --c-blue: var(--b-500);   --c-blue-bg: rgba(31, 111, 224, .14);
        --c-teal: #20857f;        --c-teal-bg: rgba(32, 133, 127, .14);
        --c-green: #25935a;       --c-green-bg: rgba(37, 147, 90, .14);
        --c-amber: #bc7a1a;       --c-amber-bg: rgba(188, 122, 26, .15);
        --c-rose: #c62f45;        --c-rose-bg: rgba(198, 47, 69, .14);
        --c-violet: #814cbd;      --c-violet-bg: rgba(129, 76, 189, .15);
        /* Violeta oscurecido hasta ser legible como TEXTO sobre su propio tinte;
           --c-violet es relleno de serie y no llega al mínimo AA. Mismo criterio
           que --gold / --gold-ink. */
        --c-violet-ink: #6a3a9e;  --c-violet-line: rgba(129, 76, 189, .3);

        /* Radios y sombras

           La escala de radio es PRIMITIVA: no se retematiza. La generación
           anterior daba a --r/--r-sm/--r-xs un valor aquí (20/14/10) y otro
           dentro de body.app-body (12/9/7), así que el mismo token valía
           distinto según el layout. Esa es la razón de fondo por la que había
           286 border-radius escritos a mano y solo 38 con token: sustituir un
           literal por el token le cambiaba el valor, de modo que no se podía.

           El único consumidor real de la generación de 20px eran las páginas de
           error (body.error-body), que ahora comparten el radio de la
           aplicación. auth.css no usaba ningún var(--r*). */
        --r-2xs: 4px;  --r-xs: 7px;   --r-sm: 9px;
        --r: 12px;     --r-lg: 16px;  --r-xl: 22px;
        --r-pill: 999px;              --r-round: 50%;

        /* ---------- Altura de control ----------
           E-c. Habia tokens de tipo, color y radio, y ninguno de altura: cada
           componente elegia la suya. Medido con el invariante D-1 sobre las 32
           pantallas, los controles computaban DIECIOCHO alturas distintas, y en
           una sola pantalla convivian un boton de 37 px con tres campos de 38,
           46 y 48. Nadie decidio eso; se acumulo.

           40 y 32 estan en la rejilla de 4 px y quedan cerca de lo que ya habia,
           asi que el contrato ordena sin rediseñar. En tactil no mandan estos
           dos: manda el suelo de 44 px de la capa `accesibilidad`, y esa
           diferencia de densidad entre raton y dedo es deliberada. */
        --control-h:    40px;
        --control-h-sm: 32px;
        --shadow: 0 12px 34px -12px rgba(22, 60, 110, .28), 0 3px 10px -4px rgba(22, 60, 110, .12);
        --shadow-sm: 0 4px 14px -8px rgba(22, 60, 110, .22);
        --shadow-lg: 0 28px 64px -24px rgba(22, 60, 110, .38), 0 8px 20px -10px rgba(22, 60, 110, .18);
        --ring: 0 0 0 3px rgba(31, 111, 224, .22);

        --sans: "PP Neue Montreal", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;

        /* ---------- Pesos ----------
           Los tres únicos que la familia dibuja en estilo normal. Antes había
           quince valores distintos (550, 580, 600, 650, 720, 750, 760, 780,
           800, 850, 900, 950…): nadie los eligió, salieron de ajustar a ojo
           sobre una fuente variable del sistema. Con Neue Montreal los ciento
           sesenta y tres que pedían un peso inexistente caían todos en el
           mismo Bold, así que la jerarquía se aplanaba sin avisar.
           --fw-medium es el énfasis de etiquetas, navegación y cabeceras de
           tabla; --fw-bold el de títulos, cifras y alertas. */
        --fw-regular: 400;
        --fw-medium: 500;
        --fw-bold: 700;

        /* ---------- Tracking ----------
           Por función, no por peldaño de la escala. Neue Montreal no tiene eje
           de anchura, así que el aire entre letras es lo único que tensa un
           título; pero junta las letras, NO estrecha los glifos: no es una
           condensed y no debe usarse como si lo fuera.
           Pasado cierto punto empieza a comerse los diacríticos del castellano
           y el usuario objetivo ronda los 55 años.

           El tracking es óptico: cuanto mayor es el tamaño, más sobra el aire
           que la fuente reserva para leerse pequeña. Por eso son cuatro
           peldaños y no un valor único, y por eso el tope general de -.02em se
           levanta a -.035em solo a partir de 40px.

           --tracking-label va en positivo: las mayúsculas pequeñas necesitan
           aire, no menos. */
        --tracking-hero: -.035em;     /* 40px y más: portadas, cifras a sangre */
        --tracking-display: -.02em;   /* 28-34px: cifras de KPI, títulos de portada */
        --tracking-heading: -.01em;   /* 20-24px: títulos de página y de sección */
        --tracking-body: 0;           /* cuerpo y todo lo que baje de 20px */
        --tracking-label: .06em;      /* antetítulos y rótulos en mayúsculas */

        /* ---------- Escala tipográfica ----------
           Antes había 68 tamaños distintos, muchos de ellos residuos de una
           conversión mecánica de rem a px: 10px, 11px, 13px, 16px…
           Nadie eligió esos valores, salieron de una división. Estos peldaños
           los sustituyen. Cambiar la densidad de toda la aplicación es tocar
           este bloque.

           SUELO SUBIDO EL 25/08/2026 (Q-2). La escala estaba impecablemente
           sistematizada alrededor del número equivocado: 10px en rótulos, 11 en
           metadatos, 13 en celdas y 15 de cuerpo, para una población que ronda
           los 55 años y lee desde el móvil, a veces dentro de un coche.

           Toda la rampa sube un peldaño y el de 10px desaparece: no hay ningún
           texto funcional que deba leerse a 10px, y mientras el token existiera
           alguien lo iba a usar. Sus 25 usos pasaron a --fs-xs.

           --fs-xl sube de 17 a 18 para conservar su distancia con el cuerpo: si
           se quedaba en 17 con el cuerpo en 16, un título de panel dejaba de
           leerse como título.

           NOTA sobre el nombre: el cuerpo del documento es --fs-lg, no
           --fs-base (ver la regla `body` más abajo). Los nombres vienen de la
           escala anterior y mienten un poco; renombrarlos tocaría 400
           declaraciones para no cambiar nada visible, así que se quedan. */
        --fs-xs:   12px;   /* antetítulos, metadatos, códigos de tabla */
        --fs-sm:   13px;   /* etiquetas de formulario, badges */
        --fs-md:   14px;   /* texto secundario, celdas de tabla */
        --fs-base: 15px;   /* navegación, controles */
        --fs-lg:   16px;   /* CUERPO del documento */
        --fs-xl:   18px;   /* títulos de panel */
        --fs-2xl:  20px;   /* títulos de sección */
        --fs-3xl:  24px;   /* título de página */
        --fs-4xl:  28px;   /* cifras de KPI */
        --fs-5xl:  34px;   /* cifras heroicas */

        /* ---------- Escala de espaciado ----------
           Rejilla de 4px con dos medios peldaños abajo, donde el ajuste fino sí se
           nota (separación icono-texto, altura de badge). Antes convivían 35 valores
           sueltos —1, 2, 3, 5, 7, 9, 11, 13, 15, 17, 21, 26, 34, 42, 59, 62px…— sin
           ninguna relación entre ellos. */
        --sp-0:  0;
        --sp-1:  2px;
        --sp-2:  4px;
        --sp-3:  6px;
        --sp-4:  8px;
        --sp-5:  12px;
        --sp-6:  16px;
        --sp-7:  20px;
        --sp-8:  24px;
        --sp-9:  32px;
        --sp-10: 40px;
        --sp-11: 48px;
        --sp-12: 64px;

        /* ---------- Puntos de ruptura ----------
           NO hay escala canónica. Se intentó consolidar los 27 valores en seis y
           se revirtió: redondear hacia arriba no es conservador (620→760 activa el
           diseño estrecho 140px de más) y no hay cobertura de regresión visual con
           la que verificarlo. bin/test_css_normalizado.php congela el inventario:
           no admite valores nuevos, pero no impone ninguna escala. La consolidación
           real se hará por componente. Ver docs/SISTEMA-DE-DISENO.md.

           Una custom property NO puede parametrizar una condición @media
           (`@media (max-width: var(--bp))` no funciona sin preprocesado). Para
           adaptar un componente a su contenedor, y no a la ventana, se usa
           @container, que no necesita punto de ruptura global. */

        --content-max: 2040px;

        /* Motion (ui-animation) — tokens centralizados */
        --ease: ease;
        --ease-enter: cubic-bezier(.22, 1, .36, 1);
        --ease-move: cubic-bezier(.25, 1, .5, 1);
        --dur-1: .12s;
        --dur-2: .2s;
        --dur-3: .28s;
    }

    body {
        font-family: var(--sans);
        color: var(--ink);
        font-size: var(--fs-lg);
        line-height: 1.5;
        background-color: var(--app-bg);
        background-image:
            radial-gradient(900px 620px at 82% -8%, rgba(255, 255, 255, .14) 0%, rgba(255, 255, 255, 0) 55%),
            linear-gradient(150deg, var(--app-bg-1) 0%, var(--app-bg-2) 52%, var(--app-bg-3) 100%);
        background-repeat: no-repeat;
        background-size: cover;
        background-position: center;
        background-attachment: fixed;
    }

    /* ---------- App shell ---------- */
    .app-shell { display: flex; min-height: 100vh; }

    /* ---------- Llegada crítica desde el acceso ----------
       Vive en app.css porque esta hoja ya está cargada y parseada antes de
       enviar el login. De ese modo el primer frame del documento privado no
       depende de alcanzar un bloque tardío de campaign.css. */
    .login-arrival {
        position: fixed;
        inset: 0;
        z-index: 9999;
        isolation: isolate;
        display: grid;
        place-items: center;
        overflow: hidden;
        pointer-events: none;
        opacity: 1;
        animation: login-arrival-release .4s var(--ease) .1s both;
    }
    .login-arrival.is-preview-held,
    .login-arrival.is-preview-held .login-arrival__scene { animation-play-state: paused; }
    .login-arrival__veil {
        position: absolute;
        inset: 0;
        z-index: -1;
        background:
            radial-gradient(circle at 50% 47%, rgba(93, 190, 255, .24), transparent 33%),
            linear-gradient(145deg, rgba(2, 16, 44, .68), rgba(4, 55, 119, .55) 54%, rgba(12, 95, 162, .45));
        -webkit-backdrop-filter: blur(14px) saturate(118%) brightness(.94);
        backdrop-filter: blur(14px) saturate(118%) brightness(.94);
    }
    .login-arrival__scene {
        position: relative;
        display: grid;
        place-items: center;
        width: 188px;
        height: 188px;
        transform-origin: center;
        animation: login-arrival-scene .4s var(--ease) .1s both;
    }
    .login-arrival__lens {
        position: relative;
        z-index: 2;
        display: grid;
        place-items: center;
        width: 164px;
        height: 164px;
        border: 1px solid rgba(226, 246, 255, .38);
        border-radius: var(--r-round);
        background: rgba(6, 63, 143, .34);
        -webkit-backdrop-filter: blur(10px) saturate(118%);
        backdrop-filter: blur(10px) saturate(118%);
        box-shadow: inset 0 1px 0 rgba(255, 255, 255, .16), 0 24px 70px -30px rgba(44, 163, 255, .86);
    }
    .login-arrival__lens img {
        display: block;
        width: 76%;
        height: auto;
        filter: drop-shadow(0 3px 7px rgba(2, 26, 63, .22));
    }
    .login-arrival__orbit {
        position: absolute;
        border: 1px solid rgba(161, 218, 255, .24);
        border-radius: var(--r-round);
    }
    .login-arrival__orbit--outer { inset: -28px; opacity: .44; }
    .login-arrival__orbit--inner { inset: -9px; opacity: .7; }
    .login-arrival__caption {
        position: absolute;
        top: calc(100% + 24px);
        width: max-content;
        color: rgba(232, 247, 255, .88);
        font-size: var(--fs-sm);
        font-weight: 500;
        letter-spacing: .045em;
        text-shadow: 0 2px 12px rgba(2, 26, 63, .28);
    }
    @keyframes login-arrival-release {
        from { opacity: 1; }
        to { opacity: 0; }
    }
    @keyframes login-arrival-scene {
        from { transform: scale(1); }
        to { transform: scale(.965); }
    }
    @supports not ((-webkit-backdrop-filter: blur(2px)) or (backdrop-filter: blur(2px))) {
        .login-arrival__veil { background: linear-gradient(145deg, rgba(2, 13, 35, .97), rgba(3, 50, 111, .95) 52%, rgba(7, 82, 153, .93)); }
        .login-arrival__lens { background: rgb(7, 70, 150); }
    }
    @media (max-width: 820px) {
        .login-arrival__veil {
            -webkit-backdrop-filter: blur(9px) saturate(110%);
            backdrop-filter: blur(9px) saturate(110%);
        }
        .login-arrival__scene { width: 154px; height: 154px; }
        .login-arrival__lens { width: 138px; height: 138px; }
        .login-arrival__orbit--outer { inset: -22px; }
        .login-arrival__orbit--inner { inset: -7px; }
        .login-arrival__caption { top: calc(100% + 20px); }
    }
    @media (prefers-reduced-transparency: reduce) {
        .login-arrival__veil {
            background: linear-gradient(145deg, rgb(2, 13, 35), rgb(3, 50, 111) 52%, rgb(7, 82, 153));
            -webkit-backdrop-filter: none;
            backdrop-filter: none;
        }
        .login-arrival__lens {
            background: rgb(7, 70, 150);
            -webkit-backdrop-filter: none;
            backdrop-filter: none;
        }
    }
    @media (prefers-reduced-motion: reduce) {
        .login-arrival { animation: login-arrival-release .01ms linear forwards; }
        .login-arrival__scene { animation: none; }
    }

    .main-content { flex: 1; display: flex; flex-direction: column; min-width: 0; overflow-x: clip; }

    /* ---------- Topbar (controles flotantes glass) ---------- */
    .topbar { display: grid; grid-template-columns: 1fr minmax(0, 520px) 1fr; align-items: center; gap: var(--sp-7); padding: var(--sp-5) var(--sp-9); width: 100%; max-width: var(--content-max); margin-inline: auto; }
    .topbar__menu { display: none; }
    .topbar__search { flex: 1; max-width: 520px; margin: 0; display: flex; align-items: center; gap: var(--sp-5); padding: 0 var(--sp-6); height: 46px; border-radius: var(--r); background: var(--surface); -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur); border: 1px solid var(--surface-border); box-shadow: var(--shadow-sm); }
    .search-bar { display: flex; gap: var(--sp-4); margin-bottom: 24px; max-width: 640px; }
    .search-bar input { flex: 1; min-width: 0; padding: 0 var(--sp-6); height: 46px; border-radius: var(--r); border: 1px solid var(--surface-border); background: var(--surface-raised); font-size: var(--fs-base); outline: none; }
    .search-bar input:focus { box-shadow: var(--ring); border-color: var(--brand); }
    .topbar__search svg { width: 18px; height: 18px; color: var(--ink-3); flex-shrink: 0; }
    .topbar__search input { border: 0; background: transparent; flex: 1; min-width: 0; outline: none; font-size: var(--fs-lg); text-overflow: ellipsis; }
    .topbar__search input::placeholder { color: var(--ink-3); }
    .topbar__kbd { font-size: var(--fs-xs); color: var(--ink-3); border: 1px solid var(--line); border-radius: var(--r-xs); padding: var(--sp-1) var(--sp-4); font-weight: 500; }
    .topbar__bell { position: relative; width: 44px; height: 44px; border-radius: var(--r); background: var(--surface); -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur); border: 1px solid var(--surface-border); display: grid; place-items: center; color: var(--ink-2); text-decoration: none; box-shadow: var(--shadow-sm); }
    .topbar__bell svg { width: 19px; height: 19px; }
    .avatar { width: 34px; height: 34px; border-radius: var(--r-round); background: var(--brand-grad); color: #fff; display: grid; place-items: center; font-size: var(--fs-base); font-weight: 700; flex-shrink: 0; }
    .topbar .btn--ghost { color: var(--on-blue); border-color: rgba(255, 255, 255, .35); }
    .topbar .btn--ghost:hover { background: rgba(255, 255, 255, .14); color: #fff; }

    .page { padding: var(--sp-5) var(--sp-8) var(--sp-9); max-width: var(--content-max); width: 100%; margin-inline: auto; }

    /* ---------- Botones ---------- */
    .btn { display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-4); padding: var(--sp-4) var(--sp-6); border-radius: var(--r); border: 1px solid transparent; font-size: var(--fs-base); font-weight: 500; text-decoration: none; line-height: 1; white-space: nowrap; transition: filter .15s, background .15s, border-color .15s, box-shadow .15s, transform .05s; }
    .btn:active { transform: translateY(1px); }
    /* A-51. `pointer-events: none` hacia mas de lo que decia. Un boton
       deshabilitado ya no responde al clic por si mismo —el navegador lo
       impide—, asi que esto solo servia para dos cosas: impedir que se viera
       el `title` que explica POR QUE esta deshabilitado, y quitarle el cursor
       que lo distingue. Justo la ayuda que necesita quien se lo encuentra.

       `cursor: not-allowed` dice que no se puede pulsar; el `title` dice por
       que. El clic lo sigue impidiendo el atributo `disabled`, que es de quien
       era el trabajo. Se conserva en `.is-loading`, que NO es un boton
       deshabilitado sino uno que ya se pulso: ahi si hay que parar el segundo
       clic, porque el elemento sigue habilitado. */
    .btn:disabled { opacity: .55; cursor: not-allowed; }
    .btn.is-loading { opacity: .55; pointer-events: none; }
    .btn--primary { background: var(--brand-grad); color: #fff; border-color: rgba(255, 255, 255, .28); box-shadow: var(--shadow); }
    .btn--primary:hover { filter: brightness(1.05); }
    .btn--secondary { background: var(--surface-raised); color: var(--ink); border-color: var(--surface-border); -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur); }
    .btn--secondary:hover { background: #fff; }
    .btn--ghost { background: transparent; color: var(--ink-2); }
    .btn--ghost:hover { background: rgba(255, 255, 255, .5); color: var(--ink); }
    .btn--danger { background: var(--bad); color: #fff; border-color: var(--bad-line); box-shadow: 0 8px 20px -10px rgba(199, 35, 57, .6); }
    .btn--danger:hover { filter: brightness(1.05); }
    /* N-17. `justify-content` venia de una segunda definicion en campaign.css
       que ganaba por orden de carga. Las dos declaraciones del mismo modificador
       viven ahora donde vive el boton. */
    .btn--block { width: 100%; justify-content: center; }
    /* Formulario que solo envuelve un botón (p. ej. eliminar): el botón participa
       directamente en el layout flex del contenedor de acciones. */
    .inline-form { display: inline-flex; margin: 0; }
    .btn--sm { padding: var(--sp-4) var(--sp-5); font-size: var(--fs-md); border-radius: var(--r-sm); }
    .btn:focus-visible { outline: none; box-shadow: var(--ring); }

    /* ---------- Formularios ---------- */
    .form-field { display: flex; flex-direction: column; gap: var(--sp-3); margin-bottom: 16px; }
    /* El rotulo de un GRUPO de controles. No puede ser un <label>: una
       etiqueta etiqueta un control, y estos tres —«Horario por dia», «Cargos
       que asisten»— nombran un conjunto. El contenedor los declara con
       role="group" y aria-labelledby; aqui solo se les da el mismo aspecto
       para que el formulario siga leyendose igual. */
    .form-field label,
    .form-field .form-field__rotulo { font-size: var(--fs-md); font-weight: 500; color: var(--ink-2); }
    .form-field .form-field__rotulo { display: block; }
    .form-field input, .form-field select, .form-field textarea { padding: var(--sp-4) var(--sp-5); border: 1px solid var(--line); border-radius: var(--r); background: rgba(255, 255, 255, .7); width: 100%; color: var(--ink); transition: border-color .15s, box-shadow .15s, background .15s; }
    .form-field input:focus, .form-field select:focus, .form-field textarea:focus,
    .comment-form textarea:focus { outline: none; border-color: var(--brand); background: #fff; box-shadow: var(--ring); }
    .form-section { background: var(--surface); -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur); border: 1px solid var(--surface-border); border-radius: var(--r); padding: var(--sp-8); margin-bottom: 20px; box-shadow: var(--shadow); }
    /* Cubre <legend> y <h3>: los fieldset nombran su sección con legend (que es
       lo correcto y no entra en el orden de encabezados) y los .form-section que
       son <div> siguen usando h3. :is() no cambia la especificidad. */
    .form-section > :is(legend, h3) { font-size: var(--fs-base); margin-bottom: 16px; color: var(--ink); display: flex; align-items: center; gap: var(--sp-4); padding: 0; float: none; }
    .form-section > :is(legend, h3)::before { content: ""; width: 3px; height: 15px; border-radius: var(--r-2xs); background: var(--brand); }
    /* Edición parcial por rol: secciones en solo lectura (fieldset deshabilitado) */
    fieldset.form-section { min-width: 0; }
    .form-section--ro { background: var(--surface-raised); }
    .form-section input:disabled, .form-section select:disabled, .form-section textarea:disabled { background: var(--muted-bg); color: var(--ink-2); -webkit-text-fill-color: var(--ink-2); cursor: not-allowed; opacity: 1; }
    .form-section--ro .check-line { color: var(--ink-3); cursor: not-allowed; }
    .ro-tag { margin-left: auto; font-size: var(--fs-xs); font-weight: 700; text-transform: uppercase; letter-spacing: var(--tracking-label); color: var(--ink-3); background: var(--muted-bg); border: 1px solid var(--line); border-radius: var(--r-xs); padding: var(--sp-1) var(--sp-4); }
    .form-note { background: var(--brand-soft); border: 1px solid var(--line); color: var(--ink-2); border-radius: var(--r-sm); padding: var(--sp-5) var(--sp-5); font-size: var(--fs-md); line-height: 1.45; margin-bottom: 16px; }
    /* Bloque opcional plegable dentro de un formulario (nativo <details>, sin JS). */
    .form-foldout { border: 1px solid var(--line); border-radius: var(--r-sm); padding: 0 var(--sp-5); margin: 4px 0 2px; background: var(--brand-soft); }
    .form-foldout > summary { cursor: pointer; padding: var(--sp-5) var(--sp-1); font-weight: 700; font-size: var(--fs-base); color: var(--ink-2); list-style: none; }
    .form-foldout > summary::-webkit-details-marker { display: none; }
    .form-foldout > summary::before { content: '\25B8'; margin-right: 8px; color: var(--ink-3); }
    .form-foldout[open] > summary::before { content: '\25BE'; }
    .form-foldout[open] > summary { margin-bottom: 4px; }
    .form-foldout > *:last-child { margin-bottom: 12px; }

    /* ---------- Paneles (cristal) ---------- */
    .panel { background: var(--surface); -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur); border: 1px solid var(--surface-border); border-radius: var(--r); padding: var(--sp-7) var(--sp-8); box-shadow: var(--shadow); margin-bottom: 20px; }
    /* N-32. Panel cuyo unico contenido es explicar que todavia no toca. Se le
       quita el aire de un panel con datos: mantener la altura completa seria
       decir con el espacio que ahi hay algo importante. */
    .panel--plegado { padding-top: var(--sp-5); padding-bottom: var(--sp-5); }
    .panel--plegado .empty-state { padding: 0; }
    .panel > p { margin-bottom: 10px; } .panel > p:last-child { margin-bottom: 0; }
    .panel--info { position: relative; padding-left: var(--sp-9); }

    /* ---------- Tablas ---------- */
    .data-table { width: 100%; border-collapse: collapse; font-size: var(--fs-base); }
    .data-table th, .data-table td { text-align: left; padding: var(--sp-4) var(--sp-5); border-bottom: 1px solid var(--line); }
    .data-table th { font-weight: 500; color: var(--ink-3); font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: var(--tracking-label); }
    .data-table tbody tr:last-child td { border-bottom: 0; }
    .data-table tbody tr { transition: background .12s; }
    .data-table tbody tr:hover { background: rgba(255, 255, 255, .45); }
    .data-table td:last-child { text-align: right; }

    /* ---------- Badges ---------- */
    .badge { display: inline-flex; align-items: center; gap: var(--sp-3); padding: var(--sp-2) var(--sp-5); border-radius: var(--r-pill); font-size: var(--fs-sm); font-weight: 700; line-height: 1.5; border: 1px solid transparent; }
    .badge--success { background: var(--c-green-bg); color: var(--ok); border-color: rgba(22, 124, 69, .42); }
    .badge--warning { background: var(--gold-bg); color: var(--warn-800); border-color: rgba(155, 96, 13, .5); }
    .badge--danger  { background: var(--bad-bg); color: var(--bad); border-color: rgba(199, 35, 57, .44); }
    .badge--info    { background: var(--b-200); color: var(--b-700); border-color: rgba(31, 111, 224, .44); }
    .badge--muted   { background: var(--n-200); color: var(--n-600); border-color: rgba(97, 119, 152, .48); }

    /* ---------- Aviso flotante (N-10) ---------- */
    /* Había DOS componentes para esto —`.qa-toast` en campaign.css y
       `.cal-toast` en calendar.css— con dos aspectos, dos duraciones y dos
       implementaciones. Ahora hay uno, y vive aquí porque lo usan dos módulos:
       el sitio de un componente compartido es el armazón, no una de las dos
       hojas que lo comparten. Es la deuda que describe N-17.

       Se conserva la variante de tono del calendario y se descarta el fondo
       oscuro de `.qa-toast`: un aviso que no distingue un acierto de un fallo
       obliga a leerlo entero para saber si algo fue mal. */
    .toast {
        position: fixed;
        left: 50%;
        bottom: 22px;
        transform: translateX(-50%);
        z-index: 300;
        max-width: min(480px, calc(100vw - 40px));
        padding: var(--sp-4) var(--sp-7);
        border: 1px solid var(--line);
        border-radius: var(--r);
        background: #fff;
        color: var(--ink);
        font-size: var(--fs-md);
        font-weight: 700;
        text-align: center;
        box-shadow: var(--shadow-lg);
        /* Un acierto no se toca: se lee y se va. Un fallo sí, para cerrarlo. */
        pointer-events: none;
    }
    .toast--ok { background: var(--c-green-bg); border-color: rgba(22, 124, 69, .4); color: var(--ok); }
    .toast--error { background: var(--bad-bg); border-color: rgba(199, 35, 57, .44); color: var(--bad); pointer-events: auto; cursor: pointer; }

    /* ---------- Alertas / flash ---------- */
    /* P-10. El boton de deshacer va pegado al final del aviso, antes de la
       equis de cerrar: se lee despues de saber que ha pasado, que es cuando
       uno decide si quiere revertirlo. */
    .alert__accion { margin-left: auto; flex-shrink: 0; }
    .alert__accion .btn { white-space: nowrap; }

    .alert { position: relative; isolation: isolate; display: flex; align-items: center; gap: var(--sp-4); padding: var(--sp-5) var(--sp-6) var(--sp-5) var(--sp-8); border-radius: var(--r); border: 1px solid var(--line); margin-bottom: 10px; font-size: var(--fs-base); font-weight: 500; background: #fff; box-shadow: var(--shadow); --accent: var(--muted); }
    /* Barra vertical interior del color del estado (separada del canto por padding). */
    .alert--success { background: var(--alert-success-bg); border-color: rgba(22, 124, 69, .4); --accent: var(--ok); color: var(--ok); }
    .alert--error   { background: var(--alert-error-bg); border-color: rgba(199, 35, 57, .42); --accent: var(--bad); color: var(--bad); }
    .alert--warning { background: var(--alert-warning-bg); border-color: rgba(155, 96, 13, .45); --accent: var(--warn); color: var(--warn); }
    .alert--info    { background: var(--alert-info-bg); border-color: rgba(31, 111, 224, .4); --accent: var(--b-500); color: var(--b-700); }
    .alert__message { flex: 1 1 auto; min-width: 0; line-height: 1.45; }
    .alert__close { display: grid; flex: 0 0 auto; place-items: center; width: 28px; height: 28px; margin: -4px -6px -4px 0; padding: 0; border: 0; border-radius: var(--r-xs); background: transparent; font-size: var(--fs-xl); line-height: 1; color: inherit; opacity: .6; }
    .alert__close:hover { opacity: 1; }
    .alert.is-fading { opacity: 0; transition: opacity .4s; }

    /* ---------- Cabecera de página (páginas internas) ---------- */
    .page-header { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-6); margin-bottom: 20px; }
    .page-header__title { font-size: var(--fs-3xl); font-weight: 700; color: #fff; letter-spacing: var(--tracking-heading); text-shadow: 0 1px 3px rgba(8, 28, 66, .42); }
    .page-header__subtitle { color: var(--on-blue-2); font-size: var(--fs-base); margin-top: 6px; display: flex; flex-wrap: wrap; gap: var(--sp-4); align-items: center; }
    .page-header__actions { display: flex; gap: var(--sp-4); flex-shrink: 0; align-items: center; }

    /* Cabecera compacta: el Resumen operativo arranca en los indicadores y esa
       densidad es deliberada. Pero era la única pantalla sin título visible, y
       en móvil —donde la miga de pan está oculta— eso la dejaba sin nombre
       ninguno (P-17). Con la variante compacta dice cómo se llama sin robarle
       sitio a los datos. */
    .page-header--compacta { margin-bottom: var(--sp-5); }
    .page-header--compacta .page-header__title { font-size: var(--fs-2xl); }

    /* ---------- Páginas de error / acceso comparten el cielo ---------- */
    .error-body { display: grid; place-items: center; min-height: 100vh; padding: var(--sp-8); }
    .error-card { background: var(--surface-raised); -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur); border: 1px solid var(--surface-border); border-radius: var(--r); padding: var(--sp-11); max-width: 460px; text-align: center; box-shadow: var(--shadow); }
    .error-card__logo { width: 150px; height: auto; margin: 0 auto 24px; }
    .error-code { font-size: 46px; font-weight: 700; color: var(--brand-texto); letter-spacing: var(--tracking-display); }
    .error-card h1 { font-size: var(--fs-2xl); margin: 6px 0; }
    .error-card p { color: var(--ink-2); margin-bottom: 24px; }
    .error-debug { text-align: left; background: rgba(255,255,255,.6); border: 1px solid var(--line); border-radius: var(--r); padding: var(--sp-5); font-size: var(--fs-sm); overflow: auto; margin-bottom: 16px; white-space: pre-wrap; max-height: 340px; }
    .error-ref { font-size: var(--fs-md); }
    .error-ref code { background: rgba(255,255,255,.6); border: 1px solid var(--line); border-radius: var(--r-xs); padding: var(--sp-1) var(--sp-3); font-size: var(--fs-sm); }

    /* ---------- Animación de entrada ----------
       Era una entrada escalonada: 0,33s de retardo máximo más 0,55s de duración,
       así que el contenido no terminaba de asentarse hasta pasados ~0,9s. En una
       aplicación de recarga completa que se abre decenas de veces al día, eso no
       se percibe como calidad sino como lentitud, y se paga en cada navegación.
       Se conserva la aparición, pero corta y sin escalonar: la página se lee
       entera desde el primer fotograma.

       N-19. Este comentario decía que las clases .anim-1…6 «siguen existiendo
       para no tener que tocar las vistas». No existían: se habían retirado al
       quitar el escalonado, y las vistas seguían emitiéndolas. Ocho atributos
       que no hacían nada, y un comentario que afirmaba lo contrario, que es
       peor: el siguiente en pasar por aquí se lo habría creído. Retiradas de
       dashboard/index.php y my_day/index.php. */
    @keyframes rise { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
    @media (prefers-reduced-motion: no-preference) {
        .anim { animation: rise .18s var(--ease-enter) both; }
    }

    /* ---------- Backdrop sidebar (móvil) ---------- */
    .sidebar-backdrop { display: none; }

    /* ---------- Responsive ----------
       El menú se oculta fuera de pantalla y lo devuelve el script. Para quien no
       tenga JavaScript, el layout imprime un bloque <noscript> con la navegación
       completa y la salida de sesión: no se puede condicionar el CSS a la presencia
       de JS porque la CSP de la aplicación (script-src 'self') prohíbe los scripts
       en línea, que es lo único que llegaría a tiempo de evitar el parpadeo. */
    @media (max-width: 1180px) {
        body { background-attachment: scroll; }
        .sidebar { position: fixed; left: 0; top: 0; z-index: 40; height: 100dvh; overflow-y: auto; overscroll-behavior: contain; transform: translateX(-100%); transition: transform .22s ease; }
        body.sidebar-open .sidebar { transform: translateX(0); }
        .topbar__menu { display: inline-flex; align-items: center; justify-content: center; }
        .sidebar-backdrop { border: 0; padding: 0; }
        body.sidebar-open .sidebar-backdrop { display: block; position: fixed; inset: 0; z-index: 35; background: rgba(15, 40, 70, .4); }
        .page { padding: var(--sp-4) var(--sp-6) var(--sp-8); }
        .topbar { padding: var(--sp-5) var(--sp-6) var(--sp-2); }
        .page-header { flex-direction: column; }
    }

    /* =====================================================================
       UX refresh 2026
       Superficie privada mas calmada y operativa: marca en navegacion,
       contenido sobre fondo claro, paneles solidos y tablas legibles en movil.
       ===================================================================== */
    html { overflow-x: hidden; }

    body.app-body {
        --surface: #fff;
        --surface-2: var(--b-50);
        --surface-raised: #fff;
        --surface-border: var(--c-blue-bg);
        --line: var(--b-100);
        --blur: none;
        /* El radio ya no se retematiza: la escala vive completa en :root. */
        --shadow: 0 18px 40px -28px rgba(18, 54, 103, .34), 0 1px 0 rgba(18, 54, 103, .05);
        --shadow-sm: 0 10px 24px -20px rgba(18, 54, 103, .28), 0 1px 0 rgba(18, 54, 103, .04);
        background-color: var(--b-50);
        background-image: linear-gradient(180deg, var(--b-50) 0%, var(--n-50) 360px, var(--b-50) 100%);
        background-attachment: scroll;
    }

    body.app-body .app-shell { background: transparent; }

    body.app-body .topbar {
        position: sticky;
        top: 0;
        z-index: 20;
        max-width: none;
        padding: var(--sp-6) var(--sp-9);
        background: rgba(243, 246, 251, .82);
        -webkit-backdrop-filter: saturate(180%) blur(14px);
        backdrop-filter: saturate(180%) blur(14px);
        border-bottom: 1px solid rgba(217, 228, 242, .8);
    }

    body.app-body .topbar__search,
    body.app-body .icon-btn,
    body.app-body .topbar__menu {
        background: #fff;
        border-color: var(--surface-border);
        box-shadow: var(--shadow-sm);
    }

    body.app-body .topbar .btn--ghost {
        color: var(--ink-2);
        border-color: var(--surface-border);
    }

    body.app-body .topbar .btn--ghost:hover {
        background: var(--surface-2);
        color: var(--ink);
    }

    body.app-body .page {
        padding: var(--sp-8) var(--sp-8) var(--sp-10);
    }

    body.app-body .page-header {
        align-items: center;
        margin-bottom: 20px;
    }

    body.app-body .page-header__title {
        color: var(--ink);
        text-shadow: none;
        /* El retematizado anulaba aquí el tracking (letter-spacing: 0). Tenía
           sentido con la fuente del sistema; con Neue Montreal deja sin tensión
           el título de página en el único tema que hoy se pinta. */
        letter-spacing: var(--tracking-heading);
    }

    body.app-body .page-header__subtitle,
    body.app-body .back-link {
        color: var(--ink-2);
        text-shadow: none;
    }

    body.app-body .back-link:hover { color: var(--brand-texto); }

    body.app-body .panel,
    body.app-body .form-section,
    body.app-body .filters-bar {
        background: #fff;
        border-color: var(--surface-border);
        box-shadow: var(--shadow);
    }

    body.app-body .panel,
    body.app-body .form-section { padding: var(--sp-7) var(--sp-7); }

    body.app-body .panel__head {
        margin-bottom: 14px;
        padding-bottom: var(--sp-4);
        border-bottom: 1px solid var(--line);
    }

    body.app-body .panel__title {
        font-size: var(--fs-base);
        letter-spacing: 0;
    }

    body.app-body .btn {
        border-radius: var(--r-sm);
        min-height: 36px;
    }

    body.app-body .btn--primary {
        background: linear-gradient(145deg, var(--b-500) 0%, var(--b-600) 58%, var(--b-700) 100%);
        box-shadow: 0 12px 22px -16px rgba(26, 94, 188, .75), inset 0 1px 0 rgba(255, 255, 255, .24);
    }

    @media (hover: hover) and (pointer: fine) {
        body.app-body .btn--primary:hover {
            filter: none;
            background: linear-gradient(145deg, var(--b-400) 0%, var(--b-500) 56%, var(--b-600) 100%);
            border-color: rgba(255, 255, 255, .42);
            box-shadow: 0 16px 28px -16px rgba(15, 83, 176, .82), inset 0 1px 0 rgba(255, 255, 255, .3);
        }
    }

    body.app-body .btn--secondary {
        background: var(--n-50);
        border-color: var(--n-200);
    }

    body.app-body .btn--ghost:hover {
        background: var(--b-50);
    }

    /* El color de borde y el fondo salen de variables con valor por defecto, no de
       literales. Así el estado de error puede cambiarlos desde .form-field--error,
       que tiene menos especificidad que esta regla y de otro modo nunca ganaría.
       Es la misma técnica con la que --blur: none apaga los desenfoques: mover la
       decisión al token en lugar de pelear con el selector. */
    body.app-body .form-field input,
    body.app-body .form-field select,
    body.app-body .form-field textarea,
    body.app-body .inline-status select {
        background: var(--field-bg, #fff);
        border-color: var(--field-border, var(--n-200));
        border-radius: var(--r-sm);
    }

    body.app-body .form-field input:hover,
    body.app-body .form-field select:hover,
    body.app-body .form-field textarea:hover {
        border-color: var(--n-300);
    }

    body.app-body .data-table {
        font-size: var(--fs-md);
    }

    body.app-body .data-table th {
        color: var(--n-500);
        background: var(--b-50);
        border-bottom-color: var(--n-200);
    }

    body.app-body .data-table th:first-child { border-top-left-radius: var(--r-xs); }
    body.app-body .data-table th:last-child { border-top-right-radius: var(--r-xs); }

    body.app-body .data-table td {
        border-bottom-color: var(--line);
        vertical-align: top;
    }

    body.app-body .data-table tbody tr:hover {
        background: var(--n-50);
    }

    body.app-body .badge {
        border-radius: var(--r-xs);
        padding: var(--sp-2) var(--sp-4);
    }

    @media (max-width: 1000px) {
        body.app-body .topbar {
            padding: var(--sp-5) var(--sp-6);
        }

        body.app-body .page {
            padding: var(--sp-7) var(--sp-6) var(--sp-8);
        }

        body.app-body .page-header {
            align-items: stretch;
        }

        body.app-body .page-header__actions {
            flex-wrap: wrap;
        }
    }

    @media (max-width: 760px) {
        body.app-body .topbar {
            gap: var(--sp-4);
            grid-template-columns: auto 1fr auto;
        }

        body.app-body .topbar__search {
            max-width: none;
            min-width: 0;
            height: 40px;
        }

        body.app-body .topbar__kbd,
        body.app-body .breadcrumb {
            display: none;
        }

        body.app-body .icon-btn,
        body.app-body .topbar__menu,
        body.app-body .topbar__avatar {
            width: 40px;
            height: 40px;
            flex: 0 0 auto;
        }

        body.app-body .panel,
        body.app-body .form-section {
            padding: var(--sp-6);
        }

        body.app-body .table-scroll {
            overflow: visible;
        }

        body.app-body .data-table,
        body.app-body .data-table thead,
        body.app-body .data-table tbody,
        body.app-body .data-table tr,
        body.app-body .data-table th,
        body.app-body .data-table td {
            display: block;
            width: 100%;
        }

        body.app-body .data-table thead {
            position: absolute;
            width: 1px;
            height: 1px;
            padding: 0;
            margin: -1px;
            overflow: hidden;
            clip: rect(0, 0, 0, 0);
            white-space: nowrap;
            border: 0;
        }

        body.app-body .data-table tbody {
            display: grid;
            gap: var(--sp-4);
        }

        body.app-body .data-table tbody tr {
            background: #fff;
            border: 1px solid var(--surface-border);
            border-radius: var(--r);
            box-shadow: var(--shadow-sm);
            overflow: hidden;
        }

        body.app-body .data-table tbody tr:hover {
            background: #fff;
        }

        body.app-body .data-table td {
            display: grid;
            grid-template-columns: minmax(106px, 38%) minmax(0, 1fr);
            gap: var(--sp-5);
            align-items: start;
            padding: var(--sp-4) var(--sp-5);
            text-align: left !important;
            border-bottom: 1px solid var(--line);
        }

        body.app-body .data-table td:last-child {
            border-bottom: 0;
        }

        body.app-body .data-table td::before {
            content: attr(data-label);
            color: var(--ink-3);
            font-size: var(--fs-xs);
            font-weight: 700;
            text-transform: uppercase;
            letter-spacing: var(--tracking-label);
        }

        body.app-body .data-table td[data-label=""] {
            display: block;
        }

        body.app-body .data-table td[data-label=""]::before {
            content: none;
        }
    }

    @media (max-width: 520px) {
        .auth-body {
            display: flex;
            align-items: center;
            justify-content: flex-start;
            padding: var(--sp-6);
            overflow-x: hidden;
        }

        .auth-wrapper {
            width: min(358px, calc(100vw - 32px));
            min-width: 0;
            margin: 0;
        }

        .auth-card {
            width: 100%;
            padding: var(--sp-8) var(--sp-7);
            border-radius: var(--r-lg);
        }

        .auth-card__logo {
            width: min(180px, 72%);
        }
    }

    /* N-17. Aqui vivian `.auth-body`, `.auth-wrapper` y `.auth-card`, que ya
       estaban definidas en auth.css. Las dos hojas estan en @layer legacy, asi
       que dentro de la capa manda el orden de carga y auth.css —que va despues—
       ganaba: `width: min(410px, …)` y `max-width: none` de aqui no se aplicaban
       NUNCA. Es el mismo mecanismo que produjo P-04.

       Lo que si llegaba a aplicarse —`overflow-x`, `min-width`, el `width` de la
       tarjeta— se ha movido a auth.css, junto al componente. La pantalla de
       acceso no cambia un pixel: la linea base lo comprueba en las seis
       anchuras. */

    /* =====================================================================
       Topbar refinada — barra fina, búsqueda centrada, botones estandarizados
       ===================================================================== */
    .topbar__zone { display: flex; align-items: center; gap: var(--sp-4); min-width: 0; }
    .topbar__zone--end { justify-self: end; }

    /* ---------- Cuenta atrás electoral (chip en topbar) ----------
       El color y el pulso cambian con la fase: pre = info (azul), oficial = warn
       (ámbar), reflexión/jornada = danger (rojo, con pulso vivo). */
    .countdown { display: inline-flex; align-items: center; gap: var(--sp-4); padding: var(--sp-4) var(--sp-5) var(--sp-4) var(--sp-5);
        border-radius: var(--r-pill); border: 1px solid var(--line); background: #fff;
        box-shadow: var(--shadow-sm); color: var(--ink); font-size: var(--fs-md); font-weight: 700;
        line-height: 1.1; white-space: nowrap; }
    .countdown__ico { width: 15px; height: 15px; flex-shrink: 0; }
    .countdown__txt { font-weight: 700; letter-spacing: -.005em; }
    .countdown--info    { color: var(--brand-700);   border-color: rgba(31, 111, 224, .3);  background: linear-gradient(180deg, var(--b-100), #fff); }
    .countdown--warn    { color: var(--warn);              border-color: rgba(155, 96, 13, .42); background: linear-gradient(180deg, var(--c-amber-bg), #fff); }
    .countdown--danger  { color: var(--bad);              border-color: rgba(199, 35, 57, .42);  background: linear-gradient(180deg, var(--c-rose-bg), #fff); }
    .countdown--muted   { color: var(--ink-3); }
    @media (max-width: 1080px) {
        .countdown { padding: var(--sp-2) var(--sp-4) var(--sp-2) var(--sp-4); font-size: var(--fs-sm); }
    }
    @media (max-width: 920px) {
        /* En pantallas estrechas el chip se aprieta para no comerle sitio al
           buscador ni a la zona de iconos. Por debajo de 720px desaparece.

           Aquí había además una regla que insertaba attr(data-short) para mostrar
           solo «347 d». Ningún marcado emite ese atributo —ni la vista ni el JS—,
           así que insertaba una cadena vacía: prometía un comportamiento que no
           existía. Si algún día se quiere la forma corta, hay que emitir el
           atributo en partials/topbar.php y ocultar el texto largo. */
        .countdown { padding: var(--sp-2) var(--sp-4); gap: var(--sp-3); }
        .countdown__txt { font-size: var(--fs-xs); }
    }
    @media (max-width: 720px) {
        /* En móvil ya no queda hueco: el chip se oculta y la información sigue
           disponible en Mi día (cuenta atrás grande). */
        .countdown { display: none; }
    }

    .breadcrumb { display: flex; align-items: center; gap: var(--sp-4); min-width: 0; font-size: var(--fs-base); }
    .breadcrumb__sep { width: 16px; height: 16px; color: var(--ink-3); flex-shrink: 0; }
    .breadcrumb__muted { color: var(--ink-3); font-weight: 500; font-size: var(--fs-base); white-space: nowrap; }
    /* El primer nivel de la miga navega (P-18). Se ve como el texto que era
       —no debe competir con el título— pero subraya al pasar por encima y
       tiene el mismo suelo táctil que cualquier otro enlace de la cabecera. */
    .breadcrumb__up { color: var(--ink-3); font-weight: 500; font-size: var(--fs-base); white-space: nowrap; text-decoration: none; }
    .breadcrumb__up:hover { color: var(--ink); text-decoration: underline; }
    .breadcrumb__current { color: var(--ink); font-weight: 700; font-size: var(--fs-xl); letter-spacing: -.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 34vw; }

    /* Botón-icono estandarizado (reutilizable: campana, salir, etc.) */
    .icon-btn { position: relative; display: inline-grid; place-items: center; width: 44px; height: 44px; border-radius: var(--r); border: 1px solid var(--surface-border); background: var(--surface); color: var(--ink-2); cursor: pointer; text-decoration: none; transition: background var(--dur-2) var(--ease), color var(--dur-2) var(--ease), border-color var(--dur-2) var(--ease), transform var(--dur-1) var(--ease-enter), box-shadow var(--dur-2) var(--ease); }
    .icon-btn svg { width: 19px; height: 19px; }
    .icon-btn:active { transform: scale(.94); }
    .icon-btn:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
    @media (hover: hover) and (pointer: fine) {
        .icon-btn:hover { color: var(--ink); border-color: var(--brand); transform: translateY(-1px); box-shadow: var(--shadow-sm); }
    }
    .icon-btn__badge { position: absolute; top: -6px; right: -6px; min-width: 19px; height: 19px; padding: 0 var(--sp-3); border-radius: var(--r-pill); background: var(--c-rose); color: #fff; font-size: var(--fs-xs); font-weight: 700; display: grid; place-items: center; box-shadow: 0 2px 6px rgba(199, 35, 57, .5); }

    /* Hamburguesa (móvil): mismo lenguaje visual que .icon-btn */
    .topbar__menu { width: 44px; height: 44px; border-radius: var(--r); border: 1px solid var(--surface-border); background: var(--surface); color: var(--ink-2); padding: 0; }
    .topbar__menu svg { width: 20px; height: 20px; }

    .topbar__user-caret { width: 16px; height: 16px; color: var(--ink-3); flex-shrink: 0; transition: transform var(--dur-2) var(--ease); }
    .topbar__avatar { width: 44px; height: 44px; border-radius: var(--r-round); background: var(--brand-grad); color: #fff; display: grid; place-items: center; font-size: var(--fs-lg); font-weight: 700; flex-shrink: 0; box-shadow: var(--shadow-sm); transition: transform var(--dur-1) var(--ease-enter); }
    @media (hover: hover) and (pointer: fine) { .topbar__avatar:hover { transform: translateY(-1px); } }

    /* ---------- Menú de cuenta (avatar → flyout) ---------- */
    .account { position: relative; }
    .account__trigger { display: inline-flex; align-items: center; gap: var(--sp-3); padding: var(--sp-2) var(--sp-3) var(--sp-2) var(--sp-2); border: 1px solid transparent; border-radius: var(--r-pill); background: transparent; cursor: pointer; transition: background var(--dur-2) var(--ease), border-color var(--dur-2) var(--ease); }
    .account__trigger:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
    @media (hover: hover) and (pointer: fine) {
        .account__trigger:hover { background: var(--surface); border-color: var(--surface-border); }
        /* El feedback de hover lo da la píldora; el avatar interior no debe saltar. */
        .account__trigger:hover .topbar__avatar { transform: none; }
    }
    .account__trigger[aria-expanded="true"] { background: var(--surface); border-color: var(--surface-border); }
    .account__trigger[aria-expanded="true"] .topbar__user-caret { transform: rotate(180deg); color: var(--brand-texto); }

    .account__flyout { position: absolute; top: calc(100% + 10px); right: 0; z-index: 60; width: 268px; padding: var(--sp-3); background: var(--surface-raised); -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur); border: 1px solid var(--surface-border); border-radius: var(--r-sm); box-shadow: var(--shadow); transform-origin: top right;
        /* Estado cerrado (también el de partida del enter). Apertura ~.2s; el cierre
           lo acorta .is-closing. Reabrir mientras cierra retoma sin salto. */
        opacity: 0; transform: translateY(-6px) scale(.97);
        transition: opacity var(--dur-2) var(--ease-enter), transform var(--dur-2) var(--ease-enter); }
    .account__flyout[hidden] { display: none; }
    .account__flyout.is-open { opacity: 1; transform: translateY(0) scale(1); }
    .account__flyout.is-closing { opacity: 0; transform: translateY(-4px) scale(.98); transition-duration: var(--dur-1); }
    @media (prefers-reduced-motion: reduce) { .account__flyout { transition: none; } }

    .account__head { display: flex; align-items: center; gap: var(--sp-5); padding: var(--sp-5) var(--sp-5) var(--sp-5); }
    .account__avatar { width: 44px; height: 44px; border-radius: var(--r-round); background: var(--brand-grad); color: #fff; display: grid; place-items: center; font-size: var(--fs-xl); font-weight: 700; flex-shrink: 0; box-shadow: var(--shadow-sm); }
    .account__id { display: flex; flex-direction: column; gap: var(--sp-1); min-width: 0; }
    .account__name { font-size: var(--fs-base); }
    .account__email { font-size: var(--fs-sm); color: var(--ink-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .account__role { margin-top: 6px; align-self: flex-start; font-size: var(--fs-xs); font-weight: 700; text-transform: uppercase; letter-spacing: var(--tracking-label); color: var(--brand-700); background: var(--brand-soft); border-radius: var(--r-xs); padding: var(--sp-1) var(--sp-4); }

    .account__menu { border-top: 1px solid var(--line); padding-top: var(--sp-3); display: flex; flex-direction: column; gap: var(--sp-1); }
    .account__logout { margin: 0; }
    .account__item { display: flex; align-items: center; gap: var(--sp-4); width: 100%; padding: var(--sp-4) var(--sp-5); border: 0; border-radius: var(--r-sm); background: transparent; color: var(--ink-2); font-size: var(--fs-base); font-weight: 500; font-family: inherit; text-align: left; text-decoration: none; cursor: pointer; transition: background var(--dur-2) var(--ease), color var(--dur-2) var(--ease); }
    .account__item svg { width: 18px; height: 18px; flex-shrink: 0; color: var(--ink-3); transition: color var(--dur-2) var(--ease); }
    @media (hover: hover) and (pointer: fine) { .account__item:hover { background: var(--surface); color: var(--ink); } .account__item:hover svg { color: var(--brand-texto); } }
    .account__item:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
    .account__item--danger { color: var(--c-rose); }
    .account__item--danger svg { color: var(--c-rose); }
    @media (hover: hover) and (pointer: fine) { .account__item--danger:hover { background: var(--c-rose-bg); color: var(--c-rose); } .account__item--danger:hover svg { color: var(--c-rose); } }

    /* Búsqueda centrada en la barra */
    .topbar__search { justify-self: center; flex: 0 1 520px; width: 100%; max-width: 520px; min-width: 0; height: 46px; border-radius: var(--r); transition: border-color var(--dur-2) var(--ease), box-shadow var(--dur-2) var(--ease), background var(--dur-2) var(--ease); }
    .topbar__search:focus-within { border-color: var(--brand); box-shadow: var(--ring); background: #fff; }

    /* En pantallas medias el grid pasa a "borde · centro flexible · zona derecha
       con su ancho natural", para que el chip de cuenta atrás + iconos + avatar
       no se solapen con la búsqueda cuando 1fr a cada lado deja poco hueco. */
    @media (max-width: 1180px) {
        .topbar { grid-template-columns: minmax(0, 1fr) minmax(0, 420px) auto; gap: var(--sp-5); padding: var(--sp-5) var(--sp-7); }
        .topbar__search { max-width: 420px; }
    }
    @media (max-width: 980px) {
        .topbar { grid-template-columns: auto minmax(0, 1fr) auto; }
        .topbar__search { max-width: none; }
    }

    /* =====================================================================
       Capa de movimiento (ui-animation) — solo transform/opacity.
       Enter cubic-bezier(.22,1,.36,1) · Move cubic-bezier(.25,1,.5,1).
       Hover gated tras (hover:hover); entradas tras prefers-reduced-motion.
       ===================================================================== */

    /* Botones: feedback de pulsación + elevación al pasar el cursor */
    .btn { transition: transform var(--dur-1) var(--ease-enter), box-shadow var(--dur-2) var(--ease), background var(--dur-2) var(--ease), color var(--dur-2) var(--ease), border-color var(--dur-2) var(--ease); }
    .btn:active { transform: scale(.97); }
    @media (hover: hover) and (pointer: fine) {
        .btn--primary:hover, .btn--secondary:hover { transform: translateY(-1px); }
    }

    /* Enlaces de la barra lateral */

    /* Tarjetas accionables: leve elevación al pasar el cursor */
    @media (hover: hover) and (pointer: fine) {
        .stat-card, .shortcut { transition: transform var(--dur-2) var(--ease-enter), box-shadow var(--dur-2) var(--ease); }
        .stat-card:hover, .shortcut:hover { transform: translateY(-3px); box-shadow: var(--shadow); }
    }

    /* Filas de tabla */
    .data-table tbody tr { transition: background var(--dur-2) var(--ease); }

    /* Entradas (page-load) — gated en reduce */
    @media (prefers-reduced-motion: no-preference) {
        .page-header { animation: rise .5s var(--ease-enter) both; }
        .flash-stack .alert { animation: flash-in .32s var(--ease-enter) both; }
        dialog.modal[open] { animation: dialog-in var(--dur-3) var(--ease-enter); }
        dialog.modal[open]::backdrop { animation: backdrop-in var(--dur-3) var(--ease); }
    }
    @keyframes flash-in { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: none; } }
    @keyframes dialog-in { from { opacity: 0; transform: translateY(8px) scale(.96); } to { opacity: 1; transform: none; } }
    @keyframes backdrop-in { from { opacity: 0; } to { opacity: 1; } }

    /* Preferencia de menos movimiento: sin desplazamientos ni entradas */
    @media (prefers-reduced-motion: reduce) {
        .btn, .icon-btn, .topbar__avatar,
        .stat-card, .shortcut, .topbar__search { transition: none; }
        .page-header, .flash-stack .alert,
        dialog.modal[open], dialog.modal[open]::backdrop { animation: none; }
    }

    /* Foco visible consistente en elementos accionables (accesibilidad) */
    .panel__link:focus-visible,
    .quick-filters__link:focus-visible,
    .cmd-item:focus-visible,
    .row-link:focus-visible,
    .link-strong:focus-visible,
    .back-link:focus-visible,
    .modal__close:focus-visible,
    .data-table a:focus-visible,
    .seg__btn:focus-visible,
    .pill-tab:focus-visible {
        outline: 2px solid var(--brand);
        outline-offset: 2px;
        border-radius: var(--r-2xs);
    }

    /* =====================================================================
       UTILIDADES COMPARTIDAS
       Reglas que antes vivían duplicadas en varias hojas. Se declaran una vez
       aquí, en la hoja base, para que todas las pantallas hereden el mismo
       comportamiento y no existan dos versiones del mismo patrón.
       ===================================================================== */

    /* Oculto en pantalla, disponible para lectores de pantalla. Sustituye a
       .people-table__caption y .dir-st-sr, que repetían este mismo bloque. */
    .sr-only {
        position: absolute;
        width: 1px;
        height: 1px;
        padding: 0;
        margin: -1px;
        overflow: hidden;
        clip: rect(0, 0, 0, 0);
        white-space: nowrap;
        border: 0;
    }

    /* Variante que sí se imprime: en papel no hay lector de pantalla, así que la
       tabla accesible pasa a ser la fuente de datos legible. */
    @media print {
        .sr-only--print {
            position: static;
            width: auto;
            height: auto;
            margin: 8px 0 0;
            overflow: visible;
            clip: auto;
            white-space: normal;
        }
    }

    /* Barra de acento lateral. Catorce componentes repetían estas siete
       declaraciones cambiando solo la sangría, la separación vertical y el color;
       ahora los tres valores son variables y el patrón es uno solo. El contenedor
       debe llevar position: relative, como ya lo llevaban todos. */
    .panel--info::before,
    .alert::before,
    .map-tooltip::before,
    .form-note::before,
    .hint::before,
    .aviso-row::before,
    .myday-tasks::before,
    .coverage-alert::before,
    body.app-body .cmd-item::before,
    body.app-body .readiness::before,
    body.app-body .dir-agenda::before,
    body.app-body .dir-load__row--alert::before,
    body.app-body .briefing-card::before,
    body.app-body .dir-mx-block__interpret::before {
        content: "";
        position: absolute;
        left: var(--bar-inset, 7px);
        top: var(--bar-gap, 8px);
        bottom: var(--bar-gap, 8px);
        width: var(--bar-width, 3px);
        border-radius: var(--r-2xs);
        background: var(--bar-color, var(--accent, var(--brand)));
    }

    /* Solo los desvíos respecto a los valores por defecto de arriba. */
    .panel--info { --bar-inset: 10px; --bar-gap: var(--sp-6); --bar-color: var(--brand); }
    .form-note { --bar-color: rgba(31, 111, 224, .5); }
    .hint { --bar-color: var(--brand); }
    .aviso-row { --bar-inset: 9px; --bar-gap: var(--sp-5); }
    .myday-tasks { --bar-inset: 5px; --bar-gap: var(--sp-1); }
    body.app-body .cmd-item { --bar-inset: 6px; --bar-gap: 7px; }
    body.app-body .readiness { --bar-inset: 6px; --bar-gap: var(--sp-2); }
    body.app-body .dir-load__row--alert { --bar-inset: 4px; --bar-color: var(--bad); }
    body.app-body .briefing-card { --bar-inset: 10px; --bar-gap: var(--sp-6); }

    /* ---- Patrones que varios componentes compartían por duplicado ---- */

    /* Relleno de barra de progreso: arranca a cero y lo anima el JS. */
    .zbar__fill {
        display: block;
        width: 0;
        height: 100%;
        border-radius: var(--r-pill);
        background: var(--brand);
    }

    /* Texto de una sola línea que se corta con puntos suspensivos. */
    .cmd-item__text,
    body.app-body .dir-st-donut-legend__name,
    body.app-body .dir-st-bar__label {
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        font-size: var(--fs-md);
        font-weight: 500;
        color: var(--ink);
    }

    /* Lista apilada sin marcadores. */
    .attendee-list,
    body.app-body .dir-decision-list,
    body.app-body .dir-risk-list,
    body.app-body .dir-agenda-list {
        display: flex;
        flex-direction: column;
        list-style: none;
        margin: 0;
        padding: 0;
    }

    /* Variante en rejilla, sin separación entre filas. */
    .day-focus-list,
    .territory-work-list {
        display: grid;
        gap: 0;
        margin: 0;
        padding: 0;
        list-style: none;
    }

    /* Cuerpo flexible de una fila con icono o marca a la izquierda. */
    .myday-event__body,
    body.app-body .dir-agenda__body,
    .dash-list__main,
    body.app-body .dir-evt__main {
        display: flex;
        flex: 1;
        flex-direction: column;
        gap: var(--sp-2);
        min-width: 0;
    }

    /* Punto de color de 9 px usado en leyendas y estados. */
    .cal-ready,
    .zmap-legend__dot,
    .cmd-dot,
    body.app-body .dir-status-pill__dot {
        width: 9px;
        height: 9px;
        border-radius: var(--r-round);
        flex-shrink: 0;
    }

    /* Los tres mapas de la aplicación escalan igual dentro de su contenedor. */
    .zmap svg,
    .terr-map svg,
    body.app-body .dir-st-area svg {
        display: block;
        width: 100%;
        height: auto;
    }

    /* Tooltip flotante. Lo comparten los tres mapas (Concejos, Candidatos y Áreas
       territoriales) y el gráfico de Dirección, que antes tenían tres
       implementaciones del mismo rectángulo azul oscuro y dos convenciones
       distintas para «visible». El estado es .is-visible en todos. */
    .map-tooltip {
        position: absolute;
        left: 0;
        top: 0;
        z-index: 6;
        pointer-events: none;
        padding: var(--sp-3) var(--sp-5) var(--sp-3) var(--sp-6);
        border: 1px solid rgba(255, 255, 255, .16);
        border-radius: var(--r-sm);
        background: rgba(18, 32, 56, .92);
        color: #fff;
        font-size: var(--fs-sm);
        font-weight: 500;
        white-space: nowrap;
        transform: translate(-50%, -100%);
        opacity: 0;
        transition: opacity .12s;
        box-shadow: var(--shadow);
        -webkit-backdrop-filter: blur(6px);
        backdrop-filter: blur(6px);
        --accent: var(--muted);
        --bar-inset: 6px;
        --bar-gap: 5px;
    }

    .map-tooltip.is-visible { opacity: 1; }

    /* Punta hacia el cursor. Solo la llevan los tooltips de mapa, que se colocan
       justo encima del punto señalado; el del gráfico se ancla de otra forma. */
    .map-tooltip--pointer::after {
        content: "";
        position: absolute;
        left: 50%;
        top: 100%;
        transform: translateX(-50%);
        border: 5px solid transparent;
        border-top-color: rgba(18, 32, 56, .92);
    }

    /* Los mapas se posicionan igual respecto a su contenedor.

       N-17. Aqui estaba tambien `.territorial-map-wrap`, cuyo resto de
       declaraciones vivia en campaign.css. Se ha ido entero alli: un componente
       partido entre dos hojas de la misma capa es el mecanismo que produjo
       P-04, y la precedencia la acababa decidiendo el orden de carga. */
    .terr-map-wrap,
    .zmap-wrap {
        position: relative;
    }

    /* ---- Selectores de estado ----
       Dos formas, una implementación cada una. Antes eran cuatro variantes
       (.zmap-modes, .cal-tabs, .avisos-tab, .quick-filters__link) con tres
       colores distintos de opción activa; ahora el activo es var(--brand)
       en todas.

       .seg      -> control segmentado unido: elegir qué muestra una vista.
       .pill-tab -> pestañas sueltas con contador: filtrar una lista. */

    .seg {
        display: inline-flex;
        border: 1px solid var(--line);
        border-radius: var(--r-sm);
        overflow: hidden;
        background: var(--surface-2);
    }

    .seg__btn {
        padding: var(--sp-3) var(--sp-4);
        border: 0;
        background: transparent;
        color: var(--ink-2);
        font: inherit;
        font-size: var(--fs-sm);
        font-weight: 700;
        cursor: pointer;
    }

    .seg__btn + .seg__btn { border-left: 1px solid var(--line); }
    .seg__btn.is-active { background: var(--brand); color: #fff; }
    .seg__btn:not(.is-active):hover { background: var(--brand-soft); color: var(--brand-700); }

    .pill-tabs {
        display: flex;
        flex-wrap: wrap;
        gap: var(--sp-3);
    }

    .pill-tab {
        display: inline-flex;
        align-items: center;
        gap: var(--sp-4);
        padding: var(--sp-4) var(--sp-5);
        border: 1px solid var(--line);
        border-radius: var(--r-pill);
        background: #fff;
        color: var(--ink-2);
        font: inherit;
        font-size: var(--fs-md);
        font-weight: 700;
        cursor: pointer;
        transition: background var(--dur-1, .12s), color var(--dur-1, .12s), border-color var(--dur-1, .12s);
    }

    .pill-tab:hover { background: var(--brand-soft); color: var(--brand-700); }
    .pill-tab.is-active { background: var(--brand); border-color: var(--brand); color: #fff; }

    /* Contador dentro de la pestaña. */
    /* El contador va sobre --b-600 y no sobre el azul claro anterior:
       blanco sobre #5793e8 daba 3,12. */
    .pill-tab__n {
        display: inline-grid;
        place-items: center;
        min-width: 17px;
        height: 17px;
        padding: 0 var(--sp-3);
        border-radius: var(--r-pill);
        background: var(--muted-bg);
        color: var(--ink-3);
        font-size: var(--fs-xs);
        font-variant-numeric: tabular-nums;
    }

    /* Sobre la pestaña activa el contador se OSCURECE en vez de aclararse:
       un blanco al 25 % sobre el azul de marca compone #5793e8, y blanco sobre
       eso da 3,12. Oscurecer conserva el mismo efecto de «hueco» y sí contrasta. */
    .pill-tab.is-active .pill-tab__n { background: rgba(0, 0, 0, .28); color: #fff; }

    /* Versión compacta para barras con muchas pestañas, como la del calendario. */
    .pill-tabs--compact { gap: var(--sp-3); margin-bottom: 12px; }
    .pill-tabs--compact .pill-tab { gap: var(--sp-3); padding: var(--sp-3) var(--sp-4); font-size: var(--fs-sm); background: transparent; }
    .pill-tabs--compact .pill-tab.is-active { background: var(--brand); }

    /* .cmd-item es una rejilla de áreas con nombre desde que se arregló P-01
       (ver campaign.css). El título ocupa el área «titulo»; el `flex: 1` que
       había aquí era inerte dentro de una rejilla y describía un contenedor
       que ya no existe. */
    .cmd-item__text { grid-area: titulo; }
    /* Botón de cierre de cajones y diálogos. */
    .cal-drawer__close,
    .cal-dialog__close {
        flex-shrink: 0;
        width: 28px;
        height: 28px;
        border: 1px solid var(--line);
        border-radius: var(--r-xs);
        background: transparent;
        color: var(--ink-2);
        font-size: var(--fs-xl);
        line-height: 1;
        cursor: pointer;
    }
    /* N-17. El hover estaba en calendar.css, separado de la caja del boton. */
    .cal-drawer__close:hover,
    .cal-dialog__close:hover { background: var(--muted-bg); }

    /* Rótulo de sección en versalitas. */
    .myday-subhead,
    body.app-body .dir-load__subhead {
        font-size: var(--fs-xs);
        font-weight: 700;
        text-transform: uppercase;
        letter-spacing: var(--tracking-label);
        color: var(--ink-3);
    }
    /* N-17. El margen estaba solo en campaign.css: la tipografia del rotulo en
       una hoja y su separacion en otra. */
    .myday-subhead { margin: 20px 0 8px; }

    /* Nombre truncado de una fila. */
    .account__name,
    body.app-body .dir-decision__title,
    body.app-body .dir-agenda__title {
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        font-size: var(--fs-base);
        font-weight: 700;
        color: var(--ink);
    }

    /* Fila de elementos en línea que envuelve. La separación es lo único que
       cambia entre los cuatro sitios donde aparece. */
    .resp-cell,
    .invite-link-row,
    body.app-body .dir-brief__badges,
    .row-actions {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: var(--row-gap, 8px);
    }

    body.app-body .dir-brief__badges,
    .row-actions { --row-gap: var(--sp-3); }

    /* Enlace de una línea, algo mayor, en listas de nombres. */
    .utask__main a,
    body.app-body .dir-st-top__name {
        display: block;
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        font-size: var(--fs-base);
        font-weight: 500;
        color: var(--ink);
        text-decoration: none;
    }

    /* =====================================================================
       Componentes añadidos en la revisión de interfaz (agosto 2026)
       ===================================================================== */

    /* ---------- Salto al contenido ----------
       No existía ninguno. Con hasta diecinueve enlaces de navegación antes del
       contenido, quien navega con teclado o lector de pantalla los recorría enteros
       en cada página. Aparece solo al recibir el foco. */
    .skip-link {
        position: absolute;
        left: 8px;
        top: -64px;
        z-index: 100;
        padding: var(--sp-4) var(--sp-6);
        border-radius: var(--r-sm);
        background: #fff;
        color: var(--ink);
        font-size: var(--fs-md);
        font-weight: 700;
        text-decoration: none;
        box-shadow: var(--shadow);
        transition: top var(--dur-2) var(--ease-enter);
    }
    .skip-link:focus { top: 8px; }

    /* ---------- Salto sobre un bloque largo (P-19) ----------
       El salto de página de arriba es absoluto y flota sobre la cabecera. Éste
       no puede serlo: vive dentro del contenido, donde no hay contexto de
       posicionamiento fiable, y un contorno flotando sobre otra cosa se lee
       como un fallo de pintado. Así que en reposo no ocupa espacio —tamaño
       cero y recortado— y al recibir el foco vuelve al flujo normal como un
       enlace corriente. Empuja el mapa unos píxeles hacia abajo durante el
       instante en que está enfocado, y ese es exactamente el momento en que
       queremos que se note que existe. */
    /* Se esconde SACÁNDOLO de la pantalla, no encogiéndolo. La primera
       versión usaba el recorte clásico —1x1 px y clip-path— y puso en rojo el
       invariante de tamaño táctil en las cuatro pantallas donde se añadió: un
       enlace enfocable que mide un píxel es un objetivo de un píxel, y la
       norma no pregunta si se ve. Fuera de pantalla conserva su tamaño real. */
    .skip-inline {
        position: absolute;
        top: -100px;
        left: 8px;
        padding: var(--sp-5) var(--sp-6);
        white-space: nowrap;
    }
    .skip-inline:focus {
        position: static;
        width: auto;
        height: auto;
        clip-path: none;
        display: inline-block;
        margin-bottom: var(--sp-3);
        padding: var(--sp-5) var(--sp-6);
        border-radius: var(--r-sm);
        background: #fff;
        color: var(--ink);
        font-size: var(--fs-md);
        font-weight: 700;
        text-decoration: none;
        box-shadow: var(--shadow);
    }
    /* Calendario V2: la superficie ocupa exactamente el hueco que deja la
       topbar. El modificador evita cambiar el desplazamiento de las demás
       vistas que comparten `.page`. */
    body.app-body .app-shell:has(.page--alto-completo) { height: 100vh; }
    body.app-body .page--alto-completo {
        flex: 1 1 auto;
        min-height: 0;
        width: 100%;
        max-width: none;
        margin: 0;
        padding: 0;
        display: flex;
        flex-direction: column;
        overflow: hidden;
    }

    .page:focus { outline: none; }
    .page:focus-visible { outline: none; box-shadow: var(--ring); border-radius: var(--r-sm); }

    /* ---------- Aviso sin JavaScript ----------
       El menú de cuenta (perfil y cerrar sesión) solo lo abre el script. Si el
       script no llega, esto da una salida de sesión utilizable. */
    .nojs-bar {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: var(--sp-5);
        margin: var(--sp-5) var(--sp-8) var(--sp-0);
        padding: var(--sp-4) var(--sp-6);
        border: 1px solid var(--warn-line);
        border-radius: var(--r-sm);
        background: var(--warn-bg);
        color: var(--warn);
        font-size: var(--fs-md);
    }

    /* ---------- Cabecera de página ----------
       .page-header__id agrupa título y subtítulo; sustituye al <div> sin clase que
       había copiado en 39 vistas. */
    .page-header__id { min-width: 0; }

    /* ---------- Campos de formulario con error ----------
       Hasta ahora la validación pintaba un banner arriba y ningún campo quedaba
       señalado. El borde y el icono son refuerzo: el mensaje va enlazado por
       aria-describedby, y el color nunca es el único indicador. */
    .form-field__req { color: var(--bad); font-weight: 700; }
    .form-field__hint { margin: var(--sp-0); color: var(--ink-3); font-size: var(--fs-sm); line-height: 1.4; }

    /* Se redefinen las variables del campo en vez de su borde: la regla de la capa
       de retematizado (body.app-body .form-field input) es más específica y ganaría
       a cualquier declaración directa que se pusiera aquí. */
    .form-field--error {
        --field-border: var(--bad);
        --field-bg: var(--bad-bg);
    }
    .form-field--error input,
    .form-field--error select,
    .form-field--error textarea {
        border-color: var(--bad);
        background: var(--bad-bg);
    }
    .form-field--error input:focus,
    .form-field--error select:focus,
    .form-field--error textarea:focus {
        box-shadow: 0 0 0 3px rgba(199, 35, 57, .22);
    }
    .form-field__error {
        display: flex;
        align-items: flex-start;
        gap: var(--sp-3);
        margin: var(--sp-0);
        color: var(--bad);
        font-size: var(--fs-sm);
        font-weight: 500;
        line-height: 1.4;
    }
    .form-field__error-ico { width: 14px; height: 14px; flex-shrink: 0; margin-top: var(--sp-1); }

    /* ---------- Resumen de errores ----------
       Va al principio del formulario, recibe el foco al cargar y cada línea enlaza
       con su campo. En el formulario de acto, con 34 campos, es la diferencia entre
       corregir en un clic y buscar a mano. */
    .form-errors {
        margin-bottom: var(--sp-7);
        padding: var(--sp-6) var(--sp-7);
        border: 1px solid var(--bad-line);
        border-left: 4px solid var(--bad);
        border-radius: var(--r-sm);
        background: var(--bad-bg);
        color: var(--bad-900);
    }
    .form-errors:focus { outline: none; }
    .form-errors:focus-visible { box-shadow: var(--ring); }
    .form-errors__title {
        display: flex;
        align-items: center;
        gap: var(--sp-4);
        margin: var(--sp-0) var(--sp-0) var(--sp-4);
        /* A-3. `--bad` sobre la superficie del propio aviso daba 4,26 y no llega
           a AA. No se veia porque ninguna de las 32 pantallas del arnes muestra
           un formulario con errores: ese estado solo existe tras un POST
           fallido, y la galeria fue la primera en pintarlo. */
        color: var(--bad-800);
        font-size: var(--fs-base);
        font-weight: 700;
    }
    .form-errors__title svg { width: 18px; height: 18px; flex-shrink: 0; }
    .form-errors ul { margin: var(--sp-0); padding-left: var(--sp-8); }
    .form-errors li { margin-bottom: var(--sp-2); font-size: var(--fs-md); }
    .form-errors a { color: var(--bad-900); font-weight: 500; }

    /* Navegación de respaldo sin JavaScript: enlaces en línea, compacta, y
       siempre por debajo de la cabecera para no desplazar el contenido real. */
    .nojs-bar { flex-direction: column; align-items: stretch; }
    .nojs-bar__note { margin: var(--sp-0); }
    .nojs-bar__nav { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-5); }
    .nojs-bar__nav a { color: var(--warn); font-weight: 500; text-decoration: underline; }

    /* ---------- Objetivos táctiles en móvil ----------
       Medido a 375×812: los enlaces del menú quedaban en 36px de alto, el botón de
       menú y la campana en 40×40, y los enlaces de pie de tarjeta en 19px. El
       usuario objetivo de esta herramienta ronda los 55 años y trabaja desde el
       móvil sobre el terreno; 44px es el mínimo que recomiendan tanto WCAG 2.5.5
       como las guías de iOS y Android. Se amplía solo donde hay dedo, no en
       escritorio, para no inflar la densidad de las pantallas de trabajo. */
    /* Este bloque es el suelo táctil ANTERIOR, el que no llegaba a cumplirse:
       vive dentro de `@layer legacy`, así que lo anula cualquier hoja que
       cargue después con la misma especificidad. El suelo que sí manda está al
       final del fichero, fuera de toda capa. Aquí se conserva lo que no
       colisiona con nadie.

       `.sidebar__link` se ha retirado de la lista: murió con el rediseño de
       navegación. Era uno de los ocho selectores que este bloque decía
       proteger, y llevaba desde entonces protegiendo a nadie. */
    @media (any-pointer: coarse) {
        .topbar__menu,
        .icon-btn { min-width: 44px; min-height: 44px; }

        .myday-foot-link,
        .panel__link,
        .quick-filters__link,
        .back-link {
            min-height: 44px;
            display: inline-flex;
            align-items: center;
        }

        /* El buscador de la cabecera se quedaba en 24px de alto al comprimirse. */
        .topbar__search { min-height: 44px; }
        .topbar__search input { min-height: 40px; }
    }
}

/* =====================================================================
   Primer componente en su capa definitiva.

   `componentes` va ANTES que `legacy` en el orden declarado, así que lo que
   se ponga aquí pierde frente a cualquier regla legacy que compita. Para un
   componente nuevo —sin gemela en legacy— eso da igual, y a cambio empieza a
   poblarse la arquitectura correcta en vez de seguir engordando el cajón.
   ===================================================================== */
@layer componentes {
    /* Ranura de metadatos de la cabecera: badges, chips y estado bajo el
       título. Antes cada vista de detalle se lo inventaba por su cuenta. */
    .page-header__meta {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: var(--sp-4);
        margin-top: var(--sp-3);
    }
}

@layer componentes {
    /* Grupos de la barra lateral.

       Aquí vivía además el estilo del grupo «TU TRABAJO», que repetía arriba
       las entradas del rol y las dejaba también abajo en su grupo de siempre.
       Ese grupo desapareció el 25/08/2026 (Q-1): ahora cada entrada aparece una
       sola vez y el perfil del rol decide el ORDEN. El orden es la señal, así
       que no queda nada que pintar: ni filo, ni rótulo distinto, ni marca sobre
       las entradas destacadas. */

    /* Nota al pie de una barra de acciones masivas: explica qué NO se puede
       hacer en bloque y por qué. Ocupa fila completa para no competir con los
       controles, que es donde va la vista. */
    .bulk-note {
        flex-basis: 100%;
        margin: var(--sp-2) 0 0;
        font-size: var(--fs-xs);
        color: var(--ink-3);
    }

    /* =====================================================================
       Chip de concejo — la unidad de identidad territorial

       Ésta es la pieza que la auditoría del 25/08 identificó como la que más
       personalidad da por unidad de esfuerzo, y la única que ningún dashboard
       genérico tiene: Asturias tiene 78 concejos y esta campaña se gana o se
       pierde en ellos, pero fuera del mapa el contexto territorial era
       invisible. Una tarea de Llanes se veía exactamente igual que una de
       Oviedo.

       El color NO es decoración: es la circunscripción, y usa exactamente la
       misma gramática que el mapa y las gráficas (occidental verde, central
       azul, oriental violeta), de modo que el color significa lo mismo en toda
       la aplicación. Un dato convertido en orientación.

       Se usa en cualquier sitio donde aparezca una entidad con concejo: listas
       de atención, resultados de búsqueda, fichas.
       ===================================================================== */
    .concejo-chip {
        display: inline-flex;
        align-items: center;
        gap: var(--sp-2);
        padding: var(--sp-1) var(--sp-3);
        border-radius: var(--r-pill);
        border: 1px solid var(--concejo-line, var(--line));
        background: var(--concejo-bg, var(--muted-bg));
        color: var(--concejo-ink, var(--ink-2));
        font-size: var(--fs-xs);
        font-weight: var(--fw-medium);
        letter-spacing: var(--tracking-body);
        white-space: nowrap;
        vertical-align: baseline;
    }
    .concejo-chip::before {
        content: "";
        width: 6px;
        height: 6px;
        border-radius: var(--r-round);
        background: currentColor;
        flex-shrink: 0;
        opacity: .85;
    }

    .concejo-chip > a {
        color: inherit;
        text-decoration: none;
    }
    .concejo-chip:has(> a) {
        transition: box-shadow .16s var(--ease), transform .16s var(--ease);
    }
    .concejo-chip:has(> a):hover {
        box-shadow: inset 0 0 0 1px currentColor;
        transform: translateY(-1px);
    }

    .concejo-chip--occidental {
        --concejo-ink: var(--ok);
        --concejo-bg: var(--ok-bg);
        --concejo-line: var(--ok-line);
    }
    .concejo-chip--central {
        --concejo-ink: var(--brand-700);
        --concejo-bg: var(--info-bg);
        --concejo-line: var(--info-line);
    }
    .concejo-chip--oriental {
        --concejo-ink: var(--c-violet-ink);
        --concejo-bg: var(--c-violet-bg);
        --concejo-line: var(--c-violet-line);
    }

    /* La acción esperada de un elemento de atención. Va en su propia línea y
       en terciario: es la respuesta a «¿y yo qué hago con esto?», que antes
       simplemente no estaba. */
    .cmd-item__action {
        grid-area: accion;
        font-size: var(--fs-xs);
        color: var(--ink-3);
    }

    /* Qué hace desaparecer un grupo entero de alertas.

       Sin esto, una lista de condiciones se convierte con el tiempo en un
       vertedero permanentemente rojo: nadie sabe qué tiene que pasar para que
       algo salga de ahí, así que nadie espera que salga, así que se deja de
       mirar. Decirlo por escrito es barato y es lo que separa una lista de
       enlaces de una lista de trabajo. */
    .cmd-clears {
        margin: 0 0 var(--sp-5);
        font-size: var(--fs-xs);
        color: var(--ink-3);
    }

    /* N-09. El contador vivía aquí, pegado a `.quick-filters`, pero lo que
       contaba era una PESTAÑA. Al separar los dos componentes se quedó sin un
       solo uso, y el trinquete de CSS muerto lo cantó en el mismo commit.
       Está ahora en campaign.css, junto a `.section-tabs`, que es quien lo usa. */

    /* =====================================================================
       Paleta de comandos (Ctrl+K)

       Deliberadamente sobria: es una herramienta de paso, no una pantalla. Se
       apoya en la superficie elevada y la línea de siempre, sin cristal ni
       degradados propios, para que no parezca una aplicación distinta dentro
       de la aplicación.
       ===================================================================== */
    .cmdk {
        position: fixed;
        inset: 0;
        z-index: 200;
        display: grid;
        /* Alto, no centrado: el ojo ya está arriba y así la lista crece hacia
           abajo sin mover el campo de sitio. */
        align-content: start;
        justify-items: center;
        padding: 10vh var(--sp-5) var(--sp-5);
    }
    .cmdk[hidden] { display: none; }

    .cmdk__velo {
        position: absolute;
        inset: 0;
        background: rgba(13, 22, 34, .42);
    }

    .cmdk__caja {
        position: relative;
        width: min(620px, 100%);
        max-height: 70vh;
        display: flex;
        flex-direction: column;
        background: var(--surface-raised);
        border: 1px solid var(--surface-border);
        border-radius: var(--r-lg);
        box-shadow: var(--shadow);
        overflow: hidden;
    }

    .cmdk__head {
        display: flex;
        align-items: center;
        gap: var(--sp-4);
        padding: var(--sp-5) var(--sp-6);
        border-bottom: 1px solid var(--line);
    }
    .cmdk__input {
        flex: 1;
        border: 0;
        background: transparent;
        font-size: var(--fs-lg);
        color: var(--ink);
        outline: none;
    }
    .cmdk__esc {
        font-size: var(--fs-xs);
        color: var(--ink-3);
        border: 1px solid var(--line);
        border-radius: var(--r-2xs);
        padding: var(--sp-1) var(--sp-3);
    }

    .cmdk__lista {
        list-style: none;
        margin: 0;
        padding: var(--sp-3) 0;
        overflow-y: auto;
        flex: 1;
    }
    .cmdk__grupo {
        padding: var(--sp-4) var(--sp-6) var(--sp-2);
        font-size: var(--fs-xs);
        font-weight: var(--fw-medium);
        letter-spacing: var(--tracking-label);
        text-transform: uppercase;
        color: var(--ink-3);
    }
    .cmdk__item { margin: 0; }
    .cmdk__enlace {
        display: flex;
        align-items: baseline;
        justify-content: space-between;
        gap: var(--sp-5);
        padding: var(--sp-4) var(--sp-6);
        text-decoration: none;
        color: var(--ink);
    }
    .cmdk__item.is-on { background: var(--brand-soft); }
    .cmdk__item.is-on .cmdk__enlace { color: var(--brand-700); }
    .cmdk__label { font-size: var(--fs-base); font-weight: var(--fw-medium); }
    .cmdk__meta {
        font-size: var(--fs-xs);
        color: var(--ink-3);
        text-align: right;
        /* El texto de ayuda cede espacio antes que la etiqueta: el nombre es
           lo que se está buscando. */
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    .cmdk__estado {
        margin: 0;
        padding: var(--sp-4) var(--sp-6);
        border-top: 1px solid var(--line);
        font-size: var(--fs-xs);
        color: var(--ink-3);
    }

    /* Con la paleta abierta el fondo no se desplaza. */
    body.cmdk-abierta { overflow: hidden; }

    /* =====================================================================
       Vistas guardadas (E-6)

       La aplicación no recordaba nada y cada sesión empezaba de cero. Esto es
       lo que hace que el coordinador de seis concejos deje de reconstruir su
       filtro dos veces al día.

       Va discreta a propósito: se usa una vez y se consulta muchas, así que
       las vistas se leen como pestañas ligeras y guardar queda plegado.
       ===================================================================== */
    .saved-views {
        display: flex;
        flex-wrap: wrap;
        align-items: baseline;
        gap: var(--sp-3) var(--sp-5);
        margin: var(--sp-5) 0;
    }
    .saved-views__rotulo {
        font-size: var(--fs-xs);
        font-weight: var(--fw-medium);
        letter-spacing: var(--tracking-label);
        text-transform: uppercase;
        color: var(--ink-3);
    }
    .saved-views__lista {
        display: flex;
        flex-wrap: wrap;
        gap: var(--sp-3);
        list-style: none;
        margin: 0;
        padding: 0;
    }
    .saved-views__item {
        display: inline-flex;
        align-items: center;
        border: 1px solid var(--line);
        border-radius: var(--r-pill);
        background: var(--surface);
    }
    .saved-views__item.is-active {
        border-color: var(--info-line);
        background: var(--brand-soft);
    }
    .saved-views__enlace {
        padding: var(--sp-2) var(--sp-4);
        font-size: var(--fs-sm);
        color: var(--ink-2);
        text-decoration: none;
    }
    .saved-views__item.is-active .saved-views__enlace {
        color: var(--brand-700);
        font-weight: var(--fw-medium);
    }
    .saved-views__borrar { display: inline-flex; margin: 0; }
    .saved-views__x {
        border: 0;
        background: none;
        padding: var(--sp-2) var(--sp-3) var(--sp-2) 0;
        color: var(--ink-3);
        font-size: var(--fs-base);
        line-height: 1;
    }
    .saved-views__x:hover { color: var(--bad); }

    .saved-views__nueva > summary {
        font-size: var(--fs-sm);
        color: var(--ink-3);
        cursor: pointer;
    }
    .saved-views__nueva form {
        display: flex;
        flex-wrap: wrap;
        align-items: end;
        gap: var(--sp-3);
        margin-top: var(--sp-4);
    }
    .saved-views__nueva label {
        font-size: var(--fs-sm);
        color: var(--ink-2);
    }
    .saved-views__nueva input[type="text"] {
        min-width: 220px;
        padding: var(--sp-3) var(--sp-4);
        border: 1px solid var(--line);
        border-radius: var(--r-sm);
        background: var(--surface);
        font-size: var(--fs-md);
    }
    .saved-views__nota {
        margin: var(--sp-3) 0 0;
        font-size: var(--fs-xs);
        color: var(--ink-3);
        max-width: 60ch;
    }

    /* Estrella de concejo favorito. Con 78 concejos y 3-8 por persona, es la
       diferencia entre buscar y llegar. */
    .fav-form { display: inline; margin: 0; }
    .fav-btn {
        border: 0;
        background: none;
        padding: 0 var(--sp-3) 0 0;
        color: var(--n-300);
        font-size: var(--fs-base);
        line-height: 1;
        /* El objetivo táctil real es mayor que el glifo: en móvil una estrella
           de 14px es imposible de acertar. */
        min-width: 32px;
        min-height: 32px;
    }
    .fav-btn.is-on { color: var(--gold-ink); }
    .fav-btn:hover { color: var(--gold-ink); }

    /* =====================================================================
       Marca institucional (rescate del dorado)

       El dorado asturiano estaba definido con cuidado —`--gold` para relleno,
       `--gold-ink` como único válido para texto, y deliberadamente separado del
       ámbar de «atención» para que un dorado no se leyera como una alerta— y
       apenas se usaba: quince apariciones en toda la hoja principal.

       Aquí recupera su trabajo: marcar lo INSTITUCIONAL, que no es un estado.
       Que el candidato asista a un acto no es «información» genérica ni una
       alerta; es la circunstancia que cambia cómo se prepara ese acto, y hasta
       ahora se pintaba con el mismo azul que cualquier otro dato.

       Deliberadamente NO se toca la cuenta atrás: sus colores codifican la
       fase electoral —informativa, de aviso, urgente— y volverla dorada
       borraría esa escalada, que es información real.
       ===================================================================== */
    .badge--institucional {
        color: var(--gold-ink);
        background: var(--gold-bg);
        border-color: var(--gold-line);
    }

    /* Hitos: los electorales son fechas que no dependen de nosotros; los
       operativos, sí. La distinción se veía sólo leyendo el texto. */
    .milestone-strip a.is-electoral time { color: var(--gold-ink); }

    /* Fila que acaba de guardarse sin recargar. En una tabla de cincuenta
       filas, cambiar un badge en silencio es indistinguible de no hacer nada:
       esto dice DÓNDE ocurrió. Se apaga solo. */
    .is-just-saved {
        animation: fila-guardada 1.6s var(--ease) both;
    }
    @keyframes fila-guardada {
        0%   { background: var(--ok-bg); }
        70%  { background: var(--ok-bg); }
        100% { background: transparent; }
    }
    @media (prefers-reduced-motion: reduce) {
        /* Sin animación, pero la señal no se pierde: se queda el fondo el
           mismo tiempo y desaparece de golpe. */
        .is-just-saved { animation: none; background: var(--ok-bg); }
    }

    /* =====================================================================
       Navegación por workspaces

       La columna estrecha responde «dónde estoy»; el panel contiguo, «qué
       puedo hacer aquí». Solo esta pieza adopta la composición clara del nuevo
       boceto: el resto del sistema conserva sus tokens y superficies.
       ===================================================================== */
    body.app-body {
        --nav-rail-w: 108px;
        --nav-context-w: 326px;
        /* Una sola geometría para los dos lados del armazón. Antes la marca
           medía 104 px y la topbar unos 72 px, de modo que sus separadores
           nunca podían continuar la misma línea. */
        --shell-header-h: 88px;
    }

    @media (min-width: 1181px) {
        body.app-body .topbar {
            height: var(--shell-header-h);
            min-height: var(--shell-header-h);
            padding-block: 0;
            flex: 0 0 var(--shell-header-h);
        }
    }

    body.app-body .sidebar {
        position: sticky;
        top: 0;
        z-index: 30;
        display: flex;
        width: calc(var(--nav-rail-w) + var(--nav-context-w) + 12px);
        height: 100vh;
        padding: 0;
        overflow: visible;
        flex-shrink: 0;
        background: transparent;
        background-image: none;
        border: 0;
        box-shadow: none;
        isolation: isolate;
        transition: width var(--dur-3) var(--ease-move);
    }

    body.app-body .sidebar__rail {
        position: relative;
        z-index: 2;
        display: flex;
        width: var(--nav-rail-w);
        height: 100%;
        flex: 0 0 var(--nav-rail-w);
        flex-direction: column;
        overflow: visible;
        background:
            radial-gradient(120px 160px at 50% 0%, rgba(31, 111, 224, .09), transparent 72%),
            linear-gradient(180deg, rgba(255, 255, 255, .99), rgba(247, 250, 255, .98));
        border-right: 1px solid var(--surface-border);
        box-shadow: 8px 0 30px -28px rgba(18, 54, 103, .48);
    }

    body.app-body .sidebar__brand {
        display: grid;
        place-items: center;
        width: 100%;
        height: var(--shell-header-h);
        padding: var(--sp-5);
        flex: 0 0 var(--shell-header-h);
        border-bottom: 1px solid rgba(217, 228, 242, .78);
    }

    body.app-body .sidebar__logo {
        display: block;
        width: 78px;
        max-width: 100%;
        height: auto;
        filter: drop-shadow(0 4px 10px rgba(31, 111, 224, .12));
    }

    .sidebar__workspaces {
        display: flex;
        min-height: 0;
        padding: var(--sp-5) var(--sp-4);
        flex: 1;
        flex-direction: column;
        gap: var(--sp-2);
    }

    .sidebar__workspace {
        position: relative;
        display: flex;
        width: 100%;
        min-height: 72px;
        padding: var(--sp-4) var(--sp-2);
        align-items: center;
        justify-content: center;
        flex-direction: column;
        gap: var(--sp-3);
        border: 0;
        border-radius: var(--r-sm);
        background:
            radial-gradient(72% 82% at 50% 108%, rgba(240, 179, 25, 0) 0%, rgba(240, 179, 25, 0) 54%, transparent 76%),
            linear-gradient(180deg, rgba(255, 255, 255, 0), rgba(230, 238, 249, 0));
        color: var(--ink-3);
        font: inherit;
        cursor: pointer;
        transition: color var(--dur-2) var(--ease), background var(--dur-2) var(--ease), transform var(--dur-1) var(--ease-enter), box-shadow var(--dur-2) var(--ease);
    }

    .sidebar__workspace::before {
        content: "";
        position: absolute;
        left: -8px;
        top: var(--sp-5);
        bottom: var(--sp-5);
        width: var(--sp-1);
        border-radius: 0 var(--r-2xs) var(--r-2xs) 0;
        background: var(--gold);
        opacity: 0;
        transform: scaleY(.4);
        transition: opacity var(--dur-2) var(--ease), transform var(--dur-3) var(--ease-enter);
    }

    .sidebar__workspace::after {
        content: "";
        position: absolute;
        z-index: 3;
        right: calc(-1 * var(--sp-5));
        top: 50%;
        width: var(--sp-5);
        height: 3px;
        background: linear-gradient(90deg, var(--gold), var(--brand));
        border-radius: var(--r-pill);
        box-shadow:
            0 0 6px rgba(240, 179, 25, .26),
            0 0 10px rgba(31, 111, 224, .24);
        opacity: 0;
        transform: translateY(-50%) scaleX(.25);
        transform-origin: left center;
        transition: opacity var(--dur-2) var(--ease), transform var(--dur-2) var(--ease-enter);
    }

    .sidebar__workspace-icon {
        display: grid;
        width: 29px;
        height: 28px;
        place-items: center;
    }
    .sidebar__workspace-icon svg { width: 27px; height: 27px; }
    .sidebar__workspace-label {
        max-width: 88px;
        overflow: hidden;
        font-size: var(--fs-xs);
        font-weight: var(--fw-medium);
        letter-spacing: -.01em;
        line-height: 1.1;
        text-align: center;
        text-overflow: ellipsis;
    }

    @media (hover: hover) and (pointer: fine) {
        .sidebar__workspace:hover {
            color: var(--brand-700);
            background:
                radial-gradient(72% 82% at 50% 108%, rgba(240, 179, 25, .27) 0%, rgba(240, 179, 25, .08) 54%, transparent 76%),
                linear-gradient(180deg, rgba(255, 255, 255, .98), rgba(230, 238, 249, .82));
            transform: translateY(-1px);
            box-shadow:
                inset 0 1px 0 rgba(255, 255, 255, .96),
                inset 0 0 0 1px rgba(31, 111, 224, .1),
                0 9px 20px -18px rgba(31, 111, 224, .72);
        }
        .sidebar__workspace:hover .sidebar__workspace-icon { color: var(--brand-texto); }
    }

    .sidebar__workspace.is-active {
        color: var(--brand-700);
        background:
            radial-gradient(72% 82% at 50% 108%, rgba(240, 179, 25, .54) 0%, rgba(240, 179, 25, .16) 54%, transparent 76%),
            linear-gradient(180deg, #fff 0%, var(--b-100) 100%);
        box-shadow:
            inset 0 1px 0 #fff,
            inset 0 0 0 1px rgba(31, 111, 224, .16),
            0 13px 26px -19px rgba(31, 111, 224, .72),
            0 5px 10px -8px rgba(18, 54, 103, .28);
    }
    .sidebar__workspace.is-active .sidebar__workspace-icon { color: var(--brand-texto); }
    .sidebar__workspace.is-active::before { opacity: 1; transform: scaleY(1); }
    .sidebar__workspace.is-active::after { opacity: 1; transform: translateY(-50%) scaleX(1); }

    .sidebar__workspace:focus-visible,
    .sidebar__context-close:focus-visible,
    .sidebar__section:focus-visible {
        outline: none;
        box-shadow: var(--ring);
    }
    .sidebar__context {
        position: relative;
        z-index: 1;
        width: var(--nav-context-w);
        height: calc(100vh - 24px);
        margin: 12px 12px 12px 0;
        flex: 0 0 var(--nav-context-w);
        overflow: hidden;
        background:
            radial-gradient(280px 180px at 16% 0%, rgba(31, 111, 224, .075), transparent 72%),
            rgba(252, 253, 255, .98);
        border: 1px solid var(--surface-border);
        border-left: 0;
        border-radius: 0 var(--r) var(--r) 0;
        box-shadow: 20px 22px 48px -40px rgba(18, 54, 103, .55), inset 0 1px 0 rgba(255, 255, 255, .96);
        opacity: 1;
        transform: translateX(0);
        transform-origin: left center;
        transition: opacity var(--dur-2) var(--ease), transform var(--dur-3) var(--ease-move), visibility 0s linear 0s;
    }

    body.sidebar-context-closed .sidebar { width: var(--nav-rail-w); }
    body.sidebar-context-closed .sidebar__context {
        opacity: 0;
        transform: translateX(-12px) scaleX(.97);
        visibility: hidden;
        pointer-events: none;
        transition: opacity var(--dur-1) var(--ease), transform var(--dur-3) var(--ease-move), visibility 0s linear var(--dur-3);
    }
    body.sidebar-context-closed .sidebar__workspace::after { opacity: 0; }

    .sidebar__workspace-panel {
        display: flex;
        min-width: var(--nav-context-w);
        height: 100%;
        padding: var(--sp-8) var(--sp-6) var(--sp-6);
        flex-direction: column;
        overflow-y: auto;
        overscroll-behavior: contain;
        scrollbar-width: thin;
        scrollbar-color: var(--b-100) transparent;
        animation: workspace-panel-in var(--dur-3) var(--ease-enter) both;
    }
    .sidebar__workspace-panel[hidden] { display: none; }
    @keyframes workspace-panel-in {
        from { opacity: 0; transform: translateX(calc(-1 * var(--sp-4))); }
        to { opacity: 1; transform: none; }
    }

    .sidebar__context-head {
        display: grid;
        padding: var(--sp-4) var(--sp-2) var(--sp-7);
        grid-template-columns: 62px minmax(0, 1fr) 34px;
        align-items: center;
        gap: var(--sp-5);
        border-bottom: 1px solid var(--line);
    }
    .sidebar__context-icon {
        display: grid;
        width: 62px;
        height: 62px;
        place-items: center;
        border: 1px solid rgba(31, 111, 224, .14);
        border-radius: var(--r);
        background:
            radial-gradient(circle at 38% 30%, rgba(255, 255, 255, .95), transparent 38%),
            linear-gradient(145deg, var(--b-50), var(--b-100));
        color: var(--brand-texto);
        box-shadow: 0 14px 28px -24px rgba(31, 111, 224, .72), inset 0 1px 0 #fff;
    }
    .sidebar__context-icon svg { width: 32px; height: 32px; stroke-width: 1.8; }
    .sidebar__context-copy { display: flex; min-width: 0; flex-direction: column; gap: var(--sp-2); }
    .sidebar__context-copy strong {
        color: var(--ink);
        font-size: var(--fs-2xl);
        font-weight: var(--fw-bold);
        letter-spacing: var(--tracking-heading);
        line-height: 1.05;
    }
    .sidebar__context-copy > span {
        color: var(--ink-3);
        font-size: var(--fs-xs);
        line-height: 1.35;
    }
    .sidebar__context-close {
        display: grid;
        width: 34px;
        height: 34px;
        padding: 0;
        place-items: center;
        align-self: start;
        border: 1px solid var(--line);
        border-radius: var(--r-xs);
        background: rgba(255, 255, 255, .72);
        color: var(--ink-3);
        font-size: var(--fs-4xl);
        font-weight: 400;
        line-height: 1;
        cursor: pointer;
        transition: color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
    }
    .sidebar__context-close span { transform: translateY(-1px); }
    .sidebar__context-close:hover { color: var(--ink); border-color: var(--info-line); background: #fff; }

    /* La franja bajo esta lista queda VACÍA a propósito. Auditoría conjunta
       Claude + Sol, 26/08/2026.

       Aquí vivió el bloque «Recientes», que se retiró. Quedó su separador:
       una línea horizontal sin nada debajo, prometiendo un contenido que ya
       no existe. Por eso se quita `border-bottom` — el resto del hueco no es
       un defecto, es la decisión.

       Se evaluaron y descartaron: favoritos de concejo, una línea de estado
       por área («4 tareas vencidas»), la acción de crear del área, y un
       recordatorio de Ctrl+K. Los motivos, por orden de peso:

       1. El panel es TRANSITORIO: se abre, se elige destino y se cierra. No
          es un tablero. Meterle vigilancia lo convierte en una segunda
          superficie de monitorización que puede divergir de `Attention`,
          que es la fuente única.
       2. Mezcla de niveles: las secciones son secciones; un concejo favorito
          es una entidad. Ponerlos juntos debilita el modelo mental.
       3. La densidad la fija la arquitectura de información, no la altura
          disponible. Rellenar para «equilibrar» produce decoración.
       4. Es la decisión reversible: cuando un área crezca de 6 a 12
          secciones el hueco desaparece solo, y cualquier bloque de hoy
          tendría entonces que competir por altura.

       NO reducir la altura del panel para disimular el hueco: daría paneles
       de alturas distintas y movimiento visual al cambiar de área.

       Reabrir sólo con datos de uso reales, nunca por los píxeles libres. */
    .sidebar__sections {
        display: flex;
        padding: var(--sp-6) 0 var(--sp-5);
        flex-direction: column;
        gap: var(--sp-1);
    }
    .sidebar__section {
        position: relative;
        display: grid;
        min-height: 52px;
        padding: var(--sp-4) var(--sp-5);
        grid-template-columns: 27px minmax(0, 1fr) 18px;
        align-items: center;
        gap: var(--sp-5);
        border-radius: var(--r-sm);
        background:
            radial-gradient(58% 90% at 42% 112%, rgba(240, 179, 25, 0) 0%, rgba(240, 179, 25, 0) 55%, transparent 78%),
            linear-gradient(90deg, rgba(255, 255, 255, 0), rgba(230, 238, 249, 0));
        color: var(--ink-2);
        font-size: var(--fs-md);
        font-weight: var(--fw-medium);
        text-decoration: none;
        transition: color var(--dur-2) var(--ease), background var(--dur-2) var(--ease), transform var(--dur-1) var(--ease-enter), box-shadow var(--dur-2) var(--ease);
    }
    .sidebar__section::before {
        content: "";
        position: absolute;
        left: 0;
        top: 10px;
        bottom: 10px;
        width: var(--sp-1);
        border-radius: 0 var(--r-2xs) var(--r-2xs) 0;
        background: var(--gold);
        box-shadow: 2px 0 8px rgba(240, 179, 25, .3);
        opacity: 0;
        transform: scaleY(.4);
        transition: opacity var(--dur-2) var(--ease), transform var(--dur-2) var(--ease-enter);
    }
    .sidebar__section-icon { display: grid; place-items: center; color: var(--ink-3); }
    .sidebar__section-icon svg { width: 21px; height: 21px; }
    .sidebar__section-chevron { width: 17px; height: 17px; color: var(--ink-3); }
    @media (hover: hover) and (pointer: fine) {
        .sidebar__section:hover {
            color: var(--brand-700);
            background:
                radial-gradient(58% 90% at 42% 112%, rgba(240, 179, 25, .22) 0%, rgba(240, 179, 25, .07) 55%, transparent 78%),
                linear-gradient(90deg, rgba(255, 255, 255, .98), rgba(230, 238, 249, .7));
            box-shadow:
                inset 0 1px 0 rgba(255, 255, 255, .96),
                inset 0 0 0 1px rgba(31, 111, 224, .08),
                0 8px 18px -17px rgba(31, 111, 224, .65);
            transform: translateX(2px);
        }
        .sidebar__section:hover .sidebar__section-icon,
        .sidebar__section:hover .sidebar__section-chevron { color: var(--brand-texto); }
    }
    .sidebar__section.is-active {
        color: var(--brand-700);
        background:
            radial-gradient(58% 90% at 42% 112%, rgba(240, 179, 25, .46) 0%, rgba(240, 179, 25, .14) 55%, transparent 78%),
            linear-gradient(90deg, #fff 0%, rgba(230, 238, 249, .9) 100%);
        box-shadow:
            inset 0 1px 0 #fff,
            inset 0 0 0 1px rgba(31, 111, 224, .14),
            0 12px 24px -18px rgba(31, 111, 224, .66),
            0 4px 9px -8px rgba(18, 54, 103, .24);
    }
    .sidebar__section.is-active::before { opacity: 1; transform: scaleY(1); }
    .sidebar__section.is-active .sidebar__section-icon,
    .sidebar__section.is-active .sidebar__section-chevron { color: var(--brand-texto); }
    @media (max-height: 720px) and (min-width: 1181px) {
        body.app-body .sidebar__brand { height: var(--shell-header-h); flex-basis: var(--shell-header-h); padding-block: var(--sp-5); }
        body.app-body .sidebar__logo { width: 68px; }
        .sidebar__workspaces { padding-block: var(--sp-4); gap: var(--sp-1); }
        .sidebar__workspace { min-height: 60px; padding-block: var(--sp-3); gap: var(--sp-1); }
        .sidebar__workspace-icon { width: 25px; height: 25px; }
        .sidebar__workspace-icon svg { width: 23px; height: 23px; }
    }

    @media (max-width: 1180px) {
        body.app-body .sidebar {
            position: fixed;
            left: 0;
            top: 0;
            z-index: 40;
            width: calc(var(--nav-rail-w) + var(--nav-context-w) + 12px);
            height: 100dvh;
            transform: translateX(-100%);
            transition: transform var(--dur-3) var(--ease-move), width var(--dur-3) var(--ease-move);
        }
        body.sidebar-open .sidebar { transform: translateX(0); }
        body.sidebar-context-closed .sidebar { width: var(--nav-rail-w); }
        body.app-body .topbar__menu { display: inline-flex; align-items: center; justify-content: center; }
        .sidebar-backdrop { border: 0; padding: 0; }
        body.sidebar-open .sidebar-backdrop {
            display: block;
            position: fixed;
            inset: 0;
            z-index: 35;
            background: rgba(15, 40, 70, .4);
            -webkit-backdrop-filter: blur(2px);
            backdrop-filter: blur(2px);
        }
    }

    @media (max-width: 520px) {
        body.app-body {
            --nav-rail-w: 88px;
            --nav-context-w: calc(100vw - 104px);
        }
        body.app-body .sidebar { width: calc(100vw - 8px); }
        body.sidebar-context-closed .sidebar { width: var(--nav-rail-w); }
        body.app-body .sidebar__brand { height: 86px; flex-basis: 86px; padding: var(--sp-5) var(--sp-4); }
        body.app-body .sidebar__logo { width: 66px; }
        .sidebar__workspaces { padding: var(--sp-4) var(--sp-3); gap: var(--sp-1); }
        .sidebar__workspace { min-height: 64px; padding-inline: var(--sp-1); }
        .sidebar__workspace-label { max-width: 76px; font-size: var(--fs-xs); }
        .sidebar__workspace::before { left: -6px; }
        .sidebar__workspace::after { right: -11px; width: 11px; }
        .sidebar__context { height: calc(100dvh - 16px); margin: 8px 8px 8px 0; }
        .sidebar__workspace-panel { min-width: var(--nav-context-w); padding: var(--sp-6) var(--sp-5) var(--sp-5); }
        .sidebar__context-head { padding: var(--sp-2) var(--sp-1) var(--sp-6); grid-template-columns: 48px minmax(0, 1fr) 32px; gap: var(--sp-4); }
        .sidebar__context-icon { width: 48px; height: 48px; }
        .sidebar__context-icon svg { width: 25px; height: 25px; }
        .sidebar__context-copy strong { font-size: var(--fs-xl); }
        .sidebar__context-copy > span { font-size: var(--fs-xs); }
        .sidebar__section { min-height: 48px; padding-inline: var(--sp-4); grid-template-columns: 24px minmax(0, 1fr) 16px; gap: var(--sp-4); }
        }

    @media (prefers-reduced-motion: reduce) {
        body.app-body .sidebar,
        .sidebar__context,
        .sidebar__workspace,
        .sidebar__workspace::before,
        .sidebar__workspace::after,
        .sidebar__section,
        .sidebar__workspace-panel { animation: none; transition: none; }
    }

    @media (max-width: 560px) {
        .cmdk { padding: var(--sp-5); }
        .cmdk__caja { max-height: 84vh; }
        .cmdk__enlace { flex-direction: column; gap: var(--sp-1); align-items: stretch; }
        .cmdk__meta { text-align: left; }
    }
}

/* =====================================================================
   CAPA «accesibilidad» — la última de la cascada, a propósito
   =====================================================================

   AQUÍ VIVEN LOS DOS SUELOS DE ACCESIBILIDAD, y el motivo está medido.

   El problema original: un suelo escrito dentro de `@layer legacy` no
   funciona. `.quick-filters__link` computaba 34 px porque campaign.css la
   redeclara con la misma especificidad y carga después (P-04), y
   `.topbar__search:focus-within` no llegaba a aplicarse porque
   `body.app-body .topbar__search` redeclara sus tres propiedades con más
   especificidad y sin condición de foco (P-15). En los dos casos el estilo
   correcto EXISTÍA y perdía la cascada en silencio.

   PRIMER INTENTO, Y POR QUÉ SE DESCARTÓ. Los dos suelos vivieron un tiempo
   FUERA de toda capa, que es la única posición que gana a `legacy` sin tocar
   el orden declarado. Funcionaba y era demasiado bruto: una declaración sin
   capa gana a TODO lo que esté en capas, sin importar la especificidad, así
   que el suelo de foco pisaba, en silencio, las doce anillas que los
   componentes ya tenían definidas. Ninguna de las doce empeoraba —se comprobó
   una por una, midiendo—, pero el problema no era el resultado: era que
   ningún componente PODÍA discrepar del suelo aunque tuviera razón, y que
   nadie se enteraba de que su regla había dejado de aplicarse.

   El caso que lo destapó es instructivo por lo que enseña sobre medir en vez
   de deducir. Una revisión externa señaló que el aviso de «Mi día»
   (`.myday-hero__avisos`) declara `outline: 2px solid #fff` y calculó que el
   suelo lo dejaba en 1,17 : 1, leyendo del CSS que detrás había un degradado
   de marca. Detrás no hay un degradado: hay una FOTOGRAFÍA de paisaje,
   pintada en un `::before` con z-index negativo. Medido sobre el píxel real
   —rgb(105, 134, 152)—, el blanco da 3,84 : 1 y el azul del suelo 3,42 : 1.
   Los dos pasan el 3 : 1 de 1.4.11. El hallazgo era falso y la lección no:
   deducir el fondo leyendo hojas de estilo falla exactamente igual que
   deducir la cascada leyéndolas, que es de lo que trata todo este bloque.

   LO QUE SE HACE AHORA. Una capa propia, declarada la ÚLTIMA en la línea 17.
   Gana a `legacy` igual que ganaba estar fuera de capa —eso es lo que hacía
   falta— pero dentro de ella vuelve a mandar la especificidad. Así:

     · los suelos usan `:where(...)`, que tiene especificidad CERO, y por eso
       sólo se aplican donde ningún componente ha dicho nada;
     · un componente que necesite un contorno distinto —porque su fondo lo
       exige— lo declara AQUÍ, con su selector normal, y gana.

   La diferencia con el estado anterior no es de fuerza sino de gobierno:
   antes ningún componente podía discrepar del suelo; ahora puede, pero tiene
   que hacerlo en esta capa, a la vista, y no por accidente desde `legacy`.
   ===================================================================== */

@layer accesibilidad {

    /* ================================================================
       A-50 · La señal de «lo he pulsado»

       Había 123 reglas `:hover` y CUATRO `:active`, todas en botones. En un
       ratón eso se nota poco: el puntero ya iba encima y el `:hover` daba señal.
       En táctil `:hover` no existe, así que enlaces, tarjetas, chips, filtros
       rápidos y filas de tabla no devolvían NADA al pulsarse. El dedo toca, no
       pasa nada visible, y la pantalla nueva llega doscientos milisegundos
       después. La aplicación responde igual de rápido y se percibe lenta,
       porque durante esos doscientos milisegundos el usuario no sabe si le ha
       hecho caso.

       Dos tratamientos, según lo que admite cada cosa:

         · lo que es una caja —tarjetas, chips, pastillas, elementos de lista—
           se hunde un poco;
         · las filas de tabla NO se transforman: `transform` sobre `tr` no está
           definido de forma fiable entre navegadores. Se les cambia el fondo,
           que es la misma señal por otro medio.

       Y quien pide menos movimiento sigue recibiendo la señal: se le quita el
       desplazamiento, no el aviso.
       ================================================================ */
    .cmd-item:active,
    .stat-card:active,
    .shortcut:active,
    .section-card:active,
    .quick-filters__link:active,
    .pill-tab:active,
    .seg__btn:active,
    .concejo-chip:has(> a):active,
    .sidebar__section:active,
    .sidebar__workspace:active,
    .myday-shortcut:active,
    .evt-item:active,
    .adoption-kpi:active,
    .dir-mx-card:active,
    .saved-views__enlace:active,
    .row-link:active,
    .hchip:active,
    .fav-btn:active,
    .back-link:active,
    .panel__link:active {
        transform: scale(.985);
    }

    /* Las filas no se transforman: se oscurecen. */
    .data-table tbody tr:active,
    .dash-row:active,
    .dir-decision:active,
    .utask:active {
        background: var(--muted-bg);
    }

    @media (prefers-reduced-motion: reduce) {
        .cmd-item:active,
        .stat-card:active,
        .shortcut:active,
        .section-card:active,
        .quick-filters__link:active,
        .pill-tab:active,
        .seg__btn:active,
        .concejo-chip:has(> a):active,
        .sidebar__section:active,
        .sidebar__workspace:active,
        .myday-shortcut:active,
        .evt-item:active,
        .adoption-kpi:active,
        .dir-mx-card:active,
        .saved-views__enlace:active,
        .row-link:active,
        .hchip:active,
        .fav-btn:active,
        .back-link:active,
        .panel__link:active {
            transform: none;
            background: var(--muted-bg);
        }
    }
/* =====================================================================
   SUELO DE ACCESIBILIDAD TÁCTIL — fuera de toda capa, a propósito
   =====================================================================

   ESTA ES LA ÚNICA REGLA DEL PROYECTO QUE VIVE FUERA DE @layer, y el motivo
   está medido, no supuesto.

   El bloque `@media (any-pointer: coarse)` de más arriba ya fijaba 44 px para ocho
   selectores. No servía: `.quick-filters__link` computaba 34 px y medía 39,
   porque `campaign.css:1237` la redeclara con la MISMA especificidad y su hoja
   carga después. Las dos viven en `@layer legacy`, así que la capa no protegía
   nada: dentro de una capa la precedencia la decide el orden de carga.

   El orden declarado en la línea 17 pone `legacy` la ÚLTIMA, de modo que
   ninguna capa nueva puede ganarle. La única posición que sí gana es fuera de
   toda capa: una declaración normal sin capa vence a todas las declaraciones
   normales que estén en capas, sea cual sea la hoja y el orden de carga.

   Por eso el suelo táctil vive aquí y no en `componentes`. No es una excusa
   para escribir CSS sin capa: es la excepción deliberada de un contrato de
   accesibilidad que NO debe poder anularse por accidente al mover un
   componente de sitio. Si mañana alguien migra `.quick-filters__link` a su
   capa definitiva, este suelo sigue en pie.

   Se aplica sólo con dedo, no en escritorio: inflar la densidad de una pantalla
   de trabajo con ratón sería empeorarla para el otro 60 % de las sesiones.
   ===================================================================== */
@media (any-pointer: coarse) {

    /* Botones y controles de formulario. --control-h se define en el tramo E;
       hasta entonces el suelo es el número, que es lo que exige la norma. */
    .btn,
    button,
    input[type="button"],
    input[type="submit"],
    input[type="reset"],
    select,
    textarea,
    summary.btn {
        min-height: 44px;
    }

    input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]) {
        min-height: 44px;
    }

    /* Casillas y radios: no se estiran a 44 px porque se dibujarían enormes.
       Suben a 24 px, que es el mínimo de WCAG 2.5.8 para el propio control, y
       el objetivo de 44 lo aporta su etiqueta —que es lo que de verdad se
       toca—. Donde no haya etiqueta, la casilla no cumple: eso se arregla en
       el marcado, no aquí. */
    input[type="checkbox"],
    input[type="radio"] {
        width: 24px;
        height: 24px;
    }
    /* Sólo ALTURA, nunca `display`.
       La primera versión ponía `display: inline-flex` a toda etiqueta con una
       casilla dentro, y eso no es agrandar un objetivo: es cambiar el contexto
       de formato de un componente. Se llevaba por delante los mosaicos de
       categoría de solicitud, el selector de concejos y las tres opciones de
       pulso, que son rejillas cuyas superficies rellenan la celda: convertidos
       en contenedores flex dejaban de rellenarla.
       Es la misma clase de colisión que ya provocó la regresión de `.cmd-item`,
       y ninguna instantánea del arnés cubre esos tres componentes. Un suelo
       que gana a todas las capas tiene que tocar lo mínimo: el tamaño del
       objetivo y nada más. */
    label:has(> input[type="checkbox"]),
    label:has(> input[type="radio"]) {
        min-height: 44px;
    }

    /* Enlaces que son controles: pestañas, filtros, pies de tarjeta y —sobre
       todo— el TÍTULO DE CADA FILA, que es el elemento más tocado de la
       aplicación y medía 21 px de alto. */
    .quick-filters__link,
    /* Los enlaces del resumen de errores median 18 px de alto. Nadie lo habia
       visto porque NINGUNA de las 32 pantallas del arnes muestra un formulario
       con errores: ese estado solo existe despues de un POST fallido. Lo canto
       el invariante A-2 en cuanto la galeria lo pinto. Y es justo el enlace que
       mas falta hace acertar con el dedo: el que lleva al campo que esta mal. */
    .form-errors a,
    .saved-views__enlace,
    /* Todos estos son el ENLACE PRINCIPAL de su fila o tarjeta: el titulo de una
       tarea, el de una mencion, el nombre de un concejo en el ranking, el chip
       de la portada y el enlace al briefing. Median entre 20 y 42 px. No se via
       porque el arnes solo mide pantallas con datos del banco, y ninguna pinta
       a la vez la portada de Mi dia con tareas vencidas, el ranking de
       Estadisticas y el briefing con huecos. La galeria si. */
    /* Los chips de tarea de todo el dia del calendario median 24 px. Solo
       aparecen cuando hay tareas sin hora en la semana, que es un caso que el
       banco normal no produce: lo canto el banco saturado (S-1). En tactil, un
       objetivo de 24 px dentro de una rejilla densa es de los peores que hay. */
    .cal-chip,
    .myday-task__title,
    .myday-mention__body,
    .myday-event__title,
    .dir-st-top__name,
    .briefing-card__title h3 a,
    .dir-brief__open,
    .hchip,
    /* N-09. La pestana de seccion nacio de partir `.quick-filters` en dos, y
       el suelo tactil se quedo con el nombre viejo: el invariante A-2 lo canto
       en Direccion a los treinta segundos, con 35 px de alto. Partir un
       componente parte tambien lo que lo protegia. */
    .section-tabs__link,
    .panel__link,
    .myday-foot-link,
    .back-link,
    .skip-link,
    .link-strong,
    .row-link,
    .pill-tab,
    .seg__btn,
    .icon-btn,
    .topbar__menu,
    .data-table td a,
    .data-table th a {
        min-height: 44px;
        min-width: 44px;
        display: inline-flex;
        align-items: center;
        justify-content: flex-start;
    }

    /* Estos dos controles solo contienen un icono. Al ampliar su superficie
       táctil no deben heredar la alineación de inicio de enlaces con texto. */
    .topbar__menu,
    .topbar__zone--end > .icon-btn {
        justify-content: center;
    }

    /* Casilla de selección masiva: el objetivo es su etiqueta, no el dibujo
       del control. Sin ella, una casilla de 24 px es un objetivo de 24 px por
       mucho que la norma admita ese mínimo para el control en sí. */
    .bulk-check-hit {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        min-width: 44px;
        min-height: 44px;
        margin: -10px;
        cursor: pointer;
    }

    /* El buscador de la cabecera y su caja. La caja llevaba 44 y el campo de
       dentro 40: el objetivo real es el campo. */
    .topbar__search,
    .topbar__search input,
    .search-bar input {
        min-height: 44px;
    }

    /* Enlaces que ocupan su propia línea: elementos de lista, valores de ficha,
       pies de panel, pestañas de aviso y la tira de hitos. Son controles, no
       texto corrido, y medían entre 18 y 23 px de alto. */
    /* Lista EXPLÍCITA, no «li > a». La primera versión usaba ese selector
       genérico y le puso `display: inline-flex` a .cmd-item, que es un enlace
       dentro de un <li> y acababa de pasar a rejilla para arreglar P-01: el
       ancho cero volvió al instante bajo (pointer: coarse). Lo cazó el propio
       invariante R-1 en la corrida siguiente, pero el aviso es claro: un suelo
       de accesibilidad que gana a todas las capas no puede permitirse
       selectores anchos. */
    .day-focus-list a,
    .dash-list__main a,
    .utask__main a,
    .dir-load__name a,
    .myday-list a,
    .territory-work-list a,
    .notif-list a,
    .scope-coverage-card a,
    .scope-user-card,
    .dash-list__item > a,
    dd > a,
    .panel__foot a,
    .milestone-strip a,
    .dir-agenda__title,
    .login-forgot,
    .detail-value a {
        min-height: 44px;
        min-width: 44px;
        display: inline-flex;
        align-items: center;
    }

    /* Alternativa explícita a un control exento por geometría esencial. La
       clase es propia para no cambiar por orden el primer `.btn` que ancla la
       línea base estructural. */
    .a2-equivalent-link {
        min-height: 44px;
        min-width: 44px;
        display: inline-flex;
        align-items: center;
    }

    /* El resumen de un <details> conserva su display de list-item para no
       perder el triángulo: aquí sólo se le da altura. */
    summary {
        min-height: 44px;
    }

    /* Los botones de icono no llevan texto que los ensanche. Se hace sobre
       todo botón porque los que sí llevan texto ya lo superan de sobra. */
    .btn,
    button,
    .icon-btn,
    .topbar__menu {
        min-width: 44px;
    }
}

/* =====================================================================
   SUELO DE FOCO VISIBLE — fuera de toda capa, por el mismo motivo
   =====================================================================

   P-15. El buscador de la cabecera y el de las listas llevaban `outline: none`
   sin sustituto, y NO era por descuido: el sustituto estaba escrito.
   `.topbar__search:focus-within` (línea 1263) fija `border-color` y
   `box-shadow`, y nunca llegó a aplicarse porque
   `body.app-body .topbar__search` (línea 813) redeclara las tres propiedades
   —fondo, borde y sombra— con más especificidad y sin condición de foco. Las
   dos viven en `@layer legacy`. El estilo de foco existía, estaba bien pensado
   y perdía la cascada en silencio: nadie lo notó porque un estilo que pierde
   no deja error, sólo deja de verse.

   Es la misma lección que el suelo táctil de arriba, y por eso vive al lado y
   con la misma técnica: fuera de toda capa. Una declaración normal sin capa
   vence a todas las declaraciones normales que estén en capas, sea cual sea la
   especificidad y el orden de carga. Así, el indicador de foco no depende de
   quién gane una pelea de selectores que nadie está arbitrando.

   POR QUÉ `outline` Y NO `box-shadow`. Porque `outline` no es lo que usan los
   estilos que perdían. Este suelo no discute con el diseño: se suma a él. Un
   componente que ya pinta un buen anillo lo sigue pintando, y encima queda
   garantizado el contorno. Y `outline` no ocupa espacio, así que no descoloca
   ninguna rejilla al aparecer.

   POR QUÉ `:focus-visible` Y NO `:focus`. Para no dibujar el contorno al hacer
   clic con el ratón, que es lo que llevó a alguien a escribir `outline: none`
   en primer lugar. Quien navega con teclado lo ve; quien usa ratón, no.

   POR QUÉ SE APLICA SIEMPRE, y no sólo con `any-pointer: coarse` como el suelo
   táctil: el teclado es sobre todo de escritorio. Restringirlo al dedo sería
   dejarlo fuera justo donde hace falta.

   COLOR. #10305c sobre los fondos claros del producto da 11,6 : 1, muy por
   encima del 3 : 1 que pide 1.4.11 para un indicador. Va con `outline-offset`
   para que se lea también sobre controles de fondo azul de marca, donde un
   contorno pegado se confundiría con el borde del propio botón.
   ===================================================================== */
:where(
    a[href],
    area[href],
    button,
    input,
    select,
    textarea,
    summary,
    [tabindex]:not([tabindex="-1"]),
    [role="button"],
    [role="option"],
    [role="tab"],
    [contenteditable="true"]
):focus-visible {
    outline: 3px solid #10305c;
    outline-offset: 2px;
}

/* ---------------------------------------------------------------------
   Contrato dimensional de los controles (E-c)
   ---------------------------------------------------------------------

   Vive aquí, en la capa `accesibilidad`, por el mismo motivo que los dos
   suelos: es un contrato que no debe poder perderse por accidente en una
   pelea de especificidad dentro de `legacy`, que es donde vive casi todo el
   CSS de componentes. Con `:where(...)` tiene especificidad cero, así que un
   componente que necesite otra altura la declara con su selector normal y
   gana, igual que la anilla de foco del aviso de «Mi día».

   Se aplica sólo donde NO hay ningún puntero grueso. Donde lo hay manda el
   suelo táctil de 44 px, que está más arriba en esta misma capa y es más alto:
   la diferencia de densidad entre puntero fino y grueso es deliberada.

   POR QUÉ `any-pointer` Y NO `pointer`. Porque `pointer` describe sólo el
   dispositivo PRIMARIO. Un portátil con pantalla táctil y trackpad cumple
   `pointer: fine` y `any-pointer: coarse` a la vez: con la condición anterior
   recibía controles de 40 px y NO recibía el suelo táctil de 44, que también
   preguntaba por el puntero primario. O sea que el dispositivo donde más falta
   hace un objetivo grande —el que se toca con el dedo sobre una interfaz
   pensada para ratón— era justo el que se quedaba sin él.

   Lo señaló una revisión externa. Ni el contrato ni el suelo lo contemplaban, y
   ninguna de las 192 capturas lo habría visto: el arnés emula un puntero u
   otro, nunca los dos.
   --------------------------------------------------------------------- */

/* Los <select> nativos centran su texto dentro de la caja del sistema. El
   padding vertical heredado de .form-field reducía además su caja de contenido
   y PP Neue Montreal perdía los descendentes, sobre todo en los filtros
   compactos. Conservamos el padding horizontal y dejamos al control nativo
   resolver el centrado vertical a cualquier altura. */
:where(select:not([multiple])) {
    padding-block: 0;
}

/* ---------------------------------------------------------------------
   SUELO TIPOGRÁFICO DE LOS CAMPOS EDITABLES

   16 px o más en todo lo que se pueda escribir. No es estética: por debajo de
   16 px, iOS Safari amplía la página al enfocar el campo y deja al usuario con
   la interfaz desencuadrada.

   Estaba sin declarar. Lo sostenía por accidente la normalización
   `input, button, textarea, select { font: inherit }`, que ganaba por orden de
   carga dentro de `legacy`; al debilitarla en el primer intento de D-2, seis
   pantallas bajaron sus campos a 14 px. Un contrato que depende de que nadie
   reordene una hoja no es un contrato.

   Se aplica SÓLO con puntero grueso: en escritorio la densidad de una pantalla
   de trabajo es una decisión legítima, y ahí no hay zoom que evitar.
   --------------------------------------------------------------------- */
@media (any-pointer: coarse) {
    input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]),
    select,
    textarea {
        font-size: max(16px, 1em);
    }
}

@media not all and (any-pointer: coarse) {
    /* Los campos de UNA LÍNEA llevan altura, no altura mínima. `min-height` no
       puede encoger, y el problema medido era justamente por arriba: en la
       pantalla de Tareas convivían un campo de 38 px, otro de 46 y otro de 48,
       todos con la misma función. Un mínimo los habría dejado igual de
       desiguales. Un `select` y un `input` de texto tienen una altura bien
       definida, así que se les da. */
    :where(
        input[type="text"],
        input[type="search"],
        input[type="email"],
        input[type="password"],
        input[type="url"],
        input[type="tel"],
        input[type="number"],
        input[type="date"],
        input[type="time"],
        input[type="datetime-local"],
        input:not([type]),
        select
    ) {
        height: var(--control-h);
        box-sizing: border-box;
    }

    /* Los botones llevan MÍNIMO, no altura: su texto puede ocupar dos líneas
       —«Guardar y crear otra», «Plantillas de checklist»— y recortarlo a una
       altura fija sería cambiar un defecto de acabado por uno de contenido. */
    :where(.btn, button) {
        min-height: var(--control-h);
    }

    /* Variante compacta: barras de filtros y todo lo que se declare pequeño. */
    :where(.btn--sm, .btn--xs) {
        min-height: var(--control-h-sm);
    }
    :where(.filters-bar input, .filters-bar select) {
        height: var(--control-h-sm);
    }

    /* Un textarea crece con su contenido: fijarle altura sería el defecto que
       esto viene a arreglar, no su arreglo. Se le da el mínimo y nada más. */
    :where(textarea) {
        min-height: calc(var(--control-h) * 2);
    }
}

/* La tarjeta de Avisos de «Mi día» es blanca y su contorno cae fuera de ella,
   sobre la fotografía de paisaje que pinta `.main-content::before`. Medido
   sobre el píxel real —rgb(105, 134, 152)— el blanco da 3,84 : 1 y el azul del
   suelo daría 3,42 : 1: los dos cumplen 1.4.11, así que esto no arregla nada
   roto. Se mantiene porque es la anilla que el componente eligió y porque
   ahora, con la capa `accesibilidad`, un componente PUEDE elegir: la
   discrepancia está aquí, a la vista y con su medición al lado, en vez de
   escondida en `legacy` ganando o perdiendo por accidente.

   Lo vigila tools/uiharness/teclado.mjs, que ante un fondo con imagen deja de
   deducir y lee el píxel de la pantalla. */
.myday-hero__avisos:focus-visible {
    outline: 2px solid #fff;
    outline-offset: 3px;
}

/* El contenedor del buscador reacciona junto a su campo: el campo no tiene
   borde propio —lo pone la caja— y un contorno alrededor de un input invisible
   dentro de una caja que no se inmuta se lee como un fallo de pintado. */
.topbar__search:focus-within,
.search-bar:focus-within {
    outline: 3px solid #10305c;
    outline-offset: 2px;
}
.topbar__search:focus-within input:focus-visible,
.search-bar:focus-within input:focus-visible {
    outline: none;
}

}
