.bggradient{
  background: linear-gradient(135deg,#f8fafc,#eef2f7);
}

/* ===== Tablas: separación de filas visible + encabezado marcado (look profesional) ===== */
.table {
  --ins-table-border-color: #e5e9f0;                 /* borde entre filas visible (antes casi invisible) */
  --ins-table-hover-bg: rgba(101, 88, 221, .05);     /* hover con tinte primario sutil */
}
.table > thead > tr > th {
  border-bottom: 2px solid #dde2ea;                  /* subrayado del encabezado más marcado */
  font-weight: 600;
  white-space: nowrap;
  color: #495364;
}
.table > tbody > tr {
  border-bottom: 1px solid #eceff4;                  /* refuerza la línea entre filas */
}
.table > tbody > tr:last-child { border-bottom: 0; }

.ql-toolbar{
  background: #f8f8f8!important;
}

.ql-container{
  background: #fff !important;
}
/****************AJUSTES TEMA**********************************/
body{

  min-height:100vh;

  background: #f4f4f4;

}


/* Sub-ítems del menú: icono compacto + espacio para el conector de árbol */
.side-nav .sub-menu .side-nav-item .side-nav-link {
    font-size: var(--ins-sidenav-sub-item-font-size);
    font-weight: var(--ins-sidenav-sub-item-font-weight);
    font-family: var(--ins-font-sans-serif);
    gap: 8px;
    padding: var(--ins-sidenav-sub-item-padding-y) var(--ins-sidenav-item-padding-x) var(--ins-sidenav-sub-item-padding-y) 34px;
    background: transparent;
}
/* El icono del sub-ítem NO usa la columna ancha del ítem padre */
.side-nav .sub-menu .side-nav-link .menu-icon {
    min-width: auto;
    font-size: 1rem;
    line-height: 1;
}

/* ---- Conectores tipo árbol en los submenús (estilo .sitemap-list de Ubold:
   trazo punteado con el color de borde del tema) ---- */
.side-nav .sub-menu {
  --tree-x: 20px;         /* posición de la línea vertical */
  position: relative;
}
.side-nav .sub-menu .side-nav-item {
  position: relative;
}
/* Segmento vertical que atraviesa cada item */
.side-nav .sub-menu .side-nav-item::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--tree-x);
  border-left: 1px dashed var(--ins-border-color);
}
/* Ramal horizontal hacia el icono del sub-ítem */
.side-nav .sub-menu .side-nav-item::after {
  content: "";
  position: absolute;
  top: 50%;
  left: var(--tree-x);
  width: 10px;
  border-top: 1px dashed var(--ins-border-color);
}
/* El último item corta la vertical a la altura del ramal (no cuelga) */
.side-nav .sub-menu .side-nav-item:last-child::before {
  bottom: auto;
  height: 50%;
}


.card:focus-within{
  z-index: 20;
}

.card{

  background-color: #ffffff;
  border-radius: 7px;
  box-shadow:
    0 10px 25px rgba(0,0,0,0.08),
    0 2px 6px rgba(0,0,0,0.05);
  transition: all .25s ease;
  border: 1px solid #eef2f7;
  /* ⚠ Acá había un `will-change: height` y hubo que quitarlo (2026-08-24).
     `will-change` **crea un bloque contenedor para los elementos `position: fixed` de adentro**:
     cualquier cosa fija dentro de una tarjeta —un menú desplegable, un tooltip— deja de
     posicionarse contra la ventana y pasa a hacerlo contra la TARJETA. El resultado es que
     aparece corrida hacia la izquierda y hacia arriba, tanto como la tarjeta esté separada del
     borde. Es lo que le pasaba al menú «Opciones» del listado de facturas.
     Además no aportaba nada: se puso para la animación de plegar la tarjeta, pero `will-change`
     es para aplicarse un instante antes de animar, no de forma permanente en TODAS las tarjetas
     —cada una se lleva su capa de composición—. */
}



.card-header{
  background: transparent;
  border-bottom: 1px solid rgba(0,0,0,0.06);
  font-weight: 600;
  padding: 16px 20px;
}

.table tbody tr{
  background: rgba(255,255,255,0.1)!important;

  /* ⚠ Acá había un `backdrop-filter: blur(17px)` en CADA FILA, y hubo que quitarlo (2026-08-24).
     Un `backdrop-filter` distinto de `none` convierte al elemento en **bloque contenedor de todo
     lo que esté `position: fixed` adentro**: un menú desplegable de la fila dejaba de posicionarse
     contra la ventana y pasaba a hacerlo contra la FILA, apareciendo corrido hacia la izquierda y
     hacia arriba —tanto como la fila esté separada del borde—. Era el defecto del menú «Opciones».

     Y no se veía nada: desenfocar el fondo de una fila sobre otra fila blanca no produce ningún
     efecto visible. Solo costaba una capa de composición por fila y rompía el posicionamiento.

     Si algún día se quiere el efecto de vidrio, va sobre la TARJETA —una sola capa— y nunca sobre
     un contenedor que aloje menús o tooltips. */

  transition: all .18s ease;

}

.table{
  border-collapse: separate;
  border-spacing: 0 4px;
}

/*.table tbody tr:hover{
  background: rgba(255,255,255,0.3);
  transform: translateY(-1px);
  box-shadow: 0 4px 10px rgba(0,0,0,0.06);
}*/

.table td,
.table th{
  border: none;
  padding: 14px 16px;
  vertical-align: middle;
  background: transparent !important;
}


.btnFloat{
  position: fixed;
  right: 10px;
  top: 50%;
  z-index: 1050;
}


.form-control,
.form-select{
  background: rgba(255,255,255,0.7);
  border:1px solid rgba(0,0,0,0.08);
  padding:10px 12px;
  transition: all .2s ease;

}


.form-control:focus,
.form-select:focus{
  border-color:#7c3aed;
  box-shadow:0 0 0 3px rgba(124,58,237,0.15);
}


/******************FIN AJUSTES********************************/

/* Aplica a TODA tabla DataTables (productos, facturas, clientes y futuras):
   DataTables colapsa la tabla y pisa el border-spacing general, así que se
   restaura el look de filas separadas del tema. */
table.dataTable{
  border-collapse: separate!important;
  border-spacing: 0 4px!important;
}

.table-container {
    max-height: 600px;
    min: 400px;
    overflow-y: auto;
    /* Explícito, aunque el navegador ya lo deduce: con `overflow-y: auto` el eje X pasa a `auto`
       solo. Escrito se lee la intención — la tabla scrollea DENTRO de su caja. */
    overflow-x: auto;
}

/* La tabla se recorre de lado dentro de su caja: se le deja un scroll visible y suave, porque un
   scroll horizontal que no se ve es un scroll que nadie usa. En el teléfono el gesto es natural;
   en escritorio, la barra es la única pista de que hay más columnas a la derecha. */
.table-container {
    scrollbar-width: thin;
    -webkit-overflow-scrolling: touch;
}

/* ── Que la tabla no estire la página ────────────────────────────────────────────────────────
   Una columna de Bootstrap es un elemento flex y, aunque tenga `width: 100%`, su `min-width`
   vale `auto`: no se encoge por debajo del ancho MÍNIMO de su contenido. Con una tabla de trece
   columnas ese mínimo son ~1.400 px, así que la columna crecía hasta ahí y con ella la tarjeta,
   la fila y la página entera. En el teléfono se desplazaba todo —menú lateral incluido— en vez de
   que la tabla hiciera scroll por dentro.

   `min-width: 0` deja que se encojan y el desbordamiento quede confinado al contenedor de la
   tabla, que es quien tiene el scroll. Es también lo que permite que la extensión Responsive de
   DataTables funcione: mide el ancho del contenedor, y un contenedor que se estira nunca «se
   queda corto», así que nunca escondía ninguna columna. */
.row > [class*="col-"],
.card,
.card-body {
    min-width: 0;
}

/* ⚠ Y el de verdad: `body` es una GRILLA (así arma el tema el layout con la barra lateral), y en
   una grilla los ítems tampoco se encogen por debajo de su contenido —`min-width: auto`—. Sin
   esto, `.wrapper` crecía hasta los ~1.500 px de la tabla y arrastraba la página entera: la barra
   lateral, la tarjeta y hasta las modales quedaban desplazadas a 390 px.
   Es el arreglo que faltaba para el desborde preexistente de la lista de facturas. */
.wrapper,
.content-page {
    min-width: 0;
}



.table td {
  padding: 7px 16px!important;
}

.table-container thead th {
    position: sticky;
    top: 0;
    background: #fff!important;
    z-index: 2;
}

.table-container thead th:first-child{
  border-top-left-radius: 8px;
}

.table-container thead th:last-child{
  border-top-right-radius: 8px;
}



.pointer{
	cursor: pointer!important;
}

.img-variante {
  width: 60px;
  height: 60px;
  object-fit: cover;
  border-radius: 6px;
}


#global-preloader{
  position: fixed;
  inset: 0;
  background: rgba(255,255,255,1);
  z-index: 9999;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
}

/* Barra */
.loader-bar{
  width: 160px;
  height: 4px;
  background: #e5e5e5;
  overflow: hidden;
  position: relative;
  border-radius: 2px;
}

.loader-bar::after{
  content:'';
  position:absolute;
  left:-40%;
  width:40%;
  height:100%;
  background:#000;
  animation: barMove 1.2s ease-in-out infinite;
}

@keyframes barMove{
  to { left:100%; }
}

/* Texto */
.loader-text{
  font-size: 14px;
  color: #333;
}



/******************************/

/*Miniaturas cargar imágenes*/



/*sortable IMAGES*/

.sortable-ghost {
  opacity: 0.2;
}
.img-fluid{
  box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.4);
}

#imagenes-producto > .imagen-item:first-child img {
  box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.4);
}


#imagenes-producto .imagen-item:first-of-type .drag-handle {
  display: none;
}


/* Texto superpuesto SOLO en la primera imagen */
#imagenes-producto .imagen-item:first-of-type::after {
  content: "Imagen principal";
  position: absolute;
  bottom: 0;
  left: 0%;
  background: rgba(200, 0, 0, 0.75);
  padding: 0 3px 0 3px;
  color: #fff;
  font-size: 12px;
  font-weight: 500;
  pointer-events: none;
  z-index: 5;
  width: 100%;
  text-align: center;
}


.imagen-item > a > img{
  border: 5px solid #fff!important;
}

.imagen-item {
  position: relative;
  height: 160px;
}

.imagen-item a {
  display: block;
  width: 100%;
  height: 100%;
}

.imagen-item img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}


.imagen-item:hover img{
  filter: grayscale(100%);
  transition: filter 0.3s ease;
}


.drag-handle {
  position: absolute;
  bottom: 7px;
  left: 50%;
  transform: translateX(-50%);
  font-size: 14px;
  background: rgba(234,0,0,0.7);
  color: #fff;
  padding: 2px 12px;
  border-radius: 4px;
  cursor: grab;
  user-select: none;
  z-index: 10;
}

@media (pointer: coarse) {/*Móvil*/
  .drag-handle {
    width: 40px;
    height: 40px;
    display: flex;
    align-items: center;
    justify-content: center;
  }
}

@media (pointer: fine) {/*Desktop*/
  .drag-handle {

  }
}

.imagen-item:active {
  cursor: grabbing;
}

.imagen-item.activa {
  transition: box-shadow 0.2s ease, transform 0.2s ease;
}

