@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=IBM+Plex+Mono:wght@500;600&display=swap');

/* ── Variables ─────────────────────────────────────────────────────── */
/* Paleta TIFlows POS -- valores exactos tomados de la landing de
   referencia (tiflows.com), pasada por José 2026-09-12. Mapeo de roles:
   --accent (marca, azul) = --brand de la landing; --success (secundario,
   de éxito) = --pos de la landing, ahora INDEPENDIENTE de --accent --
   antes de este cambio --success era un alias directo de --accent porque
   el teal viejo servía a la vez de marca y de éxito (ver comentario que
   reemplaza este bloque). --danger e --info no los redefine la landing
   (no trae rojo ni un azul "informativo" aparte del propio --brand) --
   se dejan con sus valores previos a propósito, sin inventar reemplazo. */
:root {
  color-scheme: dark; /* barras de scroll, calendario y controles nativos acordes al tema oscuro (default) */
  /* Backgrounds */
  --bg:            #0E1621;
  --bg-elevated:   #16202E;
  --bg-elevated-2: #1D2A3A;

  /* Borders */
  --border:        #243347;
  --border-strong: #31445C;

  /* Text */
  --text-primary:   #E8EEF5;
  --text-secondary: #9DB0C6;
  --text-muted:     #5F7288;

  /* Accent — azul (marca, "--brand" de la landing) */
  --accent:        #4C8BD9;
  --accent-hover:  #6BA0E0;
  --accent-dim:    rgba(76,139,217,0.14);
  --accent-border: rgba(76,139,217,0.28);

  /* Danger — sin cambios, la landing no define un rojo propio */
  --danger:        #F4574F;
  --danger-dim:    rgba(244,87,79,0.12);
  --danger-border: rgba(244,87,79,0.28);

  /* Warning — "--warn" de la landing */
  --warning:        #E0A93F;
  --warning-dim:    rgba(224,169,63,0.12);
  --warning-border: rgba(224,169,63,0.28);

  /* Info — azul paralelo (USDT, pendientes) -- sin cambios, la landing no
     define un segundo azul informativo distinto de --brand */
  --info:        #5B9EF6;
  --info-dim:    rgba(91,158,246,0.12);
  --info-border: rgba(91,158,246,0.28);

  /* Typography */
  --font-sans:    'Inter', system-ui, -apple-system, sans-serif;
  --font-heading: 'Inter', system-ui, sans-serif;
  --font-mono:    'IBM Plex Mono', 'Fira Code', monospace;

  /* Radii */
  --radius:    14px;
  --radius-sm:  9px;
  --radius-lg: 22px;

  /* Shadows */
  --shadow-card:  0 1px 2px rgba(0,0,0,0.2), 0 4px 16px rgba(0,0,0,0.28);
  --shadow-lg:    0 16px 48px rgba(0,0,0,0.5);
  --shadow-focus: 0 0 0 3px var(--accent-dim);

  /* ── Aliases de compatibilidad (los 7 HTML limpios usan estos nombres) */
  --bg-card:        var(--bg-elevated);
  --bg-input:       var(--bg-elevated-2);
  --text:           var(--text-primary);
  --border-focus:   var(--accent);
  --error:          var(--danger);
  --error-bg:       var(--danger-dim);
  --error-border:   var(--danger-border);
  /* --success ya NO es alias de --accent -- turquesa "--pos" de la landing,
     color de éxito independiente de la marca (azul) */
  --success:        #2AB99A;
  --success-bg:     rgba(42,185,154,0.14);
  --success-border: rgba(42,185,154,0.28);
  --accent-glow:    var(--accent-dim);
}

/* ── Modo claro ────────────────────────────────────────────────────── */
[data-theme="light"] {
  color-scheme: light;
  /* Fondo/superficie NO son el valor exacto de la landing (#F5F7FA /
     #FFFFFF, casi blanco) -- José ya había rechazado ese tono en este
     proyecto ("tan claro que no se aprecia bien la pantalla", feedback
     confirmado, ver memoria). Se mantiene un gris-azulado propio en el
     mismo rango que index.html ya usa (#D6E0EA) en vez de adoptarlo
     literalmente; confirmado con José 2026-09-12 antes de este cambio.
     --text-muted y las sombras de esta sección se recalibraron a mano
     (más oscuro / más opacas que el valor exacto de la landing) porque
     ese valor exacto se midió pensado para un fondo casi blanco y pierde
     contraste real contra este fondo más oscuro -- todo lo demás de este
     bloque SÍ es el valor exacto de la landing. */
  --bg:            #D8E1EA;
  --bg-elevated:   #E7EEF4;
  --bg-elevated-2: #CBD8E3;
  --border:        rgba(15,27,45,0.10);
  --border-strong: rgba(15,27,45,0.18);
  --text-primary:   #0F1B2D;
  --text-secondary: #4A5A6E;
  --text-muted:     #566878;
  --accent:        #1B4D8F;
  --accent-hover:  #163E73;
  --accent-dim:    #E8EFF8;
  --accent-border: rgba(27,77,143,0.28);
  --danger:        #D6362E;
  --danger-dim:    rgba(214,54,46,0.10);
  --danger-border: rgba(214,54,46,0.25);
  --warning:        #B7791F;
  --warning-dim:    rgba(183,121,31,0.10);
  --warning-border: rgba(183,121,31,0.25);
  --info:        #2C6FCC;
  --info-dim:    rgba(44,111,204,0.10);
  --info-border: rgba(44,111,204,0.25);
  --success:        #0E7C6B;
  --success-bg:     #E1F2EE;
  --success-border: rgba(14,124,107,0.28);
  --shadow-card:  0 1px 2px rgba(15,27,45,0.08), 0 4px 16px rgba(15,27,45,0.10);
  --shadow-lg:    0 12px 40px rgba(15,27,45,0.20);
  --shadow-focus: 0 0 0 3px var(--accent-dim);
}

/* Modo claro — ajustes estructurales que no resuelven vía variables */
[data-theme="light"] .btn .spinner { border-color: rgba(0,0,0,0.18); border-top-color: #fff; }
[data-theme="light"] .btn-ghost:hover:not(:disabled) { background: rgba(0,0,0,0.05); }

/* ── Botón de tema (sol/luna) ─────────────────────────────────────── */
.vp-theme-btn {
  background: none;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  cursor: pointer;
  color: var(--text-secondary);
  padding: 5px 8px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: color 0.15s, border-color 0.15s, background 0.15s;
  line-height: 0;
  flex-shrink: 0;
}
.vp-theme-btn:hover { color: var(--text); border-color: var(--border-focus); }
.vp-theme-btn svg { display: block; }

/* ── Reset ─────────────────────────────────────────────────────────── */
/* [hidden] needs !important because author display:flex/grid/block rules
   have higher cascade priority than the UA stylesheet's display:none.
   Without this, elements like .cart-footer[hidden] stay visible. */
[hidden] { display: none !important; }

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

html {
  font-size: 16px;
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  font-family: var(--font-sans);
  background: var(--bg);
  color: var(--text);
  min-height: 100dvh;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* ── Typography ────────────────────────────────────────────────────── */
h1, h2, h3, h4, h5 {
  font-family: var(--font-heading);
  font-weight: 700;
  line-height: 1.2;
  color: var(--text);
}

h1 { font-size: clamp(1.75rem, 4vw, 2.25rem); }
h2 { font-size: clamp(1.25rem, 3vw, 1.625rem); }
h3 { font-size: 1.125rem; }

p { color: var(--text-secondary); }

a {
  color: var(--accent);
  text-decoration: none;
}
a:hover { text-decoration: underline; }

.mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }

/* ── Scroll-reveal (utilidad, portada de la landing tiflows.com) ──────
   Disponible para páginas de tipo "landing" con contenido estático que
   se recorre de arriba a abajo una sola vez. Deliberadamente NO se cablea
   en ninguna de las 12 pantallas operativas del sistema (POS, inventario,
   créditos, etc.) ni en catalogo-publico.html: esas pantallas repintan su
   contenido en cada interacción (filtro, búsqueda, F5 de datos en vivo) y
   una animación de entrada que se repite en cada repintado se siente como
   parpadeo, no como pulido -- ver auditoría de rebranding, 2026-09-12.
   Para usarla en una página nueva que sí lo amerite: agregar la clase
   "reveal" al elemento, y una vez que entra en el viewport (vía
   IntersectionObserver) agregarle "in". */
.reveal { opacity: 0; transform: translateY(20px); transition: opacity .6s, transform .6s; }
.reveal.in { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1; transform: none; transition: none; }
}

/* ── Higiene global (auditoría Web Interface Guidelines, 2026-09-24) ──
   touch-action: manipulation quita el retraso de doble-toque-para-zoom en
   táctil SIN bloquear el zoom con pellizco (accesibilidad). Se declara
   también en <dialog> porque, al abrirse con showModal(), va en la capa
   superior del navegador. overscroll-behavior: contain evita que el scroll
   "se escape" de un diálogo/panel hacia la página de atrás. */
html, body, dialog { touch-action: manipulation; }

/* Skip-link (auditoría Web Interface Guidelines, 2026-09-24): invisible
   hasta recibir foco con Tab -- salta el menú superior (~13 Tab en POS). */
.skip-link {
  position: absolute;
  left: 8px;
  top: -60px;
  z-index: 10000;
  padding: 10px 16px;
  background: var(--accent);
  color: var(--bg);
  border-radius: var(--radius-sm);
  font-weight: 600;
  text-decoration: none;
}
.skip-link:focus { top: 8px; }
#main-content:focus { outline: none; }
dialog, .dialog-body, .pos-cart, .cart-body { overscroll-behavior: contain; }

/* Quien tiene "reducir movimiento" en su sistema: sin animaciones ni
   transiciones decorativas (entradas, fundidos, pulsos, hover). Los
   spinners de carga NO se apagan: son el único indicador de que una
   acción está en curso y no son movimiento decorativo. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    transition-duration: 0.01ms !important;
    transition-delay: 0s !important;
  }
  *:not(.spinner):not(.spinner-sm), *:not(.spinner):not(.spinner-sm)::before, *:not(.spinner):not(.spinner-sm)::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    animation-delay: 0s !important;
  }
}

/* ── Card ──────────────────────────────────────────────────────────── */
.card {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  /* Regresión de móvil del rebranding (2026-09-13, ver docs/RUNBOOK.md) --
     `.card` a secas nunca tuvo padding propio (cada página lo agregaba por
     su cuenta: `.gs-section-card`/`.gs-rent-card`/`.auth-card`, o
     `#dash-payment-card`/`#dash-alertas-card` -- este último ya fue el
     mismo bug, corregido "Punto 2" 2026-09-12, pero solo ahí). Las 8
     tarjetas de gestion.html usan `class="card"` sin ningún modificador --
     sin padding, el texto queda pegado a los 4 bordes (incluida la esquina
     redondeada), y en el borde derecho, con una palabra terminando justo
     en el límite, se percibe como texto cortado en vez de que falte
     aire alrededor. Puesto acá, en la base, para que cubra cualquier
     `class="card"` nuevo por default en vez de que cada página tenga que
     acordarse de agregarlo -- páginas que ya fijan su propio padding
     (`.gs-section-card`, `.gs-rent-card`, `.auth-card`, los 2 ID de
     dashboard) aparecen más abajo en este archivo y siguen ganando en la
     cascada, así que no cambian. */
  padding: 16px 20px;
  /* Red de seguridad, mismo criterio que `.product-card-child-name`/
     `.cat-card-name` (Fase 9C): rompe una palabra sola SOLO si no cabe
     en el ancho disponible, texto normal sigue envolviendo en los
     espacios como siempre. */
  overflow-wrap: break-word;
}

/* ── Form ──────────────────────────────────────────────────────────── */
.form-group {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

label {
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--text-secondary);
  user-select: none;
}

.input-wrapper {
  position: relative;
  display: flex;
  align-items: center;
}

/* input[type="search"] salió de estas 2 listas en la Fase 15 (2026-09-04)
   -- los 4 campos que usaban ese tipo pasaron a <div contenteditable>
   (técnica validada contra el "manual fallback" de contraseñas de Chrome,
   ver docs/RUNBOOK.md, "Fase 14"/"Fase 15"; su apariencia vive ahora en
   .ce-search-input más abajo). No queda ningún <input type="search"> en
   el proyecto -- confirmado con grep antes de quitarlo, no dejarlo "por si
   acaso" (regla de oro del proyecto sobre código muerto conectado). */
input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"] {
  width: 100%;
  padding: 10px 14px;
  background: var(--bg-input);
  border: 1px solid var(--border);
  border-radius: 8px;
  color: var(--text);
  font-family: var(--font-sans);
  font-size: 1rem;
  line-height: 1.5;
  outline: none;
  transition: border-color 0.12s ease;
  -webkit-appearance: none;
  appearance: none;
}

input[type="number"] { -moz-appearance: textfield; }
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; }

input[type="text"]:focus,
input[type="email"]:focus,
input[type="password"]:focus,
input[type="number"]:focus {
  border-color: var(--accent);
}

input.has-toggle { padding-right: 48px; }

input::placeholder { color: var(--text-muted); }

/* Fase 18 (2026-09-06) -- no existía ninguna regla para <textarea> en todo
   el proyecto (confirmado con grep): #anular-motivo (gestion.html, ya
   existente) y #factura-detalle (nuevo, ver gastos.html) se dibujaban con
   el estilo por defecto del navegador -- una caja blanca fuera de lugar
   en el tema oscuro. Mismo look que input[type="text"] de arriba, con
   resize:vertical (a diferencia de un input, un textarea sí tiene sentido
   que el usuario lo agrande). */
textarea {
  display: block;
  width: 100%;
  padding: 10px 14px;
  background: var(--bg-input);
  border: 1px solid var(--border);
  border-radius: 8px;
  color: var(--text);
  font-family: var(--font-sans);
  font-size: 1rem;
  line-height: 1.5;
  outline: none;
  transition: border-color 0.12s ease;
  resize: vertical;
}
textarea:focus { border-color: var(--accent); }
textarea::placeholder { color: var(--text-muted); }

/* Fase 21 (2026-09-08) -- .ce-search-input sumado al selector: algunos de
   los <input>/<textarea> convertidos a <div contenteditable> en esta fase
   (inventario.html) validan con classList.toggle('field-invalid', ...),
   igual que siempre -- sin esto el borde rojo de error dejaba de verse en
   esos campos específicos tras la conversión. */
input.field-invalid,
.ce-search-input.field-invalid {
  border-color: var(--error);
}

input.field-invalid:focus,
.ce-search-input.field-invalid:focus {
  box-shadow: 0 0 0 3px var(--danger-dim);
}

/* Autocomplete background fix on Chrome/Safari */
input:-webkit-autofill,
input:-webkit-autofill:focus {
  -webkit-box-shadow: 0 0 0 1000px var(--bg-input) inset;
  -webkit-text-fill-color: var(--text);
  caret-color: var(--text);
}

.field-error {
  font-size: 0.8125rem;
  color: var(--error);
  min-height: 1em;
  line-height: 1.3;
}

/* Password toggle */
.password-toggle {
  position: absolute;
  right: 12px;
  top: 50%;
  transform: translateY(-50%);
  background: none;
  border: none;
  cursor: pointer;
  color: var(--text-muted);
  padding: 4px;
  display: flex;
  align-items: center;
  border-radius: var(--radius-sm);
  transition: color 0.15s;
  line-height: 0;
}
.password-toggle:hover { color: var(--text-secondary); }
.password-toggle:focus-visible {
  outline: 2px solid var(--border-focus);
  outline-offset: 2px;
}

/* ── Buttons ───────────────────────────────────────────────────────── */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 10px 18px;
  border: none;
  border-radius: 8px;
  font-family: var(--font-sans);
  font-size: 0.9375rem;
  font-weight: 600;
  cursor: pointer;
  position: relative;
  transition: background 0.12s ease, transform 0.1s ease, opacity 0.15s ease, border-color 0.12s ease;
  -webkit-tap-highlight-color: transparent;
}

.btn:active:not(:disabled) { transform: scale(0.985); }

.btn:disabled {
  cursor: not-allowed;
  opacity: 0.65;
}

.btn-primary {
  background: var(--accent);
  color: var(--bg);
  width: 100%;
}

.btn-primary:hover:not(:disabled) { background: var(--accent-hover); }

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

/* Loading state */
.btn .btn-label {
  transition: opacity 0.15s;
}

.btn .spinner {
  width: 18px;
  height: 18px;
  border: 2px solid rgba(255,255,255,0.25);
  border-top-color: #fff;
  border-radius: 50%;
  animation: spin 0.65s linear infinite;
  display: none;
  flex-shrink: 0;
}

.btn.loading .btn-label { opacity: 0; }
.btn.loading .spinner   { display: block; }

@keyframes spin {
  to { transform: rotate(360deg); }
}

/* Link button (inline, no background) */
.link-btn {
  background: none;
  border: none;
  cursor: pointer;
  color: var(--accent);
  font-family: var(--font-sans);
  font-size: inherit;
  font-weight: 600;
  padding: 0;
  text-decoration: none;
  transition: color 0.15s;
}
.link-btn:hover { color: var(--accent-hover); text-decoration: underline; }

/* ── Alerts ────────────────────────────────────────────────────────── */
.alert {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 12px 14px;
  border-radius: var(--radius);
  font-size: 0.9rem;
  line-height: 1.45;
  border: 1px solid transparent;
}

.alert[hidden] { display: none; }

.alert-error {
  background: var(--error-bg);
  border-color: var(--error-border);
  color: var(--danger);
}

.alert-success {
  background: var(--success-bg);
  border-color: var(--success-border);
  color: var(--accent);
}

/* Fase 3 (aislamiento configurable de sucursal, 2026-09-11) -- primer uso
   real de los tokens --info/--info-dim/--info-border (ya existían en los 2
   temas, sin ningún .alert que los usara todavía). Para el banner "Mostrando
   [Sucursal X]" -- informativo, no un error ni una confirmación. */
.alert-info {
  background: var(--info-dim);
  border-color: var(--info-border);
  color: var(--info);
}

/* ── Auth page layout ──────────────────────────────────────────────── */
.auth-page {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100dvh;
  padding: 24px 16px;
  background:
    radial-gradient(ellipse at 15% 60%, rgba(76,139,217,0.07) 0%, transparent 55%),
    radial-gradient(ellipse at 85% 15%, rgba(76,139,217,0.04) 0%, transparent 45%),
    var(--bg);
}

