/* ============================================================
   Nexel Cloud — Sistema de diseño
   Implementación de DESIGN.md. Todos los estilos del proyecto
   viven en este único archivo.
   ============================================================ */

/* ---------- 1. Tokens ---------- */
:root {
  --bg:        #f4f6fb;
  --canvas:    #888a8e;
  --surface:   #ffffff;
  --border:    #e2e8f0;
  --primary:   #475569;
  --primary-h: #3a4759;
  --danger:    #ef4444;
  --success:   #22c55e;
  --warn:      #f59e0b;
  --info:      #3b82f6;
  --purple:    #8b5cf6;
  --text:      #1e293b;
  --muted:     #64748b;
  --radius:    10px;
  --shadow:    0 1px 4px rgba(0,0,0,.08);
  --shadow-lg: 0 8px 32px rgba(0,0,0,.12);
  --row-hover: #f1f5f9;
}

[data-theme="dark"] {
  --bg:        #32373D;
  --canvas:    #2b2c2f;
  --surface:   #3d4248;
  --border:    #555b63;
  --primary:   #475569;
  --primary-h: #3a4759;
  --danger:    #ef4444;
  --success:   #22c55e;
  --warn:      #f59e0b;
  --text:      #f0f0f0;
  --muted:     #aaaaaa;
  --shadow:    0 1px 4px rgba(0,0,0,.4);
  --shadow-lg: 0 8px 32px rgba(0,0,0,.6);
  --row-hover: #4a5058;
}

/* ---------- 1.5. Banda de marca y chrome ----------
   Mismo modelo que `panel/` (ver su DESIGN.md §1.1): el chrome tiene DOS
   planos y no hay que confundirlos.

     · BANDA DE MARCA — bloque del logo (§4) + topbar (§5). Una sola
       franja de 60px a lo ancho de la pantalla, pintada con el color de
       marca e igual en los dos temas.
     · CUERPO DEL SIDEBAR — la nav, de la banda para abajo. Superficie
       NEUTRA que sigue al tema, con los tokens `--chrome-*`.

   La diferencia con el panel es CUÁL es ese color de marca. Allá la
   banda se pinta con `--primary` porque marca y acción primaria son el
   mismo azul; acá no pueden serlo: la marca es el rojo `#D90000` y un
   `.btn-primary` rojo quedaría a un tono de `.btn-danger`, con "Guardar"
   leyéndose igual que "Eliminar". Por eso la banda tiene tokens propios
   `--brand-*` y `--primary` se queda en grafito.

   Sobre la banda los hijos van en BLANCO y en opacidades de blanco,
   nunca en `--chrome-text` / `--chrome-muted` / `--border`: esos tokens
   están calibrados contra el gris del chrome y sobre el rojo se ensucian.
   Es la misma regla que rige la barra de título de los modales (§14).

   Fuera de la banda no se pinta ninguna superficie entera con la marca:
   en el cuerpo del sidebar el rojo entra sólo como ACENTO — el lavado
   del ítem activo, su barra de 3px y su ícono.

   Nota: el banner `.version-banner` NO se toca — es una notificación
   de la plataforma nexel y va siempre en `#15803d` (ver sección 17). */