.contentPrincipal{
  background: linear-gradient(180deg, #f5f5f5 0%, #e0e0e0 100%);
}

.contentPrincipal > img{
  border: 5px solid #fff!important;
}


/******************************/

.seo-file-label {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;

  height: 44px;
  padding: 0 18px;
  border-radius: 5px;

  background: linear-gradient(180deg, #7b5cff, #5a3ee6);
  color: #fff;

  font-size: 15px;
  font-weight: 600;
  letter-spacing: .2px;

  cursor: pointer;
  user-select: none;
  -webkit-tap-highlight-color: transparent;

  box-shadow:
    0 6px 12px rgba(90,62,230,.35),
    inset 0 -1px 0 rgba(255,255,255,.15);

  transition:
    transform .12s ease,
    box-shadow .12s ease,
    background .12s ease;
}

/* Press (mobile + desktop) */
.seo-file-label:active {
  transform: scale(.96);
  box-shadow:
    0 3px 6px rgba(90,62,230,.25),
    inset 0 2px 4px rgba(0,0,0,.2);
}

/* Hover solo en desktop */
@media (hover: hover) {
  .seo-file-label:hover {
    box-shadow:
      0 8px 16px rgba(90,62,230,.4),
      inset 0 -1px 0 rgba(255,255,255,.15);
  }
}

.seo-icon {
  font-size: 18px;
  line-height: 1;
}

/******************************/

.check-showinweb{
  position: absolute;
  top: 15px;
  left:27px;
  transform: scale(1.4);
  cursor: pointer;
  z-index: 10;
}

@media (pointer: fine) {
  .check-showinweb{
    opacity: 0;
    pointer-events: none;
    transition: opacity .2s ease;
  }

  .imagen-item:hover .check-showinweb{
    opacity: 1;
    pointer-events: auto;
  }
}

/******************************/

.btn-del-image-product{
  position: absolute;
  right: 24px;
  top: 12px;
  width: 20px;
  height: 20px;
  z-index: 15;
  border-radius: 50%;
  background: #ffffff;
  display: flex;
  align-items: center;    /* centra vertical */
  justify-content: center;/* centra horizontal */
}

/* Recortar: mismo tratamiento que el borrar, a su izquierda. Se agregó el 2026-09-10 para poder
   reencuadrar una foto sin volver a subirla. */
.btn-crop-image-product{
  position: absolute;
  right: 52px;            /* 24 del borrar + 20 de ancho + 8 de aire */
  top: 12px;
  width: 20px;
  height: 20px;
  z-index: 15;
  border-radius: 50%;
  background: #ffffff;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* La marca de «esta foto está recortada»: se puede volver a la original desde el mismo botón. */
.imagen-item.recortada .btn-crop-image-product{ color: var(--ins-warning) !important; }

/* Desktop: ocultar */
@media (pointer: fine) {
  .btn-del-image-product,
  .btn-crop-image-product{
    opacity: 0;
    pointer-events: none;
    transition: opacity .2s ease;
  }

  /* Mostrar solo al hover del contenedor */
  .imagen-item:hover .btn-del-image-product,
  .imagen-item:hover .btn-crop-image-product{
    opacity: 1;
    pointer-events: auto;
  }
}



/* ⚠ Acá había un `.dropzone` con `min-height`, `width` y `padding` en `!important`, de cuando la
   modal era la del tema. Al rediseñarla (2026-09-08) esos `!important` ganaban por encima de
   `.mod-img__*` —`!important` vence a la especificidad— y provocaban justo lo que se veía en
   escritorio: el área de soltar con 15 px de relleno en vez de 24, y la grilla de miniaturas
   arrastrando 150 px de alto aunque estuviera vacía, o sea un hueco muerto bajo el recuadro.
   Se quitó: el único dropzone del sistema es esta modal y ya tiene sus propios estilos. */
.dropzone { width: 100%; }



@media (pointer: coarse) {/*Móvil*/
  .dropzone .dz-preview.dz-image-preview {
    background: #fffafa;
    border-radius: 15px;
    width: 100%;
    height: 200px;
  }

  .dropzone .dz-preview .dz-image {
      width: 100%!important;
      height: 100%!important;
  }

  .dropzone .dz-preview .dz-image img {
      display: block;
      width: 100%;
      height: auto;
  }

  .dropzone .dz-preview.dz-image-preview {
      left: -15px!important;
  }
}


.skeleton-box {
  width: 100%;
  aspect-ratio: 1 / 1;
  border-radius: 6px;
  background: linear-gradient(
    100deg,
    #e4e4e4 40%,
    #f2f2f2 50%,
    #e4e4e4 60%
  );
  background-size: 200% 100%;
  animation: skeleton-loading 1.1s infinite;
}

@keyframes skeleton-loading {
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}



/************* Otros estilos *********************/

.content-miniaturas{
  overflow-y: auto;  max-height: 450px; padding: 25px;border-radius: 4px;
  /* Sin `background-size: cover` ni `background-position: center`: eran residuo de un
     `background-image` que quedó comentado, y estiraban el papel tapiz —que se define más abajo,
     junto al del tablero— hasta deformar una sola copia del dibujo. */
  min-height: 150px;
}

.inset-shadow1 {
  box-shadow:
  inset 0 2px 4px rgba(0, 0, 0, 0.18),
  inset 0 -2px 6px rgba(255, 255, 255, 0.7);
}


/**************************************/

.form-control-sm {
    max-height: 15px!important;
  }

/*.form-control {
  border: 1px solid lightslategrey;
  padding: 0.7rem 0.9rem;
  font-size: 0.9rem;
  transition: background-color .15s ease, box-shadow .15s ease;
  box-shadow: inset 2px 2px 3px rgba(0, 0, 0, 0.1);
}*/

.form-control:hover {
  background-color: #f1f3f9;
  border-color: #d0d5f7;
}

.form-control:focus {
  transition: background-color .15s ease, box-shadow .15s ease;
  box-shadow: inset 2px 2px 6px rgba(0, 0, 0, 0.1);
}


/*****************/

.variante-bloqueada {
  opacity: 0.3;
  pointer-events: none; /* evita interacción */
  transition: opacity 0.2s ease;
}

.variante-bloqueada input {
  background-color: #f5f5f5 !important;
}

/* El mensaje de validación se saca del flujo para que aparecer no empuje el resto del
   formulario hacia abajo. Sin `top`/`left` se queda en su posición estática —justo debajo del
   campo—, así que en un contenedor normal no se nota. */
.invalid-feedback{
  position:absolute!important;
}

/* …pero dentro de un `input-group` sí se nota, y feo. Por especificación, un hijo absoluto de un
   contenedor FLEX no se queda en su posición estática: se ancla al origen del contenedor, o sea
   la esquina superior izquierda del grupo, y el mensaje termina encima del campo.
   Acá se devuelve al flujo: `.input-group` es flex con `flex-wrap`, y como `.invalid-feedback`
   es `width:100%`, baja solo a la línea siguiente. Es el comportamiento que espera Bootstrap
   —por eso el grupo lleva `has-validation`— y el que muestra `form-validation.html`. */
.input-group .invalid-feedback{
  position:static!important;
}

/*******************/

.contnotfounddata{
  padding: 0px;
  min-height: 450px;
}


.crop-container{
    width:100%;
    max-height:500px;
    overflow:hidden;
}

.crop-container img{
    max-width:100%;
}

#preview{
  width:48px;
  height:48px;
  min-width:48px;
  min-height:48px;
  background: #cdcdcd;
  background-image: url('../images/image-placeholder.jpg');
  background-size: cover;
}

.preview-md{
    overflow:hidden;
    background:#eee;
    width:73px;
    height:73px;
}

/* ─────────────────────────────────────────────────────────────
   Acciones de cabecera en MÓVIL — convención de Mario (2026-08-15)
   Los botones de acción de una pantalla NUNCA se apilan uno por fila:
   van de dos en dos, al 50 % exacto y alineados entre sí, para no
   comerse el alto de la pantalla. El texto de cada botón se mantiene
   al mínimo (una palabra) para que no salte a dos líneas.
   Uso: <div class="acciones-cabecera d-flex flex-wrap gap-2 justify-content-end">
        dentro de un contenedor con flex-wrap.
   ───────────────────────────────────────────────────────────── */
@media (max-width: 767.98px) {
  .acciones-cabecera {
    width: 100%;
    margin-top: .5rem;
  }
  /* flex-grow en 0: un botón impar se queda al 50 % y NO se estira a fila completa.

     ⚠ Se resta MEDIO rem, no un cuarto. La clase se usa siempre con `gap-2` (.5rem), y con
     `calc(50% - .25rem)` la cuenta daba exacta —2 × (50 % − 4 px) + 8 px = 100 %— así que los dos
     botones no entraban por un pelo y se apilaban de uno, que es justo lo contrario de lo que
     pide esta regla. Medido el 2026-09-12 en un contenedor de 300 px: dos botones de 146 px, uno
     por renglón. Con .5rem sobran 8 px y entran. Afecta a las diez pantallas que la usan. */
  .acciones-cabecera > .btn {
    flex: 0 0 calc(50% - .5rem);
    max-width: calc(50% - .5rem);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
}

/* Flecha del menú lateral: el tema la rota con DOS reglas —`.side-nav-item.active` (la sección
   de la página actual) y `[aria-expanded=true]`— pero no tiene ninguna que la devuelva a 0. Al
   colapsar a mano una sección que además es la activa, la primera regla la deja apuntando hacia
   arriba como si siguiera abierta.
   El estado real de un desplegable lo dice `aria-expanded`, así que esa es la que manda. */
.side-nav .side-nav-item > a[aria-expanded="false"] .menu-arrow {
  transform: rotate(0deg) !important;
}

/* ── Apertura de modales: sin deformación ─────────────────────────────────────
   El tema (app.css) anima `.modal.show .modal-dialog` con `zoomInModal`, que va de
   `scale(0.85)` a `scale(1)`. Escalar el contenedor **escala todo lo de adentro**: durante esos
   100 ms el texto, los campos y los íconos se ven estirados. En una modal chica casi no se nota;
   en una grande —como la de registrar compra a mano— es evidente, y Mario lo reportó como que
   «se deforman los elementos» (2026-08-21).

   Se reemplaza por un desplazamiento corto con desvanecido, que es lo que hace Bootstrap de
   fábrica: mueve, no escala, así que nada se deforma. Va acá y no en `app.css` porque ese
   archivo es del tema y se pisa al actualizarlo. */
.modal.fade .modal-dialog {
  transform: translateY(-14px);
  transition: transform .18s ease-out, opacity .18s ease-out;
  opacity: 0;
}
.modal.show .modal-dialog {
  animation: none;          /* anula zoomInModal del tema */
  transform: translateY(0);
  opacity: 1;
}

/* Quien pidió menos animación no debería recibir ninguna. */
@media (prefers-reduced-motion: reduce) {
  .modal.fade .modal-dialog,
  .modal.show .modal-dialog {
    transition: none;
    animation: none;
    transform: none;
  }
}

/* ── Fila «Bodega destino» de la cabecera de líneas ───────────────────────────
   El select necesita un ancho útil en escritorio (suelto en un flex, `.form-select` se estira
   y se come la fila), pero en móvil un ancho fijo la desborda. Se fija un ancho cómodo desde
   `md` y se deja fluido abajo, con la fila envolviendo. */
.fila-bodega > .form-select { width: 100%; }

@media (min-width: 768px) {
  .fila-bodega > .form-select { width: 210px; }
}

/* En teléfono, select y botón comparten la fila al 50 %, como el resto de las acciones
   del sistema (ver `.acciones-cabecera`). */
@media (max-width: 767.98px) {
  .fila-bodega { width: 100%; }
  .fila-bodega > label { flex: 0 0 100%; }
  .fila-bodega > .form-select,
  .fila-bodega > .btn { flex: 1 1 calc(50% - .25rem); min-width: 0; }
}

/* ── Facturación: el contador de cantidad de cada línea ──────────────────────────────────────
   En el teléfono el número desaparecía entre el «−» y el «+»: dentro de un `input-group`,
   Bootstrap le da al campo `width: 1%` con `flex: 1 1 auto`, así que con la celda estrecha se
   encogía hasta no mostrar ni un dígito. Un ancho mínimo lo evita, y sin las flechitas nativas
   —que en un campo de 3 caracteres se comen la mitad— caben hasta cuatro cifras. */
/* ⚠ La especificidad importa: Bootstrap declara `.input-group > .form-control { min-width: 0 }`
   y un `.fld-cant { min-width: … }` a secas pierde y se queda en 0. */
.input-group > .form-control.fld-cant {
    min-width: 3.25rem;
}
/* ⚠ Y el `input-group` de Bootstrap es `flex-wrap: wrap`: en cuanto el campo dejó de encogerse,
   el «−», el número y el «+» se partieron en tres renglones. El contador va en una sola línea;
   si no cabe, que la tabla haga su scroll horizontal, que para eso está en `.table-responsive`. */
.grupo-cant {
    flex-wrap: nowrap;
}
.fld-cant::-webkit-outer-spin-button,
.fld-cant::-webkit-inner-spin-button {
    -webkit-appearance: none;
    margin: 0;
}
.fld-cant {
    -moz-appearance: textfield;
    appearance: textfield;
}

/* ── Iconos del árbol (jstree) ───────────────────────────────────────────────────────────────
   Vienen del tema Ubold 8 (`assets/css/app.min.css` de la plantilla) pero **el build de
   app.min.css instalado acá es anterior y no los trae** — se comprobó: 10 clases `treeview-*`
   en la plantilla, 0 en el instalado. Se copian solo estas 10 reglas en vez de reemplazar el CSS
   del tema entero, que tocaría todas las pantallas por un puñado de iconos. */
.treeview-folder-filled{width:16px!important;background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='%23f8ac59'%3E%3Cpath stroke='none' d='M0 0h24v24H0z' fill='none'/%3E%3Cpath d='M9 3a1 1 0 0 1 .608 .206l.1 .087l2.706 2.707h6.586a3 3 0 0 1 2.995 2.824l.005 .176v8a3 3 0 0 1 -2.824 2.995l-.176 .005h-14a3 3 0 0 1 -2.995 -2.824l-.005 -.176v-11a3 3 0 0 1 2.824 -2.995l.176 -.005h4z'/%3E%3C/svg%3E") no-repeat center/contain!important}
.treeview-folder-open{width:16px!important;background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23f8ac59' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z' fill='none'/%3E%3Cpath d='M5 19l2.757 -7.351a1 1 0 0 1 .936 -.649h12.307a1 1 0 0 1 .986 1.164l-.996 5.211a2 2 0 0 1 -1.964 1.625h-14.026a2 2 0 0 1 -2 -2v-11a2 2 0 0 1 2 -2h4l3 3h7a2 2 0 0 1 2 2v2'/%3E%3C/svg%3E") no-repeat center/contain!important}
.treeview-article{width:16px!important;background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23587581' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='icon icon-tabler icons-tabler-outline icon-tabler-article'%3E%3Cpath stroke='none' d='M0 0h24v24H0z' fill='none'/%3E%3Cpath d='M3 4m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z'/%3E%3Cpath d='M7 8h10'/%3E%3Cpath d='M7 12h10'/%3E%3Cpath d='M7 16h10'/%3E%3C/svg%3E") no-repeat center/contain!important}
.treeview-star{width:14px!important;background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23587581' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='icon icon-tabler icons-tabler-outline icon-tabler-star'%3E%3Cpath stroke='none' d='M0 0h24v24H0z' fill='none'/%3E%3Cpath d='M12 17.75l-6.172 3.245l1.179 -6.873l-5 -4.867l6.9 -1l3.086 -6.253l3.086 6.253l6.9 1l-5 4.867l1.179 6.873z'/%3E%3C/svg%3E") no-repeat center/contain!important}
.treeview-file{width:14px!important;background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23587581' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z' fill='none'/%3E%3Cpath d='M14 3v4a1 1 0 0 0 1 1h4'/%3E%3Cpath d='M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z'/%3E%3Cpath d='M9 9l1 0'/%3E%3Cpath d='M9 13l6 0'/%3E%3Cpath d='M9 17l6 0'/%3E%3C/svg%3E") no-repeat center/contain!important}
.treeview-icon-ban{width:14px!important;background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23587581' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='icon icon-tabler icons-tabler-outline icon-tabler-ban'%3E%3Cpath stroke='none' d='M0 0h24v24H0z' fill='none'/%3E%3Cpath d='M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0'/%3E%3Cpath d='M5.7 5.7l12.6 12.6'/%3E%3C/svg%3E") no-repeat center/contain!important}
.treeview-icon-user{width:14px!important;background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23587581' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='icon icon-tabler icons-tabler-outline icon-tabler-user'%3E%3Cpath stroke='none' d='M0 0h24v24H0z' fill='none'/%3E%3Cpath d='M8 7a4 4 0 1 0 8 0a4 4 0 0 0 -8 0'/%3E%3Cpath d='M6 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2'/%3E%3C/svg%3E") no-repeat center/contain!important}
.treeview-icon-link{width:14px!important;background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23587581' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='icon icon-tabler icons-tabler-outline icon-tabler-link'%3E%3Cpath stroke='none' d='M0 0h24v24H0z' fill='none'/%3E%3Cpath d='M9 15l6 -6'/%3E%3Cpath d='M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464'/%3E%3Cpath d='M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463'/%3E%3C/svg%3E") no-repeat center/contain!important}
.treeview-icon-database{width:14px!important;background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23587581' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='icon icon-tabler icons-tabler-outline icon-tabler-database'%3E%3Cpath stroke='none' d='M0 0h24v24H0z' fill='none'/%3E%3Cpath d='M12 6m-8 0a8 3 0 1 0 16 0a8 3 0 1 0 -16 0'/%3E%3Cpath d='M4 6v6a8 3 0 0 0 16 0v-6'/%3E%3Cpath d='M4 12v6a8 3 0 0 0 16 0v-6'/%3E%3C/svg%3E") no-repeat center/contain!important}
.treeview-icon-arrow{width:14px!important;background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23587581' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='icon icon-tabler icons-tabler-outline icon-tabler-arrow-right'%3E%3Cpath stroke='none' d='M0 0h24v24H0z' fill='none'/%3E%3Cpath d='M5 12l14 0'/%3E%3Cpath d='M13 18l6 -6'/%3E%3Cpath d='M13 6l6 6'/%3E%3C/svg%3E") no-repeat center/contain!important}




/*─── BOTÓN IA*/

    * { box-sizing: border-box; }

    body {
      min-height: 100vh;
      margin: 0;
      display: grid;
      place-items: center;
      background: #f4f6f5;
      font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
    }

    .boton-perlado {
      position: relative;
      isolation: isolate;
      overflow: hidden;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      min-height: 48px;
      padding: 0;
      border: 0;
      border-radius: 999px;
      background: transparent;
      color: #202523;
      cursor: pointer;
      filter: drop-shadow(0 4px 8px rgba(29, 74, 51, .14));
    }

    /* Borde base y destello que recorre el contorno */
    .boton-perlado::before {
      content: "";
      position: absolute;
      z-index: -2;
      left: 50%;
      top: 50%;
      width: 190%;
      aspect-ratio: 1;
      background:
        conic-gradient(
          #c8d1cc 0deg 238deg,
          #eafff3 255deg,
          #ffffff 273deg,
          #a5ffd1 291deg,
          #36ce8c 310deg,
          #ddffed 330deg,
          #c8d1cc 347deg 360deg
        );
      animation: recorrer-borde 3.2s linear infinite;
      box-shadow:
        0 0 7px rgba(102, 255, 183, .45),
        0 0 16px rgba(56, 210, 139, .20);
    }

    /* Superficie blanca: deja visible únicamente el borde */
    .boton-perlado::after {
      content: "";
      position: absolute;
      z-index: -1;
      inset: 2px;
      border-radius: inherit;
      background: linear-gradient(180deg, #ffffff 0%, #f8faf9 100%);
      box-shadow:
        inset 0 1px 0 #ffffff,
        inset 0 -1px 0 rgba(27, 83, 54, .05);
    }

    .boton-perlado__contenido {
      display: inline-flex;
      align-items: center;
      gap: 8px;
      padding: 0 19px;
      font-size: 15px;
      font-weight: 650;
      letter-spacing: -.01em;
    }

    .boton-perlado svg {
      width: 19px;
      height: 19px;
      color: #187c4d;
    }

    .boton-perlado:hover {
      transform: translateY(-1px);
      filter: drop-shadow(0 6px 11px rgba(29, 110, 67, .20));
    }

    .boton-perlado:hover::before {
      animation-duration: 1.8s;
    }

    .boton-perlado:active {
      transform: translateY(0) scale(.98);
    }

    .boton-perlado:focus-visible {
      outline: 3px solid rgba(44, 208, 132, .28);
      outline-offset: 5px;
    }

    @keyframes recorrer-borde {
      from { transform: translate(-50%, -50%) rotate(0deg); }
      to   { transform: translate(-50%, -50%) rotate(360deg); }
    }

    @media (prefers-reduced-motion: reduce) {
      .boton-perlado::before { animation: none; }
    }
/* ── SweetAlert siempre por encima del preloader ─────────────────────────────────────────────
   El preloader es una capa BLANCA OPACA en `z-index: 9999` y SweetAlert2 vive en 1060: cualquier
   código que abra un diálogo sin apagar antes el preloader deja la pantalla en blanco y sin
   respuesta —parece que el sistema se colgó—. Pasó el 2026-08-22 al clonar un documento con
   exoneraciones, que muestra un aviso antes de abrir el editor.

   Lo correcto es apagar el preloader (así quedó ese caso), pero esto lo vuelve imposible de
   arruinar en la próxima pantalla que lo olvide. */
.swal2-container {
    z-index: 10050 !important;
}

/* ── Marca en la esquina de un badge ─────────────────────────────────────────────────────────
   Un circulito sobre la esquina del distintivo, para decir algo más sin ocupar ancho: en una
   tabla de trece columnas, un segundo badge de texto ensancha esa columna en TODAS las filas,
   aunque solo unas pocas tengan algo que decir.

   Se usa hoy en el estado «Rechazado» del listado de facturas: `?` mientras no se haya emitido el
   comprobante que lo sustituye, `✓` cuando ya se reemplazó. El detalle va en el `title`.

   ⚠ El borde blanco no es decorativo: sin él, el círculo se confunde con el badge cuando los dos
   son de color fuerte. */
.con-marca {
    position: relative;
    display: inline-block;
}

.marca-esquina {
    position: absolute;
    top: -5px;
    right: -7px;
    width: 15px;
    height: 15px;
    line-height: 14px;
    border-radius: 50%;
    border: 1px solid #fff;
    font-size: 10px;
    font-weight: 700;
    text-align: center;
    cursor: help;   /* el puntero avisa de que hay algo que leer */
}

.marca-pendiente {
    background-color: var(--ins-warning, #f9c851);
    color: #6b4e00;
}

.marca-lista {
    background-color: var(--ins-success, #0acf97);
    color: #fff;
}

/* Acreditada en parte. Morado y no ámbar: el ámbar ya significa «pendiente» en la marca de los
   rechazados, y esto no es algo pendiente —es un hecho consumado sobre la factura—. */
.marca-parcial {
    background-color: var(--ins-primary, #6658dd);
    color: #fff;
    font-size: 11px;
}

/* Vino de la tienda web: nadie digitó este comprobante, lo importó el cron desde un pedido.

   ⚠ OSCURO y no uno de los colores de estado —ámbar «pendiente», verde «listo», índigo
   «parcial»—, por dos razones. La primera es de significado: de dónde vino un documento no es un
   estado suyo, y en esta lista cada color ya dice uno. La segunda es de legibilidad, y es la que
   decidió: se probó primero en cian y el carrito blanco sobre cian claro quedaba como una mancha
   sobre el badge verde del tiquete —medido ampliando la celda ocho veces—. Oscuro contrasta
   contra los cinco colores de tipo de documento (índigo, ámbar, verde y rojo) por igual.

   El pedido del que salió va en el `title`; `.marca-esquina` ya lo muestra al tocarlo. */
.marca-tienda {
    background-color: var(--ins-dark, #313a46);
    color: #fff;
    /* ⚠ El carrito es un GLIFO, no un carácter: a los 10-11 px con los que se leen el «✓» y el
       «?» de las otras marcas no se reconoce. Por eso este círculo es dos píxeles más grande. */
    width: 17px;
    height: 17px;
    line-height: 16px;
    font-size: 13px;
}

/* El área tocable, más grande que el dibujo. Un círculo de 15 px es cómodo de leer y muy chico
   para un dedo —lo recomendado son unos 24—, así que se agranda la zona sensible sin cambiar
   nada de lo que se ve. */
/* ⚠ Solo las que abren burbuja: una marca `sin-burbuja` no responde al toque, así que agrandarle
   la zona sensible únicamente le robaría clics al badge que tiene debajo. */
.marca-esquina:not(.sin-burbuja)::after {
    content: '';
    position: absolute;
    inset: -7px;
}


/* ───────── La franja de título dice de qué tipo es el producto ─────────
   Los dos tipos se editan en la MISMA pantalla y la diferencia solo se notaba leyendo los rótulos
   de adentro. Morado = producto simple · Verde = producto con variantes.
   Las clases las pone `editar-producto.js` con el `es_simple` que manda el backend. */
.page-title-head.es-tipo-producto {
    color: #fff;
    border-radius: var(--ins-border-radius, .375rem);
    padding: .65rem 1rem;
    /* Sin esto la franja queda pegada al borde de arriba: el `page-title-head` de las demás
       pantallas no tiene fondo, así que su falta de margen no se notaba. */
    margin-top: .75rem;
}
/* ⚠ Las variables del tema son `--ins-*`, NO `--bs-*`. Con `var(--bs-purple)` la variable no
   existía y siempre caía en el hexadecimal de respaldo, así que el color quedaba clavado y no
   seguía al selector de tema (el ícono de paleta de la barra superior cambia `--ins-purple` y
   `--ins-success`). Tampoco sirven las clases `bg-purple`/`text-bg-purple`: existen en el código
   fuente de Ubold pero NO en el `app.min.css` que se sirve. */
.page-title-head.es-producto-simple    { background-color: var(--ins-purple,  #7e5fc7); }
.page-title-head.es-producto-variantes { background-color: var(--ins-success, #10b981); }

/* El breadcrumb de la derecha tiene su propio color y sobre un fondo lleno quedaría ilegible. */
.page-title-head.es-tipo-producto .breadcrumb-item,
.page-title-head.es-tipo-producto .breadcrumb-item.active,
.page-title-head.es-tipo-producto .breadcrumb-item a {
    color: rgba(255, 255, 255, .85);
}
.page-title-head.es-tipo-producto .breadcrumb-item + .breadcrumb-item::before {
    color: rgba(255, 255, 255, .5);
}

/* Distintivo blanco sobre la franja; el tema no trae `text-purple`, así que el color va acá. */
.badge-tipo-producto            { background-color: #fff; }
.es-producto-simple    .badge-tipo-producto { color: var(--ins-purple,  #7e5fc7); }
.es-producto-variantes .badge-tipo-producto { color: var(--ins-success, #10b981); }


/* ───────── Empresa y sucursal: ahora vive DENTRO del menú lateral (2026-09-04) ─────────
   Acá había cinco reglas que le daban un `margin-left` del ancho del menú, porque la franja se
   pintaba fuera de `.content-page`, arrancaba en x=0 y el menú —que es `position: fixed`— le tapaba
   el principio: a 1280 px se comía 202 px y se leía «DESIGN · Sucursal 001».

   ⚠ Al mudarla dentro del menú esas reglas quedaron dañinas: le sumaban el ancho del menú a un
     bloque que YA está dentro del menú, y lo aplastaban a 48 px. Se descubrió midiendo el bloque
     aislado —48 px dentro de un lateral de 250—, no mirando la pantalla. Reglas de posicionamiento
     que sobreviven a una mudanza no quedan inertes: siguen aplicándose en el lugar equivocado.

   No hace falta CSS propio en la ubicación nueva: el bloque hereda el ancho del menú y el recorte
   lo hace `text-truncate` con un `min-width:0` en línea (la clase `min-w-0` NO existía en el
   `app.min.css` desplegado — comprobado; desde el 2026-09-27 la define este archivo, abajo). */


/* ═══════════════════════════════════════════════════════════════════════════════════════════
   .min-w-0 — la utilidad que el tema NO compiló
   ═══════════════════════════════════════════════════════════════════════════════════════════
   Bootstrap la documenta y el `app.min.css` desplegado no la trae: cero coincidencias, medido.
   Once sitios del proyecto la escriben —el POS, facturación, compras, banco, facturas, atención,
   el salón, dos modales de facturación— convencidos de que hacen algo, y no hacía nada en ninguno.

   Importa porque es la que permite que `text-truncate` funcione DENTRO de un flex: un hijo de
   flex arranca con `min-width: auto`, o sea que se niega a encogerse por debajo de su contenido,
   y entonces el texto largo no se recorta: empuja a sus hermanos fuera de la caja. En la ficha de
   «Dividir la cuenta» eso salía como el monto cortado a media cifra («₡3 451,3»).

   ⚠ Va acá y no en el CSS de un módulo justamente porque ya la usan diez pantallas: una copia por
   módulo es cómo se separaron los dos tableros. */
.min-w-0 { min-width: 0 !important; }


/* ═══════════════════════════════════════════════════════════════════════════════════════════
   TABLERO KANBAN — estilo compartido de la casa
   ═══════════════════════════════════════════════════════════════════════════════════════════
   Vive acá y no en el CSS de un módulo porque lo usan varias pantallas: el tablero de
   conversaciones de WhatsApp y el de Tareas hoy, cualquier otro tablero mañana. Antes cada uno
   tenía su copia y se fueron separando sin que nadie lo notara —el ghost de uno era amarillo y
   el del otro gris—; con una sola definición, arreglar uno arregla los dos.

   Cuelga de `.kanban-app`, que es la clase CON LA QUE EL TEMA envuelve un tablero: la pantalla
   ya tiene que ponerla para que el kanban del tema funcione —es de ahí que cuelga, por ejemplo,
   la regla que le quita la viñeta a la lista de tarjetas—, así que colgando de ella el estilo de
   la casa llega solo a cualquier tablero que se haga mañana. Se probó con una clase aparte y el
   resultado fue una pantalla con las tarjetas viñeteadas por no llevar también `kanban-app`.

   Qué le hace al tema: el tema pone las columnas pegadas, separadas por un borde punteado, a
   320 px y sin fondo. Acá van separadas, más angostas y sobre un panel gris con las tarjetas en
   blanco: el contraste lo pone el fondo, no el color de cada columna. El color del estado queda
   reducido al icono de la cabecera — con la columna entera teñida, cinco estados hacen ruido y
   ninguno se distingue.

   Lo que NO va acá: lo propio de cada pantalla (colores de sus etiquetas, alto de columna,
   cabecera con sus filtros). Eso sigue en el CSS de su módulo. */

.kanban-app .kanban-content {
  gap: 12px;
  padding: 12px;
  display: flex;
  overflow-x: auto;
  /* `stretch` y no `flex-start`: todas las columnas llegan hasta abajo, también las vacías. Con
     `flex-start` la columna sin tarjetas se encogía a su cabecera y dejaba tres píxeles para
     soltar la tarjeta que se arrastra, que es el gesto principal de estas pantallas. */
  align-items: stretch;
  /* Se queda con lo que sobre del alto de la tarjeta. Ver «El alto», más abajo. */
  flex: 1 1 auto;
  min-height: 0;
  /* Misma barra delgada que el resto del sistema.
     ⚠ NO se le agrega el truco de «rueda vertical → horizontal»: dentro de cada columna el
     contenido scrollea en VERTICAL, y secuestrar la rueda dejaría sin forma de bajar en una
     columna llena. Acá la barra alcanza. */
  scrollbar-width: thin;
  scrollbar-color: #c7ced4 transparent;
}
.kanban-app .kanban-content::-webkit-scrollbar { height: 8px; }
.kanban-app .kanban-content::-webkit-scrollbar-track { background: transparent; }
.kanban-app .kanban-content::-webkit-scrollbar-thumb { background: #c7ced4; border-radius: 4px; }
.kanban-app .kanban-content::-webkit-scrollbar-thumb:hover { background: #aab4bd; }

/* 320 → 260: a 260 px entran cinco columnas en un portátil sin scroll horizontal, y la tarjeta
   sigue mostrando su título y una línea más sin cortarse. */
.kanban-app .kanban-content .kanban-board {
  min-width: 260px;
  width: 260px;
  /* Y si sobra ancho, se reparten lo que sobra en vez de dejar una franja gris muerta a la
     derecha: con cinco columnas de 260 px en una pantalla ancha sobraban 270 px de nada.
     ⚠ El tope de 380 px es para el tablero de pocas columnas: sin él, dos columnas se van a
     700 px cada una y la tarjeta —que muestra dos líneas de texto— queda ridícula. */
  flex-grow: 1;
  max-width: 380px;
  /* ⚠ `background-color` y no `background`: la taquigrafía borra la imagen de la regla de abajo si
     alguna vez cambia el orden de las reglas. El color es el mismo de siempre; encima va el
     papel tapiz, que no lo tapa porque es solo trazo, sin relleno. */
  background-color: var(--ins-tertiary-bg, #f1f3f5);
  border-right: 0;                      /* el punteado del tema sobra cuando hay separación real */
  border-radius: var(--bs-border-radius, .375rem);
  box-shadow: 0 1px 2px rgba(0, 0, 0, .06);
  padding-bottom: 6px;
}

/* ── El papel tapiz gris ────────────────────────────────────────────────────────────────────
   Los mismos garabatos del hilo de WhatsApp —corazones, cámaras, tazas, cohetes—, en blanco sobre
   un gris claro. Pedido de Mario el 2026-09-05 para las columnas del tablero, y reutilizado el
   2026-09-08 en la galería del producto cuando está vacía: ese hueco gris parecía un error de
   carga, y con el dibujo se lee como un espacio que todavía no tiene fotos.

   Los dos usos comparten esta única declaración a propósito. Si algún día se suma un tercero, va
   a este mismo selector: el archivo y el tamaño de mosaico tienen que seguir siendo uno solo.

   En la galería el papel tapiz está SIEMPRE, no solo con el hueco vacío (pedido de Mario el
   2026-09-08): las miniaturas son tarjetas opacas y se apoyan sobre él sin estorbarse.

   Es OTRO archivo y no el del chat: el dibujo es el mismo y sale del mismo generador
   (`~/goroute-seed/whatsapp_fondo_svg.py`, que produce los dos de una), pero con opacidad plena.
   Sobre el beige del chat el blanco al 62 % se lee; sobre este gris casi blanco desaparece.
   ⚠ El SVG NO se edita a mano: se cambia el generador y se vuelve a correr.

   Mosaico de 300 px, el mismo del chat: cada figura está dibujada también corrida un mosaico en
   cada dirección, así que al repetirse no aparecen las «calles» vacías que delatan un patrón
   hecho a la ligera. */
.kanban-app .kanban-content .kanban-board,
.content-miniaturas {
  background-image: url("../images/patron-tablero.svg");
  /* El MISMO gris del tablero, que es sobre el que este dibujo blanco está pensado para leerse.
     ⚠ En la galería no es opcional: `.bggradient` pinta el hueco con la taquigrafía `background`,
     que esta regla gana por especificidad —y al ganarla se lleva puesto el color—. Sin declararlo
     acá, el dibujo queda blanco sobre blanco y no se ve nada. */
  background-color: var(--ins-tertiary-bg, #f1f3f5);
  background-size: 300px 300px;
  /* Desde arriba a la izquierda del contenedor, no del viewport: si no, dos columnas vecinas
     cortan el dibujo en un punto distinto y se nota el desfase entre ellas.
     ⚠ En la galería hace falta además porque `.content-miniaturas` traía `background-size: cover`,
     que estiraría una sola copia del dibujo hasta deformarla. */
  background-position: 0 0;
}

/* Cabecera de columna: blanca, para que se despegue del cuerpo gris. El primer `.kanban-item` de
   la columna es la cabecera; las tarjetas son los `.kanban-item` de adentro del `<ul>`. */
.kanban-app .kanban-content .kanban-board > .kanban-item {
  background: #fff;
  border-radius: var(--bs-border-radius, .375rem) var(--bs-border-radius, .375rem) 0 0;
  border-bottom: 1px solid var(--ins-border-color, #e9edef);
}

.kanban-app .kanban-content .kanban-board-group { padding: 8px; }

/* ── El alto: el tablero llega hasta el pie ─────────────────────────────────────────────────
   Pedido de Mario el 2026-09-10. Va acá y no en cada módulo por lo de siempre: los tres tableros
   tenían tres cuentas distintas —450 px fijos en Tareas, el `calc(100vh - 300px)` que el tema le
   clava al grupo en el de conversaciones, y un `calc` propio en Tickets— y se veían distintos
   entre sí aunque los tres dicen ser el mismo tablero.

   ⚠ El `calc` va sobre la TARJETA, no sobre las columnas. Sobre las columnas habría que restar
   también el alto de la cabecera, que es distinta en cada pantalla —una tiene buscador, otra dos
   desplegables— y la misma cuenta quedaba mal en dos de las tres. Sobre la tarjeta los 164 px son
   los mismos siempre, y salen de medir: 70 de la barra superior, 24 del `mt-3`, 46 del pie y 24
   de aire antes de él.

   De ahí para adentro manda el flex: la tarjeta reparte, el contenido se queda con lo que sobra y
   el cuerpo de cada columna es lo único que scrollea. `min-height: 0` en cada eslabón porque sin
   él un hijo flex se niega a encogerse por debajo de su contenido: el scroll interno no aparece
   nunca y en su lugar crece la PÁGINA.

   El `min-height` es el piso para pantallas muy bajas; ahí la página vuelve a scrollear como
   cualquier otra, que es preferible a un tablero de 200 px. */
.kanban-app > .card {
  display: flex;
  flex-direction: column;
  height: calc(100vh - 164px);
  min-height: 420px;
}
.kanban-app > .card > .card-body {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}
.kanban-app .kanban-content .kanban-board { display: flex; flex-direction: column; }

/* El cuerpo de la columna: lo único con scroll propio.
   ⚠ `height: auto` a la fuerza — el tema le clava `height: calc(100vh - 300px)`, que acá sería
   una segunda cuenta del mismo alto, desincronizada de la de arriba desde el primer cambio. */
.kanban-app .kanban-content .kanban-board-group {
  height: auto;
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}

/* ── En el teléfono: columnas más altas y barra de scroll agarrable ──────────────────────────
   Pedido de Mario (2026-09-11), y medido en los tres tableros: a 390 px la cabecera de la
   tarjeta —buscador, tres botones, cerrar turno, «+ Nueva»— se come 354 px, así que con la
   tarjeta encajada en la pantalla a la columna le quedaban 257 px: dos tarjetas y media.

   La salida es dejar de encajar la tarjeta en el alto de la pantalla. Acá manda el piso de la
   COLUMNA y la tarjeta crece detrás; la página vuelve a scrollear, que en un teléfono es el
   gesto natural y es preferible a una columna en la que no cabe el trabajo del día.

   ⚠ `dvh` y no solo `vh`: en el teléfono `100vh` mide como si la barra del navegador estuviera
   escondida —unos 60 px de más— y todo lo que se calcule con él queda por debajo del borde. El
   `vh` va primero como respaldo para el navegador que no entienda `dvh`. */
@media (max-width: 767.98px) {

  .kanban-app > .card {
    height: auto;
    min-height: calc(100vh - 110px);
    min-height: calc(100dvh - 110px);
  }

  .kanban-app .kanban-content .kanban-board-group {
    min-height: 62vh;
    min-height: 62dvh;
  }

  /* La barra de scroll, más gruesa: la de 8 px se pensó para el ratón y con el dedo no se
     agarra. Van las dos —la vertical de cada columna y la horizontal que corre entre columnas—
     porque en el teléfono las dos se usan a diario.

     ⚠ En el navegador de escritorio esto se ve tal cual, pero un teléfono de verdad puede
     dibujar SU barra flotante y pasar del ancho. No es un respaldo que se pueda escribir: es
     decisión del navegador. */
  .kanban-app .kanban-content,
  .kanban-app .kanban-content .kanban-board-group {
    scrollbar-width: auto;                    /* Firefox */
    scrollbar-color: #aab4bd transparent;
  }

  .kanban-app .kanban-content::-webkit-scrollbar,
  .kanban-app .kanban-content .kanban-board-group::-webkit-scrollbar {
    width: 12px;
    height: 12px;
  }

  .kanban-app .kanban-content .kanban-board-group::-webkit-scrollbar-track {
    background: transparent;
  }

  .kanban-app .kanban-content .kanban-board-group::-webkit-scrollbar-thumb {
    background: #c7ced4;
    border-radius: 6px;
  }

  /* ⚠ La horizontal ya tiene su regla arriba, fuera de la consulta de medios, con 8 px y el
     mismo color. Acá solo se le cambia el grueso: repetir el pulgar entero sería una segunda
     definición del mismo adorno, y la próxima vez que alguien cambie el color en una se
     olvidaría de la otra. */
}

/* «Nada por acá». Compartido: estaba solo en Tareas y las otras dos dejaban la columna vacía sin
   una palabra, que con el papel tapiz se lee como que todavía está cargando. */
.kanban-app .col-vacia {
  list-style: none;
  font-size: .78rem;
  color: var(--bs-secondary-color, #6c757d);
  text-align: center;
  padding: 14px 8px;
}

/* ── Que se vea que se puede arrastrar ──────────────────────────────────────────────────────
   Tres señales, porque una sola no alcanza: el asa `sort-handle` del tema (que ya trae
   `cursor:move` y se pinta de primario al pasar por encima), la mano abierta sobre toda la
   tarjeta, y el puño cerrado mientras se arrastra. */
.kanban-app .kanban-item[data-id] { cursor: grab; }
.kanban-app .kanban-item[data-id]:active { cursor: grabbing; }
.sortable-fallback, .sortable-fallback * { cursor: grabbing !important; }

/* El asa aparece al pasar por encima de la tarjeta: mostrarla siempre en cinco columnas llenas
   ensucia, y sobre la tarjeta apuntada dice lo mismo justo cuando hace falta. */
.kanban-app .kanban-item[data-id] .sort-handle {
  opacity: 0;
  transition: opacity .12s;
  padding-right: 0;
}
.kanban-app .kanban-item[data-id]:hover .sort-handle { opacity: .45; }

/* En táctil no hay `:hover`: ahí el asa se queda visible o no se ve nunca. */
@media (hover: none) {
  .kanban-app .kanban-item[data-id] .sort-handle { opacity: .35; }
}

/* Punteado en vez de sólido: sobre el panel gris dice «esto se despega y se mueve», que es lo que
   el borde continuo —el mismo de cualquier tarjeta fija del sistema— no dice. El azul lo eligió
   Mario. */
.kanban-app { --gr-tablero-borde: #477aab; }

.kanban-app .kanban-item[data-id] .card {
  border: 1px dashed var(--gr-tablero-borde);
  transition: box-shadow .12s, border-color .12s;
}
.kanban-app .kanban-item[data-id]:hover .card {
  box-shadow: 0 2px 6px rgba(0, 0, 0, .10) !important;
}

/* El hueco que deja la tarjeta mientras se arrastra, en amarillo también cuando la tarjeta NO
   lleva un `.card` adentro. El tema pinta `.sortable-item-ghost .card`, y las tarjetas del
   tablero de tickets son la tarjeta misma: sin esto, el hueco salía casi blanco ahí y amarillo en
   los otros dos tableros. */
.kanban-app .sortable-item-ghost { background-color: rgba(var(--ins-warning-rgb), .1); }

/* ── Mientras se arrastra ───────────────────────────────────────────────────────────────────
   ⚠ Acá NO va nada: los tres estados los pone el TEMA y se piden desde el JS de cada tablero con
   las opciones de Sortable.
     · el hueco que deja la tarjeta → `ghostClass: 'sortable-item-ghost'`, que el tema pinta de
       amarillo claro (`rgba(--ins-warning-rgb, .1)`). Ese amarillo es la seña de «acá va a caer».
     · la tarjeta elegida            → `chosenClass: 'sortable-item-active'`.
     · la que viaja con el cursor    → el tema ya la inclina 10° con `.sortable-fallback .card`.

   Se intentó definirlos acá con clases propias y fue justo lo que separó a los dos tableros: el
   hueco salía gris en vez de amarillo, y una regla propia de giro pisaba los 10° del tema y los
   dejaba en 3°. Si algún día hay que cambiar el gesto, se cambia en el tema o se anota abajo con
   la razón — pero por defecto, manda el tema. */

/* ── Teléfono ────────────────────────────────────────────────────────────────────────────────
   Columnas más angostas para que se vea que asoma la siguiente y se entienda que el tablero se
   desplaza de lado. Va en el bloque compartido y no en un módulo: la razón vale para cualquier
   tablero, y tenerla en uno solo era otra forma de que se separaran. */
@media (max-width: 575.98px) {
  .kanban-app .kanban-content .kanban-board { min-width: 240px; width: 240px; }
  .kanban-app .kanban-content { padding: 8px; gap: 8px; }
}

/* ── Logos de proveedores de IA ──────────────────────────────────────────────────────────────
   ⚠ Los SVG se dibujan en `ia-cuentas.js`, NO son los archivos oficiales de marca: los logotipos
   de OpenAI, Anthropic y Google son marcas registradas y traerlos al servidor es otra discusión.
   Estos evocan forma y color, que es lo que hace falta para reconocerlos en una tabla.
   El color lo pone el JS en línea, porque es propio de cada proveedor. */
.ia-logo { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; }
.ia-logo svg { width: 100%; height: 100%; }

/* El toast de esta pantalla, con el contenedor que evita la capa desenfocada. */
.ia-toast-cont { backdrop-filter: none !important; background: transparent !important; }

/* ── Espera de una conversación sin tomar (bandeja, tablero y lista) ────────────────────────────
   Colgado de las clases propias y no de un módulo: la misma señal se pinta en tres pantallas y
   tres copias del estilo terminan separándose. Los colores salen de las variables del TEMA
   (`--ins-*`), que es lo que hace que se vea como el resto del sistema aunque cambie la paleta. */
.wa-espera {
  display: inline-flex;
  align-items: center;
  gap: .25rem;
  padding: .05rem .4rem;
  border-radius: 1rem;
  font-size: .7rem;
  font-weight: 600;
  line-height: 1.4;
  white-space: nowrap;
}

.wa-espera i { font-size: .85rem; }

/* Ámbar: lleva un rato. Rojo: hay que mirarlo ya. Sin un tercer nivel «tranquilo»: una señal que
   está siempre encendida deja de ser una señal. */
.wa-espera-aviso {
  color: var(--ins-warning, #f9c851);
  background-color: rgba(var(--ins-warning-rgb, 249, 200, 81), .15);
}

.wa-espera-urgente {
  color: var(--ins-danger, #ef5f5f);
  background-color: rgba(var(--ins-danger-rgb, 239, 95, 95), .15);
}

/* ── Secciones colapsables de /bot ──────────────────────────────────────────────────────────────
   El título hace de disparador, así que la flechita indica si está abierto o cerrado. Gira en vez
   de cambiar de ícono: un solo elemento y sin salto al redibujar. */
.bot-chevron { transition: transform .15s ease-in-out; }
[aria-expanded="true"] .bot-chevron { transform: rotate(90deg); }

/* ── Vista previa de una repregunta (/bot) ──────────────────────────────────────────────────────
   Imita la burbuja de WhatsApp para que se vea ANTES de guardar cómo le llega al cliente: sobre
   todo el corte de los 3 botones, que a partir de 4 opciones WhatsApp convierte en una lista. */
/* ── El lienzo llega hasta el pie ──────────────────────────────────────────────────────────
   Mismo patrón que `.kanban-app`, y los mismos 164 px: 70 de barra + 24 del `mt-3` + 46 del pie
   + 24 de aire. No se inventa una cuenta nueva — ya hay una medida y probada en este archivo.

   ⚠ Antes esto lo hacía `ajustarAlto()` en JS, midiendo el hueco contra el pie y escribiendo el
   alto en línea. Dependía de correr en el momento justo, y cuando algo cambiaba de alto después
   —plegar la ayuda, las fichas ajustándose— quedaba una franja vacía sobre el pie. Mario lo
   reportó dos veces. En CSS no hay momento justo: el alto es el que es.

   `min-height: 0` en cada eslabón porque sin él un hijo flex no se encoge por debajo de su
   contenido, y el lienzo empujaría la página en vez de repartirse el alto. */
.flujo-pantalla > .card {
  display: flex;
  flex-direction: column;
  height: calc(100vh - 164px);
  min-height: 420px;
}

/* ── El lienzo a sangre, como la bandeja ───────────────────────────────────────────────────
   Pedido de Mario (2026-09-16), con una captura marcando en rojo los márgenes laterales y el
   pie: «como en whatsapp aprovechar esos espacios y quitar el footer».

   ⚠ Va por clase propia y NO tocando `.content-page` ni `.container-fluid` a secas: esas dos
   las usan las 48 pantallas del sistema. Es el mismo criterio que `.wa-pantalla`, de donde
   sale este patrón. */
.content-page.flujo-full,
.container-fluid.flujo-full {
  padding-left: 0;
  padding-right: 0;
}

/* ⚠ Y hay que neutralizar el canal del `.row`, o el arreglo de arriba crea un desborde nuevo:
   un `.row` lleva `margin` NEGATIVO de media canaleta a cada lado, que normalmente compensa el
   relleno del contenedor. Al quitar ese relleno, los márgenes negativos quedan sueltos. Medido a
   1440: `scrollWidth` 1452 contra `clientWidth` 1440 — 12 px de desborde horizontal que no
   estaban antes. La bandeja no lo sufre porque no usa `.row`.

   ⚠ Y NO alcanza con `--bs-gutter-x: 0`, que es lo que dice la documentación de Bootstrap:
   medido, la variable queda en 0 y el `margin-left` calculado sigue en -12px. En este tema el
   `.row` no deriva su margen de esa variable. Va explícito, y el relleno de la columna con él
   —si no, el borde interno queda a 12 px de un lienzo que se quería a ras—. */
.container-fluid.flujo-full > .row {
  margin-left: 0;
  margin-right: 0;
}

.container-fluid.flujo-full > .row > [class*="col-"] {
  padding-left: 0;
  padding-right: 0;
}

/* ⚠ La cuenta del alto cambia CON el pie quitado: antes eran 164 px —70 de barra + 24 del
   `mt-3` + 46 del pie + 24 de aire— y ahora solo la barra. Si se devuelve el pie hay que
   devolver los 164, o el lienzo se le monta encima.
   ⚠ `dvh` además de `vh`: en el teléfono la barra del navegador se esconde al desplazar y con
   `vh` quedaría un trozo bajo el borde (ver la memoria `movil_100vh_no_es_lo_que_se_ve`). */
.flujo-full .flujo-pantalla > .card {
  height: calc(100vh - 70px);
  height: calc(100dvh - 70px);
  margin-bottom: 0;
  border-left: 0;
  border-right: 0;
  border-bottom: 0;
  border-radius: 0;
}

.flujo-pantalla > .card > .card-body {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}

.flujo-pantalla .flujo-area {
  flex: 1 1 auto;
  min-height: 0;
}

/* ⚠ `height: auto` a la fuerza: el alto de arranque de 420 px existe porque LogicFlow mide su
   contenedor al crearse y con alto cero no dibuja nada, pero acá el alto lo reparte el flex. */
.flujo-pantalla .flujo-lienzo {
  height: auto;
  min-height: 0;
}

/* ══ El flujo del asistente ═════════════════════════════════════════════════════════════════
   Panel a la izquierda —el índice—, lienzo a la derecha. Un grafo se lee mejor mirándolo, pero
   encontrar algo concreto en él no escala: con diez preguntas se busca por nombre, no a ojo. */
.flujo-area {
  display: flex;
  gap: 1rem;
  align-items: stretch;
}

.flujo-panel {
  flex: 0 0 15rem;
  display: flex;
  flex-direction: column;
  min-height: 0;                 /* sin esto el hijo no se encoge y la lista nunca hace scroll */
  border: 1px solid var(--ins-border-color, #e7e9eb);
  border-radius: var(--ins-border-radius, 6px);
  background-color: var(--ins-secondary-bg, #fff);
  overflow: hidden;
}

/* ── En el teléfono, uno debajo del otro ───────────────────────────────────────────────────
   En fila, el panel se lleva 15rem fijas (`flex: 0 0`) y a 390 px al lienzo le quedaba una franja
   donde un grafo no se puede ni mirar. Apilados, el lienzo se queda con TODO el ancho del
   teléfono, que es lo que pidió Mario.

   ⚠ El corte es 767.98 px, el mismo `md` de Bootstrap con el que ya trabaja `.acciones-cabecera`:
   dos cortes distintos en la misma pantalla hacen que la cabecera se reacomode en un ancho y el
   cuerpo en otro.

   ⚠ En columna, `flex-basis` pasa a ser ALTO: las mismas 15rem dejarían un panel de 240 px
   comiéndose la pantalla antes de llegar al lienzo. Se sujeta con `max-height` y la lista, que ya
   tiene su propio scroll, se encarga del resto. */
@media (max-width: 767.98px) {
  .flujo-area {
    flex-direction: column;
  }

  .flujo-panel {
    flex: 0 0 auto;
    max-height: 11rem;
  }

  /* ⚠ El lienzo se SALE del relleno del `card-body` (24 px por lado, medidos): de los 98 px que
     se perdían entre el borde del teléfono y el lienzo, 48 son de ahí y son los únicos que se
     pueden recuperar sin desarmar el marco de la tarjeta. Medido a 390 px: de 292 a 340.
     Los bordes laterales y el redondeo se van con ellos, o quedaría una caja flotando a ras. */
  .flujo-lienzo {
    width: auto;
    margin-left: -24px;
    margin-right: -24px;
    border-left: 0;
    border-right: 0;
    border-radius: 0;
  }

  /* ⚠ En el teléfono la tarjeta NO se ata a la ventana y el lienzo recupera su piso. Apilados, el
     `calc(100vh - 164px)` tiene que alcanzar para el panel Y el lienzo, y el `min-height: 0` que
     en escritorio deja repartir el alto acá lo dejaba en 2 px —medido a 390—: un lienzo invisible.
     Con la tarjeta en alto automático la página scrollea, que en un teléfono es lo normal. */
  .flujo-pantalla > .card,
  .flujo-full .flujo-pantalla > .card {
    height: auto;
    min-height: 0;
  }

  .flujo-pantalla .flujo-lienzo {
    min-height: 420px;
  }
}

.flujo-panel-buscar {
  padding: .6rem;
  border-bottom: 1px solid var(--ins-border-color, #e7e9eb);
}

.flujo-panel-lista {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: .4rem 0 .6rem;
}

/* El rótulo de cada grupo. Más aire ARRIBA que abajo: así pertenece a lo que sigue y no flota
   entre dos grupos. */
.flujo-grupo {
  padding: .9rem .75rem .25rem;
  font-size: .6875rem;
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--ins-secondary-color, #9ba6b7);
}

.flujo-grupo:first-child { padding-top: .35rem; }

.flujo-item {
  display: flex;
  align-items: center;
  gap: .5rem;
  width: 100%;
  padding: .4rem .75rem;
  border: 0;
  background: none;
  text-align: left;
  font-size: .8125rem;
  color: var(--ins-body-color, #4c4c5c);
  cursor: pointer;
}

.flujo-item:hover,
.flujo-item:focus-visible {
  background-color: var(--ins-tertiary-bg, #f6f7fb);
  outline: none;
}

/* La marca de color dice de qué tipo es sin leer el grupo: es la misma señal que en el lienzo. */
.flujo-item-punto {
  flex: 0 0 auto;
  width: .5rem;
  height: .5rem;
  border-radius: 50%;
}

.flujo-item-texto {
  flex: 1 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.flujo-item-cuenta {
  flex: 0 0 auto;
  font-size: .6875rem;
  color: var(--ins-secondary-color, #9ba6b7);
  font-variant-numeric: tabular-nums;
}

/* ── Textos editables dentro de una ficha ──────────────────────────────────────────────────
   Sin marca visual, por pedido de Mario (2026-09-14): el subrayado punteado que había al pasar
   por encima llenaba la ficha de rayas y le ganaba al contenido, que es lo que hay que leer. La
   señal de que se puede editar queda en el cursor de texto y en el rótulo emergente. */
.flujo-editable {
  cursor: text;
}

/* Lo que recibe el cliente, a la vista: sin verlo no se puede editar en el lugar. Dos líneas como
   tope — es el texto más largo de la ficha y sin recorte empujaría las opciones fuera del alto que
   LogicFlow tiene calculado. */
.flujo-nodo-texto {
  padding: .25rem .6rem;
  font-size: .6875rem;
  line-height: 1.3;
  color: var(--ins-secondary-color, #9ba6b7);
  /* Sin recorte: la ficha se ajusta a su contenido, así que cortar el texto a dos líneas sería
     esconder justo lo que se quiere leer y editar. El alto lo mide `ajustarAlContenido()`. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* El campo mientras se edita. Hereda tamaño y color para que el texto no salte al entrar y salir
   del modo edición: un salto se lee como que la ficha cambió de contenido. */
.flujo-editor {
  width: 100%;
  font: inherit;
  color: inherit;
  line-height: inherit;
  background: var(--ins-secondary-bg, #fff);
  border: 1px solid var(--ins-primary, #7f56d9);
  border-radius: 3px;
  padding: 0 .2rem;
  resize: none;
}

.flujo-editor:focus {
  outline: none;
  box-shadow: 0 0 0 2px rgba(127, 86, 217, .18);
}

/* ── Una ficha que está FUERA del lienzo ───────────────────────────────────────────────────
   Sigue en el panel —es la única forma de devolverla— pero no es lo mismo que las demás: no se
   puede centrar porque no está dibujada, y sí se puede arrastrar. Se apaga en vez de esconderse:
   una lista donde las fichas desaparecen al sacarlas no deja forma de traerlas de vuelta. */
.flujo-item.esta-fuera {
  color: var(--ins-secondary-color, #9ba6b7);
  cursor: grab;
}

.flujo-item.esta-fuera:active {
  cursor: grabbing;
}

/* El punto de color se apaga con la tarjeta: encendido, la ficha parecería estar en el dibujo. */
.flujo-item.esta-fuera .flujo-item-punto {
  opacity: .35;
}

.flujo-item-fuera {
  flex: 0 0 auto;
  font-size: .875rem;
  opacity: .6;
}

/* Las dos salidas de una opción, apiladas: lo que el cliente recibe después y dónde queda
   clasificada la conversación. Se distinguen por color, no por adorno — el recorrido lleva el
   color principal y la clasificación se atenúa, igual que sus flechas en el lienzo. */
/* El mensaje que sale al tocar el botón. Naranja, como su ficha y su flecha: el color es lo que
   dice de qué tipo de cosa se trata, en la fila y en el lienzo. */
.flujo-opcion-msg {
  color: var(--ins-orange, #fd7e14);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 100%;
}

/* La misma ficha, otro papel: en una rama que redacta la IA ese texto no se manda, la alimenta.
   Va del morado de la IA y no del naranja de los mensajes, que es el color de lo que el cliente
   SÍ recibe tal cual. Mismo #6d28d9 de `.flujo-opcion-ia`: 7.1:1 sobre blanco. */
.flujo-opcion-msg.es-contexto { color: #6d28d9; }

.flujo-opcion-salidas {
  flex: 0 1 auto;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  min-width: 0;
  text-align: right;
}

.flujo-opcion-sigue {
  color: var(--ins-primary, #6658dd);
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 100%;
}

/* ── El «+» para meter una pregunta en una flecha ──────────────────────────────────────────
   Aparece en el medio de la línea, con el ratón encima o con la flecha elegida. Lo segundo no es
   un adorno: en el teléfono no hay hover y sin eso el botón no existiría ahí.

   El área táctil se estira con un `::before` invisible en vez de agrandar el círculo: un botón de
   44 px dibujado encima de la línea taparía justo lo que hay que ver. */
.flujo-mas {
  position: absolute;
  z-index: 5;
  transform: translate(-50%, -50%);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border: 1.5px solid var(--ins-primary, #6658dd);
  border-radius: 50%;
  background-color: var(--ins-secondary-bg, #fff);
  color: var(--ins-primary, #6658dd);
  font-size: .75rem;
  line-height: 1;
  cursor: pointer;
  box-shadow: 0 1px 4px rgba(16, 24, 40, .18);
}

.flujo-mas::before {
  content: '';
  position: absolute;
  top: -11px;
  right: -11px;
  bottom: -11px;
  left: -11px;
}

.flujo-mas:hover,
.flujo-mas:focus-visible {
  background-color: var(--ins-primary, #6658dd);
  color: #fff;
  outline: none;
}

/* ── Confirmación anclada ──────────────────────────────────────────────────────────────────
   Comparte caja con el menú para que la pantalla hable con una sola voz. No lleva scroll ni
   buscador: son dos frases y dos botones, y si necesitara más ya no sería una confirmación. */
/* ⚠ Sin `width` propio: `.gr-menu` la declara después en este archivo y ganaría igual. Se queda
   con el ancho del menú, que además es lo correcto — las dos cajas salen del mismo botón y que
   midan distinto se nota. */
.gr-confirmar {
  max-height: none;
}

/* El velo: asienta la caja y da dónde tocar para salir sin decidir. Tenue a propósito — lo que hay
   detrás es la conversación sobre la que se está decidiendo y conviene poder mirarla. */
.gr-velo {
  position: fixed;
  inset: 0;
  z-index: 1080;
  background: rgba(15, 23, 42, .32);
}

.gr-confirmar-cuerpo {
  padding: .85rem 1rem .6rem;
}

.gr-confirmar-titulo {
  font-size: .875rem;
  font-weight: 600;
  color: var(--ins-body-color, #4c4c5c);
  margin-bottom: .3rem;
}

.gr-confirmar-texto {
  font-size: .8125rem;
  line-height: 1.45;
  /* ⚠ No `--ins-secondary-color`: da 2.46:1 sobre blanco, por debajo del 4.5:1 que necesita un
     texto. Acá importa de verdad —es la frase que dice qué va a pasar si se acepta—, así que va
     el mismo matiz oscurecido hasta 4.86:1. */
  color: #6a727e;
}

/* Los botones a la derecha y Cancelar primero: la salida queda donde el pulgar la busca y la
   acción no es lo primero que se toca. */
.gr-confirmar-pie {
  display: flex;
  justify-content: flex-end;
  gap: .4rem;
  padding: .6rem 1rem .85rem;
}

/* ⚠ Este menú dejó de ser solo del lienzo: la bandeja de WhatsApp lo usa para transferir una
   conversación (2026-09-15). Por eso cada regla lleva también `.gr-menu`, con nombre neutro. Se
   conserva `.flujo-menu` en vez de renombrar: la pantalla del lienzo ya funciona y cambiarle las
   clases para ganar prolijidad es arriesgar algo que anda. Si aparece un tercer uso, conviene
   quedarse solo con `.gr-menu` y migrar el lienzo de una vez.

/* ── El menú que sale del «+» ──────────────────────────────────────────────────────────────
   Reemplaza al diálogo centrado. En un lienzo, una ventana en el medio de la pantalla te saca de
   donde estabas mirando y te obliga a reconstruir el contexto al volver; este sale PEGADO a la
   flecha sobre la que apretaste, que es de lo que trata la decisión. Mario: «se ve muy
   desordenado». */
.flujo-menu,
.gr-menu {
  position: absolute;
  z-index: 20;
  width: 17rem;
  max-height: 20rem;
  display: flex;
  flex-direction: column;
  border: 1px solid var(--ins-border-color, #e7e9eb);
  border-radius: 6px;
  background-color: var(--ins-secondary-bg, #fff);
  box-shadow: 0 6px 24px rgba(16, 24, 40, .16);
  overflow: hidden;
}

/* ── Centrado: vale para TODA caja `.gr-menu` (menús de opciones y confirmaciones) ───────────
   ⚠ Va DESPUÉS del bloque de arriba a propósito. Aquel declara `position: absolute` con la misma
   especificidad —una clase—, así que puesto antes perdía y el centrado no se aplicaba. Antes no
   se notaba porque el JS ponía `position: fixed` en el atributo `style`, que gana a las dos; al
   sacarlo, el orden de este archivo pasó a ser lo único que decide.

   ⚠ Y solo `.gr-menu`, NUNCA `.flujo-menu`. El lienzo del asistente comparte estas reglas por el
   selector doble, pero allá el menú sale de una flecha concreta del dibujo y anclarlo es lo que
   dice sobre qué se está eligiendo. En la bandeja no: el menú se abre siempre sobre la
   conversación que ya está en pantalla, así que el punto del clic no agregaba nada y sí tapaba
   los mensajes (Mario, 2026-09-15).

   El centrado va con `transform` y no con coordenadas en JS para que aguante un cambio de alto o
   un giro del teléfono. */
.gr-menu {
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  z-index: 1081;
  max-width: min(92vw, 360px);
}

.flujo-menu-buscar,
.gr-menu-buscar {
  padding: .5rem;
  border-bottom: 1px solid var(--ins-border-color, #e7e9eb);
}

/* ⚠ `min-height: 0` o la lista no se encoge y el buscador se va fuera de la caja. */
.flujo-menu-lista,
.gr-menu-lista {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: .25rem 0 .4rem;
}

.flujo-menu-grupo,
.gr-menu-grupo {
  padding: .45rem .7rem .2rem;
  font-size: .625rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--ins-secondary-color, #9ba6b7);
}

.flujo-menu-item,
.gr-menu-item {
  display: flex;
  align-items: center;
  gap: .45rem;
  width: 100%;
  padding: .35rem .7rem;
  border: 0;
  background: none;
  text-align: left;
  font-size: .8125rem;
  color: var(--ins-body-color, #4c4c5c);
  cursor: pointer;
}

.flujo-menu-item:hover,
.flujo-menu-item:focus-visible,
.flujo-menu-item.activa,
.gr-menu-item:hover,
.gr-menu-item:focus-visible,
.gr-menu-item.activa {
  background-color: var(--ins-tertiary-bg, #f6f7fb);
  outline: none;
}

.flujo-menu-item i,
.gr-menu-item i {
  flex: 0 0 auto;
  font-size: .875rem;
}

.flujo-menu-item-texto,
.gr-menu-item-texto {
  flex: 1 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.flujo-menu-pie,
.gr-menu-pie {
  padding: .4rem .7rem;
  border-top: 1px solid var(--ins-border-color, #e7e9eb);
  font-size: .6875rem;
  line-height: 1.35;
  /* Mismo matiz que `--ins-secondary-color` pero legible: aquel da 2.46:1 sobre blanco. */
  color: #6a727e;
}

.flujo-menu-vacio,
.gr-menu-vacio {
  padding: 1rem .7rem;
  text-align: center;
  font-size: .75rem;
  color: var(--ins-secondary-color, #9ba6b7);
}

.flujo-panel-vacio {
  padding: 1.5rem .75rem;
  text-align: center;
  font-size: .75rem;
  color: var(--ins-secondary-color, #9ba6b7);
}

/* ── Las cajas del lienzo ───────────────────────────────────────────────────────────────────
   Son nodos HTML de LogicFlow, no rectángulos: cabecera con el color del tipo y cuerpo con lo
   que ese nodo hace. Una pregunta enseña sus opciones y a dónde va cada una — que es la
   información que antes estaba desparramada en los rótulos de las flechas, pisándose entre sí. */
.flujo-nodo {
  height: 100%;
  border: 1px solid var(--ins-border-color, #e7e9eb);
  border-radius: 8px;
  background-color: var(--ins-secondary-bg, #fff);
  box-shadow: 0 1px 2px rgba(16, 24, 40, .06), 0 4px 10px rgba(16, 24, 40, .05);
  overflow: hidden;
  font-size: .75rem;
  transition: box-shadow .16s ease, transform .16s ease;
}

.flujo-nodo:hover {
  transform: translateY(-1px);
  box-shadow: 0 2px 4px rgba(16, 24, 40, .08), 0 10px 22px rgba(16, 24, 40, .10);
}

.flujo-nodo-cab {
  display: flex;
  align-items: center;
  gap: .4rem;
  padding: .5rem .6rem;
  font-weight: 600;
  line-height: 1.25;
  border-bottom: 1px solid var(--ins-border-color, #e7e9eb);
}

.flujo-nodo-cab i { font-size: .95rem; }

.flujo-nodo-titulo {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.flujo-nodo-cuerpo { padding: .35rem 0; }

/* Cada opción del menú, con su destino a la derecha. Tabular para que los destinos se alineen. */
.flujo-opcion {
  display: flex;
  align-items: baseline;
  gap: .5rem;
  padding: .2rem .6rem;
  color: var(--ins-body-color, #4c4c5c);
}

.flujo-opcion-rotulo {
  flex: 1 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.flujo-opcion-destino {
  flex: 0 0 auto;
  font-size: .6875rem;
  color: var(--ins-secondary-color, #9ba6b7);
}

.flujo-opcion-destino.es-vacio { opacity: .55; }

/* La fila de una opción se puede tocar: abre el menú que decide si ahí contesta una persona. El
   puntero y el realce son lo único que lo anuncia —sin ellos el gesto existe y nadie lo encuentra. */
.flujo-opcion { cursor: pointer; border-radius: 3px; }
.flujo-opcion:hover { background: var(--ins-tertiary-bg, #f6f7fb); }
/* El rótulo conserva el suyo: ahí el gesto es doble clic para renombrar, no abrir el menú. */
.flujo-opcion .flujo-opcion-rotulo { cursor: text; }

/* ── «Acá contesta una persona» ──────────────────────────────────────────────────────────────
   Azul fuerte, pedido de Mario (2026-09-15), para distinguir de un vistazo dónde deja de
   contestar el bot. #1d4ed8 da 6.70:1 sobre blanco y 6.16:1 sobre el azul tenue de la fila:
   sirve para texto chico, que es lo que es.

   ⚠ El color NO es lo único que lo dice —la fila lleva además «pasa a un agente» escrito—: quien
   no distinga azules vería una opción igual a las demás. */
.flujo-opcion-persona {
  flex: 0 0 auto;
  font-size: .6875rem;
  font-weight: 600;
  color: #1d4ed8;
}

/* El robot: dice que ahí contesta el asistente, que es lo normal. Va apagado a propósito —es el
   estado por omisión y no debe competir con lo que sí cambia—, pero presente en todas las filas:
   es lo que anuncia que la fila se puede tocar. */
.flujo-opcion-bot {
  flex: 0 0 auto;
  font-size: .75rem;
  color: var(--ins-secondary-color, #9ba6b7);
  opacity: .75;
}
.flujo-opcion:hover .flujo-opcion-bot { opacity: 1; color: #1d4ed8; }

/* La IA redactando: morado, para no confundirlo ni con el azul de «persona» ni con el gris del
   asistente. #6d28d9 da 7.1:1 sobre blanco. */
.flujo-opcion-ia {
  flex: 0 0 auto;
  font-size: .6875rem;
  font-weight: 600;
  color: #6d28d9;
}

/* La fila se tiñe apenas, para encontrarla sin leer. El borde va a la izquierda y de 2px: es el
   ancho que el lienzo ya usa para señalar, y engrosarlo movería el texto de su sitio. */
.flujo-opcion.es-persona {
  background: #eff6ff;
  border-left: 2px solid #1d4ed8;
  padding-left: .25rem;
}
.flujo-opcion.es-ia {
  background: #f5f3ff;
  border-left: 2px solid #6d28d9;
  padding-left: .25rem;
}

.flujo-nodo-pie {
  padding: .25rem .6rem .45rem;
  font-size: .6875rem;
  color: var(--ins-secondary-color, #9ba6b7);
}

/* ⚠ Acotado a las FLECHAS, y acotarlo fue un arreglo de verdad. La versión anterior decía
   `foreignObject:not(.lf-html-node)` creyendo que eximía a las cajas, y no: los nodos HTML son
   `foreignObject` SIN clase, así que la regla los alcanzaba y les quitaba el puntero. Con eso, las
   cajas no recibían nada —ni el hover que saca los anclajes— y no se podían crear conexiones
   nuevas. Medido: `pointer-events` calculado en `none` sobre los cuatro nodos del lienzo.

   El texto de una flecha sí tiene que quedar fuera del puntero.
   Con `autoWrap`, LogicFlow pinta los rótulos dentro de un `<foreignObject>` con un `<div>`
   encima del dibujo. El de una CAJA tapa su propio centro; peor: el rótulo de una FLECHA
   —«Soporte técnico»— queda flotando sobre la caja que cruza, así que quien intenta arrastrar
   esa caja agarra el texto de otra cosa y no pasa nada. Medido con `elementFromPoint` sobre el
   centro de un nodo: devolvía el `div` del rótulo de una línea.

   ⚠ Las cajas HTML quedan EXCLUIDAS: son `foreignObject` también, y sin la excepción dejarían de
   poder arrastrarse. */
.flujo-lienzo g.lf-edge foreignObject,
.flujo-lienzo g.lf-edge foreignObject * {
  pointer-events: none;
}

.flujo-lienzo {
  /* ⚠ `position: relative` para que el «+» de las flechas, que es un botón HTML posicionado
     sobre el punto medio de la línea, se ubique contra el lienzo y no contra la página. */
  position: relative;

  /* ⚠ `flex: 1 1 auto` + `min-width: 0`, o el panel lo empuja y el lienzo se queda con el ancho
     de su contenido —medido, 300 px de 1.150 disponibles—. El `min-width: 0` es el que importa:
     sin él un ítem flex se niega a encogerse por debajo de su contenido y el `1 1 auto` no
     alcanza. */
  flex: 1 1 auto;
  min-width: 0;

  /* Alto de arranque nada más: el definitivo lo calcula `ajustarAlto()` en `flujo.js`, midiendo
     dónde empieza el lienzo de verdad. Acá hace falta un alto CUALQUIERA pero distinto de cero,
     porque LogicFlow mide su contenedor al crearse y con alto automático mide 0 y no dibuja nada
     (§5 de `interfaz-ubold`: la misma trampa que Leaflet). */
  height: 420px;
  min-height: 360px;
  border: 1px solid var(--ins-border-color, #dee2e6);
  border-radius: var(--ins-border-radius, 6px);
  background-color: var(--ins-tertiary-bg, #f8f9fa);
}

/* ── El mapa de la esquina ───────────────────────────────────────────────────────────────────
   UNA definición para los DOS lienzos, igual que la barra: el del flujo —que lo crea LogicFlow—
   y el del plano del salón, que usa LAS MISMAS CLASES a propósito para heredarla sin una segunda
   hoja (Mario, 2026-09-27: «ya existe, no debes inventar»).

   ⚠ Acá van también las propiedades base que trae `extension.css`. La pantalla de mesas no carga
   esa hoja —no usa LogicFlow— así que sin repetirlas el mapa saldría sin caja.

   Viene enorme de fábrica y tapa un cuarto del lienzo: se achica y se apoya en la esquina con el
   mismo borde que el resto. */
.flujo-lienzo .lf-mini-map,
.pl-zona .lf-mini-map {
  position: absolute;
  z-index: 4;
  width: 190px !important;
  height: 130px !important;
  right: .5rem !important;
  bottom: .5rem !important;
  left: auto !important;
  top: auto !important;
  padding: 0;
  background: #fefefe;
  border: 1px solid var(--ins-border-color, #e7e9eb);
  border-radius: 6px;
  box-shadow: 0 2px 6px rgba(16, 24, 40, .08);
  overflow: hidden;
}

.flujo-lienzo .lf-mini-map-graph,
/* ⚠ Fondo gris en el del salón: varias mesas son BLANCAS —«desocupada» lo es— y sobre blanco
   desaparecerían. El del flujo no lo necesita porque sus fichas siempre traen color. */
.pl-zona .lf-mini-map-graph {
  position: relative;
  background: var(--ins-tertiary-bg, #f1f3f5);
  width: 190px !important;
  height: 130px !important;
  overflow: hidden;
}



/* El rectángulo de «acá estás mirando». Se arrastra para moverse, como en el flujo. */
.pl-zona .lf-minimap-viewport {
  position: absolute;
  background-color: rgba(217, 217, 217, .5);
  border: 1px solid rgba(90, 90, 90, .55);
  border-radius: 2px;
  cursor: grab;
}

/* El rótulo de arriba: en el flujo lo pone la librería, acá lo ponemos nosotros. */
.pl-zona .lf-mini-map-header {
  position: absolute;
  top: 0;
  right: 0;
  left: 0;
  z-index: 1;
  padding: 2px 6px;
  background: rgba(255, 255, 255, .85);
  color: var(--ins-secondary-color, #8a94a6);
  font-size: 11px;
  line-height: 16px;
  pointer-events: none;
}

/* ⚠ La miniatura es un CLON del lienzo, no un dibujo aparte: así se ve igual que el plano —con
   sus formas, sus sillas y sus colores— como pasa en el flujo, donde el mapa muestra el diagrama
   de verdad. Dibujar cuadraditos propios era inventar una segunda versión del salón que podía
   contar otra cosa (Mario, 2026-09-27). */
.pl-zona .lf-mini-copia {
  position: absolute;
  top: 0;
  left: 0;
  transform-origin: top left;
  pointer-events: none;
}

/* ⚠ En el teléfono ese mapa tapa el lienzo en vez de ayudar: 190 px sobre un lienzo de 362 es
   más de la mitad del ancho, y el diagrama se mira en un rectángulo de 362 × 420. Se esconde.
   No se achica más: por debajo de ~150 px la miniatura ya no se distingue, así que sería ocupar
   lugar sin informar. El corte es el mismo `md` que usa el resto de esta pantalla. */
@media (max-width: 767.98px) {
  .flujo-lienzo .lf-mini-map,
  .pl-zona .lf-mini-map { display: none !important; }
}

/* ── La barra de un lienzo: alejar, acercar, encuadrar ──────────────────────────────────────
   UNA definición, y una sola, porque las dos pantallas con lienzo usan LAS MISMAS CLASES: la del
   flujo del asistente las crea LogicFlow y la del plano del salón las escribe a mano. No hay
   selectores paralelos que mantener sincronizados — tenerlos ya las había separado (Mario,
   2026-09-27).

   ⚠ Acá van también las propiedades base que trae `extension.css`, porque la pantalla de mesas
   no carga esa hoja: no usa LogicFlow, solo su marcado.

   Los íconos son los de Tabler en las dos: a LogicFlow se le pasan en `iconClass`, que las
   escribe tal cual en el `<i>`. */
.lf-control {
  /* ⚠ La colocación es la del paquete, tal cual: `top: 0; right: 10px; margin: 5px`. Estaba
     puesta aparte para el salón y daba 5 px de diferencia entre las dos barras — la única que
     quedaba, además del contenido. Acá no hay nada por pantalla. */
  position: absolute;
  top: 0;
  right: 10px;
  margin: 5px;
  z-index: 4;

  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  padding: 4px 6px;

  background: #fff;
  border-radius: 10px;
  box-shadow: 0 2px 10px rgba(16, 24, 40, .12);
}

.lf-control-item {
  display: flex;
  flex: 0 0 auto;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  margin: 0 2px;
  padding: 8px 10px;

  /* Los reinicios del `<button>`: la clase no distingue el elemento, y el salón usa botones de
     verdad para que se llegue con el teclado. */
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: #4b5563;
  font-size: 12px;
  line-height: 1;
  cursor: pointer;
  transition: background .2s ease-in-out;
}

.lf-control-item i { font-size: 20px; }
.lf-control-item:hover { background: #f1f3f5; }

.lf-control-item.disabled,
.lf-control-item:disabled {
  filter: opacity(.4);
  pointer-events: none;
}

/* ⚠ El paquete le pone `margin-top: 5px` al rótulo y eso dejaba una barra 5 px más alta que la
   otra para el mismo contenido. El aire lo pone el `gap` del ítem, uno solo para las dos. */
.lf-control-text { margin-top: 0; }

/* ⚠ En el teléfono los rótulos se van y el ícono crece. Medido en el flujo: con rótulos la barra
   ocupa 256 × 63 sobre un lienzo de 362 × 420 —el 42 % de lo que se mira—; sin ellos baja al
   12 % del alto y el ícono sigue siendo tocable. El `title` queda para quien lo necesite. */
@media (max-width: 767.98px) {
  .lf-control-text { display: none; }
  .lf-control { padding: 2px 3px; }

  .lf-control-item {
    min-width: 40px;
    min-height: 40px;
    margin: 0 1px;
    padding: 8px;
  }

  .lf-control-item i { font-size: 22px; }
}



/* ── Las líneas del flujo, en movimiento ────────────────────────────────────────────────────
   ⚠ La animación va por CSS y no por la API. `edgeModel.isAnimation = true` sí cambia el color de
   la línea, pero las propiedades `animation-*` que LogicFlow guarda en ese tema no llegan a
   aplicarse y la línea queda quieta. Los `@keyframes lf_animate_dash` los trae la propia hoja de
   la librería: acá solo se usan.

   ⚠ Y el selector es `polyline`, NO `path`. La línea visible de una arista es un `<polyline>`;
   los `<path>` de adentro son el área de clic (`stroke: transparent`) y las puntas de flecha, así
   que apuntarles no anima nada — medido buscando `path` y encontrando cero. */
.flujo-lienzo g.lf-edge polyline:not([stroke="transparent"]) {
  stroke-dasharray: 8 6;
  animation: lf_animate_dash 6s linear infinite;
  transition: stroke-width .15s ease;
}

/* Al pasar por encima, la flecha se engorda y corre más rápido: así se sigue UNA línea con la
   vista cuando hay varias cruzándose. La transición es solo de `stroke-width` — animar `all`
   interpolaría también el `stroke-dashoffset`, que ya está animado, y la línea da un tirón. */
.flujo-lienzo g.lf-edge:hover polyline:not([stroke="transparent"]) {
  stroke-width: 3;
  animation-duration: 2s;
}

/* Quien pide el sistema quieto no ve nada moverse. Lo pide el sistema operativo y hay que
   respetarlo, más en una pantalla que alguien puede dejar abierta todo el día. */
@media (prefers-reduced-motion: reduce) {
  .flujo-lienzo g.lf-edge polyline:not([stroke="transparent"]),
  .flujo-lienzo g.lf-edge:hover polyline:not([stroke="transparent"]) {
    animation: none;
  }

  .flujo-nodo,
  .flujo-nodo:hover {
    transition: none;
    transform: none;
  }
}


/* ── El destino de cada opción del bot ──────────────────────────────────────────────────────
   ⚠ Primero vivió DENTRO del `input-group` del rótulo, y estuvo mal: dos campos peleando por el
   mismo ancho. Medido en el teléfono de Mario, los dos quedaban cortados —«Sistema a la»,
   «Alquilar mó»— y esa segunda columna se leía como si fuera lo que ve el cliente, cuando es
   configuración de adentro.

   Ahora va debajo, chico y apagado: el rótulo se queda con la fila entera porque es lo que se
   escribe y lo que el cliente lee. El `<select>` sí lleva tope de ancho —§2 de `interfaz-ubold`:
   no se achica con `w-auto` porque es `display:block`— para que no cruce la caja en escritorio. */
.bot-opcion-destino {
  display: flex;
  align-items: center;
  gap: .35rem;
  margin-top: .15rem;
  padding-left: .25rem;
}

.bot-opcion-destino select {
  flex: 0 0 auto;
  width: auto;
  max-width: 12rem;
}

/* ── En pantalla ancha, el rótulo y su destino en LA MISMA fila ────────────────────────────────
   Pedido de Mario (2026-09-16), con una captura del formulario en escritorio: «que sea más ancha
   o ajusta elementos para que se ajusten al ancho y no cambien de fila». Cada opción ocupaba dos
   renglones y una pregunta de tres opciones se comía la pantalla.

   ⚠ Solo de 576 px para arriba, y eso NO contradice la medición que puso el destino abajo: esa
   fue en el teléfono, donde los dos campos compartiendo fila quedaban cortados —«Sistema a la»,
   «Alquilar mó»—. Debajo de 576 px se sigue apilando, que es lo que ahí funciona. Las dos cosas
   son ciertas en su ancho. */
@media (min-width: 576px) {
  .bot-opcion {
    display: flex;
    align-items: center;
    gap: .5rem;
  }

  /* El rótulo se queda con lo que sobra: es lo que se escribe y lo que el cliente lee. */
  .bot-opcion > .input-group {
    flex: 1 1 auto;
    min-width: 0;            /* §2 de `interfaz-ubold`: sin esto un ítem flex no se encoge */
  }

  .bot-opcion-destino {
    flex: 0 0 auto;
    margin-top: 0;
    padding-left: 0;
  }
}

.bot-previa {
  background-color: var(--ins-tertiary-bg, #f1f3f5);
  border-radius: .5rem;
  padding: .5rem;
  max-width: 320px;
}

.bot-previa-burbuja {
  background-color: var(--ins-secondary-bg, #fff);
  border-radius: .5rem;
  padding: .4rem .6rem;
  font-size: .8rem;
  box-shadow: 0 1px 1px rgba(0, 0, 0, .06);
}

/* Los botones de WhatsApp van pegados debajo de la burbuja, uno por línea y centrados. */
.bot-previa-boton,
.bot-previa-lista {
  background-color: var(--ins-secondary-bg, #fff);
  border-radius: .5rem;
  margin-top: 2px;
  padding: .35rem;
  text-align: center;
  font-size: .78rem;
  color: var(--ins-info, #4dabf7);
  box-shadow: 0 1px 1px rgba(0, 0, 0, .06);
}

.bot-previa-lista i { margin-right: .25rem; }


/* ══════════════════════════════════════════════════════════════════════════════════════════
   MODAL DE CARGA DE IMÁGENES DE PRODUCTO
   Diseño aprobado por Mario el 2026-09-08 (`~/goroutecr.com/2026-09-08_diseno_modal-imagenes.html`).

   Se le quitó el `card` con sombra que envolvía el dropzone: la ventana YA es la superficie
   elevada, y encima había tres apiladas —ventana, card y formulario— para un solo contenido.

   ⚠ La decisión que ordena todo lo demás: desde que el botón abre el explorador directo, la modal
   se abre CON las imágenes ya elegidas. El área de soltar grande es el estado excepcional, no el
   normal, así que manda la grilla de miniaturas y el área se esconde en cuanto hay algo.
   ══════════════════════════════════════════════════════════════════════════════════════════ */

.mod-img .modal-header { display:block; }
.mod-img__sub { margin:2px 0 0; font-size:.8125rem; color:var(--ins-secondary-color,#6b7280); }

/* El área de soltar lleva el MISMO papel tapiz que el hueco de la galería que queda detrás: donde
   se sueltan y donde quedan se ven igual, así que la modal se lee como una extensión de la
   galería y no como otra ventana. La regla del dibujo vive arriba, junto a la del tablero. */
/* ⚠ Va prefijado con `.mod-img` a propósito: `dropzone.css` se carga DESPUÉS de esta hoja y su
   `.dropzone` tiene la misma especificidad, así que con `.mod-img__soltar` a secas el padding del
   plugin ganaba por orden y el contenido quedaba apretado contra los bordes. */
.mod-img .mod-img__soltar {
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:14px; text-align:center;
  min-height:190px; padding:32px;
  border:1.5px dashed #cdd3e1; border-radius:12px;
  background-color:var(--ins-tertiary-bg,#f6f7fb);
  background-image:url("../images/patron-tablero.svg");
  background-size:300px 300px;
  transition:border-color .15s ease;
}
.mod-img .mod-img__soltar:hover,
.mod-img .mod-img__soltar.dz-drag-hover { border-color:#a9b2c9; }

/* En escritorio el recuadro respira más: es la vista prioritaria y hay sitio de sobra. */
@media (min-width:768px){
  .mod-img .mod-img__soltar { min-height:240px; padding:40px; gap:16px; }
}

/* La línea de límites se separa del botón: es información aparte, no su pie. */
.mod-img__limite { margin-top:2px; }

.mod-img__icono {
  width:44px; height:44px; border-radius:50%;
  display:grid; place-items:center;
  background:#fff; box-shadow:0 1px 3px rgba(16,24,40,.10);
  color:var(--ins-primary,#6558dd);
}
.mod-img__invita { margin:0; font-weight:650; font-size:.9375rem; }
/* Los límites se dicen ANTES de fallar. Antes solo aparecían como reproche, cuando ya te habías
   pasado de las 7 imágenes o de los 7 MB. */
.mod-img__limite { margin:0; font-size:.75rem; color:#8a91a3; }

.mod-img__resumen {
  display:flex; align-items:center; justify-content:space-between; gap:12px; margin-bottom:14px;
}
.mod-img__cuenta { font-size:.875rem; font-weight:650; }
.mod-img__peso { font-size:.75rem; color:#8a91a3; font-weight:400; }

/* ⚠ `minmax(0,1fr)` y no `1fr` a secas: `1fr` es `minmax(auto,1fr)`, y ese `auto` deja que el
   contenido imponga el ancho mínimo de su columna. Bastaba el texto «Agregar más» —o el nombre de
   un archivo largo— para que su columna se estirara a 120 px y las vecinas se encogieran a 75:
   miniaturas convertidas en tiras verticales, con la foto recortada. Con `minmax(0,...)` el
   contenido no manda y las columnas quedan iguales. */
.mod-img__grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:12px; }
@media (min-width:600px){ .mod-img__grid { grid-template-columns:repeat(4,minmax(0,1fr)); } }

.mod-img__item {
  position:relative; border-radius:10px; overflow:hidden; aspect-ratio:1; background:#f2f4f8;
}
.mod-img__item img { width:100%; height:100%; object-fit:cover; display:block; }

.mod-img__quitar {
  position:absolute; top:6px; right:6px;
  width:26px; height:26px; padding:0; border:0; border-radius:50%;
  background:rgba(17,24,39,.62); color:#fff; font-size:14px; line-height:1;
  display:grid; place-items:center; cursor:pointer;
}
.mod-img__quitar:hover { background:rgba(17,24,39,.82); }

.mod-img__nombre {
  position:absolute; left:0; right:0; bottom:0;
  padding:14px 8px 6px; font-size:.6875rem; color:#fff;
  background:linear-gradient(to top, rgba(17,24,39,.72), transparent);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}

/* La grilla lleva la clase `dropzone` para heredar el estilo de progreso, marcas y errores del
   plugin, cuyos selectores cuelgan todos de `.dropzone .dz-preview`. Eso arrastra el marco y el
   relleno de una zona de soltar, que acá no lo es: se anulan. Son cuatro líneas, contra las
   cuarenta que costaba reescribir un indicador que ya existía. */
.mod-img__grid.dropzone {
  min-height:0; border:0; border-radius:0; padding:0; background:transparent;
}
/* Y el preview del plugin se coloca solo, en línea y con margen: acá lo posiciona la grilla. */
.mod-img__grid.dropzone .dz-preview { display:block; margin:0; min-height:0; }

/* ⚠ La barra de progreso del plugin nace VISIBLE y solo se apaga al completarse, porque su
   preview la enseña desde el principio. En una celda cuadrada eso es un óvalo negro en medio de
   cada foto, antes siquiera de pulsar Subir. Se muestra solo mientras sube: no se le cambia ni la
   forma ni el color, solo cuándo aparece. */
.mod-img__grid.dropzone .dz-preview .dz-progress { opacity:0; }
.mod-img__grid.dropzone .dz-preview.dz-processing .dz-progress { opacity:1; }

/* El mensaje de error se posiciona para el preview de 120 px del plugin (`top:130px; left:-10px`),
   así que en esta celda caía fuera y el fallo se quedaba sin motivo. Solo se reubica: el color, la
   forma y cuándo aparece siguen siendo del plugin. */
.mod-img__grid.dropzone .dz-error-message {
  top:auto; bottom:0; left:0; width:100%; border-radius:0; font-size:.6875rem;
}
.mod-img__grid.dropzone .dz-error-message:after { display:none; }

/* «Agregar más» mide lo mismo que una miniatura, en vez de ser un bloque aparte que empuja la
   grilla hacia abajo. `order` lo deja SIEMPRE al final: Dropzone inserta cada preview con
   `appendChild`, así que sin esto quedaría antes de las imágenes que se van agregando. */
.mod-img__mas {
  display:flex; flex-direction:column; align-items:center; justify-content:center; gap:6px;
  aspect-ratio:1; border-radius:10px; cursor:pointer; order:999;
  border:1.5px dashed #cdd3e1;
  background-color:var(--ins-tertiary-bg,#f6f7fb);
  background-image:url("../images/patron-tablero.svg");
  background-size:300px 300px;
  color:var(--ins-secondary-color,#6b7280); font-size:.75rem; font-weight:600;
}
.mod-img__mas:hover { border-color:#a9b2c9; }
.mod-img__mas .mod-img__mas-signo { font-size:20px; line-height:1; color:var(--ins-primary,#6558dd); }

/* El pie traía 20 px del tema contra los 24 de la cabecera y el cuerpo: los botones quedaban 4 px
   corridos respecto del recuadro de arriba, que es lo que se leía como «desalineado». */
.mod-img .modal-footer { padding: 16px 24px; }

/* La grilla vacía no ocupa: sin miniaturas no hay nada que mostrar, y su hueco dejaba el contenido
   apelotonado arriba con un vacío debajo. */
.mod-img__grid:not(:has(.mod-img__item)) { display: none; }

/* A 390 px la acción principal va sola y ancha, y las secundarias se reparten la fila. */
@media (max-width:575.98px){
  .mod-img .modal-footer { flex-wrap:wrap; }
  .mod-img .modal-footer .btn-subir { order:-1; width:100%; margin:0 0 8px; }
  .mod-img .modal-footer .btn-sec { flex:1; }
}

/* ── Buscador global de la barra superior ─────────────────────────────────────────────────────
   El panel de resultados. Va en `personalizados.css` y no en el CSS de un módulo porque la caja
   vive en el layout: se ve en todas las pantallas del sistema. */
.buscador-panel {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  width: min(460px, 90vw);
  max-height: 420px;
  overflow-y: auto;
  /* Por encima del contenido pero por debajo de las modales, que son las que sí deben tapar. */
  z-index: 1035;
}
.buscador-item {
  display: flex;
  align-items: center;
  padding: .5rem .9rem;
}
/* `min-width:0` es lo que hace funcionar al `text-truncate` de adentro: sin él, un nombre largo
   estira la fila y el panel crece hasta desbordar. */
.buscador-item .buscador-texto { min-width: 0; flex: 1; }
/* Clase propia y no `.active` de Bootstrap: `.active` pinta el fondo con el color primario y el
   texto queda blanco sobre morado, que en una lista larga cansa. */
.buscador-item.activo { background: var(--ins-tertiary-bg); }

/* El encabezado de cada bloque. Antes era `fs-xxs text-uppercase` a secas y quedaba igual de tenue
   que el detalle de los resultados: con tres bloques seguidos no se veía dónde terminaba uno y
   empezaba el siguiente. Ahora tiene peso, color de texto normal y una línea que lo separa. */
.buscador-grupo {
  font-size: .7rem;
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--ins-emphasis-color);
  background: var(--ins-tertiary-bg);
  padding: .4rem .9rem;
  margin: 0;
}
/* El primero no lleva línea arriba: ahí no separa nada, solo pega el panel. */
.buscador-panel .buscador-grupo + * { margin-top: .15rem; }
.buscador-panel .buscador-grupo:not(:first-child) { margin-top: .25rem; }

/* El título del resultado, un punto por encima del detalle: es lo que uno lee para decidir. */
.buscador-item .buscador-texto > span:first-child {
  font-weight: 500;
  color: var(--ins-emphasis-color);
}

/* ── Toasts sin desenfoque, en TODO el sistema ────────────────────────────────────────────────
   SweetAlert le pone `backdrop-filter: blur(2px)` al contenedor de cualquier aviso, también a los
   toasts, y `backdrop: false` no sirve: la propia librería avisa por consola que es incompatible
   con ellos. Resultado: un aviso de dos segundos desenfocaba la pantalla entera detrás, y el
   contenedor —que cubre todo— se comía los clics mientras duraba.

   ⚠ Esto YA estaba resuelto, pero solo en `whatsapp.css` y a través de una clase que había que
   acordarse de pasarle a cada `Swal.fire` (`customClass.container`). De los diez `avisar()` que hay
   repartidos por el sistema, cuatro la ponían y seis no: las otras seis pantallas seguían con el
   desenfoque. Acá se ataca el caso genérico y deja de depender de que cada pantalla se acuerde.

   ⚠ Con `:has()`, que es lo que permite distinguir «contenedor que lleva un toast» de un modal de
   verdad —los dos usan las mismas clases—. Se había descartado por «soporte del navegador»: eso era
   cierto cuando se escribió, hoy lo soportan todas las versiones vigentes de Chrome, Firefox y
   Safari. Las confirmaciones conservan su fondo, que ahí el bloqueo es intencional: hay que decidir
   algo antes de seguir. */
.swal2-container:has(.swal2-toast) {
  backdrop-filter: none !important;
  -webkit-backdrop-filter: none !important;
  background: transparent !important;
  pointer-events: none;
}
.swal2-container:has(.swal2-toast) .swal2-toast { pointer-events: auto; }


/* ── Modal de casillas: elegir varios de una lista larga ────────────────────────────────────
   La usa la modal «Módulos» de roles, que elige cuáles de los 30 bloques de permisos se
   muestran. Va acá y no en un CSS de roles porque no tiene nada de roles: es una lista de
   casillas con buscador, y la próxima pantalla que tenga que elegir varios de una lista larga
   la pide con estas mismas clases en vez de copiarlas. */

/* El buscador no se lleva la fila entera: al lado van los tres atajos.
   ⚠ Con `max-width` y no con `w-auto`: un `.form-control` es `display:block` y, como hijo de un
   contenedor flex, `width:auto` termina significando «llená lo que haya». */
.mm-buscar { max-width: 260px; }

/* La fila entera es zona de clic, no solo el cuadrito de 16 px. */
.mm-fila {
  border-radius: 6px;
  padding: 4px 8px 4px 28px;
  margin: 0;
}
.mm-fila:hover { background: var(--ins-tertiary-bg, #f1f3f5); }
.mm-fila .form-check-label { cursor: pointer; }

/* «2/4»: cuántos permisos tiene marcados ese módulo. En gris cuando no tiene ninguno y en
   primario cuando sí — es lo que deja ver dónde está lo del rol sin abrir un solo bloque. */
.mm-cuenta {
  font-size: .72rem;
  color: var(--bs-secondary-color, #6c757d);
  font-variant-numeric: tabular-nums;
  flex-shrink: 0;
}
.mm-cuenta.tiene { color: var(--ins-primary); font-weight: 600; }


/* ── Un `<select>` de filtro que no se coma la fila ─────────────────────────────────────────
   ⚠ La trampa más repetida de este tema: un `.form-select` es `display: block` y, como hijo de
   un contenedor flex, el navegador lo blockifica igual —`w-auto` y `d-inline-block` NO lo
   salvan—, así que `width: auto` termina significando «llená lo que haya». Medido: 500 px por
   filtro, y en una cabecera con varios se van cada uno a su propia fila.

   Un tope de ancho es lo único que lo sujeta. Ya existían dos copias con otro nombre
   (`.tk-filtro` en tickets y `.wa-filtro` en WhatsApp); esta es la versión sin dueño para que la
   próxima pantalla la pida en vez de escribir la tercera. */
.filtro-corto  { max-width: 190px; }
.filtro-cortito { max-width: 96px; }


/* ── Panel lateral que se queda pegado al hacer scroll ──────────────────────────────────────
   Lo usa el panel de filtros de la cuadrícula de productos. Pedido de Mario el 2026-09-10: la
   cuadrícula es larga y había que subir hasta arriba del todo para tocar un filtro.

   ⚠ Solo de `xl` para arriba. Por debajo el mismo elemento ES un `offcanvas` —un cajón que entra
   desde la izquierda—, y un `position: sticky` sobre un cajón fijo lo deja a medio camino.

   ⚠ Y con su propio `overflow-y`: el panel tiene cuatro bloques y en una pantalla baja es más
   alto que la ventana; sin el tope, la parte de abajo —el estado— quedaba inalcanzable, porque
   lo pegado no se puede pasar de largo. Los 118 px son 70 de la barra superior, 24 de aire
   arriba y 24 abajo. */
@media (min-width: 1200px) {
  .panel-pegado {
    position: sticky;
    top: 94px;                       /* 70 de la barra + 24 del aire */
    max-height: calc(100vh - 118px);
    overflow-y: auto;
    scrollbar-width: thin;
  }
  /* La tarjeta de adentro no pelea por el alto: manda el contenedor pegado. */
  .panel-pegado > .card { height: auto !important; }
}

/* La barra de precio, con el color de la casa.
   ⚠ Con `.noUi-target` delante a propósito. `nouislider.min.css` trae su propio
   `.noUi-connect { background:#3FB8AF }` —un verde agua— y en esta pantalla se carga DESPUÉS de
   este archivo, así que a igual especificidad ganaba él. Dos clases le ganan sin `!important` y
   sin depender del orden de las hojas, que es lo que cambiaría al mover un `<link>`. */
.noUi-target .noUi-connect { background: var(--ins-primary); }
.noUi-target .noUi-handle  { border-color: var(--ins-primary); box-shadow: none; }


/* ── Reseñas ────────────────────────────────────────────────────────────────────────────────
   Dos reglas y nada más: lo demás lo pone el tema. Van acá y no en un CSS del módulo porque son
   dos, y un archivo entero para dos reglas es una dependencia más que mantener. */

/* Las estrellas del formulario: botones, no un desplegable. En el teléfono, elegir «4» de una
   lista se lee peor que tocar la cuarta estrella. */
.rs-estrellas .rs-estrella { color: var(--ins-warning); font-size: 1.35rem; line-height: 1;
                             text-decoration: none; }
.rs-estrellas .rs-estrella:hover { transform: scale(1.1); }

/* El comentario en la tabla, a dos líneas. Uno de 5.000 caracteres estiraría la fila y dejaría
   las demás fuera de la pantalla; completo se lee en la modal. */
.rs-recorte {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* ── Línea de tiempo (Tickets y Tareas) ─────────────────────────────────────────────────────
   Una línea vertical con puntos, hecha con bordes: no hace falta plugin. Vive acá y no en el
   CSS de cada módulo porque la usan dos —`.tk-linea` es el nombre viejo, de Tickets, y se
   conserva para no tocar su markup—. */
.linea-tiempo,
.tk-linea { list-style: none; margin: 0; padding: 0 0 0 22px; position: relative; }

.linea-tiempo::before,
.tk-linea::before {
  content: '';
  position: absolute;
  left: 6px; top: 4px; bottom: 4px;
  width: 2px;
  background: var(--ins-border-color);
}

.linea-tiempo li,
.tk-linea li { position: relative; padding-bottom: 14px; }

.linea-tiempo li::before,
.tk-linea li::before {
  content: '';
  position: absolute;
  left: -21px; top: 5px;
  width: 10px; height: 10px;
  border-radius: 50%;
  background: var(--ins-secondary-bg);
  border: 2px solid var(--ins-border-color);
}

/* Lo que escribió una persona se distingue de lo que anotó el sistema: es lo que se busca al
   abrir la línea, y con el punto igual hay que leerlo todo para encontrarlo. */
.linea-tiempo li.es-nota::before,
.tk-linea li.es-nota::before { border-color: var(--ins-primary); }

/* La última entrada no necesita el tramo de línea que le sigue. */
.linea-tiempo li:last-child { padding-bottom: 0; }

/* ── Grupos de campos ───────────────────────────────────────────────────────────────────────
   La sombra que pidió Mario para los `<fieldset>` que agrupan campos (modal de tareas).

   ⚠ Va en una CLASE y no en el elemento `fieldset` a secas: el proyecto usa fieldset en dos
   lugares más —los montos por medio de pago en facturación y los datos extraídos en la modal de
   SEO— y un selector de elemento les cambiaría la pinta a los dos sin que nadie lo pidiera. Para
   ponérsela a alguno, se le agrega la clase. */
.grupo-campos { box-shadow: 0 5px 6px rgba(0, 0, 0, .06); }

/* El título del grupo, como una pastilla apoyada sobre el borde.

   ⚠ `float: none` y `width: auto` van ACÁ además de en las utilidades del markup: Bootstrap le
   pone al `legend` `float:left; width:100%`, y sin eso el título ocupa una fila entera y el
   borde del grupo le cruza por encima. Dejarlo solo en las clases del HTML significa que el
   grupo que se escriba mañana sin copiarlas sale roto y no se entiende por qué.

   ⚠ El fondo NO puede ser transparente: la pastilla se apoya sobre la línea del borde y sin
   fondo propio la línea se le ve por detrás, atravesando el texto. */
.grupo-campos > legend {
  float: none;
  width: auto;
  margin-bottom: .75rem;
  padding: .25rem .85rem;
  font-size: .8125rem;
  font-weight: 600;
  line-height: 1.4;
  color: var(--ins-secondary-color);
  background-color: var(--ins-secondary-bg);
  border: 1px solid var(--ins-border-color);
  border-radius: 50rem;
}


/* ══════════════════════════════════════════════════════════════════════════════════════════
   MENÚ COLAPSADO — la página deja de estirarse a 1900 px
   ------------------------------------------------------------------------------------------
   ⚠ Esto corrige al TEMA, no a una pantalla nuestra. Ubold trae, literal, en `app.css`:

       html[data-sidenav-size=condensed] .content-page { min-height: 1900px; }

   Un número fijo, sin relación con nada. Al colapsar el menú, CUALQUIER pantalla del sistema
   pasa a medir 1900 px y aparece la barra de scroll de la página con un vacío enorme debajo del
   contenido. Lo reportó Mario el 2026-09-12 desde la bandeja de WhatsApp, que es donde más se
   nota —es una app de alto fijo (`calc(100dvh - 70px)`), así que el chat se queda de 507 px y
   debajo quedan 1400 px en blanco—, pero pasaba en todas.

   El 1900 no es un capricho del tema: en modo colapsado el riel pasa a `position:absolute` y el
   tema le APAGA su propio scroll (`overflow:visible !important` sobre las capas de simplebar).
   Sin scroll interno, un menú más alto que la ventana solo se puede recorrer estirando la
   página. Por eso acá se arreglan las dos cosas juntas: se le devuelve el scroll al riel y
   entonces el piso de 1900 px sobra.

   ⚠ Comprobado antes de tocarlo, porque el `overflow:visible` del tema suele estar para dejar
   salir el submenú flotante al pasar el mouse —y recortar el eje vertical recorta también el
   horizontal—: en esta versión el submenú del menú colapsado **no se abre**, ni al pasar el
   mouse ni al hacer clic (`display:none; position:static`). No hay nada que deba escaparse del
   riel, así que darle scroll no rompe ninguna navegación.

   Medido a 1440×577 en la bandeja: la página pasa de 1970 px a 577 px y el scroll desaparece.
   ══════════════════════════════════════════════════════════════════════════════════════════ */
html[data-sidenav-size="condensed"] .content-page {
  /* El mismo piso que usa el tema en modo normal, en vez del 1900 fijo. */
  min-height: calc(100vh - calc(var(--ins-topbar-height) + 1px));
}

/* ⚠ `fixed`, no el `absolute` del tema — y esto es corrección de la corrección. Con solo
   `height:100vh` el riel mide una ventana pero sigue anclado al DOCUMENTO: al bajar por una
   página larga se queda arriba y debajo aparece una franja blanca del alto que se haya
   desplazado. Lo vio Mario el 2026-09-12 en `/banco`, bajando 500 px: 500 px de hueco.

   Antes no se notaba porque el `min-height:1900px` del tema hacía el riel más alto que
   cualquier pantalla, y lo que tapaba el hueco era justamente el defecto que se quitó.

   `fixed` es lo que ya hace el tema en modo normal, y no descoloca nada: `.content-page` y
   `.app-topbar` se apartan con `margin-left: var(--ins-sidenav-width-sm)`, no por el flujo. */
html[data-sidenav-size="condensed"] .sidenav-menu {
  position: fixed;
  height: 100vh;
}

/* Se le devuelve el scroll a las capas de simplebar que el tema deja en `visible`. Van con
   `!important` porque las del tema también lo llevan. */
html[data-sidenav-size="condensed"] .sidenav-menu .simplebar-mask,
html[data-sidenav-size="condensed"] .sidenav-menu .simplebar-content-wrapper {
  overflow-y: auto !important;
}

/* ── La barra del riel, invisible ────────────────────────────────────────────────────────
   Pedido de Mario (2026-09-12): el riel es de 75 px y una barra ahí se come el aire alrededor
   de los iconos.

   ⚠ Esconder una barra NO es gratis siempre, y en este mismo proyecto ya costó caro: en las
   pestañas de estado y en las categorías de respuestas del chat se había puesto
   `scrollbar-width: none` y la fila quedó **inalcanzable con el mouse** —la rueda vertical no
   desplaza en horizontal—; hubo que devolverlas. Acá es distinto y por eso se puede: el riel
   se desplaza en VERTICAL, y la rueda lo mueve con solo tener el puntero encima. Además los
   iconos quedan cortados en el borde inferior, que es la pista de que hay más abajo.

   Pero «invisible» acá NO es esconderlas las dos. La que se ve es la **nativa** del contenedor,
   que además roba ancho; esa se apaga. La de **simplebar** se deja encendida —el tema la
   apagaba en este modo—, y es la que resuelve el compromiso:

     · es `position:absolute`, o sea que se dibuja ENCIMA y no corre los iconos ni un píxel;
     · su `::before` nace en `opacity: 0` y solo sube a `.5` mientras uno desplaza
       (`.simplebar-visible`, `app.css` línea 21539).

   O sea: en reposo el riel se ve limpio, que es lo pedido, y en el momento en que lo movés
   aparece la guía. Sin ella el riel quedaría sin una sola pista de que hay más abajo ni forma
   de arrastrarlo, y eso ya se pagó caro en este proyecto con las barras horizontales.

   ⚠ No se apoya en «igual está la rueda del mouse»: eso no lo pude comprobar. El navegador de
   pruebas manda una rueda sintética que tampoco desplaza `.wa-mensajes` —un contenedor que sí
   funciona a mano—, así que la herramienta no sirve para medirlo y no se decide con una
   suposición. */
html[data-sidenav-size="condensed"] .sidenav-menu .simplebar-scrollbar {
  display: block !important;
}
html[data-sidenav-size="condensed"] .sidenav-menu .simplebar-content-wrapper {
  scrollbar-width: none;                 /* Firefox */
  -ms-overflow-style: none;              /* Edge heredado */
}
html[data-sidenav-size="condensed"] .sidenav-menu .simplebar-content-wrapper::-webkit-scrollbar {
  width: 0;
  height: 0;
}


/* ══════════════════════════════════════════════════════════════════════════════════════════
   APARIENCIA CONFIGURABLE — encabezado y pie de las modales
   ------------------------------------------------------------------------------------------
   Los colores los pone `aplicarApariencia()` (en `global-helpers.js`) como variables sobre el
   `<html>`, leyéndolos de `/auth/me`. El módulo que los configura es `/apariencia/`.

   ⚠ Las reglas cuelgan de una CLASE que el JS pone solo cuando hay un color configurado. Sin
   ella no aplica ninguna, así que una apariencia sin configurar deja el sistema exactamente
   como estaba — y es lo que permite usar `!important` sin miedo.

   ⚠ Y `!important` hace falta: 17 encabezados y 11 pies del sistema traen `bg-light`, que es
   una utilidad de Bootstrap y también lo lleva. Sin esto se quedaban con su gris de siempre
   PERO con el color de letra nuevo — blanco sobre gris claro, ilegible. Se vio en tickets y en
   facturar antes de corregirlo.

   ⚠ El selector es `.apariencia-mh .modal-header` y NO `html.apariencia-mh …`: así la misma
   regla sirve cuando la clase está en el `<html>` (el sistema entero) y cuando está en un
   recuadro suelto (la vista previa del módulo). Con el prefijo `html` habría que escribir cada
   regla dos veces, y la previa se separaría de la realidad en cuanto alguien tocara una sola.

   Van sobre `.modal-header` / `.modal-footer`, clases del TEMA, para alcanzar a las más de cien
   modales que ya existen sin tocar el markup de ninguna.
   ══════════════════════════════════════════════════════════════════════════════════════════ */
.apariencia-mh .modal-header {
  background-color: var(--app-modal-head-bg) !important;
  color: var(--app-modal-head-fg) !important;
}

/* ⚠ TODO lo que viva en el encabezado hereda el color, no solo el título. Estaba enumerando
   `.modal-title`, su ícono y `small`, y se coló el subtítulo de la modal de diseños —un
   `.text-muted`, que en Bootstrap lleva `!important`—: quedó gris ilegible sobre el color. Lo
   marcó Mario el 2026-09-12.

   Se excluyen los controles que tienen color propio a propósito: botones, badges y campos. Un
   botón dentro del encabezado tiene que seguir viéndose como un botón. */
/* ⚠ SOLO los hijos. Incluir aquí `.modal-header` hacía que el encabezado heredara el color del
   cuerpo de la modal y pisara su propio `var(--app-modal-head-fg)` —misma especificidad y esta
   regla viene después—: el color configurado dejaba de verse y todo salía gris. Me pasó al
   escribirla, el 2026-09-12. */
.apariencia-mh .modal-header *:not(.btn):not(.badge):not(.form-control):not(.form-select):not(.btn-close):not(.btn *) {
  color: inherit !important;
}

/* El subtítulo sigue leyéndose como secundario, pero del color correcto: la jerarquía la da la
   transparencia, no un gris que pelea con el fondo. */
.apariencia-mh .modal-header .text-muted,
.apariencia-mh .modal-header small {
  opacity: .8;
}

/* La ✕ es una imagen de fondo oscura: sobre un fondo oscuro desaparece. El filtro lo pone el JS
   solo cuando la letra calculada salió blanca, o sea cuando el fondo es oscuro. */
.apariencia-mh .modal-header .btn-close {
  filter: var(--app-modal-head-close, none);
  opacity: .9;
}
.apariencia-mh .modal-header .btn-close:hover { opacity: 1; }

.apariencia-mf .modal-footer {
  background-color: var(--app-modal-foot-bg) !important;
  color: var(--app-modal-foot-fg) !important;
}

/* ── Selector de color dentro de un grupo de campos ─────────────────────────────────────────
   Bootstrap le fija a `.form-control-color` un alto propio —38 px— y al lado de un
   `.form-control` de este tema, que mide 43, queda flotando: se ve como si el grupo estuviera
   torcido. Lo notó Mario el 2026-09-12 en el módulo de Apariencia.

   ⚠ Acotado a `.input-group >` a propósito: los selectores de color SUELTOS de otras pantallas
   —configuración de empresa, calendario, catálogos de tareas, tickets— no están en un grupo y
   ahí el alto de Bootstrap está bien. Una regla sobre `.form-control-color` a secas les
   cambiaría la pinta a todas sin que nadie lo pidiera. */
.input-group > .form-control-color {
  height: auto;
  align-self: stretch;
}

/* ── Fila de filtros por columna ────────────────────────────────────────────────────────────
   La segunda fila de la cabecera de una tabla, con un control por columna (la lista de facturas
   la usa; cualquier otra la hereda poniéndole la clase a su `<tr>`).

   ⚠ Cuelga de `.table-container`, que es donde vive el `position: sticky` de la cabecera: la
   fila de filtros también tiene que quedar pegada, pero NO en `top: 0` —ahí está la de títulos—.
   El desplazamiento lo pone el JS midiendo el alto real de la primera fila, porque depende del
   relleno que el tema tenga puesto.

   El fondo va declarado igual que en los títulos: sin él, las filas de datos se ven pasar por
   debajo de los controles al desplazarse. */
.table-container thead tr.fila-filtros th {
  background: #fff !important;
  border-top: 0;
  z-index: 2;
  font-weight: 400;
  text-transform: none;
}

/* Los controles no llevan el ancho mínimo de un formulario: acá mandan las columnas. */
.table-container thead tr.fila-filtros .form-control,
.table-container thead tr.fila-filtros .form-select {
  min-width: 0;
  padding: 4px 8px;
  font-size: .75rem;
  background: #fff;
}

/* El aspa del `type="search"` en Chrome ocupa lugar y se pisa con el texto en una celda angosta. */
.table-container thead tr.fila-filtros input[type="search"]::-webkit-search-cancel-button {
  -webkit-appearance: none;
  appearance: none;
}

/* ── Marca de quién responde en el lienzo del flujo ────────────────────────────────────────
   Chica y redonda: no es un paso de la conversación —el cliente nunca la ve— sino la anotación
   de una rama que ya existe. El color y el fondo los pone el JS, que es quien sabe si esa rama
   la contesta la IA o el asistente. */
.flujo-marca {
  width: 100%;
  height: 100%;
  border: 2px solid currentColor;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.25rem;
  box-shadow: 0 1px 3px rgba(0, 0, 0, .12);
}

/* El ícono de la cabecera de una ficha es también su control: dice el papel y se toca para
   cambiarlo. El puntero lo anuncia; sin eso parece decoración. */
.flujo-papel { cursor: pointer; }
.flujo-papel:hover { opacity: .7; }

/* ── Ventana de prueba del asistente ───────────────────────────────────────────────────────
   ⚠ Las burbujas NO se redefinen: se usan `.wa-fila` y `.wa-burbuja` de `whatsapp.css`, las mismas
   que pinta la bandeja. Tener una copia «parecida» aquí garantiza que el día que se ajuste el
   tamaño de una burbuja, la prueba deje de parecerse a lo que el cliente ve — y entonces probar
   deja de servir. Lo único propio es lo que la bandeja no tiene: el marco del hilo, los botones
   que se pueden tocar y la nota de lo que pasó por dentro. */
.pb-hilo {
  background-color: var(--wa-fondo, #efeae2);
  background-image: url("../images/whatsapp-fondo.svg");
  background-size: 300px 300px;
  padding: 18px 14px;
  min-height: 22rem;
}

/* ⚠ La fuente vuelve a ser la del tema. `whatsapp.css` le pone a `.wa-burbuja` la pila
   `'Noto Color Emoji', …` a propósito: esa fuente solo declara RANGOS de emoji —los define
   `emojis-locales.css`— así que las letras caen en la siguiente y los emojis se ven iguales en
   todos los equipos.

   Pero eso solo se cumple si `emojis-locales.css` está cargado, y son **2 MB en diez trozos** que
   una ventana modal no tiene por qué arrastrar (lo dice el propio comentario de esa regla: «se
   aplica donde de verdad se ven emojis y no al body entero»). Sin esa hoja, el navegador usa la
   `Noto Color Emoji` DEL SISTEMA —que sí cubre las letras— y el texto sale con las palabras
   separadas, como si estuviera justificado. Medido acá el 2026-09-16.

   ⚠ Y el fallo es INTERMITENTE por equipo: aparece solo donde el sistema tiene esa fuente
   instalada. En Windows no se vería y en Linux sí, lo que lo vuelve imposible de reproducir para
   quien lo reporte. */
.pb-hilo .wa-burbuja { font-family: var(--ins-body-font-family); }

/* Los botones que el cliente vería. Se pueden tocar: recorrer el flujo es el punto de la ventana. */
.pb-op {
  display: block;
  width: 100%;
  margin-top: 4px;
  border: 0;
  border-top: 1px solid rgba(0, 0, 0, .08);
  background: transparent;
  padding: 7px 4px 2px;
  font-size: 14px;
  color: #027eb5;
  text-align: center;
}
.pb-op:hover:not(:disabled) { color: #015d87; }
.pb-op:disabled { color: var(--wa-sec, #667781); opacity: .55; cursor: default; }

/* En el teléfono, el mismo aire que usa la bandeja real (`.wa-mensajes` a 14px/12px): con el
   padding de escritorio la burbuja se queda sin ancho y el texto se parte cada dos palabras. */
@media (max-width: 767.98px) {
  .pb-hilo { padding: 14px 12px; min-height: 16rem; }
  /* ⚠ El 65% de `.wa-burbuja` está pensado para el hilo a pantalla completa. Dentro de una modal
     angosta deja la burbuja demasiado flaca, así que acá se le permite casi todo el ancho — que es
     como se ve WhatsApp en un teléfono. */
  .pb-hilo .wa-burbuja { max-width: 88%; }
}

/* Lo que pasó por dentro. Fuera de la burbuja y en chico: es información para quien configura,
   no parte de la conversación — mezclarla adentro haría dudar de si el cliente la ve. */
.pb-motivo {
  font-size: 11px;
  color: var(--wa-sec, #667781);
  background: rgba(255, 255, 255, .75);
  border-radius: 6px;
  padding: 1px 7px;
  margin: -2px auto 10px;
  width: fit-content;
}

/* La consulta al sistema, en la fila de una opción. Verde como los departamentos: las dos cosas
   salen de datos de la empresa, no de un texto que alguien escribió. */
.flujo-opcion-consulta {
  flex: 0 0 auto;
  font-size: .6875rem;
  font-weight: 600;
  color: #0f766e;
}
.flujo-opcion-consulta .ti { font-size: .75rem; }

/* ⚠ El cuerpo del formulario de una pregunta hace scroll, no crece. Con cinco opciones y la vista
   previa, el diálogo superaba el alto de un portátil y los botones de Guardar y Cancelar quedaban
   fuera de la pantalla — la misma familia que la modal sin botones de §3 de `interfaz-ubold`.
   `65vh` deja lugar para el título y el pie del propio diálogo. */
.swal2-html-container.bot-form-scroll {
  max-height: 65vh;
  overflow-y: auto;
  overflow-x: hidden;
}

/* ── El índice del flujo, en el panel derecho (2026-09-17) ─────────────────────────────────────
   Antes era una columna fija de 15rem dentro del lienzo, siempre abierta. Lo que se hace con él
   —buscar una ficha, arrastrar una de vuelta— es ocasional, así que se llevaba un sexto del ancho
   todo el tiempo. Pedido de Mario.

   ⚠ `z-index` por debajo del fantasma que dibuja LogicFlow al arrastrar. Si el panel quedara
   encima, la ficha arrastrada se vería cortada al salir de él y el gesto parecería roto. */
/* ⚠ SIN `width` propio: se usa el del tema (`--ins-offcanvas-width`, 400 px), como pidió Mario.
   Un ancho a mano acá sería una medida más que mantener y que se separaría del resto de los
   paneles del sistema al primer ajuste del tema.
   El `max-width` sí se queda: a 390 px un panel de 400 desborda la pantalla. */
.offcanvas.flujo-panel-oc {
  max-width: 100%;
}

/* El buscador va FUERA del cuerpo que hace scroll: se queda fijo arriba mientras la lista corre,
   que es lo que uno espera de un buscador —y lo que pidió Mario, «que igual tenga el buscador en
   la parte superior»—. */
.flujo-panel-oc .flujo-panel-buscar {
  flex: 0 0 auto;
  border-bottom: 1px solid var(--ins-border-color, #e7e9eb);
}

.flujo-panel-oc .offcanvas-body {
  display: flex;
  flex-direction: column;
  min-height: 0;          /* sin esto la lista no se encoge y el scroll no aparece nunca */
}

.flujo-panel-oc .flujo-panel-lista {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}

/* ⚠ Y el lienzo recupera el ancho: `.flujo-area` ya no reparte con nadie. */
.flujo-area > .flujo-lienzo:only-child {
  flex: 1 1 100%;
}

/* ── Badge de proforma ───────────────────────────────────────────────────────────────────────
   Una proforma NO es un comprobante electrónico, y este badge es lo único que lo dice de reojo
   en una pantalla que por lo demás se ve igual que la de una factura.

   ⚠ Va acá y no en el CSS de facturación porque lo necesitan al menos tres pantallas —el editor,
   el listado de facturas y el PDF— y una copia por pantalla es exactamente cómo se separaron los
   colores del tablero de WhatsApp de los del de tareas.

   ⚠ Por qué no es una clase del tema: TODOS los `bg-*` sólidos ya significan un estado fiscal
   (primary=Generada, dark=en trámite, success=Aceptado, danger=Rechazado, info=Borrador,
   warning=Anulada). El único libre era el morado suave, y de su familia el tema solo compiló
   `.bg-purple-subtle`: `text-purple-emphasis` y `border-purple-subtle` NO existen en el
   `app.min.css` desplegado, aunque sus tokens sí. De ahí que se arme con los tokens. */
/* ⚠ El listado de facturas pinta la PLANTILLA recurrente con `bg-purple`, y esa clase NO EXISTE
   en ninguna hoja del sitio —el tema solo compiló `.bg-purple-subtle`—: el badge venía saliendo
   sin fondo desde que se escribió, sin un solo error en consola. Se le da el morado sólido que su
   propio comentario dice que quería, con el token del tema. */
.badge.bg-purple {
  background-color: var(--ins-purple) !important;
  color: #fff;
}

.badge.badge-proforma {
  background-color: var(--ins-purple-bg-subtle);
  color: var(--ins-purple-text-emphasis);
  border: 1px solid var(--ins-purple-border-subtle);
}

/* ── Paginación de DataTables que no desborda la página ──────────────────────────────────────
   ⚠ MEDIDO el 2026-09-18 a 390 px con 22.211 productos (889 páginas): la barra de paginación
   medía **447 px dentro de un contenedor de 292**, y como no se encoge ni se envuelve, empujaba
   el ancho del documento a 496 px. El síntoma no parece de la tabla: la tarjeta entera queda
   corrida y el usuario ve la página «deformada».

   Son DOS reglas y hacen falta las dos:
   · `flex-wrap` en el `<ul>` deja que los botones bajen de línea en vez de crecer a lo ancho.
   · `min-width: 0` en el contenedor: sin él, un hijo flex se niega a encogerse por debajo del
     ancho de su contenido, y el `flex-wrap` del `<ul>` no llega a aplicarse nunca.

   Cuelga de `.dt-container`, que es la clase del propio DataTables, así que vale para TODAS las
   tablas del sistema y no solo para la de productos. */
.dt-paging {
  min-width: 0;
  max-width: 100%;
}

/* ⚠ La fila que contiene info+paginación se MUEVE fuera del contenedor de DataTables
   (`moverPieAlaTarjeta()` la saca de `#tabla_wrapper` y la mete en el `card-footer`), así que
   `.dt-container` deja de ser ancestro. Colgar de él parecía lo correcto y no aplicaba nunca:
   comprobado midiendo `getComputedStyle`, que seguía dando `flex-wrap: nowrap`.
   Por eso las reglas cuelgan de `.dt-paging` a secas, que viaja con el elemento. */
.dt-paging .pagination {
  flex-wrap: wrap;
  justify-content: flex-end;
  row-gap: .35rem;
  margin-bottom: 0;
}

/* En el teléfono los botones se aprietan un poco: con 889 páginas son once, y cada píxel de
   relleno se multiplica por once. */
@media (max-width: 575.98px) {
  .dt-paging .page-link {
    padding: .25rem .5rem;
    font-size: .8125rem;
  }
}


/* ── El indicador de carga de DataTables, fijo en la pantalla ───────────────────────────────── */
/*
 * El tema lo trae `position: absolute` y centrado sobre LA TABLA, no sobre la ventana: con un
 * listado largo, al bajar el scroll el indicador se queda arriba y desaparece de la vista. Se ve
 * como que el sistema no responde al paginar.
 *
 * ⚠ Va con `transform` y `margin: 0`: el tema centra con `margin-left:-50px; margin-top:-22px`,
 *   calculados para su ancho fijo de 100 px. Dejarlos puestos lo corre 50 px a la izquierda.
 *
 * ⚠ Y necesita FONDO PROPIO. Sobre la tabla heredaba el blanco del `card`; flotando sobre el
 *   contenido, sin fondo, el texto se mezcla con las filas de abajo. El borde solo no alcanza.
 *
 * Aplica a TODAS las tablas del sistema, que tienen el mismo comportamiento.
 */
.dt-processing {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  margin: 0;
  width: auto;
  min-width: 160px;
  padding: 14px 20px;
  background: var(--ins-secondary-bg, #fff);
  color: var(--ins-body-color);
  border: 1px solid var(--ins-border-color);
  border-radius: 8px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, .18);
  /* Por encima de la barra superior y del menú lateral, por debajo del velo global (9999). */
  z-index: 1080;
}



/* ───────── La franja de «estás en otra empresa» y el layout del tema ─────────
   La franja la pone `superadmin-contexto.js` como primer hijo del `<body>`, o sea DENTRO del
   flujo. Eso corre hacia abajo lo que también está en el flujo —el contenido y la barra
   superior, que es `sticky`— pero no al menú lateral, que el tema clava con `position: fixed;
   top: 0`. Medido con la franja puesta: barra superior en 56 px y menú en 0, así que el logo
   quedaba 56 px MÁS ARRIBA que la barra, que es como se ve «desalineado» al entrar a una
   empresa ajena (Mario, 2026-09-25).

   Y al bajar por la página aparecía el segundo: franja y barra superior se pegan las dos a
   `top: 0`, así que la franja —que gana por z-index— tapaba la barra entera.

   Las dos se arreglan con el mismo offset. `--aviso-empresa-alto` la escribe el JS MIDIENDO la
   franja, porque su alto depende de cuánto envuelva el texto y de si está el botón de «ver como
   el cliente»: en el teléfono no son 56 px.

   ⚠ Al menú hay que bajarlo y ENCOGERLO a la vez: es `height: 100%` contra la ventana, así que
   con solo bajarlo se pasa de largo por abajo y su scroll interno queda con un trozo fuera. */
body.con-aviso-empresa .sidenav-menu {
    top: var(--aviso-empresa-alto, 0px);
    height: calc(100% - var(--aviso-empresa-alto, 0px));
}

/* Que se pegue DEBAJO de la franja, no encima de ella. */
body.con-aviso-empresa .app-topbar {
    top: var(--aviso-empresa-alto, 0px);
}

/* ⚠ Y a lo ANCHO la franja tampoco se estira sola. `body` es una GRILLA con `place-items: center`
   —lo dejó pegado el bloque «BOTÓN IA» de este mismo archivo, que es un demo con reglas globales
   de `body` y de `*` dentro—, y la franja es hijo directo del body: como no se le dice lo
   contrario, se queda en su ancho de CONTENIDO (1.172 px medidos) y se centra. En una pantalla de
   1.920 eso la deja empezando en 374 px, con el menú y el borde izquierdo al descubierto y el
   contenido pasando por detrás al bajar (Mario, 2026-09-25).

   No se arregla quitando la grilla del `body`: hay pantallas que ya cuentan con ella —la tabla de
   facturas se desplaza dentro de su caja gracias a eso—. Se arregla diciendo que esta franja
   ocupa la fila entera, que es lo que un aviso de «estás en otra empresa» tiene que hacer. */
#avisoEmpresaAjena {
    grid-column: 1 / -1;
    justify-self: stretch;
}


/* ── Botón morado ─────────────────────────────────────────────────────────────────────────────
   ⚠ `btn-purple` NO EXISTE en el tema: del morado solo se compiló `.bg-purple-subtle` (medido en
   el `app.min.css` desplegado). Un botón con esa clase sale gris y sin un solo error, que es la
   trampa de siempre —el TOKEN existe, la CLASE no—. Igual que ya pasó con `.badge.bg-purple`.

   El color sale de `--ins-purple`, así que sigue al selector de tema de la barra superior en vez
   de quedar clavado en un hexadecimal.

   ⚠ El estado activo se oscurece con `color-mix` contra negro y no con un segundo hexadecimal:
   con dos colores escritos a mano, el día que el tema cambie el morado el botón queda de un color
   y su pulsación de otro. */
.btn-purple {
    --ins-btn-color: #fff;
    --ins-btn-bg: var(--ins-purple, #7e5fc7);
    --ins-btn-border-color: var(--ins-purple, #7e5fc7);
    --ins-btn-hover-color: #fff;
    --ins-btn-hover-bg: color-mix(in srgb, var(--ins-purple, #7e5fc7) 85%, #000);
    --ins-btn-hover-border-color: color-mix(in srgb, var(--ins-purple, #7e5fc7) 85%, #000);
    --ins-btn-active-color: #fff;
    --ins-btn-active-bg: color-mix(in srgb, var(--ins-purple, #7e5fc7) 75%, #000);
    --ins-btn-active-border-color: color-mix(in srgb, var(--ins-purple, #7e5fc7) 75%, #000);
    --ins-btn-focus-shadow-rgb: 126, 95, 199;

    /* ⚠ Respaldo directo por si esta versión del tema no lee las variables `--ins-btn-*` en un
       botón sin clase de color: sin esto el botón saldría transparente, que es peor que gris. */
    background-color: var(--ins-purple, #7e5fc7);
    border-color: var(--ins-purple, #7e5fc7);
    color: #fff;
}

.btn-purple:hover,
.btn-purple:focus-visible,
.btn-purple.show {
    background-color: color-mix(in srgb, var(--ins-purple, #7e5fc7) 85%, #000);
    border-color: color-mix(in srgb, var(--ins-purple, #7e5fc7) 85%, #000);
    color: #fff;
}


/* ═══════════════════════════════════════════════════════════════════════════════════════════
   Imagen de las pantallas de acceso
   ═══════════════════════════════════════════════════════════════════════════════════════════
   La usan CUATRO pantallas —login, recuperar, restablecer y perfil de usuario— a través de
   `.card-side-img`, que el tema define en `app.min.css` como `url(../images/auth.jpg)`.

   ⚠ Y ahí está la trampa: esa URL **no lleva `?v=`**. Reemplazar el archivo en el servidor no le
     cambia nada a quien ya entró alguna vez: su navegador sigue sirviendo la copia guardada, y
     puede tardar días. Es el mismo fallo del `?v=` que el proyecto ya conoce en los `.js` y
     `.css`, pero escondido dentro de una regla del tema, donde nadie lo va a buscar.

     Se resuelve acá, redeclarando la regla con la versión pegada. `personalizados.css` se carga
     DESPUÉS de `app.min.css` y la especificidad es la misma, así que esta gana por orden. Al
     cambiar la imagen otra vez, subir el número.

   ⚠ La foto es panorámica (1672×941, 1,78:1) y la columna es casi cuadrada (~1,18:1), así que
     `cover` recorta a lo ANCHO y no a lo alto. Centrada deja a las dos personas y la pantalla
     dentro del encuadre, que es lo que tiene que contar la imagen. */
.card-side-img {
    background-image: url(../images/auth.jpg?v=2);
}