/* La versión clara tenía este mismo glow hardcodeado al RGB del acento
   OSCURO sin importar el tema (bug preexistente, no introducido por el
   rebranding) -- de paso queda corregido para que seguir el acento real
   de cada tema, igual que el resto de la paleta. */
[data-theme="light"] .auth-page {
  background:
    radial-gradient(ellipse at 15% 60%, rgba(27,77,143,0.07) 0%, transparent 55%),
    radial-gradient(ellipse at 85% 15%, rgba(27,77,143,0.04) 0%, transparent 45%),
    var(--bg);
}

.auth-container {
  width: 100%;
  max-width: 420px;
  display: flex;
  flex-direction: column;
  gap: 28px;
}

.auth-brand {
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
}

.brand-mark {
  width: 52px;
  height: 52px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.brand-mark img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  filter: drop-shadow(0 4px 12px var(--accent-glow));
}

.brand-name {
  font-family: var(--font-heading);
  font-size: 1.625rem;
  font-weight: 800;
  color: var(--text);
  letter-spacing: -0.5px;
}

.brand-tagline {
  font-size: 0.9rem;
  color: var(--text-muted);
}

.auth-card {
  padding: 32px 28px;
}

@media (max-width: 440px) {
  .auth-card { padding: 24px 20px; border-radius: var(--radius); }
}

.panel-title {
  font-size: 1.25rem;
  font-weight: 700;
  margin-bottom: 24px;
  color: var(--text);
}

/* Auth panel transitions */
.auth-panel {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.auth-panel.hidden { display: none; }

@keyframes panelFadeIn {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}

.auth-panel.animating {
  animation: panelFadeIn 0.2s ease forwards;
}

/* Form inside panel */
.auth-panel form {
  display: flex;
  flex-direction: column;
  gap: 18px;
}

.auth-switch {
  text-align: center;
  font-size: 0.9rem;
  color: var(--text-secondary);
  padding-top: 4px;
}

/* ── App shell — header ────────────────────────────────────────── */

.app-header {
  background: var(--bg-card);
  border-bottom: 1px solid var(--border);
  position: sticky;
  top: 0;
  z-index: 100;
}

.app-header-inner {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0.625rem 1.25rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

.header-brand {
  display: flex;
  align-items: center;
  gap: 8px;
  text-decoration: none;
  /* Regresión de móvil del rebranding (2026-09-13, ver docs/RUNBOOK.md) --
     sin esto, en una pantalla angosta donde .header-right (theme+sucursal+
     email+contraseña+cerrar sesión) no entra en una sola fila,
     `.app-header-inner` (flex, sin wrap) shrinkeaba este item por debajo de
     su propio contenido -- confirmado con getBoundingClientRect() real
     (width:0) -- dejando que .brand-mark-sm (40x40 fijo) se saliera de su
     caja y quedara pisado por el primer control de .header-right (el botón
     de tema, inyectado por theme.js). flex-shrink:0 lo fija en su tamaño
     natural siempre; el que cede espacio es .header-right (ver ahí). */
  flex-shrink: 0;
}

.header-brand:hover { text-decoration: none; }

.brand-mark-sm {
  /* 40px y no 32px -- el archivo del logo es un lockup completo (círculo +
     "TIFlows" en el mismo trazo, sin separar); a 32px la palabra queda
     ilegible, confirmado con captura real y con José 2026-09-12. */
  width: 40px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}

.brand-mark-sm img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.brand-name-sm {
  font-family: var(--font-heading);
  font-size: 1rem;
  font-weight: 700;
  color: var(--text);
  letter-spacing: -0.3px;
}

.header-right {
  display: flex;
  align-items: center;
  gap: 12px;
  /* Regresión de móvil del rebranding (2026-09-13, ver docs/RUNBOOK.md) --
     sin flex-wrap, en una pantalla angosta los 5 controles (tema, sucursal,
     correo, contraseña, cerrar sesión) no entraban en una sola fila y el
     contenedor completo se desbordaba por fuera del viewport en vez de
     encogerse -- "Cerrar sesión" quedaba fuera del área visible, sin
     scroll ni ninguna señal (confirmado con scrollWidth real: 463px de
     contenido en un viewport de 375px). Con flex-wrap los controles bajan
     a una segunda fila dentro del ancho real disponible -- mismo patrón ya
     usado en `.cat-search-row`/`.inv-toolbar` de este archivo. En
     escritorio no cambia nada (hay espacio de sobra para una sola fila).
     justify-content:flex-end alinea la fila envuelta contra el borde
     derecho, coherente con dónde ya vivían estos controles. */
  flex-wrap: wrap;
  justify-content: flex-end;
}

.user-email {
  font-size: 0.8125rem;
  color: var(--text-muted);
  max-width: 200px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Punto 1 (2026-09-10) -- selector de sucursal activa, montado por
   js/v5/sucursal-selector.js en el header de las 10 páginas con sesión
   (antes solo existía en pos.html). Agrandado a propósito respecto al
   resto de los controles del header (font-size/padding más grandes que
   .pay-select, borde y fondo propios) -- José reportó que costaba ver de
   un vistazo cuál sucursal estaba activa. */
.sucursal-select-wrap {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-shrink: 0;
}

.sucursal-select-icon {
  font-size: 0.9375rem;
  line-height: 1;
}

.sucursal-select-header {
  font-size: 0.875rem;
  font-weight: 600;
  padding: 7px 30px 7px 12px;
  border-radius: var(--radius);
  border: 1px solid var(--border-strong);
  background: var(--bg-input);
  color: var(--text);
  max-width: 220px;
}

/* Caso Cajero/Dueño de 1 sola sucursal -- sin selector, solo el nombre
   como dato informativo, pero con el mismo criterio de legibilidad. */
.sucursal-label {
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--text);
}

.btn-ghost {
  background: transparent;
  color: var(--text-secondary);
  border: 1px solid var(--border-strong);
  width: auto;
}

.btn-ghost:hover:not(:disabled) {
  background: rgba(255, 255, 255, 0.04);
  color: var(--text);
  border-color: var(--text-secondary);
}

.btn-sm {
  padding: 8px 14px;
  font-size: 0.8125rem;
  font-weight: 500;
  border-radius: 6px;
}

/* Punto 3 (2026-09-10) -- botón cuadrado, solo ícono + title/aria-label
   (nunca solo el ícono sin alternativa accesible) -- primer uso real:
   "🔑 Contraseña" del header pasó a solo "🔑" para liberar espacio real
   sin sacarle el acceso a nadie (ver change-password.js). */
.btn-icon-only {
  padding: 8px 10px;
  line-height: 1;
}

/* ── App shell — navigation ────────────────────────────────────── */

/* Fase C, Bloque 4 (2026-09-09) -- affordance de scroll: en móvil (medido
   real con Playwright, ver docs/RUNBOOK.md) el nav desborda fuerte (solo 3
   de 10 ítems visibles en 390px) sin ninguna señal de que hay más para
   scrollear. Se resuelve con un degradado sutil en el borde derecho,
   visible SOLO cuando `.app-nav` (adentro) todavía tiene contenido fuera de
   vista -- toggle real vía scroll/resize en wireNavScrollFade() (nav.js),
   nunca solo CSS estático. El wrapper (`position: relative`, no scrollea)
   es necesario para que el degradado quede fijo al borde del viewport en
   vez de moverse junto con el contenido que scrollea adentro.
   Decisión explícita de José (Recomendado B, no agrupar en dropdown): el
   nav ya reducido (Bloque 1) funciona bien en escritorio (los 10 ítems
   entran en una sola fila sin scroll a 1280px) -- una reestructuración
   completa en "secciones madre" habría sido resolver un problema que ahí
   no existe, a costa de tocar 10 archivos + cambiar el hábito ya aprendido
   de los cajeros, en la página más sensible del proyecto. */
.app-nav-wrap {
  position: relative;
}
.app-nav-wrap::after {
  content: '';
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: 28px;
  background: linear-gradient(to right, transparent, var(--bg-card));
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.15s ease;
}
.app-nav-wrap.has-more-right::after { opacity: 1; }

.app-nav {
  background: var(--bg-card);
  border-bottom: 1px solid var(--border);
  display: flex;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}

.app-nav::-webkit-scrollbar { display: none; }

.nav-item {
  flex-shrink: 0;
  padding: 0.75rem 1.125rem;
  color: var(--text-muted);
  font-family: var(--font-sans);
  font-size: 0.875rem;
  font-weight: 500;
  white-space: nowrap;
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  cursor: pointer;
  transition: color 0.15s, border-color 0.15s;
  -webkit-tap-highlight-color: transparent;
  text-decoration: none;
  display: inline-block;
}

.nav-item:hover { color: var(--text-secondary); text-decoration: none; }

.nav-item.active {
  color: var(--accent);
  border-bottom-color: var(--accent);
}

/* ── App shell — main area ──────────────────────────────────────── */

.app-main {
  max-width: 1200px;
  margin: 0 auto;
  padding: 1.5rem 1.25rem 3rem;
}

/* ── Section header ─────────────────────────────────────────────── */

.section-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 2rem;
  flex-wrap: wrap;
}

.section-title {
  font-size: 1.375rem;
  font-weight: 600;
  color: var(--text);
}

.section-date {
  font-size: 0.8125rem;
  color: var(--text-muted);
  text-transform: capitalize;
}

/* ── Acordeón de Gestión (Fase 11) ───────────────────────────────────
   Solo aplica donde .section-header también lleva .acc-header -- las
   demás páginas (dashboard, reportes, etc.) usan .section-header sin
   estas clases y no se ven afectadas. */

.acc-section {
  border-bottom: 1px solid var(--border);
}


.acc-header {
  margin-bottom: 0;
  padding: 1rem 0;
  cursor: pointer;
  -webkit-user-select: none;
  user-select: none;
}

.acc-header:hover .section-title {
  color: var(--accent);
}

.acc-header:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.acc-header-actions {
  display: flex;
  align-items: center;
  gap: 12px;
}

.acc-chevron {
  font-size: 1rem;
  color: var(--text-muted);
  transition: transform .15s ease;
  flex-shrink: 0;
}

.acc-header[aria-expanded="true"] .acc-chevron {
  transform: rotate(180deg);
}

.acc-body {
  padding-bottom: 1.5rem;
}

.acc-body[hidden] {
  display: none;
}

/* ── Sidebar de categorías de Gestión (Fase B, Bloque 5) ──────────────
   Sidebar angosto fijo a la izquierda + el resto del ancho para las
   secciones de la categoría activa (las demás quedan `hidden` por JS,
   nunca removidas del DOM -- ver gestion.html). En pantallas angostas
   pasa a una barra horizontal con scroll propio arriba del contenido
   (nunca scroll horizontal del body -- regla general de responsive del
   proyecto). */
.gestion-layout {
  display: flex;
  gap: 24px;
  align-items: flex-start;
}

.gestion-sidebar {
  flex-shrink: 0;
  width: 180px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  position: sticky;
  top: 16px;
}

.gestion-sidebar-divider {
  height: 1px;
  background: var(--border);
  margin: 8px 4px;
}

.gestion-cat-btn {
  background: none;
  border: none;
  border-radius: var(--radius-sm);
  text-align: left;
  padding: 10px 12px;
  font-family: var(--font-sans);
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--text-secondary);
  cursor: pointer;
  transition: background 0.12s, color 0.12s;
}

.gestion-cat-btn:hover { background: var(--bg-input); color: var(--text); }

.gestion-cat-btn.active {
  background: var(--accent-dim);
  color: var(--accent);
  font-weight: 600;
}

/* "Ayuda" queda visualmente aparte de las 4 categorías (separador arriba,
   nunca un 5to botón igual a los otros) -- es referencia/documentación,
   no una categoría de gestión real. */
.gestion-cat-btn--ayuda {
  color: var(--text-muted);
}
.gestion-cat-btn--ayuda.active {
  background: var(--bg-input);
  color: var(--text);
}

.gestion-sections {
  flex: 1;
  min-width: 0;
  /* Fase B, Bloque 5 -- el borde superior vive en el CONTENEDOR, no en
     ":first-child" -- desde el sidebar de categorías, la sección
     visualmente primera de cada categoría casi nunca es el ":first-child"
     real del DOM (las 9 secciones no se reordenaron físicamente, solo se
     muestran/ocultan por categoría, ver gestion.html). Antes de este fix,
     cambiar a "Contactos" (Cajeros no es el :first-child real) dejaba la
     sección de arriba sin línea superior -- encontrado con una captura
     real, no a ojo. */
  border-top: 1px solid var(--border);
}

@media (max-width: 767px) {
  .gestion-layout {
    flex-direction: column;
  }
  .gestion-sidebar {
    position: static;
    width: 100%;
    flex-direction: row;
    overflow-x: auto;
    gap: 4px;
    padding-bottom: 4px;
  }
  .gestion-sidebar-divider {
    width: 1px;
    height: auto;
    align-self: stretch;
    margin: 0 4px;
  }
  .gestion-cat-btn { flex-shrink: 0; }
}

/* ── Metric cards ───────────────────────────────────────────────── */

.metrics-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(155px, 1fr));
  gap: 12px;
}

.metrics-grid[hidden] { display: none; }

.metric-card {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 16px 20px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  transition: border-color 0.2s;
}

.metric-card:hover { border-color: var(--border-focus); }

.metric-card--accent {
  border-color: var(--accent-border);
  background: linear-gradient(135deg, var(--bg-card) 0%, var(--accent-dim) 100%);
}

.metric-card--accent:hover { border-color: var(--accent); }

.metric-label {
  font-size: 0.6875rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-secondary);
  line-height: 1.35;
}

.metric-value {
  font-family: var(--font-mono);
  font-size: 1.625rem;
  font-weight: 600;
  color: var(--text);
  line-height: 1.1;
  letter-spacing: -0.5px;
}

.metric-sub {
  font-size: 0.6875rem;
  color: var(--text-muted);
  line-height: 1.4;
}

/* ── Metrics loading state ──────────────────────────────────────── */

.metrics-loading {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 2rem 0;
  color: var(--text-muted);
  font-size: 0.9rem;
}

.metrics-loading[hidden] { display: none; }

.spinner-sm {
  width: 16px;
  height: 16px;
  border: 2px solid var(--border);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin 0.65s linear infinite;
  flex-shrink: 0;
}

/* ── Coming soon banner ─────────────────────────────────────────── */

.coming-soon-banner {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 11px 14px;
  background: var(--info-dim);
  border: 1px solid var(--info-border);
  border-radius: var(--radius);
  color: var(--text-secondary);
  font-size: 0.875rem;
  margin-bottom: 1.25rem;
}

.coming-soon-banner[hidden] { display: none; }

/* ── Responsive overrides ───────────────────────────────────────── */

@media (max-width: 480px) {
  .app-header-inner { padding: 0.5rem 1rem; }
  .brand-name-sm    { display: none; }
  .user-email       { max-width: 130px; }
  /* Punto 1 -- el selector agrandado no entra completo junto al resto del
     header en una pantalla angosta; el email ya se recorta arriba, así
     que este también, mismo criterio, para que "Cerrar sesión" nunca se
     empuje fuera de la vista. */
  .sucursal-select-header { max-width: 110px; font-size: 0.8125rem; padding: 6px 24px 6px 10px; }
  .sucursal-label         { max-width: 110px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .app-main         { padding: 1rem 1rem 2.5rem; }
}

/* ── Inventory page ─────────────────────────────────────────────── */

/* Regresión de móvil del rebranding (2026-09-13) -- `.inv-toolbar` pasa de
   una sola fila (buscador + filtros + botones todos compitiendo por
   espacio) a columna, con el buscador en su propia fila completa
   (`.inv-search-row`) y todo lo demás debajo (`.inv-filters-row`) -- mismo
   patrón que `.cat-toolbar`/`.cat-search-row` de pos.html, que ya no tiene
   este problema. Ver docs/RUNBOOK.md, sección "Regresión de móvil del
   rebranding", para el detalle completo (incluye por qué el intento
   anterior de arreglar solo `.search-wrapper` a 480px no funcionaba). */
.inv-toolbar {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  margin-bottom: 1.25rem;
}

.inv-search-row {
  display: flex;
}

/* Con el buscador ahora solo en su fila, `flex:1` (heredado de
   `.search-wrapper` base) lo estiraría a todo el ancho del contenedor en
   escritorio -- tope razonable, mismo criterio que `.cat-search-row
   .search-wrapper` de pos.html. */
.inv-search-row .search-wrapper { max-width: 420px; }

.inv-filters-row {
  display: flex;
  gap: 0.75rem;
  align-items: center;
  flex-wrap: wrap;
}

.search-wrapper {
  position: relative;
  flex: 1;
  min-width: 180px;
}

.search-icon {
  position: absolute;
  right: 12px;
  top: 50%;
  transform: translateY(-50%);
  color: var(--text-muted);
  pointer-events: none;
}

/* .search-input (regla que era solo `padding-right: 38px`) se quitó en la
   Fase 15 -- sin ningún <input class="search-input"> restante en el
   proyecto (los 4 pasaron a <div class="ce-search-input">), confirmado
   con grep. Ese padding-right ahora vive en `.search-wrapper
   .ce-search-input` más abajo. */

/* ── .ce-search-input -- técnica validada en los 4 campos de búsqueda del
   proyecto: PILOTO en #cat-search (Fase 14), confirmado en vivo por José
   (el dropdown de contraseñas de Chrome desapareció, sin ningún efecto
   secundario) y extendido a los otros 3 (#cobro-cust-search,
   #search-products, #catpub-search) en Fase 15 -- ver docs/RUNBOOK.md.
   Reemplaza <input type="search"> por <div contenteditable> en los 4 --
   ya no queda ningún input[type="search"] en el proyecto (esa entrada se
   quitó de las 2 listas de selectores más arriba), así que se replica acá
   TODA la apariencia a mano en vez de repartir la mitad en una clase
   pensada para <input>. Clase compartida (no una regla por ID
   como en el piloto original) -- con los 4 campos usando exactamente el
   mismo patrón, repetir el bloque a mano 4 veces habría sido justo el
   tipo de duplicación que este proyecto evita. */
.ce-search-input {
  display: block;
  width: 100%;
  padding: 10px 14px;
  background: var(--bg-input);
  border: 1px solid var(--border);
  border-radius: 8px;
  color: var(--text);
  font-family: var(--font-sans);
  font-size: 1rem;
  line-height: 1.5;
  outline: none;
  transition: border-color 0.12s ease;
  /* Un <input> nunca envuelve texto (hace scroll horizontal); un
     contenteditable por defecto SÍ envuelve -- sin esto, un nombre largo
     haría crecer la fila de alto en vez de desplazarse. */
  white-space: nowrap;
  overflow-x: auto;
  overflow-y: hidden;
}

.ce-search-input:focus { border-color: var(--accent); }

/* Los <div> no tienen atributo placeholder nativo -- se simula visualmente
   con :empty::before + data-placeholder (el aria-placeholder del elemento,
   en el HTML, cubre la parte de accesibilidad). normalizeSearchCeEmpty()
   (una copia por página, mismo patrón que el resto del filtrado de estos
   campos -- ver comentario en cada <script>) se encarga de que el div
   quede realmente vacío (sin un <br> huérfano) para que este selector
   aplique en el momento correcto. */
.ce-search-input:empty::before {
  content: attr(data-placeholder);
  color: var(--text-muted);
  pointer-events: none;
}

/* 3 de los 4 (#cat-search, #search-products, #catpub-search) viven dentro
   de un .search-wrapper con el ícono de lupa superpuesto a la derecha --
   necesitan el espacio extra a la derecha para no quedar debajo del
   ícono. #cobro-cust-search no tiene ícono (no vive en un .search-wrapper),
   así que se queda con el padding parejo de arriba. */
.search-wrapper .ce-search-input { padding-right: 38px; }

/* ── .ce-textarea-input -- Fase 21 (2026-09-08), variante multilínea de
   .ce-search-input para los <textarea> convertidos a <div contenteditable>
   (#anular-motivo en gestion.html, #factura-detalle en gastos.html,
   #chat-input en chat.html -- ver docs/RUNBOOK.md, sección "Referencia").
   Mismo look/foco que .ce-search-input, pero SIN white-space:nowrap (acá sí
   debe envolver en varias líneas y crecer de alto, como el <textarea> que
   reemplaza) y con min-height en vez de una sola línea fija. */
.ce-textarea-input {
  display: block;
  width: 100%;
  min-height: calc(1.5em + 20px + 2px);
  padding: 10px 14px;
  background: var(--bg-input);
  border: 1px solid var(--border);
  border-radius: 8px;
  color: var(--text);
  font-family: var(--font-sans);
  font-size: 1rem;
  line-height: 1.5;
  outline: none;
  transition: border-color 0.12s ease;
  white-space: pre-wrap;
  word-break: break-word;
  overflow-y: auto;
}
.ce-textarea-input:focus { border-color: var(--accent); }
.ce-textarea-input:empty::before {
  content: attr(data-placeholder);
  color: var(--text-muted);
  pointer-events: none;
}

.btn-inline { width: auto; }

/* ── Selector de vista grilla/tabla (Fase A, inventario.html) ────────── */

.view-toggle {
  display: inline-flex;
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
  flex-shrink: 0;
}

.view-toggle-btn {
  background: var(--bg-input);
  border: none;
  color: var(--text-secondary);
  cursor: pointer;
  font-size: 1rem;
  line-height: 1;
  padding: 9px 12px;
  transition: background 0.12s, color 0.12s;
}

.view-toggle-btn + .view-toggle-btn { border-left: 1px solid var(--border); }

.view-toggle-btn:hover { color: var(--text); }

.view-toggle-btn.active {
  background: var(--accent-dim);
  color: var(--accent);
}

/* ── Product cards grid ─────────────────────────────────────────── */

.products-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 12px;
}

