/* ============================================================================
   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 borde de la casilla SIN marcar. NO es `--border-strong`: ése da
       1.5:1 sobre blanco y la casilla hay que buscarla. Éste da 3.4:1, que es
       lo que la WCAG 1.4.11 pide para el contorno de un control. Hay una
       prueba que calcula la cifra. */
    --checkbox-border: #8d88a3;

    /* 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-page:    22px;   /* el título de la pantalla, en el banner */
    --fs-number:  30px;   /* una cifra que ES el contenido */
    --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;

    /* Bordes y profundidad */
    --border: #e7e4ef;
    --border-strong: #d6d1e4;
    --radius: 12px;
    --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);
    /* 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;
    --warn-bg: #fff6dc;    --warn-text: #9a7b00;
    --danger-bg: #fdebeb;  --danger-text: #b3382e;
    --info-bg: #eaf2fd;    --info-text: #2c66ad;
    /* 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; }
a:hover { text-decoration: underline; }

/* ── 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: 32px 20px;
}

.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;
}
.auth-brand-mark {
    width: 30px; height: 30px;
    border-radius: 9px;
    background: linear-gradient(135deg, var(--primary) 0%, #a78bfa 55%, #f0abfc 100%);
    box-shadow: 0 3px 10px rgba(124, 92, 240, .35);
    flex-shrink: 0;
}
.auth-brand-name {
    font-size: var(--fs-section);
    font-weight: 700;
    letter-spacing: -.35px;
}

.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;
}

/* 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: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:not([type]) { width: 100%; }

input[type="email"]:focus,
input[type="text"]:focus,
input[type="number"]:focus,
input[type="password"]: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. */
.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. */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--sp-2);
    padding: var(--sp-2) 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);
    cursor: pointer;
    transition: background .16s, box-shadow .16s, transform .08s;
}

/* `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);
}
.btn-ghost:hover { background: #fff; 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; }
.topbar-brand { font-weight: 700; letter-spacing: -.3px; }
.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: 28px; height: 28px; 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%);
}
.user-menu-pop {
    position: absolute; right: 0; top: calc(100% + 8px); z-index: 10;
    min-width: 208px; padding: 14px;
    background: var(--card-bg); color: var(--text);
    border: 1px solid var(--border); border-radius: var(--radius);
    box-shadow: var(--shadow-lg);
}
.user-menu-who { margin: 0 0 10px; font-size: var(--fs-body); color: var(--text-light); }
.user-menu-pop .btn { width: 100%; }

.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;
}
.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: 9px 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 {
    display: block; 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 clamp(14px, 2.4vw, 28px);
    background: var(--bg);
}

/* 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 {
    position: relative; padding: 54px 0 40px; text-align: center;
    /* `clip` en X y nada en Y. Los orbes se salen a propósito —su
       contenedor lleva `inset: … -12% …` para que la máscara alcance a
       desvanecerlos antes del recorte— y a 390px eso empujaba el `body` a
       hacer scroll lateral. `clip` recorta sin volverse contenedor de
       scroll, así que la caja de resultados del buscador, que cuelga hacia
       ABAJO, sigue saliéndose por donde tiene que salirse. Con `hidden` el
       otro eje pasaría a `auto` y la caja quedaría recortada. */
    overflow-x: clip;
}

.home-orbs {
    position: absolute; inset: -60px -12% -180px; z-index: 0;
    pointer-events: none; overflow: hidden;
    -webkit-mask-image: radial-gradient(58% 52% at 50% 38%, #000 30%, transparent 100%);
    mask-image: radial-gradient(58% 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. */
.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: 999px;
    box-shadow: var(--shadow);
}
.home-metrics-head {
    display: flex; flex-direction: column; justify-content: center; gap: 2px;
    padding: 8px 18px 8px 14px;
}
.home-metrics-scope {
    font-size: var(--fs-badge); font-weight: 700; letter-spacing: .08em;
    text-transform: uppercase; color: var(--text-light); opacity: .75;
}
/* El select del período es el de la casa —su flecha, su foco— pero SIN la
   caja (pedido de Renzo, 2026-08-03): dentro de la píldora de la tira, un
   borde dentro de otro borde era una caja dentro de una caja. Solo se le
   quita el chrome; el markup sigue siendo un select de la casa. */
.home-period { display: flex; align-items: center; }
.home-period select {
    border: none; background-color: transparent; box-shadow: none;
    padding-left: 0;
}
.home-period select:focus-visible {
    outline: 2px solid var(--accent); outline-offset: 2px;
    box-shadow: none;
}
.home-metric {
    display: flex; flex-direction: column; justify-content: center; gap: 3px;
    padding: 8px 22px;
    border-left: 1px solid var(--border); border-radius: 14px;
    transition: background .15s ease;
}
.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-page); 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: 20px; 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: 3px;
    font-size: var(--fs-meta); font-weight: 700; line-height: 1;
    padding: 3px 7px 3px 5px; border-radius: 999px;
    font-variant-numeric: tabular-nums;
}
.home-delta svg { width: 12px; height: 12px; }
.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 6px;
    font-size: var(--fs-number); font-weight: 700;
    letter-spacing: -.025em; line-height: 1.2;
}

