/* ============================================================================
   Agrupa — sistema de diseño
   ----------------------------------------------------------------------------
   Portado del dashboard de tiendapp. Este archivo define los tokens y las
   piezas que NO dependen de módulos ni de permisos: lienzo, tipografía,
   tarjetas, formularios, botones y los orbes de fondo.

   El kernel de UI (P0-c) construye el shell sobre estos tokens; no los
   redefine. Si un componente necesita un color nuevo, se agrega aquí como
   variable, nunca como literal en una plantilla.
   ========================================================================= */

/* ── Inter, vendorizada ────────────────────────────────────────────────────
   Sin CDN, por la misma razón que HTMX y Alpine: §5.5 del spec de P0-c2 dice
   que varios clientes van a correr detrás de redes cerradas. Una hoja de
   estilo de un tercero es peor que un script: `<link rel=stylesheet>` BLOQUEA
   el render, así que detrás de una red que no resuelve fonts.googleapis.com
   la pantalla se queda en blanco hasta que el navegador se rinde.

   Solo los subconjuntos `latin` y `latin-ext`: es un ERP colombiano, y los
   quince archivos de cirílico, griego y vietnamita no los pide nadie.
   ------------------------------------------------------------------------ */

@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../fonts/inter-400-latin-ext.01ba6c2a184b.woff2") format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../fonts/inter-400-latin.65850a373e25.woff2") format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("../fonts/inter-500-latin-ext.01ba6c2a184b.woff2") format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("../fonts/inter-500-latin.65850a373e25.woff2") format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("../fonts/inter-600-latin-ext.01ba6c2a184b.woff2") format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("../fonts/inter-600-latin.65850a373e25.woff2") format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("../fonts/inter-700-latin-ext.01ba6c2a184b.woff2") format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("../fonts/inter-700-latin.65850a373e25.woff2") format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

:root {
    /* Marca */
    --primary: #7c5cf0;
    --primary-dark: #5b3ec8;
    --primary-soft: #f1edfe;
    /* El contorno del bloque morado suave. `--border` es gris y sobre
       `--primary-soft` se lee como suciedad; `--primary` a plena fuerza
       convierte un enlace en un botón. Éste es el borde que la caja de la
       factura necesita: visible, del mismo tinte, sin gritar. */
    --primary-soft-border: #ddd3fb;
    /* Las zonas seguras del teléfono, con el mismo nombre que en `pos.css`:
       valen 0 en un navegador de escritorio y lo que mida el notch en la app. */
    --sat: env(safe-area-inset-top, 0px);
    --sab: env(safe-area-inset-bottom, 0px);
    /* El borde de la casilla SIN marcar. NO es `--border-strong`: ése da
       1.5:1 sobre blanco y la casilla hay que buscarla. Hay una prueba que
       calcula la cifra.

       `#9691ac` desde el 2026-08-17: Renzo lo quería MÁS CLARO —pidió
       `#d2cfdf`— y ése da 1.53:1, o sea la casilla que hay que buscar otra
       vez. Éste es lo más claro que sigue pasando el 3:1 que la WCAG 1.4.11
       pide para el contorno de un control: da 3.02:1. Se eligió con la
       calculadora de la propia guarda enfrente, no a ojo. */
    --checkbox-border: #9691ac;

    /* Lienzo y superficies */
    --bg: #f7f6fb;
    --card-bg: #ffffff;
    --canvas: #0d0b16;

    /* El banner de sección va sobre `--canvas`, que es el mismo negro de la
       topbar: la pantalla lee como una pieza y no como dos temas pegados.
       Por eso el par de texto vive acá y no es un hex suelto dentro de la
       regla — el día que el tema cambie, se cambia junto a su fondo.

       `--banner-sub` NO es `--text-light`: aquel está calibrado contra
       blanco y sobre el negro da 3.1:1, que para texto normal es ilegible.
       Hay dos pruebas que calculan las dos cifras. */
    --banner-text: #ffffff;
    --banner-sub: #b3aecb;
    /* La banda es un DEGRADADO de dos morados oscuros, no un negro con un
       resplandor encima. Es la diferencia entre una banda que se ve profunda y
       una que se ve apagada, y son los dos valores exactos de `.page-hero` del
       dashboard de tiendapp. */
    --banner-from: #16132a;
    --banner-to:   #241e42;

    /* Texto */
    --text: #1c1830;
    --text-light: #5d5873;
    --text-faint: #8d88a3;

    /* ── Tipografía ──────────────────────────────────────────────────────
       SIETE pasos, nombrados por PAPEL y no por tamaño. Antes había catorce
       números sueltos repartidos en 1.585 líneas, elegidos de a uno: por eso
       las pantallas no se sentían una pieza — no lo eran.

       Los valores salen de medir el dashboard de tiendapp, que es la
       referencia visual. Ahí también están sueltos —diecinueve tamaños
       distintos en 2.247 líneas— así que lo que se porta no es su hoja sino
       los valores que de hecho funcionan, ya ordenados.

       El paso que más cambia es el cuerpo: 15px → 13px. Todo lo nuestro venía
       un 15% más grande, y esa sola cifra es la mitad de la diferencia.

       Nombrar por papel es lo que vuelve barato el cambio que viene: el día
       que una empresa quiera su propia densidad, se reasignan siete valores y
       no se buscan cuarenta en la hoja. */
    --fs-hero:    34px;   /* el saludo de la portada, y nada más */
    --fs-page:    22px;   /* el título de la pantalla, en el banner */
    --fs-number:  30px;   /* una cifra que ES el contenido */
    --fs-stat:    17px;   /* la cifra de la tira del escritorio: un vistazo,
                             no el contenido. Dos pasos por debajo de
                             `number` — la tira se lee de reojo, y a 22px
                             empujaba la píldora a partirse en dos filas */
    --fs-section: 13.5px; /* el título de una tarjeta o de una sección */
    --fs-body:    13px;   /* texto, celdas, campos, botones */
    --fs-small:   12px;   /* el subtítulo de algo, un dato de apoyo */
    --fs-label:   11.5px; /* el rótulo de un campo */
    --fs-meta:    11px;   /* ayuda, subtítulos, lo secundario */
    --fs-badge:   10px;   /* pastillas y estados, en versalitas */
    /* Un GLIFO, no un texto: el tamaño de un icono dibujado con una letra.
       Está en la escala porque si no queda como el único número suelto de la
       hoja, y un número suelto es por donde vuelven los otros cuarenta. */
    --fs-glyph:   16px;

    /* ── Espaciado ───────────────────────────────────────────────────────
       Seis pasos sobre una rejilla de 4. Antes había veinticinco valores
       distintos, del 1px al 54px, sin relación entre ellos.

       tiendapp usa 14 y 18 en varios sitios, que quedan fuera de la rejilla.
       Se redondean a 16 y 20: la diferencia no se ve, y una rejilla con dos
       excepciones deja de ser una rejilla al tercer componente. */
    --sp-1: 4px;
    --sp-2: 8px;
    --sp-3: 12px;
    --sp-4: 16px;
    --sp-5: 20px;
    --sp-6: 24px;
    --sp-7: 32px;
    /* El octavo escalón, que llegó con el árbol de la lista de etiquetas
       (2026-09-09). La escala es ORDINAL a partir del sexto —`--sp-7` son 32
       y no 28—, así que el que sigue es el ocho y no un `--sp-10`: los
       números nombran el peldaño, no los píxeles. */
    --sp-8: 40px;
    /* El respiro lateral del contenido. Es una variable y no un número
       dentro de `.content` porque la portada lo tiene que CANCELAR: los
       orbes van de borde a borde de la aplicación, y para eso necesitan
       saber cuánto padding deshacer. */
    --content-pad: clamp(14px, 2.4vw, 28px);

    /* Bordes y profundidad */
    --border: #e7e4ef;
    --border-strong: #d6d1e4;
    /* La banda del encabezado de una tabla, y la línea entre sus filas.

       Los dos son PROPIOS y no `--bg` y `--border` porque la tabla los usa
       en dosis distintas de todo lo demás: el fondo tiene que separarse del
       blanco de la tarjeta sin llegar a leerse como una fila, y la línea se
       repite quinientas veces —con `--border` la lista se lee como una reja
       y las filas dejan de ser filas—. Tenerlos nombrados es lo que deja
       ajustar la densidad de la tabla sin tocar el resto del ERP. */
    --table-head-bg: #faf9fc;
    --table-line: #f0eef6;
    /* Lo que ABRE una fila: la miniatura de un producto, la inicial de un
       tercero, la baldosa con el icono de una ubicación. Un solo tamaño
       para los tres (Renzo, 2026-08-14).

       Eran DOS familias con dos tamaños: `.cell-thumb` a 36px en cuatro
       plantillas —las que llevan foto— y `.tile-sm` a 26px en treinta y
       cinco. Puestas una debajo de otra al navegar entre listas, las filas
       cambiaban de altura de pantalla en pantalla sin que nada lo
       justificara: la diferencia no decía nada, solo se notaba.

       Token y no un literal porque hay TRES sitios que tienen que valer lo
       mismo: las dos familias y el corrimiento del rótulo de esa columna
       (`.th-lead`), que se calcula a partir de acá. Escritos como literales,
       cambiar el tamaño desalinea el encabezado y nada lo avisa. */
    --cell-thumb: 35px;

    /* El alto de un botón de fila, y el de la BANDA que tiene que poder
       contenerlo.

       Derivado y no dos números sueltos: la banda existe para llevar un
       título y, a veces, los verbos de esa tarjeta. Si el botón crece y la
       banda no, o el botón se sale o la banda da un salto al aparecer — que
       es exactamente lo que pasó el 2026-08-14 al subir `.btn-small` de 22
       a 30: la banda de selección medía 55 y el encabezado que reemplaza
       46, y la tabla brincaba nueve píxeles en cada clic.

       Con el alto atado al del botón, cambiar el tamaño de los botones de
       fila reajusta las bandas solo. El `+ 1px` es el borde inferior. */
    /* El botón de pantalla mide 38 por padding + interlínea; el nombre existe
       para que los tres puntos de la banda midan LO MISMO sin repetir la
       cuenta. */
    --btn-h: 38px;
    --btn-small-h: 30px;
    --band-h: calc(var(--btn-small-h) + var(--sp-3) * 2 + 1px);
    --radius: 12px;
    /* Existía en dos pantallas como referencia sin definición —el navegador
       ignoraba el `border-radius` entero y nadie lo notó. Definida el
       2026-08-08 al armar la de reseñas. */
    --radius-sm: 8px;
    --radius-lg: 20px;
    /* El radio de una TARJETA. Era `--radius-lg` (20px) y se veía blando al
       lado de la referencia; tiendapp usa 12px en todas. `--radius-lg` se
       queda para lo que de verdad es grande —el drawer, el shell— porque ahí
       sí lo pide la escala de la pieza. */
    --radius-card: 12px;
    /* El de un control. Más cerrado que el de la tarjeta que lo contiene: al
       revés, el campo se come visualmente su propia caja. */
    --radius-field: 10px;
    --shadow: 0 1px 3px rgba(16, 18, 22, .05), 0 8px 24px rgba(16, 18, 22, .06);
    --shadow-lg: 0 2px 6px rgba(16, 18, 22, .06), 0 18px 48px rgba(16, 18, 22, .10);
    /* ── El vidrio de la paleta (Renzo, 2026-09-15) ──────────────────────
       La paleta de ⌘K flota SOBRE la pantalla y no dentro de ella: es la
       única superficie del ERP que se pinta oscura encima de lo que haya
       debajo, así que trae su propio vocabulario en vez de torcer los
       tokens claros de las tarjetas. Si el vidrio no queda, se van estos
       siete y la paleta vuelve a ser una tarjeta. */
    /* 62 % y no 78 (Renzo, 2026-09-15): con más transparencia el desenfoque
       de atrás se nota, que es de lo que vive el vidrio. */
    --glass-bg: rgb(26 24 40 / 62%);
    --glass-line: rgb(255 255 255 / 12%);
    --glass-soft: rgb(255 255 255 / 8%);
    --glass-hover: rgb(255 255 255 / 6%);
    --glass-text: #f4f2ff;
    --glass-text-soft: rgb(244 242 255 / 62%);
    --glass-activa: rgb(124 92 240 / 42%);
    /* Va HACIA ARRIBA: el contenedor del ERP flota sobre el lienzo oscuro y
       la sombra tiene que despegarlo de la topbar, no hundirlo. Ninguna de
       las dos de arriba sirve, y un literal en el shell dejaría la única
       sombra del ERP fuera del sistema. */
    --shadow-shell: 0 -2px 40px -20px rgba(0, 0, 0, .5);

    /* Semántica: cada estado lleva su par fondo/texto */
    /* Fortaleza de contraseña. Son tonos de BARRA, no de texto: sobre blanco
       dan entre 1.6 y 3.1 de contraste, suficiente para una forma e ilegible
       para una palabra. Por eso el nivel no se escribe, se cuenta.
       El nivel 2 interpola entre el rojo y el ámbar para que la rampa de
       cuatro pasos no salte. */
    --strength-1: #ff5a4c;
    --strength-2: #ff823e;
    --strength-3: #ffaa30;
    --strength-4: #3de775;

    --ok-bg: #e8f6ec;      --ok-text: #1f7a3d;
    /* El verde de «esto ya está» en los medidores (Integrantes, inducciones):
       más lima que el `--ok-text` de las pastillas, que Renzo veía «muy
       pasto» (2026-09-14). Dos tonos: el claro para la barra y la baldosa,
       el oscuro para la cifra, que tiene que leerse sobre blanco. */
    --done: #53cf54;       --done-text: #2e9a2f;
    --warn-bg: #fff6dc;    --warn-text: #9a7b00;
    --danger-bg: #fdebeb;  --danger-text: #b3382e;
    --info-bg: #eaf2fd;    --info-text: #2c66ad;

    /* Los mismos cuatro estados, VIVOS: los de la pastilla (Renzo,
       2026-08-14). Son un juego aparte y no un reemplazo porque los de
       arriba tiñen superficies grandes —el fondo de un aviso, el de una fila
       degradada— y a ese tamaño un tono saturado grita. La pastilla es del
       tamaño de una palabra: ahí el tono pálido no se lee como estado, se
       lee como texto con un halo.

       El texto es más oscuro que el de la familia pálida, y no por gusto: el
       fondo vivo sube la luminancia, y con `--ok-text` sobre `#7effa3` el
       contraste cae a 4.28 — por debajo del 4.5 que pide un texto de 12px.
       Con estos pares queda entre 5.3 y 7.1, medido, los cuatro. Un estado
       que no se puede leer no es un estado. */
    --ok-vivid: #7effa3;      --ok-vivid-text: #0f5527;
    --warn-vivid: #ffe27a;    --warn-vivid-text: #6b4e00;
    --danger-vivid: #ffb3ad;  --danger-vivid-text: #8c1f16;
    --info-vivid: #a8d4ff;    --info-vivid-text: #0f4a8a;
    /* La pastilla SIN modificador. Va acá, con las otras cuatro, y no como
       un hex suelto dentro de la regla: el día que el tema cambie, se cambia
       en un solo sitio junto a sus hermanas. */
    --pill-bg: #ece9f6;    --pill-text: #514c66;

    --font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}

/* ── Base ──────────────────────────────────────────────────────────────── */

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

body {
    margin: 0;
    font-family: var(--font);
    font-size: var(--fs-body);
    line-height: 1.55;
    color: var(--text);
    background: var(--bg);
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
}

a { color: var(--primary-dark); text-decoration: none; }
/* Sin subrayado al pasar por encima. Con la mitad de los enlaces del ERP
   pintados como botones —tarjetas, íconos de acción, pastillas— el subrayado
   solo aparecía en unos pocos y se leía como un enlace roto entre los demás.
   El estado se lo dan el color y el fondo, que ya cambian en `:hover`. */
a:hover { text-decoration: none; }

/* ── Orbes de fondo ────────────────────────────────────────────────────────
   `mix-blend-mode: multiply` es lo que los hace funcionar sobre fondo claro:
   tiñen el lienzo en vez de flotar encima como manchas. Con `normal` se ven
   sucios y con opacidad alta, lavados.
   ------------------------------------------------------------------------ */

.orbs {
    position: fixed;
    inset: -10vh -10vw;
    z-index: 0;
    pointer-events: none;
    overflow: hidden;
}

.orb {
    position: absolute;
    border-radius: 50%;
    filter: blur(58px);
    mix-blend-mode: multiply;
    will-change: transform;
}

.orb-lavender {
    width: 34vmax; height: 34vmax; top: 40%; left: 12%; opacity: .26;
    background: radial-gradient(circle, #ded4ff 0%, #bda9ff 48%, transparent 72%);
    animation: orb-drift-a 19s ease-in-out infinite;
}
.orb-cyan {
    width: 28vmax; height: 28vmax; top: 2%; right: 2%; opacity: .42;
    background: radial-gradient(circle, #7ee8f2 0%, #3ec9de 42%, transparent 70%);
    animation: orb-drift-b 23s ease-in-out infinite;
}
.orb-yellow {
    width: 22vmax; height: 22vmax; top: -14%; left: 4%; opacity: .36;
    background: radial-gradient(circle, #fff08a 0%, #ffd84d 50%, transparent 88%);
    animation: orb-drift-c 17s ease-in-out infinite;
}
.orb-violet {
    width: 26vmax; height: 26vmax; bottom: -8%; right: 18%; opacity: .32;
    background: radial-gradient(circle, #ff48f3a6 0%, #f05ce4a8 45%, transparent 70%);
    animation: orb-drift-d 21s ease-in-out infinite;
}
.orb-glow {
    width: 36vmax; height: 36vmax; top: 0; left: 50%;
    transform: translateX(-50%); opacity: .30;
    background: radial-gradient(circle, rgba(201, 184, 255, .9) 0%,
                rgba(126, 232, 242, .35) 38%, transparent 66%);
    filter: blur(70px);
    mix-blend-mode: normal;
    animation: orb-pulse 9s ease-in-out infinite;
}

@keyframes orb-drift-a {
    0%, 100% { transform: translate(0, 0) scale(1); }
    33%      { transform: translate(5vw, 4vh) scale(1.06); }
    66%      { transform: translate(-3vw, -3vh) scale(.95); }
}
@keyframes orb-drift-b {
    0%, 100% { transform: translate(0, 0) scale(1); }
    33%      { transform: translate(-6vw, 5vh) scale(1.08); }
    66%      { transform: translate(3vw, -2vh) scale(.94); }
}
@keyframes orb-drift-c {
    0%, 100% { transform: translate(0, 0) scale(1); }
    50%      { transform: translate(4vw, 5vh) scale(1.12); }
}
@keyframes orb-drift-d {
    0%, 100% { transform: translate(0, 0) scale(1); }
    33%      { transform: translate(-4vw, -5vh) scale(1.05); }
    66%      { transform: translate(3vw, 2vh) scale(.97); }
}
@keyframes orb-pulse {
    0%, 100% { opacity: .22; transform: translateX(-50%) scale(1); }
    50%      { opacity: .34; transform: translateX(-50%) scale(1.1); }
}

/* ── Pantallas de autenticación ────────────────────────────────────────── */

.auth-stage {
    position: relative;
    z-index: 1;
    min-height: 100vh;
    min-height: 100dvh;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: calc(32px + var(--sat)) 20px calc(32px + var(--sab));
}

.auth-card {
    width: 100%;
    max-width: 408px;
    background: var(--card-bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg);
    padding: 40px 36px 32px;
    animation: card-rise .55s cubic-bezier(.16, 1, .3, 1) both;
}

@keyframes card-rise {
    from { opacity: 0; transform: translateY(16px); }
    to   { opacity: 1; transform: translateY(0); }
}

.auth-brand {
    display: flex;
    align-items: center;
    gap: 9px;
    margin-bottom: 26px;
}
/* El logo de verdad, desde el 2026-08-04. Antes había un cuadrado con
   degradado y la palabra al lado: un marcador de posición que se quedó
   desde que existe la pantalla. */
.auth-brand .brand-logo { height: 30px; }

.auth-title {
    font-size: var(--fs-page);
    font-weight: 700;
    letter-spacing: -.5px;
    margin: 0 0 6px;
}
.auth-subtitle {
    color: var(--text-light);
    font-size: var(--fs-body);
    margin: 0 0 26px;
}

/* El correo con el que se está entrando, en los pasos 2 y 3: ya se escribió
   en el paso 1 y no se vuelve a pedir, pero tiene que estar A LA VISTA — es
   la única forma de darse cuenta de un dedazo antes de pelearse con una
   contraseña que sí era la correcta. */
.auth-who {
    font-size: var(--fs-small);
    color: var(--text-light);
    margin: 0 0 18px;
}
.auth-who a { margin-left: 8px; }

/* Un botón que se lee como enlace. Existe porque «entrar con mi contraseña»
   tiene que MANDAR el correo que ya se escribió —y eso es un POST— pero no
   es una acción principal: pintarlo de botón compite con «Entrar». */
.btn-link {
    background: none;
    border: 0;
    padding: 0;
    font: inherit;
    color: var(--primary);
    cursor: pointer;
    text-decoration: underline;
}

/* La otra puerta: es un formulario aparte —no se puede anidar uno dentro de
   otro— así que sin esto los dos botones quedan pegados. */
.auth-alt { margin-top: 10px; }

.auth-foot {
    margin: 18px 0 0;
    text-align: center;
    font-size: var(--fs-small);
}

/* Reveal escalonado: la tarjeta sube, y su contenido aparece detrás. */
.auth-card > * { animation: fade-up .45s ease both; }
.auth-card > *:nth-child(1) { animation-delay: .10s; }
.auth-card > *:nth-child(2) { animation-delay: .16s; }
.auth-card > *:nth-child(3) { animation-delay: .21s; }
.auth-card > *:nth-child(4) { animation-delay: .26s; }
.auth-card > *:nth-child(5) { animation-delay: .30s; }

@keyframes fade-up {
    from { opacity: 0; transform: translateY(7px); }
    to   { opacity: 1; transform: translateY(0); }
}

/* ── Formularios ───────────────────────────────────────────────────────── */

.field { margin-bottom: 17px; }

.field label {
    display: block;
    font-size: var(--fs-small);
    font-weight: 600;
    color: var(--text-light);
    margin-bottom: 7px;
}
/* El rótulo y su `(i)` en una fila. El rótulo es `display: block`, así que sin
   esto el ícono caía al renglón siguiente y quedaba flotando sobre el control.
   El `margin-bottom` se muda del rótulo a la fila, o se sumaban dos. */
.field-label { display: flex; align-items: center; gap: 6px; margin-bottom: 7px; }
.field-label > label { margin-bottom: 0; }

/* Por ELEMENTO y no por `.field`, y es la tercera vez que esta forma aparece.

   Estaba como `.field input[...]`, así que un `<input>` que no viviera dentro de
   un `.field` caía al control por omisión del navegador — más angosto, con otra
   letra y sin el foco del ERP. Ya pasó con el filtro de texto de la F1-b, que
   por eso tiene su propia guarda, y volvió a pasar con la casilla de comentario
   de la ficha del tercero. Dos veces es casualidad; tres es el selector.

   El ancho es lo único que se queda en `.field`: adentro de un campo el control
   llena su columna, pero en una fila —la casilla de comentario al lado de su
   botón— es el `flex` quien decide, y un `width: 100%` ahí lo empuja fuera. */
/* `number` e `input:not([type])` entran los dos, y llegaron por caminos
   distintos a la MISMA forma de defecto: la regla estaba escrita y no alcanzaba
   al control.

   `number` lo midió la ficha del producto —el monto de una línea de costo salía
   con el borde gris del navegador, radio 0 y 22px de alto contra los 34 del
   concepto de al lado—. Son once controles en tres pantallas.

   `input:not([type])` lo midió el editor de formularios: un `<input>` sin
   atributo `type` ES un campo de texto —lo dice el navegador— pero
   `input[type="text"]` NO lo selecciona, y hay 24 escritos así. El título del
   paso medía 240px al lado de un textarea de ancho completo, sin padding y sin
   foco.

   Que dos fases lo encontraran por separado es el dato: la lista de selectores
   es la que se queda atrás cada vez que alguien escribe un control nuevo. */
input[type="email"],
input[type="text"],
input[type="number"],
input[type="password"],
input[type="datetime-local"],
input:not([type]) {
    padding: var(--sp-2) var(--sp-3);
    font-family: inherit;
    font-size: var(--fs-body);
    color: var(--text);
    background: #fff;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-field);
    outline: none;
    transition: border-color .16s, box-shadow .16s;
}
/* `number` NO va acá: su `width` ya lo fija el bloque de controles del final,
   y repetirlo es exactamente lo que `test_no_selector_sets_the_same_property_in_two_places`
   existe para impedir. */
.field input[type="email"],
.field input[type="text"],
.field input[type="password"],
.field input[type="datetime-local"],
.field input:not([type]) { width: 100%; }

input[type="email"]:focus,
input[type="text"]:focus,
input[type="number"]:focus,
input[type="password"]:focus,
input[type="datetime-local"]:focus,
input:not([type]):focus {
    border-color: var(--primary);
    box-shadow: 0 0 0 3.5px var(--primary-soft);
}

input[aria-invalid="true"] {
    border-color: var(--danger-text);
}

/* Las reglas de contraseña las emite AUTH_PASSWORD_VALIDATORS como un <ul>
   ya armado (password_validators_help_text_html), así que el estilo tiene que
   apuntar a esa lista y no a un marcado propio. Se muestran como ayuda antes
   de escribir, no como reproche después. */
/* La ayuda que escribe la plantilla. `.field-help` es la del `help_text` del
   widget —que en la pantalla de contraseña trae una lista dentro— y por eso
   siguen siendo dos clases; comparten la tipografía en una sola regla para
   que no se separen. `.field-hint` venía de la F1-a sin ninguna. */
/* La etiqueta de un campo con un control a su derecha. Lo estrena la ficha del
   artículo: el interruptor «Editar como HTML» tiene que estar PEGADO a lo que
   gobierna —el cuerpo— y no perdido al final del formulario, porque la
   pregunta que responde es «con qué edito esto». */
/* Dos casillas que son UN filtro: desde/hasta, mínimo/máximo. Lado a lado
   porque son los dos extremos de lo mismo — una debajo de la otra las lee
   como dos preguntas distintas. En el celular caen a una columna. */
.field-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
@media (max-width: 600px) { .field-pair { grid-template-columns: 1fr; } }

.field-head {
    display: flex; align-items: center; justify-content: space-between;
    gap: 12px; margin-bottom: 6px; flex-wrap: wrap;
}
.field-head label { margin-bottom: 0; }

.field-hint, .field-help {
    font-size: var(--fs-small); line-height: 1.45; color: var(--text-light);
}
.field-hint { margin: 6px 0 0; }
.field-help { margin-top: 9px; }
.field-help ul {
    margin: 0;
    padding: 0;
    list-style: none;
    font-size: var(--fs-small);
    color: var(--text-faint);
    line-height: 1.55;
}
.field-help li { padding-left: 15px; position: relative; }
.field-help li::before {
    content: '';
    position: absolute;
    left: 3px; top: 8px;
    width: 4px; height: 4px;
    border-radius: 50%;
    background: var(--border-strong);
}

.field-error {
    margin: 7px 0 0;
    font-size: var(--fs-small);
    font-weight: 500;
    color: var(--danger-text);
}

/* ── Fortaleza de contraseña ───────────────────────────────────────────────
   El estado lo calcula el servidor (kernel/identity/password_feedback.py) con
   los MISMOS validadores que rechazan al enviar. Aquí solo se pinta.
   ------------------------------------------------------------------------ */

.pw-feedback { margin-top: 10px; }
.pw-feedback:empty { display: none; }

.pw-strength {
    display: flex;
    gap: 5px;
}

.pw-seg {
    flex: 1;
    height: 5px;
    border-radius: 99px;
    background: var(--border);
    transition: background .28s ease;
}

/* El nivel llena tantos segmentos como su número. La CANTIDAD es la señal
   que sobrevive a la ceguera al color; el tono solo la refuerza. */
.pw-strength[data-level="1"] .pw-seg:nth-child(-n+1) { background: var(--strength-1); }
.pw-strength[data-level="2"] .pw-seg:nth-child(-n+2) { background: var(--strength-2); }
.pw-strength[data-level="3"] .pw-seg:nth-child(-n+3) { background: var(--strength-3); }
.pw-strength[data-level="4"] .pw-seg:nth-child(-n+4) { background: var(--strength-4); }

/* Solo se listan las reglas pendientes: una que se cumple, desaparece. */
.pw-rules {
    margin: 11px 0 0;
    padding: 0;
    list-style: none;
    font-size: var(--fs-small);
    line-height: 1.45;
    color: var(--text-light);
}
.pw-rules li {
    display: flex;
    align-items: flex-start;
    gap: 7px;
    padding: 2.5px 0;
    animation: rule-in .2s ease both;
}
.pw-rules svg {
    width: 13px; height: 13px;
    flex-shrink: 0;
    margin-top: 3px;
    color: var(--danger-text);
}

@keyframes rule-in {
    from { opacity: 0; transform: translateY(-2px); }
    to   { opacity: 1; transform: translateY(0); }
}

.pw-ready {
    display: flex;
    align-items: center;
    gap: 7px;
    margin: 11px 0 0;
    font-size: var(--fs-small);
    font-weight: 600;
    color: var(--ok-text);
    animation: rule-in .22s ease both;
}
.pw-ready svg { width: 14px; height: 14px; flex-shrink: 0; }

.alert {
    display: flex;
    gap: 9px;
    padding: 11px 13px;
    border-radius: var(--radius);
    font-size: var(--fs-body);
    font-weight: 500;
    margin-bottom: 20px;
}
.alert-danger { background: var(--danger-bg); color: var(--danger-text); }
.alert-warn   { background: var(--warn-bg);   color: var(--warn-text); }
.alert-ok     { background: var(--ok-bg);     color: var(--ok-text); }
.alert svg { flex-shrink: 0; margin-top: 1px; }

/* ── Botones ───────────────────────────────────────────────────────────── */

/* 12px, decidido mirando la pantalla. El escalón de arriba —14.5— dejaba los
   controles pesados al lado del texto de la interfaz.

   Los campos de TEXTO no bajaron con ellos, y eso no es una inconsistencia:
   iOS Safari hace zoom a la página al enfocar un campo de menos de 16px, así
   que ahí el tamaño no es una decisión estética sino una página que salta y
   hay que volver a alejar a mano.

   El relleno se queda en 12px/20px: bajar la letra sin bajar el relleno es lo
   que mantiene el botón por encima de los 40px de alto, que es lo que un
   pulgar necesita. */
/* ── El sistema de botones ─────────────────────────────────────────────────
   TRES tamaños y UN radio (Renzo, 2026-08-14). Antes había dos tamaños con
   ocho píxeles de diferencia y ningún criterio escrito de cuándo usar cuál,
   así que la misma acción salía chica en una pantalla y grande en la de al
   lado.

       .btn            38px   la acción de una pantalla o de un formulario
       .btn-small      30px   dentro de una fila, una banda o una tarjeta
       .btn-icon       30px   cuadrado, solo glifo, con `aria-label`

   El RADIO es `--radius-field` y no `--radius`: aquél es el token de un
   CONTROL —el mismo del campo de texto y del `select`— y éste el de una
   TARJETA. Un botón al lado de un campo con dos radios distintos se lee
   como dos sistemas; con el mismo, como una fila de controles. Es el mismo
   argumento que ya está escrito en `--radius-field`.
   ------------------------------------------------------------------------ */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--sp-2);
    padding: 11px var(--sp-5);
    font-family: inherit;
    font-size: var(--fs-body);
    font-weight: 600;
    line-height: 1;
    border: 1px solid transparent;
    border-radius: var(--radius-field);
    cursor: pointer;
    transition: background .16s, box-shadow .16s, transform .08s;
}
/* El glifo de un botón se dimensiona ACÁ y no en el `<svg>` de cada plantilla:
   `{% icon %}` emite los trazos y quien llama pone el elemento, así que sin
   esta regla cada botón elegiría su propio tamaño. `flex: none` porque el
   `.btn` es flex y un `<svg>` sin ancho intrínseco se deja aplastar por el
   texto largo — «Empezar preparación» le comía el icono. */
.btn svg { width: 15px; height: 15px; flex: none; }

/* `width: auto`, que es lo que un botón es.

   Estaba en `100%` por omisión —salió de la pantalla de entrar, donde el
   botón sí ocupa la tarjeta— y dos reglas lo devolvían a `auto` para el
   formulario y para el banner. La tercera pantalla que lo usó sin saberlo fue
   la barra de filtros, y ahí «Filtrar» salió como una franja morada de lado a
   lado. Una omisión que necesita excepciones es la omisión equivocada: ahora
   el que quiere ocupar todo lo pide con `btn-block`. */
.btn-primary {
    color: #fff;
    background: var(--primary);
    box-shadow: 0 2px 8px rgba(124, 92, 240, .28);
}
.btn-primary:hover  { background: var(--primary-dark); box-shadow: 0 4px 14px rgba(124, 92, 240, .34); }
.btn-primary:active { transform: translateY(1px); }

/* El que SÍ ocupa todo el ancho, y lo pide. Es el de entrar y el de aceptar
   una invitación: pantallas de una sola tarjeta angosta donde el botón es la
   única acción que hay. */
.btn-block { width: 100%; }

.btn-ghost {
    color: var(--text-light);
    background: transparent;
    border-color: var(--border-strong);
}
/* ── El botón de AGREGAR UNO MÁS ───────────────────────────────────────────
   Punteado y en tinta de marca. El punteado es la convención de «acá cabe otro
   y todavía no está»: un borde entero promete una cosa que ya existe.

   No es un tono nuevo por capricho — es lo que resuelve una competencia real.
   El botón de agregar vive DENTRO de una tarjeta, y en primario competía con
   «Guardar» de la barra, que es el único que cierra la ficha; en fantasma se
   perdía contra el panel gris en el que ahora está parado. */
.btn-dashed {
    color: var(--primary-dark);
    background: transparent;
    border-style: dashed; border-color: var(--primary);
}
.btn-dashed:hover { background: var(--primary-soft); }
/* El fondo se OSCURECE, no se aclara. El hover viejo pintaba `#fff` sobre
   tarjetas que ya son blancas: no se notaba nada, y por eso parecía que el
   botón no reaccionaba. Texto blanco tampoco sirve acá —`.btn-ghost` es
   transparente y son 110 pantallas sobre fondo claro— así que el estado lo da
   el fondo. Los del banner y los de la savebar tienen su propia regla porque
   viven sobre oscuro. */
.btn-ghost:hover { background: var(--bg); color: var(--text); }

.btn:focus-visible {
    outline: none;
    box-shadow: 0 0 0 3.5px var(--primary-soft);
}

/* ── El shell del ERP ──────────────────────────────────────────────────────
   Portado del dashboard de tiendapp (static/dashboard/css/style.css:404-520).
   `--canvas` lleva declarado desde P0-a sin que lo usara nadie: es esto.

   Lo que había acá antes —`.shell*` de la landing, `.placeholder-card` y
   `.module-slots`— eran el andamio con las ocho ranuras escritas a mano. Se
   pusieron para que se vieran al reemplazarlas, y esto es reemplazarlas.
   ------------------------------------------------------------------------ */

:root { --sidebar: 250px; --rail: 68px; --topbar: 64px; }

body.app { background: var(--canvas); }

.topbar {
    position: fixed; inset: 0 0 auto 0; height: var(--topbar);
    display: flex; align-items: center; justify-content: space-between;
    gap: 12px; padding: 0 clamp(14px, 3vw, 32px);
    background: var(--canvas); color: #fff; z-index: 100;
}
.topbar-left { display: flex; align-items: center; gap: 10px; min-width: 0; }
/* El logo, con la altura como única medida: el `<svg>` no trae `width` ni
   `height` y el ancho sale de su relación de aspecto. `currentColor` es lo
   que pinta el wordmark, así que el color se hereda de donde esté puesto —
   blanco en la barra oscura, tinta en la tarjeta de entrar. */
.brand-logo { display: block; width: auto; }
.topbar-brand .brand-logo { height: 30px; }
.topbar-brand {
    display: inline-flex; align-items: center;
    color: inherit; text-decoration: none; flex-shrink: 0;
}
.topbar-brand:focus-visible {
    outline: 2px solid #fff; outline-offset: 4px; border-radius: 4px;
}
.topbar-crumb {
    color: rgba(255, 255, 255, .62); font-size: var(--fs-body);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.topbar-crumb a { color: inherit; }
.topbar-crumb strong { color: #fff; font-weight: 600; }
.topbar-collapse { display: flex; margin: 0; }

.icon-btn {
    display: grid; place-items: center;
    width: 32px; height: 32px; padding: 0;
    color: rgba(255, 255, 255, .8); background: transparent;
    border: 0; border-radius: 9px; cursor: pointer; font-size: var(--fs-glyph);
}
.icon-btn:hover { background: rgba(255, 255, 255, .1); color: #fff; }
.icon-btn:focus-visible { outline: 2px solid #fff; outline-offset: 1px; }

.user-menu { position: relative; }
.user-menu summary {
    display: flex; align-items: center; gap: 9px;
    padding: 5px 9px 5px 5px; border-radius: 99px; cursor: pointer;
    list-style: none; color: #fff; font-size: var(--fs-body); font-weight: 600;
}
.user-menu summary::-webkit-details-marker { display: none; }
.user-menu summary:hover { background: rgba(255, 255, 255, .1); }
.user-menu summary:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
.user-menu-name {
    max-width: 22ch; overflow: hidden; text-overflow: ellipsis;
    white-space: nowrap;
}
.avatar {
    width: 32px; height: 32px; flex-shrink: 0;
    display: grid; place-items: center;
    border-radius: 50%; font-size: var(--fs-small); font-weight: 700; color: #fff;
    background: linear-gradient(135deg, var(--primary) 0%, #a78bfa 100%);
}
/* La foto en el círculo de cualquier avatar (`ui/_avatar.html`): la clase
   del tamaño la pone quien llama; esta solo evita que una foto vertical se
   aplaste. */
.avatar-foto { object-fit: cover; }
/* La cara de la empresa en el disparador (`ui/_company_mark.html`): un
   cuadrado con esquinas y no el círculo del avatar, porque un logo recortado
   en círculo es otro logo. Con logo, fondo blanco y `contain`: el archivo se
   ve entero, sea cuadrado u horizontal; sin logo, la inicial sobre el
   degradado de la casa. */
.company-mark {
    width: 32px; height: 32px; flex-shrink: 0;
    display: grid; place-items: center;
    border-radius: 20px; font-size: var(--fs-small); font-weight: 700; color: #fff;
    background: linear-gradient(135deg, var(--primary) 0%, #a78bfa 100%);
}
/* Redondo y sin relleno ni fondo blanco (Renzo, 2026-09-14): el logo llena
   el círculo entero, como la foto de un avatar. */
img.company-mark-logo { object-fit: contain; padding: 0; box-sizing: border-box; }
/* El menú del usuario es un `.menu-pop` más (misma caja, mismos ítems); lo
   suyo es la cabecera de quién es —foto, nombre y correo— que abre el bloque
   personal, debajo del bloque de la empresa. */
.user-menu-pop { top: calc(100% + 8px); min-width: 240px; }
.user-menu-who {
    display: grid; grid-template-columns: auto minmax(0, 1fr);
    column-gap: 10px; row-gap: 1px; align-items: center;
    padding: 8px 10px 6px;
}
.user-menu-who .avatar { grid-row: 1 / span 2; width: 30px; height: 30px; }
.user-menu-nombre { font-size: var(--fs-body); font-weight: 600; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.user-menu-correo { font-size: var(--fs-small); color: var(--text-light); overflow: hidden; text-overflow: ellipsis; }
/* ── La campanita de los avisos ─────────────────────────────────────────
   El disparador es el mismo `<summary>` blanco de la topbar que ya tiene el
   menú de la casa; lo suyo es el glifo y el punto. El desplegable hereda de
   `.menu-pop` la caja entera (fondo, borde, sombra, posición): acá solo se
   le da el ancho y el desplazamiento, que es lo único que una lista de
   avisos necesita y un menú de cuatro filas no. */
.notices { position: relative; }
.notices summary {
    display: flex; align-items: center; padding: 7px; border-radius: 99px;
    list-style: none; cursor: pointer; color: #fff; position: relative;
}
.notices summary::-webkit-details-marker { display: none; }
.notices summary:hover { background: rgba(255, 255, 255, .1); }
.notices summary:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
/* El eje de giro arriba, donde cuelga la campana: girada por el centro se
   ve como un glifo que tiembla, no como una campana que suena. */
.notices summary svg { width: 20px; height: 20px; transform-origin: 50% 3px; }
/* MIENTRAS HAYA ALGO SIN LEER, LA CAMPANA VIBRA: un vaivén corto cada
   cinco segundos, el resto del ciclo quieta. `:has(.notices-dot)` porque el
   punto solo existe con avisos. Un punto quieto en la esquina de una barra
   oscura no lo ve nadie —la gente no se enteraba de que le habían
   escrito—, y un anillo que late en rojo con opacidad sobre negro tampoco
   («no se ve casi… mejor un buzz o algo que se mueva en la campanita»,
   Renzo, 2026-09-22). Cuando llega uno nuevo, `shell-live.js` reinicia la
   animación para que vibre en ese instante y le pone `is-nueva` al
   `<summary>` para que el punto salte. El bloque universal de movimiento
   reducido apaga las dos. */
.notices summary:has(.notices-dot) svg { animation: campana-vibra 5s ease-in-out infinite; }
.notices summary.is-nueva .notices-dot { animation: campana-salta .6s cubic-bezier(.2, 1.4, .4, 1); }
@keyframes campana-vibra {
    0%, 18%, 100% { transform: rotate(0deg); }
    3% { transform: rotate(16deg); }
    6% { transform: rotate(-14deg); }
    9% { transform: rotate(10deg); }
    12% { transform: rotate(-6deg); }
    15% { transform: rotate(3deg); }
}
@keyframes campana-salta {
    0% { transform: scale(.3); }
    60% { transform: scale(1.3); }
    100% { transform: scale(1); }
}
/* El punto se apoya en la esquina del glifo. `--danger-text` es el mismo
   rojo del `.nav-badge.tone-danger`: dos rojos para «tienes algo pendiente»
   sería el ERP hablando con dos voces. */
.notices-dot {
    position: absolute; top: 2px; right: 2px;
    min-width: 17px; padding: 0 4px; border-radius: 99px;
    background: var(--danger-text); color: #fff;
    font-size: var(--fs-badge); font-weight: 700; line-height: 17px;
    text-align: center;
}
/* 360 y no 320: con 320 «Renzo: Te aparece notificación en la campanita»
   se cortaba en la segunda palabra útil. `min()` con el ancho de la
   ventana para que en el teléfono no se salga por la izquierda. */
.notices-pop { top: calc(100% + 8px); width: min(360px, calc(100vw - 24px));
    max-height: min(70vh, 540px); overflow-y: auto; padding: 4px; }
/* EL TEXTO ENVUELVE. `.menu-pop` fija `white-space: nowrap` —lo suyo son
   filas de una línea— y está MÁS ABAJO en la hoja que `.notices-pop`, así
   que con la misma especificidad ganaba y el aviso salía cortado por el
   borde derecho («…en la campanita de ag», Renzo, 2026-09-22). Dos clases
   le ganan por peso, no por orden. */
.notices .notices-pop { white-space: normal; }
/* El vacío de la campanita: el estado vacío de la casa, centrado y con la
   campana, pero sin el aire que `.empty` reserva para un botón que acá no
   hay — ese hueco de abajo era lo que se veía «raro». */
.notices-pop .empty { padding: 22px 16px 18px; }
.notices-pop .empty-text { margin-bottom: 0; font-size: var(--fs-small); }

.avisos-lista { list-style: none; margin: 0; padding: 0; }
.avisos-item + .avisos-item { border-top: 1px solid var(--border); }
/* Tres columnas: la inicial a la izquierda ocupando las dos filas, el texto
   en el medio y a la derecha lo pequeño —la hora arriba, el contador
   abajo—, como una lista de chats. `minmax(0, 1fr)` y no `1fr`: `1fr` es
   `minmax(auto, 1fr)` y una palabra larga estiraría la columna por fuera
   de la caja en vez de cortarse. */
.avisos-item > a {
    display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
    column-gap: 10px; row-gap: 2px; align-items: start;
    padding: 10px; border-radius: var(--radius-sm);
    color: var(--text); text-decoration: none;
}
.avisos-item > a:hover { background: var(--primary-soft); }
/* La inicial del título con el `.avatar` de la casa, a lo alto de las dos
   filas. Sin glifo por `kind`: el kernel no sabe qué módulo publica cada
   familia y la guarda del shell se lo prohíbe. */
.avisos-item > a > .avatar { grid-row: 1 / span 2; }
.avisos-title {
    font-size: var(--fs-body); font-weight: 600; line-height: 1.35;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.avisos-when { font-size: var(--fs-meta); color: var(--text-faint); white-space: nowrap; padding-top: 2px; }
/* Dos líneas y puntos suspensivos: el aviso trae el último mensaje entero
   y un párrafo de WhatsApp haría la fila de diez renglones. */
.avisos-text {
    grid-column: 2; font-size: var(--fs-small); color: var(--text-light);
    line-height: 1.4; overflow: hidden; display: -webkit-box;
    -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow-wrap: anywhere;
}
.avisos-count {
    grid-column: 3; justify-self: end; min-width: 20px; padding: 1px 6px;
    border-radius: 99px; background: var(--primary); color: #fff;
    font-size: var(--fs-meta); font-weight: 700; text-align: center;
}

/* La ranura vacía no ocupa lugar: existe para que el JS tenga dónde
   escribir, no para reservar un hueco que se vea. */
.badge-slot:empty { display: none; }
/* El `display` del sub-ítem vive en SU bloque (`.nav-subitem`, más abajo):
   declarado acá perdía contra el `block` de allá y la ranura quedaba pegada
   al texto en vez de irse a la derecha. */
.nav-subitem .badge-slot { margin-left: auto; }

/* Las filas del menú son `_btn.html` con `extra_class="menu-item"`: lo de
   botón (borde, alto, centrado) se apaga y queda la fila del menú. */
.menu-pop .btn.menu-item {
    height: auto; min-height: 0; border: 0; box-shadow: none;
    justify-content: flex-start; font-weight: 500;
}
.menu-pop .btn.menu-item:hover { background: var(--primary-soft); color: var(--primary-dark); border: 0; }

.shell {
    max-width: 1480px; margin: var(--topbar) auto 0;
    min-height: calc(100dvh - var(--topbar));
    display: flex; overflow: clip;
    background: var(--card-bg);
    border-radius: 24px 24px 0 0;
    box-shadow: var(--shadow-shell);
}

.sidebar {
    flex: none; width: var(--sidebar);
    padding: 14px 10px; border-right: 1px solid var(--border);
    position: sticky; top: var(--topbar);
    height: calc(100dvh - var(--topbar)); overflow-y: auto;
}

/* ── El panel lateral que un módulo publica ────────────────────────────── */
:root { --side-panel: 380px; }

.topbar-right { display: flex; align-items: center; gap: 4px; }
.topbar-sep { width: 1px; height: 24px; background: rgba(255, 255, 255, .22); margin: 0 6px; }
.side-panel-form { display: inline-flex; }
.side-panel-toggle { width: 38px; height: 38px; padding: 5px; }
.side-panel-toggle[aria-expanded="true"] { background: rgb(255 255 255 / 0%); }
/* El personaje del panel: las burbujas del loader con cara. En reposo
   respira, parpadea y sonríe; con `is-thinking` en el botón mira arriba,
   aparecen los puntitos y el anillo gira. Prefijo `compi-` para no chocar
   con las clases y keyframes del loader de la barra. */
.side-panel-mark { display: block; width: 30px; height: 30px; }
.compi { --compi-pulse: 2s; --compi-loop: 6s; --compi-think: 2.4s; }
.compi-ring, .compi-arm, .compi-core, .compi-au, .compi-white,
.compi-size, .compi-body, .compi-face, .compi-gaze, .compi-smile, .compi-eye { transform-origin: 0 0; }
.compi-white { fill: #fff; opacity: 0; }
.compi-ring { transform: rotate(30deg); }
.compi-arm { transform: translateX(-70px) scale(1.08); }
.compi-core { transform: scale(1.14); }
.compi-size { transform: scale(1.95); }
.compi .compi-ring { animation: compi-form-turn calc(var(--compi-pulse) * .5) linear both; }
.compi .compi-arm { animation: compi-form-arm calc(var(--compi-pulse) * .5) cubic-bezier(.45,0,.55,1) both; }
.compi .compi-core { animation: compi-form-core calc(var(--compi-pulse) * .5) cubic-bezier(.45,0,.55,1) both; }
.compi .compi-white { animation: compi-form-color calc(var(--compi-pulse) * .5) cubic-bezier(.45,0,.55,1) both; }
.compi .compi-face { animation: compi-face-in .46s cubic-bezier(.2,.7,.25,1.15) calc(var(--compi-pulse) * .5) both; }
.compi .compi-size { animation: compi-grow calc(var(--compi-pulse) * .5) cubic-bezier(.45,0,.55,1) both; }
.compi .compi-body { animation: compi-breathe var(--compi-loop) ease-in-out calc(var(--compi-pulse) * .5) infinite; }
.compi .compi-gaze { animation: compi-gaze var(--compi-loop) ease-in-out calc(var(--compi-pulse) * .5 + .46s) infinite; }
.compi .compi-eye { animation: compi-blink var(--compi-loop) linear calc(var(--compi-pulse) * .5 + .46s) infinite; }
.compi .compi-smile { animation: compi-smile var(--compi-loop) ease-in-out calc(var(--compi-pulse) * .5) infinite; }
.compi .compi-au1 { animation: compi-drift1 var(--compi-loop) ease-in-out infinite; }
.compi .compi-au2 { animation: compi-drift2 var(--compi-loop) ease-in-out infinite; }
.compi .compi-au3 { animation: compi-drift3 var(--compi-loop) ease-in-out infinite; }
.is-thinking .compi-gaze { animation: compi-think-gaze var(--compi-think) ease-in-out infinite; }
.is-thinking .compi-eye { animation: compi-think-blink calc(var(--compi-think) * 2) linear infinite; }
.is-thinking .compi-body { animation: compi-think-tilt var(--compi-think) ease-in-out infinite; }
.is-thinking .compi-ring { animation: compi-think-spin 9s linear infinite; }
.is-thinking .compi-arm { animation: compi-think-arm calc(var(--compi-think) * 1.25) ease-in-out infinite; }
.is-thinking .compi-ring > g:nth-child(2) .compi-arm { animation-delay: -.5s; }
.is-thinking .compi-ring > g:nth-child(3) .compi-arm { animation-delay: -1s; }
.is-thinking .compi-ring > g:nth-child(4) .compi-arm { animation-delay: -1.5s; }
.is-thinking .compi-ring > g:nth-child(5) .compi-arm { animation-delay: -2s; }
.is-thinking .compi-ring > g:nth-child(6) .compi-arm { animation-delay: -2.5s; }
.is-thinking .compi-core { animation: compi-think-core calc(var(--compi-think) * 1.25) ease-in-out infinite; }
.is-thinking .compi-au1, .is-thinking .compi-au2, .is-thinking .compi-au3 { animation-duration: calc(var(--compi-loop) * .35); }
@keyframes compi-form-turn { from { transform: rotate(0deg); } to { transform: rotate(30deg); } }
@keyframes compi-form-arm { from { transform: translateX(0) scale(1); } to { transform: translateX(-70px) scale(1.08); } }
@keyframes compi-form-core { from { transform: scale(.94); } to { transform: scale(1.14); } }
@keyframes compi-form-color { from { opacity: 1; } to { opacity: 0; } }
@keyframes compi-grow { from { transform: scale(1); } to { transform: scale(1.95); } }
@keyframes compi-face-in { from { opacity: 0; transform: translateY(9px) scale(.82); } to { opacity: 1; transform: translateY(0) scale(1); } }
@keyframes compi-breathe { 0%, 100% { transform: translateY(0) rotate(0deg) scale(1); } 25% { transform: translateY(-4px) rotate(-1.2deg) scale(1.014); } 50% { transform: translateY(-7px) rotate(0deg) scale(1.025); } 75% { transform: translateY(-3px) rotate(1.2deg) scale(1.01); } }
@keyframes compi-blink { 0%, 39%, 43%, 70%, 74%, 100% { transform: scaleY(1); } 41%, 72% { transform: scaleY(.08); } }
@keyframes compi-gaze { 0%, 17%, 48%, 66%, 100% { transform: translate(0, 0); } 26%, 36% { transform: translate(3px, -1.5px); } 76%, 86% { transform: translate(-2.5px, .5px); } }
@keyframes compi-smile { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.06, 1.035); } }
@keyframes compi-drift1 { 0%, 100% { transform: translate(-40px, 30px); } 50% { transform: translate(110px, -80px); } }
@keyframes compi-drift2 { 0%, 100% { transform: translate(80px, 60px); } 50% { transform: translate(-70px, -40px); } }
@keyframes compi-drift3 { 0%, 100% { transform: translate(20px, -90px); } 50% { transform: translate(-60px, 90px); } }
@keyframes compi-think-spin { from { transform: rotate(30deg); } to { transform: rotate(390deg); } }
@keyframes compi-think-arm { 0%, 100% { transform: translateX(-70px) scale(1.08); } 50% { transform: translateX(-52px) scale(1.2); } }
@keyframes compi-think-core { 0%, 100% { transform: scale(1.14); } 50% { transform: scale(1.24); } }
@keyframes compi-think-gaze { 0%, 38%, 100% { transform: translate(4px, -6px); } 50%, 88% { transform: translate(-3.5px, -6.5px); } }
@keyframes compi-think-blink { 0%, 44%, 48%, 100% { transform: scaleY(.82); } 46% { transform: scaleY(.08); } }
@keyframes compi-think-tilt { 0%, 100% { transform: translateY(-2px) rotate(-3.5deg) scale(1.01); } 50% { transform: translateY(-4px) rotate(3.5deg) scale(1.015); } }
.hilo-aviso .pill { white-space: normal; text-align: center; line-height: 1.4; }

.side-panel {
    flex: none; width: 0; overflow: hidden;
    border-left: 1px solid var(--border);
    position: sticky; top: var(--topbar);
    height: calc(100dvh - var(--topbar));
    background: var(--card-bg);
    transition: width .22s ease;
}
body.panel-open .side-panel { width: var(--side-panel); }
.side-panel-body {
    width: var(--side-panel); height: 100%;
    display: flex; flex-direction: column; min-height: 0;
}
.side-panel-body noscript a {
    display: block; padding: var(--sp-4); font-size: var(--fs-body);
}

/* ── El hilo del asistente ──────────────────────────────────────────────── */
.hilo { display: flex; flex-direction: column; height: 100%; min-height: 0; }
.hilo-pagina { padding: 0; }
.hilo-pagina .hilo { height: calc(100dvh - var(--topbar) - 48px); }
.hilo-cabecera {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--sp-2); padding: var(--sp-3) var(--sp-4);
    border-bottom: 1px solid var(--border);
}
.hilo-titulo {
    margin: 0; font-size: var(--fs-section); font-weight: 600;
    min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.hilo-acciones { display: flex; align-items: center; gap: var(--sp-2); flex: none; }
/* El menú de hilos vive dentro del panel: cabe en su ancho y los títulos
   largos se cortan con puntos suspensivos en vez de estirarlo. */
.hilo-acciones .menu-pop {
    white-space: normal; width: min(320px, calc(var(--side-panel) - 40px));
    max-width: calc(100vw - 32px);
}
.hilo-mensajes {
    flex: 1; min-height: 0; overflow-y: auto;
    padding: var(--sp-4); display: flex; flex-direction: column; gap: var(--sp-3);
}
.hilo-bienvenida { margin: auto 0; padding: var(--sp-5) 2px; }
.hilo-bienvenida h3 { margin: 0 0 12px; font-size: var(--fs-page); line-height: 1.2; letter-spacing: -.6px; font-weight: 600; }
.hilo-bienvenida p { margin: 0; color: var(--text-light); font-size: var(--fs-body); line-height: 1.6; }
.hilo-bienvenida .hilo-bienvenida-voz { margin-top: 20px; }
.burbuja {
    max-width: 92%; padding: var(--sp-2) var(--sp-3);
    border-radius: var(--radius); font-size: var(--fs-body); line-height: 1.45;
}
.burbuja-usuario { background: var(--primary-soft); color: var(--text); border-bottom-right-radius: 6px; }
.burbuja-asistente { background: var(--card-bg); border: 1px solid var(--border); border-bottom-left-radius: 6px; }
/* «Usar en otra pantalla»: el drawer de las pantallas con PIN. */
.pantallas-crear { margin-bottom: var(--sp-5); }
.pantallas-pins { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-3); }
.pantalla-ficha { margin-bottom: var(--sp-3); }
.pantalla-ficha-cabeza { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-2); margin-bottom: var(--sp-3); }
.pantalla-ficha-fecha { font-size: var(--fs-meta); color: var(--text-light); }
.pantalla-ficha-cuerpo { display: flex; gap: var(--sp-4); align-items: flex-start; }
.pantalla-qr { flex: none; width: 120px; height: 120px; border-radius: var(--radius-sm); background: #fff; padding: 4px; border: 1px solid var(--border); }
.pantalla-qr svg { width: 100%; height: 100%; display: block; }
.pantalla-ficha-datos { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: var(--sp-3); }
.pantalla-pin-form { display: flex; gap: var(--sp-2); align-items: center; flex-wrap: wrap; }
.pantalla-pin-form input { width: 110px; }
@media (max-width: 560px) { .pantalla-ficha-cuerpo { flex-direction: column; } .pantallas-pins { grid-template-columns: 1fr; } }
/* La pantalla del Asistente (URL con PIN): el hilo a pantalla completa, sin
 * shell. La cabecera es una banda con la empresa, el nombre y «Salir». */
/* Sobre los orbes del lienzo (`position: fixed; z-index: 0`): la pantalla
 * es una capa encima, como `.auth-stage`, y la tarjeta del chat es cristal
 * —fondo casi transparente con desenfoque— para que los orbes se vean
 * detrás y no encima. */
/* `height` y no `min-height` (Renzo, 2026-09-15): la pantalla del Asistente
   mide EXACTAMENTE la ventana, así que la tarjeta del chat es lo que queda
   bajo la cabecera y los mensajes se desplazan por dentro
   (`.hilo-mensajes` ya trae su `overflow-y: auto`). Con `min-height`, una
   conversación larga estiraba la página entera y la caja de escribir se iba
   al fondo del documento: en una tablet del mostrador había que bajar toda
   la pantalla para contestar. Esta clase la usa una sola plantilla
   (`assistant/screen.html`), que es lo que hace seguro fijarle el alto. */
.pantalla { position: relative; z-index: 1; height: 100dvh; display: flex; flex-direction: column; padding: 0 max(12px, env(safe-area-inset-right)) max(12px, env(safe-area-inset-bottom)) max(12px, env(safe-area-inset-left)); gap: var(--sp-4); }
/* La cabecera calca la topbar del ERP, más alta que la del cajón: fondo
 * oscuro, el logo en blanco por `currentColor`, la empresa y el nombre. */
.pantalla-cabecera { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); margin: 0 calc(-1 * max(12px, env(safe-area-inset-right))) 0 calc(-1 * max(12px, env(safe-area-inset-left))); padding: calc(max(14px, env(safe-area-inset-top)) + 4px) clamp(18px, 3.5vw, 36px) 18px; background: var(--canvas); color: #fff; border-radius: 0 0 var(--radius-card) var(--radius-card); box-shadow: 0 8px 30px rgba(13, 11, 22, .18); }
.pantalla-marca { display: flex; align-items: center; gap: 16px; min-width: 0; }
.pantalla-logo .brand-logo { height: 36px; }
.pantalla-sep { width: 1px; height: 36px; background: rgba(255, 255, 255, .25); flex: none; }
.pantalla-textos { display: flex; flex-direction: column; min-width: 0; line-height: 1.25; gap: 2px; }
.pantalla-empresa { font-size: var(--fs-small); color: rgba(255, 255, 255, .7); text-transform: uppercase; letter-spacing: .06em; }
.pantalla-nombre { font-size: var(--fs-stat); font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pantalla-salir { color: #fff; border-color: rgba(255, 255, 255, .35); }
.pantalla-salir:hover { background: rgba(255, 255, 255, .12); }
/* Cristal: blanco al 55 % con el fondo desenfocado y saturado, un borde
 * claro y un brillo interior arriba. `-webkit-` primero por Safari. */
.pantalla-cuerpo { flex: 1; min-height: 0; display: flex; flex-direction: column; background: rgba(255, 255, 255, .55); border: 1px solid rgba(255, 255, 255, .65); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .8), 0 12px 40px rgba(60, 40, 120, .12); -webkit-backdrop-filter: blur(22px) saturate(160%); backdrop-filter: blur(22px) saturate(160%); }
.pantalla-cuerpo .hilo { flex: 1; min-height: 0; height: auto; }
.pantalla-cuerpo .burbuja-asistente { background: rgba(255, 255, 255, .72); }
.pantalla-cuerpo .hilo-caja { background: rgba(255, 255, 255, .8); }
/* Cada mensaje es una fila: avatar, burbuja y hora. El Asistente a la
 * izquierda con el personaje; la persona a la derecha con su foto o
 * inicial (la misma de la topbar). */
.hilo-fila { display: flex; align-items: flex-end; gap: var(--sp-2); max-width: 100%; }
.hilo-fila-asistente { align-self: flex-start; }
.hilo-fila-usuario { align-self: flex-end; }
.hilo-fila-cuerpo { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.hilo-fila-usuario .hilo-fila-cuerpo { align-items: flex-end; }
.hilo-fila .burbuja { max-width: 100%; }
.hilo-avatar { width: 28px; height: 28px; flex: none; border-radius: 50%; display: grid; place-items: center; font-size: var(--fs-meta); font-weight: 700; color: #fff; background: var(--primary); }
/* `overflow` oculto a propósito: con `visible` se asomaban, arriba y a la
 * izquierda, los bordes de la máscara del arte (rayitas). */
.hilo-avatar-compi { background: transparent; border-radius: 0; overflow: hidden; }
/* El personaje del hilo se FORMA (la animación de aparición corre una
 * vez) y después queda quieto: sin respirar, parpadear ni derivar. La
 * clase va en la copia del símbolo, así todas las caritas del hilo la
 * heredan y la del panel sigue viva. */
.compi-quieto .compi-body, .compi-quieto .compi-gaze, .compi-quieto .compi-eye, .compi-quieto .compi-smile,
.compi-quieto .compi-au1, .compi-quieto .compi-au2, .compi-quieto .compi-au3 { animation: none; }
.hilo-compi-defs { position: absolute; }
.hilo-hora { font-size: var(--fs-meta); color: var(--text-faint); padding: 0 var(--sp-1); }
/* La respuesta en vivo solo se ve mientras la burbuja no esté oculta. */
.hilo-fila-escribiendo:not(:has(#hilo-escribiendo:not([hidden]))) { display: none; }
/* Las cifras de un resumen: tarjetas con el número grande y el rótulo. */
.hilo-cifras { margin: 0; padding: 0; list-style: none; display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: var(--sp-2); align-self: stretch; }
.cifra { display: flex; flex-direction: column; gap: 2px; padding: var(--sp-3); border-radius: var(--radius); background: var(--primary-soft); border: 1px solid var(--primary-soft-border); min-width: 0; }
.cifra-valor { font-size: var(--fs-stat); font-weight: 700; font-variant-numeric: tabular-nums; letter-spacing: -.3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cifra-rotulo { font-size: var(--fs-meta); color: var(--text-light); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cifra-2 { background: var(--ok-bg); border-color: color-mix(in srgb, var(--ok-text) 18%, transparent); }
.cifra-3 { background: var(--warn-bg); border-color: color-mix(in srgb, var(--warn-text) 18%, transparent); }
.cifra-4 { background: var(--danger-bg); border-color: color-mix(in srgb, var(--danger-text) 14%, transparent); }
.cifra-mas { justify-content: center; align-items: center; color: var(--text-faint); font-size: var(--fs-meta); background: transparent; border-style: dashed; }
.burbuja p { margin: 0 0 var(--sp-2); }
.burbuja p:last-child { margin-bottom: 0; }
.burbuja ul, .burbuja ol { margin: 0 0 var(--sp-2); padding-left: var(--sp-5); }
.burbuja code { font-size: var(--fs-small); }
.hilo-escribiendo:empty { display: none; }
.hilo-pensando { margin: 0; font-size: var(--fs-meta); color: var(--text-faint); }
/* El paso a paso: lo que el asistente va haciendo, tachado a medida que
   termina; al final queda plegado en `.hilo-hecho`. */
.hilo-proceso { align-self: stretch; }
.hilo-pasos { margin: 0; padding: 0 0 0 var(--sp-2); list-style: none; display: flex; flex-direction: column; gap: 6px; }
.hilo-paso {
    position: relative; padding-left: 18px;
    font-size: var(--fs-small); color: var(--text-light); line-height: 1.4;
}
.hilo-paso::before {
    content: ''; position: absolute; left: 0; top: .45em; width: 8px; height: 8px;
    border-radius: 50%; border: 2px solid var(--text-faint); box-sizing: border-box;
}
.hilo-paso.is-activo { color: var(--text); }
.hilo-paso.is-activo::before { border-color: var(--ok-text); animation: live-pulse 1.2s ease-in-out infinite; }
.hilo-paso.is-listo::before { background: var(--ok-text); border-color: var(--ok-text); }
.hilo-paso.is-fallo::before { background: var(--danger-text); border-color: var(--danger-text); }
.hilo-paso-detalle { color: var(--text-faint); }
.hilo-paso-resumen { color: var(--text-faint); font-size: var(--fs-meta); }
.hilo-hecho { align-self: stretch; font-size: var(--fs-small); }
.hilo-hecho-resumen { cursor: pointer; color: var(--text-faint); list-style: none; }
.hilo-hecho-resumen::-webkit-details-marker { display: none; }
.hilo-hecho[open] .hilo-hecho-resumen { margin-bottom: var(--sp-2); }
.hilo-fichas { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: var(--sp-2); align-self: stretch; }
.ficha-mini { margin: 0; padding: 0; }
.ficha-mini-link {
    display: flex; align-items: center; gap: var(--sp-3);
    padding: var(--sp-3);
    border: 1px solid var(--border); border-radius: var(--radius);
    background: var(--card-bg); color: var(--text); text-decoration: none;
    transition: border-color .12s ease, box-shadow .12s ease;
}
.ficha-mini-link:hover { border-color: var(--primary-soft-border); box-shadow: 0 2px 10px color-mix(in srgb, var(--primary) 10%, transparent); }
.ficha-mini-icono {
    flex: none; width: 32px; height: 32px; display: grid; place-items: center;
    border-radius: var(--radius-sm); background: var(--primary-soft); color: var(--primary-dark);
}
.ficha-mini-icono svg { width: 18px; height: 18px; }
.ficha-mini-cuerpo { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.ficha-mini-titulo { font-size: var(--fs-body); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ficha-mini-meta { font-size: var(--fs-meta); color: var(--text-light); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ficha-mini-monto { flex: none; font-size: var(--fs-body); font-weight: 600; font-variant-numeric: tabular-nums; }
.hilo-fichas-mas { color: var(--text-faint); font-size: var(--fs-meta); padding: 0 var(--sp-3); }
.hilo-aviso { align-self: center; }
.hilo-composer {
    display: flex; flex-direction: column; gap: var(--sp-2);
    padding: var(--sp-3) var(--sp-4); flex: none;
}
.hilo-composer.is-arrastrando { outline: 2px dashed var(--primary); outline-offset: -4px; }
/* La caja del mensaje: un solo borde redondo para adjuntar, texto y
   enviar; el foco lo pinta la caja, no el textarea. */
.hilo-caja {
    display: flex; flex-direction: column; gap: 4px;
    padding: 10px;
    border: 1px solid var(--border); border-radius: 18px;
    background: var(--card-bg);
}
.hilo-composer-herramientas { display: flex; align-items: center; gap: 6px; width: 100%; }
.hilo-composer-herramientas > [type=submit] { margin-left: auto; }
.hilo-composer-herramientas .btn, .hilo-composer-herramientas .hilo-adjuntar { min-height: 40px; }
.hilo-composer-herramientas .btn-icon, .hilo-composer-herramientas .hilo-adjuntar { width: 40px; }
.hilo-caja:focus-within { border-color: var(--primary); box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 18%, transparent); }
.hilo-texto {
    width: 100%; flex: none; min-width: 0; resize: none; margin: 0;
    padding: 6px 4px; min-height: 32px; max-height: 160px;
    border: 0; background: transparent; box-shadow: none;
    font-size: var(--fs-body); line-height: 1.4;
}
.hilo-texto:focus { outline: none; box-shadow: none; }
.hilo-caja .btn-icon { flex: none; border-radius: 50%; }
/* La voz tiene su propio espacio. El nivel SIEMPRE representa al micrófono,
   y los estados escritos siguen disponibles sin animación ni color. */
.hilo-hablar { color: var(--primary); }
.hilo-hablar[aria-pressed=true] { background: color-mix(in srgb, var(--primary) 10%, var(--card-bg)); }
.hilo-voz {
    flex: none; margin: 0 var(--sp-4); padding: 12px 16px;
    border: 1px solid color-mix(in srgb, var(--primary) 22%, var(--border));
    border-radius: 20px; background: color-mix(in srgb, var(--primary) 4%, var(--card-bg));
    color: var(--text); min-width: 0;
}
.hilo-voz[hidden], .hilo-voz [hidden] { display: none; }
.hilo-voz-cabecera { display: flex; align-items: center; gap: 10px; min-height: 36px; }
.hilo-voz-identidad { display: flex; align-items: center; gap: 8px; min-width: 0; flex: 1; font-size: var(--fs-small); color: var(--text-light); white-space: nowrap; }
.hilo-voz-titulo, .hilo-voz-resumen { overflow: hidden; text-overflow: ellipsis; }
.hilo-voz-punto { width: 8px; height: 8px; border-radius: 50%; background: var(--primary); flex: none; box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 18%, transparent); }
.hilo-voz[data-estado=listening] .hilo-voz-punto, .hilo-voz[data-estado=hearing] .hilo-voz-punto { background: var(--ok-text); box-shadow: 0 0 0 3px color-mix(in srgb, var(--ok-text) 22%, transparent); }
.hilo-voz[data-estado=hearing] .hilo-voz-punto { animation: live-pulse .9s ease-in-out infinite; }
.hilo-voz[data-estado=thinking] .hilo-voz-punto { animation: live-pulse 1.4s ease-in-out infinite; }
.hilo-voz[data-estado=muted] .hilo-voz-punto { background: var(--text-faint); box-shadow: none; }
.hilo-voz[data-estado=error] .hilo-voz-punto { background: var(--danger-text); box-shadow: none; }
.hilo-voz-botones { display: flex; align-items: center; gap: 2px; flex: none; }
.hilo-voz-icono { display: inline-grid; place-items: center; width: 34px; height: 34px; border: 0; border-radius: 50%; background: transparent; color: var(--text-light); cursor: pointer; padding: 0; }
.hilo-voz-icono svg { width: 18px; height: 18px; }
.hilo-voz-icono:hover { background: color-mix(in srgb, var(--primary) 10%, transparent); color: var(--text); }
.hilo-voz-icono[aria-pressed=true] { color: var(--danger-text); background: color-mix(in srgb, var(--danger-text) 10%, transparent); }
.hilo-voz-icono[data-voz-plegar] svg { transition: transform .18s ease; }
.hilo-voz-icono[aria-expanded=true] svg { transform: rotate(180deg); }
.hilo-voz-terminar { color: var(--danger-text); }
.hilo-voz-terminar:hover { background: color-mix(in srgb, var(--danger-text) 10%, transparent); color: var(--danger-text); }
/* Desplegado: solo título; el micrófono y el medidor viven en la escena. */
.hilo-voz:not(.is-plegado) .hilo-voz-resumen, .hilo-voz:not(.is-plegado) .hilo-voz-nivel-mini, .hilo-voz:not(.is-plegado) .hilo-voz-mini-silenciar { display: none; }
.hilo-voz-escena { text-align: center; padding: 8px 0 12px; }
.hilo-voz-estado { margin: 0; font-size: var(--fs-page); line-height: 1.25; font-weight: 600; letter-spacing: -.4px; }
.hilo-voz-ayuda { margin: 6px 0 0; font-size: var(--fs-small); color: var(--text-light); line-height: 1.4; }
.hilo-voz-nivel { display: flex; align-items: center; justify-content: center; gap: 6px; height: 58px; margin: 8px 0 2px; color: var(--primary); }
.hilo-voz-nivel i { display: block; width: 5px; height: calc(5px + 43px * var(--nivel, 0)); border-radius: 4px; background: currentColor; transition: height 80ms linear; }
/* Plegado: una píldora de una línea —estado, mini medidor, tres iconos—.
 * Se pliega solo al llegar la primera respuesta del ERP para que se lea
 * en el hilo; la persona lo vuelve a abrir con el chevron. */
.hilo-voz.is-plegado { padding: 4px 6px 4px 14px; border-radius: 999px; }
.hilo-voz.is-plegado .hilo-voz-titulo { display: none; }
.hilo-voz.is-plegado .hilo-voz-resumen { color: var(--text); font-weight: 500; }
.hilo-voz.is-plegado .hilo-voz-escena, .hilo-voz.is-plegado .hilo-voz-subtitulos, .hilo-voz.is-plegado .hilo-voz-controles { display: none; }
.hilo-voz-nivel-mini { height: 22px; gap: 3px; margin: 0; }
.hilo-voz-nivel-mini i { width: 3px; height: calc(4px + 14px * var(--nivel, 0)); }
.hilo-voz[data-estado=muted] .hilo-voz-nivel { color: var(--text-faint); }
.hilo-voz[data-estado=connecting] .hilo-voz-punto { animation: live-pulse 1.2s ease-in-out infinite; }
.hilo-voz-subtitulos { display: flex; flex-direction: column; gap: 8px; max-height: 110px; overflow-y: auto; overflow-wrap: anywhere; }
.hilo-voz-subtitulos p { display: flex; align-items: baseline; gap: 10px; margin: 0; font-size: var(--fs-body); line-height: 1.5; }
.hilo-voz-quien { width: 56px; flex: none; font-size: var(--fs-meta); color: var(--text-light); }
.hilo-voz-controles { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; padding-top: 12px; }
.hilo-voz-control { display: inline-flex; align-items: center; justify-content: center; gap: 7px; min-height: 40px; padding: 8px 13px; border-radius: 12px; border: 1px solid var(--border); background: var(--card-bg); color: var(--text); font: inherit; font-size: var(--fs-small); cursor: pointer; }
.hilo-voz-control svg { width: 16px; height: 16px; }
.hilo-voz-control:hover { border-color: var(--primary); }
.hilo-voz-control[aria-pressed=true] { color: var(--danger-text); border-color: currentColor; }
.hilo-voz button:focus-visible { outline: 3px solid color-mix(in srgb, var(--primary) 45%, transparent); outline-offset: 2px; }
.hilo-caja.is-pensando { border-color: color-mix(in srgb, var(--primary) 40%, var(--border)); }
@media (max-height: 640px) {
    .hilo-voz-escena { padding: 2px 0 6px; }
    .hilo-voz-nivel { height: 30px; margin: 2px 0; }
    .hilo-voz-nivel i { max-height: 28px; }
    .hilo-voz-subtitulos { max-height: 66px; }
}
/* Con movimiento reducido, el bloque general de la hoja ya apaga la
   transición de las barras y el pulso del punto: no se enumeran acá. */
/* Adjuntar: el <label> abre el selector; el <input> queda oculto pero vivo
   (camino sin script). La miniatura con × aparece antes de enviar. */
.hilo-adjuntar {
    flex: none; width: 30px; height: 30px; display: grid; place-items: center;
    border-radius: 50%; color: var(--text-light); cursor: pointer;
}
.hilo-adjuntar:hover { color: var(--text); background: var(--bg); }
.hilo-adjuntar svg { width: 18px; height: 18px; }
.hilo-adjunto { position: relative; align-self: flex-start; }
.hilo-adjunto-miniatura {
    display: block; max-width: 120px; max-height: 120px;
    border-radius: var(--radius-sm); border: 1px solid var(--border);
}
.hilo-adjunto-quitar {
    position: absolute; top: -8px; right: -8px; width: 22px; height: 22px;
    display: grid; place-items: center; padding: 0;
    border: 1px solid var(--border); border-radius: 50%;
    background: var(--card-bg); color: var(--text); cursor: pointer;
}
.hilo-adjunto-quitar svg { width: 12px; height: 12px; }
.burbuja-imagen {
    display: block; max-width: 100%; max-height: 240px;
    border-radius: var(--radius-sm); margin-bottom: var(--sp-2);
}
.burbuja-imagen:last-child { margin-bottom: 0; }
.burbuja-texto { white-space: pre-wrap; }
/* La lista de hilos es un menú de la casa: el ítem, el glifo, la raya y el
   rótulo salen de `.menu-item`. Acá solo vive lo que es del hilo — cuánto
   mide la hoja y cómo se reparten el título y la fecha. El tope de ancho es
   para que un título largo se corte con elipsis en vez de estirar el menú
   hasta el otro lado de la pantalla. */
.hilo-lista { min-width: 280px; max-width: 360px; }
.hilo-lista-titulo { flex: 1; min-width: 0; overflow: hidden;
                     text-overflow: ellipsis; white-space: nowrap; }
.hilo-lista-fecha { flex: none; color: var(--text-faint); font-size: var(--fs-meta); }
.hilo-lista-vacia .empty { padding: 18px 12px 12px; }
.hilo-lista-vacia .empty-text { margin-bottom: 0; font-size: var(--fs-small); }

.propuesta {
    align-self: stretch; padding: var(--sp-3);
    border: 1px solid var(--border); border-left: 3px solid var(--text-light);
    border-radius: var(--radius); background: var(--card-bg);
    display: flex; flex-direction: column; gap: var(--sp-2);
}
.propuesta-pendiente, .propuesta-confirmada { border-left-color: var(--ok-text); }
.propuesta-rechazada, .propuesta-vencida { border-left-color: var(--text-faint); opacity: .8; }
.propuesta-enlace { margin-left: var(--sp-2); font-size: var(--fs-small); }
.propuesta-fallida { border-left-color: var(--danger-text); }
.propuesta-titulo { margin: 0; font-size: var(--fs-small); color: var(--text-light); }
.propuesta-texto { margin: 0; font-size: var(--fs-body); font-weight: 600; }
.propuesta-params { margin: 0; display: grid; grid-template-columns: max-content 1fr; gap: 2px var(--sp-3); font-size: var(--fs-small); }
.propuesta-params dt { color: var(--text-faint); }
.propuesta-params dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
.propuesta-form { display: flex; flex-direction: column; gap: var(--sp-2); }
.propuesta-form-no { align-self: flex-start; }
.propuesta-botones { display: flex; gap: var(--sp-2); }
.propuesta-codigo { font-size: var(--fs-small); }
.propuesta-estado { margin: 0; font-size: var(--fs-small); display: flex; align-items: center; gap: var(--sp-2); }

.ajustes-consumo { display: grid; grid-template-columns: max-content 1fr; gap: var(--sp-1) var(--sp-4); margin: 0; font-size: var(--fs-body); }
.ajustes-consumo dt { color: var(--text-faint); }
.ajustes-consumo dd { margin: 0; }
.sidebar-label {
    margin: 14px 10px 6px; font-size: var(--fs-badge); font-weight: 700;
    letter-spacing: .1em; text-transform: uppercase;
    color: var(--text-faint);
}
.sidebar-label:first-child { margin-top: 4px; }
.nav-list { list-style: none; margin: 0; padding: 0; }
.nav-item {
    display: flex; align-items: center; gap: 10px;
    padding: 7px 12px; margin-bottom: 2px;
    color: var(--text-light); font-size: var(--fs-body); font-weight: 500;
    border-radius: var(--radius); text-decoration: none;
    transition: background .16s, color .16s;
}
.nav-item:hover { background: var(--bg); color: var(--text); text-decoration: none; }
.nav-item.active { background: var(--primary); color: #fff; font-weight: 600; }
.nav-item:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--primary-soft); }
.nav-icon { width: 17px; height: 17px; flex-shrink: 0; }
.nav-label { flex: 1; min-width: 0; }
.nav-badge {
    flex-shrink: 0; min-width: 20px; padding: 1px 6px;
    font-size: var(--fs-meta); font-weight: 700; text-align: center;
    color: #fff; background: var(--primary); border-radius: 99px;
}
.nav-item.active .nav-badge { background: rgba(255, 255, 255, .28); }
.nav-badge.tone-warn   { background: var(--warn-text); }
.nav-badge.tone-danger { background: var(--danger-text); }

/* ── El grupo del menú: una entrada con las pantallas que cuelgan de ella ──
   El `<summary>` no se pinta: quien lleva el aspecto es el `.nav-item` de
   adentro, que es EL MISMO enlace que en una entrada sin hijas. Así un grupo
   no puede verse distinto del resto del menú, que es lo que pasa cuando el
   contenedor se pinta aparte. */
/* Sin summary, sin flecha: el padre es un ENLACE PLANO y el acordeón lo
   sirve el servidor — apretar al padre navega, y en su página se pinta SU
   submenú y ninguno más. Decisión del 2026-08-02. */
/* La sangría NO es la del rótulo del padre (39px = relleno 12 + icono 17 +
   separación 10). Alinear la hija bajo el rótulo del padre se lee bien con un
   solo nivel y se rompe con dos: el nieto caería en 78px y a la mitad del
   ancho del menú no queda rótulo que leer. 36px es lo que deja pasar el
   conector —la línea baja por 20px y la punta termina en 33.5—, y deja
   escalones de 12px para lo que viene: 36 → 48 → 60, con 157px de rótulo
   todavía en el tercer nivel. */
.nav-sublist {
    --nav-sub: 36px;
    /* Los dos huecos que el conector tiene que cruzar, escritos UNA vez: el
       aire entre hijas y el que el ítem del padre deja debajo (su
       `margin-bottom`). Con el número suelto en cada regla, mover el aire
       parte la línea en rayitas y nadie relaciona una cosa con la otra. */
    --nav-gap: 1px; --nav-lead: 2px;
    /* El riel del conector cae en el centro del icono del padre: 12 de relleno
       del ítem más la mitad de sus 17px. Movido de ahí, la línea deja de salir
       del icono y pasa a ser una raya al lado del menú. */
    --nav-rail: 20px; --nav-line: 2px;
    list-style: none; margin: 0 0 6px; padding: 0;
}
/* La hija NO se escribe más chica que el padre. El tamaño no es lo que dice
   que algo cuelga de otra cosa —eso lo dicen la sangría y el conector—; lo
   único que consigue es una lista de nombres que cuesta leer. La sangría vive
   en el enlace y no en la lista para que la pastilla de hover ocupe el ancho
   del menú, como la del padre. */
.nav-subitem {
    /* Flex y no block desde que la hija puede llevar badge (avisos,
       2026-09-10): la ranura se empuja a la derecha con `margin-left: auto`,
       que en un bloque no hace nada. */
    display: flex; align-items: center;
    padding: 7px 12px 7px var(--nav-sub); margin-bottom: var(--nav-gap);
    /* Más claro que --text-light a propósito (pedido de Renzo, 2026-08-02):
       la hija pesa menos que su padre, y el gris del texto normal las dejaba
       compitiendo. */
    color: #9b96af; font-size: var(--fs-body); font-weight: 500;
    border-radius: var(--radius); text-decoration: none;
}
.nav-subitem:hover { background: var(--bg); color: var(--text); text-decoration: none; }
/* La hija activa se marca con COLOR y no con la pastilla del padre: dos
   pastillas moradas seguidas compiten, y la de arriba es la que dice en qué
   módulo estás. */
.nav-subitem.active { color: var(--primary); font-weight: 600; }
.nav-subitem:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--primary-soft); }

/* ── El conector: del icono del padre hasta la hija abierta ────────────────
   La misma flecha de tiendapp. Baja desde el CENTRO DEL ICONO del padre
   (12 de relleno + 17/2 ≈ 20px), recorre las hijas que quedan por encima y
   dobla con una punta sobre la que está abierta. Dice dos cosas que la
   sangría sola no dice: de quién cuelga esta lista y en cuál de sus hijas
   estás parado.

   Sin una línea de JavaScript —tiendapp la marca con un `forEach` que reparte
   clases y mide el codo—: `:has()` pregunta por el hermano siguiente, que es
   exactamente la pregunta que allá se contestaba recorriendo el DOM. Donde
   `:has()` no exista no hay flecha y el menú sigue navegándose igual.

   El codo mide `50%` —el centro de la fila— en vez de un alto fijo: así no hay
   que reescribirlo cuando cambie el relleno del enlace. Le SUMA medio trazo
   porque con `border-box` el borde de abajo se dibuja DENTRO del alto, así que
   su centro cae en `alto - 1`: sin la suma la línea entra dos píxeles por
   encima de la punta y el doblez se ve quebrado. Lo mismo vale para la punta,
   que se centra sola en `50%` con su `translateY(-50%)`. El día que haya un
   tercer nivel, el `li` de en medio deja de ser una fila y esa regla necesita
   su propio alto. */
.nav-sublist > li { position: relative; }
/* El tramo: las hijas por encima de la abierta. Baja `--nav-gap` de más para
   entrar en el `li` siguiente; si termina en su propio borde, la línea sale
   punteada. */
.nav-sublist > li:has(~ li > .nav-subitem.active)::before {
    content: ''; position: absolute; left: var(--nav-rail); top: 0;
    bottom: calc(var(--nav-gap) * -1);
    width: var(--nav-line); background: var(--primary);
}
/* El codo y su punta, sobre la hija abierta. */
.nav-sublist > li:has(> .nav-subitem.active)::before {
    content: ''; position: absolute; left: var(--nav-rail); top: 0;
    width: 12px; height: calc(50% + var(--nav-line) / 2);
    border-left: var(--nav-line) solid var(--primary);
    border-bottom: var(--nav-line) solid var(--primary);
    border-bottom-left-radius: 4px;
}
.nav-sublist > li:has(> .nav-subitem.active)::after {
    content: ''; position: absolute; top: 50%;
    left: calc(var(--nav-rail) + 5px); width: 7px; height: 7px;
    border-top: var(--nav-line) solid var(--primary);
    border-right: var(--nav-line) solid var(--primary);
    transform: translateY(-50%) rotate(45deg);
}
/* La primera hija arranca la línea DENTRO de la pastilla del padre. Naciendo
   en el borde de la lista queda flotando: se lee como una raya al lado del
   menú y no como algo que sale del icono. */
.nav-sublist > li:first-child:has(~ li > .nav-subitem.active)::before {
    top: calc(var(--nav-lead) * -1);
}
.nav-sublist > li:first-child:has(> .nav-subitem.active)::before {
    top: calc(var(--nav-lead) * -1);
    height: calc(50% + var(--nav-lead) + var(--nav-line) / 2);
}
.nav-sublist > li::before, .nav-sublist > li::after { pointer-events: none; }

.content {
    flex: 1; min-width: 0;
    padding: 24px var(--content-pad);
    background: var(--bg);
    /* Contenedor de consulta: lo que vive adentro puede responder al ANCHO
       DEL CONTENIDO y no al de la ventana. Con el panel lateral abierto el
       contenido pierde 380px sin que la ventana cambie, y un `@media` no
       se entera. */
    container-type: inline-size; container-name: content;
}

/* Colapsado: riel de iconos. La etiqueta se va, el icono queda centrado. */
body.sidebar-collapsed .sidebar { width: var(--rail); }
body.sidebar-collapsed .nav-label,
body.sidebar-collapsed .nav-badge,
/* El submenú se va con el rótulo: colapsado el sidebar mide un icono, y una
   lista de nombres debajo de un icono no cabe ni se entiende. */
body.sidebar-collapsed .nav-sublist,
body.sidebar-collapsed .sidebar-label { display: none; }
body.sidebar-collapsed .nav-item { justify-content: center; padding: 10px 0; }

/* El cajón de móvil: un checkbox, sin una línea de JS.
   El checkbox ES el botón —se pinta como la hamburguesa— para que se pueda
   accionar con Espacio. Una <label> recibe foco y no se activa con teclado. */
.drawer-toggle, .drawer-overlay { display: none; }

.drawer-toggle {
    appearance: none; -webkit-appearance: none;
    margin: 0; padding: 0; font: inherit;
    position: fixed; z-index: 101;
    top: calc((var(--topbar) - 32px) / 2); left: clamp(14px, 3vw, 32px);
    width: 32px; height: 32px;
    color: rgba(255, 255, 255, .8); background: transparent;
    border: 0; border-radius: 9px; cursor: pointer;
}
.drawer-toggle::before {
    content: '☰';
    display: grid; place-items: center;
    width: 100%; height: 100%; font-size: var(--fs-glyph); line-height: 1;
}
.drawer-toggle:hover { background: rgba(255, 255, 255, .1); color: #fff; }
.drawer-toggle:focus-visible { outline: 2px solid #fff; outline-offset: 1px; }
.drawer-toggle:checked::before { content: '✕'; }

/* ── El escritorio ─────────────────────────────────────────────────────────
   Portado de tiendapp (style.css:1976-2135). Los orbes viven DENTRO del
   escenario, con la máscara radial que los desvanece antes de que el
   contenedor los recorte — sin ella el difuminado se corta en seco contra
   el borde. No son fondo de la aplicación: eso es lo que hace `base.html`
   en las pantallas de autenticación, y ahí se queda.
   ------------------------------------------------------------------------ */

.home-stage {
    /* 62/46 de tiendapp: la respiración del escenario es parte de la cara. */
    position: relative; padding: 62px 0 46px; text-align: center;
    /* SIN recorte propio (pedido de Renzo, 2026-08-04): el `clip` de acá
       cortaba los orbes en seco contra los bordes de la columna — se veía
       la costura en los laterales. Quien impide el scroll lateral del
       `body` es el `overflow: clip` que `.shell` ya tiene, así que los
       orbes sangran hasta el borde de la aplicación y la máscara los
       desvanece antes de que ninguna costura se vea. */
}

/* De borde a borde de la aplicación: `inset` a cero en X y el padding del
   contenido cancelado con margen negativo, así el lienzo llega hasta donde
   llega la pantalla en vez de terminar 28px antes (Renzo, 2026-08-04 — se
   veía la costura en los laterales). Y el radio horizontal de la máscara es
   50% y no 58% JUSTAMENTE por eso: con 58% el desvanecido todavía valía un
   20% de opacidad al llegar al borde, y ahí es donde se cortaba en seco. */
.home-orbs {
    position: absolute; inset: -60px 0 -180px; z-index: 0;
    margin-inline: calc(var(--content-pad) * -1);
    pointer-events: none; overflow: hidden;
    -webkit-mask-image: radial-gradient(50% 52% at 50% 38%, #000 30%, transparent 100%);
    mask-image: radial-gradient(50% 52% at 50% 38%, #000 30%, transparent 100%);
}
.home-orb {
    position: absolute; border-radius: 50%;
    filter: blur(58px); mix-blend-mode: multiply; will-change: transform;
}
.home-orb.orb-lavender {
    width: 30vmax; height: 30vmax; top: 46%; left: 24%; opacity: .26;
    animation: orb-drift-a 16s ease-in-out infinite;
}
.home-orb.orb-cyan {
    width: 26vmax; height: 26vmax; top: 6%; right: 4%; opacity: .42;
    animation: orb-drift-b 19s ease-in-out infinite;
}
.home-orb.orb-yellow {
    width: 20vmax; height: 20vmax; top: -12%; left: 6%; opacity: .36;
    animation: orb-drift-c 22s ease-in-out infinite;
}
.home-orb.orb-violet {
    width: 24vmax; height: 24vmax; top: 26%; right: 26%; bottom: auto;
    opacity: .32; animation: orb-drift-d 17s ease-in-out infinite;
}
.home-orb.orb-glow {
    width: 34vmax; height: 34vmax; top: 2%; left: 50%;
    transform: translateX(-50%); opacity: .30;
    filter: blur(70px); mix-blend-mode: normal;
    animation: orb-pulse 9s ease-in-out infinite;
}

/* ── La tira de métricas, portada de tiendapp ────────────────────────────────
   Las casillas las define la vista (HOME_SLOTS); los números los publican los
   módulos por `metrics_for()`. Los tamaños vienen de la escala de la casa y
   no de los px de tiendapp: la tira es una pantalla más, no una embajada. */
/* El radio de una TARJETA y no una píldora (pedido de Renzo, 2026-08-12). Con
   `999px` la tira era la única pieza del ERP con esa forma: al lado de las
   tarjetas cuadradas de abajo se leía como un control —una barra de búsqueda,
   un conmutador— y no como el bloque de datos que es. */
.home-metrics {
    display: flex; align-items: stretch; gap: 0; flex-wrap: wrap;
    margin-bottom: var(--sp-5); padding: 6px;
    background: var(--card-bg);
    border: 1px solid var(--border); border-radius: var(--radius-card);
    box-shadow: var(--shadow);
}
.home-metrics-head {
    display: flex; flex-direction: column; justify-content: center; gap: 2px;
    padding: 6px 14px 6px 12px;
}
.home-metrics-scope {
    font-size: var(--fs-badge); font-weight: 700; letter-spacing: .08em;
    text-transform: uppercase; color: var(--text-light); opacity: .75;
}
/* ── El selector de período (`ui/_period.html`) ─────────────────────────────
   Dos disparadores, uno por pregunta —qué fechas y contra qué— y cada uno
   abre su panel. Son `<details class="menu">` como cualquier menú de la casa:
   `menu.js` los cierra al clic de afuera y con Esc.

   El disparador va SIN caja (pedido de Renzo, 2026-08-03, sobre el select de
   antes): dentro de la píldora de la tira, un borde dentro de otro borde era
   una caja dentro de una caja. El fondo aparece al pasar y mientras está
   abierto, que es cuando hace falta saber qué se tocó. */
.period { display: flex; align-items: center; gap: 2px; flex-wrap: wrap; }
.period-menu { position: relative; }
.period-trigger {
    display: flex; align-items: center; gap: 8px;
    padding: 5px 8px; border-radius: var(--radius-sm);
    list-style: none; cursor: pointer; user-select: none; color: var(--text);
}
.period-trigger::-webkit-details-marker { display: none; }
.period-trigger:hover,
.period-menu[open] > .period-trigger { background: var(--primary-soft); }
.period-trigger:focus-visible {
    outline: 2px solid var(--primary); outline-offset: 2px;
}
.period-trigger-glyph { width: 18px; height: 18px; flex: none; color: var(--primary); }
.period-compare .period-trigger-glyph { color: var(--text-light); }
.period-trigger-text {
    display: flex; flex-direction: column; min-width: 0; line-height: 1.25;
}
.period-trigger-label { font-size: var(--fs-body); font-weight: 600; white-space: nowrap; }
.period-trigger-dates { font-size: var(--fs-small); color: var(--text-light); white-space: nowrap; }
.period-trigger-chevron {
    width: 15px; height: 15px; flex: none; color: var(--text-light);
    transition: transform .16s;
}
.period-menu[open] .period-trigger-chevron { transform: rotate(180deg); }
/* El panel abre hacia la DERECHA de su disparador —el selector vive a la
   izquierda de la tira y del tablero—, al revés del menú de tres puntos.
   `.period` delante para ganarle a `.menu-pop`, que está más abajo. */
.period .menu-pop {
    left: 0; right: auto; white-space: normal; max-width: calc(100vw - 32px);
}
/* `max-content`: un absoluto se encoge al ancho de su bloque contenedor —el
   `<details>` del disparador, 230px— y el calendario de dos meses quedaba
   desbordando el panel por la derecha. */
.period .period-pop { display: flex; width: max-content; padding: 0; }
.period .period-compare-pop { min-width: 260px; }
.period-presets {
    display: flex; flex-direction: column; gap: 1px;
    margin: 0; padding: 8px; border: 0; border-right: 1px solid var(--border);
    min-width: 200px;
    /* El alto lo pone `period-picker.js` en el panel según el espacio que
       queda en la ventana; cada columna lo hereda y se desplaza por dentro
       en vez de cortarse contra el borde. */
    max-height: inherit; overflow-y: auto;
}
/* Cada opción es una fila con su radio ESCONDIDO adentro: viaja en el GET
   como cualquier campo, y la fila marcada se pinta con `:has`. */
.period-option {
    position: relative;
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
    padding: 7px 10px; border-radius: var(--radius-sm);
    font-size: var(--fs-body); white-space: nowrap; cursor: pointer;
}
.period-option:hover { background: var(--bg); }
.period-option:has(input:checked) {
    background: var(--primary-soft); color: var(--primary-dark); font-weight: 600;
}
.period-option:has(input:focus-visible) {
    outline: 2px solid var(--primary); outline-offset: -2px;
}
/* La marca de la elegida, dibujada: dos bordes de una caja girada. */
.period-option:has(input:checked)::after {
    content: ""; flex: none; width: 5px; height: 10px; margin: -3px 4px 0 0;
    border: solid currentColor; border-width: 0 2px 2px 0;
    transform: rotate(45deg);
}
.period-sep { height: 1px; margin: 5px 6px; background: var(--border); }
.period-body {
    display: flex; flex-direction: column; gap: var(--sp-3);
    padding: 14px 16px 0; max-height: inherit; overflow-y: auto;
}
.period-fields { display: flex; align-items: center; gap: var(--sp-2); }
.period-field { flex: 1 1 0; min-width: 0; }
.period-field input { width: 100%; }
.period-field input[aria-invalid="true"] { border-color: var(--danger-vivid); }
.period-arrow { width: 16px; height: 16px; flex: none; color: var(--text-light); }
/* Alto reservado: el calendario se arma al abrir el panel por primera vez,
   y sin piso el panel saltaría de tamaño debajo del cursor. */
.period-calendar { display: flex; justify-content: center; min-height: 290px; }
/* La casilla a la que flatpickr se engancha: en línea no muestra nada, pero
   el vendor la vuelve de texto y quedaba pintada al lado de los meses. */
.period-calendar > input { display: none; }
/* El pie va ABAJO del panel aunque el calendario sea más bajo que la
   lista de presets, y PEGADO: con el panel recortado por la ventana, lo que
   se desplaza es el calendario y no los botones (Renzo, 2026-09-15: con el
   selector abajo en la pantalla, «Aplicar» quedaba fuera de vista). */
.period-foot {
    position: sticky; bottom: 0;
    display: flex; justify-content: flex-end; gap: var(--sp-2);
    margin-top: auto; padding: var(--sp-3) 0;
    border-top: 1px solid var(--border); background: var(--card-bg);
}
/* «Aplicar» sin nada nuevo que aplicar se ve apagado: la casa todavía no
   tiene un `.btn:disabled` general, y un morado lleno que no hace nada se
   lee como un botón roto. */
.period-foot .btn:disabled { opacity: .45; cursor: not-allowed; }
/* En el teléfono el panel es una HOJA pegada abajo: anclado a un disparador
   de 150px se saldría por el borde. Los presets pasan a fichas arriba y el
   calendario muestra un mes (lo decide `period-picker.js` al armarlo). */
@media (max-width: 640px) {
    .period .menu-pop {
        position: fixed; top: auto; left: 8px; right: 8px; bottom: 8px;
        max-width: none; max-height: calc(100dvh - 16px); overflow-y: auto;
        z-index: 60;
    }
    /* `auto` y no el `max-content` del escritorio: con los dos bordes
       fijados es la hoja la que da el ancho, no su contenido. */
    .period .period-pop { flex-direction: column; width: auto; }
    /* En la hoja desplaza la HOJA entera; si el cuerpo fuera su propio
       contenedor, el pie pegado se quedaría abajo de él, fuera de vista. */
    .period-presets, .period-body { max-height: none; overflow: visible; }
    .period-presets {
        flex-direction: row; flex-wrap: wrap; gap: 6px; min-width: 0;
        border-right: 0; border-bottom: 1px solid var(--border);
    }
    .period-presets .period-option {
        border: 1px solid var(--border); border-radius: 999px;
        padding: 5px 12px; font-size: var(--fs-label);
    }
    .period-presets .period-option:has(input:checked)::after,
    .period-sep { display: none; }
}
/* La división entre casillas es una RAYA RECTA, dibujada aparte.

   Antes el `border-left` y un `border-radius: 14px` vivían en el mismo
   elemento, y un borde se curva con el radio de su caja: la línea salía
   arqueada en las dos puntas y las casillas parecían pegadas con una costura
   redonda. Los dos eran correctos por separado y juntos no.

   Separados, cada uno hace lo suyo: el radio redondea el FONDO del hover, y la
   raya la pinta un `::before` que no sabe nada de radios. Va sangrada 7px
   arriba y abajo para que no toque el relleno de la tira — una raya que llega
   al filo se lee como que la tarjeta está partida en dos. */
.home-metric {
    position: relative;
    display: flex; flex-direction: column; justify-content: center; gap: 2px;
    padding: 6px 13px; border-radius: 10px;
    transition: background .15s ease;
}
.home-metric::before {
    content: ''; position: absolute; left: 0; top: 7px; bottom: 7px;
    width: 1px; background: var(--border);
}
.home-metric:hover { background: var(--bg); }
.home-metric-label {
    font-size: var(--fs-badge); font-weight: 600; color: var(--text-light);
    text-transform: uppercase; letter-spacing: .07em; white-space: nowrap;
}
.home-metric-row { display: flex; align-items: center; gap: 8px; }
.home-metric-value {
    font-size: var(--fs-stat); font-weight: 700; color: var(--text);
    letter-spacing: -.01em; line-height: 1.1;
    font-variant-numeric: tabular-nums;
}
.home-metric-live {
    margin-left: auto; padding-right: 14px; text-align: right;
}
.home-metric-live .home-metric-row { justify-content: flex-end; }
.home-metric-live:hover { background: transparent; }

.home-delta {
    display: inline-flex; align-items: center; gap: 2px;
    font-size: var(--fs-badge); font-weight: 700; line-height: 1;
    padding: 2px 6px 2px 4px; border-radius: 999px;
    font-variant-numeric: tabular-nums;
}
.home-delta svg { width: 11px; height: 11px; }
.home-delta.up { color: var(--ok-text); background: var(--ok-bg); }
.home-delta.down { color: var(--danger-text); background: var(--danger-bg); }

/* El punto que dice «esto se está moviendo». Solo aparece cuando hay dato:
   un pulso junto a un guion prometería una medición que no existe. */
.live-dot {
    position: relative; display: inline-block;
    width: 8px; height: 8px; border-radius: 50%;
    background: var(--ok-text);
}
.live-dot::after {
    content: ''; position: absolute; inset: -4px;
    border-radius: 50%; background: var(--ok-text); opacity: .28;
    animation: live-ping 2s cubic-bezier(0, 0, .2, 1) infinite;
}
@keyframes live-ping {
    0% { transform: scale(.6); opacity: .5; }
    70%, 100% { transform: scale(1.5); opacity: 0; }
}

.home-greeting-title {
    position: relative; z-index: 1;
    margin: 0 0 8px;
    /* El paso `hero` son los 34px de tiendapp: al ladear las capturas, los
       30px de `--fs-number` se notaban. */
    font-size: var(--fs-hero); font-weight: 700;
    letter-spacing: -.025em; line-height: 1.2;
    animation: home-rise .5s ease both;
}

/* La frase de gratitud, portada con sus diez frases reales. Un saludo.
   `home-rise` es la entrada escalonada de tiendapp: la frase primero, los
   chips después. El bloque universal de movimiento reducido la apaga. */
.home-gratitude {
    position: relative; z-index: 1;
    margin: 0 0 30px;
    font-size: var(--fs-section); color: var(--text-light);
    animation: home-rise .5s ease .06s both;
}
@keyframes home-rise {
    from { opacity: 0; transform: translateY(8px); }
    to { opacity: 1; transform: none; }
}

.home-search {
    position: relative; z-index: 2; max-width: 620px; margin: 0 auto;
    /* Tercer paso de la entrada escalonada: título, frase, buscador,
       chips, feed. */
    animation: home-rise .5s ease .12s both;
}
.home-search-icon {
    position: absolute; left: 20px; top: 27px; transform: translateY(-50%);
    width: 18px; height: 18px; color: var(--text-light);
    transition: color .18s ease; pointer-events: none;
}
/* Con `[type="search"]` escrito: la regla genérica de campos —más abajo,
   (0,1,1)— empataba con `.home-search input` y ganaba por orden. La píldora
   salía con el padding de un campo cualquiera y el icono encima del texto.
   Lo mismo su `:focus` contra el `:focus` genérico. */
.home-search input[type="search"] {
    width: 100%; padding: 16px 20px 16px 50px;
    font: inherit; font-size: var(--fs-body);
    border: 1px solid var(--border); border-radius: 999px;
    background: var(--card-bg); color: var(--text);
    box-shadow: var(--shadow); outline: none;
    transition: border-color .18s ease, box-shadow .18s ease;
}
.home-search input::placeholder { color: var(--text-light); opacity: .8; }
.home-search input[type="search"]:focus {
    border-color: var(--primary); box-shadow: 0 0 0 4px var(--primary-soft);
}
.home-search:focus-within .home-search-icon { color: var(--primary); }

/* `:empty` y no una clase: el fragmento que devuelve el servidor viene vacío
   cuando no hay término, así que la caja se abre sola cuando hay algo que
   mostrar y se cierra sola cuando no. Sin JS de por medio. */
.home-search-results {
    position: absolute; top: calc(100% + 10px); left: 0; right: 0; z-index: 40;
    max-height: 380px; overflow-y: auto; padding: 6px; text-align: left;
    background: var(--card-bg); border: 1px solid var(--border);
    border-radius: var(--radius-lg); box-shadow: var(--shadow-lg);
}
.home-search-results:empty { display: none; }
.search-list { list-style: none; margin: 0; padding: 0; }
/* La fila de un resultado: el icono de la pantalla, su nombre, de qué se
   trata y la flecha de que se entra. La baldosa es la misma de las listas
   (`tile-sm`), y a propósito: una pantalla se reconoce por el mismo icono en
   la barra de la izquierda, en el encabezado de su tabla y acá. */
.search-list a {
    display: flex; align-items: center; gap: var(--sp-3);
    padding: 7px 10px; border-radius: 10px;
    color: var(--text); font-size: var(--fs-body); text-decoration: none;
}
.search-list a:hover { background: var(--primary-soft); color: var(--primary-dark); }
/* El nombre puede ser largo —«Distribuidora de Café del Sur SAS»— y la pista
   de la derecha no puede quedar empujada fuera de la caja: encoge el nombre,
   que es lo que se entiende a medias sin perder el renglón. */
.search-label {
    flex: 0 1 auto; min-width: 0; font-weight: 500;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Qué es y de dónde cuelga, en UNA pista a la derecha: separadas eran dos
   grises en el mismo renglón, que se leen como uno partido en dos. */
.search-meta {
    margin-left: auto; flex: none;
    color: var(--text-light); font-size: var(--fs-small); white-space: nowrap;
}
.search-meta-sep { color: var(--text-faint); }
.search-go { width: 16px; height: 16px; flex: none; color: var(--text-faint); }
/* Buscar y no encontrar TAMBIÉN es una respuesta. El fragmento se quedaba
   mudo con un término sin resultados, y una caja que se abre vacía al
   teclear se lee como un buscador roto. */
.search-none {
    margin: 0; padding: 9px 12px;
    color: var(--text-light); font-size: var(--fs-body);
}

/* ── La paleta de ⌘K ─────────────────────────────────────────────────────────
   Un `<dialog>` anclado ARRIBA y no al centro: la pantalla de la que uno
   viene se sigue viendo debajo, y el ojo ya estaba arriba —es donde vive la
   barra—. Mide lo mismo que el buscador del Inicio, que es la otra caja de
   buscar de Agrupa.

   `display` solo con `[open]`: un `<dialog>` cerrado es `display: none`, y
   ponerle `flex` a secas lo deja pintado encima de toda pantalla del ERP. */
.paleta {
    width: min(620px, calc(100vw - 2 * var(--sp-4)));
    max-height: min(70vh, 560px);
    margin: 12vh auto auto; padding: 0;
    border: 1px solid var(--glass-line); border-radius: var(--radius-lg);
    /* VIDRIO: el fondo translúcido y el desenfoque de lo que hay detrás.
       `backdrop-filter` no es decoración acá — sin él, el translúcido deja
       leer el texto de la pantalla de atrás entre los renglones. */
    background: var(--glass-bg); color: var(--glass-text);
    -webkit-backdrop-filter: blur(20px) saturate(140%);
    backdrop-filter: blur(20px) saturate(140%);
    box-shadow: var(--shadow-lg);
    overflow: hidden;
}
.paleta[open] { display: flex; flex-direction: column; }
.paleta::backdrop { background: rgb(12 10 24 / 55%); }
.paleta-caja {
    display: flex; align-items: center; gap: var(--sp-3);
    padding: var(--sp-4);
    border-bottom: 1px solid var(--glass-line);
}
.paleta-glifo { width: 18px; height: 18px; flex: none; color: var(--glass-text-soft); }
/* El campo sin caja propia: la caja es la paleta entera. Con el borde de
   `input` puesto quedaba una caja dentro de otra, que es lo mismo que le
   pasaba a la barra de filtros dentro de su tarjeta. */
.paleta-caja input[type="search"] {
    flex: 1 1 auto; min-width: 0;
    padding: 3px 0; border: 0; background: none; box-shadow: none; outline: none;
    font: inherit; font-size: var(--fs-section); color: var(--glass-text);
}
.paleta-caja input[type="search"]::placeholder { color: var(--glass-text-soft); }
/* SIN anillo de foco acá adentro. La regla general de los campos lo pinta
   al enfocar —y este nace enfocado, que es lo que hace la paleta—, así que
   dibujaba un óvalo pegado al texto por los cuatro lados (Renzo,
   2026-09-15: «mejorar un tri el padding del buscador»). El campo no es una
   casilla sobre una tarjeta: la boca de la paleta es la caja entera, y el
   aire lo pone ella. */
.paleta-caja input[type="search"]:focus { border: 0; box-shadow: none; }
.paleta-kbd {
    flex: none; padding: 1px 6px;
    border: 1px solid var(--glass-line); border-radius: var(--radius-sm);
    background: var(--glass-soft);
    color: var(--glass-text-soft); font: inherit; font-size: var(--fs-meta);
}
.paleta-resultados { padding: var(--sp-2); overflow-y: auto; }
/* Las filas, sobre vidrio: el texto se aclara, la baldosa del icono deja de
   ser morada clara —sobre oscuro se lee como un borrón— y la flecha vive en
   su propio cuadro, que es lo que la vuelve un botón a la vista. */
.paleta .search-list a { color: var(--glass-text); }
.paleta .tile { background: var(--glass-soft); color: var(--glass-text); }
.paleta .search-meta { color: var(--glass-text-soft); }
.paleta .search-meta-sep { color: var(--glass-text-soft); }
.paleta .search-go {
    width: 26px; height: 26px; padding: 5px; border-radius: var(--radius-sm);
    background: var(--glass-soft); color: var(--glass-text-soft);
}
.paleta .search-list a:hover { background: var(--glass-hover); color: var(--glass-text); }
.paleta .search-list a.is-activa {
    background: var(--glass-activa); color: #fff;
    box-shadow: inset 0 0 0 1px var(--glass-line);
}
.paleta .search-list a.is-activa .search-go { background: var(--primary); color: #fff; }
.paleta .search-none, .paleta .search-more { color: var(--glass-text-soft); }
/* La fila que Enter abre. Se marca con una clase y no con `:hover` porque
   acá manda el teclado: el ratón sigue a la marca, no al revés. */
.search-list a.is-activa { background: var(--primary-soft); color: var(--primary-dark); }
/* Y su flecha se llena: en una lista de diez filas iguales, el fondo suave
   solo dice «esta»; la flecha llena dice «y Enter entra acá». */
.search-list a.is-activa .search-go {
    width: 24px; height: 24px; padding: 4px; border-radius: var(--radius-sm);
    background: var(--primary); color: #fff;
}
/* El `hover` lo pone la regla de vidrio de arriba (`.paleta .search-list
   a:hover`). Acá vivía el gris claro del tema, con la MISMA especificidad y
   más abajo: le ganaba, y sobre el vidrio oscuro dejaba una barra blanca
   con el texto blanco encima — ilegible (Renzo, 2026-09-15: «el over blanco
   no se lee»). Dos reglas para el mismo estado es cómo se llega a eso. */
/* Las teclas, escritas al pie: un atajo que no se anuncia lo conocen los
   tres que estaban cuando se hizo. */
.paleta-pie {
    display: flex; align-items: center; gap: var(--sp-4); flex-wrap: wrap;
    padding: var(--sp-2) var(--sp-4);
    border-top: 1px solid var(--glass-line); background: var(--glass-hover);
    color: var(--glass-text-soft); font-size: var(--fs-meta);
}
.paleta-pista { display: inline-flex; align-items: center; gap: 5px; }
.paleta-pie kbd {
    padding: 1px 6px; border: 1px solid var(--glass-line); border-radius: 6px;
    background: var(--glass-soft); color: var(--glass-text); font: inherit;
}
.search-label { overflow-wrap: anywhere; }
.search-more {
    margin: 6px 0 2px; padding: 0 12px 6px;
    color: var(--text-light); font-size: var(--fs-small);
}

.home-chips {
    position: relative; z-index: 1;
    display: flex; gap: 10px; justify-content: center; flex-wrap: wrap;
    margin-top: 22px;
    animation: home-rise .5s ease .18s both;
}
.home-chip {
    display: inline-flex; align-items: center; gap: 9px;
    padding: 8px 8px 8px 16px;
    border: 1px solid var(--border); border-radius: 999px;
    background: var(--card-bg); color: var(--text);
    font-size: var(--fs-body); font-weight: 500; text-decoration: none;
    box-shadow: var(--shadow);
    transition: transform .15s ease, border-color .15s ease;
}
.home-chip:hover {
    border-color: var(--primary); transform: translateY(-1px);
    color: var(--text); text-decoration: none;
}
.home-chip:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--primary-soft); }
.home-chip-count {
    padding: 3px 9px; border-radius: 999px;
    background: var(--primary-soft); color: var(--primary-dark);
    font-size: var(--fs-meta); font-weight: 700; font-variant-numeric: tabular-nums;
}
.home-chip.tone-warn .home-chip-count   { background: var(--warn-bg);   color: var(--warn-text); }
.home-chip.tone-danger .home-chip-count { background: var(--danger-bg); color: var(--danger-text); }

/* ── El home angosto ────────────────────────────────────────────────────────
   La píldora de la tira solo funciona en UNA fila: en cuanto envuelve, el
   radio de 999px y los separadores internos se leen como celdas rotas
   (Renzo lo vio redimensionando, 2026-08-04). Las seis casillas piden unos
   730px, y el ancho que les queda es el de la ventana MENOS el sidebar
   —250px— y el respiro del contenido; por eso el corte va en 1100 y no en
   los 900 de tiendapp, que no tenía sidebar fijo. Debajo de ahí la tira es
   una tarjeta: radio normal, sin separadores, el rango en su propia línea.

   El saludo baja reasignando el paso `hero`. La escala sigue siendo el
   único camino: un tamaño se renombra por pantalla, no se teclea. */
@media (max-width: 1100px) {
    :root { --fs-hero: 28px; }
    .home-metrics { border-radius: var(--radius-lg); }
    .home-metric { border-left: 0; padding: 6px 12px; }
    .home-metrics-head {
        flex-basis: 100%; flex-direction: row; align-items: baseline;
        justify-content: flex-start; gap: 8px; padding: 4px 12px 0;
    }
    .home-metric-live { margin-left: 0; padding-right: 12px;
                        text-align: left; }
    .home-metric-live .home-metric-row { justify-content: flex-start; }
    .home-stage { padding: 46px 0 36px; }
}

/* Lo mismo que el corte de 1100px, pero por el ancho del CONTENIDO: con el
   panel lateral abierto la tira se quedaba sin sitio a cualquier ancho de
   ventana y partía sus casillas en dos filas sin pasar a tarjeta. 820px es
   lo que le queda al contenido en una ventana de 1100 con el menú puesto. */
@container content (max-width: 820px) {
    .home-metrics { border-radius: var(--radius-lg); }
    .home-metric { border-left: 0; padding: 6px 12px; }
    .home-metrics-head {
        flex-basis: 100%; flex-direction: row; align-items: baseline;
        justify-content: flex-start; gap: 8px; padding: 4px 12px 0;
    }
    .home-metric-live { margin-left: 0; padding-right: 12px;
                        text-align: left; }
    .home-metric-live .home-metric-row { justify-content: flex-start; }
}

/* ── El escritorio en el teléfono ───────────────────────────────────────────
   Renzo, 2026-09-15, con la portada abierta en su iPhone: «se ve todo como
   muy grave». Lo que pasaba, medido a 390px: el saludo y su frase se comían
   la primera pantalla —el buscador y los pendientes quedaban debajo del
   pliegue—, la tira de números se partía en 2 y luego 3 casillas con la
   rayita de cada una arrancando a media pantalla, y los pendientes eran
   pastillas centradas de anchos distintos. */
@media (max-width: 640px) {
    :root { --fs-hero: 20px; }
    .home-stage { padding: 18px 0 14px; }
    .home-greeting-title { margin-bottom: 4px; }
    .home-gratitude { margin-bottom: 18px; font-size: var(--fs-small); }
    /* La tira se vuelve REJILLA de dos columnas. Las líneas son el `gap` de
       1px sobre el fondo del contenedor y no un borde por casilla: así valen
       para CUALQUIER número de casillas, y son las que el Escritorio decide
       desde /analiticas/t/escritorio/. */
    .home-metrics {
        display: grid; grid-template-columns: 1fr 1fr;
        gap: 1px; padding: 1px; overflow: hidden;
        background: var(--border);
    }
    .home-metrics-head, .home-metric { background: var(--card-bg); }
    .home-metrics-head { grid-column: 1 / -1; padding: 8px 12px; }
    /* «RANGO» se va: el glifo del calendario ya dice qué es, y su rótulo le
       robaba el renglón a los dos disparadores, que caían uno debajo del
       otro. */
    .home-metrics-scope { display: none; }
    .home-metric { padding: 9px 12px; border-radius: 0; }
    .home-metric::before { display: none; }
    /* La última casilla, cuando queda sola en su renglón, ocupa el ancho:
       media casilla vacía deja ver el fondo de las líneas como un bloque
       gris. Es `even` porque el primer hijo de la rejilla es el rango. */
    .home-metric:last-child:nth-child(even) { grid-column: 1 / -1; }
    .home-metric:hover, .home-metric-live:hover { background: var(--card-bg); }
    /* Los pendientes, en columna y a todo el ancho, con el conteo a la
       derecha: se leen como una lista de lo que falta y se tocan con el
       pulgar. */
    .home-chips {
        flex-direction: column; align-items: stretch;
        gap: 8px; margin-top: 16px;
    }
    .home-chip { justify-content: space-between; padding: 10px 12px 10px 16px; }
    /* La hora baja debajo del texto y alineada con él: al lado le dejaba al
       texto una columna de dos palabras. */
    .home-feed-item { align-items: flex-start; gap: 10px; flex-wrap: wrap; }
    .home-feed-glyph { width: 28px; height: 28px; }
    .home-feed-when { flex-basis: 100%; padding-left: 38px; }
}

/* ── Encabezado de pantalla ────────────────────────────────────────────── */

/* ── El banner de sección ──────────────────────────────────────────────────

   Lo pinta el shell, una vez por vista. Antes cada plantilla escribía su
   propio `<div class="page-header">`: veinticuatro copias del mismo
   contenedor, y solo cinco sabían dónde poner una acción.

   El resplandor va en el FONDO y no en un elemento: un `<div>` decorativo
   sería un nodo más que un lector de pantalla recorre y que hay que
   acordarse de esconder. Un degradado no existe para el árbol de
   accesibilidad, y hay una prueba que lo fija.

   `flex-wrap` y no un `grid` de dos columnas: a 390px el título y sus dos
   botones no caben en una línea, y con el grid los botones se comprimían
   hasta partir la palabra «Guardar». Envolviendo, bajan enteros. */
.page-banner {
    display: flex; justify-content: space-between;
    /* CENTRADO, no arriba: con `flex-start` los botones quedaban colgando del
       borde superior mientras el título ocupaba dos líneas. */
    align-items: center;
    gap: var(--sp-4); flex-wrap: wrap;
    margin-bottom: var(--sp-5); padding: var(--sp-6) var(--sp-7);
    border-radius: var(--radius-lg);
    color: var(--banner-text);
    background: linear-gradient(135deg, var(--banner-from), var(--banner-to));
    /* Para el círculo de abajo, que se recorta contra los bordes de la banda. */
    position: relative; overflow: hidden;
}

/* El círculo decorativo. Un `::after` y NO un `<div>`: un elemento sería un
   nodo más que un lector de pantalla recorre y que hay que acordarse de
   esconder; un pseudo-elemento no existe para el árbol de accesibilidad. */
.page-banner::after {
    content: ''; position: absolute; top: -40px; right: -20px;
    width: 220px; height: 220px; border-radius: 50%;
    background: var(--primary); opacity: .12;
}
/* Y el contenido por encima del círculo. */
.page-banner > * { position: relative; z-index: 1; }
.page-banner h1 {
    margin: 0; font-size: var(--fs-page); font-weight: 700;
    letter-spacing: -.02em; color: var(--banner-text);
}
/* `min-width: 0` para que el texto largo se rompa en vez de estirar la
   columna y empujar los botones fuera de la banda. */
/* `flex: 1` para que el texto se coma el sobrante y el avatar quede PEGADO a
   él. Sin esto, con tres hijos el `space-between` del banner reparte el hueco y
   la inicial se va al borde izquierdo, a un palmo del nombre al que pertenece.
   Con dos hijos —el caso de las demás pantallas— el resultado no cambia. */
/* La BASE va en cero y no en `auto`, y esa palabra es la que sostiene el
   banner de tres hijos.

   Con `flex-wrap: wrap` el navegador parte la línea ANTES de encoger: reparte
   por el tamaño hipotético de cada hijo —que con base `auto` es el ancho de su
   contenido— y sólo después aplica `flex-shrink` dentro de cada línea. El
   párrafo de esta pantalla mide 1.010px de contenido, y con la baldosa
   delante los tres pedían 1.168 contra 1.152 de banda: se pasaba por
   dieciséis píxeles, la banda partía, y el botón «Volver» caía debajo del
   título alineado a la izquierda. `min-width: 0` no lo salvaba — permite
   encoger, pero para entonces ya se había partido.

   Con la base en cero los tres caben siempre, y el texto crece hasta llenar
   lo que sobra, que es lo que decía el párrafo de abajo y ahora es verdad
   también con tres hijos. */
/* Y el PISO, que es la otra mitad de esa misma trampa: con la base en cero
   el tamaño hipotético del texto también es cero, así que los tres hijos
   «caben» SIEMPRE y la banda no envuelve nunca — ni a 390px, donde el texto
   quedaba en una columna de seis píxeles y el título se desbordaba por debajo
   de los botones. Renzo lo vio en la ficha de un integrante y en la pantalla
   de su PIN el 2026-09-03.

   El piso le da al texto un tamaño hipotético de verdad para el reparto: si
   la baldosa, el piso y los botones no caben en una línea, los botones bajan
   enteros y el título se queda con la suya, que después crece hasta llenarla.
   `14rem` cabe con holgura en cualquier banda de escritorio junto a la
   baldosa y las acciones —así que ahí nada cambia—, y el `60%` es para un
   teléfono angosto, donde 14rem más la baldosa ya no entran. */
.page-banner-text { min-width: min(14rem, 60%); flex: 1 1 0; }
.page-banner-meta {
    margin: 5px 0 0; color: var(--banner-sub); font-size: var(--fs-body);
}
/* `.mono` dentro del banner HEREDA el color de la banda, no el suyo.

   `.mono` fija `--text-light`, que es un gris pensado para fondo claro: sobre
   la banda oscura daba 3.11:1 —por debajo del 4.5 de la WCAG— y el NIT del
   tercero salía casi ilegible justo en el sitio donde se coteja contra una
   factura. El resto de la meta da 9.83, así que el defecto era del contexto y
   no del color de la banda.

   Viene de antes de la ficha: el formulario ya pintaba el documento con
   `class="page-banner-meta mono"`. Se arregla acá, en el banner, y no
   cambiándole el color a `.mono`, que sobre blanco está bien. */
.page-banner-meta.mono, .page-banner-meta .mono { color: inherit; }
/* Un enlace en la meta —«Se usa en Descuento PARCEROSVIP»— también hereda:
   el color de enlace es para fondo claro y sobre la banda no contrasta. Lo
   que lo distingue es el subrayado. */
.page-banner-meta a { color: inherit; text-decoration: underline; }
/* Las acciones NO se estiran y se mantienen juntas: envueltas a 390px caen
   como un bloque debajo del título, en el orden en que se leen. */
.page-banner-actions {
    display: flex; align-items: center; gap: 10px;
    flex-wrap: wrap; flex-shrink: 0;
}
/* Un `<form>` no anida, así que las acciones de la banda que van en POST
   —empezar la preparación, deshacerla— traen cada una el suyo y quedan como
   hermanas de los enlaces. Sin esto, el margen por omisión del formulario las
   desalinea contra los botones de al lado. */
.page-banner-actions .banner-form { margin: 0; display: contents; }
/* Sobre la banda, el botón fantasma se dibuja con el borde claro: el de
   `--border` desaparece contra el negro y quedaba un texto flotando. */
.page-banner .btn-ghost {
    color: var(--banner-text);
    border-color: color-mix(in srgb, var(--banner-text) 28%, transparent);
    background: transparent;
}
.page-banner .btn-ghost:hover {
    background: color-mix(in srgb, var(--banner-text) 12%, transparent);
    color: var(--banner-text);
}
/* Y lo DESTRUCTIVO sobre la banda, que es la otra mitad que faltaba.

   `.btn-danger-ghost` se pinta con `--text-light` y sólo se vuelve rojo al
   pasar por encima: correcto sobre una tarjeta blanca —lo destructivo no
   grita hasta que lo tocas— y un defecto sobre el negro, donde ese gris da
   2:1 y el botón se lee como DESHABILITADO. Renzo lo vio así en la ficha de
   una variante el 2026-08-17: «Eliminar» parecía apagado al lado de
   «Cancelar», que sí tenía su regla de banda desde siempre.

   Es la misma forma de defecto que el pie de tabla de esta misma sesión: una
   regla escrita para una variante y no para su hermana.

   RELLENO y no fantasma, con el par de `badge-soft-danger` —que es el que la
   banda ya usa para decir peligro—: sobre el negro un contorno rojo compite
   con el borde claro de «Cancelar» y las dos acciones se leen igual de
   pesadas. Rellena, se ve cuál es la que borra sin leerla. */
.page-banner .btn-danger-ghost,
.page-banner .btn-danger {
    background: var(--danger-bg);
    color: var(--danger-text);
    border-color: var(--danger-bg);
}
.page-banner .btn-danger-ghost:hover,
.page-banner .btn-danger:hover {
    background: var(--danger-vivid);
    color: var(--danger-vivid-text);
    border-color: var(--danger-vivid);
}

/* ── Los tres puntos de la banda ──────────────────────────────────────────
   TODA acción de la banda vive dentro del menú, aunque sea una sola (Renzo,
   2026-09-13). El shell envuelve `page_actions` en `.page-banner-menu`; acá
   los botones que las pantallas ya escriben se visten de filas del menú, y
   las reglas de banda de arriba —fantasma claro, peligro relleno— se apagan
   porque el desplegable es una tarjeta clara y no la banda oscura. Van con
   TRES clases (0,3,0) a propósito: `.page-banner .btn-ghost` pesa (0,2,0) y
   está más arriba, y con el mismo peso ganaría por orden. */
.page-banner-menu { flex-shrink: 0; }
/* Sin acciones, sin menú: un botón de tres puntos que no ofrece nada es una
   promesa rota. `:has` mira lo que el bloque trajo. */
.page-banner-menu:not(:has(.btn, button, a, label)) { display: none; }
/* El disparador se dibuja como el botón fantasma de la banda, con su borde
   claro: los 32px grises del menú de una fila desaparecen contra el negro. */
.page-banner .page-banner-menu > .menu-trigger {
    width: var(--btn-h); height: var(--btn-h);
    color: var(--banner-text);
    border: 1px solid color-mix(in srgb, var(--banner-text) 28%, transparent);
    border-radius: var(--radius-sm);
}
.page-banner .page-banner-menu > .menu-trigger:hover,
.page-banner .page-banner-menu[open] > .menu-trigger {
    background: color-mix(in srgb, var(--banner-text) 12%, transparent);
    color: var(--banner-text);
}
/* El envoltorio que las pantallas ya traen deja de ser una fila: sus hijos
   pasan a ser filas del menú, una debajo de otra. */
.page-banner .menu-pop .page-banner-actions { display: contents; }
/* Sin relleno arriba: la primera fila ya trae su `margin-top`, y sumarle el
   del desplegable dejaba más aire arriba que abajo. */
.page-banner .menu-pop { min-width: 240px; padding-top: 0; }
/* Cada botón, del tono que sea, es una fila del menú: sin borde ni relleno de
   color, a la izquierda y al ancho. El glifo que `_btn.html` ya trae queda
   como el de `.menu-item`. */
.page-banner .menu-pop .btn {
    display: flex; width: 100%; height: auto; min-height: 0;
    justify-content: flex-start; gap: var(--sp-2);
    padding: 8px 10px; border: 0; border-radius: var(--radius-sm);
    box-shadow: none; background: transparent;
    color: var(--text); font-weight: 500; font-size: var(--fs-body);
}
/* Aire entre filas (Renzo, 2026-09-13): con 1px el fondo del hover pegaba
   contra la raya. La raya va al medio del hueco. */
.page-banner .menu-pop .btn { margin-top: var(--sp-2); position: relative; }
/* Una raya entre fila y fila (Renzo, 2026-09-13). Va como pseudoelemento y
   no como borde porque las filas viven dentro de `<form>` y `<div>` con
   `display: contents`, y un borde en un elemento que no pinta caja no se
   ve. La primera fila no la lleva: la raya separa, no encabeza. */
.page-banner .menu-pop .btn::before {
    content: ''; position: absolute; left: 10px; right: 10px;
    top: calc(var(--sp-1) * -1); height: 1px; background: var(--border);
}
/* Cuatro formas de «primera fila»: el botón suelto, el botón dentro de su
   `<form>`, y las dos mismas dentro del envoltorio `.page-banner-actions`.
   El `:not(.page-banner-actions)` importa: sin él, «primer hijo > .btn»
   casaba con TODOS los botones del envoltorio y no salía ni una raya. */
.page-banner .menu-pop > :first-child:not(.page-banner-actions)::before,
.page-banner .menu-pop > :first-child:not(.page-banner-actions) > .btn::before,
.page-banner .menu-pop > .page-banner-actions > :first-child::before,
.page-banner .menu-pop > .page-banner-actions > :first-child > .btn::before { display: none; }
.page-banner .menu-pop .btn:where(:not(:disabled)):hover {
    background: var(--primary-soft); color: var(--primary-dark);
    border: 0; box-shadow: none;
}
/* Lo que BORRA se pone rojo al pasar por encima y no en reposo, como
   `.menu-item-danger`: una lista con un ítem rojo permanente se lee como una
   alarma abierta. */
.page-banner .menu-pop .btn-danger:where(:not(:disabled)):hover,
.page-banner .menu-pop .btn-danger-ghost:where(:not(:disabled)):hover {
    background: var(--danger-bg); color: var(--danger-text);
}

.page-search { display: flex; gap: 8px; margin-bottom: 16px; max-width: 560px; }
.page-search input {
    flex: 1; min-width: 0; padding: 11px 14px;
    font: inherit; font-size: var(--fs-body); color: var(--text);
    background: var(--card-bg);
    border: 1px solid var(--border-strong); border-radius: var(--radius-field);
    outline: none;
}
.page-search input:focus {
    border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-soft);
}

/* ── La tabla ──────────────────────────────────────────────────────────────
   El scroll horizontal vive ACÁ, nunca en el `body`. Es lo que todas las
   pantallas con tabla necesitan igual, y por eso es lo que el componente
   aporta; las celdas las escribe cada pantalla.
   ------------------------------------------------------------------------ */

.table-wrap {
    /* NADA DE SCROLL VERTICAL PROPIO (Renzo, 2026-08-14). La página es la
       que se desplaza, y la tabla mide lo que midan sus filas.

       Tuvo un `max-height` para tener un scrollport contra el cual pegar el
       `thead`. Funcionaba, y el precio era una caja que se desplaza dentro
       de otra que se desplaza: la rueda del ratón hace una cosa distinta
       según dónde esté el puntero, la barra de la tabla y la de la página
       conviven a dos centímetros, y llegar al pie de una lista de cincuenta
       filas obliga a desplazar dos veces. Un ERP con una sola barra se
       opera sin pensar en dónde estás parado.

       EL PRECIO, dicho: el `thead` pegajoso se pierde. `overflow-x: auto`
       hace que el otro eje compute a `auto`, así que esto SIGUE siendo un
       contenedor de scroll —hace falta para el desplazamiento lateral— y un
       `position: sticky` de adentro se pega a ESTA caja, que ya no se
       desplaza en vertical. Recuperarlo pidiendo que se pegue a la página
       exige que la tabla deje de ser contenedor de scroll, y con eso el
       `body` se desplazaría de lado, que es lo que §6.1 prohíbe. Son
       excluyentes: se eligió la barra única. */
    overflow-x: auto;
    background: var(--card-bg);
    /* SIN borde propio. Lo tuvo mientras una tabla podía vivir suelta sobre el
       fondo; ahora las once listas van dentro de una tarjeta que ya trae el
       suyo, así que el de acá era un segundo contorno a un píxel del primero.
       El radio se queda: es el que redondea las esquinas de abajo cuando la
       tabla es lo último de la tarjeta. */
    border-radius: var(--radius-lg);
    /* Que la barra de scroll se vea siempre en macOS: una tabla que se
       desplaza sin señal de que se desplaza es una tabla a la que le faltan
       columnas. */
    scrollbar-width: thin;
}
.table {
    width: 100%; border-collapse: collapse;
    font-size: var(--fs-body); white-space: nowrap;
}
/* El encabezado, en FRASE NORMAL sobre una banda.

   Era versalitas de 11px en negrita con `letter-spacing`. Cinco rasgos
   tipográficos —caja alta, tamaño chico, peso 700, espaciado, color
   apagado— para decir «esto es un rótulo», cuando la posición ya lo dice:
   está arriba de la columna. El resultado era un rótulo más difícil de leer
   que el dato que rotula, y con «CONCILIABLE» al lado de «ESTADO» la caja
   alta además borra la frontera entre dos palabras.

   Ahora lo que separa el encabezado de los datos es el FONDO, que es una
   señal que no compite con la lectura. Y en frase normal el rótulo largo
   —«Método de pago»— se lee de un golpe en vez de deletrearse.

   El peso 500 y no 400: sobre la banda, a 400 el rótulo se hunde. */
/* Sin `position: sticky`. Lo llevaba, y desde que la tabla dejó de tener
   scrollport propio no se pegaba a nada: se quedaba escrito prometiendo algo
   que no ocurre, que es como llegó a estar roto la primera vez. Ver el
   comentario de `.table-wrap` para por qué las dos cosas son excluyentes. */
.table thead th {
    padding: var(--sp-3) var(--sp-4); text-align: left;
    font-size: var(--fs-small); font-weight: 500;
    color: var(--text-light);
    background: var(--table-head-bg); border-bottom: 1px solid var(--border);
}
.table tbody td {
    /* RELLENO A LOS LADOS, que es lo que cambió el 2026-08-14. No lo tuvo
       nunca: la separación salía del ancho del contenido, con el argumento
       de que en tablas de texto sobra. Sobra hasta que no: la lista de
       productos mostraba «Accesorios4» —el tipo pegado a la cantidad de
       variantes— y el plan de cuentas leía «CONCILIABLEESTADO». Las dos son
       la misma falla, y el parche era una clase (`.cell-pad`) que cada
       celda corta tenía que acordarse de llevar. Acordarse no escala; el
       relleno de la casa sí.

       Y POCO relleno arriba y abajo (Renzo, 2026-08-14). Decía «filas que
       respiran» y llevaba `--sp-3`, con el argumento de que a 11px la
       primera celda de dos líneas quedaba apretada. Ese argumento envejeció
       con la piel nueva: ahora hay una línea entre fila y fila, así que la
       separación ya no la tiene que dar el aire. Con `--sp-3` la fila medía
       60px y en una pantalla cabían nueve productos de once; con `--sp-1`
       mide 44 y caben todos, que es lo que se le pide a una lista.

       La ALTURA de la fila no la fija esto: la fija la miniatura de 36px,
       que es más alta que el texto. Bajar más el relleno no compacta nada
       —tocaría bajar `--cell-thumb`— y sí dejaría el texto de las celdas
       sin miniatura pegado a las líneas de arriba y abajo. */
    padding: var(--sp-1) var(--sp-4);
    /* Y el resto de la fila es SECUNDARIO. Lo que identifica una fila es su
       primera celda: el tipo, la cantidad de variantes y el contacto están
       para consultarse, no para leerse de corrido. En el mismo negro que el
       nombre compiten con él, y quinientas filas de texto negro parejo son un
       muro. El nombre vuelve a `--text` en la regla del enlace. */
    color: var(--text-light);
    vertical-align: middle;
    /* La LÍNEA entre filas. No la tuvo nunca: lo único que separaba una fila
       de la siguiente era el hover, o sea nada mientras el puntero está en
       otra parte. En una tabla de seis columnas eso obliga a seguir el
       renglón con la vista sin nada que lo sostenga, y a la cuarta columna
       ya se está leyendo la fila de abajo.

       Va en `--table-line` y no en `--border`: quinientas líneas del color
       del borde de una tarjeta convierten la lista en una reja. */
    border-bottom: 1px solid var(--table-line);
}
/* La última no lleva línea: debajo no hay fila que separar, y contra el
   filo de la tarjeta se lee como un borde de más. */
.table tbody tr:last-child > td { border-bottom: 0; }
/* Los dos EXTREMOS respiran más. Con el mismo relleno que el resto, la
   primera celda arranca a 16px del filo de la tarjeta, que trae los suyos:
   la columna se lee metida hacia adentro respecto del título de la tarjeta
   que la encabeza. */
/* El PIE va incluido desde el 2026-08-17 (Renzo: «este footer se ve raro»).
   Estaba escrito solo para `thead` y `tbody`, así que la fila de totales
   perdía los 20px de los dos extremos y sobresalía por los dos lados de su
   propia tabla: «Total valorizado» arrancaba a la izquierda del rótulo de su
   columna y la cifra terminaba pasada del borde. Alcanza a las tres tablas
   del ERP que llevan pie —existencias, valorización y los costos de un
   producto—, que tenían el mismo defecto sin que nadie lo hubiera nombrado. */
.table thead th:first-child,
.table tbody td:first-child,
.table tfoot td:first-child { padding-left: var(--sp-5); }
.table thead th:last-child,
.table tbody td:last-child,
.table tfoot td:last-child { padding-right: var(--sp-5); }

/* El rótulo de una columna que ABRE CON MINIATURA se corre hasta el nombre.

   La celda mide miniatura + hueco + texto, así que el rótulo pegado al filo
   queda encima de la foto: rotula lo que no nombra, y la columna se lee
   corrida respecto de su propio encabezado. Es la última pieza que separaba
   esta tabla de la referencia (Renzo, 2026-08-14).

   El corrimiento se DERIVA de la miniatura y su hueco —los mismos tokens
   que pinta `.cell-thumb` y el `gap` de `.cell-media`—, no se escribe. Con
   un `48px` a mano, agrandar la foto desalinea el encabezado y nada lo
   avisa: quedaría casi bien, que es la peor forma de estar mal.

   Va DESPUÉS de la regla del `:first-child` a propósito: las dos pesan
   (0,2,1) y a igual especificidad manda el orden del archivo. La lección de
   `.cell-pad` y de `.field-check`, que estaban escritas y no hacían nada. */
.table thead th.th-lead {
    padding-left: calc(var(--sp-5) + var(--cell-thumb) + var(--sp-3));
}

/* La canaleta de la casilla de selección.

   Ancho FIJO y no «lo que ocupe»: la casilla mide 20px y el rótulo de la
   columna de al lado empezaría a 20px del filo, o sea encima de ella. Con
   la canaleta declarada, la primera columna de datos arranca siempre en el
   mismo sitio esté o no encendida la selección — que es lo que deja que
   una lista con verbos y otra sin ellos se lean como la misma tabla.

   `line-height: 0` porque el `<input>` es `inline` y la caja de línea le
   agrega unos píxeles debajo que descuadran la fila contra sus vecinas.

   Cuando esta columna existe, es ella la primera y por lo tanto la que
   toma el relleno de borde: la regla de `:first-child` ya la alcanza sola.
   Lo que sí hay que quitarle es el corrimiento de `.th-lead`, que se
   calculó suponiendo que la columna de la foto era la primera. */
.cell-check {
    width: calc(var(--sp-5) + 20px);
    line-height: 0;
}
/* SIN relleno a la derecha. Lo heredaba de `.table tbody td` y se sumaba al
   de la izquierda de la celda vecina: 16 + 16 = 32px entre la casilla y la
   miniatura, que a esa distancia dejan de leerse como parte de la misma
   fila. La separación entre dos columnas la pone UNA de las dos, y acá le
   toca a la vecina, que es la que tiene contenido que separar.

   VA NOMBRADO en el selector, y ésta es la CUARTA vez que la misma cascada
   muerde en este archivo —`.cell-pad`, `.field-check`, `.num` y ahora
   ésta—. Escrito `.cell-check { padding-right: 0 }` no hace nada: pesa
   (0,1,0) contra los (0,1,2) de `.table tbody td`, la regla queda escrita,
   se lee bien y el hueco sigue en 32px. Si vuelves a tocar el relleno de
   una celda concreta, nombra la tabla y el elemento o no pasará nada. */
.table thead th.cell-check,
.table tbody td.cell-check { padding-right: 0; }

/* ── La barra de la selección múltiple ────────────────────────────────────
   Encima de la tabla y dentro de la tarjeta, no flotando sobre la pantalla.
   Flotante taparía filas —justo las que se están mirando para decidir— y en
   una tabla larga hay que poder desplazarse con la selección puesta.

   Se pinta VISIBLE y la esconde el script: sin él, unas casillas sin barra
   son casillas que no llevan a ninguna parte. Ver `assets/js/bulk.js`.
   ------------------------------------------------------------------------ */
/* La barra ES el encabezado de la tarjeta con otro contenido, no una franja
   nueva (Renzo, 2026-08-14).

   Primer intento: una banda morada que aparecía ENCIMA del encabezado.
   Empujaba la tabla 47px hacia abajo, así que la fila que se iba a marcar
   se movía justo antes del clic siguiente. Segundo intento: turnarse con el
   encabezado, pero pintada de morado seguía leyéndose como otra cosa que
   aparece, que es exactamente lo que no se quería.

   Lo que va es UNA banda que cambia de contenido: «14 PRODUCTOS» cuando no
   hay nada marcado, «3 seleccionados» y los verbos cuando sí. Por eso copia
   al encabezado en todo lo que se ve —fondo, relleno, borde— y el conteo
   usa `.card-count`, la misma versalita del número que reemplaza. Lo único
   que cambia entre los dos estados son las palabras y los botones.

   `bulk.js` las turna, y por eso miden lo mismo: mismo relleno y un solo
   borde abajo. Un píxel de diferencia sería un píxel de salto en cada clic.

   Sin margen propio: al filo la lleva el `<form>`, que ya lo hace por ella
   y por la tabla. */
.bulkbar {
    display: flex; flex-wrap: wrap; align-items: center;
    gap: var(--sp-3) var(--sp-4);
    padding: var(--sp-3) var(--sp-5);
    /* SIN fondo, igual que el encabezado que reemplaza. Lo llevaba en
       `--card-bg` para «copiarlo», y el encabezado no tiene ninguno: es
       transparente sobre el blanco de la tarjeta. La diferencia importa
       porque esta banda llega a los filos, así que un fondo propio se pinta
       POR ENCIMA del radio de 12px de la tarjeta y le cuadra las esquinas
       de arriba — «al seleccionar se eliminan los bordes con radius».
       Transparente, la esquina la sigue dibujando la tarjeta. */
    border-bottom: 1px solid var(--border);
    font-size: var(--fs-body);
}
/* `hidden` TIENE que ganarle al `display` de arriba.

   La hoja del navegador trae `[hidden] { display: none }` con especificidad
   (0,1,0), y `.bulkbar` con su `display: flex` vale lo mismo pero va
   después: gana el nuestro, y el elemento queda con el atributo puesto y a
   la vista. Se vio exactamente así —`bar.hidden === true` y
   `getComputedStyle(bar).display === 'flex'`—, con la barra anunciando «0
   seleccionados» encima de la tabla.

   Vale para CUALQUIER componente de esta hoja que declare `display` y se
   apague con `hidden`: los `<p>` de aquí abajo se ocultan solos porque no
   declaran ninguno, y por eso el defecto salió en uno y no en los otros. */
.bulkbar[hidden] { display: none; }
/* Y el mismo margen superior NEGATIVO que el encabezado que reemplaza.

   `.card-header` lleva `margin-top: -16px` para meterse dentro del relleno
   de la tarjeta y empezar en su borde. La barra vive un nivel más adentro
   —dentro del `<form>`— y sin ese margen empezaba 16px más abajo: medido,
   la tabla bajaba 17px al marcar la primera casilla (16 del relleno más el
   píxel de diferencia de altura). Un salto pequeño y en cada clic, que es
   peor que uno grande y una vez.

   Va sobre la barra y no sobre el formulario: el formulario también
   contiene la tabla, y subirlo entero la metería debajo del encabezado
   cuando NO hay selección. */
.card-table > [data-bulk-form] > .bulkbar {
    margin-top: calc(var(--sp-4) * -1);
}
/* AQUÍ VIVIÓ una excepción al tamaño de los botones, y duró un día.

   Decía «el botón NO manda la altura de la banda» y le bajaba el relleno
   para que midiera 20 en vez de 22, porque el título medía 20,9 y esa
   diferencia era un píxel de salto al seleccionar. Funcionaba, y era un
   parche local a un problema general: dos piezas que tienen que medir lo
   mismo, igualadas ajustando una de las dos a mano.

   Se retira con el sistema de tamaños: ahora la banda declara su alto
   (`--band-h`) y ese alto se deriva del botón que tiene que poder llevar.
   El contenido ya no negocia la altura del contenedor.

   (Y no era la sombra: el botón destructivo lleva una, pero `box-shadow` se
   pinta fuera del flujo y no ocupa maquetación.) */
.bulkbar-all { margin: 0; color: var(--text-light); font-size: var(--fs-small); }
/* A la derecha del todo: los verbos son el final de la frase que empieza en
   «3 seleccionados». `margin-left: auto` y no `justify-content` porque las
   dos líneas de la izquierda aparecen y desaparecen. */
.bulkbar-actions {
    display: flex; flex-wrap: wrap; gap: var(--sp-2);
    margin-left: auto;
}
/* El tope, en el renglón de abajo y a lo ancho: es una advertencia, no un
   dato de la selección, y metida entre el contador y los botones se leería
   como parte de lo que está pasando ahora. */
.bulkbar-cap {
    flex-basis: 100%;
    margin: 0; font-size: var(--fs-meta); color: var(--text-light);
}
.table thead th.cell-check + th.th-lead,
.table thead th.cell-check ~ th.th-lead {
    padding-left: calc(var(--cell-thumb) + var(--sp-3) + var(--sp-4));
}
.table tbody tr:hover { background: var(--bg); }
/* La fila que abre una ficha (`data-row-href`, `js/rows.js`): la mano dice
   que toda la fila es el enlace, no solo el nombre. */
html.js .table tbody tr[data-row-href] { cursor: pointer; }
/* La fila que se está editando desde el formulario de arriba de la tabla.
   Con la tabla desplazada, el campo y la fila no se ven a la vez: esto es lo
   único que las relaciona. `background-color` y no el atajo `background`, que
   le borraría la flecha a cualquier `select` que caiga dentro. */
.row-editing td { background-color: var(--primary-soft); }
/* ── El pie que totaliza ───────────────────────────────────────────────────
   Lo pinta `ui/_total.html`. RESPIRA MÁS que una fila del cuerpo (Renzo,
   2026-08-17): una fila se recorre y el total se lee, y con el mismo alto que
   sus cincuenta vecinas se lee como la fila cincuenta y uno. Los 11px de
   antes eran los de una fila.

   El relleno horizontal NO se declara acá. Con el atajo `padding` se borraba
   el que la regla de los extremos le acababa de dar —va después y gana— y la
   fila de totales sobresalía por los dos lados de su propia tabla. Es la misma
   forma de defecto que ya costó tres veces en esta hoja, ahora por un atajo y
   no por especificidad. */
/* FILAS DE SECCIÓN Y DE SUBTOTAL dentro del cuerpo (2026-09-17). Los estados
   financieros agrupan cuentas por sección y cierran cada sección con su
   total, y escribían esas filas con `<th>`: la hoja solo viste el `thead`,
   así que el navegador las centraba y las ponía en negrita a su manera
   (Renzo, capturas del flujo de efectivo y el balance). La sección se lee
   como un encabezado menor, en la tinta apagada del `thead`; el subtotal
   cierra la sección con una raya fina y el total general con la raya doble
   del pie. Ver «Tabla» en docs/marco-ui.md. */
.table tbody tr.row-group > td {
    padding-top: var(--sp-4); padding-bottom: var(--sp-2);
    font-size: var(--fs-small); font-weight: 600; letter-spacing: .04em;
    text-transform: uppercase; color: var(--text-light);
    background: var(--table-head-bg);
}
.table tbody tr.row-total > td { font-weight: 600; border-top: 1px solid var(--border); }
.table tbody tr.row-total-strong > td { font-weight: 700; border-top: 2px solid var(--border); }
.table tfoot td {
    /* EL MISMO relleno lateral que una celda del cuerpo, y por eso escrito y
       no heredado: la regla base es `.table tbody td` y nunca alcanzó al pie,
       así que sus celdas se quedaban con el `padding: 1px` que trae el
       navegador. La cifra caía 15px a la izquierda de la columna que
       totaliza, y a simple vista se lee como un número puesto en el aire
       (Renzo, 2026-08-17: «nada que está alineado»). Medido en el navegador,
       que es la única forma de cazar esto: el `<td>` estaba en su sitio y lo
       corrido era su contenido. */
    padding: var(--sp-4) var(--sp-4);
    border-top: 2px solid var(--border);
    font-weight: 700;
}
/* El rótulo, con el peso de un nombre de fila y en la tinta del texto: el
   `700` de la regla de arriba lo heredaba también el segundo renglón, y una
   aclaración en negrita deja de leerse como aclaración. */
.total-label { font-weight: 600; color: var(--text); }
.table tfoot .cell-sub { font-weight: 400; }
/* La CIFRA es lo que se viene a leer: en tinta de marca y con cifras de ancho
   fijo para que cuadre con la columna que cierra.

   En `--fs-stat`, que es el token de «una cifra de un vistazo» —el de la tira
   del escritorio— y no `--fs-section`: aquél mide 13.5px contra los 13 del
   rótulo de al lado, o sea medio píxel de diferencia, que no es una jerarquía,
   es una casualidad. `--fs-number` es la cifra que ES el contenido de una
   pantalla entera, y ésta cierra una tabla. */
.total-value {
    font-size: var(--fs-stat); color: var(--primary-dark);
    font-variant-numeric: tabular-nums;
}
/* Una suma PARCIAL. El fondo entero y no solo la cifra: lo que está
   incompleto es el renglón, no el número — y teñir solo la cifra deja el
   rótulo diciendo «Total» en negro, que es la mitad que se lee primero. */
.total-danger td { color: var(--danger-text); background: var(--danger-bg); }
.total-danger .total-value, .total-danger .total-label { color: var(--danger-text); }
/* Una celda de PLATA. A la derecha para que las cifras cuadren por su unidad:
   alineadas a la izquierda, «$1.400» y «$14.537» arrancan juntas y terminan en
   dos columnas distintas, y comparar dos números obliga a leerlos enteros. */
.table .cell-num { text-align: right; }

/* `.cell-pad` VIVIÓ AQUÍ, y se retiró el 2026-08-14.

   Existió porque las celdas no llevaban relleno horizontal: la separación
   salía del ancho del contenido, que alcanza hasta que el contenido es más
   angosto que su rótulo. Un interruptor bajo «CONCILIABLE» y una pastilla
   bajo «ESTADO» dejaban leer «CONCILIABLEESTADO» en el plan de cuentas, y
   la clase se le ponía a la celda corta para separarla de su vecina.

   Se retira porque el arreglo estaba en el lugar equivocado. La misma falla
   apareció en productos —«Accesorios4», el tipo pegado a las variantes— en
   una celda donde nadie se acordó de ponerla, y ése es el punto: un defecto
   que se corrige celda por celda vuelve en la próxima tabla que se escriba.
   Ahora el relleno lo pone `.table tbody td`, y ochenta y seis usos de la
   clase se quitaron con ella.

   No la vuelvas a escribir: `tests/test_marco_ui.py` lo caza, y una celda
   con las dos cosas queda con el doble de relleno que sus vecinas. */

/* La celda de TEXTO LARGO no manda el ancho de la tabla.

   `.table` fija `white-space: nowrap`, y con eso un solo nombre de 149
   caracteres —13551510, «Anticipo de retencion en la fuente en Compra de
   bienes…»— le fija el ancho a toda su columna y empuja el resto de la
   tabla fuera de la pantalla: 1.534 filas pagan el scroll horizontal de
   una. La celda que puede crecer declara que PARTE línea, con tope, y el
   tope va en `ch` porque lo que se está limitando es texto. */
.cell-wrap {
    white-space: normal;
    max-width: 56ch;
}
.nowrap { white-space: nowrap; }

/* La celda de una CANTIDAD, y el rótulo que la encabeza.

   A la derecha porque es lo que alinea las unidades bajo las unidades: en una
   columna centrada o a la izquierda, 8 y 1.240 empiezan en el mismo punto y
   comparar dos sedes de un vistazo deja de ser posible. Y con cifras
   tabulares porque la letra del ERP es proporcional —el 1 mide menos que el
   8—, así que sin esto las columnas de la pantalla de existencias quedan
   dentadas aunque todas terminen alineadas.

   El `th` lleva la misma clase: un encabezado a la izquierda sobre una columna
   a la derecha se lee como si rotulara la columna de al lado. Y va NOMBRADO en
   el selector porque `.table thead th` fija `text-align: left` y le gana por
   especificidad —(0,1,1) contra (0,1,0)—: es la misma cascada que ya dejó sin
   efecto a `.field-check` y al botón destructivo dentro de una tabla, con la
   regla escrita y sin hacer nada. */
.num,
.table thead th.num {
    text-align: right;
    font-variant-numeric: tabular-nums;
}

/* El encabezado que ordena.

   Hereda el color apagado del `th` y solo se enciende al activarse: dieciocho
   encabezados clicables pintados con énfasis compiten con los datos, que es lo
   que la tabla existe para mostrar.

   El `inherit` de `color` y `font` no es adorno: sin él el `<a>` toma el azul
   de enlace del navegador y la fila de encabezados se lee como una barra de
   navegación en vez de como los rótulos de las columnas. */
.table thead th .th-sort {
    display: inline-flex; align-items: center; gap: var(--sp-1);
    color: inherit; font: inherit; letter-spacing: inherit;
    text-decoration: none; cursor: pointer;
}
.table thead th .th-sort:hover { color: var(--text); }
.table thead th .th-sorted-asc,
.table thead th .th-sorted-desc { color: var(--text); }

/* La flecha. `chevron` apunta abajo, o sea descendente —lo más reciente
   primero, que es como abre toda tabla del ERP—; ascendente lo gira. Un solo
   glifo para los dos estados evita que el set crezca por una rotación. */
.th-arrow {
    display: inline-flex;
    /* El glifo del set viene a 24px y acá es una marca al lado de un rótulo
       de 11px: sin el tope tapa la palabra que califica. */
    width: 14px; height: 14px;
}
.th-arrow svg { width: 100%; height: 100%; }
.th-sorted-asc .th-arrow { transform: rotate(180deg); }

/* El renglón del total de un desglose, en sus dos estados.

   Los DOS existen, y el segundo es el que importa: cuando a un costo le falta
   una línea porque un proveedor no contestó, pintar la suma con el mismo
   énfasis que un total completo es exactamente el «número limpio que miente»
   que §11 prohíbe. Quien lo lee lo usa para poner un precio.

   Se distingue por color y además por la PALABRA —«Suma parcial» en vez de
   «Total», que la pone la plantilla—: el color solo no le dice nada a quien
   no distingue el rojo del gris.

   Se retiró el 2026-08-17: el pie de esa tabla pasó a `ui/_total.html`, y el
   tono degradado es ahora `.total-danger` —el `danger` del vocabulario de
   siempre— en vez de una clase con el nombre de su única pantalla. Las dos
   reglas que vivían acá se fueron con ella. */

/* La fila de algo YA HECHO: el registro de DNS que la comprobación encontró
   puesto. Existía en el marcado sin una sola regla —la clase estaba, el color
   no—, así que la tabla de registros se leía toda igual y había que ir pastilla
   por pastilla para saber cuáles faltaban, que es justo lo que la pantalla
   viene a contestar de un vistazo.

   En el fondo de la celda y no en el de la `<tr>`: `.table tbody tr:hover`
   pinta la fila, y un fondo en la fila lo tapa el del hover. Es la misma forma
   que `.total-danger` de la fila de totales. */
.row-done td { background: var(--ok-bg); }

/* El VALOR de un registro de DNS. Una clave DKIM son 200 y pico de caracteres
   sin un solo espacio donde partir, así que sin esto la celda estira la tabla
   entera y las otras tres columnas se aplastan. `anywhere` y no `break-all`:
   parte sólo cuando hace falta, y un valor corto —`ltdc.agrupa.co`— se queda
   entero en su renglón.

   Es la misma forma que ya tenía la referencia de la pasarela en la ficha del
   pago; acá la clase estaba puesta y la regla no existía. */
.record-value { word-break: break-word; overflow-wrap: anywhere; }

/* El renglón de NOTA que cuelga del registro de arriba: es una continuación de
   esa fila, no una fila más. Sin borde por encima —el que la tabla le pone a
   toda fila la separaría justo de aquello a lo que pertenece— y sin el relleno
   de arriba, para que se lea pegada. */
.row-note td { border-top: 0; padding-top: 0; }

/* El FONDO va en la clase base y no solo en los modificadores. Sin él, un
   `class="pill"` a secas —los papeles de un tercero, el estado de una
   ubicación— salía como texto en negrita al lado de una pastilla de verdad.
   Una clase llamada «pill» que no se ve como una pastilla es un defecto de la
   clase base, no de quien la usa. */
/* La pastilla, en FRASE NORMAL desde el 2026-08-14.

   Era versalitas de 10px. A ese tamaño la caja alta no es énfasis, es una
   multa de legibilidad: «SIN VARIANTES» y «CONCILIABLE» se deletrean, y en
   una fila de tabla la pastilla compite con el nombre del producto que sí
   hay que leer. Lo que hace que un estado se lea como estado es el FONDO de
   color y la forma redondeada, no la caja de la letra.

   Alcanza a TODO el ERP —fichas, banners, el POS, el KDS—, no solo a las
   tablas, y eso se decidió a sabiendas (Renzo, 2026-08-14): dos formas de
   la misma pastilla según la pantalla es peor que una sola que cambia.

   El texto lo escriben las plantillas ya en frase normal («Activo»,
   «Borrador»), así que quitar el `text-transform` no deja ninguna en
   minúsculas: era la CSS la que las gritaba. */
.pill {
    display: inline-block; padding: 2px var(--sp-3); border-radius: 999px;
    font-size: var(--fs-small); font-weight: 500;
    background: var(--pill-bg); color: var(--pill-text);
    white-space: nowrap;
}
/* Los cuatro estados, con la familia VIVA. La pastilla es lo único que la
   usa: es la pieza del tamaño de una palabra, donde el tono pálido no
   alcanza a leerse como estado. Los avisos y las filas teñidas siguen con la
   familia pálida — ver el comentario de los tokens. */
.pill-ok     { background: var(--ok-vivid);     color: var(--ok-vivid-text); }
.pill-danger { background: var(--danger-vivid); color: var(--danger-vivid-text); }
.pill-warn   { background: var(--warn-vivid);   color: var(--warn-vivid-text); }
.pill-muted  { background: var(--bg);        color: var(--text-light); }
/* Lo que está PASANDO, que no es ni un logro ni una alarma: el pedido que
   alguien está empacando. En verde se leería como despachado y en ámbar como
   un problema. */
.pill-info   { background: var(--info-vivid); color: var(--info-vivid-text); }

/* La pastilla CON glifo, que pinta `ui/_pill.html`. Es un modificador y no la
   clase base porque `.pill` es `inline-block` en más de cien sitios y pasarla
   entera a `inline-flex` le cambiaría la línea base a todos por un caso.

   El relleno de la izquierda baja: el glifo ya trae su propio aire y con el
   relleno completo la pastilla queda descentrada hacia la derecha. */
.pill-icon {
    display: inline-flex; align-items: center; gap: 4px;
    padding-left: 6px; vertical-align: middle;
}
/* 11px y no `--fs-badge`: el glifo tiene que leerse a la altura de una
   versalita de 10px, y a 10px un trazo de 2.4 se empasta. */
.pill-icon svg { width: 11px; height: 11px; flex: none; }

/* La celda que ABRE una fila: una baldosa y el nombre, en la misma celda.
   `inline-flex` y no `flex` para que la celda pueda seguir alineando su
   contenido como cualquier otra —una columna de plata a la derecha, por
   ejemplo— en vez de que el envoltorio se coma el ancho entero.

   Va como envoltorio DENTRO de la celda y no como una columna aparte: una
   columna propia para el icono se lleva un ancho de tabla sin decir nada, y
   al reordenar columnas el icono se separa de lo que acompaña. */
/* `gap` de `--sp-3` y no de `--sp-2`, o sea el MISMO que `.cell-media`.

   Las dos abren una fila con algo cuadrado y su nombre al lado, y estaban a
   8px una y a 12 la otra. Con los dos tamaños ya iguales, el hueco distinto
   era lo último que hacía que dos listas del mismo ERP se leyeran con otro
   ritmo. Y no es solo estética: `.th-lead` corre el rótulo de esa columna
   `--cell-thumb + --sp-3`, así que con 8px el encabezado de estas treinta y
   cinco caería 4px a la derecha del nombre que rotula. */
.cell-lead { display: inline-flex; align-items: center; gap: var(--sp-3); min-width: 0; }

/* La flecha que abre las variantes de un producto en la lista de etiquetas.

   Un BOTÓN y no un enlace: no lleva a ninguna parte, despliega lo que ya
   está en la página. Va del tamaño de una celda para que la columna no baile
   entre una fila con flecha y una sin ella — 226 de los 476 productos de LTDC
   tienen una sola variante y no la llevan, así que la mitad de las filas
   alternarían de sangría si esto no ocupara su sitio siempre. */
.row-toggle {
    flex: 0 0 auto; width: 18px; padding: 0; border: 0; background: none;
    color: var(--text-light); font-size: var(--fs-small); line-height: 1;
    cursor: pointer; text-align: left;
}
.row-toggle:hover { color: var(--text); }
/* El chevron apunta a la derecha cerrado y abajo abierto: es el mismo trazo
   de `{% icon 'chevron' %}` que ya usan los encabezados ordenables, girado. */
.row-toggle svg { width: 14px; height: 14px; transform: rotate(-90deg); transition: transform .12s ease; }
.row-toggle-on svg { transform: rotate(0deg); }

/* LA LÍNEA DEL ÁRBOL. La dibuja el CSS y no un carácter en el marcado: un
   «└» escrito en la celda se lo lee un lector de pantalla como contenido, y
   lo que quiere decir —«esto cuelga de lo de arriba»— ya lo dice la sangría
   para quien ve y el orden del DOM para quien escucha.

   El codo cuelga desde arriba de la fila hasta su centro y gira: sin el
   giro, catorce variantes seguidas se leen como una lista suelta y no como
   las hijas de la familia que quedó arriba. */
/* LA SANGRÍA DE UNA FILA CON ÁRBOL es mayor que la de una fila sangrada a
   secas, y no es capricho: el codo arranca en `--sp-5` (20) y mide `--sp-3`
   (12), o sea que termina en 32. Con los 24 de `cell-indent` el texto empezaba
   ANTES de que la línea acabara y la línea lo cruzaba —«Grano / 340g» salía
   tachado (Renzo, 2026-09-09)—. Los 40 dejan ocho de aire después del codo.

   Va sobre `.cell-tree` y no cambiando `.cell-indent`: esa la comparte el
   menú de la tienda, que sangra sin dibujar ningún codo y a la que los 24 le
   sientan bien.

   Y la celda del árbol NO lleva además `cell-indent`, aunque la sangría sea
   lo que las dos hacen: con las dos clases puestas ganaba `.cell-indent` por
   ir más abajo en la hoja —misma especificidad (0,2,1)— y la sangría se
   quedaba en 24. Es la cuarta vez en dos días que la cascada de este archivo
   muerde por el orden y no por el peso. */
/* `--tree-level` es EN CUÁL ESCALÓN va esta fila: 1 la hija, 2 la nieta. Lo
   escribe quien pinta la tabla, en línea sobre la celda, porque es un dato de
   la fila y no de la pantalla —igual que `--callout-tint` en el bloque de
   aviso—. Sin él vale 1, así que la lista de etiquetas, que solo tiene un
   nivel, sigue pintando exactamente igual que antes de que esto existiera.

   Cada escalón corre el codo y el texto `--sp-5` (20) más a la derecha: es lo
   que ya separa al primer escalón del margen, así que el ojo lee el mismo
   paso dos veces en vez de dos pasos distintos. Lo estrenó el árbol de
   Ubicaciones (Sede Central con sus bodegas adentro), donde una nieta y su
   madre sangradas igual se leen como hermanas. */
/* DOS SELECTORES para la misma declaración, y es la cascada otra vez.

   `.table tbody td:first-child { padding-left: var(--sp-5) }` —los 20px que
   separan la primera columna del filo— pesa (0,2,2) contra los (0,2,1) de
   `.table td.cell-tree`: gana el filo y la sangría no existe. En la lista de
   etiquetas nunca se notó porque ahí la celda del árbol NO es la primera —va
   detrás de la casilla de marcar—, así que la pieza llevaba el defecto
   dormido desde el 2026-09-09. Ubicaciones, que no tiene casillas, lo
   despertó: las cuatro bodegas salían a ras de su sede.

   La variante `:first-child` sube a (0,3,2) y gana siempre, esté la celda
   donde esté. No se arregla al revés —excluyendo `.cell-tree` del selector
   del filo— porque esa regla también la usan el `thead` y el pie, y tocarla
   mueve tablas que no tienen nada que ver con esto. */
.table td.cell-tree,
.table tbody td.cell-tree:first-child {
    position: relative;
    padding-left: calc(var(--sp-8) + (var(--tree-level, 1) - 1) * var(--sp-5));
}
.table td.cell-tree::before {
    content: ""; position: absolute; top: 0; bottom: 50%;
    left: calc(var(--sp-5) + (var(--tree-level, 1) - 1) * var(--sp-5));
    width: 1px; background: var(--border);
}
.table td.cell-tree::after {
    content: ""; position: absolute; top: 50%;
    left: calc(var(--sp-5) + (var(--tree-level, 1) - 1) * var(--sp-5));
    width: var(--sp-3); height: 1px; background: var(--border);
}
/* La última hija corta el tramo vertical en el codo: si sigue de largo,
   parece que abajo viene otra hermana que no está. */
.table tr.row-child:last-of-type td.cell-tree::before { bottom: 50%; }

/* ── La cabecera de una lista con buscador y pestañas ──────────────────────
   Estrenada por la lista de etiquetas. El buscador va DENTRO de la tarjeta y
   no en una tarjeta de filtros aparte: en una lista de 476 familias buscar
   es lo primero que se hace, y una tarjeta encima empuja la tabla fuera de
   la primera pantalla. */
.list-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-4); flex-wrap: wrap; }
/* `.card-header .list-title` y no `.list-title` a secas: dentro del
   encabezado manda `.card-header h2`, que con (0,1,1) le gana a una clase
   suelta con (0,1,0). Es la MISMA trampa que este archivo ya documenta unas
   líneas más arriba para `.card-count`, y volví a caer en ella. */
.card-header .list-title {
    margin: 0; font-size: var(--fs-section); font-weight: 600;
    letter-spacing: 0; text-transform: none;
}
.list-head-tools { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }
.list-search { position: relative; display: flex; align-items: center; }
/* `input[type="search"]` en el selector y no `input` a secas, y el PADDING
   COMPLETO en una sola declaración.
   
   La regla base de los campos —`textarea, input[type="date"],
   input[type="search"]…`— fija `padding: 11px 13px` con el atajo, tiene la
   misma especificidad (0,1,1) que `.list-search input` y va más abajo: le
   pisaba los dos lados. El síntoma era la lupa encima de la «B» de «Buscar»
   y el ⌘K recortando el texto. Con el tipo en el selector son (0,2,1) y
   gana; con el atajo completo, ningún lado se queda sin poner.
   
   Los 34 y los 44 son el hueco de la lupa y el de la × que limpia: si
   cambian de tamaño, cambian acá. Hasta el 2026-09-15 el de la derecha era
   el de la pista `⌘K`, que se fue con el atajo a la paleta. */
.list-search input[type="search"] { min-width: 280px; padding: 11px 44px 11px 34px; }

/* Las pestañas con conteo. Un enlace por pestaña y no un botón: cada una es
   una URL, así que se puede compartir, abrir en otra lengüeta y volver
   atrás. */
/* La línea de avisos de una lista: los datos sucios dichos de un vistazo,
   con el porqué en la ayuda. Va DENTRO de la tarjeta y en una sola línea
   porque un aviso no puede tapar el trabajo. */
.list-note { margin: 0; color: var(--text-light); font-size: var(--fs-small); }
.list-note-item { display: inline-flex; align-items: center; gap: 4px; }
.list-note-item + .list-note-item::before { content: "·"; margin: 0 var(--sp-2) 0 0; }

.list-tabs { display: flex; gap: var(--sp-2); flex-wrap: wrap; }
.list-tab {
    display: inline-flex; align-items: center; gap: var(--sp-2);
    padding: 5px 10px; border-radius: 999px; text-decoration: none;
    color: var(--text-light); font-size: var(--fs-small);
}
.list-tab:hover { background: var(--bg); color: var(--text); }
.list-tab-on { background: var(--primary-soft); color: var(--primary-dark); font-weight: 600; }
.list-tab-count { color: inherit; opacity: .7; font-variant-numeric: tabular-nums; }

/* La tarjeta de filtros, separada de la de la tabla: arriba se decide QUÉ
   mirar y abajo se trabaja sobre lo elegido. Con todo en una, la barra de
   selección quedaba pegada a las pestañas y las dos se leían como el mismo
   control. */
.list-filters { padding: var(--sp-5); margin-bottom: var(--sp-4); }
.list-filters .list-head { padding: 0; }

/* La fila de pestañas, con los avisos al otro extremo. `wrap` porque en el
   celular no caben las cinco y una fila que se sale es peor que dos filas. */
.list-tabs-row {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--sp-4); flex-wrap: wrap; margin-top: var(--sp-4);
}
/* Pastillas con borde: sin él, la pestaña elegida y las demás se distinguen
   solo por el fondo, y con el fondo claro del ERP eso es un matiz. */
.list-tab { border: 1px solid var(--border); background: var(--card-bg); }
.list-tab-on { border-color: var(--primary); }

/* El buscador con su lupa y su ×. Las dos van DENTRO del campo: un icono
   fuera obliga a mirar dos sitios para entender un control. */
.list-search-icon {
    position: absolute; left: var(--sp-3); display: flex;
    pointer-events: none; color: var(--text-light);
}
.list-search-icon svg { width: 16px; height: 16px; }
.list-search-clear {
    position: absolute; right: var(--sp-2); display: flex; padding: 2px;
    border-radius: 4px; color: var(--text-light);
}
.list-search-clear:hover { background: var(--bg); color: var(--text); }
.list-search-clear svg { width: 14px; height: 14px; }

/* El pie: cuántas se ven, las páginas y cuántas por página. */
.list-foot {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--sp-4); flex-wrap: wrap;
    padding: var(--sp-3) var(--sp-5); border-top: 1px solid var(--border);
}
.list-foot-count { margin: 0; color: var(--text-light); font-size: var(--fs-small); }
.list-foot .pager { margin-top: 0; }
.list-foot-size select { font-size: var(--fs-small); }

/* La celda del kebab: angosta y a la derecha, para que no le robe ancho a
   los datos. */
.table td.cell-actions, .table th.cell-actions { width: 1%; text-align: right; }

/* Un pie DENTRO de una tarjeta de tabla.

   `.card-foot` trae `padding-top` y nada más, porque nació dentro de una
   `.card` con relleno propio: ahí los lados y el fondo se los pone la
   tarjeta. Una `.card-table` no tiene relleno abajo —la tabla llega a los
   bordes a propósito— así que el pie quedaba pegado al borde de abajo. Se
   veía apeñuscado, que es exactamente la palabra que usó Renzo.

   Y a los LADOS llega al filo, como la banda y la tabla: con solo el relleno
   quedaba doble sangría —la raya empezaba 20px adentro del borde y el texto
   40px, corrido contra la baldosa del encabezado—. Medido en la cola de
   Envíos el 2026-09-15 («está muy chueca»): tarjeta 275–999, pie 296–978.

   Va acotado a la combinación y no arreglando `.card-foot`: los otros cinco
   pies del ERP viven en tarjetas con relleno y ahí la regla actual es
   correcta. */
.card-table > .card-foot {
    margin: 0 calc(var(--sp-5) * -1); padding: var(--sp-4) var(--sp-5);
}

/* El segundo renglón de una celda: la razón social debajo del nombre. Sin
   `display: block` salía pegada en la misma línea —«Café del Sur SAS
   Comercializadora Café del Sur SAS»— y parecía un error de datos. */
.cell-sub {
    display: block; margin-top: 2px;
    font-size: var(--fs-meta); color: var(--text-light);
}

/* LOS PAGOS DE LA FICHA, que NO son una tabla y por eso viven acá.

   `.table thead th` no lleva relleno horizontal —en una tabla ancha las
   columnas se separan con el espacio sobrante—, así que dentro de la tarjeta
   angosta de la lateral los encabezados se tocaban: «MÉTODOCOMPROBANTEQUIÉN».
   Y `.table-wrap` no salvaba nada: su scroll horizontal esconde la última
   columna, que es la del monto.

   La fila resuelve las dos: cuándo y cuánto en el renglón que se lee, el resto
   apagado debajo, y cabe en cualquier ancho sin desbordar. */
.pay-rows { list-style: none; margin: 0; padding: 0; }
.pay-row { padding: var(--sp-3) 0; border-top: 1px solid var(--border); }
.pay-row:first-child { border-top: none; padding-top: 0; }
.pay-row-head { display: flex; align-items: baseline; gap: var(--sp-3); }
.pay-row-when { font-size: var(--fs-small); color: var(--text-light); }
/* El monto a la derecha y con cifras de ancho fijo: es la columna que el ojo
   recorre de arriba abajo para sumar, y con cifras proporcionales los miles no
   se alinean. `margin-left: auto` en vez de `justify-content: space-between`
   para que la fecha no se estire cuando el monto es corto. */
.pay-row-amount { margin-left: auto; font-variant-numeric: tabular-nums; }
.pay-row-sub {
    margin: 2px 0 0; font-size: var(--fs-meta); color: var(--text-light);
    /* Envuelve, y tiene que hacerlo: el método, quién y la referencia sumados
       pasan del ancho de la tarjeta. Lo único que NO se parte es la
       referencia, que va en su propio span. */
    overflow-wrap: anywhere;
}
/* La referencia de la pasarela: 40 caracteres sin un espacio donde partir. El
   corte lo hace el servidor con `truncatechars` —el valor entero viaja en el
   `title`, que es donde alguien la va a buscar para pegarla en el panel de
   Wompi—; acá solo se le prohíbe partirse a mitad. */
.pay-row-ref { white-space: nowrap; }
/* «Pagada … 06 Ago 2026 · 14:18», el renglón de tiendapp: el rótulo apagado a
   la izquierda y el cuándo a la derecha. Va como par y no como `.facts`
   porque es UNO solo — un `dl` de una fila trae la rejilla de dos columnas
   para nada. */
.pay-settled {
    display: flex; justify-content: space-between; gap: var(--sp-3);
    margin: var(--sp-4) 0 var(--sp-3);
    font-size: var(--fs-small); color: var(--text-light);
}

/* Una celda que dice que no hay nada —«Sin productos»— frente a otras que sí
   dicen algo. Va apagada porque es la AUSENCIA del dato: en su columna las
   demás celdas son enlaces, y con el mismo color el ojo se detiene en la única
   que no lleva a ninguna parte. */
.cell-muted { color: var(--text-light); }

.pager {
    display: flex; align-items: center; justify-content: center; gap: 12px;
    margin-top: 16px;
}
.pager-status {
    color: var(--text-light); font-size: var(--fs-body);
    font-variant-numeric: tabular-nums;
}
.pager .btn { padding: 9px 16px; }

/* ── Tarjeta y feed ────────────────────────────────────────────────────── */

.card {
    background: var(--card-bg); border: 1px solid var(--border);
    border-radius: var(--radius-card); box-shadow: var(--shadow);
    padding: var(--sp-4) var(--sp-5);
}
/* Una PILA de tarjetas: las separa el contenedor, y por eso hay un solo sitio
   donde está escrito cuánto.

   El primer intento fue `.card + .card { margin-top }`, por hermano adyacente,
   y se pisó con lo que ya existía: `.product-main` y `.product-rail` ya
   separaban con `gap`, así que las tarjetas de la ficha del producto pasaron a
   32px —16 de gap más 16 de margen— sin que nadie lo pidiera. Un margen en el
   hijo y un `gap` en el padre resuelven lo mismo y se suman, que es la forma de
   equivocarse propia de esta pareja.

   `.product-main` y `.product-rail` son esta misma idea con dos nombres más
   viejos; se dejan porque además fijan columna y `min-width`. */
.card-stack { display: flex; flex-direction: column; gap: var(--sp-4); min-width: 0; }

/* Y la separación POR OMISIÓN, para la pantalla que no envuelve nada.
   `.card-stack` es opt-in: hay que acordarse de ponerlo, y con 27 usos contra
   154 tarjetas la mayoría de las pantallas no lo tenía. Sus tarjetas no salían
   «un poco juntas» — salían PEGADAS, con el borde de una contra el de la
   siguiente, y lo que parecía un hueco era la sombra. Dominios, medido el
   2026-08-11: `bottom: 464` y `top: 464`.

   Va sobre los HIJOS DIRECTOS de `.content`, y ahí está el porqué de que no se
   sume con lo de arriba: una tarjeta dentro de una pila es `.card-stack > .card`
   y no `.content > .card`, así que esta regla no la alcanza. Es la trampa que
   se documentó al escribir `.card-stack` —un margen en el hijo y un `gap` en el
   padre resuelven lo mismo y se suman— evitada por selector en vez de por
   acordarse.

   El hermano adyacente y no `margin-bottom`: la última tarjeta no cuelga un
   margen del que después alguien tenga que defenderse.

   Alcanza también a `.card-stack` y a `.table-wrap` sueltos porque el problema
   no es «la tarjeta»: es que dos bloques de contenido seguidos se toquen.

   Lo que sigue a una tarjeta es CUALQUIER COSA y no otra tarjeta, y esa
   generalización es la que costó una regresión. El modelo de lista del ERP no
   pone la tabla como hija de `.content`: la pone dentro de `#results`, que es
   el destino del intercambio de htmx. Con la regla escrita como
   `.card + .card` la barra de filtros y su tabla quedaban PEGADAS en las
   cincuenta y una listas —medido en Productos: `bottom: 390`, `top: 390`—
   porque en medio hay un `<div id="results">` que no es una tarjeta.

   Antes las separaba el `margin-bottom` de `.filters-card`, el mismo que se
   quitó arriba por sumarse dentro de una pila. Quitarlo sin generalizar esto
   fue cambiar un defecto por otro. */
.content > .card + *,
.content > .card-stack + *,
.content > .table-wrap + *,
.content > #results + * { margin-top: var(--sp-4); }
/* Y DENTRO del destino del intercambio: Solicitudes pinta dos tarjetas
   —«Para resolver» y «Las mías»— dentro de `#results`, y ahí ninguna de las
   reglas de arriba alcanza (no son hijas de `.content`). Salían pegadas
   (Renzo, 2026-09-14). Solo tarjeta con tarjeta: una tabla con su paginador
   se separan solos. */
#results > .card + .card { margin-top: var(--sp-4); }

/* Al aterrizar en un ancla (`/ajustes/perfil/empresa/#seccion-marca`), la
   barra de arriba es fija y tapaba el encabezado de la tarjeta: se llegaba
   «más abajo» y no se sabía dónde (Renzo, 2026-09-14). El margen de
   desplazamiento deja la tarjeta entera a la vista, con su aire. */
.content [id] { scroll-margin-top: calc(var(--topbar) + var(--sp-4)); }

/* ── El banner de una ficha: avatar, pastillas y cifras ────────────────────
   Lo estrena el tercero y lo va a querer igual el empleado de la F2, así que
   nada de esto se llama `party-*`. */

/* La inicial sobre el tinte de marca, como la primera celda de una tabla. Un
   cuadro gris vacío se lee como algo que falta. */
.banner-avatar {
    flex: none; display: grid; place-items: center;
    width: 46px; height: 46px; border-radius: 50%;
    background: var(--primary); color: #fff;
    font-size: var(--fs-section); font-weight: 600; text-transform: uppercase;
}
/* En LÍNEA con el nombre, dentro del `h1`. `inline-flex` con `vertical-align`
   y no `flex`: como bloque rompería el renglón del título y volvería a quedar
   debajo, que es de donde se la trajo. */
.banner-badges {
    display: inline-flex; flex-wrap: wrap; gap: 6px;
    margin-left: var(--sp-3); vertical-align: middle;
}
/* Pastilla del banner: fondo propio y no transparente, porque sobre la banda
   oscura una pastilla sin relleno es un borde flotando.

   La FORMA la llevan también las variantes de color: media docena de
   pantallas escriben `badge-soft-muted` o `badge-soft-info` a secas, y sin
   esto salían como texto resaltado sin redondear (Renzo, 2026-09-12, en
   la ficha de una serie de tareas). */
.badge-soft, .badge-soft-danger, .badge-soft-warn, .badge-soft-info,
.badge-soft-muted {
    display: inline-block; padding: 4px 12px; border-radius: 999px;
    font-size: var(--fs-small); font-weight: 600; line-height: 1.4;
}
.badge-soft { background: var(--ok-bg); color: var(--ok-text); }
.badge-soft-danger { background: var(--danger-bg); color: var(--danger-text); }
/* Las dos que la banda del pedido necesita para contar en qué va: el ámbar de
   lo que quedó a medias —pago parcial, envío parcial— y el azul de lo que
   está pasando. `preparing` NO puede ir en verde: el verde de `.badge-soft`
   se lee como «listo», y un pedido que se está empacando todavía no salió. */
.badge-soft-warn { background: var(--warn-bg); color: var(--warn-text); }
.badge-soft-info { background: var(--info-bg); color: var(--info-text); }
/* El cargo NO es un estado. Con el verde de `.badge-soft` las dos pastillas del
   encabezado se leían como dos estados y el ojo no sabía cuál mirar; el color
   se lo queda el que dice si la persona sigue trabajando acá. */
.badge-soft-muted { background: var(--pill-bg); color: var(--pill-text); }

/* Las cifras. `dd` ARRIBA y `dt` abajo en el marcado, que es el orden en que se
   leen —el número primero y el rótulo debajo—; el orden semántico del `dl` se
   respeta porque cada par va en su propio `div`. */
.banner-stats {
    display: flex; flex-wrap: wrap; gap: var(--sp-3) var(--sp-6);
    margin: 0; flex: none;
}
.banner-stats > div { display: flex; flex-direction: column; gap: 2px; }
.banner-stats dd {
    margin: 0; color: #fff;
    font-size: var(--fs-page); font-weight: 600;
    font-variant-numeric: tabular-nums;
}
.banner-stats dt {
    color: var(--banner-sub);
    font-size: var(--fs-badge); font-weight: 600;
    text-transform: uppercase; letter-spacing: .04em;
}

/* Una casilla y su botón en una fila. La casilla se estira; el botón no, o a
   390px se parte en dos renglones con el texto cortado.

   Se llamaba `.comment-row` cuando su único uso era el comentario del tercero.
   El segundo consumidor —los tipos de producto, que se crean y se renombran
   inline— no es un comentario, y una clase con el nombre de su primer caso es
   la que después nadie reusa: se copia la regla con otro nombre y quedan dos
   formas distintas de la misma fila. */
.inline-row {
    display: flex; gap: var(--sp-3); align-items: center;
    /* Con `nowrap` la casilla paga TODO lo que no cabe: medido a 434px, con el
       rótulo «Renombrar «Prueba F1e»» y los dos botones de esa fila, quedaba de
       26 píxeles — un campo de texto en el que no se lee lo que se escribe. */
    flex-wrap: wrap;
}
/* La casilla se estira, y `220px` es su base: es lo que la baja a su propio
   renglón antes de dejarse aplastar. `min-width: 0` para que ese renglón pueda
   ser más angosto que la base cuando la pantalla lo es. */
.inline-row input { flex: 1 1 220px; min-width: 0; }
.inline-row .btn { flex: none; }
/* Un campo CON RÓTULO dentro de la fila: `.field` trae el rótulo arriba y 17px
   de margen abajo, y con `align-items: center` el botón quedaba a media altura
   entre el rótulo y la casilla («Asignar» junto a «Repartidor» en la ficha de
   la ruta, 2026-09-14). El campo pierde el margen y el botón baja al borde,
   que es donde está la casilla. Y dos campos en la fila se alinean por
   ARRIBA: con una pista debajo de uno solo («Clave» en Repartidores),
   centrarlos dejaba el rótulo del otro flotando a media altura. */
.inline-row > .field { margin-bottom: 0; align-self: flex-start; }
/* Y mide lo que mide el control de al lado: el botón de la casa trae 11px de
   relleno y `line-height: 1` (39px) y la casilla 36px; alineados por abajo,
   los 3px de más asomaban por arriba (Renzo, 2026-09-14, «Asignar» junto a
   «Repartidor»). Una sola altura para los dos, escrita una vez. */
.inline-row > .field select,
.inline-row > .field input:not([type="checkbox"]):not([type="radio"]) { height: 36px; }
.inline-row > .field ~ .btn {
    align-self: flex-end; height: 36px; padding-top: 0; padding-bottom: 0;
}
/* La misma fila DENTRO de una celda de acciones (la clave nueva del
   repartidor con su Guardar): ahí no se parte, porque a dos renglones la
   fila mide más que la tabla y se lee como dos controles sueltos. La
   casilla se acorta a lo que cabe: es un PIN, no una frase. */
.row-actions .inline-row { flex-wrap: nowrap; }
.row-actions .inline-row input { flex: 0 1 150px; min-width: 110px; }
/* La misma fila cuando vive entre el encabezado de una tarjeta y su tabla.
   Relleno POR LOS CUATRO LADOS y no solo abajo: la fila es un bloque de
   trabajo —se escribe y se aprieta Agregar— y pegada al filo del relleno de la
   tarjeta se leía como parte del encabezado de arriba en vez de como algo
   aparte. Los 12px de más a los lados son a propósito: es lo que la separa de
   la tabla que viene debajo, que sí llega al borde.

   Alcanza a las seis filas de este tipo —las dos de líneas de costo, las dos de
   canales y las dos de colecciones—, que es justamente lo que se quiere: una
   sola forma para la misma fila. */
.inline-row-card { padding: var(--sp-3) 0; }
/* La fila con la que se AGREGA una condición: al pie de la tarjeta y en su
   propio panel. Puesta al final sin fondo se lee como una condición más —una
   a medio llenar—, y el panel es lo que dice «esto todavía no es una».

   El margen de abajo lo pone ella y no la tarjeta: `.card-table` lleva
   `padding-bottom: 0` para que una tabla llegue al filo, y un panel pegado al
   borde inferior se ve cortado. */
.rule-builder {
    margin-bottom: var(--sp-4); padding: var(--sp-3) var(--sp-4);
    border: 1px solid var(--border); border-radius: var(--radius-field);
    background: var(--bg);
}
/* EL ALTA PLEGADA: «＋ Agregar regla de IVA» al pie de la tabla, y el
   formulario recién al abrirla. La Configuración contable tiene once altas, y
   abiertas todas a la vez la pantalla era once formularios vacíos con una
   tabla encima de cada uno: lo que se venía a LEER —qué reglas hay— pesaba
   menos que lo que casi nunca se usa (Renzo, 2026-09-20: «todavía lo veo muy
   raro»). Es el plegable de la casa (`details.fold` + `ui/_fold_head.html`),
   no uno nuevo; acá solo va lo que cambia por vivir dentro de una tarjeta.

   Una tarjeta con DOS altas —ICA y autorretención, una tasa a mano y un rango
   oficial— lleva dos plegables, y el rótulo de cada uno dice cuál es.

   El margen de abajo lo pone el último y no la tarjeta, por lo mismo que en
   `.rule-builder`: `.card-table` lleva `padding-bottom: 0`. */
.card-table > .rule-add { margin-top: var(--sp-3); }
.card-table > .rule-add:last-child { margin-bottom: var(--sp-4); }
.rule-add > .fold-body { padding: var(--sp-4); background: var(--bg); }
/* Y el formulario es UNA FILA: cada casilla con su rótulo y el botón al
   final, a la altura de las casillas (eso ya lo hace `.inline-row > .field`).
   Tres anchos y no una rejilla pareja: en tres columnas iguales el «%» medía
   lo mismo que la cuenta, sobraba media fila y el botón caía solo en un
   tercer renglón. `field-short` para lo que son cuatro dígitos, `field-grow`
   para la cuenta, que es la que trae nombre. Cuando no cabe, envuelve. Y con
   TOPE: un alta de dos casillas en una tarjeta de 1.200px estiraba la cuenta
   a lo ancho de la pantalla y el botón quedaba a un metro de lo que envía. */
.rule-row { align-items: flex-start; }
.rule-row > .field { flex: 1 1 170px; min-width: 0; max-width: 280px; }
.rule-row > .field-short { flex: 0 1 112px; }
.rule-row > .field-grow { flex: 2 1 260px; max-width: 460px; }
@media (max-width: 720px) {
    .rule-row > .field, .rule-row > .field-grow { max-width: none; }
}
/* Un rótulo en dos renglones baja SU casilla y la fila deja de ser una fila. */
.rule-row > .field > label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Un monto tampoco necesita el ancho de un concepto: son seis dígitos. Sin el
   tope, la casilla de la plata mide lo mismo que la del texto y la fila se lee
   como dos campos iguales cuando no lo son.

   Va con `.inline-row` DELANTE por la cascada, y es la tercera vez que esta
   forma aparece en el proyecto: `.inline-row input` tiene una clase y un
   elemento, así que le ganaba a `.input-amount`, que tiene una clase sola. La
   regla estaba escrita y no hacía nada — medido en el navegador: 255px, el
   mismo ancho que el concepto. Igual que `.field-check` contra `.field label`
   y que el botón destructivo dentro de una tabla. */
.inline-row .input-amount { flex: 0 1 150px; }

/* Los TRES controles de una condición, tratados como uno.

   Campo, operador y valor son una sola idea —«tipo de producto es Café»— y
   el botón «Agregar» es otra. Sin el envoltorio, la fila reparte su espacio
   entre cuatro cosas y al angostarse el botón se va solo a su renglón
   dejando los selects arriba; con él, la condición se parte por dentro y el
   botón la sigue.

   La clase existía en el marcado desde `de8d0a0` SIN una sola regla, o sea
   que el `<div>` era de bloque y los tres selects se apilaban uno debajo de
   otro. La guarda de huérfanas lo cazó: es exactamente para esto.

   `min-width: 0` en los selects porque «Todavía no has creado ninguno» mide
   más que la columna que le toca, y sin eso ese `<option>` le fija el ancho
   a la fila entera. */
.inline-row-controls {
    display: flex; gap: var(--sp-3); align-items: center; flex-wrap: wrap;
    flex: 1 1 auto; min-width: 0;
}
.inline-row-controls select { min-width: 0; }

/* ── El editor de valores de una opción ────────────────────────────────────
   Filas que se arrastran para reordenar. El orden es información —es el que
   van a tener las opciones en la variante— y con un `textarea` cambiarlo era
   cortar y pegar texto. */
.value-editor { margin-bottom: var(--sp-4); }
.value-rows { list-style: none; margin: 0 0 var(--sp-3); padding: 0; }
.value-row {
    display: flex; align-items: center; gap: var(--sp-2);
    padding: var(--sp-2) 0;
}
/* La casilla del valor es la que crece; los botones no se encogen ni se
   estiran, o a 390px «Quitar» se parte en dos renglones. */
.value-row input { flex: 1 1 auto; min-width: 0; }
.value-row .btn { flex: none; }
/* El asa dice «esto se arrastra». `grab` y no `move`: el cursor de mover
   promete que ya lo estás moviendo. */
.value-grip {
    flex: none; cursor: grab; user-select: none;
    padding: 0 var(--sp-1); color: var(--text-faint);
    font-size: var(--fs-body); line-height: 1;
}
/* El botón deshabilitado —la flecha de la primera o la última fila— se apaga
   en vez de desaparecer: una fila con tres botones y la de al lado con dos
   baila cada vez que se reordena. */
.value-row .btn:disabled { opacity: .35; cursor: default; }

/* ── El campo de fichas ────────────────────────────────────────────────────
   Lo elegido es lo único que se ve. Con 22 tipos en casillas, saber cuáles
   están marcados obliga a barrer los veintidós.

   Se llaman `token` y no `chip` porque `.chip` YA existe: es la pastilla de
   solo lectura que pinta los valores de un eje en la tabla de variantes.
   Reusar el nombre habría dejado dos reglas para la misma clase, con la de
   más abajo ganando en las dos pantallas — y hay una prueba que lo caza. */
/* `relative` porque la lista cuelga del campo: dentro del flujo empujaría
   todo lo de abajo cada vez que alguien pone el cursor en la caja, y un
   formulario que salta mientras se usa se lee como una pantalla rota. */
.token-field { position: relative; margin-bottom: var(--sp-4); }
/* La caja ENTERA es el control desde el 2026-09-09 —las fichas y el buscador
   viven adentro—, así que se pinta como se pinta un campo: mismo borde,
   mismo radio, y el foco se dibuja acá y no en el `input` de adentro, que no
   tiene borde propio. `cursor: text` porque eso es lo que promete. */
.tokens {
    display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2);
    min-height: 44px; padding: var(--sp-2) var(--sp-3);
    border: 1px solid var(--border); border-radius: var(--radius);
    background: var(--card-bg); cursor: text;
}
.tokens.is-open { border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-soft); }
/* Sin borde ni fondo: el que se ve es el de la caja. `flex: 1` con una base
   chica para que quepa junto a las fichas y crezca al renglón siguiente
   cuando ya no. */
/* Con `.token-field` delante por la cascada: la regla genérica de los
   campos es `input[type="text"]` —una clase y un atributo— y le ganaba a
   una clase sola, así que el buscador salía con su propio borde y su
   propio fondo DENTRO de la caja de fichas: dos cajas, una adentro de la
   otra. Es la quinta vez que esta forma aparece en el proyecto. */
.token-field .token-input {
    flex: 1 1 90px; min-width: 90px; width: auto; padding: 4px 0;
    border: 0; border-radius: 0; background: transparent; color: var(--text);
    font: inherit; font-size: var(--fs-small);
}
.token-field .token-input:focus { outline: none; box-shadow: none; }
/* CERRADO por omisión y abierto por una clase atada. Ver la nota del
   parcial: `x-show` no aplica su estado inicial sobre un nodo que htmx
   inyecta, y así el fallo cae del lado bueno — un menú que no se abre se
   ve; tres menús abiertos encima del formulario, también, pero tarde. */
.token-menu {
    display: none;
    position: absolute; z-index: 20; left: 0; right: 0; top: 100%;
    max-height: 260px; overflow-y: auto; margin-top: 4px; padding: var(--sp-1);
    border: 1px solid var(--border); border-radius: var(--radius);
    background: var(--card-bg); box-shadow: var(--shadow-lg);
}
/* Botones y no filas con un `click`: se tabulan, se accionan con Enter, y el
   lector de pantalla las anuncia como lo que son. */
.token-menu.is-open { display: block; }
.token-option {
    display: block; width: 100%; padding: var(--sp-2) var(--sp-3);
    border: 0; border-radius: var(--radius-sm);
    background: transparent; color: var(--text);
    font-size: var(--fs-small); text-align: left; cursor: pointer;
}
.token-option.is-active { background: var(--primary-soft); color: var(--primary-dark); }
/* «Crear «…»» del campo con alta libre (`create`): escondida hasta que lo
   escrito no existe. La prende `pintar()`, como a `.token-none`. */
.token-create:not(.is-shown) { display: none; }
.token-create { font-weight: 600; color: var(--primary-dark); }
.token-none {
    display: none; margin: 0; padding: var(--sp-2) var(--sp-3);
    font-size: var(--fs-small); color: var(--text-light);
}
.token-none.is-shown { display: block; }
.token-preset { display: none; margin-top: var(--sp-2); }
.token-preset.is-shown { display: inline-block; }
.token {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 4px 6px 4px 12px;
    font-size: var(--fs-small); font-weight: 600; color: var(--text);
    background: var(--primary-soft); border-radius: 999px;
}
/* La equis es un BOTÓN y no un aspa dibujada: se tabula, se acciona con
   Enter, y su objetivo de clic mide lo que mide un dedo. */
.token-remove {
    display: inline-flex; align-items: center; justify-content: center;
    width: 20px; height: 20px; padding: 0;
    border: 0; border-radius: 50%;
    background: transparent; color: var(--primary-dark);
    font-size: var(--fs-body); line-height: 1; cursor: pointer;
}
.token-remove:hover { background: rgba(255, 255, 255, .7); }
.token-remove:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }
.tokens-empty { font-size: var(--fs-small); color: var(--text-light); }

/* Una lista de casillas en columna: cada una es una decisión, y repartidas en
   dos columnas hay que barrer de lado a lado para saber si falta alguna.
   `align-items: start` o cada pastilla se estira al ancho de la más larga. */
.check-list {
    display: flex; flex-direction: column; align-items: flex-start;
    gap: var(--sp-2); margin-bottom: var(--sp-4);
}
/* Dentro de la lista el aire lo pone `gap`: la pastilla trae margen propio
   para cuando va suelta, y sumados los dos la lista respira el doble — las
   tres pastillas de Apps se leían como tres tarjetas separadas. */
.check-list .field-check { margin-bottom: 0; }

/* Una casilla con un prefijo pegado: lo que se teclea es un pedazo de una
   dirección, y sin ver el resto no se sabe qué se está escribiendo. El prefijo
   es TEXTO —no un control— y por eso no lleva borde propio ni recibe foco. */
.prefixed { display: flex; align-items: center; gap: 0; }
.field-prefix {
    flex: none; padding-right: var(--sp-2);
    font-size: var(--fs-small); color: var(--text-light);
    white-space: nowrap;
}
/* Con `.prefixed` delante por la cascada: la regla genérica del `input` de una
   fila tiene una clase y un elemento y le ganaría a una clase sola. Es la misma
   forma que ya costó tres defectos. */
.prefixed input { flex: 1 1 auto; min-width: 0; }

/* Los dos radios de la coincidencia, en fila y dentro de la tarjeta.

   Con relleno ARRIBA (Renzo, 2026-08-16, con la pantalla enfrente). Iba en
   cero, que estaba bien mientras las pastillas eran dos casillas de texto
   plano: una casilla no tiene borde propio y el aire se lo daba su renglón.
   Las pastillas SÍ lo tienen, y pegadas al filo del encabezado se leen como
   parte de la banda de arriba en vez de como el primer control de la tarjeta.
   Es el mismo argumento por el que `.inline-row-card` lleva relleno por
   arriba.

   Y a los lados CERO (Renzo, con la pantalla enfrente): el relleno lateral lo
   pone `.card`, y repetir `--sp-5` acá lo sumaba — las pastillas arrancaban
   21px más adentro que las condiciones de abajo y que el panel de agregar, o
   sea la única fila de la tarjeta que no alineaba con las demás. Es la misma
   trampa que `.card-lines`, que sí lo repite porque su fila va sangrada a
   propósito. */
.match-row {
    display: flex; flex-wrap: wrap; gap: var(--sp-3);
    padding: var(--sp-4) 0;
}

/* ── Las líneas de una tarjeta ─────────────────────────────────────────────
   Una fila por cosa, con su borde arriba, dentro de una tarjeta: las líneas
   de costo de un producto y las condiciones de una colección automática. En
   las de costo cada fila son DOS formularios —el que edita y el que borra—
   que tienen que quedar en el mismo renglón.

   Se llamaba `.cost-lines` mientras su único consumidor eran los costos. El
   segundo —las condiciones, 2026-08-16— no es un costo, y una clase con el
   nombre de su primer caso es la que después nadie reusa: se copia la regla
   con otro nombre y quedan dos formas de la misma fila. Es la misma lección
   que ya movió `.comment-row` a `.inline-row`. */
.card-lines { list-style: none; margin: 0; padding: 0; }
.card-lines li {
    display: flex; align-items: center; gap: var(--sp-3);
    flex-wrap: wrap;
    padding: var(--sp-2) var(--sp-5);
    border-top: 1px solid var(--border);
}
/* La fila de la línea NO envuelve: concepto, monto y «Guardar» son una sola
   cosa y partidos en tres renglones dejan de leerse como una fila. Quien baja
   de renglón cuando no cabe es «Eliminar», que es el `li` el que envuelve. Las
   casillas pueden encogerse porque las dos llevan `min-width: 0`.

   El selector era `form.inline-row` y dejó de acertar cuando la fila pasó a
   ser un `<div>` con `form="…"` —el `<form>` se mudó afuera para poder vivir
   dentro del formulario del producto—, así que las dos reglas quedaron
   escritas sin hacer nada. Es la misma forma de defecto de siempre, ahora por
   un elemento que cambió y no por especificidad. */
.card-lines .inline-row { flex-wrap: nowrap; }
/* La fila de edición se estira; «Eliminar» no. Sin esto queda pegado al monto
   y el renglón se lee como un solo control. */
.card-lines .inline-row { flex: 1 1 auto; min-width: 0; }

/* ── Una condición ya puesta ───────────────────────────────────────────────
   Leída como la frase que es —«Tipo de producto es igual a accesorios»— y no
   como cuatro columnas. Era una `<table>` con encabezado, y con dos
   condiciones eso es más encabezado que dato: la pieza más pesada del marco
   puesta al servicio de la lista más corta. La caja es `.boxed-row`; acá van
   solo las partes de la frase.

   `margin-bottom` en la lista porque el panel de agregar viene justo debajo y
   sin él las dos cosas se tocan. */
.rule-list { margin-bottom: var(--sp-4); }
/* Las tres partes se ALINEAN entre filas, que es lo que deja leer cinco
   condiciones de un vistazo en vez de cinco frases de largos distintos. Por
   ancho mínimo y no por una rejilla: la fila es la `.boxed-row` del marco,
   que es flexible y la comparte con el editor de examen — meterle una
   rejilla acá le cambiaría la forma a la otra pantalla. Un campo más largo
   que su mínimo desalinea SU fila y nada más, que es el modo correcto de
   fallar. */
.rule-field { flex: none; min-width: 150px; font-weight: 600; color: var(--text); }
/* El operador en tinta de marca y apagado de peso: es la juntura de la frase,
   no el dato, y en gris de texto competía con el campo. */
.rule-op {
    flex: none; min-width: 96px;
    color: var(--primary-dark); font-size: var(--fs-small);
}
/* El valor empuja al botón contra el filo derecho: es el margen automático y
   no un `flex-grow` en el valor, porque el valor es una PASTILLA y estirarla
   pintaría un óvalo del ancho de la tarjeta para decir «origen». */
.rule-value { margin-right: auto; }
/* El botón, contra el filo y detrás de un separador: sin la raya se lee como
   la última palabra de la frase. `flex: none` para que no lo aplaste el
   operador que se estira. */
.rule-remove {
    flex: none; padding-left: var(--sp-3);
    border-left: 1px solid var(--border);
}
/* En ANGOSTO la frase envuelve. Los mínimos de campo y operador alinean
   cinco condiciones en una tarjeta ancha, y en un celular son justo lo que no
   cabe: sumados a una pastilla que no parte («$500.000 · últimos 90 días»),
   la fila pedía 624px dentro de 305 y la página entera se corría 285px de
   lado (medido en la ficha del segmento, 2026-09-12). Por el ancho del
   CONTENIDO y no de la ventana, como la tira del escritorio: con el panel
   lateral abierto el contenido se angosta sin que la ventana cambie. Vale
   igual para las condiciones de una colección, que son esta misma fila. */
@container content (max-width: 640px) {
    .rule-list .boxed-row { flex-wrap: wrap; row-gap: var(--sp-2); }
    .rule-field, .rule-op { min-width: 0; }
    .rule-value { min-width: 0; white-space: normal; }
}

/* La nota de una tarjeta que viene pegada a una lista en caja necesita el
   mismo aire que `.field` le deja en las tarjetas de formulario: sin él se lee
   como una fila más de la lista. Salió midiendo la tarjeta de «Desde qué
   bodega sale» contra la de ajustes que tiene debajo — 0px contra 17px. Va por
   hermandad y no con una clase para no pedirle nada al marcado: cualquier
   tarjeta que ponga su nota bajo un `.boxed-list` la hereda. */
.boxed-list + .card-note { margin-top: var(--sp-4); }

/* Una nota dentro de una tarjeta: dice algo del contenido que hay debajo y no
   es un error. Apagada porque no es una alarma —lo reemplazado es una decisión
   de quien escribió la línea, no un problema— pero presente, porque un total
   al que le falta una línea sin explicación es un número que miente. */
.card-note {
    /* Flexible desde que lleva un botón al lado: el ofrecimiento de traer el
       logo del tema, en Marca. Sin esto el botón caía al renglón siguiente,
       alineado a la izquierda debajo de su frase, y se leía como otra cosa.
       Una nota sin botón se ve igual que siempre — un solo hijo en una caja
       flexible no cambia de sitio. */
    display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2);
    margin: 0 0 var(--sp-4);
    font-size: var(--fs-small); line-height: 1.5; color: var(--text-light);
}

/* Las viñetas del Resumen (la ficha del descuento, calcada de la de
   Shopify): condiciones y rendimiento como lista corta de hechos. Con sus
   puntos y su sangría propia porque el reset global de listas los quita, y
   una lista de condiciones sin viñetas se lee como un párrafo cortado. */
.summary-list {
    margin: 0 0 var(--sp-4); padding-left: var(--sp-5);
    list-style: disc;
    font-size: var(--fs-small); line-height: 1.6; color: var(--text);
}
.summary-list li { margin: 0 0 var(--sp-1); }

/* Los papeles que explican un documento —hoy la factura de un pedido, mañana
   lo que aporte otro módulo por el mismo hook—. Con caja y borde y no como
   enlace suelto: es el destino más pesado de la tarjeta y compite con una
   tabla de abonos que ocupa diez veces más alto.

   `a:not(.btn)` no hace falta acá porque no está dentro de una tabla, pero el
   ancho SÍ: sin `display: flex` el icono se sienta en la línea base y queda
   dos píxeles bajo el texto. */
.doc-links { list-style: none; margin: 0 0 var(--sp-4); padding: 0; display: grid; gap: 6px; }
.doc-link {
    display: flex; align-items: center; gap: var(--sp-2);
    padding: 9px var(--sp-3);
    border: 1px solid var(--primary-soft-border);
    border-radius: var(--radius);
    background: var(--primary-soft);
    color: var(--primary-dark); font-weight: 600; text-decoration: none;
}
.doc-link svg { width: 15px; height: 15px; flex: none; }
.doc-link:hover { background: var(--bg); }

/* Un identificador NO necesita el ancho de un nombre: son dos o tres palabras
   con guiones. Sin el tope, dos casillas en la misma fila se reparten el
   espacio a la mitad y la que se lee —el nombre— queda igual de angosta que la
   que casi nunca se toca. `flex-grow: 0` o el reparto se lo lleva igual. */
.inline-row .input-handle { flex: 0 1 200px; }
/* El rótulo de la fila: al lado del campo y no encima, porque la fila entera
   mide un renglón. `flex: none` o se estira y le come el ancho a la casilla, que
   es la única que tiene que crecer. `margin-bottom: 0` para ganarle al margen de
   `.field label`, que lo empujaría medio renglón por debajo del campo. */
.inline-row-label {
    flex: none; margin-bottom: 0;
    font-size: var(--fs-small); font-weight: 600; color: var(--text-light);
}

/* La lista del riel: un enlace, su pastilla y una línea de detalle debajo. */
.rail-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.rail-list li {
    display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
    padding: 9px 0; border-bottom: 1px solid var(--border);
}
.rail-list li:last-child { border-bottom: 0; padding-bottom: 0; }
/* Ocupa el renglón entero: es el detalle de la línea de arriba, no otra cosa
   al lado. */
.rail-list-sub {
    flex: 1 0 100%;
    font-size: var(--fs-meta); color: var(--text-light);
}

/* ── La ficha de lectura: hechos y actividad ───────────────────────────────
   Dos formas que la ficha de un tercero estrena y que van a repetirse en cuanto
   haya una de un empleado o de un pedido: la lista de HECHOS del riel y la
   ACTIVIDAD leída de la bitácora. */

/* `dl` de verdad y no dos columnas de `div`: rótulo y valor son un par, y un
   lector de pantalla lo anuncia como tal. La grilla los alinea sin tabla. */
/* `minmax(0, 1fr)` y no `1fr` en el valor, y `minmax(0, auto)` en el
   rótulo: `1fr` es `minmax(auto, 1fr)`, y ese mínimo `auto` es el
   min-content de la celda. Un dato de una sola palabra larga —un canal
   importado como «Subscription_Contract_Checkout_One_Time», una URL, un
   correo— imponía un mínimo más ancho que la tarjeta y se salía por el
   borde derecho (visto en la ficha de un tercero, 2026-09-15). Con el
   mínimo en cero la columna puede encogerse, y `overflow-wrap` es lo que
   parte esa palabra en vez de estirarla. */
.facts { display: grid; grid-template-columns: minmax(0, auto) minmax(0, 1fr);
         gap: 7px var(--sp-3); margin: 0; }
.facts dt { font-size: var(--fs-label); font-weight: 600; color: var(--text-light);
            overflow-wrap: anywhere; }
/* El valor a la DERECHA, que es como se leen los pares de una ficha: el ojo
   baja por una sola columna en vez de perseguir el final de cada rótulo. */
.facts dd { margin: 0; font-size: var(--fs-small); color: var(--text);
            text-align: right; overflow-wrap: anywhere; }
.facts-text { margin: 0; font-size: var(--fs-small); line-height: 1.5; color: var(--text-light); }
/* Un valor que no cabe: UNA línea con elipsis y la salida al lado
   (`ui/_valor_largo.html`). El texto es lo que se encoge —`min-width: 0`, la
   misma razón de siempre— y el botón nunca: cortarle el rótulo a la salida
   sería el chiste de la pantalla. Alineado a la derecha porque el valor de
   una ficha se lee por su final. */
.valor-largo { display: flex; align-items: center; justify-content: flex-end;
               gap: var(--sp-2); max-width: 100%; min-width: 0; }
.valor-largo-texto { min-width: 0; overflow: hidden;
                     text-overflow: ellipsis; white-space: nowrap; }
/* La salida no se encoge nunca: es 18 px y es la única forma de leer lo que
   la elipsis se comió. */
.valor-largo .help { flex: none; }
/* El disparador con rótulo escrito al lado del glifo, para donde sí hay
   sitio; en una ficha no lo hay, y ahí va el ícono pelado. */
.help-icon-rotulo { display: inline-flex; align-items: center; gap: 4px;
                    white-space: nowrap; font-size: var(--fs-label);
                    font-weight: 600; }
/* Un dato de la ficha que se CAMBIA en su sitio —la estación de una orden o
   de una receta—: el select y su botón, alineados a la derecha como el
   valor que reemplazan, sin JavaScript. */
.facts-form { display: inline-flex; gap: var(--sp-2); align-items: center; margin: 0; }
.facts-form select { width: auto; }

/* ── La ficha en dos columnas ──────────────────────────────────────────────
   La forma de la ficha de pedidos de `tiendapp` (su `.o-layout`): el
   documento a la izquierda —ancho, porque lleva la tabla— y el contexto a
   la derecha. `minmax(0, …)` en la principal por la razón de siempre: sin
   él, una tabla ancha estira la columna en vez de desbordar con su scroll.
   La estrena el pedido confirmado; el borrador NO la usa — su columna única
   es la pantalla de captura, y partir el flujo del lector en dos columnas
   sería mover la vista a mitad de escaneo. */
.ficha-cols {
    display: grid; grid-template-columns: minmax(0, 2fr) minmax(280px, 1fr);
    gap: var(--sp-4); align-items: start;
}
.ficha-main, .ficha-side {
    display: flex; flex-direction: column; gap: var(--sp-4); min-width: 0;
}
/* El punto es el de tiendapp (1000px): más angosto, la lateral cae debajo. */
@media (max-width: 1000px) {
    .ficha-cols { grid-template-columns: 1fr; }
}

/* Un valor CON su acción al lado —«Puesto · Cambiar», «Sin invitar · Invitar»—
   sigue siendo un valor de la ficha: se alinea a la derecha como los demás, y
   el botón viaja en la misma línea. `flex-wrap` porque el enlace de invitación
   trae un input de ancho completo debajo. */
.fact-action {
    display: flex; align-items: center; justify-content: flex-end;
    gap: var(--sp-2); flex-wrap: wrap;
}
.fact-action form { display: contents; }

/* La CIFRA de la tarjeta de salario. Es el dato por el que alguien abre la
   tarjeta, así que se lee como cifra y no como un renglón más de la rejilla. */
.fact-figure {
    margin: 0; font-size: var(--fs-number); font-weight: 700;
    font-variant-numeric: tabular-nums; letter-spacing: -0.02em;
}

/* El historial debajo de la cifra, separado por la misma línea que separa
   cualquier pie de tarjeta. Las razones van en su propio renglón chico: en 312px
   «$2.000.000 · Salario al migrar desde tiendapp» no cabe en una línea y
   partirse a mitad de la cifra es peor que apilarse. */
.facts-history {
    margin-top: var(--sp-3); padding-top: var(--sp-3);
    border-top: 1px solid var(--border);
}
.fact-note {
    display: block; font-size: var(--fs-label); color: var(--text-light);
}

/* La LÍNEA la dibuja la lista con un `::before`, no un borde por entrada: con
   el borde queda cortada en el espacio entre entradas y se ve punteada. */
/* `margin-top` para separarse de la casilla de comentario: sin él la primera
   tarjeta nacía pegada al input y las dos se leían como un solo bloque. */
.timeline {
    position: relative; list-style: none;
    margin: var(--sp-5) 0 0; padding: 0 0 0 var(--sp-6);
    display: flex; flex-direction: column; gap: var(--sp-4);
}
/* La línea arranca y termina EN las bolitas de los extremos, no en el borde de
   la lista: sobresalir por arriba y por abajo se ve como un cable suelto. */
.timeline::before {
    content: ''; position: absolute; top: 10px; bottom: 10px; left: 5px;
    width: 1.5px; background: var(--border);
}
.timeline-item {
    position: relative;
    display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px var(--sp-3);
}
/* La bolita tapa la línea a su altura: sin fondo propio se ve el cable pasando
   por debajo y deja de leerse como un hito. */
/* Alineada con el RENGLÓN, no con el borde de la entrada: una bolita a la altura
   del espacio entre líneas no marca nada. En una entrada suelta el renglón
   empieza arriba; en un comentario empieza después del relleno de la tarjeta, y
   por eso los dos desplazamientos son distintos. */
.timeline-dot {
    position: absolute; left: calc(var(--sp-6) * -1 + 1px); top: 5px;
    width: 9px; height: 9px; border-radius: 50%;
    background: var(--border-strong);
    box-shadow: 0 0 0 3px var(--card-bg);
}
/* De marca, y a la altura del nombre de quien escribió: lo que dijo una persona
   es lo que se busca al recorrer la lista. */
.timeline-comment > .timeline-dot { background: var(--primary); top: 23px; }

/* La fecha en tabular para que las columnas de dígitos se alineen entre filas;
   con cifras proporcionales una lista de veinte marcas de tiempo baila. */
.timeline-when {
    flex: none;
    font-size: var(--fs-meta); color: var(--text-light);
    font-variant-numeric: tabular-nums;
}
.timeline-what { flex: 1 1 auto; font-size: var(--fs-small); color: var(--text); }
.timeline-who { color: var(--text-light); }

/* El comentario, en tarjeta. */
.timeline-card {
    flex: 1 1 100%; min-width: 0;
    padding: var(--sp-4) var(--sp-4) var(--sp-4) var(--sp-5);
    background: var(--card-bg);
    border: 1px solid var(--border); border-radius: var(--radius);
}
/* El cuerpo respira respecto al encabezado: con 4px el comentario se leía
   colgado del nombre en vez de debajo de él. */
.timeline-head {
    display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3);
    margin-bottom: var(--sp-2);
}
.timeline-author { font-size: var(--fs-small); font-weight: 600; color: var(--text); }
/* Las acciones se van a la derecha del encabezado, y ahí termina el renglón. */
.timeline-actions { margin-left: auto; display: flex; align-items: center; gap: 6px; }
.timeline-body { margin: 0; font-size: var(--fs-small); line-height: 1.5; color: var(--text); }

/* El `<details>` que revela el formulario de edición en su sitio. El marcador
   nativo se quita: el `<summary>` se pinta como botón y la flechita al lado de
   «Editar» lo delataría como algo que se despliega y no como algo que se
   aprieta. */
.inline-edit { position: relative; }
.inline-edit > summary { list-style: none; cursor: pointer; }
.inline-edit > summary::-webkit-details-marker { display: none; }
.inline-edit[open] > summary { border-color: var(--primary); color: var(--primary); }
/* ABSOLUTO y anclado a la derecha del botón. Dentro del flujo caía adentro de
   `.timeline-actions`, que es una fila alineada a la derecha, así que la casilla
   quedaba de dos centímetros. Y `min()` con `vw` para que a 390px no se salga
   por el borde. */
.inline-edit > form {
    position: absolute; top: calc(100% + 6px); right: 0; z-index: 5;
    width: min(420px, 78vw);
    padding: var(--sp-3);
    background: var(--card-bg);
    border: 1px solid var(--border); border-radius: var(--radius);
    box-shadow: var(--shadow-lg);
}

/* ── La ayuda de un campo: el `(i)` y su modal ─────────────────────────────
   Ver `ui/_help.html` para por qué es una casilla y no un `<details>`. */
/* El envoltorio mide EXACTAMENTE el ícono, y de ahí sale el centrado.

   Sin la altura fija su caja calculaba 22px con un ícono de 15 dentro, así que
   estiraba la fila del rótulo y el `(i)` salía tres píxeles y medio por encima
   del texto — que es la diferencia entre un ícono alineado y uno montado.
   `align-self: center` no lo arreglaba, porque no era estiramiento: era la
   altura del envoltorio.

   Un solo número para los dos: si el ícono crece y el envoltorio no, vuelve el
   mismo defecto por el otro lado. */
.help {
    --help-size: 15px;
    position: relative; display: inline-flex; flex: none;
    width: var(--help-size); height: var(--help-size);
}
/* Oculta pero enfocable. NO se le agrega a la lista de excepciones del
   selector de casillas: esa lista es para las que no deben LLEVAR nuestro
   dibujo —el cajón se pinta como hamburguesa—, y ésta simplemente no se ve.
   Que herede el dibujo en 1px invisible no le hace daño a nadie. */
.help-toggle { position: absolute; width: 1px; height: 1px; opacity: 0; margin: 0; }
/* El círculo lo dibuja el SVG. Antes era un borde CSS con una «i» de texto
   adentro, y no había forma de centrarla: la itálica la corría a la derecha y
   la métrica de la fuente la subía sobre el rótulo. Un glifo trazado cae donde
   se pone, y a 15px eso es la diferencia entre un ícono y una errata.

   `display: block` en el SVG y no `inline`: como inline se sienta en la línea
   base y arrastra el descendente, que es el píxel y medio que lo dejaba alto
   respecto al rótulo de al lado. */
.help-icon {
    display: block; width: var(--help-size); height: var(--help-size);
    color: var(--text-light); cursor: pointer;
    transition: color .16s;
}
.help-icon svg { display: block; width: 100%; height: 100%; }
.help-icon:hover { color: var(--primary); }
/* El foco se pinta en el ÍCONO porque la casilla mide un píxel: sin esto, el
   tabulador llega a la ayuda y no se ve que llegó. */
.help-toggle:focus-visible + .help-icon {
    outline: 2px solid var(--primary); outline-offset: 2px;
    border-radius: 50%; color: var(--primary);
}
.help-sheet { display: none; }
.help-toggle:checked ~ .help-sheet {
    display: flex; position: fixed; inset: 0; z-index: 80;
    align-items: center; justify-content: center; padding: var(--sp-5);
}
.help-backdrop { position: absolute; inset: 0; background: rgba(17, 16, 29, .44); cursor: pointer; }
.help-panel {
    position: relative; display: flex; flex-direction: column; gap: var(--sp-3);
    width: 100%; max-width: 420px;
    padding: var(--sp-5);
    background: var(--card-bg); border-radius: var(--radius-card);
    box-shadow: var(--shadow-lg);
    text-align: left;
}
.help-panel-title { font-size: var(--fs-section); font-weight: 600; color: var(--text); }
.help-panel-text { font-size: var(--fs-body); line-height: 1.5; color: var(--text-light); }
/* `align-self` y no `width: 100%`: un botón que cruza el modal entero se lee
   como la acción de una pantalla y no como el cierre de un aviso. */
.help-close { align-self: flex-start; cursor: pointer; }
.home-feed-card {
    max-width: 720px; margin: 0 auto;
    /* Encima de los orbes SIEMPRE: el escenario sangra 180px hacia abajo a
       propósito (el desvanecido), y sin contexto de apilamiento propio esa
       decoración quedaba pintada sobre la tarjeta de actividad. */
    position: relative; z-index: 1;
    /* Última en entrar: cierra la escalera que abre el saludo. */
    animation: home-rise .5s ease .24s both;
}
/* El encabezado DENTRO de una tarjeta: título a la izquierda, y a la derecha
   una pista gris o un botón.

   El margen negativo lleva la divisoria hasta los bordes de la tarjeta. Sin
   él la línea queda flotando con veintidós píxeles de aire a cada lado, que
   se lee como una raya y no como el borde de una banda. Los números salen
   del `padding` de `.card` — si aquél cambia, éste cambia con él. */
.card-header {
    display: flex; align-items: center; justify-content: space-between;
    gap: 14px; flex-wrap: wrap;
    margin: calc(var(--sp-4) * -1) calc(var(--sp-5) * -1) var(--sp-4);
    padding: var(--sp-3) var(--sp-5);
    border-bottom: 1px solid var(--border);
}
/* `hidden` tiene que ganarle al `display: flex` de arriba, igual que en
   `.bulkbar` y por lo mismo: la hoja del navegador trae `[hidden]` con la
   misma especificidad y nuestra regla va después. Lo apaga `bulk.js`
   mientras hay filas seleccionadas, para que la barra ocupe este sitio en
   vez de empujar la tabla. */
.card-header[hidden] { display: none; }
/* Una tarjeta que es SOLO su encabezado —la del emisor en la configuración
   contable: título, pista, pastilla y «Configurar», y nada debajo—. El
   margen de abajo de la banda y el relleno de la tarjeta dejaban una franja
   vacía bajo el filete (Renzo, 2026-09-19); la banda última se come ese
   relleno y no pinta filete, porque no separa nada de nada. */
.card > .card-header:last-child { margin-bottom: calc(var(--sp-4) * -1); border-bottom: 0; }
/* En MAYÚSCULAS como tiendapp — decisión de Renzo del 2026-08-04, cerrando
   la pendiente anotada en F1-e. Vale para TODAS las tarjetas del ERP, los
   conteos-encabezado de las listas incluidos. */
/* `.bulkbar h2` va EN ESTA REGLA y no imitándola desde su propio bloque.

   La barra de selección ocupa el sitio de esta banda, así que su conteo
   tiene que pintarse exactamente igual. El primer intento fue ponerle la
   clase `.card-count` —la misma que lleva el `<h2>` de acá— y no alcanzó:
   dentro del encabezado manda `.card-header h2`, que con (0,1,1) le gana a
   `.card-count` con (0,1,0). O sea que el número del encabezado NUNCA se
   pintó con `.card-count`, y copiar la clase copiaba la etiqueta pero no
   la apariencia: 13,5px/600 contra 11,5px/700, y el texto cambiaba de
   tamaño al seleccionar.

   Compartir el selector es lo único que garantiza que sigan iguales cuando
   alguien toque uno de los dos. */
/* Las DOS bandas miden lo mismo, y lo que fija ese alto es el botón que
   tienen que poder llevar (`--band-h`).

   Antes se igualaban por coincidencia: el título medía 20,9 y el botón 22,
   así que la banda con verbos salía un píxel más alta y la tabla brincaba
   ese píxel. Se arregló bajándole el relleno al botón —una excepción al
   sistema de tamaños, escondida dentro de la barra— y esa excepción se
   retira acá: el alto lo declara la banda, no lo negocia su contenido.

   Alcanza a TODAS las tarjetas del ERP y no solo a las listas, a sabiendas
   (Renzo, 2026-08-14): una banda que puede llevar un botón se dimensiona
   para llevarlo, tenga uno o no, o dos tarjetas de la misma pantalla se
   leen con dos alturas según lo que les hayan puesto adentro. */
.card-header,
.bulkbar { min-height: var(--band-h); }

.card-header h2,
.bulkbar h2 {
    margin: 0; font-size: var(--fs-section); font-weight: 600;
    text-transform: uppercase; letter-spacing: .02em;
}
/* Texto, no acción. Es «primera = principal» o «lo que se vende son las
   variantes»: dice algo del contenido de la tarjeta y no se puede tocar. */
.card-hint { margin: 0; color: var(--text-light); font-size: var(--fs-small); }

/* El conteo COMO encabezado de la tabla: «475 productos». Es lo que unifica las
   listas del ERP —la barra de filtros en su tarjeta arriba, la tabla en la suya
   con su número— y vive DENTRO del fragmento que htmx intercambia, así que
   filtrar lo actualiza. En versalitas porque es un rótulo de sección y no un
   título: compite con el nombre de la pantalla si se pinta como un `h2` normal.

   `tabular-nums` para que el número no baile de ancho al cambiar de cuatro
   dígitos a tres mientras se escribe en el buscador. */
.card-count {
    margin: 0;
    font-size: var(--fs-label); font-weight: 700;
    letter-spacing: .06em; text-transform: uppercase;
    color: var(--text); font-variant-numeric: tabular-nums;
}

/* EL PANEL DE COLUMNAS, en la cabecera de la tarjeta.

   `position: relative` sobre el `<details>` y absoluto el panel: en flujo,
   abrirlo empujaba la tabla entera hacia abajo y la fila que estabas
   mirando se iba de la pantalla. Un menú que mueve lo que venías leyendo
   se siente como que la pantalla se rompió.

   El marcador del `<summary>` se quita por partida doble —`list-style` y
   el pseudo-elemento de WebKit— porque cada navegador lo dibuja por su
   lado, y con uno solo quedaba el triángulo encima del glifo en Safari. */
.colmenu { position: relative; }
.colmenu > summary { list-style: none; cursor: pointer; }
.colmenu > summary::-webkit-details-marker { display: none; }
.colmenu-panel {
    position: absolute; right: 0; top: calc(100% + 6px); z-index: 30;
    min-width: 210px; padding: var(--sp-3);
    /* `--card-bg` y no un `--surface` inventado: ese token NO EXISTE en esta
       hoja, y un `background` que no resuelve deja el panel TRANSPARENTE —
       las filas de la tabla se leen a través de las casillas. No lo caza
       ninguna prueba: la guarda de `test_stylesheet.py` comprueba que cada
       clase tenga regla, no que cada variable esté definida. Solo apareció
       mirando la pantalla. */
    background: var(--card-bg); border: 1px solid var(--border);
    border-radius: var(--radius); box-shadow: 0 10px 30px rgba(16, 15, 40, .16);
}
.colmenu-list { list-style: none; margin: 0 0 var(--sp-3); padding: 0; }
/* El área de clic es la ETIQUETA entera y no solo la casilla: doce píxeles
   cuadrados es un blanco que se falla, y fallarlo acá cierra el panel. */
.colmenu-item {
    display: flex; align-items: center; gap: 10px;
    padding: 6px 4px; cursor: pointer;
    font-size: var(--fs-body); color: var(--text);
}
.colmenu-item:hover { background: var(--bg); border-radius: var(--radius-field); }
.colmenu-foot { margin-bottom: 6px; }
/* En angosto el panel se ANCLA A LA IZQUIERDA del botón, no a la derecha.

   Medido a 375px: la cabecera de la tarjeta envuelve y deja el botón cerca
   del borde izquierdo, así que un panel de 210px colgando hacia la izquierda
   empezaba en -61px — las casillas quedaban fuera de la pantalla, y como el
   desborde se recorta en vez de generar scroll, no había forma de llegar a
   ellas. El `max-width` es el cinturón: con una etiqueta larga, `left: 0` no
   alcanza para impedir que se salga por el otro lado. */
@media (max-width: 767px) {
    .colmenu-panel { right: auto; left: 0; max-width: calc(100vw - 32px); }
}

/* ── El interior del encabezado: baldosa, título y pista ───────────────────
   Lo que `ui/_card_head.html` pinta DENTRO de `.card-header`. La banda no
   cambia —sus márgenes negativos, su borde, su altura— así que una tarjeta
   con baldosa y una sin ella siguen midiendo igual y pegando igual contra el
   `thead` de su tabla.

   Nació cerrando un empate: `.card-header` manda la pista a la DERECHA del
   título, y una pista que explica de qué va la tarjeta —«Estos los administra
   el sistema»— se lee debajo, no al lado. La pantalla que lo necesitó primero
   —pasarelas— se inventó `gateway-name`/`gateway-hint` para tenerlo, y
   Dominios lo copió. Dos encabezados de tarjeta con dos nombres es la deriva;
   esto es el nombre único. */
.card-head-lead { display: flex; align-items: center; gap: var(--sp-3); min-width: 0; }
/* `min-width: 0` en los dos: sin él un título largo estira la banda y se lleva
   por delante lo que haya a la derecha en vez de romperse. */
.card-head-text { min-width: 0; }
/* El `<h2>` lo pinta `.card-header h2` — versalitas, 13.5px — y no se repite
   acá: el título de una tarjeta se ve igual la traiga o no la baldosa. Lo
   único que cambia es que ahora tiene una pista DEBAJO, y eso es un margen. */
.card-head-text .card-hint { margin-top: 3px; }
/* Lo que la tarjeta quiere decir de sí misma, al otro extremo de la banda: hoy
   qué significa la coincidencia elegida en las condiciones de una colección.

   En su caja y no como texto suelto: la banda ya tiene un texto apagado —la
   pista, debajo del título— y dos grises en el mismo renglón se leen como uno
   partido en dos. `min-width: 0` y `flex-wrap` heredados de `.card-header`
   hacen que baje a su propio renglón cuando la tarjeta se angosta, en vez de
   aplastar el título. */
.card-head-note {
    display: flex; align-items: center; gap: var(--sp-2);
    margin: 0; padding: 6px var(--sp-3);
    border-radius: var(--radius-field); background: var(--bg);
    font-size: var(--fs-small); color: var(--text-light);
}

/* ── La baldosa ────────────────────────────────────────────────────────────
   Un icono dentro de un cuadro redondeado con tinte. Es la pieza que le pone
   cara a una sección sin gastarle una ilustración: el encabezado de una
   tarjeta, la primera celda de una fila, el banner de una pantalla.

   El TINTE dice de qué se trata y reusa los pares de la semántica —los mismos
   que las pastillas—, así que un ámbar significa lo mismo en los dos sitios.
   `brand` es el neutro: «esto es una sección», sin carga.

   El `<svg>` se dimensiona ACÁ y no en cada plantilla, igual que en `.btn`:
   con el tamaño escrito en el marcado, la tercera baldosa sale de 18px porque
   alguien la copió de otro sitio. */
.tile {
    flex: none; display: grid; place-items: center;
    width: 38px; height: 38px; border-radius: var(--radius-sm);
    background: var(--primary-soft); color: var(--primary-dark);
}
.tile svg { width: 19px; height: 19px; }
.tile-brand  { background: var(--primary-soft); color: var(--primary-dark); }
.tile-ok     { background: var(--ok-bg);     color: var(--ok-text); }
.tile-warn   { background: var(--warn-bg);   color: var(--warn-text); }
.tile-danger { background: var(--danger-bg); color: var(--danger-text); }
.tile-info   { background: var(--info-bg);   color: var(--info-text); }
/* El gris de «esto no lo decides tú»: los dominios que da el sistema, un
   registro que sólo se lee. */
.tile-muted  { background: var(--bg); color: var(--text-faint); }
/* La de una CELDA, y del MISMO tamaño que la miniatura de una foto.

   Decía «más chica y más cerrada: en una fila de 40px la grande manda el
   alto, y veinte filas crecen por un adorno», y era cierto mientras fuera
   la única forma de abrir una fila. Dejó de serlo: `.cell-thumb` abre las
   listas que llevan foto —productos, terceros, archivos, usuarios— a 35px,
   y ésta abría las otras treinta y cinco a 26. Navegando entre listas, las
   filas cambiaban de altura sin que la diferencia dijera nada.

   El argumento del alto sigue valiendo y por eso el tamaño común es 35 y no
   38: lo que se igualó es el de CELDA, no el de pantalla.

   El radio y el glifo van derivados del tamaño, no escritos: a 26px un
   radio de 7 y un glifo de 14 eran proporción, y copiarlos tal cual a 35
   los dejaría de serlo. */
.tile-sm {
    width: var(--cell-thumb); height: var(--cell-thumb);
    border-radius: 9px;
}
.tile-sm svg { width: 18px; height: 18px; }
/* La del BANNER, sobre el negro. El tinte claro de las otras se ve como un
   parche encendido ahí, así que ésta es un vidrio: el mismo fondo, un poco
   más claro.

   Va ANIDADA bajo `.page-banner` y no suelta, y no es por especificidad: es
   la única de las baldosas que pinta con `--banner-text`, que es blanco puro.
   Suelta, quien la copiara a una tarjeta se llevaría un glifo blanco sobre
   fondo blanco — el contenido entero, invisible. Anidada, fuera del banner
   simplemente no aplica y cae al tinte de marca de `.tile`, que se ve.
   `test_the_banner_text_colour_is_only_used_on_dark_blocks` lo verifica, y es
   quien lo cazó al escribir esto. */
.page-banner .tile-banner {
    width: 44px; height: 44px; border-radius: var(--radius-sm);
    background: rgba(255, 255, 255, .1); color: var(--banner-text);
}
.page-banner .tile-banner svg { width: 22px; height: 22px; }

/* La tarjeta de la barra de filtros. Solo el relleno: lo demás lo pone `.card`,
   y una barra que se ve distinta de la tarjeta de abajo rompe justo lo que este
   par existe para dar — que las once listas se operen igual. */
/* Sólo el relleno. El margen propio que traía —`margin-bottom: var(--sp-4)`—
   se SUMABA al `gap` de la pila que la envuelve en las once listas: 32px entre
   la barra de filtros y su tabla, contra 16 en todo lo demás. Es la trampa que
   está escrita arriba de `.card-stack`, viva desde entonces y medida en el
   plan de cuentas el 2026-08-11 (`bottom: 317`, `top: 349`).

   Se quita y no se compensa: quien separa es el contenedor, siempre — la pila
   con su `gap`, y `.content` con la regla de hermano adyacente para la
   pantalla que no envuelve nada. */
.filters-card { padding: var(--sp-4) var(--sp-5); }

/* En una tarjeta que ES una tabla, el encabezado pega con el `thead` y no deja
   un renglón en blanco entre los dos. `.card-header` trae `margin-bottom` para
   separarse del contenido normal de una tarjeta —un formulario, un párrafo—,
   pero acá lo que sigue es la cabecera de la tabla, que tiene su propio borde:
   con el hueco en medio se leen DOS líneas separadas por una franja vacía, que
   es exactamente lo que se ve en la captura. */
.card-table > .card-header { margin-bottom: 0; }

/* Y cuando la paginación va DENTRO de la tarjeta —debajo de la tabla— la tabla
   deja de ser lo último: sus esquinas redondeadas caen a media tarjeta, con el
   paginador colgando debajo y pegado al filo, porque `.card-table` le quita a
   la tarjeta el relleno de abajo justamente para que la tabla llegue al borde.

   Se cuadran las esquinas cuando hay algo después, y el relleno se lo pone ese
   algo. `:not(:last-child)` y no una clase nueva: quien decide es la posición,
   y una clase habría que acordarse de ponerla en las tres listas paginadas. */
.card-table > .table-wrap:not(:last-child),
.card-table > [data-bulk-form]:not(:last-child) > .table-wrap { border-radius: 0; }
.card-table > .pager { margin-top: 0; padding: var(--sp-4) var(--sp-5); }

/* Y lo mismo para «Agregar fila», que es lo otro que puede ir DEBAJO de una
   tabla: el relleno se lo pone él, igual que el paginador, porque la tarjeta
   se lo quitó para que la tabla llegara al filo.

   Al ras de la izquierda y no centrado: la fila que va a crear empieza ahí,
   y un botón centrado bajo una tabla se lee como el pie del formulario —el
   sitio de guardar—, que es justo lo que NO es. */
.table-add { padding: var(--sp-4) var(--sp-5); }

/* La tabla llega a los BORDES de la tarjeta (Renzo, 2026-08-14).

   Se quedaba dentro del relleno, y mientras el encabezado fue blanco no se
   notaba: no había nada que dibujara dónde terminaba la tabla. Con la banda
   gris sí — arranca a 21px del filo y deja una franja blanca a cada lado,
   que se lee como una tabla despegada de su propia tarjeta.

   El margen negativo estuvo antes y se quitó por un argumento correcto: con
   la tabla de variantes compartiendo tarjeta con la caja de ejes, la sangría
   dejaba dos anchos distintos y el ojo perdía la columna. Ese argumento se
   respeta y por eso vuelve DE OTRA FORMA: la caja se ensancha hasta el filo,
   pero el TEXTO no se mueve, porque la primera y la última celda llevan
   exactamente el relleno que la tarjeta acaba de ceder (`--sp-5` las dos).
   El nombre de la primera fila cae en la misma vertical que el título de la
   tarjeta y que cualquier otro contenido; lo único que se estira es el
   fondo. Es la diferencia entre sangrar la tabla y sangrar sus datos.

   `--sp-5` va escrito y no derivado de `.card`: son el mismo valor y tienen
   que seguir siéndolo. Si algún día el relleno de la tarjeta cambia, esto
   deja de cuadrar — y ahí es donde hay que mirar. */
.card-table { padding-bottom: 0; }
/* El `<form>` de la selección múltiple se metió EN MEDIO, y con él la tabla
   dejó de ser hija directa de la tarjeta: la regla de arriba dejó de
   acertar, la sangría volvió y el radio de 20px de `.table-wrap` reapareció
   —«se separó otra vez y quedó curvo el encabezado»—.

   La salida no es repetir el selector con un nivel más, sino que el que
   llegue al filo sea el FORMULARIO: así la barra y la tabla van a lo ancho
   por estar dentro de él, sin que ninguna de las dos lleve margen propio, y
   el día que algo más entre en ese formulario no hay una tercera regla que
   acordarse de escribir. */
.card-table > [data-bulk-form] {
    margin: 0 calc(var(--sp-5) * -1);
    /* El ancho no lo da `width: auto`: con márgenes negativos a los dos
       lados, un contenedor de scroll sigue midiendo el ancho de su padre y
       se desbordaría por la derecha. */
    width: auto;
}
/* La tabla SIN selección: es ella la que llega al filo, porque no hay
   formulario que lo haga por ella. */
.card-table > .table-wrap {
    margin: 0 calc(var(--sp-5) * -1);
    width: auto;
    border-radius: 0 0 var(--radius-card) var(--radius-card);
}
/* Y la tabla DENTRO del formulario no lleva margen: el filo ya lo alcanzó
   su padre, y repetirlo la sacaría otros 20px por cada lado.
   Las dos declaraciones van en UN bloque por selector, no repartidas entre
   dos: `test_stylesheet` prohíbe que el mismo selector fije la misma
   propiedad en dos sitios, y con razón — gana el de más abajo, que es un
   orden que nadie ve al leer el de arriba. */
.card-table > [data-bulk-form] > .table-wrap {
    margin: 0;
    border-radius: 0 0 var(--radius-card) var(--radius-card);
}

/* ── Ficha en dos columnas ────────────────────────────────────────────── */

/* Lo que se escribe leyendo a la izquierda; lo que se elige de una lista y no
   se relee, a la derecha. Un producto de LTDC tiene trece variantes y cinco
   campos de su app, y en una sola columna eso son tres pantallas de scroll con
   el estado enterrado en la mitad.

   `align-items: start` o el riel estira sus tarjetas hasta el alto de la
   columna izquierda, y una tarjeta de un `select` mide media pantalla.

   El riel es una MEDIDA FIJA y la principal es la que cede: al revés, en un
   monitor ancho el riel se estira y un desplegable de 500px de ancho se lee
   como un error. `minmax(0, 1fr)` y no `1fr` porque una tabla ancha adentro
   —la de variantes lo es— infla la columna por encima de la rejilla. */
.product-layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 312px;
    gap: var(--sp-4); align-items: start;
}
.product-main, .product-rail {
    display: flex; flex-direction: column; gap: var(--sp-4); min-width: 0;
}
/* Las tarjetas de la ficha NO llevan el tope de lectura de `.form-card`: ya
   están acotadas por la columna, y sumarle 760px encima dejaba una franja
   vacía a la derecha de cada una. */
.product-main > .card { width: 100%; }

/* Un solo eje cuando el riel dejaría a la columna principal más angosta que
   una tabla de seis columnas. Debajo de eso el riel va ARRIBA: el estado y el
   tipo son lo primero que alguien mira al abrir un producto, y mandarlos al
   final de tres pantallas de scroll es esconderlos. */
@media (max-width: 1100px) {
    .product-layout { grid-template-columns: minmax(0, 1fr); }
    .product-rail { order: -1; }
}

/* ── Ejes de opción ───────────────────────────────────────────────────── */

/* Los ejes no son un modelo: son lo que dicen las variantes, agrupado. Trece
   filas con «Presentación» y «Molienda» son en realidad dos ejes de pocos
   valores, y verlos así es la diferencia entre entender un producto de un
   vistazo y deducirlo leyendo la tabla. */
/* ── Los ejes de opción, y su orden ────────────────────────────────────────
   Cada eje es un RENGLÓN con su nombre arriba y sus valores debajo, separados
   por una línea. Antes iban en una sola fila —nombre a la izquierda, valores a
   la derecha— y con tres ejes de seis valores eso es una pared: el nombre
   quedaba huérfano contra una hilera que envolvía tres veces.

   El renglón es además lo que hace agarrable el orden: se arrastra o se sube
   con la flecha, y las dos cosas necesitan una fila con altura, no una línea
   de texto. */
.option-axes {
    display: flex; flex-direction: column;
    /* Aire contra el encabezado de la tarjeta. `--sp-4` son 16px y no los 15
       del boceto: el proyecto tiene escala, y un número suelto acá es el
       primero de los que después nadie sabe si eran a propósito. */
    margin-top: var(--sp-4);
    margin-bottom: var(--sp-2);
    border: 1px solid var(--border); border-radius: var(--radius);
    background: var(--card-bg); overflow: hidden;
}
/* Las tres piezas van con la celda ESCRITA, ninguna colocada sola. La versión
   anterior dejaba el nombre en automático y salía en la TERCERA columna, con
   el bloque de flechas ocupando la segunda encima de los valores: la
   colocación automática ubica primero los ítems que declaran fila —la manija
   la declaraba— y el que no declara nada se va al hueco que quede. Medido en
   la ficha: el nombre en x=828 y los valores en x=335, o sea el título a la
   derecha de sus propios valores. */
.option-axis {
    display: grid; align-items: center; gap: 4px 12px;
    grid-template-columns: auto minmax(0, 1fr);
    padding: var(--sp-3) var(--sp-4);
}
.option-axis + .option-axis { border-top: 1px solid var(--border); }
.option-axis.is-dragging { opacity: .45; }
.option-axis.is-over { background: var(--primary-soft); }
/* La manija dice «esto se agarra», y es lo ÚNICO que mueve: sin JavaScript no
   hay orden que cambiar. */
.option-axis-grip {
    grid-column: 1; grid-row: 1 / 3;
    color: var(--text-faint); font-size: var(--fs-body);
    line-height: 1; cursor: grab; user-select: none;
}
.option-axes[data-axis-sort] .option-axis-grip:active { cursor: grabbing; }
.option-axis-name {
    grid-column: 2; grid-row: 1;
    color: var(--text); font-size: var(--fs-body); font-weight: 600;
}
.option-axis-values {
    grid-column: 2; grid-row: 2; display: flex; gap: 6px; flex-wrap: wrap;
}
.option-axes-hint {
    margin: 0 0 var(--sp-4); color: var(--text-light); font-size: var(--fs-small);
}
/* Rellenas y sin borde: son una MUESTRA de los valores que hay, no controles.
   Con borde competían con los campos del formulario de arriba. */
.chip {
    padding: 3px 10px; border-radius: 8px;
    background: var(--bg); border: 1px solid transparent;
    color: var(--text); font-size: var(--fs-small); white-space: nowrap;
}
/* Con la tinta de la marca cuando la pastilla es el DATO de la frase y no una
   muestra: el valor de una condición es lo que alguien escribió, y en el gris
   de fondo se perdía entre el campo y el operador. Es un modificador de
   `.chip` y no otra clase: sigue siendo la misma pastilla de solo lectura. */
.chip-brand { background: var(--primary-soft); color: var(--primary-dark); font-weight: 600; }
/* El tope, dicho. Sin borde y sin fondo: no es un valor del eje, es la cuenta
   de los que no caben — pintarla igual que a los demás la haría parecer uno. */
.chip-more {
    background: none; border-color: transparent;
    color: var(--text-faint); font-style: italic;
}

/* ── El feed con la piel de tiendapp ─────────────────────────────────────────
   «En vivo» dice cómo se lee la lista, no que haya un socket. El glifo por
   fila viene de `icons.py`; la denegación se tiñe de peligro porque lo que se
   guarda es que NO pasó nada, y eso se tiene que ver distinto. */
.home-feed-header { display: flex; align-items: center; justify-content: space-between; }
.home-feed-live {
    display: inline-flex; align-items: center; gap: 6px;
    font-size: var(--fs-meta); font-weight: 600; color: var(--text-light);
    text-transform: uppercase; letter-spacing: .06em;
}
.home-feed-live::before {
    content: ''; width: 6px; height: 6px; border-radius: 50%;
    background: var(--ok-text); animation: live-pulse 1.8s ease-in-out infinite;
}
@keyframes live-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .3; } }

.home-feed { list-style: none; margin: 0; padding: 0; }
.home-feed-item {
    display: flex; align-items: center; gap: 13px;
    padding: 11px 6px; border-bottom: 1px solid var(--border);
    font-size: var(--fs-body); border-radius: 10px;
    transition: background .15s ease;
}
.home-feed-item:hover { background: var(--bg); }
.home-feed-item:last-child { border-bottom: none; }
.home-feed-glyph {
    flex: 0 0 auto; display: inline-flex; align-items: center;
    justify-content: center; width: 32px; height: 32px;
    border-radius: 10px; background: var(--bg); color: var(--text-light);
}
.home-feed-glyph svg { width: 16px; height: 16px; }
/* El tinte dice QUÉ pasó sin leer: verde es plata que entró, morado es
   alguien que llegó, rojo es un cobro que no pasó. La plantilla solo mira
   `kind` — el texto es de quien lo publica. */
.home-feed-item.kind-order .home-feed-glyph { background: var(--ok-bg); color: var(--ok-text); }
.home-feed-item.kind-charge .home-feed-glyph { background: var(--ok-bg); color: var(--ok-text); }
.home-feed-item.kind-customer .home-feed-glyph { background: var(--primary-soft); color: var(--primary-dark); }
.home-feed-item.kind-charge_failed .home-feed-glyph { background: var(--danger-bg); color: var(--danger-text); }
.home-feed-text { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
.home-feed-when {
    flex: 0 0 auto; color: var(--text-light); font-size: var(--fs-small);
    white-space: nowrap; font-variant-numeric: tabular-nums;
}
.mono {
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: var(--fs-small); color: var(--text-light);
}
/* La casilla de un JSON o un arreglo. Hereda la letra de `.mono` pero NO su
   gris: acá el texto no es un dato secundario, es lo que se está editando.
   `tab-size` para que un JSON pegado con tabulaciones no salga escalonado a
   ocho espacios, y `nowrap` + scroll horizontal porque una línea larga
   partida a la mitad deja de ser JSON legible. */
textarea.mono {
    color: var(--text); line-height: 1.5; tab-size: 2;
    white-space: pre; overflow-wrap: normal; overflow-x: auto;
}

.empty { padding: 28px 20px; text-align: center; }
.empty-compact { padding: 18px 0; text-align: left; }
.empty-icono {
    display: block; width: 36px; height: 36px; margin: 0 auto 10px;
    padding: 8px; box-sizing: border-box; border-radius: 50%;
    background: var(--bg); color: var(--text-light);
}
.empty-compact .empty-icono { margin-left: 0; }
.empty-title { margin: 0 0 4px; font-weight: 600; font-size: var(--fs-body); }
.empty-text { margin: 0 0 14px; color: var(--text-light); font-size: var(--fs-body); }

@media (max-width: 1023px) {
    body:not(.sidebar-collapsed) .sidebar { width: var(--rail); }
    body:not(.sidebar-collapsed) .nav-label,
    body:not(.sidebar-collapsed) .nav-badge,
    body:not(.sidebar-collapsed) .nav-sublist,
    body:not(.sidebar-collapsed) .sidebar-label { display: none; }
    body:not(.sidebar-collapsed) .nav-item { justify-content: center; padding: 10px 0; }
}

@media (max-width: 767px) {
    .drawer-toggle { display: block; }
    /* Le hace sitio a la hamburguesa, que es fija sobre la topbar. */
    .topbar-left { padding-left: 40px; }
    .topbar-collapse, .topbar-crumb { display: none; }
    /* EL ISOTIPO SOLO. El lockup mide 105px fijos —el `<svg>` no encoge— y
       `.topbar-left` sí se encoge por debajo de eso, así que el wordmark se
       salía de su caja y quedaba DEBAJO de la campana (Renzo, 2026-09-15,
       desde su iPhone; medido a 390px: la marca llega a 159 y la campana
       empieza en 149). Se recorta por ancho y no se esconde nada por dentro
       porque el isotipo es el primer cuadrado del `viewBox`: 30px de ancho
       son exactamente los siete puntos, sin tocar el archivo de la marca. */
    .topbar-brand { max-width: 34px; overflow: hidden; }
    /* `flex: none` es lo que hace que esto RECORTE en vez de encoger: el
       `<svg>` es hijo de un flex y por omisión se achica hasta caber, así
       que el lockup entero salía dibujado dentro de los 34px —«lo veo
       diminuto», Renzo, 2026-09-15—. Con la medida propia puesta, el ancho
       sobrante lo corta el `overflow` del enlace y queda el isotipo. La
       altura sube de 30 a 34 para igualar a la campana y al logo de la
       empresa, que es con quienes comparte renglón. */
    .topbar-brand .brand-logo { flex: none; height: 34px; }
    /* A LA DERECHA, SOLO LOS AVATARES (Renzo, 2026-09-15). El nombre de la
       empresa gastaba media barra —hasta 22 caracteres— y está repetido
       dentro del menú que abre ese mismo logo. Quedan la campana, la
       empresa y el compi, tres cosas redondas del mismo tamaño. */
    .user-menu-name { display: none; }
    .user-menu summary { gap: 0; padding: 5px; }
    /* EL DESPLEGABLE DE LA CAMPANITA A TODO LO ANCHO. Colgado del
       disparador con `right: 0` se salía 47px por la izquierda a 390px
       —la campana está a 77px del borde y la caja mide 360—, y la inicial
       y el arranque del título quedaban fuera de la ventana. Fijo contra
       la ventana, bajo la topbar, con el mismo margen a los dos lados. */
    .notices .notices-pop {
        position: fixed; left: 12px; right: 12px; width: auto;
        top: calc(var(--topbar) + 6px);
    }
    /* `clip` recorta también a los descendientes fijos, y en este ancho el
       sidebar ES fijo: sin esto el cajón sale recortado por su propio
       contenedor. */
    .shell { margin-top: var(--topbar); border-radius: 18px 18px 0 0; overflow: visible; }
    .sidebar {
        position: fixed; top: var(--topbar); left: 0; z-index: 90;
        width: min(280px, 84vw) !important;
        transform: translateX(-102%); transition: transform .22s ease;
        background: var(--card-bg);
    }
    /* El checkbox manda: sin JS, el cajón abre y cierra igual. */
    .drawer-toggle:checked ~ .shell .sidebar { transform: translateX(0); }
    .drawer-toggle:checked ~ .drawer-overlay {
        display: block; position: fixed; inset: var(--topbar) 0 0 0;
        z-index: 89; background: rgba(13, 11, 22, .45);
    }
    .sidebar .nav-label, .sidebar .nav-badge,
    .sidebar .nav-sublist, .sidebar .sidebar-label {
        display: revert !important;
    }
    .sidebar .nav-item { justify-content: flex-start !important; padding: 9px 12px !important; }
    /* El radio se pinta acá y no en `.shell` porque en este ancho el shell
       no recorta —tiene que dejar salir el cajón—, así que las esquinas
       cuadradas del contenido taparían las suyas. */
    .content { padding: 16px 14px; border-radius: 18px 18px 0 0; }
    /* En el celular el panel tapa la página entera, como el menú. */
    body.panel-open .side-panel {
        position: fixed; inset: var(--topbar) 0 0 0; z-index: 95;
        width: 100vw; height: auto; border-left: 0;
    }
    .side-panel-body { width: 100%; }
}

/* ── Accesibilidad y pantallas chicas ──────────────────────────────────── */

@media (max-width: 480px) {
    .auth-card { padding: 30px 22px 26px; border-radius: var(--radius); }
    .auth-title { font-size: var(--fs-page); }
}

@media (prefers-reduced-motion: reduce) {
    .orb, .auth-card, .auth-card > * { animation: none; }
    /* `!important` y no más especificidad: las animaciones del escritorio
       viven en selectores compuestos (`.home-orb.orb-cyan`) para poder pisar
       las de página completa, así que un `.home-orb` a secas acá pierde por
       especificidad AUNQUE venga después — y los cinco orbes seguían
       derivando con `prefers-reduced-motion` puesto. Enumerar los cinco
       compuestos arreglaría este caso y no el del sexto que llegue. */
    .home-orb { animation: none !important; }
    .sidebar { transition: none; }
    .side-panel { transition: none; }
    /* El personaje llega formado y quieto; sin respirar ni girar. */
    .compi *, .is-thinking .compi * { animation: none !important; }
    /* El panel llega puesto en vez de deslizándose. `drawer.js` no depende de
       `transitionend` para cerrar —tiene un plazo de respaldo— justo porque
       acá el evento no llega nunca. */
    dialog.drawer:modal { transition: none; transform: none; opacity: 1; }
    .drawer::backdrop { transition: none; }
    /* Y el sexto llegó: `.pw-rules li` y `.pw-ready` animan `rule-in` y no
       los alcanzaba NINGUNO de los selectores de arriba —el `li` es nieto de
       la tarjeta, y `animation` no se hereda del padre—, así que la
       contraseña se escribía con un deslizamiento por tecla, porque HTMX
       reemplaza ese fragmento en cada una. La lista no es el mecanismo: el
       `*` sí, y por eso ahora apaga las dos cosas y no solo las
       transiciones.

       `iteration-count` además de `duration`: los orbes son `infinite`, y una
       animación infinita de .01ms no se detiene, se repite miles de veces por
       segundo. Apagar el movimiento no puede costar más que dejarlo. */
    * {
        transition-duration: .01ms !important;
        animation-duration: .01ms !important;
        animation-iteration-count: 1 !important;
    }
}

/* ── El buscador ───────────────────────────────────────────────────────────
   WebKit les pone `appearance: searchfield` a los `input[type="search"]` y
   con ella IGNORA el padding declarado: en Safari el buscador del home salía
   bajo y el icono —posicionado para un campo alto— caía encima del
   placeholder. Va por selector de elemento, como la casilla: los cuatro
   buscadores del ERP declaran su propio padding y a todos les pasa lo mismo.
   ------------------------------------------------------------------------ */

input[type="search"] {
    appearance: none; -webkit-appearance: none;
}

/* ── La casilla ────────────────────────────────────────────────────────────
   `appearance: none` y control propio. `accent-color` solo TIÑE la del
   sistema operativo: en macOS sigue siendo un cuadrado con las esquinas de
   macOS, en Windows otra cosa, y en ninguna de las dos se parece al ERP.

   Va por SELECTOR DE ELEMENTO y no por clase. Con una clase, «todas las
   casillas» dura hasta que alguien escriba un `<input type="checkbox">` sin
   acordarse de ponerla, y nadie lo nota: la del sistema también se ve como
   una casilla. La única excepción viaja escrita en el propio selector.
   ------------------------------------------------------------------------ */

input[type="checkbox"]:not(.drawer-toggle):not(.help-toggle) {
    appearance: none; -webkit-appearance: none;
    flex: none;
    width: 20px; height: 20px; margin: 0;
    /* 1px desde el 2026-08-17 (Renzo). Era 1.5px, y con el borde además más
       claro el trazo grueso era lo que quedaba gritando. EL RADIO BAJÓ CON
       ELLA: los dos controles conviven en la misma tarjeta —«Activo» al lado
       de «Todas las condiciones»— y uno a 1px junto a otro a 1.5px se lee
       como que uno de los dos está en otro estado. */
    border: 1px solid var(--checkbox-border);
    /* CUADRADA con las esquinas redondeadas, no un círculo (Renzo,
       2026-08-14). Fue círculo hasta que la selección múltiple puso
       cincuenta casillas juntas en una tabla: en columna, el círculo se lee
       como un radio —«elige una»— y lo que hacen es «elige las que
       quieras». El radio del ERP sigue siendo redondo y ahora se distinguen
       de un vistazo, que antes no.

       6px sobre 20: bastante para que no sea una caja dura, poco para que
       no se confunda con el círculo. No usa `--radius-sm` (8px) porque ése
       es de una tarjeta pequeña y a esta escala ya redondea de más. */
    border-radius: 6px;
    background: var(--card-bg) center / 11px 8px no-repeat;
    cursor: pointer;
    transition: background-color .14s ease, border-color .14s ease;
}
/* El palomito va DIBUJADO. Con `appearance: none` el del sistema desaparece,
   y una casilla marcada que solo cambia de color no dice que está marcada:
   dice que es de otro color. El SVG va en línea —no hay petición— y el trazo
   es blanco sobre el morado de la marca, que da 4.5:1. */
input[type="checkbox"]:not(.drawer-toggle):not(.help-toggle):checked {
    background-color: var(--primary);
    border-color: var(--primary);
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 9'%3E%3Cpath d='M1.2 4.6 4.4 7.8 10.8 1.3' fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
input[type="checkbox"]:not(.drawer-toggle):not(.help-toggle):hover:not(:checked) {
    border-color: var(--primary);
}
/* ALGUNAS, ni todas ni ninguna: la casilla del encabezado de una tabla con
   parte de sus filas marcadas. No existía porque hasta ahora ninguna casilla
   mandaba sobre otras.

   Una RAYA y no un palomito, que es lo universal y además lo único honesto:
   con el palomito, «tres de cincuenta» y «cincuenta de cincuenta» se ven
   igual, y quien apriete «Archivar» creyendo lo segundo archiva lo primero
   —o al revés—. El estado lo pone el JavaScript (`indeterminate` no es un
   atributo del marcado, es una propiedad del elemento), así que sin script
   la casilla solo tiene dos estados y ninguno miente. */
input[type="checkbox"]:not(.drawer-toggle):not(.help-toggle):indeterminate {
    background-color: var(--primary);
    border-color: var(--primary);
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 9'%3E%3Cpath d='M2 4.5h8' fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E");
}

/* El RADIO, con el mismo trato que la casilla y por la misma razón que estaba
   escrita arriba: el del sistema es azul en macOS y cuadrado-gris en Windows,
   y al lado de una interfaz morada se lee como un control de otro programa.

   Se agrega en F2-e porque hasta acá no había ninguno: el primero es el de
   «cuál opción es la correcta» del examen, y ahí la diferencia saltaba —cuatro
   radios azules del sistema debajo de campos con foco morado—.

   El punto va DIBUJADO en el borde y no con `background-image`: un círculo
   sólido se dibuja con `box-shadow` hacia adentro y así no hay que codificar
   un SVG para una sola forma. */
input[type="radio"] {
    appearance: none; -webkit-appearance: none;
    flex: none;
    width: 20px; height: 20px; margin: 0;
    border: 1px solid var(--checkbox-border);
    border-radius: 50%;
    background: var(--card-bg);
    cursor: pointer;
    transition: border-color .14s ease, box-shadow .14s ease;
}
input[type="radio"]:checked {
    border-color: var(--primary);
    box-shadow: inset 0 0 0 5px var(--primary);
}
input[type="radio"]:hover:not(:checked) { border-color: var(--primary); }
input[type="radio"]:focus-visible {
    outline: 2px solid var(--primary); outline-offset: 2px;
}
input[type="checkbox"]:not(.drawer-toggle):not(.help-toggle):focus-visible {
    outline: 2px solid var(--primary); outline-offset: 2px;
}
input[type="checkbox"]:not(.drawer-toggle):not(.help-toggle):disabled {
    cursor: not-allowed; opacity: .5;
}

/* La pastilla: casilla y rótulo adentro de un mismo contorno. El objetivo de
   clic pasa a ser la frase entera y no un cuadradito de 20px, que es la
   diferencia entre marcarla al primer intento y al segundo.

   `width: fit-content` porque vive dentro de `.form-grid`, cuyas columnas
   miden 260px: sin eso la pastilla se estira hasta el borde y deja el rótulo
   flotando en medio de la nada. */
/* Los DOS selectores hacen falta. Dentro de `ui/_field.html` la pastilla es
   un `<label>` anidado en un `div.field`, y `.field label` —que existe desde
   P0 y fija `display: block`— le ganaba por especificidad: (0,1,1) contra
   (0,1,0). La pastilla salía en bloque, estirada a lo ancho de la columna y
   con la casilla encima del rótulo. El marcado estaba bien y la regla
   escrita: lo que fallaba era la cascada, que ninguna prueba de texto ve. */
.field .field-check,
.field-check {
    display: inline-flex; align-items: center; gap: 10px;
    width: fit-content; max-width: 100%;
    margin-bottom: 17px; padding: 8px 16px 8px 9px;
    border: 1px solid var(--border); border-radius: 999px;
    background: var(--card-bg);
    cursor: pointer;
    transition: border-color .14s ease, box-shadow .14s ease;
}
/* Dentro de `.field` el margen lo pone el envoltorio: `_field.html` anida la
   pastilla dentro de un `div.field`, y con los dos márgenes la casilla
   quedaba al doble de separación que cualquier otro campo. En terceros las
   dos clases van en la MISMA etiqueta, así que ahí no se suman. */
.field > .field-check { margin-bottom: 0; }
/* Varias pastillas del mismo grupo van juntas y se acomodan solas. Puestas
   en `.form-grid` caía una por columna: «Cliente» a la izquierda,
   «Proveedor» a medio metro y «Aliado» en el renglón de abajo, que es tres
   opciones de lo mismo leídas como tres campos distintos. */
.check-row {
    display: flex; flex-wrap: wrap; gap: 10px;
    margin-bottom: 17px;
}
.check-row .field-check { margin-bottom: 0; }
/* ── Una casilla como celda de la grilla ─────────────────────────────────
   Cuando los campos los declara una EMPRESA, el orden lo eligió ella y no se
   puede reagrupar: `.check-row` junta las casillas de un mismo grupo, pero acá
   vienen intercaladas —caja, país, calidad, exclusiva, código— y moverlas
   sería reescribir una decisión de quien administra.

   Así que la celda se arregla en su sitio, con dos cosas:

   `justify-content: flex-end` — la pastilla se alinea con los CONTROLES de al
   lado y no con sus rótulos. Sin esto flota a la altura de la palabra «País de
   compra», con el control de al lado un renglón más abajo, y la fila entera se
   lee torcida. Es lo mismo que `.field-bottom` hace a mano; acá sale solo
   porque `_field.html` marca la celda.

   `width: 100%` — la pastilla llena su columna. Con `fit-content` medía lo que
   midiera su texto, así que dos casillas seguidas salían de dos anchos
   distintos y con medio metro de aire al lado: eso es lo que se lee como
   campos sueltos en un patio y no como una grilla. */
.form-grid > .field-toggle {
    display: flex; flex-direction: column; justify-content: flex-end;
}
.form-grid > .field-toggle > .field-check { width: 100%; }
.field-check:hover { border-color: var(--border-strong); }
/* El foco se pinta en la PASTILLA y no en la casilla: el contorno de 20px
   dentro de otro contorno se lee como un error de dibujo. */
.field-check:focus-within {
    outline: 2px solid var(--primary); outline-offset: 2px;
}
.field-check:focus-within input[type="checkbox"] { outline: none; }
/* El rótulo NO hereda de `.field label`, que es en bloque y va arriba. */
.field-check span, .field-check label {
    display: inline; margin: 0;
    font-size: var(--fs-small); font-weight: 500; color: var(--text);
    text-transform: none; letter-spacing: normal;
}

/* ── El selector de color ──────────────────────────────────────────────────
   Por el mismo argumento que la casilla y la flecha del `select`: el control
   del sistema es un rectángulo gris en Windows, un botón redondo con borde
   azul en macOS y una barra en Linux. Estaba SIN UNA SOLA REGLA en toda la
   hoja, así que los ocho sitios que lo usan —los ajustes del tema, el
   customizer, el bloque de aviso, el examen, las láminas de una pantalla—
   salían cada uno con la cara de su sistema operativo, dentro de un
   formulario donde todo lo demás lo dibujamos nosotros.

   Va al ELEMENTO y no a una clase: eran ocho llamadas escritas a mano en
   cinco plantillas, y una clase obliga a acordarse de ponerla en la novena.
   `.mail-swatch-free` sigue ganando donde ya decidía su tamaño —es una clase,
   le gana por especificidad—, así que la fila de colores del editor de correo
   no cambia.

   Los tres pseudoelementos hacen falta y no son intercambiables: el color
   pintado vive dentro de un nodo del navegador, y sin quitarle su borde queda
   una línea gris de 1px entre nuestro borde y el color. Firefox lo llama de
   otro modo que WebKit; lo que no exista se ignora solo. */
input[type="color"] {
    appearance: none; -webkit-appearance: none;
    width: 56px; height: 40px; padding: 4px;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-field);
    background-color: var(--card-bg);
    cursor: pointer;
    transition: border-color .14s ease;
}
input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; }
input[type="color"]::-webkit-color-swatch {
    border: 0; border-radius: 6px;
}
input[type="color"]::-moz-color-swatch {
    border: 0; border-radius: 6px;
}
input[type="color"]:hover { border-color: var(--primary); }
input[type="color"]:focus-visible {
    outline: 2px solid var(--primary); outline-offset: 2px;
}

/* ── La flecha de un `select` ──────────────────────────────────────────────
   Va DIBUJADA, por lo mismo que la casilla: la del sistema es azul en macOS,
   un triángulo en Windows, y en las dos vive pegada al borde derecho del
   control — a medio palmo del texto en cuanto el `select` es ancho.

   Va en TODOS y no en `.field select`: los del ERP viven en tres sitios —el
   formulario, la barra de filtros y la tabla de roles— y con la regla en uno
   solo, los otros dos seguían con la del sistema al lado de los nuestros. La
   nuestra es además más angosta, que es lo que le quita ancho a la columna de
   acciones de la tabla de roles.

   `padding-right` le deja sitio: sin eso, una opción larga pasa por debajo.
   ------------------------------------------------------------------------ */

select {
    appearance: none; -webkit-appearance: none;
    /* La CAJA también va acá, y no una vez por contexto. Estaba definida tres
       veces —formulario, filtros, ajustes— y los `select` de la tabla de
       roles no caían en ninguna: salían con la caja por omisión del navegador
       y nuestra flecha pegada encima, dos dedos debajo del «Alcance» de la
       misma pantalla, que sí se veía bien. */
    padding: 9px 34px 9px 12px;
    font-family: inherit; font-size: var(--fs-body); color: var(--text);
    background-color: var(--card-bg);
    border: 1px solid var(--border-strong); border-radius: var(--radius-field);
    outline: none;
    cursor: pointer;
    transition: border-color .16s, box-shadow .16s;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%235d5873' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 12px center;
    background-size: 10px 6px;
}

select:focus-visible {
    border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-soft);
}

/* El select MÚLTIPLE no es un desplegable: la flecha de arriba encima de una
   caja de varias filas anuncia un menú que no existe, y el relleno del
   desplegable deja las opciones descolgadas del borde. Sin flecha, con un
   alto que muestra ocho filas y cada opción con su aire — lo estrenan los
   canales y los terceros del alta de listas de precios. `background-image`
   explícito y no el atajo `background`, por la guarda de la flecha. */
select[multiple] {
    background-image: none;
    padding: 6px; height: 172px; cursor: default;
}
select[multiple] option { padding: 7px 10px; border-radius: 8px; }
.field select[multiple] { width: 100%; }
/* La casilla que FILTRA un select múltiple, encima de él: sin este aire las
   dos cajas se tocan y se leen como un solo control roto. El ancho ya lo da
   la regla de `input[type="text"]` dentro de `.field`. */
.field .field-filter { margin-bottom: 6px; }

/* ── El buscador de variantes ──────────────────────────────────────────────
   El panel que reemplaza al `datalist` nativo (que sale AL LADO y no se
   puede vestir): DEBAJO del campo, con la foto del producto, el título y el
   SKU en mono. Lo estrena la carga de precios; el día que la captura del
   pedido lo quiera, estas clases ya no se llaman `price-*`.

   `finder-*` y NO `picker-*`: el selector de iconos ya se llama así, y la
   guarda de propiedades duplicadas cazó el choque antes de que las dos
   pantallas se vistieran mutuamente. */
.variant-picker { position: relative; }
/* Dentro de una `inline-row` el que se estira es EL ENVOLTORIO y no la
   casilla: el panel se posiciona contra él, y con el envoltorio al ancho de su
   contenido los resultados salían de 0 píxeles. */
.inline-row .variant-picker { flex: 1 1 220px; min-width: 0; }
.inline-row .variant-picker input { width: 100%; }
/* El producto de una celda de tabla: miniatura y título en una línea. Reusa
   `finder-thumb` para que lo que se eligió en el panel se vea igual en la
   lista — dos tamaños de la misma foto se leen como dos productos. */
.cell-prod {
    display: flex; align-items: center; gap: 10px; min-width: 0;
}
.finder-panel {
    position: absolute; top: calc(100% + 6px); left: 0; right: 0; z-index: 30;
    background: var(--card-bg);
    border: 1px solid var(--border-strong); border-radius: var(--radius-field);
    box-shadow: 0 12px 32px rgba(0, 0, 0, .12);
    max-height: 340px; overflow-y: auto;
    padding: 6px;
}
/* Abierto hacia ARRIBA. Un panel absoluto no le suma alto al documento: con
   el campo cerca del fondo —el buscador de la colección vive DEBAJO de la
   ficha—, lo que sobresale queda bajo el borde de la página y no hay scroll
   que lo alcance (visto en /productos/colecciones/, 2026-08-24). El JS mide
   y voltea; esta clase solo dice hacia dónde. */
.finder-panel-up {
    top: auto; bottom: calc(100% + 6px);
    box-shadow: 0 -12px 32px rgba(0, 0, 0, .12);
}
/* La fila es un `<button type="button">`: el clic elige, el TAB no entra —
   la lista se recorre con las flechas, no robándole tabulaciones al precio
   que viene después. */
.finder-row {
    display: flex; align-items: center; gap: 12px;
    width: 100%; padding: 8px 10px; border: none; border-radius: 10px;
    background: transparent; cursor: pointer; text-align: left;
    font-family: inherit;
}
.finder-row-active { background: var(--primary-soft); }
.finder-thumb {
    flex: none; width: 38px; height: 38px; border-radius: 9px;
    object-fit: cover; background: var(--bg);
    border: 1px solid var(--border);
}
/* Sin foto, la INICIAL sobre el tinte de marca — el mismo criterio del
   `banner-avatar`: un cuadro gris vacío se lee como algo que falta. */
.finder-thumb-letter {
    display: grid; place-items: center;
    background: var(--primary); color: #fff; border-color: var(--primary);
    font-size: var(--fs-body); font-weight: 600; text-transform: uppercase;
}
.finder-info { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.finder-title {
    font-size: var(--fs-small); font-weight: 600; color: var(--text);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.finder-sku { font-size: var(--fs-label); color: var(--text-light); }
/* El resto que NO se pinta, dicho: un panel de ocho sobre mil setecientas
   variantes que se calla el recorte se lee como «no está». */
.finder-more {
    margin: 4px 0 2px; padding: 6px 10px 2px;
    border-top: 1px solid var(--border);
    font-size: var(--fs-label); color: var(--text-light);
}

/* ── El desplegable de una lista larga: la flecha adentro ─────────────────
   La flecha que el `<datalist>` nativo pintaba a la derecha, dibujada por
   la casa: sin ella la casilla se lee como un campo de texto y nadie va a
   hacerle clic esperando una lista. Mismo criterio —y misma trampa— que la
   lupa del buscador de artículos: `pointer-events` apagados o la flecha se
   come el clic que debía enfocar la casilla. El `mousedown` que la abre lo
   cuelga `combo.js`, que sí la escucha. */
.combo-caret {
    position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
    width: 18px; height: 18px; color: var(--text-faint);
    pointer-events: auto; cursor: pointer;
}
.combo input { width: 100%; padding-right: 34px; }
/* EL NOMBRE de lo elegido, al lado del código. La casilla guarda «240805» y
   eso es lo que mostraba: veintiocho números de seis cifras en la
   Configuración contable, y para saber qué cuenta era cada uno había que
   abrir el desplegable. El nombre va ENCIMA de la casilla y no dentro de su
   valor —lo que viaja en el POST sigue siendo el código—; `left` lo pone
   `combo.js` midiendo el código, y se esconde mientras se escribe. */
.combo-name {
    position: absolute; top: 50%; right: 34px; transform: translateY(-50%);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    color: var(--text-light); pointer-events: none;
}
.combo-name[hidden] { display: none; }

/* ── El campo de un artículo: la lupa adentro ──────────────────────────────
   La lupa DENTRO de la casilla y no un rótulo al lado que diga «buscar»: lo
   que tiene que anunciar el campo es que no hay que saberse el SKU de
   memoria, y eso lo dice el glifo en el sitio donde va a caer el cursor.

   Absoluta contra `.variant-picker`, que ya era `position: relative` porque
   el panel se posiciona contra él. `pointer-events: none` o la lupa se come
   el clic que debería enfocar la casilla — el defecto clásico de este
   patrón. */
.sku-field-glyph {
    position: absolute; left: 11px; top: 50%; transform: translateY(-50%);
    width: 16px; height: 16px; color: var(--text-faint);
    pointer-events: none;
}
.sku-field input[type="text"] { width: 100%; padding-left: 32px; }

/* ── Las filas de movimiento de producción ─────────────────────────────────
   La celda del código es una FILA y no un bloque: el buscador se estira y el
   rótulo de la salida principal se le pega al lado, que es donde lo lee
   quien está tecleando ahí. */
.mov-c-code { display: flex; align-items: center; gap: var(--sp-2); }
.mov-c-code .variant-picker { flex: 1 1 auto; min-width: 0; }
/* La papelera no manda el ancho de la tabla: `1%` es el truco de siempre
   para una columna que mide lo que mide su contenido. */
.mov-c-x { width: 1%; text-align: right; white-space: nowrap; }

/* ── El editor del borrador ────────────────────────────────────────────────
   La piel del `order_editor` de tiendapp sobre los componentes de acá: los
   buscadores reusan `finder-*`, la grilla reusa `ficha-cols`, y esto es lo
   que el editor trae de propio — las celdas editables de la tabla de
   líneas, el panel de pago y la pila de campos laterales. */
.editor-cell {
    width: 100%; box-sizing: border-box; padding: 7px 9px;
    border: 1px solid var(--border-strong); border-radius: 8px;
    font-size: var(--fs-small); font-family: inherit; text-align: right;
    color: var(--text); background: var(--card-bg);
}
.editor-cell:focus {
    outline: none; border-color: var(--primary);
    box-shadow: 0 0 0 3px var(--primary-soft);
}
/* Angostas y pegadas a su columna: una celda editable al ancho completo de
   una columna de tabla se lee como un campo de formulario perdido. */
.editor-lines td.num .editor-cell { max-width: 110px; margin-left: auto; }
/* El aire horizontal que las tablas del sistema no llevan: acá CUATRO
   columnas numéricas viven pegadas, y sin esto los encabezados se leen
   «CANT.PRECIODCTO %» — la pasada visual del editor lo leyó tal cual. Por
   selector propio: la regla general de `.table` no se toca. */
.editor-lines thead th, .editor-lines tbody td {
    padding-left: 6px; padding-right: 6px;
}
/* Mínimos POR CAMPO y no por gancho `js-*` — esos son markers y la guarda
   exige que sigan sin regla. El precio es el ancho que manda: seis cifras
   y centavos no caben en lo que la columna del producto deje. */
.editor-lines input[name="line_quantity"],
.editor-lines input[name="line_discount_pct"] { min-width: 64px; }
.editor-lines input[name="line_unit_price"] { min-width: 108px; }
.editor-prod {
    display: flex; align-items: center; gap: 10px; min-width: 0;
    font-weight: 600; color: var(--text);
}
/* La celda del producto se queda con el sobrante: sin esto, las cuatro
   columnas numéricas se reparten el ancho y el título termina partido. */
.editor-c-prod { width: 100%; min-width: 220px; }
.editor-vt { color: var(--text-light); font-weight: 500; }
/* «Sale de» vive DEBAJO del título, dentro de la celda del producto, y no
   en columna propia: como columna, el editor pasaba de 757 a 908px de
   tabla dentro de una tarjeta de 703, y a 1440px —la resolución de
   escritorio más común— el total de la línea y el botón de quitar quedaban
   detrás de un desplazamiento horizontal. `.table-wrap` está para el
   celular, no para que el escritorio pierda la columna de plata. Es el
   mismo patrón que `.editor-vt` y `.cell-sub` ya usan en esta misma celda
   para la variante. */
.editor-prod-name {
    display: flex; flex-direction: column; align-items: flex-start;
    gap: 5px; min-width: 0;
}
.editor-origin {
    display: inline-flex; align-items: center; gap: 6px;
    font-size: var(--fs-meta); font-weight: 500; color: var(--text-light);
}
/* El desplegable, a la medida de un subtítulo. La caja base del `select`
   es de formulario —13px y 9px de alto— y debajo del título se lee como
   otro campo del formulario en vez de como el dato de apoyo que es. Se
   ajusta el TAMAÑO, no la caja: el borde y la flecha siguen siendo los de
   la hoja, y los 30px de la derecha son los que la flecha necesita para
   que una opción larga no se le meta por debajo. */
.editor-origin select { padding: 4px 30px 4px 9px; font-size: var(--fs-small); }
.editor-c-total { font-weight: 700; color: var(--text); }
.editor-c-x { width: 30px; text-align: center; }
.editor-x {
    border: none; background: none; color: var(--text-light);
    font-size: var(--fs-page); line-height: 1; cursor: pointer;
    padding: 0 4px; border-radius: 6px;
}
.editor-x:hover { color: var(--danger-text); background: var(--danger-bg); }
/* El precio en el panel de resultados, a la derecha y en tinta de marca —
   es EL dato por el que este buscador existe. */
.editor-hit-price {
    margin-left: auto; color: var(--primary); font-weight: 700;
    white-space: nowrap;
}
/* El precio de lista que quedó ATRÁS, tachado y apagado al lado del
   cobrado: un descuento que no se ve aplicado se lee como un descuento
   que no corrió — Renzo vio 41.895 sin saber que la base era 52.369. */
.editor-hit-was {
    color: var(--text-light); font-weight: 500;
    font-size: var(--fs-label);
}

.editor-pay { display: flex; flex-direction: column; gap: 2px; }
.editor-pay-row {
    display: flex; justify-content: space-between; align-items: center;
    padding: 9px 2px; font-size: var(--fs-small);
}
.editor-pay-row span { color: var(--text-light); }
.editor-pay-row b { font-weight: 600; color: var(--text); }
.editor-pay-muted b, .editor-pay-muted span {
    color: var(--text-light); font-size: var(--fs-label);
}
.editor-pay-total {
    border-top: 1px solid var(--border); margin-top: 4px; padding-top: 13px;
}
.editor-pay-total span {
    color: var(--text); font-weight: 700; font-size: var(--fs-body);
}
.editor-pay-total b { font-size: var(--fs-section); font-weight: 800; }
.editor-ship {
    color: var(--text-light); display: inline-flex; align-items: center;
    gap: 4px;
}
.editor-ship .editor-cell { max-width: 100px; }

/* La pila de campos de las tarjetas laterales: mismo `.field` del sistema,
   apilado con su aire. */
.editor-fields { display: flex; flex-direction: column; gap: 11px; }
.editor-fields .field { margin: 0; }

/* La fila de un beneficio del plan (F8): clave y valor LADO A LADO con su
   quitar al final — pedido por Renzo con la fila de la referencia
   enfrente. Flex y no `form-grid-two`: la fila es 1fr + 1fr + auto, y el
   botón de quitar no puede quedarse con media columna.

   `.benefit-row input` por CONTEXTO y con el flex explícito: los anchos
   por elemento del formulario (`.field input[...]`) traen `width: 100%`,
   y sin esta regla cada casilla ocupa el renglón entero — que es
   exactamente cómo la fila salió apilada la primera vez. */
.benefit-row { display: flex; gap: var(--sp-3); align-items: center; margin-bottom: var(--sp-3); }
.benefit-row input { flex: 1; width: auto; min-width: 0; }
.benefit-row .btn { flex-shrink: 0; }

/* ── Filtros ───────────────────────────────────────────────────────────────
   Un `<form>` GET. La URL queda compartible, así que una bitácora filtrada
   se pega en un chat y el otro ve lo mismo.
   ------------------------------------------------------------------------ */

/* La barra de filtros es una TARJETA, como en la referencia.

   Antes era una fila suelta sobre el fondo de la página, con el rótulo de
   cada filtro encima en versalitas: dos alturas para decir una palabra, y sin
   caja que dijera dónde empieza y dónde termina el filtro. Ahora el rótulo va
   adentro del control y la barra es una tarjeta — los valores son los de
   `.filters` del dashboard de tiendapp. */
/* Solo la DISPOSICIÓN. La caja —fondo, borde, sombra y relleno— la pone
   `.filters-card`, que es la tarjeta que ahora la envuelve en las once listas.
   Cuando la barra traía las dos cosas, envolverla dejó una caja dentro de otra
   caja: dos bordes y dos sombras con cuatro píxeles de aire en medio. */
.filters {
    display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
}

/* El buscador ocupa su propio renglón. `flex: 1 1 100%` fuerza el salto, que
   es lo que separa «buscar» —una frase que se escribe— de «acotar», que son
   cuatro listas cerradas. En la referencia se ve igual y no es casualidad. */
.filters-search { flex: 1 1 100%; }
/* El campo de fichas en la barra (tags de pedidos, 2026-09-14): crece como
   el buscador pero comparte renglón con los desplegables, y sin el margen de
   pie que trae de los formularios. Su buscador interno no se pinta como las
   casillas de la barra: la caja entera es el control. */
.filters-tokens { flex: 1 1 260px; min-width: 0; }
.filters-tokens .token-field { margin-bottom: 0; }
.filters-tokens .tokens { min-height: 0; padding-block: 3px; border-color: var(--border-strong); }
.filters .filters-tokens .token-input { padding: 4px 0; border: 0; box-shadow: none; background: transparent; }

.filters select, .filters input {
    padding: var(--sp-2) var(--sp-3);
    font: inherit; font-size: var(--fs-small); color: var(--text);
    background-color: var(--card-bg);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-field);
    outline: none;
    transition: border-color .16s, box-shadow .16s;
}
.filters select { min-width: 145px; cursor: pointer; }
.filters select:focus, .filters input:focus {
    border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-soft);
}
.filters-submit { flex-shrink: 0; }

/* ── Drawer ────────────────────────────────────────────────────────────────
   `<dialog open>` se pinta en el flujo aunque Alpine no cargue; con Alpine
   se asciende a modal y gana trampa de foco, Esc y capa de fondo.
   ------------------------------------------------------------------------ */

.drawer {
    /* Lateral y de alto completo: es un cajón, no un globo. Pegado a la
       derecha con `margin: auto` a la izquierda, que es como se empuja un
       diálogo modal sin sacarlo del centrado nativo. */
    width: min(560px, 100%); max-width: 100%;
    height: 100dvh; max-height: 100dvh;
    margin: 0 0 0 auto;
    padding: 0; border: 0;
    border-radius: var(--radius-lg) 0 0 var(--radius-lg);
    background: var(--card-bg); color: var(--text);
    box-shadow: var(--shadow-lg);
    display: flex; flex-direction: column;
    /* Nace AFUERA y `drawer.js` lo trae con `.is-open` al cuadro siguiente.
       Es lo que hace que se vea entrar en vez de aparecer.

       Sin script se queda así —desplazado y transparente— y por eso el
       `:not([open])` de abajo: el `<dialog open>` que el servidor pinta se ve
       igual que siempre, y solo el que ya se ascendió a modal empieza afuera.
       Un `@starting-style` sería lo moderno, pero Safari 17 —la versión de
       los iPad del mostrador— lo ignora y el panel entraría de golpe ahí. */
    transition: transform .22s ease-out, opacity .22s ease-out;
}
dialog.drawer:modal { transform: translateX(100%); opacity: 0; }
dialog.drawer:modal.is-open { transform: translateX(0); opacity: 1; }
/* La capa de fondo entra con él. `::backdrop` no hereda de nadie, así que
   repite su propia transición. */
.drawer::backdrop { opacity: 0; transition: opacity .22s ease-out; }
.drawer.is-open::backdrop { opacity: 1; }
.drawer-body { flex: 1; overflow-y: auto; }
/* No hace falta un `.drawer[open] { display: block }`: `dialog:not([open])`
   solo aplica cuando NO está abierto. Y sí hacía daño — con (0,2,0) le ganaba
   a `.drawer` y anulaba el `display: flex`, así que el encabezado no quedaba
   fijo y el cuerpo no hacía scroll propio. */
.drawer::backdrop { background: rgba(13, 11, 22, .45); }
.drawer-head {
    flex: none;
    display: flex; align-items: center; justify-content: space-between;
    gap: 12px; padding: 16px 20px;
    background: #ffffff; color: #000000;
    border-radius: var(--radius-lg) 0 0 0;
    border-bottom: solid 1px #bfbfbf;
}
.drawer-head h2 { margin: 0; font-size: var(--fs-section); font-weight: 600; }
/* El encabezado del drawer pasó a fondo BLANCO, y `.icon-btn-dark` es la
   clase de un icono sobre fondo oscuro: la «×» quedaba blanca sobre blanco,
   invisible pero clicable. Se acota al drawer en vez de cambiar la clase
   —la usa también la barra superior, que sigue siendo oscura— y así ninguna
   de las dos superficies decide por la otra. */
.drawer-head .icon-btn-dark { color: var(--text-light); }
.drawer-head .icon-btn-dark:hover { color: var(--text); background: var(--bg); }
.icon-btn-dark { color: rgba(255, 255, 255, .8); text-decoration: none; }
.icon-btn-dark:hover { text-decoration: none; }
.drawer-body { padding: 18px 20px 22px; }
.drawer-meta {
    display: grid; grid-template-columns: auto 1fr; gap: 6px 16px;
    margin: 0 0 18px; font-size: var(--fs-body);
}
.drawer-meta dt { color: var(--text-faint); font-weight: 600; }
.drawer-meta dd { margin: 0; }
.drawer-note { margin: 0; color: var(--text-light); font-size: var(--fs-body); }
/* La lista de formularios del cohete: una fila por formulario, asignado o no.
   Es una lista de DECISIÓN, no de lectura, así que cada fila lleva su acción a
   la derecha y el estado a la izquierda — y la asignada se distingue por el
   fondo, no solo por el rótulo del botón: con veinte filas, leer «Quitar» en
   cada una para saber cuáles tiene es trabajo que el color hace de un vistazo. */
.forms-list { list-style: none; margin: 0; padding: 0; }
.forms-row {
    display: flex; align-items: center; gap: var(--sp-3);
    padding: 10px var(--sp-3); border: 1px solid var(--border);
    border-radius: var(--radius-sm); margin-bottom: 8px;
}
.forms-row.is-assigned { background: var(--bg); border-color: var(--border-strong); }
/* `min-width: 0` para que el nombre largo se acorte en vez de empujar el botón
   fuera del diálogo, que a 390px ocupa todo el ancho. */
.forms-row-main { flex: 1; min-width: 0; }
.forms-row-name {
    display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
    margin: 0; font-weight: 600; font-size: var(--fs-body);
}
.forms-row-meta {
    margin: 2px 0 0; color: var(--text-light); font-size: var(--fs-small);
}
.forms-row-action { flex: none; margin: 0; }

.diff { white-space: normal; }
.diff td { overflow-wrap: anywhere; }
.diff .diff-before { color: var(--danger-text); text-decoration: line-through; }
.diff .diff-after  { color: var(--ok-text); font-weight: 500; }

@media (max-width: 767px) {
    .drawer { width: 100%; border-radius: 0; }
    .drawer-head { border-radius: 0; }
    .filters select, .filters input { width: 100%; }
}

/* ── Casillas de formulario del ERP ───────────────────────────────────────
   `.field` ya existe para las pantallas de autenticación; acá se agregan los
   controles que aquellas no usan y el estado degradado.
   ------------------------------------------------------------------------ */

/* La APARIENCIA de estos controles NO depende de `.field`, y ese es el
   arreglo. Estaba toda ahí adentro, así que un `<textarea>` o un
   `input[type=number]` fuera de un `.field` —los editores de bloque, que usan
   `.block-edit`— salía con el borde crudo del navegador al lado de campos bien
   pintados. Se veía como un formulario a medio hacer, y lo era: la mitad de
   los controles no tenía estilo.

   Solo el ANCHO se queda en `.field`, y por la razón que ya estaba escrita
   arriba: adentro de un campo el control llena su columna, pero en una fila es
   el `flex` quien decide y un `width: 100%` ahí lo empuja fuera. */
/* `select` NO está en este grupo y `number` tampoco: los dos ya se estilan a
   nivel de elemento más arriba —el `select` con su flecha y sus 34px a la
   derecha, el `number` en el grupo de texto—. Ponerlos acá los pisaría con un
   padding sin sitio para el caret, que es el defecto que la regla de arriba
   vino a cerrar. Hay un test que lo vigila. */
textarea,
input[type="date"],
input[type="search"],
input[type="url"],
input[type="tel"] {
    padding: 11px 13px;
    font-family: inherit; font-size: var(--fs-body); color: var(--text);
    /* `background-color` y NO el atajo `background`: el atajo resetea
       `background-image` a `none`, y esta regla es más específica que la que
       le dibuja la flecha al `select`. Se la borraba. */
    background-color: #fff;
    border: 1px solid var(--border-strong); border-radius: var(--radius-field);
    outline: none;
    transition: border-color .16s, box-shadow .16s;
}
.field select, .field textarea, .field input[type="date"],
.field input[type="search"], .field input[type="number"],
.field input[type="url"], .field input[type="tel"] { width: 100%; }
textarea:focus, input[type="date"]:focus,
input[type="search"]:focus,
input[type="url"]:focus, input[type="tel"]:focus {
    border-color: var(--primary); box-shadow: 0 0 0 3.5px var(--primary-soft);
}
textarea { resize: vertical; min-height: 76px; }

/* En una pantalla TÁCTIL ningún campo baja de `1rem`.

   iOS acerca la página al tocar un campo de menos de 16px —para que se lea
   lo que uno escribe— y no la vuelve a alejar: la pantalla queda más ancha
   que el teléfono y hay que pellizcar para volver. Renzo lo vio el
   2026-09-03 en la pantalla del PIN de un integrante, con el campo a 13px.

   `pointer: coarse` y no un ancho: el iPad también acerca, y un portátil
   angosto no. `1rem` y no un paso de la escala: no es un tamaño que se
   eligió, es el umbral del navegador, y por eso se escribe como lo que es.

   `!important` a propósito, como en el bloque de `prefers-reduced-motion`:
   hay once reglas que le ponen tamaño a un campo por su contexto —los
   filtros, el buscador, los ajustes— y las once tienen más especificidad
   que el selector del control. Repetirlas acá sería una lista que se queda
   atrás con el siguiente campo que alguien escriba; esto es una exigencia
   sobre TODOS, y así se escribe una exigencia. */
@media (pointer: coarse) {
    input[type="email"], input[type="text"], input[type="number"],
    input[type="password"], input[type="datetime-local"], input:not([type]),
    input[type="date"], input[type="search"], input[type="url"],
    input[type="tel"], select, textarea {
        font-size: max(var(--fs-body), 1rem) !important;
    }
}

/* Ámbar y no rojo: no es un error de quien está escribiendo, es un dato
   viejo que hay que reemplazar. El rojo acusaría a la persona equivocada. */
.field-degraded input, .field-degraded select, .field-degraded textarea {
    border-color: var(--warn-text);
}
.field-warn {
    margin: 7px 0 0; padding: 8px 10px;
    background: var(--warn-bg); color: var(--warn-text);
    border-radius: var(--radius); font-size: var(--fs-small); line-height: 1.5;
}
.field-warn code {
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: var(--fs-small); overflow-wrap: anywhere;
}

.form-section { margin-bottom: 22px; }
.form-section-title {
    margin: 0 0 12px; padding-bottom: 7px;
    font-size: var(--fs-meta); font-weight: 700; letter-spacing: .06em;
    text-transform: uppercase; color: var(--text-faint);
    border-bottom: 1px solid var(--border);
}
/* El tope de 420px es lo que impide que a 1440px una columna mida 600 y el
   `select` de «Tipo» —tres opciones cortas— se estire hasta allá, dejando la
   flecha a medio palmo del texto. También le pone un largo de línea legible
   a los campos de texto: nadie escribe un teléfono en 600px. */
/* Las columnas LLENAN la tarjeta, y el tope se mueve al CAMPO.

   Estaba en la columna —`minmax(260px, 420px)`— y eso dejaba una franja vacía
   a la derecha de cada campo ancho: el título y la descripción de un producto
   ocupaban media tarjeta y el resto era aire. Se veía como un formulario a
   medio armar.
   
   Pero quitarlo del todo tampoco: a 1440px una columna suelta mide 600, y un
   `select` de tres opciones cortas estirado hasta allá deja la flecha a medio
   palmo del texto — y nadie escribe un teléfono en 600px. Así que el tope
   sigue, sobre lo que de verdad lo necesita: el campo NORMAL. El que se
   declara ancho lo dice, y llena. */
/* 320px y no 240 en el `minmax`: con la tarjeta llenando el ancho, `auto-fit`
   decide cuántas columnas caben, y en 240 salían CUATRO de unos 250px a
   1440px — el nombre de un producto no se lee en 250px. En 320 salen tres de
   unos 350, que es el ancho que ya tenían las dos de antes. Se aprovecha el
   ancho con más columnas, no con campos más largos.

   El `min(…, 100%)` no es adorno y se pagó mirándolo: un `minmax` con un
   mínimo FIJO no baja de ese mínimo, así que a 355px de viewport la grilla
   medía 250 y su columna 320, y los campos salían 49px POR FUERA de la
   tarjeta. Con `min(320px, 100%)` el mínimo cede cuando el contenedor es más
   angosto que él, que es justo el caso del teléfono. */
.form-grid {
    display: grid; gap: 0 var(--sp-4);
    grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr));
}
/* La misma grilla en UNA columna, para las fichas de pocos controles cortos —el
   tipo de producto, la opción—: en varias columnas quedan pares desparejos, con
   media fila en blanco al lado del último campo, y el ojo tiene que decidir por
   dónde sigue leyendo.

   Va DESPUÉS de `.form-grid` y no antes: las dos son una clase sola, así que
   entre ellas no decide la especificidad sino el orden. Escrita arriba, la
   regla estaba puesta y la ficha seguía saliendo en tres columnas. */
.form-grid-one { grid-template-columns: minmax(0, 1fr); }
/* Y en DOS fijas, para el alta que EMPAREJA campos: el nombre con su bandera
   de IVA, los canales con los terceros. El `auto-fit` de la base en una
   tarjeta ancha reparte en tres o cuatro columnas y el formulario se lee
   regado — lo pagó el alta de listas de precios, con el nombre en una
   esquina y la casilla del IVA flotando a media tarjeta (2026-08-02). */
.form-grid-two { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (max-width: 720px) {
    .form-grid-two { grid-template-columns: minmax(0, 1fr); }
}
/* Un campo SIN rótulo dentro de la grilla —una casilla suelta— se alinea con
   los controles de al lado y no con sus rótulos. Sin esto la pastilla se pega
   al techo de su celda, a la altura de la palabra «Nombre», y la fila entera se
   lee torcida. */
.field-bottom { display: flex; flex-direction: column; justify-content: flex-end; }
.form-grid > .field { max-width: 420px; }
/* Ocupa la fila entera de la grilla. Sin esto —y estuvo sin esto desde la
   F1-a— la dirección de una sede quedaba en una columna de 260px al lado de
   un hueco vacío, que es justo lo contrario de lo que el nombre promete. */
.form-grid .field-wide { grid-column: 1 / -1; max-width: none; }
.form-actions { display: flex; gap: 10px; align-items: center; margin-top: 18px; }

/* ── Formularios de pantalla y confirmación ───────────────────────────── */

/* `.page-header`, `.page-header-actions` y `.page-sub` se BORRARON al pasar
   el encabezado al shell. No se dejan como alias: mientras tengan regla,
   `test_every_class_a_template_uses_has_a_rule` deja pasar una plantilla que
   vuelva a la forma vieja, y la guarda de la migración desaparece.
   `.page-banner .btn-primary` hace lo que hacía la última línea. */
.page-banner .btn-primary { flex-shrink: 0; }

/* La tarjeta NO topa, y el tope de lectura vive en el campo.

   Topaba en 760px «por ancho de lectura», pero lo que hay que proteger de
   estirarse es el CAMPO, y eso ya lo hace `.form-grid > .field`. Sobre la
   tarjeta el tope hacía otra cosa: a 1440px dejaba 374px de aire muerto a la
   derecha del formulario, con la tarjeta de abajo —la del costo de llegada, que
   es una tabla y nunca topó— midiendo 1134. Dos tarjetas de la misma pantalla
   con anchos distintos se leen como una pantalla a medio armar, y ése era el
   síntoma.

   Y necesitaba excepciones, que es la señal de que era el default equivocado:
   la matriz de roles ya se había salido con `form-card-wide`. Mismo argumento
   que el `width: 100%` de `.btn-primary`. Ahora el default llena y quien quiera
   angostarse lo pide con `form-card-narrow` — que es UNA plantilla, la de
   invitar a alguien. */
.form-card { max-width: none; }
.form-card-narrow { max-width: 460px; }
/* El tope de LECTURA, como opt-in y no como default — el default lleno ya
   se litigó arriba. Lo piden las fichas de catálogo contable (cuenta,
   diario): campos cortos en una columna, y a pantalla completa la tarjeta
   era un río de aire con un formulario en la esquina. 760px es el ancho de
   la referencia de tiendapp que la revisión pidió copiar. */
.form-card-reading { max-width: 760px; }

/* EL DESTRUCTIVO NO ES ROJO EN REPOSO (Renzo, 2026-08-14).

   Era rojo relleno con sombra roja, y se leía como un botón de plantilla:
   la pieza más gritona de la pantalla puesta sobre la acción que casi nunca
   se aprieta. Un rojo permanente además se gasta — cuando todo lo que puede
   destruir está en rojo, el rojo deja de avisar.

   Ahora es el mismo contorno neutro que cualquier secundario, y lo que dice
   qué hace es el GLIFO: el bote de basura en «Eliminar», la caja con tapa
   en «Archivar». El icono distingue las dos cosas, que el color no
   distinguía.

   El rojo aparece AL PASAR POR ENCIMA, que es el instante en que importa:
   el puntero ya está sobre el botón y todavía no se ha hecho clic. Es la
   última señal antes de la confirmación, no la decoración de la pantalla.

   Y NO es lo único que avisa, por lo mismo que está escrito en las
   pastillas: quien no distingue el rojo del gris se entera por la palabra
   —«Eliminar»—, por el glifo y por la página de confirmación, que es la que
   de verdad gatea. El color es la cuarta señal, no la primera.

   `.btn-danger` y `.btn-danger-ghost` quedan IGUALES y comparten regla. Eran
   dos tonos para la misma idea —4 usos y 50— y ahora que el relleno rojo se
   fue no hay nada que los distinga. El nombre viejo se conserva para no
   tocar cincuenta plantillas por un renombre; la migración al componente se
   lo lleva. */
.btn-danger,
.btn-danger-ghost {
    color: var(--text-light);
    background: transparent;
    border-color: var(--border-strong);
}
.btn-danger:hover,
.btn-danger-ghost:hover {
    background: var(--danger-bg);
    color: var(--danger-text);
    border-color: var(--danger-bg);
}

.confirm-text { margin: 0 0 18px; color: var(--text-light); font-size: var(--fs-body); }

/* `stretch` y no `flex-start`: la zona punteada ocupa el ancho de su tarjeta,
   como la del producto. Con `flex-start` se encogía al ancho de su texto y una
   zona de arrastre del tamaño de una frase no se lee como una zona. */
.upload { display: flex; flex-direction: column; align-items: stretch; gap: 9px; }
/* La previa sí se queda de su tamaño, y redonda solo para una persona: la de
   un producto es cuadrada porque una etiqueta recortada en círculo se pierde. */
.upload-preview { width: 72px; height: 72px; object-fit: cover; align-self: flex-start;
    border-radius: 10px; border: 1px solid var(--border); }
/* Sin Alpine el atributo nunca se quita, así que la previa vacía no ocupa
   sitio en vez de dejar un cuadro roto. */
[x-cloak] { display: none !important; }

/* ── Mensajes ─────────────────────────────────────────────────────────── */

.messages { list-style: none; margin: 0 0 16px; padding: 0; }
.messages li {
    padding: 11px 14px; margin-bottom: 8px;
    border-radius: var(--radius); font-size: var(--fs-body); font-weight: 500;
}
.messages .success { background: var(--ok-bg);     color: var(--ok-text); }
.messages .error   { background: var(--danger-bg); color: var(--danger-text); }
.messages .warning { background: var(--warn-bg);   color: var(--warn-text); }
.messages .info    { background: var(--info-bg);   color: var(--info-text); }
/* Cuando la página vuelve a UN PUNTO —un formulario que postea a `#su-tarjeta`
   para no devolver a quien agregó una regla al tope de una pantalla de cuatro
   mil píxeles— el aviso no puede quedarse arriba, fuera de vista: «Regla
   creada» o, peor, el error, no los leería nadie. Se pega bajo la barra y
   acompaña. Sin fragmento en la URL no cambia nada, y un navegador sin
   `:has()` deja el aviso donde siempre estuvo. */
.content:has(:target) > .messages {
    position: sticky; top: calc(var(--topbar) + var(--sp-2)); z-index: 5;
}
.content:has(:target) > .messages li { box-shadow: var(--shadow); }
/* Y el destino baja lo que mide el aviso: pegado a 8px de la barra, tapaba el
   título de la tarjeta a la que se acababa de volver (medido: 72 contra 80). */
.content:has(> .messages) :target { scroll-margin-top: calc(var(--topbar) + var(--sp-4) + 64px); }

/* Dentro del drawer la tabla no compite con la altura de la pantalla: el
   drawer ya hace scroll, y un tope acá dejaría dos barras anidadas. */
.table-wrap-inline { max-height: none; min-height: 0; }

/* ── Listas de tarjetas y controles menores ───────────────────────────── */

.card-list { list-style: none; margin: 0 0 20px; padding: 0; display: grid; gap: 10px; }
.card-row {
    display: flex; align-items: center; justify-content: space-between;
    gap: 14px; padding: 15px 18px;
}
.card-title { font-weight: 600; font-size: var(--fs-section); }
.card-sub { margin: 3px 0 0; color: var(--text-light); font-size: var(--fs-small); }

.inline-form { display: inline; margin: 0; }
.icon-btn-light {
    color: var(--text-light); width: 26px; height: 26px; font-size: var(--fs-body);
}
.icon-btn-light:hover { background: var(--bg); color: var(--text); }
.icon-btn-light:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }
/* Baja CON `.btn`, o deja de ser pequeño. Cuando el botón normal pasó a 12px,
   éste se quedaba en 12.5 y quedaba más grande que él — un nombre que miente
   en la única dimensión que lo justifica. Hay una prueba que compara los dos. */
/* El de una FILA, una banda o una tarjeta. 30px, no 22.

   Medía `--sp-1 --sp-3` con `--fs-small`, o sea 22px de alto, y ahí no cabe
   un glifo legible: a 14px el icono ocupaba casi todo el botón y a 11 no se
   distinguía de un punto. Con los CTA llevando icono, 22px dejó de ser un
   tamaño posible.

   Sigue siendo claramente más chico que el de pantalla —30 contra 38— que
   es lo que tiene que decir: esta acción es de esta fila, no de la
   pantalla. */
.btn-small {
    /* El alto va DECLARADO y no se deja al relleno: es el número del que
       cuelga `--band-h`, y derivarlo de padding + interlínea + bordes lo
       dejaría cambiando solo cada vez que alguien toque la tipografía. */
    min-height: var(--btn-small-h);
    padding: 0 var(--sp-4);
    font-size: var(--fs-small);
}
.btn-small svg { width: 14px; height: 14px; }

/* SOLO EL GLIFO, cuadrado. Es el «⋯» de una fila, el «✕» de un panel: los
   que hoy se resuelven caso por caso con una clase distinta en cada
   pantalla —`.icon-btn` sobre oscuro, `.forms-row-action`, y varios `.btn`
   con el texto quitado a mano—.

   Cuadrado y del alto de `.btn-small` para que en una fila de acciones
   mezclada —«Editar» y un «⋯»— los dos midan igual de alto y el cuadrado
   no se lea como un botón a medio hacer.

   Quien lo use TIENE que poner `aria-label`: un botón cuyo nombre accesible
   es la cadena vacía no existe para quien navega con lector de pantalla. */
.btn-icon {
    width: 30px; height: 30px;
    padding: 0;
    gap: 0;
}
.btn-icon svg { width: 15px; height: 15px; }

.preview-card { margin-top: 22px; }

/* ── Ajustes ───────────────────────────────────────────────────────────── */

.setting { padding: 16px 0; border-bottom: 1px solid var(--border); }
.setting:last-of-type { border-bottom: none; }
.setting-head {
    display: flex; align-items: center; gap: 10px; margin-bottom: 8px;
}
.setting-label { font-size: var(--fs-body); font-weight: 600; }
.setting input[type="text"], .setting select {
    width: 100%; max-width: 380px; padding: 11px 13px;
    font-family: inherit; font-size: var(--fs-body); color: var(--text);
    /* Longhand: el atajo `background` resetea `background-image`, y esta
       regla es más específica que la que le dibuja la flecha al `select`. */
    background-color: #fff;
    border: 1px solid var(--border-strong); border-radius: var(--radius-field);
    outline: none;
}
.setting input:focus, .setting select:focus {
    border-color: var(--primary); box-shadow: 0 0 0 3.5px var(--primary-soft);
}

/* Los tres contextos fijan un `padding` propio —para igualar el alto de sus
   vecinos— y con eso pisan el sitio que la flecha necesita a la derecha. Se
   lo devuelven acá, en una sola línea.

   Y va DESPUÉS de los tres a propósito: entre selectores idénticos manda el
   orden, y puesta más arriba, la regla de ajustes —que vive al final— se la
   volvía a pisar. Hay una prueba que resuelve ese orden y exige 30px. */
.field select, .filters select, .setting select { padding-right: 34px; }
.setting-default {
    display: flex; gap: 10px; flex-wrap: wrap; align-items: center;
    margin: 7px 0 0; color: var(--text-light); font-size: var(--fs-small);
}
/* La línea de estado y el campo que la sigue (la casilla «Geocodificar con
   Google» debajo de la pastilla «Conectado») no se tocan: el aire entre las
   dos es el de un campo con el siguiente (Renzo, 2026-09-14). */
.setting-default + .field { margin-top: var(--sp-3); }
.setting-key { color: var(--text-faint); }
/* La URL del webhook de un proveedor de datáfono: para copiarla entera de
   un toque (`user-select: all`) y que no reviente el ancho de la tarjeta. */
.webhook-url {
    display: block; margin-top: 8px; padding: 8px 10px;
    border-radius: var(--radius-sm); background: var(--bg);
    font-size: var(--fs-small); word-break: break-all; user-select: all;
}
.setting-reset { margin-top: 9px; }
.setting.field-degraded input { border-color: var(--warn-text); }

/* Medios de pago: una tarjeta por pasarela, sobre el kit de Ajustes. */
.gateway-card { margin-bottom: 18px; }
/* `gateway-head`, `gateway-name` y `gateway-hint` se retiraron el 2026-08-11.
   Eran `.card-header` con otro nombre: la pantalla de pasarelas los escribió
   porque necesitaba la pista DEBAJO del título y `.card-header` la manda a la
   derecha, y Dominios los copió. Eso ahora lo hace `.card-head-lead` —una sola
   forma, con su parcial— y los diecisiete usos migraron. Ver docs/marco-ui.md. */
/* ── Marketplace ──────────────────────────────────────────────────────── */

.section-title {
    margin: 26px 0 12px; font-size: var(--fs-meta); font-weight: 700;
    letter-spacing: .08em; text-transform: uppercase; color: var(--text-faint);
}
.section-title:first-of-type { margin-top: 0; }
.card-actions { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
/* Perfil: los botones de cada tarjeta van abajo, con aire, y pueden partir
   en dos líneas en una pantalla angosta. `.card-actions` a secas vive en la
   cabecera y no se parte. */
.profile-actions { margin-top: 16px; flex-wrap: wrap; }
/* Un dato que se muestra y no se edita (el correo, que es la llave). */
.field-static { margin: 0; padding: 10px 0; color: var(--text-light); }
/* La zona de peligro y sus confirmaciones: el mismo borde que el aviso rojo. */
/* La zona de peligro con cara de peligro (Renzo, 2026-09-14): tinte rojo
   de fondo, borde rojo, el título en rojo. Antes era una tarjeta blanca con
   un borde fino que se leía como cualquier otra. */
.card-danger {
    border: 1px solid color-mix(in srgb, var(--danger-text) 35%, white);
    background: color-mix(in srgb, var(--danger-bg) 60%, white);
    box-shadow: none;
}
.card-danger .card-header { border-bottom-color: color-mix(in srgb, var(--danger-text) 25%, white); }
.card-danger .card-header h2 { color: var(--danger-text); }
.card-danger-text { margin: 0 0 var(--sp-3); color: var(--danger-text); font-size: var(--fs-body); }
/* Los avisos que llegan DENTRO de una tarjeta repintada por la barra. */
.messages-inline { margin: 0 0 var(--sp-3); }
/* El botón de una tarjeta que la barra de guardar esconde
   (`data-savebar-fallback`): `hidden` tiene que ganarle al `display: flex`
   de `.card-actions`, igual que en `.card-header[hidden]` y por lo mismo. */
.card-actions[hidden] { display: none; }
.alert-danger ul, .alert-warn ul { margin: 8px 0 0; padding-left: 18px; }
.alert p { margin: 0 0 6px; }
.alert p:last-child { margin-bottom: 0; }

.fill-list { list-style: none; margin: 10px 0 0; padding: 0; display: grid; gap: 10px; }
.fill-list label { display: block; margin-bottom: 5px; font-size: var(--fs-body); }
.fill-list input {
    width: 100%; padding: 9px 12px; font: inherit; font-size: var(--fs-body);
    border: 1px solid var(--border-strong); border-radius: var(--radius-field);
    background: #fff; outline: none;
}
.fill-list input:focus { border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-soft); }

.table td .card-sub { display: block; }

/* Una acción y su regla, apiladas: en columnas la tabla no cabía. */
.action-rule {
    display: flex; align-items: center; justify-content: space-between;
    gap: 10px; padding: 3px 0; font-size: var(--fs-body);
}
.action-rule + .action-rule { border-top: 1px solid var(--border); }
.action-rule select { min-width: 132px; }

/* El párrafo que explica de qué va un formulario, dentro de su tarjeta.

   Antes esta prosa vivía en el subtítulo del encabezado. En el banner oscuro
   dos renglones de explicación pesan el doble que sobre blanco, y además
   compiten con los datos de la fila —el identificador, el correo, cuántas
   variantes— que es lo que uno va a mirar ahí. Baja acá, que es donde alguien
   la lee una vez y no en cada visita. */
.form-lead {
    margin: 0 0 18px; color: var(--text-light); font-size: var(--fs-body);
    max-width: 62ch;
}

/* ── Zona de arrastre de archivos ─────────────────────────────────────── */

/* Y funciona SIN una línea de JavaScript. El `<input type="file">` se estira
   invisible sobre toda el área: los navegadores aceptan que se le suelte un
   archivo encima, así que arrastrar sale gratis y no hay estado que mantener
   ni evento que capturar. El `<label>` de al lado solo dibuja.

   Un dropzone de JavaScript sería más lucido y dejaría la pantalla sin subir
   imágenes en cuanto el script falle, que es la razón por la que el protocolo
   de esta plataforma pide que todo sirva sin JS. */
.dropzone {
    position: relative; display: block;
    padding: var(--sp-6) var(--sp-5);
    border: 1.5px dashed var(--border-strong); border-radius: var(--radius);
    background: var(--bg);
    color: var(--text-light); font-size: var(--fs-body); text-align: center;
    cursor: pointer;
    transition: border-color .16s, background .16s, color .16s;
}
/* El archivo YA ELEGIDO, dentro de la zona. En negrita y con su glifo: es la
   diferencia entre una zona que invita y una que confirma, y sin confirmación
   nadie sabe si el clic hizo algo — el `Choose File` del navegador al menos
   escribía el nombre al lado. */
.dropzone-chosen {
    display: inline-flex; align-items: center; gap: var(--sp-2);
    color: var(--text); font-weight: 600;
}
.dropzone-chosen svg { width: 16px; height: 16px; color: var(--primary); flex: none; }
.dropzone:hover, .dropzone:focus-within {
    border-color: var(--primary); background: var(--primary-soft);
    color: var(--text);
}
/* Invisible pero PRESENTE: sigue siendo el control que recibe el archivo, el
   que abre el selector al hacer clic y el que el teclado enfoca. `display:
   none` lo sacaría de los tres. */
.dropzone input[type="file"] {
    position: absolute; inset: 0; width: 100%; height: 100%;
    opacity: 0; cursor: pointer;
}

/* La miniatura de lo que ya está guardado. */
.thumb-row { display: flex; flex-wrap: wrap; gap: var(--sp-3); margin-bottom: var(--sp-3); }
.thumb {
    position: relative; width: 84px; height: 84px; flex-shrink: 0;
    border: 1px solid var(--border); border-radius: 10px;
    background: #fff center/cover no-repeat;
}
.thumb-badge {
    position: absolute; bottom: 4px; left: 50%; transform: translateX(-50%);
    padding: 2px 6px; border-radius: 999px;
    background: rgba(0, 0, 0, .55); color: #fff;
    font-size: var(--fs-badge); font-weight: 700; letter-spacing: .04em;
    white-space: nowrap;
}

/* ── Etiquetas ─────────────────────────────────────────────────────────── */

/* Las guardadas se PINTAN como fichas; el campo de abajo es el que las edita.
   Un editor de fichas de verdad —escribir, Enter, borrar con backspace— es
   JavaScript, y sin él la pantalla se quedaría sin poder etiquetar. Así se ve
   lo que hay y se sigue pudiendo cambiar con el teclado y nada más. */
.tag-row { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: var(--sp-2); }
.tag {
    padding: 3px 10px; border-radius: 999px;
    background: var(--primary-soft); color: var(--primary-dark);
    border: 1px solid color-mix(in srgb, var(--primary) 22%, transparent);
    font-size: var(--fs-meta); font-weight: 600; white-space: nowrap;
}

/* ── La primera celda de una tabla ────────────────────────────────────── */

/* Miniatura + nombre + subtítulo. Es la forma que se repite en seis de las
   nueve tablas del ERP —la medición de §9.2 del spec de la F1 la nombra como
   lo primero que convendría extraer— y acá deja de ser tres copias parecidas.

   La miniatura no es decoración: en una lista de quinientos productos es lo
   que deja reconocer uno de un vistazo, sin leer. */
.cell-media { display: flex; align-items: center; gap: var(--sp-3); }
.cell-text { min-width: 0; }
.cell-thumb {
    width: var(--cell-thumb); height: var(--cell-thumb); flex-shrink: 0;
    border: 1px solid var(--border); border-radius: 9px;
    background: #fff center/cover no-repeat;
}
/* Sin imagen, la inicial sobre el tinte de marca. Un cuadro gris vacío se lee
   como algo que falta; una inicial se lee como lo que es —todavía no tiene
   foto— y encima ayuda a distinguir la fila. */
.cell-thumb-empty {
    display: flex; align-items: center; justify-content: center;
    border-color: transparent; background: var(--primary-soft);
    color: var(--primary-dark);
    font-size: var(--fs-small); font-weight: 700; text-transform: uppercase;
}
/* El nombre pesa; el enlace no se pinta de color. En una tabla de quinientas
   filas, quinientos enlaces morados son ruido: lo que distingue la fila es el
   nombre, y el subrayado al pasar por encima ya dice que se puede abrir.

   `:not(.btn)` porque un BOTÓN también puede ser un `<a>`, y esta regla —dos
   clases y dos elementos— le ganaba por especificidad a la suya. El efecto se
   veía en tres pantallas a la vez: «Desactivar» en ubicaciones, «Eliminar» en
   las direcciones del tercero y «Eliminar» en los tipos de producto salían con
   el borde rosado de `btn-danger-ghost` y el texto NEGRO, que es un botón
   destructivo que no se lee como destructivo. Es la misma forma del error de la
   pastilla contra `.field label`: la regla del botón estaba escrita y perdía en
   la cascada. Excluirlo es mejor que subir la especificidad del botón, porque
   así el que manda sigue siendo el botón en cualquier contenedor futuro. */
.cell-media a:not(.btn), .table tbody a:not(.btn) {
    color: var(--text); font-weight: 600;
}
.cell-media a:not(.btn):hover, .table tbody a:not(.btn):hover {
    color: var(--primary-dark);
}

/* Una celda que enumera cosas: no crece, se corta y lo dice. Sin esto, cuatro
   etiquetas de veinte caracteres empujan las columnas de la derecha fuera de
   la pantalla y la fila deja de poder barrerse. */
.cell-tags { max-width: 260px; }
.cell-more { color: var(--text-faint); font-size: var(--fs-meta); }

/* ───────────────────────────────────────────────────────────────────────────
   INTEGRANTES: la grilla de tarjetas y las barras de progreso.

   Es la ÚNICA lista del ERP que no usa la tarjeta-tabla, y es una excepción
   decidida: cada integrante lleva el medidor de su ficha más una barra por
   inducción —cuatro en el caso de LTDC—, y en una tabla eso son cuatro
   columnas que solo significan algo leídas juntas. A 1440px la fila se vuelve
   ilegible antes de la tercera. Cargos y Tareas siguen con la tabla.
   ─────────────────────────────────────────────────────────────────────────── */

/* El rótulo del grupo lleva el conteo pegado, no en otra columna: «Barista 10»
   se lee como una sola cosa. La línea que sigue al texto lo separa del
   siguiente grupo sin gastar un margen entero. */
.people-group { margin-bottom: var(--sp-5); }
.people-group-title {
    display: flex; align-items: center; gap: var(--sp-2);
    margin: 0 0 var(--sp-3); font-size: var(--fs-body); font-weight: 700;
    color: var(--text);
}
.people-group-title::after {
    content: ""; flex: 1; height: 1px; background: var(--border);
}
.people-group-count { font-weight: 600; color: var(--text-light); }

/* `min(340px, 100%)` y no un mínimo fijo, por lo mismo que `.form-grid`: con el
   mínimo fijo, a 360px de viewport la columna mide más que la grilla y las
   tarjetas salen por fuera. */
.people-grid {
    display: grid; gap: var(--sp-3);
    grid-template-columns: repeat(auto-fill, minmax(min(340px, 100%), 1fr));
}
.people-card {
    display: flex; flex-direction: column; gap: var(--sp-3);
    padding: var(--sp-4); background: var(--card-bg);
    border-radius: var(--radius-lg);
    /* Sin borde y con la sombra suave: el referente (2026-09-13) flota
       sobre el fondo en vez de recortarse con una línea. */
    box-shadow: var(--shadow);
}
.people-card-head { display: flex; align-items: flex-start; gap: var(--sp-3); }
.people-avatar {
    flex-shrink: 0; width: 48px; height: 48px; border-radius: 50%;
    object-fit: cover;
}
.people-avatar-empty {
    display: grid; place-items: center; background: var(--primary-soft);
    color: var(--primary); font-weight: 700; font-size: var(--fs-stat);
}
/* `min-width: 0` es lo que permite que el correo se recorte: sin él, un flex
   item no baja de su contenido y un correo largo estira la tarjeta entera. */
.people-card-who {
    flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px;
    padding-top: 2px;
}
.people-card-name {
    font-weight: 700; font-size: var(--fs-body);
    color: var(--text); text-decoration: none;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.people-card-name:hover { color: var(--primary-dark); }
.people-card-mail {
    font-size: var(--fs-small); color: var(--text-light);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* El estado y el acceso, bajo el correo: pastillas con punto de color. */
.people-card-pills { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 5px; }
.pill-dot {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 2px 10px 2px 8px; font-weight: 600;
}
.pill-dot::before {
    content: ''; width: 6px; height: 6px; border-radius: 50%;
    background: currentColor; flex: none;
}
.people-card-tools { display: flex; gap: 6px; flex-shrink: 0; }
/* `.icon-btn` está calibrado contra el banner oscuro —hover blanco—, así que en
   una tarjeta clara desaparecía. Éste es su par para fondo claro. */
.card-icon-btn {
    display: grid; place-items: center; width: 32px; height: 32px;
    border: 1px solid var(--border); border-radius: var(--radius-field);
    color: var(--text-light); background: var(--card-bg);
}
.card-icon-btn:hover {
    color: var(--primary-dark); border-color: var(--primary);
}
.card-icon-btn:focus-visible {
    outline: 2px solid var(--primary); outline-offset: 1px;
}
.card-icon-btn svg { width: 16px; height: 16px; }
/* Los medidores: la baldosa del ícono a la izquierda y, a su derecha, el
   rótulo con la cifra y la barra. Sin panel —ni fondo ni borde ni relleno—
   desde el 2026-09-14: Renzo pidió recortar el alto de la tarjeta, y el
   panel gris alrededor de cada medidor era lo que la estiraba. Completo =
   verde lima en la baldosa, la cifra y la barra. */
.people-card-meters {
    display: flex; flex-direction: column; gap: 10px;
    padding-top: var(--sp-3); border-top: 1px solid var(--border);
}
.meter-panel { display: flex; align-items: center; gap: var(--sp-3); padding: 0; }
.meter-tile {
    flex: none; display: grid; place-items: center; width: 34px; height: 34px;
    border-radius: var(--radius-sm); background: var(--primary-soft);
    color: var(--primary);
}
.meter-tile svg { width: 16px; height: 16px; }
.meter-panel-done .meter-tile {
    background: color-mix(in srgb, var(--done) 16%, white);
    color: var(--done-text);
}
.meter-panel .meter { flex: 1; min-width: 0; gap: 4px; }
.meter-panel .meter-label { color: var(--text); }
.meter-panel .meter-track { height: 5px; background: var(--primary-soft); }
.meter-panel-done .meter-track { background: color-mix(in srgb, var(--done) 16%, white); }
.people-card-none {
    margin: 0; font-size: var(--fs-small); font-style: italic; color: var(--text-light);
}
/* La tarjeta Acceso de la ficha: aire entre los pares y la nota de abajo, que
   pegada al último renglón se leía como parte de él. */
.access-card .facts { gap: 12px var(--sp-3); }
.access-card .card-hint { margin-top: var(--sp-3); }
.access-card .input-copy { margin-top: .35rem; }

/* La barra. `--primary` mientras falta algo y verde cuando llega al final: el
   verde es la señal de «esto ya está» y usarlo siempre lo dejaría sin decir
   nada. */
.meter { display: flex; flex-direction: column; gap: 5px; }
.meter-head {
    display: flex; align-items: baseline; justify-content: space-between;
    gap: var(--sp-2);
}
.meter-label {
    display: inline-flex; align-items: center; gap: 5px;
    font-size: var(--fs-label); font-weight: 600; letter-spacing: .02em;
    text-transform: uppercase; color: var(--text-light);
}
.meter-icon { display: inline-flex; color: var(--text-light); }
.meter-icon svg { width: 13px; height: 13px; }
.meter-value {
    font-size: var(--fs-small); font-weight: 700; color: var(--primary-dark);
    white-space: nowrap;
}
.meter-value-done { color: var(--done-text); }
.meter-track {
    height: 5px; border-radius: 999px; background: var(--warn-bg);
    overflow: hidden;
}
.meter-fill {
    display: block; height: 100%; border-radius: 999px;
    background: var(--primary);
}
.meter-fill-done { background: var(--done); }

/* La tarjeta de una inducción. Reusa `.people-grid` y `.people-card`: son la
   misma forma —una tarjeta con cabecera, cuerpo y pie— y darles dos nombres
   sería mantener dos veces el mismo ancho de columna. */
.onboarding-card-head {
    display: flex; align-items: flex-start; justify-content: space-between;
    gap: var(--sp-2);
}
.onboarding-card-name {
    margin: 0; font-size: var(--fs-body); font-weight: 700; color: var(--text);
}
.onboarding-card-badges {
    display: flex; flex-wrap: wrap; gap: 4px; justify-content: flex-end;
}
.onboarding-card-text {
    margin: 0; font-size: var(--fs-small); line-height: 1.5;
    color: var(--text-light);
}
/* Los pasos como pastillas y no como lista numerada: lo que se busca acá es
   «¿qué cubre esta inducción?», y una lista vertical de cinco renglones hace la
   tarjeta el doble de alta para decir lo mismo. */
.onboarding-steps { display: flex; flex-wrap: wrap; gap: 6px; }
/* En MAYÚSCULAS y con su ícono, como la referencia. La versión anterior las
   pintaba en caja normal y del tamaño del texto de la tarjeta, así que un
   título largo —«Día 1 — Cultura y presentación personal»— competía con la
   descripción en vez de leerse como una etiqueta. */
.onboarding-step {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 4px 10px; border-radius: 999px;
    background: var(--primary-soft); color: var(--primary-dark);
    font-size: var(--fs-badge); font-weight: 700;
    text-transform: uppercase; letter-spacing: .03em;
}
.onboarding-step-icon { width: 13px; height: 13px; flex-shrink: 0; }
.onboarding-card-foot {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--sp-2); margin-top: auto;
    padding-top: var(--sp-3); border-top: 1px solid var(--border);
}
.onboarding-card-counts {
    font-size: var(--fs-small); color: var(--text-light);
}
/* El tacho va rojo DESDE EL REPOSO, con fondo tenue.

   Decía lo contrario —«tres tachos rojos en una grilla de cinco tarjetas
   gritan más que el contenido»— y el argumento no se sostuvo al verlo: en gris
   el botón no se leía como botón, y al lado de «Editar» con su rótulo parecía
   un recuadro vacío. El rojo tenue lo identifica sin gritar; el fuerte queda
   para el hover. */
.card-icon-btn-danger {
    color: var(--danger-text); border-color: var(--danger-bg);
    background: var(--danger-bg);
}
.card-icon-btn-danger:hover {
    color: var(--danger-text); border-color: var(--danger-text);
}
.form-note { margin-top: var(--sp-3); }

/* ── El kiosco de marcaje ──────────────────────────────────────────────────
   Reusa `.auth-stage` y `.auth-card`, que ya son la caja centrada a pantalla
   completa del login: es la misma forma y darle otro nombre sería mantener dos
   veces el mismo ancho.

   Lo que cambia es la ESCALA. Esta pantalla se opera de pie, a un brazo de
   distancia y muchas veces con las manos ocupadas, así que el PIN y el botón
   son deliberadamente más grandes que en cualquier otro formulario del ERP. */
.kiosk-card { max-width: 460px; }
/* La sede, ARRIBA del título y en versalitas: quien llega al mostrador tiene
   que poder confirmar de un vistazo que está marcando en la tienda correcta, y
   un kiosco abierto en la sede equivocada registra el turno de otra gente. */
.kiosk-site {
    margin: 0 0 var(--sp-1); text-transform: uppercase; letter-spacing: .08em;
    font-size: var(--fs-badge); font-weight: 700; color: var(--primary);
}
/* Ancho completo, cifras grandes y espaciadas. `letter-spacing` porque son
   dígitos tapados por puntos: sin separación, cuatro puntos pegados no dejan
   contar cuántos se tecleó. */
.kiosk-pin {
    width: 100%; padding: var(--sp-4); text-align: center;
    font-size: var(--fs-number); letter-spacing: .5em; text-indent: .5em;
    border: 1px solid var(--border-strong); border-radius: var(--radius-field);
    background: var(--card-bg); color: var(--text);
}
.kiosk-pin:focus { outline: 2px solid var(--primary); outline-offset: 1px; }
/* La confirmación del toque. En verde y con el nombre completo: el barista
   necesita saber que quedó registrado ÉL y no la persona de antes. */
.kiosk-result {
    margin: 0 0 var(--sp-3); padding: var(--sp-3);
    border-radius: var(--radius-field);
    background: var(--ok-bg); color: var(--ok-text);
    font-size: var(--fs-body); font-weight: 600; text-align: center;
}
/* Quién está adentro. Es lo único que le dice al barista que su toque quedó
   registrado después de que el mensaje se va, y a quien cierra la tienda,
   quién se olvidó de marcar salida. */
.kiosk-inside {
    margin-top: var(--sp-5); padding-top: var(--sp-4);
    border-top: 1px solid var(--border);
}
.kiosk-inside-row {
    display: flex; justify-content: space-between; gap: var(--sp-2);
    margin: 0 0 var(--sp-2); font-size: var(--fs-body); color: var(--text);
}
/* Salir del kiosco va abajo, chico y sin color: cierra la sesión, así que un
   botón llamativo sobre un mostrador es un accidente esperando. */
.kiosk-exit { margin-top: var(--sp-4); text-align: center; }

/* ── El planificador semanal ───────────────────────────────────────────────
   La grilla es sede × día, sobre `.table`: es una tabla de verdad —encabezado
   de columna y encabezado de fila— y montarla con `grid` le quitaría la
   semántica sin comprar nada.

   El ancho lo reparte el navegador con `table-layout: fixed`: sin eso, la
   columna del día con más turnos se come a las otras seis y la semana deja de
   leerse de un vistazo. */
.week-nav {
    display: flex; align-items: center; justify-content: center;
    gap: var(--sp-3); margin-bottom: var(--sp-4);
}
.week-label {
    font-size: var(--fs-small); font-weight: 700; color: var(--text-light);
    letter-spacing: .04em;
}
.week-grid { table-layout: fixed; }
/* La abreviatura del día arriba y el número debajo: quien planifica busca por
   día de la semana, y confirma por fecha. */
.week-day { text-align: center; text-transform: capitalize; }
.week-daynum {
    display: block; font-size: var(--fs-meta); font-weight: 400;
    color: var(--text-light);
}
.week-site {
    text-align: left; font-weight: 600; color: var(--text);
    white-space: nowrap;
}
.week-cell { vertical-align: top; padding: var(--sp-2); }
/* Cada turno es un enlace-bloque: el objetivo de toque es la tarjeta entera y
   no un texto de 12px, que sobre una tablet no se acierta. */
.week-shift {
    display: block; margin-bottom: 4px; padding: 5px 7px;
    border-radius: var(--radius-field);
    background: var(--primary-soft); color: var(--primary-dark);
    text-decoration: none;
}
.week-shift:hover { background: var(--primary); color: #fff; }
.week-shift-who {
    display: block; font-size: var(--fs-small); font-weight: 600;
}
.week-shift-when { display: block; font-size: var(--fs-meta); opacity: .85; }
/* ── El indicador de carga ────────────────────────────────────────────────
   Uno solo, fijo y centrado, para todo pedido de htmx. Lo enciende
   `js/loader.js` con la clase `is-on` — y con 250ms de retardo, que es lo que
   evita que los filtros de las listas, que salen en cada tecla, pinten y
   despinten un velo sobre la pantalla entera cuatro veces por palabra.

   `visibility` ADEMÁS de `opacity`: con opacidad sola el velo sigue estando y
   se come los clics de toda la página mientras es invisible. Y la transición
   la incluye para que apagarse no sea un corte seco.
   ------------------------------------------------------------------------ */
.loader {
    position: fixed; inset: 0; z-index: 200;
    display: grid; place-items: center;
    background: rgba(13, 11, 22, .38);
    backdrop-filter: blur(2px);
    opacity: 0; visibility: hidden;
    transition: opacity .18s ease, visibility 0s linear .18s;
}
.loader.is-on {
    opacity: 1; visibility: visible;
    transition: opacity .18s ease, visibility 0s;
}
/* 72px: un 40% menos que los 120 con los que viene dibujado. Se puede escalar
   sin perder nada porque es vectorial y su desenfoque está en unidades del
   `viewBox` —el filtro encoge con el dibujo—, así que el tamaño es una
   decisión de pantalla y no una restricción del archivo. */
.loader-art { display: block; width: 72px; height: 72px; }
.loader-art-svg { display: block; width: 100%; height: 100%; }

/* Para lo que se anuncia y no se ve. `clip-path` y no `display: none`: lo
   escondido con `display` los lectores de pantalla tampoco lo leen. */
.visually-hidden {
    position: absolute; width: 1px; height: 1px;
    margin: -1px; padding: 0; border: 0;
    clip-path: inset(50%); overflow: hidden; white-space: nowrap;
}

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

/* ── La barra de guardado ─────────────────────────────────────────────────
   Pegada al techo del contenido, debajo de la topbar fija. Aparece cuando hay
   algo sin guardar y se va cuando no — pero NACE VISIBLE en el HTML y la
   esconde `js/savebar.js`. Esa inversión es lo único que la hace degradar: sin
   JavaScript se queda puesta y la ficha se guarda igual.

   `position: sticky` y no `fixed`: fija taparía el contenido de las pantallas
   que no la tienen, y obligaría a reservarle alto a todas. Pegajosa ocupa su
   renglón solo donde existe.
   ------------------------------------------------------------------------ */
.savebar {
    /* FIJA y centrada, dentro de la franja de la topbar. Ahí el medio está
       vacío —a la izquierda la marca, a la derecha la empresa— y es donde el
       ojo ya está cuando algo cambia arriba.

       Fija y no pegajosa: pegajosa vive dentro del contenido, así que se
       despega al llegar al final de la ficha y desaparece justo cuando alguien
       terminó de escribir y va a guardar.

       `z-index` por encima de la topbar, que es 100: la barra la TAPA a
       propósito mientras hay algo pendiente. */
    position: fixed; top: 12px; left: 50%; transform: translateX(-50%);
    z-index: 110;
    /* Se encoge con la pantalla y no se desborda: `min()` la deja crecer hasta
       560px y después la ata al ancho disponible menos el aire de los lados. */
    width: min(560px, calc(100vw - 2 * var(--sp-4)));
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--sp-3);
    padding: 8px 8px 8px var(--sp-4);
    /* Un punto más clara que la topbar: del mismo color se leería como un
       hueco recortado en ella en vez de como algo que se posó encima. */
    background: #1b1830; color: #fff;
    border: 1px solid rgba(255, 255, 255, .1);
    border-radius: 999px;
    box-shadow: 0 8px 28px rgba(0, 0, 0, .45);
}
.savebar-text {
    display: flex; align-items: center; gap: 10px;
    font-size: var(--fs-body); font-weight: 600;
}
/* Un punto y no un icono: lo que dice es «hay algo pendiente», y para eso una
   forma de seis píxeles alcanza. Un triángulo de alerta diría que algo está
   mal, y no lo está — todavía no se guardó, nada más. */
.savebar-dot {
    width: 7px; height: 7px; border-radius: 50%;
    background: var(--warn-text); flex: none;
}
.savebar-actions { display: flex; align-items: center; gap: 6px; flex: none; }
/* El texto cede primero: con la pantalla angosta lo que no puede faltar son
   los botones, y «Cambios sin guardar» se recorta antes que ellos. */
.savebar-text { min-width: 0; overflow: hidden; text-overflow: ellipsis;
                white-space: nowrap; }
/* El fantasma sobre fondo oscuro: el de la hoja es para fondo claro y acá
   quedaba texto gris sobre casi negro. */
.savebar .btn-ghost {
    color: #fff; background: rgba(255, 255, 255, .12);
    border-color: transparent;
}
.savebar .btn-ghost:hover { background: rgba(255, 255, 255, .2); }

/* Con JavaScript: escondida hasta que algo cambia. `js/savebar.js` marca el
   `<html>` para que la regla exista solo cuando hay quien la encienda — sin
   eso, un JS que no cargue dejaría la barra escondida y la ficha sin guardar
   por ninguna puerta. */
html.js .savebar { display: none; }
html.js .savebar.is-dirty { display: flex; }
/* Falló el guardado: la barra se queda —hay cambios sin guardar— y lo dice
   con el borde. El detalle del error lo pinta la ficha; la barra solo avisa
   que no entró. La marcan los modos grupo y dirigido de `savebar.js`. */
.savebar.is-error { border-color: var(--danger-vivid); }
.savebar.is-error .savebar-text { color: var(--danger-vivid); }

/* La barra de ACCIÓN: la misma barrita como puro feedback de viaje —
   «Publicando…» mientras el POST navega—. Escondida SIN condición de JS, al
   revés de la de guardar: no gobierna nada, así que sin JavaScript no hay
   nada que encender ni puerta que perder. */
.savebar.actionbar { display: none; }
html.js .savebar.actionbar.is-active { display: flex; }

/* Angosto: la barra se va ABAJO. Arriba no cabe —la topbar ya lleva la
   hamburguesa, la marca y el nombre de la empresa, así que el medio deja de
   estar libre— y puesta debajo de ella tapaba el título de la ficha, que es lo
   primero que alguien mira para saber qué está editando.

   Abajo no tapa nada que se lea, queda donde llega el pulgar, y es donde
   vivían los botones antes de esta pasada. */
@media (max-width: 767px) {
    .savebar {
        top: auto; bottom: var(--sp-3);
        width: calc(100vw - 2 * var(--sp-3));
        border-radius: var(--radius);
    }
}
/* Y en una pantalla de verdad chica el rótulo sobra: los dos botones dicen lo
   mismo. `340px` y no `380`: un iPhone mini mide 375 y ahí el rótulo TODAVÍA
   cabe — cortarlo en 380 lo escondía en un teléfono corriente. */
@media (max-width: 340px) {
    .savebar-text { display: none; }
    .savebar { justify-content: flex-end; }
}

/* ── El dibujo del loader ─────────────────────────────────────────────────
   Las animaciones que venían dentro del SVG, movidas acá. Incrustado y con sus
   reglas afuera, la página puede PAUSARLO — que es lo que hace falta: quieto
   en la barra de guardado, girando solo mientras se guarda.

   Corren únicamente bajo `.is-spinning`. Al revés —animando siempre y pausando
   con una clase— un dibujo que aparece en una barra que casi siempre está
   quieta estaría gastando cuadros todo el tiempo para nada.
   ------------------------------------------------------------------------ */
.loader-bg { fill: var(--loader-bg, #0d0d14); }
.ring, .arm, .core, .au, .white { transform-box: fill-box; transform-origin: center; }
.white { fill: var(--loader-rest, #fff); }

.is-spinning .ring  { animation: loader-spin calc(var(--loader-pulse, 2s) * 6) linear infinite; }
.is-spinning .arm   { animation: loader-merge var(--loader-pulse, 2s) cubic-bezier(.45,0,.55,1) infinite; }
.is-spinning .core  { animation: loader-core var(--loader-pulse, 2s) cubic-bezier(.45,0,.55,1) infinite; }
.is-spinning .white { animation: loader-whiteout var(--loader-pulse, 2s) cubic-bezier(.45,0,.55,1) infinite; }
.is-spinning .au1 { animation: loader-drift1 calc(var(--loader-pulse, 2s) * 3.5) ease-in-out infinite; }
.is-spinning .au2 { animation: loader-drift2 calc(var(--loader-pulse, 2s) * 4.5) ease-in-out infinite; }
.is-spinning .au3 { animation: loader-drift3 calc(var(--loader-pulse, 2s) * 5.5) ease-in-out infinite; }

@keyframes loader-spin  { to { transform: rotate(360deg); } }
@keyframes loader-merge { 0%, 100% { transform: translateX(0) scale(1); }
                          50%      { transform: translateX(-70px) scale(1.08); } }
@keyframes loader-core  { 0%, 100% { transform: scale(.94); }
                          50%      { transform: scale(1.14); } }
@keyframes loader-whiteout { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
@keyframes loader-drift1 { 0%, 100% { transform: translate(-40px, 30px); }
                           50%      { transform: translate(110px, -80px); } }
@keyframes loader-drift2 { 0%, 100% { transform: translate(80px, 60px); }
                           50%      { transform: translate(-70px, -40px); } }
@keyframes loader-drift3 { 0%, 100% { transform: translate(20px, -90px); }
                           50%      { transform: translate(-60px, 90px); } }

/* La marca de la barra: el mismo dibujo, del tamaño de un punto. Reemplaza al
   punto ámbar que había — dice lo mismo cuando está quieto y además puede
   decir «estoy guardando» sin cambiar de forma. */
.savebar-mark { width: 20px; height: 20px; flex: none; display: block; }

@media (prefers-reduced-motion: reduce) {
    .is-spinning .ring, .is-spinning .arm, .is-spinning .core,
    .is-spinning .white, .is-spinning .au { animation: none; }
    .is-spinning .white { opacity: 1; }
}

/* ── Los bloques de una inducción ────────────────────────────────────────────
   Nueve tipos, y el contenedor los separa a todos igual: el ritmo vertical de
   un paso lo pone el CONTENEDOR y no cada tipo, porque con nueve márgenes
   propios el espacio entre un texto y un aviso dependería de cuál vino antes.

   `.block-prose` es el único sitio donde se pinta marcado que no escribimos
   nosotros —lo que `|rich` deja pasar—, así que sus reglas van por descendencia
   y no por clase: el HTML migrado no trae ninguna de las nuestras. */
.block { margin-bottom: var(--sp-5); }
.block:last-child { margin-bottom: 0; }

.block-title {
    margin: 0 0 var(--sp-2); font-size: var(--fs-section); font-weight: 700;
    color: var(--text);
}

.block-prose { font-size: var(--fs-body); color: var(--text); line-height: 1.65; }
.block-prose > *:first-child { margin-top: 0; }
.block-prose > *:last-child { margin-bottom: 0; }
.block-prose p { margin: 0 0 var(--sp-3); }
.block-prose ul, .block-prose ol { margin: 0 0 var(--sp-3); padding-left: var(--sp-5); }
.block-prose li { margin-bottom: 4px; }
.block-prose h2, .block-prose h3, .block-prose h4 {
    margin: var(--sp-4) 0 var(--sp-2); font-size: var(--fs-section);
    font-weight: 700;
}
.block-prose a { color: var(--primary-dark); text-decoration: underline; }
.block-prose blockquote {
    margin: 0 0 var(--sp-3); padding-left: var(--sp-3);
    border-left: 3px solid var(--border-strong); color: var(--text-light);
}
.block-prose code {
    padding: 1px 5px; border-radius: var(--radius-field);
    background: var(--bg); font-size: var(--fs-small);
}
/* Las tablas del recetario tienen seis columnas y no caben en un teléfono. El
   desbordamiento se scrollea DENTRO del bloque: sin esto, la página entera se
   corre de lado y la barra lateral queda a media pantalla. */
.block-prose table {
    display: block; overflow-x: auto; width: 100%;
    border-collapse: collapse; margin: 0 0 var(--sp-3);
    font-size: var(--fs-small);
}
.block-prose th, .block-prose td {
    padding: 6px 10px; border: 1px solid var(--border); text-align: left;
    vertical-align: top;
}
.block-prose hr { margin: var(--sp-4) 0; border: 0; border-top: 1px solid var(--border); }

/* El tinte llega en una variable que el validador ya comprobó contra un
   hexadecimal. Por eso `sanitize` puede sacar `style` de su lista blanca sin
   perder el color del aviso: acá entra por un canal con forma conocida. */
.block-callout {
    display: flex; gap: var(--sp-3); padding: var(--sp-4);
    border-radius: var(--radius-card);
    background: var(--callout-tint, var(--primary-soft));
    border: 1px solid rgba(0, 0, 0, .05);
}
.block-callout-emoji { font-size: 1.4em; line-height: 1.4; }
.block-callout-body { min-width: 0; flex: 1; }

.block-divider { margin: var(--sp-5) 0; border: 0; border-top: 1px solid var(--border); }

.block-image-one {
    display: block; max-width: 100%; height: auto;
    border-radius: var(--radius-card); margin-bottom: var(--sp-3);
}
.block-image-one:last-child { margin-bottom: 0; }

.block-video-player {
    width: 100%; border-radius: var(--radius-card); background: var(--canvas);
    margin-bottom: var(--sp-3);
}

/* ── La checklist de una inducción, compacta ──────────────────────────────
   `.check-row` la comparten DOS componentes: esta lista y la grilla de campos
   de empresa, que le pone `margin-bottom: 17px`. Ese margen era el aire que
   estiraba una tarjeta de veintidós ítems a tres pantallas de scroll. Se acota
   a `.block-checklist` en vez de renombrar la clase: la grilla también la usa
   y renombrarla movería una decisión que no es de acá.
   ------------------------------------------------------------------------ */
.block-checklist .check-list { margin-top: 6px; margin-bottom: var(--sp-3); gap: 0; }
.check-head {
    display: flex; align-items: baseline; justify-content: space-between;
    gap: var(--sp-2);
}
.check-head .block-title { margin: 0; font-size: var(--fs-body); }
.check-count {
    flex: none; font-size: var(--fs-small); font-weight: 600;
    color: var(--text-light); font-variant-numeric: tabular-nums;
}
.block-checklist .check-row {
    display: flex; align-items: center; gap: 10px; flex-wrap: nowrap;
    margin: 0; padding: 5px 0; font-size: var(--fs-body);
}
/* Tachado Y apagado. Es la misma información que la casilla puesta, a
   propósito: con veintidós ítems «qué falta» se lee por el tachado de un
   vistazo, y contando casillas marcadas no. */
.block-checklist .check-row.is-done span {
    text-decoration: line-through; color: var(--text-faint);
}

.block-quiz-rule, .block-upload-rule {
    margin: 0 0 var(--sp-3); font-size: var(--fs-small); color: var(--text-light);
}
.block-quiz-card {
    margin: 0 0 var(--sp-4); padding: var(--sp-4);
    border: 1px solid var(--border); border-radius: var(--radius-card);
}
.block-quiz-question {
    padding: 0; margin-bottom: var(--sp-3); font-size: var(--fs-body);
    font-weight: 600; color: var(--text);
}
.block-quiz-image {
    display: block; max-width: 100%; height: auto;
    border-radius: var(--radius-field); margin-bottom: var(--sp-3);
}
.block-quiz-score {
    margin: 0; padding: var(--sp-3) var(--sp-4);
    border-radius: var(--radius-card); font-size: var(--fs-body);
    font-weight: 600;
}
.block-quiz-score.is-passed { background: var(--ok-bg); color: var(--ok-text); }
.block-quiz-score.is-failed { background: var(--danger-bg); color: var(--danger-text); }

.block-upload-list {
    margin: 0 0 var(--sp-3); padding-left: var(--sp-5);
    font-size: var(--fs-small);
}
.block-survey .field { margin-bottom: var(--sp-3); }

/* ── El runner de la inducción ───────────────────────────────────────────────
   Los pasos son tarjetas apiladas y no un acordeón: quien hace la inducción la
   lee de arriba abajo una vez, y esconder lo ya hecho detrás de un clic
   obligaría a abrir cinco cosas para encontrar dónde iba. */
.run-meter { margin-bottom: var(--sp-5); }
.run-steps { display: flex; flex-direction: column; gap: var(--sp-4); }
.run-step { padding: var(--sp-5); }
.run-step-body { margin-top: var(--sp-4); }
/* El paso cerrado se apaga, no se esconde: tiene que verse que existe y que
   falta, o la inducción parece más corta de lo que es. */
.run-step.is-locked { opacity: .72; }
.run-step-locked {
    margin: var(--sp-3) 0 0; font-size: var(--fs-small);
    color: var(--text-light);
}
.run-step.is-done { border-color: var(--ok-text); }
/* Varias inducciones a la vez es el caso normal: en LTDC hay 35 para 32
   personas, porque el pre-ingreso y la de barista conviven. */
.run-block { margin-bottom: var(--sp-7); }
.run-block:last-child { margin-bottom: 0; }

/* ── Las inducciones DENTRO de la ficha ──────────────────────────────────────
   Una tarjeta por inducción, y la audiencia decide la forma: la de la persona
   sale compacta y la de administración sale abierta y marcable. */
.induccion-card .card-header h2 {
    display: flex; align-items: center; gap: var(--sp-2); min-width: 0;
}
.card-title-icon {
    width: 18px; height: 18px; flex: none; color: var(--text-light);
}

/* El paso NO vuelve a ser una tarjeta adentro de la tarjeta de la inducción.
   Una caja dentro de otra a un píxel de distancia es el defecto que ya se
   corrigió en la barra de filtros y en `.table-wrap`: acá el paso es una
   franja, separada por una línea y no por un segundo contorno. */
.induccion-card .run-steps { gap: 0; }
.induccion-card .run-step {
    border: 0; border-radius: 0; box-shadow: none; background: none;
    padding: var(--sp-4) 0; border-top: 1px solid var(--border);
}
/* `is-done` pinta el borde del paso, y acá ese borde es la línea que lo separa
   del anterior: dejarla verde marcaría la separación y no el paso. */
.induccion-card .run-step.is-done { border-top-color: var(--border); }

/* La forma compacta: una línea por paso, con lo que la persona entregó. */
.step-list { list-style: none; margin: 0; padding: 0; }
.step-line {
    display: flex; align-items: flex-start; gap: var(--sp-3);
    padding: var(--sp-3) 0; border-top: 1px solid var(--border);
}
.step-line:first-child { border-top: 0; padding-top: 0; }
.step-line-mark { color: var(--text-light); line-height: 1.4; }
.step-line.is-done .step-line-mark { color: var(--ok-text); }
.step-line-text {
    display: flex; flex-direction: column; gap: var(--sp-2); min-width: 0;
}
.step-line-title { font-weight: 600; }
.step-line-when { font-size: var(--fs-small); color: var(--text-light); }
/* La nota, al lado de la fecha. Verde solo cuando APROBÓ: un 60% en verde se
   lee como que pasó, y la nota mínima la pone quien arma el examen. */
.step-line-mark-score { color: var(--text); }
.step-line-mark-score.is-passed { color: var(--ok-text); }
/* Los archivos envuelven: un paso migrado de `tiendapp` trae hasta ocho, y en
   una sola línea el octavo se sale de la tarjeta. */
.step-line-files { display: flex; flex-wrap: wrap; gap: var(--sp-2); }

/* La acción que no pertenece a ninguna de las tarjetas de la lista. */
.section-action { margin: 0; }

/* ── Los roles del integrante, en el riel ────────────────────────────────────
   Nombre a la izquierda y «Quitar» a la derecha, en una fila por rol: en dos
   líneas el botón se lee como si fuera de la tarjeta y no del rol. */
.role-list { list-style: none; margin: 0 0 var(--sp-3); padding: 0; }
.role-line {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--sp-3); padding: var(--sp-2) 0;
    border-top: 1px solid var(--border);
}
.role-line:first-child { border-top: 0; }
.role-line-name { font-weight: 600; min-width: 0; }
/* El desplegable cede y el botón no: con 312px de riel, un nombre de rol largo
   empujaba «Asignar» fuera de la tarjeta. */
.role-assign { display: flex; gap: var(--sp-2); align-items: center; }
.role-assign select { min-width: 0; flex: 1; }
.role-assign .btn { flex: none; }

/* El enlace de invitación se copia a mano —Agrupa no manda correo—, así que el
   control existe para SELECCIONARLO, no para escribir en él. */
.input-copy {
    width: 100%; font-size: var(--fs-small);
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
/* Un formulario POR BLOQUE: un paso de tiendapp tiene hasta 21, y con uno solo
   marcar una casilla mandaría también el examen a medio contestar. */
.block-form { margin-bottom: var(--sp-5); }
.block-form:last-child { margin-bottom: 0; }
.block-form .block { margin-bottom: var(--sp-3); }
.block-form-foot { display: flex; justify-content: flex-end; }

/* ── El editor de pasos de un flujo ──────────────────────────────────────────
   Cada paso es una fila con su información a la izquierda y sus controles a la
   derecha, y los controles son BOTONES y no arrastre: reordenar tiene que
   funcionar con teclado. */
.step-row { display: flex; align-items: center; gap: var(--sp-3); }
.step-row-main { flex: 1; min-width: 0; }
.step-row-tools { display: flex; align-items: center; gap: 4px; }
/* Un paso inactivo se ve apagado y no se esconde: tiene que notarse que existe
   y que no cuenta, o nadie entiende por qué el total no cuadra. */
.step-row.is-off { opacity: .6; }
.step-add { margin-top: var(--sp-4); }
.step-add-row { display: flex; gap: var(--sp-2); }
.step-add-row input { flex: 1; }
/* Los formularios de las flechas son de un botón: sin esto cada uno ocupa su
   propia línea y los controles se apilan en vertical. */
.step-row-tools form { display: inline; }

/* ── El selector de bloques ──────────────────────────────────────────────────
   Sin una línea de JavaScript, con el mismo truco que `ui/_help.html`: una
   casilla oculta como interruptor. Un selector que no abre deja el editor
   inutilizable —no degradado, inutilizable— y el protocolo de la plataforma
   pide que todo sirva sin script. */
.picker { display: inline-block; }
.picker-toggle { position: absolute; opacity: 0; width: 0; height: 0; }
.picker-open { cursor: pointer; }
.picker-sheet { display: none; }
.picker-toggle:checked ~ .picker-sheet { display: block; }
/* La casilla está OCULTA pero presente: la recibe el tabulador y la barra
   espaciadora la acciona, así que el selector se abre con teclado. El foco se
   dibuja sobre el rótulo, que es lo que se ve. */
.picker-toggle:focus-visible + .picker-open {
    outline: 2px solid var(--primary); outline-offset: 2px;
}
.picker-backdrop {
    position: fixed; inset: 0; z-index: 40; background: rgba(13, 11, 22, .45);
}
.picker-panel {
    position: fixed; z-index: 41; top: 50%; left: 50%;
    transform: translate(-50%, -50%);
    width: min(520px, calc(100vw - var(--sp-6)));
    max-height: calc(100vh - var(--sp-6)); overflow-y: auto;
    background: var(--card-bg); border-radius: var(--radius-lg);
    padding: var(--sp-5); box-shadow: 0 24px 60px rgba(13, 11, 22, .28);
}
.picker-head {
    display: flex; align-items: center; justify-content: space-between;
    margin-bottom: var(--sp-4);
}
.picker-title { font-size: var(--fs-section); font-weight: 700; }
.picker-close {
    cursor: pointer; font-size: var(--fs-page); line-height: 1;
    color: var(--text-light); padding: 0 6px;
}
/* Tres columnas, como la referencia. En un teléfono son dos: con tres, cada
   tarjeta queda de 90px y el rótulo se parte en dos líneas. */
.picker-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--sp-3); }
@media (max-width: 520px) { .picker-grid { grid-template-columns: repeat(2, 1fr); } }
.picker-card {
    display: flex; flex-direction: column; align-items: center; gap: 10px;
    padding: var(--sp-4) var(--sp-2); border: 1px solid var(--border);
    border-radius: var(--radius-card); background: var(--card-bg);
    cursor: pointer; color: var(--text); font-size: var(--fs-body);
    font-weight: 600; font-family: inherit;
}
.picker-card:hover {
    border-color: var(--primary); background: var(--primary-soft);
    color: var(--primary-dark);
}
.picker-card-icon { width: 24px; height: 24px; color: var(--primary); }
.picker-card-name { text-align: center; }

/* ── El pie de una tarjeta de formulario ─────────────────────────────────────
   La casilla a la izquierda y el guardar a la derecha, separados por una línea
   del resto: es lo último que se decide antes de guardar. Arriba, entre los
   campos, «Formulario activo» se leía como un dato más de la ficha. */
.card-foot {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--sp-3); flex-wrap: wrap;
    margin-top: var(--sp-4); padding-top: var(--sp-4);
    border-top: 1px solid var(--border);
}
.card-foot .field-check { margin: 0; }
.field-required { color: var(--danger-text); }

/* ── El desbloqueo de un paso ────────────────────────────────────────────────
   Una FILA y no una tarjeta con dos controles sueltos: es una sola decisión
   —cuándo abre— y partida en dos campos se leía como dos. */
.unlock-row {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--sp-3); flex-wrap: wrap; padding: var(--sp-4) var(--sp-5);
}
.unlock-label {
    display: inline-flex; align-items: center; gap: var(--sp-2);
    font-weight: 600; color: var(--text);
}
.unlock-label label { margin: 0; }
.unlock-icon { width: 18px; height: 18px; color: var(--primary); flex-shrink: 0; }
.unlock-controls { display: inline-flex; align-items: center; gap: var(--sp-2); }
/* El rótulo mas largo es «A las N horas de arrancar la inducción», y a 200px
   se cortaba. `max-content` deja que el `<select>` mida lo que su opción mas
   larga necesita: es un conjunto cerrado de tres, no una lista que crezca. */
.unlock-controls select { width: max-content; min-width: 200px; }
.unlock-unit { font-size: var(--fs-small); color: var(--text-light); }

/* Las horas SOLO se teclean en el modo que las usa. Las otras dos —«al
   completar el paso anterior» y «al día siguiente»— no llevan número, y el
   campo encendido al lado invitaba a escribir algo que el servidor descarta.

   El apagado se ve en las DOS piezas, el campo y su unidad: apagar solo el
   campo dejaba un «horas» a plena tinta al lado de una casilla gris, que se
   lee como que la unidad sigue contando. */
.unlock-controls input:disabled {
    background: var(--bg); color: var(--text-faint);
    border-color: var(--border); cursor: not-allowed;
}
/* La unidad se apaga con su campo, por hermano adyacente. Sin esto quedaba un
   «horas» a plena tinta al lado de una casilla gris, que se lee como que la
   unidad sigue contando. Por CSS y no con un `:class` de Alpine: el estado ya
   lo dice el `disabled`, y un segundo binding es una copia que se
   desincroniza. */
.unlock-controls input:disabled + .unlock-unit { color: var(--text-faint); }

/* ── Las pestañas de los pasos ───────────────────────────────────────────────
   Cada pestaña es un ENLACE y no un interruptor de JavaScript, por la misma
   razón que el guardado es por bloque: el contenido de cada paso lo trae el
   servidor. Pintar los cinco a la vez para que el cambio fuera instantáneo
   serían ~100 editores de texto en el DOM — «Inducción Barista» tiene 30+
   bloques ella sola.

   La tira desborda a lo ancho y no se parte en dos filas: con cinco pasos de
   título largo, envolver mueve el editor hacia abajo cada vez que se renombra
   uno. */
.step-tabs {
    display: flex; align-items: center; gap: 6px;
    margin: var(--sp-5) 0 var(--sp-4); padding-bottom: var(--sp-2);
    overflow-x: auto; scrollbar-width: thin;
}
.step-tab {
    display: inline-flex; align-items: center; gap: var(--sp-2);
    flex-shrink: 0; max-width: 260px; padding: 10px var(--sp-4);
    border: 1px solid var(--border); border-radius: var(--radius-field);
    background: var(--card-bg); color: var(--text-light);
    font-size: var(--fs-body); font-weight: 600; text-decoration: none;
}
.step-tab:hover { background: var(--bg); color: var(--text); }
/* Las pestañas DENTRO del panel del editor de bloques: envuelven en vez
   de correrse a un lado.

   `overflow-x: auto` es lo correcto en la pantalla de integrantes, donde
   la tira ocupa el ancho entero; acá el panel mide ~315px y con tres
   pasos el «+ Paso» queda detrás de un scroll horizontal que nadie
   descubre — la acción principal de la tira, escondida. */
.vento-customizer__panel .step-tabs {
    flex-wrap: wrap; overflow-x: visible;
}
.vento-customizer__panel .step-tab { max-width: 100%; }
.step-tab.is-active {
    background: var(--primary); border-color: var(--primary); color: #fff;
}
/* El número va en su propia pastilla: con cinco pasos que empiezan todos por
   «Día», el número es lo único que los distingue de un vistazo. */
.step-tab-num {
    display: inline-flex; align-items: center; justify-content: center;
    flex-shrink: 0; width: 22px; height: 22px; border-radius: 8px;
    background: var(--bg); color: var(--text-light);
    font-size: var(--fs-small); font-weight: 700;
}
.step-tab.is-active .step-tab-num {
    background: rgba(255, 255, 255, .25); color: #fff;
}
/* El título se recorta y no se parte: una pestaña de dos líneas empuja toda la
   tira, y el nombre completo ya está en la cabecera del paso. */
.step-tab-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* La × vive PEGADA a la pestaña activa y encima de ella, no en una columna
   propia: en una columna, la tira crecería y encogería al cambiar de pestaña y
   los títulos bailarían de sitio. */
.step-tab-slot { position: relative; display: inline-flex; flex-shrink: 0; }
.step-tab-slot.is-active .step-tab { padding-right: var(--sp-6); }
.step-tab-close {
    position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
    display: inline-flex; align-items: center; justify-content: center;
    width: 20px; height: 20px; border-radius: 6px;
    color: rgba(255, 255, 255, .7); font-size: var(--fs-body); line-height: 1;
    text-decoration: none;
}
.step-tab-close:hover { background: rgba(255, 255, 255, .2); color: #fff; }
.step-tab-add-form { display: inline-flex; flex-shrink: 0; }
.step-tab-add {
    padding: 10px var(--sp-4); border: 1px dashed var(--border);
    border-radius: var(--radius-field); background: transparent; cursor: pointer;
    color: var(--primary); font-size: var(--fs-body); font-weight: 600;
    font-family: inherit;
}
.step-tab-add:hover { background: var(--primary-soft); border-color: var(--primary); }

/* ── El bloque dentro del editor ─────────────────────────────────────────── */
.block-editor-head { margin: var(--sp-6) 0 var(--sp-3); }
/* El bloque va sobre un fondo TENUE y no sobre la tarjeta blanca: un paso de
   `tiendapp` tiene hasta 21, y en blanco sobre blanco no se ve dónde termina
   uno y empieza el siguiente. El borde aparece al pasar por encima, que es lo
   que dice cuál se está por tocar. */
/* BLANCA, y con aire arriba además de abajo (pedido de Renzo, 2026-08-12).
   Era gris —`--bg`, el del lienzo— y sobre el lienzo gris de la pantalla los
   bloques se leían como huecos en la tarjeta en vez de como piezas encima de
   ella. `--card-bg` y no `#ffffff`: es la misma variable que usa `.card`, así
   que el día que el tema cambie, cambian juntas.

   El margen va en los DOS lados y no solo abajo. Acá sí es margen del hijo y
   no `gap` del padre —al revés que las tarjetas— porque entre dos bloques va
   `.block-between`, que es un elemento con su propia altura: con `gap` la
   separación se sumaría a la de él. */
.block-card {
    margin-top: var(--sp-3); margin-bottom: var(--sp-3);
    border: 1px solid transparent;
    background: var(--card-bg);
}
.block-card:hover { border-color: var(--border); }
/* Solo el borde: el fondo ya es blanco, así que la línea que cambiaba a
   `--card-bg` no hacía nada y hacía dudar de si el foco pintaba algo más. */
.block-card:focus-within { border-color: var(--primary); }
.block-card-head {
    display: flex; align-items: center; justify-content: space-between;
    margin-bottom: var(--sp-3);
}
/* El chip lleva el color PRIMARIO y no el del `badge-soft` genérico: ahí es
   verde, que en esta plataforma significa «completo», y un bloque recién
   agregado no está completo de nada. */
.block-card-kind {
    background: var(--primary-soft); color: var(--primary-dark);
    text-transform: uppercase; letter-spacing: .04em; font-size: var(--fs-small);
    font-weight: 700;
}
.block-card-tools { display: flex; align-items: center; gap: 4px; }
.block-card-tools form { display: inline; }
.block-edit { display: flex; flex-direction: column; gap: var(--sp-2); }
/* Vacío se ve PUNTEADO y atenuado; escrito, sólido. El título del bloque es
   opcional en los nueve tipos, y un campo vacío con borde sólido se lee como
   uno que falta llenar — de los 87 bloques reales, la mayoría no lo usa. */
.block-edit-title { font-weight: 600; }
.block-edit-title:placeholder-shown { border-style: dashed; opacity: .65; }
.block-edit-title:focus { border-style: solid; opacity: 1; }
.block-edit-html { font-family: ui-monospace, SFMono-Regular, monospace; }
.block-edit-row { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.block-edit-row .field-label { margin-bottom: 0; }
.block-edit-foot { display: flex; justify-content: flex-end; }
.block-edit-none { margin: 0; font-size: var(--fs-small); color: var(--text-light); }
/* El «+» entre dos bloques: la línea lo cruza, así que se lee como un punto de
   inserción y no como un botón suelto. */
/* El carril entre dos bloques, donde vive el «+». Era de 20px y con los
   bloques ya blancos y con su propio margen quedaba apretado: el botón tocaba
   las dos tarjetas a la vez y la línea punteada no se leía como un carril.

   48px y no los 50 que salieron de medir en el navegador: la rejilla de
   espaciado es de 4, la diferencia de dos píxeles no se ve, y una rejilla con
   una excepción deja de serlo al tercer componente — está escrito arriba de
   `--sp-1`, y es de donde salieron los 14 y los 18 de tiendapp. */
.block-between {
    display: flex; justify-content: center; align-items: center;
    height: calc(var(--sp-6) * 2); position: relative;
}
.block-between::before {
    content: ""; position: absolute; left: 0; right: 0; top: 50%;
    border-top: 1px dashed var(--border);
}
.block-between-btn {
    position: relative; z-index: 1; width: 26px; height: 26px;
    display: inline-flex; align-items: center; justify-content: center;
    border-radius: 50%; border: 1px solid var(--border);
    background: var(--card-bg); color: var(--text-light);
    font-size: var(--fs-body); line-height: 1;
}
.block-between-btn:hover { border-color: var(--primary); color: var(--primary); }
/* Los dos botones del pie van en COLUMNA y no centrados en una fila: son
   «Agregar bloque» y «Reordenar», los dos punteados y del mismo ancho, como la
   referencia. Antes había uno solo y centrado tenía sentido. */
.block-add-foot {
    display: flex; flex-direction: column; gap: var(--sp-3);
    margin-top: var(--sp-4);
}

/* ── El editor de texto rico ─────────────────────────────────────────────────
   Degrada a un textarea: sin JavaScript queda el HTML crudo, feo y guardable.
   Con `richtext.js` aparecen la barra y el área de TipTap, y el textarea se
   esconde — pero sigue siendo él quien viaja en el POST, así que el guardado es
   el mismo camino en los dos casos.

   `.richtext-raw` se esconde con una CLASE que agrega el script, no con
   `display:none` de entrada: sin script tiene que verse, y una regla estática
   dejaría el campo invisible justo cuando es lo único que hay. */
.richtext { border: 1px solid var(--border); border-radius: var(--radius-field); }
.richtext-bar {
    display: flex; flex-wrap: wrap; align-items: center; gap: 2px;
    padding: 6px; border-bottom: 1px solid var(--border); background: var(--bg);
    border-radius: var(--radius-field) var(--radius-field) 0 0;
}
.richtext-btn {
    min-width: 28px; height: 28px; padding: 0 6px; border: 0; cursor: pointer;
    background: transparent; border-radius: var(--radius-field);
    color: var(--text); font-size: var(--fs-body); font-family: inherit;
}
.richtext-btn:hover { background: var(--primary-soft); color: var(--primary-dark); }
.richtext-btn.is-active { background: var(--primary-soft); color: var(--primary-dark); }
.richtext-sep { width: 1px; height: 18px; margin: 0 4px; background: var(--border); }
/* `background-color` y NO el atajo, y 30px a la derecha: la flecha del `select`
   la dibuja una regla de elemento —la menos específica de la hoja— así que el
   atajo se la borra, y sin el sitio una opción larga se le mete por debajo.
   Las dos cosas las vigila `tests/test_stylesheet.py`. */
/* `width: auto` y no heredado: el editor vive dentro de un `.field`, y
   `.field select { width: 100% }` estiraba este selector a lo ancho de la
   columna. La barra terminaba pareciendo un formulario en vez de una barra —
   se vio en la pantalla, no en una prueba. */
.richtext-select {
    width: auto; height: 28px; padding: 0 30px 0 var(--sp-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-field); background-color: var(--card-bg);
    font-size: var(--fs-small); font-family: inherit; color: var(--text);
}
/* El HTML crudo va a la DERECHA y separado: no es una opción de formato, es
   cambiar de modo. Mezclarlo con las negritas invita a tocarlo sin querer. */
.richtext-html {
    margin-left: auto; padding: 0 var(--sp-3); border: 1px solid var(--border);
    font-size: var(--fs-small); font-weight: 600; color: var(--text-light);
}
.richtext-area {
    padding: var(--sp-3); min-height: 120px; font-size: var(--fs-body);
    line-height: 1.65; color: var(--text);
}
.richtext-area:focus, .richtext-area .ProseMirror:focus { outline: none; }
.richtext:focus-within { border-color: var(--primary); }
.richtext-area p { margin: 0 0 var(--sp-3); }
.richtext-area ul, .richtext-area ol { margin: 0 0 var(--sp-3); padding-left: var(--sp-5); }
.richtext-area blockquote {
    margin: var(--sp-3) 0; padding-left: var(--sp-3);
    border-left: 3px solid var(--primary); color: var(--text-light);
}
.richtext-area img { max-width: 100%; height: auto; border-radius: var(--radius-field); }
/* Las tablas del recetario tienen seis columnas y no caben en un teléfono, así
   que el área desborda a lo ancho en vez de romper la página. */
.richtext-area table { border-collapse: collapse; width: 100%; margin: var(--sp-3) 0; }
.richtext-area th, .richtext-area td {
    border: 1px solid var(--border); padding: var(--sp-2) var(--sp-3);
    font-size: var(--fs-small); vertical-align: top;
}
.richtext-area th { background: var(--bg); font-weight: 600; }
.richtext-area .selectedCell { background-color: var(--primary-soft); }
.richtext-raw {
    width: 100%; border: 0; border-radius: 0 0 var(--radius-field) var(--radius-field);
    font-family: ui-monospace, SFMono-Regular, monospace; font-size: var(--fs-small);
}
.richtext-raw-hidden { display: none; }
.richtext-raw-hidden.richtext-raw-open { display: block; }

/* El selector de emoji. Va pegado al botón y NO es un modal: interrumpir la
   escritura para poner una carita es peor que no tener el botón. */
/* UN panel anclado para el emoji y para los cinco formatos. Antes eran cinco
   `<select>`: dentro de un `.field` del formulario se estiran a lo ancho, y la
   barra terminaba más alta que el área de escritura.

   Columna por omisión —una opción por renglón, legible— y las dos variantes
   que necesitan rejilla la piden por su clase. */
.richtext-pop {
    position: absolute; z-index: 60; display: flex; flex-direction: column;
    /* 340 y no 260: con la sección de marca, la paleta de veintitrés y el
       selector libre, el tope viejo dejaba «Quitar el color» abajo del
       borde. Una opción que no se ve es una opción que no existe. */
    gap: 1px; min-width: 132px; max-height: 340px; overflow-y: auto;
    padding: var(--sp-2); background: var(--card-bg);
    border: 1px solid var(--border); border-radius: var(--radius-card);
    box-shadow: var(--shadow-lg);
}
.richtext-pop.is-emoji { flex-flow: row wrap; gap: 2px; max-width: 264px; }

/* El panel de color va por SECCIONES —los de la marca primero, después la
   paleta— porque un correo de la empresa se escribe en los colores de la
   empresa, y buscarlos entre veintitrés genéricos es no tenerlos. */
.richtext-pop-title {
    margin: var(--sp-1) 0 2px; padding: 0 var(--sp-1);
    font-size: var(--fs-label); font-weight: 600; color: var(--text-light);
}
.richtext-pop-title:first-child { margin-top: 0; }
.richtext-swatches {
    display: grid; grid-template-columns: repeat(6, 1fr); gap: 3px;
    max-width: 216px;
}

/* El selector libre del navegador: la etiqueta ocupa la fila y el campo es el
   cuadro de color a la derecha. Se estila como una opción más para que no se
   lea como un control ajeno metido en el panel. */
.richtext-free { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); cursor: pointer; }
.richtext-free-input {
    width: 26px; height: 22px; padding: 0; border: 1px solid var(--border);
    border-radius: var(--radius-field); background: none; cursor: pointer;
}

/* Cada opción SE MUESTRA A SÍ MISMA: el tamaño a su tamaño, la tipografía en
   su tipografía. Por eso el estilo en línea lo pone el script — es el dato,
   no decoración, y una clase por cada valor sería una hoja imposible. */
.richtext-pick {
    padding: var(--sp-1) var(--sp-2); border: 0; cursor: pointer;
    background: transparent; border-radius: var(--radius-field);
    color: var(--text); font-size: var(--fs-small); font-family: inherit;
    text-align: left; line-height: 1.4;
}
.richtext-pick:hover { background: var(--bg); }
.richtext-pop.is-emoji .richtext-pick {
    width: 32px; height: 32px; padding: 0; text-align: center;
    font-size: var(--fs-glyph);
}

/* Un color se elige VIÉNDOLO. El borde va por dentro para que un color claro
   siga teniendo forma sobre el fondo de la tarjeta. */
.richtext-swatch {
    width: 30px; height: 30px; border: 0; cursor: pointer;
    border-radius: var(--radius-field); box-shadow: inset 0 0 0 1px rgb(0 0 0 / .12);
}
.richtext-swatch:hover { box-shadow: inset 0 0 0 1px rgb(0 0 0 / .35); }

/* La «A» del color lleva debajo la raya de lo que aplica. */
.richtext-color { font-weight: 700; text-decoration: underline; text-underline-offset: 3px; }

/* ── Las filas repetibles de un editor ───────────────────────────────────────
   Una fila vacía al final es la que ofrece agregar la siguiente: agregar es
   escribir y guardar, sin un botón aparte y sin JavaScript. */
.rows { display: flex; flex-direction: column; gap: var(--sp-2); }
/* Cada fila es una TARJETA y no dos controles sueltos en una línea. Con doce
   ítems —el checklist de presentación personal tiene doce— sin el recuadro no
   se ve dónde termina un ítem y empieza el siguiente, que es el mismo problema
   que `.block-card` resuelve un nivel más arriba. */
.rows-row {
    display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
    padding: var(--sp-2) var(--sp-3); border: 1px solid var(--border);
    border-radius: var(--radius-field); background: var(--card-bg);
}
.rows-row:focus-within { border-color: var(--primary); }
.rows-row > input[name$="label"], .rows-row > input[name$="text"],
.rows-row > input[name$="url"], .rows-row > input[name$="alt"] {
    flex: 1; min-width: 180px;
}
/* La pastilla de `.field-check` está calibrada para un formulario, donde hay
   una o dos. Acá hay UNA POR FILA —doce en el checklist más largo— y a ese
   ritmo la pastilla pesa más que el ítem que califica. Adentro de `.rows` se
   desarma y queda la casilla con su rótulo. */
.rows-row .field-check {
    margin: 0; padding: 0; gap: 6px; border: 0; background: transparent;
    white-space: nowrap;
}
.rows-row .field-check:hover { border-color: transparent; }
.rows-row .field-check span { font-size: var(--fs-small); color: var(--text-light); }

/* Los controles de un editor de bloque no viven adentro de un `.field`, así
   que no heredan su ancho. Sin esto, un textarea de instrucciones queda del
   tamaño que el navegador decida —20 columnas— al lado de campos que llenan
   la tarjeta. */
.block-edit > input, .block-edit > textarea, .block-edit > select { width: 100%; }
.block-edit textarea { min-height: 60px; resize: vertical; font-family: inherit; }
/* Los tres números del bloque de carga no llenan la fila: un campo de ancho
   completo para un «10» se lee como si esperara un párrafo. */
.block-edit .input-narrow { width: 88px; flex: none; }

/* ── La galería del bloque imagen ────────────────────────────────────────────
   Las que ya están, con su texto alternativo y su casilla de conservar. Quitar
   una es DESMARCARLA: la URL de un archivo subido no la escribió nadie a mano,
   y pedir que se borre a mano es pedir que se edite una cadena de sesenta
   caracteres sin equivocarse. */
.gallery {
    display: grid; gap: var(--sp-3);
    grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
}
.gallery-item {
    display: flex; flex-direction: column; gap: var(--sp-2); margin: 0;
    padding: var(--sp-2); border: 1px solid var(--border);
    border-radius: var(--radius-field); background: var(--card-bg);
}
.gallery-thumb {
    width: 100%; height: 100px; object-fit: cover;
    border-radius: var(--radius-field); background: var(--bg);
}
.gallery-alt { font-size: var(--fs-small); }
.gallery-keep {
    display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
    font-size: var(--fs-small); color: var(--text-light);
}
/* Desmarcada, la tarjeta se apaga: es lo que dice que al guardar se va, y sin
   la señal la casilla es un cambio sin consecuencia visible. */
.gallery-item:has(.gallery-keep input:not(:checked)) { opacity: .45; }

/* ── La previa del bloque video ──────────────────────────────────────────── */
.video-preview {
    display: flex; align-items: center; justify-content: center;
    aspect-ratio: 16 / 9; max-width: 320px; overflow: hidden;
    border-radius: var(--radius-field); background: var(--canvas);
}
.video-preview-frame { width: 100%; height: 100%; }
.video-preview-link {
    padding: var(--sp-3); color: #fff; font-size: var(--fs-small);
    text-align: center; word-break: break-all;
}

/* ── La pastilla que se ELIGE ────────────────────────────────────────────────
   Cada una es una CASILLA con su rótulo y no un botón con estado en
   JavaScript: así se marca con teclado, viaja en el formulario y no hace falta
   una línea de script. La casilla se esconde y el rótulo entero es el control.

   Se llama `choice-pill` y no `pill` a secas, y la lección vale más que el
   nombre: nació como `.pill` y colisionó con la pastilla de ESTADO que el ERP
   usa en catálogo, ubicaciones y la lista de formularios. El resultado fue que
   «ACTIVO» pasó a verse como un botón enorme con borde en pantallas que esta
   fase no tocó. Un nombre genérico en una hoja global no es un nombre: es una
   apuesta a que nadie más lo haya pensado. */
.choice-pills { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.choice-pill {
    display: inline-flex; align-items: center; cursor: pointer;
    padding: 6px var(--sp-4); border: 1px solid var(--border);
    border-radius: 999px; background: var(--card-bg);
    font-size: var(--fs-small); font-weight: 500; color: var(--text-light);
}
.choice-pill input { position: absolute; opacity: 0; width: 0; height: 0; }
/* El glifo de la pastilla, cuando la elección se reconoce mirando. Hereda el
   color del rótulo —`currentColor`— y por eso se vuelve blanco solo cuando la
   pastilla se rellena: pintarlo aparte habría dejado dos sitios donde decir de
   qué color está lo elegido. `gap` acá y no en `.choice-pill` porque una
   pastilla SIN glifo no tiene nada que separar. */
.choice-pill:has(.choice-pill-icon) { gap: var(--sp-2); }
.choice-pill-icon { width: 16px; height: 16px; flex: none; }
.choice-pill:hover { border-color: var(--primary); color: var(--primary-dark); }
.choice-pill:has(input:checked) {
    background: var(--primary); border-color: var(--primary); color: #fff;
}
/* El foco se dibuja en la PASTILLA porque la casilla es invisible: sin esto,
   quien navega con teclado no ve dónde está. */
.choice-pill:focus-within { outline: 2px solid var(--primary); outline-offset: 2px; }
.quiz-card { display: flex; flex-direction: column; gap: var(--sp-2); }
.quiz-card-text { flex: 1; min-width: 200px; }

/* ── El editor de examen ─────────────────────────────────────────────────────
   Dos interfaces sobre el mismo contrato: la lista con modal cuando hay
   script, y el `fieldset` de respaldo cuando no. Alpine DESHABILITA el
   respaldo, y eso es lo que impide que el servidor reciba cada pregunta dos
   veces — un campo deshabilitado no viaja en el POST, uno escondido sí. */
.quiz-fallback { margin: 0; padding: 0; border: 0; }
.quiz-fallback[disabled] { display: none; }
.quiz-settings { display: flex; align-items: center; gap: var(--sp-2); }

/* ── Una lista cuyas filas son CAJAS ───────────────────────────────────────
   Cada fila con su borde y su radio, separadas por un hueco: es lo que hace
   que una fila se lea como una cosa entera y no como un renglón de una tabla.
   Va donde la fila TIENE PARTES —un glifo, un rótulo, un valor, sus botones—;
   donde es un dato plano por columna, la pieza es `.card-lines`.

   Se llamaba `.quiz-list`/`.quiz-row` mientras su único consumidor era el
   editor de examen. El segundo —las condiciones de una colección, 2026-08-16—
   no es un examen, y una clase con el nombre de su primer caso es la que
   después nadie reusa: se copia la regla con otro nombre y quedan dos formas
   de la misma fila. Los hijos `quiz-row-*` NO se promueven: el número de la
   pregunta y su asa de arrastre sí son del examen. */
.boxed-list { display: flex; flex-direction: column; gap: 6px; }
.boxed-row {
    display: flex; align-items: center; gap: var(--sp-3);
    padding: var(--sp-2) var(--sp-3); border: 1px solid var(--border);
    border-radius: var(--radius-field); background: var(--card-bg);
}
.quiz-row-num {
    display: inline-flex; align-items: center; justify-content: center;
    flex-shrink: 0; width: 24px; height: 24px; border-radius: 8px;
    background: var(--primary); color: #fff;
    font-size: var(--fs-small); font-weight: 700;
}
/* El ícono del TIPO, antes del chip. El chip obliga a leer para saber de cuál
   se trata, y una lista de 25 se recorre mirando. */
.quiz-row-icon { width: 18px; height: 18px; flex-shrink: 0; color: var(--primary); }
.quiz-row-kind {
    flex-shrink: 0; padding: 3px var(--sp-3); border-radius: 999px;
    background: var(--bg); color: var(--text-light);
    font-size: var(--fs-small); font-weight: 600;
}
/* El texto se recorta en UNA línea: una pregunta de 140 caracteres en una
   lista de 25 convierte la lista en un muro y se pierde el orden, que es lo
   único que esta pantalla sirve para ver. */
.quiz-row-text {
    flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;
    white-space: nowrap; font-size: var(--fs-body);
}
.quiz-row-tools { display: flex; gap: 4px; flex-shrink: 0; }
.quiz-add {
    align-self: flex-start; padding: var(--sp-2) var(--sp-3); border: 0;
    background: transparent; cursor: pointer; color: var(--primary);
    font-size: var(--fs-small); font-weight: 600; font-family: inherit;
    border-radius: var(--radius-field);
}
.quiz-add:hover { background: var(--primary-soft); }

/* El modal es un `<dialog>`: el navegador le da el fondo, el atrapado del foco
   y el cierre con Escape. Es el mismo elemento que `ui/_confirm.html` usa. */
.quiz-modal {
    width: min(560px, calc(100vw - var(--sp-6))); max-height: 88vh;
    padding: 0; border: 1px solid var(--border);
    border-radius: var(--radius-lg); background: var(--card-bg);
    color: var(--text); box-shadow: var(--shadow-lg);
}
.quiz-modal::backdrop { background: rgba(13, 11, 22, .45); }
/* El cuerpo NO usa `gap`: el ritmo lo dan los rótulos. Con un gap uniforme, la
   distancia entre un rótulo y SU campo era la misma que entre ese campo y el
   rótulo siguiente, así que las ocho secciones se leían como una lista de
   dieciséis cosas sueltas. */
.quiz-modal-body {
    display: flex; flex-direction: column;
    max-height: 88vh; overflow-y: auto; padding: 0 var(--sp-5) var(--sp-5);
}
/* La cabecera se queda arriba al desplazar: con ocho secciones el modal
   desborda, y sin esto el «cerrar» se va de pantalla. */
.quiz-modal-head {
    position: sticky; top: 0; z-index: 1;
    display: flex; align-items: center; justify-content: space-between;
    margin: 0 calc(var(--sp-5) * -1) var(--sp-5);
    padding: var(--sp-5); border-bottom: 1px solid var(--border);
    background: var(--card-bg);
}
.quiz-modal-head h3 { margin: 0; font-size: var(--fs-section); font-weight: 700; }
/* La baldosa y el título, juntos y a la izquierda; la × se queda a la derecha
   por el `space-between` del encabezado. */
.quiz-modal-head-lead { display: flex; align-items: center; gap: var(--sp-3); }

/* La pista bajo un RÓTULO del modal: qué es esta sección y para qué sirve. Va
   pegada hacia arriba porque explica al rótulo que tiene encima.

   La que va debajo de un CONTROL —«Pega la URL de una imagen pública»— NO usa
   esta clase sino `.field-hint`, que es la de la casa para eso. Usaban la
   misma y con el margen negativo la de abajo quedaba montada sobre su input:
   una clase con dos papeles opuestos no puede acertar en los dos. */
.quiz-modal-hint {
    margin: -2px 0 var(--sp-3);
    font-size: var(--fs-small); color: var(--text-light);
}

/* ── La raya entre secciones del modal ─────────────────────────────────────
   El modal tiene ocho secciones y ninguna frontera: «Explicación» arrancaba
   pegada a «Respuesta correcta» y las dos se leían como una sola.

   El aire va en la RAYA y no en los rótulos, y por eso es una etiqueta y no un
   `border-top`: puesta como borde de la sección siguiente, cada una tendría
   que acordarse de llevarlo —y las que se pintan condicionalmente estrenarían
   una raya colgando cuando su tipo de tarjeta no las muestra—. Como `<hr>`
   dentro del bloque condicional, aparece y desaparece con él.

   El margen es asimétrico a propósito: más arriba que abajo, para que la raya
   quede más cerca del rótulo que abre que del contenido que cierra. Es lo que
   hace que se lea como «empieza acá» y no como «terminó allá». */
.modal-rule {
    height: 0; border: 0; border-top: 1px solid var(--border);
    margin: var(--sp-5) 0 var(--sp-4);
}
/* Sin raya arriba del todo: la primera sección ya la separa el encabezado. */
.quiz-modal-body > .modal-rule:first-of-type { margin-top: var(--sp-4); }

/* El motivo por el que no se pudo guardar, pegado al pie. `.field-error` ya lo
   pinta en rojo; acá sólo se le da el aire que necesita separado del último
   campo y del botón. */
.quiz-modal-error { margin: var(--sp-3) 0 0; }

/* El aviso de una subida fallida en el editor de texto, debajo de su barra.
   Lo crea `richtext.js`, así que no aparece en ninguna plantilla: la guarda de
   la hoja mira plantilla→CSS y por eso no lo reclamaría, pero sin regla se
   pintaría con el tamaño y el color del cuerpo — que es exactamente el defecto
   que `field-hint` y `cell-sub` tuvieron antes. Lo rojo lo pone `.field-error`
   en la misma etiqueta; acá va sólo su sitio. */
.richtext-error { margin: 6px 0 0; }

/* ── El contador de caracteres, dentro de su caja ──────────────────────────
   `maxlength` impide pasarse pero no avisa de cuánto queda: quien escribe
   descubre el tope cuando el teclado deja de responder, y eso se lee como que
   la pantalla se colgó.

   DENTRO y no debajo: debajo empuja el campo siguiente y en un modal de ocho
   secciones eso son ocho renglones de más. El relleno de abajo del control le
   hace sitio, así que el texto nunca pasa por encima del número. */
.field-counted { position: relative; }
.field-counted textarea { padding-bottom: 22px; }
.field-counted input { padding-right: 62px; }
.field-count {
    position: absolute; right: 10px; bottom: 8px;
    font-size: var(--fs-meta); color: var(--text-faint);
    font-variant-numeric: tabular-nums; pointer-events: none;
}
/* En una línea el número va centrado en vertical: ahí no hay «abajo». */
.field-counted:has(input) .field-count { top: 50%; bottom: auto; transform: translateY(-50%); }

/* ── Las dos mitades de una elección binaria ───────────────────────────────
   Verdadera y Falsa reparten el ancho, como la referencia. Es un modificador
   de `.choice-pills` y no otra clase: el control sigue siendo el mismo par de
   radios —con teclado se recorren igual— y lo único que cambia es el reparto.

   El radio de píldora se cierra al de un control: dos mitades de 999px pegadas
   dejan un hueco con forma de lente en la costura. */
.choice-segmented { display: grid; grid-template-columns: 1fr 1fr; }
.choice-segmented .choice-pill {
    justify-content: center; gap: var(--sp-2);
    padding: 10px var(--sp-4); border-radius: var(--radius-field);
    font-size: var(--fs-body); font-weight: 600;
}
/* El visto de la elegida. El `<svg>` está SIEMPRE en el marcado y quien decide
   si se ve es el CSS, leyendo el mismo `input:checked` que pinta el fondo: una
   sola fuente de verdad, y un glifo de verdad en vez del aro que dibujaba un
   `::after` —que a este tamaño se leía como un radio sin marcar, justo lo
   contrario de lo que quiere decir. */
.choice-check { display: none; width: 16px; height: 16px; flex: none; }
.choice-segmented .choice-pill:has(input:checked) .choice-check { display: block; }

/* ── Los dos caminos de la imagen: pegar una dirección o subir un archivo ── */
.quiz-modal-url { flex: 1; min-width: 0; }
/* La «o» que los separa, en su disco. Sin el disco es una letra suelta entre
   dos controles y se lee como parte del de al lado. */
.quiz-modal-or {
    flex: none; display: grid; place-items: center;
    width: 26px; height: 26px; border-radius: 50%;
    background: var(--bg); border: 1px solid var(--border);
    font-size: var(--fs-small); color: var(--text-light);
}
.quiz-modal-drop-title { font-weight: 600; color: var(--text); }
.quiz-modal-drop-hint { font-size: var(--fs-meta); color: var(--text-light); }
/* El ancho y el respiro valen a CUALQUIER profundidad, no solo en los hijos
   directos. Los campos de cada tipo viven dentro de un `<template x-if>`, que
   agrega un `<div>` en el medio: con el selector limitado a `>`, «URL del
   video» y «Epígrafe» salían del ancho que el navegador les diera —unos
   200px— al lado de campos que sí llenaban la fila. Se veía como maquetación
   descuidada y era un selector demasiado estrecho. */
.quiz-modal-body input:not([type="radio"]):not([type="color"]),
.quiz-modal-body select,
.quiz-modal-body textarea { width: 100%; }
.quiz-modal-body select, .quiz-modal-body textarea,
.quiz-modal-body .quiz-modal-image-row,
.quiz-modal-body > input, .quiz-modal-body > div > input {
    margin-bottom: var(--sp-5);
}
/* El de una pareja o una opción respira menos: son una lista, y con el espacio
   grande se leen como campos sueltos. */
.quiz-modal-body .quiz-option input,
.quiz-modal-body .quiz-pair input { margin-bottom: 0; }
.quiz-modal-body .input-narrow { width: 120px; }
.quiz-modal-body .quiz-modal-image-row > input { margin-bottom: 0; }
.quiz-modal-body .quiz-modal-image { margin-bottom: var(--sp-5); }
.quiz-modal-image {
    max-width: 100%; max-height: 160px; align-self: flex-start;
    border-radius: var(--radius-field);
}
.quiz-modal-foot {
    display: flex; justify-content: flex-end; gap: var(--sp-2);
    margin-top: var(--sp-4); padding-top: var(--sp-4);
    border-top: 1px solid var(--border);
}
/* Los rótulos del modal pesan más que los de un formulario normal: son ocho en
   una columna angosta, y en gris claro la pantalla se lee como un borrador. */
/* Aire ARRIBA además de abajo. Los rótulos que no abren sección —Emoji,
   Subtítulo, Cuerpo, URL del video— no llevan raya delante, y sin margen
   propio quedaban pegados al control de encima.

   Y se cancela justo detrás de una raya, que ya puso el suyo: sumados daban
   el doble de aire en las cuatro secciones que sí la tienen, y una pantalla
   con dos ritmos distintos se lee como un error de alineación. */
.quiz-modal-label {
    display: block; margin: var(--sp-4) 0 7px;
    font-size: var(--fs-body); font-weight: 700; color: var(--text);
}
.modal-rule + .quiz-modal-label,
.modal-rule + .field-label .quiz-modal-label { margin-top: 0; }

/* La dirección a la izquierda y el cuadro punteado a la derecha, como la
   referencia. El cuadro es un BOTÓN y no una zona de arrastre: `_upload.html`
   trae su propio input de archivo, y adentro de un `<dialog>` que vive dentro
   del formulario del bloque ese input viajaría en el POST del examen. */
/* El campo se alinea ARRIBA y el cuadro es cuadrado. Estirado a la altura del
   input se veía como un botón raro; cuadrado se lee como zona de imagen, que
   es lo que la referencia hace. */
/* CENTRADO en vertical, no arriba: los tres —la dirección, la «o» y la zona—
   son tres caminos del mismo paso, y alineados arriba la «o» quedaba a la
   altura del rótulo del primero en vez de entre los dos. */
.quiz-modal-image-row {
    display: flex; align-items: center; gap: var(--sp-3);
}
/* La zona deja de ser un cuadro de 104 con un icono solo y pasa a DECIR qué
   es y qué acepta, como la referencia. En columna y más ancha: tres líneas
   apiladas en 104px de lado se parten en seis. */
.quiz-modal-drop {
    display: flex; flex-direction: column; align-items: center;
    justify-content: center; gap: 5px; flex-shrink: 0;
    width: 190px; padding: var(--sp-4) var(--sp-3);
    border: 2px dashed var(--border-strong);
    border-radius: var(--radius-card); background: transparent; cursor: pointer;
    color: var(--text-light); text-align: center;
}
.quiz-modal-drop:hover {
    border-color: var(--primary); color: var(--primary);
    background: var(--primary-soft);
}
.quiz-modal-drop svg { width: 20px; height: 20px; }

.quiz-option {
    display: flex; align-items: center; gap: var(--sp-3);
    margin-bottom: var(--sp-2);
}
.quiz-option > input[type="radio"] { flex-shrink: 0; }
.quiz-option > input:not([type]) { flex: 1; min-width: 0; }
/* La × de quitar una opción va ROJA y sin recuadro, como la referencia: dentro
   del modal es la única acción destructiva, y con borde competía visualmente
   con el campo que está al lado. */
.quiz-option-drop {
    display: grid; place-items: center; flex-shrink: 0;
    width: 28px; height: 28px; padding: 0; border: 0; cursor: pointer;
    background: transparent; color: var(--danger-text);
    border-radius: var(--radius-field);
}
.quiz-option-drop:hover { background: var(--danger-bg); }
.quiz-option-drop svg { width: 18px; height: 18px; }

/* Una pareja del emparejar: dos campos y el signo que los une. Se guardan como
   «izquierda = derecha» en una línea —que es lo que `read_form` lee— y se
   editan como dos, porque escribir el signo a mano es un formato que se
   equivoca a la tercera fila. */
.quiz-pair {
    display: flex; align-items: center; gap: var(--sp-2);
    margin-bottom: var(--sp-2);
}
.quiz-pair > input { flex: 1; min-width: 0; }
.quiz-pair-sign {
    flex-shrink: 0; color: var(--text-light); font-weight: 700;
}

/* ── Las tarjetas de contenido de un examen ──────────────────────────────────
   Portada, información, video, producto y cierre. Se ven distintas de una
   pregunta A PROPÓSITO: quien está presentando un examen tiene que poder
   distinguir de un vistazo lo que le piden contestar de lo que solo le
   muestran, y con la misma caja las cinco se leen como preguntas sin opciones. */
.quiz-cover {
    padding: var(--sp-6) var(--sp-5); margin-bottom: var(--sp-4);
    border-radius: var(--radius-lg); background: var(--primary-soft);
    text-align: center;
}
.quiz-cover.is-finish { background: var(--bg); }
/* El emoji de una portada usa `--fs-number`: ahí el emoji ES el contenido de
   la tarjeta, igual que una cifra en un tablero, y la escala está cerrada en
   nueve pasos a propósito —dos pasos nuevos para esto serían dos números
   sueltos con otro nombre—. */
.quiz-cover-emoji { font-size: var(--fs-number); display: block; margin-bottom: var(--sp-2); }
.quiz-cover-title { margin: 0; font-size: var(--fs-page); font-weight: 700; }
.quiz-cover-sub { margin: var(--sp-2) 0 0; color: var(--text-light); }

.quiz-fact, .quiz-product {
    display: flex; gap: var(--sp-4); align-items: flex-start;
    padding: var(--sp-4) var(--sp-5); margin-bottom: var(--sp-4);
    border: 1px solid var(--border); border-radius: var(--radius-card);
    background: var(--card-bg);
}
.quiz-fact-emoji { font-size: var(--fs-page); flex-shrink: 0; }
.quiz-fact-title { margin: 0 0 var(--sp-2); font-size: var(--fs-section); font-weight: 700; }
.quiz-media { width: 100%; max-width: 420px; border-radius: var(--radius-field); }
.quiz-media-link { color: var(--primary); font-weight: 600; }
.quiz-product-image {
    width: 120px; height: 120px; object-fit: cover; flex-shrink: 0;
    border-radius: var(--radius-field);
}
.quiz-product-price { margin: 0 0 var(--sp-2); font-weight: 700; color: var(--primary-dark); }
.quiz-product-features { margin: var(--sp-2) 0 0; padding-left: var(--sp-5); color: var(--text-light); }

/* Una fila del emparejar: la izquierda fija y un desplegable con las derechas
   revueltas. Con `<select>` y no arrastrando, por lo mismo que reordenar un
   bloque tiene flechas: arrastrar no funciona con teclado. */
.quiz-match-row {
    display: flex; align-items: center; gap: var(--sp-3);
    margin-bottom: var(--sp-2); flex-wrap: wrap;
}
.quiz-match-left { flex: 1; min-width: 140px; font-weight: 600; }
.quiz-match-row select { flex: 1; min-width: 160px; }

/* ── Reordenar los bloques arrastrando ───────────────────────────────────────
   Es un MODO y no un estado permanente: las asas al lado de cada bloque, todo
   el tiempo, compiten con el contenido — que es lo que alguien vino a editar.
   Y las flechas ↑↓ no se retiran: arrastrar no funciona con teclado, y un
   formulario de 21 bloques tiene que poder reordenarse sin ratón. */
.block-drag { display: none; }
.is-reordering .block-drag {
    display: inline-flex; align-items: center; cursor: grab;
    color: var(--text-light); font-size: var(--fs-glyph); line-height: 1;
    padding-right: var(--sp-2);
}
.is-reordering .block-drag:active { cursor: grabbing; }
/* En modo reordenar el contenido del bloque se pliega: lo que se está
   ordenando es la SECUENCIA, y con los editores abiertos no entran cinco
   bloques en una pantalla. */
.is-reordering .block-edit, .is-reordering .block-edit-none { display: none; }
.is-reordering .block-card { margin-bottom: 6px; }
.is-reordering .block-between { display: none; }
.block-card.is-dragging { opacity: .4; }

/* Los dos botones del pie son PUNTEADOS y del mismo ancho, como la referencia:
   son las dos acciones sobre la lista entera, y con uno relleno y otro no se
   leen como una principal y una secundaria que no lo son. */
.block-foot-btn {
    display: flex; align-items: center; justify-content: center; gap: 8px;
    width: 100%; padding: var(--sp-4); cursor: pointer;
    border: 2px dashed var(--border); border-radius: var(--radius-card);
    background: transparent; color: var(--text-light);
    font-size: var(--fs-body); font-weight: 600; font-family: inherit;
}
.block-foot-btn:hover {
    border-color: var(--primary); color: var(--primary);
    background: var(--primary-soft);
}
.block-foot-btn[aria-pressed="true"] {
    border-style: solid; border-color: var(--primary);
    background: var(--primary-soft); color: var(--primary-dark);
}

/* El asa de una tarjeta del examen. Va SIEMPRE visible, a diferencia de la de
   los bloques: una fila de examen es una línea de texto y el asa no compite
   con nada, mientras que la de un bloque se pone al lado de su editor. */
.quiz-row-drag {
    flex-shrink: 0; cursor: grab; color: var(--text-light);
    font-size: var(--fs-glyph); line-height: 1;
}
.quiz-row-drag:active { cursor: grabbing; }
.boxed-row[data-dragging="true"] { opacity: .4; }

/* El «Editar» de una tarjeta de formulario: ícono y rótulo juntos. Un ícono
   solo obliga a pasar por encima para saber qué hace, y en el pie de la
   tarjeta hay lugar para decirlo. */
.onboarding-card-edit {
    display: inline-flex; align-items: center; gap: 6px;
}
.onboarding-card-edit svg { width: 15px; height: 15px; }

/* La pastilla de audiencia: GRIS, no verde. Al lado de «ACTIVA» —que sí es
   verde porque es un estado— dos pastillas verdes se leen como una sola cosa
   partida en dos, y son cosas distintas: quién la llena y si está encendida. */
.badge-muted {
    padding: 2px 9px; border-radius: 999px;
    background: var(--bg); color: var(--text-light);
    font-size: var(--fs-badge); font-weight: 700;
    text-transform: uppercase; letter-spacing: .03em;
}

/* ── Tienda: una fila DESACTIVADA ──────────────────────────────────────────
   Un filtro inactivo sigue en la lista —desactivar no es borrar— y tiene que
   leerse como apagado sin desaparecer. La pastilla sola no alcanza: con diez
   filas, distinguir cuál está apagada obliga a leer la tercera columna de cada
   una. */
.table tr.row-off > td { opacity: .55; }

/* ── Menú de la tienda: la segunda fila es una HIJA ────────────────────────
   Sin la sangría, un menú de dos niveles se lee como una lista plana y no hay
   forma de saber qué cuelga de qué. El fondo apenas más oscuro es lo que hace
   que la sangría se lea como pertenencia y no como un error de alineación. */
.table tr.row-child > td { background: var(--bg); }
.table td.cell-indent { padding-left: var(--sp-6); }


/* ── Editor de código del tema ─────────────────────────────────────────────
   Dos columnas: el árbol a la izquierda y el archivo a la derecha. En una sola
   columna, elegir un archivo empuja el editor fuera de la pantalla y hay que
   subir para cambiar de archivo — con 152, eso es todo el trabajo. */
.editor-split { display: grid; grid-template-columns: 260px 1fr;
                gap: var(--sp-4); align-items: start; }
@media (max-width: 900px) { .editor-split { grid-template-columns: 1fr; } }

/* El árbol hace scroll propio: 152 archivos no caben, y sin esto el scroll es
   de la página entera y el editor se va con él. */
.editor-tree { max-height: 70vh; overflow-y: auto; }
.editor-folder { font-size: var(--fs-label); color: var(--pill-text);
                 text-transform: uppercase; letter-spacing: .04em;
                 margin: var(--sp-3) 0 var(--sp-1); }
.editor-files { list-style: none; margin: 0; padding: 0; }
/* `--text` y NO `--banner-text`. El segundo es blanco puro: es el color del
   texto sobre el banner OSCURO, y acá el fondo es una tarjeta blanca. Con él,
   el árbol de archivos del editor se pintaba blanco sobre blanco — estaba
   entero y no se veía. */
.editor-file { display: block; padding: var(--sp-1) var(--sp-2);
               border-radius: var(--radius-field); font-size: var(--fs-small);
               color: var(--text); text-decoration: none;
               word-break: break-all; }
.editor-file:hover { background: var(--bg); }
.editor-file.is-open { background: var(--primary-soft); font-weight: 600; }

/* El archivo ROTO. Un punto y no solo color: el color solo no lo distingue
   quien no ve la diferencia entre rojo y gris. */
.editor-file.file-broken::before { content: '● '; color: var(--primary-dark); }

.editor-pane { min-width: 0; }
/* CodeMirror montado sobre el textarea. Clases generadas por JS: no las ve
   el barrido de plantillas. La altura replica la del textarea de respaldo. */
.editor-pane .CodeMirror { height: 65vh; border: 1px solid var(--border);
                           border-radius: var(--radius-field);
                           font-size: var(--fs-small); }
.editor-errors { margin-top: var(--sp-2); }
.editor-area { width: 100%; font-family: ui-monospace, monospace;
               font-size: var(--fs-small); line-height: 1.5;
               white-space: pre; overflow-x: auto; tab-size: 2; }

/* La lista de errores del tema, en el editor y en la importación. */
.error-list { margin: var(--sp-2) 0; padding-left: var(--sp-4);
              font-size: var(--fs-small); color: var(--text); }

/* Un título dentro del formulario de ajustes del tema. `header` y `paragraph`
   no guardan valor: organizan el panel, y sin distinguirse del rótulo de un
   campo no organizan nada. */
.setting-header { grid-column: 1 / -1; font-weight: 600;
                  margin: var(--sp-4) 0 0; }
.setting-note { grid-column: 1 / -1; }

/* ── El customizer ───────────────────────────────────────────────────────── */
/* El layout es el del customizer de tiendapp, PORTADO: un panel angosto y la
   tienda ocupando todo lo demás, dentro de un marco con borde propio. El
   panel lo pinta customizer.js; lo que sigue en clases `customizer-*` es el
   RESPALDO server-rendered para operar sin JavaScript. */
.vento-customizer { display: flex; height: calc(100vh - 230px);
                    min-height: 520px; border: 1px solid var(--border);
                    border-radius: var(--radius-card); overflow: hidden;
                    background: var(--card-bg); box-shadow: var(--shadow); }
.vento-customizer__panel { width: 340px; flex-shrink: 0; overflow-y: auto;
                           display: flex; flex-direction: column;
                           border-right: 1px solid var(--border);
                           background: var(--bg); }
.vento-customizer__panel-head { padding: var(--sp-2) var(--sp-3);
                                border-bottom: 1px solid var(--border); }
.vento-customizer__pages { display: flex; gap: var(--sp-1); }
.vento-customizer__pages select { flex: 1; min-width: 0; }
.vento-customizer__panel-body { padding: var(--sp-2) var(--sp-3) var(--sp-4);
                                flex: 1; }
.vento-customizer__preview { flex: 1; position: relative;
                             background: var(--bg); }
.vento-customizer__preview iframe { width: 100%; height: 100%; border: 0;
                                    background: #fff; }
.vento-customizer__nodomain { padding: var(--sp-5); }
@media (max-width: 900px) {
  /* Apilado: el panel arriba con su propio tope de alto, la tienda abajo. */
  .vento-customizer { flex-direction: column; height: auto; }
  .vento-customizer__panel { width: 100%; max-height: 45vh;
                             border-right: 0;
                             border-bottom: 1px solid var(--border); }
  .vento-customizer__preview { min-height: 60vh; }
}

/* El aviso de que ninguna página publicada usa la plantilla elegida. */
.bc-template-notice { position: absolute; top: 12px; left: 50%;
  transform: translateX(-50%); max-width: 85%; padding: 10px 16px;
  background: #fdf6df; border: 1px solid #e8d48a; border-radius: var(--radius-field);
  color: #7a6410; font-size: var(--fs-small); font-weight: 600;
  text-align: center; box-shadow: var(--shadow); z-index: 5; }
.bc-template-notice[hidden] { display: none; }

/* La fila de una sección en el panel. */
.bc-section { display: flex; align-items: center; gap: 8px; padding: 9px 11px;
  border: 1px solid var(--border); border-radius: var(--radius-field);
  margin-bottom: 6px; background: var(--card-bg); cursor: pointer;
  font-size: var(--fs-small); color: var(--text); transition: border-color .15s; }
.bc-section:hover { border-color: var(--primary); }
.bc-section.hidden-section { opacity: .45; }
.bc-section .drag { cursor: grab; color: var(--text-faint); }
.bc-section .name { flex: 1; font-weight: 600; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bc-section .actions { display: flex; gap: 2px; }
.bc-section .actions button { border: 0; background: none;
  color: var(--text-faint); padding: 2px 4px; cursor: pointer;
  font-family: inherit; font-size: var(--fs-small); }
.bc-section .actions button:hover { color: var(--primary); }
.bc-add-section { width: 100%; margin-top: var(--sp-1); }

/* Campos generados desde el schema. */
.bc-field { margin-bottom: 15px; }
.bc-field label { display: block; font-size: var(--fs-label);
  font-weight: 700; color: var(--text-light); text-transform: uppercase;
  letter-spacing: .04em; margin-bottom: 6px; }
.bc-field input[type=text], .bc-field input[type=number],
.bc-field textarea, .bc-field select { width: 100%; }
.bc-field input[type=color] { width: 100%; height: 36px; padding: 3px;
  border: 1px solid var(--border); border-radius: var(--radius-field);
  background: var(--card-bg); cursor: pointer; }
.bc-field input[type=range] { width: 100%; }
.bc-back { font-size: var(--fs-small); color: var(--primary); cursor: pointer;
  margin-bottom: 10px; display: inline-block; font-weight: 600; }
.bc-heading { font-weight: 700; font-size: var(--fs-small);
  color: var(--text); margin: 20px 0 10px; padding-bottom: 6px;
  border-bottom: 1px solid var(--border); }
.bc-help { font-size: var(--fs-small); color: var(--text-light);
  margin: 4px 0 10px; line-height: 1.5; }
.bc-img-thumb { width: 100%; height: 96px; object-fit: cover;
  border-radius: var(--radius-field); border: 1px solid var(--border);
  margin-bottom: 6px; background: var(--bg); display: block; }

/* Radios en filas, y la alineación como control segmentado. */
.bc-radio { display: flex; align-items: center; gap: 9px; padding: 5px 0;
  font-size: var(--fs-small); color: var(--text); cursor: pointer; }
.bc-seg { display: flex; border: 1px solid var(--border);
  border-radius: var(--radius-field); overflow: hidden;
  background: var(--card-bg); }
.bc-align { flex: 1; display: flex; align-items: center; justify-content: center;
  padding: 8px 0; cursor: pointer; color: var(--text-faint);
  transition: background .15s, color .15s; position: relative; margin: 0; }
.bc-align + .bc-align { border-left: 1px solid var(--border); }
.bc-align input { position: absolute; opacity: 0; pointer-events: none; }
.bc-align:hover { color: var(--text); }
.bc-align:has(input:checked) { background: var(--primary-soft);
  color: var(--primary); }
.bc-align svg { display: block; }

/* Bloques dentro de una sección. */
.bc-blocks { list-style: none; margin: 0 0 8px; padding: 0; }
.bc-blocks li { display: flex; align-items: center; gap: 8px; padding: 7px 10px;
  border: 1px solid var(--border); border-radius: var(--radius-field);
  margin-bottom: 5px; background: var(--card-bg); font-size: var(--fs-small); }
.bc-blocks li a { cursor: pointer; color: var(--text); text-decoration: none; }
.bc-blocks li a[data-act="block-edit"] { flex: 1; font-weight: 600;
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bc-blocks li a:hover { color: var(--primary); }
.bc-blocks .bc-drag { cursor: grab; color: var(--text-faint); }
.bc-off { opacity: .45; }
.bc-add { font-size: var(--fs-small); color: var(--primary); cursor: pointer;
  font-weight: 600; display: inline-block; margin: 2px 8px 2px 0; }

/* El modal de agregar sección: lista con búsqueda, nunca un prompt(). */
.bc-modal-overlay { position: fixed; inset: 0; z-index: 10000;
  background: rgba(13, 11, 22, 0.45); display: flex;
  align-items: flex-start; justify-content: center; padding: 9vh 16px 16px; }
.bc-modal { background: var(--card-bg); border-radius: var(--radius-card);
  width: 420px; max-width: 100%; max-height: 74vh; display: flex;
  flex-direction: column; box-shadow: 0 18px 50px rgba(0, 0, 0, 0.22);
  border: 1px solid var(--border); }
.bc-modal__head { display: flex; align-items: center;
  justify-content: space-between; padding: 16px 18px 10px; }
.bc-modal__head h3 { margin: 0; font-size: var(--fs-section); font-weight: 700; }
.bc-modal__close { background: none; border: none; font-size: var(--fs-page);
  line-height: 1; cursor: pointer; color: var(--text-faint); padding: 2px 6px;
  border-radius: var(--radius-field); }
.bc-modal__close:hover { background: var(--bg); color: var(--text); }
.bc-modal__search { margin: 0 18px 10px; }
.bc-modal__list { overflow-y: auto; padding: 0 10px 12px; }
.bc-modal__item { display: flex; flex-direction: column; gap: 1px; width: 100%;
  text-align: left; background: none; border: none; cursor: pointer;
  padding: 9px 10px; border-radius: var(--radius-field); font-family: inherit; }
.bc-modal__item:hover { background: var(--primary-soft); }
.bc-modal__item-label { font-size: var(--fs-small); font-weight: 600;
  color: var(--text); }
.bc-modal__item-type { font-size: var(--fs-label); color: var(--text-faint); }
.bc-modal__empty { padding: 14px 10px; font-size: var(--fs-small);
  color: var(--text-faint); }

/* ── El respaldo sin JavaScript del customizer ──────────────────────────── */
.customizer-panel { max-height: 78vh; overflow-y: auto; }
.customizer-form { max-height: 78vh; overflow-y: auto; min-width: 0; }

.customizer-title { font-size: var(--fs-label); color: var(--pill-text);
                    text-transform: uppercase; letter-spacing: .04em;
                    margin: var(--sp-3) 0 var(--sp-1); }

.customizer-tree { list-style: none; margin: 0; padding: 0; }
.customizer-node { display: flex; align-items: center; gap: var(--sp-1);
                   border-radius: var(--radius-field); }
.customizer-node:hover { background: var(--bg); }
.customizer-node.is-open { background: var(--primary-soft); }
/* Oculta ≠ borrada: el trabajo de configurarla sigue ahí. Se atenúa, no se
   quita, o no habría cómo volver a mostrarla. */
.customizer-node.is-hidden .customizer-node-name { opacity: .55; }
.customizer-node-name { flex: 1; min-width: 0; display: block;
                        padding: var(--sp-1) var(--sp-2);
                        font-size: var(--fs-small); color: var(--text);
                        text-decoration: none; word-break: break-word; }
/* Los botones de la fila viven cada uno en su `<form>`: sin esto, cada uno
   sería un bloque y la fila se partiría en cuatro líneas. */
.customizer-node-actions { display: flex; align-items: center; gap: 2px; }
.customizer-node-actions form { display: inline; }

.customizer-empty { font-size: var(--fs-small); color: var(--pill-text);
                    padding: var(--sp-2); }

.customizer-blocks { list-style: none; margin: 0; padding: 0; }
.customizer-block { border: 1px solid var(--border);
                    border-radius: var(--radius-field); padding: var(--sp-2);
                    margin-bottom: var(--sp-2); }
.customizer-block.is-hidden { opacity: .55; }
.customizer-block-name { font-weight: 600; font-size: var(--fs-small);
                         margin: 0 0 var(--sp-1); }
.customizer-block-form { margin-bottom: var(--sp-2); }
.customizer-block-add { display: flex; gap: var(--sp-2); align-items: end;
                        flex-wrap: wrap; }

/* El rechazo que el panel muestra sin recargar. Arriba de todo porque abajo,
   con el árbol de por medio, no se ve. */
.customizer-error { grid-column: 1 / -1; margin: 0 0 var(--sp-2);
                    padding: var(--sp-2) var(--sp-3);
                    border-radius: var(--radius-field);
                    background: var(--primary-soft);
                    color: var(--text); font-size: var(--fs-small); }

/* El modal de agregar: una LISTA con búsqueda. Con 44 secciones, un `prompt()`
   del navegador obliga a saberse el nombre exacto de memoria. */
.section-picker { margin-top: var(--sp-3); }
.section-picker-search { width: 100%; margin: var(--sp-2) 0; }
.section-picker-list { list-style: none; margin: 0; padding: 0;
                       max-height: 40vh; overflow-y: auto; }
.section-picker-item { margin: 0; }
.section-picker-add { display: block; width: 100%; text-align: left;
                      padding: var(--sp-1) var(--sp-2); border: 0;
                      background: none; cursor: pointer;
                      border-radius: var(--radius-field);
                      font-size: var(--fs-small); color: var(--text); }
.section-picker-add:hover { background: var(--primary-soft); }

/* ── La pantalla de temas ────────────────────────────────────────────────── */
/* Las acciones de la fila van en `.cell-actions` + `.row-actions`, las de
   toda tabla. Tuvieron regla propia con `flex-wrap: wrap`, que con el error
   del tema ensanchando la primera columna apilaba los cinco botones. */

/* El PRIMER error del tema, debajo de su nombre. Con su archivo: un «tiene
   errores» sin el archivo obliga a abrir el editor y buscar entre 152. */
.theme-error { margin: var(--sp-1) 0 0; }

/* La casilla que solo existe para copiar de ella: monoespaciada porque lo que
   lleva es un token, y a un token hay que poder distinguirle un 1 de una l. */
.copy-field {
    width: 100%; margin-top: 8px; padding: 11px 13px;
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    /* El token y no `13px`: los siete pasos se nombran por PAPEL, y hay una
       prueba que rechaza un tamaño en píxeles crudos. Un número suelto acá es
       cómo la hoja volvía a tener catorce tamaños elegidos de a uno. */
    font-size: var(--fs-body);
    color: var(--text);
    background: var(--bg);
    border: 1px solid var(--border); border-radius: 10px;
}

/* ── Campo de medio del tema, y la biblioteca ─────────────────────────── */

/* El campo vive en dos anchos muy distintos: el panel del customizer tiene
   ~320px y la pantalla de ajustes del tema es una columna de formulario
   normal. Todo se mide en relativo por eso — nada de anchos fijos. */
.media-field { display: flex; flex-direction: column; gap: 6px; }

.media-field__preview {
    border: 1px solid var(--border); border-radius: var(--radius-field);
    background: var(--bg); overflow: hidden; line-height: 0;
}
.media-field__preview[hidden] { display: none; }
.media-field__preview img, .media-field__preview video {
    width: 100%; max-height: 160px; object-fit: contain; display: block;
}

/* La misma zona de `.dropzone` pero más baja: acá va dentro de un panel
   angosto con veinte campos, y la altura de la ficha de un producto dejaría
   la mitad de los ajustes debajo del pliegue. */
.media-field__zone {
    position: relative; display: block; padding: 10px 12px;
    border: 1.5px dashed var(--border-strong); border-radius: var(--radius-field);
    background: var(--bg); color: var(--text-light);
    font-size: var(--fs-small); text-align: center; cursor: pointer;
    transition: border-color .16s, background .16s, color .16s;
}
.media-field__zone:hover, .media-field__zone:focus-within {
    border-color: var(--primary); background: var(--primary-soft);
    color: var(--text);
}
/* Invisible pero PRESENTE, igual que en `.dropzone`: sigue siendo el control
   que recibe el archivo soltado, el que abre el diálogo al hacer clic y el que
   el teclado enfoca. `display: none` lo sacaría de los tres. */
.media-field__zone input[type="file"] {
    position: absolute; inset: 0; width: 100%; height: 100%;
    opacity: 0; cursor: pointer;
}
/* El rótulo de la zona. Sin regla propia heredaría el tamaño del formulario,
   que en el panel del customizer parte el texto en tres líneas. */
.media-field__hint { display: block; line-height: 1.4; }
.media-field.is-uploading .media-field__zone {
    border-style: solid; border-color: var(--primary); opacity: .6;
}

/* La casilla con la ruta: chica y gris. Es el respaldo —se puede pegar una
   dirección a mano— pero no es lo que uno viene a hacer, así que no compite
   con la zona por la atención. */
.media-field__value {
    font-size: var(--fs-small); color: var(--text-light);
    font-family: var(--font-mono, monospace);
}
.media-field__actions {
    display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 0;
}
.media-field__error { font-size: var(--fs-small); color: var(--danger-text); }
.media-field__error[hidden] { display: none; }

/* ── Marca: las cuatro ranuras de logo ───────────────────────────────────
   Cada ranura es una TARJETA, y la previa y la zona de arrastre ocupan LA
   MISMA celda. Apiladas —que es como salían— una ranura llena mide el doble
   que una vacía, las filas de la rejilla dejan de alinearse y las cuatro se
   leen como una pila desordenada en vez de como cuatro cosas del mismo tipo
   (Renzo, 2026-08-25, con la pantalla enfrente).

   Con logo puesto la zona se CALLA —borde y fondo transparentes— y deja ver
   la previa; al pasar por encima vuelve, con su fondo, y ahí es el botón de
   reemplazar. Sin logo se ve exactamente como en el resto del ERP, que es lo
   que Renzo pidió conservar. */
/* El encabezado de un GRUPO dentro de una tarjeta —baldosa, título y pista—
   para la tarjeta que reúne dos cosas (los logos y los colores de Marca).
   Reusa `.form-section-title` y `.field-hint`; lo suyo es la baldosa al lado
   y el aire de abajo. */
.form-section-head {
    display: flex; align-items: flex-start; gap: var(--sp-3);
    margin: 0 0 var(--sp-4);
}
.form-section-head + .form-section-head,
.form-grid + .form-section-head { margin-top: var(--sp-2); }
.form-section-head .tile { flex: none; }
.form-section-text { min-width: 0; }
.form-section-head .form-section-title { margin: 0 0 2px; }
.form-section-head .field-hint { margin: 0; }

.brand-slot, .brand-color {
    border: 1px solid var(--border); border-radius: var(--radius-card);
    padding: var(--sp-3);
    /* El aire de ABAJO lo pone la tarjeta y no la rejilla: `.form-grid` trae
       `gap: 0 var(--sp-4)` —solo de columna— así que las dos filas se tocaban
       y las cuatro ranuras volvían a leerse como un bloque. */
    margin: 0 0 var(--sp-4);
}
/* La cabecera de la ranura: nombre y para qué sirve, y la pastilla
   «Recomendado» a la derecha en la horizontal clara. */
.brand-slot-head {
    display: flex; align-items: flex-start; justify-content: space-between;
    gap: var(--sp-2); margin-bottom: var(--sp-3);
}
.brand-slot-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.brand-slot-title { margin: 0; font-weight: 600; color: var(--text); }
.brand-slot-hint { font-size: var(--fs-small); color: var(--text-light); line-height: 1.4; }
.brand-slot .field-hint { margin-bottom: 0; }

.brand-slot .media-field { display: grid; grid-template-columns: 1fr; gap: var(--sp-2); }
/* La misma celda para las dos. */
.brand-slot .media-field__preview,
.brand-slot .media-field__zone { grid-row: 1; grid-column: 1; }

.brand-slot .media-field__preview {
    height: 116px; padding: var(--sp-3); line-height: normal;
    display: flex; align-items: center; justify-content: center;
    /* Transparente al puntero: el control que recibe el clic y el archivo
       soltado es la ZONA, que está debajo. Sin esto, la mitad llena de la
       pantalla deja de aceptar un arrastre y no hay nada que lo explique. */
    pointer-events: none;
}
.brand-slot .media-field__preview img {
    width: auto; max-width: 100%; max-height: 100%;
}
.brand-slot .media-field__zone {
    display: flex; align-items: center; justify-content: center;
    min-height: 116px;
}
.brand-slot .media-field__hint { transition: opacity .16s; }
.brand-slot .media-field__preview:not([hidden]) ~ .media-field__zone {
    border-color: transparent; background: transparent;
}
.brand-slot .media-field__preview:not([hidden]) ~ .media-field__zone .media-field__hint {
    opacity: 0;
}
.brand-slot .media-field__preview:not([hidden]) ~ .media-field__zone:hover,
.brand-slot .media-field__preview:not([hidden]) ~ .media-field__zone:focus-within {
    border-color: var(--primary); background: rgba(247, 246, 251, .93);
}
.brand-slot .media-field__preview:not([hidden]) ~ .media-field__zone:hover .media-field__hint,
.brand-slot .media-field__preview:not([hidden]) ~ .media-field__zone:focus-within .media-field__hint {
    opacity: 1;
}

/* La ranura de fondo oscuro se juzga SOBRE fondo oscuro: un logo negro se ve
   perfecto contra el blanco del editor y desaparece en el correo, que es
   exactamente lo que estas dos ranuras existen para evitar.

   Solo la PREVIA. La zona VACÍA se queda con la piel de arrastre de la casa
   —fondo claro y borde punteado, la misma de todo el ERP—: pintarla de negro
   la convertía en un panel raro que no se parece a ningún otro sitio donde se
   sueltan archivos, y la convención es lo que hace que se entienda sin leer
   (Renzo, 2026-08-25). El fondo oscuro es para juzgar un logo, no para pedir
   uno. */
.brand-slot--oscuro .media-field__preview { background: var(--banner-from); }
.brand-slot--oscuro .media-field__preview:not([hidden]) ~ .media-field__zone:hover,
.brand-slot--oscuro .media-field__preview:not([hidden]) ~ .media-field__zone:focus-within {
    background: rgba(22, 19, 42, .93); color: var(--banner-sub);
    border-color: var(--banner-sub);
}
/* Sin la clase la ranura clara no se distingue de la oscura en el marcado, y
   un `--claro` que no pinta nada es una clase que miente. Pinta su lienzo
   EXPLÍCITAMENTE, así el par se lee como un par. */
/* Un tablero de ajedrez suave bajo el logo claro: un PNG con fondo
   transparente se ve como transparente, y un logo blanco no desaparece
   contra el lienzo (el referente de Renzo, 2026-09-14). */
.brand-slot--claro .media-field__preview {
    background-color: #fff;
    background-image:
        linear-gradient(45deg, var(--bg) 25%, transparent 25%, transparent 75%, var(--bg) 75%),
        linear-gradient(45deg, var(--bg) 25%, transparent 25%, transparent 75%, var(--bg) 75%);
    background-size: 16px 16px; background-position: 0 0, 8px 8px;
}

/* El aviso de una ranura que solo tiene SVG: es una advertencia, no un error
   —el archivo sirve para la tienda—, así que va en ámbar y no en rojo. */
.field-hint--warn { color: var(--warn-text); }

/* La tarjeta de un color. Mismo marco que la de un logo —lo comparte arriba—
   porque son lo mismo para quien mira: una pieza de la marca con su nombre,
   su control y su explicación. Sin el marco, la mitad de abajo de la pantalla
   parecía un formulario pegado debajo de una rejilla de tarjetas. */
.brand-color > label { display: block; margin-bottom: var(--sp-2); }
.brand-color .field-hint { margin-bottom: 0; }

/* El par selector + hexadecimal. El nativo no deja teclear el código que
   alguien tiene copiado de su manual de marca, y la casilla sola obliga a
   adivinar el color a ojo: van juntos o ninguno de los dos sirve. */
.color-field { display: flex; align-items: center; gap: var(--sp-2); }
.color-field input[type="color"] {
    width: 48px; height: 38px; flex: none; padding: 2px; cursor: pointer;
    border: 1px solid var(--border); border-radius: var(--radius-field);
    background: var(--card-bg);
}
/* El chip lleva su propio filete POR DENTRO. Sin él, «Tarjeta» en #ffffff se
   pinta blanco sobre la tarjeta blanca y se lee como una casilla VACÍA —o sea
   como «no elegí ningún color»— que es lo contrario de lo que dice. Los dos
   seudoelementos son el mismo control en los dos motores. */
.color-field input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; }
.color-field input[type="color"]::-webkit-color-swatch {
    border: 1px solid rgba(28, 24, 48, .14); border-radius: 7px;
}
.color-field input[type="color"]::-moz-color-swatch {
    border: 1px solid rgba(28, 24, 48, .14); border-radius: 7px;
}
.color-field__hex {
    flex: 1; min-width: 0; text-transform: lowercase;
    font-family: var(--font-mono, monospace);
}

/* El selector de la biblioteca. */
.media-picker {
    position: fixed; inset: 0; z-index: 1200;
    display: flex; align-items: center; justify-content: center;
    background: rgba(17, 15, 22, .5);
}
/* `display: flex` le gana al atributo `hidden` del navegador — hay que
   restaurarlo explícito o el modal nace abierto.

   Y no solo el modal: TODO lo que adentro se apaga con `hidden` necesita lo
   mismo, porque `.btn` también trae `display`. Sin esta segunda regla, el «Ver
   más» se veía con la grilla vacía — un botón que ofrece más de nada. Va como
   regla de descendencia y no una por control: con dos que ya cayeron, la
   tercera cae igual. */
.media-picker[hidden] { display: none; }
.media-picker [hidden] { display: none; }

/* El ✕ del modal. `.icon-btn` está pintado para la barra oscura —blanco al
   80%— y acá quedaba blanco sobre blanco: el botón existía, medía sus 32px y
   no se veía. */
.media-picker .icon-btn { color: var(--text-light); }
.media-picker .icon-btn:hover { color: var(--text); }
.media-picker__box {
    display: flex; flex-direction: column; gap: 12px;
    width: min(680px, 92vw); max-height: 82vh; padding: 18px;
    background: var(--card-bg); border-radius: var(--radius);
    box-shadow: 0 18px 50px rgba(17, 15, 22, .28); overflow: hidden;
}
.media-picker__head {
    display: flex; align-items: center; justify-content: space-between;
}
/* Los ejes del picker: Todos · Imágenes · Videos. Botones y no un `<select>`
   porque son tres y se alternan de un clic — un desplegable serían dos.
   `flex: none` sobre la fila para que no la encoja el `overflow-y` de la
   grilla que va debajo. */
.media-picker__axes { display: flex; gap: 6px; flex: none; }
.media-picker__axis {
    padding: 5px 12px; border-radius: 999px; cursor: pointer;
    border: 1px solid var(--border);
    background: transparent; color: var(--text-light);
    font-family: inherit; font-size: var(--fs-small); font-weight: 600;
}
.media-picker__axis:hover { color: var(--text); }
.media-picker__axis.is-on {
    background: var(--primary-soft); border-color: var(--primary-soft-border);
    color: var(--primary-dark);
}
.media-picker__grid {
    display: grid; grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
    gap: 10px; margin: 0; padding: 0; list-style: none; overflow-y: auto;
}
.media-picker__cell {
    display: flex; flex-direction: column; gap: 4px; width: 100%;
    padding: 4px; border: 1.5px solid transparent;
    border-radius: var(--radius-field); background: none; cursor: pointer;
}
.media-picker__cell:hover, .media-picker__cell:focus-visible {
    border-color: var(--primary); background: var(--primary-soft);
}
.media-picker__thumb {
    display: flex; align-items: center; justify-content: center;
    aspect-ratio: 1; border-radius: 6px;
    background: var(--bg) center/cover no-repeat;
    font-size: var(--fs-small); color: var(--text-faint);
}
/* El nombre en UNA línea con puntos suspensivos: los nombres del CDN de
   Shopify traen el prompt de la imagen adentro —hubo uno de 118 caracteres— y
   sin esto una ficha empuja a las otras tres de su fila. */
.media-picker__name {
    font-size: var(--fs-small); color: var(--text-light);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.media-picker__empty { font-size: var(--fs-small); color: var(--text-light); }
.media-picker__empty[hidden] { display: none; }

/* El conmutador tabla / íconos. Vive en el encabezado de la tarjeta, al lado
   del conteo: cambiar de forma no puede dejarlo marcando la otra. */
.view-switch { display: inline-flex; gap: 2px; margin-left: auto; }
.view-switch__btn {
    display: grid; place-items: center; width: 32px; height: 32px;
    border-radius: var(--radius-field); color: var(--text-faint);
    transition: color .16s, background .16s;
}
.view-switch__btn:hover { color: var(--text); background: var(--bg); }
.view-switch__btn.is-active { color: var(--primary); background: var(--primary-soft); }
.view-switch__btn svg { width: 18px; height: 18px; }

/* La grilla de la pantalla de Archivos. Grilla y no tabla porque a una imagen
   se la reconoce MIRÁNDOLA: una columna «Nombre» con `IMG_4471.jpg` no
   reconoce nada. */
.media-grid {
    display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
    gap: var(--sp-4); margin: 0; padding: var(--sp-4); list-style: none;
}
.media-cell { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.media-cell a { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.media-thumb {
    display: flex; align-items: center; justify-content: center;
    aspect-ratio: 4 / 3; border: 1px solid var(--border); border-radius: 8px;
    background: var(--bg) center/cover no-repeat;
}
.media-thumb-empty {
    font-size: var(--fs-small); color: var(--text-faint);
    text-transform: uppercase; letter-spacing: .04em;
}
.media-name {
    font-size: var(--fs-small); color: var(--text); font-weight: 600;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.media-meta { font-size: var(--fs-small); color: var(--text-faint); }

/* ── La galería del producto ──────────────────────────────────────────── */

/* La celda se arrastra entera (media-sort.js) y la estrella marca la
   principal. `position: relative` porque la estrella se ancla a la esquina
   superior derecha de la MINIATURA, que es la primera hija. */
/* La grilla dentro de la tarjeta no repite el respiro de abajo: lo pone la
   zona de subida que la sigue. */
.galeria-grid { padding-bottom: 0; }
.galeria-cell { position: relative; cursor: grab; }
/* La previa es el primer cuadro del video; los eventos apagados para que el
   drag agarre la celda y no los controles del reproductor. */
.galeria-video-preview { object-fit: cover; pointer-events: none; }
.galeria-cell:active { cursor: grabbing; }
.galeria-cell.is-dragging { opacity: .4; }
.galeria-cell.is-over .media-thumb {
    border-color: var(--primary);
    box-shadow: 0 0 0 2px var(--primary-soft);
}
.media-star {
    position: absolute; top: 6px; right: 6px; width: 26px; height: 26px;
    display: flex; align-items: center; justify-content: center;
    border-radius: 999px; background: white;
    color: var(--primary);
    box-shadow: 0 1px 4px rgba(15, 18, 34, .18);
    pointer-events: none;
}
.media-star svg { width: 15px; height: 15px; }

/* La zona de arrastre y su botón son dos cosas: la zona recibe, el botón
   confirma. Pegados se leen como un solo control con dos bordes. */
.galeria-upload { margin-top: var(--sp-4); }
.galeria-upload-foot {
    margin-top: var(--sp-3); display: flex; justify-content: flex-end;
}

/* ── Filas arrastrables (menús, colecciones) y editables ─────────────── */

/* El agarre de punticos: es EL único sitio por donde la fila se arrastra —
   la fila entera pelearía con seleccionar el texto de sus inputs. */
.drag-cell { width: 28px; padding-right: 0; }
.drag-handle {
    display: inline-flex; align-items: center; justify-content: center;
    width: 22px; height: 26px; cursor: grab; color: var(--text-faint);
    border-radius: 6px;
}
.drag-handle:hover { color: var(--text); background: var(--bg); }
.drag-handle:active { cursor: grabbing; }
.drag-handle svg { width: 14px; height: 14px; }
/* En una lista de CAJAS (`boxed-list`, las reglas de «Desde qué bodega sale»)
   el agarre no es una celda con ancho sino un hijo flex de la fila, y el
   navegador lo ENCOGE antes que dejar envolver el texto de al lado: medido a
   375px, 22px se volvían 14 —el ancho pelado de su glifo— y el área de toque
   se perdía justo en el aparato donde más falta hace. Es la misma trampa del
   `<svg>` de `.setting-default` (v409). Solo muerde donde el agarre es hijo
   directo de una `.boxed-row`: en una `<table>` vive dentro de su `<td>` y
   en el tablero dentro de `.grid-tools`, y ninguno de los dos pasa por acá. */
.boxed-row > .drag-handle { flex: none; }
.drag-row.is-dragging { opacity: .4; }
/* La marca de destino va ARRIBA de la fila: es donde la pieza va a caer. */
.drag-row.is-over td { border-top: 2px solid var(--primary); }
/* Los inputs de la fila no gritan: se leen como texto hasta que se tocan.
   El margen negativo compensa el padding propio del input (--sp-3): sin él,
   «Cafés» arranca un padding a la derecha de su cabecera «Texto». */
.drag-row input[type="text"] {
    border-color: transparent; background: transparent;
    margin-left: calc(var(--sp-3) * -1);
    width: calc(100% + var(--sp-3));
}
.drag-row input[type="text"]:hover { border-color: var(--border); }
.drag-row input[type="text"]:focus {
    border-color: var(--primary); background: var(--card-bg);
}

.plain-list { margin: 0 0 var(--sp-3); padding-left: var(--sp-5); }
.plain-list li { font-size: var(--fs-body); color: var(--text); padding: 2px 0; }

/* ── La ficha de un archivo ───────────────────────────────────────────── */

/* DOS columnas, y el reparto es la decisión: la previa manda porque a un
   archivo se lo reconoce mirándolo, y los datos son de consulta. Apiladas
   —como estaba— la previa empujaba el nombre y el alt debajo del pliegue, y
   esta pantalla existe justamente para renombrar y describir.

   `minmax(0, …)` en la primera pista: sin el mínimo cero, una imagen ancha
   estira la columna y el `grid` desborda a la derecha en vez de encogerla. */
.file-detail {
    display: grid; grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
    gap: var(--sp-4); align-items: start;
}
/* Un umbral y no un `md`: debajo de ~900px la columna de datos queda tan
   angosta que «Dimensiones» parte en dos renglones. */
@media (max-width: 900px) {
    .file-detail { grid-template-columns: minmax(0, 1fr); }
}

.file-detail__media {
    display: flex; align-items: center; justify-content: center;
    padding: var(--sp-5); min-height: 240px;
}
.file-detail__media img, .file-detail__media video {
    max-width: 100%; max-height: 60vh; object-fit: contain;
    border-radius: var(--radius-field);
}
.file-detail__nopreview {
    margin: 0; font-size: var(--fs-section); font-weight: 700;
    color: var(--text-faint); text-transform: uppercase; letter-spacing: .06em;
}

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

/* Etiqueta y dato en dos columnas. `dl` y no una tabla: son pares
   nombre-valor, que es literalmente para lo que existe. */
.file-detail__facts {
    display: grid; grid-template-columns: auto 1fr;
    gap: 4px var(--sp-3); margin: var(--sp-3) 0;
}
.file-detail__facts dt {
    font-size: var(--fs-small); color: var(--text-faint); font-weight: 600;
}
.file-detail__facts dd {
    margin: 0; font-size: var(--fs-small); color: var(--text);
}

/* ── Reseñas ──────────────────────────────────────────────────────────────
   La pantalla de moderación. Prefijo `rsn-` y no nombres genéricos —`stars`,
   `hint`— a propósito: son cinco reglas de UNA pantalla, y un `.stars` suelto
   en una hoja de 5.200 líneas es un nombre que el próximo que necesite
   estrellas va a reusar sin mirar qué le hace a esta tabla.

   El sello «Compra verificada» NO tiene regla propia: usa `.badge-soft`, que
   ya existe. Una clase nueva para la misma pastilla es cómo se termina con
   tres verdes distintos en el mismo ERP. */
/* `.table` lleva `white-space: nowrap` para toda la casa, y con un cuerpo de
   reseña de 600 caracteres eso hace una columna de miles de píxels: la tabla
   sale con scroll horizontal y el autor, el estado y los botones quedan fuera
   de la pantalla. Se corrige por CELDA y no tocando la regla global — el
   nowrap está bien para las once listas que muestran datos cortos.

   `max-width` en `ch` y no en píxels: lo que hay que acotar es cuántos
   CARACTERES entran por renglón, que es lo que decide si un párrafo se lee. */
.rsn-body { white-space: normal; line-height: 1.45; }
/* El ancho lo da la COLUMNA (ver `.rsn-table .rsn-body`), pero el texto no
   puede correr los ~90 caracteres de la celda entera: un párrafo se lee hasta
   ~65. El tope va en el TEXTO y no en la celda para que la fila siga ocupando
   el ancho completo y las otras columnas queden alineadas. */
.rsn-clamp { max-width: 62ch; display: block; }
/* La reseña es la columna ANCHA a propósito: `width: 100%` en una tabla
   automática le da a esta celda todo lo que las otras no reclamen, y las
   otras tres quedan del ancho de su contenido — es lo que mata el hueco
   muerto que había entre el texto y el autor cuando cada columna tenía su
   ancho rígido. */
/* Las PROPORCIONES van declaradas y no al azar del contenido: con la reseña
   en «lo que sobre», cuatro renglones cortos dejaban un desierto entre el
   texto y el autor, y todo lo demás apretado contra el borde derecho. La
   reseña se lleva la mitad, el autor un cuarto, y estado y acciones el
   resto — medido contra el referente de Renzo. */
.rsn-table .rsn-body { width: 50%; }
.rsn-table .rsn-author { width: 26%; white-space: normal; }
.rsn-meta { margin-top: var(--sp-2); }

/* Los filtros en la cabecera estándar de la tarjeta, como el referente. */
.rsn-cardfilters { display: flex; align-items: center; gap: var(--sp-2); }

/* ── Las estrellas dibujadas, con su nota ───────────────────────────── */
.rsn-stars-row {
    margin: 0 0 var(--sp-1);
    display: flex; align-items: center; gap: var(--sp-2);
}
.rsn-star { width: 17px; height: 17px; }
.rsn-star path { fill: var(--border); }
.rsn-star--on path { fill: var(--text); }
.rsn-score {
    font-size: var(--fs-meta); font-weight: 600; color: var(--text-light);
    background: var(--bg); border-radius: 4px; padding: .15em .5em;
}
.rsn-title { display: block; margin-bottom: 2px; }

/* ── El autor: avatar con iniciales, y el correo debajo ─────────────── */
.rsn-person { display: flex; gap: var(--sp-2); align-items: center; }
/* El tono viene de la vista (`--rsn-h`, estable por nombre): claro de fondo y
   oscuro de letra, del MISMO tono, para que cualquier pareja combine. */
.rsn-avatar {
    display: grid; place-items: center; flex: none;
    width: 28px; height: 28px; border-radius: 50%;
    font-size: var(--fs-badge); font-weight: 600;
    background: hsl(var(--rsn-h, 220) 45% 92%);
    color: hsl(var(--rsn-h, 220) 30% 40%);
}
.rsn-person-info { min-width: 0; font-size: var(--fs-body); }
/* La RESEÑA arriba —es el texto que se lee— y las columnas cortas (autor,
   estado, acciones) al CENTRO de su fila, como en el referente: arriba
   quedaban flotando con un vacío debajo en cada fila alta, y esa masa
   despareja era lo que Renzo leía como columnas «pesadas». */
.rsn-table td { vertical-align: middle; }
.rsn-table .rsn-body { vertical-align: top; }
/* El correo es la línea más larga de la celda: si no corta, ÉL decide el ancho
   de la columna entera. `anywhere` y no `word-break`: corta solo cuando no hay
   más remedio. Y TENUE, no en negrita: `.table tbody a` pinta los enlaces
   oscuros y en 600, pero este es la segunda línea de la celda — si pesa más
   que el nombre, la columna se lee al revés. */
.rsn-email a { overflow-wrap: anywhere; }
.table tbody .rsn-email a:not(.btn) {
    color: var(--text-light); font-weight: 400;
}
/* ESTA tabla no vive en un contenedor de scroll: sus celdas envuelven —nada
   desborda a lo ancho— y el tope de alto solo servía para el thead pegajoso.
   A cambio, el menú de acciones puede FLOTAR: dentro del scrollport se
   cortaba contra el borde, y abrirlo en flujo empujaba la fila — el «salto
   raro» que Renzo reportó el 2026-08-08. */
.rsn-wrap { overflow: visible; max-height: none; }
/* El menú de la fila: los tres punticos. Flota anclado al botón y no mueve
   NADA al abrir — la tarjeta no recorta (borde redondeado sin `overflow`),
   así que hasta el de la última fila se ve entero. */
.rsn-actions { text-align: right; }
.rsn-menu { display: inline-block; text-align: left; position: relative; }
/* El summary ya se estiliza por `.rsn-menu > summary`; la clase propia
   existe para que la guarda de la hoja tenga una regla que encontrar — una
   clase usada sin regla es exactamente lo que esa prueba caza. */
.rsn-menu-toggle { user-select: none; }
.rsn-menu > summary {
    list-style: none; cursor: pointer;
    font-size: var(--fs-body); line-height: 1; padding: .55rem .9rem;
}
.rsn-menu > summary::-webkit-details-marker { display: none; }
.rsn-menu[open] > summary { background: var(--bg); }
.rsn-menu-list {
    position: absolute; right: 0; top: calc(100% + 4px); z-index: 30;
    width: max-content; min-width: 13rem;
    padding: var(--sp-1);
    display: flex; flex-direction: column; gap: 2px;
    background: var(--card-bg);
    border: 1px solid var(--border); border-radius: var(--radius-sm);
    box-shadow: var(--shadow-lg);
}
.rsn-menu-list .inline-form { display: block; }
/* Ítems de menú, no botones apilados: sin borde, y el hover es el fondo —
   cuatro pastillas con borde una sobre otra se leen como un formulario. */
.rsn-menu-list .btn {
    display: block; width: 100%; text-align: left; border: 0;
}
.rsn-menu-list .btn:hover { background: var(--bg); }
.rsn-menu-list .rsn-reply { display: block; }
.rsn-menu-list .rsn-reply > summary { display: block; text-align: left; }
.rsn-stars { white-space: nowrap; display: inline-flex; gap: 2px; }
/* La cabecera «Acciones» a la derecha, sobre su columna de kebabs. */
.rsn-table thead .rsn-th-actions { text-align: right; }

/* ── El pie: «Mostrando X a Y» y los números de página ──────────────── */
.rsn-foot {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--sp-3); flex-wrap: wrap;
    padding: var(--sp-3) var(--sp-4);
    border-top: 1px solid var(--border);
}
.rsn-foot-status { font-size: var(--fs-small); color: var(--text-light); }
.rsn-pages { display: flex; align-items: center; gap: var(--sp-1); }
.rsn-page {
    display: grid; place-items: center; min-width: 32px; height: 32px;
    padding: 0 .6rem; border-radius: var(--radius-sm);
    border: 1px solid var(--border); text-decoration: none; color: inherit;
    font-size: var(--fs-small);
}
.rsn-page:hover { border-color: var(--text-light); }
.rsn-page--current {
    background: var(--text); border-color: var(--text); color: #fff;
    font-weight: 600;
}
.rsn-page-gap { color: var(--text-faint); padding: 0 .3rem; }
.rsn-thumbs { display: flex; gap: var(--sp-1); margin-top: var(--sp-2); }
.rsn-thumbs img {
    width: 56px; height: 56px; object-fit: cover;
    border-radius: var(--radius-sm);
}
/* El `<details>` de responder. `inline-block` para que el resumen no empuje a
   los botones de estado a otra línea: en una lista de doscientos renglones,
   una fila más alta que las demás por un desplegable cerrado se lee como si
   esa reseña fuera distinta. */
.rsn-reply { display: inline-block; }
.rsn-reply textarea {
    display: block; width: 260px; margin: var(--sp-2) 0;
}

/* ============================================================
   AUTOMATIZACIONES — la lista, la barra del builder y la bitácora.
   El lienzo de nodos trae su propia hoja (`automations/builder.css`,
   compilada con el bundle); acá vive solo lo que pinta el shell.
   ============================================================ */

/* La barra del builder reemplaza al banner del shell: volver, el nombre
   editable en el sitio, el estado, y las acciones a la derecha. */
.atm-toolbar {
    display: flex; justify-content: space-between; align-items: center;
    gap: var(--sp-3); flex-wrap: wrap;
    padding: var(--sp-2) var(--sp-3);
    background: var(--card-bg); border: 1px solid var(--border);
    border-radius: var(--radius) var(--radius) 0 0; border-bottom: none;
}
.atm-toolbar-left { display: flex; align-items: center; gap: var(--sp-2); min-width: 0; }
.atm-toolbar-actions { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.atm-back { text-decoration: none; color: var(--text-light); font-size: var(--fs-page); }
/* El nombre se edita donde se lee — el mismo gesto del builder de tiendapp,
   sin modal: h1 con contenteditable y un borde que solo aparece al entrar. */
.atm-toolbar h1 {
    margin: 0; font-size: var(--fs-page); font-weight: 600;
    padding: 2px 6px; border-radius: var(--radius-field);
    border: 1px solid transparent; cursor: text; min-width: 40px;
    max-width: 46ch; overflow: hidden; text-overflow: ellipsis;
    white-space: nowrap;
}
.atm-toolbar h1:hover { background: var(--bg); }
.atm-toolbar h1:focus { outline: none; background: var(--card-bg); border-color: var(--primary); }
/* La URL del webhook se selecciona entera de un clic: es para copiarla,
   no para leerla. */
.atm-webhook {
    font-size: var(--fs-label); color: var(--text-light);
    background: var(--bg); padding: 4px 8px;
    border-radius: var(--radius-field); user-select: all;
    max-width: 32ch; overflow: hidden; text-overflow: ellipsis;
    white-space: nowrap;
}
.atm-delete { color: var(--danger-text); }

/* El lienzo: alto de pantalla menos el shell — el flujo se recorre
   con el canvas, no con el scroll de la página. */
.atm-canvas {
    height: calc(100vh - 180px); min-height: 420px;
    background: var(--card-bg); border: 1px solid var(--border);
    border-radius: 0 0 var(--radius) var(--radius); overflow: hidden;
}

/* La bitácora: el paso a paso se abre en el renglón. */
.atm-runs td { vertical-align: top; }
.atm-run-detail summary { cursor: pointer; color: var(--text-light); font-size: var(--fs-small); }
.atm-steps { margin: var(--sp-2) 0 0; padding-left: var(--sp-4); }
.atm-steps li { margin-bottom: var(--sp-1); font-size: var(--fs-small); }
.atm-step-name { font-weight: 500; margin-right: var(--sp-2); }
.atm-step-status { font-size: var(--fs-label); margin-right: var(--sp-2); }
.atm-step-completed { color: var(--ok-text); }
.atm-step-failed { color: var(--danger-text); }
.atm-step-skipped, .atm-step-pending { color: var(--text-faint); }
.atm-step-running { color: var(--info-text); }
.atm-step-error {
    margin-top: 2px; padding: var(--sp-1) var(--sp-2);
    background: var(--danger-bg); color: var(--danger-text);
    border-radius: var(--radius-field); font-size: var(--fs-label);
    white-space: pre-wrap;
}

/* ============================================================
   INTEGRACIONES — el directorio tipo apps: una tarjeta por servicio
   conectable, con su estado y su botón. Las tarjetas las contribuyen
   los módulos dueños por hook; esta pantalla solo agrega.
   ============================================================ */
/* Un grupo por clase (cobros, envíos, mensajería…): el rótulo con su
   conteo y, debajo, la rejilla. Como los grupos de Integrantes. */
.int-group + .int-group { margin-top: var(--sp-5); }
.int-group__head {
    display: flex; align-items: baseline; gap: var(--sp-2);
    margin: 0 0 var(--sp-3); padding-bottom: 6px;
    border-bottom: 1px solid var(--border);
}
.int-group__title { margin: 0; font-size: var(--fs-body); font-weight: 700; }
.int-group__count { font-size: var(--fs-small); color: var(--text-light); }
.int-grid {
    display: grid; gap: var(--sp-3);
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}
.int-card { display: flex; flex-direction: column; padding: var(--sp-4); }
/* Lo que todavía no existe se apaga un poco: sigue ahí para que se sepa
   que viene, pero no compite con lo que sí se puede conectar hoy. */
.int-card--pending { opacity: .72; }
.int-card--pending:hover { opacity: 1; }
.int-card__head { display: flex; align-items: flex-start; gap: var(--sp-3); }
/* El glifo del tercero es un emoji en una baldosa: no pasa por el set del
   kernel a propósito — el logo ajeno no es vocabulario de la casa. */
.int-card__icon {
    display: grid; place-items: center; flex: none;
    width: 44px; height: 44px; border-radius: var(--radius-field);
    background: var(--bg); font-size: var(--fs-glyph);
}
.int-card__title { min-width: 0; }
.int-card__title h3 {
    margin: 0 0 4px; font-size: var(--fs-section);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.int-card__desc {
    margin: var(--sp-3) 0; color: var(--text-light);
    font-size: var(--fs-small); line-height: 1.5; flex: 1;
}
.int-card__actions { display: flex; justify-content: flex-end; }

/* ── Plantillas: el editor de correos ──────────────────────────────────
   La fila hereda la anatomía del árbol del customizer; lo propio del
   correo es la agarradera SIEMPRE visible y el resumen bajo el nombre —
   un correo se lee de arriba a abajo y el orden es contenido. El preview
   lleva la barra de un cliente de correo: el asunto rendido es parte de
   lo que se está diseñando, no un adorno. */
.mail-editor-drag { cursor: grab; color: var(--pill-text); flex-shrink: 0;
                    padding: 0 2px; user-select: none; }
.mail-editor-drag:active { cursor: grabbing; }
.customizer-node.is-dragging { opacity: .45; }
.mail-editor-sub { display: block; font-size: var(--fs-small);
                   color: var(--pill-text); white-space: nowrap;
                   overflow: hidden; text-overflow: ellipsis;
                   font-weight: 400; }
.mail-editor-formbar { display: flex; align-items: center;
                       justify-content: space-between;
                       margin-bottom: var(--sp-2); }
.mail-preview { display: flex; flex-direction: column; height: 100%; }
.mail-preview-chrome { padding: var(--sp-2) var(--sp-3);
                       border-bottom: 1px solid var(--border);
                       background: var(--card-bg); }
.mail-preview-subject { margin: 0; font-weight: 600; }
/* La barra de la previa: la frase, el selector de datos y «Enviar prueba»
   en una fila que se PARTE en pantalla angosta, con separación vertical —
   apilados y pegados se leen como un solo control. El `gap` de dos valores
   es justo eso: menos aire entre filas que entre columnas. */
.mail-preview-meta { display: flex; flex-wrap: wrap; align-items: center;
                     gap: var(--sp-1) var(--sp-2);
                     margin: 0; font-size: var(--fs-small);
                     color: var(--pill-text); }
/* La frase va en UN solo `<span>` y no suelta: en un contenedor flex cada
   nodo de texto es un item aparte, y «De:», el remitente y «— rendido con»
   se partirían en tres renglones distintos.

   Y acá NO va el `flex: 1 1 0` que manda `docs/marco-ui.md` para el banner.
   Allá el problema era que la línea se partía cuando debía ENCOGER; acá
   partirse es justo lo que se quiere en pantalla angosta, y con `1 1 0` la
   frase se estira y empuja el selector al otro extremo — lejos del
   «rendido con» del que es continuación. Queda el `flex` por omisión, y
   `min-width: 0` para que un remitente largo encoja en vez de desbordar. */
.mail-preview-from { min-width: 0; }
/* El select se alinea al BOTÓN y no al revés: el botón es el de la casa y
   su alto es el mismo en todo el ERP, mientras que este select es uno suelto
   dentro de una frase. Con el relleno por omisión quedaban 35 contra 39 —el
   glifo de 15px del botón manda sobre la interlínea de 13—, y cuatro píxeles
   de diferencia entre dos controles lado a lado se ven torcidos.

   Los dos números son los que declara `.btn`, no calibrados a ojo: el
   relleno de 11px es el suyo, y la interlínea de 15px es el alto de su
   glifo (`.btn svg`), que es lo que le fija el alto del contenido. Con
   `line-height: normal` el select daba 39,5 contra 39 — medio píxel, que
   según el zoom se ve como uno. Si `.btn` cambia, acá hay que cambiarlo, y
   por eso está dicho. */
.mail-preview-meta > select { flex: none; padding-block: 11px;
                              line-height: 15px; }
.mail-preview iframe { flex: 1; width: 100%; height: auto; border: 0;
                       background: #eef0f7; }

/* «Enviar prueba» usa EL botón de la casa (`ui/_btn.html`, tono ghost,
   tamaño small): trae su borde, su alto y su glifo —que sale del verbo
   «Enviar», no de esta pantalla—. Ghost y no `primary` porque compite con
   «Guardar», que es la acción de la pantalla: dos botones sólidos al lado
   convierten una decisión en dos. Acá solo va el acuse, que no es un botón. */
.mail-preview-test-acuse { color: var(--pill-text); }

/* El selector de bloques: tarjetas alargadas con su glifo, dos por fila —
   el molde del selector de bloques de tiendapp, con la factura del kit. */
.mail-picker { display: grid; grid-template-columns: 1fr 1fr;
               gap: var(--sp-1); margin-top: var(--sp-1); }
.mail-picker-item { display: flex; align-items: center; gap: var(--sp-1);
                    padding: 10px 12px; background: var(--card-bg);
                    border: 1px solid var(--border);
                    border-radius: var(--radius-field); cursor: pointer;
                    font: inherit; font-size: var(--fs-small);
                    font-weight: 500; color: inherit; text-align: left;
                    width: 100%; }
.mail-picker-item:hover { border-color: var(--primary-soft-border);
                          background: var(--primary-soft);
                          color: var(--primary-dark); }
.mail-picker-item svg { width: 18px; height: 18px; flex-shrink: 0;
                        color: var(--primary); }

/* El botón que abre el modal de agregar: una fila entera, porque es la
   única acción del pie de la lista de bloques. */
.mail-picker-open { margin-top: var(--sp-2); }
.mail-picker-open .btn { width: 100%; justify-content: center; }

/* EL modal de agregar bloque: el buscador del customizer con las tarjetas
   del selector adentro. Mismo molde que `.quiz-modal` —el `<dialog>` de la
   casa—, más angosto porque su contenido son dos columnas de tarjetas y no
   un formulario de ocho campos. */
.block-picker {
    width: min(480px, calc(100vw - var(--sp-6))); max-height: 82vh;
    padding: 0; border: 1px solid var(--border);
    border-radius: var(--radius-lg); background-color: var(--card-bg);
    color: var(--text); box-shadow: var(--shadow-lg);
}
.block-picker::backdrop { background-color: rgba(13, 11, 22, .45); }
.block-picker-body { max-height: 82vh; overflow-y: auto;
                     padding: 0 var(--sp-5) var(--sp-5); }
/* La cabecera se queda arriba al desplazar, como la del modal del examen:
   con la lista larga, el «cerrar» se iba de pantalla. */
.block-picker-head {
    position: sticky; top: 0; z-index: 1;
    display: flex; align-items: center; justify-content: space-between;
    margin: 0 calc(var(--sp-5) * -1) var(--sp-4);
    padding: var(--sp-4) var(--sp-5); border-bottom: 1px solid var(--border);
    background-color: var(--card-bg);
}
.block-picker-head h3 { margin: 0; font-size: var(--fs-section);
                        font-weight: 700; }
.block-picker-search { width: 100%; margin-bottom: var(--sp-2); }

/* Los ajustes generales de la página: una fila por lado, con la escala de
   pasos a la derecha. Rótulo y escala en la misma línea porque son quince
   controles en un panel angosto — uno debajo del otro, la sección sola
   ocupaba la pantalla entera. */
.design-panel { margin-top: var(--sp-3); }
.design-row { display: flex; align-items: center; gap: var(--sp-2);
              margin-bottom: 6px; }
.design-row-label { flex: 1; color: var(--text-light);
                    font-size: var(--fs-small); }
.design-scale { display: inline-flex; gap: 2px; flex-shrink: 0; }
/* Los pasos son un segmentado: mismos anchos, para que la escala se lea
   como una escala y no como cinco botones sueltos de tamaños distintos. */
.design-step {
    min-width: 30px; padding: 5px 4px; cursor: pointer;
    border: 1px solid var(--border); background-color: var(--card-bg);
    color: var(--text-light); font: inherit; font-size: var(--fs-small);
    font-weight: 600; border-radius: var(--radius-field);
}
.design-step:hover:not(.is-active) { border-color: var(--primary-soft-border);
                                     background-color: var(--primary-soft);
                                     color: var(--primary-dark); }
.design-step.is-active { border-color: var(--primary);
                         background-color: var(--primary);
                         color: #fff; }
.design-step:disabled { cursor: default; opacity: .55; }
.design-bg { display: flex; align-items: center; gap: var(--sp-2); }
.design-check { margin: var(--sp-2) 0 4px; }

/* El menú de la barra `/` del editor de plantillas: la piel del finder,
   flotando bajo el campo activo — fijo porque el campo puede vivir dentro
   de un panel con su propio scroll. */
.mail-var-menu { position: fixed; z-index: 60; max-height: 280px;
                 overflow-y: auto; }

/* La tarjeta de un valor de captura: Guarda/Botón arriba, el icono y su
   color abajo — una fila apretada de cinco controles no se entendía. */
.mail-value-card { border: 1px solid var(--border);
                   border-radius: var(--radius-field);
                   padding: var(--sp-2); margin-bottom: var(--sp-1);
                   background: var(--card-bg); }

/* El selector de icono y el picker de color de un valor de captura. */
.mail-icon-row { display: flex; align-items: center; gap: 4px;
                 margin-bottom: var(--sp-1); flex-wrap: wrap; }
.mail-icon-choice { width: 34px; height: 34px; display: inline-flex;
                    align-items: center; justify-content: center;
                    background: var(--card-bg);
                    border: 1px solid var(--border);
                    border-radius: var(--radius-field); cursor: pointer;
                    color: var(--pill-text); padding: 0; }
.mail-icon-choice svg { width: 18px; height: 18px; }
.mail-icon-choice:hover { border-color: var(--primary-soft-border);
                          color: var(--primary); }
.mail-icon-choice.is-active { border-color: var(--primary);
                              background: var(--primary-soft);
                              color: var(--primary); }
.mail-swatch-row { display: inline-flex; align-items: center; gap: 4px; }
.mail-swatch { width: 22px; height: 22px; border-radius: 50%;
               border: 1px solid var(--border); cursor: pointer;
               padding: 0; }
.mail-swatch.is-active { outline: 2px solid var(--primary);
                         outline-offset: 1px; }
.mail-swatch-free { width: 34px; height: 26px; padding: 0;
                    border: 1px solid var(--border);
                    border-radius: var(--radius-field);
                    background: var(--card-bg); cursor: pointer; }

/* El conmutador del preview de plantillas: Correo / Después del clic. */
.mail-preview-toprow { display: flex; align-items: center;
                       justify-content: space-between; gap: var(--sp-2); }
.mail-seg { display: inline-flex; border: 1px solid var(--border);
            border-radius: var(--radius-field); overflow: hidden;
            flex-shrink: 0; }
.mail-seg button { border: 0; background: var(--card-bg); cursor: pointer;
                   font: inherit; font-size: var(--fs-small);
                   padding: 6px 12px; color: var(--pill-text); }
.mail-seg button + button { border-left: 1px solid var(--border); }
.mail-seg button.is-active { background: var(--primary-soft);
                             color: var(--primary-dark);
                             font-weight: 600; }

/* Las filas de bloques del editor de plantillas como TARJETAS — el mismo
   lenguaje del picker de agregar: borde, radio, glifo morado. */
.mail-blocks .customizer-node { background: var(--card-bg);
                                border: 1px solid var(--border);
                                border-radius: var(--radius-field);
                                padding: 8px 10px;
                                margin-bottom: var(--sp-1); }
.mail-blocks .customizer-node:hover { background: var(--card-bg);
                                      border-color:
                                        var(--primary-soft-border); }
.mail-blocks .customizer-node.is-open { background: var(--primary-soft); }
.mail-block-ico { flex-shrink: 0; width: 18px; height: 18px;
                  color: var(--primary); }
.mail-block-ico svg { width: 18px; height: 18px; display: block; }

/* Las pestañas de variante de una página: «lo que ve el que respondió
   5» — chips arriba de la lista de bloques. */
.mail-variants { display: flex; flex-wrap: wrap; gap: 4px;
                 margin-bottom: var(--sp-1); align-items: center; }
.mail-variant-chip { border: 1px solid var(--border);
                     background: var(--card-bg); cursor: pointer;
                     font: inherit; font-size: var(--fs-small);
                     padding: 5px 10px; border-radius: 999px;
                     color: var(--pill-text); }
.mail-variant-chip.is-active { background: var(--primary-soft);
                               border-color: var(--primary-soft-border);
                               color: var(--primary-dark);
                               font-weight: 600; }
.mail-variant-new { border: 1px dashed var(--border);
                    border-radius: 999px; padding: 5px 10px;
                    font: inherit; font-size: var(--fs-small);
                    background: transparent; width: 7.5em; }

/* ── El asistente «Nueva pregunta por correo» ─────────────────────────
   Dos columnas: los cuatro pasos a la izquierda, el preview del motor
   real a la derecha. En angosto, el preview cae abajo. */
.asistente { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
             gap: var(--sp-2); align-items: start; }
@media (max-width: 1000px) { .asistente { grid-template-columns: 1fr; } }
.asistente__pasos { padding: var(--sp-3); }
.asistente__pasos h2 { margin: 0 0 var(--sp-2); }
.asistente__miga { display: flex; gap: var(--sp-1); list-style: none;
                   margin: 0 0 var(--sp-3); padding: 0; flex-wrap: wrap; }
.asistente__miga li { display: flex; align-items: center; gap: 6px;
                      font-size: var(--fs-small); color: var(--text-light);
                      cursor: default; }
.asistente__miga li.is-hecho { cursor: pointer; color: var(--primary-dark); }
.asistente__miga li.is-activo { color: var(--text); font-weight: 600; }
.asistente__miga-num { display: inline-flex; align-items: center;
                       justify-content: center; width: 22px; height: 22px;
                       border-radius: 50%; border: 1px solid var(--border);
                       font-size: var(--fs-small); }
/* El rótulo del paso, al lado de su número. Sin regla propia el paso se
   leía como un número suelto: la migaja hereda `--fs-small` del `li`,
   así que lo único que falta acá es que el activo pese. */
.asistente__miga-rotulo { white-space: nowrap; }
.asistente__miga li.is-activo .asistente__miga-rotulo { font-weight: 600; }
.asistente__miga li.is-activo .asistente__miga-num,
.asistente__miga li.is-hecho .asistente__miga-num {
  background: var(--primary-soft); border-color: var(--primary-soft-border);
  color: var(--primary-dark); }
.asistente__recetas { display: flex; gap: var(--sp-1); flex-wrap: wrap; }
.asistente__chips { display: flex; flex-direction: column; gap: 6px;
                    margin-bottom: var(--sp-2); }
.asistente__chip { display: flex; align-items: center; gap: 8px;
                   border: 1px solid var(--border); border-radius: 10px;
                   padding: 6px 10px; background: var(--card-bg); }
.asistente__chip input[type="text"] { flex: 1; min-width: 0; }
.asistente__chip-glifo { font-size: var(--fs-glyph); line-height: 1;
                         min-width: 22px;
                         text-align: center; }
.asistente__tabs { display: flex; flex-wrap: wrap; gap: 4px;
                   margin-bottom: var(--sp-2); }
.asistente__tab { display: inline-flex; align-items: center; gap: 6px;
                  border: 1px solid var(--border); background: var(--card-bg);
                  cursor: pointer; font: inherit;
                  font-size: var(--fs-small); padding: 5px 10px;
                  border-radius: 999px; color: var(--pill-text); }
.asistente__tab.is-activa { background: var(--primary-soft);
                            border-color: var(--primary-soft-border);
                            color: var(--primary-dark); font-weight: 600; }
.asistente__check { display: flex; align-items: center; gap: 8px;
                    margin: var(--sp-1) 0; cursor: pointer; }
.asistente__disparadores { display: flex; flex-direction: column;
                           gap: 8px; margin-bottom: var(--sp-2); }
.asistente__disparador { display: grid;
                         grid-template-columns: auto 1fr; gap: 2px 10px;
                         border: 1px solid var(--border);
                         border-radius: 10px; padding: 10px 12px;
                         cursor: pointer; }
.asistente__disparador.is-activo { border-color: var(--primary-soft-border);
                                   background: var(--primary-soft); }
.asistente__disparador input { grid-row: span 2; align-self: center; }
.asistente__disparador-rotulo { font-weight: 600; }
.asistente__disparador-desc { grid-column: 2; font-size: var(--fs-small);
                              color: var(--text-light); }
.asistente__errores { border: 1px solid #fca5a5; background: #fef2f2;
                      color: #b91c1c; border-radius: 10px;
                      padding: 10px 12px; margin-bottom: var(--sp-2); }
.asistente__errores p { margin: 2px 0; }
.asistente__pie { display: flex; gap: var(--sp-1); align-items: center;
                  margin-top: var(--sp-2); }
.asistente__preview { padding: 0; overflow: hidden; position: sticky;
                      top: var(--sp-2); }
.asistente__preview-cabeza { padding: 10px 14px;
                             border-bottom: 1px solid var(--border);
                             font-size: var(--fs-small);
                             color: var(--text-light); }
.asistente__preview iframe { width: 100%; height: 70vh; border: 0;
                             display: block; background: #f6f6f7; }

/* El × de eliminar en la lista de plantillas: `.icon-btn` nació blanco
   para la topbar oscura — dentro de una celda necesita su propio tinte,
   y en rojo al pasar: borrar se anuncia. */
.cell-actions { width: 1%; white-space: nowrap; }
/* Los controles del final de una fila, juntos y pegados a la derecha.
   NO es `.inline-row`: aquélla es de FORMULARIO y envuelve a propósito —está
   escrito en su regla, y es lo correcto cuando un campo compite con dos
   botones—, pero `.cell-actions` mide `width: 1%`, así que ahí envolver
   significa apilar cada control en su propio renglón. Se vio con «Abrir» y el
   menú de Dominios, uno debajo del otro.

   El hueco es de 6px y no de `--sp-3`: son controles de la misma acción, no
   dos campos de un formulario. */
.row-actions {
    display: flex; align-items: center; justify-content: flex-end;
    gap: 6px; flex-wrap: nowrap;
}
.cell-actions .icon-btn { color: var(--text-light); }
.cell-actions .icon-btn:hover { background: #fee2e2; color: #b91c1c; }

/* ── El menú de más acciones ───────────────────────────────────────────────
   Un `<details class="menu">` con su disparador y su desplegable. Lo pinta
   `ui/_menu_button.html`; lo que lleva adentro lo escribe cada pantalla.

   Es el TERCER menú que se escribe en esta hoja y el único que queda. Antes
   hubo `.user-menu` —el de la topbar, que se queda porque es oscuro y vive en
   otra superficie, pero ya comparte la clase para heredar el cierre— y
   `.crear-menu`, el de Plantillas, que se borró acá: sus 26 líneas tenían
   CERO usos en plantillas el 2026-08-11. La guarda de la hoja mira
   plantilla→CSS y no al revés, así que el CSS muerto no lo caza nadie; se vio
   contando a mano al escribir esto.

   Lo que hace que sea uno y no cuatro es que el disparador sea un parcial. La
   parte que se copia mal —apagar el marcador en Safari, poner el
   `aria-label`— es justo la que no se ve al mirar la pantalla.

   El cierre al clic de afuera y con Esc lo pone `menu.js`, en `document`. */
.menu { position: relative; }
.menu-trigger {
    list-style: none; cursor: pointer;
    display: inline-grid; place-items: center;
    width: 32px; height: 32px; border-radius: var(--radius-sm);
    color: var(--text-faint);
}
/* Safari pinta su propio triángulo y lo hace por `::-webkit-details-marker`,
   no por `list-style`. Las dos líneas hacen falta. */
.menu-trigger::-webkit-details-marker { display: none; }
.menu-trigger svg { width: 18px; height: 18px; }
.menu-trigger:hover { background: var(--bg); color: var(--text); }
.menu-trigger:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.menu[open] .menu-trigger { background: var(--bg); color: var(--text); }
/* A la DERECHA porque el menú vive al final de una fila: anclado a la
   izquierda, el desplegable de la última columna se sale de la tabla. */
/* EL MENÚ DE LA ÚLTIMA FILA SE CORTABA (Renzo, 2026-09-08, mirando las
   direcciones de un tercero). `.table-wrap` lleva `overflow-x: auto` para el
   desplazamiento lateral, y su propio comentario ya lo advierte: eso hace que
   el OTRO eje compute a `auto`, así que la tabla también recorta en vertical.
   El menú de la última fila caía 94px por debajo del borde y ahí se perdía
   —no se puede desplazar hasta él, porque un absoluto recortado no alarga
   nada—.

   **Se abre hacia arriba**, que es lo que hace todo el mundo y no mueve un
   píxel de la página. Con TRES filas o más, porque lo que recorta no es la
   ventana sino la tabla: encima de la última fila tiene que caber el menú, y
   eso son 97px contra una fila de 52 más el encabezado. Medido, no estimado.

   Con una o dos filas no cabe ni arriba ni abajo DENTRO de la tabla, y eso no
   lo arregla la hoja: lo hace `menu.js`, que en ese caso pasa el menú a
   `fixed` y lo decide contra la ventana. Acá vivió un `padding-bottom` que
   hacía crecer la tabla para darle sitio; se quitó porque empujaba media
   pantalla hacia abajo cada vez que alguien abría el menú de una ficha con
   una sola dirección —«lo veo todo mal», Renzo, el mismo día—.

   Sin `overflow: visible`: quitarle el recorte a la tabla mientras el menú
   está abierto le devolvería el scroll lateral a cero y dejaría que una
   tabla ancha se pintara fuera de la tarjeta, que es lo que §6.1 prohíbe. */
.table tbody:has(tr:nth-child(3)) tr:nth-last-child(-n+2) .menu-pop {
    top: auto; bottom: calc(100% + 6px);
}
.menu-pop {
    position: absolute; right: 0; top: calc(100% + 6px); z-index: 30;
    min-width: 220px; padding: 6px;
    background: var(--card-bg); color: var(--text);
    border: 1px solid var(--border); border-radius: var(--radius);
    box-shadow: var(--shadow-lg);
    text-align: left; white-space: nowrap;
}
/* Un `<form>` dentro del menú es el camino de lo destructivo —POST, nunca un
   GET que borra— y no tiene por qué separar de sus vecinos: la separación la
   pone el relleno del ítem. */
.menu-pop form { margin: 0; }
/* Sirve a un `<a>` y a un `<button>` con la misma regla: el ítem que navega y
   el que envía un formulario se ven igual porque para quien lee son lo
   mismo. `font: inherit` y `text-align` existen sólo por el `<button>`, que
   trae los suyos del navegador. */
.menu-item {
    display: flex; align-items: center; gap: var(--sp-2); width: 100%;
    padding: 8px 10px; border: 0; border-radius: var(--radius-sm);
    background: transparent; cursor: pointer;
    font: inherit; font-size: var(--fs-body); text-align: left;
    color: var(--text); text-decoration: none;
}
.menu-item:hover { background: var(--primary-soft); color: var(--primary-dark); }
.menu-item svg { width: 15px; height: 15px; flex: none; }
/* Lo que BORRA, en rojo y al final de la lista. El tinte llega en el hover y
   no en reposo: una lista con un ítem rojo permanente se lee como una alarma
   abierta, y esto es sólo una opción más hasta que se apunta a ella. */
.menu-item-danger:hover { background: var(--danger-bg); color: var(--danger-text); }
/* La raya entre lo que navega y lo que borra. Un `<hr>` traería sus márgenes
   de fábrica y habría que deshacerlos. */
.menu-sep { height: 1px; margin: 5px 6px; background: var(--border); }
/* El rótulo de un grupo de ítems, cuando el menú lista COSAS y no acciones
   —los hilos del Asistente— y hace falta decir de qué son. Se lee como el
   encabezado de una tarjeta, que es el mismo gesto en otro tamaño. */
.menu-titulo { margin: 4px 10px 6px; font-size: var(--fs-label); font-weight: 600;
               letter-spacing: .04em; text-transform: uppercase;
               color: var(--text-faint); }
/* El ítem VIGENTE: el orden que ya está aplicado, el hilo que estás leyendo.
   Nació en el ⋮ del tablero de Alistamiento y lo necesitó el menú de hilos:
   por la regla de la casa, sube acá en vez de copiarse. */
.menu-item[aria-current="true"] { font-weight: 600; color: var(--primary-dark); }

/* Con un menú abierto, el banner deja de recortar — su `overflow: hidden`
   existe para el círculo decorativo y se comería el desplegable; el círculo
   se esconde ese instante para no sangrar por las esquinas. */
.page-banner:has(.menu[open]) { overflow: visible; z-index: 30; }
.page-banner:has(.menu[open])::after { display: none; }
/* Y la tabla tampoco: `.table-wrap` hace scroll propio —es lo que evita que
   el `body` se desplace de lado— y ese scroll RECORTA. Sin esto, el menú de
   la última fila sale cortado por el filo de la tarjeta, que es un defecto
   que sólo aparece con datos y nunca en una pantalla de ejemplo con dos. */
.table-wrap:has(.menu[open]) { overflow: visible; }
/* Y lo mismo con el panel del buscador, que desde el 2026-08-26 vive DENTRO
   de una tabla —la fila de un movimiento de producción—: sus 340px de alto
   salían cortados a la altura del filo de la tarjeta, con los últimos
   resultados invisibles y sin barra que los alcanzara. Medido: 251px por
   debajo del recorte.

   `:not([hidden])` porque el panel siempre está en el marcado y lo que
   cambia es su atributo: sin eso, la tabla dejaría de recortar para
   siempre y perdería el desplazamiento lateral que §6.1 le exige. */
.table-wrap:has(.finder-panel:not([hidden])) { overflow: visible; }

/* Los ajustes de la superficie en el editor de bloques: plegados al pie
   del panel. El tope es de los bloques — la anatomía del customizer,
   donde los ajustes del tema viven en su propio botón. */
/* Los ajustes de la superficie (correo, página) plegados al pie del panel.
   Una TARJETA y no una línea gris con «⚙»: Renzo (2026-09-07) la veía
   perdida —glifo chiquito, rótulo chiquito—. El summary es un botón ancho
   con el engranaje a 20px, el rótulo al cuerpo y la flecha que gira. */
.fold { margin-top: var(--sp-3); }
.fold > summary {
    list-style: none; cursor: pointer;
    display: flex; align-items: center; gap: var(--sp-3);
    padding: 10px 12px;
    border: 1px solid var(--border); border-radius: var(--radius);
    background: var(--bg); color: var(--text);
}
.fold > summary::-webkit-details-marker { display: none; }
.fold > summary:hover { border-color: var(--text-faint); }
.fold[open] > summary { border-bottom-left-radius: 0; border-bottom-right-radius: 0; background: var(--card-bg); }
.fold-icon { flex: none; width: 20px; height: 20px; color: var(--primary); }
.fold-label { flex: 1; font-size: var(--fs-body); font-weight: 600; }
/* Cuántas cosas hay adentro, para decidir si vale la pena abrir. */
.fold-count {
    flex: none; padding: 1px 7px; border-radius: 999px;
    background: var(--primary-soft); color: var(--primary-dark);
    font-size: var(--fs-meta); font-weight: 600;
    font-variant-numeric: tabular-nums;
}
.fold-chevron { flex: none; width: 16px; height: 16px; color: var(--text-faint); transition: transform .15s ease; }
.fold[open] .fold-chevron { transform: rotate(180deg); }
.fold-body {
    padding: var(--sp-3) 12px var(--sp-2);
    border: 1px solid var(--border); border-top: 0;
    border-radius: 0 0 var(--radius) var(--radius);
}

/* ── Comandas: la barra ────────────────────────────────────────────────────
   La única pantalla del ERP que se lee DE REOJO, a un metro de distancia y
   con las manos ocupadas. Todo lo de acá sale de esa restricción y no del
   gusto: el número grande porque es lo que se grita, la grilla de tarjetas
   porque una tabla obliga a seguir una fila con el dedo, y los botones
   anchos porque se tocan con el pulgar y no con un cursor.

   `auto-fill` con mínimo de 260px: la misma pantalla sirve en la tablet del
   mostrador (dos tarjetas) y en el monitor de la barra (cinco o seis), sin
   una consulta de medios por aparato. */
.kds-board {
    display: grid; gap: var(--sp-3);
    grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
    align-items: start;
}
.kds-card { padding: var(--sp-3); display: flex; flex-direction: column;
            gap: var(--sp-2); }
/* La demorada, en rojo y con borde: el color solo no alcanza a un metro de
   distancia, y el borde es lo que la separa del resto de un vistazo. Solo
   mientras sigue abierta — ver el comentario de la plantilla. */
.kds-card-late { background: var(--danger-bg); border-color: var(--danger-text); }
.kds-card-head { display: flex; align-items: baseline;
                 justify-content: space-between; gap: var(--sp-2); }
/* El número ES el contenido de la tarjeta: es lo que el barista grita y lo
   que el cliente mira en su tiquete. Enlaza a la ficha, y por eso se
   subraya al pasar por encima como cualquier enlace del ERP. */
.kds-number { font-size: var(--fs-number); font-weight: 700; line-height: 1;
              color: var(--text); text-decoration: none; }
.kds-number:hover { text-decoration: underline; }
/* Los minutos esperando. En versalitas y apagado: informa, no compite con
   el número. En la demorada hereda el rojo de la tarjeta. */
.kds-clock { font-size: var(--fs-badge); font-weight: 700; color: var(--text-light);
             text-transform: uppercase; letter-spacing: .06em; }
.kds-card-late .kds-clock { color: var(--danger-text); }
.kds-customer { margin: 0; font-size: var(--fs-small); color: var(--text-light); }
.kds-lines { margin: 0; padding: 0; list-style: none; display: flex;
             flex-direction: column; gap: 4px; }
.kds-line { display: flex; gap: var(--sp-2); font-size: var(--fs-body);
            color: var(--text); }
/* La cantidad en su propia columna y tabular: «2 un» y «250 g» tienen que
   alinearse, o la lista se lee como un párrafo. */
.kds-line-qty { flex: 0 0 4.5rem; font-weight: 700;
                font-variant-numeric: tabular-nums; }
.kds-note { margin: 0; padding: 6px 8px; border-radius: var(--radius-field);
            background: var(--warn-bg); color: var(--warn-text);
            font-size: var(--fs-small); }
/* El avance ocupa el ancho; cancelar va debajo, chico y sin color. Es la
   misma jerarquía que el «Salir» del kiosco: lo destructivo sobre una
   pantalla que se toca todo el día no puede verse como lo frecuente. */
.kds-actions { margin-top: auto; display: flex; flex-direction: column;
               gap: 6px; align-items: stretch; }
.kds-actions form { margin: 0; }
.kds-actions .btn-small { align-self: center; }

/* ── La dirección que alguien va a teclear en un aparato ───────────────────
   La URL que se copia y se pega en la tablet del mostrador, o que se teclea
   con el control remoto de un televisor. En monoespaciada y en su propia caja
   porque es un dato que se LEE carácter por carácter, no una frase.

   Nació como `.kds-url` y se renombró al llegar la segunda pantalla que la
   necesitaba —el editor de Pantallas, con la misma tarea: copiar una
   dirección y llevarla a un aparato—. Un `.kds-url` dentro de `signage` es
   cómo empiezan los primos que `docs/marco-ui.md` viene a evitar; el nombre
   dice qué ES y no de qué módulo salió. */
.url-box {
    display: flex; align-items: center; gap: var(--sp-2);
    padding: var(--sp-2) var(--sp-3); margin-bottom: var(--sp-2);
    border: 1px solid var(--border); border-radius: var(--radius-field);
    background: var(--bg);
}
/* Dentro de una celda el margen de abajo sobra: la celda centra el bloque
   entero, y con el margen la caja quedaba unos píxeles más arriba que el
   nombre de al lado (Repartidores, 2026-09-14). */
.table td .url-box { margin-bottom: 0; }
/* `break-all` y no `nowrap`: en una pantalla angosta la URL con un UUID no
   cabe, y cortarla con puntos suspensivos esconde justo la parte que hay que
   copiar. */
.url-box-valor { flex: 1; font-size: var(--fs-small); color: var(--text);
                 word-break: break-all; }
.url-box-acciones { display: flex; gap: var(--sp-2); }
/* El botón de copiar. Existía el marcado —`data-copy` en la pantalla de
   dominios— y no existían ni la regla ni el JavaScript: el botón se veía
   como texto suelto y no copiaba nada. `assets/js/copy.js` es la otra
   mitad. */
.btn-copy { white-space: nowrap; }

/* ── El televisor del editor de Pantallas ──────────────────────────────────
   El marco donde vive el reproductor de verdad, en un `<iframe>`. La
   proporción es la del APARATO —16/9, la de cualquier televisor de sala— y se
   mantiene aunque la cartelera de adentro esté girada: lo que gira es el
   contenido, no el aparato colgado de la pared.

   `aspect-ratio` y no el truco del `padding-top`: el truco obliga a
   posicionar el hijo en absoluto y a que el ALTO sea una consecuencia del
   ancho, así que no hay forma de ponerle un techo sin romperlo. Acá el techo
   es una línea (`max-height`) y hace falta: a 1600px de ventana la columna
   principal mide 900 y un 16/9 sin tope son 506px de televisor, que empuja el
   guion —la tabla de láminas— debajo del pliegue. */
.tv-marco {
    position: relative; width: 100%;
    aspect-ratio: 16 / 9; max-height: 60vh;
    /* Negro literal y no un token: es lo que se ve entre dos láminas y lo que
       muestra una pantalla apagada, o sea el color del APARATO. Un gris de la
       hoja lo pintaría de ERP y ya no se parecería al televisor. */
    background: #000;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-field);
    overflow: hidden;
}
.tv-marco iframe {
    position: absolute; inset: 0; width: 100%; height: 100%; border: 0;
    display: block;
}
/* El rótulo de por qué está negro.
   Un rectángulo negro no distingue «apagada» de «sin láminas» de «se está
   cargando», y las tres se arreglan de forma distinta. Sin esto, quien mira
   el editor cree que el reproductor está roto — que es justamente lo que la
   vista previa venía a descartar antes de subirse a la silla. */
.tv-aviso {
    position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
    padding: var(--sp-2) var(--sp-4);
    border-radius: 999px;
    background: rgba(255, 255, 255, .10);
    color: #fff; font-size: var(--fs-small); font-weight: 600;
    text-align: center; pointer-events: none;
}

/* La banda de abajo del marco: el pie con lo que el televisor está haciendo.
   Va PEGADA al marco y no en el encabezado de la tarjeta porque describe lo
   que se está mirando —«5 láminas · 40 s de vuelta»—, y a media pantalla de
   distancia de la imagen eso deja de leerse como su pie. */
.tv-pie {
    display: flex; flex-wrap: wrap; align-items: center;
    gap: var(--sp-1) var(--sp-3);
    margin-top: var(--sp-2);
    color: var(--text-light); font-size: var(--fs-small);
}

/* ── Reparto ─────────────────────────────────────────────────────────────
   El tablero del día: una columna por zona, más «Sin ubicar» y «Sin zona».
   Es una tira que SE DESPLAZA de lado en su propio contenedor y no en el
   body — la regla de la casa para lo ancho, la misma de `table-wrap`: con
   seis zonas y una pantalla de portátil, envolver las columnas en dos filas
   rompe la metáfora de las pilas de la bodega. */
.reparto-tablero {
    display: flex; gap: var(--sp-4);
    overflow-x: auto; padding-bottom: var(--sp-2);
}
.reparto-columna { flex: 0 0 320px; max-width: 320px; }

/* El color de la zona, el mismo del polígono en el mapa: es lo que hace que
   una columna y su dibujo se reconozcan como la misma cosa de un vistazo. */
.reparto-color {
    display: inline-block; width: 14px; height: 14px; border-radius: 4px;
    flex: 0 0 auto;
}

.reparto-pila { list-style: none; margin: 0; padding: 0; }
.reparto-pila > li {
    padding: var(--sp-2) 0;
    border-bottom: 1px solid var(--border);
}
.reparto-pila > li:last-child { border-bottom: 0; }
.reparto-pila-mas { margin: var(--sp-2) 0 0; color: var(--text-light); font-size: var(--fs-small); }

/* El desplegable de corregir un pin a mano: cerrado por omisión porque son
   dos clics al año, y abierto ocupa el alto de un mapa por cada pedido sin
   ubicar. */
.reparto-pin > summary { list-style: none; cursor: pointer; }
.reparto-pin > summary::-webkit-details-marker { display: none; }


/* **El mapa se le montaba a la banda del ERP.** Leaflet pinta sus paneles en
   `z-index: 400` y sus controles en `1000`, y la escala de esta hoja es
   topbar 100, sidebar 101, savebar 110, drawer 200: le gana a todas.

   La salida NO es subir la topbar —eso es una carrera que se pierde con el
   siguiente widget— sino encerrar el mapa en su propio contexto de
   apilamiento: un elemento posicionado con `z-index` distinto de `auto`
   contiene adentro toda la escala de sus hijos, así que el 1000 de Leaflet
   pasa a valer 1000 DENTRO del cuadro y 0 fuera de él. */
.reparto-mapa { position: relative; z-index: 0; }

.reparto-mapa {
    height: 260px; margin: var(--sp-2) 0;
    border: 1px solid var(--border); border-radius: var(--radius);
    overflow: hidden;
}
.reparto-mapa-alto { height: 420px; }

/* La hoja de ruta impresa: papel, porque el teléfono se descarga y la moto
   no tiene cargador. */
.reparto-hoja { padding: var(--sp-5); color: #000; background: #fff; }
.reparto-hoja h1 { margin: 0 0 var(--sp-2); }

/* El pin numerado que pinta Leaflet. Vive en la hoja del ERP y también en la
   del reparto: son dos superficies distintas que dibujan el mismo mapa, y un
   pin sin regla se ve como un cuadro azul sin número. */
.mapa-pin span {
    display: grid; place-items: center;
    width: 26px; height: 26px; border-radius: 50%;
    color: #fff; font-size: var(--fs-small); font-weight: 700;
    box-shadow: 0 1px 4px rgba(0, 0, 0, .4);
}

/* El enlace de un repartidor dentro de su fila. Se trunca porque es un UUID
   —nadie lo lee, se copia— y a lo ancho empujaba el menú de acciones fuera
   de la tarjeta. */
.reparto-enlace { max-width: 320px; }
.reparto-enlace .url-box-valor {
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* Los avisos del reparto: «Sin ubicar» y «Sin zona», ARRIBA del tablero.
   Envuelven en vez de desplazarse —al revés que la tira de zonas— porque son
   lo que alguien tiene que atender hoy, y lo que se atiende no puede quedar
   fuera de la pantalla. */
.reparto-avisos {
    display: flex; flex-wrap: wrap; gap: var(--sp-4);
    margin-bottom: var(--sp-4);
}
.reparto-aviso { flex: 1 1 320px; min-width: 300px; }

/* AQUÍ VIVIÓ el aire de la columna que sigue a una cifra, retirado el
   2026-08-14 junto con `.cell-pad`.

   Decía: «una columna `num` está pegada a su borde derecho por definición,
   así que la siguiente empieza justo ahí» — «POR COBRARESTADO» en el
   encabezado, el monto besando la pastilla de estado en cada fila. Su
   premisa era que `.table thead th` no lleva relleno horizontal, y desde
   hoy sí lo lleva: la regla pedía exactamente `var(--sp-4)`, que es el
   valor por omisión, o sea que ya no hacía nada.

   Vale la pena dejar dicho el patrón, porque es el argumento del cambio
   entero: el MISMO defecto se parchó TRES veces por separado —`.cell-pad`
   en contabilidad, esta regla en pedidos, y «Accesorios4» en productos, que
   no se parchó nunca— antes de que alguien lo arreglara donde nace. Tres
   parches locales de una falla global son la señal de que el arreglo está
   en el lugar equivocado. */

/* ── El tablero del reparto, segunda pasada ──────────────────────────────
   Lo que cambió y por qué: la columna de una zona era un encabezado de
   tarjeta con la cuenta en letra pequeña, y la pregunta que se hace parado
   frente al tablero es «¿cuántos hay en el norte?». Ahora la cifra es lo
   grande y el nombre lo acompaña — al revés de como estaba. */
.reparto-zona-head {
    display: flex; align-items: center; gap: var(--sp-3);
    padding: var(--sp-4) var(--sp-4) var(--sp-3);
}
/* La baldosa con el COLOR de la zona: el mismo del polígono en el mapa, que
   es lo que hace que la columna y su dibujo se reconozcan como la misma
   cosa. El color entra por `--zona` desde la fila, porque lo elige la
   empresa y una hoja no puede tener una regla por zona de cada cliente. */
.reparto-tile { --zona: var(--primary); }
.reparto-tile .tile { background: var(--zona); color: #fff; }
.reparto-zona-nombre {
    margin: 0; font-size: var(--fs-small); font-weight: 700;
    letter-spacing: .04em; text-transform: uppercase;
    color: var(--text-light);
}
.reparto-zona-cuenta { margin: 2px 0 0; color: var(--text-light); }
.reparto-zona-cuenta strong {
    font-size: var(--fs-section); color: var(--text); margin-right: 4px;
}
.reparto-armar { padding: var(--sp-3) var(--sp-4) var(--sp-4); }

/* El enlace que continúa una frase: «…una zona que los cubra. Dibuja una
   zona ›». La flecha va en `::after` y no en el texto para que no se pueda
   copiar ni la lea un lector de pantalla como un carácter suelto. */
.link-next { font-weight: 600; white-space: nowrap; }
.link-next::after { content: " ›"; }

/* El avance de una ruta: una longitud, no dos números que hay que dividir.
   El texto se queda al lado porque una barra sola no dice cuántas paradas
   son — y «0 de 3» y «0 de 30» se ven igual de vacías. */
.reparto-avance { display: flex; align-items: center; gap: var(--sp-3); }
.reparto-barra {
    display: block; width: 120px; height: 6px; border-radius: 999px;
    background: var(--border); overflow: hidden; flex: 0 0 auto;
}
.reparto-barra > span {
    display: block; height: 100%; border-radius: 999px;
    background: var(--primary);
}
.reparto-avance-texto {
    color: var(--text-light); font-size: var(--fs-small);
    font-variant-numeric: tabular-nums;
}

/* El estado con punto. NO es una pastilla: en una tabla de veinte rutas
   veinte pastillas pintan más que las filas. El punto da el color y la
   palabra da el significado a quien no distingue los colores. */
.reparto-estado { display: inline-flex; align-items: center; gap: var(--sp-2); }
.reparto-estado::before {
    content: ""; width: 7px; height: 7px; border-radius: 50%;
    background: var(--text-light); flex: 0 0 auto;
}
.reparto-estado-draft::before { background: var(--text-light); }
.reparto-estado-assigned::before { background: var(--primary); }
.reparto-estado-in_progress::before { background: var(--ok-text); }
.reparto-estado-closed::before { background: var(--border); }
.reparto-estado-cancelled::before { background: var(--danger-text); }

/* ── El árbol de trazabilidad ──────────────────────────────────────────────
   Los dos árboles de la ficha del lote: de dónde vino la tanda y a dónde
   fue. La sangría con borde a la izquierda y no con viñetas: lo que hay que
   seguir con el ojo es la PROFUNDIDAD —cuántos pasos hay entre el saco del
   proveedor y la bolsa que se vendió—, y una viñeta por nivel compite con
   el enlace del lote, que es lo único en lo que se hace clic.

   Sin `list-style` y sin números por la misma razón: los hermanos de un
   nivel no tienen orden que importe —dos componentes de una receta entraron
   juntos— y numerarlos sugeriría una secuencia que el libro no afirma. */
.trace-branch { list-style: none; margin: 0; padding: 0 0 0 var(--sp-3); }
.trace-branch .trace-branch {
    border-left: 1px solid var(--border); margin-top: var(--sp-2);
}
.trace-node { padding: var(--sp-2) 0 0 var(--sp-3); font-size: var(--fs-small); }
/* La hoja —una entrada o una venta— apagada contra el lote: el lote es el
   nodo que se sigue, y esto es el dato que lo explica. */
.trace-leaf {
    margin: 2px 0 0; font-size: var(--fs-small); color: var(--text-light);
}

/* La sub-fila de una tabla: el reparto por lote de una línea de despacho.
   Apagada y con sangría contra su fila madre — es el detalle de la fila de
   arriba, no una fila más: sin la distinción, una línea con tres lotes se
   lee como cuatro líneas despachadas. */
.row-sub > td { background: var(--bg); font-size: var(--fs-small); }
.row-sub > td:first-child { padding-left: var(--sp-4); }

/* Paquetes: la hija de un paquete se sangra bajo su contenedora en la
   tabla de líneas del pedido y en la captura. Solo un margen y el tono
   apagado — no es otra tabla. */
.table .line-child td:first-child { padding-left: 2.25rem; color: var(--text-light); }
.line-child-mark { margin-right: .35rem; }

/* ── Analíticas ──────────────────────────────────────────────────────────
   Los bloques de un informe, en cualquier superficie: el tablero, la
   biblioteca, la vista previa del constructor y —desde el plan d— la ficha
   de otro módulo.

   `--fs-stat` y `tabular-nums` son los mismos de `.home-metric-value`, y eso
   es a propósito: la tira del escritorio va a PASAR a ser un bloque de
   éstos, y si los dos números no se ven idénticos la migración se nota. Las
   reglas de `.home-metric*` se van con ella. */
/* El título de una tarjeta que se puede abrir. Va en la familia
   `card-head-*` y no se llama `.block-title`: ese nombre ya es del editor
   de bloques de pasos, y dos cosas distintas con el mismo nombre es cómo
   empezaron los tres menús desplegables.

   Es un enlace pero no se pinta como uno: en una rejilla de seis, seis
   títulos subrayados son ruido. Se delata al pasar el mouse, que es cuando
   alguien está por hacer clic. */
.card-head-link { color: inherit; text-decoration: none; }
.card-head-link:hover { color: var(--primary); text-decoration: underline; }

.block-value {
    font-size: var(--fs-stat); font-weight: 700; color: var(--text);
    letter-spacing: -.01em; line-height: 1.1; margin: 0;
    font-variant-numeric: tabular-nums;
}
.block-delta {
    display: inline-flex; align-items: center; gap: 2px; margin: 4px 0 0;
    font-size: var(--fs-badge); font-weight: 700; line-height: 1;
}
.block-delta-up { color: var(--ok-text); }
.block-delta-down { color: var(--danger-text); }
/* La nota del pie: lo que el número NO dice. «Sin período anterior con qué
   comparar» vive acá, y es la razón por la que un delta ausente no se pinta
   como 0%. */
.block-note {
    margin: 6px 0 0; font-size: var(--fs-small); color: var(--text-light);
}
/* El bloque que no se pudo calcular. NO es un error de sistema: es una
   pregunta que ya no se puede hacer porque su fuente cambió, y se lee como
   tal — tono de aviso, no de alarma. */
.block-error {
    margin: 0; padding: var(--sp-3); border-radius: var(--radius);
    background: var(--warn-bg); color: var(--warn-text);
    font-size: var(--fs-small);
}
/* Alto FIJO y no automático: Chart.js mide su contenedor al arrancar, y con
   alto automático el lienzo nace en 0 y la gráfica no se ve nunca. */
.block-chart { position: relative; height: 260px; margin-top: var(--sp-2); }

/* La serie con DOS líneas: cada una con su punto, su nombre y su total en
   una fila. Los colores son los mismos tokens que `analiticas.js` lee para
   las líneas —`--primary` y `--info-text`—, así el punto y la línea no se
   pueden separar. */
.series-legend {
    list-style: none; margin: 0; padding: 0;
    display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-5);
}
.series-legend-item { display: flex; align-items: baseline; gap: var(--sp-2); }
.series-legend-label { font-size: var(--fs-small); color: var(--text-light); }
.series-dot {
    flex: none; width: 10px; height: 10px; border-radius: 3px;
    align-self: center; background: var(--primary);
}
.series-dot-1 { background: var(--info-text); }
.series-legend .block-delta { margin: 0; color: var(--text-light); }

/* El embudo: barras propias, sin librería. Cuatro divs no justifican una. */
.funnel { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-2); }
.funnel-step {
    display: grid; grid-template-columns: 12rem 1fr auto; gap: var(--sp-3);
    align-items: center;
}
.funnel-label { font-weight: 600; }
.funnel-bar {
    height: 22px; border-radius: var(--radius);
    background: var(--primary); width: var(--ancho, 0%); min-width: 2px;
}
.funnel-rate {
    font-size: var(--fs-small); color: var(--text-light);
    font-variant-numeric: tabular-nums; white-space: nowrap;
}

/* EL CONSTRUCTOR.

   El informe ocupa el lienzo y las decisiones viven en un riel a la
   derecha. Hasta el 2026-09-09 eran dos tarjetas de formulario arriba y la
   vista previa de tercera, debajo del pliegue: se armaba a ciegas. Un
   informe se lee mirándolo mientras se cambia una cosa a la vez, y para eso
   las dos tienen que caber en la misma pantalla.

   El riel tiene ancho FIJO y el lienzo se queda con el resto: con dos
   fracciones, una tabla de ocho columnas empujaba el riel hasta dejar los
   rótulos en dos renglones. */
.builder {
    display: grid; gap: var(--sp-5);
    grid-template-columns: minmax(0, 1fr) 300px;
    align-items: start;
}
.builder-canvas { min-width: 0; }
/* Pegajoso: el riel es más corto que un informe con tabla larga, y bajar a
   ver una fila no debería costar el control que se estaba tocando. El tope
   lo pone la topbar del shell. */
.builder-controls {
    position: sticky; top: var(--sp-4);
    max-height: calc(100vh - var(--sp-7)); overflow-y: auto;
    padding: var(--sp-4);
    border: 1px solid var(--border); border-radius: var(--radius);
    background: var(--card-bg);
}

/* La banda: de dónde sale y de cuándo. Las tres decisiones que cambian
   TODO lo demás, en una fila y separadas de las que solo cambian el
   dibujo. El nombre se estira porque es el único texto libre. */
.builder-band {
    display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--sp-3);
    margin-bottom: var(--sp-2);
}
.band-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.band-field:first-child { flex: 1 1 260px; }
.band-label {
    font-size: var(--fs-label); font-weight: 600; color: var(--text-light);
}
.builder-band select, .builder-band input { width: auto; min-width: 160px; }
.band-field:first-child input { width: 100%; }
.builder-note {
    margin: 0 0 var(--sp-4); font-size: var(--fs-small);
    color: var(--text-light);
}

.control-group { margin-bottom: var(--sp-5); }
.control-group:last-child { margin-bottom: 0; }
.control-title {
    margin: 0 0 var(--sp-3);
    font-size: var(--fs-small); font-weight: 600; color: var(--text);
}
.control-note {
    margin: 0; font-size: var(--fs-small); color: var(--text-light);
}

/* Las medidas y las agrupaciones se ven COMPLETAS y no dentro de un
   desplegable: cada módulo declara qué mide, y esconder esa lista detrás de
   un clic es esconder justo lo que la fuente sabe hacer. Radios de verdad,
   así que el cambio viaja sin JavaScript. */
.pick { display: flex; flex-direction: column; gap: 2px; }
.pick-row {
    display: flex; align-items: center; gap: var(--sp-3);
    padding: 7px var(--sp-2); border-radius: var(--radius-sm);
    font-size: var(--fs-small); cursor: pointer;
}
.pick-row:hover { background: var(--bg); }
.pick-row:has(input:checked) { background: var(--primary-soft); }
.pick-row input[type="radio"] { width: 16px; height: 16px; }
.pick-row input:checked { box-shadow: inset 0 0 0 4px var(--primary); }
.pick-name { flex: 1; min-width: 0; }
.pick-note { color: var(--text-light); }

/* Las formas de pintar, en baldosas: son ocho y se distinguen por su
   glifo antes que por su nombre. El radio se esconde acá —y SOLO acá— y la
   baldosa entera es el control; el foco se dibuja en la baldosa. */
.viz-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px; }
.viz-tile {
    display: flex; flex-direction: column; align-items: center; gap: 4px;
    padding: var(--sp-2) 2px; border-radius: var(--radius-sm);
    color: var(--text-light); cursor: pointer; text-align: center;
}
.viz-tile:hover { background: var(--bg); color: var(--text); }
.viz-tile input[type="radio"] { position: absolute; opacity: 0; pointer-events: none; }
.viz-tile:has(input:checked) {
    background: var(--primary-soft); color: var(--primary-dark);
}
.viz-tile:has(input:focus-visible) { outline: 2px solid var(--primary); outline-offset: 1px; }
.viz-glyph { width: 18px; height: 18px; }
.viz-name { font-size: var(--fs-meta); line-height: 1.2; }

/* El informe pintado. Es lo que ocupa el lienzo, y por eso acá —y solo
   acá— crece: en un tablero el anillo comparte fila con otros dos bloques y
   200px es lo que le toca; en el constructor tiene el lienzo entero y ese
   mismo anillo se lee como una miniatura de sí mismo. La gráfica también
   respira más alto por la misma razón. */
.analytics-preview { margin-top: var(--sp-2); }
/* En el lienzo la tarjeta no lleva encabezado: el nombre del informe ya
   está en el banner de la pantalla Y en el campo «Nombre» a dos dedos de
   ahí, y la ventana la dice la banda. Tres veces lo mismo en cien píxeles.
   Lo que queda es el informe, que es de lo que va la pantalla. En un
   tablero el encabezado SÍ hace falta: ahí la tarjeta es una entre varias
   y su nombre es lo único que la distingue. */
.builder-canvas .block > .card-header { display: none; }
.builder-canvas .ring { grid-template-columns: minmax(0, 320px) minmax(0, 1fr); }
.builder-canvas .block-chart { min-height: 320px; }

/* La sección de filtros es un plegable de la casa —`.fold`, el mismo de
   «Ajustes del correo»— y acá solo se le quita el aire de más: dentro del
   riel arranca pegada al grupo anterior y su cuerpo no necesita el sangrado
   de un panel ancho. */
.control-fold { margin: 0; }
.control-fold > .fold-body { padding: var(--sp-3) var(--sp-2) var(--sp-2); }
/* Lo puesto arriba, lo demás como lista de nombres. Sin JavaScript: un
   `<details>` se abre solo, y el filtro que se llena sube al repintar. */
.filters-set .field:last-child { margin-bottom: var(--sp-3); }
/* Cada filtro libre es UNA línea que se abre sola. Abrir «Agregar un
   filtro» y encontrar siete controles era la misma pared de campos que la
   sección venía a evitar, solo que detrás de una puerta. El marcador se
   quita por partida doble: `list-style` para todos y el pseudoelemento de
   WebKit, que no lo obedece. */
/* El renglón arranca DEBAJO de la línea, no encima: resaltado y pegado al
   separador se leen como una sola pieza (Renzo, 2026-09-09). Poco cerrado
   —la lista es de ocho— y más cuando está abierto, que es cuando el
   resaltado tiene un control debajo y necesita marcar dónde empieza. */
.filter-slot + .filter-slot { border-top: 1px solid var(--border); }
.filter-slot { padding-top: var(--sp-1); }
.filter-name {
    display: flex; align-items: center; gap: var(--sp-2);
    padding: 7px var(--sp-2); border-radius: var(--radius-sm);
    font-size: var(--fs-small); color: var(--text); cursor: pointer;
    list-style: none;
    /* CONSTANTE, no solo al abrir. Cerrado separa el renglón de la línea
       de abajo; abierto es el aire hasta el control. Con un valor por
       estado el renglón se corría cuatro píxeles al abrirse y se veía
       como un brinco (Renzo, 2026-09-09). */
    margin-bottom: var(--sp-1);
}
.filter-name::-webkit-details-marker { display: none; }
.filter-name::before {
    content: '+'; width: 14px;
    color: var(--text-light); font-weight: 600; text-align: center;
}
/* Abierto, el renglón se separa del control: resaltado y pegado al campo
   se leían como una sola pieza —el rótulo pintado del mismo color que la
   caja de abajo— (Renzo, 2026-09-09). El mismo aire que lleva por debajo. */
.filter-slot[open] > .filter-name { font-weight: 600; }
.filter-slot[open] > .filter-name::before { content: '−'; }
.filter-name:hover { background: var(--bg); }
/* El aire de abajo lo pone el RENGLÓN con relleno, no el campo con
   margen: el margen del campo se colapsa hacia afuera del `<details>`
   —que no tiene ni borde ni relleno que lo detenga— y el control quedaba
   pegado a la línea de separación de abajo (Renzo, 2026-09-09). El
   relleno no colapsa nunca. */
/* Solo por DEBAJO: lo que se agregue arriba mueve el renglón al abrirlo, y
   eso es el brinco. El aire de arriba lo pone `.filter-slot`, siempre. */
.filter-slot[open] { padding-bottom: var(--sp-3); }
/* El campo, ya sin rótulo: lo dice el renglón que se acaba de abrir. */
.field-desnudo { margin: 0 var(--sp-2); }
.filters-list { border-top: 1px solid var(--border); }
.filters-list .field:last-child { margin-bottom: 0; }

/* La rejilla del tablero: doce columnas, y cada bloque dice cuántas ocupa
   con `--ancho-bloque`. Doceavos y no porcentajes porque es el vocabulario
   que el shell ya usa, y dos rejillas con dos vocabularios es cómo se llega
   a que un bloque de «media pantalla» mida distinto en dos sitios. */
.analytics-grid {
    display: grid; gap: var(--sp-4);
    grid-template-columns: repeat(12, minmax(0, 1fr));
}
/* Cada tarjeta de una fila mide lo que la más alta.

   La rejilla ya estira la CELDA —`align-items: stretch` es el defecto de
   grid—, pero la tarjeta de adentro conservaba su alto natural y quedaba
   flotando arriba con un hueco debajo. Tres reglas: la celda es un flex,
   la tarjeta crece hasta llenarla, y lo que crece adentro es la GRÁFICA —
   no el título ni la nota del pie, que miden lo que miden.

   Así una fila con un anillo, una serie y una tabla de una fila termina
   pareja, y las tres gráficas quedan con el mismo alto útil. */
.analytics-grid-item { grid-column: span var(--ancho-bloque, 6); display: flex; }
.analytics-grid-item > .block {
    flex: 1; display: flex; flex-direction: column; min-width: 0;
}
.analytics-grid-item > .block > .block-chart,
.analytics-grid-item > .block > .ring,
.analytics-grid-item > .block > .funnel {
    flex: 1; min-height: 180px;
}
/* La tabla no se estira: una tabla de dos filas estirada a 400px es una
   fila de datos y trescientos píxeles de nada. Lo que crece es el hueco
   debajo, que es honesto. */
.analytics-grid-item > .block > .table { margin-bottom: auto; }
@media (max-width: 900px) {
    .analytics-grid-item { grid-column: 1 / -1; }
}
@media (max-width: 1100px) {
    /* El riel baja debajo del informe: a menos de eso, 300px de controles
       dejan la gráfica en una tira. Y deja de ser pegajoso, o taparía lo
       que se está mirando. */
    .builder { grid-template-columns: minmax(0, 1fr); }
    .builder-controls { position: static; max-height: none; }
}
@media (max-width: 900px) {
    .funnel-step { grid-template-columns: 1fr; gap: 2px; }
}

/* El anillo: el total en el centro y la leyenda al lado, como el referente
   de Shopify que Renzo pasó el 2026-09-08. La leyenda es HTML y no la de
   Chart.js —que solo pinta rótulos— porque quien mira un anillo quiere el
   número al lado, no pasar el mouse por cada gajo. */
.ring {
    display: grid; gap: var(--sp-4); align-items: center;
    grid-template-columns: minmax(0, 200px) minmax(0, 1fr);
    margin-top: var(--sp-2);
}
.ring-chart { position: relative; height: 200px; }
/* El centro NO intercepta el mouse: encima del lienzo, Chart.js dejaría de
   recibir el hover y se perderían los tooltips de cada gajo. */
.ring-center {
    position: absolute; inset: 0; pointer-events: none;
    display: flex; flex-direction: column; align-items: center;
    justify-content: center; gap: 2px; text-align: center;
}
.ring-total {
    font-size: var(--fs-stat); font-weight: 700; color: var(--text);
    line-height: 1.05; letter-spacing: -.01em;
    font-variant-numeric: tabular-nums;
}
.ring-dash { color: var(--text-light); font-size: var(--fs-small); }
.ring-legend { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
/* El rótulo primero y el valor debajo cuando no cabe: en una tarjeta de un
   tercio de pantalla, «Bodega Itagüí · $256.168.130» en una línea recortaba
   el NOMBRE a «B.» — que es la mitad inútil de la fila. */
.ring-legend-item {
    display: grid; grid-template-columns: 10px minmax(0, 1fr);
    gap: 2px var(--sp-2); align-items: baseline;
    font-size: var(--fs-small);
}
.ring-legend-value { grid-column: 2; }
.ring-dot { width: 10px; height: 10px; border-radius: 3px; background: var(--border); }
.ring-legend-label {
    color: var(--text); overflow: hidden; text-overflow: ellipsis;
    white-space: nowrap; font-weight: 600;
}
.ring-legend-value {
    color: var(--text-light); font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* La banda de período del tablero: el mismo control del escritorio, en su
   propia píldora sobre la rejilla. */
.analytics-period {
    display: flex; align-items: center; gap: var(--sp-2);
    padding: 6px 14px;
    background: var(--card-bg); border: 1px solid var(--border);
    border-radius: var(--radius-card); box-shadow: var(--shadow);
    width: fit-content;
}
/* La fila de arriba del tablero: el período a la izquierda y, editando, la
   barra de edición a la derecha. El margen de abajo es de la FILA y no de
   la píldora: así las dos piezas bajan parejas. */
.analytics-toolbar {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--sp-3); flex-wrap: wrap; margin-bottom: var(--sp-4);
}

/* ── El tablero en modo edición ──────────────────────────────────────────
   Se edita ENCIMA del tablero, con los datos puestos: lo que se acomoda es
   lo que se va a ver. El panel de la izquierda ofrece lo que falta; cada
   tarjeta trae su agarre, su × y el tirador de la esquina. Los asideros de
   JavaScript son atributos `data-`; las clases de acá solo visten. */
/* Primera y a todo lo ancho: es lo primero que hay que saber al entrar, y
   una nota angosta al lado del rango se leía como parte del rango. */
.tablero-editbar {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--sp-3); flex-wrap: wrap;
    margin-bottom: var(--sp-4); padding: 6px 6px 6px 14px;
    background: var(--primary-soft); border: 1px solid var(--border);
    border-radius: var(--radius-card);
}
.tablero-editbar-text { margin: 0; font-size: var(--fs-small); color: var(--text); }
.tablero-editbar-actions { display: flex; gap: var(--sp-2); }

.tablero-editando {
    display: grid; grid-template-columns: 300px minmax(0, 1fr);
    gap: var(--sp-4); align-items: start;
}
/* El panel se queda a la vista mientras la rejilla hace scroll: se agrega
   mirando el tablero, no volviendo arriba cada vez. */
.tablero-panel { position: sticky; top: var(--sp-4); }
.tablero-panel-search { margin-bottom: var(--sp-3); }
.tablero-panel-search input[type="search"] { min-width: 0; width: 100%; }
.tablero-panel-group {
    margin: var(--sp-4) 0 var(--sp-2); font-size: var(--fs-small);
    font-weight: 600; letter-spacing: .04em; text-transform: uppercase;
    color: var(--text-light);
}
.tablero-panel-list { list-style: none; margin: 0; padding: 0; }
.tablero-panel-row {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--sp-3); padding: 7px 0; border-top: 1px solid var(--border);
}
/* `hidden` es «ya está en el tablero». El `display: flex` de arriba le gana
   a la regla del navegador para `[hidden]`, así que sin esto la fila se
   veía igual con el atributo puesto. */
.tablero-panel-row[hidden] { display: none; }
.tablero-panel-lead { display: flex; flex-direction: column; min-width: 0; }
.tablero-panel-name {
    font-size: var(--fs-body); color: var(--text);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tablero-panel-hint { font-size: var(--fs-small); color: var(--text-light); }
.tablero-panel-empty { padding: 7px 0; font-size: var(--fs-small); color: var(--text-light); }
/* Lo que el buscador dejó afuera. Aparte de `hidden` —que es «ya está en el
   tablero»— para que borrar la búsqueda no devuelva lo que ya está puesto. */
.tablero-panel-row.is-filtered { display: none; }

/* Las tarjetas mientras se edita: la rejilla arrastrable es la marca. La
   banda de la tarjeta deja sitio al agarre y a la × en vez de taparlos. */
.analytics-grid[data-drag-sort] .analytics-grid-item { position: relative; }
.analytics-grid[data-drag-sort] .analytics-grid-item > .block > .card-header {
    padding-left: calc(var(--sp-5) + 26px); padding-right: calc(var(--sp-5) + 30px);
}
.analytics-grid[data-drag-sort] .analytics-grid-item:hover > .block {
    outline: 2px dashed var(--primary); outline-offset: -2px;
}
.grid-tools {
    position: absolute; top: 9px; right: 9px; left: 10px; z-index: 2;
    display: flex; align-items: center; justify-content: space-between;
    pointer-events: none;
}
.grid-tools > * { pointer-events: auto; }
.grid-tools .drag-handle { background: var(--card-bg); border: 1px solid var(--border); }
/* El tirador: la esquina inferior derecha, en L. Se ve tenue hasta que se
   pasa por encima o se está estirando. */
.grid-resize {
    position: absolute; right: 5px; bottom: 5px; z-index: 2;
    width: 14px; height: 14px; cursor: nwse-resize; opacity: .5;
    border-right: 3px solid var(--primary); border-bottom: 3px solid var(--primary);
    border-radius: 0 0 5px 0;
}
.grid-resize:hover, .analytics-grid-item.is-resizing .grid-resize { opacity: 1; }
.analytics-grid-item.is-resizing > .block,
.analytics-grid-item.is-over > .block {
    outline: 2px solid var(--primary); outline-offset: -2px;
}
.analytics-grid-item.is-dragging { opacity: .4; }
.analytics-grid-empty { grid-column: 1 / -1; }
@media (max-width: 1100px) {
    /* El panel baja arriba de la rejilla y deja de ser pegajoso, como el
       riel del constructor: a menos de eso, 300px se comen el tablero. */
    .tablero-editando { grid-template-columns: minmax(0, 1fr); }
    .tablero-panel { position: static; }
}
@media (max-width: 900px) {
    /* Cada bloque ya ocupa el ancho entero: no hay nada que estirar. */
    .grid-resize { display: none; }
}

@media (max-width: 700px) {
    .ring { grid-template-columns: minmax(0, 1fr); }
}

/* ── SEO: la tarjeta de Buscadores y los patrones ─────────────────────────
   La PREVIA imita el resultado de Google: título, descripción y nada más.
   No copia sus colores exactos —sería una promesa que Google rompe cuando
   cambie su diseño— pero sí su jerarquía, que es lo que se está juzgando.

   `.is-derived` es lo que NO escribió una persona: sale del patrón de la
   tienda o del contenido. Va en gris para que se lea «esto ya está resuelto»
   y no «esto está vacío», que es la diferencia que la tarjeta existe para
   comunicar. */
.seo-preview {
    display: flex; flex-direction: column; gap: 4px;
    margin: 0 0 18px; padding: 14px 16px;
    background: var(--bg); border: 1px solid var(--border);
    border-radius: var(--radius-sm);
}
.seo-preview-title {
    font-size: var(--fs-body); font-weight: 600; color: var(--primary-dark);
    /* Se corta como lo corta un buscador, no se envuelve en tres renglones:
       ver el corte ES la información que la previa da. */
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.seo-preview-desc {
    font-size: var(--fs-small); line-height: 1.45; color: var(--text);
}
.seo-preview-title.is-derived, .seo-preview-desc.is-derived {
    color: var(--text-light); font-style: italic;
}
.seo-preview-note {
    margin-top: 6px; font-size: var(--fs-small); color: var(--text-light);
}

/* El renglón de fichas debajo de un patrón. Envuelve: son tres hoy y el día
   que sean seis no puede empujar el ancho del formulario. */
.field-tokens {
    display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 0;
}
.field-tokens .chip { cursor: pointer; border: 0; font: inherit; }

/* El encabezado del grupo de patrones, dentro del formulario de ajustes. */
.seo-patterns { margin-top: 8px; }
.seo-patterns h3 {
    margin: 0 0 4px; font-size: var(--fs-body); font-weight: 600;
}

/* La previa de la imagen para compartir, en Ajustes. */
.setting-preview {
    display: block; height: auto; margin: 0 0 10px;
    border: 1px solid var(--border); border-radius: var(--radius-sm);
}

/* ── Tareas: tablero, calendario y tarjetas ─────────────────────────── */
.tareas-head { display: flex; align-items: center; gap: var(--sp-3); }
.tareas-tablero { display: grid; gap: var(--sp-3); padding: var(--sp-3);
                  grid-template-columns: repeat(4, minmax(220px, 1fr)); }
@media (max-width: 900px) { .tareas-tablero { grid-template-columns: 1fr; } }
/* Las columnas: un punto de color por estado, el contador en pastilla, la
   columna con tareas tintada y la vacía con su recuadro punteado que dice
   qué llega ahí (Renzo, 2026-09-12, con el boceto enfrente). */
.tareas-columna { background: var(--bg); border-radius: var(--radius-card);
                  padding: var(--sp-3); min-height: 200px;
                  display: flex; flex-direction: column; }
.tareas-columna.has-tareas {
    background: color-mix(in srgb, var(--primary-soft) 55%, var(--bg));
}
.tareas-columna.is-over { outline: 2px dashed var(--primary); outline-offset: -2px; }
.tareas-columna-head { display: flex; align-items: center; gap: var(--sp-2);
                       font-size: var(--fs-body); font-weight: 600;
                       margin: 0 0 var(--sp-3); }
.tareas-columna-nombre { flex: 1; }
.tareas-columna-punto { width: 18px; height: 18px; border-radius: 999px;
                        border: 4px solid; flex: none; }
.tareas-columna-punto.is-open { border-color: var(--border-strong); background: var(--card-bg); }
.tareas-columna-punto.is-in_progress { border-color: var(--primary); background: var(--primary-soft); }
.tareas-columna-punto.is-review { border-color: var(--warn-vivid); background: var(--warn-bg); }
.tareas-columna-punto.is-done { border-color: var(--ok-vivid); background: var(--ok-bg); }
.tareas-columna-count { min-width: 24px; height: 24px; padding: 0 8px;
                        border-radius: 999px; display: inline-grid;
                        place-items: center; font-size: var(--fs-small);
                        font-weight: 600; background: var(--card-bg);
                        color: var(--text-light); }
.tareas-lista { list-style: none; margin: 0; padding: 0; display: flex;
                flex-direction: column; gap: var(--sp-2); flex: 1; }
.tareas-vacio { flex: 1; display: flex; flex-direction: column;
                align-items: center; justify-content: center; gap: 4px;
                padding: var(--sp-4) var(--sp-3); text-align: center;
                border: 1px dashed var(--border-strong);
                border-radius: var(--radius-field); color: var(--text-light); }
.tareas-vacio svg { width: 28px; height: 28px; margin-bottom: var(--sp-1);
                    color: var(--text-faint); }
.tareas-vacio strong { color: var(--text); font-weight: 600; }
.tareas-vacio small { font-size: var(--fs-small); }

/* El tablero de Alistamiento (rediseño del 2026-09-14, con el referente de
   Renzo). Clases propias y no las de Tareas: son dos tableros con reglas
   distintas y compartir clases los ataría. Cada columna tiene TONO —el de su
   baldosa— y el tono repite lo que dice el rótulo, nunca lo reemplaza. */
/* TODO EN UN RENGLÓN (Renzo, 2026-09-15): la sede, los tags y el botón
   caben en una fila, y cada renglón que se ahorra acá es alto que gana el
   tablero. Antes los tags bajaban a un segundo renglón y una nota de ayuda
   («Selecciona la sede para ver los pedidos…») ocupaba una columna para
   repetir lo que ya dicen el rótulo del campo y el botón. */
/* EL RÓTULO VA AL LADO, NO ENCIMA (Renzo, 2026-09-15: «centrar esto, se ve
   raro»). Con «Sede» sobre el select, esa celda medía rótulo + control y las
   otras solo control: centrando cada una, el select bajaba un renglón
   respecto al campo de tags y al botón, y alineándolas por abajo el bloque
   quedaba pegado al filo de la tarjeta. En línea, las cuatro celdas miden lo
   mismo, comparten su eje y el conjunto queda centrado de verdad. */
.al-sede { display: grid; align-items: center; gap: var(--sp-3) var(--sp-4);
           grid-template-columns: auto minmax(0, 1fr) minmax(0, 1fr) auto;
           padding: var(--sp-3) var(--sp-5); }
.al-sede-campo { display: flex; align-items: center; gap: var(--sp-3);
                 margin: 0; min-width: 0; }
.al-sede-campo .field-label { margin: 0; flex: none; }
/* LOS TRES CONTROLES MIDEN LO MISMO (Renzo, 2026-09-15). Sueltos venían de
   tres sitios distintos de la hoja —el select 36 px, la caja de fichas 45 y
   el botón 39— y una fila de tres alturas se lee como tres filas. La medida
   la fija la tarjeta, no la casa: unificarla en el select, `.tokens` y
   `.btn` de todo el ERP es un cambio transversal que no pidió nadie. */
.al-sede { --al-control: 40px; }
.al-sede-campo select,
.al-sede .tokens,
.al-sede .btn { min-height: var(--al-control); }
/* `.tokens` trae los 44 px de un campo de formulario y su relleno los sube a
   45: acá manda la fila, así que se le baja el relleno vertical para que los
   40 los cumpla de verdad. El de adentro sigue creciendo con las fichas. */
.al-sede .tokens { padding-top: 3px; padding-bottom: 3px; }
.al-sede-campo select { min-width: 0; }
/* La caja de fichas trae el hueco del campo de un formulario; acá es una
   celda de una fila y ese hueco la descuadra. */
.al-sede .token-field { margin-bottom: 0; }
.al-sede-tags { min-width: 0; }
.al-sede-tags .token-field { margin-bottom: 0; }
.al-tip svg { width: 18px; height: 18px; flex: none; color: var(--primary); }
/* Angosto: la baldosa y la sede arriba, los tags y el botón debajo. */
@media (max-width: 900px) {
    .al-sede { grid-template-columns: auto minmax(0, 1fr); }
    .al-sede-tags { grid-column: 1 / -1; }
}

.al-tablero-wrap { display: flex; flex-direction: column; gap: var(--sp-3);
                   margin-top: var(--sp-3); }
/* La barra de selección está SIEMPRE a la vista (Renzo, 2026-09-14):
   aparecer al marcar empujaba el tablero. Se queda pegada arriba para que
   quien baja por una columna larga no pierda el «Mover a…»; lo que cambia
   es si hay algo marcado. Tinte de la marca, como el referente. */
.al-seleccion { position: sticky; top: var(--sp-3); z-index: 5; display: flex;
                flex-wrap: wrap; align-items: center; gap: var(--sp-3);
                min-height: 64px; padding: var(--sp-3) var(--sp-4);
                background: color-mix(in srgb, var(--primary-soft) 60%, var(--card-bg));
                border: 1px solid var(--primary-soft);
                border-radius: var(--radius-card); box-shadow: var(--shadow); }
.al-seleccion-marca { width: 34px; height: 34px; flex: none; display: inline-grid;
                      place-items: center; border-radius: var(--radius-field);
                      background: var(--primary); color: #fff; }
.al-seleccion-marca svg { width: 18px; height: 18px; }
.al-seleccion.is-vacia .al-seleccion-marca { background: var(--card-bg);
                                             color: var(--text-faint);
                                             border: 1px solid var(--border); }
.al-seleccion-cuenta { flex: 1; min-width: 0; margin: 0;
                       font-size: var(--fs-section); font-weight: 600;
                       color: var(--primary-dark); }
.al-seleccion.is-vacia .al-seleccion-cuenta { font-weight: 400;
                                              font-size: var(--fs-body);
                                              color: var(--text-light); }
.al-seleccion-acciones { display: flex; align-items: center; gap: var(--sp-3); }
.al-seleccion-sep { width: 1px; height: 28px; background: var(--border-strong); }
/* El disparador del menú con rótulo: mide y se lee como un botón de la
   casa, porque la casa solo trae el de tres puntos. */
.al-mover-trigger { list-style: none; cursor: pointer; display: inline-flex;
                    align-items: center; gap: var(--sp-2); min-width: 190px;
                    padding: 8px 12px; background: var(--card-bg);
                    border: 1px solid var(--border-strong);
                    border-radius: var(--radius-field);
                    font-size: var(--fs-body); font-weight: 600;
                    color: var(--primary-dark); }
.al-mover-trigger::-webkit-details-marker { display: none; }
.al-mover-trigger span { flex: 1; }
.al-mover-trigger svg { width: 16px; height: 16px; flex: none; }
.al-mover-trigger:hover { border-color: var(--primary); }
.al-mover-trigger:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.al-mover[open] .al-mover-chevron { transform: rotate(180deg); }
/* Sin nada marcado el menú no abre (lo impide el script): se tiene que VER
   apagado. La casa no trae aún un apagado general (rama `boton-apagado`
   sin mezclar), así que la barra trae el suyo. */
.al-seleccion.is-vacia .al-mover-trigger { opacity: .5; cursor: not-allowed; }
.al-seleccion .menu-item:disabled { opacity: .45; cursor: not-allowed;
                                    background: transparent; color: var(--text-light); }
.al-seleccion .btn:disabled { opacity: .45; cursor: not-allowed;
                              box-shadow: none; }

/* LAS CUATRO COLUMNAS MIDEN LO MISMO Y SIEMPRE LO MISMO (Renzo,
   2026-09-15): la que se llena de pedidos estiraba la página entera y
   buscar en Empacando obligaba a bajar por Por alistar. El alto sale de
   la ventana —lo que queda bajo la banda, la sede y la barra— acotado
   arriba y abajo para que ni en una pantalla corta quede un buzón ni en
   un monitor grande una columna interminable. Lo que se desplaza es la
   lista de tarjetas; cabecera, «Seleccionar todos» y pie se quedan. */
.al-tablero { display: grid; gap: var(--sp-3);
              --al-alto-columna: clamp(340px, calc(100dvh - 30rem), 720px);
              grid-template-columns: repeat(4, minmax(230px, 1fr)); }
@media (max-width: 1100px) {
    .al-tablero { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 700px) { .al-tablero { grid-template-columns: 1fr; } }
.al-columna { display: flex; flex-direction: column; gap: var(--sp-3);
              height: var(--al-alto-columna); min-height: 0;
              padding: var(--sp-4);
              background: var(--card-bg); border: 1px solid var(--border);
              border-radius: var(--radius-card); box-shadow: var(--shadow); }
.al-columna.is-over { outline: 2px dashed var(--primary); outline-offset: -2px; }
.al-columna-head { display: flex; align-items: flex-start; gap: var(--sp-3); }
.al-columna-texto { flex: 1; min-width: 0; }
.al-columna-nombre { margin: 0; font-size: var(--fs-section); font-weight: 700;
                     color: var(--text); }
.al-columna-sub { margin: 2px 0 0; font-size: var(--fs-small);
                  color: var(--text-light); }
.al-columna-count { min-width: 26px; height: 26px; padding: 0 8px;
                    border-radius: 999px; display: inline-grid;
                    place-items: center; font-size: var(--fs-small);
                    font-weight: 700; }
.al-columna.is-brand .al-columna-count { background: var(--primary-soft);
                                        color: var(--primary-dark); }
.al-columna.is-info .al-columna-count { background: var(--info-bg);
                                       color: var(--info-text); }
.al-columna.is-warn .al-columna-count { background: var(--warn-bg);
                                       color: var(--warn-text); }
.al-columna.is-ok .al-columna-count { background: var(--ok-bg);
                                     color: var(--ok-text); }
.al-columna-controles { display: flex; flex-wrap: wrap; align-items: center;
                        justify-content: space-between; gap: var(--sp-2); }
.al-todos { display: inline-flex; align-items: center; gap: var(--sp-2);
            font-size: var(--fs-small); color: var(--text-light);
            white-space: nowrap; cursor: pointer; }
/* El ⋮ de cada columna: sus opciones según su naturaleza (ordenar y tomar,
   seguir con el más antiguo, ver lo despachado). Pegado a la esquina, junto
   al contador; la opción vigente se marca con su glifo y su peso. */
.al-columna-menu { margin: -3px -6px 0 -4px; }

/* `min-height: 0` es lo que deja encogerse a un hijo flexible: sin eso la
   lista mide lo que miden sus tarjetas y desborda la columna en vez de
   desplazarse. La sangría de la derecha es para que la barra no se pare
   encima del borde de la tarjeta ni lo mueva al aparecer. */
.al-lista { list-style: none; margin: 0 calc(var(--sp-2) * -1) 0 0; padding: 0;
            padding-right: var(--sp-2); display: flex;
            flex-direction: column; gap: var(--sp-2); flex: 1;
            min-height: 0; overflow-y: auto; overscroll-behavior: contain;
            scrollbar-width: thin; scrollbar-gutter: stable; }
.al-tarjeta { position: relative; display: flex; flex-wrap: wrap;
              align-items: flex-start; gap: var(--sp-2); padding: var(--sp-3);
              background: var(--card-bg); border: 1px solid var(--border);
              border-radius: var(--radius-field);
              transition: border-color .15s, box-shadow .15s; }
.al-tarjeta:hover { border-color: var(--border-strong); box-shadow: var(--shadow); }
.al-tarjeta.is-marcada { border-color: var(--primary);
                         box-shadow: 0 0 0 1px var(--primary); }
.al-tarjeta.is-dragging { opacity: .45; }
/* El candado (2026-09-14): lo que tomó otra persona se ve, pero no se
   agarra. Borde punteado y el candado donde iría el agarre. */
.al-tarjeta.is-bloqueada { border-style: dashed; }
.al-tarjeta-candado { display: flex; align-items: flex-start; padding-top: 3px;
                      color: var(--text-light); }
.al-tarjeta-candado svg { width: 16px; height: 16px; }
/* La que llega al tablero aparece; las que cambian de columna se deslizan
   con `transform` desde alistamiento-tablero.js. */
@keyframes al-llega { from { opacity: 0; transform: scale(.96); }
                      to { opacity: 1; transform: none; } }
.al-tarjeta.is-llegando { animation: al-llega .3s ease; }
@media (prefers-reduced-motion: reduce) {
    .al-tarjeta.is-llegando { animation: none; }
}
.al-tarjeta-asas { display: flex; flex-direction: column; align-items: center;
                   gap: var(--sp-2); }
.al-tarjeta-check { margin: 0; }
.al-tarjeta-cuerpo { flex: 1; min-width: 0; display: flex;
                     flex-direction: column; gap: 3px; color: var(--text);
                     text-decoration: none; }
.al-tarjeta-folio { font-size: var(--fs-section); font-weight: 700; }
.al-tarjeta-cliente { font-size: var(--fs-body); color: var(--text); }
.al-tarjeta-dato { display: inline-flex; align-items: center; gap: 6px;
                   font-size: var(--fs-small); color: var(--text-light); }
.al-tarjeta-dato svg { width: 14px; height: 14px; flex: none; }
.al-tarjeta-pills { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 2px; }
/* El glifo es un chevrón hacia abajo; girado, apunta a la ficha. */
.al-tarjeta-ir { width: 16px; height: 16px; flex: none; margin-top: 4px;
                 color: var(--text-faint); transform: rotate(-90deg); }
.al-tarjeta-nota { flex-basis: 100%; }

.al-vacio { flex: 1; min-height: 0; display: flex; flex-direction: column;
            align-items: center; justify-content: center; gap: 4px;
            padding: var(--sp-4) var(--sp-3); text-align: center;
            border: 1px dashed var(--border-strong);
            border-radius: var(--radius-field); color: var(--text-light); }
.al-vacio svg { width: 40px; height: 40px; margin-bottom: var(--sp-2);
                color: var(--text-faint); }
.al-vacio strong { color: var(--text); font-weight: 600; }
.al-vacio small { font-size: var(--fs-small); }


.al-tip { display: flex; align-items: center; gap: var(--sp-3);
          margin: var(--sp-3) 0 0; padding: var(--sp-3) var(--sp-4);
          font-size: var(--fs-small); color: var(--text-light); }
.tareas-tarjeta { position: relative; background: var(--card-bg);
                  border: 1px solid var(--border); border-radius: var(--radius-field);
                  padding: var(--sp-3); cursor: grab; box-shadow: var(--shadow); }
.tareas-tarjeta:hover { border-color: var(--border-strong); }
.tareas-tarjeta.is-dragging { opacity: .5; }
.tareas-tarjeta.is-vencida { border-color: var(--danger-text); }
.tareas-tarjeta-title { font-weight: 600; text-decoration: none;
                        display: block; padding-right: 28px; }
.tareas-tarjeta-meta { font-size: var(--fs-small); color: var(--text-light);
                       margin-top: 2px; }
.tareas-tarjeta-pills { display: flex; gap: var(--sp-1); flex-wrap: wrap;
                        margin-top: var(--sp-2); }
.tareas-menu { position: absolute; top: var(--sp-2); right: var(--sp-2); }
.tareas-cal-nav { display: flex; gap: var(--sp-2); padding: var(--sp-3)
                  var(--sp-3) 0; }
.tareas-calendario { display: grid; grid-template-columns: repeat(7, 1fr);
                     gap: 1px; background: var(--border); margin: var(--sp-3);
                     border: 1px solid var(--border); }
.tareas-cal-head { background: var(--bg); padding: var(--sp-1);
                   font-size: var(--fs-small); text-align: center;
                   color: var(--text-light); }
.tareas-dia { background: var(--card-bg); min-height: 96px; padding: var(--sp-1);
              display: flex; flex-direction: column; gap: 2px; }
.tareas-dia.is-otro-mes { background: var(--bg); color: var(--text-light); }
.tareas-dia.is-hoy .tareas-dia-num { background: var(--primary); color: #fff;
                                     border-radius: 999px; }
.tareas-dia-num { font-size: var(--fs-small); width: 22px; height: 22px;
                  display: inline-flex; align-items: center;
                  justify-content: center; }
.tareas-item { font-size: var(--fs-small); background: var(--primary-soft);
               color: var(--primary-dark); border-radius: 4px; padding: 1px 4px;
               text-decoration: none; overflow: hidden; white-space: nowrap;
               text-overflow: ellipsis; }
.tareas-item.is-alta { background: var(--warn-bg); color: var(--warn-text); }
.tareas-proyeccion { font-size: var(--fs-small); border: 1px dashed var(--border);
                     background: transparent; color: var(--text-light);
                     border-radius: 4px; padding: 1px 4px; width: 100%;
                     text-align: left; cursor: pointer; }
@media (max-width: 768px) {
  .tareas-calendario { grid-template-columns: 1fr; }
  .tareas-cal-head { display: none; }
  .tareas-dia.is-otro-mes { display: none; }
  .tareas-dia { min-height: 0; flex-direction: row; flex-wrap: wrap;
                align-items: center; gap: var(--sp-1); }
}
/* El conmutador de Tareas es de PALABRAS, no de glifos: tres rótulos no
   caben en los 32px del botón de íconos y se encimaban. */
.view-switch--texto .view-switch__btn { width: auto; padding: 0 var(--sp-3);
                                        font-size: var(--fs-small);
                                        font-weight: 500; text-decoration: none;
                                        display: inline-flex; align-items: center;
                                        gap: 6px; }
.view-switch--texto .view-switch__btn svg { width: 16px; height: 16px; }
/* La ficha en el drawer (Renzo, 2026-09-12, con el boceto enfrente):
   cabecera con baldosa y menú, datos con glifo, botones de estado a lo
   ancho, títulos de sección con filo, campos con glifo adentro, opciones
   como tarjetas explicadas, alertas que se rellenan al marcarse, y el pie
   con Cancelar / Guardar. El historial cierra. */
/* El `h2` del drawer mide lo que mide su texto; con la cabecera adentro
   tiene que ocupar todo el ancho para que el menú «…» caiga junto a la ×. */
.drawer-head h2:has(> .tareas-cabecera) { flex: 1; min-width: 0; }
.tareas-cabecera { display: flex; align-items: flex-start; gap: var(--sp-3);
                   min-width: 0; }
.tareas-cabecera-texto { display: flex; flex-direction: column; gap: 6px;
                         min-width: 0; }
.tareas-titulo { font-size: var(--fs-section); font-weight: 600;
                 line-height: 1.25; overflow-wrap: anywhere; }
.tareas-estado { display: flex; gap: var(--sp-1); flex-wrap: wrap; }
.tareas-menu-ficha { margin-left: auto; align-self: center; }
.tareas-glifo { width: 16px; height: 16px; flex: none; }
.tareas-resumen { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4);
                  font-size: var(--fs-body); color: var(--text-light);
                  margin: 0 0 var(--sp-4); }
.tareas-resumen > * { display: inline-flex; align-items: center; gap: 6px; }
.tareas-resumen a { color: var(--primary); text-decoration: none; }
.tareas-resumen .is-vencida { color: var(--danger-text); font-weight: 600; }
.tareas-acciones { display: grid; gap: var(--sp-2);
                   grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
                   margin-bottom: var(--sp-5); }
.tareas-acciones form { display: contents; }
.tareas-acciones .btn { width: 100%; justify-content: center; }
.tareas-seccion { display: flex; align-items: center; gap: var(--sp-3);
                  font-size: var(--fs-body); font-weight: 600;
                  color: var(--text); margin: var(--sp-5) 0 var(--sp-3); }
.tareas-seccion::after { content: ""; flex: 1; height: 1px; background: var(--border); }
.form-grid--drawer { grid-template-columns: 1fr 1fr; gap: 0 var(--sp-3); }
.form-grid--drawer > .field-wide { grid-column: 1 / -1; }
.form-grid--drawer .field { margin-bottom: var(--sp-3); }
.form-grid--drawer label.is-required::after { content: " *"; color: var(--danger-text); }
/* Y en cualquier rejilla, no solo en la del drawer: `required_css_class =
   'is-required'` lo declaran ya varios formularios (Tareas, Solicitudes) y
   fuera del drawer el asterisco no salía. */
.form-grid label.is-required::after { content: " *"; color: var(--danger-text); }
/* El glifo va ADENTRO de la casilla: el envoltorio se posiciona y la
   casilla —o la que flatpickr pone en su lugar— le deja el hueco. */
.tareas-con-glifo { position: relative; }
/* `bottom` cuenta el margen inferior del campo (el envoltorio es ítem de
   grilla y lo contiene), y `z-index` porque flatpickr envuelve su casilla
   en un `position: relative` que de otro modo la pinta encima del glifo. */
.tareas-con-glifo > svg { position: absolute; left: 13px;
                          bottom: calc(var(--sp-3) + 13px); z-index: 1;
                          width: 16px; height: 16px; color: var(--text-light);
                          pointer-events: none; }
/* Altura FIJA del control para que el glifo quede centrado de verdad: con
   la altura natural (padding + línea) el centro se corría dos píxeles. */
.tareas-con-glifo input, .tareas-con-glifo select { padding-left: 38px;
                                                     height: 42px; }
.tareas-descripcion { position: relative; }
.tareas-contador { display: block; text-align: right; margin-top: -10px;
                   font-size: var(--fs-meta); color: var(--text-light); }
.tareas-opciones { display: grid; gap: var(--sp-2);
                   grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
.tareas-opcion { display: flex; gap: var(--sp-2); align-items: flex-start;
                 padding: var(--sp-3); border: 1px solid var(--border);
                 border-radius: var(--radius-field); cursor: pointer;
                 background: var(--card-bg); }
.tareas-opcion:hover { border-color: var(--border-strong); }
.tareas-opcion:has(input:checked) { border-color: var(--primary);
                                     background: var(--primary-soft); }
.tareas-opcion input { margin-top: 3px; flex: none; }
.tareas-opcion strong { display: block; font-weight: 600; }
.tareas-opcion small { display: block; color: var(--text-light);
                       font-size: var(--fs-small); margin-top: 2px; }
.check-list--fila { flex-direction: row; flex-wrap: wrap; gap: var(--sp-2);
                    margin-bottom: 0; }
.check-list--fila .field-check:has(input:checked) {
    background: var(--primary-soft); border-color: var(--primary);
    color: var(--primary-dark); font-weight: 600;
}
.tareas-sono { display: block; font-size: var(--fs-meta); color: var(--text-light);
               font-weight: 400; }
.tareas-guardar { display: flex; justify-content: flex-end; gap: var(--sp-2);
                  margin-top: var(--sp-5); padding-top: var(--sp-4);
                  border-top: 1px solid var(--border); }
.tareas-tarjeta-title { cursor: pointer; }

/* ── Tareas: los tableros ─────────────────────────────────────────────── */
.tableros-grid { display: grid; gap: var(--sp-3);
                 grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); }
.tablero-card { display: flex; gap: var(--sp-3); padding: var(--sp-4);
                background: var(--card-bg); border: 1px solid var(--border);
                border-radius: var(--radius-card); text-decoration: none;
                color: var(--text); box-shadow: var(--shadow);
                transition: border-color .14s, transform .14s; }
.tablero-card:hover { border-color: var(--primary); transform: translateY(-1px); }
.tablero-card--nuevo { border-style: dashed; box-shadow: none;
                       background: transparent; color: var(--text-light); }
.tablero-card.is-archivado { opacity: .7; }
.tablero-card-color { width: 6px; border-radius: 999px; flex: none;
                      background: var(--primary); }
.tablero-card-color.is-morado { background: var(--primary); }
.tablero-card-color.is-azul { background: var(--info-vivid); }
.tablero-card-color.is-verde { background: var(--ok-vivid); }
.tablero-card-color.is-ambar { background: var(--warn-vivid); }
.tablero-card-color.is-rojo { background: var(--danger-vivid); }
.tablero-card-color.is-gris { background: var(--border-strong); }
.tablero-card-body { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.tablero-card-name { font-weight: 600; font-size: var(--fs-body); }
.tablero-card-desc { color: var(--text-light); font-size: var(--fs-small); }
.tablero-card-stats { display: flex; gap: var(--sp-3); margin-top: var(--sp-2);
                      font-size: var(--fs-small); color: var(--text-light); }
.tablero-card-stats .is-vencida { color: var(--danger-text); font-weight: 600; }
.tableros-archivados { margin-top: var(--sp-5); }
.tableros-archivados > summary { cursor: pointer; color: var(--text-light);
                                 font-size: var(--fs-small); font-weight: 600;
                                 margin-bottom: var(--sp-3); }
/* El color se elige con bolitas, no con un desplegable de nombres. */
.tablero-colores { display: flex; gap: var(--sp-2); }
/* `.field label` es más específica y pone `display: block`; por eso el
   selector va con `.field` adelante, o la bolita mide cero. */
.field .tablero-color-opcion { position: relative; width: 32px; height: 32px;
                               border-radius: 999px; cursor: pointer;
                               border: 2px solid transparent; display: grid;
                               place-items: center; margin: 0; }
.tablero-color-opcion input { position: absolute; opacity: 0; inset: 0;
                              cursor: pointer; }
.tablero-color-opcion span { display: block; width: 22px; height: 22px;
                             border-radius: 999px; background: var(--primary); }
.tablero-color-opcion.is-azul span { background: var(--info-vivid); }
.tablero-color-opcion.is-verde span { background: var(--ok-vivid); }
.tablero-color-opcion.is-ambar span { background: var(--warn-vivid); }
.tablero-color-opcion.is-rojo span { background: var(--danger-vivid); }
.tablero-color-opcion.is-gris span { background: var(--border-strong); }
.tablero-color-opcion:has(input:checked) { border-color: var(--text); }
.tablero-archivar { display: flex; align-items: center; gap: var(--sp-3);
                    margin-top: var(--sp-4); padding-top: var(--sp-4);
                    border-top: 1px solid var(--border); }
/* «+ Nueva tarea» al pie de cada columna, y el bloque plegable de
   recurrencia del drawer de creación. */
.tareas-agregar { display: block; margin-top: var(--sp-2); padding: var(--sp-2);
                  border-radius: var(--radius-field); text-align: center;
                  font-size: var(--fs-small); font-weight: 600;
                  color: var(--text-light); text-decoration: none; }
.tareas-agregar:hover { background: var(--card-bg); color: var(--primary); }
.tareas-repite > summary { list-style: none; cursor: pointer; }
.tareas-repite > summary::-webkit-details-marker { display: none; }
.tareas-repite > summary::before { content: "▸"; margin-right: var(--sp-2);
                                   color: var(--text-light); }
.tareas-repite[open] > summary::before { content: "▾"; }

/* ── Segmentos ─────────────────────────────────────────────────────────────
   La ficha usa las piezas del editor de condiciones de las colecciones
   (`match-row`, `boxed-list`, `rule-*`, `rule-builder`). Lo que sigue es solo
   lo que el segmento tiene y la colección no. */

/* Los ajustes opcionales de una condición («En los últimos N días», «Canal»)
   en su PROPIA línea del panel: puestos al lado del valor partían la fila en
   un lugar distinto según el ancho, y se leían como otro valor más. */
.rule-builder-extra {
    flex: 1 1 100%; display: flex; flex-wrap: wrap; align-items: center;
    gap: var(--sp-3); margin: 0;
}
.rule-param { display: inline-flex; align-items: center; gap: var(--sp-2); }
/* Las etiquetas del borrador son el campo de fichas del marco, y dentro de
   una fila no llevan su margen de abajo: lo pone el panel. */
.rule-builder .token-field { flex: 1 1 240px; min-width: 0; margin-bottom: 0; }

/* Cuántos entran, en la tarjeta de la vista previa: la cifra es lo que se
   mira, la palabra la acompaña. */
.segment-count {
    display: flex; align-items: baseline; gap: var(--sp-2);
    margin: 0 0 var(--sp-3); color: var(--text-light);
}
.segment-count strong {
    font-size: 2rem; line-height: 1; color: var(--text);
    font-variant-numeric: tabular-nums;
}

/* ── Bins: las columnas del Finder ────────────────────────────────────── */
/* Sede › zona › pasillo › estante › nivel › bin como columnas, una al lado
   de la otra: el clic en un elemento abre la siguiente (Renzo, 2026-09-12,
   después de probar las carpetas de a una pantalla). El contenedor se
   desplaza en horizontal cuando la ruta es más ancha que la tarjeta. */
.cols-browser { display: flex; overflow-x: auto; align-items: stretch;
                margin: 0 calc(-1 * var(--sp-4)); }
.cols-col { flex: 0 0 260px; min-width: 260px; display: flex;
            flex-direction: column; gap: 2px; padding: var(--sp-2);
            border-right: 1px solid var(--border); }
.cols-col:last-child { border-right: 0; }
.cols-head { margin: 0; padding: 6px 10px 8px; font-size: var(--fs-label);
             font-weight: 600; color: var(--text-light);
             text-transform: uppercase; letter-spacing: .04em; }
.cols-item { display: flex; align-items: center; justify-content: space-between;
             gap: 10px; padding: 8px 10px; border-radius: 10px;
             color: inherit; text-decoration: none; }
.cols-item:hover { background-color: var(--primary-soft); }
.cols-item-selected { background-color: var(--primary-soft); color: var(--primary-dark);
                      font-weight: 600; }
.cols-item-off { opacity: .55; }
.cols-item-text { display: flex; flex-direction: column; min-width: 0; }
.cols-item-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cols-item-code { font-size: var(--fs-label); color: var(--text-light); }
/* El conteo: gris cuando es cero, de marca cuando hay algo adentro. */
.cols-count { flex-shrink: 0; min-width: 28px; text-align: center; padding: 2px 8px;
              border-radius: 999px; background-color: var(--border);
              font-size: var(--fs-label); font-weight: 600; font-variant-numeric: tabular-nums; }
.cols-count-on { background-color: var(--primary); color: #fff; }
.cols-empty { margin: 0; padding: 10px; color: var(--text-light); font-size: var(--fs-label); }
.cols-create { padding: 6px 4px 2px; margin-top: auto; }
/* El buscador de artículos (`.sku-field`) dentro de una fila con su botón:
   es él, y no su `<input>`, quien reparte el ancho con el botón. */
.inline-row .sku-field { flex: 1 1 220px; min-width: 0; }

/* ── Bins: navegación y referencia espacial compactas ────────────────── */
/* La ficha de un nivel o de un bin (maqueta de Renzo, 2026-09-13): tres
   columnas debajo de la banda —galería, cifras y datos, pistas— que en
   angosto se apilan. Las fotos son del estante tomadas con el celular,
   apaisadas o verticales, y lo que importa es reconocer el lugar, no verlas
   enteras: `object-fit: cover` en la grande y en las miniaturas. */
.place-card { display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(0, 1.5fr) minmax(0, 1fr); gap: var(--sp-4); padding-top: var(--sp-4); align-items: start; }
/* Sin pistas —la ficha de una zona, un estante o un nivel— no hay tercera
   columna: las cifras se quedan con el resto del ancho. */
.place-card:not(:has(.place-hints)) { grid-template-columns: minmax(0, 1fr) minmax(0, 1.6fr); }
.place-slider { min-width: 0; }
.place-slider .splide__track { border-radius: var(--radius-field); }
.place-photo { display: block; aspect-ratio: 4 / 3; border: 1px solid var(--border); border-radius: var(--radius-field); background-color: var(--bg); overflow: hidden; line-height: 0; }
.place-photo img { display: block; width: 100%; height: 100%; object-fit: cover; }
.place-photo-none { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--sp-2); color: var(--text-faint); font-size: var(--fs-small); line-height: 1.3; }
.place-photo-none svg { width: 28px; height: 28px; }
/* Sin script, la lista del slider se apila con aire entre fotos. */
.place-slider:not(.is-initialized) .splide__list { display: flex; flex-direction: column; gap: var(--sp-2); }
/* Las flechas y los puntos de Splide con la piel del ERP: el `core` trae
   solo la mecánica, sin colores, y así no hay dos estilos peleando. */
.place-slider .splide__arrow { position: absolute; top: 50%; transform: translateY(-50%); z-index: 1; width: 30px; height: 30px; border: 0; border-radius: 999px; background-color: rgba(255, 255, 255, .92); color: var(--text); box-shadow: 0 1px 4px rgba(0, 0, 0, .18); cursor: pointer; display: grid; place-items: center; opacity: .9; }
.place-slider .splide__arrow:hover { opacity: 1; color: var(--primary-dark); }
.place-slider .splide__arrow:disabled { opacity: .3; cursor: default; }
.place-slider .splide__arrow svg { width: 14px; height: 14px; fill: currentColor; }
.place-slider .splide__arrow--prev { left: 8px; }
.place-slider .splide__arrow--prev svg { transform: scaleX(-1); }
.place-slider .splide__arrow--next { right: 8px; }
.place-slider .splide__pagination { display: flex; justify-content: center; gap: 6px; margin: var(--sp-2) 0 0; padding: 0; list-style: none; }
.place-slider .splide__pagination__page { width: 8px; height: 8px; padding: 0; border: 0; border-radius: 999px; background-color: var(--border-strong); cursor: pointer; }
.place-slider .splide__pagination__page.is-active { background-color: var(--primary); }
.place-facts { display: flex; flex-direction: column; gap: var(--sp-3); min-width: 0; }
.place-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: var(--sp-3); }
.place-tile, .place-fact { display: flex; gap: var(--sp-3); padding: var(--sp-3); border-radius: var(--radius-card); background-color: var(--bg); min-width: 0; }
.place-fact { align-items: center; }
.place-tile { flex-direction: column; align-items: flex-start; }
.place-tile-text, .place-fact-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
.place-tile-label, .place-fact-label { font-size: var(--fs-label); color: var(--text-light); }
.place-tile-value { font-size: var(--fs-stat); font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1.1; }
.place-fact-value { font-size: var(--fs-body); overflow-wrap: anywhere; }
.place-text { margin: 0; color: var(--text); line-height: 1.5; }
.place-hints { display: flex; flex-direction: column; gap: var(--sp-3); min-width: 0; }
.place-hint { display: flex; align-items: flex-start; gap: var(--sp-3); padding: var(--sp-3); border-radius: var(--radius-card); background-color: var(--primary-soft); }
.place-hint-text { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.place-hint-title { font-weight: 600; color: var(--primary-dark); }
.place-hint-body { font-size: var(--fs-small); color: var(--text-light); line-height: 1.5; }
@media (max-width: 960px) { .place-card { grid-template-columns: 1fr; } }

/* Las fotos que ya hay, en el drawer de editar: cada una con su ×, que es
   la casilla «remove_photos» vestida. Marcada, la foto se atenúa y lo dice. */
.place-edit-photos { display: flex; flex-wrap: wrap; gap: var(--sp-3); margin-bottom: var(--sp-3); }
.place-edit-photo { position: relative; display: flex; flex-direction: column; gap: 4px; width: 96px; cursor: pointer; }
.place-edit-photo img { width: 96px; height: 72px; object-fit: cover; border: 1px solid var(--border); border-radius: var(--radius-sm); display: block; transition: opacity .16s; }
.place-edit-x, .upload-file-x { position: absolute; top: -6px; right: -6px; width: 22px; height: 22px; border: 0; border-radius: 999px; background-color: var(--text); color: #fff; font-size: var(--fs-glyph); line-height: 22px; text-align: center; cursor: pointer; padding: 0; box-shadow: 0 1px 3px rgba(0, 0, 0, .25); }
.place-edit-check { position: absolute; inset: 0; opacity: 0; cursor: pointer; margin: 0; }
.place-edit-check:focus-visible ~ .place-edit-x { outline: 2px solid var(--primary); outline-offset: 1px; }
.place-edit-state { font-size: var(--fs-label); color: var(--danger-text); visibility: hidden; }
.place-edit-photo:has(.place-edit-check:checked) img { opacity: .35; }
.place-edit-photo:has(.place-edit-check:checked) .place-edit-x { background-color: var(--danger-text); }
.place-edit-photo:has(.place-edit-check:checked) .place-edit-state { visibility: visible; }
.field-note { font-weight: 400; color: var(--text-faint); }

/* Lo elegido en `ui/_upload.html` con `multiple`: una miniatura por archivo
   con su ×, antes de guardar. Un archivo que no es imagen muestra su nombre. */
.upload-files { display: flex; flex-wrap: wrap; gap: var(--sp-3); }
.upload-file { position: relative; width: 72px; height: 72px; border: 1px solid var(--border); border-radius: 10px; background-color: var(--bg); overflow: visible; }
.upload-file img { width: 100%; height: 100%; object-fit: cover; border-radius: 10px; display: block; }
.upload-file-name { display: block; padding: 6px; font-size: var(--fs-label); color: var(--text-light); overflow: hidden; text-overflow: ellipsis; word-break: break-all; max-height: 100%; }

.bins-top-row { --bins-card-min-height: 242px; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; align-items: stretch; }
.bins-top-row > .card { min-height: var(--bins-card-min-height); min-width: 0; }
.bins-search-card .inline-row { flex-wrap: wrap; }
.bins-search-card .inline-row input { min-width: 0; flex: 1 1 140px; }
.bins-search-card .bins-scan-row { margin-top: 18px; line-height: 1.8; }
.bin-contents-card { scroll-margin-top: calc(var(--topbar, 64px) + 20px); }
.bin-contents-card > .form-card { padding-top: 18px; }
.bin-contents-card > .form-card > .form-lead { margin-top: 16px; line-height: 1.8; }
.rack-card { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); padding: 0; overflow: hidden; }
.rack-stage { position: relative; min-width: 0; min-height: calc(var(--bins-card-min-height, 242px) - 2px); background: var(--bg); grid-column: 1; grid-row: 1 / 3; }
.rack-canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; touch-action: none; cursor: grab; }
.rack-canvas:active { cursor: grabbing; }
.rack-toolbar { position: absolute; top: 10px; right: 10px; }
.rack-gestures { position: absolute; bottom: 10px; inset-inline: 10px; display: flex; flex-wrap: wrap; justify-content: center; gap: 3px 10px; pointer-events: none; font-size: var(--fs-meta); color: var(--text-light); }
.rack-error { position: absolute; inset: 30px; display: grid; place-items: center; text-align: center; color: var(--text-light); font-size: var(--fs-label); }
.rack-error[hidden] { display: none; }
.rack-hover { position: absolute; top: 42px; left: 10px; max-width: 80%; padding: 5px 9px; border-radius: 6px; background: var(--surface, white); color: var(--text); font-size: var(--fs-label); pointer-events: none; }
.rack-header { grid-column: 2; grid-row: 1; align-self: end; padding: 20px 18px 10px; }
.rack-title { margin: 0 0 8px; font-size: var(--fs-page); line-height: 1.25; letter-spacing: -.4px; font-weight: 650; }
.rack-trail { margin: 0; font-size: var(--fs-label); color: var(--text-light); line-height: 1.7; }
.rack-current { display: inline-flex; align-items: center; gap: 7px; color: var(--primary); font-size: var(--fs-label); margin-top: 12px; }
.rack-dot { width: 7px; height: 7px; border-radius: 2px; background: var(--primary); display: inline-block; flex-shrink: 0; }
.rack-caption { grid-column: 2; grid-row: 2; align-self: start; margin: 0; padding: 0 18px 20px; font-size: var(--fs-label); color: var(--text-light); line-height: 1.6; overflow-wrap: anywhere; }
.rack-guide { grid-column: 1 / -1; align-self: center; padding: 22px 24px; }
.rack-guide-icon { width: 34px; height: 34px; color: var(--primary); float: left; margin: 0 14px 8px 0; }
.rack-guide-context { margin: 0 0 7px; color: var(--text-light); font-size: var(--fs-label); }
.rack-hint { color: var(--text-light); font-size: var(--fs-label); line-height: 1.7; margin: 10px 0 0; }
.rack-card-guide { background: var(--bg); }
.content:has([data-bins-workspace]) .page-banner { padding-block: 18px; }
@media (max-width: 1000px) {
  .bins-top-row { grid-template-columns: 1fr; }
}
@media (max-width: 480px) {
  .rack-card { grid-template-columns: 1fr; }
  .rack-header { grid-column: 1; grid-row: 1; padding: 14px 18px 8px; }
  .rack-stage { grid-column: 1; grid-row: 2; }
  .rack-caption { grid-column: 1; grid-row: 3; padding: 10px 18px 14px; }
}


/* ── Plan y pago (kernel/billing) ─────────────────────────────────────────
   Las tarjetas de plan son etiquetas con un radio adentro: elegir es tocar
   la tarjeta entera, y la elegida se marca por `:has(input:checked)` (y por
   `.is-selected`, que pone plan-pago.js para quien no tenga `:has`). */
.plan-alert {
    display: flex; align-items: center; justify-content: space-between; gap: 16px;
    padding: 16px 20px; border-radius: var(--radius); border: 1px solid var(--border);
}
.plan-alert p { margin: 4px 0 0; color: var(--text-light); font-size: var(--fs-small); }
.plan-alert-danger { background: var(--danger-bg); border-color: var(--danger-text); }
.plan-alert-warn { background: var(--warn-bg); border-color: var(--warn-text); }
.plan-alert-ok { background: var(--primary-soft); border-color: var(--primary-soft-border); }

.plan-period-toggle { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.plan-period-option {
    display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
    padding: 6px 14px; border: 1px solid var(--border); border-radius: 999px;
    font-size: var(--fs-small); font-weight: 600;
    background: var(--card-bg); color: inherit; font-family: inherit;
}
.plan-period-option[aria-pressed="true"] { border-color: var(--primary); color: var(--primary-dark); background: var(--primary-soft); }

.plan-pick { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; margin-top: 16px; }
.plan-pick-card {
    position: relative; display: flex; flex-direction: column; gap: 6px;
    padding: 20px; border: 1px solid var(--border); border-radius: var(--radius);
    background: var(--card-bg); cursor: pointer;
}
.plan-pick-card input[type="radio"] { position: absolute; top: 18px; right: 18px; width: 18px; height: 18px; accent-color: var(--primary); }
.plan-pick-card:has(input:checked), .plan-pick-card.is-selected { border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-soft); }
.plan-pick-quote { cursor: default; }
.plan-pick-name { font-size: var(--fs-stat); font-weight: 700; margin-top: 8px; }
.plan-pick-lead { color: var(--text-light); font-size: var(--fs-small); }
.plan-pick-price { font-size: var(--fs-number); font-weight: 800; letter-spacing: -.02em; margin-top: 10px; }
.plan-pick-per { color: var(--text-light); font-size: var(--fs-small); }
.plan-pick-list { list-style: none; margin: 12px 0 0; padding: 12px 0 0; border-top: 1px solid var(--border); display: grid; gap: 8px; font-size: var(--fs-small); }
.plan-pick-list li { display: flex; gap: 8px; align-items: flex-start; }
.plan-pick-list li::before { content: '✓'; flex: none; width: 18px; height: 18px; border-radius: 6px; display: grid; place-items: center; font-size: var(--fs-meta); background: var(--primary-soft); color: var(--primary-dark); }
.plan-pick-quote .btn { margin-top: auto; justify-content: center; }

.pay-grid { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); gap: 24px; margin-top: 16px; }
.pay-grid .field-wide { grid-column: 1 / -1; }
.pay-summary { padding: 20px; border-radius: var(--radius); background: var(--primary-soft); display: grid; gap: 10px; align-content: start; }
.pay-summary h3 { margin: 0 0 6px; font-size: var(--fs-section); }
.pay-summary-row { display: flex; justify-content: space-between; gap: 12px; font-size: var(--fs-small); color: var(--text-light); }
.pay-summary-row strong { color: inherit; }
.pay-summary-total { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; padding-top: 10px; border-top: 1px solid var(--border); }
.pay-summary-total strong { font-size: var(--fs-page); }
.pay-summary .btn { justify-content: center; }

@media (max-width: 820px) {
    .plan-pick { grid-template-columns: 1fr; }
    .pay-grid { grid-template-columns: 1fr; }
    .plan-alert { flex-direction: column; align-items: flex-start; }
}

/* La sección de tarjeta de Plan y pago: glifo dentro de la casilla, la
   marca detectada a la derecha del número, y «Pago seguro · SSL» arriba. */
.pay-secure { display: flex; align-items: center; gap: 8px; color: var(--text-light); font-size: var(--fs-small); }
.pay-secure svg { width: 16px; height: 16px; }
.pay-field { position: relative; display: flex; align-items: center; }
.pay-field > svg { position: absolute; left: 12px; width: 16px; height: 16px; color: var(--text-light); pointer-events: none; }
.pay-field input { padding-left: 38px; width: 100%; }
/* Las franquicias (ui/_franquicias.html): apagadas hasta que el número
   diga cuál es; en Plan y pago van dentro de la casilla, a la derecha. */
.franquicias { display: inline-flex; gap: 6px; align-items: center; }
.franquicia { width: 30px; height: 20px; opacity: .35; filter: grayscale(1); transition: opacity .15s, filter .15s; }
.franquicia.is-on { opacity: 1; filter: none; }
.franquicias:not(:has(.is-on)) .franquicia { opacity: .6; filter: none; }
.pay-field .franquicias { position: absolute; right: 10px; }
.pay-field input { padding-right: 156px; }

/* La cuenta regresiva de Plan y pago: cuántos días quedan de prueba o de
   cortesía. Cabeza con glifo, título y pastilla; pie con el número y la
   fecha; la barra ancha, y lo lleno es lo que queda. El tono lo pone
   .plan-alert-*. */
.plan-countdown { display: block; }
.plan-countdown-head { display: flex; align-items: flex-start; gap: 16px; }
.plan-countdown-tile { background: var(--card-bg); }
.plan-alert-ok .plan-countdown-tile { color: var(--primary-dark); }
.plan-alert-warn .plan-countdown-tile { color: var(--warn-text); }
.plan-alert-danger .plan-countdown-tile { color: var(--danger-text); }
.plan-countdown-text { flex: 1 1 auto; min-width: 0; }
.plan-countdown-title { margin: 0 0 6px; font-size: var(--fs-page); font-weight: 800; letter-spacing: -.02em; }
.plan-countdown-text p { margin: 0; line-height: 1.5; }
.plan-countdown-foot { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; margin-top: 18px; font-size: var(--fs-small); color: var(--text-light); }
.plan-countdown-foot strong { font-size: var(--fs-stat); color: var(--text); }
.plan-progress { margin-top: 8px; height: 12px; border-radius: 999px; background: var(--card-bg); overflow: hidden; }
.plan-progress i { display: block; height: 100%; border-radius: 999px; }
.plan-alert-ok .plan-progress i { background: var(--primary); }
.plan-alert-warn .plan-progress i { background: var(--warn-text); }
.plan-alert-danger .plan-progress i { background: var(--danger-text); }
@media (max-width: 820px) {
    .plan-countdown-head { flex-wrap: wrap; }
}

/* La etiqueta de estado de la cuenta regresiva, en el tono de la tarjeta.
   No es una pastilla de la casa: ese vocabulario (ok/warn/danger/info/
   muted) no se amplía, y acá el tono tranquilo es el morado de la marca. */
.plan-countdown-state { padding: 6px 14px; border-radius: 999px; font-size: var(--fs-small); font-weight: 600; white-space: nowrap; background: var(--card-bg); }
.plan-alert-ok .plan-countdown-state { color: var(--primary-dark); }
.plan-alert-warn .plan-countdown-state { color: var(--warn-text); }
.plan-alert-danger .plan-countdown-state { color: var(--danger-text); }

/* El gancho «Ahorra 2 meses» del interruptor, en el morado de la marca:
   Renzo no quiso verdes en Plan y pago (2026-09-19). */
.plan-save-tag { padding: 6px 12px; border-radius: 999px; font-size: var(--fs-small); font-weight: 700; white-space: nowrap; background: var(--primary); color: #fff; }

/* La ficha de una empresa en la Flota: los datos de la cuenta como una
   fila de hechos, y las acciones como tarjetas iguales con el botón al
   pie. */
.flota-facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 16px; margin: 0; }
.flota-facts div { display: grid; gap: 4px; align-content: start; }
.flota-facts dt { margin: 0; font-size: var(--fs-meta); text-transform: uppercase; letter-spacing: .06em; color: var(--text-light); }
.flota-facts dd { margin: 0; display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }
.flota-facts dd strong { font-size: var(--fs-stat); }
.flota-actions { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 16px; }
.flota-action { display: flex; flex-direction: column; gap: 10px; padding: 18px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--card-bg); }
.flota-action h3 { margin: 0; font-size: var(--fs-section); }
.flota-action .card-actions { margin-top: auto; padding-top: 6px; }
.flota-action-danger { border-color: var(--danger-vivid); background: var(--danger-bg); }