.products-grid[hidden] { display: none; }

/* ── Vista de tabla compacta (Fase A) -- alternativa a la grilla de
   arriba para catálogos grandes. Ancho completo con scroll horizontal
   propio (nunca el body -- ver regla de responsive del proyecto) si la
   pantalla es angosta. ──────────────────────────────────────────────── */
.products-table-wrap {
  overflow-x: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-card);
}

.products-table-wrap[hidden] { display: none; }

.products-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.875rem;
}

.products-table th {
  text-align: left;
  font-size: 0.6875rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
  padding: 10px 14px;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}

.products-table td {
  padding: 10px 14px;
  border-bottom: 1px solid var(--border);
  color: var(--text);
  white-space: nowrap;
}

.products-table tbody tr:last-child td { border-bottom: none; }
.products-table tbody tr:hover { background: var(--bg-input); }

.products-table-name {
  font-weight: 500;
  white-space: normal;
  min-width: 160px;
}

.products-table-actions {
  display: flex;
  gap: 6px;
  white-space: nowrap;
}

.product-card {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 16px 20px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  transition: border-color 0.15s;
}

.product-card:hover { border-color: var(--border-focus); }

.product-card-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.5rem;
  /* Fase 9 Parte C, corrección -- el badge de stock puede volverse largo
     ("489 (bajo)" y similares); sin wrap, un badge angosto empujaba el
     nombre contra la miniatura en vez de bajar a su propia línea (bug
     real reportado por José, tarjeta padre dentro de .product-group). */
  flex-wrap: wrap;
}

/* ── Foto de producto (Fase 7 Parte 2) — miniatura + respaldo genérico ──
   Sin foto (la gran mayoría de productos existentes del cliente real, ya
   que es un campo aditivo/opcional) muestra un emoji genérico -- nunca un
   espacio vacío. No existía ningún icono/emoji por producto antes de esta
   fase; este es el primero. */
.product-thumb {
  width: 44px;
  height: 44px;
  flex-shrink: 0;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--bg-input);
  border: 1px solid var(--border);
  display: flex;
  align-items: center;
  justify-content: center;
}

.product-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.product-thumb-fallback {
  font-size: 1.25rem;
  line-height: 1;
}

.product-card-title-block {
  flex: 1;
  min-width: 0;
}

.product-name {
  font-weight: 600;
  font-size: 0.9375rem;
  color: var(--text);
  line-height: 1.35;
}

.product-meta {
  font-size: 0.75rem;
  color: var(--text-muted);
}

.product-prices {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-top: 2px;
}

.product-price-row {
  display: flex;
  gap: 6px;
  align-items: baseline;
  font-size: 0.875rem;
}

.price-label {
  color: var(--text-muted);
  font-size: 0.75rem;
  min-width: 42px;
}

.price-value {
  font-family: var(--font-mono);
  color: var(--text);
  font-size: 0.9rem;
}

.product-card-footer {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
  padding-top: 0.625rem;
  margin-top: auto;
  border-top: 1px solid var(--border);
}

.product-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  flex: 1;
}

.product-actions {
  display: flex;
  gap: 6px;
  flex-shrink: 0;
  margin-left: auto;
}

/* ── Badges ─────────────────────────────────────────────────────── */

.badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 10px;
  border-radius: 999px;
  font-size: 0.6875rem;
  font-weight: 600;
  white-space: nowrap;
  line-height: 1.6;
}

.badge-green {
  background: var(--success-bg);
  border: 1px solid var(--success-border);
  color: var(--accent);
}

.badge-yellow {
  background: var(--warning-dim);
  border: 1px solid var(--warning-border);
  color: var(--warning);
}

.badge-red {
  background: var(--error-bg);
  border: 1px solid var(--error-border);
  color: var(--danger);
}

.badge-neutral {
  background: rgba(255,255,255,0.04);
  border: 1px solid var(--border);
  color: var(--text-muted);
}

/* Fase A (2026-09-08) -- azul informativo, reusa --info (ya usado para
   USDT/paralelo) para distinguir visualmente "Compra de mercancía" de
   "Gasto operativo" en Facturas de proveedores (gastos.html). No existía
   ningún .badge-info antes de esto. */
.badge-info {
  background: var(--info-dim);
  border: 1px solid var(--info-border);
  color: var(--info);
}

/* ── Empty state ────────────────────────────────────────────────── */

.empty-state {
  text-align: center;
  padding: 3.5rem 1rem;
  color: var(--text-muted);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.75rem;
}

.empty-state[hidden] { display: none; }

.empty-state h2 {
  font-size: 1rem;
  font-weight: 600;
  color: var(--text-secondary);
}

.empty-state p {
  font-size: 0.9rem;
  max-width: 320px;
}

.empty-box-icon {
  width: 48px;
  height: 48px;
  color: var(--text-muted);
  opacity: 0.5;
}

/* ── Dialog (modal) ─────────────────────────────────────────────── */

dialog {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  color: var(--text);
  padding: 0;
  max-width: 480px;
  width: calc(100% - 2rem);
  box-shadow: var(--shadow-card);
  margin: auto;
}

dialog[open] { display: flex; flex-direction: column; }

dialog::backdrop {
  background: rgba(0, 0, 0, 0.72);
  backdrop-filter: blur(2px);
}

.dialog-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1.125rem 1.25rem 0;
  gap: 1rem;
}

.dialog-title {
  font-size: 1.0625rem;
  font-weight: 600;
  color: var(--text);
}

.dialog-body {
  padding: 1rem 1.25rem;
  overflow-y: auto;
  flex: 1;
}

.dialog-body form {
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.form-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
}

.dialog-footer {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  padding: 0.75rem 1.25rem 1.125rem;
  border-top: 1px solid var(--border);
}

/* Modificador para diálogos con más contenido que un formulario (ej. el
   manual de usuario) -- mismo componente <dialog>, solo más ancho. La
   altura sigue acotada por el límite por defecto del navegador para
   <dialog> (~calc(100% - 6em) del viewport); .dialog-body ya tiene
   overflow-y:auto, así que el contenido largo hace scroll adentro, nunca
   desborda la pantalla. */
dialog.dialog-lg {
  max-width: 720px;
}

/* ── Vista previa de reporte (Fase A, 2026-09-08) -- reportes.html ───────
   El PDF real (mismo blob que ya generaría la descarga) dentro de un
   <iframe>, sin bajar ningún archivo -- el navegador lo renderiza con su
   propio visor de PDF nativo. Mucho más ancho/alto que .dialog-lg: acá el
   contenido es una página completa, no un formulario. */
dialog.dialog-preview {
  max-width: min(900px, calc(100% - 2rem));
  width: 100%;
  height: calc(100% - 4rem);
}

.dialog-preview .dialog-body {
  padding: 0;
  display: flex;
}

.report-preview-frame {
  flex: 1;
  width: 100%;
  border: none;
  background: #fff;
}

.report-preview-loading {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-muted);
  gap: 8px;
}

/* ── Manual de usuario (contenido inyectado desde
   js/manual-usuario-contenido.json, ver gestion.html) ──────────────── */

.manual-index {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 0 0 20px;
  padding: 0;
  list-style: none;
}

.manual-index a {
  display: inline-block;
  padding: 4px 12px;
  border-radius: 999px;
  border: 1px solid var(--border);
  color: var(--text-secondary);
  font-size: 12px;
  text-decoration: none;
  transition: border-color 120ms, color 120ms;
}

.manual-index a:hover {
  border-color: var(--border-strong);
  color: var(--text-primary);
}

.manual-section {
  padding-top: 20px;
  margin-top: 20px;
  border-top: 1px solid var(--border);
}

.manual-section:first-of-type {
  padding-top: 0;
  margin-top: 0;
  border-top: none;
}

.manual-section h3 {
  margin: 0 0 8px;
  font-size: 15px;
  font-weight: 600;
  color: var(--text-primary);
}

.manual-section p {
  margin: 0 0 10px;
  font-size: 14px;
  line-height: 1.55;
  color: var(--text-secondary);
}

.manual-section p:last-child {
  margin-bottom: 0;
}

.manual-section ul {
  margin: 0 0 10px;
  padding-left: 20px;
  font-size: 14px;
  line-height: 1.55;
  color: var(--text-secondary);
}

.manual-section li {
  margin-bottom: 6px;
}

.manual-section li:last-child {
  margin-bottom: 0;
}

.manual-section strong {
  color: var(--text-primary);
  font-weight: 600;
}

.btn-close {
  background: none;
  border: none;
  cursor: pointer;
  color: var(--text-muted);
  font-size: 1.375rem;
  line-height: 1;
  padding: 2px 4px;
  border-radius: var(--radius-sm);
  transition: color 0.15s;
  flex-shrink: 0;
}

.btn-close:hover { color: var(--text); }

.btn-danger {
  background: var(--error);
  color: #fff;
  width: auto;
  border: none;
}

.btn-danger:hover:not(:disabled) {
  background: var(--danger);
}

.returnable-section {
  padding: 10px 12px;
  background: var(--accent-dim);
  border: 1px solid var(--accent-border);
  border-radius: var(--radius-sm);
}

select {
  width: 100%;
  padding: 12px 16px;
  background: var(--bg-input);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: 1rem;
  outline: none;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%234d5f76' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 14px center;
  padding-right: 36px;
}

select:focus {
  border-color: var(--border-focus);
  box-shadow: var(--shadow-focus);
}

@media (max-width: 480px) {
  .form-row { grid-template-columns: 1fr; }
  .inv-toolbar { gap: 0.5rem; }
  .search-wrapper { min-width: 0; width: 100%; }
  .btn-inline { width: 100%; }
}

/* ── POS page ───────────────────────────────────────────────────── */

.pos-main { max-width: 1400px; padding: 1rem 1rem 5rem; }

.pos-layout {
  display: grid;
  /* 380px (antes 320px) -- pedido del cliente piloto: carrito más grande.
     Se sube el ANCHO de la columna en escritorio, no el max-height de
     .pos-cart -- ese cálculo (calc(100vh - 178px)) ya se rompió dos veces
     antes por ajustes sin verificación visual real (ver el historial de
     comentarios en .pos-cart/.cart-body/.cart-footer más abajo), así que
     no se toca de nuevo sin esa misma verificación. Un carrito más ancho
     reduce el wrapping de nombres largos de producto -- menos líneas por
     item, más items visibles en la misma altura, sin tocar el cálculo
     frágil. Verificado con capturas reales en escritorio y móvil (móvil
     no usa esta regla, ver @media abajo -- ahí el carrito ya es
     full-width). */
  grid-template-columns: 1fr 380px;
  gap: 1rem;
  align-items: start;
}

/* ── Catalog column ─────────────────────────────────────────────── */

.cat-toolbar {
  display: flex;
  flex-direction: column;
  gap: 0.625rem;
  margin-bottom: 0.75rem;
}

.cat-search-row {
  display: flex;
  gap: 0.5rem;
  align-items: center;
  /* Punto 4 (2026-09-12) -- antes solo 2 hijos (buscador + Escanear), ahora
     5 con "Cobrar / Devolver" e "Historial de ventas" movidos acá desde el
     nav global (ver docs/RUNBOOK.md, sección "Punto 4") -- flex-wrap para
     que los botones bajen a una segunda fila en vez de aplastarse en
     pantallas angostas. */
  flex-wrap: wrap;
}

/* flex-basis chico + max-width (antes flex:1, ocupaba toda la fila) --
   deja espacio real para los 3 botones de la barra de herramientas junto
   al buscador, sin dejar de encogerse en pantallas angostas. */
.cat-search-row .search-wrapper { flex: 1 1 200px; max-width: 320px; }

/* Los 3 botones de la barra (Cobrar/Devolver, Historial, Escanear) no se
   aplastan -- con flex-wrap arriba, bajan enteros a la fila siguiente en
   vez de comprimir su texto. */
.cat-toolbar-action { flex-shrink: 0; }

/* Solo móvil (<= 480px, el mismo corte de .btn-inline): los 4 botones de la
   barra (Cobrar/Devolver, Historial, Escanear, Cierre de caja) pasan de 4
   filas a ancho completo a una cuadrícula 2x2 debajo del buscador. Cada uno
   mide >= 44px de alto (área táctil). !important solo para ganarle al
   style="width:auto" en línea de #btn-escanear-pos y a `.btn-inline {
   width:100% }`. El buscador conserva su ancho (max-width: 320px de arriba)
   y ocupa su propia fila. Escritorio/tablet (> 480px) no cambian. */
@media (max-width: 480px) {
  .cat-search-row .search-wrapper { flex-basis: 100%; }
  .cat-search-row .cat-toolbar-action {
    flex: 1 1 calc(50% - 0.25rem);
    width: auto !important;
    min-width: 0;
    min-height: 44px;
    padding: 6px 8px;
    justify-content: center;
    text-align: center;
    white-space: normal;
    line-height: 1.2;
  }
}

.currency-pills {
  display: flex;
  gap: 0.5rem 0.375rem;
  flex-wrap: wrap;
}

/* Área táctil de 44px (auditoría Web Interface Guidelines, 2026-09-24): la
   píldora mide 36px de alto y ::after suma 4px invisibles arriba y abajo.
   El paso entre filas envueltas es 44px (36 + row-gap 8), así que las zonas
   táctiles de filas vecinas no se pisan. */
.currency-pill {
  position: relative;
  display: inline-flex;
  align-items: center;
  min-height: 36px;
  padding: 4px 14px;
  font-size: 0.75rem;
  font-weight: 600;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: none;
  color: var(--text-muted);
  cursor: pointer;
  transition: border-color 0.15s, color 0.15s, background-color 0.15s;
}

.currency-pill::after, .cat-pill::after {
  content: '';
  position: absolute;
  inset: -4px 0;
}

.currency-pill:hover { border-color: var(--accent); color: var(--accent); }
.currency-pill.active {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--bg);
}

.cat-categories {
  display: flex;
  gap: 0.5rem 0.375rem;
  flex-wrap: wrap;
  margin-bottom: 0.75rem;
}

.cat-pill {
  position: relative;
  display: inline-flex;
  align-items: center;
  min-height: 36px;
  padding: 3px 14px;
  font-size: 0.8125rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: none;
  color: var(--text-muted);
  cursor: pointer;
  transition: border-color 0.15s, color 0.15s, background-color 0.15s;
}

.cat-pill:hover { color: var(--text); border-color: var(--border-focus); }
.cat-pill.active { background: var(--bg-card); border-color: var(--border-focus); color: var(--text); }

.cat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
  gap: 0.5rem;
}

.cat-card {
  background: var(--bg-card);
  /* Fase C, Bloque 5 (2026-09-09) -- pedido explícito de José: el borde
     fino que ya existía (var(--border), 8% de opacidad) prácticamente no
     se distinguía a simple vista contra el fondo oscuro (confirmado con
     captura de pantalla real, no a ojo desde el código). var(--border-strong)
     (16%) es el mismo token que ya usa .btn-ghost para un borde visible
     pero discreto -- no un valor nuevo inventado para este caso. */
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  padding: 0.625rem 0.75rem;
  text-align: left;
  cursor: pointer;
  transition: border-color 0.15s, transform 0.1s;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  min-height: 88px;
}

.cat-card:hover:not(:disabled) { border-color: var(--accent); }
.cat-card:active:not(:disabled) { transform: scale(0.97); }
.cat-card:disabled { opacity: 0.38; cursor: not-allowed; }