/* 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 26px;
    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; }
.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; }
.search-list a {
    display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap;
    padding: 9px 12px; 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); }
.search-kind {
    flex-shrink: 0; font-size: var(--fs-badge); font-weight: 700;
    text-transform: uppercase; letter-spacing: .07em; color: var(--text-faint);
}
.search-label { font-weight: 500; }
.search-hint {
    color: var(--text-light); font-size: var(--fs-small); overflow-wrap: anywhere;
}
.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); }

/* ── 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. */
.page-banner-text { min-width: 0; flex: 1 1 auto; }
.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; }
/* 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;
}
/* 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);
}

.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 {
    /* `overflow-x: auto` hace que el otro eje compute a `auto`, así que esto
       ES un contenedor de scroll. Sin un alto máximo no tiene scrollport
       vertical contra el que pegarse, y el `thead` pegajoso no se pegaba a
       nada: la promesa de §6.1 estaba escrita y no ocurría. Con el tope, la
       tabla se desplaza dentro de su caja y el encabezado y el paginador
       quedan siempre a la vista. */
    max-height: calc(100dvh - var(--topbar) - 190px);
    min-height: 120px;
    overflow: 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;
}
.table thead th {
    position: sticky; top: 0; z-index: 1;
    padding: var(--sp-3) 0; text-align: left;
    font-size: var(--fs-meta); font-weight: 700; letter-spacing: .06em;
    text-transform: uppercase; color: var(--text-faint);
    background: var(--card-bg); border-bottom: 1px solid var(--border);
}
.table tbody td {
    /* Filas que respiran. Con 11px arriba y abajo la primera celda —nombre y
       subtítulo, dos líneas— quedaba apretada contra las de arriba y abajo, y
       una lista de quinientos productos se leía como un bloque de texto. */
    padding: var(--sp-3) 0;
    /* 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;
}
.table tbody tr:hover { background: var(--bg); }
/* 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); }
.table tfoot td {
    padding: 11px 0; border-top: 2px solid var(--border);
    font-weight: 700;
}
/* 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; }

/* La columna ANGOSTA al lado de otra angosta.

   Las celdas no llevan relleno horizontal a propósito: la separación sale
   del ancho del contenido, y en las tablas de texto sobra. Pero cuando el
   contenido es más angosto que su rótulo —un interruptor bajo
   «CONCILIABLE», una pastilla bajo «ESTADO»— el encabezado invade al
   vecino y se lee «CONCILIABLEESTADO», que fue exactamente lo que la
   revisión del plan de cuentas encontró.

   La clase la pide la CELDA corta (su th y sus td), no la tabla entera:
   global cambiaría el ritmo de todas las pantallas ya revisadas. */
.cell-pad { padding-right: var(--sp-5); }

/* 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 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. */
.cost-total td { color: var(--text); }
.cost-degraded td { color: var(--danger-text); background: var(--danger-bg); }

/* 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. */
.pill {
    display: inline-block; padding: 2px var(--sp-2); border-radius: 999px;
    font-size: var(--fs-badge); font-weight: 600;
    text-transform: uppercase; letter-spacing: .03em;
    background: var(--pill-bg); color: var(--pill-text);
    white-space: nowrap;
}
.pill-ok     { background: var(--ok-bg);     color: var(--ok-text); }
.pill-danger { background: var(--danger-bg); color: var(--danger-text); }
.pill-warn   { background: var(--warn-bg);   color: var(--warn-text); }
.pill-muted  { background: var(--bg);        color: var(--text-light); }

/* 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);
}

/* 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; }

/* ── 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. */
.badge-soft {
    padding: 2px 9px; border-radius: 999px;
    background: var(--ok-bg); color: var(--ok-text);
    font-size: var(--fs-badge); font-weight: 600;
}
.badge-soft-danger { background: var(--danger-bg); color: var(--danger-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; }
/* 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; }
/* 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; }

/* ── 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. */
.token-field { margin-bottom: var(--sp-4); }
.tokens {
    display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2);
    min-height: 44px; margin-bottom: var(--sp-3);
    padding: var(--sp-2) var(--sp-3);
    border: 1px solid var(--border); border-radius: var(--radius);
    background: var(--card-bg);
}
.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);
}

