/* ═══════════════════════════════════════════════════════════════════
   Crumb · Glass theme — lenguaje visual oficial (aprobado 2026-07-02)
   Origen: maqueta /{rut}/maqueta2 (cliente/maqueta/dashboard2.html),
   síntesis de referencias del usuario (glass estilo Apple actual).

   Principios:
   · lienzo greige cálido (#e9eae5) con brillos ambientales que derivan
   · tarjetas de vidrio esmerilado con tinte radial según el dato
   · Inter variable autoalojada: números enormes weight 200, títulos 600
   · acentos: verde #29bf12 · rojo #f4442e · azul #3c91e6 · lima #e3f380
   · tramas diagonales para series comparativas
   · coreografía de entrada (reveal escalonado) + microanimaciones

   Compatibilidad: reimplementa las clases públicas de admin/theme.css
   (.btn*, .form-*, .modal-*, .badge*, .banner, .filters, tablas,
   .empty-state) para que las páginas migren sin reescribir su JS.

   Modo ligero (POS / hardware modesto): <body class="glass-lite">
   desactiva backdrop-filter y brillos animados conservando el lenguaje.
   ═══════════════════════════════════════════════════════════════════ */

/* ── Inter variable autoalojada (el CSP bloquea Google Fonts) ─────── */
@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 200 700;
  font-display: swap;
  src: url("/static/fonts/inter-var-latin.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: 200 700;
  font-display: swap;
  src: url("/static/fonts/inter-var-latin-ext.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;
}

:root {
  /* Paleta glass */
  --ink: #272727;
  --ink-soft: #4a4a46;
  --muted: #82827c;
  --muted-2: #a6a69f;
  --green: #29bf12;
  --green-deep: #1f8c0e;
  --red: #f4442e;
  --red-deep: #cf2c18;
  --blue: #3c91e6;
  --blue-deep: #1f6cba;
  --lime: #e3f380;
  --lime-deep: #5a6414;
  /* FIX 2026-07-06 — ámbar de advertencia: la paleta usaba el lima como "amarillo"
     en banners/diálogos/semáforos, pero el lima es acento decorativo y no comunica
     alerta. Nuevo par para todos los estados de advertencia. */
  --amber: #f59e0b;
  --amber-deep: #7c4a03;
  --canvas: #e9eae5;
  --hairline: rgba(39, 39, 39, .08);
  --glass: rgba(255, 255, 255, .55);
  --glass-strong: rgba(255, 255, 255, .78);
  --glass-border: rgba(255, 255, 255, .65);

  /* Radios y sombras */
  --r-lg: 26px;
  --r-md: 18px;
  --r-sm: 12px;
  --r-pill: 999px;
  --shadow: 0 1px 2px rgba(35, 35, 30, .05), 0 14px 34px rgba(35, 35, 30, .06);
  --shadow-hover: 0 2px 4px rgba(35, 35, 30, .05), 0 22px 48px rgba(35, 35, 30, .10);
  --shadow-pop: 0 2px 6px rgba(35, 35, 30, .06), 0 26px 64px rgba(35, 35, 30, .18);

  /* Timing */
  --ease: cubic-bezier(.22, .61, .36, 1);
  --ease-spring: cubic-bezier(.34, 1.3, .5, 1);

  --font: "Inter", -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif;

  /* ── Aliases legacy (admin/theme.css) → equivalentes glass ────────
     Las páginas migradas pueden conservar referencias a estas variables
     en sus estilos inline sin romper la coherencia visual. */
  --color-bg: var(--glass);
  --color-bg-alt: rgba(39, 39, 39, .05);
  --color-text: var(--ink);
  --color-text-secondary: var(--muted);
  --color-text-tertiary: var(--muted-2);
  --color-border: rgba(39, 39, 39, .14);
  --color-border-light: var(--hairline);
  --color-accent: var(--blue-deep);
  --color-accent-hover: #185a9d;
  --color-success: var(--green-deep);
  --color-error: var(--red-deep);
  --color-focus-ring: rgba(60, 145, 230, .45);
  --color-etapa-pendiente-bg: rgba(244, 68, 46, .12);
  --color-etapa-pendiente-fg: var(--red-deep);
  --color-etapa-proceso-bg: rgba(60, 145, 230, .13);
  --color-etapa-proceso-fg: var(--blue-deep);
  --color-etapa-produccion-bg: rgba(41, 191, 18, .13);
  --color-etapa-produccion-fg: var(--green-deep);
  --font-stack: var(--font);
  --text-hero: clamp(38px, 4.6vw, 58px);
  --text-title: clamp(28px, 3vw, 38px);
  --text-heading: 22px;
  --text-subheading: 17px;
  --text-body-lg: 16px;
  --text-body: 14px;
  --text-caption: 13px;
  --text-small: 11.5px;
  --radius-sm: var(--r-sm);
  --radius-md: var(--r-md);
  --radius-lg: var(--r-lg);
  --radius-pill: var(--r-pill);
  --shadow-card: var(--shadow);
  --shadow-card-hover: var(--shadow-hover);
}

* { box-sizing: border-box; margin: 0; padding: 0; }

html, body {
  background: var(--canvas);
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--text-body);
  line-height: 1.5;
  min-height: 100vh;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
/* Sticky footer: el body es columna flex; main se estira y el pie (.gfoot,
   inyectado a nivel de body) queda anclado abajo aunque haya poco contenido. */
body { overflow-x: hidden; display: flex; flex-direction: column; }

a { color: var(--blue-deep); text-decoration: none; transition: color .2s var(--ease); }
a:hover { color: var(--color-accent-hover); }

/* ── Brillos ambientales (los inyecta GlassUI.init) ───────────────── */
.ambient { position: fixed; inset: -20%; z-index: -1; pointer-events: none; filter: blur(60px); }
.ambient i { position: absolute; border-radius: 50%; opacity: 0; transition: opacity 1.8s ease; }
.ambient.on i { opacity: 1; }
.ambient .a1 { width: 55vw; height: 55vw; top: -18%; right: -12%; background: radial-gradient(circle, rgba(41, 191, 18, .10), transparent 65%); animation: drift1 46s ease-in-out infinite alternate; }
.ambient .a2 { width: 48vw; height: 48vw; top: 32%; left: -16%; background: radial-gradient(circle, rgba(60, 145, 230, .09), transparent 65%); animation: drift2 58s ease-in-out infinite alternate; }
.ambient .a3 { width: 40vw; height: 40vw; bottom: -22%; right: 18%; background: radial-gradient(circle, rgba(244, 68, 46, .06), transparent 65%); animation: drift3 52s ease-in-out infinite alternate; }
@keyframes drift1 { to { transform: translate(-9vw, 7vh); } }
@keyframes drift2 { to { transform: translate(7vw, -6vh); } }
@keyframes drift3 { to { transform: translate(-6vw, -8vh); } }

/* ── Layout ────────────────────────────────────────────────────────── */
main, .page, .page-wide { max-width: 1180px; margin: 0 auto; padding: 20px 28px 48px; width: 100%; flex: 1 0 auto; }
.page { max-width: 980px; }

/* ── Barra superior glass (la construye GlassUI.topbar) ───────────── */
.gtop { display: flex; align-items: center; gap: 14px; padding: 10px 0 26px; }
.brand { display: flex; align-items: center; gap: 9px; color: var(--ink); }
/* Logo oficial: /static/logocrumbpos.svg (wordmark tinta, viewBox 138.95x36.74).
   34px = ajuste "+30%" pedido por el usuario (2026-07-02). */
/* FIX 2026-07-15 — logo Nanuc +30% en todo el sitio (pedido del usuario). */
.brand img { height: 44px; width: auto; display: block; }

.navpills {
  position: relative; display: flex; gap: 2px;
  background: var(--glass);
  backdrop-filter: blur(20px) saturate(1.4);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  border: 1px solid var(--glass-border);
  border-radius: var(--r-pill);
  padding: 5px; box-shadow: var(--shadow); margin: 0 auto;
}
.navpills a {
  position: relative; z-index: 1;
  font-size: 13.5px; font-weight: 500; color: var(--muted);
  padding: 9px 17px; border-radius: var(--r-pill);
  text-decoration: none; cursor: pointer; white-space: nowrap;
  transition: color .35s var(--ease);
}
.navpills a:hover { color: var(--ink); }
.navpills a.active { color: #fff; }
.navpills a.soon { color: var(--muted-2); cursor: default; }
.navpills .thumb {
  position: absolute; top: 5px; bottom: 5px; z-index: 0;
  background: var(--ink); border-radius: var(--r-pill);
  transition: left .45s var(--ease-spring), width .45s var(--ease-spring);
}

.icirc {
  width: 42px; height: 42px; border-radius: 50%; flex: none;
  background: var(--glass); border: 1px solid var(--glass-border);
  backdrop-filter: blur(20px) saturate(1.4);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  display: grid; place-items: center; cursor: pointer; position: relative;
  box-shadow: var(--shadow);
  transition: transform .3s var(--ease), box-shadow .3s var(--ease);
}
.icirc:hover { transform: translateY(-2px); box-shadow: var(--shadow-hover); }
.icirc:active { transform: scale(.94); }
.icirc svg { width: 18px; height: 18px; stroke: var(--ink); fill: none; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.icirc .dot { position: absolute; top: 10px; right: 11px; width: 7px; height: 7px; border-radius: 50%; background: var(--red); border: 1.5px solid #fff; transition: opacity .4s var(--ease); }
.avatar {
  width: 42px; height: 42px; border-radius: 50%; flex: none;
  background: var(--ink); color: var(--lime);
  display: grid; place-items: center;
  font-size: 14px; font-weight: 700; letter-spacing: .02em;
  box-shadow: var(--shadow); cursor: pointer;
  transition: transform .3s var(--ease);
}
.avatar:hover { transform: translateY(-2px); }

/* Volver: círculo con chevron a la izquierda del brand en subpáginas.
   FIX 2026-07-06 — pedido del usuario: 20% más chico que el .icirc
   estándar (42px → 34px; el chevron baja proporcional 16px → 13px). */
.gback { text-decoration: none; width: 34px; height: 34px; }
.gback svg { width: 13px; height: 13px; }

/* ── Panel lateral flotante (PRUEBA 2026-07-22, referencia del usuario) ──
   Disposición alternativa a las píldoras superiores: los módulos viven en
   un panel izquierdo FLOTANTE — tarjeta glass con puntas redondeadas,
   separada de los bordes del viewport (no un rectángulo blanco pegado al
   costado) — con botón para plegarlo a un riel de solo iconos (estado
   persistido en localStorage). Solo desktop (>1020px): en móvil sigue el
   menú hamburguesa de siempre. Lo construye GlassUI.sidebar(); el body
   recibe .gside-on (+ .gside-min al plegar). Con el panel activo, la
   barra superior conserva solo volver + campana + avatar (el brand y las
   píldoras se ocultan en desktop). */
:root { --gside-w: 244px; --gside-w-min: 76px; --gside-gap: 14px; }
.gside {
  position: fixed; left: var(--gside-gap); top: var(--gside-gap); bottom: var(--gside-gap);
  width: var(--gside-w); z-index: 50;
  display: flex; flex-direction: column;
  background: var(--glass-strong);
  backdrop-filter: blur(24px) saturate(1.4);
  -webkit-backdrop-filter: blur(24px) saturate(1.4);
  border: 1px solid var(--glass-border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow);
  padding: 18px 12px 14px;
  overflow: hidden;
  transition: width .45s var(--ease-spring);
}
body.gside-min .gside { width: var(--gside-w-min); }
/* Brand con crossfade (FIX 2026-07-22, pedido del usuario): el wordmark y
   el isotipo viven superpuestos dentro del mismo <a>; al plegar, el ancho
   del contenedor se contrae con el panel y las opacidades se CRUZAN en
   sincronía (fade out del wordmark, fade in del iso) — cambio sutil, no
   un salto de un logo al otro. */
/* FIX 2026-07-22 — pedido del usuario (2ª iteración): el wordmark
   desplegado se JUSTIFICA con los textos de los ítems del menú (mismo
   borde izquierdo que "Dashboard", "Facturación"…). Ese borde queda a
   44px del padding del panel (2px nav + 11px padding del ítem + 19px de
   icono + 12px de gap). Al plegar, el padding vuelve a 0 y el contenedor
   se centra (transición incluida para que no salte). */
.gside-brand {
  display: flex; align-items: center; justify-content: flex-start;
  padding: 2px 6px 16px 44px;
  transition: padding .45s var(--ease-spring);
}
body.gside-min .gside-brand { justify-content: center; padding: 2px 0 16px; }
.gside-brand .brand {
  position: relative; display: block; overflow: hidden;
  width: 158px; height: 52px;
  transition: width .45s var(--ease-spring);
}
.gside-brand .gword {
  position: absolute; left: 0; top: 50%; transform: translateY(-50%);
  height: 48px; width: auto; opacity: 1; transition: opacity .32s var(--ease);
}
.gside-brand .giso {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  height: 42px; width: auto; opacity: 0; transition: opacity .32s var(--ease);
}
body.gside-min .gside-brand .brand { width: 40px; }
body.gside-min .gside-brand .gword { opacity: 0; }
body.gside-min .gside-brand .giso { opacity: 1; }
/* El botón de plegado se ancla al panel en absoluto: así su viaje desde la
   esquina superior derecha (desplegado) hasta bajo el isotipo (plegado)
   también se anima, en vez de saltar de un layout a otro. */
.gside-toggle {
  position: absolute; top: 28px; right: 14px;
  width: 30px; height: 30px; border-radius: 50%;
  border: 1px solid var(--hairline); background: rgba(255, 255, 255, .7);
  display: grid; place-items: center; cursor: pointer;
  transition: top .45s var(--ease-spring), right .45s var(--ease-spring),
              transform .3s var(--ease), box-shadow .3s var(--ease);
}
.gside-toggle:hover { box-shadow: var(--shadow); transform: translateY(-1px); }
.gside-toggle:active { transform: scale(.92); }
.gside-toggle svg {
  width: 14px; height: 14px; stroke: var(--ink); fill: none;
  stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round;
  transition: transform .45s var(--ease-spring);
}
body.gside-min .gside-toggle { top: 78px; right: 23px; }
body.gside-min .gside-toggle svg { transform: rotate(180deg); }
.gside-nav {
  flex: 1; display: flex; flex-direction: column; gap: 3px;
  overflow-y: auto; overflow-x: hidden; padding: 2px;
  transition: padding-top .45s var(--ease-spring);
}
body.gside-min .gside-nav { padding-top: 40px; }
/* NUEVO 2026-07-23 — HOVER-PEEK (pedido del usuario): con el panel PLEGADO,
   pasar el mouse lo despliega temporalmente como OVERLAY (el contenido de
   la página no se mueve: .gside es fixed y flota encima con sombra pop) y
   al salir se repliega solo. Si el usuario lo dejó desplegado, no aplica.
   GlassUI pone/quita body.gside-peek en mouseenter/mouseleave. */
body.gside-min.gside-peek .gside { width: var(--gside-w); box-shadow: var(--shadow-pop); }
body.gside-min.gside-peek .gside-brand { justify-content: flex-start; padding: 2px 6px 16px 44px; }
body.gside-min.gside-peek .gside-brand .brand { width: 158px; }
body.gside-min.gside-peek .gside-brand .gword { opacity: 1; }
body.gside-min.gside-peek .gside-brand .giso { opacity: 0; }
body.gside-min.gside-peek .gside-toggle { top: 28px; right: 14px; }
body.gside-min.gside-peek .gside-nav { padding-top: 2px; }
/* FIX 2026-07-23 (2ª iteración del peek) — faltaba revertir las reglas que
   ocultan las ETIQUETAS y centran los ítems al plegar (el panel crecía
   pero seguía viéndose plegado: solo iconos e isotipo). */
body.gside-min.gside-peek .gside .sitem { justify-content: flex-start; padding: 10px 11px; }
body.gside-min.gside-peek .gside .slabel,
body.gside-min.gside-peek .gside .soon-tag { display: block; }
.gside .sitem {
  display: flex; align-items: center; gap: 12px;
  padding: 10px 11px; border-radius: 14px;
  font-size: 13.5px; font-weight: 500; color: var(--muted);
  white-space: nowrap; cursor: pointer; text-decoration: none;
  transition: background .25s var(--ease), color .25s var(--ease);
}
.gside .sitem svg { width: 19px; height: 19px; flex: none; stroke: currentColor; fill: none; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
.gside .sitem:hover { color: var(--ink); background: rgba(39, 39, 39, .05); }
.gside .sitem.active { background: var(--ink); color: #fff; }
.gside .sitem.active:hover { color: #fff; }
.gside .sitem.soon { color: var(--muted-2); cursor: default; }
.gside .sitem.soon:hover { background: none; color: var(--muted-2); }
.gside .sitem .slabel { overflow: hidden; text-overflow: ellipsis; }
.gside .sitem .soon-tag {
  margin-left: auto; flex: none; font-size: 9.5px; font-weight: 600; color: var(--muted-2);
  background: rgba(39, 39, 39, .06); padding: 2px 7px; border-radius: var(--r-pill);
}
body.gside-min .gside .sitem { justify-content: center; padding: 11px 0; }
body.gside-min .gside .slabel, body.gside-min .gside .soon-tag { display: none; }

/* Con el panel activo, el contenido se corre a la derecha del panel. */
body.gside-on { padding-left: calc(var(--gside-w) + var(--gside-gap) * 2); transition: padding-left .45s var(--ease-spring); }
body.gside-on.gside-min { padding-left: calc(var(--gside-w-min) + var(--gside-gap) * 2); }
body.gside-on .gtop .brand, body.gside-on .gtop .navpills { display: none; }
body.gside-on .gtop { justify-content: flex-end; }
body.gside-on .gtop .gback { margin-right: auto; }
@media (max-width: 1020px) {
  .gside { display: none; }
  body.gside-on { padding-left: 0; }
  body.gside-on .gtop { justify-content: flex-start; }
  body.gside-on .gtop .brand { display: flex; }
}

/* ── Modo dispositivo: objetivos táctiles agrandados (NUEVO 2026-07-29) ──
   Directriz del usuario: unificar TODAS las superficies de tablet
   (tienda.html, sdp_sala.html, insumos.html, distribucion_despachos.html)
   bajo un mismo lenguaje visual sin perder la ergonomía táctil. Los
   círculos de la barra (`.icirc`/`.avatar`, 42px) y el botón volver
   (`.gback`, 34px) están pensados para clic de mouse en el resto de la
   consola; en un kiosco tocado con el dedo son el ejemplo textual que dio
   el usuario de "algo que resulta chico". Se agrandan SOLO dentro de la
   barra (`.gtop`) y SOLO en `body.modo-dispositivo` (la marca GlassUI.init
   cuando corresponde, ver ui.js) — el resto de la consola (mouse/trackpad)
   no cambia. */
body.modo-dispositivo .gtop .icirc, body.modo-dispositivo .gtop .avatar { width: 52px; height: 52px; }
body.modo-dispositivo .gtop .icirc svg { width: 22px; height: 22px; }
body.modo-dispositivo .gtop .avatar { font-size: 16px; }
body.modo-dispositivo .gtop .gback { width: 44px; height: 44px; }
body.modo-dispositivo .gtop .gback svg { width: 17px; height: 17px; }
body.modo-dispositivo .gtop { gap: 12px; }
/* NUEVO 2026-07-31 — el usuario revierte la agrupación logo+controles al
   lado izquierdo (ver el historial en glass/ui.js, junto a `navHtml`
   dentro de topbar()): vuelve el layout estándar (logo izquierda, tema+
   avatar derecha). En modo dispositivo `.gtop` no tiene `.navpills` ni
   menú hamburguesa con `margin:auto` que empuje los controles al borde,
   así que GlassUI.topbar() inserta este spacer flexible entre el brand y
   el grupo de controles — únicamente en modo dispositivo, el resto de la
   consola sigue con su propio mecanismo (`.navpills { margin: 0 auto }`). */
.gtop-spacer { flex: 1 1 auto; }

/* ── Encabezado de página ──────────────────────────────────────────── */
.page-head { display: flex; justify-content: space-between; align-items: flex-end; gap: 32px; margin: 8px 0 30px; flex-wrap: wrap; }
.eyebrow, .page-eyebrow { font-size: 13px; font-weight: 500; color: var(--muted); margin-bottom: 12px; letter-spacing: .01em; text-transform: none; }
h1.title, .page-title, .page-header h1 {
  font-size: clamp(30px, 3.6vw, 44px);
  font-weight: 300; letter-spacing: -.03em; line-height: 1.08;
  color: var(--ink); text-wrap: balance; margin-bottom: 0;
}
h1.title-hero { font-size: clamp(38px, 4.6vw, 58px); }
.page-subtitle, .page-header .subtitle, .page-header p {
  margin-top: 10px; font-size: 14.5px; color: var(--muted); max-width: 640px;
}
.page-actions { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; padding-bottom: 6px; }
/* Regla de UX (2026-07-02): en desktop el bloque del título (saludo incluido)
   no pasa del 50% del ancho — un saludo largo envuelve en vez de estirarse. */
@media (min-width: 1021px) {
  .page-head > :first-child { max-width: 50%; }
}

/* ── Tarjetas de vidrio ────────────────────────────────────────────── */
.card {
  position: relative;
  background: var(--glass);
  border: 1px solid var(--glass-border);
  border-radius: var(--r-lg);
  backdrop-filter: blur(26px) saturate(1.5);
  -webkit-backdrop-filter: blur(26px) saturate(1.5);
  box-shadow: var(--shadow);
  padding: 24px;
  overflow: hidden;
  transition: transform .4s var(--ease), box-shadow .4s var(--ease);
}
.card.lift:hover, .card-hover:hover { transform: translateY(-3px); box-shadow: var(--shadow-hover); }
.card-hover { cursor: pointer; }
.card::before { content: ""; position: absolute; inset: 0; pointer-events: none; border-radius: inherit; }
/* FIX 2026-07-02 — tintes radiales reforzados a pedido del usuario (los
   originales de la maqueta se percibían muy tenues en pantalla real). */
.card.tint-green::before { background: radial-gradient(70% 60% at 78% 85%, rgba(41, 191, 18, .16), transparent 72%); }
.card.tint-red::before { background: radial-gradient(70% 60% at 80% 15%, rgba(244, 68, 46, .13), transparent 72%); }
.card.tint-blue::before { background: radial-gradient(70% 60% at 20% 85%, rgba(60, 145, 230, .14), transparent 72%); }
/* FIX 2026-07-06 — tinte lima quedó demasiado intenso vs los otros tintes → revertido al valor original de la maqueta.
   Historial: en el refuerzo del 2026-07-02 el lima se subió de rgba(227,243,128,.35) a rgba(217,238,90,.60).
   Causa raíz: ese refuerzo lo dejó mucho más opaco/saturado que green/red/blue (.13–.16); se nota sobre todo en
   la tarjeta "Emitir documento" de facturación. Solución: volver al color/opacidad originales (el .35 es mayor en
   número porque el lima es un color muy claro; ese era el balance visual original). */
.card.tint-lime::before { background: radial-gradient(70% 60% at 80% 20%, rgba(227, 243, 128, .35), transparent 70%); }
/* FIX 2026-07-06 — faltaba un tinte de advertencia real en la paleta glass.
   Historial: el KPI "Bajo stock ideal" de insumos usaba tint-lime como sustituto del amarillo de la espec,
   pero el lima es un acento decorativo (mismo de "Emitir documento") y no comunica alerta.
   Causa raíz: la paleta glass no tenía ámbar/naranjo; se reutilizó el color más parecido.
   Solución: nuevo tint-amber al mismo nivel de intensidad que green/red/blue para estados de advertencia. */
.card.tint-amber::before { background: radial-gradient(70% 60% at 80% 20%, rgba(245, 158, 11, .15), transparent 72%); }
.card > * { position: relative; }
.card-disabled { opacity: .55; cursor: default; }
.card-disabled:hover { transform: none; box-shadow: var(--shadow); }

.chead { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; margin-bottom: 4px; }
.ctitle, .card-title { font-size: 16px; font-weight: 600; letter-spacing: -.01em; color: var(--ink); margin-bottom: 0; }
.csub, .card-desc { font-size: 12.5px; color: var(--muted); margin-top: 3px; font-weight: 400; line-height: 1.45; }
.card-footer { margin-top: 20px; color: var(--blue-deep); font-size: 13px; font-weight: 500; }
.clink {
  font-size: 12.5px; font-weight: 600; color: var(--ink);
  background: var(--glass-strong); border: 1px solid var(--glass-border);
  border-radius: var(--r-pill); padding: 7px 14px; cursor: pointer;
  display: inline-flex; align-items: center; gap: 6px;
  transition: transform .3s var(--ease), box-shadow .3s var(--ease);
  text-decoration: none;
}
.clink:hover { transform: translateY(-1px); box-shadow: var(--shadow); color: var(--ink); }
.clink svg { width: 12px; height: 12px; stroke: currentColor; fill: none; stroke-width: 2; stroke-linecap: round; }

/* Números protagonistas */
.bignum { font-size: clamp(44px, 4.4vw, 62px); font-weight: 200; letter-spacing: -.03em; line-height: 1; font-variant-numeric: tabular-nums; display: flex; align-items: baseline; gap: 6px; }
.bignum .unit { font-size: .45em; font-weight: 300; color: var(--ink-soft); }
.midnum { font-size: clamp(34px, 3vw, 42px); font-weight: 200; letter-spacing: -.025em; line-height: 1; font-variant-numeric: tabular-nums; }
/* NUEVO 2026-07-15 — pedido del usuario: en los dashboards el número del
   KPI va SIEMPRE anclado al borde inferior de la caja, para que los
   números queden alineados entre sí aunque los títulos/subtítulos ocupen
   1 o 2 líneas (los items de una grid comparten altura por defecto). */
.card.kpi { display: flex; flex-direction: column; }
.card.kpi .midnum, .card.kpi .bignum { margin-top: auto; padding-top: 14px; }
.delta { display: inline-flex; align-items: center; gap: 5px; font-size: 13px; font-weight: 600; font-variant-numeric: tabular-nums; }
.delta .tri { width: 0; height: 0; border-left: 5px solid transparent; border-right: 5px solid transparent; }
.delta.up { color: var(--green-deep); }
.delta.up .tri { border-bottom: 7px solid var(--green); }
.delta.down { color: var(--red); }
.delta.down .tri { border-top: 7px solid var(--red); }

/* ── Rejillas ──────────────────────────────────────────────────────── */
.grid { display: grid; gap: 16px; margin-bottom: 16px; }
.options-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
@media (max-width: 900px) { .options-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 600px) { .options-grid { grid-template-columns: 1fr; } }

/* ── Botones ───────────────────────────────────────────────────────── */
.btn, .qbtn {
  display: inline-flex; align-items: center; justify-content: center; gap: 9px;
  font-family: inherit; font-size: 13.5px; font-weight: 500;
  padding: 12px 20px; border-radius: var(--r-pill); cursor: pointer;
  background: var(--glass); color: var(--ink);
  border: 1px solid var(--glass-border);
  backdrop-filter: blur(20px) saturate(1.4);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  box-shadow: var(--shadow);
  transition: transform .3s var(--ease), box-shadow .3s var(--ease), background .3s var(--ease);
  white-space: nowrap; text-decoration: none;
}
.btn:hover, .qbtn:hover { transform: translateY(-2px); box-shadow: var(--shadow-hover); color: var(--ink); }
.btn:active, .qbtn:active { transform: scale(.96); }
.btn svg, .qbtn svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
.btn-primary, .qbtn.dark { background: var(--ink); color: #fff; border-color: transparent; position: relative; overflow: hidden; }
.btn-primary:hover, .qbtn.dark:hover { color: #fff; }
.btn-primary svg, .qbtn.dark svg { stroke: var(--lime); }
/* NUEVO 2026-07-02 — destello al hover en los botones oscuros.
   FIX 2026-07-02 — ajuste del usuario: el fondo NO se aclara al hover
   (antes pasaba a #3a3a36) y el barrido se repite mientras dure el hover,
   a la misma velocidad del shimmer de los skeleton (1.8s ease-in-out
   infinite). Antes era un barrido único que costaba percibir.
   FIX 2026-07-07 — banda 30% más ancha (37%→63%, antes 40%→60%): en las
   cápsulas chicas del menú se veía muy delgada (pedido del usuario). */
.btn-primary::after, .qbtn.dark::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  /* FIX 2026-07-07 (iteración 3) — núcleo en MESETA (brillo pleno
     sostenido 45%→55%), no un pico puntual: el pico se leía delgado. */
  background: linear-gradient(105deg,
    transparent 35%, rgba(255, 255, 255, .16) 41%,
    rgba(255, 255, 255, .45) 45%, rgba(255, 255, 255, .45) 55%,
    rgba(255, 255, 255, .16) 59%, transparent 65%);
  /* FIX 2026-07-02 (iteración final con el usuario) — aparición instantánea
     + barrido rápido y fluido con ease-in-out ("easy ease"):
     · reposo: banda totalmente oculta (-130%);
     · al hover el keyframe inicial la pone tocando el borde (-55%) → asoma
       en los primeros frames aunque la curva arranque suave;
     · 0.9s por barrido, en loop mientras dure el hover. */
  transform: translateX(-130%);
}
.btn-primary:not(:disabled):hover::after, .qbtn.dark:not(:disabled):hover::after {
  animation: btn-shine 1.7s ease-in-out infinite;
}
/* FIX 2026-07-07 — pausa entre barridos (pedido del usuario: "se repite
   muy rápido"). El ciclo pasa de 0.9s continuo a 1.7s donde el barrido
   ocupa el 53% (≈0.9s, misma velocidad y arranque instantáneo de
   siempre) y el resto es descanso con la banda fuera de vista. El
   ease-in-out aplica por segmento → el barrido conserva su curva. */
@keyframes btn-shine {
  0% { transform: translateX(-55%); }
  53% { transform: translateX(130%); }
  100% { transform: translateX(130%); }
}
.btn-secondary { /* hereda el glass por defecto */ }
.btn-danger { color: var(--red-deep); }
.btn-danger:hover { color: var(--red-deep); background: rgba(244, 68, 46, .10); }
.btn-ghost { background: transparent; border-color: transparent; box-shadow: none; color: var(--muted); }
.btn-ghost:hover { color: var(--ink); box-shadow: none; transform: none; }
.btn-small { padding: 8px 15px; font-size: 12.5px; }
.btn:disabled, .qbtn:disabled { opacity: .45; cursor: not-allowed; transform: none; box-shadow: var(--shadow); }
.qbtn.soon { color: var(--muted-2); }
.qbtn.soon:hover { transform: none; box-shadow: var(--shadow); }

/* Microanimaciones de iconos en botones */
.qbtn svg, .btn svg, .clink svg, .icirc svg { transition: transform .45s var(--ease-spring); }
.qbtn:not(.soon):hover .ic-plus, .btn:not(:disabled):hover .ic-plus { transform: rotate(90deg); }
.qbtn:not(.soon):hover .ic-box { transform: translateY(-2px) rotate(-8deg); }
.clink:hover svg { transform: translate(2px, -2px); }
.icirc:hover .ic-bell { transform-origin: 50% 12%; animation: bell .8s var(--ease); }
@keyframes bell {
  0% { transform: rotate(0); } 25% { transform: rotate(13deg); }
  50% { transform: rotate(-10deg); } 74% { transform: rotate(5deg); } 100% { transform: rotate(0); }
}

/* ── Popovers ──────────────────────────────────────────────────────── */
.pop-wrap { position: relative; }
/* FIX 2026-07-29 — el corrimiento defensivo de clampPopover() (glass/ui.js)
   se aplica con `translateX(var(--pop-shift-x))` compuesto DENTRO de la
   misma propiedad transform que ya anima la apertura (scale+translateY),
   en vez de tocar `left`/`right` a mano — nunca pisa la animación de
   apertura. Con --pop-shift-x sin definir, translateX(0px) es un no-op. */
.pop {
  position: absolute; top: calc(100% + 12px); right: 0; z-index: 60;
  width: max-content; min-width: 296px; max-width: min(360px, calc(100vw - 40px));
  background: var(--glass-strong);
  backdrop-filter: blur(32px) saturate(1.6);
  -webkit-backdrop-filter: blur(32px) saturate(1.6);
  border: 1px solid var(--glass-border);
  border-radius: 20px;
  box-shadow: var(--shadow-pop);
  padding: 8px;
  opacity: 0; transform: translateY(-10px) scale(.93) translateX(var(--pop-shift-x, 0px)); transform-origin: top right;
  pointer-events: none;
  transition: opacity .28s var(--ease), transform .42s var(--ease-spring);
  /* FIX 2026-08-08 — reporte del usuario: scroll fantasma vacío después
     del pie en los hubs de la consola. Causa raíz: este popover se oculta
     con opacity:0 pero SIGUE ocupando layout (position:absolute extiende
     el alto del documento) — con muchas notificaciones medía >1.400px y
     colgaba cientos de px bajo el footer. Tope de alto + scroll interno:
     ya no estira la página (ni abierto ni cerrado) y de paso un popover
     kilométrico pasa a ser desplazable en vez de inusable. */
  max-height: min(72vh, 560px);
  overflow-y: auto;
  overscroll-behavior: contain;
}
.pop.open { opacity: 1; transform: translateX(var(--pop-shift-x, 0px)); pointer-events: auto; }
/* Historial — NUEVO 2026-07-29: mientras el avatar de modo dispositivo
   vivió junto al logo del lado IZQUIERDO de la barra, este popover ancló
   por el borde izquierdo (`.pop-left`, `left:0`) para no abrirse hacia un
   borde de pantalla ya próximo. FIX 2026-07-31 — el usuario revirtió esa
   agrupación (ver el historial en glass/ui.js junto a `navHtml`): el
   avatar volvió al borde derecho de la barra en los dos modos, así que
   este popover vuelve también a su ancla ÚNICA (`right:0`, arriba) sin
   necesidad de `.pop-left` — clampPopover() sigue siendo la defensa real
   contra cualquier desborde remanente, verificada de nuevo con este
   anclaje en tablet vertical. */
.pop .stag { opacity: 0; transform: translateY(-7px); transition: opacity .35s var(--ease), transform .4s var(--ease); }
.pop.open .stag { opacity: 1; transform: none; }
.pop.open .stag:nth-child(2) { transition-delay: 40ms; }
.pop.open .stag:nth-child(3) { transition-delay: 80ms; }
.pop.open .stag:nth-child(4) { transition-delay: 120ms; }
.pop.open .stag:nth-child(5) { transition-delay: 160ms; }
.pop.open .stag:nth-child(6) { transition-delay: 200ms; }
.pop.open .stag:nth-child(7) { transition-delay: 240ms; }
.pop-head { display: flex; justify-content: space-between; align-items: center; padding: 10px 12px 8px; }
.pop-title { font-size: 14px; font-weight: 600; }
.pop-action { font: inherit; font-size: 12px; font-weight: 600; color: var(--blue-deep); background: none; border: 0; padding: 0; cursor: pointer; }
.pop-action:hover { text-decoration: underline; }
.pop-sep { height: 1px; background: var(--hairline); margin: 6px 10px; }
.pop-user { padding: 12px 12px 10px; }
.pop-user b { display: block; font-size: 14px; font-weight: 600; }
.pop-user span { font-size: 12px; color: var(--muted); }
.rol-chip { display: inline-block; margin-top: 8px; font-size: 11px; font-weight: 600; background: rgba(39, 39, 39, .07); color: var(--ink-soft); padding: 3px 10px; border-radius: var(--r-pill); }
/* Variante inline: el rol acompaña al nombre, discreto (pedido 2026-07-02) */
.rol-chip.rol-inline { margin: 0 0 0 8px; font-size: 10px; padding: 2px 8px; vertical-align: 1px; color: var(--muted); }
.pop-empty { padding: 26px 18px; text-align: center; font-size: 13px; color: var(--muted); }

.notif { display: flex; gap: 12px; align-items: flex-start; padding: 11px 12px; border-radius: 14px; cursor: pointer; transition: background .25s var(--ease); }
.notif:hover { background: rgba(255, 255, 255, .72); }
.notif .nicon { width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center; flex: none; }
.notif .nicon svg { width: 16px; height: 16px; fill: none; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
.notif .nicon.g { background: rgba(41, 191, 18, .13); } .notif .nicon.g svg { stroke: var(--green-deep); }
.notif .nicon.r { background: rgba(244, 68, 46, .12); } .notif .nicon.r svg { stroke: var(--red-deep); }
.notif .nicon.b { background: rgba(60, 145, 230, .13); } .notif .nicon.b svg { stroke: var(--blue-deep); }
.notif .nicon.n { background: rgba(39, 39, 39, .07); } .notif .nicon.n svg { stroke: var(--muted); }
.notif .ntxt { flex: 1; min-width: 0; }
.notif .ntitle { font-size: 13px; font-weight: 600; line-height: 1.35; display: block; }
.notif .nsub { font-size: 12px; color: var(--muted); margin-top: 2px; display: block; }
.notif .ntime { font-size: 11px; color: var(--muted-2); white-space: nowrap; padding-top: 2px; }
.notif .undot { width: 7px; height: 7px; border-radius: 50%; background: var(--blue); margin-top: 6px; flex: none; }
.notif.read .undot { visibility: hidden; }

.mitem { display: flex; align-items: center; gap: 12px; padding: 11px 12px; border-radius: 14px; font-size: 13.5px; font-weight: 500; color: var(--ink); cursor: pointer; text-decoration: none; transition: background .25s var(--ease); }
.mitem:hover { background: rgba(255, 255, 255, .72); color: var(--ink); }
.mitem svg { width: 17px; height: 17px; flex: none; stroke: var(--muted); fill: none; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.mitem small { display: block; font-size: 11.5px; font-weight: 400; color: var(--muted); margin-top: 1px; }
.mitem.danger { color: var(--red-deep); }
.mitem.danger svg { stroke: var(--red-deep); }
.mitem.on { background: rgba(39, 39, 39, .05); font-weight: 600; }
.mitem .soon-tag { margin-left: auto; font-size: 10.5px; font-weight: 600; color: var(--muted-2); background: rgba(39, 39, 39, .06); padding: 2px 8px; border-radius: var(--r-pill); }
.only-mobile { display: none; }

/* ── Toggle segmentado ─────────────────────────────────────────────── */
.toggle { display: flex; position: relative; background: rgba(39, 39, 39, .06); border-radius: var(--r-pill); padding: 3px; }
.toggle button { position: relative; z-index: 1; font: inherit; font-size: 12px; font-weight: 500; color: var(--muted); background: none; border: 0; padding: 6px 14px; border-radius: var(--r-pill); cursor: pointer; transition: color .3s var(--ease); }
.toggle button.on { color: var(--ink); }
.toggle .tthumb { position: absolute; top: 3px; bottom: 3px; z-index: 0; background: #fff; border-radius: var(--r-pill); box-shadow: 0 2px 8px rgba(35, 35, 30, .10); transition: left .4s var(--ease-spring), width .4s var(--ease-spring); }

/* ── Formularios (contrato admin/theme.css, re-estilizado) ────────── */
.form-card {
  background: var(--glass);
  border: 1px solid var(--glass-border);
  border-radius: var(--r-lg);
  backdrop-filter: blur(26px) saturate(1.5);
  -webkit-backdrop-filter: blur(26px) saturate(1.5);
  box-shadow: var(--shadow);
  padding: 28px;
  margin-bottom: 16px;
}
.form-card-title { font-size: 16px; font-weight: 600; color: var(--ink); margin-bottom: 4px; }
.form-card-desc { font-size: 12.5px; color: var(--muted); margin-bottom: 22px; line-height: 1.45; }
.form-row { margin-bottom: 14px; }
/* FIX 2026-07-23 — pedido del usuario: los botones de acción quedaban
   pegados al campo que los precede (login y varias secciones). Aire
   estándar entre el último elemento de formulario y el botón que sigue. */
.form-row + .btn, .form-row + .btn-primary, form > .btn:last-child { margin-top: 28px; }
.form-row:last-of-type { margin-bottom: 0; }
.form-row.full { grid-column: 1 / -1; }
.form-label, .field label { display: block; font-size: 12.5px; color: var(--muted); font-weight: 500; margin-bottom: 6px; }
.form-input, .form-select, .field input, .field select, .field textarea, textarea.form-input {
  width: 100%; font: inherit; font-size: 14px; color: var(--ink);
  padding: 11px 14px;
  background: rgba(255, 255, 255, .72);
  border: 1px solid var(--hairline);
  border-radius: 14px;
  outline: none;
  transition: border-color .25s var(--ease), box-shadow .25s var(--ease), background .25s var(--ease);
}
.form-input:hover, .form-select:hover { background: rgba(255, 255, 255, .85); }
.form-input:focus, .form-select:focus, .field input:focus, .field select:focus, .field textarea:focus {
  border-color: rgba(60, 145, 230, .55);
  box-shadow: 0 0 0 3px var(--color-focus-ring);
  background: #fff;
}
.form-input.error, .form-select.error { border-color: var(--red); }
.form-error { font-size: 11.5px; color: var(--red-deep); margin-top: 5px; display: none; }
.form-error.visible { display: block; }
.form-hint, .field .hint { font-size: 11.5px; color: var(--muted-2); margin-top: 5px; }
/* FIX 2026-07-29 — modo oscuro fase 2 (barrido sistemático): placeholder y
   estado deshabilitado no tenían regla propia en NINGÚN tema; el navegador
   decidía el color por defecto, que en algunas superficies glass oscuras
   quedaba casi invisible. Se fija con tokens, válidos en ambos temas. */
.form-input::placeholder, .form-select::placeholder, .field input::placeholder,
.field textarea::placeholder, textarea.form-input::placeholder,
.filter-input::placeholder { color: var(--muted-2); opacity: 1; }
.form-input:disabled, .form-select:disabled, .field input:disabled,
.field select:disabled, .field textarea:disabled, textarea.form-input:disabled,
.filter-input:disabled, .filter-select:disabled {
  opacity: .55; cursor: not-allowed; background: var(--color-bg-alt);
}
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.form-grid .full { grid-column: 1 / -1; }
@media (max-width: 480px) { .form-grid { grid-template-columns: 1fr; } }
.field { display: flex; flex-direction: column; }

/* ── Ojo de contraseña (NUEVO 2026-07-31) ────────────────────────────
   Ver el comentario largo junto a GlassUI.aplicarOjoPassword (glass/ui.js):
   TODO input[type=password] se envuelve en `.pwd-wrap` con un botón
   `.pwd-toggle` (SVG ojo/ojo-tachado) posicionado a la derecha, objetivo
   táctil de 44px (mínimo recomendado para toque). El botón nunca es un
   <input>, así que la red de seguridad de modo oscuro para campos ("campos
   solo trazo", más abajo) no lo alcanza ni hace falta que lo alcance —
   usa var(--muted)/var(--ink), ya duales en ambos temas.
     El padding que reserva espacio para el ícono usa `!important` por el
   MISMO motivo ya documentado en el FIX 2026-07-30 de esa red de
   seguridad: los campos de contraseña de hoy llegan con contratos de
   página distintos (.form-input, .big-input propio de pos_venta.html,
   .form-input-lg, inputs SIN clase) y cualquiera puede empatar o superar
   la especificidad de `.pwd-wrap > input` en una página futura — sin
   `!important` un campo nuevo podría perder el padding y tapar el ícono
   detrás del texto. */
.pwd-wrap { position: relative; display: block; width: 100%; }
.pwd-wrap > input { padding-right: 44px !important; }
/* Campos numéricos CENTRADOS (PIN de supervisor/cajero: text-align:center
   + letter-spacing) — GlassUI.aplicarOjoPassword agrega esta clase cuando
   detecta el centrado, para reservar el mismo espacio a la izquierda y no
   correr el punto medio visual del texto hacia la derecha. */
.pwd-wrap.pwd-wrap-center > input { padding-left: 44px !important; }
.pwd-toggle {
  position: absolute; top: 0; right: 0; height: 100%;
  width: 44px; min-width: 44px;
  display: flex; align-items: center; justify-content: center;
  background: none; border: 0; padding: 0; margin: 0;
  cursor: pointer; color: var(--muted);
  -webkit-tap-highlight-color: transparent;
}
.pwd-toggle:hover { color: var(--ink); }
.pwd-toggle:focus-visible { outline: 2px solid var(--blue); outline-offset: -2px; border-radius: var(--r-sm); }
.pwd-toggle svg {
  width: 19px; height: 19px; stroke: currentColor; fill: none;
  stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round;
  pointer-events: none;
}

/* Filtros */
.filters { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 20px; align-items: center; }
.filter-select, .filter-input {
  padding: 8px 13px; height: 36px;
  border: 1px solid var(--hairline); border-radius: var(--r-pill);
  font-size: 12.5px; font-family: inherit; color: var(--ink);
  background: rgba(255, 255, 255, .72);
  transition: border-color .25s var(--ease), box-shadow .25s var(--ease);
}
.filter-select:focus, .filter-input:focus { outline: none; border-color: rgba(60, 145, 230, .55); box-shadow: 0 0 0 3px var(--color-focus-ring); }
/* Buscador con lupa (NUEVO 2026-07-06, pedido del usuario): envolver el
   .filter-input en .filter-search y anteponer el svg de lupa. La lupa es
   del INPUT de una lista — no confundir con la decisión de producto de
   NO tener buscador global en la topbar. */
.filter-search { position: relative; display: inline-flex; align-items: center; }
.filter-search > svg {
  position: absolute; left: 12px; width: 14px; height: 14px;
  stroke: var(--muted); fill: none; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round; pointer-events: none;
}
.filter-search .filter-input { padding-left: 33px; }

/* ── Tablas ────────────────────────────────────────────────────────── */
.table-wrap {
  background: var(--glass);
  border: 1px solid var(--glass-border);
  border-radius: var(--r-lg);
  backdrop-filter: blur(26px) saturate(1.5);
  -webkit-backdrop-filter: blur(26px) saturate(1.5);
  box-shadow: var(--shadow);
  overflow: hidden; overflow-x: auto; -webkit-overflow-scrolling: touch;
}
table { width: 100%; border-collapse: collapse; font-size: 13px; }
thead th {
  background: transparent;
  padding: 12px 14px; text-align: left;
  font-size: 12px; font-weight: 500; color: var(--muted);
  border-bottom: 1px solid var(--hairline);
  text-transform: none; white-space: nowrap;
}
tbody td { padding: 13px 14px; border-bottom: 1px solid var(--hairline); color: var(--ink); font-variant-numeric: tabular-nums; }
tbody tr { transition: background .25s var(--ease); }
tbody tr:last-child td { border-bottom: none; }
tbody tr:hover { background: rgba(255, 255, 255, .5); }
.td-right, td.num { text-align: right; font-variant-numeric: tabular-nums; }
.cell-primary { color: var(--ink); font-size: 13.5px; font-weight: 600; }
.cell-sub { display: block; color: var(--muted-2); font-size: 11.5px; margin-top: 2px; font-weight: 400; }

/* ── Badges ────────────────────────────────────────────────────────── */
.badge {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 11px; border-radius: var(--r-pill);
  font-size: 12px; font-weight: 600; white-space: nowrap;
}
.badge::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.badge.ok, .badge-produccion { background: rgba(41, 191, 18, .12); color: var(--green-deep); }
.badge.proc, .badge-proceso { background: rgba(60, 145, 230, .13); color: var(--blue-deep); }
.badge.no, .badge-pendiente { background: rgba(244, 68, 46, .12); color: var(--red-deep); }
.badge-plan { background: rgba(39, 39, 39, .06); color: var(--muted); }
.badge-plan::before { display: none; }
.ambiente-badge { display: inline-block; font-size: 11px; font-weight: 600; padding: 4px 12px; border-radius: var(--r-pill); }
.ambiente-badge.cert { background: rgba(60, 145, 230, .13); color: var(--blue-deep); }
.ambiente-badge.prod { background: rgba(41, 191, 18, .13); color: var(--green-deep); }
/* NUEVO 2026-07-20 — tercer ambiente demo */
.ambiente-badge.demo { background: rgba(39, 39, 39, .10); color: var(--ink); }

/* ── Banners y alertas ─────────────────────────────────────────────── */
.banner, .alert, .form-banner {
  padding: 12px 16px; border-radius: 14px; font-size: 12.5px; font-weight: 500;
  margin-bottom: 20px; border: 1px solid transparent;
}
.banner, .form-banner { display: none; }
.banner.visible, .form-banner.visible { display: block; }
.banner.success, .form-banner.success, .alert-success { background: rgba(41, 191, 18, .12); color: var(--green-deep); border-color: rgba(41, 191, 18, .18); }
.banner.error, .form-banner.error, .alert-error { background: rgba(244, 68, 46, .10); color: var(--red-deep); border-color: rgba(244, 68, 46, .16); }
.banner.info, .alert-info { background: rgba(60, 145, 230, .11); color: var(--blue-deep); border-color: rgba(60, 145, 230, .18); }
/* FIX 2026-07-06 — warning pasa de lima a ámbar (el lima queda como acento decorativo). */
.banner.warning { background: rgba(245, 158, 11, .14); color: var(--amber-deep); border-color: rgba(245, 158, 11, .25); }

/* ── Modales glass ─────────────────────────────────────────────────── */
.modal-backdrop {
  position: fixed; inset: 0; z-index: 1000;
  background: rgba(35, 35, 30, .32);
  backdrop-filter: blur(10px) saturate(1.2);
  -webkit-backdrop-filter: blur(10px) saturate(1.2);
  display: none; align-items: center; justify-content: center; padding: 20px;
}
.modal-backdrop.visible { display: flex; animation: backdrop-in .3s var(--ease); }
@keyframes backdrop-in { from { opacity: 0; } }
/* FIX 2026-07-23 — con un modal abierto la rueda scrolleaba la PÁGINA de
   fondo (reporte del usuario en "Nuevo producto" con variantes: el cuerpo
   del modal no desborda y el wheel se encadena al body). Con :has se
   bloquea el scroll del body mientras haya un modal visible — aplica a
   todos los modales del software sin tocar JS. */
body:has(.modal-backdrop.visible) { overflow: hidden; }
.modal-card { overscroll-behavior: contain; }
.modal-card {
  background: rgba(255, 255, 255, .92);
  backdrop-filter: blur(34px) saturate(1.6);
  -webkit-backdrop-filter: blur(34px) saturate(1.6);
  border: 1px solid var(--glass-border);
  border-radius: 24px;
  padding: 30px 30px 26px;
  width: 100%; max-width: 500px; max-height: 90vh; overflow-y: auto;
  box-shadow: var(--shadow-pop);
}
.modal-backdrop.visible .modal-card { animation: modal-in .5s var(--ease-spring); }
@keyframes modal-in { from { opacity: 0; transform: translateY(16px) scale(.95); } }
.modal-title { font-size: 21px; font-weight: 600; letter-spacing: -.02em; margin-bottom: 6px; color: var(--ink); }
.modal-subtitle { color: var(--muted); font-size: 13px; margin-bottom: 22px; }
.modal-footer { display: flex; justify-content: flex-end; gap: 10px; margin-top: 28px; }
.modal-divider { border: none; border-top: 1px solid var(--hairline); margin: 18px 0; }
.modal-section { border-top: 1px solid var(--hairline); padding-top: 20px; margin-top: 20px; }
.modal-section:first-of-type { border-top: 0; padding-top: 0; margin-top: 0; }
.modal-section-label, .modal-section-title { font-size: 12.5px; font-weight: 600; color: var(--muted); text-transform: none; margin-bottom: 12px; }

/* ── Diálogos del sistema (reemplazo de confirm/alert/prompt nativos) ─
   NUEVO 2026-07-02 — pedido del usuario: las ventanas emergentes del
   navegador rompen el lenguaje visual. GlassUI.confirmar/avisar/pedir
   generan este modal compacto. */
.gdlg-card { max-width: 400px; padding: 26px 26px 22px; position: relative; }
/* FIX 2026-07-02 — tinte de atención en los diálogos (pedido del usuario);
   los destructivos (.peligro) llevan tinte rojo.
   FIX 2026-07-06 — el tinte de atención pasa de lima a ámbar: el lima es acento
   decorativo de la marca y no comunica advertencia (mismo criterio que banner.warning). */
.gdlg-card::before { content: ""; position: absolute; inset: 0; pointer-events: none; border-radius: inherit; background: radial-gradient(75% 65% at 82% 12%, rgba(245, 158, 11, .16), transparent 72%); }
/* FIX 2026-07-16 — pedido del usuario: la LUZ de fondo del diálogo de
   error debe verse claramente ROJA (antes .13 era indistinguible de la
   ámbar por defecto .16): más alpha y más cobertura. */
.gdlg-card.peligro::before { background: radial-gradient(85% 75% at 82% 12%, rgba(244, 68, 46, .24), transparent 76%); }
.gdlg-card > * { position: relative; }
/* Título en la voz tipográfica de los titulares (delgada), tamaño contenido */
.gdlg-title { font-size: 24px; font-weight: 300; letter-spacing: -.02em; line-height: 1.15; color: var(--ink); margin-bottom: 10px; }
/* FIX 2026-07-17 — white-space pre-line: los diálogos con mensajes de
   varios párrafos (\n\n) los renderizan como tales; los de una línea
   no cambian. */
.gdlg-msg { font-size: 13.5px; color: var(--ink-soft); line-height: 1.55; white-space: pre-line; }
.gdlg-input { margin-top: 14px; }
.gdlg-footer { display: flex; justify-content: flex-end; gap: 10px; margin-top: 22px; }
.btn-peligro-solido { background: var(--red-deep); color: #fff; border-color: transparent; }
.btn-peligro-solido:hover { background: #b32413; color: #fff; }

/* ── Estado vacío ──────────────────────────────────────────────────── */
.empty-state {
  text-align: center; padding: 72px 24px;
  background: var(--glass); border: 1px solid var(--glass-border);
  border-radius: var(--r-lg);
  backdrop-filter: blur(26px) saturate(1.5);
  -webkit-backdrop-filter: blur(26px) saturate(1.5);
  box-shadow: var(--shadow);
}
.empty-state-title { font-size: 17px; font-weight: 600; color: var(--ink); margin-bottom: 8px; }
.empty-state-desc { font-size: 13px; color: var(--muted); margin-bottom: 22px; }

/* ── Trama diagonal (series comparativas / segmentos destacados) ──── */
.hatch {
  background:
    repeating-linear-gradient(-55deg, rgba(39, 39, 39, .28) 0 2px, transparent 2px 5.5px),
    rgba(39, 39, 39, .07);
}

/* ── Pie de página (lo inyecta GlassUI.init en todas las páginas) ──── */
.gfoot {
  text-align: center;
  font-size: 12px;
  font-weight: 500;
  color: var(--muted-2);
  flex: none;
  padding: 18px 28px 26px;
}
.gfoot a { color: var(--muted); font-weight: 600; }
.gfoot a:hover { color: var(--ink); }

/* ── Cruce de vistas (switch dentro de una página) ─────────────────
   NUEVO 2026-07-03 — parámetros APROBADOS por el usuario en el switch
   Catálogo ↔ Productos variables ("quedó muy buena en velocidad y
   efectos"): ESTÁNDAR para todo elemento futuro con este tipo de
   transición. La vista saliente recibe .swap-izq o .swap-der (según la
   dirección), al terminar se le pone [hidden]; la entrante parte con la
   clase del lado contrario (sin transición, forzando reflow) y se le
   retira para animar a neutro. El JS que oculta al terminar debe usar
   220 ms (calzar con la duración de aquí). Solo anima opacity/transform/
   filter (GPU, 60 fps). Referencia viva: inventario.html (cambiarVista). */
.vista-swap {
  transition: opacity .22s var(--ease), transform .22s var(--ease), filter .22s var(--ease);
}
.vista-swap[hidden] { display: none !important; }
.vista-swap.swap-izq { opacity: 0; transform: translateX(-18px); filter: blur(4px); pointer-events: none; }
.vista-swap.swap-der { opacity: 0; transform: translateX(18px); filter: blur(4px); pointer-events: none; }
/* Variante para ACCIONES contextuales (FIX 2026-07-03, regla del usuario):
   los botones NO se desplazan — solo fade in/out EN SU LUGAR (el
   translateX del saliente reducía el ancho ocupado y el entrante caía de
   línea). El contenido (listas) mantiene el deslizamiento de arriba.
   Mismo JS: acepta swap-izq/swap-der pero aquí solo animan opacidad. */
.vista-fade { transition: opacity .22s var(--ease); }
/* FIX 2026-07-03 (raíz del salto reportado): el grupo oculto NO sale del
   layout — visibility en vez de display:none. Así la celda de grid donde
   se apilan las acciones SIEMPRE mide como el grupo más ancho y el
   contenedor nunca cambia de línea al cruzar (a anchos medios, el grupo
   ancho envolvía bajo el título y el angosto no → el contenedor entero
   saltaba 254↔174px). Reproducido y verificado a 1150px. */
.vista-fade[hidden] { display: flex !important; visibility: hidden !important; pointer-events: none; }
.vista-fade.swap-izq, .vista-fade.swap-der { opacity: 0; pointer-events: none; }

/* ── Despliegue animado (expansión de detalle en listas) ───────────
   NUEVO 2026-07-05 — pedido del usuario: el detalle de una fila (p. ej.
   artículos de una bodega) se despliega fluido en vez de saltar. Patrón
   grid-template-rows 0fr→1fr (anima altura auto sin medir con JS).
   Uso: <div class="despliegue [abierto]"><div class="despliegue-inner">…
   El JS agrega .abierto en un rAF tras render (para que la transición
   corra) y al cerrar la quita y re-renderiza tras ~300ms. */
.despliegue {
  display: grid; grid-template-rows: 0fr;
  opacity: 0;
  transition: grid-template-rows .28s var(--ease), opacity .22s var(--ease);
}
.despliegue.abierto { grid-template-rows: 1fr; opacity: 1; }
.despliegue > .despliegue-inner { overflow: hidden; min-height: 0; }

/* ── Menú de módulos (filas de opción) ──────────────────────────────
   NUEVO 2026-07-06 — rediseño aprobado por el usuario en el hub de
   Insumos y replicado a todos los hubs: las cards verticales de
   submódulos se leían igual que los KPIs del dashboard y todo se
   mezclaba. La navegación ahora son FILAS DE OPCIÓN — horizontales,
   superficie más sólida y radio menor que las cards de datos, icono en
   cápsula + chevron. Estructura:
     .menu-list > a.menu-item > .mi-icon(svg) + .mi-text(.mi-title +
     .mi-desc) + .mi-chev(svg chevron) — deshabilitado: div.menu-item
     .disabled con .coming-badge en lugar del chevron. */
.menu-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); gap: 12px; }
.menu-item {
  display: flex; align-items: center; gap: 16px;
  padding: 15px 18px; text-decoration: none; cursor: pointer;
  background: rgba(255, 255, 255, .72);
  border: 1px solid rgba(255, 255, 255, .7);
  border-radius: 16px;
  box-shadow: 0 1px 2px rgba(39, 39, 39, .04);
  transition: background .22s var(--ease), border-color .22s var(--ease),
              transform .22s var(--ease);
}
.menu-item:hover { background: rgba(255, 255, 255, .94); border-color: rgba(39, 39, 39, .12); }
.menu-item:hover .mi-chev svg { transform: translateX(3px); }
.menu-item:hover .mi-icon { background: var(--ink); color: #fff; }
.mi-icon {
  flex: none; width: 42px; height: 42px; border-radius: 12px;
  display: grid; place-items: center;
  position: relative; overflow: hidden;   /* contiene el destello */
  background: rgba(39, 39, 39, .06); color: var(--ink-soft);
  transition: background .22s var(--ease), color .22s var(--ease);
}
.mi-icon svg { width: 21px; height: 21px; stroke-width: 1.6; stroke: currentColor; fill: none; stroke-linecap: round; stroke-linejoin: round; position: relative; }
/* Destello en la cápsula cuando se pone negra (pedido del usuario
   2026-07-06): mismo timing del estándar de botones oscuros (btn-shine
   0.9s ease-in-out en loop, aparición instantánea).
   FIX 2026-07-07 — la banda del gradiente es RELATIVA AL ANCHO del
   elemento: los stops de los botones (37%→63%) en una cápsula de 42px
   daban ~11px de banda — se seguía viendo delgada tras el primer
   ensanche. En elementos chicos los stops se abren mucho más
   (28%→72% ≈ 18px) para que el barrido se perciba igual de grueso. */
/* FIX 2026-07-07 (iteración 2, verificada con capturas cuadro a cuadro) —
   abrir los stops SOLO diluía la luz: banda ancha pero tenue, se seguía
   viendo "delgada". La percepción de grosor necesita un NÚCLEO brillante
   (.55) con hombros suaves — un brillo definido, no un velo. */
/* FIX 2026-07-07 (iteración 3) — el núcleo era un pico que tocaba el
   máximo solo en el 50%: seguía leyéndose delgado. Ahora es una MESETA
   (brillo pleno sostenido del 42% al 58% ≈ un tercio de la cápsula). */
.mi-icon::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(105deg,
    transparent 24%, rgba(255, 255, 255, .20) 34%,
    rgba(255, 255, 255, .55) 42%, rgba(255, 255, 255, .55) 58%,
    rgba(255, 255, 255, .20) 66%, transparent 76%);
  transform: translateX(-130%);
}
.menu-item:not(.disabled):hover .mi-icon::after {
  animation: btn-shine 1.7s ease-in-out infinite;
}
.mi-text { min-width: 0; }
.mi-title { display: block; font-size: 14.5px; font-weight: 600; color: var(--ink); }
.mi-desc {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  font-size: 12px; color: var(--muted); line-height: 1.4; margin-top: 2px;
  overflow: hidden;
}
.mi-chev { flex: none; margin-left: auto; line-height: 0; color: var(--muted-2); }
.mi-chev svg { width: 16px; height: 16px; stroke-width: 2; stroke: currentColor; fill: none; stroke-linecap: round; stroke-linejoin: round; transition: transform .22s var(--ease); }
.menu-item.disabled { opacity: .55; pointer-events: none; cursor: default; }
.menu-item .coming-badge {
  flex: none; margin-left: auto;
  font-size: 10.5px; font-weight: 600; color: var(--muted-2);
  background: rgba(39, 39, 39, .06); padding: 3px 10px; border-radius: var(--r-pill);
}

/* ── Foco visible ──────────────────────────────────────────────────── */
button:focus-visible, a:focus-visible, [tabindex]:focus-visible {
  outline: 2px solid rgba(60, 145, 230, .55); outline-offset: 2px;
}
.form-input:focus-visible, .form-select:focus-visible { outline: none; }

/* ── Revelado de entrada ───────────────────────────────────────────
   Al final del stylesheet a propósito: mientras el elemento tiene la
   clase .reveal esta transición debe ganarle a la de .card (hover) —
   a igual especificidad decide el orden. GlassUI retira .reveal al
   terminar la entrada. */
/* FIX 2026-07-03 — coreografía de entrada acelerada para el día a día
   (pedido del usuario: "más fluido, tipo 60 fps"): duraciones .45/.5s
   (antes .8/.9s) y los retardos escalonados de TODAS las páginas se
   escalan a la mitad con calc() — una sola palanca, sin tocar los --d
   inline de cada página. */
.reveal { opacity: 0; transform: translateY(18px); filter: blur(6px); transition: opacity .45s var(--ease), transform .5s var(--ease), filter .45s var(--ease); transition-delay: calc(var(--d, 0ms) * 0.5); }
.reveal.in { opacity: 1; transform: none; filter: none; }

/* ── Responsive ────────────────────────────────────────────────────── */
@media (max-width: 1020px) {
  .navpills { display: none; }
  .only-mobile { display: block; }
  .gtop .only-mobile { margin-left: auto; }
  .page-head { align-items: flex-start; flex-direction: column; }
}
@media (max-width: 700px) {
  main, .page, .page-wide { padding: 14px 18px 56px; }
}

/* ── Modo ligero (POS / hardware modesto) ──────────────────────────
   Mantiene el lenguaje (greige, radios, tipografía, acentos) pero
   elimina el costo GPU: sin blur real ni brillos animados. */
.glass-lite .ambient { display: none; }
.glass-lite .card, .glass-lite .table-wrap, .glass-lite .empty-state,
.glass-lite .navpills, .glass-lite .icirc, .glass-lite .btn, .glass-lite .qbtn,
.glass-lite .pop, .glass-lite .modal-card, .glass-lite .modal-backdrop {
  backdrop-filter: none; -webkit-backdrop-filter: none;
}
.glass-lite .card, .glass-lite .table-wrap, .glass-lite .empty-state { background: rgba(255, 255, 255, .82); }
.glass-lite .pop, .glass-lite .modal-card { background: #fdfdfc; }
/* FIX 2026-08-11 (reporte del usuario, tablet en modo oscuro): las
   superficies sólidas de glass-lite quedaron SOLO con su valor claro —
   cualquier .card del theme salía blanca lavada sobre el lienzo oscuro
   (se vio al llevar las cards de dashboard de la consola al módulo
   Insumos de la tablet). Equivalentes sólidos de los tokens dark. */
[data-theme="dark"] .glass-lite .card,
[data-theme="dark"] .glass-lite .table-wrap,
[data-theme="dark"] .glass-lite .empty-state { background: rgba(26, 26, 26, .96); }
[data-theme="dark"] .glass-lite .pop,
[data-theme="dark"] .glass-lite .modal-card { background: #161616; }

/* ── MODO OSCURO — fase 1: tokens duales (NUEVO 2026-07-23) ─────────
   Pedido del usuario: claro/oscuro en todo el sitio. El atributo
   [data-theme="dark"] en <html> (lo pone GlassUI leyendo la MISMA clave
   localStorage del POS, ttpos_pos_theme) redefine la paleta y ajusta las
   superficies con blancos quemados. Fase 2 (2026-07-29, ver más abajo)
   ya barre superficies genéricas. Los impresos (PDF) no cambian. */
[data-theme="dark"] {
  color-scheme: dark;
  --canvas: #0d0d0d;
  --ink: #f4f4f2;
  --ink-soft: #cfcfca;
  --muted: #8f8f88;
  --muted-2: #6a6a63;
  --hairline: rgba(255, 255, 255, .09);
  --glass: rgba(26, 26, 26, .92);
  --glass-strong: rgba(32, 32, 32, .96);
  --glass-border: rgba(255, 255, 255, .10);
  --shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 14px 34px rgba(0, 0, 0, .5);
  --shadow-hover: 0 2px 4px rgba(0, 0, 0, .45), 0 22px 48px rgba(0, 0, 0, .6);
  --shadow-pop: 0 2px 6px rgba(0, 0, 0, .5), 0 26px 64px rgba(0, 0, 0, .7);
  --color-focus-ring: rgba(96, 165, 235, .5);
  --color-bg-alt: rgba(255, 255, 255, .06);
  --color-border: rgba(255, 255, 255, .14);
  --color-border-light: var(--hairline);
}
/* Superficies con blanco quemado (se llevan a tokens en la fase 2). */
[data-theme="dark"] .form-input, [data-theme="dark"] .form-select,
[data-theme="dark"] .field input, [data-theme="dark"] .field select,
[data-theme="dark"] .field textarea, [data-theme="dark"] textarea.form-input,
[data-theme="dark"] .filter-select, [data-theme="dark"] .filter-input {
  background: rgba(255, 255, 255, .07);
  border-color: rgba(255, 255, 255, .12);
}
[data-theme="dark"] .form-input:hover, [data-theme="dark"] .form-select:hover { background: rgba(255, 255, 255, .10); }
[data-theme="dark"] .form-input:focus, [data-theme="dark"] .form-select:focus,
[data-theme="dark"] .field input:focus, [data-theme="dark"] .field select:focus,
[data-theme="dark"] .field textarea:focus { background: #242422; }
/* FIX 2026-07-29 — modo oscuro fase 2, barrido sistemático (reporte del
   usuario con captura: el campo "cantidad" de Solicitud de pedidos y luego
   el mismo defecto en la columna "Aprobar" de Gestión de pedidos quedaban
   con fondo claro y texto claro, ilegibles).
   Historial: cada página de Distribución (distribucion_pedidos.html,
   distribucion_aprobacion.html, distribucion_reportes.html,
   distribucion_despachos.html, distribucion_recepcion.html) más
   insumos_productos.html y toma_inventario.html definen su propia clase de
   input numérico grande (.sol-input, .cant-input, etc.) con
   `background: rgba(255,255,255,.9)` fijo — quedan afuera de las reglas de
   arriba porque no usan .form-input/.field.
   Causa raíz: el parche de fase 1 solo cubría las clases "oficiales" del
   contrato de formularios; cualquier <input>/<select>/<textarea> con clase
   propia de página quedaba con el blanco de la maqueta original.
   Solución de raíz (no por página) — INTENTO 1: regla genérica POR TIPO DE
   ELEMENTO con especificidad (0,1,1), pensada para ganarle a una clase de
   página de menor especificidad (0,1,0) sin usar !important.
   FIX 2026-07-30 — ese intento 1 no alcanzaba (reporte del usuario con
   captura: "Emitir factura" seguía con la tabla de ítems en gris claro
   sólido en oscuro, texto claro encima, ilegible).
   Historial: en la sesión 2026-07-29 se retiraron por "duplicados" los
   overrides oscuros locales de `.items-table input` (facturacion_emision.
   html), `.modal textarea` (facturacion_recibidas.html) y `.eleg-num
   input` (inventario.html), confiando en que la regla genérica de arriba
   ya alcanzaba. `caf.html` (`.tramo-row-edit select/input`,
   `.upload-extra input[type="number"]`, `.modal-card input[type="number"]`)
   nunca tuvo override oscuro.
   Causa raíz: estas páginas fijan el fondo claro en una regla SIN scope de
   tema (aplica en claro Y oscuro) cuyo selector tiene especificidad IGUAL
   (0,1,1) o MAYOR (0,2,1, por el atributo `[type="number"]`) a la de la
   red de seguridad. En un empate de especificidad gana la regla que
   aparece más abajo en la cascada — y el <style> de cada página siempre
   carga después del <link> a este archivo — así que la regla de la
   página seguía ganando en los empates, y en `caf.html` ganaba directo
   por especificidad mayor. Además `facturacion_emision.html` fija el
   fondo del selector de giro (`#em-giro-sel`) por ATRIBUTO `style=""`
   inline, cuya especificidad ningún selector de hoja de estilos puede
   igualar. No existe un techo de especificidad "razonable" que garantice
   ganarle SIEMPRE a cualquier página presente y futura sin tocarla.
   Solución de raíz definitiva: `!important` en fondo/borde/color, acotado
   estrictamente a `[data-theme="dark"]`. Es la opción correcta aquí (y no
   un parche) porque: (a) un `!important` de hoja de estilos SÍ le gana a
   un `style=""` inline sin `!important` (la importancia se decide ANTES
   que la especificidad en la cascada, y un inline normal es especificidad
   alta pero sigue siendo "normal", no "important"); (b) es la única forma
   de dar la garantía que pidió el usuario ("ningún campo nuevo debe volver
   a quedar con fondo claro en oscuro sin tocar este archivo"), sin
   importar cuántas clases o atributos use el selector de una página nueva
   ni el orden de carga de sus hojas de estilo.
   Para no romper la única excepción legítima y documentada que SÍ debe
   seguir ganando (pos_venta.html define su propia paleta oscura
   --pos-input/--pos-hover/--pos-elevated vía `html[data-theme="dark"]
   .form-input`, sin !important), esta red de seguridad EXCLUYE con
   :where() (especificidad cero) a los elementos que ya llevan las clases
   "oficiales" del contrato de formularios (.form-input/.form-select/
   .filter-input/.filter-select) — para esos, el mecanismo de especificidad
   documentado más abajo se mantiene intacto y una página puede seguir
   customizándolos. También excluye `:disabled` y `[readonly]`: ya tienen
   (o pueden tener, en el caso de readonly) tratamiento propio de menor
   énfasis en varias páginas que no es el defecto reportado (texto sobre
   fondo claro sólido) y no se auditó su compatibilidad con !important. */
[data-theme="dark"] input:where(
    :not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]):not(:disabled):not([readonly]):not(.form-input):not(.filter-input)
  ),
[data-theme="dark"] select:where(:not(:disabled):not(.form-select):not(.filter-select)),
[data-theme="dark"] textarea:where(:not(:disabled):not([readonly]):not(.form-input)) {
  background: rgba(255, 255, 255, .07) !important;
  border-color: rgba(255, 255, 255, .12) !important;
  color: var(--ink) !important;
}
[data-theme="dark"] input:where(:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]):not(:disabled):not([readonly]):not(.form-input):not(.filter-input)):hover,
[data-theme="dark"] select:where(:not(:disabled):not(.form-select):not(.filter-select)):hover {
  background: rgba(255, 255, 255, .10) !important;
}
[data-theme="dark"] input:where(:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]):not(:disabled):not([readonly]):not(.form-input):not(.filter-input)):focus,
[data-theme="dark"] select:where(:not(:disabled):not(.form-select):not(.filter-select)):focus,
[data-theme="dark"] textarea:where(:not(:disabled):not([readonly]):not(.form-input)):focus {
  background: #242422 !important;
}
[data-theme="dark"] input::placeholder, [data-theme="dark"] textarea::placeholder { color: var(--muted-2); opacity: 1; }
[data-theme="dark"] input:disabled, [data-theme="dark"] select:disabled, [data-theme="dark"] textarea:disabled {
  opacity: .55; background: var(--color-bg-alt);
}
/* Chips/badges de superficie neutra: usaban rgba(39,39,39,x) (tinte OSCURO
   pensado para lienzo claro) — sobre el canvas casi negro de este tema
   quedan invisibles como pastilla (el texto se sigue leyendo, pero pierde
   la superficie). Se reemplazan por el equivalente claro sobre oscuro. */
[data-theme="dark"] .gside .sitem:hover,
[data-theme="dark"] .gside .soon-tag,
[data-theme="dark"] .mitem.on,
[data-theme="dark"] .mitem .soon-tag,
[data-theme="dark"] .toggle,
[data-theme="dark"] .badge-plan,
[data-theme="dark"] .menu-item .coming-badge,
[data-theme="dark"] .mi-icon {
  background: rgba(255, 255, 255, .08);
}
[data-theme="dark"] .modal-card { background: rgba(34, 34, 32, .96); }
[data-theme="dark"] .pop { background: rgba(34, 34, 32, .97); }
[data-theme="dark"] .notif:hover, [data-theme="dark"] .mitem:hover { background: rgba(255, 255, 255, .07); }
/* FIX 2026-07-23 — badges de estado apagados en dark: los tonos -deep
   (pensados para fondo claro) no contrastan sobre las superficies oscuras.
   En dark el texto usa el tono brillante de la paleta. */
[data-theme="dark"] .badge.ok, [data-theme="dark"] .badge-produccion { color: var(--green); }
[data-theme="dark"] .badge.proc, [data-theme="dark"] .badge-proceso { color: var(--blue); }
[data-theme="dark"] .badge.no, [data-theme="dark"] .badge-pendiente { color: var(--red); }
[data-theme="dark"] tbody tr:hover { background: rgba(255, 255, 255, .04); }
[data-theme="dark"] .toggle .tthumb { background: #3c3c3a; }
[data-theme="dark"] .gside-toggle { background: rgba(255, 255, 255, .08); }
[data-theme="dark"] .icirc .dot { border-color: #222; }
[data-theme="dark"] .menu-item { background: rgba(34, 34, 32, .85); border-color: rgba(255, 255, 255, .08); }
[data-theme="dark"] .menu-item:hover { background: rgba(48, 48, 46, .95); border-color: rgba(255, 255, 255, .16); }
/* Elementos "tinta sólida": en oscuro --ink es claro → el texto blanco
   fijo se invierte a oscuro para mantener el contraste. */
[data-theme="dark"] .btn-primary, [data-theme="dark"] .qbtn.dark,
[data-theme="dark"] .menu-item:hover .mi-icon { color: #141414; }
/* FIX 2026-07-23 (pedido del usuario) — los iconos LIMA de los botones
   tinta casi no se ven en oscuro (el botón se invierte a superficie
   clara): pasan al par oscuro del lima (--lime-deep). */
[data-theme="dark"] .btn-primary svg, [data-theme="dark"] .qbtn.dark svg { stroke: var(--lime-deep); }
[data-theme="dark"] .navpills a.active, [data-theme="dark"] .gside .sitem.active { color: #141414; }
/* FIX 2026-07-31 — reporte del usuario con captura: al pasar el mouse
   sobre el ítem ACTIVO del panel lateral (p.ej. "Facturación" estando ya
   en Facturación) en modo oscuro, el texto quedaba gris oscuro sobre un
   fondo gris — casi ilegible.
   Historial: `.gside .sitem.active` es un patrón "tinta sólida"
   (background: var(--ink), línea 326) que la regla de arriba invierte en
   oscuro fijando `color: #141414` porque --ink pasa a ser claro. Pero el
   estado combinado activo+hover no tenía regla propia: el color lo fijaba
   `.gside .sitem.active:hover { color: #fff }` (base, más arriba en el
   archivo) empatado en especificidad (0,4,0) con el override oscuro de la
   línea anterior — el empate lo gana la regla que aparece más abajo en la
   cascada (esta línea), así que el color SÍ quedaba en #141414. El
   problema real era el FONDO: en ese mismo estado ganaba
   `[data-theme="dark"] .gside .sitem:hover` (0,4,0 también, por el
   :hover extra que no tiene `.active`) sobre `.gside .sitem.active`
   (0,3,0, sin :hover) — es decir la píldora sólida clara del activo
   desaparecía al hacer hover y quedaba el overlay translúcido oscuro del
   hover genérico no-activo, mientras el texto seguía forzado a #141414
   por la regla de arriba → texto oscuro sobre fondo oscuro.
   Causa raíz: ninguna regla modelaba el estado combinado activo+hover en
   oscuro; fondo y color se resolvían por separado desde reglas pensadas
   para otros estados.
   Solución: regla explícita para `.gside .sitem.active:hover` en oscuro
   con especificidad (0,5,0) — mayor que cualquiera de las reglas en
   conflicto — que reafirma la píldora sólida (var(--ink), clara en
   oscuro) y el texto oscuro (#141414), igual que el activo en reposo. */
[data-theme="dark"] .gside .sitem.active:hover { background: var(--ink); color: #141414; }
/* Patrón "tinta sólida" repetido en las páginas (background: var(--ink) +
   color #fff): en oscuro --ink es claro → el texto pasa a tinta oscura.
   Clases por página: filter-pill/badge-dark (usuarios), sec-pill/filter-tab
   (finanzas), tipo-dte-btn/btn-dark (emisión), fam-item (maestra).
   FIX 2026-07-29 — modo oscuro fase 2 (barrido sistemático): se agrega
   .orden-pill.active (reportes_personal.html), que tenía el mismo patrón
   sin cubrir — quedaba con botón activo por defecto en texto blanco sobre
   fondo claro invertido, ilegible desde la primera carga de la página. */
[data-theme="dark"] .filter-pill.active, [data-theme="dark"] .badge-dark,
[data-theme="dark"] .sec-pill.active, [data-theme="dark"] .filter-tab.active,
[data-theme="dark"] .tipo-dte-btn.active, [data-theme="dark"] .btn-dark,
[data-theme="dark"] .fam-item.active, [data-theme="dark"] .orden-pill.active { color: #141414; }
[data-theme="dark"] .rol-chip { background: rgba(255, 255, 255, .09); }
[data-theme="dark"] .avatar { color: #5a6414; }
/* Logos Nanuc/Crumb (tinta negra) → blanco. Provisional con invert();
   cuando exista la variante blanca del asset se reemplaza. */
[data-theme="dark"] img[src$="nanuc.svg"],
[data-theme="dark"] img[src$="nanuc-iso.svg"],
[data-theme="dark"] img[src$="logocrumbpos.svg"] { filter: invert(1); }

/* Los IMPRESOS no conocen el modo oscuro (regla del usuario 2026-07-23):
   ante cualquier window.print(), los tokens vuelven a la paleta clara y
   los logos dejan de invertirse. Los PDFs generados en el servidor nunca
   se ven afectados por el theme. */
@media print {
  [data-theme="dark"] {
    --canvas: #ffffff;
    --ink: #272727;
    --ink-soft: #4a4a46;
    --muted: #82827c;
    --muted-2: #a6a69f;
    --hairline: rgba(39, 39, 39, .08);
    --glass: rgba(255, 255, 255, .55);
    --glass-strong: rgba(255, 255, 255, .78);
    --glass-border: rgba(255, 255, 255, .65);
  }
  [data-theme="dark"] img[src$="nanuc.svg"],
  [data-theme="dark"] img[src$="nanuc-iso.svg"],
  [data-theme="dark"] img[src$="logocrumbpos.svg"] { filter: none; }
}

/* ── Movimiento reducido ───────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
  .reveal { transition: none; opacity: 1; transform: none; filter: none; }
  .ambient i { animation: none; }
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ══════════════════════════════════════════════════════════════════════
   TRANSITIONS.DEV (skill instalada 2026-08-10, pedido del usuario:
   animaciones canónicas para TODO el software). Piezas adaptadas del
   catálogo t-* a los tokens del theme; la guarda global de
   prefers-reduced-motion de arriba (transition/animation .01ms
   !important) cubre todo este bloque, como exige la skill.
   ══════════════════════════════════════════════════════════════════════ */
:root {
  /* Tokens de movimiento de la skill (solo los que usan estas piezas) */
  --duration-fast: 250ms;
  --ease-smooth-out: cubic-bezier(0.22, 1, 0.36, 1);
  /* 25 · Checkbox check */
  --check-box: 150ms; --check-draw: 350ms; --check-delay: 100ms;
  --check-uncheck: 150ms; --check-ease: cubic-bezier(0.22, 1, 0.36, 1);
  /* 27 · Toggle (overshoot con un rebote vía ease, sin keyframes:
     evita el replay en el primer paint de un switch ya encendido) */
  --toggle-dur: 350ms; --toggle-travel: 18px;
  --toggle-ease: cubic-bezier(0.34, 1.35, 0.64, 1);
  /* 03 · Notification badge */
  --badge-pop-dur: 500ms; --badge-blur: 2px;
  --badge-pop-ease: cubic-bezier(0.34, 1.36, 0.64, 1);
  /* 04 · Text states swap */
  --text-swap-dur: 150ms; --text-swap-translate-y: 4px;
  --text-swap-blur: 2px; --text-swap-ease: ease-in-out;
  /* 02 · Number pop-in */
  --digit-dur: 500ms; --digit-distance: 8px; --digit-stagger: 70ms;
  --digit-blur: 2px; --digit-ease: cubic-bezier(0.34, 1.45, 0.64, 1);
  /* 09 · Icon swap */
  --icon-swap-dur: 250ms; --icon-swap-blur: 2px;
  --icon-swap-start-scale: 0.25; --icon-swap-ease: ease-in-out;
  /* 10 · Success check */
  --sc-opacity-dur: 500ms; --sc-rotate-dur: 500ms; --sc-rotate-from: 80deg;
  --sc-bob-dur: 500ms; --sc-y-amount: 40px; --sc-blur-dur: 500ms;
  --sc-blur-from: 10px; --sc-path-dur: 500ms; --sc-path-delay: 80ms;
  --sc-ease-out: cubic-bezier(0.22, 1, 0.36, 1);
}

/* ── 25 · CHECKBOX CANÓNICO (upgrade CSS puro sobre el input nativo:
   cero cambios de markup en las ~13 páginas que ya los usan; :checked e
   :indeterminate reaccionan también a cambios programáticos, que es
   como cargan estado varias páginas de config). La caja pinta primero
   (--check-box) y el visto se DIBUJA después en diagonal vía clip-path
   (--check-draw + delay), replicando el stroke-draw de la skill sin
   inyectar SVG. Los inputs del switch (.toggle-switch) quedan fuera.
   Opt-out puntual: data-no-anim. ─────────────────────────────────── */
input[type="checkbox"]:not(:is(.toggle-switch input, [data-no-anim])) {
  appearance: none; -webkit-appearance: none;
  width: 16px; height: 16px; flex: none;
  border: 1.5px solid var(--muted); border-radius: 4px;
  background: transparent; cursor: pointer;
  position: relative; display: inline-block; vertical-align: -3px;
  transition: background var(--check-box) var(--check-ease),
              border-color var(--check-box) var(--check-ease);
}
input[type="checkbox"]:not(:is(.toggle-switch input, [data-no-anim]))::after {
  /* Medidas RELATIVAS a la caja: varias páginas fijan 15/16/22px al
     input (cierre del POS usa 22px) y el visto debe escalar con él. */
  content: ''; position: absolute; left: 26%; top: 6%;
  width: 29%; height: 54%;
  border-right: 1.7px solid var(--canvas);
  border-bottom: 1.7px solid var(--canvas);
  transform: rotate(43deg); opacity: 0;
  clip-path: inset(-15% 100% -15% -15%);
  transition: clip-path var(--check-uncheck) var(--check-ease),
              opacity 0ms linear var(--check-uncheck);
}
input[type="checkbox"]:not(:is(.toggle-switch input, [data-no-anim])):checked {
  background: var(--ink); border-color: var(--ink);
}
input[type="checkbox"]:not(:is(.toggle-switch input, [data-no-anim])):checked::after {
  opacity: 1; clip-path: inset(-15% -15% -15% -15%);
  transition: clip-path var(--check-draw) var(--check-ease) var(--check-delay),
              opacity 0ms linear var(--check-delay);
}
/* Tri-estado (matriz de permisos): guion centrado en vez de visto. */
input[type="checkbox"]:not(:is(.toggle-switch input, [data-no-anim])):indeterminate {
  background: var(--ink); border-color: var(--ink);
}
input[type="checkbox"]:not(:is(.toggle-switch input, [data-no-anim])):indeterminate::after {
  opacity: 1; left: 20%; top: calc(50% - 1px); width: 54%; height: 0;
  border-right: 0; border-bottom: 1.7px solid var(--canvas);
  transform: none; clip-path: none; transition: opacity var(--check-box) var(--check-ease);
}
input[type="checkbox"]:not(:is(.toggle-switch input, [data-no-anim])):focus-visible {
  outline: 2px solid var(--blue); outline-offset: 2px;
}
input[type="checkbox"]:not(:is(.toggle-switch input, [data-no-anim])):disabled {
  opacity: .45; cursor: default;
}

/* ── 27 · TOGGLE (switch booleano canónico — antes vivía duplicado en
   pos_configuracion/pos_integraciones; el viaje del pulgar gana el
   overshoot con rebote de la skill). Markup canónico:
   <label class="toggle-switch"><input type="checkbox"><span class="toggle-slider"></span></label> */
.toggle-switch { position: relative; width: 40px; height: 22px; flex: none; display: inline-block; }
.toggle-switch input { opacity: 0; width: 0; height: 0; position: absolute; }
.toggle-slider {
  position: absolute; inset: 0; border-radius: var(--r-pill);
  background: var(--hairline); cursor: pointer;
  transition: background var(--toggle-dur) var(--ease);
}
.toggle-slider::after {
  content: ''; position: absolute; left: 3px; top: 3px;
  width: 16px; height: 16px; border-radius: 50%;
  background: #fff; box-shadow: 0 1px 3px rgba(0, 0, 0, .25);
  translate: 0 0; will-change: translate;
  transition: translate var(--toggle-dur) var(--toggle-ease);
}
.toggle-switch input:checked + .toggle-slider { background: var(--blue); }
.toggle-switch input:checked + .toggle-slider::after { translate: var(--toggle-travel) 0; }
.toggle-switch input:focus-visible + .toggle-slider { outline: 2px solid var(--blue); outline-offset: 2px; }

/* ── 16 · TABS SLIDING — el segmentado del theme (.toggle + .tthumb) ya
   desliza; se alinea al token de la skill para tabs (250ms smooth-out
   en vez del spring de 400ms). ─────────────────────────────────────── */
.toggle .tthumb {
  transition: left var(--duration-fast) var(--ease-smooth-out),
              width var(--duration-fast) var(--ease-smooth-out);
}

/* ── 03 · NOTIFICATION BADGE — pop del punto rojo de la campana al
   aparecer (scale 0 → overshoot con blur). `.t-pop` es la clase
   genérica re-disparable (globito de mesas del POS, contadores). ──── */
@keyframes t-badge-pop {
  from { transform: scale(0); opacity: 0; filter: blur(var(--badge-blur)); }
  to   { transform: scale(1); opacity: 1; filter: blur(0); }
}
.icirc .dot { animation: t-badge-pop var(--badge-pop-dur) var(--badge-pop-ease); }
.t-pop { animation: t-badge-pop var(--badge-pop-dur) var(--badge-pop-ease); }

/* ── 04 · TEXT STATES SWAP (verbatim de la skill) ───────────────────── */
.t-text-swap {
  display: inline-block;
  transform: translateY(0); filter: blur(0); opacity: 1;
  transition: transform var(--text-swap-dur) var(--text-swap-ease),
              filter var(--text-swap-dur) var(--text-swap-ease),
              opacity var(--text-swap-dur) var(--text-swap-ease);
  will-change: transform, filter, opacity;
}
.t-text-swap.is-exit {
  transform: translateY(calc(var(--text-swap-translate-y) * -1));
  filter: blur(var(--text-swap-blur)); opacity: 0;
}
.t-text-swap.is-enter-start {
  transform: translateY(var(--text-swap-translate-y));
  filter: blur(var(--text-swap-blur)); opacity: 0;
  transition: none;
}

/* ── 02 · NUMBER POP-IN (verbatim; lo orquesta GlassUI.numeroPop) ──── */
@keyframes t-digit-pop-in {
  0% { transform: translateY(var(--digit-distance)); opacity: 0; filter: blur(var(--digit-blur)); }
  100% { transform: translateY(0); opacity: 1; filter: blur(0); }
}
.t-digit-group { display: inline-flex; align-items: baseline; }
.t-digit { display: inline-block; will-change: transform, opacity, filter; }
.t-digit-group.is-animating .t-digit { animation: t-digit-pop-in var(--digit-dur) var(--digit-ease) both; }
.t-digit-group.is-animating .t-digit[data-stagger="1"] { animation-delay: var(--digit-stagger); }
.t-digit-group.is-animating .t-digit[data-stagger="2"] { animation-delay: calc(var(--digit-stagger) * 2); }

/* ── 09 · ICON SWAP (verbatim; lo usa el botón de tema sol/luna) ───── */
.t-icon-swap { position: relative; display: inline-grid; }
.t-icon-swap .t-icon {
  grid-area: 1 / 1;
  transition: opacity var(--icon-swap-dur) var(--icon-swap-ease),
              filter var(--icon-swap-dur) var(--icon-swap-ease),
              transform var(--icon-swap-dur) var(--icon-swap-ease);
  will-change: opacity, filter, transform;
}
.t-icon-swap[data-state="a"] .t-icon[data-icon="a"],
.t-icon-swap[data-state="b"] .t-icon[data-icon="b"] {
  opacity: 1; filter: blur(0); transform: scale(1);
}
.t-icon-swap[data-state="a"] .t-icon[data-icon="b"],
.t-icon-swap[data-state="b"] .t-icon[data-icon="a"] {
  opacity: 0; filter: blur(var(--icon-swap-blur));
  transform: scale(var(--icon-swap-start-scale));
}

/* ── 10 · SUCCESS CHECK (verbatim con prefijo --sc-; el largo del path
   se calibra en JS con getTotalLength, regla dura de la skill) ─────── */
.t-success-check {
  display: inline-block; transform-origin: center; opacity: 0;
  will-change: transform, opacity, filter;
}
.t-success-check svg { display: block; overflow: visible; }
.t-success-check svg path { stroke-dasharray: 20; stroke-dashoffset: 20; }
.t-success-check[data-state="in"] {
  animation: t-sc-fade var(--sc-opacity-dur) var(--sc-ease-out) forwards,
             t-sc-rotate var(--sc-rotate-dur) var(--sc-ease-out) forwards,
             t-sc-blur var(--sc-blur-dur) var(--sc-ease-out) forwards,
             t-sc-bob var(--sc-bob-dur) var(--sc-ease-out) forwards;
}
.t-success-check[data-state="in"] svg path {
  animation: t-sc-draw var(--sc-path-dur) var(--sc-ease-out) var(--sc-path-delay) forwards;
}
@keyframes t-sc-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes t-sc-rotate { from { transform: rotate(var(--sc-rotate-from)); } to { transform: rotate(0deg); } }
@keyframes t-sc-blur { from { filter: blur(var(--sc-blur-from)); } to { filter: blur(0); } }
@keyframes t-sc-bob { from { translate: 0 var(--sc-y-amount); } to { translate: 0 0; } }
@keyframes t-sc-draw { to { stroke-dashoffset: 0; } }
/* Guarda extra de la skill para quien tenga reduce activo: el check
   aparece quieto y completo (la guarda global ya frena la animación). */
@media (prefers-reduced-motion: reduce) {
  .t-success-check { opacity: 1; }
  .t-success-check svg path { stroke-dashoffset: 0 !important; }
}

/* ── 09b · Flecha plegar/desplegar del panel lateral: el giro de 180°
   ya existía (body.gside-min la invierte) pero saltaba sin transición —
   ahora rota con el token del icon swap. ───────────────────────────── */
.gside-toggle svg { transition: transform var(--icon-swap-dur) var(--icon-swap-ease); }

/* ── 10b · SELLO DE RESULTADO (GlassUI.selloResultadoHtml/animarSello):
   círculo dibujándose + visto verde (éxito) o X roja (error). El aviso
   canónico lo integra con avisar({ sello: 'ok' | 'error' }). ────────── */
.sello-resultado.sello-ok svg { stroke: #29bf12; }
.sello-resultado.sello-error svg { stroke: var(--red); }
.sello-resultado .sello-circulo { opacity: .45; stroke-width: 2.6; }

/* ── SECTION-LABEL CANÓNICO (FIX 2026-08-10 — reclamo recurrente del
   usuario: "muy apretado, no hay aire entre títulos y elementos").
   La pieza se usaba en 34 páginas pero NUNCA estuvo en el theme: cada
   página la definía (o no — y quedaba pegada a las cards, como en
   Reportes → Ventas). Valores del consenso de las páginas que sí la
   definían, con el margen superior GENEROSO de la pasada de aire de
   Reportes (2026-07-24). Las definiciones locales existentes siguen
   ganando (su <style> carga después) — esto pone el piso para toda
   página presente y futura. ─────────────────────────────────────── */
.section-label {
  font-size: 13px; font-weight: 600; color: var(--muted);
  margin: 34px 0 14px;
}

/* ── OVERLAY DE PROCESO (GlassUI.procesando) — NUEVO 2026-08-11 (pedido
   del usuario: al emitir la guía de despacho "pasa un rato y uno no sabe
   si hay un error o qué"). Bloquea la pantalla con el modal canónico +
   anillo girando mientras corre una operación larga (emitir un DTE,
   generar un reporte). Cerrar SIEMPRE con .cerrar() en un finally. ── */
.proceso-spinner { width: 44px; height: 44px; margin: 0 auto 14px; display: block; animation: proceso-girar .9s linear infinite; }
.proceso-spinner circle {
  fill: none; stroke: var(--blue-deep); stroke-width: 3;
  stroke-linecap: round; stroke-dasharray: 90 150;
}
@keyframes proceso-girar { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .proceso-spinner { animation: proceso-girar 2.5s linear infinite; }
}

/* ── CHECK VERDE EN BANNERS DE ÉXITO — DIRECTRIZ 2026-08-11 (pedido del
   usuario: "cuando algo se envíe o se acepte debe salir el icono de
   check verde que tenemos para las ventas — para todo lo exitoso").
   Los diálogos modales de éxito usan el sello canónico (avisar/confirmar
   con sello:'ok'); los BANNERS verdes — la otra superficie de éxito, con
   45 páginas usándolos — reciben aquí un visto que entra con pop, sin
   tocar ninguna página: es la clase del theme. El color va inline en el
   data-URI (un background-image no hereda currentColor); mismo verde en
   light y dark (--green-deep se lee bien sobre ambos fondos del banner
   verdoso translúcido). ──────────────────────────────────────────────── */
.banner.success::before, .form-banner.success::before, .alert-success::before {
  content: ""; display: inline-block; width: 15px; height: 15px;
  margin-right: 8px; vertical-align: -2px;
  background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="%231f8c0e" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m8 12.5 2.7 2.7L16 9.5"/></svg>') center / contain no-repeat;
  animation: banner-check-pop .45s var(--ease-bounce, cubic-bezier(.34, 1.36, .64, 1)) both;
}
@keyframes banner-check-pop {
  from { opacity: 0; transform: scale(.4); }
  to   { opacity: 1; transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .banner.success::before, .form-banner.success::before, .alert-success::before { animation: none; }
}

/* FIX 2026-08-12 (reporte del usuario: al scrollear rápido "pega un salto
   y se auto-scrollea solo, aparece vacía la pantalla y se scrollea
   rápidamente" — en ambos sentidos, intermitente) — diagnóstico: el
   SCROLL ANCHORING del navegador pelea con los .reveal animados
   (opacity+transform al entrar al viewport): cuando el contenido de
   anclaje se desplaza por la animación, Chrome "compensa" moviendo el
   scroll solo. Se desactiva el anclaje global; este theme no lo
   necesita (nada inserta contenido sobre el viewport en frío).
   VIGILAR en las pruebas del usuario; si reaparece, el siguiente
   sospechoso es content-visibility de tablas largas. */
html { overflow-anchor: none; }

/* FIX 2026-08-12 (bug de scroll del usuario, confirmado con su video
   falla.mov) — lo que se veía era el REBOTE ELÁSTICO de macOS: al
   scrollear rápido deja pasar del final/inicio, muestra el vacío
   ("aparece vacía la pantalla") y resortea solo de vuelta ("se
   auto-scrollea"); con la sidebar fija el efecto se percibe doble.
   Pedido textual: "si se llega al final o a la parte superior ahí se
   termina no más el scroll". overscroll-behavior: none desactiva el
   rebote del scroller principal en Chromium/Safari 16+; el intento
   anterior (overflow-anchor: none, más arriba) atacaba otra causa y
   queda igual: ninguna de las dos features la necesita el theme. */
html, body { overscroll-behavior: none; }

/* ── BOTONERA DE MODAL SIEMPRE VISIBLE — DIRECTRIZ 2026-08-12 (pedido
   del usuario con capturas de "Mi cuenta": "los botones al final deben
   verse siempre en primer plano, no esperar a scrollear todo hacia
   abajo — aplica a todo el software"). La .modal-card ya scrollea
   (max-height 90vh); la .modal-footer pasa a STICKY dentro de ese
   scroll: en modales cortos queda donde siempre, y en modales más
   altos que la pantalla queda ANCLADA abajo con el contenido pasando
   por debajo (fondo sólido + hairline para que se lea como barra).
   Los offsets negativos compensan el padding de la card (30/26px)
   para que la barra llegue hasta los bordes reales. Cubre los 23
   archivos que usan .modal-footer y todo modal futuro, sin tocar
   ninguna página. ──────────────────────────────────────────────── */
.modal-footer {
  position: sticky; bottom: -26px; z-index: 3;
  margin-left: -30px; margin-right: -30px; margin-bottom: -26px;
  padding: 14px 30px 20px;
  background: rgba(255, 255, 255, .94);
  border-top: 1px solid var(--hairline);
  border-radius: 0 0 24px 24px;
}
[data-theme="dark"] .modal-footer { background: rgba(34, 34, 32, .97); }
[data-theme="dark"] .glass-lite .modal-footer { background: #161616; }