.cat-card-name {
  font-weight: 600;
  font-size: 0.8125rem;
  color: var(--text);
  line-height: 1.3;
  /* Fase 9 Parte D, corrección -- el recorte a 2 líneas con "…" (line-
     clamp) escondía el resto del nombre combinado ("Producto — Formato")
     cuando era largo -- bug real reportado por el cliente piloto: "se
     corta". Igual que ya se hizo en .product-card-child-name de
     inventario.html, la tarjeta ahora crece de alto lo que haga falta en
     vez de esconder texto -- .cat-card no tiene alto fijo (solo
     min-height), así que no hay nada que rompa. overflow-wrap:break-word
     se queda -- sigue haciendo falta para una palabra sola más ancha que
     la tarjeta. */
  overflow-wrap: break-word;
}

.cat-card-price {
  font-family: var(--font-mono);
  font-size: 0.875rem;
  color: var(--accent);
  font-weight: 600;
}

/* Bs. de apoyo debajo del precio en USD -- formato de moneda
   estandarizado (2026-08-27), ver docs/RUNBOOK.md. */
.cat-card-price-bs {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  color: var(--text-muted);
  font-weight: 400;
}

.cat-card-meta {
  display: flex;
  /* flex-start, no center -- con el badge envolviendo a varias líneas
     (ver .cat-card-meta .badge más abajo), center lo centraba verticalmente
     de forma rara contra el ícono de retornable/badge "N en carrito" de al
     lado, que sí siguen en una sola línea. */
  align-items: flex-start;
  gap: 4px;
  margin-top: auto;
  /* Bug real reportado por el cliente piloto: el badge de desglose
     ("6 BULTO DE ARROZ + 0 sueltas", nombre de formato largo) se salía del
     borde de la tarjeta y se superponía visualmente con la tarjeta de al
     lado -- .badge es white-space:nowrap en todo el resto de la app
     (correcto ahí, son badges cortos) pero acá el texto no tiene límite
     real, viene del nombre que el Dueño le puso a su formato. flex-wrap
     baja el badge a su propia línea si hace falta; el override de abajo
     deja que el badge MISMO envuelva su texto en vez de desbordar. */
  flex-wrap: wrap;
}

/* Ver comentario de .cat-card-meta -- excepción puntual a .badge
   (white-space:nowrap) SOLO dentro del catálogo del POS, donde el texto
   del badge puede ser tan largo como el nombre de formato que el Dueño
   haya escrito. border-radius más chico que el pill completo (999px) --
   un pill con texto de 2-3 líneas adentro se ve raro, con las puntas muy
   redondeadas lejos del texto.

   text-align: center, no left -- bug real reportado por el cliente piloto
   ("se parte en 2 líneas de forma rara"), confirmado con captura real a
   ancho de celular (390px, gap real de la verificación anterior -- solo se
   habían probado 500/700/900/1400px, ninguno angosto de POS de verdad):
   con left, la segunda línea (normalmente más corta, ej. "sueltas" sola)
   queda pegada al borde izquierdo del pill mientras la primera línea es
   más ancha -- se ve descuadrado. Centrado, las dos líneas quedan
   alineadas al centro del pill como cualquier chip/badge envuelto. */
.cat-card-meta .badge {
  white-space: normal;
  text-align: center;
  border-radius: var(--radius-sm);
}

.cat-ret-icon {
  font-size: 0.6875rem;
  color: var(--text-muted);
}

/* ── Foto de producto en el catálogo del POS (Fase 7 Parte 2) — mismo
   respaldo genérico que en inventario.html (product-thumb-fallback) para
   que un producto sin foto nunca muestre un espacio vacío. ─────────── */
.cat-card-thumb {
  width: 100%;
  height: 52px;
  flex-shrink: 0;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--bg-input);
  border: 1px solid var(--border);
  display: flex;
  align-items: center;
  justify-content: center;
}

.cat-card-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.cat-card-thumb-fallback {
  font-size: 1.5rem;
  line-height: 1;
}

/* ── Cart sidebar ───────────────────────────────────────────────── */

.pos-cart {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  display: flex;
  flex-direction: column;
  /* Bug real #3 (tercera vuelta, reportado con capturas de escritorio en
     una laptop de altura mediana -- 1366x768): calc(100vh - 120px) asumía
     que .pos-cart empieza a 120px del top de la ventana -- ese número
     venía de `top: 80px` (para cuando el position:sticky se ACTIVA, al
     hacer scroll) más un margen de 40px. Pero en la carga inicial (sin
     scroll, el estado real en el que se usa esta pantalla en la práctica)
     el panel está en su posición NATURAL del documento -- medido real con
     Chrome (getBoundingClientRect): y≈158px (header del sitio + ticker de
     tasas + toolbar del catálogo, todo ANTES del carrito en el flujo).
     Con el offset equivocado (120 en vez de ~158), .pos-cart calculaba un
     max-height más grande de lo que realmente cabía, así que su propio
     borde inferior terminaba más allá del viewport en pantallas de altura
     media -- ahí es donde el footer/botones "desaparecían" (técnicamente
     seguían ahí, solo que fuera del área visible, alcanzables solo
     scrolleando la página completa, no el carrito). 200px deja margen de
     sobra sobre el offset real medido (~158px) para absorber pequeñas
     variaciones de contenido en el header/ticker sin volver a repetir este
     bug. Ver RUNBOOK.md, "Fase 9 -- carrito, tercera pasada". */
  /* 168px (antes 178px) -- Fase 10, segunda pasada del carrito
     (2026-09-01): margen de sobra de 10px (antes 20px) sobre el mismo
     offset real de ~158px, RE-MEDIDO antes de tocar este número (sigue
     midiendo 158.39px real con Playwright -- header/ticker/toolbar no
     cambiaron) -- no se asumió que seguía siendo válido. 10px de sobra en
     vez de 20 porque el margen sí importa acá (ver Bug real #4 debajo):
     no se redujo más para no repetir ese mismo bug con menos margen del
     que hace falta. La ganancia real de este ajuste es chica a propósito
     -- el grueso del espacio para la lista de productos viene de
     .cart-body/.cart-footer/.vuelto-section/.pay-section de abajo, no de
     acá. Ver docs/RUNBOOK.md, "Fase 10 -- segunda pasada del carrito". */
  max-height: calc(100vh - 168px);
  /* Bug real #4 (encontrado verificando la propia verificación): un primer
     ajuste de esta ronda dejó el margen en 200px (158px de offset real +
     42px "por si acaso"). Ese margen de sobra es exactamente lo que
     causaba que el botón se recortara ANTES de llegar al borde real del
     viewport -- .pos-cart tiene max-height + overflow-y:auto, así que SU
     PROPIO borde inferior (no el de la ventana) es el límite real de lo
     pintado. getBoundingClientRect() de un hijo (ej. #btn-confirm-sale) NO
     tiene en cuenta el recorte de un ancestro con overflow -- devuelve su
     posición geométrica en el documento igual, esté o no realmente
     pintada. La primera verificación de esta ronda comparaba
     btn.bottom <= vh (borde de LA VENTANA) y daba "visible" en 1366x768 /
     1920x1080 -- pero el chequeo correcto es btn.bottom <=
     posCart.bottom (borde de SU PROPIO contenedor, que si max-height
     recorta, es más chico que la ventana). Confirmado con muestreo de
     píxeles reales del PNG (no solo geometría): a 1366x768 el botón SÍ
     estaba recortado un poco más de la mitad, aunque la primera pasada de
     este mismo día lo había reportado como "pasa". Corregido bajando el
     margen a 178px (158 real + 20 de sobra, no 42) -- le devuelve a
     max-height el espacio que .cart-body necesita para absorber la
     diferencia (su trabajo, ver comentario en .cart-body) antes de que
     este overflow-y:auto de último nivel tenga que activarse. Ver
     RUNBOOK.md, "Fase 9 -- carrito, tercera pasada", para la magnitud real
     medida y qué tamaños de pantalla siguen necesitando este scroll de
     todos modos (el contenido natural del footer, no el cálculo de
     offset, es la causa en esos casos). */
  overflow-y: auto;
  position: sticky;
  top: 80px;
}

.cart-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 16px;
  border-bottom: 1px solid var(--border);
  font-weight: 600;
  font-size: 0.9375rem;
  flex-shrink: 0;
}

.cart-close { display: none; }

.cart-body {
  /* Bug real #1 (reportado con capturas reales de un teléfono): dentro de
     .pos-cart (altura acotada por max-height), .cart-body competía por
     espacio con .cart-footer usando flex:1 puro, sin ningún tope propio.
     .cart-footer nunca se encoge (flex-shrink:0, ver abajo) -- así que
     cuando su contenido natural es alto (totales + selector de moneda + 2
     grupos de toggles + vuelto + nota de entrega + botones, que en un
     carrito real con pocos productos ya ocupa la mayoría del panel),
     flex:1 terminaba exprimiendo .cart-body casi a 0px de alto -- la lista
     quedaba técnicamente ahí (con scroll, por eso aparecía una barra al
     hacer zoom-out) pero invisible al tamaño normal de pantalla.

     Bug real #2 (segunda vuelta, reportado con capturas de ESCRITORIO): el
     primer fix usaba max-height en vh (40vh acá + 52vh/70vh en
     .cart-footer) -- funcionaba en el viewport de teléfono donde se
     verificó, pero un vh es un porcentaje de TODA LA VENTANA, no del
     espacio real dentro de .pos-cart. En una pantalla de escritorio alta,
     45vh es un número absoluto mucho mayor que lo que la lista necesita --
     con flex:1, .cart-body se ESTIRABA hasta ese tope aunque tuviera pocos
     productos, dejando menos espacio real para .cart-footer dentro del
     max-height de .pos-cart (que no tiene overflow propio) y empujando los
     botones fuera del área visible -- por eso solo aparecían al alejar el
     zoom (que reduce todo proporcionalmente y el desborde se hace menos
     pronunciado). Ver RUNBOOK.md, "Fase 9 -- carrito, tercera pasada".

     Fix definitivo, sin ningún vh: min-height garantiza que la lista
     SIEMPRE muestre al menos ~1-2 productos (nunca exprimida a 0) --
     mismo criterio de antes. SIN max-height acá: .cart-body absorbe TODA
     la diferencia de espacio disponible, sea mucho (escritorio) o poco
     (teléfono), dejándole a .cart-footer su tamaño NATURAL completo en
     ambos casos -- el mismo cálculo relativo funciona en cualquier alto
     de pantalla porque no depende de un número fijo de la ventana. */
  flex: 1 1 auto;
  /* 300px (antes 90px) -- Fase 10, segunda pasada del carrito
     (2026-09-01), decisión final de José entre 3 alternativas medidas
     con Playwright: 90px solo garantizaba ~1 producto visible con el
     alto real de una fila (77-114px, más alta desde que la cantidad es
     un <input> editable, ver .qty-display-input). 150px (probado
     primero, sin ningún scroll en ninguna resolución) solo daba 1
     producto completo + el siguiente asomado a 1366x768 -- José prefirió
     explícitamente 300px (3 productos visibles a esa resolución) a
     cambio de que .pos-cart necesite scroll propio y contenido (~149px,
     nunca de la página completa -- ver el overflow-y:auto de .pos-cart
     arriba, que ya existía como red de seguridad) para llegar al footer
     SOLO en la resolución más ajustada (1366x768); en 1920x1080 y móvil
     el footer sigue alcanzable sin ningún scroll con este mismo valor,
     verificado real, no asumido. El primer intento de esta fase (ancho
     de columna 320->380px) no tocaba este eje en absoluto -- el ancho de
     .pos-layout y el alto de .cart-body son ejes completamente
     distintos. Ver docs/RUNBOOK.md, sección "Fase 10 -- segunda pasada
     del carrito", para las 3 alternativas medidas y la decisión final. */
  min-height: 300px;
  overflow-y: auto;
  padding: 0.5rem 0.75rem;
}

.cart-empty-msg {
  text-align: center;
  color: var(--text-muted);
  font-size: 0.875rem;
  padding: 2rem 1rem;
}

.cart-items { display: flex; flex-direction: column; }

.cart-item {
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-rows: auto auto;
  gap: 2px 0.5rem;
  /* 0.375rem (antes 0.5rem) -- pedido del cliente piloto: carrito más
     grande, que se vea la mayoría de la lista sin scroll. Reduce el
     padding vertical de CADA fila -- más filas visibles en el mismo alto
     de .cart-body, sin tocar min-height/flex de .cart-body ni el
     max-height de .pos-cart (esos cálculos ya se rompieron dos veces
     antes, ver comentarios arriba). Verificado con capturas reales que
     los nombres largos con formato ("2 Cajas (72 unidades)") y el badge
     de descuento siguen legibles con este padding menor. */
  padding: 0.375rem 0;
  border-bottom: 1px solid var(--border);
}

.cart-item:last-child { border-bottom: none; }

.cart-item-name {
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--text);
  grid-column: 1;
  /* Bug real (reportado con nombres largos con formato, ej. "caja cerveza
     zulia (36 u.)"): un item de CSS grid tiene min-width:auto por defecto
     -- NO se encoge por debajo del ancho de su contenido intrínseco, así
     que un nombre largo empujaba/desbordaba la fila entera en vez de
     ajustarse, obligando a hacer zoom-out para leerlo completo (más
     visible en móvil, donde el ancho disponible ya es chico). min-width:0
     es la corrección estándar para que el item de grid SÍ se encoja al
     ancho de su columna; overflow-wrap para que una palabra/token largo
     salte de línea en vez de desbordar horizontalmente. Nunca se trunca
     con ellipsis a propósito -- el sufijo "(36 u.)" es información real,
     no debe ocultarse. */
  min-width: 0;
  overflow-wrap: break-word;
}

/* Antes .cart-item-line (solo Bs., quedó fuera de la estandarización de
   moneda del 2026-08-27) -- Fase 13 (2026-09-04): reemplazado por
   .money-stack (ver definición junto a .total-value), mismo componente que
   ahora reusa también el detalle de venta de pos.html. Conserva
   grid-column:1/min-width:0/overflow-wrap -- sigue siendo UN solo elemento
   de grid en esa celda, el mismo lugar que .cart-item-line ocupaba. */
.cart-item-price {
  grid-column: 1;
  min-width: 0;
  overflow-wrap: break-word;
}

.cart-item-controls {
  grid-column: 2;
  grid-row: 1 / 3;
  display: flex;
  align-items: center;
  gap: 3px;
}

.qty-btn {
  width: 24px; height: 24px;
  border: 1px solid var(--border);
  background: var(--bg-input);
  color: var(--text);
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-size: 1rem;
  line-height: 1;
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
}

.qty-btn:hover { border-color: var(--accent); color: var(--accent); }

.qty-display {
  width: 22px;
  text-align: center;
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  color: var(--text);
}

/* Cantidad editable por teclado (pedido del cliente piloto) -- reemplaza
   el <span> de solo lectura .qty-display en el carrito. Mismo tamaño/
   tipografía visual, para que la fila no cambie de alto ni se note como
   un input "de más" al lado de los botones +/-.
   Fase 20 (2026-09-08) -- pasó de <input type="number"> a <div
   contenteditable> (mismo bug de autocompletado de Chrome que el resto de
   campos de este proyecto, ver RUNBOOK.md). Las reglas de las flechas
   nativas (appearance:textfield/::-webkit-*outer/inner-spin-button) se
   quitaron -- un <div> no las tiene, quedaban 100% muertas apenas se hizo
   el cambio (a diferencia del caso de USD_P en sales.py, acá no hay
   ambigüedad posible: ningún <input type="number"> le queda a esta clase
   para que esas reglas alguna vez vuelvan a aplicar). white-space:nowrap
   -- un contenteditable envuelve texto por defecto, un <input> nunca; sin
   esto, un valor sanitizado más largo de lo esperado podría hacer crecer
   la fila en vez de desplazarse (mismo criterio que .ce-search-input). */
.qty-display-input {
  width: 32px;
  text-align: center;
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  color: var(--text);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  padding: 1px 0;
  white-space: nowrap;
  overflow: hidden;
}
.qty-display-input:hover {
  border-color: var(--border);
  background: var(--bg-input);
}
/* Foco visible y distinto del hover (auditoría Web Interface Guidelines,
   2026-09-24): antes :hover y :focus compartían regla, así que un campo con
   foco de teclado se veía idéntico a uno con el mouse encima. Borde + anillo
   de 1px extra en --accent (contraste >= 3:1 en ambos temas). */
.qty-display-input:focus {
  border-color: var(--accent);
  background: var(--bg-input);
  box-shadow: 0 0 0 1px var(--accent);
  outline: none;
}

/* ── Fase 9 Parte D -- descuento manual por línea (exclusivo Dueño) ──
   Fila propia, ancho completo, debajo de nombre/precio/controles de
   cantidad -- .cart-item es CSS Grid de 2 columnas x 2 filas explícitas
   (grid-template-rows: auto auto); esta fila cae sola en una 3ra fila
   implícita (grid-auto-rows: auto por default), sin tener que tocar esa
   declaración. grid-column: 1 / -1 para que ocupe las 2 columnas, no solo
   la primera. */
.cart-item-discount {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 2px;
}

.cart-discount-icon { font-size: 0.75rem; flex-shrink: 0; }

.cart-discount-badge {
  font-size: 0.75rem;
  color: var(--warning);
  font-weight: 600;
}

/* Botones tipo link (Descuento / Editar / Quitar) -- no son la acción
   principal de la línea (esa es +/-), un .btn normal se vería demasiado
   pesado en una fila ya angosta. */
.btn-inline-link {
  background: none;
  border: none;
  padding: 0;
  font-size: 0.75rem;
  color: var(--accent);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}

.btn-inline-link:hover { color: var(--accent-hover); }

/* Fase 21 (2026-09-08) -- pasó de <input type="number"> a <div
   contenteditable>, mismo bug de autocompletado de Chrome que
   .qty-display-input (misma fila, Fase 20). white-space:nowrap -- un
   contenteditable envuelve texto por defecto, un <input> nunca. */
.cart-discount-input {
  width: 70px;
  padding: 2px 6px;
  font-size: 0.75rem;
  font-family: var(--font-mono);
  border: 1px solid var(--border-focus);
  border-radius: var(--radius-sm);
  background: var(--bg-input);
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
}
.cart-discount-input:empty::before {
  content: attr(data-placeholder);
  color: var(--text-muted);
  pointer-events: none;
}