/* 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. */
.match-row {
    display: flex; flex-wrap: wrap; gap: var(--sp-3);
    padding: 0 var(--sp-5) var(--sp-4);
}

/* Las casillas del riel, una debajo de otra: son pocas y cada una es una
   decisión, así que en columna se leen sin barrer de lado a lado. */
.rail-checks {
    display: flex; flex-direction: column; align-items: flex-start;
    gap: var(--sp-2); margin-bottom: var(--sp-3);
}

/* Las líneas de costo: una fila por línea, y cada fila son DOS formularios —el
   que edita y el que borra— que tienen que quedar en el mismo renglón. */
.cost-lines { list-style: none; margin: 0; padding: 0; }
.cost-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. */
.cost-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. */
.cost-lines .inline-row { flex: 1 1 auto; min-width: 0; }

/* 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 {
    margin: 0 0 var(--sp-4);
    font-size: var(--fs-small); line-height: 1.5; color: var(--text-light);
}

/* 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. */
.facts { display: grid; grid-template-columns: auto 1fr; gap: 7px var(--sp-3); margin: 0; }
.facts dt { font-size: var(--fs-label); font-weight: 600; color: var(--text-light); }
/* 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; }
.facts-text { margin: 0; font-size: var(--fs-small); line-height: 1.5; color: var(--text-light); }

/* ── 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;
}
/* 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);
}
.card-header h2 { margin: 0; font-size: var(--fs-section); font-weight: 600; }
/* 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;
}

/* 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. */
.filters-card { padding: var(--sp-4) var(--sp-5); margin-bottom: var(--sp-4); }

/* 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) { border-radius: 0; }
.card-table > .pager { margin-top: 0; padding: var(--sp-4) var(--sp-5); }

/* La tabla se queda DENTRO del relleno de la tarjeta. Llevaba un margen
   negativo para llegar a los bordes; con la tabla de variantes compartiendo
   tarjeta con la caja de ejes, esa sangría a los lados dejaba dos anchos
   distintos en la misma tarjeta y el ojo perdía la columna. */
.card-table { padding-bottom: 0; }
.card-table > .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;
}
/* 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; }
.home-feed-glyph.glyph-denied { background: var(--danger-bg); color: var(--danger-text); }
.home-feed-glyph.glyph-create { background: var(--ok-bg); color: var(--ok-text); }
.home-feed-glyph.glyph-edit { background: var(--primary-soft); color: var(--primary-dark); }
.home-feed-glyph.glyph-remove { background: var(--warn-bg); color: var(--warn-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);
}

.empty { padding: 28px 20px; text-align: center; }
.empty-compact { padding: 18px 0; text-align: left; }
.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; }
    /* `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; }
}

/* ── 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; }
    /* 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;
    border: 1.5px solid var(--checkbox-border);
    border-radius: 50%;
    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);
}

/* 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: 1.5px 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;
}

/* ── 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; }
.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;
}
/* 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 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; }
.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%; }

.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;
}
.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: var(--canvas); color: #fff;
    border-radius: var(--radius-lg) 0 0 0;
}
.drawer-head h2 { margin: 0; font-size: var(--fs-section); font-weight: 600; }
.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); }
.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; }

/* Á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; }

.btn-danger {
    color: #fff; background: var(--danger-text);
    box-shadow: 0 2px 8px rgba(179, 56, 46, .25);
}
.btn-danger:hover { background: #962f26; }
.btn-danger-ghost { color: var(--danger-text); border-color: var(--danger-bg); }
.btn-danger-ghost:hover { background: var(--danger-bg); color: var(--danger-text); }

.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); }

/* 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. */
.btn-small { padding: var(--sp-1) var(--sp-3); font-size: var(--fs-small); }

