/* GENERADO por tools/tokens/generar-tokens.ps1 desde ds/tokens/core.json: NO editar a mano.
   Tema claro en :root; tema oscuro en [data-theme="dark"] (solo lo que cambia). */
:root {
  /* Tipografía */
  /* f-display: Jost, geométrica de la familia del logotipo. Solo títulos y cifras grandes, en peso 500. */
  --f-display: 'Jost','Century Gothic',system-ui,sans-serif;
  /* f-ui: Figtree, todo lo demás (texto, controles, tablas). */
  --f-ui: 'Figtree',system-ui,'Segoe UI',sans-serif;
  --f-mono: 'JetBrains Mono',ui-monospace,monospace;

  /* Verde (marca) */
  --g-50: #F7FAF1;
  --g-100: #EFF5E4;
  --g-200: #E2EDD0;
  --g-300: #CFE1B4;
  /* g-400: verde claro de la paleta oficial. */
  --g-400: #B9D394;
  --g-500: #A1B97F;
  /* g-600: verde olivo de la paleta oficial, el color de la marca. Lleva texto oscuro: con blanco no llega a 4.5:1. */
  --g-600: #889E6A;
  --g-700: #738957;
  /* g-800: el verde de la acción (relleno de la primaria, enlaces): blanco encima a 5.2:1. */
  --g-800: #5F7347;
  --g-900: #46552F;
  --g-950: #2F3A1F;

  /* Coral (acento) */
  --c-50: #FEF7F5;
  --c-100: #FDEFEC;
  --c-200: #FBE2DD;
  --c-300: #F7CBC3;
  --c-400: #F2B3A9;
  /* c-500: coral de la paleta oficial. Acento único, reservado a la acción de conversión (agendar, escribir por WhatsApp) y a los destacados. Lleva texto oscuro. */
  --c-500: #EC9A8F;
  --c-600: #D98272;
  --c-700: #BF6858;
  --c-800: #9A5244;
  --c-900: #6F3B31;

  /* Neutros */
  --n-0: #FFFFFF;
  --n-50: #F8F9F4;
  --n-100: #EFF1E9;
  --n-200: #E0E4D7;
  --n-300: #C9CFBF;
  --n-400: #A9B09F;
  --n-500: #8B9281;
  /* n-550 y n-650: pasos intermedios para cumplir AA sin oscurecer de más.
     n-550 es el borde de controles en claro (3:1 sobre surface, sunken, bg y raised). */
  --n-550: #7F8775;
  --n-600: #6E7663;
  /* n-650 es el texto terciario en claro (4.5:1 sobre surface, sunken, bg y raised). */
  --n-650: #646C5A;
  --n-700: #565E4C;
  --n-800: #414838;
  --n-900: #2F3527;
  --n-950: #23281D;

  /* Semánticos */
  --success: #2B7D3E;
  --success-soft: #EAF5EC;
  --success-bd: #C6E6CE;
  /* warning es el único semántico con dos papeles: el ámbar naranja para rellenos,
     iconos y marcas (3.4:1 sobre blanco, AA para gráficos) y warning-fg para texto sobre
     warning-soft (un amarillo legible como texto se ve café como relleno). En oscuro los
     dos papeles convergen en el mismo ámbar claro. */
  --warning: #D97706;
  --warning-fg: #8F6106;
  --warning-soft: #FFF8E1;
  --warning-bd: #FDEAA8;
  /* danger es rojo, no coral: el coral es la acción de conversión y no puede leerse como error. */
  --danger: #C62828;
  --danger-soft: #FDE8E8;
  --danger-bd: #F9C4C4;
  --info: #1565C0;
  --info-soft: #E4F0FA;
  --info-bd: #B8D7F2;
  /* review: hecho y a la espera de una decisión humana (registrado, completado). */
  --review: #6A4FB6;
  --review-soft: #EFEBF8;
  --review-bd: #D3C8EE;

  /* Roles */
  --bg: var(--n-50);
  --surface: var(--n-0);
  --sunken: var(--n-100);
  --raised: var(--n-0);
  --fg: var(--n-900);
  --fg-2: var(--n-700);
  --fg-3: var(--n-650);
  --fg-4: var(--n-400);
  --line: var(--n-200);
  --line-2: var(--n-300);
  /* line-control: el borde que IDENTIFICA a un control (caja de un campo, celda de código, riel
     del interruptor, disparador del desplegable): 3:1 contra surface, sunken, bg y raised en
     los dos temas (WCAG 1.4.11). Botones, divisores y bordes decorativos siguen en line/line-2. */
  --line-control: var(--n-550);
  --divider: var(--n-100);
  --acc: var(--g-800);
  --acc-h: var(--g-900);
  --acc-p: var(--g-950);
  --acc-fg: #FFFFFF;
  --acc-soft: var(--g-100);
  --acc-soft-2: var(--g-200);
  /* cta: la acción de conversión (boton--conversion). Una por vista como máximo y nunca junto
     a una primaria. Texto oscuro (cta-fg) en los dos temas: 6.9:1 en claro. */
  --cta: var(--c-500);
  --cta-h: var(--c-600);
  --cta-p: var(--c-600);
  --cta-fg: var(--n-950);
  --cta-soft: var(--c-100);
  --link: var(--g-800);
  --link-h: var(--g-900);
  /* ring: g-700 en claro llega a 3:1 contra todos los fondos donde puede quedar un control
     (surface, raised, bg, sunken, acc-soft); g-600 quedaba en 2.6:1 sobre sunken. Lo verifica
     tools/accesibilidad. */
  --ring: var(--g-700);
  /* ring-capa: el mismo anillo que ring, para restaurarlo en una capa clara que vive dentro de una región de marca (el desplegable del menú de usuario), donde ring se redefine a blanco. */
  --ring-capa: var(--g-700);
  --scrim: rgba(47,53,39,.45);
  --sk-a: var(--n-200);
  --sk-b: var(--n-100);

  /* Superficie de marca */
  /* marca: la superficie de la marca (riel lateral del shell, panel de acceso, encabezado del
     cajón). Es la misma en los dos temas, igual que lo que va encima. */
  --marca: var(--g-900);
  --on-marca: #FFFFFF;
  /* on-marca-2: texto secundario sobre la marca (5.8:1). on-marca-3: iconos apagados (4.9:1). */
  --on-marca-2: var(--g-300);
  --on-marca-3: var(--g-400);

  /* Series de gráfica */
  /* serie-1…4: las series de una gráfica, en este orden. Solo distinguen: ningún tono semántico.
     Cada una llega a 3:1 sobre surface en los dos temas (WCAG 1.4.11). La 1 es la principal;
     la 2, apagada, la comparación (el periodo anterior, lo proyectado). */
  --serie-1: var(--g-800);
  --serie-2: var(--n-550);
  --serie-3: var(--c-700);
  --serie-4: var(--g-950);

  /* Elevación */
  --e1: 0 1px 2px rgba(47,53,39,.05),0 1px 3px rgba(47,53,39,.04);
  --e2: 0 2px 6px rgba(47,53,39,.06),0 1px 2px rgba(47,53,39,.04);
  --e3: 0 6px 20px rgba(47,53,39,.08),0 2px 4px rgba(47,53,39,.04);
  --e4: 0 16px 40px rgba(47,53,39,.12),0 4px 8px rgba(47,53,39,.05);

  /* Radio */
  --r-xs: 6px;
  --r-sm: 8px;
  /* r-md: controles (campos, botones). r-lg: tarjetas. r-xl: modales e imágenes destacadas. */
  --r-md: 12px;
  --r-lg: 20px;
  --r-xl: 28px;
  /* r-pildora: botones de la landing y del formulario de pacientes (boton--pildora), insignias, fichas e interruptores. */
  --r-pildora: 999px;
  /* r-blob: la forma orgánica de las fotografías de la marca (landing). Solo imágenes, nunca controles ni tarjetas. */
  --r-blob: 60% 40% 30% 70% / 60% 30% 70% 40%;

  /* Espaciado */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 24px;
  --sp-6: 32px;
  --sp-7: 48px;
  --sp-8: 64px;

  /* Medidas de control */
  --ctl-y: 10px;
  --ctl-x: 13px;
  --row-y: 14px;
  --gap: 24px;

  /* Movimiento */
  --fast: 120ms;
  --med: 200ms;
  /* slow: modales y cajones (entran con fundido y suben 12px). Sin rebote ni parallax. */
  --slow: 320ms;
  --ease: cubic-bezier(.2,.7,.2,1);
}

/* Tema oscuro: las escalas (g, c, n) no cambian; se redefinen roles, semánticos y
   elevaciones. Se activa con data-theme="dark" en <html> (lo gobierna tema.js). */
[data-theme="dark"] {
  /* Semánticos */
  --success: #7DC990;
  --success-soft: #14301F;
  --success-bd: #1F4A2E;
  --warning: #F6C94C;
  --warning-fg: #F6C94C;
  --warning-soft: #33260A;
  --warning-bd: #54400F;
  --danger: #EF7676;
  --danger-soft: #3A1717;
  --danger-bd: #5C2424;
  --info: #6AAFE3;
  --info-soft: #0F2A44;
  --info-bd: #1B4266;
  --review: #B7A5E8;
  --review-soft: #241B3D;
  --review-bd: #3A2D63;

  /* Roles */
  --bg: #14180F;
  --surface: #1B2015;
  --sunken: #11140C;
  --raised: #232A1B;
  --fg: var(--n-100);
  --fg-2: var(--n-300);
  --fg-3: var(--n-400);
  --fg-4: var(--n-500);
  --line: var(--n-800);
  --line-2: var(--n-700);
  --line-control: var(--n-500);
  --divider: #232A1B;
  --acc: var(--g-400);
  --acc-h: var(--g-300);
  --acc-p: var(--g-500);
  --acc-fg: #14180F;
  --acc-soft: #2B3520;
  --acc-soft-2: #344026;
  --cta: var(--c-400);
  --cta-h: var(--c-300);
  --cta-p: var(--c-500);
  --cta-fg: var(--n-950);
  --cta-soft: #3A2420;
  --link: var(--g-300);
  --link-h: var(--g-200);
  --ring: var(--g-400);
  --ring-capa: var(--g-400);
  --scrim: rgba(8,10,6,.62);
  --sk-a: #2B3520;
  --sk-b: #1F2619;

  /* Series de gráfica */
  --serie-1: var(--g-300);
  --serie-2: var(--n-500);
  --serie-3: var(--c-400);
  --serie-4: var(--g-500);

  /* Elevación */
  --e1: 0 1px 2px rgba(0,0,0,.5);
  --e2: 0 4px 10px rgba(0,0,0,.5);
  --e3: 0 8px 24px rgba(0,0,0,.55);
  --e4: 0 16px 48px rgba(0,0,0,.6);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    /* Semánticos */
    --success: #7DC990;
    --success-soft: #14301F;
    --success-bd: #1F4A2E;
    --warning: #F6C94C;
    --warning-fg: #F6C94C;
    --warning-soft: #33260A;
    --warning-bd: #54400F;
    --danger: #EF7676;
    --danger-soft: #3A1717;
    --danger-bd: #5C2424;
    --info: #6AAFE3;
    --info-soft: #0F2A44;
    --info-bd: #1B4266;
    --review: #B7A5E8;
    --review-soft: #241B3D;
    --review-bd: #3A2D63;

    /* Roles */
    --bg: #14180F;
    --surface: #1B2015;
    --sunken: #11140C;
    --raised: #232A1B;
    --fg: var(--n-100);
    --fg-2: var(--n-300);
    --fg-3: var(--n-400);
    --fg-4: var(--n-500);
    --line: var(--n-800);
    --line-2: var(--n-700);
    --line-control: var(--n-500);
    --divider: #232A1B;
    --acc: var(--g-400);
    --acc-h: var(--g-300);
    --acc-p: var(--g-500);
    --acc-fg: #14180F;
    --acc-soft: #2B3520;
    --acc-soft-2: #344026;
    --cta: var(--c-400);
    --cta-h: var(--c-300);
    --cta-p: var(--c-500);
    --cta-fg: var(--n-950);
    --cta-soft: #3A2420;
    --link: var(--g-300);
    --link-h: var(--g-200);
    --ring: var(--g-400);
    --ring-capa: var(--g-400);
    --scrim: rgba(8,10,6,.62);
    --sk-a: #2B3520;
    --sk-b: #1F2619;

    /* Series de gráfica */
    --serie-1: var(--g-300);
    --serie-2: var(--n-500);
    --serie-3: var(--c-400);
    --serie-4: var(--g-500);

    /* Elevación */
    --e1: 0 1px 2px rgba(0,0,0,.5);
    --e2: 0 4px 10px rgba(0,0,0,.5);
    --e3: 0 8px 24px rgba(0,0,0,.55);
    --e4: 0 16px 48px rgba(0,0,0,.6);
  }
}

/* Fuentes de la marca vendored (woff2 variable, subset latin: incluye acentos, ñ, ¿ y ¡).
   font-display: swap — el primer render ocurre antes de que la fuente cargue, así que
   la pila de respaldo de cada token (--f-display/--f-ui/--f-mono) debe ser legible.
   Jost: títulos (geométrica, de la familia del logotipo). Figtree: todo lo demás. */

@font-face {
  font-family: 'Jost';
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url('/fonts/jost-latin.woff2') format('woff2');
}

@font-face {
  font-family: 'Jost';
  font-style: italic;
  font-weight: 300 700;
  font-display: swap;
  src: url('/fonts/jost-latin-italic.woff2') format('woff2');
}

@font-face {
  font-family: 'Figtree';
  font-style: normal;
  font-weight: 300 800;
  font-display: swap;
  src: url('/fonts/figtree-latin.woff2') format('woff2');
}

@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('/fonts/jetbrains-mono-latin.woff2') format('woff2');
}

/* Base de la librería de componentes del DS, construida sobre BELDS v2.26.0.
   Solo consume tokens de ds/build/tokens.css. Maquetación con var(--sp-*); el
   espaciado interno de cada componente es libre (RD-015). */

@layer reset, base, componentes;

@layer reset {
  *,
  *::before,
  *::after {
    box-sizing: border-box;
  }

  body,
  h1, h2, h3, h4,
  p,
  figure,
  ul, ol {
    margin: 0;
  }

  img {
    max-width: 100%;
    display: block;
  }

  /* El atributo hidden gana siempre, aunque el componente fije su display
     (regla UA pierde contra .boton { display: inline-flex }, etc.). */
  [hidden] {
    display: none !important;
  }
}

@layer base {
  html {
    height: 100%;
    /* Controles nativos (scrollbars, campos de fecha, selects) en el tema en curso.
       Los tokens del oscuro viven en ds/build/tokens.css; tema.js gobierna data-theme. */
    color-scheme: light;
  }

  html[data-theme="dark"] {
    color-scheme: dark;
  }

  @media (prefers-color-scheme: dark) {
    html:not([data-theme="light"]) {
      color-scheme: dark;
    }
  }

  body {
    min-height: 100%;
    background: var(--bg);
    color: var(--fg);
    font-family: var(--f-ui);
    font-size: 14px;
    line-height: 1.55;
    -webkit-font-smoothing: antialiased;
  }

  h1, h2, h3 {
    /* Serif solo para títulos (RD-004) */
    font-family: var(--f-display);
    font-weight: 400;
    line-height: 1.24;
  }

  h1 { font-size: clamp(24px, 3vw, 30px); }
  h2 { font-size: clamp(20px, 2.5vw, 25px); }
  h3 { font-size: clamp(17px, 2vw, 20px); }

  a {
    color: var(--link);
    text-decoration: none;
    transition: color var(--fast) var(--ease);

    &:hover {
      color: var(--link-h);
    }
  }

  code,
  pre {
    font-family: var(--f-mono);
  }

  /* Foco visible como COMPORTAMIENTO POR OMISIÓN (RD-021): el anillo existe para
     cualquier elemento sin lista que lo otorgue. Suprimirlo se decide caso por
     caso y SOLO donde el control ya muestre su propio foco (borde + halo) —
     nunca "porque la regla global ya lo quitó": esa supresión parece inofensiva
     hasta que la regla se invierte. */
  :focus-visible {
    outline: 2px solid var(--ring);
    outline-offset: 2px;
  }

  /* Desplazamiento reducido en campos de formulario (valor del documento) */
  input:focus-visible,
  textarea:focus-visible,
  select:focus-visible {
    outline-offset: 1px;
  }

  ::selection {
    background: var(--acc-soft-2);
  }

  /* Carga en curso (htmx, carga.js): el bloque que espera se atenúa, no recibe
     clics y una barra indeterminada de 2px recorre su borde superior — el
     esqueleto de la tarjeta, no un giro. El atributo lo pone carga.js con 150 ms
     de gracia; quien lo ponga a mano obtiene el mismo tratamiento. */
  [aria-busy="true"] {
    position: relative;
    pointer-events: none;
    cursor: progress;
  }

  [aria-busy="true"] > * {
    opacity: 0.55;
    transition: opacity var(--fast) var(--ease);
  }

  [aria-busy="true"]::before {
    content: "";
    position: absolute;
    inset-block-start: 0;
    inset-inline: 0;
    block-size: 2px;
    z-index: 2;
    border-radius: 2px;
    background: linear-gradient(90deg, transparent 35%, var(--acc) 50%, transparent 65%);
    background-size: 200% 100%;
    animation: carga-barra 1.1s var(--ease) infinite;
  }

  @keyframes carga-barra {
    from { background-position: 120% 0; }
    to { background-position: -20% 0; }
  }

  @media (prefers-reduced-motion: reduce) {
    [aria-busy="true"]::before {
      animation: none;
      background: var(--acc);
    }
  }
}

/*!inventario
{
  "componente": "Acceso",
  "proposito": "Pantallas de autenticación fuera del shell (iniciar sesión, cambiar o restablecer contraseña, segundo factor, sesión cerrada): layout partido con el panel de marca (verde) a la izquierda y el formulario de una columna a la derecha; en ventanas angostas el panel se reduce a una banda superior. La fotografía del panel es opcional y la pone la app en la custom property --acceso-imagen, desde su propio CSS (un atributo style lo bloquea la CSP de la app). El formulario lleva data-envio-unico: envio-unico.js evita el doble envío y muestra el velo de carga.",
  "raiz": "acceso",
  "variantes": {},
  "estados": {
    "[data-enviado='si']": "En el form: ya se envió (lo pone envio-unico.js); un segundo envío se descarta"
  },
  "modificadores": {
    "acceso__insignia--advertencia": "Insignia en tono warning (la contraseña vence, algo que revisar)",
    "acceso__insignia--peligro": "Insignia en tono danger (acceso denegado, cuenta bloqueada)",
    "acceso__insignia--exito": "Insignia en tono success (sesión cerrada, contraseña cambiada)"
  },
  "estructura": [
    { "clase": "acceso__panel", "elemento": "aside", "papel": "Panel de marca sobre la superficie de marca; fotografía opcional bajo velo de marca con --acceso-imagen: url(…) en el CSS de la app (.acceso__panel { --acceso-imagen: url(/images/acceso.jpg); })", "opcional": false },
    { "clase": "acceso__marca", "elemento": "div", "papel": "Logotipo blanco + separador + nombre y descripción (separador y textos opcionales: el logotipo ya lleva nombre)", "opcional": false },
    { "clase": "acceso__marca-logo", "elemento": "img", "papel": "El logotipo blanco (46px de alto; 34 en banda)", "opcional": false },
    { "clase": "acceso__marca-separador", "elemento": "span", "papel": "Filo vertical entre logo y textos", "opcional": true },
    { "clase": "acceso__marca-textos", "elemento": "div", "papel": "Nombre + descripción", "opcional": true },
    { "clase": "acceso__marca-nombre", "elemento": "span", "papel": "El nombre de la sección (p. ej. Panel) en display, mayúsculas espaciadas", "opcional": true },
    { "clase": "acceso__marca-descripcion", "elemento": "span", "papel": "La línea descriptiva en mono", "opcional": true },
    { "clase": "acceso__panel-pie", "elemento": "div", "papel": "Lema, filo y texto legal al pie del panel (se oculta en banda)", "opcional": true },
    { "clase": "acceso__lema", "elemento": "span", "papel": "Frase de la marca en display", "opcional": true },
    { "clase": "acceso__lema-filo", "elemento": "span", "papel": "Filo corto bajo el lema", "opcional": true },
    { "clase": "acceso__legal", "elemento": "span", "papel": "Aviso legal breve sobre la marca", "opcional": true },
    { "clase": "acceso__forma", "elemento": "main", "papel": "Columna del formulario; ancla del velo de carga", "opcional": false },
    { "clase": "acceso__cuerpo", "elemento": "div", "papel": "Columna centrada de 44ch: emblema, encabezado, avisos, campos y pie", "opcional": false },
    { "clase": "acceso__emblema", "elemento": "img", "papel": "Logotipo a color (logo-nutrimav-color.png) sobre el encabezado; en tema oscuro se vuelve blanco", "opcional": true },
    { "clase": "acceso__encabezado", "elemento": "div", "papel": "Insignia, título y bajada de la pantalla", "opcional": false },
    { "clase": "acceso__insignia", "elemento": "span > svg.icono", "papel": "Insignia cuadrada de 38px con el icono de la pantalla (acento por omisión)", "opcional": true },
    { "clase": "acceso__titulo", "elemento": "h1", "papel": "Título en display", "opcional": false },
    { "clase": "acceso__sub", "elemento": "p", "papel": "Bajada de una o dos frases", "opcional": true },
    { "clase": "acceso__aplicacion", "elemento": "div", "papel": "Ficha de la aplicación que pidió acceso (acceso denegado, autorizar): va en lugar del encabezado, centrada", "opcional": true },
    { "clase": "acceso__aplicacion-icono", "elemento": "img", "papel": "Icono de la aplicación, 72px, con borde y radio; sin icono propio, icono-aplicacion.svg", "opcional": true },
    { "clase": "acceso__aplicacion-nombre", "elemento": "h1", "papel": "Nombre de la aplicación en display", "opcional": true },
    { "clase": "acceso__aplicacion-descripcion", "elemento": "p", "papel": "Descripción breve de la aplicación", "opcional": true },
    { "clase": "acceso__campos", "elemento": "form", "papel": "El formulario: campos apilados con --sp-4; lleva data-envio-unico", "opcional": false },
    { "clase": "acceso__fila", "elemento": "div", "papel": "Fila auxiliar bajo los campos (casilla a la izquierda, enlace a la derecha)", "opcional": true },
    { "clase": "acceso__enlace", "elemento": "a | button", "papel": "Enlace de 12.5px en peso 600 (¿Olvidaste tu contraseña?, Reenviar código)", "opcional": true },
    { "clase": "acceso__acciones", "elemento": "div", "papel": "Botones apilados a todo el ancho (boton--bloque); la primaria primero", "opcional": false },
    { "clase": "acceso__pista", "elemento": "p", "papel": "Razón por la que la primaria está deshabilitada, bajo el botón; hidden cuando no aplica", "opcional": true },
    { "clase": "acceso__pie", "elemento": "div", "papel": "Pie de la columna: icono y nota de seguridad sobre un filo", "opcional": true },
    { "clase": "acceso__pie-texto", "elemento": "span", "papel": "El texto del pie", "opcional": true },
    { "clase": "acceso__cargando", "elemento": "div", "papel": "Velo de carga sobre la columna del formulario; hidden hasta que envio-unico.js lo muestra", "opcional": false },
    { "clase": "acceso__cargando-barra", "elemento": "span", "papel": "Barra indeterminada del velo (una barra, no un giro)", "opcional": false },
    { "clase": "acceso__cargando-texto", "elemento": "span", "papel": "Texto del velo; lo pone envio-unico.js desde data-cargando del botón", "opcional": false }
  ],
  "internas": [],
  "contexto": [
    "icono",
    "boton",
    "boton--primario",
    "boton--bloque",
    "campo",
    "aviso"
  ],
  "ejemplo": [
    "<div class='acceso'>",
    "  <aside class='acceso__panel'>",
    "    <div class='acceso__marca'>",
    "      <img class='acceso__marca-logo' src='/images/logo-nutrimav-blanco.png' alt='Mariana Vargas, Nutrióloga' />",
    "      <span class='acceso__marca-separador'></span>",
    "      <div class='acceso__marca-textos'>",
    "        <span class='acceso__marca-nombre'>Panel</span>",
    "        <span class='acceso__marca-descripcion'>Agenda y pacientes</span>",
    "      </div>",
    "    </div>",
    "    <div class='acceso__panel-pie'>",
    "      <span class='acceso__lema'>Nutrición clínica con propósito.</span>",
    "      <span class='acceso__lema-filo'></span>",
    "      <span class='acceso__legal'>Acceso solo para el equipo del consultorio. Cada sesión queda registrada.</span>",
    "    </div>",
    "  </aside>",
    "  <main class='acceso__forma'>",
    "    <div class='acceso__cuerpo'>",
    "      <img class='acceso__emblema' src='/images/logo-nutrimav-color.png' alt='Mariana Vargas, Nutrióloga' />",
    "      <div class='acceso__encabezado'>",
    "        <span class='acceso__insignia'><svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-log-in' /></svg></span>",
    "        <h1 class='acceso__titulo'>Inicia sesión</h1>",
    "        <p class='acceso__sub'>Escribe tu correo y contraseña para continuar.</p>",
    "      </div>",
    "      <!-- Acceso denegado o autorizar: la ficha de la aplicación va en lugar del encabezado -->",
    "      <div class='acceso__aplicacion'>",
    "        <img class='acceso__aplicacion-icono' src='/images/icono-aplicacion.svg' alt='' />",
    "        <h1 class='acceso__aplicacion-nombre'>Panel Nutrimav</h1>",
    "        <p class='acceso__aplicacion-descripcion'>Agenda y pacientes</p>",
    "      </div>",
    "      <form class='acceso__campos' method='post' data-envio-unico>",
    "        <div class='campo'>…correo…</div>",
    "        <div class='campo'>…contraseña con campo__revelar…</div>",
    "        <div class='acceso__fila'><span></span><a class='acceso__enlace' href='/recuperar'>¿Olvidaste tu contraseña?</a></div>",
    "        <div class='acceso__acciones'>",
    "          <button type='submit' class='boton boton--primario boton--bloque' data-cargando='Iniciando sesión…'><svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-log-in' /></svg> <span data-etiqueta>Iniciar sesión</span></button>",
    "        </div>",
    "      </form>",
    "      <div class='acceso__pie'><svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-lock' /></svg><span class='acceso__pie-texto'>Tus datos viajan cifrados</span></div>",
    "    </div>",
    "    <div class='acceso__cargando' hidden aria-live='polite'><span class='acceso__cargando-barra' aria-hidden='true'></span><span class='acceso__cargando-texto'></span></div>",
    "  </main>",
    "</div>"
  ]
}
*/
/* Acceso — «Pantallas de autenticación» (sección 19 del sistema): layout partido.
   A la izquierda el panel de marca sobre la superficie de marca (con fotografía
   opcional bajo velo de marca); a la derecha el formulario, centrado y de una sola
   columna. Bajo 880 px el panel se reduce a una banda superior.

   Lo que va sobre la marca no cambia de tema: blancos traslúcidos derivados de
   --on-marca. La columna del formulario sí: consume roles (--surface, --fg…).

   La fotografía es de la app y es opcional: --acceso-imagen: url(…) en
   .acceso__panel (custom property declarada como contrato), desde el CSS de la
   app y no en un atributo style, que la CSP de la app ignora. Sin ella el
   panel queda en verde liso.

   El velo de carga sigue la regla del sistema: una barra, no un giro. Lo muestra
   envio-unico.js al enviar el formulario marcado con data-envio-unico. */

@layer componentes {
  .acceso {
    min-block-size: 100dvh;
    display: grid;
    grid-template-columns: minmax(0, 1fr) clamp(420px, 38vw, 560px);
    background: var(--surface);
  }

  /* ── Panel de marca ───────────────────────────────────────────────── */
  .acceso__panel {
    /* El anillo general (g-700) no contrasta con la marca: aquí el foco es blanco (8:1) */
    --ring: var(--on-marca);
    position: relative;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    gap: var(--sp-5);
    padding: clamp(18px, 2.6vw, 32px);
    background-color: var(--marca);
    background-image:
      linear-gradient(200deg,
        color-mix(in srgb, var(--marca) 88%, transparent),
        color-mix(in srgb, var(--marca) 70%, transparent) 42%,
        color-mix(in srgb, var(--marca) 92%, transparent)),
      var(--acceso-imagen, none);
    background-size: cover;
    background-position: center;
    color: var(--on-marca);
  }

  .acceso__marca {
    display: flex;
    align-items: center;
    gap: clamp(12px, 1.6vw, 18px);
  }

  .acceso__marca-logo {
    flex: none;
    block-size: 46px;
    inline-size: auto;
  }

  .acceso__marca-separador {
    flex: none;
    align-self: stretch;
    inline-size: 1px;
    min-block-size: 46px;
    background: color-mix(in srgb, var(--on-marca) 28%, transparent);
  }

  .acceso__marca-textos {
    display: flex;
    flex-direction: column;
    gap: 3px;
    min-inline-size: 0;
  }

  .acceso__marca-nombre {
    font-family: var(--f-display);
    font-size: clamp(15px, 1.7vw, 19px);
    line-height: 1.1;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: var(--on-marca);
  }

  .acceso__marca-descripcion {
    font-family: var(--f-mono);
    font-size: 9.5px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--on-marca-2);
  }

  .acceso__panel-pie {
    display: flex;
    flex-direction: column;
    gap: 12px;
  }

  .acceso__lema {
    font-family: var(--f-display);
    font-size: clamp(19px, 2.2vw, 27px);
    line-height: 1.26;
    color: var(--on-marca);
    text-wrap: pretty;
  }

  .acceso__lema-filo {
    inline-size: 40px;
    block-size: 2px;
    background: color-mix(in srgb, var(--on-marca) 50%, transparent);
  }

  .acceso__legal {
    max-inline-size: 46ch;
    font-size: 12.5px;
    line-height: 1.6;
    color: var(--on-marca-2);
  }

  /* ── Columna del formulario ────────────────────────────────────────────── */
  .acceso__forma {
    position: relative; /* ancla del velo de carga */
    display: flex;
    flex-direction: column;
    align-items: center;
    min-inline-size: 0;
    padding: clamp(16px, 7%, 46px);
    background: var(--surface);
    color: var(--fg);
    overflow-wrap: anywhere;
  }

  .acceso__cuerpo {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 22px;
    inline-size: 100%;
    margin-block: auto;
  }

  .acceso__cuerpo > * {
    inline-size: 100%;
    max-inline-size: 44ch;
  }

  .acceso__cuerpo > .acceso__emblema {
    inline-size: auto;
    block-size: 72px;
  }

  /* En oscuro el emblema a color se vuelve blanco (mismo trazo que logo-nutrimav-blanco.png) */
  [data-theme="dark"] .acceso__emblema {
    filter: brightness(0) invert(1);
  }

  @media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .acceso__emblema {
      filter: brightness(0) invert(1);
    }
  }

  .acceso__encabezado {
    display: flex;
    flex-direction: column;
    gap: var(--sp-2);
  }

  .acceso__insignia {
    display: grid;
    place-items: center;
    inline-size: 38px;
    block-size: 38px;
    margin-block-end: 6px;
    border-radius: var(--r-sm);
    background: var(--acc-soft);
    color: var(--acc);
  }

  .acceso__insignia .icono {
    inline-size: 20px;
    block-size: 20px;
  }

  .acceso__insignia--advertencia {
    background: var(--warning-soft);
    color: var(--warning);
  }

  .acceso__insignia--peligro {
    background: var(--danger-soft);
    color: var(--danger);
  }

  .acceso__insignia--exito {
    background: var(--success-soft);
    color: var(--success);
  }

  .acceso__titulo {
    font-family: var(--f-display);
    font-weight: 400;
    font-size: clamp(22px, 2.6vw, 27px);
    line-height: 1.22;
  }

  .acceso__sub {
    font-size: 13.5px;
    line-height: 1.6;
    color: var(--fg-2);
    text-wrap: pretty;
  }

  /* Ficha de la aplicación que pidió acceso (acceso denegado, autorizar una
     herramienta): va en lugar del encabezado, centrada */
  .acceso__aplicacion {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--sp-1);
    text-align: center;
  }

  .acceso__aplicacion-icono {
    inline-size: 72px;
    block-size: 72px;
    margin-block-end: var(--sp-2);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    background: var(--sunken);
    object-fit: cover;
  }

  .acceso__aplicacion-nombre {
    font-family: var(--f-display);
    font-weight: 400;
    font-size: clamp(20px, 2.4vw, 24px);
    line-height: 1.22;
    text-wrap: pretty;
  }

  .acceso__aplicacion-descripcion {
    font-size: 12.5px;
    color: var(--fg-3);
  }

  .acceso__campos {
    display: flex;
    flex-direction: column;
    gap: var(--sp-4);
  }

  .acceso__fila {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 14px;
  }

  .acceso__enlace {
    padding: 0;
    border: none;
    background: transparent;
    color: var(--link);
    font-family: var(--f-ui);
    font-size: 12.5px;
    font-weight: 600;
    cursor: pointer;
    text-decoration: none;
    transition: color var(--fast) var(--ease);

    &:hover {
      color: var(--link-h);
      text-decoration: underline;
      text-underline-offset: 3px;
    }
  }

  .acceso__acciones {
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding-block-start: 2px;
  }

  /* Pista bajo la primaria deshabilitada: dice por qué no se puede enviar */
  .acceso__pista {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    font-size: 11.5px;
    color: var(--danger);
    text-align: center;
  }

  .acceso__pista .icono {
    flex: none;
    inline-size: 14px;
    block-size: 14px;
  }

  .acceso__pie {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 9px;
    padding-block-start: 12px;
    border-block-start: 1px solid var(--divider);
  }

  .acceso__pie .icono {
    inline-size: 14px;
    block-size: 14px;
    color: var(--fg-3);
  }

  .acceso__pie-texto {
    flex: 1;
    min-inline-size: 0;
    font-size: 11.5px;
    line-height: 1.5;
    color: var(--fg-3);
  }

  /* ── Velo de carga sobre la columna del formulario ─────────────────────── */
  .acceso__cargando {
    position: absolute;
    inset: 0;
    z-index: 10;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 11px;
    background: color-mix(in srgb, var(--surface) 72%, transparent);
    backdrop-filter: blur(1.5px);
  }

  .acceso__cargando-barra {
    inline-size: min(220px, 60%);
    block-size: 3px;
    border-radius: 3px;
    background: linear-gradient(90deg, transparent 35%, var(--acc) 50%, transparent 65%);
    background-size: 200% 100%;
    animation: carga-barra 1.1s var(--ease) infinite;
  }

  .acceso__cargando-texto {
    font-size: 12.5px;
    font-weight: 600;
    color: var(--fg-2);
  }

  /* ── Angosto: el panel se reduce a una banda superior ──────────────────── */
  @media (max-width: 880px) {
    .acceso {
      grid-template-columns: minmax(0, 1fr);
      grid-template-rows: auto 1fr;
    }

    .acceso__panel {
      padding: 14px clamp(14px, 4vw, 22px);
    }

    .acceso__panel-pie {
      display: none;
    }

    .acceso__marca-logo {
      block-size: 34px;
    }

    .acceso__marca-separador {
      min-block-size: 34px;
    }

    .acceso__forma {
      padding-block-start: clamp(24px, 6vw, 40px);
    }

    .acceso__cuerpo {
      margin-block: 0 auto;
    }
  }

  @media (prefers-reduced-motion: reduce) {
    .acceso__cargando-barra {
      animation: none;
      background: var(--acc);
    }
  }
}

/*!inventario
{
  "componente": "Cifra destacada",
  "proposito": "Un indicador numérico de un vistazo (cuántos en conteo, cuántos pendientes): etiqueta, número grande y una nota de qué significa, con un icono en el tono del indicador. Va en rejilla, cuatro por fila cuando cabe.",
  "raiz": "cifra",
  "variantes": {
    "cifra--acento": "Tono de acento (actividad en curso)",
    "cifra--exito": "Tono success (completado)",
    "cifra--advertencia": "Tono warning (pendiente de atención)",
    "cifra--informativo": "Tono info (espera normal)",
    "cifra--revision": "Tono review (hecho, espera decisión)",
    "cifra--peligro": "Tono danger (problema)"
  },
  "estados": {},
  "modificadores": {},
  "estructura": [
    { "clase": "cifra__textos", "elemento": "div", "papel": "Columna etiqueta + valor + nota", "opcional": false },
    { "clase": "cifra__etiqueta", "elemento": "p", "papel": "Qué se cuenta", "opcional": false },
    { "clase": "cifra__valor", "elemento": "p", "papel": "El número, en display", "opcional": false },
    { "clase": "cifra__nota", "elemento": "p", "papel": "Una frase: qué significa o desde cuándo", "opcional": true },
    { "clase": "cifra__icono", "elemento": "span > svg.icono", "papel": "Cuadro con el icono del indicador, en su tono", "opcional": true }
  ],
  "internas": [],
  "contexto": ["icono"],
  "ejemplo": [
    "<div class='cifra cifra--acento'>",
    "  <div class='cifra__textos'>",
    "    <p class='cifra__etiqueta'>En conteo</p>",
    "    <p class='cifra__valor'>7</p>",
    "    <p class='cifra__nota'>Siendo escaneados ahora</p>",
    "  </div>",
    "  <span class='cifra__icono'><svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-play' /></svg></span>",
    "</div>"
  ]
}
*/
/* Cifra destacada — el número primero, el contexto después. El tono colorea el
   icono y un filo inferior; el texto permanece en los tonos del sistema. */

@layer componentes {
  .cifra {
    position: relative;
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 12px;
    padding: 16px 18px 18px;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    box-shadow: var(--e1);
    overflow: hidden;
    --cifra-tono: var(--acc);
    --cifra-tono-suave: var(--acc-soft);
  }

  /* Filo inferior en el tono */
  .cifra::after {
    content: "";
    position: absolute;
    inset-block-end: 0;
    inset-inline: 0;
    block-size: 3px;
    background: var(--cifra-tono);
  }

  .cifra__textos {
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-inline-size: 0;
  }

  .cifra__etiqueta {
    font-size: 12.5px;
    font-weight: 600;
    color: var(--fg-2);
  }

  .cifra__valor {
    font-family: var(--f-display);
    font-size: 30px;
    line-height: 1.1;
    color: var(--fg);
  }

  .cifra__nota {
    font-size: 11.5px;
    color: var(--fg-3);
  }

  .cifra__icono {
    flex: none;
    inline-size: 40px;
    block-size: 40px;
    display: grid;
    place-items: center;
    border-radius: var(--r-sm);
    background: var(--cifra-tono-suave);
    color: var(--cifra-tono);
  }

  .cifra__icono .icono {
    inline-size: 20px;
    block-size: 20px;
  }

  .cifra--acento {
    --cifra-tono: var(--acc);
    --cifra-tono-suave: var(--acc-soft);
  }

  .cifra--exito {
    --cifra-tono: var(--success);
    --cifra-tono-suave: var(--success-soft);
  }

  .cifra--advertencia {
    --cifra-tono: var(--warning);
    --cifra-tono-suave: var(--warning-soft);
  }

  .cifra--informativo {
    --cifra-tono: var(--info);
    --cifra-tono-suave: var(--info-soft);
  }

  .cifra--revision {
    --cifra-tono: var(--review);
    --cifra-tono-suave: var(--review-soft);
  }

  .cifra--peligro {
    --cifra-tono: var(--danger);
    --cifra-tono-suave: var(--danger-soft);
  }
}

/*!inventario
{
  "componente": "Ficha",
  "proposito": "Píldora de estado o etiqueta con tono: activo/inactivo, vigente/vencido, DEV/QAS/PRO, lote/sin lote… Texto siempre, icono opcional; el tono acompaña al texto, nunca lo sustituye. Distinta de chip-estatus, que está atada a los estados del dominio de tareas.",
  "raiz": "ficha",
  "variantes": {
    "ficha--exito": "Tono success (activo, vigente, exitoso)",
    "ficha--peligro": "Tono danger (inactivo, error, vencido crítico)",
    "ficha--advertencia": "Tono warning (por vencer, requiere acción, parcial)",
    "ficha--revision": "Tono review (hecho, espera decisión: registrado, completado)",
    "ficha--informativo": "Tono info",
    "ficha--acento": "Tono de acento (marcado: lote, seleccionado)",
    "ficha--neutra": "Tono neutro (sin lote, sin dato) — es también el aspecto por omisión"
  },
  "estados": {},
  "modificadores": {
    "ficha--codigo": "Clave o código en monoespaciada con espaciado (DEV, 0030); combina con un tono"
  },
  "estructura": [],
  "internas": [],
  "contexto": ["icono"],
  "ejemplo": [
    "<span class='ficha ficha--exito'>",
    "  <svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-check' /></svg>",
    "  Activa",
    "</span>",
    "<span class='ficha ficha--codigo ficha--advertencia'>QAS</span>"
  ]
}
*/
/* Ficha — píldora de estado o etiqueta. Hereda el criterio del chip de estatus
   (texto E icono, nunca solo color) pero con tonos genéricos, para que cada
   pantalla no invente la suya. */

@layer componentes {
  .ficha {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 2px 9px;
    border: 1px solid var(--line);
    border-radius: 999px;
    background: var(--sunken);
    color: var(--fg-2);
    font-size: 11.5px;
    font-weight: 600;
    line-height: 1.5;
    white-space: nowrap;
  }

  .ficha .icono {
    inline-size: 12px;
    block-size: 12px;
    flex: none;
    /* Tamaño propio ⇒ trazo propio (RD-019): 1.75 × 24 / 12 */
    stroke-width: 3.5px;
  }

  .ficha--exito {
    background: var(--success-soft);
    border-color: var(--success-bd);
    color: var(--success);
  }

  .ficha--peligro {
    background: var(--danger-soft);
    border-color: var(--danger-bd);
    color: var(--danger);
  }

  .ficha--advertencia {
    background: var(--warning-soft);
    border-color: var(--warning-bd);
    color: var(--warning-fg);
  }

  .ficha--revision {
    background: var(--review-soft);
    border-color: var(--review-bd);
    color: var(--review);
  }

  .ficha--informativo {
    background: var(--info-soft);
    border-color: var(--info-bd);
    color: var(--info);
  }

  .ficha--acento {
    background: var(--acc-soft);
    border-color: var(--acc);
    color: var(--acc);
  }

  .ficha--neutra {
    background: var(--sunken);
    border-color: var(--line);
    color: var(--fg-2);
  }

  .ficha--codigo {
    padding: 1px 9px;
    font-family: var(--f-mono);
    font-size: 11px;
    letter-spacing: 0.06em;
  }
}

/*!inventario
{
  "componente": "Línea de tiempo",
  "proposito": "Quién hizo qué, cuándo y con qué resultado, en cronología inversa. El tipo de entrada se distingue por el icono y el tono del nodo (modificadores linea-tiempo__nodo--*, como los de ficha); actor y momento no son opcionales.",
  "raiz": "linea-tiempo",
  "variantes": {
    "linea-tiempo--compacta": "Punto de 9px + conector de 1px — la de la historia del panel"
  },
  "estados": {},
  "modificadores": {
    "linea-tiempo__nodo--exito": "Tono success (aprobado, recibido)",
    "linea-tiempo__nodo--peligro": "Tono danger (rechazo, error)",
    "linea-tiempo__nodo--advertencia": "Tono warning (requiere acción, error recuperable)",
    "linea-tiempo__nodo--revision": "Tono review (hecho, espera decisión)",
    "linea-tiempo__nodo--informativo": "Tono info (aviso de otro sistema)",
    "linea-tiempo__nodo--acento": "Tono de acento (transición, comentario)",
    "linea-tiempo__nodo--neutra": "Tono apagado (--fg-3): entrada sin peso o de tipo desconocido. Sin modificador, el nodo va en --fg-2"
  },
  "estructura": [
    { "clase": "linea-tiempo__recorte", "elemento": "button", "papel": "Salida a la historia completa cuando el servidor recorta (disparador htmx)", "opcional": true },
    { "clase": "linea-tiempo__entrada", "elemento": "div", "papel": "Una entrada: eje + contenido", "opcional": false },
    { "clase": "linea-tiempo__eje", "elemento": "span", "papel": "Columna del eje: nodo + conector hacia la siguiente entrada", "opcional": false },
    { "clase": "linea-tiempo__nodo", "elemento": "span", "papel": "El nodo; la pantalla le pone el icono y el tono (linea-tiempo__nodo--*) del tipo de entrada, nunca un atributo style", "opcional": false },
    { "clase": "linea-tiempo__conector", "elemento": "span", "papel": "El trazo que une con la entrada siguiente", "opcional": false },
    { "clase": "linea-tiempo__contenido", "elemento": "div", "papel": "Columna de textos", "opcional": false },
    { "clase": "linea-tiempo__titulo", "elemento": "span", "papel": "Qué pasó", "opcional": false },
    { "clase": "linea-tiempo__momento", "elemento": "span", "papel": "Cuándo (zona local)", "opcional": false },
    { "clase": "linea-tiempo__actor", "elemento": "span", "papel": "Quién", "opcional": false },
    { "clase": "linea-tiempo__cuerpo", "elemento": "span", "papel": "Texto adicional (p. ej. la nota)", "opcional": true },
    { "clase": "linea-tiempo__vacio", "elemento": "p", "papel": "Estado vacío de la historia", "opcional": true }
  ],
  "internas": [],
  "ejemplo": [
    "<div class='linea-tiempo linea-tiempo--compacta'>",
    "  <button class='linea-tiempo__recorte' hx-get='…?handler=HistoriaCompleta'>",
    "    <svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-chevrons-up' /></svg>",
    "    12 entradas anteriores — ver la historia completa</button>",
    "  <div class='linea-tiempo__entrada'>",
    "    <span class='linea-tiempo__eje'>",
    "      <span class='linea-tiempo__nodo linea-tiempo__nodo--acento'><svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-play' /></svg></span>",
    "      <span class='linea-tiempo__conector'></span>",
    "    </span>",
    "    <div class='linea-tiempo__contenido'>",
    "      <span class='linea-tiempo__titulo'>Pasó a En curso</span>",
    "      <span class='linea-tiempo__momento'>6 ago · 10:12</span>",
    "      <span class='linea-tiempo__actor'>Vicente Betancourt Reyes</span>",
    "    </div>",
    "  </div>",
    "</div>"
  ]
}
*/
/* Línea de tiempo — contra la sección 21 del sistema v2.10.0: quién hizo qué,
   cuándo y con qué resultado, en cronología INVERSA (lo más reciente arriba).
   [CORE-05]

   Dos densidades:
   — Base: nodo circular de 28 px con icono por tipo de entrada + conector.
   — Compacta (.linea-tiempo--compacta): punto de 9 px + conector de 1 px; es la
     que usa la historia del panel de elemento de trabajo (dominio v1.5.0).
   El TIPO de entrada se distingue por el icono y el tono del nodo (lo declara la
   pantalla: transición = icono del estado destino; nota = su icono). El tono va
   en un modificador linea-tiempo__nodo--* con los mismos nombres que ficha, no en
   style="color: …": la CSP de la app (style-src 'self') ignora el atributo.
   El actor y el momento son parte de la anatomía, no opcionales.

   Recorte con salida a la vista completa: la fila .linea-tiempo__recorte se
   muestra cuando el servidor recorta la historia; es un disparador htmx que trae
   la lista completa — sin JS, el estado del recorte es del servidor.

   Uso (compacta):
     <div class="linea-tiempo linea-tiempo--compacta">
       <button class="linea-tiempo__recorte" hx-get="…historia-completa">
         (i-chevrons-up) 12 entradas anteriores — ver la historia completa
       </button>
       <div class="linea-tiempo__entrada">
         <span class="linea-tiempo__nodo linea-tiempo__nodo--acento">(icono)</span>
         <div class="linea-tiempo__contenido">
           <span class="linea-tiempo__titulo">Pasó a En curso</span>
           <span class="linea-tiempo__momento">6 ago · 10:12</span>
           <span class="linea-tiempo__actor">Jefe de Infraestructura</span>
           <span class="linea-tiempo__cuerpo">…texto de la nota…</span>  ← opcional
         </div>
       </div>…
     </div> */

@layer componentes {
  .linea-tiempo {
    display: flex;
    flex-direction: column;
  }

  .linea-tiempo__entrada {
    display: flex;
    gap: 14px;
  }

  .linea-tiempo__eje {
    display: flex;
    flex-direction: column;
    align-items: center;
    flex: none;
  }

  /* Nodo base: 28 px, icono del tipo, anillo sobre superficie */
  .linea-tiempo__nodo {
    inline-size: 28px;
    block-size: 28px;
    flex: none;
    border-radius: 999px;
    background: var(--sunken);
    color: var(--fg-2);
    border: 2px solid var(--surface);
    box-shadow: 0 0 0 1px var(--line);
    display: grid;
    place-items: center;
  }

  /* Tonos del nodo: los de ficha. El color pinta el icono (base) o el punto (compacta). */
  .linea-tiempo__nodo--exito {
    color: var(--success);
  }

  .linea-tiempo__nodo--peligro {
    color: var(--danger);
  }

  .linea-tiempo__nodo--advertencia {
    color: var(--warning-fg);
  }

  .linea-tiempo__nodo--revision {
    color: var(--review);
  }

  .linea-tiempo__nodo--informativo {
    color: var(--info);
  }

  .linea-tiempo__nodo--acento {
    color: var(--acc);
  }

  .linea-tiempo__nodo--neutra {
    color: var(--fg-3);
  }

  .linea-tiempo__conector {
    flex: 1;
    inline-size: 2px;
    background: var(--divider);
    min-block-size: 14px;
  }

  .linea-tiempo__entrada:last-child .linea-tiempo__conector {
    display: none;
  }

  .linea-tiempo__contenido {
    display: flex;
    flex-direction: column;
    gap: 3px;
    flex: 1;
    min-inline-size: 0;
    padding-block-end: 18px;
  }

  .linea-tiempo__entrada:last-child .linea-tiempo__contenido {
    padding-block-end: 0;
  }

  .linea-tiempo__titulo {
    font-size: 13.5px;
    font-weight: 700;
  }

  .linea-tiempo__momento {
    font-family: var(--f-mono);
    font-size: 10.5px;
    color: var(--fg-3);
  }

  .linea-tiempo__actor {
    font-size: 12.5px;
    color: var(--fg-2);
  }

  .linea-tiempo__cuerpo {
    font-size: 12.5px;
    line-height: 1.55;
    color: var(--fg-2);
    padding: 7px 9px;
    border-radius: var(--r-xs);
    background: var(--sunken);
    border: 1px solid var(--line);
    margin-block-start: 2px;
  }

  /* Recorte: visible solo cuando el servidor recortó; trae la vista completa */
  .linea-tiempo__recorte {
    display: flex;
    align-items: center;
    gap: 7px;
    padding: 8px 10px;
    margin-block-end: 10px;
    border: 1px solid var(--line);
    border-radius: var(--r-xs);
    background: var(--sunken);
    color: var(--fg-2);
    font-family: var(--f-ui);
    font-size: 11.5px;
    line-height: 1.45;
    cursor: pointer;
    text-align: start;

    &:hover {
      border-color: var(--line-2);
      color: var(--fg);
    }
  }

  /* Vacío: la primera entrada la escribe la primera transición, no una persona */
  .linea-tiempo__vacio {
    padding: 11px 12px;
    border: 1px dashed var(--line-2);
    border-radius: var(--r-xs);
    font-size: 11.5px;
    line-height: 1.5;
    color: var(--fg-3);
  }

  /* ── Densidad compacta (historia del panel) ─────────────────────────────── */
  .linea-tiempo--compacta {
    & .linea-tiempo__entrada {
      gap: 9px;
    }

    & .linea-tiempo__nodo {
      inline-size: 9px;
      block-size: 9px;
      margin-block-start: 4px;
      border: 1.5px solid currentColor;
      background: var(--surface);
      box-shadow: none;

      /* En compacta el nodo es punto: el icono interior no se muestra */
      & > * {
        display: none;
      }
    }

    & .linea-tiempo__conector {
      inline-size: 1px;
      min-block-size: 10px;
      background: var(--line);
    }

    & .linea-tiempo__contenido {
      gap: 2px;
      padding-block-end: 11px;
    }

    & .linea-tiempo__titulo {
      font-size: 11.5px;
      font-weight: 600;
    }

    & .linea-tiempo__momento {
      font-size: 9px;
    }

    & .linea-tiempo__actor,
    & .linea-tiempo__cuerpo {
      font-size: 11px;
    }
  }

  @media (pointer: coarse) {
    .linea-tiempo__recorte {
      min-block-size: 44px;
    }
  }
}

/*!inventario
{
  "componente": "Perfil de dos columnas",
  "proposito": "Vista de un expediente con muchos datos (sección 13 de la spec). La columna estrecha fija la identidad y el estado y no cambia al navegar; la columna ancha reparte el expediente en pestañas horizontales que abren con un Resumen, cada una con una sola pregunta. Todo se lee primero (datos, RD-022) y cada sección se edita por separado. En el encabezado de la página: «Volver al listado», el recorrido de registros (anterior, posición, siguiente) y el menú de acciones «⋯» (imprimir, exportar, dar de baja), nunca dentro de la ficha. La columna estrecha y las pestañas se fijan al desplazar; bajo 844px de región las columnas se apilan y nada queda fijo. Lo que es dominio de la app (semáforo de vigencia, cumplimiento documental, «Qué sigue») se compone dentro con componentes del sistema o con clases pg- contra tokens.",
  "raiz": "perfil",
  "variantes": {},
  "estados": {
    "[aria-current='page']": "En perfil__pestana: la pestaña activa (la resuelve el servidor; el estado vive en la URL)",
    "[disabled]": "En perfil__registro: no hay registro anterior o siguiente"
  },
  "modificadores": {},
  "estructura": [
    { "clase": "perfil__lateral", "elemento": "aside", "papel": "Columna estrecha (300px): tarjetas apiladas; fija al desplazar mientras haya dos columnas", "opcional": false },
    { "clase": "perfil__identidad", "elemento": "div", "papel": "Tarjeta de identidad: foto o iniciales, nombre, línea secundaria y estado (ficha)", "opcional": false },
    { "clase": "perfil__avatar", "elemento": "span | img", "papel": "Foto (img) o iniciales (span) de 96px", "opcional": false },
    { "clase": "perfil__nombre", "elemento": "h2", "papel": "Nombre en display, 21px", "opcional": false },
    { "clase": "perfil__sub", "elemento": "p", "papel": "Especialidad, puesto o línea secundaria", "opcional": true },
    { "clase": "perfil__bloque", "elemento": "div", "papel": "Tarjeta de la columna estrecha para datos compactos (datos--compacto) u otros resúmenes", "opcional": true },
    { "clase": "perfil__principal", "elemento": "div", "papel": "Columna ancha: tarjeta con las pestañas y el panel de la activa", "opcional": false },
    { "clase": "perfil__pestanas", "elemento": "nav", "papel": "Pestañas horizontales con aria-label; fijas bajo el borde superior de la región al desplazar; se desplazan a lo ancho si no caben", "opcional": false },
    { "clase": "perfil__pestana", "elemento": "a", "papel": "Una pestaña; la activa con aria-current='page' y filo de 2px en acento", "opcional": false },
    { "clase": "perfil__pestana-conteo", "elemento": "span", "papel": "Conteo de pendientes de la pestaña (documentos, notas)", "opcional": true },
    { "clase": "perfil__panel", "elemento": "div", "papel": "Contenido de la pestaña activa (secciones datos, cifras, línea de tiempo…)", "opcional": false },
    { "clase": "perfil__navegacion", "elemento": "div", "papel": "Grupo del encabezado de página: «Volver al listado», recorrido de registros y menú de acciones", "opcional": true },
    { "clase": "perfil__volver", "elemento": "a", "papel": "«Volver al listado» con i-arrow-left; conserva filtros y página del listado", "opcional": true },
    { "clase": "perfil__registros", "elemento": "div", "papel": "Recorrido de registros del listado: anterior, posición («3 de 128») y siguiente", "opcional": true },
    { "clase": "perfil__registro", "elemento": "a | button", "papel": "Anterior o siguiente (i-chevron-left / i-chevron-right) con aria-label", "opcional": true },
    { "clase": "perfil__posicion", "elemento": "span", "papel": "Posición en el listado, en mono", "opcional": true }
  ],
  "internas": [],
  "contexto": ["encabezado-pagina", "encabezado-pagina__acciones", "datos", "datos--compacto", "ficha", "menu-acciones", "icono"],
  "ejemplo": [
    "<div class='encabezado-pagina'>",
    "  <div class='encabezado-pagina__titulos'><h1 class='encabezado-pagina__titulo'>Expediente</h1></div>",
    "  <div class='encabezado-pagina__acciones perfil__navegacion'>",
    "    <a class='perfil__volver' href='/expedientes?pagina=1'><svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-arrow-left' /></svg> Volver al listado</a>",
    "    <div class='perfil__registros'>",
    "      <a class='perfil__registro' href='/expedientes/2' aria-label='Expediente anterior'><svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-chevron-left' /></svg></a>",
    "      <span class='perfil__posicion'>3 de 128</span>",
    "      <a class='perfil__registro' href='/expedientes/4' aria-label='Expediente siguiente'><svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-chevron-right' /></svg></a>",
    "    </div>",
    "    <div class='menu-acciones' data-componente='menu-acciones' data-abierto='no'>…</div>",
    "  </div>",
    "</div>",
    "<div class='perfil'>",
    "  <aside class='perfil__lateral'>",
    "    <div class='perfil__identidad'>",
    "      <span class='perfil__avatar'>SH</span>",
    "      <h2 class='perfil__nombre'>Sofía Herrera Campos</h2>",
    "      <p class='perfil__sub'>Gestión de proyectos</p>",
    "      <span class='ficha ficha--exito'>Vigente</span>",
    "    </div>",
    "    <div class='perfil__bloque'><section class='datos datos--compacto'>…</section></div>",
    "  </aside>",
    "  <div class='perfil__principal'>",
    "    <nav class='perfil__pestanas' aria-label='Secciones del expediente'>",
    "      <a class='perfil__pestana' href='?seccion=resumen' aria-current='page'>Resumen</a>",
    "      <a class='perfil__pestana' href='?seccion=documentacion'>Documentación <span class='perfil__pestana-conteo'>3</span></a>",
    "    </nav>",
    "    <div class='perfil__panel'><section class='datos'>…</section></div>",
    "  </div>",
    "</div>"
  ]
}
*/
/* Perfil de dos columnas — sección 13 de la spec. La identidad no se mueve; el expediente se
   reparte en pestañas. Se acomoda con flex-wrap (sin puntos de quiebre: RD-002) y lo único que
   consulta a la región es si hay espacio para fijar la columna y las pestañas (RD-010). */

@layer componentes {
  .perfil {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: var(--gap);
  }

  /* ── Columna estrecha ──────────────────────────────────────────────────── */
  .perfil__lateral {
    flex: 1 0 300px;
    max-inline-size: 300px;
    display: flex;
    flex-direction: column;
    gap: var(--sp-4);
    min-inline-size: 0;
  }

  .perfil__identidad,
  .perfil__bloque {
    padding: var(--sp-5);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    background: var(--surface);
    box-shadow: var(--e1);
  }

  .perfil__identidad {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--sp-1);
    text-align: center;
  }

  .perfil__avatar {
    display: grid;
    place-items: center;
    inline-size: 96px;
    block-size: 96px;
    margin-block-end: var(--sp-2);
    border-radius: var(--r-md);
    background: var(--acc-soft);
    color: var(--acc);
    font-family: var(--f-display);
    font-size: 32px;
    object-fit: cover;
  }

  .perfil__nombre {
    font-family: var(--f-display);
    font-size: 21px;
    font-weight: 400;
    line-height: 1.25;
    color: var(--fg);
    overflow-wrap: anywhere;
  }

  .perfil__sub {
    font-size: 12.5px;
    color: var(--fg-2);
  }

  .perfil__identidad .ficha {
    margin-block-start: var(--sp-2);
  }

  /* ── Columna ancha ─────────────────────────────────────────────────────── */
  .perfil__principal {
    flex: 1 1 520px;
    min-inline-size: 0;
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    background: var(--surface);
    box-shadow: var(--e1);
  }

  .perfil__pestanas {
    display: flex;
    gap: var(--sp-1);
    padding: 0 var(--sp-4);
    border-block-end: 1px solid var(--line);
    border-start-start-radius: var(--r-md);
    border-start-end-radius: var(--r-md);
    background: var(--surface);
    overflow-x: auto;
    scrollbar-width: thin;
  }

  .perfil__pestana {
    flex: none;
    display: inline-flex;
    align-items: center;
    gap: 7px;
    padding: 13px 12px 11px;
    border-block-end: 2px solid transparent;
    color: var(--fg-2);
    font-size: 13px;
    font-weight: 500;
    text-decoration: none;
    white-space: nowrap;
    transition: color var(--fast) var(--ease), border-color var(--fast) var(--ease);

    &:hover {
      color: var(--fg);
    }

    /* El anillo va por dentro: la barra de pestañas recorta lo que sale de ella */
    &:focus-visible {
      outline-offset: -2px;
    }
  }

  .perfil__pestana[aria-current="page"] {
    border-block-end-color: var(--acc);
    color: var(--acc);
    font-weight: 700;
  }

  .perfil__pestana-conteo {
    min-inline-size: 18px;
    padding: 0 5px;
    border-radius: 999px;
    background: var(--sunken);
    color: var(--fg-2);
    font-family: var(--f-mono);
    font-size: 11px;
    font-weight: 400;
    line-height: 18px;
    text-align: center;
  }

  .perfil__panel {
    padding: var(--sp-5);
  }

  /* ── Navegación del encabezado de página ───────────────────────────────── */
  .perfil__navegacion {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--sp-3);
  }

  .perfil__volver {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: 13px;
    font-weight: 600;
  }

  .perfil__volver .icono {
    inline-size: 15px;
    block-size: 15px;
  }

  .perfil__registros {
    display: inline-flex;
    align-items: center;
    gap: 2px;
    padding: 2px;
    border: 1px solid var(--line-2);
    border-radius: var(--r-sm);
    background: var(--surface);
  }

  .perfil__registro {
    display: grid;
    place-items: center;
    inline-size: 30px;
    block-size: 30px;
    padding: 0;
    border: none;
    border-radius: var(--r-xs);
    background: transparent;
    color: var(--fg-2);
    cursor: pointer;

    &:hover {
      background: var(--sunken);
      color: var(--fg);
    }

    &[disabled],
    &[aria-disabled="true"] {
      color: var(--fg-4);
      cursor: not-allowed;
      pointer-events: none;
    }
  }

  .perfil__registro .icono {
    inline-size: 15px;
    block-size: 15px;
  }

  .perfil__posicion {
    padding: 0 6px;
    font-family: var(--f-mono);
    font-size: 12px;
    color: var(--fg-2);
    white-space: nowrap;
  }

  /* ── Fijar al desplazar: solo con dos columnas (región de 844px o más) ──── */
  @container contenido (min-width: 844px) {
    .perfil__lateral {
      position: sticky;
      inset-block-start: 0;
    }

    .perfil__pestanas {
      position: sticky;
      inset-block-start: 0;
      z-index: 2;
    }
  }

  @media (pointer: coarse) {
    .perfil__registro {
      inline-size: 44px;
      block-size: 44px;
    }
  }
}

/*!inventario
{
  "componente": "Tabla",
  "proposito": "Tabla con orden por columna, estado vacío OBLIGATORIO, estado de carga y pie de paginación. Orden y paginación son de SERVIDOR (htmx intercambia el fragmento completo); bajo su ancho mínimo (data-tabla-min, que tabla.js pasa a --tabla-min) se desplaza, no comprime.",
  "raiz": "tabla",
  "variantes": {},
  "estados": {
    "[aria-busy='true']": "Cargando: filas de esqueleto en las celdas (en la raíz)",
    "[aria-sort='ascending|descending']": "La columna ordenada (en el th; un solo criterio visible)",
    "[aria-selected='true']": "Fila seleccionada (en el tr)",
    "[aria-current='page']": "La página actual (en tabla__pagina)"
  },
  "modificadores": {
    "tabla__celda--dato": "Folios, fechas y cifras en monoespaciada — se alinean ópticamente",
    "tabla__celda--cifra": "Además alineada a la derecha",
    "tabla__esqueleto--corto": "Esqueleto al 52%",
    "tabla__esqueleto--medio": "Esqueleto al 80%",
    "tabla__esqueleto--largo": "Esqueleto al 100%",
    "tabla__pagina--flecha": "Control de página anterior/siguiente (solo icono)",
    "tabla__accion--peligro": "Acción de fila destructiva: al pasar el puntero toma el tono danger"
  },
  "estructura": [
    { "clase": "tabla__herramientas", "elemento": "div", "papel": "Encabezado de la tarjeta: vistas, búsqueda y acciones viven pegadas a los datos que gobiernan; envuelve y separa los grupos", "opcional": true },
    { "clase": "tabla__herramientas-grupo", "elemento": "div", "papel": "Un grupo de controles del encabezado (vistas a la izquierda; búsqueda y botones con icono y texto a la derecha)", "opcional": true },
    { "clase": "tabla__desplazamiento", "elemento": "div", "papel": "El overflow horizontal: la tabla se desplaza antes que comprimir", "opcional": false },
    { "clase": "tabla__cuadricula", "elemento": "table", "papel": "La tabla; declara su mínimo en píxeles con data-tabla-min='NNN' (la suma de sus columnas). tabla.js lo pasa a --tabla-min: un atributo style lo bloquea la CSP de la app", "opcional": false },
    { "clase": "tabla__orden", "elemento": "button", "papel": "Control de orden en el th (dispara hx-get; cambiar orden vuelve a página 1)", "opcional": false },
    { "clase": "tabla__orden-icono", "elemento": "svg.icono", "papel": "Chevron del orden (12px, trazo propio)", "opcional": false },
    { "clase": "tabla__celda", "elemento": "td", "papel": "Una celda", "opcional": false },
    { "clase": "tabla__vacio", "elemento": "div", "papel": "SUSTITUYE al cuerpo cuando no hay filas — obligatorio, nunca en blanco", "opcional": true },
    { "clase": "tabla__vacio-icono", "elemento": "span > svg.icono", "papel": "Círculo con icono del vacío", "opcional": true },
    { "clase": "tabla__vacio-titulo", "elemento": "p", "papel": "Qué significa el vacío", "opcional": true },
    { "clase": "tabla__vacio-texto", "elemento": "p", "papel": "Qué hacer al respecto", "opcional": true },
    { "clase": "tabla__esqueleto", "elemento": "span", "papel": "Barra de esqueleto en la celda durante la carga (aria-hidden)", "opcional": true },
    { "clase": "tabla__pie", "elemento": "div", "papel": "Rango + tamaño SIEMPRE; controles de página solo con más de una", "opcional": false },
    { "clase": "tabla__pie-grupo", "elemento": "div", "papel": "Rango + selector de tamaño", "opcional": false },
    { "clase": "tabla__pie-info", "elemento": "span", "papel": "«Mostrando 1–20 de 143»", "opcional": false },
    { "clase": "tabla__tamano", "elemento": "select", "papel": "Tamaño de página — select NATIVO, su change dispara hx-get", "opcional": false },
    { "clase": "tabla__paginas", "elemento": "nav", "papel": "Los controles de página (aria-label='Paginación'; se OMITEN con una sola)", "opcional": true },
    { "clase": "tabla__pagina", "elemento": "button", "papel": "Una página; la actual con aria-current='page'", "opcional": true },
    { "clase": "tabla__filtros", "elemento": "form", "papel": "En las herramientas: forma de filtros en una sola línea (desplegables + búsqueda), disparador htmx con el estado en hidden", "opcional": true },
    { "clase": "tabla__filtro", "elemento": "div | select", "papel": "Raíz de un desplegable o select dentro de tabla__filtros: ancho acotado, encoge antes de envolver", "opcional": true },
    { "clase": "tabla__busqueda", "elemento": "input[type=search]", "papel": "Búsqueda dentro de tabla__filtros: encoge antes que mandar los botones a otra línea", "opcional": true },
    { "clase": "tabla__col-acciones", "elemento": "th | td", "papel": "Columna de acciones por fila (ancho fijo, sin encabezado visible)", "opcional": true },
    { "clase": "tabla__acciones", "elemento": "div", "papel": "Contenedor de las acciones de una fila, alineadas a la derecha", "opcional": true },
    { "clase": "tabla__accion", "elemento": "button | a", "papel": "Acción de fila de solo icono (30px) con title y aria-label; la destructiva lleva tabla__accion--peligro", "opcional": true }
  ],
  "internas": [],
  "contexto": ["icono"],
  "ejemplo": [
    "<div class='tabla' id='tabla-x' aria-busy='false' hx-target='#tabla-x' hx-swap='outerHTML'>",
    "  <div class='tabla__herramientas'>",
    "    <div class='tabla__herramientas-grupo'>(vistas: botones filtros__vista)</div>",
    "    <div class='tabla__herramientas-grupo'>",
    "      <form>(búsqueda: input campo__entrada type='search')</form>",
    "      <button class='boton'>(icono i-refresh-cw) Actualizar</button>",
    "    </div>",
    "  </div>",
    "  <div class='tabla__desplazamiento'>",
    "    <table class='tabla__cuadricula' data-tabla-min='760'>",
    "      <thead><tr><th aria-sort='ascending'>",
    "        <button class='tabla__orden' hx-get='…?orden=nombre'>Nombre",
    "          <svg class='icono tabla__orden-icono' aria-hidden='true'><use href='/images/iconos.svg#i-chevron-down' /></svg></button>",
    "      </th></tr></thead>",
    "      <tbody><tr><td class='tabla__celda'>…</td><td class='tabla__celda tabla__celda--dato'>2026-08-12</td></tr></tbody>",
    "    </table>",
    "  </div>",
    "  <div class='tabla__pie'>",
    "    <div class='tabla__pie-grupo'>",
    "      <span class='tabla__pie-info'>Mostrando 1–20 de 143</span>",
    "      <select class='tabla__tamano' name='tamano' hx-get='…'><option>20</option></select>",
    "    </div>",
    "    <nav class='tabla__paginas' aria-label='Paginación'>",
    "      <button class='tabla__pagina' aria-current='page'>1</button>",
    "      <button class='tabla__pagina'>2</button>",
    "    </nav>",
    "  </div>",
    "</div>"
  ]
}
*/
/* Tabla — contra «Tablas y paginación» (sección 09) del sistema v2.10.0.
   Alcance: base + ORDEN POR COLUMNA + ESTADO VACÍO + ESTADO DE CARGA + PIE DE
   PAGINACIÓN (parte 5 de la anatomía) + HERRAMIENTAS (encabezado de la tarjeta,
   lo pidió una app: vistas, búsqueda y acciones dentro de la tabla). Lo no
   pedido no está (AD-11): la selección en lote se agrega cuando un frente la pida. La
   «Tabla avanzada» (columna congelada, panel de detalle, edición) es OTRO
   componente y ningún módulo de Ola 1 la usa.

   CONTRATO DE PAGINACIÓN (las tres reglas que fallan al integrarse, declaradas):
   1. La paginación es de SERVIDOR, igual que el orden (AD-10). Los controles del
      pie disparan el intercambio htmx del fragmento completo (encabezado + cuerpo
      + pie); página, tamaño y orden viven en la URL, nunca en el cliente.
   2. ORDEN Y PÁGINA CONVIVEN: cambiar el orden VUELVE A LA PRIMERA PÁGINA. El
      servidor emite el fragmento con el aria-sort nuevo y el pie en página 1 —
      nadie se queda en la página siete de un listado reordenado.
   3. CON UNA SOLA PÁGINA el pie NO desaparece ni queda vacío: el rango («1–8 de
      8») y el tamaño de página se muestran siempre — son información y mantienen
      la altura estable al filtrar — y los controles de página (flechas y números)
      se OMITEN, sin fantasmas deshabilitados.
   El selector de tamaño es un <select> NATIVO estilizado: cero JS, accesible, y
   su change dispara el hx-get. El desplegable es el del navegador, a propósito.

   Uso (el orden es del SERVIDOR, AD-10 — htmx intercambia el fragmento; tabla.js no
   guarda estado de cliente: solo pasa data-tabla-min a --tabla-min):

     <div class="tabla" aria-busy="false">
       <div class="tabla__desplazamiento">
         <table class="tabla__cuadricula" data-tabla-min="760">  ← min-width PROPIO:
           <thead><tr>                                                    la suma de sus columnas
             <th aria-sort="ascending">
               <button class="tabla__orden" hx-get="…?orden=folio">Folio
                 <svg class="icono tabla__orden-icono">…#i-chevron-down</svg>
               </button>
             </th>…
       — Filas: <td class="tabla__celda"> · folios/fechas/cifras con
         class="tabla__celda tabla__celda--dato" (mono; se alinean ópticamente).
       — Vacío: el bloque .tabla__vacio SUSTITUYE al cuerpo. Es obligatorio:
         una tabla sin filas nunca queda en blanco.
       — Carga: filas con .tabla__esqueleto en las celdas y aria-busy="true"
         en .tabla (los esqueletos llevan aria-hidden="true").

   Degradación (la gobierna la tabla, no un punto de quiebre del sistema):
   bajo su mínimo se DESPLAZA en horizontal antes que comprimir columnas;
   el encabezado queda pegado al tope y acompaña el desplazamiento. El modo
   apilado por fila es opt-in de la pantalla, no automático — no vive aquí.

   El mínimo va en data-tabla-min y no en style="--tabla-min: …": la CSP del
   app (style-src 'self') ignora el atributo style, y tabla.js escribe la
   custom property desde el script, que sí está permitido. Sin el atributo, 640px.

   Densidad: el relleno vertical de fila es var(--row-y); ningún valor propio
   lo redefine (las dos densidades del DS se controlan desde la raíz). */

@layer componentes {
  .tabla {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    box-shadow: var(--e2);
    overflow: hidden;
  }

  /* ── Herramientas: el encabezado de la tarjeta ─────────────────────────────
     Vistas, búsqueda y acciones viven DENTRO de la tarjeta, pegadas a los datos
     que gobiernan (lo pidió una app). Dos grupos separados por space-between:
     vistas a la izquierda; búsqueda y botones —con icono y texto— a la derecha.
     La búsqueda de la pantalla debe ENCOGER antes que mandar los botones a otra
     línea (flex-basis flexible en su clase pg-*, no ancho rígido). */
  .tabla__herramientas {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 10px 14px;
    padding: 12px clamp(14px, 2vw, 18px);
    border-block-end: 1px solid var(--line);
  }

  .tabla__herramientas-grupo {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 7px;
  }

  /* La tabla completa se desplaza antes que comprimir: comprimir hasta que una
     cifra envuelva la vuelve ilegible. */
  .tabla__desplazamiento {
    overflow-x: auto;
  }

  .tabla__cuadricula {
    inline-size: 100%;
    border-collapse: collapse;
    min-inline-size: var(--tabla-min, 640px);

    & thead th {
      /* Pegado al tope al desplazarse en vertical; acompaña el horizontal.
         Una tabla sin encabezado visible deja de ser tabla. */
      position: sticky;
      inset-block-start: 0;
      z-index: 1;
      background: var(--sunken);
      border-block-end: 1px solid var(--line);
      padding: 10px 18px;
      text-align: start;
    }

    & tbody tr {
      border-block-end: 1px solid var(--divider);
      transition: background var(--fast) var(--ease);

      &:hover {
        background: var(--sunken);
      }

      &[aria-selected="true"] {
        background: var(--acc-soft);
      }
    }
  }

  .tabla__celda {
    padding: var(--row-y) 18px;
    font-size: 13.5px;
  }

  /* Folios, fechas y cifras en monoespaciada: las columnas se alinean ópticamente. */
  .tabla__celda--dato {
    font-family: var(--f-mono);
    font-size: 12.5px;
    color: var(--fg-2);
  }

  .tabla__celda--cifra {
    text-align: end;
  }

  /* ── Orden por columna ─────────────────────────────────────────────────────
     El estado vive en aria-sort del th (un solo criterio visible a la vez).
     El icono es el chevron del sprite: descendente tal cual, ascendente girado
     — sin icono nuevo. */
  .tabla__orden {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    border: none;
    background: transparent;
    padding: 0;
    font-family: var(--f-mono);
    font-size: 10px;
    letter-spacing: .08em;
    text-transform: uppercase;
    font-weight: 500;
    color: var(--fg-3);
    cursor: pointer;
    transition: color var(--fast) var(--ease);

    &:hover {
      color: var(--fg);
    }
  }

  .tabla__orden-icono {
    inline-size: 12px;
    block-size: 12px;
    /* Tamaño propio ⇒ trazo propio (RD-019): 1.75 × 24 / 12 */
    stroke-width: 3.5px;
    opacity: 0;
    transition: opacity var(--fast) var(--ease), rotate var(--fast) var(--ease);
  }

  .tabla__orden:hover .tabla__orden-icono {
    opacity: .6;
  }

  th[aria-sort="ascending"] .tabla__orden,
  th[aria-sort="descending"] .tabla__orden {
    color: var(--fg);
    font-weight: 700;
  }

  th[aria-sort="ascending"] .tabla__orden-icono,
  th[aria-sort="descending"] .tabla__orden-icono {
    opacity: 1;
  }

  th[aria-sort="ascending"] .tabla__orden-icono {
    rotate: 180deg;
  }

  @media (pointer: coarse) {
    .tabla__orden {
      min-block-size: 44px;
    }
  }

  /* ── Estado vacío (obligatorio: una tabla sin filas nunca queda en blanco) ── */
  .tabla__vacio {
    padding: 48px 24px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 11px;
    text-align: center;
  }

  .tabla__vacio-icono {
    inline-size: 44px;
    block-size: 44px;
    border-radius: 999px;
    background: var(--sunken);
    color: var(--fg-3);
    display: grid;
    place-items: center;
  }

  .tabla__vacio-titulo {
    font-size: 14.5px;
    font-weight: 700;
  }

  .tabla__vacio-texto {
    font-size: 13px;
    color: var(--fg-2);
    max-inline-size: 34ch;
    line-height: 1.55;
  }

  /* ── Estado de carga (esqueleto del DS: --sk-a/--sk-b en barrido) ─────────── */
  .tabla__esqueleto {
    block-size: 12px;
    border-radius: var(--r-xs);
    background: linear-gradient(90deg, var(--sk-b) 0, var(--sk-a) 50%, var(--sk-b) 100%);
    background-size: 200% 100%;
    animation: tabla-esqueleto 1.4s linear infinite;
  }

  .tabla__esqueleto--corto {
    inline-size: 52%;
  }

  .tabla__esqueleto--medio {
    inline-size: 80%;
  }

  .tabla__esqueleto--largo {
    inline-size: 100%;
  }

  @keyframes tabla-esqueleto {
    to {
      background-position: -200% 0;
    }
  }

  @media (prefers-reduced-motion: reduce) {
    .tabla__esqueleto {
      animation: none;
      background: var(--sk-b);
    }
  }

  /* ── Pie: rango mostrado, tamaño de página y paginación (parte 5) ──────────
     Uso:
       <div class="tabla__pie">
         <div class="tabla__pie-grupo">
           <span class="tabla__pie-info">Mostrando 1–20 de 143</span>
           <select class="tabla__tamano" name="tamano" hx-get="…" hx-target="…">
             <option>20</option>…
           </select>
         </div>
         <nav class="tabla__paginas" aria-label="Paginación">          ← solo con >1 página
           <button class="tabla__pagina tabla__pagina--flecha" hx-get="…?pagina=2"
                   aria-label="Página anterior">(icono i-chevron-left)</button>
           <button class="tabla__pagina" aria-current="page">3</button>
           …
         </nav>
       </div> */
  .tabla__pie {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 14px;
    padding: 13px 18px;
    flex-wrap: wrap;
  }

  .tabla__pie-grupo {
    display: flex;
    align-items: center;
    gap: 10px;
  }

  .tabla__pie-info {
    font-size: 12.5px;
    color: var(--fg-3);
  }

  .tabla__tamano {
    padding: 5px 8px;
    border: 1px solid var(--line);
    border-radius: var(--r-xs);
    background: var(--surface);
    color: var(--fg-2);
    font-family: var(--f-mono);
    font-size: 11.5px;
    cursor: pointer;
  }

  .tabla__paginas {
    display: flex;
    align-items: center;
    gap: 4px;
  }

  .tabla__pagina {
    min-inline-size: 32px;
    block-size: 32px;
    padding: 0 8px;
    border: 1px solid var(--line);
    border-radius: var(--r-xs);
    background: var(--surface);
    color: var(--fg-2);
    font-family: var(--f-mono);
    font-size: 12px;
    cursor: pointer;
    transition: border-color var(--fast) var(--ease), background var(--fast) var(--ease);

    &:hover {
      border-color: var(--line-2);
      background: var(--sunken);
    }

    &[aria-current="page"] {
      border-color: var(--acc);
      background: var(--acc);
      color: var(--acc-fg);
      font-weight: 700;
    }
  }

  .tabla__pagina--flecha {
    display: grid;
    place-items: center;
    padding: 0;
    inline-size: 32px;
  }

  /* Filtros y búsqueda dentro de las herramientas: una sola línea; encogen antes
     que mandar los botones a otra línea */
  .tabla__filtros {
    display: flex;
    align-items: center;
    flex-wrap: nowrap;
    gap: 7px;
    min-inline-size: 0;
  }

  .tabla__filtro {
    flex: 1 1 160px;
    min-inline-size: 130px;
    max-inline-size: 220px;
  }

  .tabla__busqueda {
    flex: 1 1 150px;
    min-inline-size: 110px;
    max-inline-size: 260px;
  }

  /* Acciones por fila: iconos de 30px a la derecha de la celda; la destructiva
     toma el tono danger solo al pasar el puntero */
  .tabla__col-acciones {
    inline-size: 88px;
  }

  .tabla__acciones {
    display: flex;
    justify-content: flex-end;
    gap: 2px;
  }

  .tabla__accion {
    inline-size: 30px;
    block-size: 30px;
    display: grid;
    place-items: center;
    border: none;
    border-radius: var(--r-sm);
    background: transparent;
    color: var(--fg-3);
    cursor: pointer;
    transition: background var(--fast) var(--ease), color var(--fast) var(--ease);

    &:hover {
      background: var(--sunken);
      color: var(--fg);
    }
  }

  .tabla__accion--peligro:hover {
    background: var(--danger-soft);
    color: var(--danger);
  }

  .tabla__accion .icono {
    inline-size: 15px;
    block-size: 15px;
    /* 1.75 × 24 / 15 */
    stroke-width: 2.8px;
  }

  @media (pointer: coarse) {
    .tabla__pagina,
    .tabla__tamano {
      min-block-size: 44px;
      min-inline-size: 44px;
    }
  }
}

/*!inventario
{
  "componente": "Pasos (asistente)",
  "proposito": "Un alta o proceso largo repartido en varias pantallas que se ven de una en una (NO es un modal): índice horizontal con el número y el título de cada paso, panel del paso activo y pie de acciones. El paso activo lo resuelve el SERVIDOR: Atrás y Siguiente son disparadores htmx que intercambian el componente completo; los pasos hechos pueden ser enlaces para regresar, los pendientes no. Bajo 740px el índice se compacta: solo círculos y el título del activo.",
  "raiz": "pasos",
  "variantes": {
    "pasos--compacto": "Índice más chico (círculos de 26px, sin subtítulos) y panel con menos relleno"
  },
  "estados": {
    "[aria-current='step']": "El paso activo (en pasos__paso): círculo lleno en acento y título en acento",
    "[data-estado='hecho']": "Paso completado (en pasos__paso): círculo en success con la palomita i-check en lugar del número; su pasos__enlace puede ser un <a> para regresar",
    "[data-estado='pendiente']": "Paso por venir (en pasos__paso): círculo hueco y textos en --fg-3; no es enlace"
  },
  "modificadores": {},
  "estructura": [
    { "clase": "pasos__indice", "elemento": "ol", "papel": "El índice: fila horizontal de pasos (role='list'); cada paso lleva su conector hacia el siguiente", "opcional": false },
    { "clase": "pasos__paso", "elemento": "li", "papel": "Un paso: lleva aria-current='step' o data-estado='hecho|pendiente'", "opcional": false },
    { "clase": "pasos__enlace", "elemento": "a | span", "papel": "Envoltura del número + textos; <a> (hx-get) solo en los hechos, <span> en el activo y los pendientes", "opcional": false },
    { "clase": "pasos__numero", "elemento": "span", "papel": "Círculo con el número del paso; en hecho trae el icono i-check en lugar del número", "opcional": false },
    { "clase": "pasos__textos", "elemento": "span", "papel": "Columna título + subtítulo (en angosto solo se ve la del activo)", "opcional": false },
    { "clase": "pasos__titulo", "elemento": "span", "papel": "Nombre del paso", "opcional": false },
    { "clase": "pasos__sub", "elemento": "span", "papel": "Qué se captura ahí, en una frase (se oculta en angosto y en compacto)", "opcional": true },
    { "clase": "pasos__panel", "elemento": "div", "papel": "El paso activo: tarjeta de superficie con borde, centrada a 880px; su contenido lo define la pantalla", "opcional": false },
    { "clase": "pasos__forma", "elemento": "form", "papel": "Forma que envuelve campos y pie sin romper la columna del panel (display: contents)", "opcional": true },
    { "clase": "pasos__pie", "elemento": "footer", "papel": "Barra de acciones al pie del panel: Atrás a la izquierda, la primaria a la derecha", "opcional": false },
    { "clase": "pasos__pie-nota", "elemento": "p", "papel": "Texto de ayuda (11.5px, --fg-3) que ocupa el espacio entre Atrás y las acciones", "opcional": true },
    { "clase": "pasos__pie-acciones", "elemento": "div", "papel": "Acciones del extremo derecho: la primaria (Siguiente, Crear, Iniciar) y, si acaso, una secundaria", "opcional": false }
  ],
  "internas": [],
  "contexto": ["icono"],
  "ejemplo": [
    "<div class='pasos' id='asistente-x'>",
    "  <ol class='pasos__indice' role='list' aria-label='Pasos del alta'>",
    "    <li class='pasos__paso' data-estado='hecho'>",
    "      <a class='pasos__enlace' href='?paso=1' hx-get='?paso=1' hx-target='#asistente-x' hx-swap='outerHTML' hx-push-url='true'>",
    "        <span class='pasos__numero'><svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-check' /></svg></span>",
    "        <span class='pasos__textos'><span class='pasos__titulo'>Datos</span><span class='pasos__sub'>Centro y almacén</span></span>",
    "      </a>",
    "    </li>",
    "    <li class='pasos__paso' aria-current='step'>",
    "      <span class='pasos__enlace'>",
    "        <span class='pasos__numero'>2</span>",
    "        <span class='pasos__textos'><span class='pasos__titulo'>Inventaristas y auditores</span><span class='pasos__sub'>Quién cuenta</span></span>",
    "      </span>",
    "    </li>",
    "    <li class='pasos__paso' data-estado='pendiente'>",
    "      <span class='pasos__enlace'>",
    "        <span class='pasos__numero'>3</span>",
    "        <span class='pasos__textos'><span class='pasos__titulo'>Supervisores y revisores</span><span class='pasos__sub'>Quién autoriza</span></span>",
    "      </span>",
    "    </li>",
    "    <li class='pasos__paso' data-estado='pendiente'>",
    "      <span class='pasos__enlace'>",
    "        <span class='pasos__numero'>4</span>",
    "        <span class='pasos__textos'><span class='pasos__titulo'>Programación</span><span class='pasos__sub'>Fecha y hora</span></span>",
    "      </span>",
    "    </li>",
    "  </ol>",
    "  <div class='pasos__panel'>",
    "    <form class='pasos__forma' method='post' hx-post='?paso=2' hx-target='#asistente-x' hx-swap='outerHTML'>",
    "      …campos del paso…",
    "      <footer class='pasos__pie'>",
    "        <button type='button' class='boton' hx-get='?paso=1' hx-target='#asistente-x' hx-swap='outerHTML'>Atrás</button>",
    "        <p class='pasos__pie-nota'>Puede cambiar la pareja después, desde el tablero.</p>",
    "        <div class='pasos__pie-acciones'>",
    "          <button type='submit' class='boton boton--primario'>Siguiente</button>",
    "        </div>",
    "      </footer>",
    "    </form>",
    "  </div>",
    "</div>"
  ]
}
*/
/* Pasos (asistente de varias pantallas) — el proceso largo que no cabe en un cajón
   se reparte en pasos y se ve de uno en uno, en la página (NO es un modal: el
   usuario conserva el shell y puede irse y volver). El estado es de SERVIDOR
   (AD-10, como pestañas): el paso activo vive en la URL (?paso=), el servidor pinta
   índice + panel + pie y Atrás/Siguiente son disparadores htmx que intercambian el
   componente completo. Por eso no trae JS.

   Anatomía:
     <div class="pasos">
       <ol class="pasos__indice" role="list">
         <li class="pasos__paso" data-estado="hecho">        ← <a> para regresar
           <a class="pasos__enlace" hx-get="?paso=1">
             <span class="pasos__numero">(i-check)</span>
             <span class="pasos__textos">
               <span class="pasos__titulo">…</span><span class="pasos__sub">…</span>
             </span>
           </a>
         </li>
         <li class="pasos__paso" aria-current="step">…</li>  ← <span>, no enlace
         <li class="pasos__paso" data-estado="pendiente">…</li>
       </ol>
       <div class="pasos__panel">
         <form class="pasos__forma">                          ← opcional, display: contents
           …campos…
           <footer class="pasos__pie">
             [Atrás] <p class="pasos__pie-nota">…</p>
             <div class="pasos__pie-acciones">[Siguiente | Crear | Iniciar]</div>
           </footer>
         </form>
       </div>
     </div>

   Tres estados por atributo en el paso: aria-current="step" (activo: círculo lleno
   en acento), data-estado="hecho" (palomita en success; el conector que sale de él
   también se pinta) y data-estado="pendiente" (círculo hueco, textos en --fg-3).
   El estado se comunica con número/icono Y texto, nunca solo con color.

   Angosto (≤ 740 px, el mismo umbral del shell): el índice se compacta a una fila
   de círculos con conectores cortos y solo el paso activo conserva su título; los
   demás títulos viajan en el aria-label del enlace, que pone el servidor. */

@layer componentes {
  .pasos {
    --pasos-ancho: 880px;
    --pasos-circulo: 32px;

    display: flex;
    flex-direction: column;
    gap: var(--sp-5);
    inline-size: 100%;
    max-inline-size: var(--pasos-ancho);
    margin-inline: auto;
    min-inline-size: 0;
  }

  /* ── Índice ────────────────────────────────────────────────────────────── */
  .pasos__indice {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    margin: 0;
    padding: 0;
    list-style: none;
  }

  /* Cada paso reparte el ancho y lleva su conector hacia el siguiente; el último
     no reparte ni conecta: se pega al extremo derecho. */
  .pasos__paso {
    flex: 1;
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    min-inline-size: 0;

    &::after {
      content: "";
      flex: 1;
      min-inline-size: var(--sp-4);
      block-size: 2px;
      border-radius: 999px;
      background: var(--line);
      transition: background var(--fast) var(--ease);
    }

    &:last-child {
      flex: none;

      &::after {
        display: none;
      }
    }

    /* El tramo recorrido: el conector que sale de un paso hecho */
    &[data-estado="hecho"]::after {
      background: var(--success);
    }
  }

  .pasos__enlace {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 6px 8px;
    margin-inline-start: -8px;
    border-radius: var(--r-sm);
    color: var(--fg);
    font: inherit;
    text-decoration: none;
    min-inline-size: 0;
    transition: background var(--fast) var(--ease);
  }

  /* Solo los hechos son <a>; el hover deja claro que se puede regresar */
  a.pasos__enlace:hover {
    background: var(--sunken);
    color: var(--fg);
  }

  a.pasos__enlace:focus-visible {
    outline: 2px solid var(--ring);
    outline-offset: 2px;
  }

  /* Círculo del número: hueco por defecto (pendiente), lleno en el activo,
     palomita en success cuando ya se hizo. */
  .pasos__numero {
    flex: none;
    inline-size: var(--pasos-circulo);
    block-size: var(--pasos-circulo);
    display: grid;
    place-items: center;
    border-radius: 999px;
    border: 1.5px solid var(--line-2);
    background: var(--surface);
    color: var(--fg-3);
    font-family: var(--f-ui);
    font-size: 13px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    line-height: 1;
    transition: background var(--fast) var(--ease), border-color var(--fast) var(--ease), color var(--fast) var(--ease);
  }

  .pasos__numero .icono {
    inline-size: 16px;
    block-size: 16px;
  }

  .pasos__paso[aria-current="step"] .pasos__numero {
    border-color: var(--acc);
    background: var(--acc);
    color: var(--acc-fg);
    box-shadow: 0 0 0 4px var(--acc-soft);
  }

  .pasos__paso[data-estado="hecho"] .pasos__numero {
    border-color: var(--success);
    background: var(--success-soft);
    color: var(--success);
  }

  .pasos__textos {
    display: flex;
    flex-direction: column;
    min-inline-size: 0;
  }

  .pasos__titulo {
    font-size: 13px;
    font-weight: 600;
    line-height: 1.25;
    color: var(--fg);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .pasos__paso[aria-current="step"] .pasos__titulo {
    color: var(--acc);
  }

  .pasos__paso[data-estado="pendiente"] .pasos__titulo {
    color: var(--fg-3);
    font-weight: 500;
  }

  .pasos__sub {
    font-size: 11.5px;
    line-height: 1.35;
    color: var(--fg-3);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  /* ── Panel del paso activo ─────────────────────────────────────────────── */
  .pasos__panel {
    display: flex;
    flex-direction: column;
    gap: var(--sp-4);
    min-inline-size: 0;
    padding: var(--sp-5);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    background: var(--surface);
  }

  /* La forma envuelve campos y pie sin romper la columna del panel */
  .pasos__forma {
    display: contents;
  }

  /* ── Pie de acciones ───────────────────────────────────────────────────── */
  .pasos__pie {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 9px;
    margin-block-start: var(--sp-2);
    padding-block-start: var(--sp-4);
    border-block-start: 1px solid var(--line);
  }

  .pasos__pie-nota {
    margin: 0;
    flex: 1;
    min-inline-size: 12ch;
    font-size: 11.5px;
    line-height: 1.35;
    color: var(--fg-3);
  }

  /* Las acciones del extremo derecho se van al final aunque no haya Atrás ni nota */
  .pasos__pie-acciones {
    display: flex;
    align-items: center;
    gap: 9px;
    margin-inline-start: auto;
  }

  /* ── Variante compacta ─────────────────────────────────────────────────── */
  .pasos--compacto {
    --pasos-circulo: 26px;

    & .pasos__numero {
      font-size: 12px;
    }

    & .pasos__numero .icono {
      inline-size: 14px;
      block-size: 14px;
      stroke-width: 3px; /* 1.75 × 24 / 14 */
    }

    & .pasos__titulo {
      font-size: 12px;
    }

    & .pasos__sub {
      display: none;
    }

    & .pasos__panel {
      padding: var(--sp-4);
    }
  }

  /* ── Angosto: fila de círculos, solo el activo conserva su título ──────── */
  @media (max-width: 740px) {
    .pasos {
      gap: var(--sp-4);
    }

    .pasos__indice {
      gap: var(--sp-1);
    }

    .pasos__paso {
      gap: var(--sp-1);

      &::after {
        min-inline-size: var(--sp-3);
      }

      /* El activo es el único que reparte ancho: los demás son solo su círculo */
      &:not([aria-current="step"]) {
        flex: none;
      }

      &:not([aria-current="step"]) .pasos__textos {
        display: none;
      }
    }

    .pasos__enlace {
      gap: 8px;
      padding: 4px 6px;
      margin-inline-start: -6px;
    }

    .pasos__sub {
      display: none;
    }

    .pasos__panel {
      padding: var(--sp-4);
    }
  }

  @media (pointer: coarse) {
    a.pasos__enlace {
      min-block-size: 44px;
    }
  }

  @media (prefers-reduced-motion: reduce) {
    .pasos__paso::after,
    .pasos__enlace,
    .pasos__numero {
      transition: none;
    }
  }
}

/*!inventario
{
  "componente": "Cápsula de carga",
  "proposito": "Señal de espera al centro de la ventana: aparece cuando la petición corre detrás de un velo (cajón o modal) y, para cualquier petición, cuando pasa de 4 s («Esto está tardando más de lo normal…»); también mientras la página tenga un elemento con data-carga-en-curso='texto' (una corrida en segundo plano sondeada por un fragmento), con ese texto y sin bloquear. La crea y retira carga.js: nunca se maqueta a mano.",
  "raiz": "carga",
  "variantes": {},
  "estados": {
    "[data-lento='si']": "Lleva más de 4 s: texto de lentitud y tono de advertencia (lo pone carga.js)",
    "[hidden]": "Sin peticiones que mostrar (carga.js la oculta en vez de destruirla)",
    "[data-carga-en-vuelo='si']": "En cualquier disparador sin `disabled` (enlace, select) mientras su petición vuela: sin puntero y atenuado (lo pone y quita carga.js)",
    "[data-carga-navegacion='si']": "En <html> mientras hay una navegación entre páginas del shell (boosted) en curso: barra indeterminada fija en el borde superior de la ventana desde el primer instante (lo pone y quita carga.js)"
  },
  "modificadores": {},
  "estructura": [
    { "clase": "carga__barra", "elemento": "span", "papel": "Barra indeterminada (misma animación carga-barra del estado aria-busy), aria-hidden", "opcional": false },
    { "clase": "carga__texto", "elemento": "p", "papel": "«Procesando…», «Esto está tardando más de lo normal…» o el texto de data-carga-en-curso", "opcional": false }
  ],
  "internas": [],
  "ejemplo": [
    "<div class='carga' role='status' aria-live='polite' data-lento='no'>",
    "  <span class='carga__barra' aria-hidden='true'></span>",
    "  <p class='carga__texto'>Procesando…</p>",
    "</div>"
  ]
}
*/
/* Cápsula de carga — complemento del estado aria-busy (base.css). Aquel atenúa
   el bloque que espera; esta se ve aunque ese bloque quede detrás del velo de un
   cajón o modal, o cuando la espera ya se hizo larga. Sigue la regla del sistema:
   una barra, no un giro. Vive por encima del cajón y del modal (z 80). */

@layer componentes {
  /* Disparador sin `disabled` (enlace, select) con una petición en vuelo: no se
     vuelve a disparar hasta que responde (lo pone y quita carga.js) */
  [data-carga-en-vuelo="si"] {
    pointer-events: none;
    opacity: 0.6;
  }

  /* Navegación del shell (boosted): barra fija arriba de todo, sin gracia, para que
     cambiar de página se lea como «cargando» y no como «trabado». El contenido
     que se reemplaza se atenúa aparte (aria-busy sobre shell__contenido). */
  html[data-carga-navegacion="si"]::after {
    content: "";
    position: fixed;
    inset-block-start: 0;
    inset-inline: 0;
    block-size: 3px;
    z-index: 100;
    pointer-events: none;
    background: linear-gradient(90deg, transparent 35%, var(--acc) 50%, transparent 65%);
    background-size: 200% 100%;
    animation: carga-barra 1.1s var(--ease) infinite;
  }

  .carga {
    position: fixed;
    inset-block-start: 50%;
    inset-inline-start: 50%;
    translate: -50% -50%;
    z-index: 90;
    display: flex;
    flex-direction: column;
    gap: 10px;
    min-inline-size: 240px;
    max-inline-size: min(360px, calc(100vw - 32px));
    padding: 14px 18px 13px;
    background: var(--raised);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    box-shadow: var(--e4);
    pointer-events: none;
    animation: carga-aparecer var(--fast) var(--ease) both;
  }

  .carga[hidden] {
    display: none;
  }

  .carga__barra {
    display: block;
    block-size: 3px;
    border-radius: 3px;
    background: linear-gradient(90deg, transparent 35%, var(--acc) 50%, transparent 65%);
    background-size: 200% 100%;
    animation: carga-barra 1.1s var(--ease) infinite;
  }

  .carga__texto {
    margin: 0;
    font-size: 13px;
    line-height: 1.4;
    color: var(--fg-2);
  }

  .carga[data-lento="si"] {
    border-color: var(--warning-bd);
  }

  .carga[data-lento="si"] .carga__barra {
    background-image: linear-gradient(90deg, transparent 35%, var(--warning) 50%, transparent 65%);
  }

  .carga[data-lento="si"] .carga__texto {
    color: var(--warning);
    font-weight: 600;
  }

  @keyframes carga-aparecer {
    from { opacity: 0; translate: -50% calc(-50% + 6px); }
    to { opacity: 1; translate: -50% -50%; }
  }

  @media (prefers-reduced-motion: reduce) {
    .carga {
      animation: none;
    }

    .carga__barra,
    html[data-carga-navegacion="si"]::after {
      animation: none;
      background: var(--acc);
    }

    .carga[data-lento="si"] .carga__barra {
      background: var(--warning);
    }
  }
}

/*!inventario
{
  "componente": "Barra de progreso",
  "proposito": "Avance determinado de un conjunto (inventarios completados de un semestral, ubicaciones contadas): carril + tramo lleno según el aria-valuenow que pinta el servidor (progreso.js lo pasa a --progreso), y la cifra al lado. No es el indicador de capacidad (carga de una persona contra su techo).",
  "raiz": "progreso",
  "variantes": {
    "progreso--exito": "Tramo en success (completado)",
    "progreso--advertencia": "Tramo en warning (atrasado, parcial)",
    "progreso--revision": "Tramo en review (hecho, espera decisión)",
    "progreso--peligro": "Tramo en danger"
  },
  "estados": {},
  "modificadores": {
    "progreso--alta": "Carril de 10px en lugar de 6px (tarjetas y resúmenes)"
  },
  "estructura": [
    { "clase": "progreso__carril", "elemento": "span", "papel": "El carril; el avance lo da aria-valuenow (con aria-valuemin y aria-valuemax) en la raíz, nunca un atributo style: la CSP de la app lo bloquea", "opcional": false },
    { "clase": "progreso__tramo", "elemento": "span", "papel": "El tramo lleno (inline-size = --progreso, que escribe progreso.js)", "opcional": false },
    { "clase": "progreso__cifra", "elemento": "span", "papel": "«n de m» o «NN%» en la fuente de interfaz con dígitos tabulares, a la derecha", "opcional": true }
  ],
  "internas": [],
  "ejemplo": [
    "<span class='progreso progreso--exito' role='progressbar' aria-valuenow='62' aria-valuemin='0' aria-valuemax='100'>",
    "  <span class='progreso__carril'><span class='progreso__tramo'></span></span>",
    "  <span class='progreso__cifra'>13 de 21</span>",
    "</span>"
  ]
}
*/
/* Barra de progreso — avance determinado de un conjunto. El color es del tono,
   nunca el único dato: la cifra acompaña.

   El servidor pinta solo aria-valuenow (y aria-valuemin/aria-valuemax si no son 0 y
   100); progreso.js calcula --progreso y lo escribe desde el script. No va un
   style="--progreso: NN%": la CSP de la app (style-src 'self') lo ignora. */

@layer componentes {
  .progreso {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    inline-size: 100%;
    min-inline-size: 0;
  }

  .progreso__carril {
    flex: 1;
    block-size: 6px;
    border-radius: 999px;
    background: var(--sunken);
    overflow: hidden;
  }

  .progreso--alta .progreso__carril {
    block-size: 10px;
  }

  .progreso__tramo {
    display: block;
    block-size: 100%;
    inline-size: var(--progreso, 0%);
    border-radius: inherit;
    background: var(--acc);
    transition: inline-size var(--med) var(--ease);
  }

  .progreso--exito .progreso__tramo {
    background: var(--success);
  }

  .progreso--advertencia .progreso__tramo {
    background: var(--warning);
  }

  .progreso--revision .progreso__tramo {
    background: var(--review);
  }

  .progreso--peligro .progreso__tramo {
    background: var(--danger);
  }

  .progreso__cifra {
    flex: none;
    /* Misma tipografía que el resto de las cifras de la pantalla (fechas, totales),
       con dígitos tabulares: en mono el 0 y el 8 se confundían a este tamaño. */
    font-family: var(--f-ui);
    font-variant-numeric: tabular-nums;
    font-weight: 600;
    font-size: 11.5px;
    color: var(--fg-2);
    white-space: nowrap;
  }

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

/*!inventario
{
  "componente": "Menú de acciones",
  "proposito": "Menú «⋯» con las acciones secundarias de un registro o una vista (imprimir, exportar, duplicar, dar de baja) en una capa flotante. Va en el encabezado de la página, junto a la acción primaria, nunca dentro de la ficha. La acción destructiva va al final, tras un separador, en --danger, y se confirma con la alerta centrada (hx-confirm). menu-acciones.js abre y cierra (clic, Escape, clic fuera) y mueve el foco con las flechas.",
  "raiz": "menu-acciones",
  "variantes": {},
  "estados": {
    "[data-abierto='si']": "Desplegable visible; 'no' = cerrado (lo alterna menu-acciones.js junto con aria-expanded del botón)"
  },
  "modificadores": {
    "menu-acciones__opcion--peligro": "La acción destructiva: texto en --danger, fondo --danger-soft al pasar el puntero; siempre al final y tras el separador"
  },
  "estructura": [
    { "clase": "menu-acciones__boton", "elemento": "button", "papel": "El disparador de 34px con i-ellipsis; va solo (trae su propio reset, sin boton); aria-haspopup='menu', aria-expanded y aria-label que diga de qué son las acciones", "opcional": false },
    { "clase": "menu-acciones__desplegable", "elemento": "div", "papel": "La capa flotante (role='menu'), alineada al borde derecho del botón", "opcional": false },
    { "clase": "menu-acciones__opcion", "elemento": "a | button", "papel": "Una acción (role='menuitem') con icono y texto", "opcional": false },
    { "clase": "menu-acciones__separador", "elemento": "div", "papel": "Línea de 1px (--line) antes de la acción destructiva (role='separator')", "opcional": true }
  ],
  "internas": [],
  "contexto": ["icono", "alerta"],
  "ejemplo": [
    "<div class='menu-acciones' data-componente='menu-acciones' data-abierto='no'>",
    "  <button type='button' class='menu-acciones__boton' data-accion='alternar-menu-acciones' aria-haspopup='menu' aria-expanded='false' aria-label='Más acciones del expediente'>",
    "    <svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-ellipsis' /></svg>",
    "  </button>",
    "  <div class='menu-acciones__desplegable' role='menu'>",
    "    <button type='button' class='menu-acciones__opcion' role='menuitem'><svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-printer' /></svg> Imprimir expediente</button>",
    "    <a class='menu-acciones__opcion' role='menuitem' href='/expedientes/3.pdf'><svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-file-down' /></svg> Exportar a PDF</a>",
    "    <div class='menu-acciones__separador' role='separator'></div>",
    "    <button type='button' class='menu-acciones__opcion menu-acciones__opcion--peligro' role='menuitem' hx-post='/expedientes/3/baja'",
    "            hx-confirm='El expediente deja de estar vigente y sale de los listados activos.' data-alerta-titulo='¿Dar de baja el expediente?' data-alerta-confirmar='Dar de baja' data-alerta-tono='peligro'>",
    "      <svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-user-x' /></svg> Dar de baja el expediente</button>",
    "  </div>",
    "</div>"
  ]
}
*/
/* Menú de acciones «⋯» — capa flotante como la del menú de usuario (raised, --e4), pero sobre
   fondo claro y con el disparador de borde del sistema. */

@layer componentes {
  /* block-size explícito: dentro de un contenedor flex la raíz no se estira, así el desplegable
     abre pegado al botón y no al fondo del contenedor */
  .menu-acciones {
    position: relative;
    display: inline-flex;
    block-size: max-content;
  }

  /* Es un <button>: trae su propio reset (va solo, sin «boton») */
  .menu-acciones__boton {
    display: grid;
    place-items: center;
    inline-size: 34px;
    block-size: 34px;
    padding: 0;
    border: 1px solid var(--line-2);
    border-radius: var(--r-sm);
    background: var(--surface);
    color: var(--fg-2);
    font: inherit;
    cursor: pointer;
    transition: background var(--fast) var(--ease), color var(--fast) var(--ease);

    &:hover {
      background: var(--sunken);
      color: var(--fg);
    }
  }

  .menu-acciones[data-abierto="si"] .menu-acciones__boton {
    background: var(--sunken);
    color: var(--fg);
  }

  .menu-acciones__boton .icono {
    inline-size: 17px;
    block-size: 17px;
  }

  .menu-acciones__desplegable {
    position: absolute;
    inset-inline-end: 0;
    inset-block-start: calc(100% + 6px);
    z-index: 70;
    min-inline-size: 220px;
    padding: 6px;
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    background: var(--raised);
    box-shadow: var(--e4);
    color: var(--fg);
  }

  .menu-acciones[data-abierto="no"] .menu-acciones__desplegable {
    display: none;
  }

  .menu-acciones__opcion {
    display: flex;
    align-items: center;
    gap: 10px;
    inline-size: 100%;
    padding: 8px 11px;
    border: none;
    border-radius: var(--r-xs);
    background: transparent;
    color: var(--fg);
    font-family: var(--f-ui);
    font-size: 13px;
    text-align: start;
    text-decoration: none;
    white-space: nowrap;
    cursor: pointer;

    &:hover {
      background: var(--sunken);
      color: var(--fg);
    }

    /* El anillo va por dentro: la capa recorta lo que sale de ella */
    &:focus-visible {
      outline-offset: -2px;
    }
  }

  .menu-acciones__opcion .icono {
    flex: none;
    inline-size: 15px;
    block-size: 15px;
  }

  .menu-acciones__opcion--peligro {
    color: var(--danger);

    &:hover {
      background: var(--danger-soft);
      color: var(--danger);
    }
  }

  .menu-acciones__separador {
    block-size: 1px;
    margin-block: 5px;
    background: var(--line);
  }

  @media (pointer: coarse) {
    .menu-acciones__boton {
      inline-size: 44px;
      block-size: 44px;
    }

    .menu-acciones__opcion {
      min-block-size: 44px;
    }
  }
}

/*!inventario
{
  "componente": "Notificaciones",
  "proposito": "Centro de notificaciones del shell: la campana (shell__accion existente) con un globo de no leídas y una bandeja en cajón lateral con la lista persistente de avisos por persona. Complementa al toast: lo que pasó mientras no se estaba viendo sigue aquí. notificaciones.js enciende la campana, pinta la lista con los datos que le da la aplicación y mantiene el conteo (también en el título de la pestaña).",
  "raiz": "notificaciones",
  "variantes": {},
  "estados": {
    "[data-leida='no']": "Ítem sin leer: fondo --acc-soft, punto a la izquierda del título y estado «Sin leer» en acento; leído: estado «Leída» con palomita en --fg-3",
    "[data-tono='exito']": "Icono del ítem en success",
    "[data-tono='informativo']": "Icono del ítem en info",
    "[data-tono='advertencia']": "Icono del ítem en warning (sobre fondo suave: --warning-fg)",
    "[data-tono='peligro']": "Icono del ítem en danger",
    "[data-tono='revision']": "Icono del ítem en review"
  },
  "modificadores": {},
  "estructura": [
    { "clase": "notificaciones__campana", "elemento": "button.shell__accion", "papel": "La campana del encabezado del shell: abre la bandeja (data-accion='abrir-cajon' data-cajon='#cajon-notificaciones')", "opcional": false },
    { "clase": "notificaciones__conteo", "elemento": "span", "papel": "Globo con el número de no leídas sobre la campana; hidden en 0, aria-label con el conteo", "opcional": false },
    { "clase": "notificaciones__lista", "elemento": "div", "papel": "Lista de ítems dentro del cuerpo del cajón (columna sin separación; cada ítem lleva su filo)", "opcional": false },
    { "clase": "notificaciones__item", "elemento": "a", "papel": "Un aviso: enlace al lugar del hecho; al clic se marca leído y navega", "opcional": false },
    { "clase": "notificaciones__item-icono", "elemento": "span > svg.icono", "papel": "Círculo de 32px con el icono en el tono del ítem", "opcional": false },
    { "clase": "notificaciones__item-cuerpo", "elemento": "div", "papel": "Columna con título, texto y la línea de fecha y estado", "opcional": false },
    { "clase": "notificaciones__item-titulo", "elemento": "p", "papel": "Qué pasó, en una línea", "opcional": false },
    { "clase": "notificaciones__item-texto", "elemento": "p", "papel": "Detalle en una frase (--fg-2)", "opcional": true },
    { "clase": "notificaciones__item-meta", "elemento": "div", "papel": "Línea inferior: fecha y estado de lectura", "opcional": false },
    { "clase": "notificaciones__item-fecha", "elemento": "time", "papel": "Cuándo, relativo («hace 5 min»); el datetime ISO va en el atributo", "opcional": false },
    { "clase": "notificaciones__item-estado", "elemento": "span", "papel": "«Sin leer» (punto en acento) o «Leída» (palomita); notificaciones.js lo cambia al marcar", "opcional": false },
    { "clase": "notificaciones__vacio", "elemento": "p", "papel": "«Sin notificaciones» cuando la lista está vacía", "opcional": false },
    { "clase": "notificaciones__acciones", "elemento": "div", "papel": "Pie del cajón con «Marcar todas como leídas» (data-accion='notificaciones-leer-todas')", "opcional": false }
  ],
  "internas": [],
  "contexto": ["shell__accion", "cajon", "cajon__cuerpo", "boton", "icono"],
  "ejemplo": [
    "<button type='button' class='shell__accion notificaciones__campana' data-accion='abrir-cajon' data-cajon='#cajon-notificaciones' aria-label='Notificaciones'>",
    "  <svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-bell' /></svg>",
    "  <span class='notificaciones__conteo' hidden>0</span>",
    "</button>",
    "<div id='cajon-notificaciones' class='cajon notificaciones' data-componente='cajon' data-abierto='no' role='dialog' aria-modal='true' aria-labelledby='titulo-notificaciones'>",
    "  <button class='cajon__velo' data-accion='cerrar-cajon' tabindex='-1' aria-label='Cerrar'></button>",
    "  <div class='cajon__panel'>",
    "    <header class='cajon__encabezado'>",
    "      <div class='cajon__superior'><span class='cajon__kicker'>Panel</span><button type='button' class='cajon__cerrar' data-accion='cerrar-cajon' aria-label='Cerrar'>(i-x)</button></div>",
    "      <h2 class='cajon__titulo' id='titulo-notificaciones'>Notificaciones</h2>",
    "    </header>",
    "    <div class='cajon__cuerpo'><div class='notificaciones__lista'></div><p class='notificaciones__vacio' hidden>Sin notificaciones</p></div>",
    "    <footer class='cajon__pie'><div class='notificaciones__acciones'><button type='button' class='boton' data-accion='notificaciones-leer-todas'>Marcar todas como leídas</button></div></footer>",
    "  </div>",
    "</div>",
    "window.notificaciones.iniciar({ cargar: async () => [...], marcarLeida: async (id) => {}, marcarTodas: async () => {} })"
  ]
}
*/
/* Notificaciones — el centro de notificaciones del shell (sección 24: lo que no
   interrumpe). La campana es la acción del encabezado que ya existía deshabilitada;
   notificaciones.js la enciende y le cuelga el globo de no leídas. La bandeja es un
   cajón del DS con la lista persistente: lo que llegó como toast mientras no se
   miraba sigue aquí hasta marcarse leído. Sin sonido, sin preferencias por usuario:
   qué llega a quién lo decide la aplicación. */
@layer componentes {
  /* La raíz va en el cajón de la bandeja: ancho propio y cuerpo sin relleno para
     que los ítems corran de borde a borde con su propio filo. */
  .notificaciones {
    --cajon-ancho: 420px;
  }

  .notificaciones .cajon__cuerpo {
    padding: 0;
    gap: 0;
  }

  .notificaciones__campana {
    position: relative;
  }

  /* Globo de no leídas sobre la campana (coral claro sobre la marca, como el punto
     de pendiente del shell; con borde de la marca para que no se pegue al icono) */
  .notificaciones__conteo {
    position: absolute;
    inset-block-start: 1px;
    inset-inline-end: 1px;
    min-inline-size: 18px;
    block-size: 18px;
    padding-inline: 5px;
    border-radius: 999px;
    display: grid;
    place-items: center;
    background: var(--danger);
    color: #FFFFFF;
    border: 1.5px solid var(--marca);
    font-family: var(--f-ui);
    font-size: 10.5px;
    font-weight: 700;
    line-height: 1;
    pointer-events: none;
  }

  .notificaciones__conteo[hidden] {
    display: none;
  }

  /* La lista va sin separación: cada ítem lleva su propio filo */
  .notificaciones__lista {
    display: flex;
    flex-direction: column;
  }

  .notificaciones__item {
    --notificacion-tono: var(--acc);
    --notificacion-tono-suave: var(--acc-soft);
    --notificacion-icono: var(--acc);
    position: relative;
    display: flex;
    align-items: flex-start;
    gap: 12px;
    padding: 14px 20px 14px 22px;
    border-block-end: 1px solid var(--divider);
    color: var(--fg);
    text-decoration: none;
    background: var(--surface);
    transition: background var(--fast) var(--ease);
  }

  .notificaciones__item:hover,
  .notificaciones__item:focus-visible {
    background: var(--sunken);
  }

  .notificaciones__item[data-leida="no"] {
    background: var(--acc-soft);
  }

  .notificaciones__item[data-leida="no"]:hover,
  .notificaciones__item[data-leida="no"]:focus-visible {
    background: var(--acc-soft-2);
  }

  /* Punto de no leída al margen izquierdo */
  .notificaciones__item[data-leida="no"]::before {
    content: "";
    position: absolute;
    inset-inline-start: 9px;
    inset-block-start: 22px;
    inline-size: 7px;
    block-size: 7px;
    border-radius: 999px;
    background: var(--acc);
  }

  .notificaciones__item[data-tono="exito"] {
    --notificacion-tono: var(--success);
    --notificacion-tono-suave: var(--success-soft);
    --notificacion-icono: var(--success);
  }

  .notificaciones__item[data-tono="informativo"] {
    --notificacion-tono: var(--info);
    --notificacion-tono-suave: var(--info-soft);
    --notificacion-icono: var(--info);
  }

  .notificaciones__item[data-tono="advertencia"] {
    --notificacion-tono: var(--warning);
    --notificacion-tono-suave: var(--warning-soft);
    --notificacion-icono: var(--warning-fg);
  }

  .notificaciones__item[data-tono="peligro"] {
    --notificacion-tono: var(--danger);
    --notificacion-tono-suave: var(--danger-soft);
    --notificacion-icono: var(--danger);
  }

  .notificaciones__item[data-tono="revision"] {
    --notificacion-tono: var(--review);
    --notificacion-tono-suave: var(--review-soft);
    --notificacion-icono: var(--review);
  }

  .notificaciones__item-icono {
    flex: none;
    inline-size: 32px;
    block-size: 32px;
    border-radius: 999px;
    display: grid;
    place-items: center;
    background: var(--notificacion-tono-suave);
    color: var(--notificacion-icono);
  }

  .notificaciones__item-icono .icono {
    inline-size: 17px;
    block-size: 17px;
  }

  .notificaciones__item-cuerpo {
    flex: 1;
    min-inline-size: 0;
    display: flex;
    flex-direction: column;
    gap: 3px;
  }

  .notificaciones__item-titulo {
    margin: 0;
    font-size: 13.5px;
    font-weight: 700;
    line-height: 1.35;
    overflow-wrap: anywhere;
  }

  .notificaciones__item-texto {
    margin: 0;
    font-size: 12.5px;
    line-height: 1.5;
    color: var(--fg-2);
    overflow-wrap: anywhere;
  }

  .notificaciones__item-meta {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-block-start: 2px;
  }

  .notificaciones__item-fecha {
    font-family: var(--f-mono);
    font-size: 10.5px;
    color: var(--fg-3);
  }

  /* Estado de lectura explícito, además del fondo: «Sin leer» con punto en
     acento; «Leída» con palomita apagada. */
  .notificaciones__item-estado {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    font-family: var(--f-ui);
    font-size: 10.5px;
    font-weight: 700;
    color: var(--fg-3);
  }

  .notificaciones__item-estado::before {
    content: "\2713";
    font-size: 11px;
    line-height: 1;
  }

  .notificaciones__item[data-leida="no"] .notificaciones__item-estado {
    color: var(--acc);
  }

  .notificaciones__item[data-leida="no"] .notificaciones__item-estado::before {
    content: "";
    inline-size: 6px;
    block-size: 6px;
    border-radius: 999px;
    background: var(--acc);
  }

  .notificaciones__vacio {
    margin: 0;
    padding: 32px 0;
    text-align: center;
    font-size: 13px;
    color: var(--fg-3);
  }

  .notificaciones__vacio[hidden] {
    display: none;
  }

  .notificaciones__acciones {
    display: flex;
    justify-content: flex-end;
    gap: 9px;
  }

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

/*!inventario
{
  "componente": "Página simple",
  "proposito": "Página FUERA del shell (sesión cerrada, acceso denegado, error): columna centrada y angosta sobre el fondo de la página, con la marca, un icono grande en el tono del mensaje, título, una frase, una nota y las acciones.",
  "raiz": "pagina-simple",
  "variantes": {},
  "estados": {},
  "modificadores": {
    "pagina-simple__icono--peligro": "Icono en tono danger (acceso denegado)",
    "pagina-simple__icono--advertencia": "Icono en tono warning (error)",
    "pagina-simple__icono--exito": "Icono en tono success (sesión cerrada)"
  },
  "estructura": [
    { "clase": "pagina-simple__tarjeta", "elemento": "div", "papel": "La tarjeta central con el contenido", "opcional": false },
    { "clase": "pagina-simple__marca", "elemento": "div", "papel": "Logotipo + nombre de la aplicación", "opcional": true },
    { "clase": "pagina-simple__logo", "elemento": "img", "papel": "El logotipo a color (logo-nutrimav-color.png), 38px de alto; en tema oscuro se vuelve blanco", "opcional": true },
    { "clase": "pagina-simple__nombre", "elemento": "span", "papel": "Nombre de la aplicación en display", "opcional": true },
    { "clase": "pagina-simple__icono", "elemento": "span > svg.icono", "papel": "Círculo de 58px con el icono del mensaje, en su tono", "opcional": true },
    { "clase": "pagina-simple__titulo", "elemento": "h1", "papel": "Qué pasó, en display", "opcional": false },
    { "clase": "pagina-simple__texto", "elemento": "p", "papel": "Una frase que lo explica", "opcional": false },
    { "clase": "pagina-simple__nota", "elemento": "p", "papel": "Detalle secundario (a quién acudir, un identificador en code)", "opcional": true },
    { "clase": "pagina-simple__acciones", "elemento": "div", "papel": "Botones de salida, centrados", "opcional": true }
  ],
  "internas": [],
  "contexto": ["icono"],
  "ejemplo": [
    "<main class='pagina-simple'>",
    "  <div class='pagina-simple__tarjeta'>",
    "    <div class='pagina-simple__marca'><img class='pagina-simple__logo' src='/images/logo-nutrimav-color.png' alt='Mariana Vargas, Nutrióloga' /></div>",
    "    <span class='pagina-simple__icono pagina-simple__icono--peligro'><svg class='icono icono--24' aria-hidden='true'><use href='/images/iconos.svg#i-shield' /></svg></span>",
    "    <h1 class='pagina-simple__titulo'>Acceso denegado</h1>",
    "    <p class='pagina-simple__texto'>Tu cuenta no tiene permiso para entrar a esta sección.</p>",
    "    <p class='pagina-simple__nota'>Si crees que es un error, pídele a la nutrióloga titular que revise tus permisos.</p>",
    "    <div class='pagina-simple__acciones'><a class='boton boton--primario' href='/'>Ir al inicio</a></div>",
    "  </div>",
    "</main>"
  ]
}
*/
/* Página fuera del shell (sesión cerrada, acceso denegado, error): columna
   centrada y angosta sobre el fondo de la página. El contenido es fijo por
   diseño (marca, icono, título, frase, nota, acciones): las pantallas no inventan
   su propia versión. */

@layer componentes {
  .pagina-simple {
    min-block-size: 100dvh;
    display: grid;
    place-items: center;
    padding: var(--sp-5);
    background: var(--bg);
  }

  .pagina-simple__tarjeta {
    inline-size: min(560px, 100%);
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    box-shadow: var(--e2);
    padding: var(--sp-6);
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: var(--sp-2);
  }

  .pagina-simple__marca {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-block-end: var(--sp-3);
  }

  .pagina-simple__logo {
    block-size: 38px;
    inline-size: auto;
  }

  /* En oscuro el logotipo a color se vuelve blanco (mismo trazo que logo-nutrimav-blanco.png):
     la elección de la persona (data-theme) y, sin elección, la del sistema operativo. */
  [data-theme="dark"] .pagina-simple__logo {
    filter: brightness(0) invert(1);
  }

  @media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .pagina-simple__logo {
      filter: brightness(0) invert(1);
    }
  }

  .pagina-simple__nombre {
    font-family: var(--f-display);
    font-size: 17px;
  }

  .pagina-simple__icono {
    inline-size: 58px;
    block-size: 58px;
    border-radius: 999px;
    display: grid;
    place-items: center;
    margin-block-end: var(--sp-2);
    background: var(--sunken);
    color: var(--fg-2);
  }

  .pagina-simple__icono--peligro {
    background: var(--danger-soft);
    color: var(--danger);
  }

  .pagina-simple__icono--advertencia {
    background: var(--warning-soft);
    color: var(--warning);
  }

  .pagina-simple__icono--exito {
    background: var(--success-soft);
    color: var(--success);
  }

  .pagina-simple__titulo {
    font-family: var(--f-display);
    font-size: 21px;
    line-height: 1.2;
  }

  .pagina-simple__texto {
    color: var(--fg-2);
    font-size: 13.5px;
    max-inline-size: 44ch;
  }

  .pagina-simple__nota {
    color: var(--fg-3);
    font-size: 12px;
  }

  /* Un identificador largo (p. ej. un TraceId de 32 caracteres) se parte solo si no
     cabe en el renglón: sin esto, a 320 px la tarjeta se ensancha y hay desplazamiento
     horizontal (WCAG 1.4.10). */
  .pagina-simple__nota code {
    font-family: var(--f-mono);
    background: var(--sunken);
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    padding: 1px 7px;
    overflow-wrap: anywhere;
  }

  .pagina-simple__acciones {
    display: flex;
    gap: 9px;
    margin-block-start: var(--sp-3);
    flex-wrap: wrap;
    justify-content: center;
  }
}

/*!inventario
{
  "componente": "Icono",
  "proposito": "Icono de trazo Lucide desde el sprite generado /images/iconos.svg, vía <use>. Hereda el color del texto; el trazo se deriva del tamaño (RD-019).",
  "raiz": "icono",
  "variantes": {},
  "estados": {},
  "modificadores": {
    "icono--20": "Autónomo mediano (escalera 16·20·24)",
    "icono--24": "Autónomo grande"
  },
  "estructura": [
    { "clase": "icono", "elemento": "svg > use", "papel": "El svg lleva aria-hidden='true'; el use apunta a /images/iconos.svg#i-<nombre>", "opcional": false }
  ],
  "internas": [],
  "ejemplo": [
    "<svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-house' /></svg>"
  ]
}
*/
/* Icono de trazo — familia Lucide, declarada por el DS (RD-018, sistema v2.12.1).
   Consume el sprite GENERADO /images/iconos.svg (tools/sprite/) vía <use>. Hereda
   el color del texto.

   EL TAMAÑO SALE DE DOS GENERADORES, no de una lista (RD-019):
   — En línea, junto a texto: tamaño = texto + 2 px (banda de 11 a 17 px).
   — Autónomo, sin texto que fije la medida: escalera 16 · 20 · 24 (estas clases).
   — Más de 24 px es decorativo: fuera de escala, conservando el trazo efectivo.

   TRAZO POR TAMAÑO: fórmula 1.75 × 24 / tamaño, para que el trazo EFECTIVO sea
   constante. REGLA DURA del documento: la regla de trazo existe PARA CADA TAMAÑO
   EN USO — un componente que declare un tamaño propio (ej. 12 px del chip) declara
   AHÍ MISMO su stroke-width con la fórmula; sin ella hereda y adelgaza.

   OJO CON EL ALCANCE: la regla va acotada AL ELEMENTO del icono (clase en el svg),
   nunca como selector de descendencia — un icono chico anidado en un contenedor
   grande debe rendir SU propio trazo, no heredar el del ancestro (defecto que el
   DS corrigió en sus maquetas y el documento de dominio en v1.6.0).

   CAPA (v2.22.1): la regla base de .icono vive en @layer base, no en componentes.
   Los componentes ajustan el icono con una sola clase (.chip-estatus__icono, el
   display: none de .fortaleza__icono-cumple…). En la misma capa empatarían con .icono
   y ganaría el archivo que se carga después; las apps concatenan
   css/components/*.css en orden alfabético, así que perdían los que van antes de
   icono.css. Desde la capa base, cualquier regla de componente gana sin importar el
   orden. Los modificadores de tamaño se quedan en componentes. */

@layer base {
  .icono {
    inline-size: 16px;
    block-size: 16px;
    flex: none;
    display: inline-block;
    vertical-align: middle;
    color: inherit;
    stroke-width: 2.625px; /* 1.75 × 24 / 16 */
  }
}

@layer componentes {
  .icono--20 {
    inline-size: 20px;
    block-size: 20px;
    stroke-width: 2.1px; /* 1.75 × 24 / 20 */
  }

  .icono--24 {
    inline-size: 24px;
    block-size: 24px;
    stroke-width: 1.75px; /* 1.75 × 24 / 24 */
  }
}

/*!inventario
{
  "componente": "Aviso en línea",
  "proposito": "Comunica algo EN EL LUGAR donde ocurre, sin bloquear ni irse solo. Texto E icono, nunca solo color. Ningún mensaje al usuario se rinde como texto suelto (§13).",
  "raiz": "aviso",
  "variantes": {
    "aviso--informativo": "Explicación (p. ej. rechazo por permiso) — icono i-info",
    "aviso--exito": "Confirmación — icono i-circle-check-big",
    "aviso--advertencia": "Algo no se completó — icono i-triangle-alert",
    "aviso--peligro": "Falla o riesgo — icono i-circle-alert"
  },
  "estados": {},
  "modificadores": {},
  "estructura": [
    { "clase": "aviso__icono", "elemento": "svg.icono", "papel": "El icono de la variante", "opcional": false },
    { "clase": "aviso__cuerpo", "elemento": "div", "papel": "Columna de textos", "opcional": false },
    { "clase": "aviso__titulo", "elemento": "p", "papel": "Título corto, color de la variante", "opcional": true },
    { "clase": "aviso__texto", "elemento": "p", "papel": "Una frase que explica y dice qué hacer", "opcional": false }
  ],
  "internas": [],
  "ejemplo": [
    "<div class='aviso aviso--advertencia' role='alert'>",
    "  <svg class='icono aviso__icono' aria-hidden='true'><use href='/images/iconos.svg#i-triangle-alert' /></svg>",
    "  <div class='aviso__cuerpo'>",
    "    <p class='aviso__texto'>No se pudo consultar el listado.</p>",
    "  </div>",
    "</div>"
  ]
}
*/
/* Aviso en línea — anatomía "Alertas en línea" del DS v2.13.0 [CORE-13].
   Comunica algo EN EL LUGAR donde ocurre, sin bloquear la tarea (para eso está la
   alerta centrada, fuera de la Ola 1) y sin irse solo (para eso el flotante).
   El estado se comunica con texto E icono, nunca solo con color.

   MARCADO (título opcional; el icono es el de la variante):

     <div class="aviso aviso--advertencia" role="alert">
       <svg class="icono aviso__icono" aria-hidden="true">
         <use href="/images/iconos.svg#i-triangle-alert" /></svg>
       <div class="aviso__cuerpo">
         <p class="aviso__titulo">Título corto</p>
         <p class="aviso__texto">Una sola frase que explique y diga qué hacer.</p>
       </div>
     </div>

   Variantes → icono del sprite:
     informativo → i-info · exito → i-circle-check-big
     advertencia → i-triangle-alert · peligro → i-circle-alert

   ACCIÓN NO AUTORIZADA (el patrón, convenciones-modulos §13): el control se rinde
   INACTIVO con su razón en `.campo__ayuda` o `title` — nunca desaparece. El aviso
   entra solo cuando el servidor rechaza (403) algo que la pantalla creyó posible:
   variante informativa, en el lugar de la acción, diciendo qué no se puede hacer
   y a quién pedirlo — nunca el nombre técnico del permiso. */

@layer componentes {
  .aviso {
    display: flex;
    gap: 12px;
    padding: 14px 16px;
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    background: var(--sunken);
  }

  .aviso__icono {
    inline-size: 17px;
    block-size: 17px;
    stroke-width: 2.471px; /* 1.75 × 24 / 17 */
    margin-block-start: 2px;
  }

  .aviso__cuerpo {
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: 2px;
  }

  .aviso__titulo {
    font-size: 13px;
    font-weight: 700;
  }

  .aviso__texto {
    font-size: 13px;
    line-height: 1.55;
    color: var(--fg-2);
  }

  /* El color de la variante pinta icono y título; el cuerpo queda legible (fg-2). */
  .aviso--informativo {
    background: var(--info-soft);
    border-color: var(--info-bd);

    .aviso__icono,
    .aviso__titulo {
      color: var(--info);
    }
  }

  .aviso--exito {
    background: var(--success-soft);
    border-color: var(--success-bd);

    .aviso__icono,
    .aviso__titulo {
      color: var(--success);
    }
  }

  .aviso--advertencia {
    background: var(--warning-soft);
    border-color: var(--warning-bd);

    .aviso__icono,
    .aviso__titulo {
      color: var(--warning-fg);
    }
  }

  .aviso--peligro {
    background: var(--danger-soft);
    border-color: var(--danger-bd);

    .aviso__icono,
    .aviso__titulo {
      color: var(--danger);
    }
  }
}

/*!inventario
{
  "componente": "Alternancia de vistas",
  "proposito": "Conmutador entre representaciones de LA MISMA consulta (tablero/tabla) que conserva filtros, orden y posición. El estado vive en la URL; sin JS propio.",
  "raiz": "alternancia-vistas",
  "variantes": {},
  "estados": {
    "[aria-current='true']": "La opción activa (en la opción)"
  },
  "modificadores": {
    "alternancia-solo-tablero": "Marca controles que solo existen en una representación; el contenedor con data-vista los muestra u oculta"
  },
  "estructura": [
    { "clase": "alternancia-vistas__opcion", "elemento": "a | button", "papel": "Una opción: icono + nombre de la representación; disparador htmx con la consulta viva", "opcional": false },
    { "clase": "alternancia-vistas__icono", "elemento": "svg.icono", "papel": "El icono de la representación", "opcional": false }
  ],
  "internas": [],
  "ejemplo": [
    "<div data-vista='tablero'>",
    "  <div class='alternancia-vistas' role='group' aria-label='Vista'>",
    "    <a class='alternancia-vistas__opcion' aria-current='true' hx-get='…?vista=tablero'>",
    "      <svg class='icono alternancia-vistas__icono' aria-hidden='true'><use href='/images/iconos.svg#i-columns-3' /></svg>",
    "      Tablero por horizonte</a>",
    "    <a class='alternancia-vistas__opcion' hx-get='…?vista=tabla'>",
    "      <svg class='icono alternancia-vistas__icono' aria-hidden='true'><use href='/images/iconos.svg#i-table' /></svg>",
    "      Tabla densa</a>",
    "  </div>",
    "</div>"
  ]
}
*/
/* Alternancia de vistas — contra «Alternancia de vistas: un contrato, varias
   representaciones» del sistema v2.13.0. [CORE-10]

   EL CONTRATO (lo que este componente aporta a cualquier par):
   - Conmutador entre representaciones que CONSERVA filtros, orden y posición:
     es la misma consulta con otra forma (pitfall del DS: perderlos equivale a
     abrir otra pantalla).
   - El estado vive en la URL y lo resuelve el servidor (AD-10): cada opción es
     un disparador htmx/enlace; el componente no guarda estado de cliente ni
     lleva JS.
   - Cada representación declara lo suyo (eje, unidad, arrastre, densidad); los
     controles que solo una declara se marcan .alternancia-solo-tablero y el
     contenedor de la pantalla los muestra u oculta con data-vista.

   HOY HAY DOS representaciones: tablero (tablero.css) y tabla (tabla.css).
   El contrato admite N y el DS registra además una cuadrícula — NO construida:
   se agrega cuando alguna pantalla la pida (AD-11).

   Uso:
     <div data-vista="tablero">                       ← lo rinde el servidor
       <div class="alternancia-vistas" role="group" aria-label="Vista">
         <a class="alternancia-vistas__opcion" aria-current="true" hx-get="…?vista=tablero">
           (icono i-columns-3) Tablero por horizonte</a>
         <a class="alternancia-vistas__opcion" hx-get="…?vista=tabla">
           (icono i-table) Tabla densa</a>
       </div>
       <button class="… alternancia-solo-tablero">(i-grip-vertical) Activar arrastre</button>
       …representación…
     </div> */

@layer componentes {
  .alternancia-vistas {
    display: flex;
    gap: 3px;
    padding: 3px;
    border-radius: var(--r-sm);
    background: var(--sunken);
    border: 1px solid var(--line);
    flex: none;
  }

  .alternancia-vistas__opcion {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 6px 12px;
    border: none;
    border-radius: var(--r-xs);
    background: transparent;
    color: var(--fg-3);
    font-family: var(--f-ui);
    font-size: 12.5px;
    font-weight: 500;
    cursor: pointer;
    white-space: nowrap;
    text-decoration: none;

    &[aria-current="true"] {
      background: var(--surface);
      color: var(--acc);
      font-weight: 700;
      box-shadow: var(--e1);
    }
  }

  .alternancia-vistas__icono {
    inline-size: 14px;
    block-size: 14px;
    flex: none;
    /* Tamaño propio ⇒ trazo propio (RD-019) */
    stroke-width: calc(1.75px * 24 / 14);
  }

  /* El control de arrastre existe solo con el tablero: lo declara la
     representación, no la alternancia. */
  [data-vista="tabla"] .alternancia-solo-tablero {
    display: none;
  }

  @media (pointer: coarse) {
    .alternancia-vistas__opcion {
      min-block-size: 44px;
    }
  }
}

/*!inventario
{
  "componente": "Datos de lectura",
  "proposito": "Sección de datos para LEER (perfil, detalle de registro, panel lateral de detalle, resumen), según RD-022 «En lectura, el valor manda»: el valor a 14.5px semibold en --fg y la etiqueta a 12px en --fg-3, sin mayúsculas ni mono; el vacío se escribe «—» en --fg-4; la mono queda solo para códigos y fechas. Los títulos de sección van a 15px en negrita, con más aire arriba que entre campos. Cada sección tiene su propio «Editar»; solo una se edita a la vez, y en edición se cambia por los campos del formulario (campo). No aplica a formularios ni a encabezados de tabla. datos.js copia el valor con datos__copiar.",
  "raiz": "datos",
  "variantes": {
    "datos--compacto": "Para columnas angostas (la columna estrecha de perfil, un cajón): un campo por renglón, etiqueta a la izquierda y valor a la derecha; el título de sección pasa a rótulo en mayúsculas espaciadas"
  },
  "estados": {
    "[data-copiado='si']": "En datos__copiar: recién copiado; el icono pasa a i-check dos segundos (lo pone datos.js)"
  },
  "modificadores": {
    "datos__valor--codigo": "Códigos, claves y fechas (RFC, CURP, CLABE, folios, 04/02/1985) en mono",
    "datos__valor--vacio": "Sin dato: se escribe «—» en --fg-4, nunca se deja en blanco ni se oculta el campo"
  },
  "estructura": [
    { "clase": "datos__encabezado", "elemento": "header", "papel": "Icono opcional, título de la sección y su acción «Editar», sobre un filo", "opcional": true },
    { "clase": "datos__icono", "elemento": "span > svg.icono", "papel": "Insignia de 28px con el icono de la sección", "opcional": true },
    { "clase": "datos__titulo", "elemento": "h2 | h3", "papel": "Título de la sección: 15px en negrita", "opcional": true },
    { "clase": "datos__editar", "elemento": "a | button", "papel": "«Editar» de la sección, con icono i-pencil; nombre accesible con la sección («Editar Identificación»)", "opcional": true },
    { "clase": "datos__lista", "elemento": "dl", "papel": "Los campos: rejilla auto-fill de 200px en la variante base; renglones en la compacta", "opcional": false },
    { "clase": "datos__campo", "elemento": "div", "papel": "Una pareja etiqueta-valor (dt + dd)", "opcional": false },
    { "clase": "datos__etiqueta", "elemento": "dt", "papel": "Nombre del dato: 12px en --fg-3, sin mayúsculas ni mono", "opcional": false },
    { "clase": "datos__valor", "elemento": "dd", "papel": "El dato: 14.5px semibold en --fg", "opcional": false },
    { "clase": "datos__nota", "elemento": "span", "papel": "Precisión junto al valor: días restantes de un vencimiento, «Sin verificar»", "opcional": true },
    { "clase": "datos__copiar", "elemento": "button", "papel": "Copia el valor (o data-copiar) al portapapeles; aria-label con el dato («Copiar RFC»)", "opcional": true }
  ],
  "internas": [],
  "contexto": ["icono"],
  "ejemplo": [
    "<section class='datos' aria-labelledby='datos-identificacion'>",
    "  <header class='datos__encabezado'>",
    "    <span class='datos__icono'><svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-id-card' /></svg></span>",
    "    <h3 class='datos__titulo' id='datos-identificacion'>Identificación</h3>",
    "    <button type='button' class='datos__editar' aria-label='Editar Identificación'><svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-pencil' /></svg> Editar</button>",
    "  </header>",
    "  <dl class='datos__lista'>",
    "    <div class='datos__campo'><dt class='datos__etiqueta'>Nombre</dt><dd class='datos__valor'>Sofía</dd></div>",
    "    <div class='datos__campo'><dt class='datos__etiqueta'>Fecha de nacimiento</dt><dd class='datos__valor datos__valor--codigo'>04/02/1985</dd></div>",
    "    <div class='datos__campo'><dt class='datos__etiqueta'>RFC</dt><dd class='datos__valor datos__valor--codigo'>OEPA850204D33",
    "      <button type='button' class='datos__copiar' aria-label='Copiar RFC'><svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-copy' /></svg></button></dd></div>",
    "    <div class='datos__campo'><dt class='datos__etiqueta'>NSS</dt><dd class='datos__valor datos__valor--vacio'>—</dd></div>",
    "    <div class='datos__campo'><dt class='datos__etiqueta'>Vigente hasta</dt><dd class='datos__valor datos__valor--codigo'>06/2027 <span class='datos__nota'>en 265 días</span></dd></div>",
    "  </dl>",
    "</section>"
  ]
}
*/
/* Datos de lectura — RD-022: en lectura, el valor manda. La etiqueta es una ayuda pequeña y
   tenue; el dato es lo primero que se lee. Secciones consecutivas se separan con más aire que
   los campos entre sí. */

@layer componentes {
  .datos {
    display: flex;
    flex-direction: column;
    gap: var(--sp-4);
  }

  .datos + .datos {
    margin-block-start: var(--sp-6);
  }

  .datos__encabezado {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    padding-block-end: var(--sp-3);
    border-block-end: 1px solid var(--line);
  }

  .datos__icono {
    flex: none;
    display: grid;
    place-items: center;
    inline-size: 28px;
    block-size: 28px;
    border-radius: var(--r-sm);
    background: var(--acc-soft);
    color: var(--acc);
  }

  .datos__icono .icono {
    inline-size: 16px;
    block-size: 16px;
  }

  .datos__titulo {
    flex: 1;
    min-inline-size: 0;
    font-family: var(--f-ui);
    font-size: 15px;
    font-weight: 700;
    line-height: 1.3;
    color: var(--fg);
  }

  .datos__editar {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 4px 6px;
    border: none;
    border-radius: var(--r-xs);
    background: transparent;
    color: var(--fg-2);
    font-family: var(--f-ui);
    font-size: 12.5px;
    font-weight: 600;
    text-decoration: none;
    cursor: pointer;
    transition: background var(--fast) var(--ease), color var(--fast) var(--ease);

    &:hover {
      background: var(--sunken);
      color: var(--fg);
    }
  }

  .datos__editar .icono {
    inline-size: 14px;
    block-size: 14px;
  }

  .datos__lista {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: var(--sp-4) var(--sp-5);
  }

  .datos__campo {
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-inline-size: 0;
  }

  .datos__etiqueta {
    font-size: 12px;
    line-height: 1.4;
    color: var(--fg-3);
  }

  .datos__valor {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 6px 8px;
    margin: 0;
    font-size: 14.5px;
    font-weight: 600;
    line-height: 1.4;
    color: var(--fg);
    overflow-wrap: anywhere;
  }

  .datos__valor--codigo {
    font-family: var(--f-mono);
    font-size: 14px;
    font-weight: 500;
  }

  .datos__valor--vacio {
    font-weight: 400;
    color: var(--fg-4);
  }

  .datos__nota {
    font-family: var(--f-ui);
    font-size: 12px;
    font-weight: 400;
    color: var(--fg-3);
  }

  .datos__copiar {
    display: inline-grid;
    place-items: center;
    inline-size: 26px;
    block-size: 26px;
    padding: 0;
    border: none;
    border-radius: var(--r-xs);
    background: transparent;
    color: var(--fg-3);
    cursor: pointer;
    transition: background var(--fast) var(--ease), color var(--fast) var(--ease);

    &:hover {
      background: var(--sunken);
      color: var(--fg);
    }
  }

  .datos__copiar[data-copiado="si"] {
    color: var(--success);
  }

  .datos__copiar .icono {
    inline-size: 14px;
    block-size: 14px;
  }

  /* ── Compacta: columnas angostas, un campo por renglón ─────────────────── */
  .datos--compacto {
    gap: var(--sp-2);
  }

  .datos--compacto .datos__encabezado {
    padding-block-end: 0;
    border-block-end: none;
  }

  .datos--compacto .datos__titulo {
    font-family: var(--f-mono);
    font-size: 9.5px;
    font-weight: 400;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--fg-3);
  }

  .datos--compacto .datos__lista {
    grid-template-columns: 1fr;
    gap: var(--sp-2);
  }

  .datos--compacto .datos__campo {
    flex-direction: row;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--sp-3);
  }

  .datos--compacto .datos__etiqueta {
    font-size: 12.5px;
  }

  .datos--compacto .datos__valor {
    justify-content: flex-end;
    font-size: 13px;
    text-align: end;
  }

  .datos--compacto .datos__valor--codigo {
    font-size: 12px;
    font-weight: 400;
  }

  @media (pointer: coarse) {
    .datos__editar,
    .datos__copiar {
      min-block-size: 44px;
      min-inline-size: 44px;
    }
  }
}

/*!inventario
{
  "componente": "Navegación lateral",
  "proposito": "La navegación de la región barra-lateral del shell B: grupos con rótulo, elementos con icono, filo activo de 2px. Se filtra por PERMISO — ocultar un enlace no autoriza.",
  "raiz": "nav-lateral",
  "variantes": {},
  "estados": {
    "[aria-current='page']": "El destino activo (el más específico que empata con la ruta — lo resuelve el servidor)"
  },
  "modificadores": {},
  "estructura": [
    { "clase": "nav-lateral__grupo", "elemento": "div", "papel": "Un grupo de destinos", "opcional": false },
    { "clase": "nav-lateral__rotulo", "elemento": "span", "papel": "Rótulo del grupo (lleva shell__solo-abierta: desaparece en riel)", "opcional": false },
    { "clase": "nav-lateral__elemento", "elemento": "a", "papel": "Un destino: icono 20 + etiqueta", "opcional": false },
    { "clase": "nav-lateral__etiqueta", "elemento": "span", "papel": "El nombre del destino (shell__solo-abierta)", "opcional": false },
    { "clase": "nav-lateral__contador", "elemento": "span", "papel": "Contador opcional a la derecha", "opcional": true }
  ],
  "internas": [],
  "contexto": ["shell"],
  "ejemplo": [
    "<nav class='nav-lateral' aria-label='Navegación principal'>",
    "  <div class='nav-lateral__grupo'>",
    "    <span class='nav-lateral__rotulo shell__solo-abierta'>Módulos</span>",
    "    <a class='nav-lateral__elemento' href='/Proyectos' aria-current='page' title='Proyectos'>",
    "      <svg class='icono icono--20' aria-hidden='true'><use href='/images/iconos.svg#i-folder-kanban' /></svg>",
    "      <span class='nav-lateral__etiqueta shell__solo-abierta'>Proyectos</span>",
    "    </a>",
    "  </div>",
    "</nav>"
  ]
}
*/
/* Navegación lateral del shell B — grupos con rótulo, elementos con icono,
   filo activo de 2 px a la izquierda y contador opcional. */

@layer componentes {
  .nav-lateral {
    flex: 1;
    padding: 16px 10px;
    display: flex;
    flex-direction: column;
    gap: 2px;
    overflow-y: auto;
    overflow-x: hidden;
  }

  .nav-lateral__grupo {
    display: flex;
    flex-direction: column;
    gap: 2px;
    margin-block-end: 14px;
  }

  .nav-lateral__rotulo {
    font-family: var(--f-mono);
    font-size: 9.5px;
    letter-spacing: .12em;
    text-transform: uppercase;
    color: var(--fg-3);
    padding: 6px 12px;
    white-space: nowrap;
    overflow: hidden;
    transition: opacity var(--fast) var(--ease);
  }

  .nav-lateral__elemento {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 9px 12px;
    border: none;
    border-inline-start: 2px solid transparent;
    border-radius: 0 var(--r-sm) var(--r-sm) 0;
    background: transparent;
    color: var(--fg-2);
    font-family: var(--f-ui);
    font-size: 13.5px;
    font-weight: 500;
    cursor: pointer;
    text-align: start;
    inline-size: 100%;
    transition: background var(--fast) var(--ease);

    &:hover {
      background: var(--sunken);
    }
  }

  .nav-lateral__elemento[aria-current="page"] {
    border-inline-start-color: var(--acc);
    background: var(--acc-soft);
    color: var(--acc);
    font-weight: 700;
  }

  .nav-lateral__etiqueta {
    white-space: nowrap;
    overflow: hidden;
    flex: 1;
    min-inline-size: 0;
    transition: opacity var(--fast) var(--ease);
  }

  .nav-lateral__contador {
    font-family: var(--f-mono);
    font-size: 10px;
    padding: 1px 7px;
    border-radius: 999px;
    background: var(--acc-soft);
    color: var(--acc);
    flex: none;
    transition: opacity var(--fast) var(--ease);
  }

  @media (pointer: coarse) {
    .nav-lateral__elemento {
      min-block-size: 44px;
    }
  }
}

/*!inventario
{
  "componente": "Encabezado de página",
  "proposito": "Arranque de toda pantalla del shell: título en display, bajada de una frase y, a la derecha, la acción primaria de la página (una sola). Envuelve cuando no cabe.",
  "raiz": "encabezado-pagina",
  "variantes": {},
  "estados": {},
  "modificadores": {},
  "estructura": [
    { "clase": "encabezado-pagina__titulos", "elemento": "div", "papel": "Columna de título, bajada y, si la página lo pide, una línea de estado debajo (p. ej. vigencia)", "opcional": false },
    { "clase": "encabezado-pagina__titulo", "elemento": "h1", "papel": "El nombre de la pantalla, en display", "opcional": false },
    { "clase": "encabezado-pagina__sub", "elemento": "p", "papel": "Bajada de una frase: qué hay aquí", "opcional": true },
    { "clase": "encabezado-pagina__acciones", "elemento": "div", "papel": "Acción primaria (y a lo sumo una secundaria) alineadas a la derecha", "opcional": true }
  ],
  "internas": [],
  "contexto": ["boton"],
  "ejemplo": [
    "<div class='encabezado-pagina'>",
    "  <div class='encabezado-pagina__titulos'>",
    "    <h1 class='encabezado-pagina__titulo'>Terminales</h1>",
    "    <p class='encabezado-pagina__sub'>Equipos de escaneo autorizados: alta, nombre y estado.</p>",
    "  </div>",
    "  <div class='encabezado-pagina__acciones'>",
    "    <button type='button' class='boton boton--primario'>(i-plus) Nueva terminal</button>",
    "  </div>",
    "</div>"
  ]
}
*/
/* Encabezado de página — el mismo arranque en todas las pantallas del shell, para
   que el ojo encuentre título y acción primaria siempre en el mismo lugar. */

@layer componentes {
  .encabezado-pagina {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--sp-4);
    flex-wrap: wrap;
  }

  .encabezado-pagina__titulos {
    min-inline-size: 0;
  }

  .encabezado-pagina__titulo {
    font-family: var(--f-display);
    font-size: clamp(20px, 2.4vw, 24px);
    line-height: 1.15;
  }

  .encabezado-pagina__sub {
    margin-top: 4px;
    color: var(--fg-3);
    font-size: 13px;
  }

  .encabezado-pagina__acciones {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px;
  }
}

/*!inventario
{
  "componente": "Alerta centrada",
  "proposito": "Diálogo que bloquea la tarea (dialog nativo): confirma una acción antes de ejecutarla o avisa el resultado de una. Icono circular grande con el tono, título corto y UNA frase; nunca solo color. alerta.js la abre desde hx-confirm (confirmación), desde alertaConfirmar/alertaAvisar (programático) y desde el evento htmx `alerta` (HX-Trigger del servidor).",
  "raiz": "alerta",
  "variantes": {},
  "estados": {
    "[data-tono='peligro']": "Acción destructiva o falla: icono danger; la confirmatoria se pinta danger sólido",
    "[data-tono='advertencia']": "Algo que revisar antes de seguir: icono warning",
    "[data-tono='exito']": "Resultado exitoso de una acción (modo aviso): icono success",
    "[data-tono='informativo']": "Información (modo aviso): icono info",
    "[open]": "Abierta (dialog nativo con showModal; el velo es ::backdrop)",
    "[data-secretos='si']": "Muestra uno o más secretos (contraseña temporal, client_secret) con botón de copiar; solo cierra con el botón, no con clic fuera ni Escape"
  },
  "modificadores": {},
  "estructura": [
    { "clase": "alerta__cuerpo", "elemento": "div", "papel": "Columna centrada con todo el contenido", "opcional": false },
    { "clase": "alerta__icono", "elemento": "span > svg.icono", "papel": "Círculo de 56px con el icono del tono (i-circle-alert por omisión)", "opcional": false },
    { "clase": "alerta__titulo", "elemento": "h2", "papel": "Título corto en display", "opcional": false },
    { "clase": "alerta__texto", "elemento": "p", "papel": "UNA frase: qué pasa y qué implica", "opcional": false },
    { "clase": "alerta__campo", "elemento": "div.campo", "papel": "Campo opcional (p. ej. motivo de un rechazo); alerta.js lo muestra si la confirmación pide texto", "opcional": true },
    { "clase": "alerta__secretos", "elemento": "dl", "papel": "Lista de secretos que se muestran UNA vez (alerta.js la pinta desde opciones.secretos)", "opcional": true },
    { "clase": "alerta__secreto", "elemento": "div", "papel": "Un secreto: etiqueta, valor y botón de copiar", "opcional": true },
    { "clase": "alerta__secreto-etiqueta", "elemento": "dt", "papel": "Qué es (client_secret, Contraseña temporal)", "opcional": true },
    { "clase": "alerta__secreto-valor", "elemento": "dd > code", "papel": "El valor en mono, seleccionable", "opcional": true },
    { "clase": "alerta__copiar", "elemento": "button", "papel": "Copia el valor al portapapeles (i-copy); data-copiado='si' muestra i-check y «Copiado» dos segundos", "opcional": true },
    { "clase": "alerta__acciones", "elemento": "div", "papel": "Fila centrada de acciones", "opcional": false },
    { "clase": "alerta__cancelar", "elemento": "button.boton", "papel": "La salida (Cancelar); oculta en modo aviso", "opcional": false },
    { "clase": "alerta__confirmar", "elemento": "button.boton.boton--primario", "papel": "La acción confirmatoria («Confirmar», o «Entendido» en modo aviso)", "opcional": false }
  ],
  "internas": [],
  "contexto": ["campo", "boton"],
  "ejemplo": [
    "<dialog id='alerta-confirmacion' class='alerta'>",
    "  <div class='alerta__cuerpo'>",
    "    <span class='alerta__icono'><svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-circle-alert' /></svg></span>",
    "    <h2 class='alerta__titulo'></h2>",
    "    <p class='alerta__texto'></p>",
    "    <dl class='alerta__secretos' hidden></dl>",
    "    <div class='alerta__campo campo' hidden>",
    "      <label class='campo__etiqueta' for='alerta-entrada'></label>",
    "      <textarea class='campo__entrada' id='alerta-entrada' rows='3' maxlength='500'></textarea>",
    "    </div>",
    "    <div class='alerta__acciones'>",
    "      <button type='button' class='boton alerta__cancelar'>Cancelar</button>",
    "      <button type='button' class='boton boton--primario alerta__confirmar'>Confirmar</button>",
    "    </div>",
    "  </div>",
    "</dialog>",
    "<button class='boton' hx-post='…' hx-confirm='La terminal dejará de aceptar escaneos.' data-alerta-titulo='¿Desactivar terminal?' data-alerta-confirmar='Sí, desactivar' data-alerta-tono='peligro'>Desactivar</button>"
  ]
}
*/
/* Alerta centrada — contra «Modales, paneles y avisos» (sección 24) del sistema:
   la alerta interrumpe y exige decisión (confirmación) o acuse (aviso). Vive UNA
   vez en el layout (dialog#alerta-confirmacion); alerta.js la reutiliza.
   - Confirmación: una acción confirmatoria y una salida siempre visible; con
     hx-confirm sustituye al confirm nativo (evento htmx:confirm).
   - Aviso (resultado de una acción; tonos exito / informativo / advertencia /
     peligro): un solo botón «Entendido»; llega por htmx (HX-Trigger «alerta») o
     por window.alertaAvisar.
   - Cierre con clic fuera, botón y Escape (dialog nativo + alerta.js). */
@layer componentes {
  .alerta {
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    background: var(--surface);
    color: var(--fg);
    box-shadow: var(--e4);
    padding: var(--sp-6);
    width: min(420px, calc(100vw - 32px));
    text-align: center;
  }

  .alerta::backdrop {
    background: var(--scrim);
  }

  .alerta__cuerpo {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--sp-3);
  }

  /* Icono circular grande, con el tono de la alerta */
  .alerta__icono {
    width: 56px;
    height: 56px;
    border-radius: 999px;
    display: grid;
    place-items: center;
    background: var(--acc-soft);
    color: var(--acc);
  }

  .alerta__icono .icono {
    width: 28px;
    height: 28px;
  }

  .alerta[data-tono="peligro"] .alerta__icono {
    background: var(--danger-soft);
    color: var(--danger);
  }

  .alerta[data-tono="advertencia"] .alerta__icono {
    background: var(--warning-soft);
    color: var(--warning);
  }

  .alerta[data-tono="exito"] .alerta__icono {
    background: var(--success-soft);
    color: var(--success);
  }

  .alerta[data-tono="informativo"] .alerta__icono {
    background: var(--info-soft);
    color: var(--info);
  }

  .alerta__titulo {
    font-family: var(--f-display);
    font-weight: 400;
    font-size: 20px;
    line-height: 1.3;
  }

  .alerta__texto {
    font-size: 13.5px;
    line-height: 1.6;
    color: var(--fg-2);
    max-width: 40ch;
  }

  /* Campo opcional (p. ej. motivo de un rechazo): reusa el componente campo,
     alineado a la izquierda dentro de la alerta centrada */
  .alerta__campo {
    width: 100%;
    text-align: left;
  }

  .alerta__campo[hidden] {
    display: none;
  }

  /* Secretos que se muestran una sola vez (contraseña temporal, client_secret):
     lista etiqueta → valor con botón de copiar. La alerta con secretos no cierra
     con clic fuera ni Escape (alerta.js): perderla sería perder el valor. */
  .alerta__secretos {
    width: 100%;
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: var(--sp-2);
    text-align: left;
  }

  .alerta__secretos[hidden] {
    display: none;
  }

  .alerta__secreto {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "etiqueta etiqueta" "valor copiar";
    align-items: center;
    column-gap: var(--sp-2);
    row-gap: 4px;
    padding: 10px 10px 10px 12px;
    background: var(--sunken);
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
  }

  .alerta__secreto-etiqueta {
    grid-area: etiqueta;
    font-size: 11.5px;
    font-weight: 700;
    color: var(--fg-3);
  }

  .alerta__secreto-valor {
    grid-area: valor;
    margin: 0;
    min-width: 0;
  }

  .alerta__secreto-valor code {
    display: block;
    font-family: var(--f-mono);
    font-size: 13px;
    line-height: 1.5;
    color: var(--fg);
    overflow-wrap: anywhere;
    user-select: all;
  }

  .alerta__copiar {
    grid-area: copiar;
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 6px 9px;
    border: 1px solid var(--line-2);
    border-radius: var(--r-xs);
    background: var(--surface);
    color: var(--fg-2);
    font-family: var(--f-ui);
    font-size: 12px;
    font-weight: 700;
    cursor: pointer;
    transition: background var(--fast) var(--ease), color var(--fast) var(--ease), border-color var(--fast) var(--ease);

    &:hover,
    &:focus-visible {
      background: var(--acc-soft);
      border-color: var(--acc);
      color: var(--acc);
    }
  }

  .alerta__copiar[data-copiado="si"] {
    background: var(--success-soft);
    border-color: var(--success-bd);
    color: var(--success);
  }

  .alerta__copiar .icono {
    width: 14px;
    height: 14px;
  }

  .alerta__acciones {
    display: flex;
    justify-content: center;
    gap: var(--sp-2);
    margin-top: var(--sp-2);
    flex-wrap: wrap;
  }

  /* Aviso (un solo botón): la salida se oculta */
  .alerta__cancelar[hidden] {
    display: none;
  }

  /* Confirmatoria destructiva: tono danger sólido (variante local de la alerta) */
  .alerta[data-tono="peligro"] .alerta__confirmar {
    background: var(--danger);
    border-color: transparent;
    color: var(--acc-fg);
  }

  .alerta[data-tono="peligro"] .alerta__confirmar:hover:not(:disabled) {
    background: color-mix(in srgb, var(--danger) 85%, var(--n-950));
  }
}

/*!inventario
{
  "componente": "Shell",
  "proposito": "El armazón de TODA pantalla del panel: encabezado sobre la superficie de marca (verde), barra lateral colapsable, ruta, contenido a todo el ancho de la región y pie. Las pantallas se arman DENTRO de shell__contenido-limite — nunca dibujan su propio encabezado ni barra. A la derecha del encabezado van, en este orden: las acciones que la app tenga (notificaciones, ayuda; ninguna es obligatoria) · cambio de tema · separador · menú de usuario. El shell es PERSISTENTE: con hx-boost='true' en el body, navegar por el menú no recarga la página; solo se intercambia el contenido (ver contrato.persistente).",
  "raiz": "shell",
  "variantes": {},
  "estados": {
    "[data-barra='riel']": "Barra colapsada a riel de 64px (el usuario la colapsó; shell.js)",
    "[data-panel='abierto']": "El panel lateral está abierto sobre el velo (solo en modo panel)"
  },
  "modificadores": {
    "shell__solo-abierta": "Marca lo que desaparece en riel (etiquetas, rótulos, textos del pie de barra)"
  },
  "estructura": [
    { "clase": "shell__encabezado-marca", "elemento": "header", "papel": "Región encabezado: ancho completo, sobre la marca (--marca); dentro viven la marca y las acciones", "opcional": false },
    { "clase": "shell__marca", "elemento": "div", "papel": "Logotipo + separador + nombre y descripción (separador y textos opcionales: el logotipo ya lleva nombre)", "opcional": false },
    { "clase": "shell__marca-logo", "elemento": "img", "papel": "El logotipo blanco (46px de alto; 34 en panel)", "opcional": false },
    { "clase": "shell__marca-separador", "elemento": "span", "papel": "Filo vertical entre logo y textos", "opcional": true },
    { "clase": "shell__marca-textos", "elemento": "div", "papel": "Nombre + descripción", "opcional": true },
    { "clase": "shell__marca-nombre", "elemento": "span", "papel": "El nombre de la sección del sistema (p. ej. Panel) en display, mayúsculas espaciadas", "opcional": true },
    { "clase": "shell__marca-descripcion", "elemento": "span", "papel": "La línea descriptiva en mono", "opcional": true },
    { "clase": "shell__encabezado-acciones", "elemento": "div", "papel": "Extremo derecho del encabezado: las acciones de la app, el cambio de tema, el separador y el menú de usuario, en ese orden", "opcional": false },
    { "clase": "shell__accion", "elemento": "button", "papel": "Acción de icono de 34px sobre la marca. Solo el tema (i-moon/i-sun) es obligatorio: button data-accion='alternar-tema' con <use> dentro — tema.js cambia el icono según lo que haría el clic; es el mismo disparador que la opción «Tema» del menú de usuario, ambos conviven. Notificaciones (i-bell, con shell__accion-punto oculto hasta que haya pendientes) y ayuda (i-circle-question-mark) se agregan cuando la app las tenga; no se ponen deshabilitadas de adorno", "opcional": false },
    { "clase": "shell__accion-punto", "elemento": "span", "papel": "Punto de pendientes de la campana de notificaciones; hidden hasta que haya algo", "opcional": true },
    { "clase": "shell__separador-acciones", "elemento": "span", "papel": "Filo vertical entre las acciones y el menú de usuario", "opcional": false },
    { "clase": "shell__control-menu", "elemento": "button", "papel": "Hamburguesa — SOLO existe en modo panel. Va sola, sin boton ni boton--icono: trae su propio reset de botón (sin borde ni fondo, 44px de toque)", "opcional": false },
    { "clase": "shell__cuerpo", "elemento": "div", "papel": "Flex: barra-lateral + principal", "opcional": false },
    { "clase": "shell__barra-lateral", "elemento": "aside", "papel": "Región barra: 232px abierta / 64 riel / panel flotante; contiene nav-lateral y el pie de barra", "opcional": false },
    { "clase": "shell__colapso", "elemento": "button", "papel": "Control de colapso del contrato v2.10.0: 28px visibles en el borde, toque 44 vía ::after", "opcional": false },
    { "clase": "shell__colapso-icono", "elemento": "svg.icono", "papel": "Chevron que gira 180° en riel", "opcional": false },
    { "clase": "shell__velo", "elemento": "button", "papel": "Velo del modo panel", "opcional": false },
    { "clase": "shell__pie-barra", "elemento": "div", "papel": "Identidad de compilación: versión y fecha de la APLICACIÓN, nunca la del DS", "opcional": false },
    { "clase": "shell__pie-barra-icono", "elemento": "span > svg.icono", "papel": "Icono i-git-commit-vertical", "opcional": false },
    { "clase": "shell__pie-barra-textos", "elemento": "span", "papel": "Versión + fecha (shell__solo-abierta)", "opcional": false },
    { "clase": "shell__pie-barra-version", "elemento": "span", "papel": "vN.N.N en mono", "opcional": false },
    { "clase": "shell__pie-barra-fecha", "elemento": "span", "papel": "«Actualizado el …»", "opcional": false },
    { "clase": "shell__principal", "elemento": "div", "papel": "Columna principal: ruta + contenido + pie", "opcional": false },
    { "clase": "shell__ruta", "elemento": "div", "papel": "Región ruta (obligatoria: el encabezado no lleva título de página). Lleva id='shell-ruta': en una navegación viaja fuera de banda (hx-swap-oob) con el contenido", "opcional": false },
    { "clase": "shell__ruta-separador", "elemento": "span", "papel": "El separador entre tramos", "opcional": false },
    { "clase": "shell__ruta-actual", "elemento": "span", "papel": "El tramo actual, en negrita", "opcional": false },
    { "clase": "shell__contenido", "elemento": "main", "papel": "Región contenido: ÚNICO contenedor con scroll; container-query 'contenido'. Lleva id='shell-contenido' y hx-history-elt: es lo que se intercambia al navegar y lo que guarda el historial", "opcional": false },
    { "clase": "shell__contenido-limite", "elemento": "div", "papel": "La columna de la pantalla — AQUÍ va la pantalla. Ocupa todo el ancho de la región (--sh-content-max: none); el texto corrido lleva su propia medida (≤ 62ch) con su clase pg-", "opcional": false },
    { "clase": "shell__pie", "elemento": "footer", "papel": "Región pie: fuera del scroll del contenido", "opcional": false },
    { "clase": "shell__pie-marca", "elemento": "span", "papel": "La marca del pie en mono", "opcional": false }
  ],
  "internas": [],
  "regiones": [
    { "region": "encabezado-marca", "clase": "shell__encabezado-marca", "papel": "Banda de 72px sobre la marca (56 en panel): marca a la izquierda, acciones a la derecha. NO lleva el título de la página." },
    { "region": "barra-lateral", "clase": "shell__barra-lateral", "papel": "Navegación por permiso + pie de compilación. 232px abierta; 64px en riel; panel flotante de min(280px, 86vw) bajo 740px o en táctil." },
    { "region": "ruta", "clase": "shell__ruta", "papel": "Migas y título actual, 56px. Obligatoria en el shell B." },
    { "region": "contenido", "clase": "shell__contenido", "papel": "El único scroll. La pantalla vive en shell__contenido-limite (a todo el ancho de la región, columna con gap --sp-4)." },
    { "region": "pie", "clase": "shell__pie", "papel": "Pie de página fuera del scroll, 56px mínimo." }
  ],
  "contrato": {
    "anchos": "brand 72px · barra abierta 232px · riel 64px · ruta 56px · contenido min 640px / sin máximo (--sh-content-max: none) · pie 56px (custom properties --sh-*)",
    "umbrales": "abierta→riel a 916px (232+640+2×22) · riel→panel a 740px (64+640+2×18) · panel siempre con pointer:coarse — DERIVADOS del piso de contenido, no inventados",
    "persistente": "Navegar no recarga la página. Marcado: <body hx-boost='true'> (htmx se carga ANTES que shell.js); <main class='shell__contenido' id='shell-contenido' hx-history-elt>; la navegación lateral con id='nav-lateral' y la ruta con id='shell-ruta'. Servidor: ante HX-Boosted o HX-History-Restore-Request responde solo <title> + <main> (con la nav y la ruta dentro, marcadas hx-swap-oob='true') y los encabezados HX-Retarget: #shell-contenido y HX-Reswap: outerHTML transition:true; un fragmento normal se reconoce porque trae HX-Request SIN esos dos. Todo enlace que no sea una página del shell (cerrar sesión, descargas, sitios externos) lleva hx-boost='false'. shell.js pone la configuración global de htmx: anti-forgery, historial sin caché, transición solo al navegar, scroll estable en los fragmentos, módulos de página (data-modulo='/js/pages/x.js' con iniciar(raiz)/destruir(); nunca @section Scripts ni DOMContentLoaded) y alerta del DS ante fallos (5xx, sin red, tiempo agotado; 401 recarga, 403 va a /Auth/AccessDenied)."
  },
  "ejemplo": [
    "<body hx-boost='true'>",
    "<div class='shell' data-componente='shell'>",
    "  <header class='shell__encabezado-marca'>",
    "    <div class='shell__marca'>",
    "      <img class='shell__marca-logo' src='/images/logo-nutrimav-blanco.png' alt='Mariana Vargas, Nutrióloga' />",
    "      <span class='shell__marca-separador'></span>",
    "      <div class='shell__marca-textos'>",
    "        <span class='shell__marca-nombre'>Panel</span>",
    "        <span class='shell__marca-descripcion'>Agenda y pacientes</span>",
    "      </div>",
    "    </div>",
    "    <div class='shell__encabezado-acciones'>",
    "      <button type='button' class='shell__accion' data-accion='alternar-tema' aria-label='Cambiar a tema oscuro'><svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-moon' /></svg></button>",
    "      <span class='shell__separador-acciones'></span>",
    "      <div class='menu-usuario' data-componente='menu-usuario' data-abierto='no'>",
    "        <button class='menu-usuario__boton' data-accion='alternar-menu-usuario' aria-haspopup='menu' aria-expanded='false'><span class='menu-usuario__inicial'>M</span><span class='menu-usuario__nombre'>Mariana</span></button>",
    "        <div class='menu-usuario__desplegable'>",
    "          <div class='menu-usuario__identidad'>Mariana Vargas <div class='menu-usuario__detalle'>Nutrióloga titular</div></div>",
    "          <a class='menu-usuario__opcion' href='/Cuenta/Perfil'><svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-user' /></svg> Mi perfil</a>",
    "          <a class='menu-usuario__opcion' href='/Cuenta/Contrasena'><svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-key-round' /></svg> Cambiar contraseña</a>",
    "          <button class='menu-usuario__opcion' type='button' data-accion='alternar-tema'><svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-moon' /></svg> <span data-tema-etiqueta>Tema oscuro</span></button>",
    "          <a class='menu-usuario__opcion menu-usuario__opcion--peligro' href='/Auth/Logout' hx-boost='false'><svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-log-out' /></svg> Cerrar sesión</a>",
    "        </div>",
    "      </div>",
    "    </div>",
    "  </header>",
    "  <div class='shell__cuerpo'>",
    "    <aside class='shell__barra-lateral'><nav class='nav-lateral' id='nav-lateral' aria-label='Navegación principal'>…</nav> …pie de barra…</aside>",
    "    <div class='shell__principal'>",
    "      <button class='shell__colapso' aria-label='Contraer la barra'><svg class='icono shell__colapso-icono' aria-hidden='true'><use href='/images/iconos.svg#i-chevron-left' /></svg></button>",
    "      <div class='shell__ruta' id='shell-ruta'>Agenda <span class='shell__ruta-separador'>/</span> <span class='shell__ruta-actual'>Semana</span></div>",
    "      <main class='shell__contenido' id='shell-contenido' hx-history-elt><div class='shell__contenido-limite'>…LA PANTALLA VA AQUÍ…</div></main>",
    "      <footer class='shell__pie'><span class='shell__pie-marca'>Nutrimav</span></footer>",
    "    </div>",
    "  </div>",
    "</div>",
    "<script src='/lib/htmx/htmx.min.js'></script>",
    "<script src='/js/components/shell.js'></script>",
    "</body>"
  ]
}
*/
/* Shell — encabezado de marca. Implementa el contrato de maquetación del
   sistema v2.9.1: 7 regiones nombradas, anchos derivados, sin puntos de quiebre
   nombrados. Los umbrales de colapso se DERIVAN del piso de contenido:

     abierta → riel : --sh-side-open 232 + --sh-content-min 640 + relleno 2×22 ≈ 916 px
     riel → panel   : --sh-side-rail  64 + --sh-content-min 640 + relleno 2×18 ≈ 740 px
     panel siempre  : pointer: coarse

   La aritmética acompaña al valor a propósito (v2.5.1): si el contrato cambia,
   se recalcula, no se hereda el número. */

@layer componentes {
  :root {
    --sh-brand-h: 72px;
    --sh-side-open: 232px;
    --sh-side-rail: 64px;
    --sh-crumb: 56px;
    --sh-pad: clamp(16px, 2.4vw, 24px);
    --sh-content-min: 640px;
    /* Sin techo: la pantalla usa todo el ancho de la región (RD-011, v2.19.0). Una app
       que necesite techo lo fija en su CSS: .shell { --sh-content-max: 1240px; } */
    --sh-content-max: none;
    --sh-foot: 56px;
    --sh-pop: 232px;
  }

  /* Alto fijo a la ventana: el shell nunca crece con el contenido, así el
     encabezado y la barra quedan fijos y solo .shell__contenido hace scroll. */
  .shell {
    block-size: 100dvh;
    display: flex;
    flex-direction: column;
  }

  /* ── encabezado-marca: ancho completo, sobre la marca; marca vive dentro ─────────── */
  .shell__encabezado-marca {
    flex: none;
    min-block-size: var(--sh-brand-h);
    background: var(--marca);
    /* El anillo general (g-700) no contrasta con la marca: aquí el foco es blanco (8:1) */
    --ring: var(--on-marca);
    color: var(--on-marca);
    display: flex;
    align-items: center;
    gap: clamp(14px, 2vw, 26px);
    padding-inline: clamp(14px, 2vw, 22px);
  }

  .shell__marca {
    display: flex;
    align-items: center;
    gap: clamp(12px, 1.6vw, 18px);
    padding-block: 11px;
    flex: none;
  }

  .shell__marca-logo {
    block-size: 46px;
    inline-size: auto;
  }

  .shell__marca-separador {
    inline-size: 1px;
    align-self: stretch;
    background: rgb(255 255 255 / .28);
  }

  .shell__marca-textos {
    display: flex;
    flex-direction: column;
    gap: 3px;
  }

  .shell__marca-nombre {
    font-family: var(--f-display);
    font-size: clamp(15px, 1.7vw, 19px);
    line-height: 1.1;
    letter-spacing: .09em;
    text-transform: uppercase;
    color: var(--on-marca);
  }

  .shell__marca-descripcion {
    font-family: var(--f-mono);
    font-size: 9.5px;
    letter-spacing: .14em;
    text-transform: uppercase;
    color: var(--on-marca-2);
  }

  .shell__encabezado-acciones {
    display: flex;
    align-items: center;
    gap: 4px;
    margin-inline-start: auto;
    flex: none;
  }

  /* El control de menú (hamburguesa) solo existe en modo panel */
  /* Es un <button>: trae su propio reset (va solo, sin «boton boton--icono») */
  .shell__control-menu {
    display: none;
    padding: 0;
    border: none;
    border-radius: var(--r-sm);
    background: transparent;
    color: var(--on-marca-2);
    font: inherit;
    cursor: pointer;
    transition: background var(--fast) var(--ease), color var(--fast) var(--ease);

    &:hover {
      background: rgb(255 255 255 / .14);
      color: var(--on-marca);
    }
  }

  /* ── cuerpo: barra-lateral + principal ─────────────────────────────────── */
  .shell__cuerpo {
    flex: 1;
    display: flex;
    min-block-size: 0;
    position: relative;
  }

  .shell__barra-lateral {
    inline-size: var(--sh-side-open);
    flex: none;
    background: var(--surface);
    border-inline-end: 1px solid var(--line);
    display: flex;
    flex-direction: column;
    overflow: hidden;
    transition: inline-size var(--med) var(--ease);
  }

  .shell[data-barra="riel"] .shell__barra-lateral {
    inline-size: var(--sh-side-rail);
  }

  .shell[data-barra="riel"] .shell__solo-abierta {
    opacity: 0;
  }

  .shell__velo {
    display: none;
  }

  /* Control de colapso — declarado en el contrato v2.10.0: vive en el borde
     interior de barra-lateral, anclado al tope; la mitad visible cae sobre
     contenido (pertenece a la barra, no viola el anidamiento). Visual 28 px;
     el toque de 44×44 lo da la zona invisible: con border-box y borde de 1 px
     la caja de relleno es 26 px, así que el inset va en -9px (26 + 18 = 44). */
  .shell__colapso {
    position: absolute;
    inset-inline-start: -14px;
    inset-block-start: 16px;
    z-index: 15;
    inline-size: 28px;
    block-size: 28px;
    display: grid;
    place-items: center;
    border: 1px solid var(--line);
    border-radius: 999px;
    background: var(--surface);
    color: var(--fg-2);
    box-shadow: var(--e2);
    padding: 0;
    cursor: pointer;
    transition: background var(--fast) var(--ease), color var(--fast) var(--ease),
      border-color var(--fast) var(--ease);

    &::after {
      content: "";
      position: absolute;
      inset: -9px;
    }

    &:hover {
      background: var(--acc);
      border-color: var(--acc);
      color: var(--acc-fg);
    }
  }

  .shell[data-barra="riel"] .shell__colapso-icono {
    rotate: 180deg;
  }

  /* Pie de barra — solo identidad de compilación: versión y fecha de la
     APLICACIÓN, nunca la del sistema de diseño. Vive aquí o en pie de página,
     nunca en los dos. En riel queda el icono con el número en tooltip. */
  .shell__pie-barra {
    flex: none;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 14px 16px;
    border-block-start: 1px solid var(--line);
    color: var(--fg-3);
  }

  .shell__pie-barra-icono {
    inline-size: 22px;
    block-size: 22px;
    flex: none;
    border-radius: var(--r-xs);
    background: var(--sunken);
    display: grid;
    place-items: center;
  }

  .shell__pie-barra-textos {
    display: flex;
    flex-direction: column;
    white-space: nowrap;
    overflow: hidden;
    transition: opacity var(--fast) var(--ease);
  }

  .shell__pie-barra-version {
    font-family: var(--f-mono);
    font-size: 11px;
    color: var(--fg-2);
  }

  .shell__pie-barra-fecha {
    font-size: 10.5px;
    line-height: 1.3;
  }

  .shell__principal {
    flex: 1;
    min-inline-size: 0;
    display: flex;
    flex-direction: column;
    /* Ancla del control de colapso (su mitad visible cae sobre esta región) */
    position: relative;
  }

  /* ── ruta: obligatoria (el encabezado no lleva título) ─────────────────── */
  .shell__ruta {
    flex: none;
    min-block-size: var(--sh-crumb);
    display: flex;
    align-items: center;
    gap: 9px;
    padding-inline: clamp(16px, 2.4vw, 26px);
    background: var(--surface);
    border-block-end: 1px solid var(--line);
    font-size: 12.5px;
    color: var(--fg-3);
  }

  .shell__ruta-separador {
    color: var(--fg-4);
  }

  .shell__ruta-actual {
    color: var(--fg);
    font-weight: 700;
  }

  /* ── contenido: único contenedor con scroll; a todo el ancho ──────────── */
  .shell__contenido {
    flex: 1;
    overflow-y: auto;
    padding: var(--sh-pad);
    background: var(--bg);
    container-type: inline-size;
    container-name: contenido;
  }

  .shell__contenido-limite {
    max-inline-size: var(--sh-content-max);
    margin-inline: auto;
    display: flex;
    flex-direction: column;
    gap: var(--sp-4);
  }

  /* ── pie: fuera del scroll de contenido ────────────────────────────────── */
  .shell__pie {
    flex: none;
    min-block-size: var(--sh-foot);
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-3);
    flex-wrap: wrap;
    padding-block: var(--sp-3);
    padding-inline: clamp(16px, 2.4vw, 26px);
    background: var(--surface);
    border-block-start: 1px solid var(--line);
    font-size: 11.5px;
    color: var(--fg-3);
  }

  .shell__pie-marca {
    font-family: var(--f-mono);
    font-size: 10.5px;
  }

  /* ── abierta → riel: 232 + 640 + 2×22 ≈ 916 px ─────────────────────────── */
  /* Acciones del encabezado (sección «Shell de encabezado de marca» del
     sistema): iconos de 34px sobre la marca, punto de pendiente y separador antes
     del menú de usuario. */
  .shell__accion {
    /* Siempre va sobre la marca: foco blanco (8:1) */
    --ring: var(--on-marca);
    position: relative;
    inline-size: 34px;
    block-size: 34px;
    display: grid;
    place-items: center;
    border: none;
    border-radius: var(--r-sm);
    background: transparent;
    color: var(--on-marca-3);
    cursor: pointer;
    transition: background var(--fast) var(--ease), color var(--fast) var(--ease);

    &:hover:not(:disabled) {
      background: rgb(255 255 255 / 0.14);
      color: var(--on-marca);
    }

    &:disabled {
      opacity: 0.45;
      cursor: not-allowed;
    }
  }

  .shell__accion .icono {
    inline-size: 17px;
    block-size: 17px;
  }

  /* Punto de pendiente: coral claro sobre la marca (los tokens
     semánticos no tienen variante «sobre la marca») */
  .shell__accion-punto {
    position: absolute;
    inset-block-start: 6px;
    inset-inline-end: 7px;
    inline-size: 7px;
    block-size: 7px;
    border-radius: 999px;
    background: var(--c-400);
    border: 1.5px solid var(--marca);
  }

  .shell__accion-punto[hidden] {
    display: none;
  }

  .shell__separador-acciones {
    flex: none;
    inline-size: 1px;
    block-size: 24px;
    margin-inline: 6px;
    background: rgb(255 255 255 / 0.22);
  }

  @media (max-width: 916px) {
    .shell:not([data-barra]) .shell__barra-lateral {
      inline-size: var(--sh-side-rail);
    }

    .shell:not([data-barra]) .shell__solo-abierta {
      opacity: 0;
    }

    .shell:not([data-barra]) .shell__colapso-icono {
      rotate: 180deg;
    }
  }

  /* ── riel → panel: 64 + 640 + 2×18 ≈ 740 px · o siempre en táctil ──────── */
  @media (max-width: 740px), (pointer: coarse) {
    .shell__barra-lateral,
    .shell[data-barra="riel"] .shell__barra-lateral {
      position: fixed;
      inset-block: 0;
      inset-inline-start: 0;
      z-index: 60;
      inline-size: min(280px, 86vw);
      translate: -100% 0;
      transition: translate var(--med) var(--ease);
      box-shadow: none;
    }

    .shell[data-panel="abierto"] .shell__barra-lateral {
      translate: 0 0;
      box-shadow: var(--e4);
    }

    .shell__solo-abierta,
    .shell[data-barra="riel"] .shell__solo-abierta {
      opacity: 1;
    }

    .shell[data-panel="abierto"] .shell__velo {
      display: block;
      position: fixed;
      inset: 0;
      z-index: 55;
      background: var(--scrim);
      border: none;
      padding: 0;
    }

    /* La hamburguesa solo existe aquí, con toque de 44 px */
    .shell__control-menu {
      display: grid;
      place-items: center;
      inline-size: 44px;
      block-size: 44px;
    }

    /* En panel el control es la hamburguesa: dos formas de abrir la misma
       barra se contradicen (contrato v2.10.0). */
    .shell__colapso {
      display: none;
    }

    /* El encabezado se compacta; el nombre se retira antes que el logotipo */
    .shell__encabezado-marca {
      min-block-size: 56px;
    }

    .shell__marca-logo {
      block-size: 34px;
    }

    .shell__marca-separador,
    .shell__marca-textos {
      display: none;
    }
  }
}

/*!inventario
{
  "componente": "Filtros y vistas guardadas",
  "proposito": "Un filtro aplicado siempre es visible como ficha removible; una combinación útil se guarda como vista con nombre. El estado vive en la URL y lo resuelve el servidor (AD-10): cada control es un disparador htmx — sin JS propio.",
  "raiz": "filtros",
  "variantes": {},
  "estados": {
    "[aria-current='true']": "La vista guardada aplicada (en filtros__vista)"
  },
  "modificadores": {},
  "estructura": [
    { "clase": "filtros__vistas", "elemento": "div", "papel": "Fila de vistas guardadas: píldoras + guardar", "opcional": true },
    { "clase": "filtros__vista", "elemento": "button | a", "papel": "Una vista guardada: icono 13px + nombre + conteo", "opcional": true },
    { "clase": "filtros__vista-conteo", "elemento": "span", "papel": "El conteo de la vista, en mono", "opcional": true },
    { "clase": "filtros__guardar", "elemento": "button", "papel": "Guardar la combinación actual como vista (píldora punteada, icono i-plus)", "opcional": true },
    { "clase": "filtros__activos", "elemento": "div", "papel": "Fila de filtros aplicados — SIEMPRE visible cuando hay filtros: es lo que comunica que están activos", "opcional": false },
    { "clase": "filtros__activos-rotulo", "elemento": "span", "papel": "El rótulo «Filtros activos»", "opcional": false },
    { "clase": "filtros__ficha", "elemento": "span", "papel": "Un filtro aplicado: campo + operador + valor + quitar", "opcional": false },
    { "clase": "filtros__ficha-campo", "elemento": "span", "papel": "El campo filtrado (negrita)", "opcional": false },
    { "clase": "filtros__ficha-operador", "elemento": "span", "papel": "El operador, atenuado", "opcional": false },
    { "clase": "filtros__ficha-valor", "elemento": "span", "papel": "El valor", "opcional": false },
    { "clase": "filtros__ficha-quitar", "elemento": "button", "papel": "Quita ESTE filtro (icono i-x) — removible una por una", "opcional": false },
    { "clase": "filtros__vacio", "elemento": "span", "papel": "«Ninguno · se muestran todos los registros.» — el vacío explica", "opcional": true },
    { "clase": "filtros__restablecer", "elemento": "button | a", "papel": "Quita todos los filtros, al extremo derecho", "opcional": false },
    { "clase": "filtros__constructor", "elemento": "div", "papel": "Constructor de condiciones (cuando la pantalla lo compone)", "opcional": true },
    { "clase": "filtros__constructor-rotulo", "elemento": "span", "papel": "El rótulo del constructor", "opcional": true },
    { "clase": "filtros__condicion", "elemento": "div", "papel": "Una condición: unión + campo + operador + valor + eliminar", "opcional": true },
    { "clase": "filtros__union", "elemento": "span", "papel": "La unión (Donde / Y / O), columna fija en mono", "opcional": true },
    { "clase": "filtros__campo", "elemento": "span.campo__selector", "papel": "El campo a filtrar — select simple de la librería (campo__selector), elástico", "opcional": true },
    { "clase": "filtros__operador", "elemento": "span.campo__selector", "papel": "El operador — select simple, ancho fijo", "opcional": true },
    { "clase": "filtros__valor", "elemento": "input.campo__entrada", "papel": "El valor de la condición, elástico", "opcional": true },
    { "clase": "filtros__eliminar", "elemento": "button", "papel": "Elimina la condición (icono i-trash-2); hover en danger", "opcional": true },
    { "clase": "filtros__agregados", "elemento": "div", "papel": "Fila de altas: añadir condición / añadir grupo", "opcional": true },
    { "clase": "filtros__agregar", "elemento": "button", "papel": "Alta punteada (i-plus condición · i-git-branch grupo)", "opcional": true },
    { "clase": "filtros__pie", "elemento": "div", "papel": "Resumen en mono + acciones (botones de la librería); Aplicar es el primario", "opcional": true },
    { "clase": "filtros__resumen", "elemento": "span", "papel": "El resumen legible del filtro compuesto", "opcional": true }
  ],
  "internas": [],
  "contexto": ["campo"],
  "ejemplo": [
    "<div class='filtros'>",
    "  <div class='filtros__vistas'>",
    "    <button class='filtros__vista' aria-current='true'>",
    "      <svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-folder-kanban' /></svg>",
    "      Mi trabajo <span class='filtros__vista-conteo'>12</span></button>",
    "    <button class='filtros__guardar'>",
    "      <svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-plus' /></svg>",
    "      Guardar vista</button>",
    "  </div>",
    "  <div class='filtros__activos'>",
    "    <span class='filtros__activos-rotulo'>Filtros activos</span>",
    "    <span class='filtros__ficha'>",
    "      <span class='filtros__ficha-campo'>Estado</span>",
    "      <span class='filtros__ficha-operador'>es</span>",
    "      <span class='filtros__ficha-valor'>Bloqueada</span>",
    "      <button class='filtros__ficha-quitar' aria-label='Quitar el filtro de estado' hx-get='…'>",
    "        <svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-x' /></svg></button>",
    "    </span>",
    "    <button class='filtros__restablecer' hx-get='…'>Restablecer</button>",
    "  </div>",
    "</div>"
  ]
}
*/
/* Filtros y vistas guardadas — contra «Filtros y vistas guardadas» (sección 28,
   componentes avanzados) del sistema v2.13.0. [CORE-18]

   La regla de la sección: UN FILTRO APLICADO SIEMPRE ES VISIBLE como ficha
   removible — el defecto del estreno era exactamente que los filtros no
   comunicaban cuándo estaban activos. Las cuatro partes de la anatomía:
   vistas guardadas · filtros activos · constructor de condiciones · pie.
   Cada parte es componible por separado: Mi semana usa activos (y vistas si
   las guarda); el constructor espera a la pantalla que lo pida.

   Los selects del constructor son el SELECT SIMPLE de la librería
   (campo__selector, nativo — decisión de [CORE-13]); el listbox a medida de
   las maquetas del DS no se reimplementa aquí. Las acciones del pie son los
   botones de la librería. Sin JS: aplicar/quitar/restablecer son disparadores
   htmx y el estado vive en la URL (AD-10).

   Los tonos del estado activo (vista aplicada, ficha) son la familia acc-soft
   del sistema — el export de la sección trae los valores como variables de
   datos, y esa familia es la que el propio sistema usa para "seleccionado"
   (tabla, etiquetas removibles). */

@layer componentes {
  .filtros {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    box-shadow: var(--e2);
    overflow: hidden;
  }

  /* ── Vistas guardadas ─────────────────────────────────────────────────── */
  .filtros__vistas {
    display: flex;
    align-items: center;
    gap: 7px;
    flex-wrap: wrap;
    padding: 12px clamp(14px, 2vw, 18px);
    border-block-end: 1px solid var(--divider);
  }

  .filtros__vista {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    padding: 7px 13px;
    border: 1px solid var(--line-2);
    border-radius: 999px;
    background: var(--surface);
    color: var(--fg-2);
    font-family: var(--f-ui);
    font-size: 12px;
    font-weight: 600;
    cursor: pointer;
    transition: background var(--fast) var(--ease), border-color var(--fast) var(--ease);

    &:hover {
      background: var(--sunken);
    }

    &[aria-current="true"] {
      background: var(--acc-soft);
      border-color: var(--acc-soft-2);
      color: var(--acc);
      font-weight: 700;
    }

    .icono {
      inline-size: 13px;
      block-size: 13px;
      stroke-width: 3.231px; /* 1.75 × 24 / 13 */
    }
  }

  .filtros__vista-conteo {
    font-family: var(--f-mono);
    font-size: 9.5px;
    color: var(--fg-3);
  }

  .filtros__vista[aria-current="true"] .filtros__vista-conteo {
    color: var(--acc);
  }

  .filtros__guardar {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 7px 12px;
    border: 1px dashed var(--line-2);
    border-radius: 999px;
    background: transparent;
    color: var(--fg-3);
    font-family: var(--f-ui);
    font-size: 12px;
    font-weight: 600;
    cursor: pointer;
    transition: border-color var(--fast) var(--ease), color var(--fast) var(--ease);

    &:hover {
      border-color: var(--acc);
      color: var(--acc);
    }

    .icono {
      inline-size: 12px;
      block-size: 12px;
      stroke-width: 3.5px; /* 1.75 × 24 / 12 */
    }
  }

  /* ── Filtros activos: la fila que comunica que HAY filtros ────────────── */
  .filtros__activos {
    display: flex;
    align-items: center;
    gap: 9px;
    flex-wrap: wrap;
    padding: 14px clamp(14px, 2vw, 18px);

    &:not(:last-child) {
      border-block-end: 1px solid var(--divider);
    }
  }

  .filtros__activos-rotulo {
    flex: none;
    font-family: var(--f-mono);
    font-size: 9.5px;
    letter-spacing: .07em;
    text-transform: uppercase;
    color: var(--fg-3);
  }

  .filtros__ficha {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    padding: 5px 7px 5px 11px;
    border: 1px solid var(--acc-soft-2);
    border-radius: var(--r-sm);
    background: var(--acc-soft);
    color: var(--acc);
    font-size: 11.5px;
  }

  .filtros__ficha-campo {
    font-weight: 700;
  }

  .filtros__ficha-operador {
    opacity: .7;
  }

  .filtros__ficha-valor {
    font-weight: 600;
  }

  .filtros__ficha-quitar {
    inline-size: 16px;
    block-size: 16px;
    display: grid;
    place-items: center;
    border: none;
    border-radius: 999px;
    background: var(--acc-soft-2);
    color: var(--acc);
    cursor: pointer;
    padding: 0;

    .icono {
      inline-size: 14px;
      block-size: 14px;
      stroke-width: 3px; /* 1.75 × 24 / 14 */
    }
  }

  .filtros__vacio {
    font-size: 12px;
    color: var(--fg-3);
    font-style: italic;
  }

  .filtros__restablecer {
    margin-inline-start: auto;
    flex: none;
    border: none;
    background: transparent;
    color: var(--link);
    font-family: var(--f-ui);
    font-size: 11.5px;
    font-weight: 600;
    cursor: pointer;
    padding: 0;

    &:hover {
      color: var(--link-h);
    }
  }

  /* ── Constructor de condiciones ───────────────────────────────────────── */
  .filtros__constructor {
    display: flex;
    flex-direction: column;
    gap: 14px;
    padding: clamp(16px, 2.4vw, 20px);
  }

  .filtros__constructor-rotulo {
    font-family: var(--f-mono);
    font-size: 10px;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--fg-3);
  }

  .filtros__condicion {
    display: flex;
    align-items: center;
    gap: 9px;
    flex-wrap: wrap;
  }

  .filtros__union {
    inline-size: 52px;
    flex: none;
    font-family: var(--f-mono);
    font-size: 10.5px;
    letter-spacing: .06em;
    text-transform: uppercase;
    text-align: end;
    color: var(--fg-3);
  }

  /* Campo y valor elásticos; operador fijo — los selects son el select simple
     de la librería (campo__selector), el input es campo__entrada. */
  .filtros__campo {
    flex: 1;
    min-inline-size: 130px;
  }

  .filtros__operador {
    flex: none;
    min-inline-size: 110px;
  }

  .filtros__valor {
    flex: 1;
    min-inline-size: 130px;
  }

  .filtros__eliminar {
    inline-size: 32px;
    block-size: 32px;
    flex: none;
    display: grid;
    place-items: center;
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    background: var(--surface);
    color: var(--fg-3);
    cursor: pointer;
    padding: 0;
    transition: background var(--fast) var(--ease), color var(--fast) var(--ease),
      border-color var(--fast) var(--ease);

    &:hover {
      background: var(--danger-soft);
      border-color: var(--danger-bd);
      color: var(--danger);
    }

    .icono {
      inline-size: 14px;
      block-size: 14px;
      stroke-width: 3px; /* 1.75 × 24 / 14 */
    }
  }

  /* 61px = unión 52 + espacio 9: las altas alinean con los controles. */
  .filtros__agregados {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    padding-inline-start: 61px;
  }

  .filtros__agregar {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 7px 12px;
    border: 1px dashed var(--line-2);
    border-radius: var(--r-sm);
    background: transparent;
    color: var(--fg-2);
    font-family: var(--f-ui);
    font-size: 11.5px;
    font-weight: 600;
    cursor: pointer;
    transition: border-color var(--fast) var(--ease), color var(--fast) var(--ease);

    &:hover {
      border-color: var(--acc);
      color: var(--acc);
    }

    .icono {
      inline-size: 12px;
      block-size: 12px;
      stroke-width: 3.5px; /* 1.75 × 24 / 12 */
    }
  }

  /* ── Pie: resumen + acciones (los botones son de la librería) ─────────── */
  .filtros__pie {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
    padding: 13px clamp(14px, 2vw, 18px);
    border-block-start: 1px solid var(--divider);
    background: var(--sunken);
  }

  .filtros__resumen {
    flex: 1;
    min-inline-size: 180px;
    font-family: var(--f-mono);
    font-size: 11px;
    color: var(--fg-2);
  }

  @media (pointer: coarse) {
    .filtros__vista,
    .filtros__guardar,
    .filtros__ficha-quitar,
    .filtros__eliminar,
    .filtros__agregar,
    .filtros__restablecer {
      min-block-size: 44px;
    }

    .filtros__ficha-quitar,
    .filtros__eliminar {
      min-inline-size: 44px;
    }
  }
}

/*!inventario
{
  "componente": "Modal centrado",
  "proposito": "Capa centrada para un flujo largo que interrumpe (un asistente de varios pasos, una captura extensa). Para confirmar o avisar está la alerta; para acompañar sin perder el contexto, el cajón. Misma anatomía y contrato del cajón: encabezado fijo, cuerpo desplazable, pie fijo; el estado vive en data-abierto y sobrevive los swaps de htmx (modal.js).",
  "raiz": "modal",
  "variantes": {},
  "estados": {
    "[data-abierto='si']": "Abierto (lo pone modal.js); 'no' = cerrado. La raíz lleva data-componente='modal'"
  },
  "modificadores": {
    "modal--angosto": "Panel de 480px (una captura corta)",
    "modal--ancho": "Panel de 800px (un asistente con tabla o dos columnas)"
  },
  "estructura": [
    { "clase": "modal__velo", "elemento": "button", "papel": "Velo que cierra al clic; tabindex='-1' y data-accion='cerrar-modal'", "opcional": false },
    { "clase": "modal__panel", "elemento": "div", "papel": "El panel centrado: 640px, 480 con modal--angosto y 800 con modal--ancho; pantalla completa bajo 720px. Otro ancho se fija con --modal-ancho en el CSS de la app, nunca en un atributo style", "opcional": false },
    { "clase": "modal__encabezado", "elemento": "header", "papel": "Fijo arriba: identidad + botón de cierre", "opcional": false },
    { "clase": "modal__cuerpo", "elemento": "div", "papel": "Lo único desplazable", "opcional": false },
    { "clase": "modal__pie", "elemento": "footer", "papel": "Fijo abajo: acciones visibles sin importar el scroll", "opcional": false }
  ],
  "internas": [],
  "ejemplo": [
    "<div class='modal' data-componente='modal' data-abierto='no' role='dialog' aria-modal='true' aria-labelledby='titulo-x'>",
    "  <button class='modal__velo' data-accion='cerrar-modal' tabindex='-1' aria-label='Cerrar'></button>",
    "  <div class='modal__panel'>",
    "    <header class='modal__encabezado'>…</header>",
    "    <div class='modal__cuerpo'>…</div>",
    "    <footer class='modal__pie'>…</footer>",
    "  </div>",
    "</div>",
    "<button type='button' class='boton' data-accion='abrir-modal' data-modal='#mi-modal'>Abrir</button>"
  ]
}
*/
/* Modal centrado — contra «Modales, paneles y avisos» (sección 24) del sistema: el
   modal interrumpe. Se reserva a flujos largos que no caben en la alerta ni tienen
   sentido en el cajón. Contrato idéntico al cajón para convivir con htmx: el estado
   abierto/cerrado vive en el marcado (data-abierto) y sobrevive los swaps; modal.js
   maneja velo, botón y Escape y devuelve el foco a quien lo abrió. */

@layer componentes {
  .modal {
    position: fixed;
    inset: 0;
    z-index: 80;
    display: grid;
    /* La fila se acota al alto de la ventana: con una fila automática, un contenido más alto
       que la pantalla la estira, el panel se centra dentro de ella y se sale por abajo en
       vez de desplazar su cuerpo. */
    grid-template-rows: minmax(0, 1fr);
    place-items: center;
    padding: var(--sp-4);
  }

  .modal[data-abierto="no"] {
    visibility: hidden;
    pointer-events: none;
  }

  .modal__velo {
    position: absolute;
    inset: 0;
    border: none;
    padding: 0;
    background: var(--scrim);
    cursor: default;
    opacity: 0;
    transition: opacity var(--med) var(--ease);
  }

  .modal[data-abierto="si"] .modal__velo {
    opacity: 1;
  }

  /* Escalera de anchos: el modificador fija la custom property que lee el panel. */
  .modal--angosto {
    --modal-ancho: 480px;
  }

  .modal--ancho {
    --modal-ancho: 800px;
  }

  .modal__panel {
    position: relative;
    inline-size: min(var(--modal-ancho, 640px), 100%);
    max-block-size: min(92dvh, 100%);
    background: var(--raised);
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    box-shadow: var(--e4);
    display: flex;
    flex-direction: column;
    opacity: 0;
    scale: .97;
    transition: opacity var(--med) var(--ease), scale var(--med) var(--ease);
  }

  .modal[data-abierto="si"] .modal__panel {
    opacity: 1;
    scale: 1;
  }

  /* Encabezado fijo: identidad y cierre siempre a la vista */
  .modal__encabezado {
    flex: none;
    display: flex;
    align-items: flex-start;
    gap: 12px;
    padding: 16px 20px;
    border-block-end: 1px solid var(--divider);
  }

  /* Cuerpo: lo único desplazable */
  .modal__cuerpo {
    flex: 1;
    overflow-y: auto;
    min-block-size: 0;
  }

  /* Pie fijo: acciones visibles sin importar el scroll */
  .modal__pie {
    flex: none;
    padding: 14px 20px;
    border-block-start: 1px solid var(--line);
    background: var(--surface);
    border-end-start-radius: var(--r-lg);
    border-end-end-radius: var(--r-lg);
    display: flex;
    flex-direction: column;
    gap: 9px;
  }

  /* Pantalla completa en ventanas chicas */
  @media (max-width: 720px) {
    .modal {
      padding: 0;
    }

    .modal__panel {
      inline-size: 100%;
      max-block-size: 100dvh;
      border-radius: 0;
      border: none;
    }

    .modal__pie {
      border-radius: 0;
    }
  }

  @media (prefers-reduced-motion: reduce) {
    .modal__velo,
    .modal__panel {
      transition: none;
    }
  }
}

/*!inventario
{
  "componente": "Código de verificación",
  "proposito": "Captura de un código de un solo uso de 6 dígitos (segundo factor, confirmación por correo). Es UN solo input —inputmode numeric y autocomplete one-time-code— segmentado únicamente en lo visual: el lector de pantalla y el autollenado del sistema ven un campo normal. Acepta pegar el código completo, con o sin espacios. Sin JavaScript sigue siendo un campo que funciona.",
  "raiz": "codigo-verificacion",
  "variantes": {},
  "estados": {
    "codigo-verificacion--con-error": "Celdas con borde danger y el mensaje visible (código incorrecto o vencido)",
    "[data-llena='si']": "En codigo-verificacion__celda: ya tiene dígito (lo pone codigo-verificacion.js)",
    "[data-activa='si']": "En codigo-verificacion__celda: la que recibirá el siguiente dígito, con el campo enfocado",
    "[disabled]": "En la entrada: inactivo mientras se verifica"
  },
  "modificadores": {},
  "estructura": [
    { "clase": "codigo-verificacion__etiqueta", "elemento": "label", "papel": "Etiqueta siempre visible, con for al input", "opcional": false },
    { "clase": "codigo-verificacion__caja", "elemento": "div", "papel": "Ancla: las celdas dibujadas detrás y el input encima", "opcional": false },
    { "clase": "codigo-verificacion__celdas", "elemento": "span", "papel": "Las seis celdas, solo visuales (aria-hidden)", "opcional": false },
    { "clase": "codigo-verificacion__celda", "elemento": "span", "papel": "Una celda de 44px", "opcional": false },
    { "clase": "codigo-verificacion__entrada", "elemento": "input", "papel": "El único input: type=text, inputmode=numeric, autocomplete=one-time-code, maxlength=6, pattern=[0-9]{6}", "opcional": false },
    { "clase": "codigo-verificacion__ayuda", "elemento": "p", "papel": "A dónde se envió el código y cuánto dura", "opcional": true },
    { "clase": "codigo-verificacion__error", "elemento": "p", "papel": "Mensaje de error con icono i-circle-alert: dice cómo corregir", "opcional": true }
  ],
  "internas": [],
  "contexto": ["icono"],
  "ejemplo": [
    "<div class='codigo-verificacion' data-componente='codigo-verificacion'>",
    "  <label class='codigo-verificacion__etiqueta' for='codigo'>Código de verificación</label>",
    "  <div class='codigo-verificacion__caja'>",
    "    <span class='codigo-verificacion__celdas' aria-hidden='true'><span class='codigo-verificacion__celda'></span><span class='codigo-verificacion__celda'></span><span class='codigo-verificacion__celda'></span><span class='codigo-verificacion__celda'></span><span class='codigo-verificacion__celda'></span><span class='codigo-verificacion__celda'></span></span>",
    "    <input class='codigo-verificacion__entrada' id='codigo' name='codigo' type='text' inputmode='numeric' autocomplete='one-time-code' maxlength='6' pattern='[0-9]{6}' required aria-describedby='codigo-ayuda' />",
    "  </div>",
    "  <p class='codigo-verificacion__ayuda' id='codigo-ayuda'>Escribe los 6 dígitos de tu aplicación de autenticación.</p>",
    "</div>"
  ]
}
*/
/* Código de verificación — un solo input, segmentado solo en lo visual.
   Las celdas se dibujan detrás; el input va encima, transparente, en mono, con un
   letter-spacing que coloca cada dígito en el centro de su celda:

     avance por dígito = celda + separación   →   letter-spacing = avance − 1ch
     primer dígito centrado                   →   padding inicial = (celda − 1ch) / 2

   El input mide una celda más que la caja para que, con los seis dígitos escritos,
   el cursor no fuerce un desplazamiento horizontal; la caja lo recorta.

   FOCO: el control muestra su propio foco (borde de las celdas y la activa en
   acento), por eso el input suprime el contorno global (RD-021). */

@layer componentes {
  .codigo-verificacion {
    --cv-celda: clamp(34px, 11vw, 44px);
    --cv-sep: 8px;

    display: flex;
    flex-direction: column;
    gap: 6px;
  }

  .codigo-verificacion__etiqueta {
    font-size: 12.5px;
    font-weight: 700;
  }

  .codigo-verificacion__caja {
    position: relative;
    inline-size: calc(6 * var(--cv-celda) + 5 * var(--cv-sep));
    max-inline-size: 100%;
    block-size: 52px;
    overflow: hidden;
  }

  .codigo-verificacion__celdas {
    position: absolute;
    inset: 0;
    display: grid;
    grid-template-columns: repeat(6, var(--cv-celda));
    gap: var(--cv-sep);
    pointer-events: none;
  }

  .codigo-verificacion__celda {
    border: 1px solid var(--line-control);
    border-radius: var(--r-sm);
    background: var(--surface);
    transition: border-color var(--fast) var(--ease), background-color var(--fast) var(--ease);
  }

  .codigo-verificacion__celda[data-llena="si"] {
    border-color: var(--fg-3);
  }

  .codigo-verificacion__entrada {
    position: absolute;
    inset-block: 0;
    inset-inline-start: 0;
    inline-size: calc(100% + var(--cv-celda) + var(--cv-sep));
    padding: 0;
    padding-inline-start: calc((var(--cv-celda) - 1ch) / 2);
    border: none;
    background: transparent;
    color: var(--fg);
    caret-color: var(--acc);
    font-family: var(--f-mono);
    font-size: 22px;
    font-weight: 600;
    letter-spacing: calc(var(--cv-celda) + var(--cv-sep) - 1ch);

    &:focus-visible {
      outline: none;
    }

    &:disabled {
      color: var(--fg-4);
      cursor: not-allowed;
    }
  }

  /* Foco: todas las celdas toman el anillo; la activa, el acento */
  .codigo-verificacion__caja:focus-within .codigo-verificacion__celda {
    border-color: var(--ring);
  }

  .codigo-verificacion__caja:focus-within .codigo-verificacion__celda[data-activa="si"] {
    border-color: var(--acc);
    background: var(--acc-soft);
  }

  .codigo-verificacion__caja:has(.codigo-verificacion__entrada:disabled) .codigo-verificacion__celda {
    border-color: var(--line);
    background: var(--sunken);
  }

  .codigo-verificacion__ayuda {
    font-size: 11.5px;
    color: var(--fg-3);
  }

  .codigo-verificacion__error {
    display: flex;
    align-items: center;
    gap: 5px;
    font-size: 11.5px;
    color: var(--danger);
  }

  .codigo-verificacion__error .icono {
    flex: none;
    inline-size: 13px;
    block-size: 13px;
  }

  .codigo-verificacion--con-error .codigo-verificacion__celda {
    border-color: var(--danger);
  }

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

/*!inventario
{
  "componente": "Tarjeta",
  "proposito": "Agrupa un tema con su icono, título, texto y acción (una especialidad, un servicio, el resumen de un paciente, un consultorio). Blanca, con borde y sin sombra en reposo; los tintes verde y coral sustituyen a la sombra para agrupar.",
  "raiz": "tarjeta",
  "variantes": {
    "tarjeta--verde": "Fondo verde tenue (acc-soft), sin borde: agrupa o destaca sin competir con la acción",
    "tarjeta--coral": "Fondo coral tenue (cta-soft), sin borde: la tarjeta que lleva a la conversión (la consulta destacada). Máximo una por grupo",
    "tarjeta--elevada": "Con sombra e2: la tarjeta flota sobre un fondo de color o una fotografía"
  },
  "estados": {
    ":hover (tarjeta--enlace)": "Sube 2px y gana la sombra e3",
    ":focus-visible (tarjeta--enlace)": "Anillo de foco alrededor de toda la tarjeta"
  },
  "modificadores": {
    "tarjeta--enlace": "Toda la tarjeta es un enlace (a.tarjeta): sube al pasar el puntero. Dentro no va otro enlace ni botón",
    "tarjeta--filete": "Filete superior de 3px en verde de marca (g-600) sobre la tarjeta blanca: identifica el tema de la tarjeta en una rejilla (las especialidades de la landing)",
    "tarjeta--filete-coral": "Con tarjeta--filete: el filete en coral (c-500)",
    "tarjeta--filete-claro": "Con tarjeta--filete: el filete en verde claro (g-400)",
    "tarjeta__icono--coral": "El cuadro del icono en coral (la tarjeta de conversión)"
  },
  "estructura": [
    { "clase": "tarjeta__icono", "elemento": "span > svg.icono", "papel": "Cuadro de 48px con radio de 12px y el icono a 24px en verde (acc-soft de fondo)", "opcional": true },
    { "clase": "tarjeta__etiqueta", "elemento": "span", "papel": "Rótulo corto en mayúsculas espaciadas (12px, 0.14em) sobre el título", "opcional": true },
    { "clase": "tarjeta__titulo", "elemento": "h2 | h3", "papel": "Título en display (Jost 500, 20px)", "opcional": false },
    { "clase": "tarjeta__texto", "elemento": "p", "papel": "Una o dos frases en fg-2", "opcional": true },
    { "clase": "tarjeta__pie", "elemento": "div", "papel": "Acciones o datos al pie, empujados al fondo de la tarjeta (las tarjetas de una rejilla alinean su pie)", "opcional": true }
  ],
  "internas": [],
  "contexto": ["icono", "boton", "chip-estatus"],
  "ejemplo": [
    "<article class='tarjeta'>",
    "  <span class='tarjeta__icono'><svg class='icono icono--24' aria-hidden='true'><use href='/images/iconos.svg#i-heart-pulse' /></svg></span>",
    "  <h3 class='tarjeta__titulo'>Diabetes Tipo 1 y 2</h3>",
    "  <p class='tarjeta__texto'>Control glucémico a través de la alimentación.</p>",
    "</article>",
    "<article class='tarjeta tarjeta--filete tarjeta--filete-coral'>…</article>",
    "<a class='tarjeta tarjeta--coral tarjeta--enlace' href='https://wa.me/…'>",
    "  <span class='tarjeta__icono tarjeta__icono--coral'><svg class='icono icono--24' aria-hidden='true'><use href='/images/iconos.svg#i-stethoscope' /></svg></span>",
    "  <span class='tarjeta__etiqueta'>Más solicitado</span>",
    "  <h3 class='tarjeta__titulo'>Consulta Privada</h3>",
    "  <p class='tarjeta__texto'>Valoración nutricional completa y plan de alimentación personalizado.</p>",
    "</a>"
  ]
}
*/
/* Tarjeta — viene del Card del DS de Nutrimav: blanca, borde de 1px, radio de 20px y sin
   sombra en reposo. Agrupar con tinte (verde o coral) en lugar de sombra. La rejilla que
   las contiene es de la página (auto-fit); la tarjeta estira su pie al fondo para que las
   de una misma fila alineen sus acciones. */

@layer componentes {
  .tarjeta {
    display: flex;
    flex-direction: column;
    gap: var(--sp-3);
    padding: var(--sp-5);
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    background: var(--surface);
    color: var(--fg);
  }

  .tarjeta--verde {
    border-color: transparent;
    background: var(--acc-soft);
  }

  .tarjeta--coral {
    border-color: transparent;
    background: var(--cta-soft);
  }

  /* Filete superior: el tono identifica el tema; es decorativo, el título lo dice en texto */
  .tarjeta--filete {
    border-block-start: 3px solid var(--g-600);
  }

  .tarjeta--filete-coral {
    border-block-start-color: var(--c-500);
  }

  .tarjeta--filete-claro {
    border-block-start-color: var(--g-400);
  }

  .tarjeta--elevada {
    border-color: transparent;
    box-shadow: var(--e2);
  }

  /* Toda la tarjeta es el enlace: sube 2px al pasar el puntero (sin rebote) */
  .tarjeta--enlace {
    text-decoration: none;
    cursor: pointer;
    transition: transform var(--med) var(--ease), box-shadow var(--med) var(--ease);

    &:hover {
      transform: translateY(-2px);
      box-shadow: var(--e3);
    }

    &:focus-visible {
      outline: 2px solid var(--ring);
      outline-offset: 2px;
    }
  }

  .tarjeta__icono {
    display: grid;
    place-items: center;
    inline-size: 48px;
    block-size: 48px;
    border-radius: var(--r-md);
    background: var(--acc-soft);
    color: var(--acc);
  }

  /* Dentro de una tarjeta verde el cuadro se separa del fondo con la superficie */
  .tarjeta--verde .tarjeta__icono {
    background: var(--surface);
  }

  .tarjeta__icono--coral {
    background: var(--cta);
    color: var(--cta-fg);
  }

  .tarjeta__etiqueta {
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--acc);
  }

  .tarjeta__titulo {
    margin: 0;
    font-family: var(--f-display);
    font-size: 20px;
    font-weight: 500;
    line-height: 1.3;
  }

  .tarjeta__texto {
    margin: 0;
    font-size: 14.5px;
    line-height: 1.55;
    color: var(--fg-2);
  }

  .tarjeta__pie {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-2);
    margin-block-start: auto;
    padding-block-start: var(--sp-2);
  }

  @media (prefers-reduced-motion: reduce) {
    .tarjeta--enlace,
    .tarjeta--enlace:hover {
      transition: none;
      transform: none;
    }
  }
}

/*!inventario
{
  "componente": "Selector de fecha",
  "proposito": "Fecha que se escribe (dd/mm/aaaa, con máscara) o se elige en un calendario en capa flotante: semana que empieza en lunes, meses en español, mínimo y máximo. Sustituye a input type='date', cuyo calendario es del navegador y cambia en cada uno. El valor viaja en un input hidden en ISO (aaaa-mm-dd). Como en desplegable, sale un solo change (burbujea) por selección terminada: al cerrarse el calendario, al salir el foco del componente o con Entrar en una entrada, y solo si el valor cambió; en el rango, uno aunque cambien los dos extremos. Los pasos intermedios (el primer clic del rango, a media escritura) no avisan, así que un filtro htmx escucha change en el formulario o en la raíz aunque reemplace el fragmento donde vive el selector. Va dentro de un campo: la entrada lleva también campo__entrada, así que la etiqueta, la ayuda y el error son los del campo. fecha.js da la máscara, valida (formato y rango, con mensaje propio) y maneja el calendario con teclado completo.",
  "raiz": "fecha",
  "variantes": {
    "fecha--rango": "Periodo desde–hasta para filtros: dos entradas, dos hidden (data-extremo='desde' y 'hasta') y un solo calendario que marca el tramo; admite atajos (Hoy, Últimos 7 días…)"
  },
  "estados": {
    "[data-abierto='si']": "Calendario visible (en la raíz); el botón refleja aria-expanded",
    "[aria-selected='true']": "En fecha__dia: el día elegido, o un extremo del periodo",
    "[aria-current='date']": "En fecha__dia: hoy",
    "[aria-disabled='true']": "En fecha__dia: fuera de data-min / data-max; se puede recorrer, no elegir",
    "[data-rango='dentro']": "En fecha__dia: día entre los dos extremos del periodo",
    "[data-atajo-activo='si']": "En fecha__atajo: el atajo que coincide con el periodo elegido"
  },
  "modificadores": {},
  "estructura": [
    { "clase": "fecha__control", "elemento": "span", "papel": "La fila de la(s) entrada(s) con el botón del calendario encima de la última", "opcional": false },
    { "clase": "fecha__entrada", "elemento": "input[type='text']", "papel": "Lleva también campo__entrada. Sin name (no viaja): inputmode='numeric', placeholder='dd/mm/aaaa', autocomplete='off'. required y data-error se ponen aquí; en rango, data-extremo y aria-label «Desde» / «Hasta»", "opcional": false },
    { "clase": "fecha__separador", "elemento": "span", "papel": "En rango: «a» entre las dos entradas (aria-hidden)", "opcional": true },
    { "clase": "fecha__boton", "elemento": "button", "papel": "Abre el calendario: i-calendar, aria-haspopup='dialog', aria-expanded y aria-label «Elegir la fecha en el calendario»; va solo (trae su propio reset)", "opcional": false },
    { "clase": "fecha__boton-icono", "elemento": "svg.icono", "papel": "El i-calendar del botón (15px)", "opcional": false },
    { "clase": "fecha__panel", "elemento": "div[role='dialog']", "papel": "La capa flotante con aria-label; fecha.js pinta dentro el calendario. En rango puede traer fecha__atajos desde el marcado", "opcional": false },
    { "clase": "fecha__calendario", "elemento": "div", "papel": "Cabecera, cuadrícula y pie (lo genera fecha.js)", "opcional": false },
    { "clase": "fecha__cabecera", "elemento": "div", "papel": "Mes anterior, nombre del mes y mes siguiente", "opcional": false },
    { "clase": "fecha__navegar", "elemento": "button", "papel": "Mes anterior / siguiente (i-chevron-left / i-chevron-right, con aria-label)", "opcional": false },
    { "clase": "fecha__mes", "elemento": "span", "papel": "«octubre 2026»; aria-live='polite' para anunciar el cambio de mes", "opcional": false },
    { "clase": "fecha__cuadricula", "elemento": "table[role='grid']", "papel": "Semanas de lunes a domingo; un solo día con tabindex='0' (foco itinerante)", "opcional": false },
    { "clase": "fecha__dia-semana", "elemento": "th", "papel": "«lu» … «do», con abbr del nombre completo", "opcional": false },
    { "clase": "fecha__dia", "elemento": "td[role='gridcell']", "papel": "Un día: data-fecha en ISO y aria-label con la fecha completa", "opcional": false },
    { "clase": "fecha__pie", "elemento": "div", "papel": "El botón «Hoy»", "opcional": false },
    { "clase": "fecha__hoy", "elemento": "button", "papel": "Elige hoy (si está dentro del mínimo y el máximo)", "opcional": false },
    { "clase": "fecha__atajos", "elemento": "div", "papel": "En rango: columna de atajos junto al calendario (desde el marcado)", "opcional": true },
    { "clase": "fecha__atajos-titulo", "elemento": "span", "papel": "Rótulo «Atajos»", "opcional": true },
    { "clase": "fecha__atajo", "elemento": "button", "papel": "data-atajo: hoy, ultimos-7, ultimos-30, este-mes, primera-quincena o segunda-quincena", "opcional": true }
  ],
  "internas": [],
  "contexto": ["campo", "campo__entrada", "icono"],
  "ejemplo": [
    "<div class='campo'>",
    "  <label class='campo__etiqueta' for='d-emision'>Fecha de emisión</label>",
    "  <div class='fecha' data-componente='fecha' data-max='2026-10-11'>",
    "    <input type='hidden' name='FechaEmision' value='2026-02-04'>",
    "    <span class='fecha__control'>",
    "      <input class='campo__entrada fecha__entrada' id='d-emision' type='text' inputmode='numeric' placeholder='dd/mm/aaaa' autocomplete='off' value='04/02/2026' required data-error='Capture la fecha de emisión.'>",
    "      <button type='button' class='fecha__boton' aria-haspopup='dialog' aria-expanded='false' aria-label='Elegir la fecha en el calendario'>",
    "        <svg class='icono fecha__boton-icono' aria-hidden='true'><use href='/images/iconos.svg#i-calendar' /></svg>",
    "      </button>",
    "    </span>",
    "    <div class='fecha__panel' role='dialog' aria-label='Calendario: fecha de emisión'></div>",
    "  </div>",
    "</div>",
    "",
    "<div class='campo'>",
    "  <label class='campo__etiqueta' for='b-desde'>Periodo</label>",
    "  <div class='fecha fecha--rango' data-componente='fecha'>",
    "    <input type='hidden' name='desde' data-extremo='desde' value=''>",
    "    <input type='hidden' name='hasta' data-extremo='hasta' value=''>",
    "    <span class='fecha__control'>",
    "      <input class='campo__entrada fecha__entrada' id='b-desde' data-extremo='desde' type='text' inputmode='numeric' placeholder='dd/mm/aaaa' autocomplete='off' aria-label='Desde'>",
    "      <span class='fecha__separador' aria-hidden='true'>a</span>",
    "      <input class='campo__entrada fecha__entrada' data-extremo='hasta' type='text' inputmode='numeric' placeholder='dd/mm/aaaa' autocomplete='off' aria-label='Hasta'>",
    "      <button type='button' class='fecha__boton' aria-haspopup='dialog' aria-expanded='false' aria-label='Elegir el periodo en el calendario'>",
    "        <svg class='icono fecha__boton-icono' aria-hidden='true'><use href='/images/iconos.svg#i-calendar' /></svg>",
    "      </button>",
    "    </span>",
    "    <div class='fecha__panel' role='dialog' aria-label='Calendario: periodo'>",
    "      <div class='fecha__atajos'>",
    "        <span class='fecha__atajos-titulo'>Atajos</span>",
    "        <button type='button' class='fecha__atajo' data-atajo='hoy'>Hoy</button>",
    "        <button type='button' class='fecha__atajo' data-atajo='ultimos-7'>Últimos 7 días</button>",
    "        <button type='button' class='fecha__atajo' data-atajo='ultimos-30'>Últimos 30 días</button>",
    "        <button type='button' class='fecha__atajo' data-atajo='este-mes'>Este mes</button>",
    "      </div>",
    "    </div>",
    "  </div>",
    "</div>"
  ]
}
*/
/* Selector de fecha — contra el selector de fechas de la sección 07 (formularios) y el
   periodo con atajos de las capas. La fecha se ESCRIBE o se elige: escribir es lo más
   rápido para una fecha lejana (un nacimiento), el calendario ayuda con las cercanas.

   El estado del valor NO vive aquí: vive en el input hidden (ISO), cuyo change burbujea
   al formulario una vez por selección terminada. Las entradas de texto no tienen name:
   nunca viajan.

   La entrada usa la anatomía de campo__entrada (borde, foco, error); aquí solo se ajusta
   la tipografía (las fechas van en mono) y el espacio del botón. */

@layer componentes {
  .fecha {
    position: relative;
  }

  .fecha__control {
    position: relative;
    display: flex;
    align-items: center;
    gap: 8px;
  }

  .fecha__entrada {
    min-inline-size: 0;
    font-family: var(--f-mono);
    font-size: 13px;
    font-variant-numeric: tabular-nums;
  }

  /* El botón va encima de la ÚLTIMA entrada (la única, o «hasta» en el rango) */
  .fecha__control .fecha__entrada:last-of-type {
    padding-inline-end: 42px;
  }

  /* El periodo cabe en una columna de formulario de unos 280 px: menos relleno lateral */
  .fecha--rango .fecha__entrada {
    flex: 1 1 0;
    padding-inline: 10px;
  }

  .fecha--rango .fecha__control .fecha__entrada:last-of-type {
    padding-inline-end: 38px;
  }

  .fecha__separador {
    flex: none;
    font-size: 12.5px;
    color: var(--fg-3);
  }

  /* Es un <button>: trae su propio reset (va solo, sin «boton») */
  .fecha__boton {
    position: absolute;
    inset-block-start: 50%;
    inset-inline-end: 6px;
    translate: 0 -50%;
    display: grid;
    place-items: center;
    inline-size: 30px;
    block-size: 30px;
    padding: 0;
    border: none;
    border-radius: var(--r-xs);
    background: transparent;
    color: var(--fg-3);
    font: inherit;
    cursor: pointer;
    transition: color var(--fast) var(--ease), background var(--fast) var(--ease);

    &:hover {
      background: var(--sunken);
      color: var(--fg);
    }
  }

  .fecha[data-abierto="si"] .fecha__boton {
    background: var(--sunken);
    color: var(--acc);
  }

  .fecha__boton-icono {
    inline-size: 15px;
    block-size: 15px;
    stroke-width: 2.8px; /* 1.75 × 24 / 15 */
  }

  /* ── La capa flotante ──────────────────────────────────────────────────── */
  .fecha__panel {
    position: absolute;
    inset-block-start: calc(100% + 6px);
    inset-inline-end: 0;
    z-index: 30;
    display: flex;
    inline-size: max-content;
    max-inline-size: calc(100vw - 16px);
    margin: 0;
    padding: 0;
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    background: var(--raised);
    box-shadow: var(--e4);
    color: var(--fg);
    overflow: hidden;
    animation: fecha-pop var(--fast) var(--ease);
  }

  .fecha:not([data-abierto="si"]) .fecha__panel {
    display: none;
  }

  @keyframes fecha-pop {
    from {
      transform: translateY(-4px);
    }
  }

  @media (prefers-reduced-motion: reduce) {
    .fecha__panel {
      animation: none;
    }
  }

  .fecha__calendario {
    display: flex;
    flex-direction: column;
    gap: 11px;
    inline-size: min(300px, calc(100vw - 18px));
    padding: 14px;
  }

  .fecha__cabecera {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
  }

  .fecha__navegar {
    display: grid;
    place-items: center;
    inline-size: 28px;
    block-size: 28px;
    padding: 0;
    border: 1px solid var(--line);
    border-radius: var(--r-xs);
    background: var(--surface);
    color: var(--fg-2);
    font: inherit;
    cursor: pointer;

    &:hover {
      background: var(--sunken);
      color: var(--fg);
    }

    .icono {
      inline-size: 14px;
      block-size: 14px;
      stroke-width: 3px; /* 1.75 × 24 / 14 */
    }
  }

  .fecha__mes {
    font-size: 13px;
    font-weight: 700;
    text-transform: capitalize;
  }

  .fecha__cuadricula {
    inline-size: 100%;
    border-collapse: separate;
    border-spacing: 0 2px;
    table-layout: fixed;
  }

  .fecha__dia-semana {
    padding: 3px 0;
    font-family: var(--f-mono);
    font-size: 9.5px;
    font-weight: 400;
    letter-spacing: .06em;
    text-transform: uppercase;
    text-align: center;
    color: var(--fg-3);
  }

  .fecha__dia {
    block-size: 32px;
    padding: 0;
    border-radius: var(--r-xs);
    font-family: var(--f-mono);
    font-size: 12px;
    text-align: center;
    color: var(--fg);
    cursor: pointer;
    transition: background var(--fast) var(--ease);

    &:empty {
      cursor: default;
    }

    &:not(:empty):hover {
      background: var(--sunken);
    }

    /* El anillo va por dentro: la capa recorta lo que sale de ella */
    &:focus-visible {
      outline-offset: -2px;
    }

    &[aria-current="date"] {
      box-shadow: inset 0 0 0 1px var(--line-control);
      font-weight: 700;
    }

    &[aria-disabled="true"] {
      color: var(--fg-4);
      text-decoration: line-through;
      cursor: not-allowed;

      &:hover {
        background: transparent;
      }
    }

    &[data-rango="dentro"] {
      border-radius: 0;
      background: var(--acc-soft);
      color: var(--acc);
    }

    &[aria-selected="true"] {
      background: var(--acc);
      color: var(--acc-fg);
      font-weight: 700;

      /* El anillo va por dentro y en oscuro es del color de --acc: sobre el día elegido se
         dibuja en --acc-fg, que contrasta con --acc en los dos temas */
      &:focus-visible {
        outline-color: var(--acc-fg);
      }
    }
  }

  /* El tramo del periodo se ve continuo: los extremos solo redondean hacia fuera */
  .fecha--rango .fecha__dia[aria-selected="true"][data-rango="inicio"] {
    border-start-end-radius: 0;
    border-end-end-radius: 0;
  }

  .fecha--rango .fecha__dia[aria-selected="true"][data-rango="fin"] {
    border-start-start-radius: 0;
    border-end-start-radius: 0;
  }

  .fecha__pie {
    display: flex;
    justify-content: flex-end;
    padding-block-start: 11px;
    border-block-start: 1px solid var(--divider);
  }

  .fecha__hoy {
    padding: 6px 11px;
    border: 1px solid var(--line-2);
    border-radius: var(--r-sm);
    background: var(--surface);
    color: var(--fg);
    font-family: var(--f-ui);
    font-size: 12.5px;
    font-weight: 600;
    cursor: pointer;

    &:hover {
      background: var(--sunken);
    }

    &:disabled {
      color: var(--fg-4);
      cursor: not-allowed;
    }
  }

  /* ── Atajos del periodo ────────────────────────────────────────────────── */
  .fecha__atajos {
    display: flex;
    flex-direction: column;
    gap: 2px;
    inline-size: 168px;
    padding: 12px 8px;
    border-inline-end: 1px solid var(--divider);
    background: var(--sunken);
  }

  .fecha__atajos-titulo {
    padding: 2px 9px 6px;
    font-family: var(--f-mono);
    font-size: 9.5px;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--fg-3);
  }

  .fecha__atajo {
    padding: 7px 9px;
    border: none;
    border-radius: var(--r-xs);
    background: transparent;
    color: var(--fg-2);
    font-family: var(--f-ui);
    font-size: 12.5px;
    text-align: start;
    cursor: pointer;

    &:hover {
      background: var(--surface);
      color: var(--fg);
    }

    &:focus-visible {
      outline-offset: -2px;
    }

    &[data-atajo-activo="si"] {
      background: var(--surface);
      color: var(--acc);
      font-weight: 700;
    }
  }

  /* Sin espacio para dos columnas: los atajos pasan arriba, en una fila que se desplaza */
  @media (max-width: 520px) {
    .fecha__panel {
      flex-direction: column;
    }

    .fecha__atajos {
      flex-direction: row;
      align-items: center;
      inline-size: auto;
      padding: 8px;
      border-inline-end: none;
      border-block-end: 1px solid var(--divider);
      overflow-x: auto;
    }

    .fecha__atajos-titulo {
      display: none;
    }

    .fecha__atajo {
      flex: none;
    }
  }

  @media (pointer: coarse) {
    .fecha__boton {
      inline-size: 40px;
      block-size: 40px;
      inset-inline-end: 2px;
    }

    .fecha__dia {
      block-size: 40px;
    }

    .fecha__navegar {
      inline-size: 40px;
      block-size: 40px;
    }
  }
}

/*!inventario
{
  "componente": "Códigos de recuperación",
  "proposito": "Lista de los códigos de recuperación del segundo factor, que se muestran una sola vez, con las acciones de copiarlos y descargarlos. Cada código sirve una vez; los ya usados se muestran tachados. codigos-recuperacion.js copia y descarga: el servidor solo rinde la lista.",
  "raiz": "codigos-recuperacion",
  "variantes": {},
  "estados": {
    "[data-usado='si']": "En codigos-recuperacion__codigo: código ya usado, tachado y atenuado",
    "[data-copiado='si']": "En el botón de copiar: recién copiado; la etiqueta dice «Copiados» dos segundos"
  },
  "modificadores": {},
  "estructura": [
    { "clase": "codigos-recuperacion__lista", "elemento": "ol", "papel": "Los códigos en rejilla de dos columnas (una en angosto)", "opcional": false },
    { "clase": "codigos-recuperacion__codigo", "elemento": "li", "papel": "Un código en mono, con espaciado", "opcional": false },
    { "clase": "codigos-recuperacion__acciones", "elemento": "div", "papel": "Copiar (data-accion='copiar-codigos') y descargar (data-accion='descargar-codigos'), botones del sistema", "opcional": false },
    { "clase": "codigos-recuperacion__nota", "elemento": "p", "papel": "Qué son, que sirven una vez y que no se volverán a mostrar", "opcional": true }
  ],
  "internas": [],
  "contexto": ["icono", "boton"],
  "ejemplo": [
    "<section class='codigos-recuperacion' data-componente='codigos-recuperacion' data-archivo='codigos-de-recuperacion.txt' data-titulo='Códigos de recuperación del panel' aria-label='Códigos de recuperación'>",
    "  <ol class='codigos-recuperacion__lista'>",
    "    <li class='codigos-recuperacion__codigo'>7K2M-94QX</li>",
    "    <li class='codigos-recuperacion__codigo'>PD3V-8HNT</li>",
    "    <li class='codigos-recuperacion__codigo' data-usado='si'>W6RC-2YJB</li>",
    "  </ol>",
    "  <div class='codigos-recuperacion__acciones'>",
    "    <button type='button' class='boton' data-accion='copiar-codigos'><svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-copy' /></svg> <span data-etiqueta>Copiar</span></button>",
    "    <button type='button' class='boton' data-accion='descargar-codigos'><svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-download' /></svg> Descargar</button>",
    "  </div>",
    "  <p class='codigos-recuperacion__nota'>Cada código sirve una sola vez. Guárdelos en un lugar seguro: no se volverán a mostrar.</p>",
    "</section>"
  ]
}
*/
/* Códigos de recuperación — se muestran UNA vez, al activar el segundo factor o al
   regenerarlos. La lista va en un bloque hundido, en mono para que se lean y se
   comparen carácter por carácter; las acciones son botones del sistema. */

@layer componentes {
  .codigos-recuperacion {
    display: flex;
    flex-direction: column;
    gap: 12px;
    padding: 14px;
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    background: var(--sunken);
  }

  .codigos-recuperacion__lista {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(128px, 1fr));
    gap: 6px;
    padding: 0;
    list-style: none;
  }

  .codigos-recuperacion__codigo {
    padding: 7px 10px;
    border: 1px solid var(--line);
    border-radius: var(--r-xs);
    background: var(--surface);
    color: var(--fg);
    font-family: var(--f-mono);
    font-size: 13px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-align: center;
    user-select: all;
  }

  .codigos-recuperacion__codigo[data-usado="si"] {
    background: transparent;
    color: var(--fg-4);
    font-weight: 400;
    text-decoration: line-through;
  }

  .codigos-recuperacion__acciones {
    display: flex;
    flex-wrap: wrap;
    gap: 9px;
  }

  .codigos-recuperacion__nota {
    font-size: 11.5px;
    line-height: 1.5;
    color: var(--fg-3);
  }
}

/*!inventario
{
  "componente": "Acordeón",
  "proposito": "Lista de preguntas (o secciones) que se abren una por una para leer su respuesta: preguntas frecuentes, instrucciones del formulario, detalles que no todos necesitan. Usa details/summary nativos: teclado y lector de pantalla funcionan sin JS.",
  "raiz": "acordeon",
  "variantes": {
    "acordeon--tarjetas": "Cada elemento es una tarjeta separada (fondo surface, borde y radio de 12px) en lugar de filas divididas por líneas"
  },
  "estados": {
    "[open]": "En el details: abierto; el chevron gira 180° y se ve la respuesta"
  },
  "modificadores": {},
  "estructura": [
    { "clase": "acordeon__item", "elemento": "details", "papel": "Una pregunta con su respuesta. Con name='…' igual en todos, abrir uno cierra los demás (comportamiento nativo)", "opcional": false },
    { "clase": "acordeon__pregunta", "elemento": "summary", "papel": "La pregunta, a todo el ancho, con el chevron a la derecha; 48px mínimos de toque", "opcional": false },
    { "clase": "acordeon__chevron", "elemento": "svg.icono", "papel": "i-chevron-down que gira al abrir", "opcional": false },
    { "clase": "acordeon__respuesta", "elemento": "div", "papel": "La respuesta: párrafos, listas o enlaces", "opcional": false }
  ],
  "internas": [],
  "contexto": ["icono"],
  "ejemplo": [
    "<div class='acordeon'>",
    "  <details class='acordeon__item' name='preguntas'>",
    "    <summary class='acordeon__pregunta'>¿Cómo funciona la consulta en línea?",
    "      <svg class='icono acordeon__chevron' aria-hidden='true'><use href='/images/iconos.svg#i-chevron-down' /></svg></summary>",
    "    <div class='acordeon__respuesta'><p>Nos vemos por videollamada; te envío el plan por correo.</p></div>",
    "  </details>",
    "</div>"
  ]
}
*/
/* Acordeón — las preguntas frecuentes de la landing, llevadas al sistema. details/summary
   nativos: sin JS, con teclado (Entrar/Espacio) y anunciados como expandibles. El grupo
   exclusivo (abrir uno cierra los demás) lo da el atributo name del details. */

@layer componentes {
  .acordeon {
    display: flex;
    flex-direction: column;
    border-block-start: 1px solid var(--line);
  }

  .acordeon__item {
    border-block-end: 1px solid var(--line);
  }

  .acordeon__pregunta {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-4);
    min-block-size: 48px;
    padding-block: var(--sp-3);
    font-family: var(--f-ui);
    font-size: 16px;
    font-weight: 600;
    line-height: 1.4;
    color: var(--fg);
    cursor: pointer;
    list-style: none;

    &::-webkit-details-marker {
      display: none;
    }

    &:hover {
      color: var(--acc);
    }
  }

  .acordeon__chevron {
    flex: none;
    inline-size: 18px;
    block-size: 18px;
    color: var(--fg-3);
    transition: transform var(--med) var(--ease);
  }

  .acordeon__item[open] .acordeon__chevron {
    transform: rotate(180deg);
  }

  .acordeon__respuesta {
    padding-block-end: var(--sp-4);
    padding-inline-end: var(--sp-6);
    font-size: 15px;
    line-height: 1.6;
    color: var(--fg-2);

    & > :first-child {
      margin-block-start: 0;
    }

    & > :last-child {
      margin-block-end: 0;
    }
  }

  /* Tarjetas separadas en lugar de filas */
  .acordeon--tarjetas {
    gap: var(--sp-3);
    border-block-start: none;

    & .acordeon__item {
      border: 1px solid var(--line);
      border-radius: var(--r-md);
      background: var(--surface);
      padding-inline: var(--sp-5);
    }
  }

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

/*!inventario
{
  "componente": "Pestañas laterales",
  "proposito": "Una pantalla dividida en secciones que se ven de una en una: índice pegajoso a la izquierda (icono, título y subtítulo por sección) y el panel de la sección activa a la derecha. La sección activa vive en la URL (?seccion=) y la resuelve el servidor; cada pestaña es un disparador htmx. Bajo 900px el índice cae arriba como fila.",
  "raiz": "pestanas",
  "variantes": {},
  "estados": {
    "[aria-current='true']": "La pestaña activa (en pestanas__pestana)"
  },
  "modificadores": {},
  "estructura": [
    { "clase": "pestanas__indice", "elemento": "nav", "papel": "El índice: columna pegajosa de pestañas (role='tablist')", "opcional": false },
    { "clase": "pestanas__pestana", "elemento": "a | button", "papel": "Una pestaña (role='tab'): icono + textos; disparador htmx con hx-push-url", "opcional": false },
    { "clase": "pestanas__icono", "elemento": "span > svg.icono", "papel": "Círculo con el icono de la sección; lleno en la activa", "opcional": false },
    { "clase": "pestanas__textos", "elemento": "span", "papel": "Columna título + subtítulo", "opcional": false },
    { "clase": "pestanas__titulo", "elemento": "span", "papel": "Nombre de la sección", "opcional": false },
    { "clase": "pestanas__sub", "elemento": "span", "papel": "Qué contiene, en una frase (se oculta en angosto)", "opcional": true },
    { "clase": "pestanas__panel", "elemento": "div", "papel": "La sección activa (role='tabpanel'); su contenido lo define la pantalla", "opcional": false }
  ],
  "internas": [],
  "contexto": ["icono"],
  "ejemplo": [
    "<div class='pestanas' id='cuerpo-x'>",
    "  <nav class='pestanas__indice' role='tablist' aria-label='Secciones'>",
    "    <a class='pestanas__pestana' role='tab' aria-current='true' href='?seccion=general' hx-get='?seccion=general' hx-target='#cuerpo-x' hx-swap='outerHTML' hx-push-url='true'>",
    "      <span class='pestanas__icono'><svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-settings' /></svg></span>",
    "      <span class='pestanas__textos'><span class='pestanas__titulo'>General</span><span class='pestanas__sub'>Nombre y ambiente</span></span>",
    "    </a>",
    "  </nav>",
    "  <div class='pestanas__panel' role='tabpanel'>…la sección…</div>",
    "</div>"
  ]
}
*/
/* Pestañas laterales — índice pegajoso + panel de la sección activa. El estado es
   de SERVIDOR (AD-10): la pestaña activa vive en la URL y el fragmento completo
   (índice + panel) se intercambia con htmx, por eso el componente no trae JS. */

@layer componentes {
  .pestanas {
    display: grid;
    grid-template-columns: 240px minmax(0, 1fr);
    gap: var(--sp-5);
    align-items: start;
  }

  .pestanas__indice {
    position: sticky;
    inset-block-start: var(--sp-4);
    display: flex;
    flex-direction: column;
    gap: 2px;
  }

  .pestanas__pestana {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 10px;
    border: none;
    border-radius: var(--r-sm);
    background: transparent;
    color: var(--fg);
    font: inherit;
    text-align: start;
    text-decoration: none;
    cursor: pointer;
    transition: background var(--fast) var(--ease);
  }

  .pestanas__pestana:hover {
    background: var(--sunken);
    color: var(--fg);
  }

  .pestanas__pestana[aria-current="true"] {
    background: var(--acc-soft);
  }

  .pestanas__icono {
    flex: none;
    inline-size: 34px;
    block-size: 34px;
    display: grid;
    place-items: center;
    border-radius: 999px;
    background: var(--acc-soft);
    color: var(--acc);
  }

  .pestanas__icono .icono {
    inline-size: 17px;
    block-size: 17px;
  }

  .pestanas__pestana[aria-current="true"] .pestanas__icono {
    background: var(--acc);
    color: var(--acc-fg);
  }

  .pestanas__textos {
    display: flex;
    flex-direction: column;
    min-inline-size: 0;
  }

  .pestanas__titulo {
    font-size: 13px;
    font-weight: 600;
    line-height: 1.25;
  }

  .pestanas__pestana[aria-current="true"] .pestanas__titulo {
    color: var(--acc);
  }

  .pestanas__sub {
    font-size: 11.5px;
    color: var(--fg-3);
  }

  .pestanas__panel {
    display: flex;
    flex-direction: column;
    gap: var(--sp-4);
    min-inline-size: 0;
  }

  /* Angosto: el índice pasa arriba como fila de pestañas */
  @media (max-width: 900px) {
    .pestanas {
      grid-template-columns: minmax(0, 1fr);
    }

    .pestanas__indice {
      position: static;
      flex-direction: row;
      flex-wrap: wrap;
      gap: 6px;
    }

    .pestanas__sub {
      display: none;
    }
  }
}

/*!inventario
{
  "componente": "Cajón lateral",
  "proposito": "Panel lateral que acompaña sin perder el contexto (NO es un modal). Encabezado fijo, cuerpo desplazable, pie fijo — la condición de cierre siempre visible. Cierra con velo, botón y Escape (cajon.js).",
  "raiz": "cajon",
  "variantes": {},
  "estados": {
    "[data-abierto='si']": "Abierto (lo pone cajon.js); 'no' = cerrado. La raíz lleva data-componente='cajon'"
  },
  "modificadores": {
    "cajon--ancho": "Panel de 720px en lugar de 480 (un detalle con tabla o dos columnas)"
  },
  "estructura": [
    { "clase": "cajon__velo", "elemento": "button", "papel": "Velo que cierra al clic; tabindex='-1' y data-accion='cerrar-cajon'", "opcional": false },
    { "clase": "cajon__panel", "elemento": "div", "papel": "El panel a 480px (720 con cajon--ancho); pantalla completa bajo 1104px de ventana. Otro ancho se fija con --cajon-ancho en el CSS de la app, nunca en un atributo style", "opcional": false },
    { "clase": "cajon__encabezado", "elemento": "header", "papel": "Fijo arriba, sobre la marca (--marca) con texto blanco: identidad + botón de cierre", "opcional": false },
    { "clase": "cajon__superior", "elemento": "div", "papel": "Primera fila del encabezado: chip y/o kicker a la izquierda, cierre a la derecha", "opcional": false },
    { "clase": "cajon__chip", "elemento": "span", "papel": "Chip mono translúcido (folio, clave o estado) en la fila superior", "opcional": true },
    { "clase": "cajon__kicker", "elemento": "span", "papel": "Contexto en mono mayúsculas (módulo o sección) en la fila superior", "opcional": true },
    { "clase": "cajon__cerrar", "elemento": "button", "papel": "Botón de cierre de 28px al extremo derecho; data-accion='cerrar-cajon' con icono i-x", "opcional": false },
    { "clase": "cajon__titulo", "elemento": "h2", "papel": "Título del cajón, 18px/700 en blanco; lleva el id que referencia aria-labelledby", "opcional": false },
    { "clase": "cajon__sub", "elemento": "p", "papel": "Bajada de una o dos líneas bajo el título (--on-marca-2)", "opcional": true },
    { "clase": "cajon__forma", "elemento": "form", "papel": "Forma que envuelve cuerpo y pie sin romper la columna del panel (display: contents)", "opcional": true },
    { "clase": "cajon__cuerpo", "elemento": "div", "papel": "Lo único desplazable; relleno 20px sobre --bg, columna con separación 20px", "opcional": false },
    { "clase": "cajon__pie", "elemento": "footer", "papel": "Fijo abajo: condición viva + control condicionado", "opcional": false },
    { "clase": "cajon__pista", "elemento": "p", "papel": "Texto breve al pie (11.5px, --fg-3) que ocupa el espacio a la izquierda de las acciones", "opcional": true },
    { "clase": "cajon__acciones", "elemento": "div", "papel": "Fila de acciones del pie alineada a la derecha: Cancelar + acción primaria", "opcional": true }
  ],
  "internas": [],
  "ejemplo": [
    "<div class='cajon' data-componente='cajon' data-abierto='no' role='dialog' aria-modal='true' aria-labelledby='titulo-x'>",
    "  <button class='cajon__velo' data-accion='cerrar-cajon' tabindex='-1' aria-label='Cerrar'></button>",
    "  <div class='cajon__panel'>",
    "    <header class='cajon__encabezado'>",
    "      <div class='cajon__superior'>",
    "        <span class='cajon__kicker'>Catálogos</span>",
    "        <button type='button' class='cajon__cerrar' data-accion='cerrar-cajon' aria-label='Cerrar'>(i-x)</button>",
    "      </div>",
    "      <h2 class='cajon__titulo' id='titulo-x'>Nuevo almacén</h2>",
    "      <p class='cajon__sub'>Una bajada de contexto.</p>",
    "    </header>",
    "    <form class='cajon__forma' method='post'>",
    "      <div class='cajon__cuerpo'>…campos…</div>",
    "      <footer class='cajon__pie'>",
    "        <div class='cajon__acciones'>",
    "          <p class='cajon__pista'>Se guarda al confirmar.</p>",
    "          <button type='button' class='boton' data-accion='cerrar-cajon'>Cancelar</button>",
    "          <button type='submit' class='boton boton--primario'>Guardar</button>",
    "        </div>",
    "      </footer>",
    "    </form>",
    "  </div>",
    "</div>",
    "<button type='button' class='boton' data-accion='abrir-cajon' data-cajon='#mi-cajon'>Abrir</button>"
  ]
}
*/
/* Cajón lateral — contra «Modales, paneles y avisos» (sección 24) del sistema
   v2.10.0: el panel lateral acompaña sin perder el contexto (el modal interrumpe;
   esto NO es un modal). [CORE-05]

   Anclaje del panel de elemento de trabajo (documento de dominio v1.5.0): vive en
   capa-flotante a 480 px y pasa a PANTALLA COMPLETA cuando el contexto detrás baja
   de 320 px — 480 + canal 24 + 320 = 824 px de contenido ⇒ ventana 1 104 px. El
   umbral va derivado con su aritmética, no inventado.

   La estructura interna ES la resolución de BD-007: encabezado fijo arriba, cuerpo
   desplazable en medio y pie fijo abajo. La fijeza del pie garantiza que la
   condición de cierre se vea sin importar cuánto haya crecido la historia — el
   cuerpo abre con los requisitos y cierra con la historia.

   Uso:
     <div class="cajon" data-componente="cajon" data-abierto="no"
          role="dialog" aria-modal="true" aria-labelledby="…">
       <button class="cajon__velo" data-accion="cerrar-cajon" tabindex="-1"></button>
       <div class="cajon__panel">
         <header class="cajon__encabezado">            ← marca, texto blanco
           <div class="cajon__superior">[chip] [kicker] <button class="cajon__cerrar">(i-x)</button></div>
           <h2 class="cajon__titulo">…</h2>
           <p class="cajon__sub">…</p>
         </header>
         <form class="cajon__forma">                    ← opcional, display: contents
           <div class="cajon__cuerpo">…requisitos primero, historia al final…</div>
           <footer class="cajon__pie">
             <div class="cajon__acciones">[pista] Cancelar · Primaria</div>
           </footer>
         </form>
       </div>
     </div>
   Abre cualquier control con data-accion="abrir-cajon" y data-cajon="#id".
   Cierra: velo, botón y Escape (cajon.js). El contenido lo sirve el servidor. */

@layer componentes {
  .cajon {
    position: fixed;
    inset: 0;
    z-index: 80;
    display: flex;
    justify-content: flex-end;
  }

  .cajon[data-abierto="no"] {
    visibility: hidden;
    pointer-events: none;
  }

  .cajon__velo {
    position: absolute;
    inset: 0;
    border: none;
    padding: 0;
    background: var(--scrim);
    cursor: default;
    opacity: 0;
    transition: opacity var(--med) var(--ease);
  }

  .cajon[data-abierto="si"] .cajon__velo {
    opacity: 1;
  }

  /* Ancho mayor: el modificador fija la custom property que lee el panel. */
  .cajon--ancho {
    --cajon-ancho: 720px;
  }

  .cajon__panel {
    position: relative;
    inline-size: min(var(--cajon-ancho, 480px), 100%);
    block-size: 100%;
    background: var(--raised);
    box-shadow: var(--e4);
    display: flex;
    flex-direction: column;
    translate: 100% 0;
    transition: translate var(--med) var(--ease);
  }

  .cajon[data-abierto="si"] .cajon__panel {
    translate: 0 0;
  }

  /* Encabezado fijo sobre la marca (sección 24 del sistema): la identidad del cajón se
     lee sobre --marca con texto blanco; primero la fila de contexto + cierre,
     luego el título y su bajada. */
  .cajon__encabezado {
    flex: none;
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 16px 20px 15px;
    background: var(--marca);
    /* El anillo general (g-700) no contrasta con la marca: aquí el foco es blanco (8:1) */
    --ring: var(--on-marca);
    color: var(--on-marca);
  }

  .cajon__superior {
    display: flex;
    align-items: center;
    gap: 10px;
    min-block-size: 28px;
  }

  .cajon__chip {
    font-family: var(--f-mono);
    font-weight: 500;
    font-size: 12px;
    line-height: 1.3;
    padding: 3px 10px;
    border-radius: 7px;
    background: rgba(255, 255, 255, 0.12);
    border: 1px solid rgba(255, 255, 255, 0.22);
    white-space: nowrap;
  }

  .cajon__kicker {
    font-family: var(--f-mono);
    font-size: 10px;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--on-marca-2);
    white-space: nowrap;
  }

  .cajon__cerrar {
    margin-inline-start: auto;
    flex: none;
    inline-size: 28px;
    block-size: 28px;
    display: grid;
    place-items: center;
    padding: 0;
    border: none;
    border-radius: var(--r-xs);
    background: transparent;
    color: var(--on-marca-3);
    cursor: pointer;
    transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
  }

  .cajon__cerrar:hover,
  .cajon__cerrar:focus-visible {
    background: rgba(255, 255, 255, 0.14);
    color: var(--on-marca);
  }

  .cajon__titulo {
    margin: 0;
    font-family: var(--f-ui);
    font-size: 18px;
    font-weight: 700;
    letter-spacing: -0.01em;
    line-height: 1.25;
    color: var(--on-marca);
    overflow-wrap: anywhere;
  }

  .cajon__sub {
    margin: 0;
    font-size: 12.5px;
    line-height: 1.5;
    color: var(--on-marca-2);
  }

  /* La forma envuelve cuerpo y pie sin romper la columna del panel */
  .cajon__forma {
    display: contents;
  }

  /* Cuerpo desplazable: abre con los requisitos, cierra con la historia (BD-007) */
  .cajon__cuerpo {
    flex: 1;
    overflow-y: auto;
    min-block-size: 0;
    padding: 20px;
    display: flex;
    flex-direction: column;
    gap: 20px;
    background: var(--bg);
  }

  /* Pie fijo: la condición viva pegada al control, visible sin importar el scroll */
  .cajon__pie {
    flex: none;
    padding: 14px 20px;
    border-block-start: 1px solid var(--line);
    background: var(--surface);
    display: flex;
    flex-direction: column;
    gap: 9px;
  }

  /* Acciones del pie: la pista ocupa la izquierda, los controles van a la derecha */
  .cajon__acciones {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    flex-wrap: wrap;
    gap: 9px;
  }

  .cajon__pista {
    margin: 0;
    flex: 1;
    min-inline-size: 12ch;
    font-size: 11.5px;
    line-height: 1.35;
    color: var(--fg-3);
  }

  /* Pantalla completa: 480 + 24 + 320 = 824 px de contenido ⇒ ventana 1104 px */
  @media (max-width: 1104px) {
    .cajon__panel {
      inline-size: 100%;
    }
  }

  @media (prefers-reduced-motion: reduce) {
    .cajon__velo,
    .cajon__panel,
    .cajon__cerrar {
      transition: none;
    }
  }
}

/*!inventario
{
  "componente": "Chip de estatus",
  "proposito": "El estado de una tarea, con texto E icono siempre (nunca solo color). Los 5 estados del dominio, cada uno con su tono y su icono fijos.",
  "raiz": "chip-estatus",
  "variantes": {
    "chip-estatus--pendiente": "Pendiente — i-circle-dashed, tono neutro",
    "chip-estatus--en-curso": "En curso — i-play, tono acento",
    "chip-estatus--bloqueada": "Bloqueada — i-ban, tono danger",
    "chip-estatus--por-validar": "Por validar — i-clipboard-check, tono info",
    "chip-estatus--cerrada": "Cerrada — i-check-check, tono success"
  },
  "estados": {},
  "modificadores": {},
  "estructura": [
    { "clase": "chip-estatus__icono", "elemento": "svg.icono", "papel": "El icono del estado (12px, trazo propio) — NO es opcional", "opcional": false }
  ],
  "internas": [],
  "ejemplo": [
    "<span class='chip-estatus chip-estatus--en-curso'>",
    "  <svg class='icono chip-estatus__icono' aria-hidden='true'><use href='/images/iconos.svg#i-play' /></svg>",
    "  En curso",
    "</span>"
  ]
}
*/
/* Chip de estatus — contra «Botones y estatus» (sección 06) del sistema v2.10.0,
   con los tonos e iconos de los 5 estados de tarea que declara el documento de
   dominio v1.5.0. [CORE-05]

   REGLA DURA: el estado se comunica con texto E icono, nunca solo color — y aquí
   pesa doble porque el chip se repite en listas largas. El icono NO es opcional.

   Estado    | icono             | tono
   Pendiente | i-circle-dashed   | neutro (sunken/line/fg-2)
   En curso  | i-play            | acento (acc-soft/acc-soft-2/acc)
   Bloqueada | i-ban             | danger
   Por validar | i-clipboard-check | info
   Cerrada   | i-check-check     | success

   Uso:
     <span class="chip-estatus chip-estatus--en-curso">
       <svg class="icono chip-estatus__icono"><use href="/images/iconos.svg#i-play" /></svg>
       En curso
     </span> */

@layer componentes {
  .chip-estatus {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 3px 9px;
    border-radius: 999px;
    border: 1px solid var(--line);
    background: var(--sunken);
    color: var(--fg-2);
    font-size: 11.5px;
    font-weight: 600;
    line-height: 1.4;
    white-space: nowrap;
  }

  .chip-estatus__icono {
    inline-size: 12px;
    block-size: 12px;
    flex: none;
    /* Tamaño propio ⇒ trazo propio (RD-019): 1.75 × 24 / 12 */
    stroke-width: 3.5px;
  }

  .chip-estatus--pendiente {
    background: var(--sunken);
    border-color: var(--line);
    color: var(--fg-2);
  }

  .chip-estatus--en-curso {
    background: var(--acc-soft);
    border-color: var(--acc-soft-2);
    color: var(--acc);
  }

  .chip-estatus--bloqueada {
    background: var(--danger-soft);
    border-color: var(--danger-bd);
    color: var(--danger);
  }

  .chip-estatus--por-validar {
    background: var(--info-soft);
    border-color: var(--info-bd);
    color: var(--info);
  }

  .chip-estatus--cerrada {
    background: var(--success-soft);
    border-color: var(--success-bd);
    color: var(--success);
  }
}

/*!inventario
{
  "componente": "Botón",
  "proposito": "La acción. Una primaria por vista; el inactivo lleva su razón en title (§13), nunca desaparece.",
  "raiz": "boton",
  "variantes": {
    "boton--primario": "LA acción primaria de la vista — acento sólido, máximo una",
    "boton--conversion": "La acción de conversión en coral (agendar, escribir por WhatsApp, enviar el formulario de un paciente). Máximo una por vista y nunca junto a una primaria",
    "boton--icono": "Solo icono (controles del encabezado y de la barra), 34px, sin borde",
    "boton--bloque": "A todo el ancho de su contenedor, contenido centrado (acciones apiladas de las pantallas de acceso, pies de cajón angostos)",
    "boton--fantasma": "Acción terciaria sin borde ni fondo, en fg-3 («Ahora no», «Cambiarla después»); nunca la principal"
  },
  "estados": {
    "[disabled]": "Inactivo con su razón en title — qué no se puede y a quién pedirlo, nunca el nombre técnico",
    "htmx-request": "Petición en vuelo (la clase la pone htmx en el disparador; carga.js además lo deshabilita): atenuado, cursor progress"
  },
  "modificadores": {
    "boton--grande": "48px de alto y texto a 16px: formularios públicos y la landing, donde quien toca es un paciente y no el personal",
    "boton--pildora": "Extremos redondos (r-pildora): el botón de la cara pública (landing y formulario de pacientes). El panel usa el radio de 12px"
  },
  "estructura": [],
  "internas": [],
  "ejemplo": [
    "<button type='button' class='boton boton--primario'>Guardar</button>",
    "<a class='boton boton--conversion boton--grande boton--pildora' href='https://wa.me/…'>Agendar mi consulta</a>",
    "<button type='button' class='boton' disabled",
    "        title='Tu cuenta no permite …. Si lo necesitas, pídelo a quien administra el sistema.'>Reabrir</button>",
    "<button type='submit' class='boton boton--primario boton--bloque'>Iniciar sesión</button>",
    "<button type='button' class='boton boton--fantasma boton--bloque'>Ahora no</button>"
  ]
}
*/
/* Botón — estados de la anatomía del DS: reposo, hover, activo, foco, deshabilitado.
   Una acción primaria por vista. Área de toque mínima de 44 px en táctil. */

@layer componentes {
  .boton {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    padding: var(--ctl-y) 16px;
    border: 1px solid var(--line-2);
    border-radius: var(--r-md);
    background: var(--surface);
    color: var(--fg);
    font-family: var(--f-ui);
    font-size: 13px;
    font-weight: 600;
    line-height: 1.2;
    text-decoration: none;
    cursor: pointer;
    transition: background var(--fast) var(--ease), box-shadow var(--fast) var(--ease),
      transform var(--med) var(--ease);

    &:hover {
      background: var(--sunken);
    }

    /* Presionado: se hunde un poco (sin rebote) */
    &:active:not(:disabled) {
      transform: scale(.98);
    }

    &:disabled {
      background: var(--sunken);
      color: var(--fg-4);
      border-color: var(--line);
      cursor: not-allowed;
    }

    /* Petición en vuelo: htmx pone la clase en el disparador; carga.js lo
       deshabilita mientras tanto (sin doble envío) */
    &.htmx-request {
      opacity: 0.7;
      cursor: progress;
    }
  }

  .boton--primario {
    border-color: transparent;
    background: var(--acc);
    color: var(--acc-fg);
    box-shadow: var(--e1);

    &:hover {
      background: var(--acc-h);
      box-shadow: var(--e2);
    }

    &:active {
      background: var(--acc-p);
      box-shadow: none;
    }
  }

  /* Conversión: el coral de la marca, con texto oscuro en los dos temas */
  .boton--conversion {
    border-color: transparent;
    background: var(--cta);
    color: var(--cta-fg);
    box-shadow: var(--e1);

    &:hover {
      background: var(--cta-h);
      box-shadow: var(--e2);
    }

    &:active {
      background: var(--cta-p);
      box-shadow: none;
    }
  }

  /* Grande: público (formularios de pacientes, landing) */
  .boton--grande {
    min-block-size: 48px;
    padding-inline: 22px;
    font-size: 16px;
  }

  /* Píldora: la cara pública (landing y formulario de pacientes) */
  .boton--pildora {
    border-radius: var(--r-pildora);
  }

  /* A todo el ancho: acciones apiladas (pantallas de acceso) */
  .boton--bloque {
    display: flex;
    inline-size: 100%;
    justify-content: center;
  }

  /* Terciaria: sin borde ni fondo; nunca la acción principal */
  .boton--fantasma {
    border-color: transparent;
    background: transparent;
    color: var(--fg-3);
    box-shadow: none;

    &:hover {
      background: transparent;
      color: var(--fg);
    }
  }

  /* Botón de solo icono (controles del encabezado y de la barra) */
  .boton--icono {
    padding: 0;
    inline-size: 34px;
    block-size: 34px;
    justify-content: center;
    border-color: transparent;
    background: transparent;
    color: var(--fg-2);

    &:hover {
      background: var(--sunken);
      color: var(--fg);
    }
  }

  @media (pointer: coarse) {
    .boton {
      min-block-size: 44px;
    }

    .boton--icono {
      inline-size: 44px;
      block-size: 44px;
    }
  }
}

/*!inventario
{
  "componente": "Campo de formulario",
  "proposito": "Campo con etiqueta siempre visible, ayuda antes del error, y error que dice cómo corregir. La validación habla con la voz del sistema (campo.js suprime el globo nativo).",
  "raiz": "campo",
  "variantes": {},
  "estados": {
    "campo--con-error": "Borde danger en la entrada; campo.js lo pone/quita con el mensaje de data-error",
    "[disabled]": "Inactivo: fondo hundido, not-allowed; si es por permiso, la razón va en campo__ayuda (§13)"
  },
  "modificadores": {},
  "estructura": [
    { "clase": "campo__etiqueta", "elemento": "label", "papel": "Siempre visible, sobre el campo", "opcional": false },
    { "clase": "campo__entrada", "elemento": "input | select | textarea", "papel": "La entrada; data-error lleva el mensaje de corrección", "opcional": false },
    { "clase": "campo__ayuda", "elemento": "p", "papel": "Texto de ayuda descriptivo (antes del error)", "opcional": true },
    { "clase": "campo__error", "elemento": "p", "papel": "Mensaje de error con icono i-circle-alert; campo.js lo crea si falta", "opcional": true },
    { "clase": "campo__error-icono", "elemento": "svg.icono", "papel": "Icono del error (13px, trazo propio)", "opcional": true },
    { "clase": "campo__selector", "elemento": "span", "papel": "Envoltura del select nativo: chevron del sprite, flecha nativa oculta", "opcional": true },
    { "clase": "campo__selector-icono", "elemento": "svg.icono", "papel": "Chevron i-chevron-down posicionado sobre el select", "opcional": true },
    { "clase": "campo__vacio", "elemento": "p", "papel": "Estado vacío del selector: explica POR QUÉ no hay opciones y qué hacer", "opcional": true },
    { "clase": "campo__opcion", "elemento": "label", "papel": "Opción con control a la izquierda (casilla, radio o interruptor) y texto a la derecha; toda la fila es clicable", "opcional": true },
    { "clase": "campo__casilla", "elemento": "input[type=checkbox]", "papel": "Casilla de 16px en color de acento (sección 07)", "opcional": true },
    { "clase": "campo__radio", "elemento": "input[type=radio]", "papel": "Radio de 16px en color de acento (opción única)", "opcional": true },
    { "clase": "campo__interruptor", "elemento": "input[type=checkbox]", "papel": "Interruptor 40×23 con pulgar de 17px (sección 07); para preferencias que aplican de inmediato o ajustes on/off", "opcional": true },
    { "clase": "campo__opcion-texto", "elemento": "span", "papel": "Título de la opción (y su nota)", "opcional": true },
    { "clase": "campo__opcion-nota", "elemento": "span", "papel": "Nota breve bajo el título de la opción (--fg-3)", "opcional": true },
    { "clase": "campo__con-unidad", "elemento": "span", "papel": "Envoltura de una entrada numérica con su unidad pegada a la derecha", "opcional": true },
    { "clase": "campo__unidad", "elemento": "span", "papel": "La unidad (días, %, segundos) al final de la entrada", "opcional": true },
    { "clase": "campo__con-icono", "elemento": "span", "papel": "Envoltura de una entrada con icono inicial y/o botón de revelar al final (usuario, contraseña)", "opcional": true },
    { "clase": "campo__icono", "elemento": "svg.icono", "papel": "Icono inicial dentro de la entrada (15px, fg-3), antes del input en el marcado", "opcional": true },
    { "clase": "campo__revelar", "elemento": "button", "papel": "Mostrar u ocultar la contraseña: campo.js alterna el type, aria-pressed, aria-label y el icono i-eye / i-eye-off", "opcional": true },
    { "clase": "campo__revelar-icono", "elemento": "svg.icono", "papel": "El icono del botón de revelar (15px)", "opcional": true }
  ],
  "internas": [],
  "ejemplo": [
    "<div class='campo'>",
    "  <label class='campo__etiqueta' for='nombre'>Proyecto nuevo</label>",
    "  <input class='campo__entrada' id='nombre' name='nombre' type='text' required",
    "         data-error='Escribe el nombre del paciente antes de presionar Entrar.' />",
    "  <p class='campo__ayuda'>Texto de ayuda descriptivo.</p>",
    "</div>",
    "<div class='campo'>",
    "  <label class='campo__etiqueta' for='tipo'>Tipo</label>",
    "  <span class='campo__selector'>",
    "    <select class='campo__entrada' id='tipo' name='tipo'>…</select>",
    "    <svg class='icono campo__selector-icono' aria-hidden='true'><use href='/images/iconos.svg#i-chevron-down' /></svg>",
    "  </span>",
    "</div>",
    "<label class='campo__opcion'>",
    "  <input class='campo__interruptor' type='checkbox' name='activo' value='true' checked />",
    "  <span class='campo__opcion-texto'>Exigir stock antes de contar",
    "    <span class='campo__opcion-nota'>Si está activado, no se puede escanear sin stock cargado.</span></span>",
    "</label>",
    "<div class='campo'>",
    "  <label class='campo__etiqueta' for='dias'>Vigencia</label>",
    "  <span class='campo__con-unidad'>",
    "    <input class='campo__entrada' id='dias' name='dias' type='number' min='1' max='365' />",
    "    <span class='campo__unidad'>días</span>",
    "  </span>",
    "</div>",
    "<div class='campo'>",
    "  <label class='campo__etiqueta' for='clave'>Contraseña</label>",
    "  <span class='campo__con-icono'>",
    "    <svg class='icono campo__icono' aria-hidden='true'><use href='/images/iconos.svg#i-lock' /></svg>",
    "    <input class='campo__entrada' id='clave' name='clave' type='password' autocomplete='current-password' required data-error='Escribe tu contraseña.' />",
    "    <button type='button' class='campo__revelar' aria-label='Mostrar contraseña' aria-pressed='false'><svg class='icono campo__revelar-icono' aria-hidden='true'><use href='/images/iconos.svg#i-eye' /></svg></button>",
    "  </span>",
    "</div>"
  ]
}
*/
/* Campo de formulario — anatomía "Formularios" del DS v2.13.0 [CORE-13].
   Etiqueta siempre visible sobre el campo. El texto de ayuda se escribe antes del
   error; el mensaje de error indica cómo corregirlo, nunca solo que algo está mal.

   MARCADO (contrato del componente):

     <div class="campo">
       <label class="campo__etiqueta" for="x">Etiqueta</label>
       <input class="campo__entrada" id="x" data-error="Cómo corregirlo.">
       <p class="campo__ayuda">Texto de ayuda descriptivo.</p>
     </div>

   Selector (select nativo — el chevron viene del sprite, el nativo se oculta):

     <span class="campo__selector">
       <select class="campo__entrada">…</select>
       <svg class="icono campo__selector-icono" aria-hidden="true">
         <use href="/images/iconos.svg#i-chevron-down" /></svg>
     </span>

   Estado vacío del selector: cuando NO hay opciones, el selector no se rinde mudo —
   en su lugar va un `.campo__vacio` que dice POR QUÉ no hay opciones y qué hacer.

   VALIDACIÓN: campo.js intercepta el evento `invalid` (suprime el globo nativo del
   navegador) y muestra `.campo__error` con el texto de `data-error` — el sistema
   habla con su propia voz. El error también puede rendirse desde el servidor.

   FOCO (RD-021): el anillo global de base.css se CONSERVA — este componente no
   suprime el contorno; solo suma el borde acentuado de la anatomía. */

@layer componentes {
  .campo {
    display: flex;
    flex-direction: column;
    gap: 6px;
  }

  .campo__etiqueta {
    font-size: 12.5px;
    font-weight: 700;
  }

  .campo__entrada {
    inline-size: 100%;
    padding: var(--ctl-y) var(--ctl-x);
    border: 1px solid var(--line-control);
    border-radius: var(--r-md);
    background: var(--surface);
    color: var(--fg);
    font-family: var(--f-ui);
    font-size: 13.5px;
    transition: border-color var(--fast) var(--ease);

    &:focus-visible {
      /* El anillo lo pone base.css; aquí solo el borde de la anatomía. */
      border-color: var(--acc);
    }

    &:disabled {
      background: var(--sunken);
      color: var(--fg-4);
      border-color: var(--line);
      cursor: not-allowed;
    }
  }

  textarea.campo__entrada {
    resize: vertical;
    line-height: 1.55;
  }

  .campo__ayuda {
    font-size: 11.5px;
    color: var(--fg-3);
  }

  /* Error: texto E icono, nunca solo color. El icono declara su trazo (RD-019). */
  .campo__error {
    display: flex;
    align-items: center;
    gap: 5px;
    font-size: 11.5px;
    color: var(--danger);

    &[hidden] {
      display: none;
    }
  }

  .campo__error-icono {
    inline-size: 13px;
    block-size: 13px;
    stroke-width: 3.231px; /* 1.75 × 24 / 13 */
  }

  .campo--con-error .campo__entrada {
    border-color: var(--danger);
  }

  /* Selector sobre <select> nativo: mismo campo, chevron del sprite. */
  .campo__selector {
    position: relative;
    display: block;

    .campo__entrada {
      appearance: none;
      padding-inline-end: 34px;
      cursor: pointer;

      &:disabled {
        cursor: not-allowed;
      }
    }
  }

  .campo__selector-icono {
    position: absolute;
    inset-block-start: 50%;
    inset-inline-end: 11px;
    translate: 0 -50%;
    inline-size: 15px;
    block-size: 15px;
    stroke-width: 2.8px; /* 1.75 × 24 / 15 */
    color: var(--fg-3);
    pointer-events: none;
  }

  /* Entrada con icono inicial y/o botón de revelar (usuario, contraseña): el
     borde sigue siendo el de la entrada; icono y botón se posicionan encima. */
  .campo__con-icono {
    position: relative;
    display: block;
  }

  .campo__icono {
    position: absolute;
    inset-block-start: 50%;
    inset-inline-start: 12px;
    translate: 0 -50%;
    inline-size: 15px;
    block-size: 15px;
    stroke-width: 2.8px; /* 1.75 × 24 / 15 */
    color: var(--fg-3);
    pointer-events: none;
  }

  .campo__con-icono:has(.campo__icono) .campo__entrada {
    padding-inline-start: 36px;
  }

  .campo__con-icono:has(.campo__revelar) .campo__entrada {
    padding-inline-end: 42px;
  }

  /* Mostrar/ocultar contraseña: campo.js alterna el type y el icono */
  .campo__revelar {
    position: absolute;
    inset-block-start: 50%;
    inset-inline-end: 6px;
    translate: 0 -50%;
    display: grid;
    place-items: center;
    inline-size: 30px;
    block-size: 30px;
    padding: 0;
    border: none;
    border-radius: var(--r-xs);
    background: transparent;
    color: var(--fg-3);
    cursor: pointer;
    transition: color var(--fast) var(--ease), background var(--fast) var(--ease);

    &:hover {
      background: var(--sunken);
      color: var(--fg);
    }
  }

  .campo__revelar-icono {
    inline-size: 15px;
    block-size: 15px;
    stroke-width: 2.8px; /* 1.75 × 24 / 15 */
  }

  /* Estado vacío del selector: explica por qué no hay opciones y qué hacer. */
  .campo__vacio {
    padding: var(--ctl-y) var(--ctl-x);
    border: 1px dashed var(--line-2);
    border-radius: var(--r-sm);
    background: var(--sunken);
    color: var(--fg-3);
    font-size: 12.5px;
    line-height: 1.5;
  }

  /* Opción: casilla, radio o interruptor a la izquierda y texto (con nota) a la
     derecha. Toda la fila es clicable (es un label). Sección 07 del sistema. */
  .campo__opcion {
    display: flex;
    align-items: flex-start;
    gap: 11px;
    font-size: 13px;
    cursor: pointer;
  }

  .campo__opcion-texto {
    display: flex;
    flex-direction: column;
    gap: 2px;
    font-weight: 600;
    line-height: 1.35;
  }

  .campo__opcion-nota {
    font-size: 12px;
    font-weight: 400;
    line-height: 1.45;
    color: var(--fg-3);
  }

  .campo__casilla,
  .campo__radio {
    flex: none;
    inline-size: 16px;
    block-size: 16px;
    margin: 2px 0 0;
    accent-color: var(--acc);
    cursor: pointer;
  }

  /* Interruptor: pastilla 40×23 con pulgar de 17px; encendido en acento. */
  .campo__interruptor {
    appearance: none;
    flex: none;
    position: relative;
    inline-size: 40px;
    block-size: 23px;
    margin: 0;
    border: none;
    border-radius: 999px;
    background: var(--line-control);
    cursor: pointer;
    transition: background var(--fast) var(--ease);

    &::after {
      content: "";
      position: absolute;
      inset-block-start: 3px;
      inset-inline-start: 3px;
      inline-size: 17px;
      block-size: 17px;
      border-radius: 999px;
      background: var(--raised);
      box-shadow: var(--e1);
      transition: translate var(--fast) var(--ease);
    }

    &:checked {
      background: var(--acc);
    }

    &:checked::after {
      translate: 17px 0;
    }

    &:disabled {
      cursor: not-allowed;
      opacity: 0.6;
    }
  }

  /* Entrada numérica con su unidad pegada (días, %, segundos). */
  .campo__con-unidad {
    display: flex;
    align-items: stretch;

    .campo__entrada {
      border-start-end-radius: 0;
      border-end-end-radius: 0;
    }
  }

  .campo__unidad {
    display: inline-flex;
    align-items: center;
    padding: 0 12px;
    border: 1px solid var(--line-control);
    border-inline-start: none;
    border-start-end-radius: var(--r-sm);
    border-end-end-radius: var(--r-sm);
    background: var(--sunken);
    color: var(--fg-2);
    font-size: 12.5px;
    white-space: nowrap;
  }

  @media (prefers-reduced-motion: reduce) {
    .campo__interruptor,
    .campo__interruptor::after {
      transition: none;
    }
  }
}

/*!inventario
{
  "componente": "Lista de acciones",
  "proposito": "Lista corta de elementos de la persona, cada uno con sus datos y una acción propia (revocar, cancelar, quitar), pensada para columnas angostas como la del layout acceso o un cajón: sesiones, dispositivos, delegaciones, invitaciones, llaves. No es una tabla: no ordena ni pagina; para colecciones largas o comparables, tabla. La acción destructiva se confirma con la alerta centrada (hx-confirm), así que no tiene comportamiento propio. Cada botón lleva un nombre accesible con el elemento al que afecta: varios «Revocar» seguidos deben distinguirse con lector de pantalla.",
  "raiz": "lista-acciones",
  "variantes": {},
  "estados": {},
  "modificadores": {},
  "estructura": [
    { "clase": "lista-acciones__elemento", "elemento": "li", "papel": "Un elemento: datos a la izquierda y acción a la derecha; bajo 14rem de ancho útil la acción baja a su propia línea", "opcional": false },
    { "clase": "lista-acciones__datos", "elemento": "div", "papel": "Título y detalles del elemento", "opcional": false },
    { "clase": "lista-acciones__titulo", "elemento": "p", "papel": "Lo que identifica al elemento (nombre, correo, dispositivo); parte palabras largas", "opcional": false },
    { "clase": "lista-acciones__detalle", "elemento": "p", "papel": "Un dato secundario por renglón (fechas, estado con ficha)", "opcional": true },
    { "clase": "lista-acciones__vacio", "elemento": "p", "papel": "Estado vacío obligatorio: dice qué no hay y, si aplica, cómo agregarlo. Va en lugar de la lista (la ul no se rinde vacía)", "opcional": true }
  ],
  "internas": [],
  "contexto": ["boton", "ficha", "alerta", "acceso", "cajon"],
  "ejemplo": [
    "<ul class='lista-acciones' aria-label='Dispositivos'>",
    "  <li class='lista-acciones__elemento'>",
    "    <div class='lista-acciones__datos'>",
    "      <p class='lista-acciones__titulo'>Teléfono de la oficina</p>",
    "      <p class='lista-acciones__detalle'>Último uso: 7 oct 2026, 14:05</p>",
    "    </div>",
    "    <form method='post' hx-boost='true' hx-confirm='Tendrá que iniciar sesión otra vez en ese dispositivo.'",
    "          data-alerta-titulo='¿Revocar este dispositivo?' data-alerta-confirmar='Revocar' data-alerta-tono='peligro'>",
    "      <button type='submit' class='boton' aria-label='Revocar Teléfono de la oficina'>Revocar</button>",
    "    </form>",
    "  </li>",
    "</ul>",
    "<p class='lista-acciones__vacio'>Aún no tiene dispositivos registrados.</p>"
  ]
}
*/
/* Lista de acciones — elementos propios de la persona con una acción cada uno, en columnas
   angostas. Solo tokens; los dos temas salen de ellos. El detalle va en fg-2: es texto que
   se lee. */

@layer componentes {
  .lista-acciones {
    display: grid;
    gap: var(--sp-3);
    padding: 0;
    list-style: none;
  }

  .lista-acciones__elemento {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-3);
    padding: var(--sp-3) var(--sp-4);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    background: var(--surface);
  }

  .lista-acciones__datos {
    display: grid;
    flex: 1 1 14rem;
    gap: var(--sp-1);
    min-inline-size: 0;
  }

  .lista-acciones__titulo {
    color: var(--fg);
    font-weight: 600;
    overflow-wrap: anywhere;
  }

  .lista-acciones__detalle {
    color: var(--fg-2);
    font-size: 12.5px;
  }

  /* Estado vacío: explica qué no hay, sobre un bloque punteado como el del selector */
  .lista-acciones__vacio {
    padding: var(--sp-4);
    border: 1px dashed var(--line-2);
    border-radius: var(--r-md);
    background: var(--sunken);
    color: var(--fg-2);
    font-size: 12.5px;
    line-height: 1.5;
    text-align: center;
  }
}

/*!inventario
{
  "componente": "Carga de archivos",
  "proposito": "Zona para soltar o elegir archivos, con el tipo y el tamaño permitidos dichos en texto. Al elegir muestra cada archivo (icono por tipo o miniatura, nombre, peso) con su botón de quitar, y rechaza antes de enviar lo que no cumple, con un mensaje en voz «tú». Envuelve un input type='file' real: el formulario (multipart, htmx) no cambia. Va dentro de un campo: la etiqueta, la ayuda y el error de «obligatorio» son los del campo. Distinto de carga, que es la espera. carga-archivo.js maneja la zona, la lista y la validación.",
  "raiz": "carga-archivo",
  "variantes": {
    "carga-archivo--imagen": "Muestra una miniatura de cada imagen en lugar del icono de tipo (fotografía, evidencia)"
  },
  "estados": {
    "[data-arrastre='si']": "Un archivo se arrastra encima de la zona (en la raíz)",
    "[data-con-archivos='si']": "Ya hay al menos un archivo elegido (en la raíz)"
  },
  "modificadores": {},
  "estructura": [
    { "clase": "carga-archivo__entrada", "elemento": "input[type='file']", "papel": "El control real, oculto a la vista pero enfocable (su foco se pinta en la zona): name, accept, multiple, required y data-error; aria-describedby a los requisitos. data-max-bytes y data-max-archivos van en la raíz", "opcional": false },
    { "clase": "carga-archivo__zona", "elemento": "div", "papel": "Área de soltar; un clic abre el selector del sistema", "opcional": false },
    { "clase": "carga-archivo__icono", "elemento": "span > svg.icono", "papel": "i-upload en círculo de acento", "opcional": false },
    { "clase": "carga-archivo__titulo", "elemento": "span", "papel": "«Arrastre el archivo aquí o elíjalo»", "opcional": false },
    { "clase": "carga-archivo__elegir", "elemento": "span", "papel": "La palabra «elíjalo», en color de enlace", "opcional": false },
    { "clase": "carga-archivo__requisitos", "elemento": "span", "papel": "Tipos y tamaño permitidos: «PDF, JPG o PNG · hasta 10 MB»", "opcional": false },
    { "clase": "carga-archivo__lista", "elemento": "ul", "papel": "Los archivos elegidos (los pinta carga-archivo.js)", "opcional": false },
    { "clase": "carga-archivo__archivo", "elemento": "li", "papel": "Un archivo: tipo o miniatura, datos y quitar", "opcional": false },
    { "clase": "carga-archivo__tipo", "elemento": "span > svg.icono", "papel": "Icono por tipo: i-file-text, i-file-spreadsheet, i-file-image, i-file-archive o i-file", "opcional": false },
    { "clase": "carga-archivo__miniatura", "elemento": "img", "papel": "En --imagen: vista previa (alt vacío; el nombre va al lado)", "opcional": true },
    { "clase": "carga-archivo__datos", "elemento": "span", "papel": "Nombre y peso", "opcional": false },
    { "clase": "carga-archivo__nombre", "elemento": "span", "papel": "Nombre del archivo; se corta con puntos suspensivos", "opcional": false },
    { "clase": "carga-archivo__peso", "elemento": "span", "papel": "«2.4 MB», en mono", "opcional": false },
    { "clase": "carga-archivo__quitar", "elemento": "button", "papel": "i-x con aria-label «Quitar <nombre>»; va solo (trae su propio reset)", "opcional": false },
    { "clase": "carga-archivo__rechazo", "elemento": "p[role='alert']", "papel": "Por qué no se aceptó un archivo (tipo, tamaño o cantidad); lo escribe carga-archivo.js", "opcional": false },
    { "clase": "carga-archivo__rechazo-icono", "elemento": "svg.icono", "papel": "i-circle-alert del rechazo (13px)", "opcional": false }
  ],
  "internas": [],
  "contexto": ["campo", "campo--con-error", "icono"],
  "ejemplo": [
    "<div class='campo'>",
    "  <label class='campo__etiqueta' for='d-archivo'>Archivo del documento</label>",
    "  <div class='carga-archivo' data-componente='carga-archivo' data-max-bytes='10485760'>",
    "    <input class='carga-archivo__entrada' id='d-archivo' name='archivo' type='file' accept='.pdf,.jpg,.jpeg,.png' required data-error='Elige el archivo del documento.' aria-describedby='d-archivo-requisitos'>",
    "    <div class='carga-archivo__zona'>",
    "      <span class='carga-archivo__icono'><svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-upload' /></svg></span>",
    "      <span class='carga-archivo__titulo'>Arrastre el archivo aquí o <span class='carga-archivo__elegir'>elíjalo</span></span>",
    "      <span class='carga-archivo__requisitos' id='d-archivo-requisitos'>PDF, JPG o PNG · hasta 10 MB</span>",
    "    </div>",
    "    <ul class='carga-archivo__lista'></ul>",
    "    <p class='carga-archivo__rechazo' role='alert' hidden><svg class='icono carga-archivo__rechazo-icono' aria-hidden='true'><use href='/images/iconos.svg#i-circle-alert' /></svg><span></span></p>",
    "  </div>",
    "</div>"
  ]
}
*/
/* Carga de archivos — la zona de soltar de la sección 07 (formularios). El control real
   es el input type='file': la zona solo lo presenta, así que el envío (multipart, htmx)
   no cambia. La validación en el navegador es una cortesía: el servidor sigue decidiendo.

   La «Fotografía de perfil» de la spec es esta misma pieza con --imagen. El recorte de
   la imagen, si una app lo necesita, es de su dominio. */

@layer componentes {
  .carga-archivo {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 8px;
  }

  /* Oculto a la vista, no al teclado ni al lector de pantalla */
  .carga-archivo .carga-archivo__entrada {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
    opacity: 0;
  }

  .carga-archivo__zona {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    padding: 18px 16px;
    border: 1px dashed var(--line-control);
    border-radius: var(--r-md);
    background: var(--sunken);
    text-align: center;
    cursor: pointer;
    transition: border-color var(--fast) var(--ease), background var(--fast) var(--ease);

    &:hover {
      border-color: var(--acc);
    }
  }

  /* El foco del input oculto se pinta en la zona, con el anillo global */
  .carga-archivo:has(.carga-archivo__entrada:focus-visible) .carga-archivo__zona {
    outline: 2px solid var(--ring);
    outline-offset: 2px;
    border-color: var(--acc);
  }

  .carga-archivo[data-arrastre="si"] .carga-archivo__zona {
    border-style: solid;
    border-color: var(--acc);
    background: var(--acc-soft);
  }

  .campo--con-error .carga-archivo__zona {
    border-color: var(--danger);
  }

  .carga-archivo__icono {
    display: grid;
    place-items: center;
    inline-size: 36px;
    block-size: 36px;
    border-radius: 999px;
    background: var(--acc-soft);
    color: var(--acc);

    .icono {
      inline-size: 17px;
      block-size: 17px;
      stroke-width: 2.471px; /* 1.75 × 24 / 17 */
    }
  }

  .carga-archivo__titulo {
    font-size: 13px;
    font-weight: 600;
    color: var(--fg);
  }

  .carga-archivo__elegir {
    color: var(--link);
    text-decoration: underline;
    text-underline-offset: 2px;
  }

  .carga-archivo__requisitos {
    font-size: 11.5px;
    color: var(--fg-3);
  }

  /* ── Archivos elegidos ─────────────────────────────────────────────────── */
  .carga-archivo__lista {
    display: flex;
    flex-direction: column;
    gap: 6px;
    margin: 0;
    padding: 0;
    list-style: none;

    &:empty {
      display: none;
    }
  }

  .carga-archivo__archivo {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 8px 8px 10px;
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    background: var(--surface);
  }

  .carga-archivo__tipo {
    flex: none;
    display: grid;
    place-items: center;
    inline-size: 32px;
    block-size: 32px;
    border-radius: var(--r-xs);
    background: var(--sunken);
    color: var(--fg-2);
  }

  .carga-archivo__miniatura {
    flex: none;
    border-radius: var(--r-xs);
    background: var(--sunken);
    object-fit: cover;
  }

  /* Con --imagen, la vista previa ocupa el lugar del icono, un poco más grande */
  .carga-archivo--imagen .carga-archivo__miniatura {
    inline-size: 48px;
    block-size: 48px;
  }

  .carga-archivo__datos {
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: 1px;
    min-inline-size: 0;
  }

  .carga-archivo__nombre {
    overflow: hidden;
    font-size: 13px;
    font-weight: 600;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .carga-archivo__peso {
    font-family: var(--f-mono);
    font-size: 11.5px;
    color: var(--fg-3);
  }

  /* Es un <button>: trae su propio reset (va solo, sin «boton») */
  .carga-archivo__quitar {
    flex: none;
    display: grid;
    place-items: center;
    inline-size: 30px;
    block-size: 30px;
    padding: 0;
    border: none;
    border-radius: var(--r-xs);
    background: transparent;
    color: var(--fg-3);
    font: inherit;
    cursor: pointer;

    &:hover {
      background: var(--danger-soft);
      color: var(--danger);
    }

    .icono {
      inline-size: 15px;
      block-size: 15px;
      stroke-width: 2.8px; /* 1.75 × 24 / 15 */
    }
  }

  /* Rechazo: texto E icono, como el error del campo */
  .carga-archivo__rechazo {
    display: flex;
    align-items: flex-start;
    gap: 5px;
    margin: 0;
    font-size: 11.5px;
    line-height: 1.45;
    color: var(--danger);
  }

  .carga-archivo__rechazo-icono {
    flex: none;
    inline-size: 13px;
    block-size: 13px;
    margin-block-start: 1px;
    stroke-width: 3.231px; /* 1.75 × 24 / 13 */
  }

  @media (pointer: coarse) {
    .carga-archivo__quitar {
      inline-size: 44px;
      block-size: 44px;
    }
  }
}

/*!inventario
{
  "componente": "Gráfica",
  "proposito": "Los cuatro tipos de gráfica de la spec y ninguno más: barras para comparar, líneas para evolucionar, dona para la composición y barra apilada para la proporción (más barras horizontales para comparar categorías de nombre largo). El servidor pinta una tabla de datos normal (grafica__datos) y grafica.js dibuja la gráfica con ella: la geometría vive en el DS y no en cada app, la tabla queda como alternativa en texto para lectores de pantalla (WCAG 1.1.1) y sin JS se ve la tabla. Ejes rotulados, leyenda cuando hay más de una serie y color solo para distinguir: las series usan --serie-1…4, nunca un tono semántico; las líneas se distinguen además por su trazo. Hasta 4 series (o 4 categorías en dona y apilada): con más, grafica.js no dibuja, deja la tabla y avisa en la consola. Sin atributos style: grafica.js escribe las medidas desde el script, que la CSP sí permite.",
  "raiz": "grafica",
  "variantes": {
    "grafica--barras": "Barras verticales agrupadas: una columna de la tabla por serie, una fila por categoría (meses, áreas). Es la variante por omisión",
    "grafica--horizontal": "Barras horizontales con el valor al final del renglón: categorías de nombre largo (remitentes, áreas). Una sola serie (la primera columna)",
    "grafica--lineas": "Líneas: una por serie, sobre una fila por punto (días, meses); la serie 1 lleva relleno suave y las demás trazo de guiones, puntos o guion y punto",
    "grafica--dona": "Composición de un total: una fila por categoría y una sola columna de valores; el total va al centro y la leyenda lleva valor y porcentaje",
    "grafica--apilada": "Proporción en una barra al 100%: mismos datos que la dona, para un espacio angosto o junto a barras horizontales"
  },
  "estados": {
    "[data-dibujada='si']": "En la raíz: grafica.js ya dibujó; la tabla queda solo para lectores de pantalla"
  },
  "modificadores": {
    "grafica__serie--1": "Color (y trazo, en líneas) de la serie 1; lo pone grafica.js. --serie-2 a --serie-4 igual",
    "grafica__serie--2": "Serie 2: apagada, para la comparación (el año anterior, lo proyectado); en líneas, guiones",
    "grafica__serie--3": "Serie 3; en líneas, puntos",
    "grafica__serie--4": "Serie 4; en líneas, guion y punto",
    "grafica__leyenda--valores": "Leyenda de dona y apilada: cada categoría con su valor y su porcentaje"
  },
  "estructura": [
    { "clase": "grafica__encabezado", "elemento": "figcaption", "papel": "Título y nota de la gráfica, sobre un filo", "opcional": false },
    { "clase": "grafica__titulo", "elemento": "span", "papel": "Qué muestra (14px en negrita)", "opcional": false },
    { "clase": "grafica__nota", "elemento": "span", "papel": "El periodo o la unidad, a la derecha en mono («Últimos 12 meses»)", "opcional": true },
    { "clase": "grafica__datos", "elemento": "table", "papel": "Los datos: thead con el nombre de las categorías y de cada serie; en tbody, th scope='row' con la categoría y un td por serie. El texto del td se muestra tal cual; el número sale de data-valor o, si falta, del texto sin comas ni unidades. Valores de 0 en adelante", "opcional": false },
    { "clase": "grafica__vacio", "elemento": "p", "papel": "En lugar de la tabla cuando no hay datos para el periodo («Sin datos para el periodo.»)", "opcional": true }
  ],
  "internas": ["grafica__lienzo", "grafica__plano", "grafica__eje-y", "grafica__marca", "grafica__area", "grafica__grupo", "grafica__barra", "grafica__eje-x", "grafica__categoria", "grafica__svg", "grafica__relleno", "grafica__linea", "grafica__dona", "grafica__pista", "grafica__porcion", "grafica__total", "grafica__total-cifra", "grafica__total-rotulo", "grafica__apilada", "grafica__tramo", "grafica__fila", "grafica__fila-rotulo", "grafica__fila-cifra", "grafica__carril", "grafica__leyenda", "grafica__leyenda-elemento", "grafica__muestra", "grafica__leyenda-nombre", "grafica__leyenda-cifra", "grafica__leyenda-porcentaje"],
  "contexto": [],
  "ejemplo": [
    "<figure class='grafica grafica--barras' data-componente='grafica' aria-labelledby='g-envios'>",
    "  <figcaption class='grafica__encabezado'>",
    "    <span class='grafica__titulo' id='g-envios'>Envíos por mes</span>",
    "    <span class='grafica__nota'>Ene – Jun</span>",
    "  </figcaption>",
    "  <table class='grafica__datos'>",
    "    <thead><tr><th scope='col'>Mes</th><th scope='col'>2026</th><th scope='col'>2025</th></tr></thead>",
    "    <tbody>",
    "      <tr><th scope='row'>Ene</th><td>142</td><td>118</td></tr>",
    "      <tr><th scope='row'>Feb</th><td data-valor='1680'>1,680</td><td>131</td></tr>",
    "    </tbody>",
    "  </table>",
    "</figure>",
    "<figure class='grafica grafica--dona' data-componente='grafica' data-rotulo-total='expedientes' aria-labelledby='g-areas'>…una fila por área, una columna de valores…</figure>"
  ]
}
*/
/* Gráfica — contra «Gráficas» (sección 11) del sistema: cuatro tipos y ninguno más,
   ejes rotulados, leyenda cuando hay más de una serie y color solo para distinguir.

   EL SERVIDOR PINTA DATOS, NO GEOMETRÍA. La app escribe una tabla normal y
   grafica.js la lee y dibuja; así el tope del eje, las marcas, el grosor de la
   barra y el trazo de cada serie son los mismos en todo el sistema, la tabla
   es la alternativa en texto (se oculta solo a la vista) y sin JS la página
   muestra los datos en tabla. Todo lo que mide grafica.js va por CSSOM o por
   atributos de SVG: nada de atributos style, que la CSP de la app ignora.

   Datos (una fila por categoría o punto, una columna por serie):
     <figure class="grafica grafica--lineas" data-componente="grafica" aria-labelledby="g-x">
       <figcaption class="grafica__encabezado">
         <span class="grafica__titulo" id="g-x">Envíos diarios</span>
         <span class="grafica__nota">Últimos 30 días</span>
       </figcaption>
       <table class="grafica__datos">
         <thead><tr><th scope="col">Día</th><th scope="col">Enviados</th><th scope="col">Rebotados</th></tr></thead>
         <tbody><tr><th scope="row">01 oct</th><td>412</td><td>9</td></tr>…</tbody>
       </table>
     </figure>
   — data-maximo en la raíz fija el tope del eje (p. ej. 100 en porcentajes); sin él,
     grafica.js lo redondea hacia arriba (240, no 228).
   — data-rotulo-total en la dona: lo que dice bajo el total («expedientes»; «total»
     si falta).
   — Valores de 0 en adelante. Hasta 4 series (4 categorías en dona y apilada):
     agrupe el resto en «Otras» en el servidor.

   Las series: --serie-1…4 (tokens), alternando claro y oscuro; en líneas, además,
   sólido, guiones, puntos y guion-punto, para que no dependan solo del color. */

@layer componentes {
  .grafica {
    position: relative;
    margin: 0;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    box-shadow: var(--e2);
    overflow: hidden;
  }

  .grafica__encabezado {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 6px 12px;
    padding: 14px 18px;
    border-block-end: 1px solid var(--divider);
  }

  .grafica__titulo {
    font-size: 14px;
    font-weight: 700;
  }

  .grafica__nota {
    font-family: var(--f-mono);
    font-size: 10.5px;
    color: var(--fg-3);
  }

  /* ── Sin JS (o antes de dibujar): la tabla de datos, legible ─────────────── */
  .grafica__datos {
    margin: 14px 18px 18px;
    border-collapse: collapse;
    font-size: 12.5px;

    & th,
    & td {
      padding: 5px 12px 5px 0;
      text-align: start;
    }

    & thead th {
      font-family: var(--f-mono);
      font-size: 10px;
      font-weight: 500;
      letter-spacing: .08em;
      text-transform: uppercase;
      color: var(--fg-3);
      border-block-end: 1px solid var(--line);
    }

    & tbody th {
      font-weight: 400;
      color: var(--fg-2);
    }

    & td {
      font-variant-numeric: tabular-nums;
      text-align: end;
    }
  }

  /* Dibujada: la tabla queda para lectores de pantalla, fuera de la vista */
  .grafica[data-dibujada="si"] .grafica__datos {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    margin: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .grafica__vacio {
    padding: 32px 18px;
    text-align: center;
    font-size: 13px;
    color: var(--fg-3);
  }

  /* ── Lo que dibuja grafica.js ───────────────────────────────────────────── */
  .grafica__lienzo {
    padding: 18px;
    /* En contraste forzado se conservan los colores: sin ellos no hay gráfica */
    forced-color-adjust: none;
  }

  /* Series: cada pieza toma su color de --grafica-color */
  .grafica__serie--1 {
    --grafica-color: var(--serie-1);
  }

  .grafica__serie--2 {
    --grafica-color: var(--serie-2);
  }

  .grafica__serie--3 {
    --grafica-color: var(--serie-3);
  }

  .grafica__serie--4 {
    --grafica-color: var(--serie-4);
  }

  /* Plano de barras y líneas: marcas del eje Y a la izquierda, área y categorías */
  .grafica__plano {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-rows: 170px auto;
    column-gap: 10px;
    row-gap: 7px;
  }

  /* Las marcas van de 0 al tope en cuatro tramos iguales: se reparten con
     space-between y la media línea negativa centra cada una en su guía */
  .grafica__eje-y {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    align-items: flex-end;
    margin-block: -0.5em;
    font-family: var(--f-mono);
    font-size: 9.5px;
    line-height: 1;
    color: var(--fg-3);
  }

  .grafica__marca {
    white-space: nowrap;
  }

  .grafica__area {
    position: relative;
    min-inline-size: 0;
    border-inline-start: 1px solid var(--line);
    border-block-end: 1px solid var(--line);
    /* Guías a cada cuarto del tope */
    background: repeating-linear-gradient(to top, transparent 0 calc(25% - 1px), var(--divider) calc(25% - 1px) 25%);
  }

  .grafica__eje-x {
    grid-column: 2;
    font-family: var(--f-mono);
    font-size: 9.5px;
    color: var(--fg-3);
  }

  .grafica__categoria {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* Barras verticales agrupadas */
  .grafica--barras {
    & .grafica__area {
      display: flex;
      align-items: stretch;
      gap: clamp(6px, 1.6vw, 16px);
      padding-inline: 4px;
    }

    & .grafica__grupo {
      flex: 1 1 0;
      min-inline-size: 0;
      display: flex;
      align-items: flex-end;
      justify-content: center;
      gap: 3px;
      padding-inline: 2px;
    }

    & .grafica__barra {
      flex: 1 1 0;
      max-inline-size: 20px;
      block-size: var(--valor, 0%);
      border-radius: var(--r-xs) var(--r-xs) 0 0;
      background: var(--grafica-color);
    }

    & .grafica__eje-x {
      display: flex;
      gap: clamp(6px, 1.6vw, 16px);
      padding-inline: 4px;
    }

    & .grafica__categoria {
      flex: 1 1 0;
      min-inline-size: 0;
      text-align: center;
    }
  }

  /* Líneas: SVG estirado al área; el trazo no se deforma (vector-effect) */
  .grafica__svg {
    display: block;
    inline-size: 100%;
    block-size: 100%;
  }

  .grafica--lineas {
    & .grafica__svg {
      position: absolute;
      inset: 0;
      overflow: visible;
    }

    & .grafica__eje-x {
      position: relative;
      block-size: 1.2em;
    }

    & .grafica__categoria {
      position: absolute;
      inset-inline-start: var(--x, 0%);
      /* Al 0% la etiqueta empieza en su punto; al 100% termina en él */
      translate: calc(var(--x, 0%) * -1) 0;
    }
  }

  .grafica__relleno {
    fill: var(--grafica-color);
    fill-opacity: .1;
    stroke: none;
  }

  .grafica__linea {
    fill: none;
    stroke: var(--grafica-color);
    stroke-width: 2;
    stroke-linejoin: round;
    stroke-linecap: round;

    &.grafica__serie--1 {
      stroke-width: 2.5;
    }

    &.grafica__serie--2 {
      stroke-dasharray: 5 4;
      stroke-linecap: butt;
    }

    &.grafica__serie--3 {
      stroke-dasharray: 1 4;
    }

    &.grafica__serie--4 {
      stroke-dasharray: 8 3 1 3;
    }
  }

  /* Barras horizontales: rótulo y cifra sobre su carril */
  .grafica--horizontal .grafica__lienzo {
    display: flex;
    flex-direction: column;
    gap: 14px;
  }

  .grafica__fila {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 5px 10px;
    font-size: 12.5px;
  }

  .grafica__fila-rotulo {
    color: var(--fg-2);
    overflow-wrap: anywhere;
  }

  .grafica__fila-cifra {
    font-family: var(--f-mono);
    font-size: 11.5px;
    color: var(--fg-3);
  }

  .grafica__carril {
    grid-column: 1 / -1;
    block-size: 7px;
    border-radius: 999px;
    background: var(--sunken);
    overflow: hidden;
  }

  .grafica--horizontal .grafica__barra {
    display: block;
    block-size: 100%;
    inline-size: var(--valor, 0%);
    border-radius: 999px;
    background: var(--grafica-color);
  }

  /* Dona: anillo de 120px con el total al centro y la leyenda al lado */
  .grafica--dona .grafica__lienzo {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 20px;
  }

  .grafica__dona {
    position: relative;
    flex: none;
    inline-size: 120px;
    block-size: 120px;
  }

  .grafica__pista,
  .grafica__porcion {
    fill: none;
    stroke-width: 7.75;
  }

  .grafica__pista {
    stroke: var(--sunken);
  }

  .grafica__porcion {
    stroke: var(--grafica-color);
  }

  .grafica__total {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
  }

  .grafica__total-cifra {
    font-size: 19px;
    font-weight: 800;
    line-height: 1;
  }

  .grafica__total-rotulo {
    font-family: var(--f-mono);
    font-size: 9px;
    color: var(--fg-3);
  }

  /* Apilada: una barra al 100% partida por categoría; el peso de cada tramo lo
     escribe grafica.js en --peso */
  .grafica--apilada .grafica__lienzo {
    display: flex;
    flex-direction: column;
    gap: 16px;
  }

  .grafica__apilada {
    display: flex;
    gap: 2px;
    block-size: 14px;
    border-radius: 999px;
    overflow: hidden;
  }

  .grafica__tramo {
    flex: var(--peso, 1) 1 0;
    min-inline-size: 2px;
    background: var(--grafica-color);
  }

  /* ── Leyenda ────────────────────────────────────────────────────────────── */
  .grafica__leyenda {
    display: flex;
    flex-wrap: wrap;
    gap: 6px 14px;
    margin: 12px 0 0;
    padding: 10px 0 0;
    border-block-start: 1px solid var(--divider);
    list-style: none;
    font-size: 11.5px;
    color: var(--fg-2);
  }

  .grafica__leyenda-elemento {
    display: inline-flex;
    align-items: center;
    gap: 6px;
  }

  .grafica__muestra {
    flex: none;
    inline-size: 9px;
    block-size: 9px;
    border-radius: 2px;
    background: var(--grafica-color);
  }

  /* En líneas, la muestra es el trazo de la serie (un SVG con su línea) */
  .grafica--lineas .grafica__muestra {
    inline-size: 18px;
    block-size: 4px;
    border-radius: 0;
    background: none;
    overflow: visible;
  }

  /* Dona y apilada: categoría, valor y porcentaje por renglón */
  .grafica__leyenda--valores {
    flex: 1;
    min-inline-size: 150px;
    flex-direction: column;
    flex-wrap: nowrap;
    gap: 9px;
    margin: 0;
    padding: 0;
    border: none;
    font-size: 12.5px;
    color: var(--fg);

    & .grafica__leyenda-elemento {
      display: flex;
      gap: 9px;
    }

    & .grafica__muestra {
      inline-size: 10px;
      block-size: 10px;
      border-radius: 3px;
    }
  }

  .grafica__leyenda-nombre {
    flex: 1;
    min-inline-size: 0;
  }

  .grafica__leyenda-cifra {
    flex: none;
    font-family: var(--f-mono);
    font-size: 11.5px;
    color: var(--fg-2);
  }

  .grafica__leyenda-porcentaje {
    flex: none;
    min-inline-size: 34px;
    font-family: var(--f-mono);
    font-size: 11px;
    color: var(--fg-3);
    text-align: end;
  }
}

/*!inventario
{
  "componente": "Desplegable",
  "proposito": "Selector en capa flotante con filtro de búsqueda — el «Dropdown con búsqueda» de la sección 07 del sistema. Sustituye al select nativo cuando la lista merece la capa del DS; con 8 opciones o más, el filtro es obligatorio. El valor vive en un input hidden del formulario (su change burbujea para htmx); abre/cierra y filtra con desplegable.js.",
  "raiz": "desplegable",
  "variantes": {},
  "estados": {
    "[data-abierto='si']": "Panel visible (en la raíz); el disparador refleja aria-expanded",
    "[aria-selected='true']": "La opción elegida — muestra su palomita (en la opción)",
    "[hidden]": "Opción o grupo fuera del filtro; el vacío se muestra cuando no queda ninguna"
  },
  "modificadores": {
    "desplegable__valor--placeholder": "El disparador aún sin valor elegido — texto atenuado"
  },
  "estructura": [
    { "clase": "desplegable__disparador", "elemento": "button[aria-haspopup='listbox']", "papel": "El control cerrado: valor actual + chevrones (i-chevrons-up-down)", "opcional": false },
    { "clase": "desplegable__valor", "elemento": "span", "papel": "La etiqueta de la opción elegida (o el placeholder)", "opcional": false },
    { "clase": "desplegable__icono", "elemento": "svg.icono", "papel": "Chevrones del disparador (15px)", "opcional": false },
    { "clase": "desplegable__panel", "elemento": "div", "papel": "La capa flotante: búsqueda + opciones", "opcional": false },
    { "clase": "desplegable__busqueda", "elemento": "label", "papel": "El filtro: icono i-search + entrada «Filtrar…»", "opcional": true },
    { "clase": "desplegable__busqueda-entrada", "elemento": "input[type='text']", "papel": "La entrada del filtro — sin name: nunca viaja en el formulario", "opcional": true },
    { "clase": "desplegable__opciones", "elemento": "div[role='listbox']", "papel": "La lista desplazable de opciones", "opcional": false },
    { "clase": "desplegable__grupo", "elemento": "span", "papel": "Encabezado de un grupo de opciones (el optgroup del DS); se oculta si su grupo queda fuera del filtro", "opcional": true },
    { "clase": "desplegable__opcion", "elemento": "button[role='option']", "papel": "Una opción; data-valor lleva el valor que se escribe al hidden", "opcional": false },
    { "clase": "desplegable__opcion-icono", "elemento": "svg.icono", "papel": "La palomita (i-check) — visible solo en la seleccionada", "opcional": false },
    { "clase": "desplegable__vacio", "elemento": "p", "papel": "«Sin resultados» cuando el filtro no deja opciones", "opcional": true }
  ],
  "internas": [],
  "ejemplo": [
    "<div class='desplegable' data-componente='desplegable'>",
    "  <input type='hidden' name='CentroId' value='6'>",
    "  <button type='button' class='desplegable__disparador' aria-haspopup='listbox' aria-expanded='false'>",
    "    <span class='desplegable__valor'>0030 — Centro</span>",
    "    <svg class='icono desplegable__icono' aria-hidden='true'><use href='/images/iconos.svg#i-chevrons-up-down' /></svg>",
    "  </button>",
    "  <div class='desplegable__panel'>",
    "    <label class='desplegable__busqueda'>",
    "      <svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-search' /></svg>",
    "      <input class='desplegable__busqueda-entrada' type='text' placeholder='Filtrar…' aria-label='Filtrar opciones'>",
    "    </label>",
    "    <div class='desplegable__opciones' role='listbox'>",
    "      <button type='button' class='desplegable__opcion' role='option' data-valor='6' aria-selected='true'>",
    "        0030 — Centro",
    "        <svg class='icono desplegable__opcion-icono' aria-hidden='true'><use href='/images/iconos.svg#i-check' /></svg>",
    "      </button>",
    "    </div>",
    "    <p class='desplegable__vacio' hidden>Sin resultados</p>",
    "  </div>",
    "</div>"
  ]
}
*/
/* Desplegable — contra «Dropdown con búsqueda» (sección 07) del sistema. Lo pidió
   una app para los filtros de catálogo (centros/almacenes con decenas de
   opciones). El estado del valor NO vive aquí: vive en el input hidden que el
   componente actualiza y cuyo change burbujea al formulario (htmx escucha ahí).
   El campo de filtro detiene sus propios input/change para no disparar al
   formulario anfitrión. */

@layer componentes {
  .desplegable {
    position: relative;
  }

  .desplegable__disparador {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    inline-size: 100%;
    padding: var(--ctl-y) var(--ctl-x);
    border: 1px solid var(--line-control);
    border-radius: var(--r-md);
    background: var(--surface);
    color: var(--fg);
    font-family: var(--f-ui);
    font-size: 13.5px;
    cursor: pointer;
    text-align: start;
    transition: border-color var(--fast) var(--ease);

    &:focus-visible {
      border-color: var(--acc);
    }
  }

  .desplegable[data-abierto="si"] .desplegable__disparador {
    border-color: var(--acc);
  }

  .desplegable__valor {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .desplegable__valor--placeholder {
    color: var(--fg-3);
  }

  .desplegable__icono {
    inline-size: 15px;
    block-size: 15px;
    color: var(--fg-3);
    flex: none;
  }

  /* ── La capa flotante ──────────────────────────────────────────────────── */
  .desplegable__panel {
    position: absolute;
    inset-block-start: calc(100% + 6px);
    inset-inline: 0;
    z-index: 30;
    min-inline-size: min(240px, 90vw);
    background: var(--raised);
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    box-shadow: var(--e4);
    padding: 6px;
    animation: desplegable-pop var(--fast) var(--ease);
  }

  .desplegable:not([data-abierto="si"]) .desplegable__panel {
    display: none;
  }

  @keyframes desplegable-pop {
    from {
      transform: translateY(-4px);
    }
  }

  @media (prefers-reduced-motion: reduce) {
    .desplegable__panel {
      animation: none;
    }
  }

  /* ── Filtro ────────────────────────────────────────────────────────────── */
  .desplegable__busqueda {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 7px 9px;
    margin-block-end: 5px;
    border-radius: var(--r-xs);
    background: var(--sunken);

    .icono {
      inline-size: 14px;
      block-size: 14px;
      color: var(--fg-3);
      flex: none;
    }
  }

  .desplegable__busqueda-entrada {
    border: none;
    background: transparent;
    color: var(--fg);
    font-family: var(--f-ui);
    font-size: 12.5px;
    inline-size: 100%;
    min-inline-size: 0;

    /* El foco lo muestra el contenedor (focus-within), no el anillo global:
       la entrada recibe foco automático al abrir y el anillo se veía como un
       borde ajeno al diseño del panel. */
    &:focus-visible {
      outline: none;
    }
  }

  /* El contenedor toma el color del anillo (2 px por dentro): 3:1 contra su fondo en los dos
     temas, como el anillo global (WCAG 1.4.11). */
  .desplegable__busqueda:focus-within {
    background: var(--surface);
    box-shadow: inset 0 0 0 2px var(--ring);
  }

  /* ── Opciones ──────────────────────────────────────────────────────────── */
  .desplegable__opciones {
    max-block-size: 180px;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
  }

  .desplegable__grupo {
    padding: 7px 10px 3px;
    font-family: var(--f-mono);
    font-size: 10px;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--fg-3);
  }

  .desplegable__opcion {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    padding: 8px 10px;
    border: none;
    border-radius: var(--r-xs);
    background: transparent;
    color: var(--fg);
    font-family: var(--f-ui);
    font-size: 12.5px;
    cursor: pointer;
    text-align: start;
    transition: background var(--fast) var(--ease);

    &:hover {
      background: var(--sunken);
    }
  }

  .desplegable__opcion-icono {
    inline-size: 14px;
    block-size: 14px;
    color: var(--acc);
    opacity: 0;
    flex: none;
  }

  .desplegable__opcion[aria-selected="true"] .desplegable__opcion-icono {
    opacity: 1;
  }

  .desplegable__vacio {
    padding: 14px 10px;
    font-size: 12px;
    color: var(--fg-3);
    text-align: center;
  }
}

/*!inventario
{
  "componente": "Código QR",
  "proposito": "Muestra un código QR para leerlo con la cámara de otro dispositivo: dar de alta una aplicación de autenticación, abrir un enlace en el teléfono, emparejar un equipo. La imagen la genera el servidor y trae lo que la cámara necesita: fondo claro opaco y su propia zona clara de 4 módulos alrededor (las cámaras leen oscuro sobre claro). Por eso el componente no pinta fondo ni invierte la imagen en tema oscuro; un PNG o SVG con fondo transparente quedaría oscuro sobre oscuro y no se leería. Se genera a 196px o más (un PNG con módulos de tamaño entero) o en SVG, que queda nítido a cualquier tamaño. Un QR nunca va solo: siempre lo acompaña el mismo dato en texto con la forma de copiarlo (datos con datos__copiar). Es la alternativa en texto para quien no ve la imagen (WCAG 1.1.1) y la única salida cuando la pantalla está abierta en el mismo teléfono que tendría que escanearla.",
  "raiz": "codigo-qr",
  "variantes": {},
  "estados": {},
  "modificadores": {},
  "estructura": [
    { "clase": "codigo-qr__imagen", "elemento": "img", "papel": "El QR a 196px, nunca más ancho que su columna. Lleva width y height en el marcado y un alt que dice para qué sirve, no lo que contiene", "opcional": false },
    { "clase": "codigo-qr__pie", "elemento": "figcaption", "papel": "Una frase sobre cómo leerlo, bajo la imagen", "opcional": true }
  ],
  "internas": [],
  "contexto": ["datos", "acceso", "modal", "cajon"],
  "ejemplo": [
    "<figure class='codigo-qr'>",
    "  <img class='codigo-qr__imagen' src='data:image/png;base64,…' width='196' height='196'",
    "       alt='Código QR para tu aplicación de autenticación' />",
    "  <figcaption class='codigo-qr__pie'>Escanéalo con la cámara de tu aplicación.</figcaption>",
    "</figure>",
    "<div class='datos'>",
    "  <dl class='datos__lista'>",
    "    <div class='datos__campo'><dt class='datos__etiqueta'>Clave para escribir a mano</dt>",
    "      <dd class='datos__valor datos__valor--codigo'>abcd efgh ijkl mnop",
    "        <button type='button' class='datos__copiar' data-copiar='ABCDEFGHIJKLMNOP' aria-label='Copiar la clave'><svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-copy' /></svg></button></dd></div>",
    "  </dl>",
    "</div>"
  ]
}
*/
/* Código QR — imagen para leer con la cámara de otro dispositivo, centrada en su columna.
   Solo tokens. El fondo claro y la zona clara los trae la imagen: el componente no los pinta
   ni invierte la imagen en tema oscuro, porque las cámaras leen oscuro sobre claro. */

@layer componentes {
  .codigo-qr {
    display: grid;
    justify-items: center;
    gap: var(--sp-2);
    margin: 0;
  }

  .codigo-qr__imagen {
    inline-size: 196px;
    max-inline-size: 100%;
    block-size: auto;
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    /* Módulos nítidos al escalar un PNG en pantallas de alta densidad; a un SVG no le afecta. */
    image-rendering: pixelated;
  }

  .codigo-qr__pie {
    color: var(--fg-3);
    font-size: 11.5px;
    text-align: center;
  }
}

/*!inventario
{
  "componente": "Fortaleza de contraseña",
  "proposito": "Medidor de fortaleza y lista de requisitos de una contraseña nueva. No trae reglas fijas: la app declara en el marcado qué requisitos mostrar (data-regla en cada renglón) y fortaleza.js los evalúa mientras la persona escribe. Texto e icono siempre: el color acompaña al nivel y a cada requisito, no los sustituye.",
  "raiz": "fortaleza",
  "variantes": {},
  "estados": {
    "[data-score='0']": "Muy débil: una barra en danger (fortaleza.js publica el score 0–4 en la raíz; sin atributo = campo vacío)",
    "[data-score='1']": "Débil: una barra en danger",
    "[data-score='2']": "Regular: dos barras en warning; el nivel como texto va en warning-fg",
    "[data-score='3']": "Aceptable: tres barras en warning",
    "[data-score='4']": "Fuerte: cuatro barras en success",
    "[data-activa='si']": "En fortaleza__barra: barra encendida",
    "[data-estado='cumple']": "En fortaleza__regla: requisito cumplido (palomita, success)",
    "[data-estado='falta']": "En fortaleza__regla: requisito pendiente con el campo ya escrito (danger); sin atributo = neutro",
    "[data-tono='advertencia']": "En fortaleza__consejo: consejo en tono de advertencia",
    "[data-tono='peligro']": "En fortaleza__consejo: consejo en tono de peligro"
  },
  "modificadores": {},
  "estructura": [
    { "clase": "fortaleza__encabezado", "elemento": "div", "papel": "Rótulo a la izquierda y nivel a la derecha", "opcional": false },
    { "clase": "fortaleza__rotulo", "elemento": "span", "papel": "«Fortaleza» en mono, mayúsculas espaciadas", "opcional": false },
    { "clase": "fortaleza__nivel", "elemento": "span", "papel": "El nivel en palabras (Débil, Regular, Aceptable, Fuerte); lo pone fortaleza.js", "opcional": false },
    { "clase": "fortaleza__barras", "elemento": "div", "papel": "Las cuatro barras del medidor", "opcional": false },
    { "clase": "fortaleza__barra", "elemento": "span", "papel": "Una barra; data-activa='si' la enciende", "opcional": false },
    { "clase": "fortaleza__consejo", "elemento": "p", "papel": "Consejo breve del estimador de la app (icono + texto); hidden cuando no hay", "opcional": true },
    { "clase": "fortaleza__reglas", "elemento": "ul", "papel": "Lista de requisitos que declara la app", "opcional": true },
    { "clase": "fortaleza__regla", "elemento": "li", "papel": "Un requisito: data-regla = longitud (con data-min) | minuscula | mayuscula | numero | simbolo | patron (con data-patron) | coincide", "opcional": true },
    { "clase": "fortaleza__marca", "elemento": "span", "papel": "Marca del requisito: palomita si cumple, raya si no", "opcional": true },
    { "clase": "fortaleza__icono-cumple", "elemento": "svg.icono", "papel": "i-check; visible solo con data-estado='cumple'", "opcional": true },
    { "clase": "fortaleza__icono-falta", "elemento": "svg.icono", "papel": "i-minus; visible mientras no cumple", "opcional": true },
    { "clase": "fortaleza__texto", "elemento": "span", "papel": "El requisito en palabras", "opcional": true }
  ],
  "internas": [],
  "contexto": ["icono"],
  "ejemplo": [
    "<div class='fortaleza' data-componente='fortaleza' data-para='#nueva' data-confirmacion='#confirmar' data-boton='#guardar' data-pista='#pista'>",
    "  <div class='fortaleza__encabezado'><span class='fortaleza__rotulo'>Fortaleza</span><span class='fortaleza__nivel'></span></div>",
    "  <div class='fortaleza__barras'><span class='fortaleza__barra'></span><span class='fortaleza__barra'></span><span class='fortaleza__barra'></span><span class='fortaleza__barra'></span></div>",
    "  <p class='fortaleza__consejo' hidden></p>",
    "  <ul class='fortaleza__reglas'>",
    "    <li class='fortaleza__regla' data-regla='longitud' data-min='12'><span class='fortaleza__marca'><svg class='icono fortaleza__icono-cumple' aria-hidden='true'><use href='/images/iconos.svg#i-check' /></svg><svg class='icono fortaleza__icono-falta' aria-hidden='true'><use href='/images/iconos.svg#i-minus' /></svg></span><span class='fortaleza__texto'>Al menos 12 caracteres</span></li>",
    "    <li class='fortaleza__regla' data-regla='numero'>…<span class='fortaleza__texto'>Al menos un número</span></li>",
    "    <li class='fortaleza__regla' data-regla='coincide'>…<span class='fortaleza__texto'>Las contraseñas coinciden</span></li>",
    "  </ul>",
    "</div>"
  ]
}
*/
/* Fortaleza de contraseña — bloque de la sección 19 del sistema. Cuatro barras:
   el número de barras dice el score y el color el tramo (mal / atención / bien).
   Como texto, la advertencia va en --warning-fg; como relleno, en --warning.

   El componente no decide qué es una contraseña válida: los requisitos los
   declara la app en el marcado y el score puede venir de un estimador de la app
   (window.fortaleza.estimador); sin estimador se deriva de los requisitos. */

@layer componentes {
  .fortaleza {
    display: flex;
    flex-direction: column;
    gap: 9px;
    padding: 14px;
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    background: var(--sunken);
  }

  .fortaleza__encabezado {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
  }

  .fortaleza__rotulo {
    font-family: var(--f-mono);
    font-size: 10px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--fg-3);
  }

  .fortaleza__nivel {
    font-size: 11.5px;
    font-weight: 700;
  }

  .fortaleza[data-score="0"] .fortaleza__nivel,
  .fortaleza[data-score="1"] .fortaleza__nivel {
    color: var(--danger);
  }

  .fortaleza[data-score="2"] .fortaleza__nivel,
  .fortaleza[data-score="3"] .fortaleza__nivel {
    color: var(--warning-fg);
  }

  .fortaleza[data-score="4"] .fortaleza__nivel {
    color: var(--success);
  }

  .fortaleza__barras {
    display: flex;
    gap: 4px;
  }

  .fortaleza__barra {
    flex: 1;
    block-size: 4px;
    border-radius: 999px;
    background: var(--line);
    transition: background-color var(--fast) var(--ease);
  }

  .fortaleza[data-score="0"] .fortaleza__barra[data-activa="si"],
  .fortaleza[data-score="1"] .fortaleza__barra[data-activa="si"] {
    background: var(--danger);
  }

  .fortaleza[data-score="2"] .fortaleza__barra[data-activa="si"],
  .fortaleza[data-score="3"] .fortaleza__barra[data-activa="si"] {
    background: var(--warning);
  }

  .fortaleza[data-score="4"] .fortaleza__barra[data-activa="si"] {
    background: var(--success);
  }

  /* Consejo del estimador: debe leerse, así que va como aviso chico */
  .fortaleza__consejo {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    padding: 8px 10px;
    border: 1px solid var(--line);
    border-radius: var(--r-xs);
    background: var(--surface);
    color: var(--fg);
    font-size: 12px;
    font-weight: 600;
    line-height: 1.5;
  }

  .fortaleza__consejo .icono {
    flex: none;
    inline-size: 14px;
    block-size: 14px;
    margin-block-start: 2px;
  }

  .fortaleza__consejo[data-tono="advertencia"] {
    border-color: var(--warning-bd);
    background: var(--warning-soft);
    color: var(--warning-fg);
  }

  .fortaleza__consejo[data-tono="peligro"] {
    border-color: var(--danger-bd);
    background: var(--danger-soft);
    color: var(--danger);
  }

  .fortaleza__reglas {
    display: flex;
    flex-direction: column;
    gap: 5px;
    padding: 4px 0 0;
    list-style: none;
  }

  .fortaleza__regla {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 11.5px;
    color: var(--fg-3);
  }

  .fortaleza__marca {
    flex: none;
    display: grid;
    place-items: center;
    inline-size: 18px;
    block-size: 18px;
    border-radius: 999px;
    background: var(--surface);
    color: var(--fg-4);
  }

  .fortaleza__marca .icono {
    inline-size: 13px;
    block-size: 13px;
  }

  .fortaleza__icono-cumple {
    display: none;
  }

  .fortaleza__texto {
    flex: 1;
    min-inline-size: 0;
    line-height: 1.4;
  }

  .fortaleza__regla[data-estado="cumple"] {
    color: var(--success);

    .fortaleza__marca {
      background: var(--success-soft);
      color: var(--success);
    }

    .fortaleza__icono-cumple {
      display: block;
    }

    .fortaleza__icono-falta {
      display: none;
    }
  }

  .fortaleza__regla[data-estado="falta"] {
    color: var(--danger);
    font-weight: 600;

    .fortaleza__marca {
      background: var(--danger-soft);
      color: var(--danger);
    }
  }

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

/*!inventario
{
  "componente": "Menú de usuario",
  "proposito": "Menú de cuenta del encabezado: botón tipo píldora sobre la marca y desplegable en capa flotante. Abre/cierra con menu-usuario.js. El menú lleva SIEMPRE las mismas cuatro opciones, en este orden, antes de cualquier opción propia: 1 Mi perfil (i-user → /Cuenta/Perfil) · 2 Cambiar contraseña (i-key-round → /Cuenta/Contrasena) · 3 Tema oscuro/Tema claro (i-moon/i-sun, button data-accion='alternar-tema' con <span data-tema-etiqueta>) · 4 Cerrar sesión (i-log-out, menu-usuario__opcion--peligro, hx-boost='false').",
  "raiz": "menu-usuario",
  "variantes": {},
  "estados": {
    "[data-abierto='no']": "Cerrado (menu-usuario.js alterna si/no en la raíz)"
  },
  "modificadores": {
    "menu-usuario__opcion--peligro": "Opción que termina algo (Cerrar sesión): texto en danger, fondo danger-soft al pasar el puntero"
  },
  "estructura": [
    { "clase": "menu-usuario__boton", "elemento": "button", "papel": "La píldora: inicial + nombre; aria-haspopup y aria-expanded", "opcional": false },
    { "clase": "menu-usuario__inicial", "elemento": "span", "papel": "Círculo con la inicial del nombre", "opcional": false },
    { "clase": "menu-usuario__nombre", "elemento": "span", "papel": "El nombre (se oculta en angosto)", "opcional": false },
    { "clase": "menu-usuario__caret", "elemento": "svg.icono", "papel": "Chevron i-chevron-down que gira 180° con el menú abierto", "opcional": true },
    { "clase": "menu-usuario__desplegable", "elemento": "div", "papel": "La capa flotante con identidad y opciones", "opcional": false },
    { "clase": "menu-usuario__identidad", "elemento": "div", "papel": "Nombre completo + detalle, arriba del desplegable", "opcional": false },
    { "clase": "menu-usuario__detalle", "elemento": "div", "papel": "El correo u otro dato secundario", "opcional": true },
    { "clase": "menu-usuario__opcion", "elemento": "a | button", "papel": "Una opción del menú. Las cuatro fijas, en este orden: Mi perfil (a, i-user), Cambiar contraseña (a, i-key-round), Tema oscuro/Tema claro (button data-accion='alternar-tema', i-moon/i-sun y [data-tema-etiqueta] que tema.js actualiza) y Cerrar sesión (a, i-log-out, --peligro); después, las propias de la app", "opcional": false },
    { "clase": "menu-usuario__separador", "elemento": "div", "papel": "Línea de 1px con --line y margen vertical, entre las cuatro opciones fijas y las propias, si las hay", "opcional": true }
  ],
  "internas": [],
  "ejemplo": [
    "<div class='menu-usuario' data-componente='menu-usuario' data-abierto='no'>",
    "  <button class='menu-usuario__boton' data-accion='alternar-menu-usuario' aria-haspopup='menu' aria-expanded='false'>",
    "    <span class='menu-usuario__inicial'>M</span>",
    "    <span class='menu-usuario__nombre'>Mariana</span>",
    "  </button>",
    "  <div class='menu-usuario__desplegable'>",
    "    <div class='menu-usuario__identidad'>Mariana Vargas <div class='menu-usuario__detalle'>Nutrióloga titular</div></div>",
    "    <a class='menu-usuario__opcion' href='/Cuenta/Perfil'><svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-user' /></svg> Mi perfil</a>",
    "    <a class='menu-usuario__opcion' href='/Cuenta/Contrasena'><svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-key-round' /></svg> Cambiar contraseña</a>",
    "    <button class='menu-usuario__opcion' type='button' data-accion='alternar-tema'><svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-moon' /></svg> <span data-tema-etiqueta>Tema oscuro</span></button>",
    "    <a class='menu-usuario__opcion menu-usuario__opcion--peligro' href='/Auth/Logout'><svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-log-out' /></svg> Cerrar sesión</a>",
    "    <div class='menu-usuario__separador'></div>",
    "    <a class='menu-usuario__opcion' href='/Preferencias'><svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-settings' /></svg> Mis horarios</a>",
    "  </div>",
    "</div>"
  ]
}
*/
/* Menú de cuenta del encabezado — botón tipo píldora sobre la marca y desplegable
   en capa-flotante con --e4 (contrato: --sh-pop 208–232 px). */

@layer componentes {
  .menu-usuario {
    position: relative;
  }

  .menu-usuario__boton {
    /* Siempre va sobre la marca: foco blanco (8:1) */
    --ring: var(--on-marca);
    display: flex;
    align-items: center;
    gap: 9px;
    padding: 4px 9px 4px 4px;
    border-radius: 999px;
    border: 1px solid rgb(255 255 255 / .22);
    background: rgb(255 255 255 / .08);
    color: var(--on-marca);
    font-family: var(--f-ui);
    cursor: pointer;
    transition: background var(--fast) var(--ease);

    &:hover {
      background: rgb(255 255 255 / .16);
    }
  }

  .menu-usuario__inicial {
    inline-size: 26px;
    block-size: 26px;
    border-radius: 999px;
    background: var(--on-marca);
    color: var(--marca);
    display: grid;
    place-items: center;
    font-size: 11px;
    font-weight: 700;
  }

  .menu-usuario__desplegable {
    /* Capa clara dentro del encabezado de marca: recupera el anillo general */
    --ring: var(--ring-capa);
    position: absolute;
    inset-inline-end: 0;
    inset-block-start: calc(100% + 8px);
    z-index: 70;
    inline-size: var(--sh-pop);
    background: var(--raised);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    box-shadow: var(--e4);
    padding: 6px;
    color: var(--fg);
  }

  .menu-usuario[data-abierto="no"] .menu-usuario__desplegable {
    display: none;
  }

  .menu-usuario__identidad {
    padding: 10px 11px 12px;
    border-block-end: 1px solid var(--divider);
    margin-block-end: 5px;
  }

  .menu-usuario__nombre {
    font-size: 13px;
    font-weight: 700;
  }

  /* En angosto (teléfono) la píldora queda solo con la inicial: un nombre largo empujaba el
     menú fuera de la pantalla. El nombre completo sigue en la identidad del desplegable. */
  @media (max-width: 560px) {
    .menu-usuario__nombre {
      display: none;
    }
  }

  .menu-usuario__detalle {
    font-size: 11.5px;
    color: var(--fg-3);
  }

  .menu-usuario__opcion {
    display: flex;
    align-items: center;
    gap: 10px;
    inline-size: 100%;
    padding: 8px 11px;
    border: none;
    border-radius: var(--r-xs);
    background: transparent;
    color: var(--fg);
    font-family: var(--f-ui);
    font-size: 13px;
    cursor: pointer;
    text-align: start;
    text-decoration: none;

    &:hover {
      background: var(--sunken);
      color: var(--fg);
    }
  }

  /* Separa las cuatro opciones fijas de las propias */
  .menu-usuario__separador {
    block-size: 1px;
    margin-block: 5px;
    background: var(--line);
  }

  /* La opción que termina algo (Cerrar sesión) va en danger: se distingue de las
     demás sin depender solo del color (siempre lleva icono y texto). */
  .menu-usuario__opcion--peligro {
    color: var(--danger);

    &:hover {
      background: var(--danger-soft);
      color: var(--danger);
    }
  }

  /* Caret del botón: chevron que gira al abrir */
  .menu-usuario__caret {
    inline-size: 14px;
    block-size: 14px;
    color: var(--on-marca-3);
    transition: transform var(--fast) var(--ease);
  }

  .menu-usuario[data-abierto="si"] .menu-usuario__caret {
    transform: rotate(180deg);
  }

  @media (pointer: coarse) {
    .menu-usuario__opcion {
      min-block-size: 44px;
    }
  }
}

/*!inventario
{
  "componente": "Toast",
  "proposito": "Aviso flotante que NO bloquea: algo ocurrió en otro lado (un inventario entró a validación, SAP rechazó un envío, llegó un documento) y quien lo ve puede ir o ignorarlo. No es feedback de la acción propia — eso sigue siendo la alerta centrada. Una sola pila fija arriba a la derecha (bajo la banda de marca del shell); cada toast entra con un rebote, se desvanece conforme su barra se consume, se pausa al pasar el cursor y siempre queda también en el centro de notificaciones. toast.js lo abre desde window.toast.mostrar o desde el evento DOM `toast` (HX-Trigger).",
  "raiz": "toast",
  "variantes": {
    "toast--exito": "Tono success (documento recibido, validación aprobada)",
    "toast--informativo": "Tono info (aviso neutro)",
    "toast--advertencia": "Tono warning (requiere acción: entró a validación); el icono va en --warning-fg porque pisa fondo suave",
    "toast--peligro": "Tono danger (rechazo, error)",
    "toast--revision": "Tono review (hecho, espera decisión)"
  },
  "estados": {
    "[data-visible='si']": "Visible: entra con rebote desde la derecha y se desvanece en el último tramo de su duración; sin el atributo está saliendo o por entrar",
    "[data-pausado='si']": "Cuenta regresiva detenida (cursor o foco encima): barra y desvanecimiento se congelan",
    "[data-permanente='si']": "Sin cierre automático (duracion 0): no hay barra ni desvanecimiento"
  },
  "modificadores": {},
  "estructura": [
    { "clase": "toast-pila", "elemento": "div", "papel": "Pila fija única arriba a la derecha (bajo la banda de marca cuando hay shell; a lo ancho en móvil), aria-live='polite'; toast.js la crea bajo body si no existe", "opcional": false },
    { "clase": "toast__icono", "elemento": "span > svg.icono", "papel": "Círculo de 32px con el icono del tono: es la única marca de color del toast junto con la barra", "opcional": false },
    { "clase": "toast__cuerpo", "elemento": "div", "papel": "Columna con título, texto y enlace", "opcional": false },
    { "clase": "toast__titulo", "elemento": "p", "papel": "Qué pasó, en una línea", "opcional": false },
    { "clase": "toast__texto", "elemento": "p", "papel": "Detalle en una frase (--fg-2)", "opcional": true },
    { "clase": "toast__enlace", "elemento": "a", "papel": "Ir al lugar del hecho («Ver»); enlace normal, respeta el shell", "opcional": true },
    { "clase": "toast__cerrar", "elemento": "button", "papel": "Cierre de 34px (icono i-x)", "opcional": false },
    { "clase": "toast__barra", "elemento": "span", "papel": "Filo inferior en el tono que se consume con el tiempo restante", "opcional": false }
  ],
  "internas": [],
  "contexto": ["icono", "shell"],
  "ejemplo": [
    "<div class='toast-pila' aria-live='polite'>",
    "  <div class='toast toast--advertencia' role='status' data-visible='si'>",
    "    <span class='toast__icono'><svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-clipboard-check' /></svg></span>",
    "    <div class='toast__cuerpo'>",
    "      <p class='toast__titulo'>Inventario por validar</p>",
    "      <p class='toast__texto'>F-01-01 · INV-2609-0217 espera su decisión.</p>",
    "      <a class='toast__enlace' href='/Inventarios/Detalle/…'>Ver</a>",
    "    </div>",
    "    <button type='button' class='toast__cerrar' aria-label='Cerrar'><svg class='icono' aria-hidden='true'><use href='/images/iconos.svg#i-x' /></svg></button>",
    "    <span class='toast__barra'></span>",
    "  </div>",
    "</div>",
    "window.toast.mostrar({ tono: 'advertencia', titulo: 'Inventario por validar', texto: '…', url: '/Inventarios/Detalle/…' })"
  ]
}
*/
/* Toast — contra «Modales, paneles y avisos» (sección 24): el toast NO interrumpe.
   Es para hechos que ocurrieron en OTRO lado (tiempo real, servidor), nunca para
   el resultado de lo que el usuario acaba de hacer (eso es la alerta centrada).
   - Una sola pila fija arriba a la derecha: bajo la banda de marca cuando hay
     shell (donde el ojo ya busca la campana); a lo ancho en móvil.
   - El tono vive en el icono y en la barra; el cuerpo no lleva filo de color
     para que la barra sea la única cuenta regresiva visible.
   - Entra con un rebote corto desde la derecha y se desvanece en el último
     tramo de su duración, al ritmo de la barra; al cerrarlo a mano se desliza.
   - Máximo 4 visibles (toast.js encola el resto); cada uno se va solo tras su
     duración, se pausa con el cursor o el foco encima y se cierra con Esc/botón.
   - La duración la escribe toast.js en --toast-duracion desde el script; nadie la
     pinta en un atributo style (la CSP de la app lo ignora).
   - Todo toast queda también en el centro de notificaciones: perderlo no es
     perder el aviso. */
@layer componentes {
  .toast-pila {
    --toast-arriba: 16px;
    position: fixed;
    inset-block-start: var(--toast-arriba);
    inset-inline-end: 16px;
    z-index: 85;
    display: flex;
    flex-direction: column;
    gap: 10px;
    inline-size: min(380px, calc(100vw - 32px));
    pointer-events: none;
  }

  /* Bajo la banda de marca del shell (72px, --sh-brand-h vive en .shell y la
     pila cuelga de body, por eso el valor va aquí) */
  .shell ~ .toast-pila {
    --toast-arriba: 84px;
  }

  .toast {
    --toast-tono: var(--acc);
    --toast-tono-suave: var(--acc-soft);
    --toast-icono: var(--acc);
    position: relative;
    display: flex;
    align-items: flex-start;
    gap: 12px;
    padding: 12px 8px 14px 14px;
    background: var(--surface);
    color: var(--fg);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    box-shadow: var(--e3);
    overflow: hidden;
    pointer-events: auto;
    /* Fuera de escena: al salir se desliza a la derecha y se apaga */
    opacity: 0;
    translate: 110% 0;
    transition: opacity var(--med) var(--ease), translate var(--med) var(--ease);
  }

  /* Visible: rebote de entrada (solo posición, para no pelear con la opacidad)
     y desvanecimiento que corre lo mismo que la barra. */
  .toast[data-visible="si"] {
    opacity: 1;
    translate: 0 0;
    animation:
      toast-entrar 520ms cubic-bezier(0.34, 1.56, 0.64, 1) both,
      toast-desvanecer var(--toast-duracion, 7000ms) linear forwards;
  }

  .toast[data-visible="si"][data-permanente="si"] {
    animation: toast-entrar 520ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
  }

  .toast--exito {
    --toast-tono: var(--success);
    --toast-tono-suave: var(--success-soft);
    --toast-icono: var(--success);
  }

  .toast--informativo {
    --toast-tono: var(--info);
    --toast-tono-suave: var(--info-soft);
    --toast-icono: var(--info);
  }

  /* El ámbar de relleno pinta la barra; el icono va sobre fondo suave, así que
     usa el ámbar de texto. */
  .toast--advertencia {
    --toast-tono: var(--warning);
    --toast-tono-suave: var(--warning-soft);
    --toast-icono: var(--warning-fg);
  }

  .toast--peligro {
    --toast-tono: var(--danger);
    --toast-tono-suave: var(--danger-soft);
    --toast-icono: var(--danger);
  }

  .toast--revision {
    --toast-tono: var(--review);
    --toast-tono-suave: var(--review-soft);
    --toast-icono: var(--review);
  }

  .toast__icono {
    flex: none;
    inline-size: 32px;
    block-size: 32px;
    border-radius: 999px;
    display: grid;
    place-items: center;
    background: var(--toast-tono-suave);
    color: var(--toast-icono);
  }

  .toast__icono .icono {
    inline-size: 17px;
    block-size: 17px;
  }

  .toast__cuerpo {
    flex: 1;
    min-inline-size: 0;
    display: flex;
    flex-direction: column;
    gap: 3px;
    padding-block-start: 5px;
  }

  .toast__titulo {
    margin: 0;
    font-size: 13.5px;
    font-weight: 700;
    line-height: 1.35;
    overflow-wrap: anywhere;
  }

  .toast__texto {
    margin: 0;
    font-size: 12.5px;
    line-height: 1.5;
    color: var(--fg-2);
    overflow-wrap: anywhere;
  }

  .toast__enlace {
    align-self: flex-start;
    margin-block-start: 2px;
    font-size: 12.5px;
    font-weight: 700;
    color: var(--link);
    text-decoration: none;
  }

  .toast__enlace:hover,
  .toast__enlace:focus-visible {
    color: var(--link-h);
    text-decoration: underline;
  }

  .toast__cerrar {
    flex: none;
    inline-size: 34px;
    block-size: 34px;
    display: grid;
    place-items: center;
    padding: 0;
    border: none;
    border-radius: var(--r-sm);
    background: transparent;
    color: var(--fg-3);
    cursor: pointer;
    transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
  }

  .toast__cerrar:hover,
  .toast__cerrar:focus-visible {
    background: var(--sunken);
    color: var(--fg);
  }

  .toast__cerrar .icono {
    inline-size: 16px;
    block-size: 16px;
  }

  /* Filo inferior que se consume: la animación dura lo que el toast; en pausa se
     detiene igual que el temporizador de toast.js y que el desvanecimiento. */
  .toast__barra {
    position: absolute;
    inset-block-end: 0;
    inset-inline-start: 0;
    block-size: 3px;
    inline-size: 100%;
    background: var(--toast-tono);
    transform-origin: left center;
    animation: toast-consumir var(--toast-duracion, 7000ms) linear forwards;
  }

  .toast[data-pausado="si"],
  .toast[data-pausado="si"] .toast__barra {
    animation-play-state: paused;
  }

  @keyframes toast-entrar {
    from { translate: 110% 0; scale: 0.96; }
    to { translate: 0 0; scale: 1; }
  }

  /* Entero casi todo el tiempo; se apaga en el último tramo, cuando a la barra
     le queda poco. */
  @keyframes toast-desvanecer {
    0%, 72% { opacity: 1; }
    100% { opacity: 0; }
  }

  @keyframes toast-consumir {
    from { transform: scaleX(1); }
    to { transform: scaleX(0); }
  }

  /* Móvil: a lo ancho bajo el borde superior */
  @media (max-width: 640px) {
    .toast-pila,
    .shell ~ .toast-pila {
      --toast-arriba: 12px;
      inset-inline: 12px;
      inline-size: auto;
    }
  }

  @media (prefers-reduced-motion: reduce) {
    .toast {
      transition: opacity var(--med) var(--ease);
      translate: 0 0;
    }

    .toast[data-visible="si"] {
      animation: none;
    }

    .toast__barra {
      animation: none;
    }
  }
}

/* Horarios: un bloque por día con sus tramos. Solo pg-*, contra tokens. */
@layer componentes {
  .pg-horario {
    display: grid;
    gap: var(--sp-4);
  }

  .pg-horario__forma {
    display: grid;
    gap: var(--sp-4);
  }

  .pg-horario__dias {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 30rem), 1fr));
    gap: var(--sp-4);
  }

  .pg-horario__dia {
    display: grid;
    align-content: start;
    gap: var(--sp-3);
  }

  .pg-horario__dia-encabezado {
    display: flex;
    gap: var(--sp-2);
    align-items: center;
    justify-content: space-between;
  }

  .pg-horario__dia-titulo {
    margin: 0;
    font: 500 16px/1.3 var(--f-display);
  }

  .pg-horario__dia-titulo::first-letter {
    text-transform: uppercase;
  }

  .pg-horario__tramos {
    display: grid;
    gap: var(--sp-2);
  }

  .pg-horario__tramo {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2);
    align-items: center;
  }

  .pg-horario__tramo .campo__selector {
    inline-size: 6.5rem;
  }

  .pg-horario__tramo .pg-horario__lugar {
    flex: 1 1 10rem;
    inline-size: auto;
  }

  .pg-horario__tramo--lectura {
    margin: 0;
    color: var(--fg-2);
  }

  .pg-horario__horas {
    font-family: var(--f-mono);
    color: var(--fg);
  }

  .pg-horario__a {
    color: var(--fg-3);
  }

  .pg-horario__descanso {
    margin: 0;
    color: var(--fg-3);
    font-size: 13px;
  }

  .pg-horario__dia:has(.pg-horario__tramo) .pg-horario__descanso {
    display: none;
  }

  .pg-horario__acciones {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-3);
    align-items: center;
    justify-content: flex-end;
  }

  .pg-horario__pista {
    margin: 0;
    color: var(--fg-3);
    font-size: 12.5px;
  }
}

/* Posiciones de la rejilla de la agenda (generadas): columnas, filas de un día completo y alturas. */
@layer componentes {
  .pg-agenda__rejilla--c1 { --pg-columnas: 1; }
  .pg-agenda__rejilla--c2 { --pg-columnas: 2; }
  .pg-agenda__rejilla--c3 { --pg-columnas: 3; }
  .pg-agenda__rejilla--c4 { --pg-columnas: 4; }
  .pg-agenda__rejilla--c5 { --pg-columnas: 5; }
  .pg-agenda__rejilla--c6 { --pg-columnas: 6; }
  .pg-agenda__rejilla--c7 { --pg-columnas: 7; }
  .pg-ag-c1 { grid-column: 1; }
  .pg-ag-c2 { grid-column: 2; }
  .pg-ag-c3 { grid-column: 3; }
  .pg-ag-c4 { grid-column: 4; }
  .pg-ag-c5 { grid-column: 5; }
  .pg-ag-c6 { grid-column: 6; }
  .pg-ag-c7 { grid-column: 7; }
  .pg-ag-c8 { grid-column: 8; }
  .pg-ag-f2 { grid-row-start: 2; }
  .pg-ag-f3 { grid-row-start: 3; }
  .pg-ag-f4 { grid-row-start: 4; }
  .pg-ag-f5 { grid-row-start: 5; }
  .pg-ag-f6 { grid-row-start: 6; }
  .pg-ag-f7 { grid-row-start: 7; }
  .pg-ag-f8 { grid-row-start: 8; }
  .pg-ag-f9 { grid-row-start: 9; }
  .pg-ag-f10 { grid-row-start: 10; }
  .pg-ag-f11 { grid-row-start: 11; }
  .pg-ag-f12 { grid-row-start: 12; }
  .pg-ag-f13 { grid-row-start: 13; }
  .pg-ag-f14 { grid-row-start: 14; }
  .pg-ag-f15 { grid-row-start: 15; }
  .pg-ag-f16 { grid-row-start: 16; }
  .pg-ag-f17 { grid-row-start: 17; }
  .pg-ag-f18 { grid-row-start: 18; }
  .pg-ag-f19 { grid-row-start: 19; }
  .pg-ag-f20 { grid-row-start: 20; }
  .pg-ag-f21 { grid-row-start: 21; }
  .pg-ag-f22 { grid-row-start: 22; }
  .pg-ag-f23 { grid-row-start: 23; }
  .pg-ag-f24 { grid-row-start: 24; }
  .pg-ag-f25 { grid-row-start: 25; }
  .pg-ag-f26 { grid-row-start: 26; }
  .pg-ag-f27 { grid-row-start: 27; }
  .pg-ag-f28 { grid-row-start: 28; }
  .pg-ag-f29 { grid-row-start: 29; }
  .pg-ag-f30 { grid-row-start: 30; }
  .pg-ag-f31 { grid-row-start: 31; }
  .pg-ag-f32 { grid-row-start: 32; }
  .pg-ag-f33 { grid-row-start: 33; }
  .pg-ag-f34 { grid-row-start: 34; }
  .pg-ag-f35 { grid-row-start: 35; }
  .pg-ag-f36 { grid-row-start: 36; }
  .pg-ag-f37 { grid-row-start: 37; }
  .pg-ag-f38 { grid-row-start: 38; }
  .pg-ag-f39 { grid-row-start: 39; }
  .pg-ag-f40 { grid-row-start: 40; }
  .pg-ag-f41 { grid-row-start: 41; }
  .pg-ag-f42 { grid-row-start: 42; }
  .pg-ag-f43 { grid-row-start: 43; }
  .pg-ag-f44 { grid-row-start: 44; }
  .pg-ag-f45 { grid-row-start: 45; }
  .pg-ag-f46 { grid-row-start: 46; }
  .pg-ag-f47 { grid-row-start: 47; }
  .pg-ag-f48 { grid-row-start: 48; }
  .pg-ag-f49 { grid-row-start: 49; }
  .pg-ag-f50 { grid-row-start: 50; }
  .pg-ag-f51 { grid-row-start: 51; }
  .pg-ag-f52 { grid-row-start: 52; }
  .pg-ag-f53 { grid-row-start: 53; }
  .pg-ag-f54 { grid-row-start: 54; }
  .pg-ag-f55 { grid-row-start: 55; }
  .pg-ag-f56 { grid-row-start: 56; }
  .pg-ag-f57 { grid-row-start: 57; }
  .pg-ag-f58 { grid-row-start: 58; }
  .pg-ag-f59 { grid-row-start: 59; }
  .pg-ag-f60 { grid-row-start: 60; }
  .pg-ag-f61 { grid-row-start: 61; }
  .pg-ag-f62 { grid-row-start: 62; }
  .pg-ag-f63 { grid-row-start: 63; }
  .pg-ag-f64 { grid-row-start: 64; }
  .pg-ag-f65 { grid-row-start: 65; }
  .pg-ag-f66 { grid-row-start: 66; }
  .pg-ag-f67 { grid-row-start: 67; }
  .pg-ag-f68 { grid-row-start: 68; }
  .pg-ag-f69 { grid-row-start: 69; }
  .pg-ag-f70 { grid-row-start: 70; }
  .pg-ag-f71 { grid-row-start: 71; }
  .pg-ag-f72 { grid-row-start: 72; }
  .pg-ag-f73 { grid-row-start: 73; }
  .pg-ag-f74 { grid-row-start: 74; }
  .pg-ag-f75 { grid-row-start: 75; }
  .pg-ag-f76 { grid-row-start: 76; }
  .pg-ag-f77 { grid-row-start: 77; }
  .pg-ag-f78 { grid-row-start: 78; }
  .pg-ag-f79 { grid-row-start: 79; }
  .pg-ag-f80 { grid-row-start: 80; }
  .pg-ag-f81 { grid-row-start: 81; }
  .pg-ag-f82 { grid-row-start: 82; }
  .pg-ag-f83 { grid-row-start: 83; }
  .pg-ag-f84 { grid-row-start: 84; }
  .pg-ag-f85 { grid-row-start: 85; }
  .pg-ag-f86 { grid-row-start: 86; }
  .pg-ag-f87 { grid-row-start: 87; }
  .pg-ag-f88 { grid-row-start: 88; }
  .pg-ag-f89 { grid-row-start: 89; }
  .pg-ag-f90 { grid-row-start: 90; }
  .pg-ag-f91 { grid-row-start: 91; }
  .pg-ag-f92 { grid-row-start: 92; }
  .pg-ag-f93 { grid-row-start: 93; }
  .pg-ag-f94 { grid-row-start: 94; }
  .pg-ag-f95 { grid-row-start: 95; }
  .pg-ag-f96 { grid-row-start: 96; }
  .pg-ag-f97 { grid-row-start: 97; }
  .pg-ag-s1 { grid-row-end: span 1; }
  .pg-ag-s2 { grid-row-end: span 2; }
  .pg-ag-s3 { grid-row-end: span 3; }
  .pg-ag-s4 { grid-row-end: span 4; }
  .pg-ag-s5 { grid-row-end: span 5; }
  .pg-ag-s6 { grid-row-end: span 6; }
  .pg-ag-s7 { grid-row-end: span 7; }
  .pg-ag-s8 { grid-row-end: span 8; }
  .pg-ag-filas-4 { grid-template-rows: auto repeat(4, var(--pg-fila)); }
  .pg-ag-filas-5 { grid-template-rows: auto repeat(5, var(--pg-fila)); }
  .pg-ag-filas-6 { grid-template-rows: auto repeat(6, var(--pg-fila)); }
  .pg-ag-filas-7 { grid-template-rows: auto repeat(7, var(--pg-fila)); }
  .pg-ag-filas-8 { grid-template-rows: auto repeat(8, var(--pg-fila)); }
  .pg-ag-filas-9 { grid-template-rows: auto repeat(9, var(--pg-fila)); }
  .pg-ag-filas-10 { grid-template-rows: auto repeat(10, var(--pg-fila)); }
  .pg-ag-filas-11 { grid-template-rows: auto repeat(11, var(--pg-fila)); }
  .pg-ag-filas-12 { grid-template-rows: auto repeat(12, var(--pg-fila)); }
  .pg-ag-filas-13 { grid-template-rows: auto repeat(13, var(--pg-fila)); }
  .pg-ag-filas-14 { grid-template-rows: auto repeat(14, var(--pg-fila)); }
  .pg-ag-filas-15 { grid-template-rows: auto repeat(15, var(--pg-fila)); }
  .pg-ag-filas-16 { grid-template-rows: auto repeat(16, var(--pg-fila)); }
  .pg-ag-filas-17 { grid-template-rows: auto repeat(17, var(--pg-fila)); }
  .pg-ag-filas-18 { grid-template-rows: auto repeat(18, var(--pg-fila)); }
  .pg-ag-filas-19 { grid-template-rows: auto repeat(19, var(--pg-fila)); }
  .pg-ag-filas-20 { grid-template-rows: auto repeat(20, var(--pg-fila)); }
  .pg-ag-filas-21 { grid-template-rows: auto repeat(21, var(--pg-fila)); }
  .pg-ag-filas-22 { grid-template-rows: auto repeat(22, var(--pg-fila)); }
  .pg-ag-filas-23 { grid-template-rows: auto repeat(23, var(--pg-fila)); }
  .pg-ag-filas-24 { grid-template-rows: auto repeat(24, var(--pg-fila)); }
  .pg-ag-filas-25 { grid-template-rows: auto repeat(25, var(--pg-fila)); }
  .pg-ag-filas-26 { grid-template-rows: auto repeat(26, var(--pg-fila)); }
  .pg-ag-filas-27 { grid-template-rows: auto repeat(27, var(--pg-fila)); }
  .pg-ag-filas-28 { grid-template-rows: auto repeat(28, var(--pg-fila)); }
  .pg-ag-filas-29 { grid-template-rows: auto repeat(29, var(--pg-fila)); }
  .pg-ag-filas-30 { grid-template-rows: auto repeat(30, var(--pg-fila)); }
  .pg-ag-filas-31 { grid-template-rows: auto repeat(31, var(--pg-fila)); }
  .pg-ag-filas-32 { grid-template-rows: auto repeat(32, var(--pg-fila)); }
  .pg-ag-filas-33 { grid-template-rows: auto repeat(33, var(--pg-fila)); }
  .pg-ag-filas-34 { grid-template-rows: auto repeat(34, var(--pg-fila)); }
  .pg-ag-filas-35 { grid-template-rows: auto repeat(35, var(--pg-fila)); }
  .pg-ag-filas-36 { grid-template-rows: auto repeat(36, var(--pg-fila)); }
  .pg-ag-filas-37 { grid-template-rows: auto repeat(37, var(--pg-fila)); }
  .pg-ag-filas-38 { grid-template-rows: auto repeat(38, var(--pg-fila)); }
  .pg-ag-filas-39 { grid-template-rows: auto repeat(39, var(--pg-fila)); }
  .pg-ag-filas-40 { grid-template-rows: auto repeat(40, var(--pg-fila)); }
  .pg-ag-filas-41 { grid-template-rows: auto repeat(41, var(--pg-fila)); }
  .pg-ag-filas-42 { grid-template-rows: auto repeat(42, var(--pg-fila)); }
  .pg-ag-filas-43 { grid-template-rows: auto repeat(43, var(--pg-fila)); }
  .pg-ag-filas-44 { grid-template-rows: auto repeat(44, var(--pg-fila)); }
  .pg-ag-filas-45 { grid-template-rows: auto repeat(45, var(--pg-fila)); }
  .pg-ag-filas-46 { grid-template-rows: auto repeat(46, var(--pg-fila)); }
  .pg-ag-filas-47 { grid-template-rows: auto repeat(47, var(--pg-fila)); }
  .pg-ag-filas-48 { grid-template-rows: auto repeat(48, var(--pg-fila)); }
  .pg-ag-filas-49 { grid-template-rows: auto repeat(49, var(--pg-fila)); }
  .pg-ag-filas-50 { grid-template-rows: auto repeat(50, var(--pg-fila)); }
  .pg-ag-filas-51 { grid-template-rows: auto repeat(51, var(--pg-fila)); }
  .pg-ag-filas-52 { grid-template-rows: auto repeat(52, var(--pg-fila)); }
  .pg-ag-filas-53 { grid-template-rows: auto repeat(53, var(--pg-fila)); }
  .pg-ag-filas-54 { grid-template-rows: auto repeat(54, var(--pg-fila)); }
  .pg-ag-filas-55 { grid-template-rows: auto repeat(55, var(--pg-fila)); }
  .pg-ag-filas-56 { grid-template-rows: auto repeat(56, var(--pg-fila)); }
  .pg-ag-filas-57 { grid-template-rows: auto repeat(57, var(--pg-fila)); }
  .pg-ag-filas-58 { grid-template-rows: auto repeat(58, var(--pg-fila)); }
  .pg-ag-filas-59 { grid-template-rows: auto repeat(59, var(--pg-fila)); }
  .pg-ag-filas-60 { grid-template-rows: auto repeat(60, var(--pg-fila)); }
  .pg-ag-filas-61 { grid-template-rows: auto repeat(61, var(--pg-fila)); }
  .pg-ag-filas-62 { grid-template-rows: auto repeat(62, var(--pg-fila)); }
  .pg-ag-filas-63 { grid-template-rows: auto repeat(63, var(--pg-fila)); }
  .pg-ag-filas-64 { grid-template-rows: auto repeat(64, var(--pg-fila)); }
  .pg-ag-filas-65 { grid-template-rows: auto repeat(65, var(--pg-fila)); }
  .pg-ag-filas-66 { grid-template-rows: auto repeat(66, var(--pg-fila)); }
  .pg-ag-filas-67 { grid-template-rows: auto repeat(67, var(--pg-fila)); }
  .pg-ag-filas-68 { grid-template-rows: auto repeat(68, var(--pg-fila)); }
  .pg-ag-filas-69 { grid-template-rows: auto repeat(69, var(--pg-fila)); }
  .pg-ag-filas-70 { grid-template-rows: auto repeat(70, var(--pg-fila)); }
  .pg-ag-filas-71 { grid-template-rows: auto repeat(71, var(--pg-fila)); }
  .pg-ag-filas-72 { grid-template-rows: auto repeat(72, var(--pg-fila)); }
  .pg-ag-filas-73 { grid-template-rows: auto repeat(73, var(--pg-fila)); }
  .pg-ag-filas-74 { grid-template-rows: auto repeat(74, var(--pg-fila)); }
  .pg-ag-filas-75 { grid-template-rows: auto repeat(75, var(--pg-fila)); }
  .pg-ag-filas-76 { grid-template-rows: auto repeat(76, var(--pg-fila)); }
  .pg-ag-filas-77 { grid-template-rows: auto repeat(77, var(--pg-fila)); }
  .pg-ag-filas-78 { grid-template-rows: auto repeat(78, var(--pg-fila)); }
  .pg-ag-filas-79 { grid-template-rows: auto repeat(79, var(--pg-fila)); }
  .pg-ag-filas-80 { grid-template-rows: auto repeat(80, var(--pg-fila)); }
  .pg-ag-filas-81 { grid-template-rows: auto repeat(81, var(--pg-fila)); }
  .pg-ag-filas-82 { grid-template-rows: auto repeat(82, var(--pg-fila)); }
  .pg-ag-filas-83 { grid-template-rows: auto repeat(83, var(--pg-fila)); }
  .pg-ag-filas-84 { grid-template-rows: auto repeat(84, var(--pg-fila)); }
  .pg-ag-filas-85 { grid-template-rows: auto repeat(85, var(--pg-fila)); }
  .pg-ag-filas-86 { grid-template-rows: auto repeat(86, var(--pg-fila)); }
  .pg-ag-filas-87 { grid-template-rows: auto repeat(87, var(--pg-fila)); }
  .pg-ag-filas-88 { grid-template-rows: auto repeat(88, var(--pg-fila)); }
  .pg-ag-filas-89 { grid-template-rows: auto repeat(89, var(--pg-fila)); }
  .pg-ag-filas-90 { grid-template-rows: auto repeat(90, var(--pg-fila)); }
  .pg-ag-filas-91 { grid-template-rows: auto repeat(91, var(--pg-fila)); }
  .pg-ag-filas-92 { grid-template-rows: auto repeat(92, var(--pg-fila)); }
  .pg-ag-filas-93 { grid-template-rows: auto repeat(93, var(--pg-fila)); }
  .pg-ag-filas-94 { grid-template-rows: auto repeat(94, var(--pg-fila)); }
  .pg-ag-filas-95 { grid-template-rows: auto repeat(95, var(--pg-fila)); }
  .pg-ag-filas-96 { grid-template-rows: auto repeat(96, var(--pg-fila)); }
}

/* Inicio: rejilla de accesos a las secciones que la persona puede abrir. */
@layer componentes {
  .pg-inicio {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: var(--gap);
  }
}

/* Formulario del paciente y páginas públicas (aviso de privacidad). Solo pg-*, contra tokens. */
@layer componentes {
  .pg-publico {
    display: flex;
    flex-direction: column;
    min-block-size: 100dvh;
    background: var(--bg);
    color: var(--fg);
  }

  .pg-publico__encabezado {
    display: flex;
    justify-content: center;
    padding: var(--sp-5) var(--sp-4) var(--sp-3);
  }

  .pg-publico__logo {
    block-size: 44px;
    inline-size: auto;
  }

  .pg-publico__contenido {
    flex: 1;
    inline-size: 100%;
    max-inline-size: 880px;
    margin-inline: auto;
    padding: var(--sp-3) var(--sp-4) var(--sp-6);
  }

  .pg-publico__pie {
    padding: var(--sp-5) var(--sp-4);
    border-block-start: 1px solid var(--line);
    color: var(--fg-3);
    font-size: 13px;
    text-align: center;
  }

  .pg-publico__pie p {
    margin: 0 0 var(--sp-1);
  }

  .pg-publico__aviso {
    min-block-size: auto;
    padding-block: var(--sp-5);
  }

  /* Solo para lectores de pantalla (y el botón que hace de envío por omisión). */
  .pg-oculto {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .pg-formulario {
    display: grid;
    gap: var(--sp-4);
  }

  .pg-formulario__encabezado {
    text-align: center;
  }

  .pg-formulario__titulo {
    margin: 0 0 var(--sp-2);
    font-family: var(--f-display);
    font-size: clamp(24px, 5vw, 32px);
    font-weight: 500;
    line-height: 1.2;
  }

  .pg-formulario__sub {
    max-inline-size: 40rem;
    margin: 0 auto;
    color: var(--fg-2);
    font-size: 14.5px;
  }

  .pg-formulario__paso {
    margin: 0;
    font-family: var(--f-display);
    font-size: 22px;
    font-weight: 500;
  }

  .pg-formulario__intro {
    margin: 0;
    color: var(--fg-2);
    font-size: 14.5px;
  }

  .pg-formulario__forma {
    gap: var(--sp-4);
  }

  .pg-formulario__campos {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 16rem), 1fr));
    gap: var(--sp-4) var(--sp-5);
    align-items: start;
  }

  .pg-formulario__grupo {
    grid-column: 1 / -1;
    margin: var(--sp-3) 0 0;
    padding-block-start: var(--sp-3);
    border-block-start: 1px solid var(--line);
    font-size: 15px;
    font-weight: 700;
  }

  .pg-formulario__grupo[hidden],
  .pg-form__campo[hidden] {
    display: none;
  }

  /* Preguntas con opciones y textos largos: el renglón completo. */
  .pg-form__opciones,
  .pg-form__campo--ancho {
    grid-column: 1 / -1;
  }

  .pg-form__opciones {
    min-inline-size: 0;
    margin: 0;
    padding: 0;
    border: 0;
  }

  .pg-form__lista-opciones {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 15rem), 1fr));
    gap: var(--sp-2) var(--sp-4);
  }

  .pg-form__lista-opciones--fila {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2) var(--sp-5);
  }

  .pg-form__corto {
    max-inline-size: 14rem;
  }

  .pg-form__nose {
    margin-block-start: var(--sp-2);
  }

  .pg-formulario__consentimiento {
    padding: var(--sp-4);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    background: var(--bg);
  }

  .pg-formulario__consentimiento .campo__opcion-texto {
    font-weight: 400;
    line-height: 1.5;
  }

  .pg-form__opciones > legend {
    padding: 0;
  }

  /* En angosto el índice no tiene lugar para el título del paso (el panel ya lo muestra arriba):
     se oculta a la vista y queda para los lectores de pantalla. */
  @media (width < 740px) {
    .pg-formulario .pasos__textos {
      position: absolute;
      inline-size: 1px;
      block-size: 1px;
      overflow: hidden;
      clip-path: inset(50%);
      white-space: nowrap;
    }
  }

  /* Aviso de privacidad */
  .pg-aviso {
    max-inline-size: 44rem;
    margin-inline: auto;
    padding: var(--sp-5);
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    background: var(--surface);
    font-size: 15px;
    line-height: 1.6;
  }

  .pg-aviso__titulo {
    margin: 0;
    font-family: var(--f-display);
    font-size: 28px;
    font-weight: 500;
  }

  .pg-aviso__version {
    margin: var(--sp-1) 0 var(--sp-4);
    color: var(--fg-3);
    font-size: 13px;
  }

  .pg-aviso h2 {
    margin: var(--sp-5) 0 var(--sp-2);
    font-size: 16px;
    font-weight: 700;
  }

  .pg-aviso p {
    margin: 0 0 var(--sp-3);
  }
}

/* Agenda: barra, rejilla de cuartos de hora, lista y formulario de la cita. Solo pg-*, contra tokens.
   La rejilla se posiciona con clases (pg-ag-*) porque la CSP no admite atributos style: están en
   agenda-posiciones.css, generadas (columnas, filas de un día completo y alturas). */
@layer componentes {
  .pg-agenda {
    display: grid;
    gap: var(--sp-4);
  }

  .pg-agenda__barra {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-3);
    align-items: center;
    justify-content: space-between;
  }

  .pg-agenda__navegacion,
  .pg-agenda__controles,
  .pg-agenda__filtros {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2);
    align-items: center;
  }

  .pg-agenda__controles {
    gap: var(--sp-3);
  }

  .pg-agenda__titulo {
    margin: 0 0 0 var(--sp-2);
    font: 500 18px/1.3 var(--f-display);
    color: var(--fg);
  }

  .pg-agenda__titulo::first-letter {
    text-transform: uppercase;
  }

  .pg-agenda__fecha {
    inline-size: 11.5rem;
  }

  .pg-agenda__nutriologa {
    min-inline-size: 13rem;
  }

  /* ── Rejilla ─────────────────────────────────────────────────────────── */
  .pg-agenda__marco {
    --pg-fila: 22px;
    overflow: auto;
    max-block-size: max(420px, calc(100dvh - 300px));
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    background: var(--surface);
  }

  .pg-agenda__marco:focus-visible {
    outline: 2px solid var(--ring);
    outline-offset: 2px;
  }

  .pg-agenda__rejilla {
    display: grid;
    grid-template-columns: 3.5rem repeat(var(--pg-columnas, 1), minmax(10rem, 1fr));
    min-inline-size: fit-content;
  }

  .pg-agenda__esquina,
  .pg-agenda__columna-titulo {
    position: sticky;
    inset-block-start: 0;
    z-index: 3;
    grid-row: 1;
    padding: var(--sp-2) var(--sp-3);
    border-block-end: 1px solid var(--line);
    background: var(--surface);
    font-size: 13px;
    font-weight: 600;
    color: var(--fg);
  }

  .pg-agenda__esquina {
    inset-inline-start: 0;
    z-index: 4;
  }

  .pg-agenda__columna-titulo {
    border-inline-start: 1px solid var(--line);
  }

  .pg-agenda__columna-titulo::first-letter {
    text-transform: uppercase;
  }

  .pg-agenda__columna-titulo--hoy {
    color: var(--g-800);
    box-shadow: inset 0 -3px 0 var(--g-600);
  }

  .pg-agenda__columna-subtitulo {
    display: block;
    font-size: 12px;
    font-weight: 400;
    color: var(--fg-3);
  }

  .pg-agenda__columna-enlace {
    color: inherit;
    text-decoration: none;
  }

  .pg-agenda__columna-enlace:hover {
    text-decoration: underline;
  }

  .pg-agenda__hora {
    position: sticky;
    inset-inline-start: 0;
    z-index: 2;
    padding: 2px var(--sp-2) 0 0;
    border-block-start: 1px solid var(--line);
    background: var(--surface);
    font: 11.5px/1.2 var(--f-mono);
    color: var(--fg-3);
    text-align: end;
  }

  /* Carril de cada columna: su borde y una línea por hora. */
  .pg-agenda__carril {
    grid-row: 2 / -1;
    border-inline-start: 1px solid var(--line);
    background-image: repeating-linear-gradient(to bottom, var(--line) 0 1px, transparent 1px calc(var(--pg-fila) * 4));
  }

  /* Un cuarto de hora dentro del horario: clic para agendar. */
  .pg-agenda__libre {
    z-index: 1;
    grid-row-end: span 1;
    margin: 0;
    padding: 0;
    border: 0;
    border-block-start: 1px solid color-mix(in srgb, var(--surface) 55%, transparent);
    background: color-mix(in srgb, var(--acc-soft) 75%, transparent);
    cursor: pointer;
  }

  .pg-agenda__libre:hover {
    background: var(--acc-soft-2);
  }

  /* Fuera del horario: sin relleno, solo se marca al pasar el puntero. */
  .pg-agenda__libre--fuera {
    border-block-start-color: transparent;
    background: transparent;
  }

  .pg-agenda__libre--fuera:hover {
    background: color-mix(in srgb, var(--sunken) 90%, transparent);
  }

  .pg-agenda__libre--inactiva,
  .pg-agenda__libre--inactiva:hover {
    background: color-mix(in srgb, var(--sunken) 80%, transparent);
    cursor: default;
  }

  /* Hora y paciente en el primer renglón; lugar y tipo en el segundo (30 min = dos filas). */
  .pg-agenda__cita {
    z-index: 2;
    display: grid;
    grid-template-columns: auto 1fr;
    column-gap: var(--sp-2);
    align-content: start;
    min-inline-size: 0;
    margin: 1px 4px;
    padding: 2px 6px;
    overflow: hidden;
    border: 1px solid var(--line-2);
    border-inline-start: 3px solid var(--g-400);
    border-radius: var(--r-sm);
    background: var(--surface);
    box-shadow: var(--e1);
    color: var(--fg);
    font: inherit;
    font-size: 12px;
    line-height: 1.3;
    text-align: start;
    cursor: pointer;
    transition: box-shadow var(--fast) var(--ease), transform var(--fast) var(--ease);
  }

  .pg-agenda__cita:hover {
    box-shadow: var(--e3);
    transform: translateY(-1px);
  }

  .pg-agenda__cita:focus-visible {
    outline: 2px solid var(--ring);
    outline-offset: 1px;
  }

  .pg-agenda__cita--primera {
    border-inline-start-color: var(--g-800);
  }

  .pg-agenda__cita--atendida {
    border-color: var(--success-bd);
    background: var(--success-soft);
  }

  .pg-agenda__cita--no-asistio {
    border-color: var(--danger-bd);
    background: var(--danger-soft);
  }

  .pg-agenda__cita--no-asistio .pg-agenda__cita-paciente {
    text-decoration: line-through;
  }

  /* 15 minutos: una sola línea con hora y paciente. */
  .pg-agenda__cita--corta {
    grid-template-columns: auto 1fr;
    gap: var(--sp-2);
    align-items: center;
    padding-block: 0;
  }

  .pg-agenda__cita--corta .pg-agenda__cita-detalle {
    display: none;
  }

  .pg-agenda__cita-hora {
    font-family: var(--f-mono);
    color: var(--fg-2);
  }

  .pg-agenda__cita-paciente {
    overflow: hidden;
    font-weight: 600;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .pg-agenda__cita-detalle {
    grid-column: 1 / -1;
    display: flex;
    gap: 4px;
    align-items: center;
    overflow: hidden;
    color: var(--fg-3);
    white-space: nowrap;
  }

  .pg-agenda__cita-detalle .icono {
    flex: none;
    inline-size: 12px;
    block-size: 12px;
  }

  .pg-agenda__leyenda {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2) var(--sp-4);
    align-items: center;
    margin: 0;
    color: var(--fg-3);
    font-size: 12.5px;
  }

  .pg-agenda__muestra {
    display: inline-block;
    inline-size: 12px;
    block-size: 12px;
    margin-inline-end: calc(var(--sp-2) * -1 + 2px);
    border: 1px solid var(--line-2);
    border-radius: 3px;
    vertical-align: middle;
  }

  .pg-agenda__muestra--libre {
    background: var(--acc-soft);
  }

  .pg-agenda__muestra--primera {
    border-inline-start: 3px solid var(--g-800);
    background: var(--surface);
  }

  .pg-agenda__muestra--atendida {
    border-color: var(--success-bd);
    background: var(--success-soft);
  }

  .pg-agenda__muestra--no-asistio {
    border-color: var(--danger-bd);
    background: var(--danger-soft);
  }

  /* ── Lista ───────────────────────────────────────────────────────────── */
  .pg-agenda__lista-dia th {
    padding: var(--sp-3) var(--sp-4) var(--sp-2);
    background: var(--sunken);
    font-size: 13px;
    font-weight: 600;
    text-align: start;
  }

  .pg-agenda__lista-dia th::first-letter {
    text-transform: uppercase;
  }

  .pg-agenda__nota {
    color: var(--fg-3);
    font-size: 12.5px;
  }

  .pg-agenda__enlace {
    color: var(--link);
  }

  .pg-agenda__enlace:hover {
    color: var(--link-h);
  }

  /* ── Formulario de la cita ───────────────────────────────────────────── */
  .pg-cita__paciente,
  .pg-cita__tipo,
  .pg-cita__horarios {
    display: grid;
    gap: var(--sp-3);
    margin: 0;
    padding: 0;
    border: none;
  }

  .pg-cita__modos {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2) var(--sp-5);
  }

  .pg-cita__cuando,
  .pg-cita__existente {
    display: grid;
    gap: var(--sp-4);
  }

  /* Paciente nuevo o existente, sin JS: lo decide el radio elegido. */
  .pg-cita__nuevo {
    display: none;
    gap: var(--sp-4);
  }

  .pg-cita__paciente:has(input[value="nuevo"]:checked) .pg-cita__nuevo {
    display: grid;
  }

  .pg-cita__paciente:has(input[value="nuevo"]:checked) .pg-cita__existente {
    display: none;
  }

  .pg-cita__lista-pacientes {
    display: grid;
    gap: var(--sp-1);
    max-block-size: 13rem;
    overflow: auto;
  }

  .pg-cita__grupo {
    display: grid;
    gap: var(--sp-2);
  }

  .pg-cita__grupo + .pg-cita__grupo {
    margin-block-start: var(--sp-3);
  }

  .pg-cita__grupo-titulo {
    color: var(--fg-3);
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
  }

  .pg-cita__huecos {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2);
  }

  .pg-cita__hueco {
    position: relative;
  }

  .pg-cita__hueco input {
    position: absolute;
    inset: 0;
    margin: 0;
    opacity: 0;
    cursor: pointer;
  }

  .pg-cita__hueco span {
    display: inline-block;
    min-inline-size: 4.25rem;
    padding: 6px 10px;
    border: 1px solid var(--line-control);
    border-radius: var(--r-md);
    background: var(--surface);
    font: 13px/1.2 var(--f-mono);
    text-align: center;
    transition: background var(--fast) var(--ease), border-color var(--fast) var(--ease);
  }

  .pg-cita__hueco:hover span {
    border-color: var(--acc);
  }

  .pg-cita__hueco input:checked + span {
    border-color: var(--acc);
    background: var(--acc);
    color: var(--acc-fg);
  }

  .pg-cita__hueco input:focus-visible + span {
    outline: 2px solid var(--ring);
    outline-offset: 2px;
  }

  @media (max-width: 720px) {
    .pg-agenda__titulo {
      flex-basis: 100%;
      margin: var(--sp-1) 0 0;
    }

    .pg-agenda__marco {
      max-block-size: none;
    }
  }
}

/* Administración: usuarios y roles. Solo maquetación de página (pg-*), contra tokens. */
@layer componentes {
  .pg-usuarios__correo {
    color: var(--fg-3);
    font-size: 13px;
  }

  .pg-usuarios__roles {
    display: grid;
    gap: var(--sp-2);
    margin: 0;
    padding: 0;
    border: none;
  }

  /* Matriz de permisos de un rol: un bloque por grupo, en columnas que se acomodan */
  .pg-roles__grupos {
    display: grid;
    gap: var(--sp-5);
  }

  .pg-roles__grupo {
    display: grid;
    gap: var(--sp-2);
    margin: 0;
    padding: 0;
    border: none;
  }

  .pg-roles__permisos {
    color: var(--fg-2);
    font-size: 13px;
  }
}

/* Pacientes: lista, ficha (perfil del DS) e historia clínica en lectura. Solo pg-*, contra tokens. */
@layer componentes {
  .pg-pacientes__conteo {
    color: var(--fg-3);
    font-size: 12.5px;
  }

  .pg-pacientes__nombre {
    font-weight: 600;
    color: var(--fg);
  }

  .pg-pacientes__secundario {
    color: var(--fg-3);
    font-size: 13px;
  }

  /* Ficha: envuelve encabezado y perfil para refrescarlos juntos; mismo ritmo que la región. */
  .pg-ficha {
    display: flex;
    flex-direction: column;
    gap: var(--sp-4);
  }

  /* Ficha: estado vacío de la historia clínica, al estilo del vacío de la tabla. */
  .pg-ficha__vacio {
    display: grid;
    justify-items: center;
    gap: var(--sp-2);
    padding: var(--sp-8) var(--sp-4);
    text-align: center;
  }

  .pg-ficha__vacio-icono {
    display: grid;
    place-items: center;
    inline-size: 48px;
    block-size: 48px;
    border-radius: var(--r-md);
    background: var(--sunken);
    color: var(--fg-3);
  }

  .pg-ficha__vacio-titulo {
    margin: 0;
    font-weight: 600;
    color: var(--fg);
  }

  .pg-ficha__vacio-texto {
    max-inline-size: 42ch;
    margin: 0;
    color: var(--fg-3);
    font-size: 13.5px;
  }

  /* Varias historias: se elige cuál leer. */
  .pg-ficha__historias {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-2);
    margin-block-end: var(--sp-4);
  }

  .pg-ficha__historias-titulo {
    color: var(--fg-3);
    font-size: 12.5px;
  }

  .pg-ficha__historia {
    padding: 4px 10px;
    border: 1px solid var(--line);
    border-radius: var(--r-pildora);
    color: var(--fg-2);
    font: 12.5px/1.4 var(--f-mono);
    text-decoration: none;
  }

  .pg-ficha__historia:hover {
    background: var(--sunken);
  }

  .pg-ficha__historia[aria-current="page"] {
    border-color: var(--acc);
    background: var(--acc-soft);
    color: var(--acc);
  }

  .pg-ficha__enlace-historia {
    margin-inline-end: var(--sp-3);
    font-family: var(--f-mono);
  }

  /* El enlace del formulario es largo: se parte en cualquier punto en lugar de desbordar el cajón. */
  .pg-enlace {
    display: flex;
    gap: var(--sp-2);
    align-items: flex-start;
  }

  .pg-enlace__url {
    min-inline-size: 0;
    overflow-wrap: anywhere;
  }

  /* Historia en lectura: los textos largos ocupan el renglón completo y conservan sus saltos. */
  .pg-historia__recibida {
    margin: 0 0 var(--sp-5);
    color: var(--fg-3);
    font-size: 13.5px;
  }

  .pg-historia__ancho {
    grid-column: 1 / -1;
  }

  .pg-historia__texto {
    white-space: pre-line;
    font-weight: 500;
  }
}