.cart-discount-ok, .cart-discount-cancel {
  width: 20px; height: 20px;
  border: 1px solid var(--border);
  background: var(--bg-input);
  color: var(--text);
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-size: 0.75rem;
  line-height: 1;
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
}

.cart-discount-ok:hover { border-color: var(--accent); color: var(--accent); }
.cart-discount-cancel:hover { border-color: var(--danger); color: var(--danger); }

.cart-footer {
  border-top: 1px solid var(--border);
  /* 10px 14px (antes 12px 16px) -- segunda pasada del carrito más grande:
     ganar unos px más para .cart-body sin afectar legibilidad. */
  padding: 10px 14px;
  /* El total/forma de pago/botones deben quedar SIEMPRE alcanzables sin
     scrollear el panel entero -- flex-shrink:0 (nunca se comprime por el
     algoritmo flex) le da a este footer su tamaño NATURAL completo
     siempre, sin importar el alto real de la pantalla (a diferencia del
     intento anterior con max-height en vh, que rompía en escritorio -- ver
     el comentario largo en .cart-body arriba). Si en el caso extremo el
     conjunto completo igual no cupiera en .pos-cart, la red de seguridad
     vive en el propio .pos-cart (overflow-y:auto ahí), no acá -- así el
     "sobrante" siempre lo absorbe .cart-body primero (su trabajo, tiene
     min-height como piso) antes de que este footer se vea afectado. */
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  /* 6px (antes 8px) -- mismo motivo que el padding de arriba: la Fase 10
     agregó 2 bloques nuevos (Cliente, Fecha de venta) a este footer sin
     volver a verificar el carrito completo -- ver docs/RUNBOOK.md,
     "Fase 10 -- segunda pasada del carrito". Este recorte es chico a
     propósito (no hay margen para más sin perder legibilidad real,
     verificado con capturas) -- el ahorro real viene de .cart-body
     arriba y de .vuelto-section/.pay-section abajo. */
  gap: 6px;
}

.cart-totals {
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.total-line {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  font-size: 0.8125rem;
  color: var(--text-muted);
}

.total-line--main {
  font-size: 1rem;
  font-weight: 600;
  color: var(--text);
}

.total-value { font-family: var(--font-mono); }

/* ── .money-stack -- formato de moneda estandarizado (2026-08-27), USD
   protagonista/bold + Bs. de apoyo chico/tenue debajo, empaquetado como
   componente reusable (Fase 13, 2026-09-04) -- antes cada pantalla
   repetía su propia versión (.cat-card-price/.cat-card-price-bs,
   .total-line/.total-line--main, .receipt-total-line--main/--sec). Se
   usa tal cual en el carrito de pos.html (cada línea de producto) y en el
   detalle de venta (Historial de ventas, Fase 13) -- no reemplaza los
   componentes viejos ya en uso en otras pantallas (cambiarlos no se pidió
   en esta fase), solo evita un CUARTO "reinventar la rueda" para código
   nuevo. Por defecto alineado a la izquierda (stack bajo un nombre, ej.
   .cart-item-price); .money-stack--right lo alinea a la derecha (stack al
   final de una fila flex, ej. .receipt-line). */
.money-stack {
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.money-stack--right { align-items: flex-end; text-align: right; }

.money-stack-usd {
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--text);
}

.money-stack-bs {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  color: var(--text-muted);
  font-weight: 400;
}

/* gap 0.25rem (antes 0.375rem) -- segunda pasada del carrito más grande,
   ver docs/RUNBOOK.md, "Fase 10 -- segunda pasada del carrito". */
.pay-section { display: flex; flex-direction: column; gap: 0.25rem; }

.pay-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.pay-label {
  font-size: 0.75rem;
  color: var(--text-muted);
  font-weight: 600;
  white-space: nowrap;
  min-width: 60px;
}

/* 5px 10px (antes 7px 10px) -- Fase 10, segunda pasada del carrito. */
.pay-select {
  flex: 1;
  padding: 5px 10px;
  font-size: 0.8125rem;
}

.sale-toggle {
  display: flex;
  background: var(--bg-input);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}

.sale-toggle-btn {
  flex: 1;
  padding: 5px 8px;
  background: none;
  border: none;
  color: var(--text-muted);
  cursor: pointer;
  font-size: 0.8125rem;
  font-weight: 600;
  transition: all 0.15s;
}

.sale-toggle-btn.active  { background: var(--accent); color: var(--bg); }

/* Payment method toggle — same visual as sale type toggle. Compartida con
   .cobro-pay-method-btn (mini-toggle de forma de pago por crédito pendiente
   dentro de "Cobrar/Devolver", Fase 17) -- misma apariencia, clase distinta
   a propósito: nunca deben responder al mismo querySelectorAll global (ver
   el comentario junto a su HTML en pos.html, renderCobroCredits()). */
.pay-method-btn,
.cobro-pay-method-btn,
.ret-pay-method-btn {
  flex: 1;
  padding: 5px 8px;
  background: none;
  border: none;
  color: var(--text-muted);
  cursor: pointer;
  font-size: 0.8125rem;
  font-weight: 600;
  transition: all 0.15s;
}
.pay-method-btn.active,
.cobro-pay-method-btn.active,
.ret-pay-method-btn.active { background: var(--accent); color: var(--bg); }

/* Fase H (2026-09-15) -- panel de "Pagar" un envase retornable (en vez de
   devolverlo), retornables.html/pos.html (Cobrar/Devolver). .ret-pay-method-btn
   arriba es una clase PROPIA (no comparte querySelectorAll con
   .pay-method-btn/.cobro-pay-method-btn), mismo criterio ya establecido para
   esas dos. */
.ret-pay-panel {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-top: 6px;
  padding: 8px;
  background: var(--bg-input);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.ret-pay-preview {
  font-size: 0.8125rem;
  color: var(--text-muted);
}
.ret-pay-actions {
  display: flex;
  gap: 6px;
}

/* Fase 10 (segunda pasada, 2026-09-01) -- el fondo/borde/padding de
   "tarjeta" ahora vive en #customer-section-fields (el contenido
   EXPANDIDO), no en el wrapper #customer-section -- antes se aplicaba
   siempre, incluso cuando solo había que mostrar el link colapsado "👤
   Cliente (opcional)", desperdiciando ~20px de alto de .cart-footer por
   nada (padding + borde alrededor de una sola línea de texto). Ver
   docs/RUNBOOK.md, "Fase 10 -- segunda pasada del carrito". */
#customer-section-fields {
  background: rgba(255,255,255,0.025);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.625rem;
}

.new-cust-form {
  margin-top: 0.5rem;
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
}

/* Fase 21 (2026-09-08) -- #nc-name/#nc-phone/#ret-nc-name/#ret-nc-phone
   pasaron de <input> a <div class="ce-search-input"> (mismo bug de
   autocompletado de Chrome) -- el selector original solo alcanzaba a
   <input>. */
.new-cust-form input,
.new-cust-form .ce-search-input { padding: 8px 10px; font-size: 0.875rem; }

/* ── Bloqueo de crédito con deuda pendiente (pos.html) ────────────── */
.credit-block {
  margin-top: 0.625rem;
  padding: 0.625rem 0.75rem;
  background: var(--warning-dim);
  border: 1px solid var(--warning-border);
  border-radius: var(--radius-sm);
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.credit-block-msg strong {
  color: var(--warning);
  font-size: 0.8125rem;
}

.credit-block-msg p {
  margin: 0.2rem 0 0;
  font-size: 0.8125rem;
  color: var(--text-secondary);
  line-height: 1.4;
}

.credit-block-msg #credit-block-amt {
  font-family: var(--font-mono);
  color: var(--warning);
  font-weight: 600;
}

.credit-block-actions {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.credit-block-actions .btn { width: auto; flex: 1; }

#credit-override-toggle-wrap { text-align: left; }

#credit-override-panel {
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
  padding-top: 0.5rem;
  border-top: 1px solid var(--warning-border);
}

.credit-override-hint {
  margin: 0;
  font-size: 0.78rem;
  color: var(--text-secondary);
  line-height: 1.4;
}

#credit-override-pin { padding: 8px 10px; font-size: 0.875rem; letter-spacing: 0.15em; }

.cart-actions {
  display: flex;
  gap: 0.5rem;
}

/* Bug real (confirmado con capturas reales de un teléfono): existe una
   regla global @media (max-width:480px) { .btn-inline { width:100% } }
   (pensada para botones de diálogo que se apilan en pantallas chicas) --
   #btn-clear-cart tiene la clase .btn-inline, así que en un teléfono real
   (375px, bajo ese breakpoint) intentaba ser 100% de ancho DENTRO de este
   flex row, junto al botón "Confirmar venta" (flex:1) -- los dos juntos
   superan el 100% del panel y se ven cortados en el borde. Misma clase de
   bug ya corregida dos veces antes en este archivo con el mismo patrón
   (.credit-block-actions .btn / .cobro-row-actions .btn) -- nunca se
   había aplicado acá. .cart-actions .btn (dos clases) gana en
   especificidad sobre .btn-inline (una clase) sin importar el orden. */
.cart-actions .btn { width: auto; }
.cart-actions .btn-primary { flex: 1; }

/* ── Mobile cart bar + drawer ───────────────────────────────────── */

.cart-bar {
  display: none; /* shown on mobile via media query */
}

.cart-backdrop {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.55);
  z-index: 190;
}

.cart-backdrop.visible { display: block; }

/* ── Receipt dialog ─────────────────────────────────────────────── */

.receipt-success {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 1rem;
  font-weight: 700;
  color: var(--accent);
  margin-bottom: 0.875rem;
}

.receipt-items {
  display: flex;
  flex-direction: column;
  gap: 3px;
  margin-bottom: 0.625rem;
}

.receipt-line {
  display: flex;
  justify-content: space-between;
  font-size: 0.8125rem;
  color: var(--text-secondary);
}

.receipt-divider {
  border: none;
  border-top: 1px solid var(--border);
  margin: 0.5rem 0;
}

.receipt-totals { margin-bottom: 0.625rem; }

.receipt-total-line {
  display: flex;
  justify-content: space-between;
  font-size: 0.875rem;
  font-family: var(--font-mono);
}

.receipt-total-line--main {
  font-size: 1rem;
  font-weight: 700;
  color: var(--text);
}

.receipt-total-line--sec { color: var(--text-muted); font-size: 0.8125rem; }

.receipt-meta {
  font-size: 0.8125rem;
  color: var(--text-muted);
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
  margin-bottom: 0.5rem;
}

.receipt-warning {
  background: var(--warning-dim);
  border: 1px solid var(--warning-border);
  border-radius: var(--radius-sm);
  padding: 0.5rem 0.625rem;
  font-size: 0.8125rem;
  color: var(--warning);
  margin-top: 0.5rem;
}

.receipt-error {
  color: var(--error);
  font-size: 0.9rem;
  padding: 0.5rem 0;
}

/* ── Vuelto (change calculator) ────────────────────────────────── */

.vuelto-section {
  background: rgba(255,255,255,0.025);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  /* 0.375rem/0.25rem (antes 0.5rem/0.375rem) -- segunda pasada del
     carrito más grande. Este bloque ("Pagó con" + grilla "A cobrar") es
     el más alto del footer (134px medido real) -- el recorte más grande
     posible sin perder la grilla de 3 monedas ni la legibilidad del
     monto. Ver docs/RUNBOOK.md, "Fase 10 -- segunda pasada del carrito",
     para las mediciones completas de qué se recortó y cuánto liberó. */
  padding: 0.375rem;
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

.pago-input-wrap {
  display: flex;
  gap: 4px;
  flex: 1;
}
/* Fase 17 (2026-09-05) -- #pago-input pasó de <input type="number"> a
   <div class="ce-search-input pago-ce-input" contenteditable> (mismo
   dropdown de contraseñas de Chrome que ya motivó la técnica en los 4
   buscadores de texto del proyecto, ver "Fase 14"/"Fase 15" en
   RUNBOOK.md -- este campo también lo disparaba). Base visual
   (fondo/borde/radio/foco) viene de .ce-search-input; acá solo se pisa lo
   que este campo necesita distinto (alineado a la derecha, monoespaciado,
   más chico que el padding por defecto de .ce-search-input). */
.pago-input-wrap .pago-ce-input {
  flex: 1;
  min-width: 0;
  /* 5px (antes 7px) -- Fase 10, segunda pasada del carrito. */
  padding: 5px 10px;
  font-family: var(--font-mono);
  text-align: right;
  /* Causa real de por qué el recorte de padding de arriba casi no se
     notaba en el <input> original: input[type="number"] (regla global,
     ~línea 191) fijaba font-size:1rem / line-height:1.5 con la misma
     especificidad, ganando por orden de cascada para todo lo que esa
     regla NO pisara explícitamente. 46px de alto real para un solo campo,
     en un footer donde el resto del texto usa 0.75-0.8125rem. Encontrado
     midiendo con Playwright (computed style), no a ojo -- ver
     docs/RUNBOOK.md, "Fase 10 -- segunda pasada del carrito". Ya no aplica
     tal cual (un <div> no matchea ese selector de tipo), pero el
     font-size/line-height explícitos se conservan a propósito. */
  font-size: 0.8125rem;
  line-height: 1.3;
}
.pago-input-wrap select {
  font-size: 0.8125rem;
  line-height: 1.3;
  /* La regla global `select {}` (~línea 1156) fija padding:12px 16px --
     esta regla (misma especificidad, un elemento + un atributo/clase)
     gana por orden de cascada para lo que SÍ pisa, pero el padding se
     quedaba sin pisar. Confirmado real con getComputedStyle antes de
     este fix: "12px 36px 12px 16px" a pesar del font-size ya reducido. */
  padding: 5px 28px 5px 10px;
}
.pago-input-wrap select { width: auto; min-width: 72px; }

.vuelto-display {
  font-size: 0.8125rem;
  min-height: 1.4em;
}

.vuelto-due   { color: var(--text-muted); font-size: .75rem; }
.vuelto-ok    { color: var(--accent); font-size: .75rem; }
.vuelto-short { color: var(--danger); font-size: .8125rem; }

.vuelto-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 4px;
  margin-top: 4px;
}
.vuelto-grid-item {
  font-family: var(--font-mono);
  font-size: .75rem;
  text-align: center;
  background: rgba(255,255,255,.04);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 4px 3px;
  line-height: 1.3;
  /* Bug real (reportado con capturas reales, panel de escritorio de 320px
     -- más angosto que el drawer móvil, que ocupa el ancho completo del
     viewport): mismo bug de siempre en un item de CSS grid -- min-width
     es auto por defecto, no se encoge por debajo de su contenido
     intrínseco. Acá el contenido es un monto largo sin espacios (ej.
     "Bs.22.060,47"), una sola "palabra" -- overflow-wrap solo no alcanza
     porque no hay ningún punto natural donde partir; word-break:break-all
     sí lo garantiza (parte en cualquier carácter si hace falta, aceptable
     acá porque son 3 celdas angostas de un solo número, no texto para
     leer con fluidez). */
  min-width: 0;
  overflow-wrap: break-word;
  word-break: break-all;
}
.vuelto-grid-item span {
  display: block;
  font-size: .6rem;
  color: var(--text-muted);
  font-family: var(--font-sans);
  margin-bottom: 1px;
}
.vuelto-grid-item--hl {
  border-color: var(--accent-border);
  background: var(--accent-dim);
  color: var(--accent);
}

/* ── Retornables dialog ─────────────────────────────────────────── */

.ret-item {
  padding: 0.625rem 0;
  border-bottom: 1px solid var(--border);
}
.ret-item:last-child { border-bottom: none; }

.ret-item-name {
  font-weight: 600;
  font-size: .875rem;
  color: var(--text);
  margin-bottom: 2px;
}
.ret-item-desc {
  font-size: .75rem;
  color: var(--text-muted);
  margin-bottom: .5rem;
}
.ret-options {
  display: flex;
  flex-direction: column;
  gap: .25rem;
}
.ret-option {
  display: flex;
  align-items: flex-start;
  gap: .5rem;
  cursor: pointer;
  font-size: .8125rem;
  color: var(--text-secondary);
  padding: 3px 0;
  line-height: 1.4;
}
.ret-option input[type="radio"] {
  width: auto;
  cursor: pointer;
  margin-top: 2px;
  flex-shrink: 0;
  accent-color: var(--accent);
}
.ret-option:has(input:checked) { color: var(--text); }

/* Envases parciales (2026-10-08) -- campo "Envases que trajo: [__] de N",
   atajos Todos/Ninguno y texto "Quedan X en deuda". En móvil (<= 480px) los
   atajos bajan debajo del campo en vez de apretarse a su lado. */
.ret-dev {
  margin: 2px 0 4px 1.5rem;
  display: flex;
  flex-direction: column;
  gap: .375rem;
}
.ret-dev[hidden] { display: none; }
.ret-dev-row {
  display: flex;
  align-items: center;
  gap: .5rem;
  flex-wrap: wrap;
  font-size: .8125rem;
  color: var(--text-secondary);
}
.ret-dev-input {
  width: 5rem;
  padding: 5px 10px;
  font-family: var(--font-mono);
  text-align: right;
}
.ret-dev-actions {
  display: flex;
  gap: .5rem;
}
.ret-dev-actions .btn { width: auto; min-height: 44px; }
.ret-deuda-msg {
  font-size: .8125rem;
  font-weight: 600;
  color: var(--text-secondary);
}
/* Con deuda: color de texto pleno (contraste AA en claro y oscuro) + borde
   izquierdo de advertencia, en vez de depender solo de un amarillo que en
   modo claro daba 3.1:1. */
.ret-deuda-msg--deuda {
  color: var(--text);
  border-left: 3px solid var(--warning);
  padding-left: .5rem;
}
@media (max-width: 480px) {
  .ret-dev { margin-left: 0; }
  .ret-dev-actions .btn { flex: 1; }
}

.ret-extra-total {
  margin-top: .625rem;
  padding: .5rem .625rem;
  background: var(--info-dim);
  border: 1px solid var(--info-border);
  border-radius: var(--radius-sm);
  font-size: .875rem;
  color: var(--text-secondary);
}

.ret-cust-section {
  margin-top: .75rem;
  padding-top: .75rem;
  border-top: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: .375rem;
}
.ret-cust-header {
  font-size: .8rem;
  color: var(--danger);
  line-height: 1.4;
  margin-bottom: 2px;
}

/* ── Widget "Cobrar / Devolver" (pos.html, cajero-safe) ──────────── */

