/* Ambar Gestión — estilos base, mobile-first.
   Fuentes locales (sin CDN): Fraunces (display) y Archivo (UI). */

@font-face {
  font-family: "Fraunces";
  src: url("../fonts/fraunces-latin.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  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: "Fraunces";
  src: url("../fonts/fraunces-latin-ext.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  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: "Archivo";
  src: url("../fonts/archivo-latin.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  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: "Archivo";
  src: url("../fonts/archivo-latin-ext.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  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;
}

/* --- Tokens (SPEC 2026-10-23 §4) ---
   Primitivos de marca + semánticos con los nombres de la colección Figma «Semántico». Este bloque
   es el modo «Móvil» (< 900 px); el modo «Escritorio» es el `@media (min-width: 900px) :root` de
   abajo. Cada valor reproduce lo que hoy se ve en ese ancho (fase A: cero cambio visual). */
:root {
  /* Primitivos de marca (Figma Primitivos wine/500, wine/700, gold/500). */
  --wine: #5E1B2E;
  --wine-d: #451323;
  --gold: #C79A55;

  /* Semánticos — modo Móvil. */
  --canvas: #F7F0E4;                      /* cream/500 */
  --surface: #FFFFFF;                     /* neutral/0 */
  --surface-subtle: #FFFFFF;              /* neutral/0 (escritorio: sidebar) */
  --surface-hover: #FAF8F8;               /* neutral/60 */
  --surface-muted: #F7F0E4;               /* cream/500 */
  --text: #2E1520;                        /* ink/brand */
  --text-muted: rgba(46, 21, 32, 0.62);   /* ink/muted-62 */
  --text-label: rgba(46, 21, 32, 0.62);   /* ink/muted-62 */
  --border: rgba(46, 21, 32, 0.14);       /* ink/line-14 */
  --border-strong: #BAB0B7;               /* neutral/350 */
  --primary: var(--wine);                 /* wine/500 */
  --primary-hover: var(--wine-d);         /* wine/700 */
  --on-primary: #F7F0E4;                  /* cream/500 */
  --primary-subtle: #F1E5E8;              /* wine/50 */
  --primary-subtle-border: #D7BEC7;       /* wine/200 */
  --topbar-bg: var(--wine);               /* wine/500 */
  --on-topbar: #F7F0E4;                   /* cream/500 */
  --accent: var(--gold);                  /* gold/500 */
  --focus: var(--gold);                   /* gold/500 (escritorio gold/700) */
  --success: #2E7D46;                     /* green/600 */
  --success-strong: #276a3c;              /* green/700 · texto OK sobre su tinte (≈ color-mix(var(--success) 85%, #000)) */
  /* Los fondos «subtle» quedan translúcidos como hoy: sobre blanco dan exactamente green/50, red/50
     y gold/50 de Figma; sobre el canvas se tiñen igual que antes (fase A no cambia el render). */
  --success-subtle: rgba(46, 125, 70, 0.1);   /* green/50 sobre blanco */
  --success-border: #C5DDCA;              /* green/200 */
  --danger: #A83216;                      /* red/600 */
  --danger-subtle: rgba(168, 50, 22, 0.1);    /* red/50 sobre blanco */
  --danger-border: #E8C6BD;               /* red/200 */
  --warning-subtle: rgba(199, 154, 85, 0.18); /* gold/50 sobre blanco */
  --warning-border: #E6D6B8;              /* gold/200 */
  --scrim: rgba(46, 21, 32, 0.45);        /* ink/scrim */
  /* Chip activo (S5/S15, Figma «Chip» Activo por modo): sólido en el teléfono, sutil en escritorio
     (mismos valores que T0). Los usan `.chip.is-active` y el «Personalizado» activo del rango. */
  --chip-active-bg: var(--primary);       /* wine/500 */
  --chip-active-fg: var(--on-primary);    /* cream/500 */
  --chip-active-border: var(--primary);   /* wine/500 */
  /* Fuente de los controles (S16, Figma Control): 16 px en el teléfono (= el body; iOS no hace zoom al
     enfocar), 13 px en escritorio. La usan los inputs/select/textarea de `.field` y `.btn`. */
  --control-font: 16px;
  /* Badge (S19, Figma Badge por modo): píldora en el teléfono, rectángulo de 5 px en escritorio.
     Mismos valores que T0 (`.badge` de app.css y desktop.css); sólo pasan a ser tokens. */
  --badge-radius: 999px;
  --badge-pad-y: 2px;
  --badge-pad-x: 9px;
  --badge-font: 0.72rem;

  --radius: 14px;
  --tap: 44px;
  --nav-h: 62px;
  /* Alto real de `.topbar` (padding 10 + 44 del área táctil + 10) más el notch: es el `top` de todo
     lo que se queda fijo debajo de ella. */
  --topbar-h: calc(64px + env(safe-area-inset-top, 0px));
  --sidebar-w: 232px;
  --font-ui: "Archivo", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-display: "Fraunces", Georgia, "Times New Roman", serif;
}

/* == Design tokens · aliases for Figma code syntax (2026-10-23 redesign, F18) ==
   Non-visual: exact copies of values already used as literals in app.css/desktop.css (origin in Figma
   variable descriptions). Nothing reads them yet; they exist so every Figma variable has a truthful
   WEB name `var(--…)`. Keep in sync with Figma «Primitivos»/«Dimensiones». */
:root {
  --color-wine-50: #f1e5e8;
  --color-wine-100: #f3ebee;
  --color-wine-150: #ead9e0;
  --color-wine-200: #d7bec7;
  --color-gold-700: #916421;
  --color-cream-500: #f7f0e4;
  --color-ink-brand: #2e1520;
  --color-neutral-25: #fcfbf9;
  --color-neutral-50: #f5f5f3;
  --color-neutral-60: #faf8f8;
  --color-neutral-75: #f6f3f4;
  --color-neutral-100: #f1eeec;
  --color-neutral-200: #e4e1e3;
  --color-neutral-300: #c8bdc2;
  --color-neutral-350: #bab0b7;
  --color-neutral-400: #aba3ac;
  --color-neutral-500: #776d77;
  --color-neutral-600: #6d6670;
  --color-neutral-700: #5b525e;
  --color-neutral-750: #5e5660;
  --color-neutral-900: #2c2830;
  --color-green-50: #eaf2ec;
  --color-green-200: #c5ddca;
  --color-green-700: #276a3c;
  --color-red-50: #f6eae7;
  --color-red-200: #e8c6bd;
  --color-gold-50: #f7efe3;
  --color-gold-200: #e6d6b8;
  --color-ink-scrim: rgba(46, 21, 32, 0.45);
  --color-ink-muted-62: rgba(46, 21, 32, 0.62);
  --color-ink-line-14: rgba(46, 21, 32, 0.14);
  --space-0-5: 2px;
  --space-1: 4px;
  --space-1-5: 6px;
  --space-2: 8px;
  --space-2-5: 10px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 20px;
  --space-6: 24px;
  --space-7: 28px;
  --space-8: 32px;
  --radius-xs: 5px;
  --radius-sm: 7px;
  --radius-md: 10px;
  --radius-xl: 18px;
  --radius-pill: 999px;
  --size-control-sm: 34px;
  --size-control-md: 38px;
  --size-topbar: 64px;
  --size-content-max: 1440px;
  --size-form-max: 980px;
}

/* Modo «Escritorio» (>= 900 px). Antes vivía como redefinición de `--cream/--ink/--muted/--line`
   en desktop.css; ahora es el único lugar con valores por ancho. */
@media (min-width: 900px) {
  :root {
    --canvas: #F5F5F3;                    /* neutral/50 */
    --surface-subtle: #FCFBF9;            /* neutral/25 */
    --surface-muted: #F1EEEC;             /* neutral/100 */
    --text: #2C2830;                      /* neutral/900 */
    --text-muted: #6D6670;                /* neutral/600 */
    --text-label: #5B525E;                /* neutral/700 */
    --border: #E4E1E3;                    /* neutral/200 */
    --on-primary: #FFFFFF;                /* neutral/0 */
    --topbar-bg: #FFFFFF;                 /* neutral/0 */
    --on-topbar: #2C2830;                 /* neutral/900 */
    --focus: #916421;                     /* gold/700 */
    --chip-active-bg: var(--primary-subtle);            /* wine/50 */
    --chip-active-fg: var(--primary);                   /* wine/500 */
    --chip-active-border: var(--primary-subtle-border); /* wine/200 */
    --control-font: 13px;
    --badge-radius: 5px;
    --badge-pad-y: 3px;
    --badge-pad-x: 7px;
    --badge-font: 11px;

    --radius: 10px;
    --tap: 40px;
    --sidebar-w: 224px;
    --topbar-h: 64px;
  }
}
@media (min-width: 900px) and (max-width: 1199px) {
  :root { --sidebar-w: 200px; }
}

* { box-sizing: border-box; }

html, body { margin: 0; padding: 0; }

body {
  background: var(--canvas);
  color: var(--text);
  font-family: var(--font-ui);
  font-size: 16px;
  line-height: 1.45;
  -webkit-text-size-adjust: 100%;
  padding-bottom: calc(var(--nav-h) + env(safe-area-inset-bottom, 0px));
}

h1, h2, h3, .display { font-family: var(--font-display); font-weight: 600; letter-spacing: -0.01em; }
h1, .display { font-size: 1.6rem; margin: 0 0 0.6rem; }
.muted { color: var(--text-muted); }

/* --- Barra superior --- */
.topbar {
  position: sticky; top: 0; z-index: 20;
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 10px 16px calc(10px + env(safe-area-inset-top, 0px));
  background: var(--topbar-bg); color: var(--on-topbar);
}
.topbar__brand { font-family: var(--font-display); font-size: 1.05rem; font-weight: 600; }
.topbar__user { display: flex; align-items: center; gap: 10px; margin: 0; }
.topbar__name { font-size: 0.85rem; opacity: 0.85; max-width: 45vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.linkish {
  background: none; border: 0; color: var(--accent); font: inherit; font-size: 0.85rem;
  min-height: var(--tap); padding: 0 4px; cursor: pointer;
}
/* Dentro del contenido el enlace-botón va en vino, como ya va en escritorio: el dorado de `--accent`
   es para la barra superior vino del teléfono («Salir»); sobre crema/blanco daba 2.0–2.6:1. */
:where(.main, .sheet) .linkish { color: var(--primary); }

/* --- Contenido --- */
/* El FAB flota sobre el final del contenido: 84px de aire para que no tape el último botón. */
.main { padding: 16px 16px 84px; max-width: 720px; margin: 0 auto; }
.card {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 16px; margin: 0 0 14px;
}
.card--auth { max-width: 420px; margin: 24px auto; }

/* --- Formularios --- */
.field { display: block; margin: 0 0 14px; }
.field__label { display: block; font-size: 0.85rem; color: var(--text-label); margin-bottom: 4px; }
.field input[type="text"], .field input[type="email"], .field input[type="password"],
.field input[type="number"], .field input[type="date"], .field input[type="search"],
.field input[type="time"], .field input[type="tel"], .field input[type="month"],
.field input[type="file"], .field select, .field textarea {
  width: 100%; min-height: var(--tap); padding: 10px 12px; font: inherit; font-size: var(--control-font);
  border: 1px solid var(--border); border-radius: 10px; background: var(--surface); color: var(--text);
}
/* Foco: un solo color por ancho, `--focus` (dorado en el teléfono, #916421 en escritorio). Hasta la
   fase B convivían los dos en todos los anchos según el elemento (cambio deliberado de a11y). */
.field input:focus-visible, .btn:focus-visible, a:focus-visible {
  outline: 3px solid var(--focus); outline-offset: 2px;
}
.field--check { display: flex; align-items: center; gap: 10px; min-height: var(--tap); }
.field--check input { width: 22px; height: 22px; }
/* Grupo de casillas (p. ej. «Ocultar en el PDF» en Configuración). */
.checks { border: 0; padding: 0; margin: 0 0 14px; }
.checks .field--check { margin: 0; min-height: var(--tap); }
.field__err { display: block; color: var(--danger); font-size: 0.82rem; margin-top: 4px; }
/* S6/S12 · Campo con error (Figma «Campo · Estado=Error»): borde en peligro además del mensaje.
   `aria-invalid` lo pone Django en cada campo con error. */
.field :is(input, select, textarea)[aria-invalid="true"] { border-color: var(--danger); }
/* S6 · Casilla: tilde en vino. */
.field--check input[type="checkbox"] { accent-color: var(--primary); flex: 0 0 auto; }

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  min-height: var(--tap); padding: 10px 18px; border-radius: 12px; border: 1px solid transparent;
  font: inherit; font-size: var(--control-font); font-weight: 600; cursor: pointer; text-decoration: none;
}
.btn--primary { background: var(--primary); color: var(--on-primary); }
.btn--primary:active { background: var(--primary-hover); }
.btn--ghost { background: transparent; border-color: var(--border); color: var(--text); }
.btn--block { width: 100%; }
/* Acción ya cumplida (p. ej. "Enviado a logística #812"): se ve, no se toca. */
.btn.is-disabled { opacity: 0.55; cursor: default; pointer-events: none; }
/* Acción destructiva (anular factura / devolución / cobro): se distingue sin depender sólo del color,
   porque además va última y el texto nombra la consecuencia. Antes vivía en sales.css. */
.btn--danger { background: transparent; border-color: var(--danger); color: var(--danger); }
.btn--danger:active { background: var(--danger-subtle); }
/* Confirmación destructiva (el botón que anula DENTRO de la hoja/página de confirmación): peligro
   lleno. El disparador de afuera sigue siendo `.btn--danger` contorneado. */
.btn--danger.btn--solid { background: var(--danger); border-color: var(--danger); color: var(--on-primary); }
.btn--danger.btn--solid:hover, .btn--danger.btn--solid:active {
  background: var(--danger);  /* respaldo sin `color-mix` */
  background: color-mix(in srgb, var(--danger) 85%, #000);
}

.alert { border-radius: 10px; padding: 10px 12px; margin: 0 0 12px; }
.alert--err { background: var(--danger-subtle); color: var(--danger); }
/* Enlaces a los renglones marcados. Vacía no dibuja nada: el alert también se usa sin lista. */
.alert__rows { margin: 6px 0 0; padding-left: 20px; }
.alert__rows:empty { display: none; }
/* Resumen de errores de `form_fields.html` (S10): título + errores generales + `.alert__rows`. */
.alert__title { margin: 0; }
.alert__text { margin: 4px 0 0; }
.alert--ok { background: var(--success-subtle); color: var(--success); }
/* Aviso con su acción al lado ("Descartado. Deshacer"): en dos renglones el botón se leía como un
   texto suelto abajo del aviso, no como lo que deshace lo que acaba de pasar (U-13). */
.alert--inline { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.alert--inline form { margin: 0; }
/* S1 · Ícono por tono (Figma «Alerta», 20 px): los SVG descargados de Figma tal cual
   (`icons/alert/*.svg`), por máscara como los del menú; el color es el del texto del aviso
   (`currentColor`: éxito / texto / peligro). Va fuera del flujo, así que `.alert--inline` (flex) y las
   listas `.alert__rows` no cambian; sólo se corre el texto. Sin cambios de plantilla. */
.alert--ok { --alert-icon: url('../icons/alert/ok.svg'); }
.alert--warn, .alert--info { --alert-icon: url('../icons/alert/aviso.svg'); }
.alert--err { --alert-icon: url('../icons/alert/error.svg'); }
.alert:is(.alert--ok, .alert--warn, .alert--info, .alert--err) { position: relative; padding-left: 42px; }
.alert:is(.alert--ok, .alert--warn, .alert--info, .alert--err)::before {
  content: ""; position: absolute; left: 12px; width: 20px; height: 20px;
  top: 12px; top: calc(10px + (1lh - 20px) / 2);  /* centrado en el primer renglón */
  background: currentColor;
  mask: var(--alert-icon) center / contain no-repeat; -webkit-mask: var(--alert-icon) center / contain no-repeat;
}
/* S10 · El resumen de errores (`tabindex="-1"` + `autofocus`) toma el foco al cargar: anillo visible. */
.alert[tabindex="-1"]:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }
/* S6 · Enlaces de un aviso de error (a cada campo/renglón marcado): color del aviso, subrayados. */
.alert--err a:not(.btn) { color: inherit; text-decoration: underline; font-weight: 600; }

.messages { list-style: none; margin: 12px 16px 0; padding: 0; max-width: 720px; }
.messages__item { border-radius: 10px; padding: 10px 12px; margin-bottom: 8px; background: var(--surface); border: 1px solid var(--border); }
.messages__item--error { color: var(--danger); }
.messages__item--success { color: var(--success); }

/* --- Banner de offline --- */
#offline-banner {
  position: fixed; top: 0; left: 0; right: 0; z-index: 60;
  background: var(--danger); color: #fff; text-align: center; padding: 8px 12px; font-size: 0.9rem;
}
#offline-banner[hidden] { display: none; }

/* --- Banner de preparación (GESTION_PRECUTOVER) ---
   En el flujo, arriba de todo, en cada pantalla (login incluido) y sin cerrar. Con sesión en escritorio el
   sidebar es fijo y el body se corre `--sidebar-w`: el banner queda en la columna del contenido. */
.precutover-banner {
  background: var(--gold); color: var(--color-ink-brand);
  text-align: center; font-weight: 600; font-size: 0.9rem; line-height: 1.35; padding: 8px 12px;
}

/* --- Navegación inferior --- */
.bottomnav {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 30;
  display: grid; grid-template-columns: repeat(5, 1fr);
  background: var(--surface); border-top: 1px solid var(--border);
  padding-bottom: env(safe-area-inset-bottom, 0px);
}
.bottomnav__item {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px;
  min-height: var(--nav-h); padding: 6px 2px;
  font-size: 0.7rem; color: var(--text-muted); text-decoration: none;
}
.bottomnav__item.is-active { color: var(--primary); font-weight: 600; }
/* Mismos SVG del sidebar por máscara (SPEC §4): el color sigue al texto del ítem (activo = vino). */
.bottomnav__icon {
  display: block; width: 22px; height: 22px; flex: 0 0 22px; background: currentColor;
  mask: var(--nav-icon) center / contain no-repeat; -webkit-mask: var(--nav-icon) center / contain no-repeat;
}
/* Íconos de navegación (sidebar y barra inferior): `data-icon` → SVG local (trazo 1.65, grilla 24). */
[data-icon="home"] { --nav-icon: url('../icons/nav/home.svg'); }
[data-icon="file"] { --nav-icon: url('../icons/nav/file.svg'); }
[data-icon="receipt"] { --nav-icon: url('../icons/nav/receipt.svg'); }
[data-icon="users"] { --nav-icon: url('../icons/nav/users.svg'); }
[data-icon="bag"] { --nav-icon: url('../icons/nav/bag.svg'); }
[data-icon="truck"] { --nav-icon: url('../icons/nav/truck.svg'); }
[data-icon="box"] { --nav-icon: url('../icons/nav/box.svg'); }
[data-icon="layers"] { --nav-icon: url('../icons/nav/layers.svg'); }
[data-icon="wallet"] { --nav-icon: url('../icons/nav/wallet.svg'); }
[data-icon="out"] { --nav-icon: url('../icons/nav/out.svg'); }
[data-icon="chart"] { --nav-icon: url('../icons/nav/chart.svg'); }
[data-icon="activity"] { --nav-icon: url('../icons/nav/activity.svg'); }
[data-icon="settings"] { --nav-icon: url('../icons/nav/settings.svg'); }
[data-icon="menu"] { --nav-icon: url('../icons/nav/menu.svg'); }

/* --- Hoja inferior (confirmaciones) --- */
.sheet {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 50;
  background: var(--surface); border-radius: 18px 18px 0 0;
  border-top: 1px solid var(--border); box-shadow: 0 -8px 30px rgba(46, 21, 32, 0.18);
  padding: 18px 16px calc(18px + env(safe-area-inset-bottom, 0px));
}
.sheet__title { font-family: var(--font-display); font-size: 1.15rem; margin: 0 0 8px; }
.sheet__actions { display: flex; gap: 10px; margin-top: 14px; }
.sheet__actions .btn { flex: 1; }
.sheet-backdrop { position: fixed; inset: 0; z-index: 40; background: var(--scrim); }

/* --- Botón flotante --- */
.fab {
  position: fixed; right: 16px; z-index: 35;
  bottom: calc(var(--nav-h) + 16px + env(safe-area-inset-bottom, 0px));
  width: 58px; height: 58px; border-radius: 50%; border: 0;
  background: var(--primary); color: var(--on-primary); font-size: 1.6rem; line-height: 1;
  display: flex; align-items: center; justify-content: center; cursor: pointer;
  box-shadow: 0 6px 18px rgba(46, 21, 32, 0.3); text-decoration: none;
}

/* --- Alpine: nada parpadea antes de que arranque --- */
[x-cloak] { display: none !important; }
/* Alpine oculta con el atributo `hidden` (`:hidden="…"`), nunca con `style="display:none"`:
   la CSP va con `style-src 'self'` y bloquea los estilos inline. `!important` porque varias
   clases de acá fijan `display: flex`. */
[hidden] { display: none !important; }

/* --- Indicador de HTMX --- */
.htmx-indicator { display: none; }
.htmx-request .htmx-indicator, .htmx-request.htmx-indicator { display: inline; }

/* --- Tarjetas y listas --- */
.card__title { font-family: var(--font-display); font-size: 1.05rem; margin: 0 0 10px; }
.small { font-size: 0.82rem; }

/* Número principal de una tarjeta (ventas del día, cobros del día). */
.stat__value {
  font-family: var(--font-display); font-weight: 600; margin: 0 0 6px;
  font-size: clamp(1.3rem, 7vw, 1.9rem); font-variant-numeric: tabular-nums; overflow-wrap: anywhere;
}

.rows { list-style: none; margin: 0; padding: 0; }
.row {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 10px 0; border-bottom: 1px solid var(--border); min-height: var(--tap);
}
.row:last-child { border-bottom: 0; }
.row--stack { flex-direction: column; align-items: flex-start; gap: 2px; }
/* Fila con acciones (pedidos web de "Hoy"): nombre + importe arriba, los botones en su PROPIO
   renglón full-width. Todo en una línea medía ~460px contra los ~324 útiles de un teléfono de 390:
   scroll horizontal en Hoy, el nombre a una letra por renglón y "Descartar" fuera de la tarjeta
   (revisión C6, U-1). El `flex-basis` del lado izquierdo evita que el nombre colapse contra un
   importe largo, que no puede encogerse (`white-space: nowrap`). */
.row--web { flex-wrap: wrap; }
.row--web .row__main { flex: 1 1 50%; }
.row--web .actions { flex: 1 0 100%; }
.row__main { display: flex; flex-direction: column; gap: 2px; min-width: 0; text-decoration: none; color: inherit; flex: 1; }
.row__title { font-weight: 600; overflow-wrap: anywhere; }
.row__side { display: flex; align-items: center; gap: 8px; }
.row__amount { font-variant-numeric: tabular-nums; white-space: nowrap; }

.badge {
  display: inline-block; border-radius: var(--badge-radius); padding: var(--badge-pad-y) var(--badge-pad-x);
  font-size: var(--badge-font); font-weight: 600;
  background: var(--canvas); border: 1px solid var(--border); color: var(--text-muted);
}
.badge--warn { background: var(--danger-subtle); border-color: transparent; color: var(--danger); }

/* --- Barra de acciones y buscadores --- */
.toolbar { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 10px; margin: 0 0 14px; }
.field--grow { flex: 1 1 220px; margin: 0; }

.results { list-style: none; margin: 0 0 14px; padding: 0; }
.result { border-bottom: 1px solid var(--border); }
.result:last-child { border-bottom: 0; }
.result__link {
  display: flex; flex-direction: column; gap: 2px; padding: 10px 4px;
  min-height: var(--tap); text-decoration: none; color: inherit;
}
.result__name { font-weight: 600; }

/* --- Menú de la hoja "Más" --- */
.menu { list-style: none; margin: 0 0 8px; padding: 0; max-height: 55vh; overflow-y: auto; }
.menu__item {
  display: flex; align-items: center; min-height: var(--tap);
  padding: 6px 2px; border-bottom: 1px solid var(--border); color: inherit; text-decoration: none;
}

/* --- Paginación --- */
.pager { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin: 0 0 16px; }

/* La navegación inferior mezcla enlaces y un botón: se ven igual. */
/* `font: inherit` también pisaba el tamaño: «Más» salía a 16 px contra .7rem de los demás. */
button.bottomnav__item { background: none; border: 0; font: inherit; font-size: 0.7rem; cursor: pointer; }

@media (min-width: 720px) {
  .main { padding: 24px 16px 84px; }
  h1, .display { font-size: 2rem; }
  .sheet { max-width: 720px; margin: 0 auto; border-radius: 18px 18px 0 0; }
}

/* --- Ventas: chips de filtro, líneas del formulario, acciones --- */
.chips { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 14px; }
.chip {
  display: inline-flex; align-items: center; min-height: var(--tap); padding: 4px 14px;
  border: 1px solid var(--border); border-radius: 999px; background: var(--surface);
  color: var(--text-muted); font: inherit; font-size: 0.85rem; text-decoration: none; cursor: pointer;
}
.chip.is-active { background: var(--chip-active-bg); border-color: var(--chip-active-border); color: var(--chip-active-fg); font-weight: 600; }
/* Los chips de filtro son enlaces (recargan y vuelven con `is-active`); los de modo son un
   `<label>` con un radio adentro y nunca recargan: sin esto el modo elegido no se distinguía.
   Va en su PROPIA regla, no en la lista de arriba: un navegador sin `:has` descartaría todo el
   selector y también se llevaría puesto el `is-active` de los filtros. */
.chip:has(input:checked) {
  background: var(--primary); border-color: var(--primary); color: var(--on-primary); font-weight: 600;
}
/* E8: el texto de esos chips es un `.field__label` (gris de etiqueta): sobre vino tiene que heredar el
   `--on-primary` del chip, si no queda gris sobre vino. */
.chip:has(input:checked) .field__label { color: inherit; }
.chip:has(input:checked) > input { accent-color: var(--on-primary); }

.badges { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 8px; }
/* Texto `--success-strong` (#276A3C): `--success` sobre su propio tinte daba 4.33:1 (piso AA 4.5). */
.badge--ok {
  background: rgba(46, 125, 70, 0.12); border-color: transparent;
  color: var(--success-strong);
}

/* Fila de línea de documento; `--flag` marca las que hay que revisar al repetir un pedido. */
.row--line { align-items: flex-start; }
.row--flag { background: rgba(199, 154, 85, 0.16); border-radius: 10px; padding-left: 8px; padding-right: 8px; }

/* Cantidad · precio · descuento en una sola fila, aun en pantallas chicas. */
/* `align-items: end`: en 320–360 px "Costo sin IVA" cae a dos líneas y sin esto los inputs quedan escalonados. */
.line-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; width: 100%; align-items: end; }
.line-grid .field { margin-bottom: 8px; }
/* Sin IVA discriminado la línea de compra solo lleva cantidad y costo. */
.line-grid--2 { grid-template-columns: repeat(2, 1fr); }

.unit-toggle { display: flex; gap: 8px; margin: 4px 0 8px; }

/* En una fila apilada (columna) `flex-basis` se mide en alto: `field--grow` dejaría 220px de aire. */
.field--full { width: 100%; flex: 0 0 auto; margin-bottom: 8px; }

/* Botones de acción de un documento: se apilan solos y respetan el área táctil. */
.actions { display: flex; flex-wrap: wrap; gap: 10px; }
.actions form { margin: 0; }

.alert--warn { background: var(--warning-subtle); color: var(--text); }
.alert--warn .btn { margin-top: 8px; }

/* --- FAB: nunca en una pantalla que es un formulario de alta/edición (revisión E2) ---
   El template propio apaga el bloque `fab`; los que no se pueden tocar marcan el body con
   `page-form` (bloque `body_class` de base.html). Ver docs/OPERACION.md. */
.page-form .fab { display: none; }

/* --- Escritorio: sidebar con los mismos destinos (spec §4, revisión E4) ---
   En teléfono no existe (la barra inferior + la hoja "Más" siguen siendo la navegación); a partir
   de 900px la columna reemplaza a la barra inferior y al FAB, sin una línea de JS. */
.sidebar { display: none; }

@media (min-width: 900px) {
  /* `has-nav` = hay sesión y por lo tanto sidebar. Sin sesión (login) no se corre el contenido. */
  body { padding-bottom: 0; }
  body.has-nav { padding-left: var(--sidebar-w); }

  .sidebar {
    position: fixed; top: 0; left: 0; bottom: 0; z-index: 10;
    width: var(--sidebar-w); overflow-y: auto;
    display: flex; flex-direction: column; gap: 2px;
    padding: 14px 12px calc(14px + env(safe-area-inset-bottom, 0px));
    background: var(--surface-subtle); border-right: 1px solid var(--border);
  }
  .sidebar__item {
    display: flex; align-items: center; min-height: var(--tap);
    padding: 8px 12px; border-radius: 10px;
    color: var(--text-muted); text-decoration: none; font-size: 0.95rem;
  }
  .sidebar__item.is-active { background: var(--primary); color: var(--on-primary); font-weight: 600; }

  /* La barra inferior es de teléfono: en escritorio la reemplaza la columna. El FAB SÍ se queda
     (spec §4: "en todas las pantallas"): sin él no hay forma de arrancar un presupuesto desde Hoy
     ni desde la lista. Solo se le baja el hueco que dejaba la barra. */
  .bottomnav { display: none; }
  .fab { bottom: calc(24px + env(safe-area-inset-bottom, 0px)); }

  .main { padding-bottom: 32px; }
}

/* --- C3: carga por renglón (buscador fijo + grilla >= 900 px) ------------------------------------
   Todo cuelga de `.lines--grid`: `.rows li.row` genérico lo usan los detalles de venta y compra y no
   se puede tocar. La misma `<li data-index>` es tarjeta en el teléfono y renglón en la compu. */

/* El buscador no se va de pantalla mientras se cargan 40 líneas. */
.lines-search {
  position: sticky; top: var(--topbar-h); z-index: 5;
  background: var(--surface); padding-top: 4px; margin: 0 0 8px;
}
/* Los resultados quedan DENTRO del bloque fijo y con scroll propio: así no tapan renglones. */
.lines-search .results { max-height: 40vh; overflow-y: auto; }
.result.is-active { background: rgba(199, 154, 85, 0.22); }

/* La línea recién agregada tiene que quedar debajo del buscador fijo, no atrás; y en el teléfono
   tampoco puede quedar debajo de la barra inferior (`scroll-margin-bottom`, revisión C3 #6). */
.row--line-entry {
  scroll-margin-top: calc(var(--topbar-h) + 5rem);
  scroll-margin-bottom: calc(var(--nav-h) + 16px);
}
.line-cell { width: 100%; min-width: 0; }
.line-cell .badge { margin: 2px 0; }

/* El aviso «Ya estaba en la lista» vive SIEMPRE en el DOM (región `aria-live`): si apareciera y
   desapareciera con `:hidden`, el lector de pantalla no lo anunciaría nunca (revisión C3 #4).
   Vacío no dibuja nada: las clases `badge badge--warn` las pone el getter `avisoClass`. */
.line-warn:empty { margin: 0; }

/* C5: la sub-línea del producto es UNA sola (lista y tier · stock · comprometido). En la grilla
   ANCHA no puede envolver: si envuelve, el renglón crece y se pierde el "una línea = un producto"
   que ganó C3 (U-6). Pero entre 900 y 1000 px la primera columna mide ~435 px (unos 66 caracteres)
   y el texto completo pasa de 75: con `ellipsis` se comía justo la parte de plata. Ahí se deja
   envolver — la grilla de C3 arranca igual en 900 px y sus columnas fijas no se mueven, solo crece
   el alto del renglón. `.line-status` es la región `role="status"` del re-precio: vive siempre en el
   DOM (si apareciera con el texto, el lector de pantalla no anunciaría nada) y vacía no ocupa nada. */
.line-sub { margin: 2px 0 0; }
.line-status { margin: 0 0 8px; }
.line-status:empty { margin: 0; }

@media (min-width: 1000px) {
  .lines--grid .line-sub {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
}

/* En el teléfono el toggle Unidad/Caja se lee ANTES de la grilla de cantidad/precio. En el DOM va
   después (así el orden de tabulación de escritorio sigue a las columnas), y acá se reordena. */
@media (max-width: 899px) {
  .row--line-entry > .line-cell { order: 1; }
  .row--line-entry > .unit-toggle { order: 2; }
  .row--line-entry > .line-grid { order: 3; }
  .row--line-entry > .row { order: 4; }
  .row--line-entry > .lines-mobile-only { order: 5; }
}

/* El teclado numérico del teléfono no tiene Enter: ese botón es el reemplazo, y sobra en la compu. */
.lines-mobile-only { margin: 6px 0 0; }

/* El encabezado de columnas no existe en el teléfono (las tarjetas ya llevan cada etiqueta). */
.lines-head { display: none; }

@media (min-width: 900px) {
  /* Siete columnas no entran en 720px de ancho; esta pantalla se ensancha, el resto no. */
  .page-lines .main { max-width: 1100px; }

  .lines--sale { --line-cols: minmax(10rem, 1fr) 5rem 9rem 7.5rem 4.5rem 7rem 2.75rem; }
  .lines--purchase { --line-cols: minmax(10rem, 1fr) 5rem 8rem 7rem 2.75rem; }
  .lines--purchase.lines--iva { --line-cols: minmax(10rem, 1fr) 5rem 8rem 5rem 7rem 2.75rem; }

  .lines--grid .lines-head,
  .lines--grid .row--line-entry {
    display: grid; grid-template-columns: var(--line-cols); gap: 10px; align-items: end;
  }
  .lines--grid .lines-head {
    padding: 6px 0; border-bottom: 1px solid var(--border);
    font-size: 0.78rem; color: var(--text-muted); min-height: 0;
  }
  /* Encabezado sin ninguna línea debajo: son siete títulos flotando solos (revisión C3 #7). */
  .lines--grid:not(:has(.row--line-entry)) .lines-head { display: none; }

  /* Los wrappers internos no dibujan caja: sus hijos son celdas de la MISMA grilla. */
  .lines--grid .row--line-entry > .line-grid,
  .lines--grid .row--line-entry > .row { display: contents; }

  /* Toda celda vive en la MISMA fila. Sin esto, el auto-placement `sparse` no deja volver atrás al
     cursor: el toggle Unidad/Caja pide la columna 3 pero en el DOM va después de Desc. % (columna
     5), así que se caía a una segunda fila y arrastraba consigo al importe y al botón Quitar. */
  .lines--grid .row--line-entry > *,
  .lines--grid .row--line-entry > .line-grid > *,
  .lines--grid .row--line-entry > .row > * { grid-row: 1; }

  .lines--grid .field { margin: 0; }
  .lines--grid .field--full { width: auto; }
  /* La columna ya la dice el encabezado: la etiqueta se oculta a la vista, no al lector de pantalla. */
  .lines--grid .field--cell > .field__label {
    position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%);
  }
  /* «Caja de 12» no entra en 9rem de una sola línea: envuelve en vez de desbordar la columna. */
  .lines--grid .unit-toggle { margin: 0; flex-wrap: wrap; }
  .lines--grid .chip { min-height: 36px; font-size: 0.78rem; padding: 2px 10px; }
  .lines--grid .row__amount { align-self: center; text-align: right; }
  .lines--grid .lines-mobile-only { display: none; }

  /* Columna FIJA por celda, no `order` + auto-placement (revisión C3 #1, blocker): el
     `.unit-toggle` de una línea sin caja sale del grid (`[hidden]`) y con auto-placement Precio se
     corría a la columna Unidad, Desc. % a Precio y así hasta el botón Quitar. Con `grid-column`
     explícito la columna sobrante queda vacía y la grilla no se mueve. */
  .lines--sale .row--line-entry > .line-cell { grid-column: 1; }
  .lines--sale .row--line-entry > .line-grid > .field--qty { grid-column: 2; }
  .lines--sale .row--line-entry > .unit-toggle { grid-column: 3; }
  .lines--sale .row--line-entry > .line-grid > .field--price { grid-column: 4; }
  .lines--sale .row--line-entry > .line-grid > .field--disc { grid-column: 5; }
  .lines--sale .row--line-entry > .row > .row__amount { grid-column: 6; }
  .lines--sale .row--line-entry > .row > .btn { grid-column: 7; }

  /* Compras sin IVA discriminado: producto · cantidad · costo · importe · quitar. */
  .lines--purchase .row--line-entry > .line-cell { grid-column: 1; }
  .lines--purchase .row--line-entry > .line-grid > .field--qty { grid-column: 2; }
  .lines--purchase .row--line-entry > .line-grid > .field--cost { grid-column: 3; }
  .lines--purchase .row--line-entry > .row > .row__amount { grid-column: 4; }
  .lines--purchase .row--line-entry > .row > .btn { grid-column: 5; }
  /* Con el check encendido entra la columna IVA % y el resto corre uno (la `<ul>` suma `lines--iva`). */
  .lines--purchase.lines--iva .row--line-entry > .line-grid > .field--iva { grid-column: 4; }
  .lines--purchase.lines--iva .row--line-entry > .row > .row__amount { grid-column: 5; }
  .lines--purchase.lines--iva .row--line-entry > .row > .btn { grid-column: 6; }
}

/* --- Recuento por lista (C4): tarjeta por producto en el teléfono, renglón en la compu ---
   Todo cuelga de `.bulk-list` para no tocar el `.row` genérico (lo usan todas las listas). */
.bulk-list .bulk-head { display: none; }
.bulk-list .bulk-row {
  flex-wrap: wrap; align-items: flex-start;
  /* Los enlaces `#row-<pk>` del alert tienen que dejar el renglón a la vista, no debajo de la barra
     superior fija ni de la barra de guardar. */
  scroll-margin-top: calc(var(--topbar-h) + 8px);
  scroll-margin-bottom: calc(var(--nav-h) + 88px);
}
.bulk-row__main { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1 1 100%; }
.bulk-cells { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; width: 100%; align-items: end; }
.bulk-cells .field { margin: 0 0 4px; }
.bulk-row__preview { margin: 0; width: 100%; }
.bulk-list .bulk-row.is-bad {
  background: var(--danger-subtle);  /* fallback para los navegadores sin `color-mix` */
  background: color-mix(in srgb, var(--danger) 10%, transparent);
  border-radius: 10px; padding: 8px;
}

/* En el teléfono el «sin caja» ya está en el subtítulo del renglón: como celda solo roba un tercio
   del ancho a Botellas y Costo, que son los campos que se tipean. */
.bulk-cells__none { display: none; }
.bulk-cells--nobox { grid-template-columns: repeat(2, 1fr); }

/* Con 432 renglones, el botón de guardar queda a un scroll infinito del renglón que se está tipeando:
   la barra lo trae siempre a mano y de paso dice cuántos renglones van a viajar en el POST. */
.bulk-bar {
  position: sticky; z-index: 8;
  bottom: calc(var(--nav-h) + 8px + env(safe-area-inset-bottom, 0px));
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  margin: 12px 0 0; padding: 10px 12px;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: 0 6px 18px rgba(46, 21, 32, 0.14);
}
.bulk-bar__count { margin: 0; }

@media (min-width: 900px) {
  /* Sin barra inferior (la reemplaza la columna): la barra de guardar baja hasta el borde. */
  .bulk-bar { bottom: 8px; }
  .bulk-cells__none { display: block; }
  .bulk-cells--nobox { grid-template-columns: repeat(3, 1fr); }
}

@media (min-width: 1000px) {
  /* La lista completa en columnas necesita más ancho que los 720px de una pantalla de formulario.
     A 900px las cinco columnas quedaban apretadas (el nombre del producto se partía en tres líneas):
     el renglón en grilla arranca recién a 1000px. */
  .page-bulk .main { max-width: 1100px; }

  .bulk-list .bulk-head,
  .bulk-list .bulk-row {
    display: grid;
    grid-template-columns: minmax(12rem, 1fr) 5.5rem 5.5rem 7.5rem minmax(9rem, 13rem);
    gap: 10px; align-items: center;
  }
  .bulk-list .bulk-head { min-height: 0; color: var(--text-muted); font-size: 0.82rem; }
  .bulk-row__main { flex: none; }
  /* Las tres celdas pasan a ser celdas de la grilla del renglón. */
  .bulk-cells { display: contents; }
  .bulk-cells .field { margin: 0; }
  /* El encabezado ya nombra las columnas: las etiquetas quedan sólo para lectores de pantalla. */
  .bulk-cells .field__label {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip-path: inset(50%); white-space: nowrap;
  }
  .bulk-list .bulk-row .field__err { grid-column: 1 / -1; }
}

/* --- Reportes: rango del período (C8) --------------------------------------------------------
   El chip "Personalizado" es el <summary> de un <details>: abre las dos fechas sin recargar y sin
   una línea de JS (con `style-src 'self'` no hay `x-show` posible). El <details> es un ítem más de
   la fila de chips y el panel se abre POR ENCIMA del contenido: en el flujo, a 390 px el selector
   se llevaba cinco filas y ~200 px de alto antes de que empezara el reporte. */
.chips--range { position: relative; margin-bottom: 8px; }
.range-custom { margin: 0; }
.range-custom > summary {
  display: inline-flex; align-items: center; min-height: var(--tap); padding: 4px 14px;
  border: 1px solid var(--border); border-radius: 999px; background: var(--surface);
  color: var(--text-muted); font-size: 0.85rem; cursor: pointer; list-style: none;
}
/* Safari dibuja su propio triangulito y no respeta `list-style` en el summary. */
.range-custom > summary::-webkit-details-marker { display: none; }
/* SOLO `.is-active`: con `[open]` el chip se pintaba de vino por estar ABIERTO, así que con
   `?rango=mes_anterior` y el panel desplegado había dos chips "activos" a la vez. */
.range-custom > summary.is-active {
  background: var(--chip-active-bg); border-color: var(--chip-active-border); color: var(--chip-active-fg); font-weight: 600;
}
/* La regla general de foco cubre `a`, `.btn` e `input`, no `summary`. */
.range-custom > summary:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }
.range-custom__form {
  position: absolute; top: calc(100% + 8px); left: 0; z-index: 20;
  width: min(340px, 100%); margin: 0; box-shadow: 0 8px 24px rgba(46, 21, 32, 0.18);
}

/* --- Reportes: portada con KPIs y gráficos (C8) -----------------------------------------------
   Los colores de las barras van acá y NO en un `fill=` inline: `style-src 'self'` no admite estilos
   inline, y así el SVG del servidor no repite ningún color de la paleta. */
.kpis-block { margin: 0 0 14px; }
.kpis { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; }
/* KPI (Figma «KPI»): `.kpi` (reportes) y `.metric` (Hoy) son el MISMO componente visual: superficie
   con borde, etiqueta, valor en Archivo con cifras tabulares, nota. El tamaño del valor depende del
   contexto (cuántos tiles por fila), no del componente. */
.kpi, .metric {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 12px; min-width: 0;
}
.kpi__label, .metric__label { margin: 0 0 6px; font: 500 0.8rem var(--font-ui); color: var(--text-muted); }
.kpi .stat__value, .metric__value {
  font-family: var(--font-ui); font-weight: 650; letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums; overflow-wrap: anywhere;
}
/* Los tiles miden ~155 px: con `1.4rem` un `$ 49.492.650,00` se partía en dos líneas y el número
   quedaba cortado por la mitad, tanto en el teléfono como en el escritorio. */
.kpi .stat__value { font-size: clamp(0.95rem, 4.2vw, 1.15rem); margin: 0 0 4px; }
.metric__value { margin: 0 0 6px; font-size: clamp(0.95rem, 3.5vw, 1.7rem); }
.kpi__delta, .metric__note { margin: 0; font-size: 0.78rem; color: var(--text-muted); }
.kpi__delta--ok { color: var(--success); }
.kpi__delta--err { color: var(--danger); }
.kpi__delta--flat { color: var(--text-muted); }

/* `max-width` y no ancho libre: el `viewBox` mide 360, así que estirarlo más agranda también el
   texto de los ejes (a 680 px daría ~17 px, que es el techo cómodo). */
.chart { display: block; width: 100%; max-width: 680px; height: auto; margin: 0 auto; }
.chart__bar { fill: var(--wine); }
.chart__bar--compare { fill: var(--border); }
.chart__bar--out { fill: var(--accent); }
/* 9 unidades del viewBox: en un teléfono de 390 px la escala es ~1 (9 px reales) y en escritorio
   la corta el `max-width` (~17 px). Con el viewBox viejo de 680 esto daba 4,8 px: ilegible. */
.chart__axis { fill: var(--text-muted); font-size: 9px; font-family: var(--font-ui); }
.chart__grid { stroke: var(--border); stroke-width: 1; }

/* Leyenda de color de los gráficos de dos series: sin esto, saber qué era el dorado y qué el vino
   sólo se podía adivinando (el `<title>` de la barra aparece recién al apoyar el dedo). */
.chart__keys { display: flex; flex-wrap: wrap; gap: 14px; margin: 0 0 6px; }
.chart__key { display: inline-flex; align-items: center; gap: 6px; font-size: 0.78rem; color: var(--text-muted); }
.chart__key::before { content: ""; width: 10px; height: 10px; border-radius: 3px; background: var(--wine); }
.chart__key--out::before { background: var(--accent); }
.chart__key--compare::before { background: var(--border); }

/* Tabla equivalente de cada gráfico: la lee el lector de pantalla, no ocupa lugar.
   `position: fixed` y no `absolute`: una <table> ignora `width: 1px` (se estira hasta su ancho
   mínimo) y, absoluta, esos ~400 px de tabla se sumaban al área scrolleable del documento —
   en el teléfono la portada arrancaba con 36 px de scroll horizontal (visto en el navegador, no
   hay test de Django que lo pesque). Fija, la tabla sigue en el árbol de accesibilidad pero no
   agranda el documento, y `clip-path` la deja invisible. */
.visually-hidden {
  position: fixed; top: 0; left: 0; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}

@media (min-width: 720px) {
  .kpis { grid-template-columns: repeat(4, 1fr); }
}

@media (min-width: 1000px) {
  /* Cuatro KPIs por fila y gráficos de 680 px de ancho piden más que los 720 px de una pantalla
     de formulario. */
  .page-reports .main { max-width: 1000px; }
}

/* ==================================================================================================
   Componentes compartidos del rediseño 2026-10-23 (SPEC §5, brief W-STYLE-B). Nombres = Figma; los
   usan las familias por clase. Variantes propias de una familia van en su `<familia>.css`.
   ================================================================================================== */

/* --- Encabezado de página: eyebrow (familia) · h1 · una línea de contexto · acciones --------------
   Teléfono: las acciones (`.page-heading__actions`) van abajo, a lo ancho. ≥900: a la derecha. */
.page-heading { display: flex; align-items: flex-start; justify-content: space-between; flex-wrap: wrap; gap: 12px; margin-bottom: 22px; }
.page-heading h1 { margin-bottom: 4px; }
.eyebrow { margin: 0 0 6px; color: var(--text-muted); font-size: 0.72rem; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; }
.page-description { margin: 0; color: var(--text-muted); font-size: 0.9rem; }
.page-date { color: var(--text-muted); font-size: 0.8rem; padding-top: 5px; }
.page-heading__actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; flex: 1 1 100%; }
.page-heading__actions .btn { flex: 1 1 auto; }
/* Sin acciones no deja hueco (Figma F05-VIS03): el contenedor vacío no ocupa lugar ni suma `gap`.
   Dos reglas: un navegador sin `:has` descartaría la lista entera y se llevaría también `:empty`. */
.page-heading__actions:empty { display: none; }
.page-heading__actions:not(:has(*)) { display: none; }

/* --- Pestañas de familia (`{% family_tabs %}`): navegación entre páginas hermanas ----------------
   Enlaces con filete inferior; la actual con `aria-current="page"`. En el teléfono la fila scrollea
   sola (sin desbordar la página). Teléfono: gap 12 + padding 2 → las 4 de Stock entran enteras a 390
   (UX-6); si no entran (320, las 6 de Reportes) components.js marca `tabs--more-start/-end` → degradé
   en el borde que tiene más, y corre la fila (sólo en horizontal) hasta la pestaña actual. */
.tabs {
  display: flex; gap: 12px; margin: 0 0 18px; border-bottom: 1px solid var(--border);
  overflow-x: auto; overscroll-behavior-x: contain; scrollbar-width: thin;
}
.tabs__item {
  display: inline-flex; align-items: center; flex: 0 0 auto; min-height: var(--tap);
  padding: 6px 2px; margin-bottom: -1px; border-bottom: 2px solid transparent;
  color: var(--text-muted); font-size: 0.9rem; text-decoration: none; white-space: nowrap;
}
/* 28 px de degradé = `FADE` de components.js. */
.tabs--more-end {
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
  mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
}
.tabs--more-start {
  -webkit-mask-image: linear-gradient(to left, #000 calc(100% - 28px), transparent);
  mask-image: linear-gradient(to left, #000 calc(100% - 28px), transparent);
}
.tabs--more-start.tabs--more-end {
  -webkit-mask-image: linear-gradient(to right, transparent, #000 28px, #000 calc(100% - 28px), transparent);
  mask-image: linear-gradient(to right, transparent, #000 28px, #000 calc(100% - 28px), transparent);
}
.tabs__item:hover { color: var(--primary); }
.tabs__item[aria-current="page"] { color: var(--primary); border-bottom-color: var(--primary); font-weight: 650; }
/* Con `overflow-x: auto` el anillo de afuera se corta: va hacia adentro. */
.tabs__item:focus-visible { outline-offset: -3px; }

/* --- Opción (radio visible): valores de formulario NUEVOS (modo del recuento, Entra/Sale) ---------
   `label.choice > input[type=radio] + .choice__body(.choice__title, .choice__text)`. Seleccionada =
   `:has(input:checked)`. Los `label.chip` con radio que ya existen NO migran (SPEC §13 E8). */
.choices { display: flex; flex-wrap: wrap; gap: 10px; margin: 0 0 14px; padding: 0; border: 0; }
.choice {
  display: flex; align-items: flex-start; gap: 12px; flex: 1 1 220px; min-width: 0;
  padding: 12px 16px; border: 1px solid var(--border); border-radius: 10px;
  background: var(--surface); color: var(--text); cursor: pointer;
}
.choice > input[type="radio"] {
  flex: 0 0 auto; width: 18px; height: 18px; margin: 2px 0 0; accent-color: var(--primary);
}
.choice__body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.choice__title { font-weight: 600; }
.choice__text { color: var(--text-muted); font-size: 0.82rem; }
.choice:hover { border-color: var(--border-strong); }
/* Borde de 2 px sin mover el contenido: el segundo píxel es una sombra interior. */
.choice:has(input:checked) {
  background: var(--primary-subtle); border-color: var(--primary); box-shadow: inset 0 0 0 1px var(--primary);
}
/* `--text-muted` sobre `--primary-subtle` daba 4.46:1 en el teléfono (4.52 en escritorio): elegida,
   la descripción va en `--text` (13.7 / 11.8:1). Sin token nuevo. */
.choice:has(input:checked) .choice__text { color: var(--text); }
.choice.is-invalid { border-color: var(--danger); box-shadow: inset 0 0 0 1px var(--danger); }
.choice:has(input:focus-visible) { outline: 3px solid var(--focus); outline-offset: 2px; }
.choice > input:focus-visible { outline: none; }
/* Compacta (Entra | Sale por renglón): segmento chico, mismo área táctil. */
.choices--compact { flex-wrap: nowrap; gap: 6px; margin: 0; }
.choice--compact {
  flex: 0 0 auto; align-items: center; gap: 8px; min-height: var(--tap);
  padding: 4px 12px; border-radius: 7px; font-weight: 600; font-size: 0.9rem;
}
.choice--compact > input[type="radio"] { margin: 0; width: 16px; height: 16px; }

/* --- Resultado de renglón: «stock 60 → queda 12 · ↓ Sale 48» junto a los inputs -----------------
   Toda baja en peligro, toda suba en éxito. La flecha va en el texto (↓/↑ están en las fuentes). */
.row-result {
  display: inline-flex; align-items: center; gap: 6px; margin: 0;
  color: var(--text-muted); font-weight: 600; font-size: 0.9rem; font-variant-numeric: tabular-nums;
}
.row-result--down, .row-result--error { color: var(--danger); }
.row-result--up { color: var(--success); }
.row-result--muted { color: var(--text-muted); font-weight: 500; }

/* --- Vacío: qué es la pantalla y qué hacer ahora (respeta los filtros activos) ------------------ */
.empty {
  display: flex; flex-direction: column; align-items: center; gap: 8px; margin: 0 0 14px;
  padding: 32px 24px; text-align: center;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
}
.empty__title { margin: 0; font: 650 1rem var(--font-ui); color: var(--text); }
.empty__text { margin: 0; max-width: 440px; color: var(--text-muted); }
.empty__actions { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; margin-top: 8px; }
/* S6 · Ícono del vacío (Figma «Vacío» · Ícono 28 px): `<span class="empty__icon" data-icon="box"
   aria-hidden="true">` — cualquier ícono `[data-icon]` del menú, por máscara, en texto apagado. */
.empty__icon {
  display: block; width: 28px; height: 28px; flex: 0 0 28px; background: var(--text-muted);
  mask: var(--nav-icon) center / contain no-repeat; -webkit-mask: var(--nav-icon) center / contain no-repeat;
}

/* --- Alerta y mensaje informativos (hoy `--info`/`--warning` no tenían estilo) ------------------- */
.alert--info { background: var(--surface); border: 1px solid var(--border); color: var(--text); }
.alert--info a:not(.btn) { color: var(--primary); font-weight: 600; }
.messages__item--info { color: var(--text); }
.messages__item--warning { background: var(--warning-subtle); border-color: var(--warning-border); color: var(--text); }

/* --- Enlaces dentro de filas, listas y tablas: color de texto, nunca el azul del navegador.
   `:where` deja la especificidad en un elemento: cualquier clase (`.btn`, `.chip`, `.linkish`…) gana. */
:where(.rows, .record-list, .results, .bulk-list, table) a { color: inherit; }

/* --- Cifras tabulares en montos y celdas ---------------------------------------------------------- */
.row__amount, .record-cell, .stat__value, .metric__value { font-variant-numeric: tabular-nums; }

/* --- Barra de acción (`.bulk-bar`) para cualquier formulario largo -------------------------------
   La barra ya es fija abajo (sobre la barra inferior en el teléfono). Estructura opcional:
   `.bulk-bar__summary` (conteo + `.bulk-bar__detail`) y `.bulk-bar__actions` (Cancelar + primaria).
   Sólo las barras con esa estructura envuelven: la del recuento sin estructura no cambia. */
.bulk-bar:has(.bulk-bar__actions) { flex-wrap: wrap; }
.bulk-bar__summary { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1 1 200px; }
.bulk-bar__detail { margin: 0; color: var(--text-muted); font-size: 0.82rem; }
.bulk-bar__detail:empty { display: none; }
.bulk-bar__actions { display: flex; align-items: center; gap: 8px; flex: 0 1 auto; margin-left: auto; position: relative; }
/* Enlace-botón compacto dentro de un renglón de texto (avisos de la barra: «Mostrar todos», «Elegir
   modo»): alto visual = el del texto; el área táctil de 44 px la da un `::before` centrado, no la
   caja. `.bulk-bar__actions` es `position: relative` y va después en el DOM, así que si el área
   invisible llega a pisar los botones, el toque es de los botones. Fuera de la barra: `.linkish--compact`. */
.bulk-bar .linkish, .linkish--compact {
  position: relative; min-height: 0; padding: 0 2px; line-height: inherit; vertical-align: baseline;
}
.bulk-bar .linkish::before, .linkish--compact::before {
  content: ""; position: absolute; left: -4px; right: -4px; top: 50%; height: var(--tap); transform: translateY(-50%);
}

/* El foco nunca queda debajo de lo fijo de abajo (WCAG 2.4.11, UX-5): `--fixed-bottom` lo escribe
   components.js sólo si hay una `.bulk-bar` visible (su alto + su `bottom`, que en el teléfono ya
   incluye la barra inferior). Sin barra: la barra inferior en el teléfono, nada en escritorio. */
html { scroll-padding-bottom: var(--fixed-bottom, calc(var(--nav-h) + env(safe-area-inset-bottom, 0px))); }

@media (max-width: 899px) {
  /* Teléfono compacto (UX-5): el resumen en una línea (conteo · detalle seguidos) y los dos botones
     en una sola fila; si entran al lado del resumen, todo va en una fila. ≈ 78 px a 390 sin aviso y
     ≈ 95 con un aviso de una línea (antes 104–123 / 169). */
  .bulk-bar:has(.bulk-bar__actions) { gap: 4px 10px; padding: 6px 10px; }
  .bulk-bar__summary {
    flex: 999 1 9rem; flex-direction: row; flex-wrap: wrap; align-items: baseline; gap: 0 6px;
    font-size: 0.85rem; line-height: 1.2;
  }
  .bulk-bar__detail { font-size: 0.8rem; }
  /* S3 · Un detalle de puro texto ocupa a lo sumo UN renglón (ellipsis; el lector de pantalla lo lee
     entero): con el detalle largo de Ventas la barra llegaba a 101 px a 320. Si el detalle tiene un
     enlace o un botón adentro no se recorta (un control nunca queda escondido). */
  .bulk-bar__detail:not(:has(a, button, input)) { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .bulk-bar__actions { flex: 1 0 auto; }
  /* S3 · Botones compactos de la barra: 36 px visibles; el área táctil sigue en 44 px con un
     `::before` que sobresale 4 px arriba y abajo (no pisa al vecino: entre filas hay ≥ 8 px). */
  /* El ancho sale del texto (`flex: 1 1 auto`, padding lateral 12): «Guardar presupuesto» entra en un
     renglón a 390; a < 360 la etiqueta baja a 0.9rem para no partirse (sólo dentro de la barra). */
  .bulk-bar__actions .btn { position: relative; flex: 1 1 auto; min-height: 36px; padding: 6px 12px; }
  .bulk-bar__actions .btn::before {
    content: ""; position: absolute; left: 0; right: 0; top: 50%; height: var(--tap); transform: translateY(-50%);
  }
}
@media (max-width: 359px) {
  .bulk-bar__actions .btn { font-size: 0.9rem; }
}

@media (min-width: 900px) {
  html { scroll-padding-bottom: var(--fixed-bottom, 0px); }
  /* S1: en escritorio el aviso tiene padding 12/16 (desktop.css) → ícono a 16 y texto a 16 + 20 + 12. */
  .alert:is(.alert--ok, .alert--warn, .alert--info, .alert--err) { padding-left: 48px; }
  .alert:is(.alert--ok, .alert--warn, .alert--info, .alert--err)::before { left: 16px; top: 12px; top: calc(12px + (1lh - 20px) / 2); }
  .page-heading { align-items: center; margin-bottom: 26px; }
  .page-heading__actions { flex: 0 0 auto; margin-left: auto; }
  .page-heading__actions .btn { flex: 0 0 auto; }
  .page-description { font-size: 13px; }
  .page-date { font-size: 12px; }
  .tabs { gap: 20px; margin-bottom: 22px; }
  .tabs__item { min-height: 40px; padding: 8px 4px; font-size: 13px; }  /* escritorio: sin cambios */
  .choice__text { font-size: 12px; }
  .choice--compact { font-size: 12px; min-height: 34px; padding: 4px 10px; }
  .row-result { font-size: 13px; }
  .empty__title { font-size: 14px; }
  .empty__text { font-size: 13px; }
  .bulk-bar__detail { font-size: 12px; }
  /* Destructiva con hover propio, como el resto de los botones de escritorio. */
  .btn--danger:hover { background: var(--danger-subtle); }
}