.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);
}
.setting-key { color: var(--text-faint); }
.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 {
    display: flex; align-items: flex-start; justify-content: space-between;
    gap: 12px; padding-bottom: 14px; border-bottom: 1px solid var(--border);
}
.gateway-name { font-size: var(--fs-section); font-weight: 700; margin: 0; }
.gateway-hint {
    margin: 3px 0 0; color: var(--text-light); font-size: var(--fs-small);
}

/* ── 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; }
.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(--line-soft); }
.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;
}
.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: 36px; height: 36px; 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: 1px solid var(--border); border-radius: var(--radius-card);
}
.people-card-head { display: flex; align-items: flex-start; gap: var(--sp-3); }
.people-avatar {
    flex-shrink: 0; width: 44px; height: 44px; border-radius: 50%;
    object-fit: cover;
}
.people-avatar-empty {
    display: grid; place-items: center; background: var(--primary-soft);
    color: var(--primary-dark); font-weight: 700; font-size: var(--fs-body);
}
/* `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;
}
.people-card-name {
    font-weight: 700; 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;
}
.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; }
.people-card-pills { display: flex; flex-wrap: wrap; gap: 6px; }
/* Las inducciones van separadas del medidor de la ficha por una línea: son dos
   clases de cosa —datos que se llenan y pasos que se hacen— y sin la línea se
   leen como una lista de cinco barras equivalentes. */
.people-card-onboardings {
    display: flex; flex-direction: column; gap: var(--sp-3);
    padding-top: var(--sp-3); border-top: 1px solid var(--border);
}
.people-card-none {
    margin: 0; padding-top: var(--sp-3); border-top: 1px solid var(--border);
    font-size: var(--fs-small); font-style: italic; color: var(--text-light);
}

/* 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(--ok-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(--ok-text); }

/* 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; }

/* 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);
}

.block-checklist .check-list { margin-top: var(--sp-2); }

.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); }
.unlock-controls select { width: auto; min-width: 200px; }
.unlock-unit { font-size: var(--fs-small); color: var(--text-light); }

/* ── 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); }
.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. */
.block-card {
    margin-bottom: var(--sp-3); border: 1px solid transparent;
    background: var(--bg);
}
.block-card:hover { border-color: var(--border); }
.block-card:focus-within { border-color: var(--primary); background: var(--card-bg); }
.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. */
.block-between {
    display: flex; justify-content: center; align-items: center;
    height: var(--sp-5); 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`. */
.richtext-select {
    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. */
.richtext-emoji {
    position: absolute; z-index: 60; display: flex; flex-wrap: wrap; gap: 2px;
    max-width: 264px; max-height: 220px; 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-emoji-pick {
    width: 32px; height: 32px; border: 0; cursor: pointer;
    font-size: var(--fs-glyph);
    background: transparent; border-radius: var(--radius-field);
}
.richtext-emoji-pick:hover { background: var(--bg); }

/* ── 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; }
.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); }

.quiz-list { display: flex; flex-direction: column; gap: 6px; }
.quiz-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; }
/* 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. */
.quiz-modal-label {
    display: block; margin-bottom: 7px;
    font-size: var(--fs-body); font-weight: 700; color: var(--text);
}

/* 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. */
.quiz-modal-image-row {
    display: flex; align-items: flex-start; gap: var(--sp-3);
}
.quiz-modal-image-row > input { flex: 1; min-width: 0; }
.quiz-modal-drop {
    display: grid; place-items: center; flex-shrink: 0;
    width: 104px; height: 104px; border: 2px dashed var(--border-strong);
    border-radius: var(--radius-card); background: transparent; cursor: pointer;
    color: var(--text-light);
}
.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; }
.quiz-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 ────────────────────────────────────────────────── */
/* Los botones de la fila en una sola línea: son cinco —personalizar, código,
   publicar, duplicar, exportar— y apilados verticalmente hacen la fila cuatro
   veces más alta que su contenido. */
.theme-actions { display: flex; gap: var(--sp-1); flex-wrap: wrap;
                 justify-content: flex-end; }

/* 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; }