.cobro-section {
  margin-top: 0.875rem;
  padding-top: 0.75rem;
  border-top: 1px solid var(--border);
}

.cobro-section-title {
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--text-secondary);
  margin-bottom: 0.5rem;
}

.cobro-row {
  padding: 0.625rem 0;
  border-bottom: 1px solid var(--border);
}
.cobro-row:last-child { border-bottom: none; }

.cobro-row-info {
  font-size: 0.8125rem;
  color: var(--text);
  margin-bottom: 0.375rem;
}

.cobro-row-actions {
  display: flex;
  gap: 0.5rem;
  align-items: center;
}
/* 110px (antes 90px, luego 80px al agregar el <select> de moneda en la
   Fase 17) -- el recorte a 80px clipeaba montos de 3+ dígitos: un <input
   type="number"> se renderiza con el separador decimal/miles del locale
   del navegador (coma en es-*, no el punto de .value), así que "500.00"
   se dibuja como "500,00" y ya no entraba. Encontrado con una captura
   real (Playwright), no a ojo -- el diálogo tiene de sobra 438px de ancho
   (medido con getBoundingClientRect), así que no hace falta competir por
   espacio con el <select>/botón de al lado. */
/* Fase 21 (2026-09-08) -- #cobro-credit-amt-* pasó de <input type="number">
   a <div class="ce-search-input pago-ce-input"> (mismo bug de
   autocompletado de Chrome que #pago-input) -- el selector original solo
   alcanzaba a <input>. */
.cobro-row-actions input,
.cobro-row-actions .ce-search-input {
  width: 110px;
  flex-shrink: 0;
}
/* Fase 17 -- moneda del pago, entre el monto y "Pagar" (antes solo había
   input + botón, siempre implícito en Bs.). Ancho fijo chico, igual
   criterio que .pago-input-wrap select del carrito -- no debe competir por
   espacio con el input del monto en una fila ya angosta. */
.cobro-row-actions select {
  flex: 0 0 auto;
  width: auto;
  min-width: 64px;
  padding: 5px 26px 5px 8px;
  font-size: 0.8125rem;
}
.cobro-row-actions .btn { width: auto; flex-shrink: 0; }

/* Fase 17 -- mini-toggle de forma de pago (Efectivo/Transferencia) por
   crédito pendiente, entre la info de la deuda y el monto a pagar. Mismo
   componente visual que .sale-toggle, más chico (esta fila ya compite por
   espacio con .cobro-row-actions debajo). */
.cobro-row-method {
  margin-bottom: 0.375rem;
  max-width: 220px;
}

/* ── POS mobile (≤ 767px) ───────────────────────────────────────── */

@media (max-width: 767px) {
  .pos-layout { grid-template-columns: 1fr; }

  .pos-cart {
    position: fixed;
    /* top: auto explícito -- sin esto, el `top: 80px` de la regla base
       (pensada para position:sticky en desktop) queda heredado acá, y
       combinado con bottom:0 fija la altura real a "viewport - 80px" en
       vez de dejar que max-height (92dvh) sea el único límite -- causaba
       que el presupuesto real disponible fuera distinto (y más chico) al
       que max-height dejaba suponer, encontrado midiendo con Chrome real
       (getBoundingClientRect), no a ojo. */
    top: auto;
    bottom: 0; left: 0; right: 0;
    max-height: 92dvh;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    border-left: none; border-right: none; border-bottom: none;
    z-index: 200;
    transform: translateY(100%);
    transition: transform 0.28s cubic-bezier(0.4, 0, 0.2, 1);
  }

  .pos-cart.open { transform: translateY(0); }

  .cart-close { display: flex; }

  .cart-bar {
    display: flex;
    position: fixed;
    bottom: 0; left: 0; right: 0;
    background: var(--accent);
    color: var(--bg);
    padding: 0.875rem 1.25rem;
    align-items: center;
    justify-content: space-between;
    z-index: 100;
    cursor: pointer;
    border-top: 1px solid rgba(255,255,255,0.15);
    font-weight: 600;
    font-size: 0.9375rem;
    user-select: none;
    -webkit-tap-highlight-color: transparent;
  }

  .cart-bar:active { background: var(--accent-hover); }

  .pos-main { padding-bottom: 4.5rem; }

  .cat-grid { grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); }
}

/* ── Créditos page ──────────────────────────────────────────────── */

.credit-summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 16px 20px;
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  margin-bottom: 2rem;
}