:root,
[data-theme="dark"] {
  --primary:   #475569;
  --primary-h: color-mix(in srgb, #475569 88%, #000);

  /* Marca. Igual en los dos temas: la banda no cambia con el tema. */
  --brand:        #D90000;
  --brand-edge:   color-mix(in srgb, var(--brand) 82%, #000);
  /* Lavado de marca: "esto está marcado" sin pintar la superficie. Lo
     usa el ítem activo del sidebar. Va contra `transparent` (o sea:
     alpha) y no contra un token de fondo, así compone bien sobre el
     chrome de los dos temas sin necesitar una variante por tema. */
  --brand-wash:   color-mix(in srgb, var(--brand) 20%, transparent);
  --brand-wash-h: color-mix(in srgb, var(--brand) 30%, transparent);
}

/* Las dos superficies de prueba de los logotipos de una cuenta (§8.2).
   NO cambian con el tema, y se declaran una sola vez por eso mismo: no
   son superficies de cloud sino REPRODUCCIONES de aquéllas a las que
   cada archivo está destinado —una oscura, y el papel de la ficha
   imprimible—. Un recuadro que se repinta según cómo tenga el tema el
   controlador deja de decir si el archivo se va a ver donde se usa. Es
   el mismo criterio que los `--hoja-*` del panel (su DESIGN.md §10.4). */
:root {
  --logo-oscuro-bg:    #16181c;
  --logo-oscuro-borde: #2e3238;
  --logo-claro-bg:     #ffffff;
  --logo-claro-borde:  #d8dde3;
}

/* Cuerpo del sidebar — superficie neutra, un plano entre el canvas y las
   tarjetas. Los valores son los del panel. */
:root {
  --chrome-bg:       #ffffff;
  --chrome-text:     #1e293b;
  --chrome-muted:    #64748b;
  --chrome-border:   #e2e8f0;
  --chrome-hover-bg: color-mix(in srgb, var(--brand) 10%, transparent);
  --chrome-sub-bg:   transparent;
}

[data-theme="dark"] {
  --bg:        #1a1a1a;
  --canvas:    #1a1a1a;
  --surface:   #242526;
  --border:    #383838;
  --row-hover: #2d2e2f;
  --text:      #f0f0f0;
  --muted:     #9ca0a4;

  --chrome-bg:       #202225;
  --chrome-text:     #f0f0f0;
  --chrome-muted:    #9ca0a4;
  --chrome-border:   #2e3033;
  --chrome-hover-bg: color-mix(in srgb, var(--brand) 22%, transparent);
  --chrome-sub-bg:   transparent;
}

/* Las reglas de pintado del chrome (sidebar/topbar) viven al final del
   archivo para ganarle por orden de fuente a las secciones 3-5. */

/* ---------- 2. Reset & base ---------- */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  background: var(--bg); color: var(--text); min-height: 100vh;
}

/* ---------- 3. Layout principal ---------- */
.layout  { display: flex; min-height: 100vh; }
.sidebar { width: 220px; background: var(--chrome-bg);
           border-right: 1px solid var(--chrome-border);
           display: flex; flex-direction: column; flex-shrink: 0; }
.main    { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
/* Topbar y bloque del logo son una sola banda de marca (§1.5): mismo
   fondo y mismo alto, para que el escalón entre sidebar y contenido no
   se note. */
.topbar  { background: var(--brand); border-bottom: 1px solid var(--brand-edge);
           padding: 0 24px; height: 60px;
           display: flex; align-items: center; justify-content: space-between;
           box-shadow: var(--shadow); }
.content { flex: 1; padding: 24px; overflow-y: auto; background: var(--canvas); }

/* ---------- 4. Sidebar ---------- */
/* El logotipo va CENTRADO en el ancho del sidebar, como en la banda del
   panel: la banda es un bloque de marca y no un ítem más de la nav, así
   que no se alinea al margen de 20px de los ítems de abajo.
   El padding asimétrico (12/9) es el del panel y no un capricho: los dos
   proyectos comparten el mismo `logo-inner.png` y el operador salta de
   una solapa a la otra, así que la caja tiene que dar el MISMO alto útil
   en los dos — si acá va 0, el logo se centra 1,5px más arriba. */
.sidebar-logo  { padding: 12px 20px 9px; height: 60px; flex-shrink: 0;
                 background: var(--brand); border-bottom: 1px solid var(--brand-edge);
                 display: flex; align-items: center; justify-content: center;
                 gap: 8px; }
/* Idéntica a la de panel.css, y tiene que seguir siéndolo: el tope es el
   ALTO (28px) y no un porcentaje del ancho del sidebar. Con `max-width`
   en % el tamaño final depende del ancho de la barra de cada proyecto y
   el logo cambia de cuerpo al cambiar de solapa — que es exactamente lo
   que pasaba acá (113x30 contra los 107x28 del panel). */
.sidebar-logo img { max-width: 100%; max-height: 28px;
                    width: auto; height: auto; object-fit: contain;
                    display: block; }
.sidebar-nav   { padding: 8px 0 12px; flex: 1; }
.sidebar-footer{ padding: 10px 20px; font-size: .75rem; color: var(--chrome-muted);
                 border-top: 1px solid var(--chrome-border); text-align: center;
                 letter-spacing: .03em; font-family: monospace; }

.nav-item { display: flex; align-items: center; gap: 10px;
            padding: 10px 20px; font-size: .9rem; color: var(--chrome-muted);
            cursor: pointer; border-left: 3px solid transparent;
            transition: background .15s, color .15s; text-decoration: none; }
.nav-item:hover  { background: var(--chrome-hover-bg); color: var(--chrome-text); }
/* El ítem activo se marca con un LAVADO de la marca, no con un relleno:
   al ser un tinte, el rótulo conserva el color de texto del chrome y se
   lee igual en los dos temas. La señal la refuerzan la barra de 3px y el
   ícono, que sí van en el rojo pleno. */
.nav-item.active { background: var(--brand-wash); color: var(--chrome-text);
                   border-left-color: var(--brand); font-weight: 600; }
.nav-item.active:hover { background: var(--brand-wash-h); }
.nav-icon { font-size: 1.1rem; width: 20px; text-align: center; flex-shrink: 0; }
/* Los glifos de FA rinden más grandes que un emoji al mismo tamaño. */
.nav-icon i { font-size: 1rem; }
.nav-item.active .nav-icon i { color: var(--brand); }

/* Grupos colapsables. La categoría es un ENCABEZADO DE SECCIÓN, no un
   link más: va en versalitas chicas para que el nivel navegable sean los
   sub-ítems y la categoría se lea como su rótulo. */
.nav-group-wrap .nav-sub      { display: none; background: var(--chrome-sub-bg); }
.nav-group-wrap.open .nav-sub { display: block; }
.nav-group-toggle             { justify-content: space-between;
                                font-size: .72rem; font-weight: 700;
                                text-transform: uppercase; letter-spacing: .08em;
                                padding-top: 14px; padding-bottom: 8px; }
.nav-group-toggle:hover       { background: none; color: var(--chrome-text); }
.nav-group-arrow              { font-size: .95rem; font-weight: 700;
                                margin-left: auto; transition: transform .2s;
                                color: var(--chrome-muted); line-height: 1; }
.nav-group-wrap.open .nav-group-arrow { transform: rotate(45deg); }
.nav-sub-item                 { padding-left: 34px; background: var(--chrome-sub-bg);
                                font-size: .88rem; }

/* ---------- 5. Topbar ---------- */
/* La topbar está SOBRE la banda de marca, así que todos sus hijos van en
   blanco y en opacidades de blanco (§1.5) — el `.user-dropdown` no, que
   cuelga por debajo de la banda y es una superficie normal. */
.topbar-title    { font-size: 1rem; font-weight: 600; flex: 1; color: #fff; }
.topbar-user     { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
/* Píldora con avatar: es el único control anclado a la topbar, así que se
   lee como pastilla y no como texto suelto. Acá lleva SÓLO el nombre de
   la persona — a diferencia del panel, que además muestra la cuenta:
   cloud no es multi-cuenta y quien entra es un controlador de la
   plataforma, no el operador de un inquilino. */
.topbar-username { background: rgba(255,255,255,.14);
                   border: 1px solid rgba(255,255,255,.28); cursor: pointer;
                   font-size: .85rem; color: #fff; font-family: inherit;
                   display: flex; align-items: center; gap: 8px;
                   padding: 4px 12px 4px 6px; border-radius: 99px;
                   transition: background .15s, border-color .15s; }
.topbar-username:hover { background: rgba(255,255,255,.24);
                         border-color: rgba(255,255,255,.45); }
.topbar-username .fa-user-circle { font-size: 1.5rem; line-height: 1; color: #fff; }
.topbar-username .fa-caret-down  { color: rgba(255,255,255,.75); font-size: .8rem; }
.user-dropdown   { display: none; position: absolute; right: 0; top: calc(100% + 6px);
                   background: var(--surface); border: 1px solid var(--border);
                   border-radius: 10px; box-shadow: var(--shadow-lg);
                   min-width: 160px; overflow: hidden; z-index: 200; }
.user-dropdown.open { display: block; }

/* ---------- 6. Botones ---------- */
.btn { padding: 8px 16px; border-radius: var(--radius); border: none;
       font-size: .88rem; font-weight: 600; cursor: pointer;
       display: inline-flex; align-items: center; gap: 6px;
       text-decoration: none;
       transition: background .15s, transform .1s; }
.btn:active           { transform: scale(.97); }
.btn:disabled         { opacity: .65; cursor: not-allowed; }
.btn-primary          { background: var(--primary); color: #fff; }
.btn-primary:hover    { background: var(--primary-h); }
.btn-danger           { background: var(--danger); color: #fff; }
.btn-danger:hover     { background: #dc2626; }
.btn-secondary        { background: var(--surface); color: var(--text);
                        border: 1px solid var(--border); }
.btn-secondary:hover  { background: var(--bg); }
.btn-ghost            { background: transparent; color: var(--muted);
                        border: 1px solid var(--border); }
.btn-ghost:hover      { background: var(--bg); color: var(--text); }
.btn-sm               { padding: 5px 12px; font-size: .8rem; }
/* El `color` NO es opcional: un <button> no hereda el color del padre —el
 * UA le aplica `color: buttontext`, que es casi negro— así que sin esto el
 * ícono se pinta oscuro sobre oscuro y desaparece. Va en `--muted` con
 * hover a `--text`, igual que `.btn-ghost`.
 * Los overrides que ya existen le ganan por especificidad: la barra de
 * título de los modales lo lleva a `#fff` (§14) y la toolbar del editor de
 * línea a `inherit`. */
.btn-icon-sm          { background: none; border: none; cursor: pointer;
                        padding: 4px 8px; border-radius: 6px; font-size: .85rem;
                        color: var(--muted); }
.btn-icon-sm:hover    { background: var(--bg); color: var(--text); }

/* ---------- 7. Inputs ---------- */
input[type=text], input[type=number], input[type=url], input[type=tel],
input[type=email], input[type=date], input[type=time], input[type=datetime-local],
input[type=month], input[type=week],
input[type=password], input[type=search], select, textarea {
  border: 1px solid var(--border); border-radius: var(--radius);
  padding: 8px 12px; font-size: .88rem; line-height: 1.4; background: var(--surface);
  color: var(--text); outline: none; transition: border .15s; font-family: inherit;
  box-sizing: border-box; height: 38px;
}
textarea { height: auto; }
input:focus, select:focus, textarea:focus {
  border-color: var(--primary); box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 18%, transparent);
}
input:disabled, select:disabled, textarea:disabled,
input[readonly], textarea[readonly] {
  color: var(--muted); background: var(--bg); cursor: not-allowed; opacity: .75;
}
textarea { resize: vertical; min-height: 60px; }

.field-error   { margin-top: 4px; font-size: .78rem; color: var(--danger); }
.form-group label .req { color: var(--danger); font-weight: 700; margin-left: 2px; }
.input-invalid { border-color: var(--danger) !important;
                 box-shadow: 0 0 0 2px rgba(239,68,68,.18); }

/* El PASO QUE HAY QUE DAR AFUERA del back office para poder completar el
   formulario. Hoy lo usa un solo modal: el alta/edición de una
   integración, con el camino para conseguir las credenciales del
   proveedor (sale de `donde`, en el catálogo).

   Va en `--warn` y no en `--info` ni en `--danger`: no es una
   aclaración sobre un campo —eso va en el globito de §8.0-bis— ni algo
   que ya falló. Es "esto te está esperando".

   Es el mismo componente que `panel.css` §8.1, y por el mismo motivo
   que el resto de los gemelos: el formulario de una integración se ve
   en los dos lados y no puede leerse distinto.

   Sin texto no se dibuja: lo esconde el JS. Una caja de advertencia
   vacía se lee como que algo no cargó. */
.form-aviso   { display: flex; gap: 10px; align-items: flex-start;
                padding: 11px 14px; border-radius: var(--radius);
                border: 1px solid color-mix(in srgb, var(--warn) 35%, var(--border));
                border-left: 4px solid var(--warn);
                background: color-mix(in srgb, var(--warn) 10%, var(--surface));
                font-size: .82rem; line-height: 1.5; color: var(--text); }
.form-aviso > i    { color: var(--warn); font-size: .95rem; line-height: 1.45; flex: 0 0 auto; }
.form-aviso strong { display: block; margin-bottom: 2px; }

/* ---------- 8. Formularios ---------- */
.form-row    { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.form-row-3  { grid-template-columns: repeat(3, 1fr); }
.form-row-4  { grid-template-columns: repeat(4, 1fr); }
/* Un campo ancho y uno angosto al lado. Lo usa el par nombre + ícono de
   Prestaciones: el segundo lleva un nombre corto (`fa-wrench`) más su
   vista previa, así que a mitad de fila sobraría ancho y se lo quitaría
   al nombre, que es el que puede tener 120 caracteres. Va como clase y no
   como `style=` inline para que el colapso a una columna del media query
   de abajo le pueda ganar. */
.form-row-ancho-angosto { grid-template-columns: 1fr 260px; }
.form-group  { display: flex; flex-direction: column; gap: 5px; }
.form-group label { font-size: .8rem; font-weight: 600; color: var(--muted); }
.form-group input, .form-group select, .form-group textarea { width: 100%; }

/* ---------- 8.0-bis. Ayuda de un campo: el `question-circle` con globito ----------
   La ÚNICA forma de explicar un campo en todo el proyecto: un ícono a la
   DERECHA del rótulo y el texto en un globito que se lee al pasar el mouse.
   Nunca como nota suelta debajo del control.

   El motivo no es estético: los campos van de a dos por renglón
   (`.form-row` es un grid de dos columnas) y una nota de tres líneas
   debajo de uno empuja hacia abajo SÓLO su mitad, dejando los dos
   controles desalineados.

   El markup lo arma `ayudaDeCampo()` — hay una copia en PHP (arriba de
   `index.php`, para el HTML estático) y otra en JS (`cloud.js`, para los
   formularios que se dibujan desde el front). Nadie escribe el `<span>`
   a mano. */
.field-help {
    position: relative;
    display: inline-flex; align-items: center; vertical-align: middle;
    margin-left: 5px;
    color: var(--muted); cursor: help;
    transition: color .15s;
}
/* El `:has()` acota el `display: flex` a los labels que REALMENTE tienen
   ayuda: `.form-group label` también lo usan los toggles y los campos sin
   globito, y volverlos flex a todos cambiaría cómo se acomoda su
   contenido. El `gap` reemplaza al `margin-left` de arriba, que sumado
   daría 10px. */
.form-group label:has(.field-help) { display: flex; align-items: center; gap: 5px; }
.form-group label .field-help      { margin-left: 0; }
.field-help:hover, .field-help:focus-visible { color: var(--text); outline: none; }
.field-help i { font-size: .85rem; }

.field-help-tip {
    position: absolute; top: calc(100% + 8px); left: 0; z-index: 20;
    width: max-content; max-width: 280px;
    padding: 9px 11px;
    background: var(--bg); color: var(--text);
    border: 1px solid var(--border); border-radius: var(--radius);
    box-shadow: var(--shadow-lg);
    /* El globito hereda del `<label>`, que va en 600 y en `--muted`: sin
       esto se lee como otro rótulo y no como una explicación. */
    font-size: .78rem; font-weight: 400; line-height: 1.5;
    text-transform: none; letter-spacing: normal; white-space: normal;
    opacity: 0; visibility: hidden;
    transition: opacity .12s ease, visibility .12s;
    /* Sin esto, el globito que cae sobre el campo de abajo se come sus clicks. */
    pointer-events: none;
}
.field-help:hover .field-help-tip,
.field-help:focus-visible .field-help-tip { opacity: 1; visibility: visible; }

/* En la ÚLTIMA COLUMNA de la grilla se ancla al borde derecho. No es
   estética: anclado a la izquierda el globito mide hasta 280px y se sale
   del modal, que al ser `overflow-y: auto` se vuelve contenedor de scroll
   en los DOS ejes — o sea que aparece una barra horizontal en un
   formulario que entra holgado. La regla mira la POSICIÓN en la grilla y
   no una clase que ponga quien llama: un campo se mueve de columna al
   reordenar el formulario y nadie se acordaría de cambiarle el
   modificador. Con `:last-child` la misma línea cubre `.form-row`,
   `.form-row-3` y `.form-row-4`. */
.form-row > *:last-child .field-help-tip { left: auto; right: 0; }

/* ---------- 8.1. Selector de color ----------
   Dos piezas: el CAMPO (`.color-field`), que en el formulario ocupa el
   lugar de un input y muestra el valor actual, y la PALETA
   (`.color-picker-*`), que vive dentro del sub-modal "Elegir color".
   La paleta no se renderiza en el formulario: 36 tonos sueltos sobre el
   form tapaban el resto de la pestaña. */

/* Campo: se lee como un input (misma altura, mismo borde, mismo focus)
   pero es un <button>. El punto de color va a la izquierda y el hex en
   monospace al lado, así el valor guardado se lee sin abrir el modal. */
.color-field { display: flex; align-items: center; gap: 10px; width: 100%;
               border: 1px solid var(--border); border-radius: var(--radius);
               padding: 7px 12px; background: var(--surface); color: var(--text);
               font-size: .88rem; font-family: inherit; cursor: pointer;
               text-align: left; transition: border-color .15s; }
.color-field:hover { border-color: var(--primary); }
.color-field:focus-visible { outline: none; border-color: var(--primary);
                             box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 18%, transparent); }
.color-field-dot  { width: 20px; height: 20px; border-radius: 50%; flex-shrink: 0;
                    border: 1px solid var(--border); background: var(--bg); }
/* Sin color: el punto queda hueco, no pintado de gris — un gris se
   confundiría con el grafito, que sí es una opción de la paleta. */
.color-field-dot.vacio { background: repeating-linear-gradient(45deg,
                           var(--bg) 0 4px, var(--border) 4px 8px); }
.color-field-hex  { flex: 1; font-family: monospace; font-size: .85rem; }
.color-field-hex.vacio { font-family: inherit; color: var(--muted); }
.color-field-icon { color: var(--muted); font-size: .9rem; flex-shrink: 0; }

/* Paleta del modal: una fila por tono, una columna por familia. Las 12
   columnas son fijas para que las filas sigan leyéndose como tonos de la
   misma familia; los swatches se encogen con el ancho disponible. */
.color-picker-grid   { display: grid; grid-template-columns: repeat(12, 1fr); gap: 6px; }
.color-picker-swatch { aspect-ratio: 1; min-width: 0; border-radius: 6px;
                       border: 1px solid rgba(0,0,0,.15); cursor: pointer;
                       padding: 0; transition: transform .12s; }
.color-picker-swatch:hover    { transform: scale(1.12); }
.color-picker-swatch.selected { outline: 3px solid var(--text); outline-offset: 2px; }

/* Muestra: el color elegido con texto blanco encima, que es exactamente la
   condición que la paleta garantiza. Sin selección se pinta con el ajedrez
   del campo vacío. */
.color-picker-preview { display: flex; align-items: center; justify-content: space-between;
                        gap: 12px; flex-wrap: wrap;
                        border-radius: var(--radius); padding: 18px 20px;
                        background: repeating-linear-gradient(45deg,
                          var(--bg) 0 6px, var(--border) 6px 12px);
                        transition: background .15s; }
.color-picker-preview-texto { color: #fff; font-weight: 600; font-size: .95rem;
                              text-shadow: none; }
.color-picker-preview-hex   { color: rgba(255,255,255,.82); font-family: monospace;
                              font-size: .82rem; }
/* Sin color el blanco sobre el ajedrez no se leería: se apaga el texto. */
.color-picker-preview.vacio .color-picker-preview-texto,
.color-picker-preview.vacio .color-picker-preview-hex { color: var(--muted); }
.color-picker-preview.vacio .color-picker-preview-hex { font-family: inherit; }

/* Entrada manual del hex. El borde y el focus ring viven en el envoltorio
   y no en el <input>, porque el `#` de la izquierda tiene que quedar
   adentro de la misma caja: dos cajas pegadas se leerían como dos campos.
   Por eso el input se queda sin borde ni sombra propios.
   El selector va descendiente —y no `.color-picker-hex-input` a secas—
   porque el borde que hay que apagar lo pone `input[type=text]` (§7), que
   con su selector de atributo le gana a una clase sola. */
.color-picker-hex-wrap  { display: flex; align-items: center;
                          border: 1px solid var(--border); border-radius: var(--radius);
                          background: var(--surface); transition: border-color .15s; }
.color-picker-hex-wrap:focus-within {
                          border-color: var(--primary);
                          box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 18%, transparent); }
.color-picker-hex-gato  { padding-left: 12px; color: var(--muted);
                          font-family: monospace; font-size: .85rem; user-select: none; }
.color-picker-hex-wrap .color-picker-hex-input {
                          flex: 1; min-width: 0; border: none; background: none;
                          padding: 8px 12px 8px 2px;
                          font-family: monospace; font-size: .85rem; letter-spacing: .06em; }
.color-picker-hex-wrap .color-picker-hex-input:focus { border: none; box-shadow: none; }

/* Botón de la paleta gradiente, al final del mismo renglón. El ícono es lo
   que se ve; el `<input type="color">` va encima en opacidad 0 ocupando
   todo el botón, así el click entra directo al control nativo —que es lo
   que ancla el selector del sistema debajo del botón— y el `<span>` no
   necesita handler propio. */
.color-picker-hex-btn { position: relative; flex-shrink: 0;
                        display: inline-flex; align-items: center; justify-content: center;
                        width: 30px; height: 26px; margin-right: 5px;
                        border: 1px solid var(--border); border-radius: 6px;
                        color: var(--muted); font-size: .82rem;
                        transition: color .15s, border-color .15s; }
.color-picker-hex-btn:hover { color: var(--text); border-color: var(--primary); }
/* `inset: -1px` y no `0`: un absoluto se posiciona contra la caja de
   relleno, así que con `0` el borde de 1px del botón queda afuera del
   nativo y se vuelve un filo muerto donde el click no abre nada. */
.color-picker-hex-btn input { position: absolute; inset: -1px;
                              width: auto; height: auto; min-width: 0;
                              margin: 0; padding: 0; border: none; opacity: 0;
                              cursor: pointer; }

/* Un hex tipeado a mano puede no llegar al 4,5:1 contra el blanco que la
   paleta sí garantiza. No se bloquea —puede ser el color exacto de una
   marca— pero se avisa, porque el rótulo que va encima es blanco. */
.color-picker-aviso     { margin-top: 5px; font-size: .78rem; color: var(--warn);
                          display: flex; align-items: flex-start; gap: 6px;
                          line-height: 1.4; }

/* ---------- 10. Tablas ---------- */
.table-card { background: var(--surface); border: 1px solid var(--border);
              border-radius: var(--radius); box-shadow: var(--shadow);
              overflow-x: auto; overflow-y: hidden; }
table        { width: 100%; border-collapse: collapse; font-size: .88rem; }
thead tr     { background: var(--bg); }
th           { padding: 10px 14px; text-align: left;
               font-size: .75rem; text-transform: uppercase; letter-spacing: .05em;
               color: var(--muted); font-weight: 600;
               border-bottom: 1px solid var(--border); white-space: nowrap; }
td           { padding: 10px 14px; border-bottom: 1px solid var(--border);
               vertical-align: middle; }
tbody tr:last-child td { border-bottom: none; }
tbody tr:hover { background: var(--row-hover); }
.actions      { display: flex; gap: 4px; }
.table-empty  { text-align: center; padding: 48px 24px; color: var(--muted); }
.td-img       { width: 44px; height: 44px; object-fit: cover;
                border-radius: 8px; border: 1px solid var(--border); }
/* Hueco de la miniatura: misma caja que `.td-img` con el ícono del rubro
 * adentro. Cubre los dos casos en que no hay imagen que mostrar — el
 * aviso todavía no tiene fotos, o la tiene pero el objeto no está en el
 * bucket — para que la fila no salte de alto ni muestre el ícono de
 * imagen rota del navegador. */
.td-img-vacia { width: 44px; height: 44px; border-radius: 8px;
                border: 1px dashed var(--border); background: var(--bg);
                display: inline-flex; align-items: center; justify-content: center;
                color: var(--muted); font-size: 1rem; }
.td-nombre    { font-weight: 600; }
.td-id        { color: var(--muted); font-size: .8rem; }

/* ---------- Avatar circular ---------- */
/* Lo usan el listado de Usuarios (chico, a la izquierda del nombre) y el
 * modal de Consultar (grande). La inicial va SIEMPRE en el div y la foto
 * encima, en absoluto: si la imagen no carga alcanza con sacarla del DOM
 * para que aparezca la letra, en vez del ícono de imagen rota. Por eso el
 * fondo pintado va en el contenedor y no en la <img>. */
.avatar       { position: relative; overflow: hidden; border-radius: 50%;
                flex-shrink: 0; display: inline-flex;
                align-items: center; justify-content: center;
                background: var(--primary); color: #fff;
                font-weight: 700; line-height: 1; user-select: none; }
.avatar img   { position: absolute; inset: 0;
                width: 100%; height: 100%; object-fit: cover; display: block; }
.avatar-sm    { width: 28px; height: 28px; font-size: .72rem; }
.avatar-lg    { width: 72px; height: 72px; font-size: 1.6rem; }

/* Celda `Nombre` del listado: círculo + texto en una línea. El `min-width:0`
 * es lo que habilita el recorte con puntos suspensivos — sin eso el item
 * flex nunca baja de su ancho de contenido y la celda se estira. */
.td-nombre-avatar      { display: flex; align-items: center; gap: 9px; min-width: 0; }
.td-nombre-avatar span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Bloque del avatar arriba del modal Consultar. Mismo fondo que las
 * tarjetas de campo (10% más oscuro que el modal), para que se lea como
 * parte de la misma grilla y no como un componente aparte. */
.consult-avatar          { display: flex; align-items: center; gap: 16px;
                           padding: 14px 16px; margin-bottom: 16px;
                           border-radius: var(--radius);
                           background: color-mix(in srgb, var(--surface) 90%, #000); }
.consult-avatar-meta     { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.consult-avatar-nombre   { font-weight: 700; font-size: .92rem; }
.consult-avatar-estado   { font-size: .78rem; color: var(--muted); }
.consult-avatar-acciones { display: flex; gap: 6px; margin-left: auto; flex-wrap: wrap; }

/* ---------- 11. Badges ---------- */
.badge         { display: inline-block; padding: 2px 10px;
                 border-radius: 99px; font-size: .75rem; font-weight: 600; }
.badge-info    { background: #eff6ff; color: #3b82f6; }
.badge-success { background: #dcfce7; color: #16a34a; }
.badge-danger  { background: #fef2f2; color: #dc2626; }
.badge-warn    { background: #fef9c3; color: #a16207; }
/* Dos variantes sin carga semántica, para etiquetas que sólo distinguen
   una categoría de otra (los subsistemas del backlog, por ejemplo) y no
   dicen si algo está bien o mal. Mismo par fondo claro / texto oscuro que
   las cuatro de arriba, así rinden igual en los dos temas. */
.badge-purple  { background: #f3e8ff; color: #7c3aed; }
.badge-neutral { background: #f1f5f9; color: #475569; }

/* ---------- 12. Stat cards ---------- */
.stats-bar  { display: flex; gap: 14px; margin-bottom: 20px; flex-wrap: wrap; }
.stat-card  { background: var(--surface); border: 1px solid var(--border);
              border-radius: var(--radius); padding: 14px 20px;
              display: flex; flex-direction: column; gap: 2px;
              flex: 1; min-width: 120px; }
.stat-label { font-size: .75rem; color: var(--muted);
              text-transform: uppercase; letter-spacing: .04em; }
.stat-value { font-size: 1.5rem; font-weight: 700; }
.stat-value.green  { color: var(--success); }
.stat-value.blue   { color: var(--primary); }
.stat-value.red    { color: var(--danger); }

.dash-link { cursor: pointer; transition: opacity .15s; }
.dash-link:hover { opacity: .75; }
.stat-card.dash-link:hover { background: var(--bg); }

/* ---------- 9. Toolbar (búsqueda + filtros + acciones) ---------- */
.toolbar       { display: flex; align-items: center; gap: 12px;
                 flex-wrap: wrap; margin-bottom: 20px; }
.toolbar-left  { display: flex; align-items: center; gap: 10px;
                 flex: 1; flex-wrap: wrap; }
.toolbar-right { display: flex; gap: 10px; }

.search-wrap        { position: relative; display: inline-flex; align-items: center; }
.search-wrap .search-input { width: 260px; padding-right: 28px; }
.search-clear       { position: absolute; right: 6px; background: none; border: none;
                      cursor: pointer; color: var(--muted); font-size: 1.1rem;
                      padding: 2px 6px; border-radius: 50%; transition: color .15s; }
.search-clear:hover { color: var(--text); }

.filter-chip        { padding: 6px 12px; border-radius: 20px;
                      border: 1.5px solid var(--border);
                      font-size: .8rem; font-weight: 600; cursor: pointer;
                      white-space: nowrap; background: var(--surface);
                      color: var(--muted); transition: all .15s; }
.filter-chip:hover  { border-color: var(--primary); color: var(--primary); }
.filter-chip.active { background: var(--primary); border-color: var(--primary); color: #fff; }
/* Un chip que hoy no lleva a ningún lado (ej. "Editar" en Proyecto cuando el
 * documento no se pudo abrir) se apaga en vez de desaparecer: el juego de
 * opciones sigue siendo el mismo y se ve por qué no está disponible. */
.filter-chip:disabled,
.filter-chip:disabled:hover { opacity: .45; cursor: not-allowed;
                              border-color: var(--border); color: var(--muted); }

/* Botones solo-ícono con badge opcional */
.btn-icon            { position: relative; padding: 7px 11px; }
.btn-icon.active     { border-color: var(--primary); color: var(--primary); }
.btn-icon-badge      { position: absolute; top: -4px; right: -4px;
                       background: var(--primary); color: #fff;
                       font-size: .65rem; font-weight: 700;
                       min-width: 16px; height: 16px; padding: 0 4px;
                       border-radius: 99px; display: inline-flex;
                       align-items: center; justify-content: center;
                       line-height: 1; }

/* ---------- 14. Modales ----------
 *
 * DENTRO DE UN MODAL, `--primary` ES LA MARCA.
 *
 * Fuera del modal siguen siendo dos colores distintos (§1.1): el rojo
 * pinta la banda —bloque del logo + topbar— y el grafito jerarquiza la
 * acción en la pantalla de fondo. Adentro no: un modal es una ventana
 * propia, su barra de título es la continuación visual de la banda de
 * arriba, y con el grafito se leía como una pieza de otro sistema.
 *
 * Se redefine el TOKEN y no cada regla, por dos razones. Una, que así
 * cae todo lo que ya decía `--primary` sin tener que enumerarlo: la
 * barra de título, los botones de la barra de acciones, la pestaña
 * activa y su contador, los chips, el focus ring de los campos, los
 * toggles, los links y los íconos pintados con un `style` inline desde
 * el JS. Y dos, que un modal nuevo lo hereda sin acordarse de nada —
 * la regla sigue siendo "la acción primaria va en `--primary`".
 *
 * El `confirmDialog` (§15) entra en la MISMA redefinición. Es una
 * ventana por encima de todo lo demás y su botón relleno es la acción
 * del sistema: en grafito era el único relleno de la aplicación que no
 * seguía a la marca y se leía como de otra pieza.
 *
 * Lo que eso cuesta, y hay que tenerlo presente al escribir un
 * `confirmar()`: `btn-danger` (#ef4444) y `btn-primary` (#D90000) pasan
 * a ser dos rojos. Nunca se ven juntos —el diálogo tiene UN solo botón
 * relleno—, así que lo que dice "esto destruye algo" tiene que ser el
 * RÓTULO (`Eliminar`, `Sobrescribir`, `Descartar`) y el texto de la
 * pregunta, no el tono. El caso que alterna por entorno —el migrador,
 * con `esProd ? 'btn-danger' : 'btn-primary'`— no pierde nada, porque
 * el color nunca fue su única señal: el título ya dice `⚠ Aplicar en
 * PRODUCCIÓN` y el cuerpo nombra la base. Una confirmación nueva se
 * escribe igual: el texto primero, el color después.
 */
.modal-backdrop,
.confirm-backdrop {
    --primary:   var(--brand);
    --primary-h: var(--brand-edge);
}
.modal-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,.45);
                  display: flex; align-items: center; justify-content: center;
                  z-index: 100; opacity: 0; pointer-events: none;
                  transition: opacity .2s; }
.modal-backdrop.open { opacity: 1; pointer-events: all; }
/* El que scrollea es el CUERPO, no el modal entero: así la barra de
 * título y la de acciones quedan fijas y la salida no se va de la vista
 * justo cuando más contenido hay. `min-height: 0` en el body no es
 * opcional — sin eso un hijo flex no baja de su alto de contenido y el
 * `overflow-y` nunca se activa. */
.modal          { background: var(--surface); border-radius: 14px;
                  width: 100%; max-width: 520px; max-height: 90vh; overflow: hidden;
                  display: flex; flex-direction: column;
                  box-shadow: var(--shadow-lg);
                  transform: scale(.96) translateY(12px);
                  transition: transform .2s; margin: 16px; }
.modal-backdrop.open .modal { transform: scale(1) translateY(0); }
.modal-header   { padding: 20px 24px 16px; border-bottom: 1px solid var(--border);
                  display: flex; align-items: center; justify-content: space-between;
                  gap: 12px; }
.modal-title    { font-size: 1rem; font-weight: 700;
                  display: inline-flex; align-items: center; gap: 8px; }
.modal-body     { padding: 20px 24px;
                  display: flex; flex-direction: column; gap: 16px;
                  flex: 1 1 auto; min-height: 0; overflow-y: auto; }
.modal-header, .modal-menubar { flex: 0 0 auto; }

/* ---------- 14-bis. Estándar de modales: título en primary + barra ----------
 * Todo modal del proyecto lleva la barra de título pintada y los botones
 * arriba, en su propia barra. No queda ningún `.modal-footer` en el
 * proyecto — por eso la clase ya no se define acá: si aparece en un modal
 * nuevo, es que está en el formato viejo. La única excepción al estándar
 * es el `confirmDialog`, que no es una pantalla sino una pregunta.
 */
.modal-header-primary {
    background: var(--primary);
    color: #fff;
    border-bottom: none;              /* con el fondo pintado, la divisoria sobra */
    border-radius: 14px 14px 0 0;     /* mismo radio que .modal */
    padding: 10px 24px;               /* más baja que el header normal */
}
.modal-header-primary .modal-title       { color: #fff; }
.modal-header-primary .modal-subtitle    { color: rgba(255,255,255,.75); }
.modal-header-primary .btn-icon-sm,
.modal-header-primary .btn-ghost         { color: #fff; }
.modal-header-primary .btn-icon-sm:hover,
.modal-header-primary .btn-ghost:hover   { background: rgba(255,255,255,.18); color: #fff; }

.modal-menubar {
    /* 14px y no los 10 del título: acá hay botones de ~30px y con 10
     * quedan tocando el primary de arriba y la divisoria de abajo. */
    padding: 14px 24px;
    border-bottom: 1px solid var(--border);
    /* Un escalón más oscuro que el cuerpo. La mezcla es ENTRE TOKENS y no
     * contra #000, así el tono sigue solo al tema claro/oscuro. */
    background: color-mix(in srgb, var(--surface) 40%, var(--bg));
    display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
}
/* La barra normaliza el tamaño sola: ningún call site se olvida el btn-sm. */
.modal-menubar .btn { white-space: nowrap; padding: 5px 12px; font-size: .8rem; }
/* La salida va RELLENA con el gris del cuerpo: sobre la franja más oscura
 * un ghost transparente se lee como un hueco al lado de los primary. Y su
 * hover SUBE a --row-hover en vez de bajar al color de la franja. */
.modal-menubar .btn-ghost       { background: var(--surface); color: var(--text); }
.modal-menubar .btn-ghost:hover { background: var(--row-hover); color: var(--text); }
/* Botón de la barra que es un toggle (ej. el auto-scroll del terminal): el
 * "encendido" se marca con un anillo interior blanco. `.btn-icon.active`
 * no sirve acá — pinta borde y texto en --primary, que sobre un primary
 * relleno es invisible. */
.modal-menubar .btn-primary.active { box-shadow: inset 0 0 0 2px rgba(255,255,255,.55); }
.modal-menubar-end  { margin-left: auto; }
.menubar-caret      { font-size: .7em; opacity: .65; margin-left: 2px; }

/* Pestañas dentro de un modal (mismo look & feel que .db-exp-tabs). */
.modal-tabs        { display: flex; gap: 4px; border-bottom: 1px solid var(--border);
                     margin: -4px -4px 4px; padding: 0 4px; flex-shrink: 0; }
.modal-tab         { background: none; border: none; cursor: pointer;
                     font-family: inherit; font-size: .85rem; font-weight: 600;
                     color: var(--muted); padding: 10px 14px;
                     border-bottom: 2px solid transparent; margin-bottom: -1px;
                     display: inline-flex; align-items: center; gap: 8px;
                     transition: color .15s, border-color .15s; }
.modal-tab:hover   { color: var(--text); }
.modal-tab.active  { color: var(--primary); border-bottom-color: var(--primary); }
.modal-tabpanel    { display: flex; flex-direction: column; gap: 16px; }
.modal-tabpanel[hidden] { display: none; }
/* El contador de la pestaña reusa `.db-exp-tab-count` (misma píldora), pero
 * su variante activa está scopeada al explorador: acá va la del modal. */
.modal-tab.active .db-exp-tab-count {
                     background: color-mix(in srgb, var(--primary) 18%, transparent);
                     color: var(--primary);
                     border-color: color-mix(in srgb, var(--primary) 40%, transparent); }

/* Editor enriquecido de descripción de línea (modal por doble click) */
.editor-linea-toolbar { display: flex; align-items: center; gap: 6px;
                        flex-wrap: wrap; padding: 6px 8px;
                        border: 1px solid var(--border);
                        border-bottom: none;
                        border-radius: var(--radius) var(--radius) 0 0;
                        background: var(--bg);
                        color: var(--text); }
/* Los <button> nativos no heredan `color` del contenedor (usan el color
   de sistema `ButtonText`), así que en dark mode los íconos de la
   toolbar quedaban negros aunque el token --text sea claro. Forzamos
   la herencia acá para que los botones de formato tomen --text. */
.editor-linea-toolbar .btn-icon-sm { color: inherit; }
.editor-linea-toolbar .editor-linea-sep {
                        width: 1px; height: 20px; background: var(--border);
                        margin: 0 4px; }
.editor-linea-cuerpo  { min-height: 200px; max-height: 50vh; overflow-y: auto;
                        padding: 12px 14px; font-size: .9rem;
                        line-height: 1.5; color: var(--text);
                        background: var(--surface);
                        border: 1px solid var(--border);
                        border-radius: 0 0 var(--radius) var(--radius);
                        outline: none; }
.editor-linea-cuerpo:focus { border-color: var(--primary);
                        box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 18%, transparent); }
.editor-linea-cuerpo b { font-weight: 700; }
.editor-linea-cuerpo i { font-style: italic; }
.editor-linea-cuerpo u { text-decoration: underline; }

/* Renderizado inline de la descripción con formato en Consulta e
   impresión. En la impresión el .pp-c-desc hereda el color/family. */
.linea-desc-rich { white-space: pre-wrap;
                   font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
/* Fuerza la misma familia también en los <span style="font-size:Npx">
   internos que agrega el editor, para que la descripción impresa
   quede con la misma fuente que el resto del comprobante
   (encabezado del emisor, cliente, totales). */
.linea-desc-rich * { font-family: inherit !important; }
.linea-desc-rich b { font-weight: 700; }
.linea-desc-rich i { font-style: italic; }
.linea-desc-rich u { text-decoration: underline; }

/* Tarjetas de campos del modal de Consulta (10% más oscuro que --surface) */
.consult-grid   { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.consult-card   { background: color-mix(in srgb, var(--surface) 90%, #000);
                  border: none; border-radius: var(--radius);
                  padding: 10px 14px; display: flex; flex-direction: column; gap: 2px; }
.consult-card.full { grid-column: 1 / -1; }
.consult-label  { font-size: .72rem; color: var(--muted);
                  text-transform: uppercase; letter-spacing: .04em; font-weight: 600; }
.consult-value  { font-size: .9rem; color: var(--text); word-break: break-word; }
@media (max-width: 540px) { .consult-grid { grid-template-columns: 1fr; } }

/* Una tabla DENTRO de una pestaña de un modal scrollea en su propia
   caja, con el encabezado pegajoso. El que scrollea en un modal es el
   cuerpo (§14), y trescientas filas lo dejarían kilométrico: la barra de
   acciones se iría de la vista justo cuando más contenido hay.

   Es el mismo componente que `panel.css` (§14.1 de su DESIGN.md). Hoy lo
   usan las pestañas Avisos y Actividad del Consultar de una
   integración. */
.modal-tabla-scroll { max-height: 48vh; overflow-y: auto; overflow-x: auto; }
.modal-tabla-scroll thead th { position: sticky; top: 0; z-index: 1;
                               background: var(--bg); }

/* La tabla de una respuesta de la API (consola de Valumotor): las
   columnas son las claves que haya traído el JSON, así que pueden ser
   catorce y ninguna se puede angostar a mano.

   UNA LÍNEA POR FILA, y lo que no entra se corta con elipsis (el valor
   entero queda en el `title`). Con `wrap`, una sola celda larga —el
   nombre de una versión— estira la fila a tres renglones y la tabla deja
   de poder barrerse con la vista, que es lo único que esta vista hace
   mejor que el JSON de al lado. Lo que se corta no se pierde: está
   completo en la otra pestaña. */
.consola-tabla th,
.consola-tabla td { white-space: nowrap; }
.consola-tabla td { max-width: 320px; overflow: hidden; text-overflow: ellipsis; }

/* ---------- Galería de fotos de una ficha ----------
   La pestaña `Fotos` de un modal de Consultar. Mismas clases y mismo
   aspecto que la del panel (`panel.css` §10.3): los dos muestran las
   MISMAS fotos del mismo aviso, así que verlas distinto según de qué
   lado del monorepo se mire es justo lo que no puede pasar.

   La grilla es auto-fill con mínimo 160px, como la biblioteca de
   imágenes de Entradas (§22): la cantidad de columnas la decide el ancho
   del modal y no un número fijo. La caja se reserva con `aspect-ratio`
   para que cargar las fotos no salte el layout. */
.consult-gallery     { display: grid; gap: 12px;
                       grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); }
.consult-foto        { position: relative; display: block; aspect-ratio: 4 / 3;
                       overflow: hidden; border-radius: var(--radius);
                       background: color-mix(in srgb, var(--surface) 90%, #000); }
.consult-foto img    { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Hueco de la foto cuando el objeto no responde: misma caja, borde
   punteado y el ícono del rubro adentro. Es la regla de `.td-img-vacia`
   (§10) y de `.entrada-img-vacia` (§22) — las fotos se sirven por URL
   pública desde S3 y un objeto que falta deja al navegador pintando su
   ícono de imagen rota. */
.consult-foto-vacia  { width: 100%; height: 100%; background: var(--bg);
                       border: 1px dashed var(--border); border-radius: var(--radius);
                       display: flex; align-items: center; justify-content: center;
                       color: var(--muted); font-size: 1.6rem; }
.consult-gallery-empty,
.consult-gallery-loading,
.consult-gallery-error { grid-column: 1 / -1; text-align: center;
                         padding: 18px; color: var(--muted); font-size: .85rem; }
.consult-gallery-error { color: var(--danger); }

/* ---------- Comparador antes / después (quitar marca de agua) ----------
   Dos paneles del mismo ancho, uno al lado del otro. La comparación es el
   control de calidad del proceso —la foto limpia pisa al original y no hay
   deshacer—, así que las dos tienen que verse al mismo tamaño y lo más
   grandes posible: el rastro que queda se mide en unos pocos niveles de
   gris y a mitad de tamaño no se juzga.

   `align-items: start` para que el panel que todavía no tiene resultado no
   estire al otro, y `object-fit: contain` porque las dos pueden tener
   proporciones distintas: al recortar el relleno blanco la foto limpia
   cambia de forma (una vertical acolchada vuelve a ser vertical). */
.ma-comparador  { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; align-items: start; }
.ma-panel       { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
/* Las dos imágenes ocupan LA MISMA caja, con alto fijo y `contain`. Es a
   propósito: al recortar el relleno blanco la foto limpia cambia de
   proporción, y si cada panel se adapta a su contenido quedan de distinto
   alto y la comparación deja de ser pareja. Con la caja igual, lo que
   cambia de un lado al otro es sólo la foto. */
.ma-panel img   { width: 100%; height: 56vh; object-fit: contain;
                  border-radius: var(--radius); background: var(--bg); }
.ma-vacio       { border: 1px dashed var(--border); border-radius: var(--radius);
                  padding: 28px 18px; text-align: center; color: var(--muted);
                  font-size: .85rem; line-height: 1.5; min-height: 180px;
                  display: flex; flex-direction: column; justify-content: center; }
.ma-detalle     { font-size: .82rem; color: var(--muted); line-height: 1.45;
                  background: color-mix(in srgb, var(--surface) 90%, #000);
                  border-radius: var(--radius); padding: 10px 14px; }
.ma-detalle.vacio { display: none; }
@media (max-width: 900px) { .ma-comparador { grid-template-columns: 1fr; } }

/* Sello de moderación de UNA foto: el disco con el glifo en la esquina
   de la miniatura. Es el mismo componente que `.foto-sello` del panel y
   que `.pub-foto-sello` del portal, con los mismos tres estados de
   `avisos_fotos` (aprobada, sin revisar, rechazada).

   Los tres llevan sello, incluida la aprobada: sin él, "sin marca"
   habría que leerlo como "está todo bien", y eso es justo lo que no se
   entiende cuando la foto de al lado sí tiene marca. El rótulo entero
   —y el motivo del rechazo, cuando lo hay— va en el `title`: en una
   miniatura de 160px no entra texto y el glifo solo no alcanza para
   saber si "en revisión" es bueno o malo.

   Abajo a la derecha, como en el panel: en una foto de inmueble el
   motivo principal suele estar arriba a la izquierda. */
.consult-foto .foto-sello {
  position: absolute; bottom: 6px; right: 6px;
  width: 22px; height: 22px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: .62rem; color: #fff;
  box-shadow: var(--shadow);
  cursor: help;
}
.consult-foto .foto-sello.is-ok     { background: var(--success); }
.consult-foto .foto-sello.is-espera { background: var(--muted); }
.consult-foto .foto-sello.is-mal    { background: var(--danger); }
/* La rechazada se atenúa además de sellarse: en una grilla de ocho
   miniaturas, un disco de 22px solo no alcanza para ver de un vistazo
   cuál es la que no está saliendo en el portal. */
.consult-foto.is-mal img { opacity: .55; }

/* ---------- Entradas: el cuerpo y su biblioteca de imágenes ----------
   La pestaña `Cuerpo` del módulo Entradas pinta lo mismo en los dos
   modales —la ficha de Consultar y la vista previa del editor— así que
   las clases son las mismas y no hay una variante por modal.

   `.entrada-cuerpo` es sólo LA CAJA: el mismo fondo que una
   `.consult-card` (el cuerpo es un campo de la ficha, el más largo) y un
   tope de alto para que un artículo largo scrollee en su propio marco en
   vez de estirar el modal. La tipografía y los bloques los pone
   `.md-body`, que se le suma cuando el cuerpo es HTML — no se redefinen
   acá encabezados, listas ni tablas.

   El modificador `.texto` es el otro modo del campo `cuerpo_formato`: el
   cuerpo es texto plano, así que los saltos de línea se respetan y no se
   interpreta nada. */
.entrada-cuerpo        { background: color-mix(in srgb, var(--surface) 90%, #000);
                         border-radius: var(--radius); padding: 16px 18px;
                         color: var(--text); max-height: 55vh; overflow-y: auto; }
.entrada-cuerpo.texto  { white-space: pre-wrap; font-size: .9rem; line-height: 1.55; }
.entrada-cuerpo.vacio  { color: var(--muted); font-size: .88rem; }
/* Una imagen pegada en el cuerpo no puede desbordar la caja. */
.entrada-cuerpo img    { max-width: 100%; height: auto; border-radius: 8px; }
/* Una tabla ancha scrollea en su propia caja, igual que en `.md-body`. */
.entrada-cuerpo table  { display: block; max-width: 100%; overflow-x: auto; }

/* Biblioteca de imágenes. Grilla auto-fill: la cantidad de columnas la
   decide el ancho del modal, no un número fijo. */
.entrada-imgs          { display: grid; gap: 12px;
                         grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); }
.entrada-img-card      { background: color-mix(in srgb, var(--surface) 90%, #000);
                         border-radius: var(--radius); overflow: hidden;
                         display: flex; flex-direction: column; }
.entrada-img-card img  { width: 100%; aspect-ratio: 4 / 3; object-fit: cover;
                         display: block; background: var(--bg); cursor: zoom-in; }
/* Hueco de la miniatura cuando el objeto no responde: misma caja, borde
   punteado y el ícono del recurso adentro (misma regla que `.td-img-vacia`
   en los listados). Las imágenes se sirven por URL pública desde S3 y un
   objeto que falta deja al navegador pintando su ícono de imagen rota. */
.entrada-img-vacia     { width: 100%; aspect-ratio: 4 / 3; background: var(--bg);
                         border-bottom: 1px dashed var(--border);
                         display: flex; align-items: center; justify-content: center;
                         color: var(--muted); font-size: 1.4rem; }
.entrada-img-meta      { padding: 8px 10px; display: flex; flex-direction: column; gap: 6px; }
.entrada-img-desc      { font-size: .78rem; color: var(--muted); word-break: break-word;
                         line-height: 1.35; }
.entrada-img-acciones  { display: flex; gap: 2px; justify-content: flex-end; }
/* Sobre una superficie más oscura que --bg el hover de un `.btn-icon-sm`
   queda al mismo tono que la tarjeta y no se nota. */
.entrada-img-acciones .btn-icon-sm:hover { background: var(--row-hover); }

/* Caja punteada de los estados vacíos de la pestaña Imágenes (sin
   imágenes todavía, o entrada sin guardar). */
.entrada-upload        { border: 1px dashed var(--border); border-radius: var(--radius);
                         padding: 18px; text-align: center;
                         color: var(--muted); font-size: .85rem; }

/* ---------- Editor visual de cuerpo (WYSIWYG) ----------
   Es el hermano de BLOQUE de `.editor-linea-*`: misma receta
   (`contenteditable` + `execCommand`, sin librerías) sobre otra caja. El
   de línea edita una descripción de una línea de comprobante; éste edita
   un artículo entero.

   La diferencia que importa: acá la superficie de edición es LA MISMA
   HOJA que la publicada —`.entrada-cuerpo.md-body` (§22/§21)—, así que
   lo que se ve mientras se escribe es literalmente lo que se va a ver
   publicado. Esto sólo le agrega la unión con la toolbar y el foco; no
   redefine ni un tamaño de tipografía. Por eso el bloque va DESPUÉS de
   §22: necesita ganarle en orden al `border-radius` de `.entrada-cuerpo`. */
.editor-rich-toolbar   { display: flex; align-items: center; gap: 4px;
                         flex-wrap: wrap; padding: 6px 8px;
                         border: 1px solid var(--border); border-bottom: none;
                         border-radius: var(--radius) var(--radius) 0 0;
                         background: var(--bg); color: var(--text); }
/* Un <button> no hereda `color` del contenedor (usa `buttontext`, casi
   negro), así que en tema oscuro los glifos quedarían negros sobre
   oscuro. Misma corrección que `.editor-linea-toolbar .btn-icon-sm`. */
.editor-rich-toolbar .btn-icon-sm { color: inherit; }
/* `width: auto` no es opcional: la toolbar vive dentro de un `.form-group`
   y §8 le da `width: 100%` a todo `select` que haya ahí adentro. Sin esto
   el combo de bloque ocupa la fila entera y empuja los botones abajo. */
.editor-rich-toolbar select       { width: auto; padding: 4px 6px; font-size: .82rem; }
.editor-rich-sep       { width: 1px; height: 20px; background: var(--border);
                         margin: 0 4px; }
.editor-rich-cuerpo    { border: 1px solid var(--border);
                         border-radius: 0 0 var(--radius) var(--radius);
                         min-height: 260px; outline: none; }
.editor-rich-cuerpo:focus { border-color: var(--primary);
                         box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 18%, transparent); }
/* `execCommand` genera b/i/u y no strong/em (ver `richEntradaCmd()`), y
   `.md-body` sólo estiliza los semánticos. Se declaran los dos lados: acá
   para el editor y en `.entrada-cuerpo` para lo publicado. */
.editor-rich-cuerpo b, .entrada-cuerpo b { font-weight: 700; }
.editor-rich-cuerpo i, .entrada-cuerpo i { font-style: italic; }
.editor-rich-cuerpo u, .entrada-cuerpo u { text-decoration: underline; }
/* Placeholder del editor vacío. `:empty` no matchea si el navegador dejó
   un <br> adentro, así que el JS vacía el nodo del todo al cargarlo. */
.editor-rich-cuerpo:empty::before { content: attr(data-placeholder);
                         color: var(--muted); }

/* ---------- El boleto de compraventa (módulo Operaciones) ----------
   El documento que el comprador imprimió desde el portal, guardado entero
   en `avisos_operaciones.boleto` y pintado tal cual en la pestaña Boleto
   de su ficha.

   El HTML lo escribe `www/lib/boleto.php` y trae sus propias clases
   `boleto-*`, que allá las estila un bloque `@media print`. Acá hay que
   estilarlas para PANTALLA, y encima hay que DESHACER lo que el CSS
   global le hace a un `<table>` suelto: ancho completo, `thead` con
   fondo, `th` en versalitas y `td` con divisoria. Sin ese reset, la
   tabla de datos de las partes se vería como un listado del back office
   y no como la grilla de un contrato.

   Es la misma hoja que el panel pinta en su ficha de Operaciones (ver
   `panel.css`, sección homónima): los dos muestran el MISMO documento y
   tienen que leerse igual. Si cambia uno, cambia el otro.

   Va con los tokens del tema y no con tinta negra sobre papel blanco: es
   un documento de TEXTO, no la foto de una hoja, y cloud arranca en
   oscuro. Acá no hay bloque `@media print` porque el back office no
   imprime el boleto — de eso se encargan el portal y el panel. */
.boleto-doc {
  background: color-mix(in srgb, var(--surface) 90%, #000);
  border-radius: var(--radius);
  padding: 26px 30px;
  color: var(--text);
  font-family: Calibri, Carlito, 'Segoe UI', Arial, Helvetica, sans-serif;
  font-size: .82rem;
  line-height: 1.5;
}
.boleto-doc p  { margin: 0; text-align: justify; }
.boleto-doc h1 { margin: 0 0 20px; font-size: 1.05rem; letter-spacing: .04em;
                 text-align: center; text-transform: uppercase; }
.boleto-doc h2 { margin: 66px 0 3px; font-size: .92rem; letter-spacing: .03em;
                 text-transform: uppercase; }

/* El cartel de borrador, con marco: lo que se está mirando es un MODELO
   y eso tiene que leerse antes que el título, igual que en el papel. */
.boleto-doc .boleto-borrador {
  margin: 0 0 10px; padding: 5px 10px;
  border: 1px solid currentColor; border-radius: 6px;
  font-family: inherit; font-size: .72rem; font-weight: 700;
  letter-spacing: .06em; text-align: center; text-transform: uppercase;
  color: var(--warn);
}
.boleto-doc .boleto-lugar { text-align: right; margin-bottom: 8px; }

/* Las rayas de lo que el portal NO sabe —dominio, matrícula, DNI— y hay
   que completar a mano. Van explícitas y no omitidas: quien firma tiene
   que ver qué le falta al documento. */
.boleto-doc .boleto-raya {
  display: inline-block; min-width: 4.5cm;
  border-bottom: 1px solid var(--muted);
}

/* Reset de lo que el CSS global le hace a cualquier `<table>`. */
.boleto-doc table    { width: 100%; border-collapse: collapse; font-size: inherit; }
.boleto-doc thead tr { background: none; }
.boleto-doc th,
.boleto-doc td {
  padding: 0 10px 0 0; border-bottom: none; white-space: normal;
  font-size: .86rem; font-weight: normal; text-transform: none;
  letter-spacing: normal; color: inherit; text-align: left;
  vertical-align: baseline;
}

/* Ficha del bien (cláusula SEGUNDA). Los documentos nuevos la traen en un
   párrafo con los pares separados por comas, así que hereda todo del `p`
   y lo único propio es la raya: en el mismo párrafo puede haber tres
   —dominio, motor y chasis— y con los 4.5cm de la suelta cada una se
   comía un renglón. Los documentos CONGELADOS antes del 2026-09-18f la
   traen en TABLA y se siguen mirando tal como se imprimieron. */
.boleto-doc p.boleto-ficha .boleto-raya { min-width: 2.6cm; }
.boleto-doc table.boleto-ficha    { margin: 0 0 12px; }
.boleto-doc table.boleto-ficha th { width: 5.2cm; color: var(--muted); }

/* Datos de las partes: dos columnas iguales, el encabezado en versalitas
   chicas. Acá SÍ se recupera algo del `th` del back office, porque "EL
   VENDEDOR" / "EL COMPRADOR" son encabezados de columna de verdad. */
.boleto-doc .boleto-partes    { margin-top: 20px; }
.boleto-doc .boleto-partes th,
.boleto-doc .boleto-partes td { width: 50%; vertical-align: top; }
.boleto-doc .boleto-partes th { font-size: .74rem; font-weight: 700;
                                letter-spacing: .05em; color: var(--muted); }
.boleto-doc .boleto-partes .boleto-raya { min-width: 2.8cm; }

.boleto-doc .boleto-ejemplares { margin-top: 10px; }

.boleto-doc .boleto-firmas    { margin-top: 110px; }
.boleto-doc .boleto-firmas td { width: 50%; padding: 0 14px; text-align: center;
                                font-size: .74rem; letter-spacing: .05em;
                                color: var(--muted); }
.boleto-doc .boleto-firma-linea { display: block; margin-bottom: 6px;
                                  border-top: 1px solid var(--muted); }

.boleto-doc .boleto-pie { margin-top: 14px; font-family: inherit;
                          font-size: .7rem; text-align: center; color: var(--muted); }

/* ============================================================
   Horarios de atención (pestaña Horarios del modal de cuenta)
   ============================================================
   Una fila por día de la semana. El día es un rótulo de ancho FIJO
   (110px) y no una columna de grilla: con `auto` las siete filas quedan
   con el rótulo a distinta distancia de su primer campo —"Miércoles" es
   mucho más largo que "Lunes"— y la columna de horas se escalona.

   Las franjas envuelven (`flex-wrap`) en vez de scrollear: un día con
   tres franjas en una pantalla angosta baja a la línea siguiente y sigue
   siendo legible. */
.horarios-editor    { display: flex; flex-direction: column; gap: 6px; }
.horarios-dia       { display: flex; align-items: center; gap: 10px;
                      padding: 8px 12px; border-radius: var(--radius);
                      background: color-mix(in srgb, var(--surface) 90%, #000); }
.horarios-dia-nombre{ width: 110px; flex-shrink: 0;
                      font-size: .82rem; font-weight: 600; color: var(--text); }
.horarios-rangos    { display: flex; align-items: center; gap: 8px;
                      flex-wrap: wrap; flex: 1; min-width: 0; }
.horarios-rango     { display: inline-flex; align-items: center; gap: 4px; }
/* El `input[type=time]` nativo trae su propio ancho, que en Chrome cambia
   según el locale. Se fija para que las columnas de los siete días queden
   alineadas entre sí. */
.horarios-rango input[type=time] { width: 108px; padding: 5px 8px; font-size: .82rem; }
.horarios-sep       { color: var(--muted); font-size: .8rem; }
/* "Cerrado" ocupa el lugar de las franjas cuando el día no tiene ninguna:
   sin esto la fila queda vacía y se lee como un día sin cargar. */
.horarios-cerrado   { font-size: .82rem; color: var(--muted); font-style: italic; }
.horarios-dia-acciones { display: flex; align-items: center; gap: 2px;
                         margin-left: auto; flex-shrink: 0; }
/* La fila es más oscura que `--bg`, así que el hover normal de
 * `.btn-icon-sm` queda casi al mismo tono que la tarjeta y no se nota.
 * Acá sube a `--row-hover`, que sí despega. */
.horarios-dia .btn-icon-sm:hover { background: var(--row-hover); }

/* Badge de estado (abierto / cerrado). Verde y rojo son semánticos acá:
   dicen si la puerta está abierta, no si algo salió bien o mal — pero son
   los mismos tokens, así que no se inventa paleta. */
.horarios-badge         { display: inline-flex; align-items: center; gap: 6px;
                          padding: 2px 10px; border-radius: 99px;
                          font-size: .75rem; font-weight: 600; }
.horarios-badge-abierto { background: #dcfce7; color: #16a34a; }
.horarios-badge-cerrado { background: #fef2f2; color: #dc2626; }
.horarios-badge-sindatos{ background: #f1f5f9; color: #475569; }
[data-theme="dark"] .horarios-badge-abierto  { background: rgba(34,197,94,.18);  color: #86efac; }
[data-theme="dark"] .horarios-badge-cerrado  { background: rgba(239,68,68,.20);  color: #f5a8a8; }
[data-theme="dark"] .horarios-badge-sindatos { background: rgba(255,255,255,.08); color: #9ca0a4; }

/* Markdown renderizado dentro de una tarjeta de Consulta */
.md-body                             { line-height: 1.5; }
.md-body > *:first-child             { margin-top: 0; }
.md-body > *:last-child              { margin-bottom: 0; }
.md-body p                           { margin: 0 0 8px; }
.md-body h1, .md-body h2, .md-body h3,
.md-body h4, .md-body h5, .md-body h6 { margin: 12px 0 6px; font-weight: 700; line-height: 1.25; }
.md-body h1 { font-size: 1.15rem; }
.md-body h2 { font-size: 1.05rem; }
.md-body h3 { font-size: .98rem; }
.md-body h4, .md-body h5, .md-body h6 { font-size: .92rem; color: var(--muted); }
.md-body strong                      { font-weight: 700; }
.md-body em                          { font-style: italic; }
.md-body ul, .md-body ol             { margin: 0 0 8px; padding-left: 22px; }
.md-body li                          { margin-bottom: 2px; }
.md-body a                           { color: var(--primary); text-decoration: underline; }
.md-body a:hover                     { color: var(--primary-h); }
.md-body code                        { background: color-mix(in srgb, var(--surface) 85%, #000);
                                       border-radius: 4px; padding: 1px 5px;
                                       font-family: monospace; font-size: .85em; }
.md-body pre                         { background: color-mix(in srgb, var(--surface) 85%, #000);
                                       border-radius: 6px; padding: 10px 12px;
                                       overflow-x: auto; margin: 0 0 8px; }
.md-body pre code                    { background: none; padding: 0; font-size: .82rem; }
.md-body blockquote                  { border-left: 3px solid var(--border);
                                       margin: 0 0 8px; padding: 2px 12px;
                                       color: var(--muted); }
.md-body hr                          { border: none; border-top: 1px solid var(--border); margin: 10px 0; }
.md-body table                       { border-collapse: collapse; margin: 0 0 8px; font-size: .85rem; }
.md-body th, .md-body td             { border: 1px solid var(--border); padding: 4px 8px; }

/* ---------- 15. Confirm dialog ---------- */
.confirm-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,.5);
                    display: flex; align-items: center; justify-content: center;
                    z-index: 150; opacity: 0; pointer-events: none;
                    transition: opacity .15s; }
.confirm-backdrop.open { opacity: 1; pointer-events: all; }
.confirm-box     { background: var(--surface); border-radius: 14px;
                   padding: 28px 28px 20px; max-width: 380px;
                   width: calc(100% - 32px); box-shadow: var(--shadow-lg);
                   transform: scale(.95); transition: transform .15s; }
.confirm-backdrop.open .confirm-box { transform: scale(1); }
.confirm-title   { font-weight: 700; margin-bottom: 8px; }
.confirm-msg     { font-size: .88rem; color: var(--muted); margin-bottom: 20px;
                   line-height: 1.45; }
.confirm-actions { display: flex; gap: 10px; justify-content: flex-end; }

/* ---------- Tools grid (sección Herramientas: launchers) ---------- */
.tools-grid { display: grid; gap: 16px;
              grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); }
.tool-card  { background: var(--surface); border: 1px solid var(--border);
              border-radius: var(--radius); box-shadow: var(--shadow);
              padding: 18px 20px; cursor: pointer;
              display: flex; flex-direction: column; gap: 6px;
              transition: transform .1s, box-shadow .15s, border-color .15s; }
.tool-card:hover { border-color: var(--primary); box-shadow: var(--shadow-lg);
                   transform: translateY(-1px); }
.tool-card:active { transform: scale(.99); }
.tool-card-icon  { font-size: 1.8rem; line-height: 1; margin-bottom: 4px; }
/* Los glifos de FA rinden más grandes que un emoji al mismo cuerpo, y son
 * monocromos: bajan un escalón y se pintan en la marca, como la tarjeta de
 * ayuda de cada módulo. */
.tool-card-icon i { font-size: 1.5rem; color: var(--primary); }
.tool-card-title { font-weight: 700; font-size: 1rem; color: var(--text); }
.tool-card-desc  { font-size: .82rem; color: var(--muted); line-height: 1.45; }
.tool-card-desc code { background: var(--bg); padding: 1px 5px; border-radius: 4px;
                       font-size: .78rem; }

/* ---------- Explorador S3 ---------- */
.s3-modal { max-width: 980px; width: calc(100% - 32px); }
.s3-toolbar { display: flex; align-items: center; justify-content: space-between;
              gap: 12px; flex-wrap: wrap; }
.s3-toolbar-right { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.s3-breadcrumbs { display: flex; align-items: center; gap: 4px; flex-wrap: wrap;
                  font-size: .88rem; color: var(--muted); min-height: 28px; }
.s3-breadcrumbs .s3-bc { background: none; border: none; cursor: pointer;
                         color: var(--primary); font-size: .88rem; font-weight: 600;
                         padding: 2px 6px; border-radius: 6px; }
.s3-breadcrumbs .s3-bc:hover { background: var(--bg); }
.s3-breadcrumbs .s3-bc.current { color: var(--text); cursor: default; }
.s3-breadcrumbs .s3-bc.current:hover { background: none; }
.s3-breadcrumbs .s3-sep { color: var(--muted); opacity: .6; }

.s3-table-card { background: var(--surface); border: 1px solid var(--border);
                 border-radius: var(--radius); overflow: auto;
                 max-height: 60vh; }
.s3-table-card table { width: 100%; border-collapse: collapse; font-size: .88rem; }
.s3-table-card thead tr  { background: var(--bg); position: sticky; top: 0; z-index: 1; }
.s3-table-card th        { padding: 10px 14px; text-align: left;
                           font-size: .75rem; text-transform: uppercase;
                           letter-spacing: .05em; color: var(--muted);
                           font-weight: 600; border-bottom: 1px solid var(--border);
                           white-space: nowrap; }
.s3-table-card td        { padding: 10px 14px; border-bottom: 1px solid var(--border);
                           vertical-align: middle; }
.s3-table-card tbody tr:last-child td { border-bottom: none; }
.s3-table-card tbody tr.row-clickable:hover { background: var(--row-hover); }
.s3-table-card .s3-name   { display: flex; align-items: center; gap: 8px;
                            font-weight: 500; }
.s3-table-card .s3-icon   { font-size: 1.05rem; width: 22px; text-align: center; }
.s3-table-card .s3-size,
.s3-table-card .s3-date   { color: var(--muted); font-size: .82rem;
                            font-family: monospace; }
.s3-footer-info { font-size: .78rem; color: var(--muted);
                  display: flex; justify-content: space-between; gap: 8px;
                  flex-wrap: wrap; }

/* ---------- Config cards (sección Configuración) ---------- */
.config-grid       { display: grid; gap: 16px;
                     grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); }
.config-card       { background: var(--surface); border: 1px solid var(--border);
                     border-radius: var(--radius); box-shadow: var(--shadow);
                     display: flex; flex-direction: column; overflow: hidden; }
.config-card-header { display: flex; gap: 14px; padding: 16px 18px; align-items: flex-start; }
/* Ancho fijo para que el texto de todas las tarjetas arranque en la misma
 * columna: los glifos de FA no miden todos lo mismo. */
.config-card-icon  { font-size: 1.8rem; line-height: 1; flex-shrink: 0;
                     width: 28px; text-align: center; }
.config-card-icon i { font-size: 1.5rem; color: var(--primary); }
.config-card-info  { display: flex; flex-direction: column; gap: 4px; }
.config-card-title { font-weight: 700; font-size: .95rem; color: var(--text); }
.config-card-desc  { font-size: .82rem; color: var(--muted); line-height: 1.4; }
.config-card-body  { padding: 14px 18px; border-top: 1px solid var(--border); }

/* ---------- 17. Toggle switches ---------- */
.toggle-switch { display: inline-flex; align-items: center; gap: 12px;
                 cursor: pointer; user-select: none; }
.toggle-switch input { display: none; }
.toggle-track  { width: 44px; height: 24px; border-radius: 12px;
                 background: var(--border); position: relative;
                 transition: background .2s; flex-shrink: 0; }
.toggle-switch input:checked + .toggle-track { background: var(--primary); }
.toggle-thumb  { position: absolute; top: 3px; left: 3px;
                 width: 18px; height: 18px; border-radius: 50%;
                 background: #fff; transition: transform .2s;
                 box-shadow: 0 1px 4px rgba(0,0,0,.2); }
.toggle-switch input:checked + .toggle-track .toggle-thumb { transform: translateX(20px); }
.toggle-label  { font-size: .9rem; color: var(--text); }

/* ---------- 20. Menú contextual ---------- */
.ctx-menu { position: fixed; background: var(--surface);
            border: 1px solid var(--border); border-radius: 10px;
            box-shadow: var(--shadow-lg); min-width: 180px;
            padding: 6px; z-index: 250; display: none; }
.ctx-menu.open { display: block; }
.ctx-menu button { display: flex; align-items: center; gap: 10px;
                   width: 100%; padding: 8px 12px; border: none;
                   background: none; cursor: pointer;
                   font-size: .88rem; color: var(--text);
                   border-radius: 6px; text-align: left; }
.ctx-menu button i { width: 16px; text-align: center; color: var(--muted); }
.ctx-menu button:hover { background: var(--bg); }
.ctx-menu .ctx-menu-sep { height: 1px; background: var(--border); margin: 6px 4px; }
.ctx-menu .ctx-menu-danger { color: var(--danger); }
.ctx-menu .ctx-menu-danger i { color: var(--danger); }
.ctx-menu .ctx-menu-danger:hover { background: rgba(239,68,68,.08); }

/* Filas clickeables del listado */
.row-clickable { cursor: pointer; }

/* Resaltado de filas en el listado de Dominios: los vencidos y los que
   estan por vencer en 30 dias se pintan con un fondo tenue + un borde
   izquierdo del color correspondiente para que salten a la vista sin
   competir con el badge que llevan en la columna Proxima renovacion. */
tr.dominio-vencido    { background: rgba(239, 68, 68, .08); }
tr.dominio-vencido:hover { background: rgba(239, 68, 68, .14); }
tr.dominio-vencido    td:first-child { box-shadow: inset 3px 0 0 var(--danger); }

tr.dominio-por-vencer { background: rgba(245, 158, 11, .09); }
tr.dominio-por-vencer:hover { background: rgba(245, 158, 11, .16); }
tr.dominio-por-vencer td:first-child { box-shadow: inset 3px 0 0 var(--warn); }

/* Cards del modal Registrar pago (Manual / MercadoPago) */
.registrar-pago-card {
  background: color-mix(in srgb, var(--surface) 90%, #000);
  border: 1.5px solid transparent; border-radius: var(--radius);
  padding: 18px 14px; cursor: pointer; color: var(--text);
  display: flex; flex-direction: column; align-items: center;
  gap: 6px; text-align: center; transition: border-color .15s, background .15s;
  font-family: inherit;
}
.registrar-pago-card:hover {
  border-color: var(--primary);
  background: color-mix(in srgb, var(--surface) 85%, #000);
}

/* ---------- Mapa de cuentas (Google Maps) ---------- */
.cuenta-map     { height: 260px; width: 100%;
                  border: 1px solid var(--border); border-radius: var(--radius);
                  overflow: hidden; background: var(--bg); }
.map-helper     { font-size: .78rem; color: var(--muted); margin-bottom: 6px; }
.map-placeholder{ display: flex; align-items: center; justify-content: center;
                  height: 100%; padding: 16px; text-align: center;
                  font-size: .85rem; color: var(--muted); line-height: 1.5; }
.map-placeholder code { background: var(--bg); padding: 1px 6px; border-radius: 4px;
                        font-family: monospace; }

/* Rótulo del campo a la izquierda y "Detectar ubicación" a la derecha, en
   el mismo renglón: el botón es una forma de completar ESTE campo, así que
   va sobre su rótulo y no flotando entre el rótulo y el mapa. El alto del
   renglón lo fija el botón; el label se centra contra él. */
.map-cabecera { display: flex; align-items: center; justify-content: space-between;
                gap: 12px; flex-wrap: wrap; margin-bottom: 2px; }

/* ---------- 16. Toasts ---------- */
.toast { position: fixed; bottom: 24px; left: 50%;
         transform: translateX(-50%) translateY(16px);
         background: #1e293b; color: #fff;
         padding: 10px 20px; border-radius: 99px;
         font-size: .88rem; opacity: 0; pointer-events: none;
         transition: opacity .2s, transform .2s;
         z-index: 200; white-space: nowrap; }
.toast.show    { opacity: 1; transform: translateX(-50%) translateY(0); }
.toast.error   { background: var(--danger);  }
.toast.success { background: var(--success); }

/* ---------- 17. Banner de nueva versión ----------
   El banner es una notificación de la plataforma, no del proyecto: avisa
   que cambió `version.txt` en el server. Por eso va SIEMPRE en un verde
   fijo (#15803d) e independiente del `--primary` y del `--brand` — así se
   lee como un aviso del sistema y no como un elemento más de la pantalla.

   El verde es el MISMO hex en cloud y en panel: es la misma notificación
   de la misma plataforma, y un controlador que salta de una solapa a la
   otra tiene que reconocerla sin releerla. Si cambia acá, cambia allá.

   No sale de `--success` ni de ningún token a propósito: los tokens
   siguen al tema y a la jerarquía de acción, y este aviso no es ninguna
   de las dos cosas. El tono es oscuro (y no el `#22c55e` de `--success`)
   porque encima va texto blanco: a 5:1 contra el blanco se lee igual que
   se leía el azul anterior, y el verde claro caía a 2,3:1.

   Los botones de los mails (api/magic_link.php, api/password_reset.php,
   api/enviar_resumen_cuenta.php) siguen en el azul heredado de dex — son
   otra pieza y no se tocaron acá.

   FORMA: es una PESTAÑA que cuelga del borde de arriba de la pantalla,
   centrada en la ventana, del ancho de su contenido y con las dos
   esquinas de abajo redondeadas. Fue una franja fija a todo lo ancho
   del viewport hasta el 2026-11-05, que tapaba la banda de marca
   entera y se llevaba la atención de toda la pantalla para decir algo
   que no es urgente. Colgando se mete en el hueco que ya estaba vacío
   entre el título y la píldora del usuario.

   Entre medio fue un trapecio, con los costados en diagonal hechos con
   dos triángulos de borde. Se descartó el 2026-11-05 por simple: a este
   tamaño el sesgo casi no se leía y pedía mantener tres medidas atadas
   entre sí (el triángulo medía `alto - radio`), más un alto fijo,
   porque el ancho de un borde no acepta porcentajes. Si alguna vez
   vuelve, el apunte que importa es que NO se hace con `clip-path`: el
   recorte se aplica después del `border-radius` y se come justo las
   curvas de abajo.

   Va `position: fixed` y centrado contra el VIEWPORT, no contra la
   topbar. Anclado a la barra queda 110px corrido a la derecha —la
   barra arranca después del sidebar de 220px— y en pantalla se lee
   descentrado aunque técnicamente no lo esté. Fijo al viewport además
   da lo mismo en mobile, donde el sidebar es overlay y la barra ocupa
   todo el ancho. No lo recorta el `overflow: hidden` de `.main`
   porque ese contenedor no crea bloque contenedor para un `fixed`.

   La forma es la MISMA que en panel/assets/css/panel.css (sección 17),
   por el mismo motivo que el verde: es la misma notificación de la
   misma plataforma. Si cambia acá, cambia allá. */
.version-banner {
  position: fixed; top: 0; left: 50%; transform: translateX(-50%);
  /* Por ENCIMA de todo, y no es exageración: es la capa más alta del
     proyecto a propósito. Abajo están `.modal-backdrop` (100),
     `.modal-encima` (120), `.confirm-backdrop` (150) y `.ctx-menu`
     (250), y los backdrops son `inset: 0` con un velo negro al 45% —
     con el aviso por debajo, cualquier modal abierto lo deja invisible
     atrás del velo aunque esté dibujado. Es lo que pasó entre que el
     aviso se mudó a la topbar (donde 60 alcanzaba) y volvió a ser
     `fixed`. No se baja de acá. */
  z-index: 9999; max-width: calc(100% - 40px); height: 43px;
  background: #15803d; color: #fff;
  border-radius: 0 0 10px 10px;
  padding: 0 20px;
  display: flex; align-items: center; gap: 12px;
  font-size: .82rem; font-weight: 500; line-height: 1; white-space: nowrap;
}

/* El ícono va DENTRO del botón, no suelto en la pestaña: el refresco es
   lo que hace el botón, y de paso sobrevive al breakpoint que esconde la
   frase —abajo de 1100px el aviso queda en botón más cuenta, y sin el
   glifo adentro se quedaba sin ninguna señal visual—. De ahí el
   `inline-flex`: un `<button>` es `inline-block` y el glifo y el texto
   no se alinean solos. */
.version-banner button {
  background: #fff; color: #15803d;
  border: none; border-radius: 6px;
  padding: 3px 11px; font-size: .78rem; font-weight: 600;
  cursor: pointer; font-family: inherit;
  display: inline-flex; align-items: center; gap: 6px;
}
.version-banner button:hover { background: #dcfce7; }
/* Un glifo de FA rinde más grande que el texto al mismo cuerpo. */
.version-banner button i { font-size: .92em; }

/* La cuenta regresiva reserva su ancho MIENTRAS CUENTA, y no existe el
   resto del tiempo. Son dos reglas y cada una ataca algo distinto:

   - `min-width` + cifras de paso fijo, para que el ancho no cambie al
     pasar de `10 s` a `9 s`. La pestaña está centrada con
     `translateX(-50%)`, así que cualquier cambio de ancho se reparte
     entre los dos lados y la correría de lugar una vez por segundo —
     y acá la cuenta arranca en 60, o sea que cruza de dos dígitos a
     uno a mitad de camino;
   - `:empty { display: none }`, para que ese ancho reservado no quede
     colgando de costado durante los 30 segundos en que la pestaña es
     sólo la frase y el botón.

   **El `:empty` se quitó una vez y hubo que devolverlo.** El argumento
   para sacarlo era que la pestaña cambia de tamaño al aparecer y
   desaparecer la cuenta; en la práctica eso pasa como mucho una vez
   cada 30 segundos —que es lo que tarda el reloj en arrancar— y se lee
   como que el aviso se desplegó, no como un parpadeo. Lo que sí se ve
   siempre, y molesta, es el hueco verde vacío al costado. */
.version-banner-cuenta {
  min-width: 10.4em; font-size: .78rem;
  font-variant-numeric: tabular-nums; color: rgba(255,255,255,.85);
}
.version-banner-cuenta:empty { display: none; }

/* En una ventana angosta se cae la frase y quedan el botón y la cuenta,
   que es lo accionable. La topbar ya tiene título y píldora, y un aviso
   que se les mete debajo es peor que uno más corto. */
@media (max-width: 1100px) {
  .version-banner-texto { display: none; }
}

/* ---------- 18. Spinner ---------- */
.spin { display: inline-block; width: 28px; height: 28px;
        border: 3px solid var(--border); border-top-color: var(--primary);
        border-radius: 50%; animation: spin .7s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

/* ---------- 19. Responsive / mobile ---------- */
/* Vive dentro de la topbar, así que va en blanco sobre la banda (§1.5). */
.hamburger        { display: none; background: none; border: none; cursor: pointer;
                    font-size: 1.4rem; color: #fff;
                    padding: 4px 8px; margin-right: 8px; line-height: 1; }
.sidebar-overlay  { display: none; position: fixed; inset: 0;
                    background: rgba(0,0,0,.45); z-index: 199; }
.sidebar-overlay.active { display: block; }

@media (max-width: 768px) {
  .hamburger { display: inline-flex; align-items: center; }
  .sidebar   { position: fixed; top: 0; left: 0; height: 100vh; z-index: 200;
               transform: translateX(-100%); transition: transform .25s; }
  .sidebar.open { transform: translateX(0); }
  .form-row, .form-row-3, .form-row-4,
  .form-row-ancho-angosto { grid-template-columns: 1fr; }
}

/* ---------- Explorador DB ---------- */
.modal.db-exp-modal        { max-width: 1080px; width: calc(100% - 32px);
                             height: calc(100vh - 64px); max-height: calc(100vh - 64px);
                             display: flex; flex-direction: column; overflow: hidden; }
.db-exp-modal .modal-body  { gap: 12px; flex: 1; min-height: 0;
                             display: flex; flex-direction: column; overflow: hidden; }

.db-exp-toolbar       { display: flex; align-items: center; justify-content: space-between;
                        gap: 12px; flex-wrap: wrap; flex-shrink: 0; }
.db-exp-toolbar-right { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.db-exp-toolbar-right .search-wrap .search-input { width: 220px; }

.db-exp-breadcrumbs   { display: flex; align-items: center; gap: 4px; flex-wrap: wrap;
                        font-size: .88rem; color: var(--muted); min-height: 28px; }
.db-exp-crumb         { background: none; border: none; cursor: pointer;
                        color: var(--primary); font-size: .88rem; font-weight: 600;
                        padding: 2px 6px; border-radius: 6px; font-family: inherit;
                        display: inline-flex; align-items: center; gap: 6px; }
.db-exp-crumb:hover   { background: var(--bg); }
.db-exp-crumb.current { color: var(--text); cursor: default; }
.db-exp-crumb.current:hover { background: none; }
.db-exp-crumb-sep     { color: var(--muted); opacity: .6; }

.db-exp-view          { display: flex; flex-direction: column; gap: 8px;
                        flex: 1; min-height: 0; }

.db-exp-tabs          { display: flex; gap: 4px; border-bottom: 1px solid var(--border);
                        margin: 0 -4px; padding: 0 4px; flex-shrink: 0; }
.db-exp-tab           { background: none; border: none; cursor: pointer;
                        font-family: inherit; font-size: .85rem; font-weight: 600;
                        color: var(--muted); padding: 10px 14px;
                        border-bottom: 2px solid transparent; margin-bottom: -1px;
                        display: inline-flex; align-items: center; gap: 8px;
                        transition: color .15s, border-color .15s; }
.db-exp-tab:hover     { color: var(--text); }
.db-exp-tab.active    { color: var(--primary); border-bottom-color: var(--primary); }
.db-exp-tab i         { font-size: .9rem; }
.db-exp-tab-count     { display: inline-flex; align-items: center; justify-content: center;
                        min-width: 18px; height: 18px; padding: 0 6px; border-radius: 99px;
                        background: var(--bg); border: 1px solid var(--border);
                        color: var(--muted); font-size: .7rem; font-weight: 700; }
.db-exp-tab.active .db-exp-tab-count {
                        background: color-mix(in srgb, var(--primary) 18%, transparent); color: var(--primary);
                        border-color: color-mix(in srgb, var(--primary) 40%, transparent); }

.db-exp-tabpanel      { flex: 1; min-height: 0; display: flex; flex-direction: column;
                        padding-top: 8px; gap: 8px; }
.db-exp-tabpanel[hidden] { display: none; }

.db-exp-recs-toolbar       { display: flex; align-items: center; justify-content: space-between;
                             gap: 12px; flex-wrap: wrap; flex-shrink: 0; }
.db-exp-recs-toolbar-left  { display: flex; align-items: center; gap: 10px; }
.db-exp-recs-toolbar-right { display: flex; align-items: center; gap: 8px; }
.db-exp-recs-toolbar-right .search-wrap .search-input { width: 240px; }
.db-exp-limite-label       { display: inline-flex; align-items: center; gap: 8px;
                             font-size: .8rem; font-weight: 600; color: var(--muted); }
.db-exp-limite-label select{ padding: 6px 10px; font-size: .85rem; }

.db-exp-table-card           { overflow: auto; }
.db-exp-fill                 { flex: 1; min-height: 0; max-height: none; }
.db-exp-table-card thead tr  { background: var(--bg); position: sticky; top: 0; z-index: 1; }
.db-exp-table-card tbody tr.row-clickable       { cursor: pointer; }
.db-exp-table-card tbody tr.row-clickable:hover { background: var(--row-hover); }
.db-exp-table-card td        { vertical-align: top; }

.db-exp-nombre     { font-weight: 600; word-break: break-all; }
.db-exp-col-nombre { font-weight: 600; color: var(--text); word-break: break-all; }
.db-exp-coment     { font-size: .75rem; color: var(--muted); margin-top: 2px;
                     font-style: italic; }
.db-exp-num        { color: var(--muted); font-size: .82rem; font-family: monospace;
                     white-space: nowrap; text-align: right; }
.db-exp-mono       { font-family: monospace; font-size: .82rem; color: var(--text);
                     white-space: nowrap; }
.db-exp-recs-card td { font-family: monospace; font-size: .8rem;
                       max-width: 280px; overflow: hidden; text-overflow: ellipsis;
                       white-space: nowrap; }
.db-exp-recs-card td:hover { white-space: normal; word-break: break-all; }
.db-exp-null       { color: var(--muted); font-style: italic; font-family: monospace;
                     font-size: .78rem; }
.db-exp-pk-icon    { color: var(--warn); font-size: .75rem; margin-left: 4px; }

.db-exp-footer-info { font-size: .78rem; color: var(--muted);
                      display: flex; justify-content: space-between;
                      gap: 8px; flex-wrap: wrap; flex-shrink: 0; }
.db-exp-empty       { padding: 30px; text-align: center; color: var(--muted); }

.db-exp-env-dev  { background: #dcfce7; color: #16a34a; }
.db-exp-env-prod { background: #fef2f2; color: #dc2626; }

/* Edición inline de celdas */
.db-exp-cell-edit                 { cursor: text; }
.db-exp-cell-edit:hover           { outline: 1px solid var(--border); outline-offset: -1px;
                                    background: rgba(0,0,0,.03); }
.db-exp-cell-lock                 { cursor: not-allowed; color: var(--muted); }
.db-exp-cell-lock:hover           { background: transparent; }
.db-exp-cell-editing              { padding: 4px 6px !important; background: var(--bg);
                                    outline: 2px solid var(--primary); outline-offset: -2px;
                                    white-space: normal !important; max-width: none !important;
                                    overflow: visible !important; }
.db-exp-cell-saving               { opacity: .6; pointer-events: none; }
.db-exp-cell-ok                   { animation: dbExpCellOk .8s ease-out; }
@keyframes dbExpCellOk {
  0%   { background: rgba(34,197,94,.35); }
  100% { background: transparent; }
}
.db-exp-edit-wrap                 { display: flex; align-items: center; gap: 4px; }
.db-exp-edit-input                { flex: 1; min-width: 80px; padding: 4px 6px !important;
                                    font-family: monospace; font-size: .82rem !important;
                                    border-radius: 6px !important; }
.db-exp-edit-actions              { display: inline-flex; gap: 2px; flex-shrink: 0; }
.db-exp-edit-actions .btn-icon-sm { padding: 2px 6px; font-size: .8rem; }

@media (max-width: 640px) {
  .db-exp-table-card th:nth-child(4),
  .db-exp-table-card td:nth-child(4),
  .db-exp-table-card th:nth-child(7),
  .db-exp-table-card td:nth-child(7) { display: none; }
}

/* ---------- Modal ancho + editor SQL/JSON read-only ---------- */
.modal.modal-wide  { max-width: 1080px; width: calc(100% - 32px); }
.modal-subtitle    { font-size: .82rem; color: var(--muted); font-weight: 500; }

.json-editor {
  width: 100%;
  min-height: 360px;
  max-height: 60vh;
  resize: vertical;
  font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
  font-size: .82rem;
  line-height: 1.5;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 10px 12px;
  white-space: pre;
  overflow: auto;
}

/* Historial de mensajes de una conversación (pestaña "Mensajes"). */
.chat-log       { display: flex; flex-direction: column; gap: 10px;
                  max-height: 60vh; overflow-y: auto; padding-right: 4px; }
.chat-msg       { border-radius: var(--radius); padding: 10px 12px;
                  background: color-mix(in srgb, var(--surface) 90%, #000);
                  max-width: 85%; }
.chat-msg-meta  { display: flex; justify-content: space-between; align-items: baseline;
                  gap: 10px; font-size: .72rem; color: var(--muted);
                  margin-bottom: 4px; }
.chat-msg-role  { text-transform: uppercase; letter-spacing: .04em; font-weight: 700; }
.chat-msg-fecha { font-family: monospace; }
.chat-msg-body  { font-size: .9rem; white-space: pre-wrap; word-break: break-word; line-height: 1.45; }

.chat-msg-user      { align-self: flex-end;
                      background: color-mix(in srgb, var(--primary) 12%, var(--surface)); }
.chat-msg-user .chat-msg-role { color: var(--primary); }

.chat-msg-assistant { align-self: flex-start; }
.chat-msg-assistant .chat-msg-role { color: var(--success); }

.chat-msg-tool      { align-self: flex-start;
                      background: color-mix(in srgb, var(--warn) 15%, var(--surface));
                      font-family: monospace; font-size: .82rem; max-width: 95%; }
.chat-msg-tool .chat-msg-role { color: var(--warn); }

.chat-msg-system    { align-self: center; max-width: 95%;
                      background: color-mix(in srgb, var(--info) 12%, var(--surface));
                      font-style: italic; }
.chat-msg-system .chat-msg-role { color: var(--info); }

/* Contenedor de preview de imagen con acciones flotantes (subir/quitar).
   Usado por el ABM de Talonarios para el campo `fondo`. */
.fondo-box              { position: relative; border: 1px dashed var(--border);
                          border-radius: var(--radius); background: var(--bg);
                          min-height: 160px; padding: 12px;
                          display: flex; align-items: center; justify-content: center;
                          overflow: hidden; }
.fondo-box img          { display: block; max-width: 100%; max-height: 240px;
                          object-fit: contain; border-radius: 6px; }
.fondo-box-placeholder  { color: var(--muted); font-size: .85rem; }
.fondo-box-actions      { position: absolute; top: 8px; right: 8px;
                          display: flex; flex-direction: column; gap: 6px; z-index: 2; }
.btn-fondo-circle       { width: 32px; height: 32px; border-radius: 50%;
                          border: 1px solid var(--border); background: var(--surface);
                          color: var(--text); cursor: pointer;
                          display: inline-flex; align-items: center; justify-content: center;
                          font-size: .85rem; box-shadow: var(--shadow);
                          transition: background .15s, color .15s, transform .1s; }
.btn-fondo-circle:hover        { background: var(--primary); color: #fff; border-color: var(--primary); }
.btn-fondo-circle:active       { transform: scale(.94); }
.btn-fondo-circle:disabled     { opacity: .6; cursor: not-allowed; }
.btn-fondo-circle-danger:hover { background: var(--danger); color: #fff; border-color: var(--danger); }

/* ------------------------------------------------------------------
   Las DOS TINTAS del logotipo de una cuenta (DESIGN.md §8.2).
   Dos `.fondo-box` lado a lado, cada una sobre la superficie a la que
   su archivo está destinado.
   ------------------------------------------------------------------ */
.logo-dual        { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
/* `min-width: 0` no es opcional: sin eso un nombre largo en la nota
   infla la columna y las dos mitades dejan de ser mitades. */
.logo-slot        { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.logo-slot-titulo { font-size: .8rem; font-weight: 600; color: var(--muted); }
.logo-slot-nota   { font-size: .75rem; color: var(--muted); line-height: 1.35; }

/* El tablero de damas delata que el fondo quedó transparente, que es la
   mitad de lo que hay que mirar en este campo —la otra es si la tinta se
   lee contra su superficie—. Es un solo gris en alpha sobre dos bases
   distintas, así que no hacen falta dos juegos de color.
   Vive en las clases de SUPERFICIE y no en `.logo-box` porque lo
   comparten el recuadro del formulario y la miniatura del Consultar. */
.logo-box-oscuro,
.logo-box-claro  { border-style: solid;
                   background-image:
                     linear-gradient(45deg,  var(--damas) 25%, transparent 25%),
                     linear-gradient(-45deg, var(--damas) 25%, transparent 25%),
                     linear-gradient(45deg,  transparent 75%, var(--damas) 75%),
                     linear-gradient(-45deg, transparent 75%, var(--damas) 75%);
                   background-size: 16px 16px;
                   background-position: 0 0, 0 8px, 8px -8px, -8px 0; }
.logo-box-oscuro { --damas: rgba(255,255,255,.07);
                   background-color: var(--logo-oscuro-bg);
                   border-color: var(--logo-oscuro-borde); }
.logo-box-claro  { --damas: rgba(0,0,0,.07);
                   background-color: var(--logo-claro-bg);
                   border-color: var(--logo-claro-borde); }

.logo-box      { min-height: 150px; }
.logo-box img  { max-height: 150px; }

/* El placeholder y los botones viven ENCIMA de esas dos superficies, que
   no siguen al tema, así que tampoco pueden salir de `--muted` ni de
   `--surface`: sobre el recuadro oscuro en tema claro desaparecían. Misma
   excepción declarada que los controles sobre una foto. */
.logo-box-oscuro .fondo-box-placeholder { color: rgba(255,255,255,.45); }
.logo-box-claro  .fondo-box-placeholder { color: rgba(0,0,0,.40); }
.logo-box-oscuro .btn-fondo-circle { background: rgba(255,255,255,.12);
                                     border-color: rgba(255,255,255,.28); color: #fff; }
.logo-box-claro  .btn-fondo-circle { background: rgba(255,255,255,.92);
                                     border-color: rgba(0,0,0,.18); color: #1e293b; }

/* Mientras la varita trabaja, el recuadro se atenúa y no acepta clicks:
   la conversión va y vuelve del servidor y sin esto se puede disparar
   dos veces, que son dos objetos subidos al bucket para quedarse con uno. */
.logo-slot.generando .logo-box { opacity: .55; pointer-events: none; }

/* Apiladas, no comprimidas: a 150px de alto cada una, dos columnas en una
   ventana angosta dejan los dos logotipos ilegibles. */
@media (max-width: 640px) { .logo-dual { grid-template-columns: 1fr; } }

/* Las dos tintas en el modal de CONSULTAR. Misma superficie y mismo
   tablero que en el formulario —el blanco se sigue mirando sobre oscuro—
   pero en miniatura, porque acá el campo se lee junto a los demás y no
   se edita. */
.consult-logos      { display: flex; gap: 18px; flex-wrap: wrap; }
.consult-logo       { display: flex; align-items: center; gap: 10px; min-width: 0; }
.consult-logo-caja  { width: 64px; height: 64px; flex-shrink: 0;
                      border-radius: 8px; border-width: 1px;
                      display: inline-flex; align-items: center; justify-content: center;
                      padding: 5px; overflow: hidden; }
.consult-logo-caja img { max-width: 100%; max-height: 100%;
                         object-fit: contain; display: block; }
.consult-logo-datos { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.consult-logo-rotulo{ font-size: .75rem; font-weight: 600; color: var(--muted);
                      text-transform: uppercase; letter-spacing: .04em; }
.consult-logo-key   { font-family: monospace; font-size: .78rem; color: var(--muted);
                      word-break: break-all; }

/* ============================================================
   Impresión de Comprobantes (Presupuestos / Facturas / Recibos)
   Vista previa sobre el fondo `assets/img/fondo.jpg` (A4 vertical).
   Todas las posiciones son porcentajes relativos a `.print-page`
   porque el fondo se sirve con `background-size:100% 100%`.
   ============================================================ */

.print-modal .print-body { padding: 20px; background: #eef2f7; }

/* La hoja NO se deforma: lo que se ajusta es la escala.
   `.modal-body` es un flex en columna y el modal tiene `max-height: 90vh`,
   así que un hijo con alto fijo se COMPRIME para entrar (los ítems flex
   traen `flex-shrink: 1`). Comprimida, la hoja perdía las dos cosas que
   la vuelven una vista previa: el fondo —servido con
   `background-size: 100% 100%`— se achataba, y todo lo anclado en px
   (el bloque Subtotal/IVA/Total, en `top: 980px`) se caía abajo del
   `overflow: hidden` y desaparecía de la pantalla aunque saliera impreso.
   Por eso la hoja va adentro de este envoltorio: ella se queda clavada en
   210×297mm y acá se aplica el `scale` que calcula
   `ajustarEscalaVistaPreviaComprobante()` en cada apertura y resize. */
.print-page-scaler {
  flex: 0 0 auto;
  width: 210mm; height: 297mm;
  transform: scale(var(--pp-escala, 1));
  transform-origin: top center;
  margin: 0 auto;
  /* El `transform` no achica la caja en el layout: sin este margen
     negativo la hoja escalada dejaría abajo el hueco del alto original. */
  margin-bottom: calc((var(--pp-escala, 1) - 1) * 297mm);
}

.print-page {
  position: relative;
  width: 210mm;
  height: 297mm;
  margin: 0 auto;
  background: #fff url('../img/fondo.jpg') no-repeat center / 100% 100%;
  box-shadow: var(--shadow-lg);
  color: #111;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  font-size: 10pt;
  overflow: hidden;
}

/* Colores del comprobante: uso tokens de marca (--primary) y valores fijos
   dark-gray-on-white porque el PDF NO debe adaptarse a dark mode — siempre
   se imprime en papel blanco con tinta oscura. Se usa #333 como color
   principal (gris oscuro, nunca negro puro) y #555/#777 para acentos y
   metadata. Esto es una excepción deliberada a la regla "sin colores
   hardcodeados" de DESIGN.md, motivada por el requisito de impresión. */

/* Cuadro blanco de la esquina superior derecha del template:
   [X] TIPO_COMPROBANTE / Nº ... / bloque emisor.
   Ojo: cae encima del recuadro blanco de fondo.jpg; si se agranda invade la
   franja azul y el título queda ilegible. */
.pp-header-right {
  position: absolute;
  top: 2.2%; left: 50%; width: 39.5%; height: calc(14.8% - 25px);
  display: flex; flex-direction: column; justify-content: flex-start;
  color: #555;
  transform: translate(50px, -5px);
}
.pp-header-right .pp-hr-top    { display: flex; align-items: flex-start; gap: 5px; }
/* Contenedor separado para la letra fiscal del comprobante.
   Se posiciona arriba, centrado horizontalmente sobre la hoja A4. */
.pp-letra-block {
  position: absolute;
  top: 2.2%; left: 50%;
  transform: translate(-50%, -12px);
  width: 30px;
  display: flex; align-items: center; justify-content: center;
  color: #555;
}
.pp-letra-block .pp-letra {
  font-size: 22pt; font-weight: 800; line-height: 1;
  color: #555;
}
.pp-header-right .pp-hr-titles { display: flex; flex-direction: column; min-width: 0; }
.pp-header-right .pp-tipo      { font-size: 16pt; font-weight: 800; letter-spacing: .04em; color: #555; line-height: 1.05; margin-top: 5px; }
.pp-header-right .pp-nro       { font-size: 11pt; font-weight: 700; line-height: 1.15; margin-top: 6px; }
.pp-header-right .pp-emisor    { margin-top: 13px; }
.pp-header-right .pp-emisor-line { font-size: 8pt; line-height: 1.25; color: #7a7a7a; }

/* Segundo rectángulo del template: datos del cliente (izq) + fechas / id (der).
   Se coloca cerca del tope del recuadro blanco para que el margen superior
   quede equivalente al lateral, como en el PDF legacy.                    */
.pp-cliente {
  position: absolute;
  top: 18.7%; left: 3%; width: calc(94% - 5px); height: calc(12% - 45px);
  display: flex; align-items: flex-start; justify-content: space-between; gap: 20px;
  color: #555;
  transform: translateY(-8px);
}
.pp-cli-left,
.pp-cli-right              { display: flex; flex-direction: column; }
.pp-cli-right              { text-align: right; }
/* 8pt para alinearse con el bloque del emisor y con la tabla de
   ítems — el "encabezado" del comprobante (cliente a la izquierda,
   fechas / identificador / CAE a la derecha) queda tipográficamente
   uniforme con el resto. La familia la hereda de .print-page. */
.pp-cli-row                { font-size: 8pt; line-height: 1.35; color: #555; }
.pp-cli-row strong         { color: #555; }

/* Rectángulo grande: tabla de ítems + total al pie.
   La cabecera de la tabla va SIN fondo — la regla global
   `thead tr { background: var(--bg) }` (dark mode) se anula acá. */
.pp-items {
  position: absolute;
  top: 33.5%; left: 3%; width: calc(94% - 5px); height: 600px;
  overflow: hidden;
  display: flex; flex-direction: column;
  color: #555;
  transform: translateY(-45px);
}
.pp-items-table {
  /* Font-size 8pt para que la cabecera de la tabla (Cantidad / Detalle /
     Unitario / Monto) y las filas de líneas queden con el mismo tipo y
     tamaño de fuente que el bloque del emisor de arriba (razón social,
     condición IVA, CUIT, IIBB, inicio). La familia la heredan de
     .print-page. */
  width: 745px; table-layout: fixed; border-collapse: collapse; font-size: 8pt;
  background: transparent;
}
.pp-items-table thead tr {
  background: transparent !important;
}
.pp-items-table thead th {
  padding: 6px 8px; text-align: left;
  font-size: 8pt; font-weight: 700; color: #555;
  background: transparent !important;
  border-bottom: 1px solid transparent;
  text-transform: none;
  letter-spacing: 0;
}
.pp-items-table tbody tr           { background: transparent !important; }
.pp-items-table tbody tr:hover     { background: transparent !important; }
.pp-items-table tbody td {
  padding: 4px 8px; vertical-align: top; color: #555;
  background: transparent;
  border-bottom: none;
}
/* +10px de padding-bottom en la última fila para separar el contenido de la
   línea. El +10px del otro lado (entre línea y "Total: …") va en
   `.pp-total-row` con padding-top: 16px (= 6px original + 10px).
   La línea separadora se dibuja ahora vía `.pp-total-row::before` para
   poder correrla horizontalmente sin afectar el ancho de las celdas. */
.pp-items-table tbody tr:last-child td { padding-bottom: 14px; }
.pp-items-table .pp-c-cant    { width: 55px;  text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.pp-items-table .pp-c-desc    { width: 460px; text-align: justify; }
.pp-items-table .pp-c-pu      { width: 90px; text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.pp-items-table .pp-c-monto   { width: 90px; text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.pp-items-table thead .pp-c-cant  { text-align: left; }
.pp-items-table thead .pp-c-pu,
.pp-items-table thead .pp-c-monto { text-align: right; }

.pp-total-row {
  position: relative;
  display: flex; flex-direction: column; align-items: flex-end;
  gap: 3px;
  /* 8pt para alinear tipo y tamaño con emisor, cliente y tabla de
     ítems — Total y monto quedan tipográficamente uniformes con el
     resto del comprobante. */
  font-size: 8pt;
  color: #555;
}
.pp-total-line {
  display: flex; justify-content: space-between; gap: 24px;
  min-width: 240px;
}
.pp-total-line-final {
  border-top: 1px solid #7a7a7a;
  margin-top: 3px;
  padding-top: 4px;
  font-weight: 700;
}
.pp-total-row .pp-total-label { color: #555; }
.pp-total-row .pp-total-value { font-variant-numeric: tabular-nums; color: #555; }

/* Contenedor separado para el bloque Subtotal / IVA / Total.
   Posicionado en absoluto sobre el fondo del template. */
.pp-total-block {
  position: absolute;
  top: 980px; right: 3%;
  width: 260px;
}

/* Debug: teñir los contenedores de la vista previa + bordes en las
   celdas de la tabla y las columnas del cliente. Se activa/desactiva
   toggleando la clase `.pp-debug` sobre `.print-page` (atajo: F12
   dentro del modal de vista previa). */
.print-page.pp-debug .pp-letra-block,
.print-page.pp-debug .pp-header-right,
.print-page.pp-debug .pp-cliente,
.print-page.pp-debug .pp-items,
.print-page.pp-debug .pp-total-block,
.print-page.pp-debug .pp-totales {
  background-color: rgba(255, 235, 59, 0.4);
  border: 1px solid #ff9800;
}
.print-page.pp-debug .pp-cli-left,
.print-page.pp-debug .pp-cli-right,
.print-page.pp-debug .pp-items-table th,
.print-page.pp-debug .pp-items-table td {
  border: 1px solid #22c55e;
}
.pp-total-row .pp-total-line-final .pp-total-value { font-weight: 800; }

/* Cuadro inferior derecho del template: observaciones.
   margin-left/bottom desplazan el bloque en px exactos preservando el
   anclaje % al template. `overflow: hidden` clipea el texto que
   excede el recuadro blanco disponible. */
.pp-totales {
  position: absolute;
  top: 980px; left: 3%; width: calc(55% - 85px); height: calc(9% - 13px);
  display: flex; align-items: flex-start;
  color: #555;
  overflow: hidden;
}
/* 8pt para igualar el tamaño y la familia (heredada de .print-page)
   del resto del comprobante — emisor, cliente y tabla de ítems. */
.pp-totales .pp-observ-inline {
  font-size: 8pt; line-height: 1.4; color: #555;
  text-align: justify;
  overflow: hidden;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 6;
  line-clamp: 6;
}

/* En pantalla, el modal se adapta al ancho para que la hoja quede visible
   completa. El encogido lo hace `.print-page-scaler`, que mira tanto el
   ancho como el alto disponibles — acá sólo se le da más lugar. */
@media (max-width: 960px) {
  .print-modal { max-width: 96vw !important; }
}

/* ---------- Impresión real ----------
   Al imprimir montamos `body.printing-comprobante`. Debemos ocultar
   toda la app y dejar SOLO la `.print-page`, sin scrollbars ni
   márgenes que corran el contenido.                                 */
@media print {
  @page { size: A4 portrait; margin: 0; }

  html, body {
    width: 210mm !important; height: 297mm !important;
    margin: 0 !important; padding: 0 !important;
    background: #fff !important;
    overflow: hidden !important;
  }

  body.printing-comprobante > *:not(#imprimirComprobanteBackdrop) { display: none !important; }

  body.printing-comprobante #imprimirComprobanteBackdrop {
    position: static !important; inset: auto !important;
    background: #fff !important; opacity: 1 !important; pointer-events: auto !important;
    display: block !important;
    overflow: visible !important;
    width: 210mm !important; height: 297mm !important;
    padding: 0 !important; margin: 0 !important;
  }

  /* .modal viene con overflow-y:auto, max-height:90vh, margin:16px y transform. */
  body.printing-comprobante .print-modal {
    max-width: none !important; width: 210mm !important; height: 297mm !important;
    max-height: none !important; min-height: 0 !important;
    margin: 0 !important; padding: 0 !important;
    box-shadow: none !important; border: none !important; border-radius: 0 !important;
    background: #fff !important;
    transform: none !important;
    overflow: visible !important;
    display: block !important;
  }
  body.printing-comprobante .no-print { display: none !important; }
  body.printing-comprobante .print-body {
    padding: 0 !important; margin: 0 !important;
    background: #fff !important;
    overflow: visible !important;
    height: auto !important; max-height: none !important;
  }
  /* En papel no hay nada que encoger: la escala de pantalla se anula. */
  body.printing-comprobante .print-page-scaler {
    width: 210mm !important; height: 297mm !important;
    margin: 0 !important; transform: none !important;
  }
  body.printing-comprobante .print-page {
    width: 210mm !important; height: 297mm !important;
    margin: 0 !important; padding: 0 !important;
    box-shadow: none !important; border: none !important;
    overflow: hidden !important;
    page-break-after: avoid;
    page-break-inside: avoid;
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }
}

/* ============================================================
   Terminal en vivo (Programador de tareas → Log de ejecución)
   ============================================================ */
.terminal-live {
  background: #0d1117;
  color: #d1d9e0;
  font-family: 'JetBrains Mono', 'Consolas', 'Monaco', monospace;
  font-size: .82rem;
  line-height: 1.45;
  padding: 14px 16px;
  border-radius: var(--radius);
  border: 1px solid var(--border);
  min-height: 340px;
  max-height: 60vh;
  overflow-y: auto;
  white-space: pre-wrap;
  word-break: break-word;
  margin: 0;
}
#btnTerminalAutoscroll.active i { color: var(--primary); }

/* Niveles de una línea de log dentro de la consola ([OK] / [WARN] / [FAIL] /
   [SKIP]). Los colores van duros y no por token: la consola es oscura en los
   dos temas, así que un `var(--success)` calibrado contra el gris del modal
   se ensucia sobre el #0d1117. */
.terminal-live .ok   { color: #3fb950; }
.terminal-live .warn { color: #d29922; }
.terminal-live .fail { color: #f85149; }
.terminal-live .skip { color: #8b949e; }

/* Línea de estado al pie de una herramienta que corre un proceso largo:
   refleja en vivo la última línea del stream y al terminar, el resultado. */
.run-status      { font-size: .85rem; color: var(--muted); min-height: 1.2em; }
.run-status.info { color: var(--info); }
.run-status.ok   { color: var(--success); }
.run-status.err  { color: var(--danger); }

/* El bloque "Chrome paint" que vivía acá se eliminó: existía sólo porque
 * las secciones 3-5 pintaban el chrome con `--surface` / `--primary` y
 * había que ganarles por orden de fuente. Ahora esas secciones usan
 * directamente `--brand-*` y `--chrome-*` (como hace panel.css), así que
 * no hay nada que sobreescribir al final. Si volvés a necesitar un
 * override de chrome, arreglá la regla base en su sección. */

/* --------- Editor de campos de una prestación --------- */
/* Filas que el controlador agrega y saca desde el modal de Alta/Edición
 * de Prestaciones. Cada una define una pregunta que el portal le hace a
 * quien contrata el servicio.
 *
 * No es una tabla: son cajas apiladas, porque cada fila tiene cuatro
 * controles de anchos muy distintos y en una tabla el más angosto manda
 * sobre toda la columna. */
.campos-editor      { display: flex; flex-direction: column; gap: 10px; }
.campos-editor-vacio{ font-size: .8rem; color: var(--muted); padding: 6px 0; }
/* Un escalón más oscuro que el fondo del modal, igual que las tarjetas de
 * la ficha: separa la fila sin dibujarle un borde. */
.campo-fila {
  background: color-mix(in srgb, var(--surface) 92%, #000);
  border-radius: var(--radius);
  padding: 10px 12px;
  display: flex; flex-direction: column; gap: 8px;
}
/* Etiqueta ancha + tipo angosto: la etiqueta es lo que se lee y el tipo
 * es una palabra de un catálogo cerrado. */
.campo-fila-top     { display: grid; grid-template-columns: 1fr 140px auto; gap: 8px; align-items: center; }
.campo-fila-bottom  { display: grid; grid-template-columns: 1fr auto; gap: 8px; align-items: center; }
.campo-fila input,
.campo-fila select,
.campo-fila textarea { width: 100%; font-size: .82rem; padding: 6px 10px; }
/* El check de obligatorio, inline con su palabra. */
.campo-obligatorio  { display: inline-flex; align-items: center; gap: 6px;
                      font-size: .8rem; color: var(--muted); white-space: nowrap;
                      cursor: pointer; }
.campo-obligatorio input { width: auto; }
@media (max-width: 768px) {
  .campo-fila-top    { grid-template-columns: 1fr; }
  .campo-fila-bottom { grid-template-columns: 1fr; }
}

/* --------- Markdown renderizado --------- */
/* Hoja del texto que devuelve `marked`. Es genérica a propósito: hoy la
 * usa el módulo Proyecto (PRODUCT.md), pero cualquier otro lugar que
 * muestre Markdown largo tiene que poder envolverlo en `.md-body` y
 * quedar igual. Todo sale de los tokens; no hay un solo color suelto.
 *
 * Los tamaños bajan respecto de los defaults del navegador: un <h1> de
 * 2em al lado de la topbar de 1rem descoloca la jerarquía de la pantalla
 * entera. */
.md-body            { font-size: .92rem; line-height: 1.65; color: var(--text);
                      overflow-wrap: break-word; }
.md-body > :first-child { margin-top: 0; }
.md-body > :last-child  { margin-bottom: 0; }

.md-body h1, .md-body h2, .md-body h3,
.md-body h4, .md-body h5, .md-body h6 {
  font-weight: 700; line-height: 1.3; color: var(--text);
  margin: 1.6em 0 .6em; scroll-margin-top: 16px;
}
/* Los dos primeros niveles llevan filete: en un documento de varias
 * pantallas el salto de tamaño solo no alcanza para ver dónde arranca un
 * capítulo. */
.md-body h1         { font-size: 1.45rem; padding-bottom: .3em;
                      border-bottom: 2px solid var(--border); }
.md-body h2         { font-size: 1.2rem;  padding-bottom: .25em;
                      border-bottom: 1px solid var(--border); }
.md-body h3         { font-size: 1.02rem; }
.md-body h4, .md-body h5, .md-body h6 { font-size: .92rem; color: var(--muted); }

.md-body p          { margin: 0 0 .9em; }
.md-body a          { color: var(--primary); text-decoration: underline;
                      text-underline-offset: 2px; }
.md-body a:hover    { text-decoration-thickness: 2px; }
.md-body strong     { font-weight: 700; }
.md-body hr         { border: none; border-top: 1px solid var(--border); margin: 1.8em 0; }
.md-body img        { max-width: 100%; height: auto; border-radius: var(--radius); }

.md-body ul, .md-body ol { margin: 0 0 .9em; padding-left: 1.4em; }
.md-body li         { margin: .25em 0; }
.md-body li > ul, .md-body li > ol { margin: .25em 0; }
/* Checklists de GFM (`- [ ]`). El check va pegado al texto y sin viñeta. */
.md-body li:has(> input[type=checkbox]) { list-style: none; margin-left: -1.2em; }
.md-body li > input[type=checkbox]      { width: auto; margin-right: .5em; vertical-align: middle; }

.md-body blockquote {
  margin: 0 0 .9em; padding: .1em 0 .1em 14px;
  border-left: 3px solid var(--primary); color: var(--muted);
}

/* Código. El bloque se distingue del cuerpo por fondo, no por borde, igual
 * que las tarjetas de la ficha. Scrollea en su propia caja: una línea larga
 * no puede estirar el ancho de la pantalla. */
.md-body code       { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
                      font-size: .85em; background: color-mix(in srgb, var(--surface) 88%, #000);
                      border-radius: 5px; padding: .15em .4em; }
.md-body pre        { background: color-mix(in srgb, var(--surface) 88%, #000);
                      border-radius: var(--radius); padding: 12px 14px;
                      margin: 0 0 .9em; overflow-x: auto; }
.md-body pre code   { background: none; padding: 0; font-size: .82rem; line-height: 1.5; }

/* Tablas. Mismo lenguaje que `.table-card` (§10) pero sin la tarjeta: acá
 * ya estamos adentro de una. */
.md-body table      { width: 100%; border-collapse: collapse; font-size: .86rem;
                      margin: 0 0 .9em; }
.md-body table th   { background: var(--bg); text-align: left; font-weight: 600;
                      color: var(--muted); font-size: .78rem;
                      text-transform: uppercase; letter-spacing: .04em; }
.md-body table th,
.md-body table td   { padding: 8px 12px; border: 1px solid var(--border); vertical-align: top; }
/* Envoltorio que le pone el render a cada tabla: una tabla ancha scrollea
 * sola en vez de ensanchar la columna del texto. */
.md-tabla-scroll    { overflow-x: auto; margin: 0 0 .9em; }
.md-tabla-scroll table { margin: 0; }

/* Diagramas Mermaid ya dibujados (el <pre> se reemplaza por el SVG). */
.md-body .md-mermaid      { background: var(--bg); border-radius: var(--radius);
                            padding: 14px; margin: 0 0 .9em; overflow-x: auto;
                            text-align: center; }
.md-body .md-mermaid svg  { max-width: 100%; height: auto; }
/* Red de contención para los diagramas que igual usan `<foreignObject>`
 * (las notas de un `sequenceDiagram`, por ejemplo): esas cajas tienen alto
 * fijo, medido por mermaid FUERA de `.md-body`, y todo lo que la hoja del
 * documento les herede acá las desborda por abajo. Se devuelve la herencia
 * a lo que mermaid asumió al medir. Los nodos de los flowcharts ya no
 * pasan por acá — van en texto SVG nativo (ver proyectoDibujarMermaid()). */
.md-body .md-mermaid foreignObject,
.md-body .md-mermaid foreignObject * { line-height: normal; }

/* --------- Módulo Proyecto (visor/editor de PRODUCT.md) --------- */
/* No es un ABM: la pantalla ES el documento. Por eso no hay listado ni
 * modales, y las dos vistas —lectura y edición— son dos layouts del mismo
 * texto. */

/* `.table-card` trae `overflow-y: hidden`, que convertiría a la tarjeta en
 * el contenedor de scroll del índice y le rompería el `position: sticky`.
 * Acá el que scrollea es `.content`, como en el resto del back office. */
.proyecto-card      { overflow: visible; }

.proyecto-doc       { display: grid; grid-template-columns: minmax(0, 1fr) 240px; }
.proyecto-doc .md-body { padding: 26px 30px; min-width: 0; }

/* Índice armado con los encabezados del propio documento. Queda a la vista
 * mientras se baja: son 300 líneas y trece capítulos. */
.proyecto-toc       { border-left: 1px solid var(--border); padding: 22px 18px;
                      position: sticky; top: 0; align-self: start;
                      max-height: calc(100vh - 92px); overflow-y: auto; }
.proyecto-toc-titulo{ font-size: .72rem; font-weight: 700; text-transform: uppercase;
                      letter-spacing: .08em; color: var(--muted); margin-bottom: 10px; }
.proyecto-toc a     { display: block; padding: 4px 8px; border-radius: 6px;
                      font-size: .8rem; color: var(--muted); text-decoration: none;
                      border-left: 2px solid transparent;
                      overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.proyecto-toc a:hover { background: var(--row-hover); color: var(--text); }
.proyecto-toc a.nivel-3 { padding-left: 20px; font-size: .76rem; }
/* El capítulo que se está leyendo, marcado igual que el ítem activo del
 * sidebar: lavado + filete, no relleno. */
.proyecto-toc a.activo { color: var(--text); font-weight: 600;
                         background: color-mix(in srgb, var(--primary) 12%, transparent);
                         border-left-color: var(--primary); }

/* Edición: fuente y vista previa lado a lado, cada una en su tarjeta y con
 * su propio scroll. La altura sale de la ventana para que el editor ocupe
 * lo que queda y no crezca con el documento. */
.proyecto-editor    { display: grid; grid-template-columns: 1fr 1fr; gap: 16px;
                      height: calc(100vh - 300px); min-height: 420px; }
.proyecto-editor .proyecto-card { display: flex; flex-direction: column; min-height: 0; }
.proyecto-pane-titulo { padding: 10px 14px; border-bottom: 1px solid var(--border);
                        font-size: .8rem; font-weight: 600; color: var(--muted);
                        display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }
.proyecto-pane-hint { margin-left: auto; font-weight: 400; font-size: .74rem; opacity: .8; }
/* El textarea es la superficie entera del panel: sin borde ni radio propios,
 * los pone la tarjeta. */
.proyecto-textarea  { flex: 1 1 auto; min-height: 0; width: 100%; resize: none;
                      border: none; border-radius: 0 0 var(--radius) var(--radius);
                      padding: 16px 18px; background: var(--surface); color: var(--text);
                      font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
                      font-size: .84rem; line-height: 1.6; tab-size: 2; }
.proyecto-textarea:focus { outline: none; box-shadow: none; border: none; }
.proyecto-preview   { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 20px 22px; }

/* Estado vacío: el documento no está montado (producción) o no se pudo
 * leer. Ocupa el lugar del texto y explica por qué, en vez de dejar la
 * tarjeta en blanco. */
/* Lo comparte el módulo Documentación, que tiene el mismo problema (una
 * tarjeta que iba a mostrar un documento y no lo tiene) y por lo tanto la
 * misma respuesta. Es el estado vacío de un VISOR DE DOCUMENTO, no de un
 * listado: por eso no es `.table-empty`. */
.proyecto-vacio,
.docs-vacio         { text-align: center; padding: 56px 28px; color: var(--muted); }
.proyecto-vacio i,
.docs-vacio i       { font-size: 2.2rem; color: var(--border); display: block; margin-bottom: 14px; }
.proyecto-vacio-msg,
.docs-vacio-msg     { font-size: .88rem; line-height: 1.55; max-width: 520px; margin: 0 auto; }
.proyecto-vacio code,
.docs-vacio code    { font-size: .82rem; }

@media (max-width: 1100px) {
  /* El índice es lo primero que sobra cuando el ancho aprieta: el documento
   * se sigue leyendo sin él. */
  .proyecto-doc     { grid-template-columns: 1fr; }
  .proyecto-toc     { display: none; }
  .proyecto-editor  { grid-template-columns: 1fr; height: auto; }
  .proyecto-editor .proyecto-card { height: 60vh; }
}
@media (max-width: 768px) {
  .proyecto-doc .md-body { padding: 18px 16px; }
}

/* --------- Módulo Documentación (Sistema → Documentación) --------- */
/* Lector de los `.md` de los microservicios de `api/v4/`. Tampoco es un ABM:
 * lo que se navega son archivos del repo, no filas de una tabla, así que no
 * hay listado con Código/Acciones ni modales.
 *
 * La pantalla se parte en dos —árbol a la izquierda, documento a la derecha—,
 * que es la forma de cualquier sitio de documentación. El índice del documento
 * abierto NO es una tercera columna: cuelga de su ítem en el árbol, así la
 * columna izquierda va de lo grueso (servicio) a lo fino (capítulo) con el
 * ancho que ya tenía. */

.docs-split         { display: grid; grid-template-columns: 280px minmax(0, 1fr);
                      gap: 20px; align-items: start; }

/* El árbol queda a la vista mientras se baja por el documento, igual que el
 * índice del módulo Proyecto. El que scrollea es `.content`; esta columna se
 * pega a su borde superior y scrollea aparte cuando no entra. */
.docs-nav           { position: sticky; top: 0; align-self: start;
                      max-height: calc(100vh - 92px); overflow-y: auto;
                      padding: 10px 0; }

.docs-servicio      { padding: 4px 0 8px; }
.docs-servicio + .docs-servicio { border-top: 1px solid var(--border); }

/* El servicio es un ENCABEZADO DE SECCIÓN, no un destino: no se puede abrir
 * "el servicio", se abren sus documentos. Por eso va en versalitas chicas,
 * como las categorías del sidebar (§4) — y como ellas, el click lo PLIEGA en
 * vez de navegar. */
.docs-servicio-head { display: flex; align-items: center; gap: 8px;
                      padding: 8px 14px 6px; cursor: pointer;
                      text-decoration: none; user-select: none;
                      font-size: .72rem; font-weight: 700; text-transform: uppercase;
                      letter-spacing: .08em; color: var(--muted); }
.docs-servicio-head:hover { color: var(--text); }
.docs-servicio-head i { color: var(--primary); font-size: .85rem; }

/* Flecha de plegado. Apunta hacia abajo cuando está abierto y gira a la
 * derecha cuando está cerrado — la convención de cualquier árbol de archivos.
 * El sidebar usa un `+` que rota 45°; acá la flecha dice además HACIA DÓNDE se
 * abre, que en una lista anidada es lo que se está preguntando. */
/* Las tres van juntas para ganarle por especificidad a `.docs-item.activo i`,
 * que si no le pinta el glifo en `--primary` y lo agranda. Sin `!important`:
 * el conflicto se resuelve con un selector más específico, no tapándolo. */
.docs-servicio-head .docs-caret,
.docs-item .docs-caret,
.docs-item.activo .docs-caret { margin-left: 6px; flex: 0 0 auto; align-self: center;
                                font-size: .65rem; color: var(--muted);
                                transition: transform .18s; }
.docs-caret.plegado { transform: rotate(-90deg); }
.docs-servicio-head:hover .docs-caret,
.docs-item:hover .docs-caret  { color: var(--text); }
.docs-servicio-base { margin-left: auto; font-weight: 400; text-transform: none;
                      letter-spacing: 0; font-size: .7rem; opacity: .75;
                      font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
                      overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Un documento del árbol. Se marca igual que el ítem activo del sidebar:
 * lavado + filete de 3px, nunca relleno sólido (regla dura #3). */
.docs-item          { display: flex; align-items: baseline; gap: 8px;
                      padding: 7px 14px; cursor: pointer; text-decoration: none;
                      border-left: 3px solid transparent;
                      transition: background .15s, color .15s; }
.docs-item:hover    { background: var(--row-hover); }
.docs-item.activo   { background: color-mix(in srgb, var(--primary) 12%, transparent);
                      border-left-color: var(--primary); }
.docs-item i        { color: var(--muted); font-size: .8rem; flex: 0 0 auto; }
.docs-item.activo i { color: var(--primary); }
.docs-item-nombre   { font-size: .83rem; color: var(--text); min-width: 0;
                      overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.docs-item.activo .docs-item-nombre { font-weight: 600; }
.docs-item-url      { margin-left: auto; font-size: .7rem; color: var(--muted);
                      font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
                      overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Índice del documento abierto, colgando de su ítem. Es el mismo rol que
 * `.proyecto-toc` pero adentro del árbol, así que hereda su escala y su marca
 * de capítulo activo. */
.docs-toc           { padding: 2px 0 6px; }
.docs-toc a         { display: block; padding: 4px 8px 4px 34px; font-size: .78rem;
                      color: var(--muted); text-decoration: none;
                      border-left: 3px solid transparent;
                      overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.docs-toc a:hover   { background: var(--row-hover); color: var(--text); }
.docs-toc a.nivel-3 { padding-left: 46px; font-size: .74rem; }
.docs-toc a.activo  { color: var(--text); font-weight: 600;
                      border-left-color: var(--primary); }

/* Franja de identificación del documento abierto: de qué archivo es, de dónde
 * sale y a qué URL pública corresponde. Va arriba del texto y no adentro,
 * porque es metadato del archivo y no parte del documento. */
.docs-doc-head      { display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
                      padding: 12px 18px; border-bottom: 1px solid var(--border);
                      background: color-mix(in srgb, var(--surface) 40%, var(--bg)); }
.docs-doc-titulo    { font-size: .95rem; font-weight: 700; color: var(--text);
                      overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.docs-doc-meta      { font-size: .76rem; color: var(--muted); margin-top: 2px;
                      font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
.docs-doc-acciones  { margin-left: auto; display: flex; gap: 8px; flex-wrap: wrap; }

.docs-doc           { padding: 24px 30px; min-width: 0; }

/* Una referencia a un archivo del REPO (`../_lib/s3.php`, un `.sql` de
 * migraciones). Existe en el documento como link, pero acá no lleva a ningún
 * lado: el visor abre `.md`, no código. Se deja visible y legible —la ruta es
 * información— y se marca como no navegable con el punteado, en vez de
 * desarmar el link y perder el dato. */
.docs-ref           { color: var(--muted) !important; cursor: default;
                      text-decoration: none !important;
                      border-bottom: 1px dashed var(--border); }

@media (max-width: 1100px) {
  /* El árbol pasa arriba del documento y deja de ser pegajoso: en una pantalla
   * angosta dos columnas de 280px + texto no entran, y el documento es lo que
   * se vino a leer. */
  .docs-split       { grid-template-columns: 1fr; }
  .docs-nav         { position: static; max-height: 320px; }
}
@media (max-width: 768px) {
  .docs-doc         { padding: 18px 16px; }
}

/* ============================================================
   MÓDULO: Objetivos (Inicio → Objetivos)
   ============================================================

   Los pendientes de los controladores. Es el único módulo del back
   office que NO es un listado a lo ancho: la pantalla se parte en dos
   —la lista a la izquierda, el calendario del mes a la derecha— porque
   las dos vistas responden preguntas distintas sobre la misma tabla
   ("qué tengo que hacer" y "cuándo cae"), y mirarlas juntas es lo que
   justifica el módulo. Un `<table>` de ABM en media pantalla queda
   ilegible, así que la lista son filas-tarjeta (`.obj-item`); todo lo
   demás —ayuda, stats-bar, toolbar, modal de filtros, Consultar,
   Alta/Edición, menú contextual— sigue `ABM.md` sin recortes.

   Los dos paneles scrollean por dentro y no estiran la página: el
   calendario tiene que quedar a la vista mientras se recorre la lista,
   que es justamente para lo que están lado a lado. */

.obj-split        { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
                    gap: 20px; align-items: start; }

.obj-panel        { background: var(--surface); border: 1px solid var(--border);
                    border-radius: var(--radius); box-shadow: var(--shadow);
                    display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
.obj-panel-header { padding: 12px 16px; border-bottom: 1px solid var(--border);
                    display: flex; align-items: center; gap: 10px; flex: 0 0 auto;
                    font-size: .9rem; font-weight: 600; }
.obj-panel-header .obj-panel-sub { margin-left: auto; font-size: .78rem;
                    font-weight: 400; color: var(--muted); }

/* ---------- La lista ---------- */

/* El alto sale de la ventana y no del contenido: con 200 objetivos la
   página crecería hasta dejar el calendario fuera de pantalla. */
.obj-lista        { overflow-y: auto; max-height: calc(100vh - 330px); min-height: 280px; }

.obj-item         { display: flex; align-items: flex-start; gap: 10px;
                    padding: 11px 14px 11px 11px;
                    border-bottom: 1px solid var(--border);
                    border-left: 3px solid transparent; cursor: pointer;
                    transition: background .15s; }
.obj-item:last-child { border-bottom: none; }
.obj-item:hover   { background: var(--row-hover); }

/* La prioridad es la barra de la izquierda. Es la señal que se puede
   leer de reojo recorriendo la columna, sin ocupar una celda. */
.obj-item.prio-alta  { border-left-color: var(--danger); }
.obj-item.prio-media { border-left-color: var(--warn); }
.obj-item.prio-baja  { border-left-color: var(--border); }

/* Cerrado: se ve que estuvo, pero deja de competir por la atención. */
.obj-item.cerrado                  { opacity: .55; }
.obj-item.cerrado .obj-item-titulo { text-decoration: line-through; }

/* El check es un botón y no un `input[type=checkbox]`: tiene que poder
   mostrar cuatro estados (pendiente, en curso, hecho, cancelado) y el
   nativo sólo tiene dos. */
.obj-check        { flex: 0 0 auto; margin-top: 1px; width: 20px; height: 20px;
                    border-radius: 50%; border: 2px solid var(--border);
                    background: none; color: var(--muted); cursor: pointer;
                    display: inline-flex; align-items: center; justify-content: center;
                    font-size: .62rem; padding: 0;
                    transition: border-color .15s, background .15s, color .15s; }
.obj-check:hover     { border-color: var(--success); color: var(--success); }
.obj-check.hecho     { background: var(--success); border-color: var(--success); color: #fff; }
.obj-check.curso     { border-color: var(--info);   color: var(--info); }
.obj-check.cancelado { border-color: var(--danger); color: var(--danger); }

.obj-item-main    { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.obj-item-titulo  { font-size: .88rem; font-weight: 600; line-height: 1.35;
                    word-break: break-word; }
.obj-item-meta    { display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
                    font-size: .76rem; color: var(--muted); }
.obj-item-meta i  { margin-right: 4px; }
/* Un pendiente con fecha pasada. El color es la única forma de que se
   note sin leer la fecha y compararla con hoy mentalmente. */
.obj-meta-vencido { color: var(--danger); font-weight: 600; }
.obj-meta-hoy     { color: var(--warn);   font-weight: 600; }

.obj-item-acciones { flex: 0 0 auto; }

/* ---------- El calendario ---------- */

.obj-cal          { padding: 12px 14px 14px; }
.obj-cal-nav      { display: flex; align-items: center; gap: 6px; margin-bottom: 10px; }
/* `toLocaleDateString` devuelve el mes en minúscula ("octubre de 2026"),
   así que sólo hay que levantar la primera letra. `text-transform:
   capitalize` NO sirve: toca cada palabra y deja "Octubre De 2026". */
.obj-cal-mes      { flex: 1; text-align: center; font-size: .9rem; font-weight: 600; }
.obj-cal-mes::first-letter { text-transform: uppercase; }

.obj-cal-grid     { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; }
.obj-cal-dow      { text-align: center; font-size: .68rem; font-weight: 700;
                    text-transform: uppercase; letter-spacing: .06em;
                    color: var(--muted); padding: 2px 0 6px; }

/* La celda es un botón: se abre con click y con Enter, y el foco se ve.
   `min-height` y no `height` — un día con tres objetivos crece y arrastra
   a toda su fila, que es lo correcto: las semanas se leen en renglones. */
.obj-cal-day      { min-height: 74px; border-radius: 8px; padding: 4px 5px;
                    border: 1px solid var(--border); background: var(--surface);
                    color: var(--text); cursor: pointer; text-align: left;
                    display: flex; flex-direction: column; gap: 3px; overflow: hidden;
                    font-family: inherit; transition: background .15s, border-color .15s; }
.obj-cal-day:hover { background: var(--row-hover); border-color: var(--primary); }
/* Los días de relleno del mes anterior y el siguiente. Siguen siendo
   clickeables —dar de alta el 1° del mes que viene desde acá es natural—
   pero se apagan para que el mes en curso se lea como un bloque. */
.obj-cal-day.otro-mes      { opacity: .4; }
.obj-cal-day.hoy           { border-color: var(--primary); border-width: 2px; padding: 3px 4px; }
.obj-cal-day.sel           { background: color-mix(in srgb, var(--primary) 18%, transparent);
                             border-color: var(--primary); }
/* Un día con pendientes vencidos. El tinte es del día entero porque la
   pregunta que contesta el calendario es "qué días están en rojo". */
.obj-cal-day.vencido       { background: color-mix(in srgb, var(--danger) 14%, transparent);
                             border-color: color-mix(in srgb, var(--danger) 45%, transparent); }
.obj-cal-day.vencido:hover { background: color-mix(in srgb, var(--danger) 22%, transparent); }

.obj-cal-num      { font-size: .76rem; font-weight: 600; display: flex;
                    align-items: center; gap: 4px; flex: 0 0 auto; }
.obj-cal-day.hoy .obj-cal-num { color: var(--primary); }
/* Punto de urgencia: el día tiene algo abierto en prioridad alta. */
.obj-cal-urgente  { width: 6px; height: 6px; border-radius: 50%;
                    background: var(--danger); margin-left: auto; flex: 0 0 auto; }

.obj-cal-chip     { font-size: .66rem; line-height: 1.25; border-radius: 4px;
                    padding: 1px 4px; color: var(--text);
                    background: color-mix(in srgb, var(--surface) 82%, #000);
                    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.obj-cal-mas      { font-size: .64rem; color: var(--muted); padding-left: 4px; }

/* ---------- Estado vacío ---------- */

.obj-vacio        { text-align: center; padding: 46px 24px; color: var(--muted); font-size: .88rem; }
.obj-vacio i      { font-size: 2rem; color: var(--border); display: block; margin-bottom: 12px; }

/* ---------- Responsive ---------- */

/* Por debajo de 1100px las dos mitades no entran: el calendario queda
   tan angosto que los chips de título no se leen. Se apilan, con la
   lista arriba — es la vista con la que se trabaja. */
@media (max-width: 1100px) {
  .obj-split { grid-template-columns: 1fr; }
  .obj-lista { max-height: 60vh; }
}
@media (max-width: 560px) {
  /* En teléfono la celda sólo muestra el número y el punto: los chips
     de título necesitan un ancho que acá no hay. Los títulos se ven
     igual al tocar el día. */
  .obj-cal-day { min-height: 44px; }
  .obj-cal-chip,
  .obj-cal-mas { display: none; }
}