.credit-summary-main {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.credit-summary-label {
  font-size: 0.75rem;
  color: var(--text-muted);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.credit-summary-value {
  font-size: 1.5rem;
  font-weight: 600;
  color: var(--text);
  font-family: var(--font-mono);
  line-height: 1.2;
}

.credit-summary-count {
  font-size: 0.8125rem;
  color: var(--text-muted);
  align-self: flex-end;
}

.credit-list {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.credit-card {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  overflow: hidden;
  transition: border-color 0.15s;
}

.credit-card:hover { border-color: var(--border-focus); }

.credit-card-main {
  padding: 16px 20px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.credit-card-info {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.credit-card-name {
  font-size: 1rem;
  font-weight: 600;
  color: var(--text);
}

/* Punto 1 (2026-09-12, clientes.html) -- antes un solo string
   "44445555 · 179718989 · laventana@gmail.com" sin decir qué era cada
   dato; ahora cada uno etiquetado (Tel/CI-RIF/Email) en su propio
   <span class="cli-contact-item">, para que .cli-contact-item nunca se
   corte a la mitad -- envuelve el ítem completo (etiqueta + valor juntos)
   a su propia línea en una tarjeta angosta, en vez de partir justo entre
   la etiqueta y el valor. flex-wrap en el contenedor, no una sola línea
   fija: con los 3 datos cargados y la tarjeta angosta, se acomodan en 2-3
   líneas solos, sin ningún ancho fijo que adivinar. */
.cli-contact-row {
  display: flex;
  flex-wrap: wrap;
  row-gap: 2px;
  column-gap: 0.875rem;
  font-size: 0.8125rem;
  color: var(--text-secondary);
}

.cli-contact-row--empty { font-style: italic; color: var(--text-muted); }

.cli-contact-item { white-space: nowrap; }

.cli-contact-label {
  color: var(--text-muted);
  font-weight: 600;
}

/* ── Semáforo de deuda (Fase B, Bloque 3, clientes.html) ─────────────────
   🟢 sin deuda / 🟡 con deuda sin vencer / 🔴 con deuda vencida -- un punto
   de color antes del nombre, nunca un badge de texto (sería redundante con
   el detalle expandible de la misma tarjeta). */
.cliente-semaforo {
  display: inline-block;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  margin-right: 6px;
  flex-shrink: 0;
}
.cliente-semaforo--verde    { background: var(--accent); }
.cliente-semaforo--amarillo { background: var(--warning); }
.cliente-semaforo--rojo     { background: var(--danger); }

/* Multi-currency amount grid — used in cards, dialog, and summary banner */
.credit-amts {
  display: flex;
  gap: 0;
}

.credit-amt {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 0.375rem 0.625rem;
  border-right: 1px solid var(--border);
}

.credit-amt:first-child { padding-left: 0; }
.credit-amt:last-child  { border-right: none; }

.credit-amt-label {
  font-size: 0.625rem;
  color: var(--text-muted);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.credit-amt-value {
  font-size: 0.9375rem;
  font-weight: 700;
  color: var(--text);
  font-family: var(--font-mono);
  white-space: nowrap;
}

/* Formato de moneda estandarizado (2026-08-27): USD protagonista/bold,
   Bs. de apoyo en tono tenue -- ver docs/RUNBOOK.md, sección
   "Estandarización de formato de moneda". */
.credit-amt--bs .credit-amt-value {
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--text-muted);
}

.credit-card-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  flex-wrap: wrap;
}

.credit-card-actions {
  display: flex;
  gap: 0.5rem;
}

/* Misma clase de bug ya corregida antes en este archivo (.cart-actions
   .btn / .credit-block-actions .btn, ver esos comentarios) -- .btn-primary
   trae `width:100%` (pensado para un botón solo en su fila, ej. "Confirmar
   venta"). Acá "Registrar pago" es uno de 4 botones en la misma fila --
   sin este override, en cuanto la fila necesita envolver (Punto 3,
   modificador --compact más abajo) ese 100% lo hace reclamar la fila
   entera para sí solo, empujándose a su propia línea en vez de compartir
   fila con los otros 3 -- lo opuesto de "más compacto". */
.credit-card-actions .btn { width: auto; }

/* Punto 3 (2026-09-12) -- tarjetas de Cuentas por Cobrar (Ver detalle /
   WhatsApp / Copiar mensaje / Registrar pago) más compactas: menos
   padding/alto general y los 4 botones más chicos -- misma información y
   funciones, solo menos espacio vertical. Modificador aplicado SOLO en
   creditos.html (ver `card.className` ahí) -- clientes.html, gestion.html
   y pos.html siguen usando `.credit-card` tal cual, sin este cambio, no
   fue lo que se pidió ahí. Los botones se achican con padding/font-size,
   nunca con `transform:scale` (eso encogería también el área táctil real,
   no solo la visual) -- 7px 10px / 0.75rem sigue siendo cómodo de tocar
   en un teléfono real (verificado con capturas reales, ver RUNBOOK.md). */
.credit-card--compact .credit-card-main { padding: 10px 14px; gap: 6px; }
.credit-card--compact .credit-card-info { gap: 0.25rem; }
.credit-card--compact .credit-card-name { font-size: 0.9375rem; }
.credit-card--compact .credit-amt { padding: 0.25rem 0.5rem; }
/* Bug real preexistente, encontrado con capturas reales al compactar (no
   introducido acá): sin `white-space:nowrap`, un botón que no entra en la
   fila se ENCOGE en vez de ir entero a la línea de abajo -- el texto de
   "Ver detalle"/"Copiar mensaje" partía en 2 líneas dentro del propio
   botón (ya pasaba antes de este cambio, solo menos notorio con los
   botones más grandes de siempre). `flex-wrap: wrap` en el contenedor +
   `white-space: nowrap` en cada botón -- ahora el que no entra baja
   ENTERO a la fila siguiente, nunca parte su texto a la mitad. */
.credit-card--compact .credit-card-actions { gap: 0.375rem; flex-wrap: wrap; }
.credit-card--compact .credit-card-actions .btn {
  /* Vertical igual a .btn-sm de siempre (8px) -- el ahorro real de
     espacio viene del padding horizontal y el font-size más chicos, sin
     bajar el alto táctil por debajo del estándar ya usado en el resto de
     la app. */
  padding: 8px 10px;
  font-size: 0.75rem;
  white-space: nowrap;
}

/* Days-past-due urgency badges — 0-14 d: green, 15-29 d: amber, 30+ d: red */
.dias-badge {
  display: inline-flex;
  align-items: center;
  padding: 3px 10px;
  border-radius: 999px;
  font-size: 0.6875rem;
  font-weight: 600;
  white-space: nowrap;
  background: var(--accent-dim);
  border: 1px solid var(--accent-border);
  color: var(--accent);
}

.dias-badge--warn {
  background: var(--warning-dim);
  border-color: var(--warning-border);
  color: var(--warning);
}

.dias-badge--danger {
  background: var(--error-bg);
  border-color: var(--error-border);
  color: var(--danger);
}

/* Accordion — per-credit breakdown */
.credit-detail {
  border-top: 1px solid var(--border);
  padding: 0.75rem 1.125rem;
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
  background: rgba(255,255,255,0.012);
}

.credit-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.5rem 0;
  border-bottom: 1px solid var(--border);
  flex-wrap: wrap;
}

.credit-row:last-child { border-bottom: none; }

.credit-row-id {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  color: var(--text-muted);
  flex-shrink: 0;
}

.credit-row-origin {
  font-size: 0.8125rem;
  color: var(--text-secondary);
  flex: 1;
  min-width: 80px;
}

.credit-row-nums {
  display: flex;
  gap: 1rem;
  font-size: 0.8125rem;
  color: var(--text-secondary);
  flex-wrap: wrap;
}

.credit-row-saldo {
  color: var(--text);
  font-weight: 600;
}

.badge-pending {
  background: var(--info-dim);
  border: 1px solid var(--info-border);
  color: var(--info);
}

.detail-empty {
  font-size: 0.875rem;
  color: var(--text-muted);
  text-align: center;
  padding: 0.375rem;
}

/* ── Credit detail — sale blocks ─────────────────────────────────── */
.credit-sale-block {
  padding: 0.625rem 0;
  border-bottom: 1px solid var(--border);
}
.credit-sale-block:last-child { border-bottom: none; }

.credit-sale-header {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex-wrap: wrap;
  margin-bottom: 0.375rem;
}

.credit-sale-date {
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--text-secondary);
  flex-shrink: 0;
  min-width: 90px;
}

.credit-sale-amts {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex: 1;
  flex-wrap: wrap;
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  color: var(--text-primary);
}

.credit-sale-saldo {
  color: var(--warning);
  font-weight: 600;
}

.credit-items-list {
  list-style: none;
  margin: 0 0 0 0.25rem;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
}

.credit-item-row {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  flex-wrap: wrap;
  padding: 0.125rem 0;
}

.credit-item-name {
  font-size: 0.8125rem;
  color: var(--text-primary);
  flex: 1;
  min-width: 100px;
}

.credit-item-qty {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  color: var(--text-secondary);
  flex-shrink: 0;
}

.credit-item-price {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  color: var(--text-muted);
  flex-shrink: 0;
}

.detail-manual {
  font-size: 0.8125rem;
  color: var(--text-muted);
  font-style: italic;
  padding: 0.25rem 0;
}

.detail-loading {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.8125rem;
  color: var(--text-muted);
  padding: 0.375rem 0;
}

/* Payment dialog extras */
.pago-deuda-actual {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 0.75rem 1rem;
  background: rgba(255,255,255,0.03);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  margin-bottom: 1rem;
}

/* When credit-amts is inside the dialog, first cell has no left-padding gap */
.pago-deuda-actual .credit-amt:first-child { padding-left: 0; }

.pago-deuda-label {
  font-size: 0.875rem;
  color: var(--text-muted);
}

.pago-deuda-valor {
  font-size: 1.125rem;
  font-weight: 700;
  font-family: var(--font-mono);
  color: var(--text);
}

.pago-equiv {
  font-size: 0.8125rem;
  color: var(--text-muted);
  min-height: 1.25em;
  margin-top: 4px;
}

.pay-preview {
  margin-top: 0.625rem;
  font-size: 0.875rem;
  min-height: 1.375em;
  line-height: 1.5;
  color: var(--text-muted);
}

.pay-preview--ok      { color: var(--accent); }
.pay-preview--partial { color: var(--warning); }
.pay-preview--over    { color: var(--danger); }

@media (max-width: 480px) {
  .credit-card-info  { flex-direction: column; align-items: flex-start; gap: 2px; }
  .credit-card-deuda { font-size: 1rem; }
  .credit-row-nums   { gap: 0.5rem; }
  .credit-summary    { flex-direction: column; align-items: flex-start; gap: 0.25rem; }
  .credit-summary-count { align-self: flex-start; }
}

/* ── Retornables page ───────────────────────────────────────────── */

.ret-summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.875rem 1.25rem;
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  margin-bottom: 1.25rem;
}

.ret-summary-main {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.ret-summary-label {
  font-size: 0.75rem;
  color: var(--text-muted);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.ret-summary-stats {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
}

.ret-summary-units {
  font-size: 1.375rem;
  font-weight: 700;
  color: var(--text);
  font-family: var(--font-mono);
  line-height: 1.2;
}

.ret-summary-sep {
  color: var(--text-muted);
  font-size: 1rem;
}

.ret-summary-value {
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--accent);
  font-family: var(--font-mono);
}

/* Bs. de apoyo en tono tenue, dentro de .ret-summary-value / .ret-value-bs
   -- formato de moneda estandarizado (2026-08-27), ver docs/RUNBOOK.md. */
.ret-summary-value-bs {
  font-weight: 400;
  color: var(--text-muted);
  font-size: 0.8125rem;
}

.ret-summary-count {
  font-size: 0.8125rem;
  color: var(--text-muted);
  align-self: flex-end;
  white-space: nowrap;
}

.ret-list {
  display: flex;
  flex-direction: column;
  gap: 0.625rem;
}

/* Customer card */
.ret-card {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  overflow: hidden;
  transition: border-color 0.15s;
}

.ret-card:hover { border-color: var(--border-focus); }

.ret-card-header {
  padding: 12px 20px;
  border-bottom: 1px solid var(--border);
  background: rgba(255,255,255,0.02);
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  flex-wrap: wrap;
}

.ret-card-name {
  font-size: 1rem;
  font-weight: 600;
  color: var(--text);
}

/* Fase A (2026-09-08) -- depósito retenido POR ESE CLIENTE, discreto a
   propósito (nunca protagonista): tamaño chico, color tenue -- el total
   agregado de TODOS los clientes (.ret-summary-value, arriba de la lista)
   sigue siendo la única cifra en tamaño grande de esta pantalla. Vacío
   (nunca "$0.00") cuando ninguno de sus productos tiene precio de envase
   cargado -- ver customerValueBs() en el <script>. */
.ret-card-deposito {
  font-size: 0.75rem;
  color: var(--text-muted);
  font-family: var(--font-mono);
  white-space: nowrap;
}

.ret-products {
  display: flex;
  flex-direction: column;
}

/* Individual product row */
.ret-product-row {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 12px 20px;
  border-bottom: 1px solid var(--border);
}

.ret-product-row:last-child { border-bottom: none; }

.ret-product-info {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  flex-wrap: wrap;
}

.ret-product-name {
  font-size: 0.9375rem;
  font-weight: 500;
  color: var(--text);
  flex: 1;
  min-width: 120px;
}

.ret-qty-badge {
  display: inline-flex;
  align-items: center;
  padding: 3px 10px;
  border-radius: 999px;
  font-size: 0.6875rem;
  font-weight: 600;
  white-space: nowrap;
  background: var(--info-dim);
  border: 1px solid var(--info-border);
  color: var(--info);
}

.ret-value-bs {
  font-size: 0.8125rem;
  font-family: var(--font-mono);
  color: var(--accent);
  white-space: nowrap;
}

/* Controls: stepper + devolver button */
.ret-controls {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  flex-wrap: wrap;
}

.ret-stepper {
  display: flex;
  align-items: stretch;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  overflow: hidden;
}

.ret-stepper-btn {
  width: 32px;
  background: var(--bg-input);
  border: none;
  color: var(--text-secondary);
  font-size: 1.125rem;
  line-height: 1;
  cursor: pointer;
  transition: background 0.12s, color 0.12s;
  flex-shrink: 0;
}

.ret-stepper-btn:hover {
  background: var(--accent);
  color: var(--bg);
}

/* Fase 21 (2026-09-08) -- pasó de <input type="number"> a <div
   contenteditable> (mismo bug de autocompletado de Chrome, mismo patrón que
   .qty-display-input en pos.html, Fase 20 -- ver docs/RUNBOOK.md). Las
   reglas de las flechas nativas (-moz-appearance:textfield/::-webkit-*
   -spin-button) se quitaron -- un <div> no las tiene, quedan 100% muertas.
   white-space:nowrap -- un contenteditable envuelve texto por defecto, un
   <input> nunca; sin esto, un valor sanitizado más largo de lo esperado
   podría hacer crecer la fila en vez de desplazarse. */
.ret-qty-field {
  width: 52px;
  background: var(--bg-input);
  border: none;
  border-left: 1px solid var(--border);
  border-right: 1px solid var(--border);
  color: var(--text);
  font-family: var(--font-mono);
  font-size: 0.9375rem;
  text-align: center;
  padding: 0.25rem 0.25rem;
  white-space: nowrap;
  overflow: hidden;
}

.ret-qty-field:focus {
  outline: none;
  background: var(--bg-card);
  border-left-color: var(--accent);
  border-right-color: var(--accent);
  box-shadow: inset 0 0 0 2px var(--accent); /* anillo completo, no solo los 2 bordes laterales */
}

/* Feedback message per row */
.ret-row-msg {
  font-size: 0.8125rem;
  line-height: 1.4;
  padding: 2px 0;
}

.ret-row-msg--warn  { color: var(--warning); }
.ret-row-msg--ok    { color: var(--accent); }
.ret-row-msg--error { color: var(--danger); }

@media (max-width: 480px) {
  .ret-summary       { flex-direction: column; align-items: flex-start; gap: 0.25rem; }
  .ret-summary-count { align-self: flex-start; }
  .ret-summary-stats { flex-wrap: wrap; }
  .ret-product-info  { gap: 0.375rem; }
  .ret-controls      { gap: 0.5rem; }
  /* .gs-rent-metrics -- el intento de columna doble para móvil vivía acá,
     pero nunca tuvo efecto real: `.gs-rent-metrics { grid-template-columns:
     repeat(4,1fr); }` (más abajo en este archivo, sección "Gastos /
     Rentabilidad") no está envuelto en media query -- misma especificidad,
     y por orden de aparición en el archivo esa regla incondicional
     GANABA siempre a esta, sin importar el ancho real de pantalla.
     Confirmado con captura real (2026-09-13, ver docs/RUNBOOK.md, sección
     "Regresión de móvil del rebranding"): las 4 tarjetas seguían en una
     sola fila desbordada a 375px. Movida junto a la regla base para que el
     orden en cascada la respete -- ver esa sección más abajo. */
  .gs-breakdown-row  { flex-wrap: wrap; }
}

/* ── Gastos / Rentabilidad ──────────────────────────────────────────── */

.gs-rent-card {
  padding: 20px 20px 16px;
}

.gs-rent-metrics {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 12px;
  margin-top: 16px;
}

/* Regresión de móvil del rebranding (2026-09-13) -- ver el comentario junto
   a `@media (max-width:480px)` más arriba en este archivo: esta regla tiene
   que aparecer DESPUÉS de la base para que la cascada la respete (misma
   especificidad, gana la última en orden de aparición). 2 columnas a
   ≤480px, 1 columna a ≤360px (4 tarjetas siguen sin entrar en 2 columnas
   de ~155px con padding real en un teléfono angosto -- confirmado con
   captura real). */
@media (max-width: 480px) {
  .gs-rent-metrics { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 360px) {
  .gs-rent-metrics { grid-template-columns: 1fr; }
}

.metric-value--positive { color: var(--accent); }
.metric-value--negative { color: var(--danger); }

.gs-warn {
  display: flex;
  align-items: flex-start;
  gap: 0.625rem;
  padding: 0.625rem 0.875rem;
  border-radius: var(--radius-sm);
  background: var(--warning-dim);
  border: 1px solid var(--warning-border);
  color: var(--warning);
  font-size: 0.8125rem;
  line-height: 1.5;
  margin-top: 0.875rem;
}

.gs-nota {
  margin-top: 0.75rem;
  font-size: 0.75rem;
  color: var(--text-muted);
  font-style: italic;
  line-height: 1.5;
}

.gs-section-card {
  padding: 16px 20px;
  margin-top: 2rem;
}

/* Punto 2 (2026-09-12) -- "Cómo entró el dinero hoy" y "Alertas" en
   dashboard.html usan `class="card"` a secas, sin `.gs-section-card` ni
   ningún padding propio -- el contenido (nombre de producto, monto,
   los 2 botones de WhatsApp de un crédito vencido) quedaba literalmente
   pegado a los 4 bordes de la tarjeta, incluida la esquina redondeada
   (`border-radius`), que con texto/botones justo encima se veía como si
   el contenido se saliera de la tarjeta -- confirmado con capturas reales
   (Playwright, cuenta QA con nombres de producto largos), no solo
   leyendo CSS. Mismo padding que ya usa `.gs-section-card` en esta misma
   hoja, sin tocar el margin-top (cada tarjeta ya trae el suyo inline). */
#dash-payment-card,
#dash-alertas-card {
  padding: 16px 20px;
}

.gs-section-label {
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-bottom: 0.75rem;
}

/* ── Gráfico de barras "Cómo entró el dinero" (Fase B, Bloque 6,
   dashboard.html) -- barras CSS simples, sin librería externa, mismo
   criterio "vanilla" del resto del proyecto. ──────────────────────────── */
.dash-bar-chart {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.dash-bar-row {
  display: grid;
  grid-template-columns: 140px 1fr auto;
  align-items: center;
  gap: 10px;
}

.dash-bar-label {
  font-size: 0.8125rem;
  color: var(--text-secondary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.dash-bar-track {
  background: var(--bg-input);
  border-radius: 999px;
  height: 10px;
  overflow: hidden;
}

.dash-bar-fill {
  background: var(--accent);
  height: 100%;
  border-radius: 999px;
  transition: width 0.2s ease;
}

.dash-bar-value {
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  color: var(--text);
  white-space: nowrap;
  min-width: 70px;
  text-align: right;
}

@media (max-width: 480px) {
  .dash-bar-row { grid-template-columns: 90px 1fr auto; }
  .dash-bar-label { font-size: 0.75rem; }
}

.gs-breakdown-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.625rem 0;
  border-bottom: 1px solid var(--border);
  gap: 0.75rem;
}
.gs-breakdown-row:last-child { border-bottom: none; }

.gs-breakdown-label {
  font-size: 0.9375rem;
  color: var(--text);
}

.gs-breakdown-right { text-align: right; }

.gs-breakdown-bs {
  font-family: var(--font-mono);
  font-size: 0.9375rem;
  color: var(--text);
  display: block;
}

.gs-breakdown-count {
  font-size: 0.75rem;
  color: var(--text-secondary);
}

.gs-expense-row {
  padding: 0.75rem 0;
  border-bottom: 1px solid var(--border);
}
.gs-expense-row:last-child { border-bottom: none; }

.gs-expense-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
}

.gs-expense-desc {
  font-size: 0.9375rem;
  color: var(--text);
  flex: 1;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.gs-expense-amount {
  font-family: var(--font-mono);
  font-size: 0.9375rem;
  color: var(--danger);
  white-space: nowrap;
}

.gs-expense-meta {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  margin-top: 0.25rem;
}

.gs-expense-cat {
  font-size: 0.6875rem;
  padding: 2px 8px;
  border-radius: 999px;
  background: rgba(255,255,255,0.06);
  color: var(--text-secondary);
}

.gs-expense-date {
  font-size: 0.75rem;
  color: var(--text-muted);
}

.gs-expense-original {
  font-size: 0.75rem;
  color: var(--text-secondary);
  font-family: var(--font-mono);
}

/* ── Expense row action buttons ─────────────────────────────────────── */

.btn-expense-edit,
.btn-expense-delete {
  background: none;
  border: none;
  cursor: pointer;
  font-family: var(--font-sans);
  font-size: 0.75rem;
  font-weight: 500;
  padding: 2px 8px;
  border-radius: var(--radius-sm);
  transition: background 0.12s, color 0.12s;
  line-height: 1.6;
}

.btn-expense-edit {
  color: var(--accent);
  border: 1px solid transparent;
}
.btn-expense-edit:hover {
  background: var(--accent-glow);
  border-color: var(--accent);
}

.btn-expense-delete {
  color: var(--text-secondary);
  border: 1px solid transparent;
}
.btn-expense-delete:hover {
  color: var(--danger);
  border-color: var(--danger-border);
  background: var(--error-bg);
}

.btn-expense-delete--confirm {
  color: var(--danger) !important;
  border-color: var(--danger-border) !important;
  background: var(--error-bg) !important;
}

/* ── Menú desplegable "⋮" de acciones por fila (Fase A, 2026-09-08) ──────
   Reemplaza los 3 botones sueltos (Editar/Marcar pagada o Revertir a
   pendiente/Eliminar) de cada factura en gastos.html -- reduce el ruido
   visual de la fila sin quitar ninguna acción. .row-menu-item--confirm
   reusa a propósito la MISMA clase --btn-expense-delete--confirm de
   arriba (twoTapConfirm() ya la aplica tal cual, sin saber si el botón
   vive suelto o dentro de un menú) -- ningún duplicado nuevo. */
.row-menu {
  position: relative;
  display: inline-block;
}

.row-menu-btn {
  background: none;
  border: 1px solid transparent;
  color: var(--text-secondary);
  cursor: pointer;
  font-size: 1rem;
  line-height: 1;
  padding: 4px 9px;
  border-radius: var(--radius-sm);
  transition: background 0.12s, border-color 0.12s;
}

.row-menu-btn:hover,
.row-menu-btn[aria-expanded="true"] {
  background: var(--bg-input);
  border-color: var(--border);
  color: var(--text);
}

.row-menu-dropdown {
  position: absolute;
  right: 0;
  top: calc(100% + 4px);
  z-index: 20;
  min-width: 172px;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-card);
  padding: 4px;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.row-menu-dropdown[hidden] { display: none; }

.row-menu-item {
  background: none;
  border: none;
  text-align: left;
  padding: 7px 10px;
  border-radius: var(--radius-sm);
  font-family: var(--font-sans);
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--text);
  cursor: pointer;
  white-space: nowrap;
  transition: background 0.12s, color 0.12s;
}

.row-menu-item:hover { background: var(--bg-input); }

.row-menu-item--danger { color: var(--danger); }
.row-menu-item--danger:hover {
  background: var(--error-bg);
}

/* ── Chat IA ────────────────────────────────────────────────────────── */

/* Upgrade gate */
.upgrade-gate {
  max-width: 480px;
  margin: 2.5rem auto 0;
  text-align: center;
}

.upgrade-gate-icon {
  width: 56px;
  height: 56px;
  border-radius: 50%;
  background: var(--accent-dim);
  border: 1px solid var(--accent-border);
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0 auto 1.25rem;
}

.upgrade-gate-title {
  font-size: 1.375rem;
  font-family: var(--font-heading);
  font-weight: 600;
  color: var(--text);
  margin-bottom: 0.5rem;
}

.upgrade-gate-sub {
  font-size: 0.9375rem;
  color: var(--text-secondary);
  margin-bottom: 1.5rem;
  line-height: 1.5;
}

.upgrade-examples {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin-bottom: 1.75rem;
  text-align: left;
}

.upgrade-example {
  display: flex;
  align-items: flex-start;
  gap: 0.625rem;
  padding: 0.625rem 0.875rem;
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-size: 0.875rem;
  color: var(--text-secondary);
  font-style: italic;
}

.upgrade-example::before {
  content: "›";
  color: var(--accent);
  font-style: normal;
  font-weight: 700;
  flex-shrink: 0;
}

.upgrade-plan-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  padding: 4px 12px;
  border-radius: 999px;
  background: var(--info-dim);
  border: 1px solid var(--info-border);
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--info);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin-bottom: 1.25rem;
}

.upgrade-contact {
  margin-top: 0.875rem;
  padding: 0.75rem 1rem;
  border-radius: var(--radius);
  background: var(--bg-card);
  border: 1px solid var(--border);
  font-size: 0.875rem;
  color: var(--text-secondary);
  line-height: 1.5;
  text-align: left;
}

/* Chat wrapper */
.chat-wrap {
  display: flex;
  flex-direction: column;
  gap: 0;
}

.chat-messages {
  min-height: 300px;
  max-height: calc(100dvh - 260px);
  overflow-y: auto;
  padding: 0.25rem 0 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  scroll-behavior: smooth;
}

/* Individual message */
.chat-msg {
  display: flex;
  flex-direction: column;
  max-width: 82%;
}

.chat-msg--user {
  align-self: flex-end;
  align-items: flex-end;
}

.chat-msg--agent {
  align-self: flex-start;
  align-items: flex-start;
}

.chat-bubble {
  padding: 0.625rem 0.875rem;
  border-radius: var(--radius);
  line-height: 1.55;
  font-size: 0.9375rem;
  word-break: break-word;
}

.chat-msg--user .chat-bubble {
  background: var(--accent);
  color: var(--bg);
  border-bottom-right-radius: 4px;
}

.chat-msg--agent .chat-bubble {
  background: var(--bg-card);
  border: 1px solid var(--border);
  color: var(--text);
  border-bottom-left-radius: 4px;
}

.chat-msg--agent .chat-bubble p { color: var(--text); margin: 0; }
.chat-msg--agent .chat-bubble p + p { margin-top: 0.5em; }
.chat-msg--agent .chat-bubble strong { color: var(--text); }

.chat-bubble-time {
  font-size: 0.6875rem;
  color: var(--text-muted);
  margin-top: 3px;
  padding: 0 2px;
}

/* Typing indicator */
.chat-typing .chat-bubble {
  padding: 0.75rem 1rem;
  display: flex;
  align-items: center;
  gap: 5px;
}

.typing-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--text-secondary);
  animation: typing-pulse 1.4s ease-in-out infinite;
  flex-shrink: 0;
}
.typing-dot:nth-child(2) { animation-delay: 0.2s; }
.typing-dot:nth-child(3) { animation-delay: 0.4s; }

@keyframes typing-pulse {
  0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
  30%            { transform: translateY(-5px); opacity: 1; }
}

/* Input bar */
.chat-input-bar {
  display: flex;
  align-items: flex-end;
  gap: 0.625rem;
  padding: 0.875rem 0 0;
  border-top: 1px solid var(--border);
  margin-top: 0.5rem;
  position: sticky;
  bottom: 0;
  background: var(--bg);
  padding-bottom: env(safe-area-inset-bottom, 0.5rem);
}

/* Fase 21 (2026-09-08) -- pasó de <textarea> a <div contenteditable>, mismo
   bug de autocompletado de Chrome que el resto del proyecto. white-space:
   pre-wrap -- los saltos de línea (Shift+Enter, o pegados) se guardan como
   '\n' literales en un nodo de texto (ver wireCampoSeguro()/
   insertTextAtCaret() en el <script>, nunca el <div>/<br> que insertaría
   el navegador por defecto) -- sin pre-wrap esos '\n' se colapsarían
   visualmente a un espacio, igual que le pasaría a cualquier <div> normal. */
.chat-input {
  flex: 1;
  resize: none;
  overflow-y: auto;
  max-height: 120px;
  min-height: 46px;
  padding: 11px 14px;
  background: var(--bg-input);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: 0.9375rem;
  line-height: 1.5;
  outline: none;
  transition: border-color 0.15s, box-shadow 0.15s;
  white-space: pre-wrap;
  word-break: break-word;
}

.chat-input:focus {
  border-color: var(--border-focus);
  box-shadow: var(--shadow-focus);
}

.chat-input:empty::before {
  content: attr(data-placeholder);
  color: var(--text-muted);
  pointer-events: none;
}

.chat-send-btn {
  flex-shrink: 0;
  width: 46px;
  height: 46px;
  background: var(--accent);
  border: none;
  border-radius: var(--radius);
  color: var(--bg);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 0.15s, transform 0.1s;
  -webkit-tap-highlight-color: transparent;
}

.chat-send-btn:hover:not(:disabled) { background: var(--accent-hover); }
.chat-send-btn:active:not(:disabled) { transform: scale(0.94); }
.chat-send-btn:disabled { opacity: 0.5; cursor: not-allowed; }

@media (max-width: 480px) {
  .chat-messages { max-height: calc(100dvh - 240px); }
  .chat-msg       { max-width: 92%; }
}

/* ── Widget flotante del Asistente IA (Fase C, Bloque 1, 2026-09-09) ──
   Reemplaza a chat.html como página propia -- ver chat-widget.js. Reusa
   .chat-wrap/.chat-messages/.chat-bubble/... y .upgrade-gate/... de arriba
   tal cual (mismo componente, ahora dentro de un panel flotante en vez de
   una página completa) -- solo se agrega el contenedor. */
.vp-chat-fab {
  position: fixed;
  bottom: 20px;
  right: 20px;
  width: 52px;
  height: 52px;
  border-radius: 50%;
  background: var(--accent);
  color: var(--bg);
  border: none;
  font-size: 1.4rem;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  box-shadow: var(--shadow-card);
  z-index: 500;
  transition: transform 0.15s ease;
}
.vp-chat-fab:hover  { transform: scale(1.06); }
.vp-chat-fab:active { transform: scale(0.94); }

.vp-chat-panel {
  position: fixed;
  bottom: 84px;
  right: 20px;
  width: 360px;
  max-width: calc(100vw - 40px);
  height: 500px;
  max-height: calc(100dvh - 120px);
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  z-index: 500;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.vp-chat-panel[hidden] { display: none; }

.vp-chat-panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.875rem 1rem;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}
.vp-chat-panel-title { font-weight: 600; color: var(--text); font-size: 0.9375rem; }
.vp-chat-panel-body  { flex: 1; min-height: 0; display: flex; flex-direction: column; overflow-y: auto; }
.vp-chat-panel-body .chat-wrap     { flex: 1; min-height: 0; }
.vp-chat-panel-body .chat-messages { max-height: none; } /* el límite lo pone .vp-chat-panel-body/.vp-chat-panel */
.vp-chat-panel-body .upgrade-gate  { padding: 1.25rem 1rem; }

@media (max-width: 480px) {
  .vp-chat-panel {
    left: 12px;
    right: 12px;
    width: auto;
    bottom: 84px;
    height: auto;
    max-height: calc(100dvh - 100px);
  }
}

/* pos.html -- el botón "Confirmar venta" del carrito vive en la esquina
   inferior DERECHA (escritorio: panel fijo del carrito; móvil: .cart-bar
   de ancho completo). Hallazgo real (Fase C, Bloque 5, ver RUNBOOK.md):
   con el FAB en su posición default (bottom-right) se superponía
   literalmente con "Confirmar venta" en escritorio (medido con
   boundingBox() real, no a ojo) -- se mueve a la izquierda en esta página
   específica, donde el catálogo (columna izquierda) siempre tiene espacio
   libre abajo. Clases agregadas por chat-widget.js solo cuando
   location.pathname === '/pos.html'. */
.vp-chat-fab.vp-chat-fab--pos,
.vp-chat-panel.vp-chat-panel--pos {
  right: auto;
  left: 20px;
}

/* pos.html móvil: .cart-bar (colapsado) ocupa el ancho completo de la
   parte inferior -- sube el FAB/panel para no taparla. Ver docs/RUNBOOK.md,
   sección "Fase C, Bloque 1". Cuando el carrito se abre EXPANDIDO
   (.pos-cart.open, hasta 92dvh) el widget entero se oculta en vez de
   reposicionarse -- no queda ninguna esquina realmente libre (Bloque 5,
   hallazgo real con captura de pantalla: incluso subido, el FAB tapaba
   "Fecha de la venta: hoy" y quedaba pegado a "Confirmar venta"). Ver
   chat-widget.js (evento `vp:cart-open`/`vp:cart-close`) y pos.html
   (openCartMobile()/closeCartMobile()). */
@media (max-width: 767px) {
  .vp-chat-fab.vp-chat-fab--pos     { bottom: 76px; }
  .vp-chat-panel.vp-chat-panel--pos { bottom: 140px; }
}

/* ── Reportes: botón de descarga contextual en páginas existentes ─ */
.btn-report {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  font-size: 0.8125rem;
  color: var(--text-secondary);
  padding: 6px 12px;
}
.btn-report:hover { color: var(--text-primary); }
.btn-report svg   { flex-shrink: 0; }

.report-err {
  font-size: 0.75rem;
  color: var(--danger);
  margin-top: 4px;
  display: block;
}

/* ── Reportes: page reportes.html ────────────────────────────────── */
/* Fase A (2026-09-08) -- grilla de 2-3 columnas (antes 1 columna vertical
   fija). auto-fit + minmax da 1 columna en móvil, 2-3 en desktop según el
   ancho real de la ventana, sin un breakpoint fijo a mano. */
.reports-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 16px;
  align-items: start;
}

.report-card {
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 16px 20px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  height: 100%;
}

.report-card-header {
  display: flex;
  align-items: flex-start;
  gap: 12px;
}

.report-card-icon {
  width: 32px;
  height: 32px;
  border-radius: var(--radius-sm);
  background: var(--bg-elevated-2);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  color: var(--accent);
}

.report-card-meta {
  flex: 1;
}

.report-card-title {
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--text-primary);
  margin: 0 0 2px;
}

.report-card-desc {
  font-size: 0.8125rem;
  color: var(--text-secondary);
  margin: 0;
  line-height: 1.5;
}

.report-card-snapshot {
  font-size: 0.75rem;
  color: var(--text-muted);
  font-style: italic;
}

.report-period {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 6px; /* 8px entre filas: con pills de 36px el paso es 44px, sin solapar zonas táctiles */
}

.report-range {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}

.report-range input[type="date"] {
  background: var(--bg-elevated-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text-primary);
  font-size: 0.8125rem;
  padding: 6px 10px;
  flex: 1;
  min-width: 130px;
}
.report-range input[type="date"]:focus {
  outline: none;
  border-color: var(--accent);
}

.report-card-footer {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}

.report-card-footer .report-err {
  margin-top: 0;
}

/* ── Nota de entrega (Fase 6) — logo dropzone + vista previa ──────────── */

.logo-dropzone {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 96px;
  padding: 16px;
  background: var(--bg-input);
  border: 1.5px dashed var(--border-strong);
  border-radius: var(--radius);
  cursor: pointer;
  transition: border-color 0.15s, background 0.15s;
}

.logo-dropzone:hover,
.logo-dropzone:focus-visible {
  border-color: var(--accent);
}

.logo-dropzone--over {
  border-color: var(--accent);
  background: var(--accent-dim);
}

.logo-dropzone-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  color: var(--text-secondary);
  font-size: 0.875rem;
  text-align: center;
}

.logo-dropzone-img {
  max-height: 80px;
  max-width: 100%;
  object-fit: contain;
  border-radius: var(--radius-sm);
}

.ne-preview {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 16px 20px;
  color: var(--text-primary);
}

.ne-preview-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 10px;
}

.ne-preview-header img {
  max-height: 48px;
  max-width: 140px;
  object-fit: contain;
}

.ne-preview-info {
  text-align: right;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.ne-preview-info span {
  font-size: 0.75rem;
  color: var(--text-muted);
}

.ne-preview-titulo {
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--accent);
  text-align: right;
  margin: 0 0 12px;
}

.ne-preview-linea {
  display: flex;
  justify-content: space-between;
  font-size: 0.8125rem;
  padding: 4px 0;
  border-bottom: 1px solid var(--border);
}

.ne-preview-totales {
  margin-top: 8px;
}

.ne-preview-linea--total {
  border-bottom: none;
  font-weight: 600;
  color: var(--accent);
  font-family: var(--font-mono);
}

.ne-preview-metodos {
  margin-top: 12px;
  font-size: 0.75rem;
  color: var(--text-muted);
  display: flex;
  flex-direction: column;
  gap: 2px;
}

/* Métodos de pago tipados (ajuste post-primera nota real) -- cada fila es
   una tarjeta con selector de tipo + los campos específicos de ese tipo. */
.ne-metodo-row {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 10px;
  margin-bottom: 8px;
  background: var(--bg-input);
}

.ne-metodo-row-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
}

.ne-metodo-row-head select {
  max-width: 200px;
}

.ne-metodo-row-head .btn-quitar-metodo {
  margin-left: auto;
}

.ne-metodo-campos {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 8px;
}

/* ── Escáner de código de barras (Fase 7 Parte 3) ──────────────────────── */

.scanner-video-wrap {
  position: relative;
  width: 100%;
  aspect-ratio: 4 / 3;
  background: #000;
  border-radius: var(--radius);
  overflow: hidden;
}

.scanner-video-wrap video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.scanner-status {
  text-align: center;
  font-size: 0.8125rem;
  color: var(--text-secondary);
  margin: 10px 0 0;
}

.scanner-manual-row {
  display: flex;
  gap: 8px;
  margin-top: 12px;
}

/* Fase 21 (2026-09-08) -- #scanner-manual-input (inventario.html y
   pos.html, dos copias del mismo diálogo) pasó de <input> a
   <div class="ce-search-input">, mismo bug de autocompletado de Chrome que
   el resto del proyecto -- el selector original solo alcanzaba a <input>. */
.scanner-manual-row input,
.scanner-manual-row .ce-search-input {
  flex: 1;
}

/* Tarjeta de producto encontrado en el diálogo de confirmación del POS
   (foto/nombre + cantidad) -- reusa product-thumb (Fase 7 Parte 2) a mayor
   tamaño para que se vea bien en un diálogo, no en una grilla densa. */
.scan-confirm-product {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px;
  background: var(--bg-input);
  border-radius: var(--radius);
  margin-bottom: 12px;
}

.scan-confirm-thumb {
  width: 56px;
  height: 56px;
  flex-shrink: 0;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--bg-card);
  border: 1px solid var(--border);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.5rem;
}

.scan-confirm-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.scan-confirm-name {
  font-weight: 600;
  color: var(--text);
  font-size: 0.9375rem;
}

.scan-confirm-price {
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  color: var(--text-secondary);
  margin-top: 2px;
}

/* ── Catálogo público para WhatsApp (Fase 8) ───────────────────────────── */

/* Fase 8.2 -- header: esta página no tenía .header-right, así que
   js/theme.js (sin tocarlo -- ya soporta esto) caía a su botón de tema
   flotante de respaldo, que colisionaba con #negocio-nombre (dato real y
   variable, a diferencia de "TIFlows POS" fijo en el resto de las
   páginas). Con .header-right ya en el HTML, el botón entra al flujo
   normal del flex -- estas reglas son la red de seguridad para que NINGÚN
   largo de nombre real pueda volver a chocar:
   - .header-brand con min-width:0 permite que su hijo trunque dentro del
     flex item (sin esto, un flex item nunca se encoge más chico que su
     contenido, por más que haya overflow:hidden en el hijo) -- inerte en
     el resto de las páginas, donde el texto ("TIFlows POS") ya es corto y
     nunca desborda.
   - #negocio-nombre trunca con "…" en vez de desbordar o envolver.
   - .catpub-header .header-right reserva un mínimo de 50px a la derecha
     (pedido explícito, además de la corrección de fondo de arriba) --
     scoped SOLO a esta página vía la clase .catpub-header nueva en el
     <header>, nunca afecta el header de las páginas autenticadas. */
.header-brand {
  min-width: 0;
}

#negocio-nombre {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

.catpub-header .header-right {
  min-width: 50px;
  justify-content: flex-end;
}

/* Buscador -- reusa .search-wrapper/.ce-search-input (mismos estilos que
   inventario.html), solo el espaciado bajo el header es propio de acá. */
.catpub-search-wrap {
  margin: 16px 0;
}

/* Grilla propia de esta página (no .products-grid -- ese lo usa
   inventario.html y no debe verse afectado por este rediseño). 2 columnas
   fijas en móvil para que quepan tarjetas compactas sin scroll excesivo;
   auto-fill más ancho en pantallas grandes. */
.catpub-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 12px;
}

.catpub-grid[hidden] { display: none; }

@media (max-width: 640px) {
  .catpub-grid {
    grid-template-columns: repeat(2, 1fr);
    gap: 8px;
  }
}

.catpub-card {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  transition: border-color 0.15s;
}

.catpub-card:hover { border-color: var(--border-strong); }

.catpub-card--agotado {
  opacity: 0.55;
}

/* Miniatura cuadrada -- protagonista de la tarjeta, no un ícono chico al
   lado del texto (ese patrón es el de inventario.html/pos.html, esta
   página pública es explícitamente un catálogo tipo vitrina). */
.catpub-card-thumb {
  position: relative;
  width: 100%;
  aspect-ratio: 1 / 1;
  background: var(--bg-input);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

.catpub-card-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.catpub-card-thumb-fallback {
  font-size: 2.25rem;
  line-height: 1;
  opacity: 0.35;
}

.catpub-card-badge {
  position: absolute;
  top: 6px;
  right: 6px;
  /* Sobre una foto real (colores impredecibles), el fondo semitransparente
     de .badge-green/.badge-red (--accent-dim/--danger-dim, ~12% opacidad)
     puede perder legibilidad -- excepción deliberada a la paleta de la
     sección 2 de la guía de diseño: no es un color de página, es un scrim
     neutro para que la etiqueta se lea igual sobre cualquier foto, en
     cualquier tema. El color del texto (verde/rojo) sigue viniendo de
     --accent/--danger en modo oscuro; en modo claro se corrige aparte
     abajo (ver el porqué). */
  background: rgba(0, 0, 0, 0.6) !important;
  border-color: rgba(0, 0, 0, 0.6) !important;
}

/* Fase 8.2 -- en modo claro, --accent/--danger son tonos más oscuros que
   en modo oscuro (necesitan más contraste sobre un fondo claro, ver guía
   de diseño sección 2) -- puestos sobre el scrim oscuro de arriba, ese
   texto oscuro queda con bajo contraste (oscuro sobre oscuro), a
   diferencia de modo oscuro donde el texto es un verde/rojo brillante
   sobre el mismo scrim y sí se lee bien (confirmado, no se toca). Fondo
   sólido + texto blanco, usando los tokens del tema (no un hex suelto) --
   scoped a .catpub-card-badge, nunca a .badge-green/.badge-red en general
   (esas se usan en TODA la app -- inventario.html, pos.html, créditos,
   etc. -- cambiarlas ahí no fue lo pedido ni se revisó ese impacto). */
[data-theme="light"] .catpub-card-badge.badge-green {
  background: var(--accent) !important;
  border-color: var(--accent) !important;
  color: #fff !important;
}

[data-theme="light"] .catpub-card-badge.badge-red {
  background: var(--danger) !important;
  border-color: var(--danger) !important;
  color: #fff !important;
}

.catpub-card-body {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 10px 12px 12px;
}

/* Fase 8.2 -- tamaños subidos para leerse sin zoom en móvil real (pruebas
   de José). El nombre pasa de 13px a 14px (escala "Cuerpo" de la guía de
   diseño); la categoría sube apenas de 11px a 11.5px para no competir con
   el nombre. La jerarquía de precio se mantiene intacta a propósito -- USD
   sigue siendo claramente el elemento más grande de toda la tarjeta. */
.catpub-card-name {
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--text);
  line-height: 1.3;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.catpub-card-category {
  font-size: 0.7188rem;
  color: var(--text-muted);
}

/* Jerarquía de precio: USD protagonista (grande, negrita), Bs de apoyo
   (chico, tenue) debajo -- ambos en JetBrains Mono, como todo monto de
   dinero en el proyecto (nunca la fuente de interfaz normal). USD sube de
   17px a 19px (protagonista real, no solo la intención); Bs sube apenas de
   11px a 12px -- sigue siendo claramente secundario, nunca compite en peso
   visual con USD (jerarquía ya construida, no se toca su relación). */
.catpub-card-price {
  display: flex;
  flex-direction: column;
  gap: 0;
  margin: 2px 0 4px;
}

.catpub-price-usd {
  font-family: var(--font-mono);
  font-size: 1.1875rem;
  font-weight: 700;
  color: var(--text);
  line-height: 1.2;
}

.catpub-price-bs {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  font-weight: 400;
  color: var(--text-muted);
}

@keyframes catpubCartFloatIn {
  from { opacity: 0; transform: translateY(18px) scale(0.92); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}

.catpub-cart-float {
  position: fixed;
  bottom: 20px;
  right: 20px;
  left: 20px;
  max-width: 400px;
  margin: 0 auto;
  z-index: 40;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 14px 20px;
  background: var(--accent);
  color: #05130F;
  font-weight: 700;
  font-size: 0.9375rem;
  border: none;
  border-radius: 999px;
  box-shadow: 0 4px 20px rgba(0,0,0,0.25);
  cursor: pointer;
  /* [hidden] es display:none (ver regla global) -- cada vez que este botón
     pasa de oculto a visible (0 -> 1 ítems en el carrito) vuelve a entrar
     en el árbol de render y la animación se reproduce de nuevo desde cero,
     sin necesitar ningún JS extra para disparar la transición. */
  animation: catpubCartFloatIn 240ms cubic-bezier(0.16, 1, 0.3, 1);
}

.catpub-cart-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.5);
  z-index: 49;
}

.catpub-cart-drawer {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 50;
  max-width: 480px;
  margin: 0 auto;
  max-height: 82vh;
  display: flex;
  flex-direction: column;
  background: var(--bg-card);
  border-top: 1px solid var(--border);
  border-radius: var(--radius) var(--radius) 0 0;
  box-shadow: 0 -4px 24px rgba(0,0,0,0.3);
}

.catpub-cart-drawer .cart-items {
  overflow-y: auto;
  padding: 0 20px;
}

.catpub-cart-total {
  text-align: center;
  font-size: 0.9375rem;
  color: var(--text);
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.catpub-toast {
  position: fixed;
  top: 16px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 60;
  background: var(--bg-card);
  border: 1px solid var(--border);
  color: var(--text);
  padding: 10px 18px;
  border-radius: var(--radius);
  font-size: 0.875rem;
  box-shadow: 0 4px 16px rgba(0,0,0,0.25);
  animation: fadeIn 0.15s;
}

/* ── Cierre y arqueo de caja (migración 035, 2026-09-13) ──────────────
   Usado en pos.html (diálogo "Cierre de caja") y gestion.html (Operaciones
   → "Cierres de caja"). Fila por categoría: nombre + esperado a la
   izquierda, campo declarado a la derecha -- mismo criterio de alineación
   que .form-row de 2 columnas, pero con el label a ancho fijo para que los
   6 nombres de categoría (posiblemente distinta longitud) no corran el
   campo de un lado a otro. */
.cierre-cat-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 10px 0;
  border-bottom: 1px solid var(--border);
}
.cierre-cat-row:last-child { border-bottom: none; }

.cierre-cat-info { display: flex; flex-direction: column; gap: 2px; }
.cierre-cat-label { font-size: 0.875rem; font-weight: 600; color: var(--text); }
.cierre-cat-esperado { font-size: 0.8125rem; color: var(--text-muted); }

.cierre-declarado-wrap { flex-shrink: 0; width: 140px; }
.cierre-declarado-input {
  text-align: right;
  font-family: var(--font-mono);
}

/* Cuando el declarado de una fila no coincide con lo esperado -- feedback
   inmediato, nunca bloqueante (regla de oro del diseño: un descuadre
   nunca impide confirmar el cierre). */
.cierre-declarado-input.cierre-descuadre {
  border-color: var(--warning-border);
  color: var(--warning);
}

/* Historial (pos.html Y gestion.html) -- detalle esperado/declarado/
   diferencia de un cierre ya creado, siempre visible (nunca oculto tras un
   "ver más": son a lo sumo 6 filas). */
.cierre-detalle-tabla {
  width: 100%;
  font-size: 0.8125rem;
  border-collapse: collapse;
  margin-top: 8px;
}
.cierre-detalle-tabla th {
  text-align: right;
  font-weight: 500;
  color: var(--text-muted);
  padding: 4px 8px;
}
.cierre-detalle-tabla th:first-child { text-align: left; }
.cierre-detalle-tabla td {
  text-align: right;
  padding: 4px 8px;
  font-family: var(--font-mono);
}
.cierre-detalle-tabla td:first-child { text-align: left; font-family: var(--font-sans); }
.cierre-detalle-tabla tr.cierre-detalle-descuadre td { color: var(--warning); }

@media (max-width: 480px) {
  .cierre-cat-row { flex-wrap: wrap; }
  .cierre-declarado-wrap { width: 100%; }
  .cierre-declarado-input { text-align: left; }
}
