/* Controly Micro — capa de diseño
   Tokens y componentes traducidos del mockup de Claude Design.
   Los tokens se definen por ROL, no por color: el tema oscuro reasigna valores sin renombrar nada.

   ── UN CIERRE DE COMENTARIO DE MÁS MATA LA REGLA DE ABAJO, EN SILENCIO ──
   (2026-08-29.) Este archivo es mitad prosa, y esa es su virtud y su trampa: al
   reordenar un bloque de comentario es fácil dejar un cierre suelto, y entonces
   el texto que sigue se lee como CSS, el parser descarta hasta poder recobrarse
   y **se lleva la regla siguiente**. No hay error en consola ni en la pestaña
   de red: la pieza simplemente hereda del cuerpo (16px, tinta) como si nadie la
   hubiera estilado. Pasó con `.panel-titulo .medida, .panel-titulo .periodo` y
   se descubrió mirando la pantalla, no el código.

   POR ESO ESTE AVISO NO PUEDE TECLEAR LOS DELIMITADORES, ni en prosa: la
   primera versión los nombraba entre acentos graves para explicarse y con eso
   se cerró a sí misma a la altura del tercer párrafo, dejando fuera del
   comentario todo lo que sigue — incluido el bloque de tokens. Se cazó al
   instante con el detector de abajo, que es exactamente para lo que existe.

   Detección, desde la raíz de `crm/` y cuesta un segundo — las dos cifras
   deben salir en 0 (borra los comentarios balanceados y cuenta lo que sobra):

     python3 -c "import re;a=chr(47)+chr(42);b=chr(42)+chr(47);\
     s=open('public_html/activos/app.css',encoding='utf-8').read();\
     t=re.sub(re.escape(a)+'.*?'+re.escape(b),'',s,flags=re.S);print(t.count(b),t.count(a))"

   Los delimitadores se arman con `chr()` y no se teclean, y eso NO es coquetería:
   escrito con el literal, el comando se cuenta a sí mismo y el detector reporta
   2 huérfanos para siempre — un check que grita en verde es tan inútil como no
   tenerlo. Se descubrió al estrenarlo, aquí mismo.

   Y la comprobación que de verdad cierra el caso es medir la pieza en el
   navegador (`getComputedStyle`), no releer el selector: la regla estaba
   escrita y correcta — lo que fallaba era que llegara. */

/* ── Tokens ─────────────────────────────────────────────────────────── */
:root{
  --app-bg:#ffffff;
  --doc-bg:#f1efec;
  /* EL GRIS DE LA CASA — y es el de la BARRA DE ICONOS de Controly, no el de
     su lienzo. Desde el 2026-08-26 vale `#F0EEE6`, que es lo que pinta la
     `.iconav` de Controly (`.bg-nav` de su `app-tema.css`, medido en vivo
     `rgb(240,238,230)`); antes era un gris frío propio, `#f6f6f7`. El criterio
     es de marca: las dos apps son la misma casa y la barra lateral es lo
     primero que se ve al entrar, así que comparte superficie con su hermana.
     Es un beige CÁLIDO, no un gris neutro — no se puede sustituir «por otro
     parecido».

     NO confundirlo con el fondo principal de Controly (`.bg-principal`,
     `#FAF9F5`): ése es su lienzo y aquí el lienzo es `--app-bg` (blanco).
     Se probó primero con el `#FAF9F5` y era el color equivocado.

     SÓLO CAMBIÓ EL CLARO. El oscuro (#252528) se queda: allá la relación
     barra/papel ya funcionaba y Controly nocturno usa otro valor.

     Lo pintan NUEVE superficies, no sólo la barra: el riel, la cabecera de la
     sábana, el peek, los paneles y la puerta del login. Es un token de ROL
     —«esto rotula, no es contenido»— y por eso se mueven todas juntas.

     ES MÁS OSCURO QUE EL GRIS QUE SUSTITUYE Y ESO CUESTA CONTRASTE, así que
     dos tintas se recalibraron en el mismo cambio o habrían caído bajo el
     mínimo de 4.5:1 sobre esta superficie — el detalle, en cada una. Lo que
     GANA es la separación contra el papel blanco: 1.080 → 1.162. */
  --app-sidebar-bg:#F0EEE6;
  /* ── LA SUPERFICIE SECUNDARIA ──────────────────────────────────────────
     El segundo gris del sistema. Lo lleva TODO lo que acompaña al contenido
     sin ser contenido y sin ser la barra: el riel del cascarón (`.app-lado`),
     las dos cabeceras de tabla (la sábana y los catálogos), las columnas del
     tablero y los tiles de KPI.

     ── LA ESCALERA, QUE ES DE LO QUE VA ESTE TOKEN ────────────────────────
     Hasta el 2026-08-26 la app tenía DOS grises y uno de ellos hacía dos
     trabajos: `--app-sidebar-bg` pintaba la barra Y todo lo demás, así que
     una cabecera de tabla pesaba exactamente lo mismo que la navegación.
     Hoy son tres escalones — barra (manda) · secundario (acompaña) · papel
     (el contenido) — y cada superficie está en el suyo.

     Se escribe como MEZCLA y no como hex para que sea la definición y no una
     copia: el día que la barra cambie de tono, esto se mueve con ella sin que
     nadie recalcule nada — que es justo lo que se paga cuando dos grises
     emparentados se escriben a mano por separado. Y sale OPACO, que no es un
     detalle: la cabecera de la sábana congela sus dos primeras celdas al
     desplazarse en horizontal y tiene que tapar lo que le pasa por debajo.
     `color-mix` ya se usaba en la hoja (`--ctl-bg-solido` del tema oscuro).

     40% Y NO 50%. Nació a medio camino exacto (1.077:1 contra el papel y
     1.079 contra la barra — el reparto perfecto) y Arif lo pidió «un tono más
     hacia el blanco» el mismo día. A 40 resuelve a rgb(249,248,245): 1.061:1
     contra el papel y 1.095 contra la barra. Ya no reparte a la mitad, y es
     lo correcto — esto ACOMPAÑA, así que se parece más al papel que a la
     barra, que es la que manda. Sigue siendo un tinte legible: el gris que la
     barra tuvo hasta el 2026-08-26 (#f6f6f7) daba 1.080 contra el blanco, o
     sea del mismo orden.

     Y NO cuesta contraste, lo regala: al aclararse, todo lo que se escribe
     encima sube (`--app-fg` 14.65→16.04, el secundario 6.36→6.97, y tanto el
     terciario como el acento cruzan el 5.00 desde 4.57 y 4.56).

     SE LLAMÓ `--app-lado-bg` DURANTE UNA HORA, y se renombró al ganar su
     segundo consumidor. Es la lección que esta hoja ya tenía escrita dos
     veces (`--btn-radio` → `--r-pill`, `--fs-peek` → `--fs-destacado`): un
     token con nombre de UN consumidor deja de reconocerse en cuanto lo usa
     otro, y quien lo necesita acaba escribiendo el literal. */
  --app-secundario-bg:color-mix(in srgb, var(--app-sidebar-bg) 50%, var(--app-bg));
  /* ── LA TINTA — el pigmento neutro del tema claro (2026-08-27) ────────
     La paleta clara dejó de ser gris el 2026-08-26 (la barra pasó al beige de
     Controly) pero las VELADURAS siguieron moliéndose de negro puro: hairline,
     hovers, chips, bordes de control, sombras — todo `rgba(0,0,0,α)`, es
     decir, el gris frío de la paleta anterior asomando en cada canto. Desde
     hoy TODA veladura neutra del claro se muele de esta tinta única: un umbra
     en el matiz de la casa (~44°, el del beige), vía
     `color-mix(in srgb, var(--app-tinta) N%, transparent)`.

     LOS PORCENTAJES NO SON LOS ALPHAS VIEJOS: están resueltos numéricamente
     para que cada veladura compuesta sobre blanco conserve LA MISMA
     luminancia que su negro-α anterior (la tinta es menos oscura que el
     negro, así que cada N% sube ~27%). Consecuencia deliberada: ningún ratio
     de contraste medido en esta hoja se movió — sólo giró el matiz. El mismo
     criterio se aplicó a las tintas de TEXTO (fg y compañía): mismo L, matiz
     cálido; sus cifras documentadas siguen valiendo al centésimo.

     El tema oscuro NO cambió: sus veladuras son blancas y ya eran neutras
     sobre fondo neutro — este token sólo gobierna el claro. */
  --app-tinta:#403620;
  --app-hairline:color-mix(in srgb,var(--app-tinta) 7.6%,transparent);
  /* ── LA ESCALERA DE BORDES (2026-08-27, pedido de Arif) ──────────────
     Dos pesos neutros y una regla. El SUTIL (`--app-hairline`, 7.6%) separa
     POR DENTRO —el border-bottom de una cabecera, el filete entre zonas— y
     borde a lo que ya está delimitado por otra cosa (la sombra de un
     flotante, el tinte de un badge, una pieza mini como `code`). El
     ESTÁNDAR (`--app-borde`, 15%) es el CANTO de una pieza que vive en la
     página: sección, tile, tarjeta, columna del tablero, la caja de datos
     del tramo, la barra de selección. Más visible que el sutil sin ser
     agresivo — el peso de un ~12% de negro, el orden del borde de card de
     Bootstrap. Entre ambos queda el filo de control (`--ctl-borde`, 12.7%),
     que es de los CONTROLES y no se mezcla con éstos; encima siguen el
     muted (19.1%, contorno de la salida) y el hover (25.4%).

     Hasta hoy todas las cajas iban en la hairline y el propio catálogo ya
     había medido la consecuencia: «.06 es el de separar bloques y aquí NO
     SE VE» (ficha del total de la consola). En OSCURO la escalera no se
     abre: `--app-borde` vale lo mismo que su hairline (.09 blanco), porque
     allá la sutil ya delimita — el encargo fue solo el tema claro. */
  --app-borde:color-mix(in srgb,var(--app-tinta) 15%,transparent);
  --app-row-hover:color-mix(in srgb,var(--app-tinta) 4.5%,transparent);
  /* A 14px el atenuado tiene que ser SUTIL, no intenso: el salto de primario a
     secundario bajó de 3.2× a 2.3× de contraste, y los dos grises pasan ahora
     el mínimo de 4.5:1. Un gris que a 15px se leía como jerarquía, a 14px se
     lee como texto deshabilitado. */
  --app-fg:#1f1c15;   /* cálido desde 2026-08-27 (era #1c1c1e): mismo L, matiz de la casa */
  --app-fg-secondary:#59554a;   /* 7.4:1 — antes 5.3. Cálido desde 2026-08-27 (era #55555c), mismo L */
  /* 5.3:1 sobre papel y 4.57:1 sobre el gris de la barra lateral, que es
     donde vive la mitad de sus usos. El #757579 original estaba calibrado
     SÓLO contra blanco (4.6) y caía a 4.2 en cuanto se posaba sobre la
     lateral — el fondo importa tanto como el color.

     TRES PASOS MÁS OSCURO desde el 2026-08-26 (#6f6f73 → #6b6b6f), y por esa
     misma lección: con la barra en el beige de Controly (#F0EEE6, más oscuro
     que el #f6f6f7 anterior) el valor viejo se quedaba en 4.31 — otra vez bajo
     el mínimo, en la misma superficie y por la misma causa. La corrección va
     en el TOKEN y no en un override de la barra porque el beige lo pintan
     nueve superficies. A ojo es el mismo gris; medido, +0.26 sobre la barra y
     +0.3 sobre el papel. */
  /* Y CÁLIDO desde el 2026-08-27 (era #6b6b6f): mismo L con el matiz de la
     casa — las medidas de arriba siguen valiendo al centésimo. Igual que el
     primario y el secundario; el criterio vive en LA TINTA. */
  --app-fg-tertiary:#6f6b60;
  /* 5.3:1 con blanco encima y 5.3:1 como texto sobre papel. El #007aff de
     antes daba 4.0 en los dos papeles: no llegaba al 4.5 ni de relleno ni de
     letra. A ojo son el mismo azul.

     UN PASO MÁS OSCURO desde el 2026-08-26 (#0069d9 → #0068d7), por el beige
     de la barra: `.pnl-enlace` («abrir →» de las secciones del panel, retirado
     sin consumidor el 2026-09-19) era acento sobre `--app-sidebar-bg`, y ahí
     el valor viejo medía 4.494 — bajo
     el mínimo por una milésima. Un solo paso lo lleva a 4.56 y sube sus otros
     dos oficios de 5.22 a 5.30, así que no hay nada que ceder a cambio. */
  --app-accent:#0068d7;
  /* El acento tiene DOS oficios y en el tema oscuro no los puede hacer el
     mismo valor. De TINTA —enlace, foco, acción de texto— necesita ir CONTRA
     el fondo nocturno: luminancia ≥0.234 para dar 4.5:1. De RELLENO con letra
     blanca encima necesita justo lo contrario: ≤0.183. No se cruzan —la misma
     aritmética que obligó a los badges a ser translúcidos—, así que el relleno
     es su propio token. En claro los dos oficios SÍ caben en un valor y esto
     no es más que el acento. */
  --app-accent-solido:var(--app-accent);
  /* El color de la LETRA que va encima del relleno. Blanco en LOS DOS TEMAS
     —5.22:1 en claro, 4.67:1 en oscuro— desde que el relleno se separó de la
     tinta (2026-08-10). Antes era un valor por tema: con un solo azul para
     los dos oficios, la única salida en oscuro era escribir en gris oscuro. */
  --app-accent-fg:#ffffff;
  /* LA MARCA — el navy del logotipo de Controly. NO es un token de interfaz:
     no se deriva del acento ni del texto y no lo usa ningún control. Existe
     porque el logotipo tiene un color propio y ése es el que manda cuando se
     escribe el nombre. En oscuro pasa a blanco puro, que es la tinta de la
     versión nocturna del símbolo (`icono-controly-w.png`).

     EL VALOR SALE DEL ARCHIVO desde el 2026-08-26: `#101A2D` es el píxel del
     anillo de `icono-controly.png`, y la palabra «controly» del logotipo de
     Controly va en `#131A2C` — el mismo navy. Antes valía `#0b1120`, que se
     le parecía sin serlo: el símbolo y el nombre escrito a su lado iban en
     dos tintas distintas. Muestreado, no estimado. Contraste 14.97:1 sobre la
     barra beige, así que no hay nada que ceder. */
  --marca-fg:#101A2D;
  /* TEAL — información y estado. Deliberadamente lejos del azul: desde que el
     azul significa «esto escribe en la base», un badge azul prometería una
     escritura que no va a ocurrir. */
  --app-info:#0ea5a5;
  /* Cada color semántico va en PAREJA: uno para teñir un fondo y otro para
     escribir encima. No son intercambiables y el error de confundirlos es
     silencioso — un punto de 6px del color del tinte sobre ese mismo tinte da
     2.56:1 y parece que está bien hasta que lo mides.
     Los `-text` son los únicos que sí varían por tema: para leerse necesitan
     ir contra el fondo, y el fondo cambia. */
  --app-info-text:#087070;
  --app-peligro-text:#b82418;
  --app-exito-text:#0f7040;
  --app-atencion:#ffcc00;
  --app-atencion-text:#8f6700;   /* 5.1:1 sobre papel · 4.8:1 sobre su tinte — antes 3.9 y 3.6 */
  --app-chip:color-mix(in srgb,var(--app-tinta) 6.4%,transparent);
  --app-pill:color-mix(in srgb,var(--app-tinta) 8.9%,transparent);
  /* Dos tonos que NO son la línea de pelo. La hairline (6%) dibuja estructura
     —separa una zona de otra— y es tan tenue que un botón hecho con ella no
     se lee como objeto. Éstos dibujan el CANTO de una pieza: el muted define
     el contorno de la salida, el tenue es su relleno de reposo. */
  --app-borde-muted:color-mix(in srgb,var(--app-tinta) 19.1%,transparent);
  --app-relleno-tenue:color-mix(in srgb,var(--app-tinta) 4.1%,transparent);
  /* ── Control ──────────────────────────────────────────────────────
     Papel con un filo, no un hueco gris. Es el estándar de Bootstrap y de
     casi todo formulario web, pero con el borde MÁS TENUE que el suyo:
     Bootstrap usa #dee2e6 ≈ 13% de negro; aquí el peso de un 10% (desde el
     2026-08-27 dicho en la tinta cálida al 12.7% — ver LA TINTA). Con 11 controles en una
     ficha, tres puntos menos de borde son la diferencia entre una rejilla y
     una lista de campos.
     En oscuro no es blanco sino un OSCURECIMIENTO relativo (22% de negro):
     así el campo se lee como hueco tanto sobre el fondo de la app como sobre
     el papel del peek, sin necesitar dos valores. */
  --ctl-bg:#ffffff;
  /* Gemelo OPACO de --ctl-bg, y existe por una sola razón: la defensa contra
     el autofill de Chrome se pinta con box-shadow inset, y un color
     translúcido deja pasar el amarillo que se quiere tapar. En claro coincide
     con --ctl-bg; en oscuro se resuelve el translúcido contra el fondo. */
  --ctl-bg-solido:#ffffff;
  --ctl-borde:color-mix(in srgb,var(--app-tinta) 12.7%,transparent);
  --ctl-borde-hover:color-mix(in srgb,var(--app-tinta) 25.4%,transparent);
  /* ── EL FILO DEL CAMPO, AGRESIVO (2026-09-20, decisión de Arif) ──────────
     TOKEN PROPIO Y NO `--ctl-borde` SUBIDO, y la distinción la pagó una
     regresión: la primera versión subió `--ctl-borde` de 12.7% a 75% y su
     hover de 25.4% a 90%, y esos dos tokens tienen **19 consumidores que no
     son el campo** — dos de ellos los usan como FONDO y no como filo: el
     pulgar de la barra de scroll (`::-webkit-scrollbar-thumb`) y el hover de
     `.ver-clave`. Los dos se volvieron marrón oscuro. Lo cazó Arif en el
     scroll del diálogo, que es donde más se ve.

     La lección: un token con veintiún lectores no se sube para arreglar a
     uno. El campo pide un filo propio, así que tiene uno propio, y lo leen
     exactamente cuatro reglas — el control y su hover, el campo de la puerta
     y el suyo.

     75% DE TINTA = **#706858** sobre blanco: el peso que Arif pidió («entre
     #666 y #777») y dentro de ese rango en claridad. No es gris neutro porque
     la casa tiene escrito que toda veladura neutra del claro se muele de
     `--app-tinta` (ver LA TINTA); un #6d6d6d frío se vería azulado al lado
     del beige de la barra.

     POR QUÉ TAN FUERTE: hoja y campo son los dos blancos en claro (1.00:1),
     así que el contorno hace TODO el trabajo de delimitar. Es la alternativa
     a teñir la hoja, que se construyó el mismo día y Arif descartó — el
     porqué completo está en `.dlg-caja`.

     LO LLEVA TAMBIÉN EL CAMPO DE LA PUERTA (`.campo-flotante input`): su
     superficie es `--app-bg`, o sea el mismo blanco, así que tenía el mismo
     problema. Lo que NO lo lleva es nada que no sea un campo.

     EN OSCURO NO APLICA y el tema lo redeclara al filo normal: allá el
     control es translúcido y se hunde solo sobre cualquier fondo. */
  --ctl-filo:color-mix(in srgb,var(--app-tinta) 75%,transparent);
  --ctl-filo-hover:color-mix(in srgb,var(--app-tinta) 90%,transparent);
  /* EL HALO DE BOOTSTRAP 5, clonado tal cual (2026-08-17, pedido de Arif).
     Son sus dos valores literales, no una aproximación con los colores de la
     casa: `$input-focus-border-color: tint-color($primary, 50%)` = #86b7fe, y
     `$input-btn-focus-box-shadow: 0 0 0 .25rem rgba($primary, .25)` =
     rgba(13,110,253,.25). Los 4px de las reglas que lo pintan SON esos
     .25rem (la raíz mide 16px), así que no se tocaron.

     Antes era teal (`--app-info`) al 16%. Lo que se pierde con el cambio es
     el argumento de que el foco hablara el idioma de la fila elegida; lo que
     se gana es un halo que se reconoce sin aprenderlo. Translúcido igual,
     así que sigue sirviendo en los dos temas sin gemelo — y Bootstrap 5.3
     tampoco lo redefine en oscuro. */
  --ctl-foco:#86b7fe;
  --ctl-halo:rgba(13,110,253,.25);
  /* ── Radios ───────────────────────────────────────────────────────
     Cada uno responde una pregunta, y por eso son cuatro y no nueve:

       --r-mini    ¿mide menos de 20px?  pista de teclado, `code`, checkbox
       --r-ctl     ¿contiene algo?       control, opción, celda, chip, tarjeta
       --r-caja    ¿flota?               menú, toast, popover
       --r-dialogo ¿flota Y mide 440+?   el diálogo, y sólo él
       --r-pill ¿se pulsa?            botones y chips de filtro

     `--r-mini` existe porque la proporción importa: 8px en una caja de 16px
     de alto ya no es una esquina suave, es medio pill — y entonces una pista
     de teclado empieza a parecer un botón.

     Fuera de estos CINCO sólo hay dos formas, y ninguna es un radio: el 50%
     de lo circular (flotante, avatares, la × de un chip) y el 0 de la
     rejilla, donde una esquina redonda dejaría ver la línea del grid por
     debajo.

     Corregido el 2026-09-19: esta lista ponía también las flechas del peek
     entre lo circular. Salieron del 50% el 2026-08-18 y llevan `--r-pill`
     sobre una caja cuadrada (`.peek-nav .flecha`, más abajo), que da el mismo
     círculo y las hace la misma pieza que la × del sistema. */
  --r-mini:4px;
  --r-ctl:8px;
  --r-caja:12px;
  /* LA PÍLDORA, que la regla nombra desde el principio —«pill lo que se
     pulsa»— y que hasta el 2026-08-16 se llamaba `--btn-radio`. El nombre era
     el problema, no el valor: cuatro radios se llamaban `--r-*` y el quinto
     llevaba el nombre de UN consumidor, así que quien necesitaba una cápsula
     que no fuera un botón —un chip de estado, una barra de progreso— no
     reconocía el token y escribía `999px`. Pasó SEIS veces, y el propio
     comentario del token predicaba lo contrario. Renombrado, la escala se lee
     como escala y esos seis literales desaparecieron.
     Y la regla se afina con ellos: pill es lo que se pulsa **y lo que es
     literalmente una cápsula**. También las flechas del peek y la tecla
     redonda (`.btn-circulo`): una caja cuadrada con `--r-pill` da un círculo. */
  --r-pill:999px;
  /* El quinto, y no responde «¿flota?» sino «¿cuánto mide?». Es la MISMA
     aritmética de proporción que obligó a `--r-mini` a existir, ahora por el
     otro extremo: 12px en una hoja de 560×400 lee como esquina cuadrada, igual
     que 8px en una caja de 16 leía como medio pill. No se subió `--r-caja` a
     16 porque sus otros consumidores —menú, toast, popover— miden lo que mide
     un control, y ahí 16 empieza a redondear el contenido. */
  --r-dialogo:16px;
  /* El aire bajo un título de página. Un solo token porque un solo valor: lo
     ponía a mano cada página —18px en no contactar y papelera, 14+16 en la
     sábana vía la barra de filtros, y NADA en la sábana desde que esa barra se
     retiró (2026-08-09), que dejó el título pegado a la tabla. Ahora lo pone
     `.panel-titulo` y ninguna página vuelve a decidirlo.

     Subió de 18 a 27 el 2026-08-13 (+50%, pedido de Arif). El día que la
     cabecera perdió su píldora bajó de 44 a 31.2px, y el titular pasó a ser
     una LÍNEA de texto: sin marco que lo separe de lo de abajo, el aire es lo
     único que dice dónde acaba la cabecera y dónde empieza el contenido. Los
     18 estaban calibrados contra una pieza que se veía sola. Del alto que se
     recuperó (13px) se devuelven 9 aquí; la tabla sigue ganando 4. */
  --esp-titulo:27px;
  /* ── EL AIRE DE LA CONFIGURACIÓN (2026-09-03, pedido de Arif) ──────
     Dos números y una queja que los originó: *«ahora es de stress, todo
     acotado verticalmente»*. La consola apretaba porque cada pieza traía su
     propio número pequeño —la sección 14px de margen, la fila 6 de padding,
     el valor 2 de hueco— y nadie los estaba mirando juntos. Con la caja fuera
     (§Las reglas → «El panel no lleva cards») el aire ES la estructura, así
     que pasa a token: el que separa dos SECCIONES y el que respira dentro de
     una OPCIÓN. La proporción entre los dos es lo que hace legible la jerarquía
     — una sección tiene que separarse más de otra sección que dos opciones
     entre sí, o el recorrido se lee como una sola lista larga.

     NO gobiernan al peek ni al diálogo: ahí el alto lo decide el contenido y
     el aire cuesta filas. Lo dice `--esp-opcion` en `.vn-renglon`, que lo
     baja a lo de siempre. */
  --esp-seccion:36px;
  --esp-opcion:14px;
  /* ── Los dos tamaños de icono ─────────────────────────────────────
     Dos en toda la app, y la pregunta que los separa NO es dónde vive el
     icono sino QUÉ TRABAJO HACE:

       --icn-texto  acompaña — va dentro de una frase o de un control, al
                    lado de palabras que ya dicen lo suyo (la flecha del
                    enlace, el ojo de la contraseña).
       --icn-fila   MARCA — es lo primero que se busca de una fila o de un
                    bloque, y la palabra viene después (el ítem de menú, la
                    alerta).

     El que marca tiene que verse: a 16 son 1.14× los 14 del texto y pesa
     MENOS que la palabra que etiqueta, así que la fila se lee al revés. A 20
     son 1.43× y manda la silueta, que es lo que distingue una fila de otra.
     El que acompaña se queda en 16 por lo contrario: dentro de una frase, un
     glifo a 1.43× no acompaña, interrumpe.

     Ninguno crece nada: 20 + los 5+5 de padding son 30px contra los 31 que
     ya medía el ítem de menú —los pone el texto a line-height 1.5—. */
  --icn-texto:16px;
  --icn-fila:20px;
  --app-check:color-mix(in srgb,var(--app-tinta) 35.6%,transparent);
  --app-paper:#ffffff;
  /* LA HOJA DEL DIÁLOGO (2026-09-20). Dos valores porque el claro y el
     oscuro llegaron a respuestas distintas mirándolos en pantalla: en claro
     la hoja se queda en papel y quien delimita el campo es su filo (75% de
     tinta, ver EL FILO DEL CONTROL); en oscuro se queda en el tono de la
     barra, que es el que Arif aprobó. El porqué de cada uno, en `.dlg-caja`. */
  --dlg-hoja-bg:var(--app-paper);
  --app-shadow:color-mix(in srgb,var(--app-tinta) 15.3%,transparent);
  /* El halo del titular: más tenue que la sombra de elevación —no eleva nada,
     sólo dice que hay una superficie encima— y calibrado por tema, porque un
     negro al 12% sobre el fondo nocturno no existe. */
  --app-halo:color-mix(in srgb,var(--app-tinta) 16.5%,transparent);
  /* El velo del diálogo NO es la sombra, aunque hasta el 2026-08-11 se pintara
     con ella: al 12% la pantalla de atrás queda casi intacta y nada dice «esto
     está en pausa» —justo lo único que un modal tiene que comunicar antes de
     que lo leas—. Token propio, con su valor por tema: en oscuro el mismo 28%
     no se ve contra un fondo que ya es negro. */
  --app-velo:color-mix(in srgb,var(--app-tinta) 35.6%,transparent);
  /* Aquí vivió `--sombra-hundida` (la sombra interior de la caja de datos
     del tramo, clonada de Controly) hasta el 2026-08-27: Arif la juzgó
     tosca — un degradado sucio en las esquinas donde este sistema dibuja
     cantos — y la caja pasó al borde ESTÁNDAR (`--app-borde`). Con ella se
     fue la única sombra de relleno: las que quedan son todas de elevación. */
  --app-group:color-mix(in srgb,var(--app-tinta) 3.8%,transparent);
  --app-amber-tint:rgba(255,204,0,.15);
  --app-accent-tint:color-mix(in srgb,var(--app-accent) 11%,transparent);
  /* ── Badges ───────────────────────────────────────────────────────
     UN token por color, sin gemelo en el tema oscuro — y funciona porque son
     TRANSLÚCIDOS: el mismo valor se mezcla con lo que tenga debajo, así que
     aclara sobre papel y oscurece sobre el fondo nocturno él solo.

     Con un color SÓLIDO esto sería imposible, y no es opinión: para que un
     fondo pase 4.5:1 con texto oscuro necesita luminancia ≥0.235, y para
     pasarlo con texto claro necesita ≤0.137. No se cruzan. El mejor gris
     posible se queda en 3.80:1 contra el peor de los dos textos. El alpha es
     lo que rompe el empate, porque el fondo deja de ser un color fijo y pasa
     a ser una tinta sobre el papel del momento.

     Los alphas NO son iguales entre sí a propósito: el amarillo necesita 22%
     para pesar lo mismo que el rojo al 15%. Igualar el número igualaría la
     fórmula, no la percepción.

     Medido, texto del body sobre cada uno — claro / oscuro:
       info 14.1 / 12.2   ·   warning 15.4 / 8.2
       danger 13.9 / 12.1 ·   success 14.7 / 10.2 */
  --app-peligro:#ff3b30;
  --app-exito:#34c759;
  --badge-info:   color-mix(in srgb,var(--app-info) 16%,transparent);
  --badge-warning:color-mix(in srgb,var(--app-atencion) 22%,transparent);
  --badge-danger: color-mix(in srgb,var(--app-peligro) 15%,transparent);
  --badge-success:color-mix(in srgb,var(--app-exito) 18%,transparent);
  /* ── Canto de estado ──────────────────────────────────────────────
     El FILO del bloque teñido, y lo único que se le tomó prestado al `.alert`
     de Bootstrap al clonarlo a la casa (2026-08-10): su alerta se delimita
     con un filo del color de su estado, y la nuestra se apoyaba sólo en el
     tinte. Un tinte al 15-22% llena, pero no cierra: el bloque no tenía dónde
     terminar. Subir el alpha del relleno lo habría cerrado a costa del
     contraste del texto, que es lo que ese alpha paga. De ahí para adentro el
     clon se queda en el idioma propio: el texto sigue siendo el del cuerpo y
     no la letra de color del original, y el info es el teal de la casa, no su
     cian.

     Se derivan del `-text`, NO del tinte. Ésa es la corrección que hace que
     funcione: el canto tiene el mismo trabajo que el icono de la alerta
     —leerse CONTRA el relleno—, y los `-text` son justamente los que cambian
     por tema para eso. Derivarlos del tinte partía el sistema en dos, porque
     el amarillo es el único cuyo tinte no se oscurece en claro: al 50% no
     pasaba de 1.23 sobre papel y en oscuro ya iba en 2.92. Con el `-text`, el
     canto y el icono del bloque hablan además con la MISMA voz.

     Y por eso basta UN alpha para los cuatro, sin la asimetría que sí
     necesitan los badges: la pareja `-text` ya hizo la ecualización.

     Medido, canto contra su propio relleno — claro / oscuro:
       info 1.91 / 2.68   ·   warning 1.83 / 2.65
       danger 2.08 / 2.15 ·   success 1.95 / 2.63
     El original mide 3.0 en oscuro; 45% deja el filo un punto por debajo —
     visible como canto, sin que el bloque se lea como una caja con marco. */
  --canto-info:   color-mix(in srgb,var(--app-info-text) 45%,transparent);
  --canto-warning:color-mix(in srgb,var(--app-atencion-text) 45%,transparent);
  --canto-danger: color-mix(in srgb,var(--app-peligro-text) 45%,transparent);
  --canto-success:color-mix(in srgb,var(--app-exito-text) 45%,transparent);
  /* La fila elegida lleva el MISMO teal que el badge info, pero en un tinte
     más suave (9% frente a 16%): estirado a lo ancho de una fila entera, el
     tinte del badge grita. Mismo color para el mismo significado —«esto es lo
     que señalaste»— con menos tinta. */
  --app-row-selected:color-mix(in srgb,var(--app-info) 9%,transparent);
  /* ── Tipografía ──────────────────────────────────────────────────
     PISO DE 14px (desde el 2026-09-08): ninguna letra por debajo; bajo esa
     cota sólo glifos. Corregido el 2026-09-19: esta cabecera decía «PISO DE
     13px, y 14 para todo lo que sea un DATO», la regla del 2026-08-08 que el
     párrafo «EL PISO SUBIÓ A 14» de abajo ya había sustituido.

     La escala anterior bajaba a 12 y 11px siguiendo la convención de las
     apps de datos densas (Linear, Attio, Things). Esa convención existe
     porque en ellas el 12px es metadato decorativo — timestamps de adorno,
     contadores que nadie mira. Aquí no hay nada de eso: el espacio es tan
     caro que sólo se pinta lo que sirve para decidir. "hace 4 sem" en la
     columna Último toque ES el dato con el que se elige a quién llamar, y
     un dato que se entrecierra los ojos para leer no es jerarquía, es una
     factura que paga la vista del usuario.

     La jerarquía la lleva el COLOR —primario, secundario, terciario— que
     para eso está resuelto y medido. El tamaño ya no la lleva. Verificado:
     la altura de fila sigue en 36px, la cabecera en 30, y cero celdas
     truncadas.

     EL RÓTULO DE UN CAMPO ES DATO, y por eso mide 14 desde el 2026-08-17.
     Hasta ese día la línea se trazaba en «14 para datos, 13 para etiquetas
     que rotulan otra cosa», y ahí el 13 tapaba una contradicción: el rótulo
     de una CASILLA nunca pasó por ese token —hereda `--fs-base`— así que la
     misma palabra medía 13 junto a un input y 14 junto a un checkbox, a
     veces en el mismo diálogo. La regla que quedó fue: **todo lo que el
     usuario LEE para decidir o para llenar mide 14**, y a 13 sólo baja lo que
     ACOMPAÑA — un chip, un badge, un aviso al pie, el rótulo de un grupo.

     ── EL PISO SUBIÓ A 14 EL 2026-09-08, Y CON ÉL SE FUE EL 13 ──
     Instrucción de Arif: **ninguna letra por debajo de 14px**, tomando el
     panel de Cuenta como el módulo donde se midió y aceptando de antemano que
     el cambio alcanza a toda la app. Y la alcanza: `--fs-micro` tenía 61
     consumidores, así que la única forma honesta de aplicarlo era el token, no
     una excepción por página.

     Lo que el caso enseña —y por qué el 13 no se defendía solo— es que la
     línea de 2026-08-17 ya se había demostrado porosa una vez: cada vez que
     había que decidir si una palabra «acompaña» o «es dato», la respuesta
     dependía de quién mirara. La cuenta lo dejó ver en un renglón: el rótulo
     de una `.cfg-fila` mide 14 en su tramo y 13 dentro de un `.cfg-grupo`,
     por un override que nadie recuerda haber pedido — la MISMA palabra,
     «Nombre», en dos tamaños en la misma pantalla. Un piso que no admite
     juicio no se puede aplicar mal.

     LOS TRES TOKENS DE 14 NO SE FUNDEN EN UNO, y es deliberado por lo que ya
     dice el párrafo de arriba: **la jerarquía la lleva el COLOR y el peso, no
     el tamaño**, así que `--fs-base`, `--fs-meta` y `--fs-micro` sobreviven
     como NOMBRES DE PAPEL —dato / meta / acompañamiento— y no como medidas.
     Sirven para dos cosas que un literal `14px` no puede: dicen qué papel juega
     lo que se está escribiendo, y dejan mover un papel entero el día que haga
     falta. Es el mismo criterio con el que `--fs-base` y `--fs-meta` llevan
     conviviendo en 14 desde el 2026-08-17 sin que nadie los fundiera.

     Por debajo de 14 sólo quedan GLIFOS —el ✓ y el – del checkbox, la flecha
     de orden, el asa y el candado del catálogo de columnas—, nunca letras. */
  --fs-base:    14px;   /* filas, barra lateral, peek, inputs, botones, menús, ETIQUETAS de campo */
  --fs-meta:    14px;   /* cabecera de columna, meta de bitácora, contadores */
  --fs-micro:   14px;   /* lo que ACOMPAÑA: chips, badges, avisos, títulos de grupo, pista de
                           teclado. Midió 13px hasta el 2026-09-08 — ver arriba */
  --fs-dialogo: 15px;   /* el rótulo de una SUPERFICIE: hoja modal, panel (peek), sección, riel */
  --fs-seccion: 17px;   /* cabecera del chat, número de día de la agenda */
  --fs-destacado: 20px; /* lo que se DESTACA dentro de una superficie: la cifra
                           de un detalle, el nombre de la ficha de conversaciones,
                           el «¡Listo!» de la reserva pública. No rotula la
                           superficie —eso es `--fs-dialogo`—: realza algo que
                           vive dentro de ella.
                           Se llamó `--fs-peek` hasta el 2026-08-18, cuando el
                           rótulo del peek bajó a 15 y lo dejó sin su consumidor
                           homónimo. Es el mismo cuento de `--btn-radio` →
                           `--r-pill`: un token con nombre de UN consumidor deja
                           de reconocerse en cuanto ese consumidor se va, y quien
                           lo necesita escribe el literal. Renombrado antes de
                           que pasara, no después. */
  --fs-titulo:  26px;   /* título de la lista */

  /* Geist quedó elegida el 2026-08-08: conserva el 97% de la altura de x de
     Inter en 6.4% menos ancho y tiene los dígitos más angostos de las
     candidatas — con una columna de teléfono eso se nota. El comparador de
     tipografías que servía para medirlo se retiró al decidir. */
  --fuente: 'Geist', -apple-system, system-ui, sans-serif;

  /* Pareja display/texto: la personalidad vive en el CHROME, no en las
     5,000 filas. Space Grotesk además YA es la tipografía de la marca
     Controly (design system de piezas sociales), así que ata el producto
     a la familia sin tocar la legibilidad de la sábana.
     Dónde va (2026-08-08): titulares >=20px, y bajo esa cota SÓLO el chrome
     que navega — enlaces y menús (barra lateral y desplegables), piezas
     cortas y escasas, y el título del estado vacío (2026-09-23). Los DATOS jamás: filas, campos, cifras y conteos se
     quedan en Geist, que para eso tiene los dígitos tabulares. */
  --fuente-titulo: 'Space Grotesk', var(--fuente);

  --app-row-h:36px;
  /* El alto de la cabecera de la sábana. Es un token porque lo consumen DOS
     reglas que tienen que cuadrar: su propia caja y el `line-height` que
     centra su texto —que es lo que hace que la celda mida lo que mide la
     cabecera y su filo la cubra entera— (2026-08-13). Más baja que la fila:
     rotula, no lleva datos. */
  --sabana-cab-h:30px;
  /* ── Alto de barra ────────────────────────────────────────────────
     El alto de TODA tira horizontal de acciones: la barra de selección y
     cualquier barra o menú horizontal que venga (el titular fue el otro
     consumidor hasta el 2026-08-13, cuando perdió su píldora).

     Sale de los tokens, no de un gusto: una fila de datos más el aire de un
     botón. Si la fila o el botón cambian, la barra los sigue sola.

     Por qué hacía falta. La app tiene DOS átomos y los dos ya eran coherentes
     entre sí: el CONTROL mide 26.89px (botón, input, flotante — todos derivan
     de `--btn-py` y `--btn-lh`) y el ÍTEM de lista mide 31px (ítem de la barra
     lateral, acción de la barra de selección, opción del menú flotante — texto
     a `line-height` 1.5 más 5px). Lo que NO era coherente era el nivel de
     arriba: la cápsula del titular medía 42.89 y la barra de selección 45, sin
     que ninguna de las dos supiera de la otra. Dos piezas que hacen el mismo
     trabajo —flotar sobre el contenido con acciones dentro— y se diferenciaban
     en 2px, que es la peor distancia posible: no se lee como intención, se lee
     como descuido.

     44 cabe holgado para los dos átomos (6.5px de aire alrededor del ítem,
     8.5 alrededor del control), queda por encima de la fila de 36 —una barra
     no debe poder confundirse con un renglón de datos— y es además el blanco
     táctil de 44px de las guías de plataforma, que es exactamente lo que estas
     tiras son en un teléfono.

     CÓMO SE USA: `min-height` + centrado, NUNCA padding vertical calculado a
     mano. Así la barra no se desajusta cuando le cambian el contenido, y la
     que hoy lleva un ítem de 31 y mañana un control de 27 sigue midiendo lo
     mismo. El padding vertical de una barra debe ser 0.

     NO CUBRE LOS MENÚS DESPLEGABLES (movido del catálogo `ui.php`,
     2026-09-19): un `.menu-flotante` es una pila, no una tira, y su alto es su
     contenido. Lo que sí comparte es el átomo: sus opciones son ítems de 31px,
     iguales a las de la barra lateral. */
  --app-barra-h:calc(var(--app-row-h) + var(--btn-py) * 2);
  --app-dur:180ms;
  --app-ease:cubic-bezier(.2,.6,.2,1);

  /* ── LA GRÁFICA ────────────────────────────────────────────────────
     Existen como TOKENS y no dentro del JS por el mismo motivo que el resto:
     un canvas no hereda CSS, así que su motor tiene que venir a buscar los
     colores aquí — y este es el único sitio donde el tema los puede cambiar.
     Ponerlos en el marcado (como hace la rueda, con el rgba escrito en un
     `data-*` de cada canvas) los deja fuera del alcance del tema para siempre.

     El teal es EL MISMO de la gráfica de ventas del índice del propietario de
     la rueda —rgb(75,192,192), relleno al 30% y borde sólido—, clonado a
     propósito. Sólo cambia en oscuro, donde ese verde se apaga contra el fondo.

     La tinta y la rejilla NO son valores: son los roles que el sistema ya
     tiene. Por eso no se redefinen en el tema oscuro — se resuelven solos
     cuando sus tokens cambian, que es la ventaja entera de nombrar por rol. */
  --graf-serie:rgba(75,192,192,.30);
  --graf-borde:#4bc0c0;
  --graf-tinta:var(--app-fg-tertiary);
  --graf-linea:var(--app-borde-muted);
  /* LOS DOS TONOS DEL FLUJO (2026-08-29). Clonados de la misma paleta de la
     rueda que dio el teal — `controly-charts` → `color-palette.md`, que mapea
     un color por MÉTRICA y no por gusto: verde (76,175,80) = ingreso de
     efectivo, naranja oscuro (225,122,0) = egreso. Mismo relleno al 30% y mismo
     borde sólido; lo único que cambia respecto al teal es el hue.

     SON TOKENS Y NO UN `data-*` EN EL CANVAS, que es como lo escribe la rueda:
     ahí el color queda fuera del alcance del tema para siempre. Aquí la gráfica
     pide su tono por NOMBRE (`tono: 'ingreso'`) y el sistema resuelve el valor,
     así que estas dos cambian de tema solas igual que la de ventas.

     Ojo con el naranja: NO es el ámbar de `--app-atencion` que usa la chispa
     del gasto. Son dos cosas distintas de la misma paleta —el gasto es un
     documento, el egreso es dinero que salió de la caja— y la rueda les da
     colores distintos a propósito. Igualarlos aquí sería perder esa lectura. */
  --graf-ingreso-serie:rgba(76,175,80,.30);
  --graf-ingreso-borde:#4caf50;
  --graf-egreso-serie:rgba(225,122,0,.30);
  --graf-egreso-borde:#e17a00;
}
[data-theme="dark"]{
  --app-bg:#1e1e20;
  --doc-bg:#131315;
  --app-sidebar-bg:#252528;
  /* La escalera de tres NO existe en oscuro: el encargo fue «sólo la versión
     clara», y allá la relación papel #1e1e20 / barra #252528 ya funciona. Se
     redeclara en vez de dejar heredar la mezcla porque `--app-bg` vale otra
     cosa aquí: sin esta línea las cinco superficies nocturnas se moverían
     solas. */
  --app-secundario-bg:var(--app-sidebar-bg);
  --app-hairline:rgba(255,255,255,.09);
  /* En oscuro la escalera de bordes no se abre (ver LA ESCALERA): la sutil
     ya delimita sobre fondo nocturno, así que el estándar pesa lo mismo. */
  --app-borde:rgba(255,255,255,.09);
  --app-row-hover:rgba(255,255,255,.05);
  --app-fg:#ececf0;
  --app-fg-secondary:#b0b0b8;   /* 7.7:1 — antes 6.2 */
  /* 4.8:1 sobre la barra lateral oscura (#252528) y 5.3 sobre el fondo. El
     #8a8a93 anterior se quedaba en 4.47 sobre la lateral — el mismo fallo que
     su gemelo del tema claro, por el mismo motivo. */
  --app-fg-tertiary:#90909a;
  /* El acento de TINTA no se oscurece: en oscuro tiene que leerse como texto
     sobre el fondo (4.56:1) y bajarlo rompería eso. El de RELLENO sí, y por
     eso son dos: con letra blanca encima da 4.67:1 —el mismo contraste que
     daba la letra oscura sobre el azul claro, con la ventaja de que ahora el
     primario se escribe igual en los dos temas— y contra el fondo de la app
     conserva 3.56:1 (3.14 sobre el papel del peek), de sobra para leerse como
     pieza: el mínimo de un objeto no textual es 3:1.
     `--app-accent-fg` no se redefine aquí. Ése es el punto. */
  --app-accent:#0a84ff;
  --app-accent-solido:#0a70e8;
  --marca-fg:#ffffff;
  --app-info-text:#2dd4d4;
  --app-peligro-text:#ff6b60;
  --app-exito-text:#4ade80;
  --app-atencion:#ffd60a;
  --app-atencion-text:#ffd60a;
  --app-chip:rgba(255,255,255,.08);
  --app-pill:rgba(255,255,255,.10);
  --app-borde-muted:rgba(255,255,255,.18);
  --app-relleno-tenue:rgba(255,255,255,.05);
  /* No un color sólido sino un oscurecimiento: así el campo se lee como hueco
     tanto sobre el fondo de la app (#1e1e20) como sobre el papel del peek
     (#28282c), con un solo valor en vez de dos. */
  --ctl-bg:rgba(0,0,0,.22);
  /* el mismo negro al 22%, ya resuelto contra el fondo de la app */
  --ctl-bg-solido:color-mix(in srgb,#000 22%,var(--app-bg));
  --ctl-borde:rgba(255,255,255,.13);
  --ctl-borde-hover:rgba(255,255,255,.24);
  /* El filo agresivo es sólo del claro: aquí el control es translúcido y se
     hunde sobre cualquier fondo, así que el campo ya se lee sin ayuda. */
  --ctl-filo:var(--ctl-borde);
  --ctl-filo-hover:var(--ctl-borde-hover);
  --app-check:rgba(255,255,255,.32);
  --app-paper:#28282c;
  /* En oscuro la hoja baja al tono de la barra en vez de quedarse en papel:
     #252528 contra #28282c separa el diálogo de lo que tiene detrás, y el
     control —translúcido— se hunde sobre él. */
  --dlg-hoja-bg:var(--app-sidebar-bg);
  --app-shadow:rgba(0,0,0,.55);
  --app-halo:rgba(0,0,0,.5);
  --app-velo:rgba(0,0,0,.62);
  --app-group:rgba(255,255,255,.045);
  --app-amber-tint:rgba(255,214,10,.12);
  --app-accent-tint:color-mix(in srgb,var(--app-accent) 18%,transparent);
  /* El teal de la serie sí se aclara: el #4bc0c0 del tema claro cae a 2.4:1
     contra el fondo oscuro y la barra se lee como una mancha. Es el mismo
     verde-agua que el sistema ya usa de tinta informativa en oscuro. */
  --graf-serie:rgba(45,212,212,.28);
  --graf-borde:#2dd4d4;
  /* Y los dos del flujo por el MISMO motivo, no por simetría: el #4caf50 da
     2.6:1 contra el fondo oscuro y el #e17a00 2.4:1 — los dos se leen como
     mancha, igual que le pasaba al teal. Suben al verde y al ámbar-naranja que
     el sistema ya usa en oscuro para exito y atención, así que el par sigue
     diciendo lo mismo (entra / sale) con la luz de este tema. */
  --graf-ingreso-serie:rgba(74,222,128,.28);
  --graf-ingreso-borde:#4ade80;
  --graf-egreso-serie:rgba(251,146,60,.28);
  --graf-egreso-borde:#fb923c;
  /* Los `--badge-*` NO se redefinen aquí, ni `--app-row-selected`: ése es el
     punto entero. Un solo valor que se mezcla con el fondo del tema. */
}

/* ── Reset ──────────────────────────────────────────────────────────
   Sustituye al Reboot de Bootstrap, que se retiró el 2026-08-08. Bootstrap
   estaba cargado desde CDN (~300 KB entre CSS y JS) sin que el marcado usara
   una sola de sus clases ni un solo componente de su JS — pero su reset SÍ
   sostenía la app, y estas tres declaraciones son exactamente lo que aportaba.
   Medido: con ellas, 17 de 17 medidas de la sábana, la barra y el pie salen
   idénticas al píxel; sin ellas, la app se descuadra.

   Las tres, y qué rompe quitar cada una:

   · box-sizing — la barra lateral pasa de 260 a 281px (los 20px de padding se
     suman por fuera) y arrastra el ancho de toda la rejilla. La ALTURA DE FILA
     también se va a 37px, y de ahí sale la aritmética índice×altura del scroll
     virtual: un píxel de más y la ventana visible deja de cuadrar.
   · [hidden] con !important — el atributo `hidden` del UA no lleva
     !important, así que cualquier `.clase{display:flex}` lo gana. Sin esta
     línea, .alta-rapida y los dos .estado-vacio se PINTAN estando ocultos y le
     comen 350px a la sábana. Es el fallo más caro y el menos evidente.
   · line-height del body — sin él las alturas heredadas pasan a `normal`, que
     cada familia resuelve distinto. Con Geist eso mueve barras y celdas. */
*,::before,::after{box-sizing:border-box}
[hidden]{display:none!important}

/* ── El corte de monitor Full HD ───────────────────────────────────────
   Dos clases que muestran una pieza sólo en un lado del corte de 1800px de
   viewport. Por encima está el monitor Full HD (1920); por debajo, toda
   laptop: MacBook Air 13" da 1440–1470, el de 15" 1710 y el MacBook Pro 16"
   1728. El corte va en 1800 y no en 1920 para dejar margen a un navegador
   Full HD que no está maximizado. Un Windows Full HD escalado al 125% da
   1536 y cae en el lado de laptop, y es correcto: lo que decide es el
   espacio, no el monitor.

   Uso típico: el rótulo largo y el corto del mismo botón, uno junto al otro
   (`asistencia.php`, «Registrar checada» / «Registrar»).

   Las dos clases sólo OCULTAN y nunca declaran un `display` visible: así la
   pieza conserva el suyo (flex, inline, block). `!important` por la misma
   razón que [hidden]: si no, un `.clase{display:flex}` gana la ocultación. */
@media (max-width:1799.98px){.solo-fhd{display:none!important}}
@media (min-width:1800px){.bajo-fhd{display:none!important}}

/* Las flechas del periodo de `asistencia.php`, sólo de ese módulo (no es
   pieza del catálogo). En laptop (769–1799) se esconden mientras la barra
   lateral está expandida y vuelven al contraerla: con la barra ancha el
   titular no tiene sitio para las flechas junto a los cuatro botones. Fuera
   del rango no se tocan: en Full HD cabe todo, y en móvil la barra es un
   cajón que no ocupa ancho. En 769–1024 la barra ya está compacta a la
   fuerza, así que ahí las flechas siempre se ven. Con `:has()` el cambio es
   inmediato al pulsar el botón de la barra, sin JS ni recarga. */
@media (min-width:769px) and (max-width:1799.98px){
  .app-shell:has(> .app-sidebar:not(.compacta)) .as-flecha-periodo{display:none}
}

/* ── La barra de scroll ────────────────────────────────────────────────
   Global desde el 2026-08-13 (pedido de Arif). Antes la dibujaba el navegador
   con su tema claro EN LOS DOS TEMAS: una franja blanca de 15px, y en oscuro
   se leía como un error. Vivió acotada al hilo del chat —donde caía junto al
   doodle oscuro y era imposible de ignorar— con una nota que decía que
   hacerlo global era «su propia tarea». Ésta es esa tarea.

   ── SON DOS MECANISMOS Y NO SE PUEDEN MEZCLAR ──
   Chromium ignora los `::-webkit-scrollbar` en cuanto el elemento declara
   `scrollbar-width` o `scrollbar-color` con valor distinto de `auto` — las
   propiedades estándar ganan. Medido en Chrome 151 sobre una caja de prueba:
   sólo pseudos → canal de 6px (el diseño); pseudos + `scrollbar-width:thin` →
   11px (native thin, los pseudos muertos); sin nada → 15px.

   Por eso el estándar va acotado a quien NO tiene los pseudos —Firefox— con
   una detección de capacidad, y no con un `@-moz-document` ni un sniff. En
   Chrome el `@supports` da falso y los pseudos siguen mandando; en Firefox da
   cierto y se lleva `thin` + los mismos colores. Mismo resultado por dos
   caminos, ninguno pisando al otro.

   `color-scheme` es la otra mitad y NO es opcional: es lo que le dice al
   navegador con qué tema pintar lo que sigue siendo suyo —el desplegable de un
   `select`, el calendario de un `date`, el resaltado de autocompletado y la
   barra de scroll de Firefox—. Sin él, el tema oscuro se rompía en cuanto
   aparecía un widget nativo. Es el cambio de mayor alcance de esta pasada, y
   el motivo de que la nota del chat lo dejara para una tarea propia. */
:root{color-scheme:light}
[data-theme="dark"]{color-scheme:dark}

/* El pulgar reusa `--ctl-borde-hover` —el filo con que un control dice «esto
   se puede tocar»—, que es literalmente lo que un pulgar de scroll es, y es el
   valor que ya usaba el chat. Al apuntarlo sube al terciario opaco: se agarra
   con el ratón, así que tiene que responder. 6px y radio 3: la barra es una
   pista, no un control con el que se navegue. El canal va TRANSPARENTE —el
   fondo lo pone la superficie de debajo, así que la misma regla vale sobre el
   papel, sobre la barra lateral y sobre el doodle del chat—. La `height` es
   para las barras HORIZONTALES (la sábana tiene las dos) y el `corner` es el
   cuadrito donde se cruzan, que sin esto el navegador pinta gris. */
/* EL ANCHO ES UN TOKEN porque lo consume alguien más (2026-08-28). Esta barra
   es CLÁSICA, no overlay: se come su ancho del contenido en cuanto un molde
   desborda, así que todo cuerpo con scroll propio tiene que RESERVARLO
   (`scrollbar-gutter:stable`) y descontarlo de su costado — si no, la misma
   página cambia de canto derecho el día que le crecen los datos. Pasó: con el
   6 escrito sólo aquí, funnels y automatización cerraban en 26 y venta, items,
   cuenta y agenda en 20, con el mismo CSS. Ver `.consola`. */
:root{--canal-scroll:6px}
::-webkit-scrollbar{width:var(--canal-scroll);height:var(--canal-scroll)}
/* ── EL CANAL LLEVA PISTA (2026-09-20, hipótesis de Arif, confirmada) ──────
   Nació transparente para que una sola regla valiera sobre cualquier
   superficie. Pero en el claro la hoja del diálogo, el papel del panel y la
   consola son blanco puro, y un canal transparente sobre blanco ES blanco: el
   pulgar flotaba sin pista y el tramo del canal que no cubre era
   indistinguible de la hoja. Arif lo vio primero en el diálogo y luego en
   todos los scrolls del claro — y es el mismo síntoma, porque la regla es una.

   La pista es TRANSLÚCIDA, así que respeta el motivo original —se resuelve
   contra el papel, la barra lateral o el doodle sin un color por superficie—
   y queda en el escalón que faltaba: hoja → canal → pulgar (30.5%) → hover
   (terciario). Chrome no pinta el track cuando el cuerpo no desborda aunque
   el canal esté reservado con `scrollbar-gutter:stable`, así que el diálogo
   corto sigue sin franja.

   TOKEN PROPIO Y NO `--app-chip`: la primera versión reusó el chip y en
   oscuro (blanco al 8% sobre la hoja nocturna) se leía bien, pero en claro
   (tinta al 6.4% sobre blanco, ~1.1:1) Arif lo vio «casi imperceptible». El
   claro necesita más veladura que el oscuro para el mismo efecto, y el chip
   tiene sus propios lectores. 16% de tinta sobre blanco ≈ #E1DFDB, ~1.3:1:
   se ve como pista y sigue tres escalones por debajo del pulgar (2.31:1). */
:root{ --scroll-canal:color-mix(in srgb,var(--app-tinta) 16%,transparent) }
[data-theme="dark"]{ --scroll-canal:var(--app-chip) }
::-webkit-scrollbar-track{background:var(--scroll-canal);border-radius:var(--r-mini)}
/* ── EL PULGAR TIENE TOKEN PROPIO (2026-09-20) ────────────────────────────
   Venía en `--ctl-borde-hover` —25.4% de tinta, #CECCC6 sobre blanco— que da
   **1.61:1** contra el papel. Para comparar: una línea de la reja da 1.44:1.
   O sea que el pulgar apenas se distinguía de un filo de tabla, y el canal de
   6px que la cabecera reserva se leía como aire muerto. Lo cazó Arif en
   `asistencia.php`: «queda un espacio tipo aire de lo que se supone es el
   scroll, pero el scroll nunca llega ahí». Llegaba; no se veía.

   45% DE TINTA = #A9A59B, 2.31:1. Sigue por debajo del hover
   (`--app-fg-tertiary`, 5.3:1), así que el gesto conserva su escalón.

   TOKEN PROPIO Y NO SUBIR `--ctl-borde-hover`, que tiene ONCE lectores —filos
   de control, cantos de tarjeta, bordes punteados—: la lección del filo del
   campo, el mismo día. Aquí los lectores son dos, el pulgar y su respaldo
   estándar.

   SÓLO EL CLARO, instrucción de Arif para esta tanda: en oscuro el pulgar es
   un aclarado sobre fondo nocturno y ya se lee, así que el tema lo devuelve
   al token de siempre.

   ── BAJÓ AL PUNTO MEDIO (2026-09-21, pedido de Arif) ──
   El 45% se leía demasiado oscuro para una pista. Arif fijó la regla: el
   pulgar va a la mitad entre el canal y el pulgar anterior, o sea
   (16% + 45%) / 2 = **30.5% de tinta** = #C5C2BB sobre blanco, 1.78:1 contra
   el papel y 1.33:1 contra el canal. Sigue sin leer ningún token de filo:
   el borde del campo es `--ctl-filo` (75%) y no tiene nada que ver aquí. */
:root{ --scroll-pulgar:color-mix(in srgb,var(--app-tinta) 30.5%,transparent) }
[data-theme="dark"]{ --scroll-pulgar:var(--ctl-borde-hover) }
::-webkit-scrollbar-thumb{background:var(--scroll-pulgar);border-radius:var(--r-mini)}
::-webkit-scrollbar-thumb:hover{background:var(--app-fg-tertiary)}
::-webkit-scrollbar-corner{background:transparent}
@supports (scrollbar-width:thin) and (not selector(::-webkit-scrollbar)){
  *{scrollbar-width:thin;scrollbar-color:var(--scroll-pulgar) var(--scroll-canal)}
}

/* ── Base ───────────────────────────────────────────────────────────── */
html,body{height:100%}
body{
  margin:0;
  font-family:var(--fuente);
  /* Los dos valores base que ponía el Reboot. 16px no se ve casi en ninguna
     parte —la app vive a 14— pero es el rem del que cuelgan los que sí. */
  font-size:1rem;
  line-height:1.5;
  /* Cifras tabulares en TODA la app: con teléfonos y fechas en columna, que
     los dígitos no bailen de ancho importa más que la familia elegida. */
  font-variant-numeric:tabular-nums;
  background:var(--app-bg);
  color:var(--app-fg);
  -webkit-font-smoothing:antialiased;
  overflow:hidden;
}
/* EL ENLACE: LETRA DEL CUERPO Y SUBRAYADO SIEMPRE (2026-08-16, decisión de
   Arif). Fue azul y sin subrayar —o sea que sólo el color lo delataba, y ese
   color era el mismo que en este sistema significa «esto escribe en la base»—.
   Al mismo tiempo `.enlace` pintaba de azul CON su flecha, así que un enlace
   se veía de dos maneras según llevara clase o no; ése fue el síntoma que lo
   destapó (funnels.php: «veo anchors color primary y sin el subrayado»).

   Con esto **el azul deja de tener excepciones**: era la última, y estaba
   declarada como tal en las reglas («convención de plataforma, no significado
   de marca»). Hoy el acento es SÓLO de lo que escribe, sin asteriscos.

   El subrayado va PERMANENTE porque pasa a ser la única señal, y es la señal
   original del enlace —anterior al azul— así que nadie tiene que aprenderla.
   En reposo lo dibuja un filo tenue (`--app-borde-muted`) para que un párrafo
   con tres enlaces no se lea como un texto rayado; al apuntarlo sube al color
   de la letra. El `offset` de 2px lo despega de las bajas de la Geist. */
a{
  color:inherit;
  text-decoration:underline;
  text-decoration-color:var(--app-borde-muted);
  text-decoration-thickness:1px;
  text-underline-offset:2px;
  transition:text-decoration-color var(--app-dur) var(--app-ease);
}
a:hover{text-decoration-color:currentColor}
input::placeholder{color:var(--app-fg-tertiary)}
::selection{background:var(--app-accent-tint)}

/* Foco para lo que NO declara el suyo: filas, opciones de menú, chips. Los
   controles de formulario dibujan su propio anillo interior — ver la página
   de catálogo (ui.php) para el inventario de los dos lenguajes de foco que
   hoy conviven. */
:focus-visible{outline:2px solid var(--app-accent);outline-offset:1px}
/* Aquí vivían cuatro reglas más (.table, .card, .btn, .form-control) cuyo
   único trabajo era deshacer los estilos de Bootstrap. Se fueron con él: sin
   la hoja que neutralizaban, no neutralizan nada. */

/* ── Iconos ──────────────────────────────────────────────────────────────
   Dibujados en casa, no importados (2026-08-08). Cada icono es un SVG en
   retícula de 14px en data-URI usado como MÁSCARA (se pinta a 16 — regla de
   tamaño en `.icn`) con `currentColor` — el mismo truco de la flecha del
   select y del icono del enlace, ahora con nombre y catálogo (ver la
   sección Iconos de ui.php). Lo que compra:
   · toma el color del texto que acompaña, así que sobrevive al tema oscuro
     y a cualquier recoloreo sin variantes;
   · cero librerías (Font Awesome y compañía son de otra época) y cero
     peticiones;
   · NO son emojis a propósito: el emoji lo dibuja el sistema operativo —
     ignora el color del tema, cambia de forma en cada plataforma y pesa
     distinto en cada fila.
   Trazo 1.5 redondeado, el de la casa. `.icn` SIEMPRE va con su
   modificador: sin él no hay máscara y se pinta un cuadrado sólido. */
:root{
  --icn-estrella:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7 1.6l1.6 3.3 3.6.5-2.6 2.6.6 3.6L7 9.9l-3.2 1.7.6-3.6L1.8 5.4l3.6-.5z'/%3E%3C/svg%3E");
  --icn-lista:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.2 4h9.6M2.2 7h9.6M2.2 10h6.4'/%3E%3C/svg%3E");
  --icn-importar:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7 1.8v6.4M4.2 5.4L7 8.2l2.8-2.8M2.2 11.8h9.6'/%3E%3C/svg%3E");
  /* LA LUPA (2026-09-09, con el buscador global de registros). Es el único
     glifo del juego con semántica de BÚSQUEDA, y nació porque no había
     ninguno: `ojo` ya significa «ver/previsualizar», `lista` son las tres
     rayas que dicen «esto es una lista» —el error que `icn-persona` vino a
     corregir en el selector de catálogo— y `entrada` es la flecha de entrar.
     Círculo y mango a 45°, el dibujo que no hay que aprender. */
  --icn-lupa:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='6' cy='6' r='4'/%3E%3Cpath d='M8.9 8.9L12.1 12.1'/%3E%3C/svg%3E");
  --icn-papelera:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.2 3.9h9.6M5.5 3.9V2.3h3v1.6M3.5 3.9l.5 7.8h6l.5-7.8'/%3E%3C/svg%3E");
  /* LA LUNA, DE CONTORNO como todo el juego (2026-08-16, lo cazó Arif). Nació
     MACIZA —`fill` sin `stroke`— y era el único glifo relleno del censo: junto
     al sol, que sí es de trazo, se leían como dos dibujos de sets distintos.
     Misma silueta, misma retícula; sólo cambia quién la pinta: el trazo de 1.5
     en vez del relleno. */
  /* SALIR (2026-08-29): puerta abierta con la flecha SALIENDO por ella. La
     flecha va hacia fuera y no hacia dentro —el mismo dibujo invertido dice
     «entrar»— y el marco se abre por el lado por el que se sale, que es lo
     único que distingue las dos siluetas de un vistazo. */
  --icn-salir:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5.4 12.2H2.9a.9.9 0 0 1-.9-.9V2.7a.9.9 0 0 1 .9-.9h2.5'/%3E%3Cpath d='M8.8 9.9 11.7 7 8.8 4.1'/%3E%3Cpath d='M11.7 7H5.2'/%3E%3C/svg%3E");
  --icn-luna:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M11.7 8.4A5.2 5.2 0 1 1 5.6 2.3a4.1 4.1 0 0 0 6.1 6.1z'/%3E%3C/svg%3E");
  --icn-sol:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='7' cy='7' r='2.6'/%3E%3Cpath d='M7 .9v1.8M7 11.3v1.8M.9 7h1.8M11.3 7h1.8M2.7 2.7l1.3 1.3M10 10l1.3 1.3M11.3 2.7L10 4M4 10l-1.3 1.3'/%3E%3C/svg%3E");
  /* LA PANTALLA nace el 2026-09-11 con su consumidor: la opción «Sistema» del
     selector de tema. El juego no tenía ninguno que dijera «lo que decida el
     equipo», y las dos alternativas eran peores — media luna partida (dice
     «entre claro y oscuro», que no es lo mismo que «lo que diga el sistema») o
     un engrane, que en esta app significaría configuración y ya la significa la
     sección entera. El monitor es la convención de las apps que traen el modo, y
     aquí además queda como el HERMANO IMPAR de luna y sol: los tres son objetos
     dibujados, no metáforas de estado. */
  --icn-pantalla:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='1.8' y='2.4' width='10.4' height='7.4' rx='1.2'/%3E%3Cpath d='M5.4 12.2h3.2M7 9.8v2.4'/%3E%3C/svg%3E");
  --icn-volver:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M11.8 7H2.2M5.6 3.6L2.2 7l3.4 3.4'/%3E%3C/svg%3E");
  --icn-entrada:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5.2 2.2h4.6a1.4 1.4 0 0 1 1.4 1.4v6.8a1.4 1.4 0 0 1-1.4 1.4H5.2M1.6 7h6.2M5.4 4.6L7.8 7 5.4 9.4'/%3E%3C/svg%3E");
  --icn-calendario:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='2' y='3.2' width='10' height='8.6' rx='1.2'/%3E%3Cpath d='M2 6.2h10M4.7 1.8v2.4M9.3 1.8v2.4'/%3E%3C/svg%3E");
  --icn-caja:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='1.8' y='2.6' width='10.4' height='2.6' rx='.6'/%3E%3Cpath d='M2.9 5.2v5.2a1 1 0 0 0 1 1h6.2a1 1 0 0 0 1-1V5.2M5.6 7.4h2.8'/%3E%3C/svg%3E");
  /* La NOTA con su corte dentado (2026-08-18): el glifo de Ventas. Nace propio
     y no reusa `icn-caja` porque ésa ya es «Catálogo», y dos filas del mismo
     menú con el mismo dibujo se leen como un error de copiado — el icono de la
     barra es lo único que queda cuando está en modo compacto. */
  --icn-venta:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.2 12.4V2.6a.6.6 0 0 1 .6-.6h6.4a.6.6 0 0 1 .6.6v9.8l-1.5-.9-1.4.9-1.4-.9-1.4.9z'/%3E%3Cpath d='M5.3 4.9h3.4M5.3 7.3h3.4'/%3E%3C/svg%3E");
  /* LA TABLILLA CON PARTIDAS, para Presupuestos (2026-09-04). Sustituye a
     `icn-lista` —las tres rayas—, que era genérico: nombraba «una lista» y no
     el documento que la fila abre. Y su vecina de abajo es Ventas, o sea la
     nota dentada: dos documentos apilados, que es justo el choque que esta
     regla persigue. **Lo que los separa a 14px es dónde está la seña**: la
     nota la lleva ABAJO (el corte dentado) y la tablilla ARRIBA (la pinza).
     Una silueta se lee por su contorno, y estos dos contornos difieren en el
     canto que el ojo mira primero.

     SIN LAS VIÑETAS de la pieza original de la que sale (`clipboard-list`
     dibuja un punto delante de cada renglón). A 14px un punto se empasta con
     el trazo de 1.5 —la misma medición que ya obligó a que la perforación de
     `icn-etiqueta` fuera un círculo de r=.9 y no un punto lleno—, así que
     sumaría ruido sin sumar forma: la pinza ya dice que es una tablilla y los
     dos renglones, que lleva partidas. */
  --icn-tablilla:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='4.7' y='1.9' width='4.6' height='2.4' rx='.7'/%3E%3Cpath d='M9.3 3.5h1.1a1.2 1.2 0 0 1 1.2 1.2v7.6a1.2 1.2 0 0 1-1.2 1.2H3.6a1.2 1.2 0 0 1-1.2-1.2V4.7a1.2 1.2 0 0 1 1.2-1.2h1.1'/%3E%3Cpath d='M4.8 7.3h4.4M4.8 9.9h4.4'/%3E%3C/svg%3E");
  /* LA BOLSA DE COMPRA, para Gastos (2026-09-04). Es el glifo NUEVO del
     cambio en que el billete se mudó a Registros (abajo), y se eligió por
     descarte razonado sobre la regla de esta sección — a 14px sólo sobrevive
     la silueta, y ninguna fila vecina puede repetirla:

     · el COMPROBANTE (el ticket del proveedor) es lo más exacto que hay y
       está vetado desde el 2026-08-27: la nota dentada de Ventas ya ES esa
       silueta, y el gasto es su vecino;
     · la CARTERA y la TARJETA son rectángulos horizontales, o sea el billete
       que queda una fila más abajo;
     · el BILLETE CON FLECHA sería repetir el dibujo con una variante, que es
       exactamente lo que esta sección lleva prohibiendo desde que nació.

     Queda el objeto que ninguna de las tres nombra: **lo que se compró**. Dos
     formas y nada más: el cuerpo, que se estrecha hacia abajo, y el asa.

     EL ASA VA POR FUERA DEL CUERPO, y eso NO es la forma de la pieza original
     —`shopping-bag` de Lucide dibuja la bolsa cerrada, con el asa como un arco
     DENTRO y los hombros angulados—. Se cambió por una medición en el
     navegador, no por gusto: a 14px la versión cerrada pierde los hombros y se
     queda en **un cuadrado redondeado con una curva dentro**, que es a un pelo
     de `icn-caja` (Catálogo) — o sea el choque que esta sección entera existe
     para evitar, cometido contra una fila que está a dos de distancia. Con el
     asa asomando arriba la silueta no se parece a ninguna otra del juego, que
     es lo único que se le pide a un glifo a este tamaño.

     Su riesgo está declarado y es de vocabulario, no de dibujo: la bolsa sabe
     a comercio, y el gasto de una agencia es renta y nómina. Se aceptó porque
     a 14px lo que se pide de un glifo es que se distinga de sus vecinos, y
     éste es el único candidato que lo hace sin mentir. */
  --icn-bolsa:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.5 5.1h9l-.7 6.4a1.1 1.1 0 0 1-1.1 1H4.3a1.1 1.1 0 0 1-1.1-1Z'/%3E%3Cpath d='M4.9 5.1V3.7a2.1 2.1 0 0 1 4.2 0v1.4'/%3E%3C/svg%3E");
  /* EL BILLETE, para REGISTROS desde el 2026-09-04 — nació el 2026-08-27 como
     el glifo de Gastos y por eso se llamaba `--icn-gasto`. **Cambió de nombre
     al cambiar de fila**, y ésa es la regla que deja: un token que se llama
     como un módulo miente el día que el dibujo se muda, así que los que
     pueden mudarse se nombran por lo que DIBUJAN. Pedido de Arif.

     POR QUÉ AQUÍ FUNCIONA MEJOR: Registros es el libro de caja, o sea la
     tabla donde el dinero de verdad entra y sale. El billete nombra el objeto
     de ese módulo directamente, mientras que en Gastos nombraba una
     consecuencia («dinero que sale»). Lo que se pierde es el argumento con el
     que nació —dos formas, rectángulo y círculo, que sobreviven a la máscara a
     este tamaño—, y no se pierde: el dibujo no se tocó, sólo su dueño.

     LO QUE ESTABA AQUÍ Y SE FUE: `--icn-flujo`, las dos flechas encontradas
     que dibujaron Registros del 2026-08-27 al 2026-09-04. Se retiró porque el
     billete se llevó a sus CINCO consumidores —la fila del menú, las dos
     gráficas del flujo y sus dos cifras del Inicio— y un icono que nadie pide
     es código muerto (mismo desenlace que `--icn-arena`, arriba).

     Lo que sí vale conservar es su DECISIÓN, porque volverá a aplicar el día
     que una fila del bloque de dinero pida glifo: **dibujaba una ACCIÓN y no
     un objeto** —entra por arriba, sale por abajo—, que era su forma de no
     competir por el mismo espacio de 14px con el documento y el billete. El
     path, si vuelve: `M2.4 4.6h9.2M9.2 2.2l2.4 2.4-2.4 2.4M11.6 9.4H2.4M4.8
     7L2.4 9.4l2.4 2.4`. */
  --icn-billete:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='1.4' y='3.4' width='11.2' height='7.2' rx='1.2'/%3E%3Ccircle cx='7' cy='7' r='1.8'/%3E%3C/svg%3E");
  /* AQUÍ VIVIÓ `--icn-arena` unas horas el 2026-08-30: el reloj de arena que
     dibujó la fila «Cartera» de la barra, retirada el mismo día (el porqué, en
     `interno/cabecera.php`). Se fue con ella porque era su ÚNICO consumidor —la
     tarjeta del Inicio dibuja ese reloj con un `<svg>` inline en el lienzo de 24
     que usan todas las de la portada, no con este token de 14—, y un icono que
     nadie pide es código muerto.

     Lo que sí vale la pena conservar es la DECISIÓN del dibujo, porque volverá a
     aplicar el día que la cartera —u otra pantalla del tiempo— pida su glifo:
     **se dibuja el tiempo y no el dinero.** Sus vecinas del nivel comercial ya
     nombran cosas de dinero (el documento, el billete, las flechas del
     movimiento) y un cuarto objeto de dinero no diría cuál es; lo que la cartera
     aporta sobre ellas es la EDAD de lo que falta cobrar. Y no habría chocado
     con `--icn-reloj` (Agenda) aunque los dos midan tiempo: a 14px la silueta
     manda, y un círculo con manecillas y dos triángulos opuestos no se
     confunden. El path, si vuelve: el SVG de la tarjeta en `index.php`. */
  --icn-palomita:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.4 7.6l3.1 3.1 6.1-7.2'/%3E%3C/svg%3E");
  /* ETIQUETA (2026-09-03): el glifo del chip de etiqueta. Se dibuja a 14 como
     todos —la retícula de la casa— y el chip lo pinta a 12. La perforación es
     un círculo de r=.9 y no un punto lleno: a 12px un punto se empasta con el
     trazo y el glifo pierde su silueta, que es lo único que se lee a ese
     tamaño. Nació con la propuesta C del canvas de diseño, para separar la
     etiqueta del BADGE DE ESTADO, con el que comparte radio, tinte y medida. */
  --icn-etiqueta:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M1.9 6.1V2.5c0-.3.3-.6.6-.6h3.6l6 6a.9.9 0 0 1 0 1.2l-3.5 3.5a.9.9 0 0 1-1.2 0L1.9 6.1Z'/%3E%3Ccircle cx='4.5' cy='4.5' r='.9'/%3E%3C/svg%3E");
  --icn-arriba:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7 11.6V3M3.2 6.8L7 3l3.8 3.8'/%3E%3C/svg%3E");

  --icn-columnas:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='1.8' y='2.4' width='10.4' height='9.2' rx='1'/%3E%3Cpath d='M5.3 2.4v9.2M8.7 2.4v9.2'/%3E%3C/svg%3E");
  /* Barra: el glifo universal de «barra lateral» (VS Code, Linear, claude.ai)
     — el marco de la pantalla con su carril izquierdo marcado. Es primo de
     `--icn-columnas` y por eso se dibujó DISTINTO a propósito: aquél lleva dos
     divisiones (una tabla) y éste una sola, pegada al borde (un carril). */
  --icn-barra:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='1.8' y='2.4' width='10.4' height='9.2' rx='1.4'/%3E%3Cpath d='M5.4 2.4v9.2'/%3E%3C/svg%3E");
  /* Sobre: el icono de todo lo que el sistema manda solo. */
  /* Reloj (Agenda: el horario es su alma) y rayo (Automatización): nacieron el
     2026-08-15 porque la barra repetía glifos — Agenda clonaba el calendario de
     Citas y Automatización cargaba el sobre de Envíos, que además no dice
     "automático" (corrección de Arif). */
  --icn-reloj:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='7' cy='7' r='5.4'/%3E%3Cpath d='M7 4.2V7l2 1.3'/%3E%3C/svg%3E");
  /* ASISTENCIA Y PRENÓMINA (2026-09-18, pedido de Arif): los dos glifos que
     usa la industria para estos módulos, dibujados en la retícula de 14 y el
     trazo de 1.5 del juego.
     · Reloj con palomita — el `clock-check` de Lucide; Material Symbols dice
       lo mismo con `punch_clock`, el reloj checador. Asistencia llevaba
       `--icn-entrada`, la puerta con flecha, que es el glifo universal de
       iniciar sesión y el espejo exacto de `--icn-salir` en el pie de la
       misma barra. El reloj solo ya es de Citas: la palomita lo distingue y
       dice «jornada registrada». El círculo se abre abajo a la derecha para
       dejar sitio a la palomita; las manecillas son las de `--icn-reloj`.
     · Mano con monedas — el `hand-coins` de Lucide, que su catálogo etiqueta
       con *wage* y *payroll*. Prenómina llevaba `--icn-billete`, el de
       Registros, y las dos filas coinciden en toda cuenta con los dos flags.
       Se probó con una sola moneda y se descartó: la moneda sobre la mano se
       lee como una persona. */
  --icn-reloj-palomita:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12.38 6.53A5.4 5.4 0 1 0 6.53 12.38'/%3E%3Cpath d='M7 4.2V7l2 1.3'/%3E%3Cpath d='M8.4 11.1l1.5 1.5 3-3'/%3E%3C/svg%3E");
  --icn-mano-monedas:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6.42 8.75h1.17a1.17 1.17 0 1 0 0-2.33H5.83c-.35 0-.64.12-.82.35L1.75 9.92'/%3E%3Cpath d='M4.08 12.25l.93-.82c.18-.23.47-.35.82-.35h2.33c.64 0 1.23-.23 1.63-.7l2.68-2.57a1.17 1.17 0 0 0-1.6-1.7l-2.45 2.28'/%3E%3Cpath d='M1.17 9.33l3.5 3.5'/%3E%3Ccircle cx='9.33' cy='5.25' r='1.69'/%3E%3Ccircle cx='3.5' cy='2.92' r='1.75'/%3E%3C/svg%3E");
  --icn-rayo:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7.7 1.3 2.1 8.1h4.2l-.8 4.6 5.6-6.8H6.9z'/%3E%3C/svg%3E");
  /* ENVÍOS: EL AVIÓN, NO EL SOBRE (2026-08-16, lo cazó Arif). El sobre decía
     «correo» y esa cola ya no es de correo: por ahí salen también los mensajes
     de WhatsApp, y el día que entre un canal más seguirá siendo la misma cola.
     El avión de papel dice el VERBO —esto se manda— y no nombra el medio, que
     es exactamente lo que la pantalla lista. Y no choca con nadie: la burbuja
     ya es Conversaciones y el rayo es Automatización.
     Es el tercer glifo que se corrige por nombrar el medio en vez del trabajo
     —el rayo y el reloj fueron los otros dos, el 2026-08-15—, así que el sobre
     se retira del juego: se quedaba sin un solo consumidor. */
  --icn-envio:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12.8 1.2L8.75 12.8 6.4 7.6 1.2 5.25z'/%3E%3Cpath d='M12.8 1.2L6.4 7.6'/%3E%3C/svg%3E");
  --icn-panel:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='1.8' y='1.8' width='4.6' height='4.6' rx='.8'/%3E%3Crect x='7.6' y='1.8' width='4.6' height='4.6' rx='.8'/%3E%3Crect x='1.8' y='7.6' width='4.6' height='4.6' rx='.8'/%3E%3Crect x='7.6' y='7.6' width='4.6' height='4.6' rx='.8'/%3E%3C/svg%3E");
  --icn-tablero:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='1.8' y='2.2' width='3.1' height='9.6' rx='.8'/%3E%3Crect x='5.9' y='2.2' width='3.1' height='6.4' rx='.8'/%3E%3Crect x='10' y='2.2' width='2.2' height='4.2' rx='.8'/%3E%3C/svg%3E");
  /* Burbuja de conversacion (conversaciones.php). Es la unica silueta que
     dice "alguien esta hablando" sin repetir el sobre del correo, que ya
     ocupan Envios y Automatizacion. */
  --icn-chat:url("data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 width=%2714%27 height=%2714%27 fill=%27none%27 stroke=%27%23000%27 stroke-width=%271.5%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27%3E%3Cpath d=%27M12.4 8.2c0 .9-.8 1.7-1.7 1.7H5.4L2.6 12V3.8c0-.9.8-1.7 1.7-1.7h6.4c.9 0 1.7.8 1.7 1.7z%27/%3E%3C/svg%3E");
  --icn-cita:url("data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 width=%2714%27 height=%2714%27 fill=%27none%27 stroke=%27%23000%27 stroke-width=%271.5%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27%3E%3Crect x=%272%27 y=%273%27 width=%2710%27 height=%279.5%27 rx=%271.5%27/%3E%3Cpath d=%27M2 6h10M4.8 1.6v2.2M9.2 1.6v2.2%27/%3E%3C/svg%3E");
  --icn-funnel:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2 2.6h10L8 7.4v3.9l-2 1.1V7.4z'/%3E%3C/svg%3E");
  /* IDENTIDAD — los dos glifos que estrenaron el alta de cuentas y usuarios
     (2026-08-16). Se dibujan y no se toman prestados por la misma razón que el
     reloj y el rayo el 2026-08-15: la barra repetía siluetas y una fila que
     comparte glifo con otra deja de decir a dónde lleva.

     · PERSONAS para Usuarios — quién entra a ESTA cuenta. Dos figuras y no una:
       una sola es «mi perfil», y esta pantalla es el reparto del acceso.
     · EDIFICIO para Cuentas — el inquilino, no la persona. Es lo único del
       juego que dice «organización», que es exactamente lo que la pantalla de
       plataforma lista. No choca con el embudo (una campaña) ni con el panel
       (una rejilla de métricas). */
  /* `--icn-personas` vivió aquí del 2026-08-16 al 17: era el glifo de la fila
     «Usuarios» del menú, que se fundió con «Cuentas» en una sola fila
     (`icn-edificio`). Se retiró porque un icono sin consumidor es inventario
     que nadie declaró — ni siquiera estaba en el catálogo de `ui.php`, que es
     donde vive el inventario de verdad. Se dijo «vuelve de ahí si algún día
     hace falta», y **volvió el 2026-09-03 como `--icn-persona`**, en singular
     y con consumidor: la opción «Contactos» del selector de catálogo (pedido
     de Arif, que lo quería *«tipo user»* y no la lista de tres rayas con la
     que nació ese diálogo). Segundo consumidor desde el 2026-09-14: la fila
     de la cuenta en la barra, que lleva el nombre del usuario. El singular no es un detalle de nombre — aquel
     dibujaba DOS siluetas porque nombraba a los usuarios de la cuenta; éste
     dibuja una porque un contacto es una persona, y la sábana los lista de a
     uno. Entra al censo de `ui.php` en el mismo cambio, que es la otra mitad
     de la regla.

     ESA ES LA REGLA DEL JUEGO DE ICONOS, y la barrida del desmontaje la aplicó
     entera el 2026-09-02: se midió cada `--icn-*` contra sus consumidores
     reales y **doce quedaron sin ninguno**, pero los doce SÍ están en `ui.php`
     — o sea inventario declarado, que es exactamente lo que esta nota bendice.
     El único que además faltaba del catálogo era `--icn-escenario` (el punto
     dentro de un círculo), y por eso es el único que se fue con ella.

     EL CENSO, para rehacerlo (movido del catálogo `ui.php`, 2026-09-19): cada
     `--icn-*` de esta hoja contra sus consumidores en el PHP y el JS de la app —
       grep -oE '\-\-icn-[a-z0-9-]+' activos/app.css | sort -u
       grep -ohE 'icn-[a-z0-9-]+' -r . --include='*.php' --include='*.js' --exclude=ui.php | sort -u
     Los doce de la barrida del 2026-09-02 fueron los seis glifos de las listas
     del modo tareas (Entrada, Hoy, Programadas, Pendientes, Algún día,
     Registro), que en `ui.php` se describen por lo que dibujan y no por la fila
     que nombraban, más `icn-columnas`, `icn-importar` e
     `icn-prohibido` —las tres filas que movía `es_outreach`— y `icn-envio`,
     `icn-papelera` e `icn-volver`, que nunca tuvieron consumidor.
     `icn-estrella` recuperó uno el 2026-09-17: la opción «Colaboradores» del
     selector de catálogo; no se dibujó una estrella nueva para la fila.

     LA OTRA MITAD DE LA REGLA ES EL ORDEN DEL ALTA: un glifo nuevo entra por la
     lista de `ui.php`, no por la fila que lo estrena. El censo del 2026-08-15
     ya había encontrado cuatro que nacieron con sus filas de la barra sin pasar
     por esa lista. Después entraron al revés tres más y hubo que darlos de alta
     a destiempo: `icn-venta` e `icn-edificio` (nacidos con
     sus filas el 2026-08-16 y 18, dados de alta el 2026-08-27) e
     `icn-etiqueta` (tres consumidores vivos, encontrado sin fila el 2026-09-11
     al correr este censo). */
  --icn-persona:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='7' cy='4.6' r='2.4'/%3E%3Cpath d='M2.5 12.2c0-2.4 2-3.9 4.5-3.9s4.5 1.5 4.5 3.9'/%3E%3C/svg%3E");
  --icn-edificio:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.6 12.4V2.9c0-.4.3-.7.7-.7h4.8c.4 0 .7.3.7.7v9.5'/%3E%3Cpath d='M8.8 6.4h2.2c.4 0 .7.3.7.7v5.3'/%3E%3Cpath d='M1.5 12.4h11'/%3E%3Cpath d='M4.8 5.2h1.6M4.8 7.6h1.6'/%3E%3C/svg%3E");
  /* El ⋮ del botón de opciones de la etapa era el GLIFO tipográfico, y por eso
     se veía casi apagado al lado de cualquier icono de la barra lateral: la
     fuente lo dibuja con puntos de ~1.2px, la mitad del trazo con el que se
     pintan los demás (1.5 en retícula de 14, que a 18px sale ~1.9). Los puntos
     van RELLENOS y no de trazo porque a este tamaño un círculo hueco de 1.9 de
     borde es una mancha: el peso se consigue con el diámetro, no con el filo.
     Corrección de Arif, 2026-09-09: «asemejarlo al estilo bold del menú
     lateral». */
  --icn-elipsis:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='%23000'%3E%3Ccircle cx='7' cy='3.1' r='1.15'/%3E%3Ccircle cx='7' cy='7' r='1.15'/%3E%3Ccircle cx='7' cy='10.9' r='1.15'/%3E%3C/svg%3E");
  --icn-prohibido:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='7' cy='7' r='5.2'/%3E%3Cpath d='M3.4 3.4l7.2 7.2'/%3E%3C/svg%3E");
  --icn-info:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='7' cy='7' r='5.2'/%3E%3Cpath d='M7 6.6v3.2'/%3E%3Ccircle cx='7' cy='4.3' r='.85' fill='%23000' stroke='none'/%3E%3C/svg%3E");
  --icn-atencion:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7 2.1l5.7 9.4H1.3z'/%3E%3Cpath d='M7 5.7v2.4'/%3E%3Ccircle cx='7' cy='9.9' r='.85' fill='%23000' stroke='none'/%3E%3C/svg%3E");
  --icn-fallo:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='7' cy='7' r='5.2'/%3E%3Cpath d='M5.2 5.2l3.6 3.6M8.8 5.2L5.2 8.8'/%3E%3C/svg%3E");
  --icn-exito:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='7' cy='7' r='5.2'/%3E%3Cpath d='M4.4 7.2l1.9 1.9 3.3-3.9'/%3E%3C/svg%3E");
  --icn-ojo:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M1 7C2.7 4.6 4.7 3.3 7 3.3C9.3 3.3 11.3 4.6 13 7C11.3 9.4 9.3 10.7 7 10.7C4.7 10.7 2.7 9.4 1 7Z'/%3E%3Ccircle cx='7' cy='7' r='1.9'/%3E%3C/svg%3E");
  --icn-ojo-off:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M1 7C2.7 4.6 4.7 3.3 7 3.3C9.3 3.3 11.3 4.6 13 7C11.3 9.4 9.3 10.7 7 10.7C4.7 10.7 2.7 9.4 1 7Z'/%3E%3Ccircle cx='7' cy='7' r='1.9'/%3E%3Cpath d='M2.4 11.6L11.6 2.4'/%3E%3C/svg%3E");
  /* EL LÁPIZ — el gesto de EDITAR dentro de un chip (2026-09-11, para la franja
     de horario del riel de la Agenda). Es icono y no glifo tipográfico por la
     misma lección que dejó escrita `--icn-elipsis` tres bloques más arriba: el ✎
     de la fuente se dibuja con un trazo de la mitad de peso que el resto del
     juego y al lado de cualquier icono se ve apagado. La × puede permitírselo
     porque es un carácter que TODAS las fuentes dibujan igual de macizo; el
     lápiz no.
     Es el `edit-2` de Feather —cuerpo en diagonal, punta abajo a la izquierda,
     sin virola: a 14px una línea más es una mancha— llevado a la retícula de 14
     y centrado en ella (x e y de 2.25 a 11.75, que con el trazo de 1.5 deja
     1.5px de aire por lado). */
  --icn-lapiz:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9.75 2.25a1.45 1.45 0 1 1 2 2L5 11l-2.75.75L3 9z'/%3E%3C/svg%3E");
  /* AYUDA: EL SIGNO DE INTERROGACIÓN EN UN CÍRCULO (2026-09-22, fila «Ayuda»
     del pie de la barra). Es el `circle-help` de Lucide, el glifo que la
     industria usa para ayuda. Comparte el círculo de `--icn-info` (r 5.2) y
     su punto relleno para que los dos se lean de la misma familia; lo que los
     separa es el trazo de dentro: la «i» informa, la interrogación responde
     una pregunta. */
  --icn-ayuda:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='7' cy='7' r='5.2'/%3E%3Cpath d='M5.4 5.5a1.65 1.65 0 0 1 3.2.55c0 1.1-1.6 1.6-1.6 1.6'/%3E%3Ccircle cx='7' cy='9.9' r='.85' fill='%23000' stroke='none'/%3E%3C/svg%3E");
}
/* El tamaño BASE es el del icono que acompaña (`--icn-texto`, 16px contra
   texto de 14): asoma un píxel por encima y uno por debajo de la letra — a
   tamaño idéntico se ve más chico que el texto, porque la letra no llena su
   caja. El que MARCA una fila o un bloque sube a `--icn-fila` desde su propia
   pieza (menú, alerta). El dibujo sigue en retícula de 14; la máscara lo
   escala, y con él el trazo. */
.icn{
  display:inline-block;width:var(--icn-texto);height:var(--icn-texto);flex:none;
  background:currentColor;vertical-align:-3px;
  -webkit-mask:var(--icn) no-repeat center / contain;
          mask:var(--icn) no-repeat center / contain;
}
.icn-estrella{--icn:var(--icn-estrella)}
.icn-lupa{--icn:var(--icn-lupa)}
.icn-lista{--icn:var(--icn-lista)}
.icn-persona{--icn:var(--icn-persona)}
.icn-importar{--icn:var(--icn-importar)}
.icn-papelera{--icn:var(--icn-papelera)}
.icn-salir{--icn:var(--icn-salir)}
.icn-luna{--icn:var(--icn-luna)}
.icn-sol{--icn:var(--icn-sol)}
.icn-pantalla{--icn:var(--icn-pantalla)}
.icn-volver{--icn:var(--icn-volver)}
.icn-entrada{--icn:var(--icn-entrada)}
.icn-calendario{--icn:var(--icn-calendario)}
.icn-reloj{--icn:var(--icn-reloj)}
.icn-reloj-palomita{--icn:var(--icn-reloj-palomita)}
.icn-mano-monedas{--icn:var(--icn-mano-monedas)}
.icn-rayo{--icn:var(--icn-rayo)}
.icn-caja{--icn:var(--icn-caja)}
.icn-venta{--icn:var(--icn-venta)}
.icn-tablilla{--icn:var(--icn-tablilla)}
.icn-bolsa{--icn:var(--icn-bolsa)}
.icn-billete{--icn:var(--icn-billete)}
.icn-palomita{--icn:var(--icn-palomita)}
.icn-etiqueta{--icn:var(--icn-etiqueta)}
.icn-prohibido{--icn:var(--icn-prohibido)}
.icn-elipsis{--icn:var(--icn-elipsis)}
.icn-tablero{--icn:var(--icn-tablero)}
.icn-funnel{--icn:var(--icn-funnel)}
.icn-edificio{--icn:var(--icn-edificio)}
.icn-cita{--icn:var(--icn-cita)}
.icn-chat{--icn:var(--icn-chat)}
.icn-panel{--icn:var(--icn-panel)}
.icn-envio{--icn:var(--icn-envio)}
.icn-columnas{--icn:var(--icn-columnas)}
.icn-barra{--icn:var(--icn-barra)}
.icn-arriba{--icn:var(--icn-arriba)}
.icn-ojo{--icn:var(--icn-ojo)}
.icn-ojo-off{--icn:var(--icn-ojo-off)}
.icn-lapiz{--icn:var(--icn-lapiz)}
.icn-ayuda{--icn:var(--icn-ayuda)}
.icn-info{--icn:var(--icn-info)}
.icn-atencion{--icn:var(--icn-atencion)}
.icn-fallo{--icn:var(--icn-fallo)}
.icn-exito{--icn:var(--icn-exito)}

/* ── Marca ──────────────────────────────────────────────────────────── */
/* El logotipo vive en CUATRO sitios —la cabecera de la barra, la fila
   «Inicio», la puerta y el 404, más el nav del catálogo `ui.php`— y todos
   dicen lo mismo: una pieza, no cinco copias. (Decía TRES hasta el
   2026-08-26; el 404 entró el 08-18 y nadie actualizó la cuenta. El censo
   vivo: `grep -rn "marca-icono" public_html/`.)

   EL SÍMBOLO NO ES UN `.icn`. Los del catálogo de arriba son máscaras de un
   solo trazo que se pintan con `currentColor`; éste tiene DOS tintas (el
   anillo y el punto ámbar) y una máscara las fundiría en una. Va como imagen
   de fondo, y el tema elige el ARCHIVO en vez del color porque las dos
   versiones no se diferencian sólo en el anillo (navy → blanco): el ámbar
   también cambia de tono para sostenerse contra el fondo oscuro.

   ── SON LOS ARCHIVOS REALES DE CONTROLY DESDE EL 2026-08-26 ──────────────
   Hasta esa fecha esto apuntaba a `logo-controly-mark.svg` / `-w.svg`: un
   símbolo DIBUJADO A MANO —un `<circle>` con `stroke-dasharray` y otro de
   relleno ámbar— que aproximaba el de Controly porque el CRM nació sin los
   originales a la vista. Se veía parecido y no lo era: el trazo iba más fino
   y el hueco del anillo terminaba en otro punto.

   Hoy son `icono-controly.png` / `icono-controly-w.png`, los MISMOS archivos
   que Controly sirve desde `activos-base/img/logo/`, copiados aquí. Copiados
   y NO referenciados a través del árbol de Controly: el CRM es un proyecto
   aparte y una ruta cross-dominio muere en producción por `open_basedir`.
   Al actualizarlos en Controly hay que volver a copiarlos — es el precio de
   la frontera, y es el correcto.

   PNG y no SVG porque es lo que existe: convertirlos sería volver a dibujar
   una aproximación, que es justamente lo que este cambio retira. A 20 y 30px
   sobre pantalla retina la fuente de 512px va de sobra. Los retirados quedan
   en `~/PRE-plan/backups/removed/crm-logos-hechos-a-mano-2026-08-26/`. */
.marca-icono{
  display:inline-block;flex:none;
  width:var(--icn-fila);height:var(--icn-fila);
  background:url("img/logo/icono-controly.png") no-repeat center / contain;
}
[data-theme="dark"] .marca-icono{background-image:url("img/logo/icono-controly-w.png")}

/* ── EL LOGOTIPO COMPLETO ─────────────────────────────────────────────────
   `logo-controly.png` / `-w.png`: símbolo + «controly / online» en dos
   líneas, los mismos archivos que Controly sirve en SU login. Es lo que va
   en las pantallas que se ven SIN cascarón —la puerta, el 404 y el nav del
   catálogo—, y son las únicas: dentro de la app no cabe (la fila «Inicio»
   mide 20px de icono) y ahí sigue mandando `.marca-icono`.

   DICE «online» Y NO «CRM», y está decidido así (Arif, 2026-08-26): es el
   logotipo de la casa, y el producto lo nombran el `<title>` de la pestaña y
   la firma del pie del login. Si algún día hay un lockup propio del CRM, se
   cambian estos dos archivos y nada más.

   180px es el ancho que usa el login de Controly; el alto sale del aspecto
   real del archivo (350x123) en vez de escribirse a mano, así que cambiar el
   ancho no descuadra nada. Mismo mecanismo de tema que su hermano: el CSS
   elige el ARCHIVO —no un `<img>` por tema— porque el `data-theme` ya está
   puesto en el `<head>` antes de pintar y así no hay destello. */
.marca-logotipo{
  display:block;flex:none;
  width:180px;aspect-ratio:350/123;
  background:url("img/logo/logo-controly.png") no-repeat center / contain;
}
[data-theme="dark"] .marca-logotipo{background-image:url("img/logo/logo-controly-w.png")}
/* EL LOGOTIPO ESCRITO: «CONTROLY» en el color de la marca y versales, y
   «Funnels» en el gris secundario. El nombre del producto ya NO va en el
   acento (lo estuvo hasta el 2026-08-17): en esta app el azul significa
   «esto escribe en la base» —es el color de los enlaces y las acciones— y la
   marca no hace nada. Puesta en el acento, el rótulo se leía como un control.

   El `letter-spacing` se anula: el negativo que traen `.sb-marca` y
   `.puerta-marca` está calibrado para caja mixta, y en versales aprieta las
   astas hasta que «CONTROLY» se lee como un bloque. */
.marca-nombre{text-transform:uppercase;letter-spacing:0;color:var(--marca-fg)}

/* ── Cascarón ───────────────────────────────────────────────────────── */
/* `overflow:clip` y NO `hidden`, desde el 2026-08-18. Los dos recortan igual,
   pero `hidden` crea un CONTENEDOR DESPLAZABLE: nada lo desplaza con la rueda,
   y aun así el navegador puede moverlo solo para traer a la vista un elemento
   que acaba de recibir el foco. Con un `.peek` cerrado —que vive fuera del
   canto derecho, empujado por `translateX(100%)`— el área desplazable mide 420
   más, así que ese auto-scroll corre la app entera y esconde la barra lateral.

   Medido el día que la lista de ventas estrenó DOS peeks (detalle y captura):
   `scrollWidth` 1860 contra 1440 de ventana, `scrollLeft` 420 en cuanto el
   panel enfocaba su primer campo. Con `clip` no hay contenedor que desplazar y
   el caso desaparece de raíz — el cascarón mide `100vh` y nunca tuvo scroll
   propio (lo tienen los moldes de dentro), así que no pierde nada. (Una
   excepción declarada desde el 2026-08-30: con el riel PLEGADO —la escalera
   responsiva, abajo— el cascarón sí scrollea en vertical; el clip se queda
   en el eje X, que es donde vivía el caso del peek.)

   `--panel-pad-x` VIVE AQUÍ y no en `.app-panel` desde el 2026-08-30: el
   costado lo consumen el panel y sus moldes por herencia, igual que antes,
   pero también el RIEL cuando el pliegue responsivo lo mete a la columna
   principal — y el riel es hermano del panel, no hijo. En el panel, el token
   no le llegaba. */
.app-shell{--panel-pad-x:32px;display:flex;height:100vh;overflow:clip;position:relative}

/* ── Barra lateral ──────────────────────────────────────────────────── */
/* La barra se dimensiona por su texto más largo, no al revés: "En cualquier
   momento" mide 150.91px en Space Grotesk (148 cuando iba en Geist, y los
   260px de entonces lo dejaban 0.33px fuera — elipsis medida, no supuesta).
   Truncar la navegación primaria no es una decisión de estilo. La barra de
   Things en macOS también es más ancha. */
.app-sidebar{
  /* El ancho es un TOKEN local porque lo consume alguien más: el tooltip de la
     barra compacta se ancla con `left`, y sin esto habría que repetir la cifra
     en cada modo (y en la media query) — dos sitios que se separan solos. */
  --sb-w:264px;
  width:var(--sb-w);flex:none;align-self:stretch;
  background:var(--app-sidebar-bg);
  border-right:1px solid var(--app-hairline);
  padding:16px 10px;
  display:flex;flex-direction:column;gap:1px;
  overflow-y:auto;
  /* El deslizamiento del interruptor (2026-08-12). Sólo corre al PULSAR: al
     cargar la página el ancho ya viene servido por PHP (la cookie), así que no
     hay estado inicial del que animarse. */
  transition:width 170ms var(--app-ease);
}
@media (prefers-reduced-motion:reduce){
  .app-sidebar{transition:none}
}

/* ── La fila de cabecera: la marca y el interruptor ──────────────────────
   La PRIMERA fila de la barra, y la única que no navega ni pertenece al pie
   (2026-08-12, pedido de Arif con claude.ai de referencia: el interruptor ahí
   arriba, a la derecha, sólo icono).

   SU ALTO ES EL DEL TITULAR DEL PANEL, y por eso está DERIVADO y no escrito:
   `--fs-titulo × 1.2` es literalmente la fórmula del alto del h1 (26 × 1.2 =
   31.2). Las dos arrancan en y=16 —el padding de la barra y el del panel son
   el mismo— y ahora también terminan igual, así que la cabecera se lee como
   una franja continua de un extremo a otro de la pantalla en vez de dos
   piezas sueltas. Derivado y no copiado: si el titular cambia de cuerpo, esto
   lo sigue solo.

   Midió `--app-barra-h` (44) del 2026-08-12 al 08-13, y era igual de correcto
   entonces: lo que había enfrente era la PÍLDORA del titular, que medía 44. Al
   retirarse la píldora el titular bajó a 31.2 y este 44 se quedó apuntando a
   una pieza que ya no existe — el botón caía 6.4px por debajo del centro del
   título. La lección para la próxima: esto no es «el alto de una barra», es
   «el alto de lo que tiene enfrente».

   Que la marca la acompañe resuelve la otra mitad: una fila con un solo icono
   flotando a la derecha se lee como un accidente. La marca ya existía en dos
   superficies —la puerta (`.puerta-marca`) y el catálogo de ui.php
   (`.ui-nav .marca`)—; lo raro era que la app no la tuviera. */
.sb-cabeza{
  display:flex;align-items:center;justify-content:flex-end;gap:8px;flex:none;
  min-height:calc(var(--fs-titulo) * 1.2);
  padding:0 2px 0 9px;margin-bottom:6px;
}
.sb-marca{
  font-family:var(--fuente-titulo);font-size:var(--fs-dialogo);font-weight:700;
  letter-spacing:-.015em;color:var(--app-fg);
  margin-right:auto;                /* el interruptor se va al extremo derecho */
  white-space:nowrap;
}
/* El reparto de tintas lo pone la marca (`.marca-nombre`);
   aquí sólo queda el tamaño y el sitio. */
/* El interruptor: botón-icono, misma familia que `.sb-tema` (la barra navega;
   lo que muta estado es un control, no una fila). Es más grande que aquél —30
   contra 24, icono de `--icn-fila` contra `--icn-texto`— y ésa es la
   diferencia de rango: el tema es un ajuste que se toca dos veces al año y
   vive apretado en el pie; éste manda sobre la forma de la pantalla, está
   solo en su banda —la que mide lo que el titular— y a 16px se leería como
   una mota. */
.sb-plegar{
  width:30px;height:30px;flex:none;
  background:transparent;border:0;border-radius:var(--r-ctl);
  cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;
  color:var(--app-fg-secondary);   /* el tono de icono de la barra */
}
.sb-plegar .icn{width:var(--icn-fila);height:var(--icn-fila);vertical-align:0}
.sb-plegar:hover{background:var(--app-pill);color:var(--app-fg)}
/* Los MENÚS van en Space Grotesk (2026-08-08): el menú es chrome que navega,
   no datos — la letra de la marca lleva la navegación y Geist se queda con
   las filas, los campos y las cifras. Por eso el CONTEO vuelve a Geist: es
   un dato, y sus dígitos tabulares tienen que cuadrar en columna. */
.sb-item{
  display:flex;align-items:center;gap:7px;
  padding:5px 9px;border-radius:var(--r-ctl);
  font-family:var(--fuente-titulo);
  font-size:var(--fs-base);color:var(--app-fg);
  cursor:pointer;user-select:none;
  text-decoration:none;
}
.sb-item:hover{background:var(--app-pill);text-decoration:none;color:var(--app-fg)}
/* LA FILA DE LA PÁGINA EN PANTALLA (2026-09-22, pedido de Arif). La marca
   `interno/cabecera.php` con `activa` y `aria-current="page"`. El fondo es
   `--app-row-selected`, el mismo que `.dlg-opcion.actual` usa para «el que está
   en pantalla» en el selector de catálogo. Se compararon otros dos en la barra
   real: `--app-pill` es el del hover y con él no se distingue la fila activa
   de la que tiene el puntero encima; `--app-bg` (blanco) se lee bien en claro,
   pero en oscuro queda más oscuro que la barra. Con el puntero encima la fila
   activa conserva su fondo. */
.sb-item.activa,.sb-item.activa:hover{background:var(--app-row-selected)}
/* LA FILA QUE ES BOTÓN (2026-09-09, con el buscador global). Tres
   declaraciones y ninguna cosmética: son los defaults del agente de usuario
   para `<button>` —fondo gris, borde, ancho al contenido— que `.sb-item` no
   pisa. La familia, el tamaño y el color ya los fija ella, así que no hace
   falta `font:inherit`.

   POR QUÉ HAY UNA FILA QUE NO ES ENLACE, siendo que el criterio de esta barra
   es «la barra navega; el pie actúa»: ese criterio separa navegar de MUTAR
   ESTADO, y sus dos ejemplos —el tema, la sesión— son mutaciones. El buscador
   no muta nada; todo lo que hay dentro de su diálogo es un `<a href>` que
   navega. El precedente exacto ya vive arriba: `#sb-catalogo` es una fila que
   abre un diálogo. La diferencia es que aquélla PUEDE ser `<a>` porque tiene
   un destino de respaldo honesto (Contactos) si el JS falla, y ésta no lo
   tiene: no hay página que haga esto.

   Y el día que la haya, esto se deshace: si nace un `buscar.php`, la fila
   vuelve a ser `<a>` con ese href y el diálogo pasa a ser su atajo. */
button.sb-item{background:transparent;border:0;width:100%}
/* El icono de la fila de menú: `--icn-fila` (20px) y un escalón bajo el texto.

   Las dos cosas son la misma decisión (2026-08-10, pedido de Arif). Las seis
   listas traían cada una su color en un `style` inline —azul, ámbar, rojo,
   teal, arena, verde— y el resto de las filas iban en `--app-fg-tertiary`.
   Eso daba una barra con dos problemas a la vez: media docena de colores que
   no significan nada (no son estado, no son alarma: son etiquetas de color
   para seis destinos) y la otra mitad atenuada, que se lee como deshabilitada
   estando perfectamente viva.

   Y el color rompía la propia regla de la casa —«el color tiñe fondos y pinta
   glifos de ESTADO»—: era la última paleta decorativa que quedaba en la app.
   Sin ella, lo que distingue una fila de otra es la SILUETA del icono, que es
   lo que ya hacía el trabajo (estrella, calendario, lista, caja, palomita no
   se confunden entre sí), y para eso hay que verla: de ahí los 20px.

   El tono es `--app-fg-secondary`, UN escalón por debajo del texto, y ése es
   el punto exacto: el terciario es el gris de lo apagado —se lee como
   deshabilitado— y el color del texto, a 20px y con el trazo escalado a ~2.1,
   pesa MÁS que la palabra que etiqueta y la fila se lee al revés. El
   secundario deja el icono presente y legible (7.4:1 en claro, 7.7 en oscuro)
   sin competir con el rótulo. Corrección de Arif el mismo día, mirándolo en
   el tema claro: «que no sea muy oscuro, pero no al nivel del texto».

   Va en `color` y no en `background` porque `.icn` se pinta con
   `currentColor`: así el icono queda del secundario y el texto conserva el
   suyo, sin tocar el color de la fila. */
.sb-item .sb-icono{
  width:var(--icn-fila);height:var(--icn-fila);flex:none;
  color:var(--app-fg-secondary);
}
.sb-item .sb-texto{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* EL RÓTULO DE GRUPO DE LA BARRA, y su aire es asimétrico A PROPÓSITO
   (2026-09-17, pedido de Arif: «intégralo a la sección»).

   Un rótulo con el mismo aire arriba y abajo flota entre los dos bloques y hay
   que deducir a cuál pertenece. Nació con `padding:2px 9px 6px` y eso daba 13px
   por encima —los 12 del `padding-top` de `.sb-pie`— contra 11 por debajo (sus
   6 más los 5 del `padding-top` de la primera `.sb-item`): prácticamente
   simétrico, así que «CONFIGURACIÓN» se leía tan pegado a «Catálogo», que es de
   la zona de trabajo, como a «Funnels», que sí es suyo.

   Con el `padding-bottom` en 0 quedan **17px arriba contra 5 abajo** (medidos en
   navegador, no calculados): el rótulo se pega a las filas que nombra y el corte
   entre secciones sube a donde le toca. La proporción es lo que comunica la
   pertenencia; los números exactos son consecuencia de lo que ya había.

   Los 9px laterales alinean el texto con el de las filas, y ésos no se tocan.

   `.dlg-grupo` —la voz hermana, en el buscador global— ya nace bien por su
   cuenta (`padding:10px 0 4px`, mismo sentido), así que este cambio no lo
   arrastra. */
.sb-titulo{
  font-size:var(--fs-micro);font-weight:600;letter-spacing:.07em;text-transform:uppercase;
  color:var(--app-fg-tertiary);padding:2px 9px 0;
}
.sb-pie{margin-top:auto;padding-top:12px}

/* EL PIE SIN FILA DE USUARIO (2026-09-14, pedido de Arif). `.sb-usuario`
   juntaba el nombre, el tema y Salir en un renglón; se deshizo: el nombre es
   el rótulo de la fila de la cuenta, el tema subió a `.sb-cabeza` y Salir es
   una `.sb-item` más, la última. Así ya no hace falta la anatomía propia de
   `.sb-usuario-salir` ni su apilado en la barra compacta: Salir hereda el
   tooltip y el sólo-icono de cualquier fila.

   `.sb-quien` es el nombre del OPERADOR, que no tiene fila de cuenta: misma
   anatomía de fila y sin hover, porque no navega. */
.sb-quien{cursor:default}
.sb-quien:hover{background:transparent;color:var(--app-fg-tertiary)}
.sb-salir{margin-top:6px}

/* El interruptor de tema: botón-icono de la cabeza de la barra, antes del
   de plegar (2026-09-14; vivió en la fila de usuario del pie del 2026-08-29 a
   ese día). Fue una fila del menú (`.interruptor-tema`, un <button> con la silueta de `.sb-item`) hasta
   el 2026-08-11 — imitar la fila lo hacía leerse como navegación siendo una
   acción, la única de toda la barra. Icono a `--icn-texto` (acompaña dentro
   de un control, no marca una fila) y en el secundario, como manda la regla
   de iconos; luna en claro, sol en oscuro — lo pone el CSS, el JS sólo pinta
   el title. */
.sb-tema{
  flex:none;
  background:transparent;border:0;border-radius:var(--r-ctl);
  cursor:pointer;display:flex;align-items:center;justify-content:center;
  width:30px;height:30px;padding:0;   /* la caja de `.sb-plegar`, su vecino */
}
.sb-tema::before{
  content:'';width:var(--icn-fila);height:var(--icn-fila);flex:none;
  background:var(--app-fg-secondary);
  -webkit-mask:var(--icn-luna) no-repeat center / contain;
          mask:var(--icn-luna) no-repeat center / contain;
}
[data-theme="dark"] .sb-tema::before{
  -webkit-mask-image:var(--icn-sol);mask-image:var(--icn-sol);
}
.sb-tema:hover{background:var(--app-pill)}

/* ── Barra COMPACTA: sólo iconos (2026-08-11; con interruptor 2026-08-12) ──
   TODO el modo cuelga de esta única clase sobre `.app-sidebar`, y por eso el
   interruptor cupo en un `classList.toggle('compacta')` (app.js) sin tocar una
   sola de las reglas de abajo.

   QUIÉN LA PONE: el USUARIO, y nadie más. Su elección viaja en la cookie
   `leads_barra` y PHP la aplica al pintar, igual en todas las pantallas.
   Cookie y no `localStorage` —que era el plan escrito aquí— porque esto es una
   app SERVIDA: PHP no puede leer `localStorage`, así que la barra nacería
   ancha y saltaría a 56px en CADA carga. Un salto de 208px por navegación es
   peor que el problema.

   El tablero la pedía por su cuenta (`$barra_compacta`, 2026-08-11), por ser
   la única pantalla que crece a lo ANCHO — cada etapa es una columna, y los
   264px del menú salían de las columnas: el panel pasaba de 1176 a 1384px,
   medido. Ese valor por página se retiró el 2026-08-12: con el interruptor, la
   medición sigue siendo cierta pero ya no justifica decidir por el usuario —
   sólo lo convertía en la pantalla que se comporta distinto.

   El TEXTO no se encoge, se quita: `display:none`. Por eso las filas llevan
   `aria-label` (el nombre accesible, que el texto oculto ya no puede dar) y
   `data-tip` (el rótulo que pinta el tooltip de más abajo). Los dos están
   SIEMPRE, en los dos modos: eso es lo que dejó al interruptor sin trabajo
   extra el día que llegó. */
.app-sidebar.compacta{--sb-w:56px;padding:16px 8px}
/* La marca no cabe (es lo único de la barra que no se reduce a un icono), así
   que la cabecera se queda con los dos botones-icono — y en 40px de ancho útil
   no caben en fila: 30 + 8 + 30 = 68. La primera versión los dejó ahí y el
   tema salía MEDIO FUERA de la barra (medido: `left:-3.5px`) con scroll
   horizontal de propina, exactamente el modo de falla que `.sb-usuario` ya
   había resuelto abajo. El arreglo es el mismo: se apilan.

   Quién manda arriba: el INTERRUPTOR, que es el ancla para volver (y a él lo
   busca el usuario que acaba de contraer). El tema baja a la segunda fila.

   Contraer NO mueve el interruptor: al apilar, el primero de la columna se
   queda donde ya estaba —arriba del todo de la banda, que es donde lo deja el
   `min-height` derivado del titular—, y el tema es el que aparece debajo. El
   `gap:2px` es el del apilado de `.sb-usuario-acciones`, por lo mismo. */
.app-sidebar.compacta .sb-cabeza{
  flex-direction:column;align-items:center;justify-content:flex-start;
  gap:2px;padding:0;
}
.app-sidebar.compacta .sb-marca{display:none}
.app-sidebar.compacta .sb-item{position:relative;justify-content:center;gap:0;padding:7px 0}
.app-sidebar.compacta .sb-texto{display:none}
/* El rótulo del grupo tampoco cabe; lo que NO puede perderse es que ahí
   empieza otra zona, así que el filo del pie hace su trabajo (es un
   `--app-hairline`). */
/* Sin texto, pero con su `margin-top` de `.sb-aparte`: en la barra contraída
   el rótulo de Configuración sigue separando esa sección de «Ayuda». */
.app-sidebar.compacta .sb-titulo{height:0;padding:0;overflow:hidden}
.app-sidebar.compacta .sb-pie{border-top:1px solid var(--app-hairline);margin-top:auto;padding-top:10px}
/* ── El rótulo que se ocultó, como tooltip ──────────────────────────────
   Sin texto, un icono es una adivinanza, y el `title` del navegador tarda ~1s
   y se pinta con el estilo del sistema operativo — el usuario lo llamó
   «simple» y tiene razón: es el único trozo de la app que no es nuestro.
   Éste sale al instante y con los tokens de la casa.

   NO se usó Bootstrap (que es lo que se pidió por nombre): salió del proyecto
   el 2026-08-08, y sus tooltips arrastran Popper — ~100KB y dos dependencias
   para reemplazar quince líneas de CSS. Esto imita su forma (cápsula oscura,
   aparición rápida) sin volver a meterlo.

   ── LOS DOS TRUCOS QUE LO HACEN POSIBLE SIN UNA LÍNEA DE JS ──

   1. `position:fixed` SIN `top`. La barra es `overflow-y:auto`, así que
      recorta a sus hijos: un tooltip `absolute` a la derecha del icono MUERE
      dentro de la barra (comprobado en pantalla antes de escribir esto). El
      fixed se cuelga del viewport y escapa del recorte. Y al no declarar
      `top`, conserva su POSICIÓN ESTÁTICA vertical — la que le tocaría dentro
      de la fila, o sea centrada con su icono, gratis y sin medir nada.
   2. `left` anclado a `--sb-w`. La barra está pegada al borde izquierdo, así
      que el borde derecho de la barra ES su ancho. Con el token, el mismo
      bloque sirve para los tres anchos (264, 180 y 56) sin repetir cifras.

   El texto sale de `data-tip` y no de `title` a propósito: con `title` el
   navegador pintaría ADEMÁS el suyo, un segundo después y encima. El nombre
   accesible lo sostiene `aria-label`, que sí está en el marcado. */
:root{
  --tip-bg:#2f2b22;   /* cálido desde 2026-08-27 (era #2b2b30), mismo L */
  --tip-fg:#ffffff;
  /* Un filo claro sobre el relleno: es lo que dibuja el CANTO de la pieza.
     En claro es casi imperceptible (la cápsula ya da 14:1 contra el papel);
     en oscuro es lo que la separa del fondo, porque ahí el relleno sólo llega
     a 1.6:1 y lo que se lee es el texto (9.1:1), no la forma. */
  --tip-canto:rgba(255,255,255,.10);
}
[data-theme="dark"]{
  --tip-bg:#42424b;
  --tip-fg:var(--app-fg);
  --tip-canto:rgba(255,255,255,.18);
}
/* Se descartó la variante invertida (cápsula clara en tema oscuro): se leía
   mejor, sí, pero un bloque blanco en una pantalla nocturna compite con el
   contenido para decir un rótulo auxiliar. Y sin flecha: el hover ya ilumina
   la fila de origen, así que el pico sólo sería adorno. */
.app-sidebar.compacta .sb-item::after,
/* Salir entra aquí como cualquier fila desde el 2026-09-14, al volverse
   `.sb-item`. */
.app-sidebar .sb-tema::after,
.app-sidebar .sb-plegar::after{
  content:attr(data-tip);
  position:fixed;left:calc(var(--sb-w) + 6px);
  padding:5px 10px;
  background:var(--tip-bg);color:var(--tip-fg);
  /* La misma letra de la fila que sustituye: el tooltip ES ese rótulo. */
  font-family:var(--fuente-titulo);font-size:var(--fs-base);line-height:1.25;
  white-space:nowrap;
  border-radius:var(--r-caja);   /* flota → el radio de lo que flota */
  box-shadow:0 6px 18px var(--app-shadow), inset 0 0 0 1px var(--tip-canto);
  opacity:0;transform:translateX(-4px);pointer-events:none;z-index:60;
  /* 90ms y sin retraso: la diferencia con el `title` del sistema no es el
     estilo, es que éste responde al cursor en vez de hacerse esperar. */
  transition:opacity 90ms var(--app-ease),transform 90ms var(--app-ease);
}
.app-sidebar.compacta .sb-item:hover::after,
.app-sidebar .sb-tema:hover::after,
.app-sidebar .sb-plegar:hover::after{opacity:1;transform:translateX(0)}
/* Los DOS botones-icono llevan tooltip en LOS DOS modos —el tema y el
   interruptor de la barra—: no tienen rótulo tampoco con la barra ancha, así
   que son los sitios donde el `title` del sistema era la única explicación. Su
   texto lo escribe app.js (cambia con el estado: «Modo oscuro»/«Modo claro»,
   «Contraer menú»/«Expandir menú»), y por eso escribe `data-tip` y no `title`.

   Un detalle del interruptor: al pulsarlo cambia `--sb-w`, y su propio tooltip
   —anclado a ese token— se reubica de golpe mientras el ancho se desliza. Se
   deja así a propósito: el puntero está encima, el rótulo acaba de cambiar de
   texto, y perseguir la animación con el tooltip pediría medir en JS lo que
   hoy resuelve una línea de CSS. */

/* Aquí vivió el apilado de `.sb-usuario` en la barra compacta (nombre fuera,
   tema y Salir en columna). Se fue con la fila el 2026-09-14: Salir es una
   `.sb-item` y el tema vive en la cabeza, con la caja de 30px de su vecino. */
/* EL TEMA SE RETIRA AL CONTRAER (2026-09-11, pedido de Arif), y con él se va el
   apilado que este bloque venía sosteniendo: sin él el pie de la barra compacta
   tiene UN control, «Salir», y la fila de usuario deja de desdoblarse en dos
   renglones para caber. Lo que se quita no es el ajuste sino este botón: el
   mismo interruptor vive en `cuenta.php` → Apariencia, que es lo que permitió
   soltarlo aquí. La regla de la barra —lo que no se reduce a un icono se va—
   ya se aplicaba al nombre del usuario y al rótulo de Salir; el tema es el
   tercero, sólo que a él no le sobraba ancho sino ALTO.

   NO SE VA EN EL RANGO DONDE EL USUARIO NO ELIGE (abajo, ≤1024): ahí la barra
   nace compacta por anchura y el interruptor que la expande está oculto, así
   que retirar el tema lo dejaría inalcanzable para quien no es admin —
   `cuenta.php` pide `esAdmin()`—. Es la misma frase que ya mata a `.sb-plegar`
   en ese rango, por los dos lados: donde no se puede elegir, no se quita nada. */
.app-sidebar.compacta .sb-tema{display:none}

/* ── Panel de contenido ─────────────────────────────────────────────── */
/* El padding superior son 16px y no un número a ojo: es EXACTAMENTE el de la
   barra lateral, así que la píldora del titular y el primer elemento del menú
   arrancan en la misma línea (y=16 los dos, medido). Desde que las dos son
   piezas del mismo gris, un desnivel entre ellas se lee como error de
   alineación, no como aire. Si se toca el padding de `.app-sidebar`, este
   sigue. */
/* El costado del panel es un TOKEN porque lo consume alguien más: un cuerpo
   con scroll propio necesita saber cuánto anular para llevar su barra hasta el
   canto del panel (ver `.consola`). Repetir el 32 ahí lo dejaría 12px dentro
   —o 4 fuera— en cuanto la media query de 1180 lo baje a 20, y el desajuste no
   da ningún error: el panel recorta en silencio. (El token se DECLARA en
   `.app-shell`, no aquí — el riel plegado también lo lee; ver el cascarón.) */
.app-panel{flex:1;min-width:0;padding:16px var(--panel-pad-x) 12px;display:flex;flex-direction:column;overflow:hidden}

/* ── El riel: la TERCERA zona del cascarón (2026-08-15, tarde) ─────────
   Barra lateral · panel · RIEL. Mismo gris que la barra (`--app-sidebar-bg`),
   mismo canto, misma altura de ventana — la simetría no es un guiño: dice que
   es chrome, no contenido. Lo de dentro acompaña al panel; lo que se opera
   vive SIEMPRE en el panel.

   POR QUÉ SALIÓ DEL PANEL. Vivió unas horas como columna del molde consola
   (`.consola-lado`) y ese sitio le costaba dos cosas a la vez: el titular
   —título y controles— se estiraba por ENCIMA de él, así que «Nuevo funnel»
   acababa flotando sobre la puesta en marcha sin gobernarla, y el lado se leía
   como una segunda columna de la página cuando lo que hace es acompañarla toda
   (pedido de Arif). Sacándolo al cascarón las dos se arreglan solas: el
   titular sólo puede medir lo que mide su panel, y el riel gana el alto
   completo que ya tiene la barra.

   SU CONTENIDO ARRANCA DONDE ARRANCA EL DEL PANEL, no donde arranca su título
   (corrección de Arif, 2026-08-15). Nació alineado con la banda del titular
   —marca · «Funnels» · sujeto del riel en una sola franja— y esa simetría
   estaba mal razonada: la banda del titular es de la BARRA y del PANEL, que
   son los dos que titulan; el riel no titula nada, acompaña a lo que hay
   debajo. Con su primera línea a la altura del h1 se leía como un tercer
   encabezado compitiendo con el de la página, y encima dejaba su contenido
   desalineado con el de la columna a la que acompaña.

   El desplazamiento va DERIVADO y no escrito: padding del panel + el alto del
   h1 (`--fs-titulo × 1.2`) + el aire que ese título pone debajo
   (`--esp-titulo`). Es exactamente la aritmética de dónde empieza `.consola`,
   así que la primera pieza del riel y la primera sección del cuerpo arrancan
   en la misma horizontal — y si el titular cambia de cuerpo o el aire cambia
   de valor, el riel los sigue solo. Un número a ojo aquí se desalinea en el
   primer retoque de la cabecera, sin dar un solo error.

   ANCHO FIJO, como la barra y por lo mismo: lo dimensiona su contenido —tres
   cifras en fila y un checklist de una línea por paso—, no un porcentaje de
   ventana. El `clamp(300px,31%,400px)` que llevó de columna era el reparto de
   una rejilla; aquí el riel no reparte nada, ocupa lo suyo. Encoge con la
   barra a partir de 1180px, en la misma media query y por la misma razón.

   NO SE ESCONDE en ventanas chicas — SE PLIEGA (2026-08-30, antes esta línea
   decía «nunca por ancho» y valía sólo para escritorio). Un riel que
   desapareciera se llevaría el único sitio donde se ve qué falta configurar,
   así que cuando el ancho ya no da para las dos columnas (≤1024, la escalera
   responsiva) el riel deja el costado y se vuelve el ÚLTIMO BLOQUE de la
   columna principal, a lo ancho y dentro del mismo scroll. Sigue completo,
   sólo cambia de sitio.

   Scroll propio como red, no como contrato: el riel se diseña para CABER. Un
   resumen que hay que scrollear ya es otra columna. */
.app-lado{
  --lado-w:336px;
  /* El aire de arriba: el del panel (16) + el titular + el aire que el titular
     pone debajo. O sea, la altura a la que el panel empieza a tener CONTENIDO. */
  --lado-pad-t:calc(16px + var(--fs-titulo) * 1.2 + var(--esp-titulo));
  --lado-cab-aire:12px;
  width:var(--lado-w);flex:none;align-self:stretch;
  background:var(--app-secundario-bg);
  border-left:1px solid var(--app-hairline);
  /* El costado descuenta el canal del scroll y lo reserva, como los moldes:
     el riel scrollea en algunas páginas y no en otras (medido 2026-08-28:
     automatización y cuenta sí, funnels e inicio no), y sin esto su contenido
     cerraba a 18px del canto o a 24 según la página. Ver la regla del riel
     lateral en ui.php. */
  scrollbar-gutter:stable;
  /* EL COSTADO VA POR TOKEN desde el 2026-09-06, y no por número, porque ahora
     hay una segunda pieza que tiene que medir exactamente lo mismo: la cabeza
     pegajosa sangra hasta los cantos del riel con estos valores en negativo, y
     en ≤1180 los dos bajan de 18 a 14. Con el número escrito dos veces, ese
     salto desalineaba la cabeza justo en la ventana donde menos se mira. */
  --lado-pad-x:18px;
  padding:var(--lado-pad-t) calc(var(--lado-pad-x) - var(--canal-scroll)) 20px var(--lado-pad-x);
  display:flex;flex-direction:column;
  overflow-y:auto;
}
/* EL AIRE DE ARRIBA SE DESCUENTA SÓLO SI HAY CABEZA (2026-09-06).
   El contrato del riel es que su primera línea cae donde cae la primera del
   panel, y `--lado-pad-t` es esa aritmética. La cabeza pegajosa necesitó aire
   propio para no tocar el canto al quedarse fija, y ese aire SALE de aquí en
   vez de sumarse — si no, el título del riel bajaría 12px respecto del cuerpo.

   `:has()` y no a secas, que es la lección que costó una pasada: **el riel del
   Inicio no tiene cabeza** (ni funnels con uno solo), y ahí el descuento no lo
   compensa nadie — su primera tarjeta subía los 12px y quedaba desalineada con
   el panel. Medido: 62.2 donde el cuerpo empieza en 74.2. El descuento es de la
   pareja padding+cabeza pegada, así que se condiciona a que la pareja exista —
   y por eso el selector pide `.pegada` y no sólo `.lado-cabeza`. */
.app-lado:has(> .lado-cabeza.pegada){padding-top:calc(var(--lado-pad-t) - var(--lado-cab-aire))}

/* La cabeza del riel: el SUJETO que acompaña (el funnel) y su estado. Ya no
   tiene alto propio derivado del h1 —lo tuvo mientras vivía en la banda del
   titular— porque aquí no titula la pantalla: es la primera pieza del riel y
   mide lo que mide su texto. */
.lado-cabeza{
  display:flex;align-items:center;gap:8px;flex:none;
  margin-bottom:14px;
  /* Si título, medida y botón no caben, el botón baja de renglón en vez de
     cortar el título con «…» (2026-09-23, «Horario sem…» en Agenda: 15px
     de más a 1440 y bastantes más con el riel de 280). */
  flex-wrap:wrap;row-gap:8px;
}

/* ── LA CABEZA QUE SE QUEDA ARRIBA (`.pegada`) ──────────────────────────
   OPT-IN, y ésa es la mitad importante de la pieza (2026-09-06, corrección de
   Arif sobre la primera pasada, que la puso en los cinco rieles a la vez).

   ── QUÉ RIEL LA PIDE Y CUÁL NO ──
   La pide el riel que es UNA LISTA de piezas del mismo rango bajo un solo
   rótulo: el catálogo de categorías de `items.php`. Ahí la cabeza titula
   TODO lo que corre debajo, así que perderla de vista es perder de qué es la
   lista y dónde está el gesto que le añade una más — y la lista crece sin
   techo, que es lo que garantiza que un día no quepa.

   NO la pide el riel de SECCIONES INDEPENDIENTES, y `cuenta.php` es el caso:
   ahí abajo hay «Usuarios» y «Webmaster», dos sujetos distintos con su propia
   cabeza cada uno. Fijar la primera la dejaría titulando contenido que no es
   suyo en cuanto se llega a la segunda — una cabeza pegajosa afirma «todo lo
   que ves cuelga de mí», y en un riel de secciones eso es MENTIRA. El mismo
   argumento vale para el horario de `citas-config.php` y el pulso de
   `venta.php`: lo que hay debajo cabe, y su cabeza no gobierna una lista.

   *Regla que deja: se pega la cabeza que titula UNA lista, no la que abre la
   PRIMERA de varias secciones.*

   Sin `.pegada`, `.lado-cabeza` es exactamente lo que era antes de hoy —los
   14px de `margin-bottom` y la hairline en el vecino de abajo—, así que los
   cuatro rieles que no la piden no cambiaron un píxel.

   ── LO QUE LA CLASE TRAE ──
   · LA HAIRLINE SUBE A LA CABEZA. Vive en `.lado-cabeza + .lado-grupo` como
     `border-top`, o sea en lo que se mueve: pegada la cabeza, la línea se iría
     con el contenido y la cabeza quedaría flotando sin cerrar por abajo. Los
     píxeles no cambian —los 14 del `margin-bottom` pasan a `padding-bottom`—,
     sólo el dueño: la frontera es de quien se queda.

   · SANGRA HASTA LOS CANTOS del riel y repone el mismo padding, porque una
     cabeza fija tiene que TAPAR lo que pasa por debajo: con el ancho del
     contenido, las tarjetas asomaban por los dos costados.

   · EL `top` NO ES CERO, que es el detalle que costó una pasada. El
     rectángulo contra el que se pega un elemento pegajoso NO es el scrollport
     a secas: es el scrollport DESCONTADO EL PADDING de su scroller. Y el riel
     tiene un padding de arriba grande a propósito —los 62px que alinean su
     primera línea con la del panel—, así que `top:0` clavaba la cabeza 62px
     por debajo del canto y las tarjetas seguían corriendo POR ENCIMA de ella.
     Medido en pantalla: `paddingTop` 62.2 y la cabeza pegada exactamente a
     62.2. El `top` negativo devuelve esos mismos 62 y la deja en el canto, y
     va DERIVADO de los dos tokens que lo producen, nunca escrito: el día que
     el titular cambie de cuerpo o el aire de valor, la cabeza los sigue
     sola. */
.lado-cabeza.pegada{
  position:sticky;top:calc(var(--lado-cab-aire) - var(--lado-pad-t));z-index:3;
  background:var(--app-secundario-bg);
  margin:0 calc((var(--lado-pad-x) - var(--canal-scroll)) * -1) 0 calc(var(--lado-pad-x) * -1);
  padding:var(--lado-cab-aire) calc(var(--lado-pad-x) - var(--canal-scroll)) 14px var(--lado-pad-x);
  border-bottom:1px solid var(--app-hairline);
}
.lado-cabeza.pegada + .lado-grupo{border-top:0;padding-top:16px}
/* ── EL HALO DE LA CABEZA DEL RIEL ─────────────────────────────────────
   La séptima de su especie, y la tercera que REUSA el gradiente en vez de
   inventarse otro: mismo `radial-gradient` que el halo del titular y que el
   del cuerpo del peek, hasta el último porcentaje, porque dicen exactamente lo
   mismo — «hay una superficie encima». Elipse al 62% del ancho, muerta en
   transparente al 72%, sin `blur` (la caída del gradiente sale gratis y no
   repinta nada en cada scroll).

   CUELGA de la cabeza con `top:100%`, que es donde el titular ya plantaba el
   suyo: una sombra nace pegada a lo que la produce. No necesita sangrar por
   los costados como aquél porque su ancla YA llega a los cantos del riel.

   SÓLO CON SCROLL — `.halo` la pone `app.js`, por el mismo oyente en captura
   que sirve al titular y a los paneles. Permanente sería un adorno. */
.lado-cabeza.pegada::after{
  content:'';position:absolute;z-index:2;pointer-events:none;
  left:0;right:0;top:100%;height:14px;
  background:radial-gradient(62% 100% at 50% 0%, var(--app-halo), transparent 72%);
  opacity:0;transition:opacity var(--app-dur) var(--app-ease);
}
.lado-cabeza.pegada.halo::after{opacity:1}

/* ── El molde del cuerpo ──────────────────────────────────────────────
   Debajo del titular, TODA página es una de CUATRO formas y no hay una
   quinta (corregido el 2026-09-19: decía CINCO y seguía contando la mesa,
   retirada el 2026-09-02 con `campos.php`; los censos de abajo se rehicieron
   ese día con grep):

     · superficie de trabajo — ocupa el panel entero (sábana, tablero);
     · hoja de lectura — `.pagina > .hoja`, 880px pegada al margen izquierdo
       (el importador y el autorrespondedor, `drip.php`; censo vivo:
       `grep -l 'class="hoja' *.php`). PERDIÓ CUATRO el 2026-08-17: la papelera de
       leads se fue a la superficie de trabajo (es la sábana), el panel a la
       consola, y las dos páginas de identidad —usuarios y cuentas— murieron
       fundidas en `cuenta.php`, que nació consola. El censo se mantiene a mano
       y por eso hay que corregirlo al mover una página: un molde que dice tener
       consumidores que ya no tiene es la forma más barata de elegir mal el
       siguiente;
     · consola — `.consola`, UNA columna que se opera + un lado compañero
       (siete páginas al 2026-09-19: funnels, citas-config, automatización,
       la cuenta, agentes, personal y venta; censo vivo:
       `grep -ln 'class="consola' *.php`). Ver más abajo. PERDIÓ EL INICIO
       el 2026-08-27, que estrenó la portada;
     · portada — `.portada > .portada-col`, tres columnas de TARJETAS que se
       leen de un vistazo y ninguna se opera (el inicio). Ver más abajo.

   Nació el 2026-08-10 de medir lo que había: CUATRO moldes distintos para
   cinco páginas. Las tres de catálogo llevaban `.nc-envoltura`/`.nc-caja`
   —el prefijo de «no contactar», que se copió a papelera y a columnas sin que
   nadie lo decidiera—, el importador tenía los suyos con la caja CENTRADA y
   su propio título 6px más chico, y el panel resolvía lo mismo con un
   `max-width` suelto. Tres anchos de 880 escritos tres veces, y ningún par de
   páginas alineado igual.

   La hoja va a la IZQUIERDA, no centrada: el titular arranca donde arranca la
   primera columna de la tabla (`.panel-titulo`), y una caja centrada deja el
   título colgado en una esquina y el contenido flotando en otra. El
   importador era el único que lo hacía y por eso era el único que no parecía
   la misma app.

   `.pagina` es flex COLUMNA y no un bloque: eso es lo que permite que el
   estado vacío se centre en el panel —ver `.estado-vacio`— en vez de quedar
   varado bajo el párrafo.

   Se llama `.hoja` y no `.caja` porque `.caja` YA EXISTE: es el checkbox de
   la fila de la sábana (15×15). El choque no daba error —la regla de abajo
   ganaba por orden— y el síntoma era la hoja de 880 renderizada a 15px, con
   el párrafo en una palabra por renglón. Nombre nuevo en este archivo: se
   busca antes de escribirlo. */
/* Reserva el canal aunque no scrollee: aquí el canto derecho NO es riel —la
   hoja se acota a 880 y va pegada a la izquierda, así que a su derecha hay
   blanco, no margen—, pero sin reservar, el ancho útil cambia en el momento en
   que la página empieza a desbordar y el estado vacío, que se centra en esta
   caja, se mueve 3px. La barra se queda donde está (dentro del costado del
   panel) y no se lleva al canto como en la consola: no hay contenido pegado a
   ella del que separarla.
   CONSECUENCIA DECLARADA: por debajo de ~1050px la hoja deja de caber en sus
   880 y pasa a llenar el panel; ahí su canto derecho SÍ es riel y cierra a 26
   en vez de a 20, porque el canal sale de su contenido. Se acepta: son dos
   páginas de administración, sólo ocurre en ventana estrecha, y el arreglo
   —el margen negativo de la consola— movería 3px el estado vacío que esta
   caja centra, que es lo único que aquí se mira. */
.pagina{flex:1;min-height:0;overflow-y:auto;scrollbar-gutter:stable;display:flex;flex-direction:column}
.hoja{width:min(880px,100%);flex:none;padding-bottom:24px}

/* ── AQUÍ VIVIÓ LA MESA DE TRABAJO — el tercer molde (2026-08-11 · 2026-09-02) ──
   Varias piezas que se operaban A LA VEZ, en tramos que se leían de izquierda a
   derecha, cada uno con su propio scroll: el pacto era que la pantalla NO se
   mueve —lo que se desplaza es un tramo, nunca la página—. Se retiró con
   `campos.php`, su único consumidor, cuando la selección de columnas se
   desmontó con el outreach (docs/DOC-semilla-crm.md §10). El sistema queda en
   CUATRO moldes: superficie, hoja, consola y portada.

   TRES COSAS QUE APRENDIÓ Y QUE NO SE VAN CON ELLA, porque el próximo molde
   multi-tramo las va a volver a necesitar:

     · **La altura no se calcula con `vh`.** Se tomó de Controly (`funnel.php`),
       donde el tramo mide `84vh` — un número mágico que asume cuánto miden el
       menú y el breadcrumb. Aquí sobraba: `.app-shell` ya fija la app a la
       ventana y el panel ya es flex column, así que `flex:1;min-height:0` mide
       lo que quede, y sigue midiendo bien cuando la cápsula del titular
       envuelve (~1130px de ventana). Con `84vh`, ahí se sale.
     · **La señal de scroll es el borde estándar, no una sombra.** La sombra
       interior de la rueda se probó y se retiró el 2026-08-27 por tosca; el
       fondo de grupo del tramo cayó el mismo día y su canto también
       (correcciones de Arif).
     · **Bajo 1200px un molde de tramos deja de serlo.** Tres cajitas de ~300px
       con scroll propio, una debajo de otra, es la pantalla donde la rueda del
       ratón nunca hace lo que esperas: se apilan y el scroll vuelve a ser UNO,
       con la barra al canto del panel y el canal descontado del costado
       —si no, el contenido pierde 6px por la derecha—. Mismo umbral que el
       `col-xl` de Controly.

   El esqueleto era el del tablero (`.tab-col`: cabeza fija + cuerpo que
   scrollea), sin refactorizar el tablero para compartirlo — su física es otra.
   La muestra y sus porqués vivían en `ui.php` §La mesa. */

/* ── La consola — el cuarto molde (2026-08-15) ────────────────────────
   UNA columna que se opera, y su acompañante FUERA del panel. Nació de medir
   la mesa de funnels.php: tres tramos de un tercio para tres listas de 4-6
   renglones dejaban dos tercios de cada tramo en aire muerto, el remitente
   cortado a media palabra y la llave sin sitio donde pintarse — el molde
   reparte piezas del MISMO rango, y esta página no tiene tres pares: tiene UNA
   configuración que se recorre y un ESTADO que se consulta.

   Nació con ese estado como SEGUNDA COLUMNA del panel (`.consola-lado`, una
   rejilla de 1fr + un tercio) y esa mitad murió el mismo día: el titular se
   estiraba por encima de las dos columnas, así que los controles de la página
   —el selector del funnel, Apagar, Nuevo funnel— quedaban gobernando un ancho
   que no era el suyo. Hoy el acompañante es el RIEL del cascarón (`.app-lado`,
   arriba) y el molde se queda con lo que de verdad es suyo: UNA columna, UN
   scroll, el del cuerpo.

   Sigue acotando sus secciones a 860px aunque el panel le dé más: una fila de
   configuración es rótulo + valor + gestos, y estirarla a 1100px separa el
   valor de sus botones con un desierto en medio — el mismo hueco muerto que
   este molde vino a matar. El tope va en los HIJOS y no en la columna para que
   el scroll siga siendo de la columna entera.

   LA BARRA DE SCROLL SE VA AL CANTO DEL PANEL, y el aire queda del lado que
   se usa (corrección de Arif, 2026-08-15). Con el costado del panel intacto,
   el canal del scroll caía pegado al borde de las secciones y los 32px del
   padding se convertían en un pasillo muerto entre la barra y el riel: aire
   donde no hace falta, cero donde sí. Se anula el costado con un margen
   negativo —de ahí el token `--panel-pad-x`, que no se copia— y se repone
   como padding DENTRO del scroller, así que ese aire lo cobra el hueco entre
   la sección y la barra. El de abajo (`padding-bottom`) es otra cosa: es el
   respiro del final del recorrido, y ése sí va donde estaba. */
.consola{
  flex:1;min-height:0;overflow-y:auto;scroll-behavior:smooth;
  scrollbar-gutter:stable;
  margin-right:calc(var(--panel-pad-x) * -1);
  padding-right:calc(var(--panel-pad-x) - var(--canal-scroll));padding-bottom:40px;
}
.consola > *{max-width:860px}

/* ── La portada — el quinto molde (2026-08-27) ────────────────────────
   TRES COLUMNAS DE TARJETAS que se leen de un vistazo, y ninguna se opera.
   Es el molde del INICIO, y entró por la misma prueba que los otros cuatro:
   ninguno podía hospedarlo sin mentir.

     · la CONSOLA es UNA columna — y aquí no hay una columna partida en tres,
       hay tres RANGOS distintos: lo que hay que atender, lo que se lee como
       serie y lo que sólo acompaña. Es justo la pregunta que decide entre
       consola y mesa, contestada por tercera vez;
     · la MESA reparte piezas del mismo rango, cada una CON SU PROPIO SCROLL y
       con el cuerpo dibujado como caja. Una tarjeta ya es una caja, así que
       meterla en un `.tramo-cuerpo` es caja dentro de caja — el error que la
       propia mesa se corrigió el 2026-08-27;
     · la SUPERFICIE DE TRABAJO trae su propio viewport para operar miles de
       filas; aquí no hay filas que operar.

   UN SOLO SCROLL, el del molde entero — al revés que la mesa, y ésa es la
   diferencia que más se nota al usarlo: un resumen cuyas columnas se
   desplazan por separado obliga a recorrer tres veces lo que se vino a mirar
   de una sola.

   Y CONVIVE CON EL RIEL, como la consola: el molde reparte el PANEL, y lo que
   acompaña a la pantalla entera sigue siendo `.app-lado`. No es un detalle de
   marcado — es de dónde salen sus medidas (abajo).

   LAS COLUMNAS LAS DECLARA LA PÁGINA, en `--portada-cols`, por el mismo
   motivo por el que `.sabana` publica su `--rejilla`: cuántas hay depende de
   los flags de la cuenta, y una columna vacía se lee como una pieza rota. El
   default es el reparto de tres; la página manda el suyo cuando le faltan
   capas. Va en una custom property y NO en `grid-template-columns` a secas
   para que la escalera de abajo pueda ganarle — un estilo en línea gana a
   cualquier hoja, y el apilado en pantalla chica dejaría de ocurrir en
   silencio.

   LAS TRES MIDEN LO MISMO (corrección de Arif, 2026-08-27). Nacieron con la
   de en medio a 1.4 —«una serie con eje pide más sitio que una cifra»— y ese
   razonamiento estaba mirando la pieza en vez del molde: lo que reparte una
   portada son RANGOS, y un rango más ancho que los otros dice que pesa más,
   que es justo lo que no significa. Con la serie en cuatro meses (el estándar
   de la casa) el argumento del ancho tampoco se sostenía: a 1440 la portada
   mide 808 y en tercios da 253 por columna — el eje cabe con sus etiquetas y
   sus ticks, medido.

   El costado negativo es el de `.consola`, copiado a propósito: es el mismo
   cuerpo con scroll dentro del mismo panel, y su barra tiene que caer en el
   mismo canto.

   EL RESPIRO DEL FINAL YA NO ES EL DE LA CONSOLA (2026-08-30, pedido de Arif:
   *«que el espaciado sea consistente incluso al final de la página»*). Era el
   mismo 40 —copiado con el costado— y aquí decía algo que en la consola no
   dice: en un molde de TARJETAS APILADAS el hueco de abajo se lee contra el
   hueco entre tarjetas, y 40 contra 14 es el doble. Se lee como si faltara una
   tarjeta, no como el final del recorrido. Ahora vale lo mismo que el
   `row-gap`, así que la última tarjeta cierra con el mismo aire con el que se
   separan las demás.

   La consola SE QUEDA en 40 y no es incoherencia: allá abajo hay una sección
   de lectura corrida, no una pieza que se repite, y no hay ritmo con el que
   ese hueco pueda desentonar. Lo que se copia entre los dos moldes es el
   canto de la barra, que es lo que la frase de arriba siempre quiso decir. */
.portada{
  flex:1;min-height:0;overflow-y:auto;
  scrollbar-gutter:stable;
  margin-right:calc(var(--panel-pad-x) * -1);
  padding-right:calc(var(--panel-pad-x) - var(--canal-scroll));padding-bottom:14px;
  display:grid;
  grid-template-columns:var(--portada-cols, repeat(3,minmax(0,1fr)));
  /* `align-items` sujeta la TARJETA a la cabeza de su fila; `align-content`
     sujeta las FILAS a la cabeza de la portada, y hace falta porque este molde
     tiene alto definido (`flex:1` dentro del panel): sin él, un grid con filas
     `auto` reparte el alto sobrante entre ellas y la segunda fila cae a media
     pantalla, con un hueco del tamaño del vacío debajo de la primera tarjeta.
     Medido: filas de 240px para tarjetas de 132. */
  /* EL HUECO SE PARTE EN DOS, y HOY los dos valen 14. Sigue partido aunque
     coincidan: son dos decisiones distintas que se escriben por separado, no
     un número con dos usos. Por eso se declara `14px 14px` y no `14px` — el
     día que el vertical vuelva a moverse, el costado no tiene por qué irse
     con él.

     EL VERTICAL ES EL QUE MANDA, porque tiene que valer IGUAL dentro de una
     columna y entre las filas que deja la escalera: con `gap` a secas, apilar
     en dos columnas ponía las tarjetas de la fila de abajo a un ritmo y las de
     la misma columna a otro, sin un error que lo delatara. Vive en DOS sitios
     que se tocan juntos — este `row-gap` y el `gap` de `.portada-col`, que es
     un flex y no hereda del grid.

     LOS 14 DEL COSTADO son el hueco ENTRE RANGOS y no se han movido nunca:
     ensancharlos o estrecharlos cambia de qué habla el molde.

     EL VERTICAL, EN CAMBIO, YA VA POR SU TERCER VALOR: nació en 12, subió a 20
     el 2026-08-29 (pedido de Arif, «más aire entre tarjetas») y bajó a 14 el
     2026-08-30, cuando la portada llegó a siete tarjetas y el aire empezó a
     costar pantalla. Al caer sobre el 14 del costado se cerró de paso la
     tensión que el catálogo había dejado escrita: con el vertical MAYOR que el
     horizontal, la proximidad tira hacia leer FILAS —que en esta portada no
     significan nada, porque cada columna es su rango y tiene su propio número
     de tarjetas—. Con los dos iguales no hay a qué agruparse. */
  gap:14px 14px;align-items:start;align-content:start;
}
/* Una columna es una PILA de tarjetas y nada más: sin rótulo y sin caja. Lo
   que rotula es cada tarjeta, y un encabezado de columna sobre piezas que ya
   se nombran solas es la etiqueta de la etiqueta. */
/* Su hueco es el `row-gap` de la portada, ESCRITO IGUAL a propósito: la
   columna es un flex y no participa del grid, así que hereda nada — el día que
   los dos números dejen de coincidir, la pantalla tiene dos ritmos y ningún
   error que lo delate. Si un día se toca, se tocan los dos. *(Se tocaron el
   2026-08-30: 20 → 14, y con ellos el `padding-bottom` de la portada, para que
   el final del recorrido no vaya al doble del hueco entre tarjetas.)* */
.portada-col{display:flex;flex-direction:column;gap:14px;min-width:0}

/* ── LA ESCALERA, y por qué es más temprana que la de la mesa ──────────
   Este molde vive CON EL RIEL PUESTO, así que su panel arranca 600px por
   debajo de la ventana (264 de barra + 336 de riel) — el doble de lo que le
   quita el cascarón a cualquier molde sin acompañante. Por eso las columnas
   se retiran antes que en la mesa, y en DOS escalones y no en uno: bajar de
   tres a una de golpe convierte un resumen en una lista.

     ≥1400 — TRES en tercios iguales. A 1440 son 253 cada una, medidos.
     ≥1000 — DOS. Con el orden del marcado la fila de arriba queda «lo que hay
             que atender | las gráficas», y lo que sólo ACOMPAÑA baja debajo:
             es exactamente el rango que sobra cuando falta ancho.
     resto — UNA, apilada en el orden del marcado.

   El umbral de 1000 cae ya dentro de la media query de 1180, donde barra y
   riel encogen a 180 y 280: ahí el panel sólo pierde 460, y por eso dos
   columnas siguen siendo legibles más abajo de lo que lo serían arriba. */
@media (max-width:1399.98px){
  .portada{grid-template-columns:repeat(2,minmax(0,1fr))}
  /* La columna de las gráficas OCUPA LAS DOS FILAS, y sin esto el escalón de
     dos no valdría la pena: con el flujo normal, la tercera columna empieza
     una fila nueva que arranca DEBAJO de la gráfica —la pieza más alta de la
     primera— y deja un hueco del alto de una tarjeta bajo la primera columna.
     Con el tramo abarcando las dos, lo que sólo acompaña cae pegado a lo que
     hay que atender y la lectura queda en dos pilas de verdad.

     SE PIDE POR CLASE Y NO POR POSICIÓN (2026-08-29). Fue `:nth-child(2)`
     mientras la única condición era `es_venta` y la segunda columna sólo podía
     ser la de la gráfica. Con `es_gasto` como flag INDEPENDIENTE eso dejó de
     ser cierto: una cuenta que mide gastos y no ventas pinta atender + acompañar,
     y ahí el `nth-child(2)` estiraba la columna equivocada —dos filas para una
     tarjeta— dejando una fila fantasma con su hueco. La clase la pone
     index.php sobre la columna de la serie, que es de quien de verdad habla
     esta regla. */
  .portada-col.serie{grid-row:span 2}
}
@media (max-width:999.98px){
  .portada{grid-template-columns:minmax(0,1fr)}
}

/* La SECCIÓN: el pariente horizontal del tramo. Mismo rótulo (--fs-dialogo en
   la fuente de titulares), misma nota — pero CON caja (borde estándar,
   --r-caja) y SIN scroll propio ni caja de datos: en una columna que ya
   scrollea, una caja
   con scroll dentro del scroll es la pantalla donde la rueda nunca hace lo
   que esperas. La caja aquí SÍ tiene trabajo — en la consola la sección es la
   única figura que agrupa renglones sobre el papel; el tramo la dejó el
   2026-08-27 porque su caja real es la del cuerpo. Los renglones se
   separan con la hairline, que a lo ancho sí tiene trabajo (guiar el ojo por
   la fila). */
/* LA SECCIÓN YA NO ES UNA CAJA (2026-09-03, decisión de Arif). Llevaba borde,
   radio y relleno —una card—, y el panel entero se leía como una pila de
   tarjetas dentro de otra caja. La consola no es una portada: **es un
   documento que se recorre**, y lo que separa sus partes en un documento es el
   AIRE y una línea, no un contorno.

   Qué gana al perder la caja, medido en la página: los 18px de relleno lateral
   por lado se los devuelve al contenido, el contenido de todas las secciones
   arranca en la MISMA vertical —con cajas, cada una empezaba dentro de la
   suya—, y el filete deja de dibujar cuatro cantos para dibujar sólo el que
   hace falta, que es el de arriba.

   LA JERARQUÍA DE SEPARADORES, que es lo que sustituye al contorno:
     · entre SECCIONES → `--app-borde` (el filo, se ve) + `--esp-seccion`
     · entre OPCIONES  → `--app-hairline` (la línea de pelo) + `--esp-opcion`
   Dos pesos y dos aires, y la proporción entre ellos ES la jerarquía. Con un
   solo peso el recorrido se lee como una lista larga sin capítulos.

   El separador PERTENECE AL BLOQUE DE ABAJO (`+ .seccion`), que es la regla
   que este sistema ya tenía escrita en el peek: colgado del de arriba se va
   con él el día que ese bloque se gatea, y el hueco aparece en una superficie
   que nadie estaba mirando.

   EL AIRE BAJO LA CABEZA TIENE DUEÑO PROPIO (2026-09-19). Lo ponía entero la
   nota (`margin-bottom:16`), así que una sección SIN nota lo perdía todo y su
   título quedaba pegado al primer grupo: medido 0px en Medición
   (`funnels.php`), Identidad y Apariencia (`cuenta.php`), los dos canales de
   `agentes.php` y Clientes (`venta.php`), contra 42 en sus hermanas con nota
   —dos aires distintos para la misma frontera, decididos por si alguien
   escribió el párrafo o no. Ahora la cabeza es el BLOQUE (el título, su meta
   y, si la hay, la nota a 4px) y el aire de abajo es suyo: lo pone la nota
   cuando existe y la adyacencia cuando no. Y son 12 y no los 16 de antes,
   que dejaban el hueco título→grupo en 42 y se leía como un salto de sección.
   Los 16 nacieron cuando la sección no tenía `.cfg-grupo` y la nota pegada se
   leía como el rótulo de la primera opción; hoy el grupo trae su propio fondo
   y esa frontera ya no la dibuja el aire.

   Es un TOKEN LOCAL porque lo consumen dos reglas —igual que `--sb-w` en la
   barra—: el mismo número escrito dos veces se separa solo. */
.seccion{--esp-cabeza:12px;padding:0;margin:0}
.seccion + .seccion{
  margin-top:var(--esp-seccion);
  padding-top:var(--esp-seccion);
  border-top:1px solid var(--app-borde);
}
.seccion-cab{display:flex;align-items:baseline;gap:8px}
.seccion-cab .hueco{flex:1}
/* EL INTERRUPTOR DEL SUJETO EN LA CABEZA DE LA SECCIÓN (2026-09-18, primer
   consumidor: «Datos del funnel» en `funnels.php`). Es la misma regla que
   `.cfg-grupo-cab`: lo que conmuta al sujeto entero va en su cabeza, no en el
   renglón de un campo suyo. La cabeza alinea por línea base porque sus piezas
   son texto; un `.sw` no tiene texto, y su línea base es su canto inferior:
   sin esto la perilla se sube media altura sobre el título. */
.seccion-cab .sw{align-self:center}
.seccion-titulo{font-family:var(--fuente-titulo);font-size:var(--fs-dialogo);font-weight:700;letter-spacing:-.01em}
.seccion-n{font-size:var(--fs-meta);color:var(--app-fg-tertiary);font-variant-numeric:tabular-nums}
/* La nota es el párrafo que explica la sección: va a 4px del título porque es
   parte de la cabeza, y el aire de abajo no es suyo sino del bloque
   (`--esp-cabeza`, con su porqué arriba). */
.seccion-nota{font-size:var(--fs-meta);color:var(--app-fg-secondary);line-height:1.55;margin:4px 0 var(--esp-cabeza);max-width:68ch}
/* La cabeza sin nota: el mismo aire, puesto por quien venga detrás. Es la
   hermana de `.lado-cab + …` en el riel, con su mismo principio —el aire tras
   una cabeza es UNO y no lo decide lo que vaya debajo— y una diferencia a
   propósito: allá se enumeran los vecinos y aquí se excluye el único que ya
   trae el aire puesto. La enumeración es la que obligó a volver a esa línea
   el día que `.cfg-grupo` entró al riel; así, un vecino nuevo nace correcto. */
.seccion-cab + :not(.seccion-nota){margin-top:var(--esp-cabeza)}
/* ── «QUÉ ES ESTO»: el botón de la explicación (2026-09-19) ───────────
   PRUEBA EN UNA SOLA SECCIÓN («Datos del funnel», `funnels.php`) antes de
   llevarla a las otras 16 que califican. El problema medido: las notas de
   sección son 27 en producción y 3,829 caracteres; se leen tres o cuatro veces
   y después son sitio gastado (8% del alto de la consola en `cuenta.php`).

   La explicación no se esconde ni se despliega: **no está en el flujo**. Su
   texto vive en el `data-info` del botón y sólo existe dentro del diálogo, así
   que la sección mide lo mismo siempre. La primera forma la escondía al pulsar
   y desplazaba la página entera — «de muy mal gusto», y con razón.

   EL COLOR ES EL TEAL DE INFORMAR y no el acento: «El azul escribe», y esto no
   escribe. Es el mismo token que el glifo de `.alerta`, que ya tiene declarada
   la excepción de icono con color —aquí el color no decora, dice de qué tipo
   es lo que se va a leer—. En reposo va al 55% para que no compita con el
   título; al posarse, entero.

   `align-self:center` por lo mismo que la perilla dos líneas más arriba: la
   cabeza alinea por línea base y un botón sin texto tiene la suya en su canto
   inferior. */
.info{
  flex:none;align-self:center;width:18px;height:18px;padding:0;margin:0;
  border:0;background:transparent;border-radius:50%;cursor:pointer;
  display:inline-flex;align-items:center;justify-content:center;
}
.info::before{
  content:'';width:14px;height:14px;opacity:.55;transition:opacity .12s var(--app-ease);
  background:var(--app-info-text);
  -webkit-mask:var(--icn-info) no-repeat center / contain;
          mask:var(--icn-info) no-repeat center / contain;
}
.info:hover::before{opacity:1}
/* La nota al pie de una sección: lo ausente-a-propósito (Google Ads) habla
   aquí en una línea, no en un renglón entero de la lista — un renglón es una
   pieza que se opera, y esto sólo se lee. */
.seccion-pie{margin-top:8px;padding-top:9px;border-top:1px solid var(--app-hairline);font-size:var(--fs-micro);color:var(--app-fg-tertiary)}

/* EL RENGLÓN DE CONFIGURACIÓN: punto de estado · rótulo · valor · gestos.
   El punto NO es la información (la frase del valor lo dice con palabras):
   es el semáforo que deja recorrer la página en un barrido — verde opera,
   gris no está, ámbar pide mirar. Decorativo a sabiendas, texto al lado.

   ── DOS LÍNEAS, NO UNA (2026-08-16, petición de Arif) ──
   Nació en una línea —`8px · 112px · 1fr · auto`— y el valor pagaba tres
   precios por caber ahí: se recortaba con elipsis (`nowrap`, así que la
   promesa de «valor COMPLETO» del molde consola sólo valía mientras cupiera),
   el rótulo se recortaba también contra su columna de 112, y el conjunto se
   leía apretado — «estresante» fue la palabra. Hoy la primera línea es
   ROTULO + GESTOS y la segunda es el valor entero, a lo ancho.

   La forma no se inventó aquí: es la de `.campo` —etiqueta encima, dato
   debajo— que el sistema ya tenía resuelta y justificada para exactamente
   esta situación (rótulo a 13/500 en el color del texto, valor a 14 normal;
   «la jerarquía la hacen el tamaño y la posición, que aquí sí bastan porque
   el rótulo está SIEMPRE encima de lo suyo y no compite con nada más»). Un
   renglón de configuración es un campo que no se edita en su sitio: mismo
   par etiqueta-dato, mismos tokens.

   El valor arranca en la columna del RÓTULO y no en la del punto: el punto
   marca el renglón entero y su sitio es el margen; alinear el valor con él
   dejaría al rótulo indentado respecto de su propio dato.

   Sólo `.cfg-detalle` se coloca a mano; punto, rótulo y gestos caen solos en
   la primera línea por orden de marcado. Así el renglón no depende de con qué
   clase envuelva sus gestos cada página. */
.cfg-fila{
  display:grid;grid-template-columns:auto minmax(0,1fr) minmax(0,auto);
  column-gap:0;row-gap:6px;align-items:center;
  padding:var(--esp-opcion) 8px;margin:0 -8px;
  border-radius:var(--r-ctl);font-size:var(--fs-base);
}
/* EL HUECO ENTRE COLUMNAS LO PONEN LAS PIEZAS y no la rejilla (2026-09-19). Es
   lo que deja que la columna del punto desaparezca DEL TODO en la fila que no
   lo lleva: con `column-gap`, la pista vacía sigue cobrando sus 12px y el
   rótulo arranca indentado sin motivo. Probado en `cuenta.php` con la columna
   fija: «Apariencia» y «Cómo trabaja esta cuenta» —dos grupos sin punto—
   colgaban 20px dentro de su caja y el riel perdía ese ancho.
   Y la columna se reserva para TODAS las filas del bloque en cuanto UNA lleva
   punto: si se reservara fila a fila, la fila sin punto correría su rótulo
   20px contra sus hermanas, que es la desalineación que esta rejilla ya tenía
   medida. Hoy no hay ningún bloque mixto —18 grupos comprobados en tres
   páginas—; la regla existe para el día que lo haya. */
/* LA COLUMNA DEL PUNTO ES DE LA FILA QUE LO LLEVA, y sólo la usa la línea del
   valor (2026-09-22, pedido de Arif con dos capturas: «Nombre» sin punto
   arrancaba en el canto y «Oferta», con punto, 20px más adentro). Medido en
   `funnels.php`: rótulo a +16 en la fila sin punto y a +36 en las once con
   punto. Hasta ese día la columna la reservaba el BLOQUE entero y el rótulo
   vivía en ella, así que todo bloque con un punto corría sus rótulos. Hoy el
   rótulo ocupa las columnas 1 y 2 (abajo) y arranca siempre en el canto; el
   punto y el valor comparten la segunda línea, el punto en la columna 1. */
.cfg-fila:has(> .cfg-dot){
  grid-template-columns:20px minmax(0,1fr) minmax(0,auto);
}
/* EL RÓTULO SE ANCLA A LAS COLUMNAS 1 Y 2, no se deja colocar por el orden
   del marcado: así arranca en el canto lleve o no punto la fila, y todas las
   filas de un bloque quedan en escuadra sin reservar nada. Estuvo anclado a
   la columna 2 del 2026-09-03 al 2026-09-22, y por eso la columna del punto
   corría el rótulo (arriba, el caso medido). */
.cfg-rotulo{grid-column:1 / 3;grid-row:1}
/* EL FILETE DEJA DE SER CONDICIONAL AL VECINO. Era `+ .cfg-fila`, así que la
   última opción de una sección cerraba sin línea y la primera de la siguiente
   tampoco la tenía: dos opciones consecutivas de secciones distintas se leían
   pegadas. Hoy lo lleva cada fila arriba y la PRIMERA de su bloque lo suelta,
   que es la misma forma que ya tienen las notas del peek. */
.cfg-fila + .cfg-fila{border-top:1px solid var(--app-hairline)}
/* SIN `min-height`. Lo tenía en `--app-row-h` (36) heredado de cuando la fila
   era de una línea; con dos o tres líneas nunca se alcanzaba, así que sólo
   servía para forzar 36px en la fila más corta — un mínimo que hoy lo pone el
   propio contenido más `--esp-opcion` por lado. */
.cfg-fila:hover{background:var(--app-row-hover)}
/* LOS GESTOS, EN LA LÍNEA DEL RÓTULO y pegados al canto derecho (2026-09-19,
   pedido de Arif sobre «Identidad» en `cuenta.php`). El reparto es de quién
   tiene ancho garantizado: el rótulo es corto y de largo conocido —«Nombre»,
   «Zona horaria», «Tema»—, así que a su derecha siempre sobra sitio; el valor
   no lo es, y se queda con la línea entera.

   VIVIERON EN UNA TERCERA LÍNEA (2026-09-03 → 2026-09-19) por una regla que
   sigue en pie y que ahora se cumple mejor: **junto al interruptor no va
   ningún otro control**. Antes eso se pagaba bajando SIEMPRE los botones; hoy
   los baja sólo la fila que además lleva perilla (`.fn-gestos + .cfg-acciones`,
   abajo), y la perilla se queda arriba porque lo que conmuta es el sujeto de
   la fila. Las filas sin perilla —la mayoría— dejan de gastar una línea.

   La tira envuelve DENTRO de su columna (`minmax(0,auto)` + `flex-wrap`): con
   muchos botones parte líneas en su sitio en vez de salirse del panel, que es
   lo que destapó el quinto gesto de la cuenta apagada en `cuenta.php` (460px
   de tira en una fila de 412, «Reiniciar» cortado y el riel con barra
   horizontal). */
.cfg-acciones{
  grid-column:3;grid-row:1;justify-self:end;align-self:center;
  display:flex;flex-wrap:wrap;gap:8px;align-items:center;
  justify-content:flex-end;max-width:100%;margin-left:12px;
}
.cfg-fila > .fn-gestos{grid-column:3;grid-row:1;justify-self:end;align-self:center;margin-left:12px}
.cfg-fila > .fn-gestos + .cfg-acciones{grid-column:2 / -1;grid-row:3;margin-left:0}
/* EL PUNTO VA CON EL VALOR, que es de lo que habla —verde opera, gris no está,
   ámbar pide mirar, y lo que pasa lo dicen las palabras del valor, no las del
   rótulo—. Arriba marcaba el renglón; abajo marca el dato. Los 7px de arriba
   lo centran en la PRIMERA línea del valor y no en su centro: (21.7 de
   interlínea − 8 del punto) / 2, y con una descripción de tres líneas un punto
   centrado flota a media altura sin nada al lado. Los 12 de la derecha son el
   hueco de columna que la rejilla no pone. */
.cfg-dot{
  grid-column:1;grid-row:2;align-self:start;margin:7px 12px 0 0;
  width:8px;height:8px;border-radius:50%;background:var(--app-borde-muted);
}
/* TRES ESTADOS Y NINGUNO MÁS (2026-09-22, regla de Arif; `ui.php` → «El punto
   dice uno de tres estados»). `warning`: se tiene que configurar y no está.
   `info`: está configurado. `success`: está atendiendo ahora —una entrada
   abierta con el funnel encendido, un canal contestando, la medición
   midiendo—. Lo opcional sin configurar NO lleva punto: el gris de «no está»
   se retiró ese día, porque ponía un punto a lo que no pedía nada. Los nombres
   son los de `.badge` y `.alerta`; hasta ese día eran `.ok` y `.ojo`. */
.cfg-dot.warning{background:var(--app-atencion)}
.cfg-dot.info{background:var(--app-info)}
.cfg-dot.success{background:var(--app-exito)}
/* Fuera del renglón de configuración (título de `.estado-vacio`): en línea, antes del texto. */
.estado-vacio .titulo .cfg-dot{display:inline-block;vertical-align:middle;margin:0 8px 2px 0}
/* El rótulo, con la anatomía de `.campo > label`: **14**, peso medio, color
   del texto. Esa paridad es el contrato —lo dice esta línea desde que nació—
   y por eso subió con él el 2026-08-17: dejarlo en 13 habría convertido un
   hermano declarado en la excepción que nadie recuerda. Ya no necesita
   elipsis ni columna fija — desde que tiene su línea entera cabe cualquier
   rótulo, incluidos los que rotulan con el NOMBRE del destinatario (avisos
   internos), que era el caso que forzaba el recorte. */
.cfg-rotulo{font-size:var(--fs-base);color:var(--app-fg);font-weight:500;min-width:0}
/* El valor: segunda línea, desde la columna del rótulo hasta el canto. Entero
   —envuelve, no recorta— porque para eso se le dio la línea; el par
   valor + metas se sienta en fila y baja si no cabe. */
.cfg-detalle{
  grid-column:2 / -1;grid-row:2;
  min-width:0;display:flex;align-items:baseline;flex-wrap:wrap;gap:2px 8px;
  color:var(--app-fg);
}
.cfg-detalle .apagado{color:var(--app-fg-tertiary)}
/* La llave, POR FIN con sitio: chip monoespaciado, clic = copiar. El molde
   viejo la escondía en un `title` porque a un tercio de panel no cabía; a lo
   ancho caben los 32 hex y sobran. Es el dato que el sitio del cliente
   incrusta — enseñarlo ES la página haciendo su trabajo. */
.cfg-llave{
  font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  font-size:var(--fs-micro);color:var(--app-fg-tertiary);
  background:var(--app-chip);border-radius:var(--r-mini);padding:2px 8px;
  cursor:pointer;overflow:hidden;text-overflow:ellipsis;flex:none;max-width:100%;
}
.cfg-llave:hover{color:var(--app-fg);background:var(--app-pill)}
/* ── EL GRUPO DE RENGLONES (2026-09-05) ───────────────────────────────
   Los renglones de un mismo sujeto —o de una misma sección— dejan de ser
   hermanos sueltos y viven dentro de una superficie, con su CABEZA cuando hay
   un sujeto que nombrar. Nació en `automatizacion.php` al listar los
   autorrespondedores de todos los funnels: con dos renglones por funnel, la
   lista plana obligaba a emparejarlos leyendo una etiqueta repetida en cada
   uno. La cabeza lo dice UNA vez y los renglones cuelgan debajo.

   SE LLAMÓ `.au-grupo` DURANTE UNAS HORAS y perdió el prefijo al día siguiente
   de nacer, cuando `funnels.php` lo pidió: es la lección que este repo ya pagó
   dos veces —`.nc-envoltura` se llamaba así por «no contactar» y acabó en tres
   módulos, `.rec-add` era del recorrido y hoy es `.fila-agregar`—. Lo que se
   comparte se nombra por lo que hace.

   EL FONDO ES EL DEL HOVER DE `.cfg-fila`, y eso resuelve de paso un defecto
   que el catálogo tenía condenado sin aplicar: ese hover pinta sobre una fila
   que NO se pulsa en ninguna página —el barrido sólo encuentra
   `boton.closest('.cfg-fila')`, o sea el botón se pulsa y de ahí se sube a leer
   sus datos—. Es literalmente lo que `.vn-total` prohíbe: *«un fondo que
   responde promete un clic que no existe»*. Al volverse el reposo del grupo,
   ese color deja de prometer nada — y por eso la fila de dentro lo suelta.

   RADIO 14 Y NO `--r-caja` (12): la escalera de radios sube con el TAMAÑO de
   lo que envuelve —control 8, caja 12, diálogo 16— y esto envuelve más que una
   caja y menos que una hoja. No lleva canto: la superficie teñida YA es la
   caja, y dibujar dos cajas dentro de una es el error que la mesa cometió con
   el gris de `.tab-col`.

   EN UN RIEL (movido del catálogo `ui.php`, 2026-09-19). Sin canto entra
   también en un riel, que ya está teñido: una veladura sobre otra superficie
   compone un ESCALÓN y no una segunda caja (medido en `cuenta.php`: claro
   #F7F6F2 → #EFEDE9, oscuro #252528 → #303033). Ahí va SIN CABEZA —se la hace
   el `.lado-titulo` de la zona, donde ya viven el conteo y el botón de alta— y
   SIN PUNTO DE ESTADO: lo que el punto hacía lo hace el riel de la puesta en
   marcha, y repetirlo sería la señal duplicada.

   EN UN PEEK ENTRÓ Y SALIÓ EL 2026-09-06. Entró en el catálogo de categorías
   de `gastos.php` por esa misma puerta del escalón (claro #FFFFFF → #F6F6F5,
   oscuro #28282C → #333337), y salió esa noche por el límite de la pieza: en
   un catálogo cada fila es su propio sujeto y su nombre ES el dato, y aquí el
   nombre baja a pretitular micro terciario mientras la cuenta contable sube a
   la línea principal («5004 · Gastos de administración» más fuerte que
   «PAPELERÍA»). Tampoco compactaba: 116px con grupo y sin él. Lo sustituyó
   `.cfg-tarjetas`. La pregunta que decide sigue siendo la de su origen —¿hay
   un sujeto que se dice una vez y partes que cuelgan de él?—: cuando la
   respuesta es no, el grupo agrupa igual pero se paga en jerarquía. */
.cfg-grupo{
  background:var(--app-row-hover);border-radius:14px;
  padding:2px 16px;margin-bottom:10px;max-width:860px;
}
/* La cabeza: el sujeto y —si lo tiene— su interruptor al extremo. Mismo
   reparto que `.lado-cabeza`: el nombre no crece, quien se va al canto se
   empuja solo. Un grupo sin sujeto que nombrar no la lleva. */
.cfg-grupo-cab{display:flex;align-items:center;gap:8px;padding:11px 0 9px}
.cfg-grupo-nom{
  font-family:var(--fuente-titulo);font-size:var(--fs-base);font-weight:600;
  letter-spacing:-.01em;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.cfg-grupo-cab .sw,.cfg-grupo-cab .cfg-grupo-marca{margin-left:auto}
/* La marca de la cabeza: el hecho que hay que atender del SUJETO entero, no de
   un renglón. Es un `.badge warning` con otro nombre y otro sitio; se declara
   aparte porque su sitio —empujada al canto— es lo que la distingue. */
.cfg-grupo-marca{
  font-size:var(--fs-micro);font-weight:500;flex:none;white-space:nowrap;
  color:var(--app-atencion-text);background:var(--badge-warning);
  border-radius:var(--r-mini);padding:1px 7px;
}

/* ── Y LA FILA, DENTRO DEL GRUPO, CAMBIA DE ANATOMÍA ──────────────────
   Sigue siendo `.cfg-fila` con sus mismas piezas —punto, rótulo, detalle,
   gestos— y por eso **migrar una página es envolver sus filas y nada más**: ni
   un `<span>` que renombrar. Lo que cambia es la geometría, y son dos cosas:

   1. EL RÓTULO ES UN PRETITULAR, no una columna. `.cfg-fila` lo pone a la
      izquierda del valor; aquí va ENCIMA. Nació como columna fija de 92px y el
      precio se veía: en un panel de 7/12 esos 92px se los quitaba al valor Y a
      los gestos, con el nombre partido en dos líneas y sus metas asomando bajo
      los botones. Puesto encima, el valor arranca donde arranca el grupo y el
      renglón recupera el ancho entero — que es **la misma anatomía que el
      catálogo ya declara para el campo**: etiqueta encima, dato debajo. Lo que
      se probó y se descartó fue la excepción, no la regla. *(Se estrenó como
      modificador `.au-pre` para comparar las dos formas en la misma pantalla y
      subió a la clase el mismo día; el modificador se borró en vez de quedarse
      — la lección de `.fx-campo`.)*
   2. LOS GESTOS VAN EN LA LÍNEA DEL RÓTULO (2026-09-19, pedido de Arif sobre
      «Identidad» en `cuenta.php`). El razonamiento es de quién tiene ancho
      garantizado: el rótulo es corto y de largo conocido —«Nombre», «Zona
      horaria», «Tema»—, así que a su derecha SIEMPRE sobra sitio; el valor es
      el variable —un nombre largo, un correo, una descripción de tres
      líneas—, así que se queda con la línea entera.
      Antes compartían línea valor y gestos, y de esa disputa salieron dos
      parches que esta geometría deja sin trabajo: los 220px de base del valor
      para forzar el envolvimiento, y `.cfg-detalle-corto` para el valor que es
      sólo un conteo (retirado el mismo día). `.cfg-fila` fuera del grupo
      manda los gestos a una TERCERA línea; aquí caben en la primera, que es lo
      que sigue haciendo compacto al grupo.

   EL PUNTO BAJA CON EL VALOR, que es de lo que habla —verde opera, gris no
   está, ámbar pide mirar, y lo que pasa lo dicen las palabras del valor, no
   las del rótulo—. Arriba marcaba el renglón; abajo marca el dato. Se alinea a
   la PRIMERA línea del valor y no a su centro: con una descripción de tres
   líneas, un punto centrado flota a media altura sin nada al lado.

   LA REJILLA es la de `.cfg-fila` de siempre (punto · contenido · gestos) con
   una diferencia: el hueco entre columnas lo ponen las piezas y no la rejilla,
   para que la columna del punto desaparezca del todo en la fila que no lo
   lleva y el rótulo arranque pegado al canto del grupo. Con `column-gap` esa
   fila se indentaría 12px contra sus hermanas. */
/* La rejilla y el reparto de las piezas son los de `.cfg-fila` —una sola
   anatomía desde el 2026-09-19—; el grupo sólo cambia lo suyo: el aire entre
   líneas, el relleno sin sangría lateral (lo pone la caja), el filete y el
   pretitular de abajo. */
.cfg-grupo .cfg-fila{
  row-gap:2px;padding:9px 0;margin:0;border-radius:0;
  border-top:1px solid var(--app-hairline);
}
/* El fondo lo pone el grupo: la fila lo suelta (arriba, el porqué). */
.cfg-grupo .cfg-fila:hover{background:transparent}
.cfg-grupo .cfg-fila:first-child{border-top:0}
/* `flex-wrap` porque el pretitular ya no tiene la línea entera para él: en el
   Webmaster de `cuenta.php` lleva el nombre de la cuenta más sus chips, y al
   compartir línea con los gestos tiene que poder partir en vez de salirse.

   EL COLOR ES EL SECUNDARIO (2026-09-22, pedido de Arif: el título de la
   tarjeta en terciario «resta visibilidad»). El terciario es el gris de lo
   apagado y daba 4.9:1 sobre el fondo del grupo en claro; el secundario da
   6.9:1 y es el tono de los iconos del menú. Tamaño y peso no cambian: el
   valor, en el color del texto y a 14, sigue siendo lo que más pesa. */
.cfg-grupo .cfg-rotulo{
  grid-column:1 / 3;grid-row:1;min-width:0;
  color:var(--app-fg-secondary);font-size:var(--fs-micro);font-weight:400;
  letter-spacing:.02em;display:flex;align-items:center;flex-wrap:wrap;gap:7px;
}
/* El valor apila sus partes (el dato y sus metas) en vez de sentarlas en fila:
   es lo único que el grupo le cambia. */
.cfg-grupo .cfg-detalle{flex-direction:column;align-items:flex-start;gap:2px}
/* `.cfg-detalle-corto` VIVIÓ AQUÍ y se retiró el 2026-09-19 con el cambio de
   arriba. Existía porque el valor pedía 220px de base para envolver y los
   gestos le disputaban la línea: un valor que es sólo un conteo («1 usuario»,
   59px) gastaba tres líneas en el riel de 370 de `cuenta.php` §Webmaster. Con
   los gestos arriba, el valor ya no compite con nadie y el modificador no
   tenía qué arreglar. Queda escrito para que no vuelva a nacer: el síntoma que
   lo pedía era de la geometría, no del valor corto. */

/* ── LA FICHA DE AYUDA (`.ay-ficha`, 2026-09-22, `ayuda.php`) ────────────
   Una respuesta corta dentro de un `.cfg-grupo`: el grupo es la sección fija
   («Tareas», «Qué significa», «Qué pasa si») y cada ficha cuelga de él con el
   mismo filete y el mismo aire vertical que `.cfg-grupo .cfg-fila`, así que la
   página se lee como el resto de las consolas de configuración.

   NO ES UNA `.cfg-fila`: la fila es rótulo + valor + gestos en una rejilla de
   tres columnas, y la ficha es un título y un párrafo o una lista que ocupan
   el ancho entero. Meterla en la rejilla obligaba a anular la rejilla. Tampoco
   reusa `.ui-pieza`: ésa vive en el `<style>` de `ui.php` y es del catálogo,
   no del sistema.

   EL TEXTO VA EN `--app-fg` y no en secundario, al revés que `.seccion-nota`:
   aquí el párrafo es el contenido que se vino a leer, no la nota que acompaña
   a otra cosa. El título se distingue por el peso, no por el tamaño: las
   fichas son muchas y del mismo rango, y un tamaño mayor en cada una haría de
   la página una lista de titulares.

   EL ANCHO DE LECTURA SE ACOTA a 68ch, el de `.seccion-nota`: la consola da
   860px y a ese ancho una línea de pasos pasa de cien caracteres. */
.ay-ficha{padding:10px 0 12px;border-top:1px solid var(--app-hairline)}
.ay-titulo{font-size:var(--fs-base);font-weight:600;color:var(--app-fg);line-height:1.4}
.ay-ficha p{margin:4px 0 0;font-size:var(--fs-base);color:var(--app-fg);line-height:1.55;max-width:68ch;text-wrap:pretty}
/* Los pasos son una `<ol>` real: el número es parte de la instrucción («en el
   paso 3…»). La sangría la pide el marcador y no más; su color es el
   secundario porque el número ordena y el texto es lo que se lee. */
.ay-pasos{margin:4px 0 0;padding-left:20px;font-size:var(--fs-base);color:var(--app-fg);line-height:1.55;max-width:68ch}
.ay-pasos li + li{margin-top:2px}
.ay-pasos li::marker{color:var(--app-fg-secondary);font-variant-numeric:tabular-nums}


/* ── RENGLONES COMO TARJETAS (`.cfg-tarjetas`) ──────────────────────────────
   La otra forma de agrupar renglones, hermana de `.cfg-grupo` y su contraria
   exacta: allí las filas comparten UNA superficie y se separan con filetes;
   aquí cada fila ES su superficie y lo que las separa es aire.

   Se usa cuando **cada fila es su propio sujeto y su nombre ES el dato** —el
   caso que la propia ficha de `.cfg-grupo` declara como el que no estaba
   pensada para servir—. Un catálogo de categorías es eso: seis cosas
   independientes, no seis facetas de una.

   TRES DECISIONES, Y LAS TRES SON LA MISMA:

   1. EL FONDO ES FIJO, Y ES EL DEL HOVER. La veladura de `--app-row-hover`
      deja de ser una respuesta al cursor y pasa a ser el reposo de la pieza,
      igual que hizo `.cfg-grupo`. Y por lo mismo el hover se apaga: la fila no
      se pulsa en ninguna página —el barrido sólo encuentra
      `boton.closest('.cfg-fila')`—, así que un fondo que responde prometía un
      clic que no existe.

   2. SIN LA COLUMNA DEL PUNTO. `.cfg-fila` abre con `8px · 12px` de canal para
      el semáforo, y en un catálogo con interruptor ese punto no existe (es la
      señal duplicada que se retiró el 2026-09-03): quedaban 20px de sangría
      sosteniendo una columna vacía. Con fondo, esa sangría se VE — es el hueco
      entre el canto de la tarjeta y su rótulo, y era el doble del de la
      derecha.

   3. SIN LA SANGRÍA NEGATIVA. `.cfg-fila` lleva `margin:0 -8px` para que su
      hover sangre hasta los cantos del riel; una tarjeta tiene canto propio y
      con la sangría se salía del grupo por los dos lados.

   El hueco entre tarjetas (6px) es menor que su padding interior: lo de dentro
   de una tarjeta tiene que agruparse más que dos tarjetas entre sí, o el
   conjunto se lee como una sola superficie con rayas.

   CONSUMIDORES (movido del catálogo `ui.php`, 2026-09-19): nació el
   2026-09-06 para el catálogo de categorías de `items.php`, y ese mismo día lo
   tomó el de `gastos.php`, un peek que llevaba unas horas con `.cfg-grupo`. Se
   midió igual: el nombre pasó de 13px/400 terciario a 14px/500 en tinta plena
   y la fila de 120.9 a 104.9px. Migrar costó cambiar una clase y el ORDEN de
   dos `<span>`. La forma con botón y perilla juntos en la segunda línea y la
   numeralia a su izquierda se dibujó y se midió (72px, 27 menos) y se
   descartó: ganaba alto a cambio de romper la regla de la perilla sola. */
.cfg-tarjetas{display:flex;flex-direction:column;gap:6px}
.cfg-tarjetas .cfg-fila{
  grid-template-columns:1fr auto;
  column-gap:8px;row-gap:6px;align-items:center;
  margin:0;padding:9px 12px;
  background:var(--app-row-hover);
  border-radius:var(--r-ctl);
  border-top:0;
}
.cfg-tarjetas .cfg-fila:hover{background:var(--app-row-hover)}
/* CADA COSA EN SU FILA: nombre · controles · numeralia (2026-09-06, pedido de
   Arif). Tres filas, y ninguna comparte trabajo con otra.

   ── EL NOMBRE SE QUEDA SOLO CON SU LÍNEA ──
   En una tarjeta de riel el nombre de la categoría NO es un rótulo que
   etiqueta un dato: **es el dato**. Compartir su línea con el interruptor
   —como hacía la primera pasada, calcando `.cfg-fila`— le quitaba 44px fijos
   de los 275 útiles, en todas las tarjetas y para siempre, por un control que
   mide lo mismo diga lo que diga el nombre. Es la misma inanición que
   `.vn-renglon` midió con los dos botones, y se arregla igual: el que no
   crece baja, el que es el dato se queda con la línea entera.

   ── LA SEGUNDA FILA ES SÓLO DE MANDOS, Y A LOS DOS CANTOS ──
   Gesto a la izquierda, interruptor a la derecha, nada en medio. Los dos
   quedan anclados a un borde, así que las cuatro o cinco tarjetas del riel
   forman DOS columnas a plomo —una de botones y una de estado— y el catálogo
   se recorre de un vistazo por cualquiera de las dos. Es lo que se pierde en
   cuanto un control flota detrás de un texto de largo variable: se probó, y
   la perilla cambiaba de sitio en cada tarjeta.

   Y así la excepción no hace falta: «junto al interruptor no va ningún otro
   control» sigue valiendo aquí igual que en la fila ancha, porque lo que hay
   junto a la perilla es el canto de la tarjeta.

   ── LA NUMERALIA VA DEBAJO, Y ES LA ÚNICA QUE CRECE ──
   Es lo accesorio —cuántos productos cuelgan, si está apagada— y es también
   lo único de la tarjeta cuyo largo no se sabe de antemano. Abajo y a lo
   ancho puede envolver sin mover nada de lo de arriba: las dos columnas de
   mandos siguen a plomo aunque una categoría diga dos cosas y sus vecinas
   una. Se midió la forma contraria —numeralia entre el nombre y los mandos—
   y cuesta lo mismo (99px las dos), así que la decide el orden de lectura y
   no el espacio: primero qué es, luego qué puedo hacer. */
/* EL MARCADO VA EN EL ORDEN EN QUE SE VE, y en `items.php` hubo que cambiarlo:
   `.cfg-fila` pone los gestos ANTES de las acciones, y con la rejilla colocando
   a mano las dos celdas eso dejaba el foco recorriendo la fila de derecha a
   izquierda —interruptor y luego el botón que está a su lado izquierdo—. La
   rejilla no necesita el orden para pintar; el teclado sí para tener sentido. */
.cfg-tarjetas .cfg-rotulo{grid-column:1 / -1;grid-row:1}
.cfg-tarjetas .cfg-acciones{grid-column:1;grid-row:2;justify-self:start;margin-top:0}
.cfg-tarjetas .fn-gestos{grid-column:2;grid-row:2;justify-self:end}
.cfg-tarjetas .cfg-detalle{grid-column:1 / -1;grid-row:3}
/* LA TARJETA SIN CONTROLES SUBE SU NUMERALIA A LA FILA 2. Con el detalle
   clavado en la 3, una tarjeta que no pinta gestos deja la fila 2 vacía — y una
   fila vacía en una rejilla mide 0 pero cobra sus DOS `row-gap`: medido, 12px
   entre el nombre y su dato donde las hermanas llevan 6.

   No es un caso raro: en el catálogo de gastos lo son la categoría del sistema
   —que a propósito no se apaga ni se edita— y **todas las filas cuando quien
   mira es un operador**, que no ve ningún gesto en ninguna. */
.cfg-tarjetas .cfg-fila:not(:has(> .cfg-acciones, > .fn-gestos)) .cfg-detalle{grid-row:2}

/* AQUÍ VIVIÓ `.cfg-sujeto`, y duró una tarde (2026-09-05).
   Era la etiqueta que decía DE QUIÉN es un renglón, y nació correcta: al
   listar los autorrespondedores de todos los funnels en una lista PLANA, un
   renglón que dice «Remitente» y no de quién no dice nada.

   SE RETIRA PORQUE `.au-grupo` contesta la misma pregunta mejor: el sujeto se
   dice UNA vez, como cabeza de su grupo, en vez de repetirse en cada renglón
   —con cinco funnels eran diez etiquetas idénticas—. La pieza no estaba mal
   resuelta; estaba resolviendo un problema que la lista plana creaba.

   La lección, que es la que vale para la próxima: **antes de inventar una
   marca que repita el sujeto en cada fila, preguntar si las filas deberían
   estar agrupadas.** Si el chip aparece más de una vez seguida diciendo lo
   mismo, la respuesta es que sí. */
/* `.cfg-extra` —el gesto que aparecía sólo al posarse— se retiró el
   2026-09-23: Arif pidió todos los gestos de renglón siempre visibles. */
/* AQUÍ VIVÍA `.cfg-apilada`, y se BORRÓ el 2026-08-16 con el renglón de dos
   líneas. Era el mismo renglón con el valor y sus metas apilados, y existía
   por una sola razón escrita en su propio comentario: «el servicio del
   consumidor real mide 32 caracteres, y a 7/12 de panel elipsaba». Esa
   premisa desapareció cuando el valor dejó de compartir línea con el rótulo y
   los gestos — hoy tiene el ancho entero y el par valor + metas cabe de sobra.
   Conservarla habría dejado dos formas para el mismo renglón, que es como
   nació ella. La lección es la de `.fx-campo`: cuando la regla general
   absorbe el caso de la variante, la cura es borrar la variante, no ajustarla. */

/* ── Lo que lleva el riel: pulso + puesta en marcha ───────────────────
   Lo de aquí abajo lo pinta `.app-lado` (arriba, en el cascarón). Vive en
   este bloque porque es el acompañante de ESTA página, no chrome de la app:
   el riel es el mueble, esto es lo que hay encima.

   GRUPOS, NO CAJAS (2026-08-15, al mudarse al riel). Fueron dos `.lado-caja`
   con canto y radio mientras vivían sobre el papel del panel; en el riel el
   canto sobra, porque la superficie teñida YA es la caja — dibujar dos cajas
   dentro de una es el mismo error que la mesa cometió con el gris de
   `.tab-col`. Lo que los separa es una línea, que es como la barra lateral
   separa sus zonas.

   Y ES LA MISMA LÍNEA Y EL MISMO AIRE QUE ENTRE DOS SECCIONES DEL PANEL
   (2026-09-06, hipótesis de Arif medida sobre `cuenta.php`: *«en el panel
   secundario el espaciado es más ajustado»*). Lo era: 16+16 aquí contra los 72
   de allá, y con un separador de la mitad de peso — el riel iba 2.25 veces más
   apretado que el panel que tiene al lado.

   No era una decisión, era una fecha. Estos 16 son del 2026-08-15, cuando la
   sección del panel todavía era una CAJA y su aire valía 14; `--esp-seccion`
   nació el 2026-09-03, el día que la caja se fue y el aire pasó a SER la
   estructura, y ese barrido no llegó hasta aquí. La prueba de que el token se
   quería aquí está en su propio comentario: declara sus excepciones POR NOMBRE
   —*«no gobiernan al peek ni al diálogo»*— y el riel no es ninguna de las dos.

   Lo que lo vuelve defecto y no diferencia de molde es que en `cuenta.php` los
   grupos del riel SON secciones: no se parecen, reutilizan sus piezas
   (`.seccion-n`, `.seccion-nota` y `.seccion-pie` dentro de un `.lado-grupo`),
   y `.lado-titulo` computa la misma tipografía que `.seccion-titulo` — medido,
   15px / 700 / -0.15px / Space Grotesk los dos. Mismo rango con distinto aire
   es la jerarquía diciendo dos cosas a la vez. Tampoco valía el argumento del
   ancho: ahí el riel es `lado-cinco`, 5/12 contra los 7/12 del panel.

   Va por TOKEN y no por número, que es lo que este archivo ya exige en el
   sentido contrario: cuando el ritmo SÍ debe cambiar, se redeclara el token con
   su porqué al lado (`.vn-renglon`, `--esp-opcion:6px`), nunca se escribe un
   `padding` suelto. Un 16 a mano no era una excepción declarada — era una copia
   que se quedó atrás, que es justo de lo que aquella nota se protegía. */
.lado-grupo + .lado-grupo{
  border-top:1px solid var(--app-borde);
  margin-top:var(--esp-seccion);
  padding-top:var(--esp-seccion);
}
.lado-cab{display:flex;align-items:center;gap:8px}
.lado-cab .hueco{flex:1}
.lado-titulo{font-family:var(--fuente-titulo);font-size:var(--fs-dialogo);font-weight:700;letter-spacing:-.01em;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* EN LA BANDA DEL TITULAR, QUIEN SE VA AL EXTREMO SE EMPUJA SOLO (2026-09-04).
   Decía `.lado-cabeza .lado-titulo{flex:1}` —el nombre crecía y empujaba a lo
   demás—, y eso reparte mal en cuanto la cabeza lleva TRES cosas o una que no
   es un chip:

   · Un `.lado-chip` es un ESTADO («En marcha», «$3,470.00») y sí va al extremo,
     como la marca empuja a los interruptores en la otra punta (`.sb-marca`).
   · Un `.seccion-n` es la MEDIDA del título —«2 de 2»— y va PEGADO a él, que es
     como lo hace el titular de la página con su `.conteo`. Con el título
     creciendo se iba al canto contrario y los dos dejaban de leerse juntos;
     lo cazó Arif sobre la pantalla: *«"Categorías" debe estar junto a "2 de 2",
     no junto al botón»*.
   · Un botón es un GESTO y se va al extremo, como en `.panel-acciones`.

   Con el empuje en quien se va —y no en quien se queda— las tres conductas
   salen de una regla y el título vuelve a su tamaño. **No pierde su elipsis**:
   `.lado-titulo` conserva `min-width:0` y `flex-shrink` por defecto, así que un
   nombre de funnel largo se sigue truncando y el chip, que es `flex:none`, no
   cede un píxel. */
.lado-cabeza .lado-chip,
.lado-cabeza > .btn-segunda{margin-left:auto}
/* CON UN GESTO AL LADO, EL CHIP SE QUEDA CON SU TÍTULO (2026-09-06). Los dos
   llevan `margin-left:auto` arriba, y en una cabeza que tiene los DOS eso
   reparte el hueco entre ellos: el chip acaba flotando a media banda y pegado
   al botón, que es exactamente lo que la regla de esta sección prohíbe —«el
   conteo se leía como parte del botón», el fallo que Arif cazó en el riel el
   2026-09-04—.

   Y NO ES UNA EXCEPCIÓN, ES LA MISMA REGLA APLICADA: un chip solo en la cabeza
   es un ESTADO («En marcha», en `funnels.php`) y su sitio es el extremo; un
   chip que convive con un gesto está haciendo de MEDIDA —«8 h por semana»
   contesta *cuánto horario hay*, igual que el `.seccion-n` de `cuenta.php`— y
   la medida va pegada a su título. Lo que decide no es la clase sino qué más
   hay en la banda, y por eso se pide con `:has()` y no con un modificador.

   Hoy sólo lo usa el horario de `citas-config.php`; `funnels.php` lleva chip
   sin botón y no lo toca. */
.lado-cabeza:has(> .btn-segunda) > .lado-chip{margin-left:0}
/* EL GESTO NO SE ENCOGE, la misma cláusula que `.pk-cab > .btn-segunda` ya
   declara y por el mismo motivo: **un rótulo de gesto no se parte ni se
   encoge**. Aquí es literal —«Categorías · 2 de 2 · Nueva categoría» mide 299
   en una banda de 299—, así que el día que el conteo crezca («12 de 40») quien
   tiene que ceder ancho y truncar es el TÍTULO, que para eso lleva
   `min-width:0` y su elipsis. Sin esto el que se parte es el botón, que es lo
   único de los tres que no se puede leer a medias. */
.lado-cabeza > .btn-segunda{flex:none}
/* LA CABEZA DEL RIEL SE SEPARA DE SU CONTENIDO CON LA MISMA LÍNEA que separa
   dos grupos (2026-09-04). El riel ya decía que «lo de dentro son GRUPOS, no
   cajas, separados por una hairline»; lo que le faltaba era que la CABEZA
   fuera la primera de esas fronteras — sin ella el título flotaba sobre la
   lista y el primer renglón se leía como si colgara de él.

   Nació al podar los dos párrafos de teoría del riel de productos: donde había
   un párrafo explicando la sección, hoy hay una línea, que separa mejor y no
   se lee. Aprovecha las cuatro cabezas que ya existían, y en todas dice lo
   mismo — dónde acaba el rótulo del riel y dónde empieza lo que se opera.

   SE QUEDA EN LA HAIRLINE Y EN LOS 16 el día que el separador entre grupos sube
   al filo y al `--esp-seccion` (2026-09-06). Nació diciendo «la misma línea que
   separa dos grupos» y ya no lo es, y está bien que no lo sea: con los dos
   iguales la frontera cabecera→contenido pesaba tanto como la frontera
   sección→sección, que es la jerarquía plana que este riel venía arrastrando.
   El vínculo de una cabeza con LO SUYO es más estrecho que el salto de una
   sección a la siguiente, y ahora los dos pesos lo dicen. Es el mismo reparto
   que el panel ya hace: su primera `.seccion` tampoco lleva filo — de ella y su
   titular se encarga `--esp-titulo`, que es el aire menor. */
.lado-cabeza + .lado-grupo{border-top:1px solid var(--app-hairline);padding-top:16px}
/* El chip de estado del funnel: tinte translúcido + texto de su pareja, la
   fórmula de los badges. */
.lado-chip{font-size:var(--fs-micro);font-weight:600;border-radius:var(--r-pill);padding:2px 9px;flex:none}
.lado-chip.marcha{background:var(--badge-info);color:var(--app-info-text)}
.lado-chip.alto{background:var(--badge-warning);color:var(--app-atencion-text)}
/* La caja de la gráfica. ALTO FIJO, y de ahí cuelga todo lo demás: el canvas
   se estira al ancho del riel y sin un alto que lo detenga crecería en la misma
   proporción, empujando fuera lo que venga debajo. Los 125px son los de su
   gemela en la rueda. `position:relative` es requisito de Chart.js para medir
   con `maintainAspectRatio:false`, no decoración. */
.lado-grafica{position:relative;height:125px;margin-top:10px}
/* El pie de la gráfica: lo que la barra no puede decir —de cuánto es el total
   del periodo—. Meta, no dato: el dato son las barras. */
.lado-graf-pie{margin-top:8px;font-size:var(--fs-meta);color:var(--app-fg-tertiary)}

/* LAS ACCIONES RÁPIDAS del riel (2026-08-30, pedido de Arif): capturar sin ir
   al módulo, estrenadas en el inicio con «Nueva venta». El contenedor es una
   fila que envuelve —la segunda acción cae al lado, la que ya no quepa baja—
   con el mismo 10 de aire tras el rótulo que usa la gráfica del riel.
   Los botones son los MISMOS objetos de sus módulos (`.btn-segunda` con su
   `data-*` de delegación): el gesto ya tiene pieza y rótulo en `venta.php` y
   aquí no se inventa una segunda. Grises y no azules porque no escriben —
   abren la capa de captura, y el que escribe es el «Guardar» de su pie (la
   regla del riel de cuenta: «Nuevo usuario»). Y NO cargan nada al pintarse:
   el catálogo del panel viaja al PRIMER clic (venta-nueva.js), que es lo que
   deja al inicio costar lo que costaba con tres mil clientes en la cuenta. */
.lado-acciones{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px}
/* EL SUB-RÓTULO de un grupo de acciones (2026-08-30, propuesta de Arif al
   llegar la cuarta acción): terciario porque rotula, no manda.
   NACIÓ EN `--fs-micro` —la voz de lo que acompaña— y subió a `--fs-base` el
   2026-08-31 (pedido de Arif): a 13px se leía como pie de página de los
   botones y no como el encabezado de su grupo, que es lo que es. El color
   terciario ya hace todo el trabajo de subordinarlo al título de la sección;
   quitarle además el tamaño estándar era cobrarle dos veces la misma
   jerarquía. La aritmética del aire es de pertenencia: 18
   arriba (separa del grupo anterior — con 12, los dos grupos se leían como
   una sola pila y Arif lo señaló el mismo día: «quedó extraño») y 6 abajo
   vía la fila que le sigue — el rótulo tiene que leerse pegado a SUS
   botones, no equidistante. El primero, tras el título de la sección, no
   carga grupo previo del que separarse y baja a 10, el aire estándar del
   riel. La regla de cuándo existe vive en la página, no aquí: SOLO con dos
   grupos visibles — un sub-rótulo sobre el único grupo es la etiqueta de la
   etiqueta (el mismo criterio de la coletilla del tablero en la portada). */
.lado-acc-rotulo{font-size:var(--fs-base);color:var(--app-fg-tertiary);margin-top:18px}
.lado-cab + .lado-acc-rotulo{margin-top:10px}
.lado-acc-rotulo + .lado-acciones{margin-top:6px}
/* AQUÍ VIVIÓ `.lado-acciones.filas` —la misma fila de acciones, APILADA— entre
   el 2026-08-31 y el 2026-09-09. La estrenó y la mató la misma sección: el
   grupo «Catálogos» del riel del inicio, su único consumidor en los dos
   sentidos. Lo que decía el apilado era «son destinos distintos, coge uno», y
   valía porque aquéllos eran ENLACES a tres listas maestras; el 2026-09-09
   Arif los cambió por tres botones que abren capas de captura, o sea variantes
   del mismo gesto — intercambiables, que es exactamente lo que dice el
   envolvente sin `filas`.
   Se retira y no se conserva «por si acaso»: una capacidad sin superficie que
   la ejercite es código que se pudre sin que nadie lo note, la misma regla con
   la que el combo perdió su segunda columna. La receta, si alguna vez vuelve a
   hacer falta, cabe en una línea — `flex-direction:column` más
   `align-items:flex-start`, nunca `stretch` (se probaron estirados al ancho
   del riel y Arif lo cortó el mismo día: dos pills a todo lo ancho debajo de
   cuatro ajustados a su texto ya no se leen como el mismo objeto). */

/* ── EL DATO SUELTO DEL RIEL (2026-08-31, «Próxima cita» y «Nuevo lead») ──
   Dos renglones: QUIÉN arriba y CUÁNDO —más el contexto que quepa— debajo.
   Nació con dos consumidores, que es el único motivo por el que nació como
   pieza y no como marcado de la página. **Desde el 2026-09-09 le queda uno**:
   Arif retiró «Nuevo contacto» del riel con su consulta. La clase se queda
   —«Próxima cita» la pinta igual— pero pierde su segundo caso, y con él el
   único que usaba TRES renglones (cuándo + funnel). Si un día no queda
   ninguno, la pieza se retira: una capacidad sin superficie que la ejercite
   es código que se pudre sin que nadie lo note.

   ES UNO, EN SINGULAR, Y ESO DECIDE TODO LO DEMÁS (pedido de Arif el mismo
   día que se estrenó en plural). Con DOS filas por sección la pieza no podía
   ser enlace: ninguno de los dos módulos tiene enlace profundo —no existe
   `?lead=` ni `?cita=`, se comprobó— así que las dos habrían llevado al mismo
   listado, y una fila que se ilumina al apuntarla promete un destino propio.
   Con UNA fila esa objeción desaparece: hay un solo destino y es el correcto,
   así que la pieza SÍ es `<a>` y sí se ilumina. Menos filas compraron un
   gesto, no sólo espacio.

   EL LISTÓN PARA QUE ALGO VUELVA A LEER EN EL RIEL (movido del catálogo
   `ui.php`, 2026-09-19): las cinco consultas que se retiraron el 2026-08-30
   eran agregados sobre la base entera de la cuenta y su costo crecía con los
   contactos; las que quedaron leen UNA fila por índice y su costo no depende
   del tamaño de la cuenta (medido con EXPLAIN: `range` sin ordenación, y en la
   de contactos con el índice fijado a mano — suelto, el motor elegía uno que
   deja `id_cuenta` como filtro posterior). Una sección que se retira se va con
   su consulta: «Nuevo contacto» salió del riel el 2026-09-09 con
   `ultimosLeads()`, que sobrevive sólo porque el MCP la usa en
   `resumen_contactos`.

   `text-decoration:none` + `color:inherit` EN LA CLASE, como toda pieza que
   es `<a>` sin leerse como enlace (`.sb-item`, `.pm-item`, `.dlg-opcion`), y
   el hover con el margen negativo de `.pm-item` para que la tinta llegue al
   canto del riel en vez de dejar un marco de aire. */
.lado-fila{display:block;padding:6px;margin:0 -6px;border-radius:var(--r-mini);
  text-decoration:none;color:inherit}
.lado-fila:hover{background:var(--app-row-hover)}
/* El nombre, con la voz del dato: `--fs-base` y peso medio. No 600 — no es un
   título, es el dato. El recorte es obligatorio a 280px de riel: una razón
   social larga partiría en tres renglones y descuadraría el grupo. */
.lado-fila-txt{font-size:var(--fs-base);font-weight:500;color:var(--app-fg);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* El acompañamiento, en la voz de `.lado-graf-pie`: meta y terciario. Se
   RECORTA también — el funnel de un lead puede tener nombre largo, y lo que
   se sacrifica primero es el contexto, nunca el cuándo, que va delante. */
.lado-fila-meta{margin-top:1px;font-size:var(--fs-meta);color:var(--app-fg-tertiary);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* Su aire tras la cabeza del grupo, el mismo que toma `.lado-acciones`.
   Y DESDE EL 2026-09-10, TAMBIÉN EL DE `.cfg-grupo` (lo cazó Arif en pantalla):
   `funnels.php` es la primera página que mete un grupo de renglones en un RIEL
   —hasta ese día `.cfg-grupo` sólo vivía en la consola, donde el aire se lo
   daba la `.seccion-nota` y ahí el problema no se veía; el 2026-09-19 se supo
   que en la consola pasaba igual en cuanto la sección no tenía nota, y ese
   aire pasó a tener dueño propio (`--esp-cabeza`)—, y sin esta línea la
   superficie teñida arranca PEGADA a la cabeza: el título y sus botones quedan
   sentados sobre el primer renglón, sin nada que diga dónde acaba uno y empieza
   el otro. Mismo 8 que sus dos hermanas y no un número propio: el aire tras una
   cabeza de riel es UNO, lo que va debajo no lo decide.
   La lección general (movida del catálogo `ui.php`, 2026-09-19): una pieza que
   se muda de molde trae su anatomía y no su contexto; lo que se rompe no es
   la pieza, es lo que su vecindario anterior le daba. */
.lado-cab + .lado-fila, .lado-cab + .vacio-linea, .lado-cab + .cfg-grupo{margin-top:8px}

/* La puesta en marcha: barra + siguiente paso + lista. La barra parte de 0 y
   el JS le fija su ancho al cargar — medio segundo de movimiento que dice
   "esto se está midiendo", no un adorno en bucle. */
.pm-barra{height:4px;border-radius:var(--r-pill);background:var(--app-pill);overflow:hidden;margin:10px 0 12px}
.pm-barra i{display:block;height:100%;width:0;background:var(--app-info);border-radius:var(--r-pill);transition:width .6s var(--app-ease)}
/* EL SIGUIENTE PASO NO TIENE PIEZA PROPIA (2026-09-19, pedido de Arif). Era
   `.pm-sig`: una caja del ancho del riel con cara de botón, su rótulo micro, el
   texto en negrita y una flecha al canto — un macro botón, y una pieza entera
   para lo que el sistema ya resuelve con `.cfg-grupo` + `.cfg-fila`, que es
   además el renglón que el mismo riel usa un grupo más arriba. Se retiró de la
   hoja, del catálogo y de las tres páginas que la montaban (funnels,
   automatizacion, agentes) el mismo día; lo que las tres pintan ahora es un
   renglón con el rótulo «Siguiente paso», el texto del paso y el gesto al canto
   derecho, con la vestimenta que le toca a su destino.

   Lo que se pierde con ella y hay que recordar: `.pm-sig-txt` existía para que
   el paso ENVOLVIERA en vez de cortarse —«Conectar el número de WhatsApp» se
   leía «Conectar el número de W…» a 400px—, y eso ahora lo da `.cfg-detalle`,
   que ocupa su línea entera por diseño. Y su `text-decoration:none` era el caso
   que enseñó a poner el no-subrayado EN LA CLASE y no en el `<a>` de la página
   (la lección sigue escrita en «UN BOTÓN QUE ES `<a>` SIGUE SIENDO UN BOTÓN»,
   más abajo en esta hoja). */
/* La lista: hecho con su palomita en un círculo verde lleno; pendiente con
   el aro vacío. Los dos en la tinta del cuerpo. Lo pendiente CON editor en esta página se pulsa; lo de otra página
   es un enlace normal. El aro vacío es la promesa de la palomita. */
.pm-item{display:flex;align-items:center;gap:9px;min-height:30px;font-size:var(--fs-base);color:var(--app-fg);border-radius:var(--r-mini);padding:0 6px;margin:0 -6px;text-decoration:none}
.pm-item.pend[data-pm], a.pm-item.pend{cursor:pointer}
.pm-item.pend[data-pm]:hover, a.pm-item.pend:hover{background:var(--app-row-hover)}
.pm-check{
  width:16px;height:16px;border-radius:50%;flex:none;
  display:flex;align-items:center;justify-content:center;
  font-size:10px;line-height:1;border:1.5px solid var(--app-borde-muted);color:transparent;
}
/* EL PASO HECHO SE LEE COMO HECHO (2026-09-22, pedido de Arif). Era una
   palomita en `--app-info-text` sobre `--badge-info` —verde azulado tenue— y
   el texto en terciario, así que lo completado se veía apagado. Hoy el
   círculo va lleno en `--app-exito`, el verde de los puntos de estado (Arif
   lo eligió el mismo día sobre `--app-exito-text`, que se probó primero y
   quedaba oscuro), y el texto conserva la tinta del cuerpo.
   LA PALOMITA VA BLANCA (`--app-accent-fg`, la tinta sobre relleno sólido),
   por decisión de Arif. Queda en 2.2:1 contra el verde, bajo el 3:1 que pide
   un glifo, y se acepta a sabiendas: el paso hecho lo dice también el círculo
   lleno contra el aro vacío de los pendientes, no sólo la palomita. Se probó
   oscura (7.7:1) y no se eligió. */
.pm-item.hecho .pm-check{background:var(--app-exito);border-color:transparent;color:var(--app-accent-fg)}
.pm-txt{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pm-tag{font-size:var(--fs-micro);color:var(--app-fg-tertiary);flex:none}

/* El destello con que una sección acusa recibo cuando el checklist salta a
   ella: una pasada de tinte y de vuelta a su papel.

   TAMBIÉN LO LLEVA EL RENGLÓN desde el 2026-09-05, y no es cosmética: al
   dejar `automatizacion.php` de mirar UN funnel, sus enlaces de entrada
   apuntan a una FILA de una lista (`#drip-f<id>`) en vez de a la sección
   entera, y el navegador te deja ahí sin decir cuál de las cuatro filas
   visibles es la que venías a ver. Es el mismo trabajo que hacía en la
   sección, un nivel más abajo. */
@keyframes destello{0%{background:var(--app-accent-tint)}100%{background:transparent}}
.seccion.destello,
.cfg-fila.destello{animation:destello 1.4s var(--app-ease)}

/* `.consola-salida` VIVIÓ AQUÍ hasta el 2026-09-22: el botón suelto de
   eliminar el funnel al pie del recorrido. Se retiró cuando eliminar ganó
   sección propia en `funnels.php` (pedido de Arif), con el gesto al canto de
   la cabeza como cualquier otra sección. */

/* La bajada: UNA frase con lo que hay que saber para operar lo que sigue, no
   el porqué del diseño (ése vive en el comentario del archivo). Medida en `ch`
   y no en px: lo que gobierna una línea legible es cuántos caracteres caben,
   no cuántos píxeles mide la caja.

   ── DÓNDE VIVE, Y ES LA MITAD QUE FALTABA ESCRITA AQUÍ ──
   En DOS sitios y en ninguno más: la **hoja de lectura** (`.pagina > .hoja`) y
   el **diálogo**, pegada bajo su título. En los dos el trabajo es el mismo —el
   lector va a leer de arriba abajo antes de actuar— y la bajada es la primera
   línea de esa lectura.

   **NO va sobre una sábana ni sobre un tablero**, que es lo que este
   comentario decía «la página» y dejaba pasar. Una tabla no se lee de arriba
   abajo: se escanea buscando una fila, y un párrafo encima empuja la primera
   fila fuera de la vista para dar un dato que el titular (`.panel-titulo` con
   su conteo y su `.medida`) o la fila de totales (`.sabana-totales`, al pie y
   bajo su columna) dan mejor. Las siete sábanas lo perdieron entre el
   2026-08-29 —ventas, gastos y registros— y el 2026-09-04, cuando cayó el
   último, el de `clientes.php`; sobrevivió cinco días porque la regla estaba
   escrita al pie de `.sabana-totales` y en una nota de `ui.php`, o sea en
   sitios que sólo se leen si YA sabes que existe el problema. La regla vive
   ahora en `ui.php` → Las reglas → «La bajada es de la hoja, no de la tabla»,
   que es lo que sí se lee al montar una pantalla nueva.

   REGLA: la bajada y el estado vacío NO coexisten. Con la página vacía habla
   el estado vacío —que además explica qué cae aquí—; con contenido habla la
   bajada. Dos explicaciones en la misma pantalla es la que nadie lee. */
.lede{
  font-size:var(--fs-base);color:var(--app-fg-secondary);
  line-height:1.55;max-width:66ch;margin:0 0 18px;
}
.lede strong{color:var(--app-fg);font-weight:600}

/* ── Barra de estado ──────────────────────────────────────────────────
   Último hijo del panel (flex:none), no `position:fixed`: el panel ya no
   scrollea —lo hace el viewport de la sábana— así que en el flujo queda fija
   igual, y de paso la sábana se encoge sola sin reservarle hueco a mano.
   Se alinea con la tabla, no con la ventana, porque hereda el padding del
   panel: la regla superior arranca donde arranca la primera columna. */
/* El gap sube de 2 a 6px: con botones que ahora tienen borde y relieve, 2px
   los pegaba en un bloque. Con texto plano ese aire no hacía falta. */
/* `.app-footer` y `.contador-filtro` MURIERON el 2026-08-11 con el pie del
   tablero, que era su último consumidor (la sábana ya lo había soltado el
   2026-08-09, mudando conteo y Exportar a la píldora del titular). Ninguna
   pantalla tiene pie hoy: el conteo vive arriba, en la cápsula. Se retiran en
   vez de quedarse esperando — un componente sin consumidor es exactamente lo
   que la regla de «la modularidad se gana» manda no conservar. */
/* ── Titular ──────────────────────────────────────────────────────────
   DOS MITADES Y NINGÚN MARCO (2026-08-13, pedido de Arif). A la izquierda el
   NOMBRE de la pantalla y su dato —el conteo, pegado al título y en tenue,
   como en Things—; a la derecha lo que se PULSA O SE TECLEA. Nada de eso va
   encapsulado: la fila entera es transparente.

   Es el tercer estado de esta cabecera y conviene tener el mapa, porque cada
   uno mató algo del anterior:

   1. (hasta el 2026-08-10) TODA la fila era una píldora gris: h1, conteo,
      buscador y botones dentro. Le ponía un marco a un dato — el nombre y su
      conteo son contenido, no chrome.
   2. (2026-08-10 → 08-13) el título salió de la píldora, pero el conteo se
      quedó dentro con los controles, y la píldora se pegó al h1 para que «7 de
      22» no quedara a media pantalla de su título. Costaba dos cosas: el dato
      se leía sobre una superficie de chrome —con su regla propia de contraste,
      porque el gris terciario pierde sobre gris— y en cuatro de las seis
      páginas la píldora no envolvía nada más que ese número.
   3. HOY: el conteo vuelve al lado del título, que es de donde nunca debió
      salir, y los controles se van al extremo derecho SIN marco. La cabecera
      se lee de un vistazo por sus dos extremos —qué estoy viendo · qué puedo
      hacer— en vez de por un bloque en medio.

   Lo que el marco decía («esto se pulsa») ya lo dicen los botones, que tienen
   borde y relieve propios desde el 2026-08-11; repetirlo con una píldora
   alrededor era decirlo dos veces. Y el buscador vuelve al radio de la familia
   de controles (`--r-ctl`): los 999px eran una consecuencia del extremo redondo
   de la píldora, no una decisión suya.

   ALINEA POR BASELINE, y el grupo de acciones se sale de esa regla
   (`align-self:center`). Son dos alineaciones porque son dos cosas distintas:
   el nombre y su dato son TEXTO —dos textos de cuerpo distinto se casan por la
   línea sobre la que se escriben, no por su centro, que es lo que pedía Arif
   con «el dato alineado abajo»—, y los controles no son texto: una fila de
   botones colgada de la baseline del título se descuelga hacia arriba, porque
   lo que se alinearía es la letra de dentro del botón y no el botón. Cada
   mitad se alinea como lo que es.

   TRES FORMAS según la página (movido del catálogo `ui.php`, 2026-09-19):
   título solo —una página que no tiene medida que enseñar—, título + dato, y
   título + dato + controles. El PASO del importador ocupa el sitio del conteo
   porque hace su mismo papel: es la medida de la pantalla, no una acción.

   ENVUELVE, y ninguno de los dos lados se encoge: cuando deja de caber, el
   grupo de acciones ENTERO baja bajo el título (un buscador estrujado o un
   botón partido es peor que cualquier alternativa). Sin `flex-wrap` se sale por
   la derecha en silencio — `.app-panel` recorta y el último botón desaparece.
   RE-MEDIDO el 2026-09-02, tras el desmontaje del outreach: el titular de la
   sábana perdió el botón «Columnas» y sus nombres de lista largos, así que el
   umbral se movió —envuelve cuando el título baja de **784px**, y ahí sin wrap
   se saldrían **63px**; a 860px de ventana, 83px—. La cifra vieja (85px fuera a
   1024px, con el titular de entonces) se deja como procedencia: el modo de
   fallo es el mismo y sólo cambió el ancho al que aparece. */
.panel-titulo{
  display:flex;align-items:baseline;gap:10px;flex:none;flex-wrap:wrap;
  margin:0 0 var(--esp-titulo);
  /* El ancla del halo (abajo). No hace nada más. */
  position:relative;
}

/* ── EL HALO DEL TITULAR ────────────────────────────────────────────────
   La señal de que ARRIBA HAY MÁS, una de las SEIS de su especie en el
   sistema — las otras cinco son el `border-bottom` de la cabecera de la
   sábana, la sombra de `.grupo-pegado`, los dos filos de
   `.dlg-cuerpo.desborda`, el fundido de `.rec-cuerpo.desborda` y el halo del
   panel (`.peek-cuerpo::before`, que reusa este mismo gradiente). Corregido
   el 2026-09-19: decía «la cuarta, las otras tres», el recuento de antes de
   que entraran el fundido y el halo del panel. Ésta es la del titular de
   página, y entró el 2026-08-16 pedida por Arif con su
   referencia: la de Gmail, «delgadita en las orillas y ensanchándose al
   centro, como si el contenido entrara en ella».

   ── LA FORMA, QUE ES TODO EL PUNTO ──
   Un `radial-gradient` elíptico anclado arriba y al centro: en el eje
   horizontal la elipse cubre el 62% del ancho, así que se desvanece antes de
   llegar a los cantos y deja las orillas limpias. Una sombra recta de borde a
   borde dice «aquí hay una línea»; ésta dice «aquí hay una superficie encima»,
   que es lo que de verdad ocurre. El difuminado no lo hace un `blur` —que
   costaría una capa de composición en cada scroll— sino la propia caída del
   gradiente: sale gratis y no repinta nada.

   ── DÓNDE SE PLANTA ──
   A MEDIO AIRE del titular: `top:calc(100% + var(--esp-titulo) / 2)`, derivado
   del mismo token que separa el titular de su contenido, y alto suficiente
   para cruzar el resto de ese aire y morir sobre los primeros píxeles del
   contenido. Dos correcciones hasta llegar ahí: plantado al EMPEZAR el
   contenido —`100%` + `--esp-titulo`— se leía como una mancha sobre la
   primera tarjeta, porque una sombra tiene que nacer pegada a lo que la
   produce; pegado al titular —`top:100%`— mordía los botones del grupo de
   acciones (corrección de Arif). Corregido el 2026-09-19: este párrafo decía
   `top:100%`, la segunda posición, cuando la regla ya usaba el medio aire.
   Y se sale del panel
   por los dos costados (`--panel-pad-x` en negativo) porque una sombra que
   respeta el padding se lee como un rectángulo dibujado, no como luz.

   ── CUÁNDO ──
   Sólo con scroll (`.halo`, que pone `app.js`). Permanente sería un adorno; y
   apareciendo sin motivo, ruido. En la SÁBANA no se usa: ahí lo fijo sobre el
   contenido no es el titular sino la cabecera de la tabla, que ya tiene su
   filo — dos señales para el mismo hecho es peor que ninguna. */
.panel-titulo::after{
  content:'';position:absolute;z-index:2;pointer-events:none;
  left:calc(-1 * var(--panel-pad-x));right:calc(-1 * var(--panel-pad-x));
  top:calc(100% + var(--esp-titulo) / 2);height:calc(var(--esp-titulo) + 6px);
  background:radial-gradient(62% 100% at 50% 0%, var(--app-halo), transparent 72%);
  opacity:0;transition:opacity var(--app-dur) var(--app-ease);
}
.panel-titulo.halo::after{opacity:1}
.panel-titulo h1{font-family:var(--fuente-titulo);font-size:var(--fs-titulo);font-weight:700;margin:0;line-height:1.2;letter-spacing:-.015em}
.panel-titulo .conteo{font-size:var(--fs-base);color:var(--app-fg-tertiary)}
/* El total de la base, que antes decía el pie ("8 de 22"). Va pegado al conteo
   de la lista y un escalón por debajo: el número que importa es el primero
   —cuántos hay AQUÍ—, el segundo sólo da la escala.

   HOY NO HAY ESCALÓN VISUAL (corregido el 2026-09-19). Este comentario decía
   que lo daba el TAMAÑO (`--fs-micro`, 13px); desde el 2026-09-08 el token vale
   14, igual que el `--fs-base` del conteo, y los dos van en
   `--app-fg-tertiary` con el mismo peso. Lo que distingue al «de 22» es la
   palabra «de» y que va pegado al conteo (`margin-left:-4px`).

   Lo que sigue en pie es la razón por la que NO se usa `opacity`: la llevó
   hasta el 2026-08-13 y atenuar un gris ya calibrado lo dejaba en 3.06:1
   sobre papel (3.59 en oscuro), por debajo del mínimo de 4.5 para texto de
   14px. Sin ella, el tono se queda en los 5.0/5.26 del terciario. Nunca una
   atenuación sobre otra. */
.panel-titulo .conteo-total{font-size:var(--fs-micro);color:var(--app-fg-tertiary);margin-left:-4px}
/* El "de N" llega vacío en el HTML y lo llena el JS al primer render. Vacío
   NO es invisible dentro de un flex con gap: seguiría cobrando sus 6px.

   LA REGLA DEL «de N» (movida del catálogo `ui.php`, 2026-09-19): se pinta
   cuando las dos cifras pueden diferir, y en una lista que ya viene recortada
   por el servidor sólo difieren FILTRANDO — las dos mitades salen del mismo
   `count()`, así que «8 de 8» permanente no informa y le quita el sitio a la
   medida (`.medida`, abajo).

   La sábana de contactos fue la excepción y dejó de serlo el 2026-09-08 sin
   que la regla cambiara: ahí el par sí eran dos medidas distintas —la lista
   abierta y la base— mientras hubo seis listas, que se retiraron el
   2026-09-02. Con una sola lista y su predicado siempre verdadero, la
   excepción se quedó sin motivo y el titular decía «402 de 402». Es el fallo
   a vigilar en todo el sistema: una excepción que sobrevive a la razón que la
   sostenía y se sigue leyendo como decisión.

   La cumplen tablero, ventas, gastos, registros, cartera, presupuestos, ventas
   programadas y los tres catálogos (contactos, clientes, productos). Citas y
   envíos todavía escriben el par en PHP (`citas.php` y `envios.php`, en el
   `span.conteo-total`, comprobado el 2026-09-19): es la deuda que queda de
   esta regla. */
.panel-titulo .conteo-total:empty{display:none}
/* ── LA MEDIDA: QUÉ ES ESE NÚMERO ──────────────────────────────────────
   La continuación del conteo cuando el conteo solo no se explica: «5 con saldo
   pendiente por cobrar», «7 con saldo pendiente por pagar». Nació con ventas y
   gastos el 2026-08-29, y nació porque el par conteo + «de N» ahí no medía
   nada: las dos mitades salían del mismo `count()` y la cabecera decía «8 de 8»
   todo el tiempo. El «de N» pasó a aparecer SÓLO al buscar —que es cuando los
   dos números difieren, y es lo que el tablero ya hacía desde su primer día— y
   el sitio que dejó libre lo ocupa esto, que sí dice algo.

   SIN PUNTO SEPARADOR, al revés que `.periodo` de aquí abajo, y la diferencia
   no es de estilo sino de gramática: esto CONTINÚA la frase del número («5 con
   saldo pendiente…») y un punto en medio la parte; el periodo es una
   APOSICIÓN («8 · del 01/08 al 31/08») y sin punto se lee «8 del 01/08», que es
   otra cosa. Cada una lleva el separador que su frase pide.

   Comparten declaración porque son la misma pieza en dos gramáticas: cuerpo
   base y terciario, alineadas con el conteo por la baseline del padre.

   Y EL PERIODO ES SU HERMANA: el rango de fechas de una página que, además de
   su vista propia, tiene una POR FECHA (`rangoDeLaUrl`, interno/f-fecha.php —
   el contrato que estrenó citas y que hoy copian ventas y gastos).

   El botón que abre el rango se rotula con `rotuloRango()` (el porqué, en
   interno/f-fecha.php): «Por fecha» desde la vista propia y «Cambiar fecha» ya
   dentro de un rango, o SIEMPRE en una página que no tiene otra vista —ventas
   y gastos con `es_registro = 0`, y registros, que no la tiene nunca porque un
   movimiento no es un documento con saldo— (movido del catálogo `ui.php`,
   2026-09-19).

   VA FUERA DEL h1, y ahí estuvo hasta el 2026-08-29. Dentro salía «Citas del
   01/08/2026 al 31/08/2026» a 26px en negrita: dos fechas que se CONSULTAN —se
   miran para confirmar qué se está viendo— con el peso del nombre del módulo,
   doblando el ancho del título y empujando el grupo de acciones a una segunda
   línea a anchos normales de trabajo. Un rango no nombra la pantalla; la mide,
   igual que el conteo. */
.panel-titulo .medida,
.panel-titulo .periodo{font-size:var(--fs-base);color:var(--app-fg-tertiary)}
/* EL PUNTO LO PONE LA PIEZA, no la página. Sin él, «8 de 8 del 01/08/2026 al
   31/08/2026» son dos grises seguidos del mismo cuerpo y el «de 8» se lee como
   si abriera la fecha. Es el mismo punto que ya separa día y hora en la celda
   de Cuándo, y va en el `::before` para que ninguna página que copie el
   contrato pueda olvidarlo — la pieza trae puesto lo que la hace legible. Los
   10px lo dejan a la misma distancia por los dos lados: el `gap` del titular
   pone los de la izquierda. */
.panel-titulo .periodo::before{content:'·';margin-right:10px}
/* ── Grupo de acciones del titular ────────────────────────────────────
   Lo que se PULSA O SE TECLEA, al extremo DERECHO y sin marco (2026-08-13).
   Fue una píldora gris con el conteo dentro hasta ese día; el porqué del
   cambio está arriba, en `.panel-titulo`.

   `margin-left:auto` y no `justify-content:space-between` en el padre: la
   mitad izquierda son TRES hijos sueltos (h1, conteo, «de N») y el
   space-between los repartiría por toda la fila. Con el auto, el título y su
   dato quedan pegados entre sí y el grupo se va solo al otro extremo.

   No tiene alto propio: lo pone su contenido —controles de 27px— y la fila la
   levanta el h1 (31.2 = 26×1.2). Mientras fue píldora medía `--app-barra-h`
   porque era la pieza que mandaba en la cabecera; hoy la que manda es el
   título, así que la cabecera bajó 13px y esos 13px se los quedó la tabla. */
.panel-acciones{
  display:flex;align-items:center;gap:8px;flex:none;flex-wrap:wrap;
  justify-content:flex-end;margin-left:auto;
  /* La excepción a la baseline de la fila: un botón no es texto (ver arriba). */
  align-self:center;
  /* La pareja del wrap, y sin ella el wrap no sirve de nada: siendo `flex:none`
     el grupo se dimensiona por su contenido, así que nunca le falta ancho
     para envolver — se sale del panel y `.app-panel` le recorta el último
     botón EN SILENCIO. El umbral vive en el comentario de `.panel-titulo`
     (re-medido el 2026-09-02: 784px de título; 63px fuera sin wrap). Con el
     tope, la última pieza baja de renglón. Cuando cabe —lo normal— ninguna de
     las dos declaraciones hace nada. */
  max-width:100%;
}
/* Ningún control se encoge cuando el título es largo. */
.panel-acciones > *{flex:none}
/* El enlace no tiene caja: con los 8px del `gap` su subrayado queda pegado al
   borde del buscador y del botón vecino (2026-09-22, «Ayuda» en
   Colaboradores). Se le suman 8px sólo del lado donde tiene vecino —16px en
   total—; entre controles el `gap` sigue en 8px, y el enlace del extremo no
   se despega del canto. */
.panel-acciones > * + .enlace,
.panel-acciones > .enlace + *{margin-left:8px}
/* El mismo aire en el cuerpo (2026-09-23, Arif): 16px entre un enlace y su
   vecino en los grupos de gestos de renglón y en las acciones del riel.
   `.fn-gestos` tiene 4px de `gap`, por eso suma 12. */
.cfg-acciones > * + .enlace,.cfg-acciones > .enlace + *,
.lado-acciones > * + .enlace,.lado-acciones > .enlace + *{margin-left:8px}
.fn-gestos > * + .enlace,.fn-gestos > .enlace + *{margin-left:12px}

/* ── Buscador ─────────────────────────────────────────────────────────
   Vive dentro de `.panel-acciones`, al extremo derecho de la fila del título.
   Conserva su `align-self:center` para poder colgarse de una fila alineada por
   BASELINE sin quedar descolgado — dentro del grupo lo centra el contenedor.

   Alto derivado de los botones, no copiado: mismo `--btn-py` menos el borde y
   mismo `--btn-lh`, igual que el resto de los controles. Si el botón cambia de
   alto, el buscador lo sigue solo. */
.buscador{position:relative;align-self:center;flex:none}
.buscador input{
  /* ANCHO FIJO (2026-08-10). Antes eran dos: 210 en reposo y 290 al enfocar,
     con la idea de no robarle sitio al título hasta que hiciera falta. En la
     práctica el efecto es que al hacer clic la fila entera se reacomoda —el
     grupo de acciones crece hacia la izquierda— y ese salto se lee como un
     fallo, no como una atención. 240 es el punto medio: cabe una frase de
     búsqueda razonable sin que el titular tenga dos anchos. */
  width:240px;
  /* lo único que no hereda: sitio a la derecha para la × */
  padding-right:26px;
}
.buscador input::placeholder{color:var(--app-fg-tertiary)}
/* La × nativa de WebKit sólo aparece al pasar el cursor, y en móvil no hay
   cursor. Se apaga y se pinta la propia, que además responde al Esc. */
.buscador input::-webkit-search-cancel-button,
/* El campo del buscador global es el mismo `type="search"` y tiene el mismo
   motivo para apagarla: aquí ni siquiera hay una × propia que la sustituya —
   Esc cierra el diálogo entero, que es la salida que el usuario busca. */
#bu-q::-webkit-search-cancel-button{-webkit-appearance:none;appearance:none}
.buscar-limpiar{
  position:absolute;right:7px;top:50%;transform:translateY(-50%);
  color:var(--app-fg-tertiary);cursor:pointer;line-height:1;
  padding:0 3px;border-radius:var(--r-ctl);user-select:none;
}
.buscar-limpiar:hover{color:var(--app-fg);background:var(--app-chip)}

/* La nota de "esto no es de esta lista". Teñida, no de color de letra: es
   estado, no acción — misma regla que los badges. */
.busqueda-nota{
  /* Vive DENTRO del aire del título (--esp-titulo lo puso arriba) y pone el
     suyo abajo: así aparecer y desaparecer no mueve la tabla de sitio. */
  flex:none;margin:0 0 12px;padding:7px 11px;
  background:var(--badge-info);border-radius:var(--r-ctl);
  font-size:var(--fs-meta);color:var(--app-fg);
}

/* ── Sábana ─────────────────────────────────────────────────────────
   Lista virtualizada propia (sin DataTables): el espaciador sostiene la
   altura total y sólo se pintan las filas de la ventana visible.
   ALTURA DE FILA FIJA — de ahí sale la aritmética del scroll. Un editor
   en línea que crezca la fila es un bug, no una mejora. */
/* `overflow:hidden` para recortar la CABECERA cuando se desplaza en X (ver
   `.sabana-cabecera`): sin él se saldría por la izquierda sobre la barra
   lateral. No afecta al scroll vertical, que vive en el viewport. */
/* LA TABLA SE CIERRA POR LOS CUATRO COSTADOS (2026-08-28). Nació abierta por
   arriba, izquierda y derecha: la cabecera sólo llevaba `border-bottom` y las
   celdas de los extremos no llevan filo por fuera (`:not(:last-child)`), así que
   los filos VERTICALES arrancaban del canto del tinte sin nada que los rematara
   y los HORIZONTALES morían en el aire por los dos lados. Es el mismo defecto
   que `.fila-rejilla` corrigió con `align-items:stretch` —«bordes completos,
   tipo Excel»— dicho en los otros dos ejes: una reja a la que le falta el marco
   se lee como cortada, no como abierta.

   ── LA CAJA MIDE SUS FILAS, NO EL PANEL ──
   El marco entró en dos pasos el mismo día y el primero fue con `flex:1`: la
   caja llenaba el panel, así que el travesaño de abajo caía en el pliegue y
   encerraba un desierto —ocho filas arriba y trescientos píxeles de nada dentro
   del marco— (corrección de Arif: «que sólo cubra las celdas activas»). Con
   `flex:0 1 auto` la caja mide lo que mide su contenido, y eso aquí es EXACTO
   sin medir nada: el espaciador del scroll virtual ya vale `filas × ALTURA`, así
   que la altura natural de la sábana es su cabecera más todas sus filas.
   Los dos regímenes salen del mismo `flex`:
     · lista corta → la caja vale lo que las filas y el marco cierra bajo la
       última (1px de hueco, medido);
     · lista larga → `flex-shrink` la recorta al alto disponible y el viewport
       scrollea, y entonces el travesaño cierra sobre la última fila VISIBLE,
       que es correcto porque ahí sí hay datos.
   ── EL FILO DE ABAJO ES UNO, NO DOS (2026-08-28, lo cazó Arif a ojo) ──
   Ajustada y sin barra horizontal, la última fila apoya en el fondo de la caja,
   así que su `border-bottom` y el del marco quedaban CONTIGUOS —no encimados—:
   dos franjas de 1px pegadas, o sea un fondo de 2px contra 1px del resto de la
   reja. Medido: fila en [392.2, 393.2] y caja en [393.2, 394.2]. Se leía como
   un borde más grueso o una sombra, que es exactamente lo que era: un accidente,
   no una decisión.
   El arreglo es el `margin-bottom:-1px` del viewport (abajo): sube 1px el canto
   del contenido de la caja, así que el filo del marco se pinta ENCIMA del de la
   fila en vez de debajo. Es el mismo colapso que una tabla HTML hace sola con
   `border-collapse`, y por eso no se resuelve suprimiendo el borde de la última
   fila: con scroll virtual, `:last-child` es la última fila PINTADA —no la
   última del listado—, así que quitárselo abriría un hueco a media lista.
   Los otros tres cantos nunca doblaron: arriba el marco va sobre el tinte de la
   cabecera y la primera fila no lleva filo superior; a los lados, las celdas de
   los extremos no llevan filo por fuera y el `padding:0 10px` de la fila las
   separa del marco.
   EFECTO COLATERAL QUE SALIÓ BIEN: `.barra-seleccion` es hermana de la sábana
   con `margin-top:10px`, y esos 10px nunca se habían visto porque la tabla se
   comía el panel entero — la barra aparecía flotando abajo. Ahora sale pegada a
   la tabla sobre la que actúa, que es lo que ese margen decía desde el principio.

   ── LOS DETALLES QUE HAY QUE SABER SI SE TOCA ──
   · VA EN `--grid-linea` Y NO EN `--app-hairline`: remata filos de la RETÍCULA,
     así que es de su familia. Se comparó en pantalla con el filo de la cabecera
     (7.6% contra 10.8%) y a 1px la diferencia no se distingue — por eso el de la
     cabecera NO se tocó: mover una decisión declarada a cambio de nada visible.
     Tampoco es `--app-borde` (15%, el de `.seccion`): ése es el canto de una
     TARJETA, y esta tabla no es una tarjeta dentro del panel — es el panel.
   · VA EN `.sabana` y no en las celdas: el marco es de la TABLA y tiene que
     quedarse quieto. En las celdas viajaría con el scroll horizontal, y el canto
     derecho se iría de la pantalla en cuanto la rejilla desbordara. `.sabana`
     es además la única de las tres cajas que no scrollea.
   · SIN RADIO. Es la superficie de trabajo, no una tarjeta: una esquina redonda
     la haría flotar dentro del panel, que es justo lo que este molde no es. Y
     con `overflow:hidden` recortaría en diagonal el tinte de la cabecera y el
     fondo opaco de las dos celdas congeladas.
   · LOS CANALES DE SCROLL QUEDAN DENTRO del marco (6px medidos, vertical y
     horizontal): en una tabla que se recorre, las barras son de la rejilla.
   · VACÍA NO SE PINTA, así que no hay caja vacía enmarcada: leads la esconde en
     `render()` (`elSabana.hidden`) y las seis que pintan en PHP la envuelven en
     un `if ($datos)` con su `.estado-vacio` al lado. */
.sabana{
  flex:0 1 auto;min-height:0;display:flex;flex-direction:column;position:relative;overflow:hidden;
  /* OPACO: tapa el filo de la última fila en vez de componer con él (ver EL GEMELO OPACO). */
  border:1px solid var(--grid-linea-solida);
}

/* La rejilla ya NO se escribe aquí: la calcula el JS a partir del juego de
   columnas del escenario y la publica en --rejilla sobre .sabana. Cabecera y
   filas la heredan, que es lo que las mantiene alineadas sin ajuste manual.

   El canal de 12px se CONSERVA como gap. La primera versión lo repartió como
   padding de celda (gap 0 + 6px por lado) y la línea quedaba centrada, sí,
   pero cada columna perdía esos 12px de texto útil y los nombres largos
   empezaban a truncarse. Se ve en cuanto se compara: "Papelería La Esperanza
   S.A. de C.V." pasaba a "…S.A. de …". */
.fila-rejilla{
  display:grid;
  grid-template-columns:var(--rejilla, 15px 230px 150px 100px 130px 64px 105px minmax(190px,1fr));
  /* `stretch` y NO `center` (2026-08-13, pedido de Arif: «bordes completos,
     tipo Excel»). Con `center` cada celda medía lo que su texto —21px de los
     36 de la fila—, así que el filo vertical cubría poco más de la mitad y la
     cuadrícula se leía como una reja rota. El centrado vertical del texto lo
     hace ahora el `line-height` de la celda (más abajo): es lo único que
     centra SIN encoger la caja, y encima conserva el `text-overflow:ellipsis`,
     que un `display:flex` en la celda habría roto —el texto es un nodo suelto,
     y el ellipsis no aplica a un hijo anónimo de flex—. */
  column-gap:12px;align-items:stretch;
  padding:0 10px;white-space:nowrap;
  /* EL ANCHO MÍNIMO DE LA TABLA (2026-08-11) — lo que hace posible el scroll
     horizontal. Sin él, el grid se conforma con el ancho del viewport,
     estruja las columnas hasta que no caben y las últimas quedan fuera del
     recorte, inalcanzables: `scrollWidth` seguía siendo igual a `clientWidth`,
     así que no había nada que scrollear. Lo calcula el JS con la misma pasada
     que arma `--rejilla` (`pintarCabecera`), porque es la misma cuenta: la
     suma de los anchos declarados, sus canales y el padding. */
  min-width:var(--tabla-ancho, 0);
}
/* La cabecera vive FUERA del viewport de las filas —para no tocar el origen
   del scroll virtual, que cuenta desde el `top:0` de `.sabana-filas`— así que
   no se entera de su scroll. Lo sigue con su PROPIO contenedor de scroll, al
   que `render()` le copia el `scrollLeft`.

   ── POR QUÉ UN ENVOLTORIO Y NO UN `transform` (2026-08-11) ──
   El primer intento movía la cabecera con `translateX(-scrollLeft)` y devolvía
   sus dos primeras celdas con `translateX(+scrollLeft)`. Funcionaba de lejos y
   estaba mal de cerca: las celdas de las filas se congelan con
   `position:sticky` —que las fija a `left:25px` DEL VIEWPORT— y las de la
   cabecera volvían a su posición natural en el grid, 25px más a la derecha.
   Resultado: el punto donde se recorta lo que pasa por debajo no coincidía
   entre cabecera y filas, y se veía como una cabecera «más ancha» que su
   columna (lo cazó Arif). El desfase estaba incluso SIN scroll.

   La cura no fue ajustar el número: fue dejar de usar DOS mecanismos para lo
   mismo. Con el envoltorio, la cabecera es un contenedor de scroll y sus
   celdas se congelan con el mismo `sticky` de `.fila-rejilla` — coinciden por
   construcción, no por cuadratura. */
/* ── EL TINTE Y EL FILO VIVEN AQUÍ, NO EN LA CABECERA (2026-09-20) ────────
   La reserva del canal de scroll es un `padding-right` que el JS mide y pone
   en ESTE envoltorio, y tiene que ir aquí porque la cabecera de dentro es el
   contenedor de la rejilla: meterle el padding a ella encogería su caja de
   contenido 6px por debajo de la suma de las columnas, que en una sábana de
   anchos fijos las desborda.

   Pero el tinte y el filo inferior vivían en la cabecera de dentro, o sea
   DENTRO del padding — así que morían 6px antes del marco y dejaban una
   muesca blanca justo encima del pulgar. Lo cazó Arif en `asistencia.php`:
   «queda un espacio tipo aire». Aquí la banda corre hasta el filo y la
   reserva sigue empujando sólo a las columnas, que es lo que tenía que pasar.

   EL ALTO NO CAMBIA: la cabecera baja a `--sabana-cab-h` menos 1px y el filo
   lo pone este envoltorio, así que el bloque sigue midiendo lo mismo y el
   `line-height` de las celdas —que ya era `cab-h - 1`— sigue cuadrando. */
.sabana-cabecera-vp{
  flex:none;overflow:hidden;
  background:var(--app-secundario-bg);
  border-bottom:1px solid var(--grid-linea);
}
.sabana-cabecera{
  /* Menos 1px: el filo inferior lo pone `.sabana-cabecera-vp` (ver allá). */
  height:calc(var(--sabana-cab-h) - 1px);
  font-size:var(--fs-meta);font-weight:500;color:var(--app-fg-tertiary);
  /* El filo inferior también: lo pinta el envoltorio. */
  /* Teñida desde el 2026-08-13 (antes era el papel, o sea invisible como
     superficie: sólo la separaba su filo). El tinte dice «esto rotula, no es
     un renglón más», que es justo lo que la cuadrícula completa hizo más
     necesario — con la reja entera, una cabecera sin fondo se lee como la
     primera fila de datos.

     OPACA a la fuerza: sus dos primeras celdas se congelan al scrollear en
     horizontal y tienen que tapar lo que les pasa por debajo (mismo motivo
     que en las filas, ver el truco de las dos capas más abajo). Por eso es un
     hex y no un `rgba` como los tintes que se componen.

     ── EL GRIS ES EL SECUNDARIO, EL DEL RIEL (2026-08-26, petición de Arif) ──
     Tuvo el suyo propio (`--tabla-cab-bg`, #fafafb/#222224) durante tres días,
     y era un segundo gris para el MISMO trabajo: «esto rotula, no es
     contenido», que es exactamente lo que dice el riel. Dos grises casi
     iguales no se leen como dos jerarquías: se leen como un descuido, y
     encima invertían la relación entre temas (más claro que la barra en
     claro, más oscuro en oscuro). El 2026-08-16 se resolvió alineando los
     tres —barra, riel y cabecera— a `--app-sidebar-bg`, y eso corrigió el
     descuido pero dejó otro: una cabecera de tabla pesando lo mismo que la
     NAVEGACIÓN. Hoy la barra se queda sola en su gris y esto va con el riel,
     en `--app-secundario-bg`. Sigue siendo opaco y cumple lo de arriba. */
  /* EL TINTE SE MUDÓ a `.sabana-cabecera-vp` el 2026-09-20 para que la banda
     llegue al marco por encima de la reserva del canal. Las dos primeras
     celdas lo repiten por su cuenta (son las congeladas), así que el freeze
     no dependía de esta línea. */
}
.sabana-cabecera .ordenable{cursor:pointer;user-select:none;overflow:hidden;text-overflow:ellipsis}
.sabana-cabecera .ordenable:hover{color:var(--app-fg-secondary)}
/* 11px y no 9: es un glifo, sí, pero un glifo que dice POR QUÉ COLUMNA está
   ordenada la lista — a 9px había que buscarlo. */
.sabana-cabecera .flecha-orden{color:var(--app-info-text);font-size:11px;margin-left:3px}

/* Scroll en los DOS ejes (2026-08-11). Nació con `overflow-x:hidden` cuando el
   juego de columnas lo fijaba el código y siempre cabía; desde que el usuario
   elige sus columnas, ese `hidden` es un recorte sin salida.

   `flex:1 1 auto` Y NO `flex:1` (2026-08-28). Son distintos en lo que importa:
   `flex:1` es `1 1 0%`, o sea base CERO, y desde que `.sabana` mide su contenido
   (arriba) una base cero hace que la caja mida la cabecera y nada más — el
   viewport colapsa a 0px y la tabla desaparece. Con base `auto` la caja suma las
   filas de verdad, y el `flex-shrink` sigue recortándolo al alto disponible
   cuando la lista es larga. El `min-height:0` es lo que permite ese recorte.

   EL `-1px` COLAPSA EL FILO DE ABAJO con el de la última fila (ver `.sabana`).
   Va como margen negativo del viewport y no como una regla sobre la fila porque
   el scroll virtual hace que `:last-child` sea la última fila PINTADA y no la
   última del listado. Cuando hay barra horizontal el marco no se apoya en
   ninguna fila —los separa el canal de 6px— y este píxel se limita a solaparse
   con el último del canal: comprobado en pantalla, el pulgar no cambia. */
.sabana-viewport{flex:1 1 auto;min-height:0;overflow:auto;position:relative;margin-bottom:-1px}
.sabana-espaciador{width:1px}

/* ── LA FILA DE TOTALES (cartera, 2026-08-30) ─────────────────────────────
   El espejo de `.sabana-cabecera`, al PIE y con la misma `--rejilla`: cada
   total cae exactamente bajo su columna. Estrenada por la cartera, donde cinco
   columnas de dinero sin su suma dejan la pregunta que abre la pantalla
   —«¿cuánto tengo en +90?»— para la aritmética mental del que mira.

   ── POR QUÉ NO ES UN `.lede` ──
   Es la alternativa al párrafo de resumen que la rueda pone ARRIBA de su
   cartera y que aquí no cabe: el `.lede` se retiró de las sábanas el
   2026-08-29 porque es la bajada de una hoja de lectura, no de una tabla. Esto
   sí es de la tabla —vive dentro del marco, hereda su rejilla y sigue su
   scroll—, así que da el mismo dato sin romper el molde. Y lo da mejor: en una
   tira de flexbox hay que emparejar el rótulo «+90» con su cifra leyendo en
   dos sitios; aquí la vista baja en recta desde la columna.

   ── SU PROPIO VIEWPORT, COMO LA CABECERA ──
   Y por el mismo motivo: vive FUERA del contenedor de scroll de las filas para
   no tocar el origen del scroll virtual. Quien lo pinta emparejar su
   `scrollLeft` con el de las filas — sin eso, al desplazarse en horizontal los
   totales se quedan quietos y quedan bajo la columna equivocada, que es peor
   que no tenerlos.

   ── EL TINTE Y EL PESO ──
   Mismo gris que la cabecera (`--app-secundario-bg`, el del riel: «esto no es
   un renglón más»), y opaco por lo mismo — sus primeras celdas se congelan al
   scrollear y tienen que tapar lo que pasa por debajo. El PESO sí difiere: la
   cabecera rotula en `--fs-meta` y tono terciario, y esto son CIFRAS que se
   leen, así que van al tamaño y al color del contenido. Un total en gris de
   rótulo se lee como una etiqueta más y es el dato que cierra la pantalla. */
.sabana-totales-vp{flex:none;overflow:hidden}
.sabana-totales{
  height:var(--sabana-cab-h);
  font-size:var(--fs-meta);font-weight:600;color:var(--app-fg);
  /* El espejo del filo de la cabecera, y en el mismo token por lo mismo. */
  border-top:1px solid var(--grid-linea);
  background:var(--app-secundario-bg);
  font-variant-numeric:tabular-nums;
}
.sabana-totales > *{line-height:calc(var(--sabana-cab-h) - 1px)}

/* AQUÍ VIVIÓ un `display:block` para la primera celda, y duró una hora
   (2026-08-30). Hacía falta porque la cartera metía el conteo DENTRO de la
   celda del rótulo («Total · 5 ventas») y el molde pone `display:flex` en toda
   primera celda —para centrar la casilla—, lo que bloquifica a cualquier hijo y
   tiraba el conteo a otra línea.

   Se cayó cuando el conteo ganó su propia columna: sin hijo que bloquificar no
   hay nada que corregir, y la regla se quedaba sin consumidor. Queda anotado
   porque el defecto es REAL y volverá el día que alguien vuelva a meter dos
   cosas en esa celda — con el detalle de que sólo se ve fuera de `.ca-sabana`,
   que anula ese flex por su cuenta. El arreglo, si vuelve a hacer falta, pide
   `.sabana-totales.fila-rejilla > :nth-child(1)`: con una sola clase empata en
   especificidad con el molde y pierde por orden. Medido. */
/* `right:auto` + `min-width:100%`, y no `right:0`: un absoluto anclado a los
   dos lados mide el ancho VISIBLE del viewport, así que las filas se habrían
   quedado en 1225px mientras la tabla pide 1468 — el contenido desbordado no
   habría empujado el `scrollWidth` y no habría scroll que hacer. */
.sabana-filas{position:absolute;top:0;left:0;right:auto;min-width:100%;will-change:transform}

/* ── El grid ──────────────────────────────────────────────────────────
   Cuadrícula tipo hoja de cálculo, un punto por debajo de Excel: Excel usa
   ~#d4d4d4 y esto es la tinta al 10.8% (el peso de un negro al .085 — ver
   LA TINTA), suficiente para leerse como retícula
   sin convertirse en el elemento dominante de la pantalla.

   Es un token propio y NO --app-hairline: la retícula se dibuja cientos de
   veces por pantalla y la línea de un separador suelto no aguanta esa
   repetición — a .06 desaparece, a .12 la tabla se vuelve una reja.

   CUIDADO CON EL ALTO: el borde inferior sólo cabe dentro de los 36px porque
   Bootstrap fija `box-sizing:border-box` en todo. Si eso cambiara, la fila
   pasaría a 37px y la aritmética índice×altura del scroll virtual empezaría a
   desviarse un píxel por fila — a la fila 500 son 500px de deriva. Está
   verificado midiendo, no supuesto. */
/* ── LA RETÍCULA SUBE, Y SÓLO EN CLARO (2026-09-20, pedido de Arif) ───────
   «Más resalte, partiendo de la cabecera de la tabla y tendiendo al fondo del
   menú lateral.» Eso ya describe una escalera, y resulta que es la que esta
   hoja tiene construida desde el 2026-08-13 — sólo le faltaba el último
   peldaño. Medida en pantalla, en claro:

     cabecera de la tabla   #F7F7F2   ≈ 4% de tinta   (medio tinte del menú)
     barra lateral          #F0EEE6   ≈ 8% de tinta   (el tinte entero)
     línea de la retícula   #D9D7D2   = 20% de tinta  ← el peldaño nuevo

   O sea: la cabecera es el menú a la mitad, y la línea es el menú al doble y
   pico. Sale del mismo pigmento, así que «más oscuro» y «tendiendo al color
   del menú» son la misma operación y no dos — subir el tinte oscurece Y
   acerca al beige a la vez. Venía en 10.8%, que apenas pasaba de la barra.

   EL COMENTARIO DE ARRIBA DECÍA «a .12 la tabla se vuelve una reja» y este
   valor lo cruza a propósito: aquella calibración se hizo cuando la retícula
   era lo único que dibujaba la tabla. Hoy la cabecera va teñida y opaca, y es
   ella la que dice dónde empieza la rejilla, así que la línea puede pesar más
   sin que la pantalla se lea como papel cuadriculado. Si alguna vez se
   quitara el tinte de la cabecera, este número vuelve a discutirse.

   EN OSCURO NO SE TOCA, y es instrucción literal de Arif («sólo en la versión
   clara»). Allá la línea es un aclarado sobre fondo nocturno y ya se lee; lo
   que no funcionaba era el claro. */
:root{ --grid-linea:color-mix(in srgb,var(--app-tinta) 20%,transparent) }
[data-theme="dark"]{ --grid-linea:rgba(255,255,255,.11) }

/* ── EL GEMELO OPACO, PARA EL MARCO (2026-09-20) ──────────────────────────
   El comentario de arriba explica que el `margin-bottom:-1px` del viewport
   existe para que el filo del MARCO se pinte ENCIMA del de la última fila, en
   vez de pegado debajo. Ese colapso **sólo funciona si el marco tapa**, y
   `--grid-linea` es translúcido: en vez de tapar, COMPONE. Dos veladuras del
   20% dan 36%, así que el canto de abajo salía más oscuro que el resto de la
   reja — una línea de 0.36 contra 0.20 en todas las demás.

   NO ES UN DEFECTO NUEVO: con la reja al 10.8% el compuesto daba 0.20 y pasaba
   por una línea normal. Al subir a 20% el mismo accidente se volvió visible.
   Lo destapó Arif al preguntar si el cambio había llegado a las demás tablas:
   `asistencia.php` no lo enseña porque su lista SCROLLEA y la última fila no
   llega al canto; `trabajadores.php`, con una fila y sin scroll, sí.

   Y NO SE ARREGLA QUITÁNDOLE EL FILO A LA ÚLTIMA FILA, por lo que ya decía
   arriba: con scroll virtual `:last-child` es la última fila PINTADA, no la
   última del listado, así que se abriría un hueco a media lista.

   Mismo patrón que `--ctl-bg-solido`, y por la misma razón: un valor que en
   claro coincide y en oscuro resuelve el translúcido contra el fondo. Lo usa
   sólo el marco de `.sabana`; la reja interior sigue translúcida a propósito,
   para componer bien sobre la fila seleccionada y sobre los tintes de estado. */
:root{ --grid-linea-solida:color-mix(in srgb,var(--app-tinta) 20%,var(--app-bg)) }
[data-theme="dark"]{ --grid-linea-solida:color-mix(in srgb,#fff 11%,var(--app-bg)) }

/* El fondo de la cabecera de la tabla: UN PASO MÁS SUAVE QUE EL MENÚ
   (2026-08-13, pedido de Arif). La barra lateral es la superficie teñida de
   referencia (`--app-sidebar-bg`, hoy el beige #F0EEE6 de la barra de iconos
   de Controly en claro); ésta lleva su mismo tinte a la mitad, así
   que se lee como cabecera sin competir con el menú por ser el gris que más
   pesa en pantalla. En oscuro la relación se conserva —el menú es #252528 y
   esto queda a medio camino entre él y el papel del panel (#1e1e20)—: no es
   «más claro» literal, es MENOS TINTE, que es lo que se ve igual en los dos
   temas. Opaco a propósito (ver `.sabana-cabecera`). */

/* El margen negativo es el truco entero: estira la caja 6px DENTRO del canal
   de 12, y el padding se los devuelve por dentro. Resultado: el texto sigue
   ocupando exactamente el ancho de su columna (cero pérdida) y el borde cae
   justo a la mitad del canal, con 6px de aire a cada lado. Un `border-right`
   a secas dibujaría la línea pegada al texto de la izquierda. */
.fila-rejilla > *:not(:last-child){
  margin-right:-6px;padding-right:6px;
  border-right:1px solid var(--grid-linea);
}
/* El centrado vertical, derivado y no a ojo: el alto de la caja menos la línea
   del grid, que `box-sizing:border-box` descuenta del contenido. Si la fila o
   la cabecera cambian de alto, el texto los sigue solo. */
.fila > *{line-height:calc(var(--app-row-h) - 1px)}
.sabana-cabecera > *{line-height:calc(var(--sabana-cab-h) - 1px)}

.fila{
  height:var(--app-row-h);
  /* `pointer`: la fila SE PULSA — un clic abre su peek. Estuvo en `default`
     desde el principio, que decía «esto es texto», y por eso había que
     descubrir el gesto probando. La celda editable no lo contradice: se edita
     con doble clic o con Enter, y eso se anuncia al entrar en modo rejilla. */
  font-size:var(--fs-base);cursor:pointer;
  border-bottom:1px solid var(--grid-linea);
}
.fila > *{overflow:hidden;text-overflow:ellipsis}

/* ── Las dos primeras columnas no viajan (2026-08-11) ─────────────────
   El check y el NOMBRE se quedan al scrollear en horizontal. Sin ellos, a la
   tercera columna estás leyendo «Culiacán · 667 123 4502» sin saber de quién
   es — y el check, que es lo que se pulsa para actuar en lote, ni siquiera
   está a mano.

   ── EL FONDO TIENE QUE SER OPACO, Y AQUÍ ESTÁ EL TRUCO ──
   Una celda pegajosa con fondo translúcido deja ver pasar el texto de las
   columnas que corren por debajo. Y los tintes de fila de esta app SON
   translúcidos a propósito (`--app-row-hover` al 3.5%, `--app-row-selected`
   un color-mix): así se componen sobre cualquier fondo sin declarar una
   variante por estado. La salida es pintar DOS capas en la celda —color de
   fondo opaco abajo, el tinte como `background-image` encima— en vez de
   opacar los tokens, que habría obligado a duplicarlos por tema y habría
   cambiado el aspecto de TODAS las filas para arreglar dos celdas.

   ── EL MARGEN NEGATIVO NO ES UN PARCHE ──
   Cada celda absorbe el aire que tiene a su izquierda: la primera, el padding
   de la fila (10px); la segunda, el canal que la separa del check (12px). Sin
   eso quedaría una franja transparente entre el borde y la celda pegada, y por
   ahí se vería correr el contenido. Con `content-box` el ancho declarado sigue
   siendo el del contenido, así que la rejilla no se mueve ni un píxel: lo
   verifica `--tabla-ancho`, que no cambia. */
.fila-rejilla > :nth-child(1),
.fila-rejilla > :nth-child(2){
  position:sticky;z-index:1;
  background-color:var(--app-bg);
  background-image:var(--tinte-fila,none);
  box-sizing:content-box;
}
.fila-rejilla > :nth-child(1){left:0;margin-left:-10px;padding-left:10px}
/* ── POR QUÉ EL FILO ENTRE LAS DOS CONGELADAS NO SE PINTA, Y DÓNDE SÍ ──
   El `border-right` de la celda 1 existe siempre y SIEMPRE está tapado: la
   celda 2 arranca 12px antes de su columna (`margin-left:-12px`, para que no
   quede una franja transparente por donde se vea correr el contenido) y pinta
   un fondo OPACO, mientras el filo de la 1 cae a sólo 6px dentro de ese mismo
   canal. Con las dos en `z-index:1` gana la última del marcado. Medido el
   2026-08-18: la celda 1 termina en x=380 y la 2 empieza en x=374.

   **Se destapa con `z-index:2` en la primera, y eso va POR PÁGINA, no aquí.**
   La regla se probó primero en el molde —las cuatro sábanas a la vez— y Arif
   la acotó el mismo día: **en leads no la quiere**. Y tiene sentido, porque la
   primera columna no es lo mismo en todas: en leads es el CHECK, un canal de
   15px que se lee como parte del nombre y al que un filo propio lo convierte
   en una columna que no es; donde la primera columna lleva DATO —el folio de
   una venta— el filo es el que la cuadrícula promete.

   Así que el criterio queda escrito y no el resultado: **la sábana cuya
   primera columna es un dato se destapa el filo; la que abre con el check, no.**
   Quién lo pide hoy vive en UNA lista, la del bloque «EL FILO ENTRE FOLIO Y
   FECHA» más abajo (decía «hoy sólo ventas», y ventas salió de ella el
   2026-08-28 al ganar su casilla). */
/* La columna del check es la ÚNICA que se centra con flex, y puede permitírselo
   porque no lleva texto: el `line-height` de la celda centra letras, no cajas,
   y `.caja` es un bloque de 15px —con la celda estirada a toda la fila
   (2026-08-13) se quedaba pegado arriba—. En las demás columnas el flex está
   prohibido: rompería el `text-overflow:ellipsis` del nombre. */
.fila-rejilla > :nth-child(1){display:flex;align-items:center}
.fila-rejilla > :nth-child(2){left:25px;margin-left:-12px;padding-left:12px}
/* EL CANTO QUE DICE «AQUÍ PASA ALGO POR DEBAJO». Sin él, la columna siguiente
   aparece cortada a media palabra pegada al nombre y se lee como un defecto de
   render, no como una columna que viaja. Sólo con scroll: en reposo no hay
   nada debajo que anunciar, y una sombra permanente sería un separador que
   ninguna otra columna tiene. Va como `box-shadow` con extensión negativa —una
   pluma hacia la derecha, sin línea— porque lo que hay que sugerir es
   profundidad, no un borde más. */
.sabana.desplazada .fila-rejilla > :nth-child(2){box-shadow:7px 0 7px -7px rgba(0,0,0,.45)}
/* El tinte de la fila viaja como variable para poder componerlo arriba. La
   fila se sigue pintando como siempre — esto sólo lo hace REUTILIZABLE. */
.fila:hover{--tinte-fila:linear-gradient(var(--app-row-hover),var(--app-row-hover))}
.fila.seleccionada{--tinte-fila:linear-gradient(var(--app-row-selected),var(--app-row-selected))}
/* La cabecera hereda el `sticky` de `.fila-rejilla` —su envoltorio es un
   contenedor de scroll, así que ahí sí ancla— y sólo necesita su fondo para
   tapar lo que le pasa por debajo. La FILA DE TOTALES va en la misma regla y
   por lo mismo: comparte envoltorio propio, tinte y celdas congeladas, así que
   sin esto sus dos primeras celdas se pintarían del color del papel y dejarían
   ver correr las cifras por debajo al desplazarse. */
.sabana-cabecera > :nth-child(1),
.sabana-cabecera > :nth-child(2),
.sabana-totales > :nth-child(1),
.sabana-totales > :nth-child(2){background-color:var(--app-secundario-bg)}
/* Sin radio: una celda de cuadrícula con esquinas redondeadas deja ver la
   línea por debajo del hover y se lee como un defecto de render. */
.fila:hover{background:var(--app-row-hover)}
.fila.seleccionada{background:var(--app-row-selected)}
.fila.cursor{box-shadow:inset 0 0 0 1.5px var(--app-accent-tint)}
/* fila recién editada que ya no cumple el filtro: se queda visible con marca
   en vez de esfumarse bajo el cursor */
.fila.fuera-de-filtro{opacity:.55;box-shadow:inset 2px 0 0 var(--app-atencion)}

.col-nombre{font-weight:500}
.col-secundaria{color:var(--app-fg-secondary)}
.col-num{font-variant-numeric:tabular-nums}

.caja{
  width:15px;height:15px;border-radius:var(--r-mini);
  border:1.5px solid var(--app-check);box-sizing:border-box;
  display:block;cursor:pointer;
  /* Su columna mide lo que mide él (15px), así que el ÁREA DE CLIC se amplía
     por fuera del flujo: sin esto, encoger la columna habría convertido un
     objetivo cómodo en uno de 15px. No ocupa espacio ni desplaza nada. */
  position:relative;
}
.caja::before{content:'';position:absolute;inset:-8px -7px}
/* El marcado se dice con una CLASE y no sólo por el estado de la fila: el de la
   cabecera —que marca todo— no vive dentro de ninguna. `.fila.seleccionada`
   sigue funcionando y las dos comparten dibujo. */
.fila.seleccionada .caja,
.caja.marcada{
  border:0;background:var(--app-accent-solido);color:var(--app-accent-fg);
  display:flex;align-items:center;justify-content:center;
  font-size:10px;line-height:1;
}
.fila.seleccionada .caja::after,
.caja.marcada::after{content:'✓'}
/* PARCIAL: hay selección, pero no toda. El guion es el estándar de la casilla
   indeterminada y dice lo único que hace falta —«algunos»—; con el ✓ mentiría
   y vacía escondería que hay selección viva. */
.caja.parcial{
  border:0;background:var(--app-accent-solido);color:var(--app-accent-fg);
  display:flex;align-items:center;justify-content:center;
  font-size:11px;line-height:1;font-weight:700;
}
.caja.parcial::after{content:'–'}

/* EL GLIFO: punto + texto corto. Lo comparten las DOS columnas de veredicto
   —Califica (manual) e Interés (calculado)— y por eso su nombre es genérico:
   se llamaba `.encaje` y ya entonces lo usaban las dos, así que el nombre
   mentía sobre una de ellas.

   LAS DOS COLUMNAS SE FUERON el 2026-09-02 —`califica` con el desmontaje del
   outreach, el interés E1–E5 con la cadencia— y con ellas la variante
   `.glifo.destacado`, que marcaba el único valor accionable de cada una para
   que el resto se leyera en gris. **`.glifo` a secas y sus variantes de color
   siguen muy vivas** (cartera, clientes, gastos, ventas, la portada): lo que se
   retiró en la barrida final fue sólo `.destacado`. */
.glifo{display:flex;align-items:center;gap:5px}
.glifo .punto{width:6px;height:6px;border-radius:50%;background:var(--app-info-text);flex:none}
.glifo .letra{color:var(--app-fg-tertiary)}
/* La variante de AVISO (2026-08-18, decisión de Arif sobre la columna
   «Pendiente» de ventas: «quita el badge, sólo el punto — más minimalista»).

   Es la pieza CORRECTA para una celda de sábana, y no el badge: un badge es un
   chip con fondo y padding propio, pensado para uno suelto o para cuatro
   estados conviviendo; en una columna donde la mayoría de las filas lleva
   marca, el chip pinta media tabla de ámbar y deja de señalar nada. El punto
   se lee en vertical, que es como se escanea una columna.

   ── EL COLOR ES EL DEL AVISO, NO EL DE SU TEXTO ──
   Decisión de Arif el mismo día: *«el punto de warning, cuando no tiene el
   fondo warning, debe ser del mismo color que el warning»*. Nació con
   `--app-atencion-text` por copiar al badge, y ahí ese token está BIEN por un
   motivo que aquí no aplica: dentro del chip el punto va sobre la tinta ámbar,
   y el color puro sobre su propia tinta no llega al 3:1. Sobre papel el punto
   no compite con ninguna tinta — compite con la idea de «esto es un aviso», y
   `#8f6700` se lee café, no ámbar.

   El número medido, para que quien lo revise no tenga que recalcularlo:
   sobre papel blanco, `--app-atencion-text` da 5.11:1 y `--app-atencion`
   1.51:1. Se elige el segundo a sabiendas, y la razón por la que se puede: el
   punto es REDUNDANTE — el dato que señala (el importe pendiente) está escrito
   a su lado, en el color del cuerpo. No es el que porta la información, es el
   que la hace escaneable en vertical. En tema oscuro la decisión no cambia
   nada: los dos tokens valen `#ffd60a` y dan 11.79:1.

   ── LA FAMILIA COMPLETA, CON LA MISMA REGLA ──
   Los cuatro nombres son los del badge, para que un color signifique lo mismo
   lleve chip o no. Y los cuatro llevan el token PURO, que es la regla que Arif
   fijó para el punto sin fondo. Medido sobre papel claro / oscuro:

     info     #0ea5a5   3.02:1 / 5.50:1
     warning  #ffcc00   1.51:1 / 11.79:1   (en oscuro el token es #ffd60a)
     danger   #ff3b30   3.55:1 / 4.69:1
     success  #34c759   2.22:1 / 7.50:1

   Los dos que no llegan a 3:1 en claro —warning y success— se aceptan por lo
   mismo que arriba: el punto acompaña a un rótulo escrito al lado. Si algún
   día un punto queda SOLO en su celda, sin texto que lo explique, esa celda
   necesita el token `-text`, no éste.

   El `.glifo` A SECAS no entra aquí y sigue con `--app-info-text`: es el que
   pintan las dos columnas de veredicto de la sábana de leads («• Califica»,
   el interés E1–E5) desde antes de esta regla, y cambiarlo les movería el
   tono sin que nadie lo haya pedido. Quien quiera el punto de tipo lo pide por
   su nombre. */
.glifo.warning .punto{background:var(--app-atencion)}
.glifo.danger  .punto{background:var(--app-peligro)}
.glifo.success .punto{background:var(--app-exito)}

/* ── Botón flotante — VOLVER ARRIBA ─────────────────────────────────── */
/* Dio de alta leads hasta el 2026-08-10; ese trabajo se mudó a «Nuevo lead»,
   dentro de la cápsula del titular. Aquí quedó lo que sólo un flotante puede
   hacer: devolver el scroll al principio sin obligar a arrastrar 180,000px de
   sábana. Sigue siendo secundario y no azul — subir no escribe nada.

   MIDE `--app-barra-h` (44px) y no los 26.89 de un botón. Un flotante que
   aparece y desaparece solo tiene que encontrarse sin buscarlo, y a 27px
   —el alto de un botón cualquiera— era la pieza más discreta de la pantalla
   haciendo el trabajo más urgente. 44 es 1.64× lo que medía; el doble justo
   (54) ya compite con el peek y no hay token que lo respalde. Y es token, no
   número: el alto común de todas las barras de la app, así que si esa medida
   cambia, el flotante la sigue solo.

   ES DEL SCROLL DE LA PÁGINA, NO DE UNA TABLA: salió de la sábana de
   Contactos el 2026-09-24 (pedido de Arif) y hoy ninguna página lo usa.
   NO SE PINTA HASTA QUE HAY DE DÓNDE VOLVER: quien lo use lo enciende pasada
   una pantalla de scroll, así que en una página corta no existe.
   El estado apagado es opacidad + un empujón hacia abajo, no `hidden`:
   `display:none` no transiciona, y un botón que aparece de golpe se lee como
   un error de pintado. `pointer-events:none` es lo que impide que el fantasma
   invisible siga siendo pulsable.

   OJO: el `::before` se ancla al propio botón porque .fab está posicionado;
   si una muestra lo pone en el flujo, debe usar `position:relative` (con
   right/bottom en auto), nunca `static`. */
.fab{
  position:absolute;right:22px;bottom:58px;
  width:var(--app-barra-h);height:var(--app-barra-h);
  border-radius:50%;overflow:hidden;
  background:var(--btn-cara-hover);color:var(--app-fg);border:0;
  display:flex;align-items:center;justify-content:center;
  box-shadow:0 2px 8px var(--app-shadow), inset 0 0 0 1px var(--app-borde-muted);
  cursor:pointer;z-index:4;
  opacity:0;pointer-events:none;transform:translateY(8px) scale(.92);
  transition:transform var(--app-dur) var(--app-ease),
             opacity var(--app-dur) var(--app-ease);
}
/* La flecha MARCA el botón —es su etiqueta entera, no un adorno junto a un
   texto—, así que va a `--icn-fila` y en el color del texto, como la palabra
   de cualquier otro botón. */
.fab .icn{width:var(--icn-fila);height:var(--icn-fila)}
.fab.visible{opacity:1;pointer-events:auto;transform:none}
.fab::before{
  content:'';position:absolute;inset:0;z-index:-1;border-radius:inherit;
  background:var(--btn-cara);
  box-shadow:inset 0 0 0 1px var(--app-borde-muted);
  transition:opacity var(--app-dur) var(--app-ease);
}
.fab.visible:hover{transform:scale(1.06)}
.fab:hover::before{opacity:0}

/* ── Barra de selección múltiple ──────────────────────────────────────
   Comparte `--app-barra-h` con la cápsula del titular. Medía 45 y la otra 43,
   sin que ninguna supiera de la otra: dos piezas que hacen el mismo trabajo
   —flotar con acciones dentro— separadas por 2px, que es la peor distancia
   posible (no se lee como intención, se lee como descuido). Padding vertical
   a 0 y manda el `min-height`, igual que allá. */
/* EL FONDO DE MENÚ, no el de papel (2026-08-11, corrección de Arif). Con
   `--app-paper` era blanco sobre blanco —el papel del panel es el mismo
   #ffffff— y sólo la separaban un hairline al 6% y una sombra al 12%: flotaba
   sobre la sábana sin leerse como una superficie propia. `--app-sidebar-bg` es
   el gris con el que esta app dice «esto es chrome, no contenido», que es
   exactamente lo que la barra es. En oscuro la diferencia ya existía
   (#252528 contra #1e1e20) y ahora es la misma en los dos temas.

   ── Y SE QUEDA EN ESE GRIS, NO PASA AL SECUNDARIO (2026-08-26) ──────────
   Cuando la cabecera de la sábana, el tablero y los tiles bajaron al gris
   secundario, ésta no fue con ellos, y no es un olvido: es la única de las
   cinco que FLOTA. Las otras se apoyan en el papel y su tinte sólo las
   rotula; ésta se despega de él con sombra y tiene que leerse como algo que
   está ENCIMA. El secundario le dejaría 1.061:1 contra el papel donde ahora
   tiene 1.162 — el mismo problema que la volvió gris el 2026-08-11, por el
   otro extremo. Regla que deja: lo que se apoya va en el secundario; lo que
   flota necesita el peso de la barra. */
/* ── VIVE EN EL FLUJO, AL PIE DEL PANEL Y A LA IZQUIERDA (2026-08-17) ───
   Flotaba absoluta —centrada, a 56px del suelo— y eso traía dos defectos a
   la vez. Uno: TAPABA filas justo mientras operas sobre ellas, que es el
   único momento en que la barra existe. Dos: su centro era el de
   `.app-shell` —la barra lateral incluida—, así que ni siquiera quedaba
   centrada sobre el panel; con la lateral desplegada se iba ~85px a la
   izquierda del centro real.

   Hoy va EN EL FLUJO de `.app-panel`, como su último hijo. La sábana es
   `flex:1` dentro de esa columna, así que se encoge exactamente lo que la
   barra ocupa: el no-solape es por construcción, no por un número que
   cuadre — y el `ResizeObserver` del viewport (app.js) recalcula el scroll
   virtual sin una línea nueva. Se pega a la IZQUIERDA con `align-self`,
   que es donde arrancan la primera columna y las casillas que la
   encendieron: la barra sale debajo de la mano que la abrió.

   AL PIE Y NO SOBRE LA CABECERA DE LA TABLA — probadas las dos posiciones el
   mismo día. Arriba agrupa mejor (queda en la banda de los otros controles),
   pero una tira que entra y sale del flujo AHÍ empuja la tabla 54px —los 44
   de la barra más los 10 de aire, fila y media— justo cuando la mano va
   camino del segundo lead: el salto no molesta, hace marcar el que no era.
   Al pie la tabla cede por el borde que nadie está mirando. Regla que deja
   este caso: en una lista, lo que aparece y desaparece se cuelga del borde
   que el ojo NO está usando; agrupar controles vale menos que no mover el
   blanco. Su `margin-top` —y no `margin-bottom`— es lo único que dice ese
   sitio aquí; el resto lo dice el orden del marcado.

   Conserva su piel de chrome flotante (fondo de menú, filete y sombra)
   porque sigue siendo una superficie de acciones sobre la sábana, no una
   fila más — sólo dejó de robarle sitio. */
/* ENVUELVE EN VEZ DE CORTARSE. Es la contrapartida de pegarla a la izquierda:
   centrada le robaba ancho a la barra lateral y por eso sus 600px cabían aún
   con el panel a 588 —a costa de invadir el chrome de al lado—; anclada a la
   izquierda, el panel la recorta (`overflow:hidden`) y lo primero que se perdía
   era la ×, que es justo la salida. Con `max-width:100%` y `wrap` la fila se
   parte en dos y no se pierde nada; el alto extra se lo cede la sábana sola,
   que es lo que se gana al vivir en el flujo. El padding vertical sigue en 0
   —regla de las barras, ver `--app-barra-h`—: el aire de la fila envuelta lo
   pone el `row-gap`. */
.barra-seleccion{
  flex:none;align-self:flex-start;margin-top:10px;max-width:100%;
  display:none;align-items:center;flex-wrap:wrap;gap:2px;row-gap:4px;
  background:var(--app-sidebar-bg);border:1px solid var(--app-borde);
  border-radius:var(--r-caja);
  min-height:var(--app-barra-h);padding:0 8px 0 14px;
  box-shadow:0 6px 24px var(--app-shadow);
  font-size:var(--fs-base);z-index:5;
}
/* Entra desde ABAJO, que es el lado por el que llega. Se llamaba `sube` y
   perdió el nombre al probarla arriba; conserva el genérico porque la
   dirección es consecuencia del sitio, y el sitio ya se discutió dos veces. */
.barra-seleccion.visible{display:flex;animation:entra-barra var(--app-dur) var(--app-ease)}
@keyframes entra-barra{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
.barra-seleccion .bs-conteo{font-weight:600;margin-right:10px}
.barra-seleccion .bs-accion{padding:5px 10px;border-radius:var(--r-ctl);cursor:pointer}
.barra-seleccion .bs-accion:hover{background:var(--app-chip)}
/* LA ACCIÓN DESTRUCTIVA, EN ROJO (2026-08-28, decisión de Arif para la barra de
   gastos). Matiza la regla que este archivo repetía en tres sitios —«no hay
   botón de peligro»— y conviene dejar dicho qué cambió y qué NO:

   · Lo que NO cambia: sigue sin haber BOTÓN de peligro. Los cuatro botones
     (acento, segunda, selector y suave) no se tiñen, y ninguna × recupera
     el ámbar. Un botón rojo compite con el acento por la
     jerarquía del pie; esto es una PALABRA en una tira, no un botón.
   · Lo que sí: una acción de la barra puede teñirse. Ahí el color no advierte
     por advertir —para eso está la hoja de confirmación, que sigue siendo
     obligatoria— sino que dice de qué clase es esa acción entre las que la
     acompañan. Una por tira: media tira en rojo dejaría de señalar nada.

   EL CENSO VIVO, medido con grep el 2026-09-19: ocho barras, las ocho con una
   teñida — «Eliminar» en ventas y gastos; «Desactivar» en contactos, clientes,
   productos, trabajadores, presupuestos y ventas por plan. Corregido ese día:
   este comentario decía que sólo se teñía la acción «ÚNICA de la tira y que
   destruye», y que en leads «Papelera» no se teñía; eso era la primera
   versión, del 2026-08-28. El criterio pasó después por «la que destruye» y
   por «la que retira de la pantalla», y en dos ocasiones el catálogo lo
   enunció sin que se barrieran sus consumidores (una barra sin teñir durante
   diez días, y una excepción que se quedó sin el hecho que la producía): por
   eso lo que queda escrito aquí es el censo, que se re-mide, y no la regla.

   Usa `--app-peligro-text` (el rojo LEGIBLE sobre papel, 13.9:1 / 12.1:1) y no
   `--app-peligro`, que es el del punto y no pasa contraste como texto. El hover
   tiñe el fondo con el mismo rojo al 15% —el `--badge-danger` que ya existe—
   en vez del `--app-chip` neutro: con la tinta roja encima, el gris se lee como
   un estado deshabilitado. */
.barra-seleccion .bs-accion.danger{color:var(--app-peligro-text)}
.barra-seleccion .bs-accion.danger:hover{background:var(--badge-danger)}
/* La × NO se declara aquí: es la MISMA pieza que la de los diálogos y
   comparte su regla (`.dlg-x, .bs-cerrar`, abajo). Lo único suyo es este
   aire: con `gap:2px` una × ya rellena se pegaba a «Papelera», y una salida
   no debe leerse como la sexta acción de la fila. */
.barra-seleccion .bs-cerrar{margin-left:6px}

/* ── Peek de detalle ────────────────────────────────────────────────── */
/* NADA VA ANCLADO FUERA DEL SCROLL (movido del catálogo `ui.php`,
   2026-09-19). Lo estuvo el compositor de toques —camino crítico de dos clics
   que nunca había que ir a buscar— y se retiró con la bitácora el 2026-09-02.
   La regla que dejó: si el gesto es de dos clics, va anclado; si es de uno, va
   donde se lee lo que produce. */
/* EL COSTADO ES 18 Y ES TOKEN (2026-08-28). Eran 24 —el último 24 que
   quedaba en la app— y el peek era la única pieza fuera de la escala de
   relleno interior: `.seccion`, `.tarjeta` y `.app-lado` llevan 18, y el
   diálogo ya había bajado de 24 a 20 el 2026-08-11 por este mismo
   razonamiento. Un panel que acompaña al contenido es del mismo rango que una
   sección del panel, así que respira lo mismo.
   Y va en una custom property porque `.peek-cuerpo` lo consume para anular su
   propio costado: ese acoplamiento estaba escrito como ADVERTENCIA («si aquél
   cambia, estos dos pares cambian con él»), que es la forma de acoplamiento
   que se rompe sola. Con el token, cambiarlo aquí lo cambia allá. */
.peek{
  --peek-pad-x:18px;
  position:absolute;top:0;right:0;bottom:0;width:420px;
  background:var(--app-paper);
  border-left:1px solid var(--app-hairline);
  box-shadow:-16px 0 40px var(--app-shadow);
  padding:20px var(--peek-pad-x);
  display:flex;flex-direction:column;overflow:hidden;z-index:6;
  transform:translateX(100%);
  transition:transform var(--app-dur) var(--app-ease);
}
.peek.abierto{transform:translateX(0)}

/* ── LA ESCALA DEL PANEL (2026-08-18) ────────────────────────────────────
   Tres medidas, y el marcado DICE cuál usa — la misma regla que ya obedecen
   los 33 diálogos de la app, ninguno de los cuales se cuelga del default
   implícito. Antes de esto había un `.peek` de 420 y un `.ancho` suelto:
   un nombre que no dice ancho CONTRA QUÉ, y que por ser el único no formaba
   escala sino excepción.
      sm 420  el default: leer un registro — la ficha, el detalle, la cita
      md 480  el intermedio
      lg 560  capturar un documento con renglones
   El `.peek` desnudo sigue midiendo 420 para que ningún marcado viejo se
   caiga, pero lo nuevo declara: `class="peek peek-sm"`.

   POR QUÉ EL SELECTOR VA COMPUESTO (`.peek.peek-lg`) y no `.peek-lg` a secas:
   la media query de 1024 redeclara `.peek{width:min(420px,100vw)}` con
   especificidad (0,1,0). Un `.peek-lg` suelto empata y PIERDE por orden —el
   panel encogería a 420 bajo 1024 sin que nadie escriba una línea—, así que
   las tres suben a (0,2,0) y ganan sin tener que repetirse dentro de la media
   query. Es la misma jugada que ya hacía `.ancho`, ahora dicha.

   Y el `min(…,100vw)` va en las tres: en una ventana angosta el panel se
   queda DENTRO de la ventana en vez de salirse por la derecha.

   md NACE SIN CONSUMIDOR, y se declara igual porque la escala es de Arif y su
   hueco es lo que evita el `.ancho` de la próxima vez. La prueba para usarlo
   es la misma con la que se ganó lg: MEDIR el contenido que no cabe (a lg le
   salió el número de su línea de captura, ~512px útiles), no elegirlo porque
   420 «se ve apretado». Si nadie lo mide, nadie lo usa — y sobra una línea,
   que es más barato que un panel a ojo. */
.peek.peek-sm{width:min(420px,100vw)}
.peek.peek-md{width:min(480px,100vw)}
.peek.peek-lg{width:min(560px,100vw)}

/* ── EL SEGUNDO PANEL: la conversación (2026-08-13) ──────────────────────
   Mismo cuerpo que el peek —hereda su fondo, su filete, su sombra y su
   transición— y una sola diferencia de sitio: vive 420px más a la izquierda,
   pegado al borde izquierdo del otro. Los dos se ven a la vez.

   `translateX(200%)` cerrado y no 100%: a 100% quedaría exactamente DEBAJO del
   peek (su propio ancho de desplazamiento lo lleva a `right:0`), o sea guardado
   pero encima de la sábana. A 200% sale de la pantalla de verdad, y de paso
   entra deslizándose desde detrás del peek, que es de donde el usuario acaba
   de invocarlo.

   z-index 5 = por DEBAJO del peek (6). Así la sombra del peek cae sobre este
   panel y el apilamiento se lee: el del lead manda, el del chat lo acompaña. */
/* ATADO AL 420, y por eso NO entra en la escala: el `right` de este panel es
   el ancho del otro. Sólo convive con la ficha del lead, que es `peek-sm`; si
   un día se le pusiera `peek-md` o `peek-lg` a su compañero, éste quedaría
   solapado o con una rendija — y el fallo sería visual y mudo. Cuando llegue
   ese día, el arreglo es un token en el ancestro común, no dos números aquí. */
.peek2{
  /* No cuelga de `.peek` en el DOM —son hermanos—, así que declara su propio
     costado. Los dos paneles se ven a la vez: si uno respirara distinto que el
     otro se leería como que uno está mal pegado. */
  --peek-pad-x:18px;
  position:absolute;top:0;right:420px;bottom:0;width:420px;
  background:var(--app-paper);
  border-left:1px solid var(--app-hairline);
  box-shadow:-16px 0 40px var(--app-shadow);
  padding:20px var(--peek-pad-x);
  display:flex;flex-direction:column;overflow:hidden;z-index:5;
  transform:translateX(200%);
  transition:transform var(--app-dur) var(--app-ease);
}
.peek2.abierto{transform:translateX(0)}
/* El hilo ES el panel: se come todo el alto que sobre, en vez de los 280px
   tope que necesitaba cuando compartía el scroll del peek. `min-height:0` es lo
   que le permite encogerse dentro de un flex column — sin él, un hilo largo
   empuja el compositor fuera de la pantalla. */
.peek2 .chat-hilo-crm{max-height:none;flex:1;min-height:0}
/* «Conversación · WhatsApp · NOMBRE DEL FUNNEL» cabe justo en 420px con el
   funnel de esta cuenta; uno más largo desbordaría. Se corta con puntos
   suspensivos en vez de partirse en dos líneas, que empujarían el hilo. */
.peek2-titulo{flex:none;margin-top:6px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* El nombre del lead: SÓLO en móvil (ver la media query). En escritorio sería
   repetir la cabecera del panel de al lado. */
.peek2-persona{display:none}
/* El pie del panel de sólo lectura: la puerta a Conversaciones, donde vivirá
   responder. Ocupa el sitio del compositor retirado —filete arriba y el mismo
   aire— para que el hilo no acabe pegado al borde.
   Sin `href` el enlace no es pulsable, y su color lo dice: gris, no el acento
   que promete acción. El día que la página exista, el `href` lo enciende y
   basta con darle `cursor:pointer` y el acento en `:hover`. */
.chat-ir{
  flex:none;padding-top:12px;margin-top:2px;
  border-top:1px solid var(--app-hairline);
  font-size:var(--fs-meta);font-weight:600;color:var(--app-fg-tertiary);
}
/* Dormido: gris y sin cursor de mano. La regla global de `a` pinta acento y
   ganaba por especificidad de elemento sobre el color heredado del contenedor,
   así que el enlace se veía azul —o sea, pulsable— sin serlo. Se nombra
   explícito para que el estado apagado LO PAREZCA. */
.chat-ir a{color:var(--app-fg-tertiary)}
.chat-ir a[href]{color:var(--app-accent);cursor:pointer}
.chat-ir a[href]:hover{text-decoration:underline}

/* LA PUERTA en el panel del lead. Es una fila pulsable, no un botón con
   relleno: en una columna de bloques rotulados, un botón sólido pesaría más
   que el contenido al que lleva. Copia el gesto de `.ficha-titulo` —el mismo
   chevron, el mismo hover— porque hace lo mismo: abrir lo que está plegado. */
.ver-conversacion{
  display:flex;align-items:center;gap:7px;cursor:pointer;user-select:none;
  font-size:var(--fs-meta);font-weight:600;color:var(--app-fg-secondary);
  padding:2px 0;
}
.ver-conversacion:hover{color:var(--app-fg)}
/* El contador de caracteres son DÍGITOS que se leen, no un glifo: se fue con el
   piso de 14 del 2026-09-08 (era el último `font-size:13px` literal de la hoja
   que llevaba letras). Toma el token del papel que juega — acompaña. */
.ver-conversacion .car{color:var(--app-fg-tertiary);font-size:var(--fs-micro);margin-left:auto}
.peek-nav{display:flex;align-items:center;gap:6px;flex:none}
/* Círculo, que es lo que le toca a un pill cuadrado. Dejarlas como rectángulo
   redondeado al lado de botones pill se ve como un descuido, no como variedad.

   AL ÁTOMO DEL CONTROL (2026-08-18, corrección de Arif). Median 26px clavados
   contra los 26.89 que mide todo lo que se pulsa en la app —botón, input,
   flotante, la × del diálogo—, y ésa es justo la peor distancia que existe:
   0.9px no se lee como intención, se lee como descuido, que es el mismo
   diagnóstico con el que la barra del titular se llevó al token. Ahora salen
   de `--btn-py` y `--btn-lh` como los demás, así que si el control cambia,
   estas siguen solas.
   Y visten como el botón suave —relleno tenue + filo de 1px por dentro— en vez
   del relleno plano sin filo que llevaban: son los tres botones que flotan
   sobre el papel del panel, y sobre papel un relleno sin contorno no se lee
   como pieza pulsable. El chevron se queda en 15: el ícono no crece con la
   caja, y a 20 un ‹ se ve como un signo suelto.

   QUIÉN LLEVA LAS FLECHAS (movido del catálogo `ui.php`, 2026-09-19): todo
   panel de lectura que se abra desde una LISTA. Con el panel puesto quedan
   ~420px de tabla, así que la fila de al lado deja de estar a la vista justo
   cuando el recorrido empieza a valer: si el panel tiene vecinos, el nav los
   ofrece. El panel de captura es la excepción declarada (`.peek.captura`: se
   llena uno, no se recorre nada). El vecino es el renglón que SE VE —las
   flechas se lo piden al anfitrión (`peek.js`, `vecino`), que camina el DOM y
   obedece el filtro y el orden puestos—. Llevaron al lado una píldora
   `.tecla` con «J K» y se retiró el mismo día: los botones dicen lo mismo sin
   aprender nada.
   DEUDA, comprobada el 2026-09-19: las llevan contactos, clientes e items;
   citas, cartera, envíos, gastos, la lista de ventas, registros, presupuestos
   y trabajadores abren un panel desde una lista y todavía no las llevan. */
/* ── Y DESDE EL 2026-09-09 LA FORMA TIENE NOMBRE: `.btn-circulo` ──────
   El tablero pidió el segundo consumidor —el botón de opciones de la columna—
   y había dos maneras de dárselo: copiar estas nueve declaraciones bajo otro
   selector, o nombrar la receta. Se nombra. Copiarla es exactamente el fallo
   que este repo ya tiene medido dos veces (`placeholder` entró 26 veces con el
   catálogo diciendo que no; `.campo-flotante` se copió en 33 campos siendo
   «recomendación»), y una tercera copia del círculo habría derivado en cuanto
   alguien tocara el átomo del control.

   Se añade a la MISMA regla en vez de mudarla a la zona de botones: entre este
   punto y aquélla viven las reglas de la × del sistema (`.dlg-x`), que pisan a
   `.peek-nav .flecha.cerrar` por orden de aparición. Mover la receta hacia
   abajo la pondría por delante de ellas y le devolvería a esa × la piel propia
   que perdió a propósito. El nombre nuevo no cuesta un byte de cascada; la
   mudanza sí costaba una regresión. */
.btn-circulo,
.peek-nav .flecha{
  width:calc(var(--fs-base) * var(--btn-lh) + var(--btn-py) * 2);
  height:calc(var(--fs-base) * var(--btn-lh) + var(--btn-py) * 2);
  /* `padding:0` es lo que hace que esta clase sea una FORMA y no un botón
     entero: la caja la fijan `width`/`height`, así que una PIEL de la familia
     puede posarse encima sin deformar el círculo — la de `.btn-segunda` trae
     14px laterales, y con `border-box` un padding mayor que el ancho empuja la
     caja y devuelve un óvalo. Para el peek es inocuo: sus flechas ya vivían
     con el padding de fábrica del `<button>` aplastado contra la medida. */
  padding:0;
  border-radius:var(--r-pill);border:0;
  background:var(--app-relleno-tenue);color:var(--app-fg-secondary);
  box-shadow:inset 0 0 0 1px var(--app-borde-muted);
  display:flex;align-items:center;justify-content:center;
  font-size:15px;line-height:1;cursor:pointer;
  transition:background var(--app-dur) var(--app-ease),color var(--app-dur) var(--app-ease);
}
.btn-circulo:hover,
.peek-nav .flecha:hover{background:var(--app-chip);color:var(--app-fg)}
/* LA FLECHA EN EL EXTREMO SE APAGA (2026-09-21, decisión de Arif): el mismo
   .45 y el cursor normal de `.btn-segunda:disabled`, y sin hover —misma
   especificidad que la regla de arriba y después de ella, así que gana—. Ni
   se esconde, porque movería la otra flecha y la × bajo el dedo, ni se queda
   pulsable sin llevar a ningún sitio (#regla-gesto-imposible). El estado lo
   pone `FLECHAS_PEEK` en app.js. */
.peek-nav .flecha:disabled{opacity:.45;cursor:default;background:var(--app-relleno-tenue);color:var(--app-fg-secondary)}
/* EL GLIFO DE LAS FLECHAS VA AL CUERPO DE LA × (2026-09-21, Arif: «¿son más
   pequeños?»). La caja ya era la misma; el signo no. Medido con la fuente del
   botón: a 15px `›` dibuja 6.7px de alto y la × a 20px 8.5 — las flechas se
   leían un quinto más chicas junto a su vecina. A 20px la flecha da 8.9. Sólo
   las del panel: `.btn-circulo` comparte la regla de arriba y lleva el «···»,
   que no entra en esta comparación. */
.peek-nav .flecha{font-size:20px}
/* Y EL TRAZO, AL DE LA × (mismo día, Arif). Medido como 2·área/perímetro de la
   tinta a 20px: × 2.2, `›` en normal 1.53 —un tercio más delgada— y en 700
   2.07; 500 no cambia nada en Arial y 600 ya cae en la negrita. `:not(.cerrar)`
   porque la × también es `.flecha`: engrosarla a ella devolvía la diferencia. */
.peek-nav .flecha:not(.cerrar){font-weight:700}
/* ── EL PEEK QUE CAPTURA: el rótulo sube a la fila del cierre (2026-08-18) ──
   Decisión de Arif, y la divide el TRABAJO del panel, no su tamaño:

   · El peek de CONSULTA se queda como está — rótulo en su propia fila, debajo
     del nav. Ahí el título es el DATO principal de la pantalla (el nombre del
     lead, el folio de la venta) y merece su renglón; encima el nav lleva las
     flechas de recorrido, así que la fila ya está ocupada.
   · El peek de CAPTURA (`.captura`) no titula un dato: rotula un formulario
     —«Nueva venta»— y ese rótulo no cambia nunca. No lleva flechas (no se
     recorre nada, se llena uno), así que su fila de nav está casi vacía con
     sólo la ×. Juntarlos recupera ~24px de alto —el rótulo y su margen— para
     el formulario, que es lo que de verdad pide sitio en un panel de captura.

   El h2 toma `flex:1` y con eso sobra el separador que empujaba la × al canto:
   una pieza menos que mantener. `min-width:0` + elipsis porque un rótulo largo
   en un panel angosto empujaría la × fuera de la fila — un botón de cerrar que
   se sale es peor que un título cortado. */
.peek.captura .peek-nav h2{
  flex:1;min-width:0;margin:0;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
/* El subtítulo pasa a colgar del nav y no del rótulo: 3px lo dejaban pegado a
   la fila de botones, que es más alta que el texto que antes tenía encima. */
.peek.captura .peek-sub{margin-top:6px}

/* Separado de las flechas: navegar y cerrar son cosas distintas y pegados se
   pulsa el que no era. Es lo ÚNICO suyo: el tamaño, la piel, el tipo y el
   hover se los da la regla compartida de la × del sistema (`.dlg-x`, abajo),
   a la que esta × se sumó — que es lo que ese comentario ya pedía: «quien
   añada otra × la pide por esta clase». Antes tenía piel propia y un hover
   que se iba al ámbar de atención: cerrar un panel que acompaña no es un acto
   que necesite advertencia, y el de la hoja modal —que sí interrumpe— nunca
   la tuvo. */
.peek-nav .flecha.cerrar{margin-left:8px}

/* ── EL NAV QUE TITULA (`.pk-nav-titulada`) ─────────────────────────────
   EXPERIMENTO del 2026-09-06 (tarde, pedido de Arif), estrenado por el
   catálogo de categorías del gasto: «Categorías · 5» al principio de la fila
   del cerrar.

   ── QUÉ COMPRA ──
   Una línea de panel. El nav de un peek SIN recorrido es un espaciador y una
   ✕ de 28px: ocupa el alto de una fila para decir una sola cosa, y el rótulo
   del panel se sienta debajo en otra. Juntos, el panel arranca su contenido
   una fila más arriba — que en un peek de 390px de ancho y alto de ventana es
   caro, porque lo que se pierde son categorías visibles sin scroll.

   ── QUÉ DICE EL RÓTULO, QUE ES LA MITAD QUE IMPORTA ──
   El TIPO de lo que se está mirando y su identificador: «Cliente 1042»,
   «Venta folio 318», «Categorías 6». Un registro y su folio, o un catálogo y su
   medida. **Nunca un nombre propio**, y ése es el porqué: un nombre no tiene
   techo. «Marisol Terán» cabe y «Comercializadora de Refacciones del Sureste
   S.A. de C.V.» no, así que un nav titulado con el nombre se rompe justo con el
   registro que más falta hace identificar. El tipo mide lo que mide siempre.

   Y EL NOMBRE NO SE PIERDE: sigue siendo el `h2` del cuerpo, que es donde tiene
   sitio para envolver. El reparto queda limpio — **el nav dice QUÉ ES, el `h2`
   dice QUIÉN**.

   ── CABE CON LAS FLECHAS, y por eso vale para TODOS los peeks ──
   Esta ficha llegó a decir que sólo cabía con el nav vacío, y era una
   suposición: se midió en un peek de 390 con recorrido y el rótulo pide **89px
   de los 388**, dejando **186 libres** entre él y las flechas. Cabe porque el
   rótulo es corto POR CONTRATO — es la misma regla de arriba mirada por el otro
   lado: lo que garantiza que quepa es que sea un tipo y no un nombre.

   Sigue siendo una clase y no una conducta de `.peek-nav` porque el rótulo hay
   que escribirlo: el nav no puede adivinar de qué es el panel.

   ── Y EL GESTO NO SUBE ──
   «Nueva categoría» se queda en su propia fila. Es la diferencia con la forma
   que este mismo rótulo tuvo por la mañana —cuando vivía en el nav y salió
   precisamente porque el conteo y el gesto no cabían con él—: sube la MEDIDA,
   que es del título y mide dos caracteres, no el gesto, que mide 134px y es lo
   único que no se puede leer a medias.

   El reparto es el de las tres cabezas del sistema: la medida pegada al
   título, y el que se va al canto lo declara él (el espaciador del nav). El
   `h2` cede ancho con elipsis, como en `.pk-cab`. */
/* `.peek-nav.pk-nav-titulada` y no `.pk-nav-titulada` a secas: `.peek h2` mide
   lo mismo (una clase y un elemento) y vive MÁS ABAJO en este archivo, así que
   ganaba por orden y el `margin-top:6px` del rótulo seguía puesto — medido, el
   `h2` caía 3px por debajo del centro de la ✕ en una fila que dice
   `align-items:center`. Con las dos clases la regla gana por especificidad y no
   por suerte de orden. */
.peek-nav.pk-nav-titulada > h2,
.peek-nav.pk-nav-titulada > .pk-nav-tipo{
  min-width:0;margin:0;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
/* ── DOS RÓTULOS DISTINTOS PARA DOS TRABAJOS DISTINTOS ──────────────────
   `<h2>` cuando el rótulo del nav ES el título del panel: un CATÁLOGO, cuyo
   cuerpo es una lista y no tiene otro encabezado («Categorías 6»).

   `.pk-nav-tipo` cuando debajo hay un REGISTRO con nombre propio. Ahí el `h2`
   del panel ya existe —es el nombre— y meter un segundo `h2` arriba crea dos
   encabezados del mismo rango: **el árbol de accesibilidad los lee como
   hermanos y el ojo los ve competir**, medido en la muestra del catálogo con
   «Cliente 1042» y «Marisol Terán» los dos en 15/700.

   Así que el tipo no es un encabezado: es CHROME —dice de qué es el panel, como
   una pestaña—, y se pinta un escalón por debajo del nombre.

   ── EL CUERPO ES EL DE UNA SUPERFICIE, Y ESO ES DELIBERADO ──
   Nació en `--fs-base` (14) y subió a `--fs-dialogo` (15) el 2026-09-06 a
   petición de Arif. **El token acierta por definición**: `--fs-dialogo` es «el
   rótulo de una SUPERFICIE — hoja modal, panel (peek), sección, riel», y esto
   rotula un panel. En 14 estaba tomando prestado el cuerpo de las filas y los
   inputs, que es otra cosa.

   LO QUE HAY QUE SABER ES QUÉ QUEDA SEPARÁNDOLO DEL NOMBRE. Eran tres
   propiedades —cuerpo, peso y tinta— y ahora son DOS: **600 contra 700 y tinta
   secundaria contra plena**. Sigue leyéndose como un escalón porque la tinta
   hace casi todo el trabajo (el secundario está calibrado a 7.4:1 contra el
   pleno), pero el margen es menor y conviene decirlo: si algún día el
   secundario se acerca al pleno, este par es el primero que deja de separarse.
   La regla de fondo no cambia —la jerarquía la llevan el peso y el color, no el
   tamaño—, y aquí el tamaño dejó de opinar a propósito. */
.peek-nav.pk-nav-titulada > .pk-nav-tipo{
  font-family:var(--fuente-titulo);
  font-size:var(--fs-dialogo);font-weight:600;letter-spacing:-.01em;
  color:var(--app-fg-secondary);
}
/* El aire de la cabeza que queda debajo lo pide ELLA, como siempre — pero ya
   no cuelga de un rótulo sino de una fila de mandos, así que pesa un punto más
   que los 6px de `.peek-nav + .pk-cab`: lo de arriba dejó de ser el titular
   del bloque y pasó a ser el chrome del panel. */
.peek-nav.pk-nav-titulada + .pk-cab{margin-top:10px}
/* ── Y EL AIRE DEBAJO DE LA CABEZA (2026-09-06, pedido de Arif) ─────────
   El cuerpo abría a ras del gesto —hueco 0 medido en pantalla— y las dos
   filas de arriba se leían como el primer renglón de la lista en vez de como
   la cabeza del panel. Un panel sin este aire no tiene cabeza: tiene un
   principio.

   14 Y NO OTRO, y no es un número a ojo: es el que `.peek-div` ya pide DEBAJO
   de sí (`margin:18px 0 14px`) — el aire con el que un bloque de este panel
   arranca después de una frontera. Aquí la frontera no es un filete sino el
   final de la cabeza, pero la pregunta que contesta el número es la misma.
   Se compararon los tres en pantalla: 10 sigue leyéndose pegado y 18 despega
   la cabeza del panel, que es el defecto contrario.

   LO PIDE EL DE ABAJO, como todo el ritmo vertical de este archivo. Y con el
   hermano general (`~`) y no el adyacente, porque `.pk-cab` sólo existe si hay
   gesto que pintar: al operador no se le da «Nueva categoría», y ahí el cuerpo
   sigue al nav directamente y necesita su aire igual. */
.peek-nav.pk-nav-titulada ~ .peek-cuerpo{margin-top:14px}
/* Y LO MISMO CUANDO LO QUE SIGUE ES EL NOMBRE DEL REGISTRO. En un peek de
   catálogo, debajo del nav va el gesto y luego la lista; en uno de registro va
   el `h2` con el nombre, y llegaba a los 6px que `.peek h2` trae de fábrica —
   pensados para un nav VACÍO, donde no hay nada de qué separarse—. Con el nav
   titulando, el tipo y el nombre casi se tocaban: dos rótulos pegados que hay
   que separar leyendo, cuando su diferencia es justo lo que la pieza quiere
   decir.

   14 Y NO 10 —los que lleva `.pk-cab`— a propósito, y la diferencia es la
   jerarquía: los 10 separan las DOS FILAS DE LA CABEZA entre sí, y los 14
   separan la cabeza de lo que titula. Lo de dentro de un bloque se agrupa más
   que el bloque de su vecino; con los dos iguales, la cabeza dejaría de leerse
   como una cosa. */
.peek-nav.pk-nav-titulada + h2{margin-top:14px}
/* EL RÓTULO DEL PANEL — 15px, no 20 (2026-08-18, corrección de Arif).
   Estaba en el token de 20 (hoy `--fs-destacado`) desde que nació, y 20 es la
   medida de un TITULAR
   dentro del contenido —la cifra de un detalle, un «¡Listo!»—, no la de un
   rótulo de superficie. El peek es una superficie, igual que la hoja modal y
   la sección: toma su mismo token, `--fs-dialogo`, con el mismo tracking de
   -.01em que llevan los demás rótulos de 15 (el -.015em es de los tamaños
   grandes; a 15 aprieta las letras sin ganar nada).
   Lo que sostiene la jerarquía contra el subtítulo de 14 no es el tamaño: es
   la fuente de titular, el peso 700 y que el sub va en color secundario — el
   mismo reparto que usa `.seccion-titulo` sobre el cuerpo. */
.peek h2{font-family:var(--fuente-titulo);font-size:var(--fs-dialogo);font-weight:700;line-height:1.2;margin:6px 0 0;letter-spacing:-.01em}
.peek .peek-sub{font-size:var(--fs-base);color:var(--app-fg-secondary);margin-top:3px}
/* EL ESTADO DEL REGISTRO, EN SU PROPIA FILA BAJO EL NOMBRE (2026-09-04, pedido
   de Arif). Es la `.alerta` del catálogo —«el estado que se queda en la página
   mientras la condición exista»—, que es exactamente lo que un registro
   desactivado es: no un aviso que sube y se va, sino una condición del sujeto
   que se está mirando.

   VA FUERA DE `.peek-cuerpo`, y ése es todo el diseño: el cuerpo scrollea y el
   estado no puede irse de la pantalla. Un panel que dice «desactivado» sólo
   mientras no bajes es un panel que lo dice a medias — y aquí importa porque el
   estado es justo lo que explica que no haya nada que pulsar.

   SUSTITUYÓ AL BADGE junto a la cifra, no se sumó a él: dos señales de la misma
   variable significan que una sobra, y la de arriba gana porque el estado se
   lee ANTES que el dato —quien abre un registro desactivado necesita saberlo
   antes de leer su precio, no después—. `flex:none` porque `.peek` es una
   columna flex: sin él, la alerta cede alto cuando el cuerpo reclama. */
.peek-alerta{margin-top:10px;flex:none}
/* El scroll del peek vive AQUÍ, no en la bitácora: así la ficha y el historial
   comparten una sola barra y el compositor de toque se queda anclado abajo.

   ── LOS DOS COSTADOS, Y POR QUÉ NO ERAN SIMÉTRICOS (2026-08-18) ──
   El costado DERECHO ya llevaba el par `margin -24 / padding 24`: saca el
   scroller hasta el canto del panel para que la barra de scroll no caiga
   pegada al último renglón, y repone el aire DENTRO. El izquierdo no lo
   llevaba, y ahí estaba el defecto: **`overflow-y:auto` obliga a
   `overflow-x` a computar `auto`**, y un scroller recorta la tinta en su caja
   de padding. Con padding-left 0, el input de ancho completo nace pegado al
   borde que recorta y el sistema le pinta encima un anillo de foco de 4px
   (`--ctl-halo`) que **no tiene dónde caer**: el halo salía cortado en recto
   por la izquierda mientras por la derecha se veía redondo y entero.

   Medido antes del arreglo: 0px de aire a la izquierda del control contra los
   4 que pide el anillo; 24 a la derecha. No era del control ni del halo —los
   dos son correctos y viven en todas las demás superficies sin problema—: era
   de la caja que los contiene.

   El arreglo espeja el par en el otro costado. El control NO se mueve —el
   margen negativo y el padding se cancelan, como del lado derecho—, así que
   la ficha, la bitácora y el separador siguen exactamente donde estaban; lo
   único que cambia es que ahora hay 24px de tinta disponible a cada lado.

   El costado sale de `--peek-pad-x`, que declara `.peek`: escrito a mano se
   sale de escuadra en cuanto aquél cambie, y así no puede.
   Y el lado derecho descuenta el canal del scroll, que es la regla del riel
   lateral de `ui.php` aplicada aquí: este cuerpo SÍ desborda (una ficha con
   bitácora larga), y sin reservar el canal el contenido cerraba a 24 o a 30
   según cuántos toques tuviera el lead. Reservado, la barra cae al canto del
   panel —como en la consola— y el texto respira sus 18 siempre. */
/* ── EL HALO DEL PANEL (2026-09-04, pedido de Arif) ─────────────────────
   LA MISMA PIEZA QUE EL HALO DEL TITULAR, en la otra superficie que tiene algo
   fijo sobre contenido que corre. Arriba del cuerpo de un panel viven el nombre,
   el subtítulo y —desde hoy— la alerta de estado; abajo, todo lo que se
   desplaza. Sin señal, el registro parece terminar donde acaba la pantalla.

   ES LA SEXTA DE SU ESPECIE y la primera que se REUSA en vez de inventarse: las
   otras son el `border-bottom` de la cabecera de la sábana, la sombra de
   `.grupo-pegado`, los dos filos de `.dlg-cuerpo.desborda`, el fundido de
   `.rec-cuerpo.desborda` y el halo del titular. Comparte con este último el
   gradiente EXACTO —misma elipse, mismo token, misma caída— porque dicen lo
   mismo: «hay una superficie encima». Una sombra recta diría «hay una línea».

   POR QUÉ `sticky` DENTRO DEL SCROLLER y no absoluta sobre el panel: la altura
   de la cabecera del panel NO es fija —hay paneles con subtítulo y sin él, y
   desde hoy con alerta de estado y sin ella—, así que una posición calculada
   desde arriba se desalinea en cuanto una cabecera cambia. Pegada al borde de
   su propio scroller, cae siempre donde empieza lo que se mueve.

   `height` + `margin-bottom` NEGATIVO del mismo valor: ocupa cero en el flujo,
   así que el primer bloque del cuerpo no se corre ni un píxel. Y sale por los
   dos costados en negativo, por lo mismo que el del titular: una sombra que
   respeta el padding se lee como un rectángulo dibujado, no como luz.

   ── LOS DOS COSTADOS NO MIDEN IGUAL, Y LA SIMETRÍA COSTABA UN SCROLL ──
   (2026-09-04.) Nació con `--peek-pad-x` en negativo a los DOS lados, y ahí
   estaba el defecto: el costado derecho del cuerpo no es `--peek-pad-x`, es
   `--peek-pad-x - --canal-scroll` —le descuenta el canal de la barra, cuatro
   líneas más abajo—, así que el negativo simétrico se pasaba de largo por la
   derecha exactamente esos 6px. Un hijo que se sale del scrollport es
   desbordamiento SCROLLEABLE, y `overflow-y:auto` obliga a `overflow-x` a
   computar `auto` —está escrito arriba, en el porqué de los costados—, así que
   el cuerpo del panel se quedaba con un eje horizontal de 6px que no tenía nada
   que enseñar.

   Lo que se veía: un empujón lateral —dos dedos en el trackpad, shift+rueda,
   arrastrar dentro del panel— y el contenido entero se corría 6px a la
   izquierda, con una barra horizontal apareciendo abajo que además se come 6px
   de alto (`clientHeight` 555 → 549) y con eso mueve el desborde VERTICAL: el
   descuadre y el scroll de más son el mismo bug. Medido antes en los tres
   contactos y con el panel recién abierto: `scrollWidth` 419 contra
   `clientWidth` 413, `scrollLeft` máximo 6. Después: 413 = 413 y `scrollLeft`
   clavado en 0. Era el ÚNICO scroller de la página con desborde horizontal
   —barrido de todos los elementos con `scrollWidth > clientWidth`—, así que no
   hay hermanos que reconciliar: el halo del titular no lo tiene porque su
   scroller no descuenta canal por ese lado.

   El arreglo ESPEJA el padding del cuerpo en vez de repetir el token, que es lo
   que ese mismo cuerpo ya hace unas líneas abajo. Lo que se pierde es que el
   halo deja de pintar bajo el canal de la barra —y ahí no pintaba nada: la
   elipse mide 62% del ancho y muere en transparente al 72%, así que esos
   últimos 6px llevaban cero tinta—. */
.peek-cuerpo::before{
  content:'';position:sticky;top:0;z-index:2;display:block;pointer-events:none;
  margin:0 calc((var(--peek-pad-x) - var(--canal-scroll)) * -1) -14px calc(var(--peek-pad-x) * -1);
  height:14px;
  background:radial-gradient(62% 100% at 50% 0%, var(--app-halo), transparent 72%);
  opacity:0;transition:opacity var(--app-dur) var(--app-ease);
}
.peek.halo .peek-cuerpo::before{opacity:1}

.peek-cuerpo{
  flex:1;min-height:0;overflow-y:auto;scrollbar-gutter:stable;
  margin-left:calc(var(--peek-pad-x) * -1);padding-left:var(--peek-pad-x);
  margin-right:calc(var(--peek-pad-x) * -1);
  padding-right:calc(var(--peek-pad-x) - var(--canal-scroll));
}
/* AQUÍ VIVIERON `.peek-canales`, `.canal` y `.canal.on` —los tres chips
   redondos de 34px que decían si el contacto tenía teléfono o correo—. Se
   retiraron el 2026-09-03 con su único consumidor (`interno/peek.php`, donde
   está el porqué). Dejan escrita una medida que sí vale para el siguiente chip:
   el apagado era `--app-chip` + `--app-fg-tertiary` a `opacity:.5`, o sea
   terciario sobre chip y encima medio velado — legible como forma, nunca como
   texto. Si vuelve un estado apagado, el velo va en la caja y el contraste se
   mide sin él. */
.peek-etiqueta{font-size:var(--fs-meta);font-weight:600;color:var(--app-fg-secondary);margin-bottom:7px}

/* ── El tablero ───────────────────────────────────────────────────────
   La capa contextual de la semilla. Columnas en flex con scroll propio;
   tarjetas arrastrables (HTML5 nativo). Población CURADA: sólo lo colocado. */
.sb-aparte{margin-top:10px}
.tablero{display:flex;gap:10px;align-items:stretch;overflow-x:auto;flex:1;min-height:0;padding-bottom:6px}
/* La columna va en el gris SECUNDARIO (2026-08-26, petición de Arif): es una
   superficie que acompaña —el continente de las tarjetas—, no navegación. */
.tab-col{
  /* 264px, ocho menos que los 272 de origen (2026-09-09, petición de
     Arif). Es una poda cosmética, no de encaje: con el gap de 10 caben
     las mismas cuatro columnas antes y después en la pantalla de
     trabajo. La tarjeta sigue con 226px de texto útil —descontados
     canto, aire del cuerpo y padding de la pieza—, y su nombre y sus
     dos líneas de meta ya recortan con elipsis, así que la resta no
     tiene dónde romper. */
  flex:0 0 264px;display:flex;flex-direction:column;min-height:0;
  background:var(--app-secundario-bg);border:1px solid var(--app-borde);
  border-radius:var(--r-ctl);
}
/* La columna bajo el cursor, SIN UN SOLO COLOR (2026-08-11, corrección de
   Arif). Llevaba el borde en acento y el fondo teñido: dos colores para decir
   algo que ni escribe en la base ni es un estado del dato — sólo «el cursor
   está aquí». Ahora lo dice el mismo canto que ya delimita las piezas del
   sistema, un punto más marcado que su hairline de reposo. La guía de
   inserción hace el resto del trabajo, y también va en gris. */
.tab-col.destino{border-color:var(--app-borde-muted)}
/* AQUÍ VIVÍA `.tab-col.nueva` —la columna PUNTEADA que el botón «Nueva etapa»
   plantaba al final del tablero con un input dentro—, retirada el 2026-09-09
   con la forma entera: la captura pasó a un diálogo del sistema (el porqué, en
   `activos/tablero.js`). Se va la clase y NO el `.tab-renombrar` de abajo, que
   sigue vivo con su otro consumidor: el input que sustituye al nombre de una
   columna al renombrarla en sitio. */
/* El aire de la derecha lo pone el propio botón de opciones —es un círculo del
   átomo del control, y su caja ya rodea al glifo—, así que la cabecera sólo
   conserva los 10px del lado del nombre. Con `padding:5px 10px` a los cuatro
   lados el círculo quedaba a 10px del canto y la cabecera se leía descentrada:
   mucho aire a la derecha y el nombre pegado a la izquierda. */
.tab-cab{
  display:flex;align-items:center;gap:6px;flex:none;
  padding:3px 5px 3px 10px;border-bottom:1px solid var(--app-hairline);
}
.tab-nombre{font-weight:600;font-size:var(--fs-base);color:var(--app-fg);cursor:default;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* EL CONTEO DE LA COLUMNA DEL TABLERO. Pedía `var(--fs-sec, 12px)` y
   `--fs-sec` NO EXISTE en la hoja —nunca existió, ni con otro valor—, así que
   llevaba once meses cayendo al literal de respaldo: 12px, el único texto de la
   app por debajo del piso, y por un token fantasma que nadie leyó como error
   porque el fallback lo hacía funcionar. Lo destapó el censo del piso de 14
   (2026-09-08). Toma el token del papel que juega: acompaña al nombre. */
.tab-n{font-size:var(--fs-micro);color:var(--app-fg-tertiary);flex:none;margin-right:auto}
/* ── AQUÍ VIVÍAN `.tab-mover` y `.tab-quitar` ─────────────────────────
   Los tres glifos sueltos de la cabecera —`‹`, `›` y la `×`—, retirados el
   2026-09-09 (petición de Arif) y sustituidos por UN botón de opciones. No se
   fueron por estética; se fueron porque medidos no eran controles:

     · `<span>` sin `tabindex`, así que el teclado NO LOS ALCANZABA. Las tres
       únicas herramientas de la etapa no existían sin ratón.
     · `opacity:0` hasta el hover de la cabecera — y un elemento a opacidad 0
       SIGUE RECIBIENDO EL CLIC. En táctil, donde no hay hover, eso es una ×
       invisible que retira la etapa al tocarla.
     · 10.97 × 15 = 165px² de superficie contra los 724 del átomo del control
       (26.9², la flecha del peek). Un cuarto de la diana, para el gesto más
       caro de la pantalla.
     · `--r-mini`, que es el radio de «¿mide menos de 20px?» — pista de
       teclado, `code`, checkbox. Vestían de anotación, no de control.

   Lo que los sustituye es `.btn-circulo` sin una línea propia de piel, que es
   la prueba de que la forma ya existía y lo que faltaba era usarla. */
.tab-menu{flex:none}
/* A 18px el icono queda entre el que acompaña texto (16) y el que marca fila
   (20): el botón mide ~30px y el glifo que sustituye ocupaba esa altura. */
.tab-menu .icn{width:18px;height:18px;vertical-align:0}
/* El botón se queda encendido mientras su menú está abierto: sin esto, apartar
   el cursor para bajar al menú apaga el hover y la tecla se lee como si se
   hubiera soltado el gesto a medias. Y se enciende POR EL MISMO CAMINO que el
   hover —desvaneciendo la cara de reposo— en vez de pintarle un fondo: teñirlo
   mataría el degradado y el botón abierto sería el único de la familia sin
   volumen, que es justo lo que este cambio vino a quitar. */
.tab-menu[aria-expanded="true"]::before{opacity:0}
.tab-renombrar{
  width:100%;font:inherit;font-weight:600;color:var(--app-fg);
  background:var(--app-bg);border:1px solid var(--ctl-borde-hover);
  border-radius:var(--r-mini);padding:2px 6px;outline:none;
}
.tab-cuerpo{flex:1;min-height:60px;overflow-y:auto;padding:8px 6px;display:flex;flex-direction:column;gap:6px}
.tab-hueco{
  border:1px dashed var(--ctl-borde);border-radius:var(--r-ctl);
  color:var(--app-fg-tertiary);font-size:var(--fs-base);text-align:center;padding:18px 0;
}
/* EL FILO ES PUNTEADO, y eso es lo que dice qué clase de objeto es (2026-09-09,
   petición de Arif). El continuo es el filo de lo que está PUESTO —el control,
   la fila de la sábana, la hoja—; el punteado, en esta hoja, ya venía diciendo
   «esto se mueve o se llena»: el hueco de la columna («Suelta aquí») lo usa
   desde que existe el tablero. La tarjeta pertenece a esa familia y no a la
   otra: en la práctica su vida es cruzar de etapa.

   NACIÓ EN `--ctl-borde` Y DURÓ MINUTOS: «casi imperceptible», corrección de
   Arif el mismo día; el peldaño siguiente de la escalera de bordes tampoco
   bastó, medido con las cuatro candidatas puestas una sobre otra en la misma
   columna. El error fue medir el TOKEN y no el DIBUJO: un filo punteado enseña
   la mitad de sus píxeles, así que al mismo color pesa la mitad que el continuo
   que sustituía — y toda la escalera de bordes (7.6% a 25.4% de tinta) está
   calibrada para trazo continuo. Puntear no cuesta un escalón: cuesta salir de
   esa escalera.

   Por eso el filo va en `--app-fg-tertiary`, que NO es un token de borde y aquí
   es lo correcto: es el color con el que esta misma pieza ya dibuja la GUÍA DE
   ARRASTRE (`.tj.cae-antes`, unas líneas abajo), o sea el vocabulario del
   gesto, no el de los contornos. El filo punteado dice lo mismo que la guía y
   ahora lo dice con la misma voz. El hover sube a `--app-fg-secondary` para
   conservar su escalón. Sigue a 1px: el peso se compra con contraste, no con
   grosor — engordar el filo de una pieza que puede repetirse veinte veces en
   la columna sí la volvería ruidosa. */
.tj{
  background:var(--app-bg);border:1px dashed var(--app-fg-tertiary);border-radius:var(--r-ctl);
  /* DOS cursores, uno por gesto, y el orden importa: en reposo `pointer`
     —porque el clic ABRE el panel, que es lo que se hace a diario— y sólo al
     apretar pasa a `grabbing`, la mano cerrada que todo drag & drop usa
     mientras se sostiene. Antes era `grab` en reposo: anunciaba el arrastre
     (lo raro) y escondía el clic (lo común). */
  padding:9px 11px;cursor:pointer;
  /* SIN selección de texto. Con arrastre por pointer events (y no el DnD
     nativo), arrastrar sobre el nombre de la tarjeta iniciaría una selección
     de texto: el navegador se queda el gesto, dispara `pointercancel` y el
     arrastre muere a mitad. La tarjeta es un objeto que se toma, no un párrafo
     que se lee. */
  user-select:none;
}
.tj:hover{border-color:var(--app-fg-secondary)}
.tj.en-vuelo{opacity:.45}   /* atenuada, NO oculta: en display:none pierde la captura del puntero y el arrastre se cancela solo */
.tj:active{cursor:grabbing}
/* ── Dónde va a caer (2026-08-11) ─────────────────────────────────────
   Desde que el orden dentro de la columna es manual, soltar sin saber el
   sitio sería adivinar: la mitad de las veces cae antes de la tarjeta que
   pisas y la otra mitad después. La línea lo dice ANTES de soltar.

   Va como `box-shadow` y no como borde ni como elemento: no ocupa espacio, así
   que la columna no da un salto de 2px cada vez que el cursor cruza el punto
   medio de una tarjeta — que es exactamente el temblor que hace que un
   arrastre se sienta roto. Azul de acento porque soltar SÍ escribe. */
/* ── Dónde va a caer ──────────────────────────────────────────────────
   Una línea fina, y SIN el acento (2026-08-11). Nació azul y duró unas horas:
   además de no gustar, rompía la regla del sistema — el acento es SÓLO para lo
   que escribe en la base, y una guía de arrastre todavía no ha escrito nada.
   En el gris terciario cumple igual: dice el sitio sin reclamar la vista.

   (El sustituto ambicioso —el hueco que se abre con las tarjetas deslizándose,
   como SortableJS— se probó y se retiró el mismo día: exige esconder la
   tarjeta original, y un elemento en `display:none` pierde la captura del
   puntero, así que el arrastre se cancelaba solo. El porqué completo, y qué
   haría falta para retomarlo, en la cabecera de la guía en tablero.js.)

   Va como `box-shadow` y no como borde: no ocupa espacio, así que la columna
   no da un salto de 2px cada vez que el cursor cruza el punto medio de una
   tarjeta — que es justo el temblor que hace que un arrastre se sienta roto. */
.tj.cae-antes{box-shadow:0 -2px 0 var(--app-fg-tertiary)}
.tab-cuerpo.cae-al-final{box-shadow:inset 0 -2px 0 var(--app-fg-tertiary)}

/* ── El arrastre, mientras dura (2026-08-11) ──────────────────────────
   El tablero dejó el drag & drop de HTML5 por POINTER EVENTS, y el motivo es
   justo este bloque: en un arrastre nativo el cursor lo pinta el navegador y
   el CSS no manda, así que `grabbing` se perdía en cuanto la tarjeta empezaba
   a moverse. Con el arrastre propio, el cursor vuelve a ser nuestro.

   `!important` y sobre TODO elemento a propósito: durante el arrastre el
   cursor pasa la mayor parte del tiempo sobre el hueco entre columnas y sobre
   otras tarjetas —cada una con su propio `cursor`—, y basta que una gane para
   que la mano se convierta en flecha a mitad del gesto. */
html.arrastrando, html.arrastrando *{cursor:grabbing !important}
/* Sin selección de texto mientras se arrastra: al cruzar sobre los nombres de
   las tarjetas, el navegador los iría marcando en azul. */
html.arrastrando{user-select:none}

/* El fantasma que sigue al puntero. Lo pintamos nosotros —el nativo lo dibujaba
   el navegador, distinto en cada uno— y por eso puede ir un punto levantado:
   sombra larga y una pizca girado, que es lo que hace que se lea como «esto lo
   llevas en la mano» y no como «esto está pegado ahí». */
.tj.fantasma{
  position:fixed;z-index:70;pointer-events:none;
  box-shadow:0 10px 26px var(--app-shadow);
  transform:rotate(1.2deg);opacity:.96;
}
.tj-nombre{font-weight:500;color:var(--app-fg);font-size:var(--fs-base)}

/* AQUÍ VIVIÓ `.sig-accion` —la fila «Siguiente acción» del peek: el campo de
   texto, su fecha y el botón `✓ Hecha`—, retirada el 2026-09-02 con el modo
   tareas. Sobrevivió al bloque que la dejó sin marcado porque sus dos campos
   estaban además dentro de los tres selectores compartidos del control base;
   la barrida final del desmontaje los sacó de ahí y con eso ya no quedaba nada
   que sostuviera estas cinco reglas. */
.peek-div{height:1px;background:var(--app-hairline);margin:18px 0 14px;flex:none}
/* ── LA NOTA, y la lápida de lo que ocupaba este sitio ──────────────────
   AQUÍ VIVIERON `.nueva-accion` + `.registrar-toque` (el compositor de toques,
   anclado FUERA del scroll porque era el camino crítico del POV y nunca había
   que ir a buscarlo) y `.bitacora` con sus `.entrada`. Se fueron con `t_accion`
   el 2026-09-02. Tres lecciones de aquel bloque que vale conservar:

   · ENVOLVER EN DOS LÍNEAS, que lo cazó Arif leyendo el peek. Cuatro controles
     en una fila no caben en un panel de ~430px: los selects miden lo que miden
     sus etiquetas y no ceden, así que cedía el INPUT —aplastado a ~40px, con
     el placeholder «¿Qué pasó?» leyéndose «¿Qué»—. El corte se fuerza desde los
     SELECTS (cada uno pidiendo `50% - 4px`), no desde el input a `basis:100%`:
     eso también cortaba, pero se llevaba el botón a una TERCERA línea, separado
     del campo que guarda.
   · UN `basis` EXPLÍCITO EN EL CAMPO LARGO (60%, no `auto`): con `auto` pedía
     su ancho natural y el botón se descolgaba.
   · EL SEPARADOR PERTENECE AL BLOQUE DE ABAJO. `.nueva-accion` existía sólo
     para quedarse con el filete y el `flex:none`, dejando a la fila de
     controles siendo sólo eso.

   La NOTA hereda la primera y la segunda, y no necesita la tercera: vive dentro
   del scroll, no anclada. Es un gesto de un dato, no de tres. */
/* LA CABECERA DE «Notas» — rótulo + su gesto (2026-09-03), la misma anatomía
   que `.pk-cab`: el gesto vive en la fila del TÍTULO y no junto al
   contenido, así que no lo mueve el dato. Los 7px que `.peek-etiqueta` lleva
   como `margin-bottom` estirarían esta fila, así que se anulan aquí y los
   reponen abajo `.nota-nueva` y `.notas` como `margin-top`. */
/* LA CABECERA DE UNA SECCIÓN DEL PEEK: rótulo a la izquierda, su gesto o su
   dato a la derecha, contenido debajo. UN nombre para las cuatro instancias
   —ente, notas, datos y etiquetas— desde el 2026-09-03: fueron `.pk-ente-cab` y
   `.pk-notas-cab`, dos copias idénticas de las mismas dos declaraciones, y al
   entrar la tercera se unificaron en una regla con tres selectores. La cuarta
   dejó claro que lo que sobraba eran los NOMBRES, no las reglas: un nombre por
   instancia obliga a tocar el CSS cada vez que una sección gana cabecera, que
   es exactamente lo que no debería costar nada. */
.pk-cab{display:flex;align-items:center;gap:8px}
.pk-cab .peek-etiqueta{flex:1;min-width:0;margin-bottom:0}
/* EL GESTO NO SE ENCOGE. `.cf-btn` trae la cláusula de fábrica —«un rótulo de
   gesto NO se parte ni se encoge»— y `.btn-segunda` no la tiene: nació para el
   grupo del titular, donde nada le disputa el ancho. Aquí sí se lo disputan, y
   es justo el fallo que mandó este gesto a la fila del rótulo (2026-09-03). Lo
   pide «Agrupar» al ganar cara el 2026-09-04; quien cede el ancho es el rótulo,
   que para eso lleva `min-width:0`. */
.pk-cab > .btn-segunda{flex:none}
/* ── Y LA MISMA CABECERA, SIRVIENDO DE CABEZA DEL PANEL (2026-09-06) ────
   `.pk-cab` nació para una SECCIÓN de dentro del peek —ente, notas, datos,
   etiquetas—, y el catálogo de categorías del gasto la pide para el panel
   entero: su única sección ES el panel, así que su rótulo es el `<h2>`.

   ── POR QUÉ EL PANEL LA NECESITA, QUE ES LO QUE LA JUSTIFICA ──
   El gesto de alta vivía dentro de `.peek-cuerpo`, y ése es el scroller: con
   la lista larga, «Nueva categoría» se iba de la pantalla justo cuando más
   falta hace. Es el mismo argumento con el que `.peek-alerta` vive fuera del
   cuerpo —«un panel que dice desactivado sólo mientras no bajes lo dice a
   medias»—, aplicado a un gesto en vez de a un estado.

   ── EL REPARTO ES EL DE `.lado-cabeza`, NO EL DEFAULT DE ESTA PIEZA ──
   Con `.peek-etiqueta` el que crece es el rótulo (`flex:1`), y eso ahí es
   correcto: a su derecha va UNA cosa. Aquí van dos —la medida y el gesto— y un
   rótulo que crece las manda a las dos al canto opuesto, con el conteo
   leyéndose como parte del botón: es el fallo exacto que Arif cazó en el riel
   el 2026-09-04, y la regla que salió de ahí vale para las tres cabezas del
   sistema (titular de página, riel, y desde hoy panel). La MEDIDA va pegada al
   título; el GESTO se va al canto; y **el que no cede ancho es el gesto**, que
   es lo único de los tres que no se puede leer a medias.

   El `h2` cede, entonces, y lo hace con elipsis: un catálogo se titula con una
   palabra, así que truncar aquí es el caso raro y no el de todos los días. */
.pk-cab > h2{min-width:0;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pk-cab > h2 ~ .btn-segunda{margin-left:auto}
/* EL AIRE SOBRE LA CABEZA lo pone ella y no el `h2`, que acaba de soltar su
   `margin-top:6px` para no descuadrarse contra el botón dentro de un flex
   centrado. Los mismos 6px, y pedidos por el bloque de ABAJO — la regla del
   separador que este archivo ya obedece en la sección y en el peek. */
.peek-nav + .pk-cab{margin-top:6px}
/* Y LA MEDIDA, dentro de la cabeza. `.seccion-n` es hermana del `h2` y no su
   hija a propósito: dentro heredaría la fuente de titular y el peso 700, y
   esto es tinta terciaria en `--fs-meta`. Lo que sí necesita es no separarse
   de su título — el `gap` de la cabeza ya la deja a 8px, el mismo canal que
   `.lado-cabeza` usa en el riel. */

/* EL RITMO VERTICAL, NORMALIZADO (2026-09-03, pedido de Arif). Antes cada
   pieza traía su propio número y los tres casos medían distinto: el compositor
   cerraba con `margin-bottom:10`, la primera nota abría a 0 (`padding-top:0`) y
   el «Sin notas todavía» abría a 10 y cerraba a 10. O sea que el bloque entero
   se movía según si había notas o no, y según si el compositor estaba abierto.

   Hoy hay UN número, 7px, y lo pide siempre el elemento de ABAJO —la misma
   regla que el separador de este panel—: `.nota-nueva` y `.notas` lo llevan
   como `margin-top`, así que la lista queda a 7 del rótulo con el compositor
   cerrado y a 7 del compositor con él abierto. Y los dos estados de la lista
   —con notas y sin— abren y cierran a ras, porque la primera nota no lleva
   padding arriba, la última no lo lleva abajo y el vacío no lleva ninguno.
   «AGREGAR» ES UN DISCLOSURE (`aria-expanded`, rótulo fijo) y no un
   interruptor: no guarda ningún booleano, sólo enseña la caja. Y «Apuntar»
   sólo aparece cuando hay texto: un guardar sobre un campo vacío ofrece una
   escritura que el modelo corta (movido del catálogo `ui.php`, 2026-09-19).
   El aire de fuera lo pone el `.peek-div`, que es de quien es. */
.nota-nueva{display:flex;gap:8px;align-items:flex-start;flex-wrap:wrap;margin-top:7px}
/* Basis en 100%: la textarea se queda con el renglón entero y «Apuntar» baja
   a su propia línea, alineado a la derecha. Con los dos en la misma fila la
   textarea se aplasta igual que se aplastaba el input del compositor viejo —
   el mismo fallo, la misma medida, y se reprodujo en la bandeja al probarlo.
   POR ELEMENTO Y NO POR CLASE: el peek pinta la textarea con `.fx-area` y la
   bandeja con `.mv-input`, así que apuntar a una de las dos deja la otra
   aplastada. Lo que las hace iguales es SER el campo del compositor. */
.nota-nueva > textarea{flex:1 1 100%;min-width:0;resize:vertical}
/* AQUÍ VIVIÓ `.nota-nueva .guardar` —«Apuntar» pintado como texto azul sobre
   nada, sin caja—. Se retiró el 2026-09-03 (reportado por Arif) porque no era
   ningún botón del sistema: el catálogo declara TRES y sólo tres (acento,
   segunda, suave), y aquél era un cuarto inventado que además usaba el azul de
   escritura como si fuera un enlace. Hoy el compositor cierra con `.btn-acento`
   —la primaria— y lo único que se conserva de la regla vieja es el
   `margin-left:auto` que lo manda al canto derecho, que ahora se pide por su
   sitio y no por su clase. */
.nota-nueva > .btn-acento{margin-left:auto;flex:none}

/* LA LISTA. Sin iconos, al revés que la bitácora: aquélla pintaba un círculo
   con las iniciales del canal porque el canal ERA la mitad del dato («¿por
   dónde le escribí?»). Una nota no tiene canal — la escribió una persona, y lo
   que hay que saber es quién y cuándo. Eso cabe en una línea de meta. */
.notas{margin-top:7px}
.notas .nota{padding:9px 0;border-top:1px solid var(--app-hairline)}
.notas .nota:first-child{border-top:0;padding-top:0}
/* La última cierra a ras, como la primera abre a ras: lo que separa este bloque
   del siguiente es el `.peek-div`, y sumarle 9px de padding lo desnivelaría
   contra el mismo bloque cuando está vacío. */
.notas .nota:last-child{padding-bottom:0}
/* `pre-wrap` porque una nota puede llevar saltos de línea (Shift+Enter) y
   `break-word` porque puede llevar una URL sin espacios, que de otro modo
   ensancha el panel entero. */
.notas .nota-txt{font-size:var(--fs-base);line-height:1.4;white-space:pre-wrap;overflow-wrap:break-word}
.notas .nota-meta{font-size:var(--fs-meta);color:var(--app-fg-tertiary);margin-top:3px}
/* SIN PADDING: el vacío tiene que ocupar el mismo sitio que ocuparía la
   primera nota, que abre y cierra a ras. Con `padding:10px 0` el bloque saltaba
   10px al quedarse sin notas y otros 10 al recibir la primera. */
.notas-vacio{font-size:var(--fs-base);color:var(--app-fg-tertiary)}
/* LA NOTA COMO CARD (2026-09-25, pedido de Arif para la ficha de
   conversaciones.php). Cada nota es un registro que se edita y se elimina, así
   que lleva caja y su menú «···» al canto (#regla-gestos-menu: un gesto
   destructivo junto a otro va a menú). La cara es la de `.rec-card` —papel,
   línea de pelo, radio de caja y sombra corta—: una tarjeta que se opera ya
   tiene forma en este sistema. El filete entre notas de `.notas .nota` no
   aplica: aquí separa el aire de 8px entre cards. El peek conserva la lista
   con filete y sin menú.
   CABEZA ARRIBA, TEXTO DEBAJO (pedido de Arif, el mismo día): autor y fecha
   van en la primera fila con el «···» al canto, y el texto de la nota ocupa
   el ancho entero debajo, sin compartir renglón con el botón. */
.nota-card{
  background:var(--app-paper);border:1px solid var(--app-hairline);
  border-radius:var(--r-caja);padding:6px 6px 11px 14px;
  box-shadow:0 1px 3px var(--app-shadow);
}
.nota-card + .nota-card{margin-top:8px}
.nota-card-cab{display:flex;align-items:center;gap:8px}
.notas .nota-card-cab .nota-meta{flex:1;min-width:0;margin-top:0}
.nota-card-cab > .btn-circulo{flex:none}
.notas .nota-card .nota-txt{margin-top:2px;padding-right:8px}
/* LA NOTA LARGA SE CORTA A 3 RENGLONES (2026-09-25, pedido de Arif; nació en
   4 y ese mismo día la pidió más acotada): la
   columna de la ficha es angosta y una nota de 2,000 caracteres empujaba las
   demás fuera de la vista. Se corta por renglones y no por caracteres: una nota
   de muchos saltos cortos ocupa alto sin llegar a ningún tope de caracteres. El
   fundido y la mano sólo van donde de verdad se corta (`.desborda`, lo pone
   conversaciones.js midiendo), con el mismo degradado que `.rec-cuerpo`: con la
   máscara puesta siempre, una nota corta parecería truncada (su tramo, abajo).
   El clic abre
   `#modal-nota-leer`; «Ver más…» es la `.cursor-pista` junto al puntero. */
.notas .nota-card .nota-txt{max-height:calc(1.4em * 3);overflow:hidden}
/* El degradado arranca más abajo que el de `.rec-cuerpo` (62%→96%), que corta
   a 9 renglones: sobre 3 o 4, ese tramo borraba el último casi entero y la
   nota se leía con un renglón fantasma. Aquí se funde sólo la mitad baja del último
   renglón. */
.notas .nota-card .nota-txt.desborda{
  cursor:pointer;
  -webkit-mask-image:linear-gradient(#000 82%,transparent 100%);
  mask-image:linear-gradient(#000 82%,transparent 100%);
}
/* El texto entero en el diálogo de lectura: los saltos de línea de la nota y
   una URL larga que no ensanche la hoja. */
.nota-leer{font-size:var(--fs-base);line-height:1.5;white-space:pre-wrap;overflow-wrap:break-word}
.peek-pie{
  display:flex;align-items:center;gap:8px;
  padding-top:12px;border-top:1px solid var(--app-hairline);flex:none;
}
.peek-pie .origen{
  font-size:var(--fs-micro);color:var(--app-fg-tertiary);flex:1;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
/* ── EL CHIP DE ETIQUETA ────────────────────────────────────────────────
   UNA clase, dos variantes. Hasta el 2026-09-03 eran DOS clases pintando lo
   mismo de dos maneras: ésta en la columna de la sábana y la cabecera del peek,
   y `.fx-etq` en el editor del peek — con `--r-pill`, o sea **el radio de los
   botones**, fondo blanco puro y su `background` declarado dos veces en
   conflicto. La doctrina de radios de este archivo ya lo decía: `--r-ctl` es
   «¿contiene algo? control, opción, celda, CHIP» y `--r-pill` es «¿SE PULSA?
   botones». Una etiqueta no se pulsa: se lee.

   LA MARCA es lo que la separa del BADGE DE ESTADO, con el que compartía radio,
   tinte y medida — sólo se distinguían leyéndolos. El glifo dice «esto lo puso
   alguien» frente a un estado que calcula la app. Hereda `currentColor` como
   todos los iconos, así que cambia de tema sola.

   El precio, aceptado con el diseño: ~14px por chip, y en la columna de la
   sábana se paga entero — con tres etiquetas en una celda estrecha, el último
   nombre se corta antes que sin ella. Identidad a cambio de nombres visibles.

   Diseñado en canvas antes de escribirse (las tres propuestas, el estado
   previo y el caso que las separa: la fila elegida con tres etiquetas). */
.etiqueta{
  display:inline-flex;align-items:center;gap:4px;
  font-size:var(--fs-micro);background:var(--app-chip);color:var(--app-fg-secondary);
  border-radius:var(--r-ctl);padding:2px 7px 2px 6px;white-space:nowrap;
  /* EL FILO ENTRÓ DESPUÉS (pedido de Arif el mismo día): la propuesta elegida
     era el chip relleno y sin borde, y en la vitrina se defendía sola. En la
     app no: el relleno es un tinte del 6.4%, así que en cuanto la FILA cambia
     de fondo —hover, fila elegida, la caja del control del editor— el chip se
     acerca a lo que tiene detrás y pierde contorno. Es el caso que la propuesta
     B del canvas venía a resolver y que la C no cubría; se toma de ahí.

     `box-shadow` y no `border`: un borde real sumaría 2px a la caja y el chip
     dejaría de medir los 21px con los que se diseñó para la fila de 36.

     Y va en `--app-borde-muted` (19.1%) y no en `--app-hairline` (7.6%), que
     fue el primer intento: a 7.6 el filo existía en la medición y no en el ojo
     —es el token de la RETÍCULA, hecho para separar celdas sin que se vea— y
     Arif pidió realce. 19.1 es el escalón que la casa ya usa cuando un filo
     tiene que leerse: el de `.btn-suave` y el del gesto de renglón. */
  box-shadow:inset 0 0 0 1px var(--app-borde-muted);
  /* `line-height` PROPIO, y no es cosmético: la celda de la fila centra su texto
     con `line-height:35px` y el chip lo heredaba. Mientras fue `inline` daba
     igual —la caja pintada de un inline no crece con la línea—, pero al pasar a
     `inline-flex` (2026-09-03, para alojar la marca) ese 35 se volvió el alto
     del contenido y el chip midió 39px dentro de una fila de 36. Con 1.3 vuelve
     a los 21px del diseño: 13 de texto + 4 de padding. */
  line-height:1.3;
}
/* 12 y no `--icn-texto`: el chip mide 21px de alto y el glifo tiene que caber
   con su aire. La opacidad lo baja medio tono respecto al nombre — acompaña, no
   compite. */
.etiqueta .icn{width:12px;height:12px;opacity:.75}
/* EL CHIP EDITABLE CRECE CON SU × (corregido el 2026-09-19: encima de este
   comentario quedaba otro que decía «La variante EDITABLE no cambia de forma,
   sólo cede el aire de la derecha», anterior al 2026-09-03), y por eso es una regla aparte del chip de
   sólo lectura: con la × a 24 el chip pasa de 21px a 28, y esos 28 **no caben
   en la fila de 36 de la sábana** — pero ahí el chip no lleva × ni esta clase.
   El editable vive en la tira de la ficha, que envuelve en sus propias líneas y
   no tiene fila que respetar. Los dos tamaños son correctos porque son dos
   sitios: el de leer cabe en la retícula, el de operar cabe en el dedo.
   El relleno derecho baja a 2 porque la × ya trae su propio aire dentro. */
.etiqueta.editable{padding:2px 2px 2px 8px;gap:5px}

/* Aquí vivía `.velo`, un cazador de clics TRANSPARENTE que quedó del popover
   del constructor de filtro (retirado el 2026-08-09) y sobrevivió como muleta
   del diálogo de exportación. Murió con él el 2026-08-11: el velo de verdad
   —el que oscurece— lo pinta ahora `.dialogo` con `--app-velo`. */

/* ── Badges ───────────────────────────────────────────────────────────
   Estado, no acción. Sólo suaves y ninguno en el azul sólido: ese color está
   reservado a lo que se pulsa, y un badge no se pulsa nunca — si algo tiene
   que hacer clic, es un botón o un chip, no esto.

   El texto es SIEMPRE `--app-fg`, el mismo del cuerpo: oscuro en claro, claro
   en oscuro. Ningún badge tiene color de letra propio, así que no hay ninguna
   pareja fondo-texto que auditar por separado.

   Radio de control (8px) y no pill: la regla del sistema es que el pill
   significa «se pulsa». Un badge con forma de botón promete un clic que no
   existe.

   Nota sobre `danger`: nació como ESTADO —«esta fila falló», «este correo
   rebotó»— y desde el 2026-08-28 existe también como ACCIÓN, pero sólo en un
   sitio y con condiciones: `.bs-accion.danger`, la acción que RETIRA lo
   seleccionado de donde se trabaja, y que es la única TEÑIDA de su barra de
   selección (ver ahí). Sigue sin haber BOTÓN de peligro, y lo que avisa antes
   de hacer algo lo dice la hoja de confirmación.

   *(Decía «la ÚNICA de su barra» hasta el 2026-08-29, y ya era falso al
   escribirse: la condición nunca fue que la barra tuviera una sola acción sino
   que una sola llevara el tinte. Ventas la estrenó con cuatro y gastos con
   cuatro; leads la sumó con seis.)* */
/* `line-height:1.5` DECLARADO (2026-09-21), el mismo que el cuerpo: fuera de
   una fila no cambia nada, porque es lo que ya heredaba. Dentro de una fila
   `.fila > *` impone `line-height:35px`, y un badge que no declaraba el suyo
   lo heredaba y medía 39px en una fila de 36 —medido en la sábana de la
   prenómina, con las faltas y las horas extra ya como badge—. Es el mismo
   arreglo que la `.sigla` tiene escrito. */
.badge{
  display:inline-flex;align-items:center;gap:5px;
  background:var(--badge-info);color:var(--app-fg);
  border-radius:var(--r-ctl);padding:2px 9px;
  font-size:var(--fs-micro);font-weight:500;line-height:1.5;
  white-space:nowrap;
}
.badge.warning{background:var(--badge-warning)}
.badge.danger{background:var(--badge-danger)}
.badge.success{background:var(--badge-success)}
/* Punto de color: cuando cuatro badges conviven en una columna, el tinte solo
   no basta para distinguirlos de un vistazo. Opcional — un badge suelto no lo
   necesita. */
/* El punto lleva el token de TEXTO y no el del tinte: el color puro sobre su
   propia tinta da entre 1.37 y 2.91:1 en tema claro — por debajo del 3:1 que
   pide un elemento gráfico. Con el de texto sube a 4.6-5.0. */
.badge .punto{width:6px;height:6px;border-radius:50%;flex:none;background:var(--app-info-text)}
.badge.warning .punto{background:var(--app-atencion-text)}
.badge.danger .punto{background:var(--app-peligro-text)}
.badge.success .punto{background:var(--app-exito-text)}

/* ── La SIGLA: la marca corta de una columna de estado ────────────────
   (2026-09-20, diseño de Arif para la columna «Incidencia» de asistencia.)

   POR QUÉ UNA SIGLA Y NO EL RÓTULO. Esa columna es una rejilla de días ×
   colaboradores donde la MAYORÍA de las filas no tiene nada: lo que marca es la
   excepción. Con el rótulo entero («Falta sugerida», «Descanso laborado»), la
   columna reserva 190px para escribir en pocas filas la misma palabra, y el
   ojo tiene que leerla para saber que hay algo. Una sigla se ve sin leerse, y
   el rótulo completo sigue estando: lo dice el tooltip al pasar el puntero.
   Esto NO contradice la regla de §Badges («en una fila, el estado va con punto
   y no con chip»): lo que aquella prohíbe es el CHIP DE TEXTO que tiñe media
   tabla; ésta es una caja de 24px de alto que además es el control de la fila.

   SE LLAMA SIGLA Y NO «INICIAL» porque no siempre es una letra: nació así el
   mismo día y Arif corrigió el mapa en el acto. «Sin entrada» y «Sin salida»
   empiezan las dos con S, y resolverlo con la letra del evento que falta (E, S)
   pedía saberse una convención; con DOS letras la sigla se lee sola. Quedan
   F falta · R retardo · SE sin entrada · SS sin salida · DL descanso laborado.
   El mapa vive en `asistencia.php`, que es quien conoce los tipos. La caja
   CRECE con su texto —`min-width` de 24 y relleno lateral— para que la de una
   letra siga siendo cuadrada y la de dos no apriete.

   DASHED = TODAVÍA NO ES UN HECHO. La propuesta la calcula el sistema y nadie
   la ha confirmado: se dibuja con el filo roto y SIN relleno. Confirmarla la
   RELLENA con el tinte de aviso y cierra el filo. Es la misma caja, la misma
   letra y el mismo sitio —el borde de 2px sigue ahí, transparente—, así que al
   confirmar nada se mueve: sólo cambia el tratamiento. El par
   punteado/relleno es el que el catálogo no tenía cuando se decidió esto el
   2026-09-16 y se resolvió entonces con la palabra «sugerida»; la palabra
   sobrevive en el tooltip, que es donde cabe.

   `line-height:1` DECLARADO, y no es cosmética: `.fila > *` impone
   `line-height:35px` para centrar el texto de la celda, y una pieza que no
   declara el suyo lo hereda y se deforma dentro de una fila. Es exactamente lo
   que le pasaba al `.badge` que vivía aquí: 39px de alto en una fila de 36. */
.sigla{
  display:inline-flex;align-items:center;justify-content:center;position:relative;
  min-width:24px;height:24px;flex:none;padding:0 5px;
  box-sizing:border-box;border:2px solid transparent;border-radius:var(--r-ctl);
  background:var(--badge-warning);color:var(--app-fg);
  font-size:var(--fs-meta);font-weight:600;line-height:1;
  /* La marca SÍ se pulsa: en esta sábana el gesto vive aquí y en la casilla,
     no en la fila (ver `.fila-sin-clic`). */
  cursor:pointer;
}
/* LA SUGERIDA VA ÁMBAR ENTERA —filo y letra— y la confirmada conserva la tinta
   del cuerpo (2026-09-20, experimento que pidió Arif; medido sobre papel y
   sobre el relleno, en los dos temas). El porqué no es el contraste: las tres
   variantes que se probaron pasan AA. Es que así cambian DOS canales al
   confirmar —se cierra el filo, se rellena la caja y la letra pasa a tinta de
   dato— en vez de uno, y cada estado se lee por lo que es: la propuesta es toda
   del sistema avisando, el hecho es un dato. Pintar de ámbar también la
   confirmada (la tercera variante) las dejaba distinguidas sólo por el relleno,
   y además es el peor par medido: 4.62:1 en claro contra 15.37:1 de ésta. */
.sigla.sugerida{background:transparent;border-style:dashed;border-color:var(--app-atencion-text);color:var(--app-atencion-text)}

/* LA FIJA: lo que la ley ya resolvió y nadie decide (2026-09-21). DF es el
   festivo del artículo 74 sin checadas y FL el festivo trabajado, que la
   prenómina paga sola. Filo continuo y SIN relleno —no es un hecho que alguien
   confirmó, pero tampoco una propuesta que espera— y la flecha de siempre: no
   se pulsa, así que no promete un gesto. La letra en tinta secundaria para que
   el ojo no la cuente entre las incidencias de la semana. */
.sigla.fija{background:transparent;border-color:var(--app-borde);color:var(--app-fg-secondary);cursor:default}

/* EL TOOLTIP ES EL DE LA BARRA, con otro ancla. Mismos tokens (`--tip-*`),
   misma sombra, mismos 90ms y el mismo `data-tip` en vez de `title` —con
   `title` el navegador pintaría ADEMÁS el suyo, un segundo después y encima—.
   Lo que cambia es de dónde cuelga: el de la barra se fija a `--sb-w` porque
   la barra está pegada al borde; éste vive en cualquier celda, así que se
   ancla a la marca. El nombre accesible lo sostiene `aria-label`.

   SE ABRE HACIA LA IZQUIERDA, y no es estética: la marca vive en la ÚLTIMA
   columna de la sábana. Abriéndose a la derecha, la cápsula sale de la caja de
   `.sabana-viewport`, que es `overflow:auto` — o sea que la recortaría o, peor,
   haría aparecer una barra horizontal al pasar el puntero. Hacia la izquierda
   cae sobre las celdas vecinas, que es justo donde hay sitio vacío, y encima de
   ellas: es la última hermana del renglón y además lleva `z-index`. */
.sigla::after{
  content:attr(data-tip);
  position:absolute;right:calc(100% + 8px);top:50%;
  transform:translateY(-50%) translateX(4px);
  padding:4px 9px;
  background:var(--tip-bg);color:var(--tip-fg);
  font-family:var(--fuente-titulo);font-size:var(--fs-meta);line-height:1.25;
  white-space:nowrap;border-radius:var(--r-caja);
  box-shadow:0 6px 18px var(--app-shadow), inset 0 0 0 1px var(--tip-canto);
  opacity:0;pointer-events:none;z-index:60;
  transition:opacity 90ms var(--app-ease),transform 90ms var(--app-ease);
}
.sigla:hover::after{opacity:1;transform:translateY(-50%) translateX(0)}

/* La celda que aloja una marca NO recorta: `.fila > *` lleva `overflow:hidden`
   para el ellipsis de su texto, y el tooltip sale de la caja de la marca. Aquí
   no hay texto que recortar, así que no se pierde nada. */
.fila > .col-marca{overflow:visible}

/* LA FILA QUE NO SE PULSA (2026-09-20, pedido de Arif para la grilla de
   asistencia). `.fila` nace con `cursor:pointer` porque en las sábanas de la
   casa la fila ENTERA abre su peek. En la grilla de asistencia no: lo que actúa
   es la casilla —que conserva el suyo— y la marca de incidencia. Ofrecer la
   mano sobre toda la fila prometía un gesto que sólo dos puntos cumplen. Es un
   modificador de la sábana y no un cambio de `.fila`: las demás siguen igual. */
.fila-sin-clic .fila{cursor:default}

/* ── La tarjeta del inicio ────────────────────────────────────────────
   El KPI de portada. Va SUELTA sobre el papel — nunca dentro de una
   `.seccion`: es una pieza de primer nivel y su jerarquía la pone la cifra,
   no la caja. Fondo transparente, filete
   hairline, radio de tarjeta (8), tres por fila.

   TODA la tarjeta navega — no lleva «abrir →» ni pie con enlace: la caja
   entera es el destino, como la fila de una tabla. De ahí su hover: el MISMO
   `--app-row-hover` de la fila, el gesto que la app ya enseñó para «esto
   entero se pulsa» (decisión de Arif, 2026-08-27) — no el filo de un
   control. La ÚNICA que no navega es la que lleva gráfica, y el porqué
   está abajo, en `.tarjeta-grafica`.

   Tres variantes sobre un macro único (cabeza con icono a 28px en el tinte
   info + título, cifra, sub): cifra sola, cifra con `.badge` — EL badge de
   la casa, radio 8 y texto del cuerpo, no un pill nuevo — o cifra con curva
   de tendencia. La curva la dibuja el markup (SVG inline con
   `preserveAspectRatio="none"` y `vector-effect="non-scaling-stroke"`); la
   clase sólo le da sitio, tope y color, y es FLEXIBLE hacia abajo para que
   una cifra larga ($2,371,946.81) mande sobre ella y no la empuje fuera de
   la caja. Nació del canvas de diseño del 2026-08-27: de cinco versiones
   quedaron la 2, 3 y 4, unificadas en esto. Muestra viva: ui.php §Tarjetas. */
/* La rejilla de MUESTRA del catálogo (sólo ui.php; la app reparte con
   `.portada`). Su hueco copia el de la portada —14 / 14 desde el 2026-08-30,
   antes 20 / 14— para que la muestra enseñe el ritmo real: una maqueta con
   otro aire del que tiene la pantalla es una maqueta que hay que corregir
   mentalmente cada vez. Esta línea es la que hay que mover con la de allá; el
   catálogo no lo puede detectar solo. */
.tarjeta{
  display:flex;flex-direction:column;gap:10px;
  padding:16px 18px;
  background:transparent;border:1px solid var(--app-borde);border-radius:var(--r-ctl);
  text-decoration:none;color:inherit;
}
a.tarjeta:hover{background:var(--app-row-hover)}
.tarjeta-cab{display:flex;align-items:center;gap:10px}
.tarjeta-icono{
  width:28px;height:28px;flex:none;border-radius:var(--r-ctl);
  background:var(--badge-info);color:var(--app-info-text);
  display:flex;align-items:center;justify-content:center;
}
.tarjeta-icono svg{display:block}
.tarjeta-t{font-size:var(--fs-base);font-weight:500;color:var(--app-fg)}
.tarjeta-medida{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}
.tarjeta-n{font-size:var(--fs-titulo);font-weight:600;color:var(--app-fg);font-variant-numeric:tabular-nums;line-height:1.1}
.tarjeta-sub{font-size:var(--fs-meta);color:var(--app-fg-tertiary)}
/* EL DELTA DEL KPI vive en la MISMA línea del importe desde el 2026-09-03
   (pedido de Arif): es el calificador de la cifra —«-49.6% vs ago» no se
   entiende sin el «$3,470» que califica— y en renglón aparte obligaba a leer
   dos líneas para una idea. `.tarjeta-medida` ya alinea por baseline, así que
   entra sin tocar el reparto.

   Y NO LLEVA COLOR PROPIO: los tonos los pone el `.glifo` que lo envuelve
   cuando la métrica tiene dirección (success / warning), y esta clase es el
   caso sin dirección — texto muted a secas. Aquí vivían `.tarjeta-sub .bien` y
   `.tarjeta-sub .mal`, las dos retiradas el mismo día: `.mal` pintaba en
   `--app-peligro-text` una caída contra el mes previo, y eso decía «falla»
   donde sólo hay información. La columna 3 era su único lector. */
.tarjeta-delta{font-size:var(--fs-meta);color:var(--app-fg-tertiary)}
/* El glifo dentro de la medida: `align-items:baseline` del padre alinearía la
   caja flex del glifo por la baseline de su primer hijo —el punto, que no
   tiene—, así que se ancla al centro y se le baja el cuerpo al de la meta. */
.tarjeta-medida .glifo{font-size:var(--fs-meta);align-self:center}
/* LA CHISPA CIERRA LA TARJETA (2026-09-03). Era hija de `.tarjeta-medida` y sus
   reglas eran de esa fila: `margin-left:auto` para empujarla al canto derecho y
   `max-width:96px` para no comerse la cifra. Fuera de ahí ninguna de las dos
   significa nada — ahora es la última hija de `.tarjeta` (flex column), toma el
   ancho entero y el `margin-top:auto` la pega al fondo, que es lo que mantiene
   las cuatro chispas de la columna alineadas aunque una tarjeta lleve delta y
   otra no. */
.tarjeta-tendencia{margin-top:auto;color:var(--app-info)}
.tarjeta-tendencia svg{display:block;width:100%;height:32px}
/* En warning: el MISMO vocabulario de estado del badge y la alerta, nada
   propio. El chip cambia a la pareja del ámbar, y una curva en warning va en
   el `-text` y no en el ámbar puro: un gráfico al tinte sobre papel da ~1.6:1
   — la misma aritmética que obligó al punto del badge a usar el token de
   texto.
   EL CHIP DEL ICONO NO SE TIÑE CON EL `tono` DE SU GRÁFICA (movido del
   catálogo `ui.php`, 2026-09-19): se queda en el teal informativo de toda
   tarjeta, y sólo el ámbar de `warning` lo mueve; si cada tarjeta tiñera su
   chip, el ámbar dejaría de querer decir algo. Las dos tarjetas del flujo
   (ingreso y egreso) comparten el glifo `icn-billete` porque salen de la misma
   tabla: las distingue el tono de sus barras y su rótulo. */
.tarjeta.warning .tarjeta-icono{background:var(--badge-warning);color:var(--app-atencion-text)}
/* LA CHISPA EN WARNING IGUALA A LAS BARRAS DE GASTO DE CONTROLY (2026-08-29,
   pedido de Arif, y corrección suya: la primera versión usaba
   `--app-atencion-text` y no se parecía en nada). El estándar de la rueda
   (`controly-charts` → `color-palette.md`) es amarillo saturado con el área al
   0.3: `rgb(255,193,7)` de borde sobre `rgba(255,193,7,.3)` de relleno, y el
   amarillo está reservado ahí al gasto operativo igual que el teal a lo que
   entra.

   NO HIZO FALTA IMPORTAR EL COLOR: `--app-atencion` (#ffcc00) es prácticamente
   el mismo amarillo que el #ffc107 de Controly, así que se iguala con el token
   que la casa ya tiene y la chispa sigue cambiando de tema sola (#ffd60a en
   oscuro). Un color suelto habría roto la regla del proyecto por una diferencia
   de tono invisible.

   ES EL TINTE PURO Y NO EL `-text`, a diferencia de todo lo demás que dibuja en
   esta app, y es una excepción DECLARADA: un trazo al tinte sobre papel da
   ~1.6:1, muy por debajo de lo que se le exige a un texto. Se acepta porque
   aquí no hay texto que leer —es la silueta de una tendencia, con 2px de trazo
   y un área del 0.3 debajo que le da la masa— y porque el criterio es igualar
   una pieza que ya existe y que el dueño mira a diario en la otra app. El chip
   del icono NO cambia: ése sí lleva un glifo dentro y se queda en el `-text`. */
.tarjeta.warning .tarjeta-tendencia{color:var(--app-atencion)}
.tarjeta.warning .tarjeta-tendencia .chispa-area{opacity:.3}

/* EL IMPORTE NO SE LEE COMO UN CONTEO, y por eso no mide como uno (pedido de
   Arif, 2026-08-29). `$12,315.50` a 26px se come el ancho de su columna —a 1440
   la portada reparte 250px— y deja a la chispa en su mínimo de 28px; un importe
   de siete cifras la aplasta del todo. Baja a `--fs-dialogo` (15) y GANA la
   fuente de título, que es lo que le devuelve el rango que pierde en tamaño:
   sigue siendo la cifra de la tarjeta, dicha en la voz de los títulos.

   Es CLASE y no `.tarjeta-medida .tarjeta-n`: por ahí también pasa un conteo
   con badge («Nuevos · 7 días», 32), que sí es un número corto y se queda
   grande. Lo que decide es el tipo de dato, no el envoltorio. */
.tarjeta-n.importe{font-size:var(--fs-dialogo);font-family:var(--fuente-titulo)}

/* LA TARJETA QUE LLEVA GRÁFICA (2026-08-27, el inicio) es la ÚNICA que no
   navega, y por eso su marcado es un `div` y no un `a`: el canvas ya tiene su
   propio gesto —posarse encima dice el importe exacto— y un enlace alrededor
   se lo come. Es la excepción a «toda la tarjeta navega» y se paga barato:
   una gráfica es lo que MENOS necesita un destino, porque ya está enseñando
   lo que ese destino iría a contar.

   ALTO FIJO, igual que `.lado-grafica` y por lo mismo: el canvas se estira al
   ancho y sin un tope crecería en la misma proporción, empujando fuera lo que
   venga debajo.

   112 y no los 150 con los que nació (Arif, 2026-08-27, el mismo día): el alto
   se pidió cuando el eje desperdiciaba casi un tercio de su área en aire —la
   librería estiraba el techo a 10,000 para un pico de 6,880— y la altura
   estaba tapando ese defecto en vez de arreglarlo. Con el techo ceñido al dato
   (ver `pasoBonito` en grafica.js) las barras usan todo lo que hay, así que la
   caja puede bajar un 25% y la serie se lee MEJOR que antes. Es el orden
   correcto de las dos correcciones: primero el eje, después la caja.

   SE PROBÓ BAJARLO MÁS EL 2026-08-29 Y SE DESCARTÓ (Arif: «no me agradaron»).
   Se deja medido para que nadie lo repita a ciegas, y porque las dos cifras
   sirven igual con el alto que quedó:

     · Este número es el del LIENZO, no el de la tarjeta. La tarjeta son 72px de
       cromo fijo —cabecera, relleno— más el lienzo, así que un −20% aplicado
       AQUÍ sólo le recorta un 12% (184 → 162). Para bajar la tarjeta un 20% el
       lienzo tendría que ir a `184 * .8 - 72 = 75`. Cualquier ajuste futuro se
       calcula así, no sobre el número escrito abajo.
     · Por debajo de ~100px Chart.js deja de dibujar las cuatro líneas del eje y
       se queda con dos ($0 y el techo). Ése fue el precio que no compensó.

   Lo que la prueba sí dejó en firme: con TRES gráficas en la columna 2 la
   tercera desborda 36px la primera pantalla a 1440×743. Es holgura de scroll,
   no una pieza rota — pero es la razón por la que este alto se vuelve a mirar
   cada vez que la columna gana una gráfica.

   `position:relative` es requisito de Chart.js con `maintainAspectRatio:false`,
   no decoración. */
.tarjeta-grafica{position:relative;height:112px}

/* ── Controles ────────────────────────────────────────────────────────
   UN tratamiento para input, select y textarea, aplicado sobre las clases que
   ya existen en vez de renombrarlas: cero cambios de marcado, cero riesgo de
   dejarse una. Si aparece un control nuevo, se le pone `.ctl` y ya.

   Qué cambió respecto de lo que había: eran once variantes de "campo donde se
   teclea o se elige", con tres radios, seis paddings y DOS lenguajes de foco
   —cuatro dibujaban un anillo interior de 1.5px y siete no declaraban nada,
   así que heredaban el `outline` exterior de 2px del `:focus-visible` global.
   Ese era el «a veces gris, a veces azul».

   MISMO ALTO QUE LOS BOTONES (27px), y derivado de ellos, no copiado: el
   padding sale de `--btn-py` menos el píxel de borde, y el interlineado es el
   mismo `--btn-lh`. Si algún día el botón cambia de alto, el control lo sigue
   solo. Un pie con un select y dos botones tiene que leerse como una fila,
   no como tres alturas. */
.ctl,
.campo input,
.campo select,
.campo textarea,
.fx-ctl,
.dlg-area,
.mv-input,
.buscador input,
.dlg-sel{
  background:var(--ctl-bg);
  border:1px solid var(--ctl-filo);
  border-radius:var(--r-ctl);
  padding:calc(var(--btn-py) - 1px) 10px;
  line-height:var(--btn-lh);
  /* var(--fuente) y no `inherit`: un control puede vivir dentro de un menú
     (mv-input) y el menú habla Space Grotesk — lo que se teclea es dato. */
  font-family:var(--fuente);font-size:var(--fs-base);color:var(--app-fg);
  outline:none;box-sizing:border-box;
  transition:border-color var(--app-dur) var(--app-ease),
             box-shadow var(--app-dur) var(--app-ease);
}
/* Al pasar el cursor el filo se marca: el campo dice que se puede tocar sin
   tener que iluminarse entero. */
.ctl:hover,.campo input:hover,.campo select:hover,.campo textarea:hover,.fx-ctl:hover,.dlg-area:hover,.mv-input:hover,.buscador input:hover,.dlg-sel:hover{border-color:var(--ctl-filo-hover)}

/* UN solo foco en todo el sistema, y es EL DE BOOTSTRAP 5 clonado tal cual:
   el filo se vuelve #86b7fe y sale un anillo de 4px (= sus .25rem) en
   rgba(13,110,253,.25). El foco sólo dice dónde estás; no promete una
   escritura. El `outline` exterior de 2px no vuelve a aparecer en un control. */
.ctl:focus,.campo input:focus,.campo select:focus,.campo textarea:focus,.fx-ctl:focus,.dlg-area:focus,.mv-input:focus,.buscador input:focus,.dlg-sel:focus{
  border-color:var(--ctl-foco);
  box-shadow:0 0 0 4px var(--ctl-halo);
}
/* ── EL INTERRUPTOR — `.sw` (2026-09-03) ──────────────────────────────────
   El control de DOS estados que se conmuta en el sitio, sin pregunta y sin
   Guardar. Nació para el umbral del contacto (verificado / no verificado) y se
   escribe como pieza del sistema porque el caso se repite: cualquier bandera
   de una fila que hoy sea un botón que dice el verbo («Verificar») es esto.

   POR QUÉ NO ES UN `.cf-btn` MÁS. Un botón dice lo que VA A PASAR y nunca lo
   que ES: «Verificar» obliga a deducir el estado por la ausencia del gesto, y
   se lee al revés en cuanto el estado ya está puesto —de ahí que ese botón
   tuviera que esconderse con `hidden`, que es el síntoma—. El interruptor dice
   el estado con su posición y el gesto con su afordancia, a la vez. Es además
   el único control de la casa cuyo cambio se ve SIN releer el texto: la
   perilla se mueve.

   POR QUÉ ES UN `<button role="switch">` Y NO UN `input[type=checkbox]`.
   El mismo argumento de `.caja`: aquí nada se envía —cada conmutación escribe
   sola contra su modelo—, así que un input dentro de ningún formulario es una
   promesa falsa. Y `role="switch"` es lo que hace que el lector de pantalla
   diga «activado / desactivado» en vez de «casilla marcada», que es
   exactamente la diferencia entre las dos piezas. El `<button>` regala el
   teclado (Espacio y Enter) y el `:focus-visible` del sistema; el estado vive
   en `aria-checked`, no en una clase, para que el atributo accesible y el
   dibujo NO PUEDAN divergir — CSS lee el mismo atributo que el lector.

   LA MEDIDA. Pista de 34×20 y perilla de 14 con 3px de aire: la perilla
   recorre 14px, que es el desplazamiento mínimo que se lee como movimiento y
   no como parpadeo. Los 20 de alto caben dentro del átomo de control (27) sin
   levantar la fila, así que un interruptor en un renglón de meta no lo empuja.

   LOS DOS ESTADOS HABLAN EL IDIOMA DE `.caja`, que es la otra pieza con
   estado propio: apagado = el mismo anillo de 1.5px en `--app-check` que el
   checkbox, sobre relleno tenue; encendido = accent sólido con la perilla en
   blanco. Y el accent está bien empleado, no es adorno: conmutar ESCRIBE en la
   base, que es lo único que el azul significa en este sistema.

   LA PERILLA APAGADA VA EN `--app-fg-tertiary`, Y ESO SE MIDIÓ. El primer
   intento la puso en `--app-check` —la tinta del checkbox— y daba **1.95:1
   contra su propia pista**: el apagado se leía como una píldora vacía, sin
   perilla. Con el terciario pasa en los dos temas, sobre el mínimo de 3:1 que
   WCAG 1.4.11 pide a lo que identifica un control y su estado. Es además la
   respuesta de Material 3, cuyo handle apagado va en su color `outline`, no en
   un gris de relleno.

   MEDIDO SOBRE `--app-paper`, que es donde vive (el peek) — claro / oscuro:

     perilla apagada vs su pista   4.96 / 3.99      ✓ ambas sobre 3:1
     perilla encendida vs su pista 5.30 / 4.67      ✓
     pista encendida vs el papel   5.30 / 3.14      ✓
     las dos pistas entre sí       4.94 / 2.70
     anillo apagado vs el papel    1.99 / 2.84

   LAS DOS ÚLTIMAS FILAS NO PASAN Y ES DELIBERADO, no un olvido. **Quien carga
   con el estado es la POSICIÓN de la perilla**, y ésa es la que tiene que pasar
   el umbral — la pasa en los dos temas y por los dos lados. El tono de la pista
   es refuerzo: ayuda en claro (4.94) y en oscuro apenas (2.70), así que el
   interruptor no puede depender de él y no depende. Y el anillo sólo dice hasta
   dónde llega el recorrido; subirlo a terciario dibujaría un apagado más pesado
   que el encendido, además de separar esta pieza del checkbox, que usa
   exactamente esa tinta a ese grosor para el mismo trabajo. Si un día el anillo
   del checkbox sube, éste sube con él.

   NUNCA VA SOLO. Un interruptor sin texto al lado no dice qué conmuta; el
   rótulo lo pone quien lo usa y **nombra el estado ENCENDIDO** («Verificado»),
   no el gesto — si el rótulo cambia con el estado, la posición y la palabra
   dicen lo mismo dos veces y una de las dos sobra. */
.sw{
  flex:none;position:relative;display:inline-block;
  width:34px;height:20px;padding:0;border:0;
  border-radius:var(--r-pill);cursor:pointer;
  background:var(--app-relleno-tenue);
  box-shadow:inset 0 0 0 1.5px var(--app-check);
  transition:background var(--app-dur) var(--app-ease),
             box-shadow var(--app-dur) var(--app-ease);
}
/* El área de clic se amplía por fuera del flujo, como en `.caja`: la pieza mide
   20px de alto y el blanco cómodo son 30. No ocupa espacio ni desplaza nada. */
.sw::before{content:'';position:absolute;inset:-5px -3px}
.sw-perilla{
  position:absolute;top:3px;left:3px;width:14px;height:14px;
  border-radius:50%;background:var(--app-fg-tertiary);
  transition:transform var(--app-dur) var(--app-ease),
             background var(--app-dur) var(--app-ease);
}
.sw:hover{background:var(--app-chip)}
/* EL ESTADO SE LEE DEL ATRIBUTO ACCESIBLE, no de una clase paralela. */
.sw[aria-checked="true"],
.sw[aria-checked="true"]:hover{background:var(--app-accent-solido);box-shadow:none}
.sw[aria-checked="true"] .sw-perilla{transform:translateX(14px);background:var(--app-accent-fg)}
/* Mientras la escritura vuela: el control no se puede volver a pulsar y lo
   dice. No se BLOQUEA el dibujo a medias —eso dejaría la perilla en el estado
   viejo mientras el servidor ya tiene el nuevo—: se conmuta optimista y se
   atenúa; si el modelo falla, quien lo llama repinta con lo que respondió. */
.sw[disabled]{opacity:.55;cursor:default}
/* El movimiento es la mitad del mensaje, así que sin él la perilla SALTA — que
   sigue diciendo el estado. Lo que no se hace es dejarla quieta. */
@media (prefers-reduced-motion:reduce){.sw,.sw-perilla{transition:none}}

/* ── EL SELECTOR DE TEMA (`.tema-sel`, 2026-09-11) ─────────────────────────
   Tres iconos en una cápsula —Sistema, claro, oscuro— y uno marcado. Vive en
   `cuenta.php` → Apariencia y es su único consumidor.

   ── POR QUÉ NO ES `.sw` NI ES `.radio` ────────────────────────────────────
   El interruptor dice SÍ o NO y aquí hay tres estados, de los cuales «Sistema»
   no es ni uno ni otro: es *no elegir*. Y `.radio` es la pieza de un
   FORMULARIO —marca redonda + rótulo, en columna, con su `<input>` real porque
   se envía—, mientras que esto surte efecto al soltarlo y no se envía a ningún
   sitio. La prueba del catálogo separa las dos familias por eso mismo (ui.php
   → Controles); lo que faltaba era la forma de «una de tres que se aplica al
   instante», y ésta es.

   ── POR QUÉ SE LLAMA `.tema-*` Y NO `.seg` ────────────────────────────────
   Porque hoy tiene UN consumidor, y la regla de esta casa es que el nombre
   genérico se gana con el segundo: `.btn-circulo` existió sin nombre dentro de
   `.peek-nav .flecha` hasta que apareció el ⋮ del tablero, y `.au-grupo` perdió
   su prefijo el día que `funnels.php` lo pidió. Si nace un segundo segmentado
   —un «día / semana / mes», por ejemplo— esta receta sube a `.seg` y el tema
   pasa a ser su primer consumidor. Nombrarlo genérico hoy sería prometer una
   generalidad que nadie ha probado.

   LA CÁPSULA ES LA DEL INTERRUPTOR, literal: mismo relleno tenue, mismo filo
   interior de 1.5px y el mismo azul sólido para lo elegido. No es ahorro de
   líneas — es que «lo que está puesto» tiene UN color en esta app, y un
   segmentado con su propio tono inventaría un segundo vocabulario para decir lo
   mismo. El icono de la opción elegida pasa a `--app-accent-fg` por lo mismo
   que la perilla.

   EL ICONO VA EN `::before` CON MÁSCARA y no como `.icn` dentro del botón: el
   botón no lleva texto, así que un `<span>` sería marcado que sólo existe para
   colgar un fondo — el mismo camino que ya toma `.sb-tema::before`.

   LOS TRES ICONOS VAN SIN RÓTULO, CON `title` + `aria-label` (movido del
   catálogo `ui.php`, 2026-09-19). El `title` es correcto aquí aunque la barra
   lateral lo evite: allá compite con el tooltip propio de la casa (`::after`),
   que en el panel no existe, y montarlo para tres botones sería una segunda
   implementación del mismo gesto. Es la misma excepción que `.sw[disabled]`
   usa para decir su motivo. */
/* ── Y SUBIÓ A `.seg` EL 2026-09-25 ──
   El segundo segmentado llegó: «Todos / Espera respuesta» en la bandeja de
   conversaciones. La cápsula es una sola receta para los dos; lo propio de
   `.seg-op` es que lleva PALABRA y no icono (ancho por su texto, letra de meta)
   y que lo elegido NO va en azul sólido sino en el teal claro de los badges
   (`--badge-info`) con la letra del cuerpo (pedido de Arif el mismo día): un
   filtro no escribe nada, y así se lee como el resto de lo elegido en la
   bandeja. `.tema-*` conserva su nombre y su azul porque su marcado y
   `window.TEMA` lo usan; comparte la cápsula en vez de copiarla. */
.tema-sel,.seg{
  flex:none;display:inline-flex;align-items:center;gap:2px;
  padding:2px;border-radius:var(--r-pill);
  background:var(--app-relleno-tenue);
  box-shadow:inset 0 0 0 1.5px var(--app-check);
}
.seg-op{
  height:24px;padding:0 12px;flex:none;border:0;
  background:transparent;border-radius:var(--r-pill);cursor:pointer;
  font:inherit;font-size:var(--fs-meta);color:var(--app-fg-secondary);white-space:nowrap;
  transition:background var(--app-dur) var(--app-ease),color var(--app-dur) var(--app-ease);
}
.seg-op:hover{color:var(--app-fg)}
.seg-op[aria-checked="true"]{background:var(--badge-info);color:var(--app-fg)}
.seg-op:focus-visible{outline:2px solid var(--app-accent);outline-offset:2px}
.tema-op{
  width:26px;height:22px;flex:none;padding:0;border:0;
  background:transparent;border-radius:var(--r-pill);cursor:pointer;
  display:flex;align-items:center;justify-content:center;
  transition:background var(--app-dur) var(--app-ease);
}
.tema-op::before{
  content:'';width:var(--icn-texto);height:var(--icn-texto);flex:none;
  background:var(--app-fg-tertiary);
  -webkit-mask:var(--icn) no-repeat center / contain;
          mask:var(--icn) no-repeat center / contain;
  transition:background var(--app-dur) var(--app-ease);
}
.tema-op[data-tema="sistema"]{--icn:var(--icn-pantalla)}
.tema-op[data-tema="light"]{--icn:var(--icn-sol)}
.tema-op[data-tema="dark"]{--icn:var(--icn-luna)}
.tema-op:hover::before{background:var(--app-fg-secondary)}
/* EL ESTADO SE LEE DEL ATRIBUTO ACCESIBLE, no de una clase paralela — la misma
   línea que `.sw` ya tenía escrita, y por el mismo motivo. */
.tema-op[aria-checked="true"]{background:var(--app-accent-solido)}
.tema-op[aria-checked="true"]::before{background:var(--app-accent-fg)}
.tema-op:focus-visible{outline:2px solid var(--app-accent);outline-offset:2px}
@media (prefers-reduced-motion:reduce){.tema-op,.tema-op::before{transition:none}}

/* ── EL RADIO: elegir UNA de dos o tres ramas de un formulario (2026-09-04) ──
   Nació con el alta manual de citas, que tiene que preguntar si el contacto ya
   existe o se captura nuevo — y las dos ramas piden campos distintos, así que
   la pregunta no puede vivir dentro de un `<select>`: lo que cambia no es un
   valor, es QUÉ se llena debajo.

   ── POR QUÉ ES UNA PIEZA NUEVA Y NO SE RESOLVIÓ CON LO QUE HABÍA ──
   El catálogo tenía la CONVENCIÓN escrita pero no el control: `.op-estado
   .exclusiva` ya dice —y aplica— que «cuadrado se marca y se desmarca, redondo
   se elige», sólo que es una opción de MENÚ flotante (`role="radio"` sobre un
   `<span>`, con su propio fondo, su padding de opción y su badge de conteo).
   Meter eso en un formulario habría traído el aspecto de un menú a un campo.
   Y un `<select>` de dos opciones esconde la segunda rama detrás de un
   desplegable: con dos, las dos se enseñan.

   ── EL `<input type="radio">` REAL SIGUE AHÍ ──
   Igual que el combo no sustituye a su `<select>`: el input está presente y sólo
   se saca del dibujo (`clip-path`, no `display:none` ni `visibility:hidden`,
   que lo sacarían del orden de tabulación). Con eso salen gratis y sin una línea
   de JS el grupo por `name`, las flechas ↑↓ ←→ entre opciones, el clic sobre el
   texto —lo da el `<label>` que envuelve—, el `:checked` que pinta la marca y
   el envío del valor. Lo único dibujado es `.radio-marca`.

   ── LA TINTA ES LA DEL CHECKBOX, NO LA DEL MENÚ ──
   1.5px de `--app-check`, exactamente lo que usa `.caja`: en un formulario los
   dos controles se ven juntos y un anillo más pesado que la casilla de al lado
   se lee como dos familias. El punto puesto va en `--app-accent-solido` y no en
   `--app-fg` como el del menú, por la regla del proyecto de que el azul
   significa «esto escribe en la base» — aquí la elección decide qué se guarda.
   `.caja` se rellena entero al marcarse y éste conserva su anillo: el relleno
   es lo que sostiene la palomita, y un punto sobre fondo sólido no se vería.

   MEDIDO SOBRE `--app-paper` (el diálogo, que es donde vive) — claro / oscuro:

     anillo puesto vs el papel      5.30 / 3.14      ✓ ambas sobre 3:1
     el punto vs el papel           5.30 / 3.14      ✓
     el rótulo vs el papel         17.00 / 12.46     ✓
     anillo APAGADO vs el papel     2.48 / 2.84      ✗

   LA ÚLTIMA NO PASA Y SE ACEPTA, con el mismo argumento que `.sw` deja escrito
   para su anillo apagado y una condición que el interruptor no tiene: un grupo
   de radios lleva SIEMPRE una opción puesta, así que lo que carga con el estado
   —el punto azul— pasa el umbral en los dos temas, y la opción sin marcar la
   nombra su rótulo, que está pegado, es pulsable y va a 12.46 en oscuro. Subir
   sólo este anillo dibujaría un apagado más pesado que el encendido y separaría
   la pieza del checkbox, que usa esa misma tinta a ese mismo grosor para el
   mismo trabajo. **Si un día el anillo del checkbox sube, éste sube con él** —
   la frase es de `.sw` y aquí se hereda al pie de la letra, que es lo que hace
   que las tres piezas sigan siendo una familia y no tres decisiones sueltas.

   NUNCA VA SOLO NI VA CON UNA SOLA OPCIÓN: un grupo de un radio es una casilla
   disfrazada que además no se puede desmarcar. Y el grupo lleva su rótulo, que
   es el `<label>` del `.campo` que lo contiene. Con CUATRO O MÁS opciones que
   no abran campos distintos vuelve a ser un `<select>`: cuatro anillos
   apilados son una lista larga que ocupa el alto de un formulario entero
   (movido del catálogo `ui.php`, 2026-09-19). */
/* UNA SOLA COLUMNA desde el 2026-09-21, por pedido de Arif al verlo en el
   diálogo de sueldo: tres grupos «Calculado por el sistema / Personalizado»
   seguidos, en fila, se leían como una sola línea de seis opciones. Es la
   misma decisión y el mismo `gap` de 6 que `.dlg-checks`: las dos piezas del
   formulario que agrupan opciones se apilan igual. Nació como `flex-wrap`
   para «Existente / Nuevo» de la cita, que también queda apilado. */
.radios{display:grid;grid-template-columns:1fr;justify-items:start;gap:6px}
/* `position:relative` para que el input escondido de abajo se ancle a SU
   etiqueta y no a lo que haya positioned más arriba en el árbol. */
.radio{position:relative;display:inline-flex;align-items:center;gap:7px;cursor:pointer;min-width:0}
/* Fuera del dibujo pero DENTRO del foco y del orden de tabulación.

   `input[type="radio"]` Y NO `input` A SECAS, y es la misma trampa que
   `textarea.dlg-area` documenta más abajo: dentro de un `.campo`, el
   `width:100%` del tratamiento común (`.campo input,.campo select,.campo
   textarea`) empata en peso (0,1,1) y **gana por orden**, porque ese bloque
   vive ~950 líneas más abajo. Medido en pantalla antes de corregirlo: los dos
   radios ocupaban 440px dentro de un cuerpo de 434 y le sacaban al diálogo una
   barra de scroll HORIZONTAL — invisibles, pero empujando. Con el atributo
   delante sube a (0,2,1) y el orden deja de decidir. */
.radio input[type="radio"]{position:absolute;width:15px;height:15px;margin:0;opacity:0;clip-path:inset(50%)}
.radio-marca{
  flex:none;position:relative;width:15px;height:15px;box-sizing:border-box;
  border:1.5px solid var(--app-check);border-radius:50%;
  display:flex;align-items:center;justify-content:center;
}
/* El área de clic la da el `<label>` entero, así que aquí no hace falta el
   `::before` ampliado que `.caja` necesita por vivir en una columna de 15px. */
.radio:hover .radio-marca{border-color:var(--ctl-borde-hover)}
.radio input[type="radio"]:checked + .radio-marca{border-color:var(--app-accent-solido)}
.radio input[type="radio"]:checked + .radio-marca::after{
  content:'';width:7px;height:7px;border-radius:50%;background:var(--app-accent-solido);
}
/* El foco se dibuja sobre la MARCA porque el input no se ve. Mismo anillo que
   el resto de los controles del sistema. */
.radio input[type="radio"]:focus-visible + .radio-marca{
  outline:2px solid var(--app-accent);outline-offset:2px;
}
.radio input[type="radio"][disabled] + .radio-marca{opacity:.55}
.radio input[type="radio"][disabled] ~ *{opacity:.55}
.radio:has(input[type="radio"][disabled]){cursor:default}

/* ── EL COMBO: un `<select>` que se puede teclear (2026-08-18) ────────────
   La pieza que se construyó en vez de meter Select2 (el porqué, medido, vive
   en la cabecera de `activos/combo.js`). El `<select>` sigue existiendo y sigue
   siendo la verdad: se esconde y encima va un `.ctl` normal, así que el campo
   hereda filo, foco, hover y tema sin una sola línea propia — que es la mitad
   de por qué esto cuesta 100 líneas y no una librería.

   LA LISTA ES `fixed` Y CUELGA DEL `<body>`, no de la caja. Misma razón que
   obligó a `telefono.js` a pasar `dropdownContainer: document.body`: el cuerpo
   del peek y el del diálogo llevan `overflow-y:auto`, y ahí dentro una lista
   `absolute` se recorta. El sitio se lo calcula el JS contra el rectángulo del
   campo.

   `z-index:70` — por encima del diálogo (60), que a su vez va por encima del
   peek (6). Es lo último que se pinta porque es lo último que se abre. */
.combo{position:relative;min-width:0}
.combo > .combo-campo{width:100%}
.combo-lista{
  position:fixed;z-index:70;
  background:var(--app-paper);border-radius:var(--r-caja);
  box-shadow:0 12px 32px var(--app-shadow), inset 0 0 0 1px var(--app-hairline);
  padding:4px;overflow-y:auto;
}
.combo-lista[hidden]{display:none}
/* Una opción es su NOMBRE y nada más. Tuvo una segunda columna declarable
   —el precio de lista— y se retiró el 2026-08-18 con su único consumidor: sin
   ella la opción no necesita ni caja flex ni nodo interior, así que la elipsis
   vive en la propia opción. Menos marcado por renglón, y son cientos. */
.combo-op{
  padding:6px 8px;border-radius:var(--r-ctl);cursor:pointer;
  font-size:var(--fs-base);color:var(--app-fg);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
/* UNA sola marca de «ésta se elige con Enter», y la mueve tanto el teclado como
   el ratón: dos resaltados distintos —uno de hover y otro de teclado— es la
   pregunta «¿cuál se va a elegir?» pintada en la pantalla. */
.combo-op.activa{background:var(--app-row-hover)}
.combo-vacio{padding:10px 8px;font-size:var(--fs-micro);color:var(--app-fg-tertiary);text-align:center}

/* El valor tecleado se queda a la vista aunque no se haya guardado —perderlo
   sería peor— pero tiene que verse que NO se guardó. */
.fx-ctl-error,.fx-ctl-error:focus{
  border-color:var(--app-atencion);
  box-shadow:0 0 0 3px var(--app-amber-tint);
}
/* ── El campo que se teclea en MAYÚSCULAS (2026-09-08) ────────────────
   Estrenada por el RFC del cliente, y con una regla que la hace sistema en vez
   de un `style` suelto: **se pone donde el dato YA se guarda en mayúsculas**,
   nunca por gusto tipográfico. El modelo del cliente lo sube con
   `mb_strtoupper` y su alfabeto sólo admite `[A-ZÑ&0-9]`, así que sin esto el
   campo era el único sitio donde el dato se veía distinto de como iba a
   quedar — y quien capturaba en minúsculas no tenía forma de saber que estaba
   bien hasta guardar y volver a abrir.

   `text-transform` PINTA pero no cambia el `value`: lo normaliza el JS de la
   página al teclear (clientes.js), conservando el cursor. Las dos mitades hacen
   falta y ninguna sustituye a la otra — sin la CSS el cambio de caso se ve como
   un salto al escribir, sin el JS el valor viaja en minúsculas.

   NO lleva `letter-spacing`: una cadena en versalitas se lee peor apretada, y
   el RFC se transcribe carácter por carácter de un papel. */
.ctl-mayus{text-transform:uppercase}

select.ctl,.campo select,select.fx-ctl,.dlg-sel{
  /* Flecha propia y no la del sistema, por lo mismo que Bootstrap: la nativa
     cambia de forma en cada navegador y de tamaño con el zoom. SVG en el
     fondo, sin peticiones ni iconos que mantener. */
  appearance:none;-webkit-appearance:none;
  padding-right:26px;cursor:pointer;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%23757579' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat:no-repeat;
  background-position:right 9px center;
}
textarea.ctl,.campo textarea,textarea.fx-ctl,.fx-area,.dlg-area{
  resize:vertical;min-height:62px;line-height:1.45;
  padding-top:6px;padding-bottom:6px;
}

/* ── Teléfono con selector de país (intl-tel-input v23.8.1) ───────────
   El único vendor visual de la app, y entra por la puerta del sistema: sus
   tokens `--iti-*` se redefinen con los de la casa en vez de pelear selectores
   —el CSS del plugin está tokenizado, así que basta con eso— y su `<link>` va
   ANTES de esta hoja (ver `interno/cabecera.php`) para que estas líneas ganen
   por orden. Sin ese orden, el modo oscuro pintaría la lista de países casi
   blanca sobre blanco: el plugin fija `--iti-dropdown-bg: white` mientras el
   nombre del país hereda el texto claro del tema.

   Por qué un vendor y no un `<select>` de ladas escrito a mano: lo que se
   compra no es el desplegable, son las longitudes válidas por país
   (libphonenumber) y el formato mientras se teclea. El motor de la casa
   (`f-normalizar.php`) sólo puede reconocer un país si el valor lo declara —
   este campo es lo que hace que lo declare.

   LOS TOKENS VAN EN `:root` Y NO EN `.iti`, y es la consecuencia de que la
   lista de países viva en el `<body>` (`dropdownContainer`, ver telefono.js):
   colgados de `.iti` no la alcanzarían y el modo oscuro se quedaría a medias.
   Por lo mismo, todo lo que estiliza la lista va SIN el prefijo `.iti`. */
:root{
  --iti-border-color:var(--ctl-borde);
  --iti-border-gray:var(--app-hairline);
  --iti-text-gray:var(--app-fg-tertiary);
  --iti-dialcode-color:var(--app-fg-secondary);
  --iti-arrow-color:var(--app-fg-tertiary);
  --iti-hover-color:var(--app-row-hover);
  --iti-dropdown-bg:var(--app-paper);
}
.iti{display:block;width:100%}
.iti input[type="tel"]{width:100%}
/* El botón de país es parte del control, no un adorno encima: mismo radio por
   el lado izquierdo para que la pastilla se lea entera. */
.iti__selected-country{border-radius:var(--r-ctl) 0 0 var(--r-ctl)}
.iti__selected-country-primary:hover{background:var(--app-row-hover)}
/* El foco del botón habla el idioma del sistema (teal), no el del navegador. */
.iti__selected-country-primary:focus-visible{
  outline:none;box-shadow:inset 0 0 0 2px var(--ctl-foco);
  border-radius:var(--r-ctl) 0 0 var(--r-ctl);
}
.iti__selected-dial-code{font-family:var(--fuente);font-size:var(--fs-base)}

/* La hoja de países es una superficie que FLOTA: mismo trato que un menú de la
   casa —papel, radio de lo que flota y las dos sombras—, no la caja cuadrada
   con borde gris del vendor. Vive en el `<body>`, así que se estiliza por su
   propia clase y no colgando de `.iti`. */
.iti__dropdown-content{
  border:none;border-radius:var(--r-caja);
  box-shadow:0 12px 32px var(--app-shadow),0 2px 8px var(--app-shadow),
             inset 0 0 0 1px var(--app-hairline);
  overflow:hidden;
}
.iti__country{font-family:var(--fuente);font-size:var(--fs-base);color:var(--app-fg)}
.iti__dial-code{color:var(--app-fg-tertiary)}
/* El buscador de la lista: el vendor no lo pinta y heredaría el control crudo
   del navegador — en tema oscuro, blanco sobre blanco. No se puede quitar
   (`countrySearch:false` está ignorado en v23.8.1, medido), así que se viste. */
.iti__search-input{
  font-family:var(--fuente);font-size:var(--fs-base);color:var(--app-fg);
  background:var(--app-paper);border:none;border-bottom:1px solid var(--app-hairline);
  padding:8px 12px;outline:none;
}

/* ── Botones ──────────────────────────────────────────────────────────
   PILL y de perfil bajo. Dos decisiones, una sola razón: el botón tiene que
   pesar menos que la fila que acompaña. A 33px competía con los 36px de la
   sábana; a 27 se lee como acción y no como bloque.

   El radio completo se paga con line-height, NO con padding horizontal: los
   14px laterales se conservan tal cual, así que el texto no se acerca más a
   la curva de lo que ya estaba. Lo único que cede es el aire de arriba y
   abajo, que a 33px sobraba.

   El radio sale de `--r-pill`, que vive con los otros cuatro allá arriba (la
   escala de radios) y no aquí: es de la GEOMETRÍA del sistema, no del botón.
   Vivió aquí como `--btn-radio` hasta el 2026-08-16 y ése fue justamente el
   motivo de que seis piezas no lo encontraran. */
:root{ --btn-py:4px; --btn-lh:1.35 }

/* Las dos CARAS del botón con volumen: la de reposo y la que aparece al
   apuntarlo (más clara — la tecla se ilumina, no cambia de color). Son
   tokens porque las usan tres piezas (segunda, selector, flotante) en los
   dos temas: cuatro copias del mismo degradado es como terminan divergiendo.
   Cálidas desde el 2026-08-27: el gris frío del fin de degradado
   (#dcdde5 / #e9eaef) giró al greige de la casa con el mismo L — el volumen
   de la tecla no cambió, sólo dejó de ser azulado. */
:root{
  --btn-cara:linear-gradient(180deg,#ffffff 0%,#e0ddd5 100%);
  --btn-cara-hover:linear-gradient(180deg,#ffffff 0%,#edeae2 100%);
}
[data-theme="dark"]{
  --btn-cara:linear-gradient(180deg,#57575f 0%,#38383f 100%);
  --btn-cara-hover:linear-gradient(180deg,#6a6a74 0%,#4e4e57 100%);
}

/* CUATRO botones y se acabó: acento, segunda, selector y suave (más la
   forma redonda `.btn-circulo`, que no es una piel sino una caja). Sin
   warning ni danger: lo que avisa antes de actuar lo dice el texto de la
   pregunta del diálogo de confirmación, no un botón de color. Los cuatro
   miden exactamente lo mismo — el que lleva borde compensa el padding— así
   que se pueden alinear en un pie sin que ninguno destaque por tamaño; la
   excepción declarada es el flotante (`.fab`, abajo).

   El orden de peso, de más a menos:  acento  >  segunda = selector  >  suave.

   Corregido el 2026-09-19: decía «TRES botones» (la regla del 2026-08-08,
   antes de que el selector se contara aparte) y que lo que avisa lo dice «el
   ámbar en el texto»; hoy lo dice la pregunta de la confirmación. */

/* — PRIMARIA. Azul sólido, plano. La única por pantalla. — */
.btn-acento{
  background:var(--app-accent-solido);color:var(--app-accent-fg);border:0;
  border-radius:var(--r-pill);padding:var(--btn-py) 14px;line-height:var(--btn-lh);
  font-size:var(--fs-base);font-weight:500;cursor:pointer;font-family:inherit;
}
.btn-acento:hover{filter:brightness(1.06)}
/* Igual que la segunda y el selector, que ya lo traían. Vivía SÓLO dentro del
   pie del diálogo de exportación —así que un acento deshabilitado en cualquier
   otro sitio se pintaba como si estuviera vivo—, y en el catálogo se fingía con
   un `style` inline. Un estado del botón pertenece al botón. */
.btn-acento:disabled{opacity:.45;cursor:default}

/* — SALIDA. Cancelar, descartar, volver: lo que NO quieres que llame.
     Relleno casi inexistente y el contorno haciendo el trabajo. Es al revés
     que el resto del sistema —aquí el borde define y el fondo se esconde—
     porque una salida tiene que ser encontrable sin ser tentadora. Al
     apuntarla se rellena: aparece cuando la buscas.
     SU SITIO ES EL PIE DE UN DIÁLOGO, NO EL TITULAR DE UN MÓDULO (movido del
     catálogo `ui.php`, 2026-09-19): en un titular no hay nada de lo que
     salir. Siete botones la usaban en las cabeceras de Citas, Envíos,
     Automatización y Configurar la agenda, y esas cuatro pantallas se veían
     de otra app que Leads y Tablero (corrección de Arif, 2026-08-13). — */
.btn-suave{
  background:var(--app-relleno-tenue);color:var(--app-fg-secondary);border:0;
  box-shadow:inset 0 0 0 1px var(--app-borde-muted);
  border-radius:var(--r-pill);padding:var(--btn-py) 14px;line-height:var(--btn-lh);
  font-size:var(--fs-base);font-weight:500;cursor:pointer;font-family:inherit;
  transition:background var(--app-dur) var(--app-ease), color var(--app-dur) var(--app-ease);
}
.btn-suave:hover{background:var(--app-chip);color:var(--app-fg)}

/* — SEGUNDA y SELECTOR. El mismo objeto físico: gris con degradado que SUBE
     DE TONO al apuntarlo — la tecla se ilumina, no cambia de color. La
     segunda es acción de verdad que no es LA acción ("Volver al mapeo" junto
     a "Aplicar"); el selector es el que abre un menú y lleva flecha. Ninguno
     de los dos se enciende en azul: LA REGLA ES DURA — el azul es sólo para
     lo que escribe en la base, y ni rehacer un paso ni desplegar opciones
     escriben nada (2026-08-08; antes la segunda se encendía en azul y
     violaba la regla).

     Cómo funciona el fundido, porque no es obvio: un `linear-gradient` NO se
     puede animar con `transition` (el navegador salta de uno a otro, no
     interpola). Así que la cara CLARA del hover vive en el fondo del botón
     TODO el tiempo y la de reposo va en un `::before` que la tapa. Al pasar
     el cursor se desvanece la capa de reposo —`opacity` sí es animable— y
     aparece la clara que ya estaba debajo.

     El filo interior (`--app-borde-muted`) vive en LAS DOS capas: en el
     `::before` para el reposo y en el propio botón para el hover — así el
     borde NO desaparece al apuntarlo (mordió una vez: el filo iba sólo en la
     capa que se desvanece). Relieve mínimo: ese filo y una sombra corta;
     el volumen lo pone el degradado, no los cantos.

     Y se hunde al pulsar. Eso no es adorno: un botón que se ilumina al
     apuntarlo y no responde al pulsarlo es un dibujo de botón. Todo se mueve
     con opacity/transform/box-shadow, o sea en GPU y sin reflow. — */
.btn-segunda,.btn-selector{
  position:relative;z-index:0;overflow:hidden;
  display:inline-flex;align-items:center;gap:6px;
  border:0;
  padding:var(--btn-py) 14px;line-height:var(--btn-lh);
  border-radius:var(--r-pill);
  font-family:inherit;font-size:var(--fs-base);font-weight:500;
  color:var(--app-fg);cursor:pointer;
  background:var(--btn-cara-hover);
  box-shadow:0 1px 2px rgba(0,0,0,.10), inset 0 0 0 1px var(--app-borde-muted);
  transition:box-shadow var(--app-dur) var(--app-ease),
             transform 90ms var(--app-ease);
}
/* La flecha, con el mismo trazo que la de los select nativos que dibujamos */
.btn-selector::after{
  content:'';width:10px;height:6px;margin-left:2px;flex:none;
  background:currentColor;opacity:.55;
  -webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") no-repeat center;
          mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") no-repeat center;
}
.btn-segunda::before,.btn-selector::before{
  content:'';position:absolute;inset:0;z-index:-1;border-radius:inherit;
  background:var(--btn-cara);
  box-shadow:inset 0 0 0 1px var(--app-borde-muted);
  transition:opacity var(--app-dur) var(--app-ease);
}
.btn-segunda:hover::before,.btn-selector:hover::before{opacity:0}
.btn-segunda:active,.btn-selector:active{
  transform:translateY(1px);
  box-shadow:inset 0 1px 3px rgba(0,0,0,.16), inset 0 0 0 1px var(--app-borde-muted);
}
.btn-segunda:disabled,.btn-selector:disabled{opacity:.45;cursor:default}
.btn-segunda:disabled::before,.btn-selector:disabled::before{opacity:1}
/* ── LA TECLA REDONDA: `.btn-segunda.btn-circulo` (2026-09-09) ────────
   Pedido de Arif: el botón de opciones de la columna del tablero tenía que
   vestir como los botones que despliegan modales —«Nuevo contacto», «Nueva
   etapa»—, que están en la misma fila de esa pantalla y son `.btn-segunda`.
   Con la piel plana de las flechas del peek, el ⋮ era la única pieza pulsable
   del tablero sin relieve, y a un palmo de dos que sí lo tienen.

   NO ES UNA PIEZA NUEVA: es la piel de la familia sobre la forma que ya
   existía, y por eso son dos clases en el marcado y no una tercera en la hoja.
   Todo el mecanismo de `.btn-segunda` —el `::before` con la cara de reposo que
   se desvanece, el filo en las dos capas, el hundido al pulsar— funciona igual
   sobre un círculo, porque el `::before` toma `border-radius:inherit`.

   Aquí sólo se corrige lo que la piel trae para una cápsula con texto: los
   14px laterales (la caja la fijan `width`/`height`) y la medida de la letra,
   que en la familia es la del cuerpo y en un botón sin palabra es la del
   glifo — desde el 2026-09-09 el ⋮ es un `.icn` y no una letra, así que ese
   `font-size` ya no dibuja nada: se queda porque la tecla puede volver a
   llevar glifo y el 15 es la medida medida para esta caja. La regla va
   DESPUÉS de la familia a propósito: con la misma
   especificidad que `.btn-segunda`, ganar por orden es lo que hace falta —
   y `.btn-segunda.btn-circulo` además la sube, así que no depende sólo de eso. */
.btn-segunda.btn-circulo{padding:0;font-size:15px}
/* ── UN BOTÓN QUE ES `<a>` SIGUE SIENDO UN BOTÓN ──────────────────────
   Cuando la acción NAVEGA —importar, no contactar, columnas y papelera en el
   grupo del titular; los dos de los estados vacíos— el elemento correcto es el
   enlace: conserva el clic-medio y el «abrir en pestaña» que un `<button>` se
   lleva. Lo que no puede conservar es la PINTA de enlace.

   Esto decía `a.btn-segunda:hover{text-decoration:none}` y era correcto cuando
   se escribió: entonces el subrayado del enlace sólo aparecía al apuntarlo. El
   2026-08-16 el subrayado pasó a ser PERMANENTE —al retirarse el azul se quedó
   como única señal de enlace, ver la regla de `a` arriba— y esta línea se
   quedó a medias sin que nada avisara: los botones-enlace pasaron a llevar un
   filo bajo el texto EN REPOSO, y sólo lo perdían al pasarles el cursor. Lo
   cazó Arif de un vistazo, en una fila donde «Nuevo lead» (button) y
   «Importar» (a) están pegados; medido, era la ÚNICA diferencia entre los dos:
   `text-decoration: none` contra `underline` de 1px a `rgba(0,0,0,.15)`.

   Por eso se apaga en el estado BASE y no en el hover: así vale para los dos y
   no vuelve a desincronizarse el día que la regla de `a` cambie otra vez. Es lo
   que ya hacen todas las demás piezas que son `<a>` y no se leen como enlace
   (`.sb-item`, `.pm-item`, `.dlg-opcion`, `.chip-lista`): apagarlo en su propia
   clase. La lección para quien toque `a`: **cambiar el estado en reposo de un
   elemento base obliga a barrer quién lo neutralizaba sólo en hover.**

   `.btn-acento` ENTRA A LA LISTA el 2026-08-18, y se descubrió por el mismo
   camino: el 404 nació con su botón en acento y llegó con el filo bajo el
   texto. El barrido de entonces cubrió las dos clases que TENÍAN un `<a>` vivo
   y dejó fuera la tercera, que no lo tenía — o sea que el hueco no era
   visible, esperaba al primer acento usado como enlace. Hoy vuelve a no haber
   ninguno (ese botón pasó a `btn-segunda`, que es lo que le tocaba: navegar no
   escribe), así que esta línea no cambia un píxel de nada. Se agrega
   precisamente por eso: para que la lista deje de depender de qué clases
   resultan tener un `<a>` esta semana. */
a.btn-segunda,a.btn-suave,a.btn-acento{text-decoration:none}

/* ── La carga en vuelo — las señales del motor de carga ─────────────────
   Piezas con UN dueño: el motor de carga de app.js las pone y las quita;
   ninguna página escribe estas clases a mano.

   LA BARRA, pegada al techo, dice QUÉ pasa con la tinta: AZUL cuando una
   escritura viaja —el azul significa «escribe en la base» desde 2026-08-08,
   y la barra es literalmente eso ocurriendo— y TEAL (`.es-nav`) cuando lo
   que corre es una navegación entre páginas: navegar es estado, no
   escritura, y el teal ya es el color del estado en todo el sistema (el
   foco, la fila elegida). Si conviven, gana el azul.
   Fija y por encima de todo lo que flota (velo 60, tooltip 60, fantasma del
   arrastre 70): una escritura puede nacer dentro de un diálogo y la señal no
   puede quedar debajo. `pointer-events:none` porque es señal, no superficie.

   El baile de transiciones es lo no-obvio: al TERMINAR, el ancho corre a 100%
   (.25s) y la opacidad espera .15s antes de fundirse — sin esa espera la
   barra se esfuma a medio camino y el «llegó» no se ve nunca. Al ARRANCAR la
   espera estorbaría (la señal debe ser inmediata), así que `.visible` la
   sobreescribe con un fundido corto y sin retraso. */
.barra-carga{
  position:fixed;top:0;left:0;height:3px;width:0;z-index:90;
  background:var(--app-accent);
  box-shadow:0 0 10px var(--app-accent),0 0 3px var(--app-accent);
  border-radius:0 var(--r-pill) var(--r-pill) 0;
  opacity:0;pointer-events:none;
  transition:width .25s var(--app-ease),opacity .3s ease .15s;
}
.barra-carga.visible{opacity:1;transition:width .25s var(--app-ease),opacity .1s ease}
.barra-carga.es-nav{
  background:var(--app-info);
  box-shadow:0 0 10px var(--app-info),0 0 3px var(--app-info);
}

/* EL DIÁLOGO OCUPADO: su escritura viaja. Los punteros se apagan en los
   HIJOS y no en el velo —`pointer-events:none` en el velo dejaría pasar los
   clics a la página de atrás, que es peor que recibirlos—; el velo sigue
   oyendo, y quien ignora el clic (y el Escape) es el cierre global de
   diálogos en app.js. Sin atenuación a propósito: el aro del botón ya dice
   «trabajando», y un fundido que entra y sale en 200ms se lee como flash. */
.dialogo.ocupado{cursor:progress}
.dialogo.ocupado > *{pointer-events:none}

/* LA PÁGINA CON UNA ACCIÓN EN VUELO. Tres señales por tiempo, todas del
   motor: el cursor `progress` desde el primer milisegundo; el REBOTE + toast
   cuando un gesto llega y el carril está cerrado (el silencio se lee como
   software roto); y la ESCARCHA si el vuelo pasa de ~350ms — un velo con
   blur que congela la página visiblemente, con lo que trabaja nítido por
   encima: el botón `.cargando` sube a z-56 y el diálogo ya vive en z-60.
   Los vuelos rápidos jamás la ven, por eso no parpadea nada. */
html.accion-en-vuelo, html.accion-en-vuelo body{cursor:progress}
.velo-carga{
  position:fixed;inset:0;z-index:55;
  background:color-mix(in srgb, var(--app-velo) 30%, transparent);
  -webkit-backdrop-filter:blur(1.5px);backdrop-filter:blur(1.5px);
  opacity:0;pointer-events:none;cursor:progress;
  transition:opacity .22s var(--app-ease);
}
.velo-carga.visible{opacity:1;pointer-events:auto}

/* El rebote: el gesto de la plataforma al tocar fuera de un modal ocupado.
   Va sobre la CAJA del diálogo o sobre el botón que trabaja — nunca sobre el
   velo, que es fijo a pantalla y no tiene nada que rebotar. */
@keyframes rebote{0%{transform:scale(1)}38%{transform:scale(1.045)}100%{transform:scale(1)}}
.rebote{animation:rebote .32s var(--app-ease)}

/* LA ETIQUETA DEL CURSOR. Chromium no repinta el cursor hasta que el mouse
   se mueve (bug 26723, abierto hace años): el `cursor:progress` de arriba
   sólo se ve al primer movimiento. Esta cápsula la pinta el motor EN EL
   INSTANTE del clic, en las coordenadas del propio clic, y sigue al puntero
   mientras dura el vuelo — abajo-derecha, donde la plataforma pone su busy.
   Es TEXTO («Trabajando…») y no otro aro a propósito: el cursor de espera ya
   gira, y dos spinners juntos son redundancia, no señal (corrección de Arif,
   2026-08-12). El fundido de .15s hace que en los vuelos de un parpadeo
   apenas se insinúe en vez de aparecer y desaparecer de golpe. */
/* LA MISMA CÁPSULA, DOS TRABAJOS (2026-08-15). `.cursor-carga` dice lo que
   ESTÁ PASANDO («Trabajando…», tras el clic); `.cursor-pista` dice lo que VA A
   PASAR («Copiar», al posarse). Comparten regla porque son la misma pieza —si
   una cambia de tono o de radio, la otra la sigue— y se distinguen sólo en el
   z-index: si las dos coinciden (se copia y al instante hay una escritura en
   vuelo), manda la que informa del estado en curso.

   La pista nació sustituyendo a un cursor DIBUJADO: primero el `+` verde del
   sistema —que es el badge de arrastrar-para-duplicar y se lee «agregar»—,
   luego un glifo de dos hojas propio, y al final la palabra. Es el mismo
   camino que ya había recorrido `.cursor-carga` cuando dejó de ser un segundo
   spinner: un glifo hay que aprenderlo, una palabra ya está leída. Y a
   diferencia de un `cursor:url()`, la cápsula SÍ hereda el tema, la fuente de
   la casa y el tamaño de texto del sistema. */
.cursor-carga,.cursor-pista{
  position:fixed;z-index:97;pointer-events:none;white-space:nowrap;
  /* ARRIBA del puntero, no abajo-derecha: el bitmap del cursor se dibuja
     hacia abajo-derecha desde su punta y tapaba parcialmente la cápsula
     (corrección de Arif mirándolo en vivo). Hacia arriba no dibuja nada. */
  margin:-30px 0 0 10px;
  background:var(--app-paper);border:1px solid var(--app-hairline);
  border-radius:var(--r-pill);padding:2px 10px;
  font-size:var(--fs-micro);font-weight:500;color:var(--app-fg);
  box-shadow:0 2px 8px var(--app-shadow);
  opacity:0;transition:opacity .15s ease;
}
.cursor-carga.visible,.cursor-pista.visible{opacity:1}
/* Debajo de la de carga: una escritura en vuelo tapa a una promesa de hover. */
.cursor-pista{z-index:96}

/* EL BOTÓN en `.cargando`: la letra se vuelve transparente —no `visibility`,
   porque el ancho del botón lo sostiene el texto y un botón que encoge al
   pulsarlo se lee como rotura— y encima gira el aro. El `disabled` real lo
   pone el motor (es lo que mata el doble clic de raíz: un botón deshabilitado
   ni siquiera despacha eventos), y por eso el estado anula la opacidad del
   45%: trabajando no es apagado. El aro va en el gris del texto secundario
   salvo sobre el acento, donde la única tinta legible es la del propio
   botón. */
button.cargando{color:transparent;cursor:progress;position:relative;z-index:56}
/* El cursor va TAMBIÉN aquí y no sólo arriba: `.btn-acento:disabled` declara
   `cursor:default` con más especificidad que `button.cargando` — sin esta
   línea, el botón volando enseñaría la flecha. */
button.cargando:disabled{opacity:1;cursor:progress}
button.cargando::after{
  content:'';position:absolute;top:50%;left:50%;
  width:14px;height:14px;margin:-7px 0 0 -7px;box-sizing:border-box;
  border:2px solid var(--app-fg-secondary);border-top-color:transparent;
  border-radius:50%;
  -webkit-mask:none;mask:none;   /* pisa la flecha del selector si un selector cayera aquí */
  animation:gira .55s linear infinite;
}
.btn-acento.cargando::after{border-color:var(--app-accent-fg);border-top-color:transparent}
@keyframes gira{to{transform:rotate(360deg)}}

/* ── Estados vacíos ─────────────────────────────────────────────────
   TRES niveles, y cada uno tiene su pieza. Mezclarlos es lo que hacía que la
   misma frase se viera de cuatro maneras según la página (2026-08-10):

     · página vacía     → `.estado-vacio`, centrado en el panel. UNO por
                          pantalla, y sólo si nada más lo está diciendo ya.
     · sección vacía    → `.vacio-linea`, una línea callada en su sitio.
     · contenedor vacío → lo dice el contenedor (`.tab-hueco`, «Suelta aquí»).

   El `margin:auto` centra en los DOS ejes, pero sólo dentro de un flex: por
   eso `.estado-vacio` va como hijo directo de `.app-panel` o de `.pagina`
   —nunca dentro de `.hoja`, que es un bloque de 880—. Dentro de la hoja el
   auto sólo centraba en horizontal, y encima contra el ancho de la caja: el
   mensaje quedaba pegado al techo y 36px a la izquierda del centro del panel
   (medido a 1280). Se leía como pieza suelta, no como el centro de nada. */
.estado-vacio{
  margin:auto;text-align:center;
  display:flex;flex-direction:column;align-items:center;gap:9px;
  padding-bottom:20px;
}
/* El vacío de SECCIÓN: no es una pantalla vacía, es un bloque que todavía no
   tiene qué mostrar dentro de una página que sí tiene contenido. Se queda en
   su renglón, con la voz baja, y jamás roba el centro. */
.vacio-linea{font-size:var(--fs-base);color:var(--app-fg-tertiary);font-style:italic}
/* El título del vacío en 15 y letra secundaria: el hueco avisa, no compite con
   el título de la página. En Space Grotesk (2026-09-23, pedido de Arif): el
   vacío es chrome, no dato. SIN CUERPO desde ese día: el título con su punto
   —warning si falta configurar, info si sólo no hay datos— y las acciones
   dicen todo; el párrafo que explicaba el módulo se retiró de todos. */
.estado-vacio .titulo{font-family:var(--fuente-titulo);font-size:var(--fs-dialogo);font-weight:600;color:var(--app-fg-secondary)}
/* ── DOS BOTONES, UNA FILA (2026-09-03) ────────────────────────────────
   `.estado-vacio` es `flex-direction:column` —lo que centra el título sobre el
   cuerpo— así que dos botones sueltos se apilaban uno bajo otro, y eso los lee
   como dos PASOS de una secuencia en vez de dos caminos alternativos. Es
   exactamente el caso de «No hay contactos aún», donde «Nuevo contacto» e
   «Importar» son dos maneras de hacer lo mismo (pedido de Arif).

   Va en un envoltorio y NO cambiando la dirección del bloque, porque lo que
   tiene que apilarse —título y acciones— sigue apilándose. Con un botón
   el envoltorio es inerte, así que puede ponerse siempre y no hay que decidir.
   `flex-wrap` porque en la escalera angosta dos botones y su hueco pueden no
   caber en angosto, y ahí sí se apilan: es plegarse, no romperse. */
.estado-vacio .vacio-acciones{
  display:flex;gap:8px;align-items:center;justify-content:center;flex-wrap:wrap;
  margin-top:5px;
}
/* El aire vertical lo pone el envoltorio una vez, no cada botón. */
.estado-vacio .vacio-acciones .btn-segunda,
.estado-vacio .vacio-acciones .btn-suave,
.estado-vacio .vacio-acciones .btn-acento{margin-top:0;padding:6px 15px}

/* ── Importador ───────────────────────────────────────────────────────
   Sus tres contenedores propios murieron el 2026-08-10: usa `.pagina`/`.hoja`
   y `.panel-titulo` como todas. El paso vive dentro de la cápsula del titular
   —es la MEDIDA de la pantalla, igual que el conteo de las demás—, así que se
   pinta con `.conteo` y no con un gris propio. */
.imp-perfil{display:flex;align-items:center;gap:10px;margin:18px 0 20px}
.imp-perfil .chip{background:var(--app-chip);border-radius:var(--r-ctl);padding:6px 12px;font-size:var(--fs-base);cursor:pointer}
.imp-rejilla{display:grid;grid-template-columns:1fr 44px 1fr;column-gap:8px}
.imp-cabecera{
  font-size:var(--fs-meta);font-weight:500;color:var(--app-fg-tertiary);
  padding-bottom:8px;border-bottom:1px solid var(--app-hairline);
}
.imp-filas{display:flex;flex-direction:column;gap:14px;padding:14px 0 4px}
.imp-fila{align-items:center}
.imp-src{
  font-weight:500;font-size:var(--fs-base);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.imp-src.ignorada{color:var(--app-fg-tertiary);text-decoration:line-through}
.imp-muestra{
  font-size:var(--fs-meta);color:var(--app-fg-tertiary);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-top:1px;
}
.imp-flecha{color:var(--app-fg-tertiary);text-align:center;font-size:var(--fs-base)}
.imp-dest{
  display:inline-flex;align-items:center;gap:7px;
  padding:6px 11px;border-radius:var(--r-ctl);font-size:var(--fs-base);
  background:var(--app-chip);color:var(--app-fg);cursor:pointer;
}
.imp-dest.sin-mapear{background:var(--app-amber-tint);color:var(--app-fg);font-weight:500}
.imp-dest.ignorada{background:transparent;color:var(--app-fg-tertiary)}
.imp-marca{font-size:var(--fs-meta);color:var(--app-fg-tertiary)}
/* Era 9px: la advertencia de que una columna se va a perder, escrita más
   pequeña que todo lo demás de la pantalla. */
.imp-dest.sin-mapear .imp-marca{font-size:var(--fs-micro);color:var(--app-fg-secondary)}
.imp-pie{
  display:flex;align-items:center;gap:10px;
  border-top:1px solid var(--app-hairline);padding-top:16px;margin-top:14px;
}
.imp-pie .resumen{font-size:var(--fs-base);color:var(--app-fg-secondary)}

/* ── LA ESCALERA RESPONSIVA (2026-08-30) ──────────────────────────────
   Cuatro peldaños, y cada uno quita UNA cosa — nunca dos a la vez, para que
   ningún ancho pierda de golpe la navegación y el contenido:

     · >1180 — todo a su ancho: la barra la decide el usuario (cookie), el
               riel al costado.
     · ≤1180 — el chrome ENCOGE: barra 180, riel 280, costado del panel a 20.
               La app entera sigue igual, sólo más apretada.
     · ≤1024 — la anchura MANDA sobre la barra (sólo iconos, el interruptor
               se esconde) y el RIEL SE PLIEGA al final de la columna
               principal (el bloque de abajo). El peek pasa a cubrir la
               pantalla y la bandeja de conversaciones apila (sus reglas
               viven con sus piezas).
     · ≤768  — móvil: la barra se vuelve CAJÓN sobre un velo, abierto desde
               la cabecera móvil (`.cab-movil`, la única pieza que existe
               sólo aquí). El bloque, al final del archivo de este tramo.

   La clase `compacta` en ≤1024 la pone el guion en línea de `cabecera.php`
   ANTES del primer pintado (misma técnica que el tema) y la sostiene app.js
   con `matchMedia` al redimensionar; la cookie del usuario NO se toca — al
   volver a >1024 se restaura su elección. */
@media (max-width:1180px){
  .app-sidebar{--sb-w:180px}
  /* La MARCA es lo primero que se va cuando falta ancho, igual que en el modo
     compacto y por el mismo motivo: es lo único de la barra que no se reduce a
     un icono. A 180px el rótulo pide 135px y le quedan 110, así que la
     alternativa era truncarlo — y un nombre propio partido («Controly Fu…») se
     lee como error de la app, no como falta de espacio. Los dos botones-icono
     se quedan solos y pegados a la derecha: `.sb-cabeza` ya alinea al final,
     así que no hace falta decirlo dos veces — y en 160px de ancho útil siguen
     cabiendo en fila (los 40px del modo compacto son los que no dan).
     ACOTADO A LA BARRA (2026-08-30): la cabecera móvil reusa `.sb-marca` y a
     ella el ancho no le quita el sitio — a pelo, esta regla la borraba. */
  .app-sidebar .sb-marca{display:none}
  /* Los 16px de arriba NO se encogen con el ancho: son los de la barra
     lateral, que tampoco cambia — es la línea donde arrancan las dos. El
     token baja en el CASCARÓN, que es donde se declara (el riel plegado
     también lo lee). */
  .app-shell{--panel-pad-x:20px}
  .app-panel{padding:16px var(--panel-pad-x) 24px}
  /* El riel encoge con la barra: son las dos piezas de chrome que le quitan
     ancho al panel, así que ceden a la vez. 280 es su piso real —las tres
     cifras del pulso siguen cabiendo en fila, que es lo que se rompe primero.
     No se esconde: ver `.app-lado`. */
  .app-lado{--lado-w:280px;--lado-pad-x:14px}
}
@media (max-width:1024px){
  .peek{width:min(420px,100vw)}
  /* DOS PANELES NO CABEN: 264 de barra + 840 de paneles pasan de 1024. Aquí el
     de la conversación deja de insertarse al lado y se SUPERPONE al del lead —
     mismo borde derecho, z-index por encima (7 > 6) y el desplazamiento de
     guardado vuelve a ser su propio ancho.
     Y por eso, y sólo aquí, aparece el nombre del lead: superpuesto, la
     cabecera del otro panel ya no se ve, así que sin él el hilo no dice de
     quién es. */
  .peek2{right:0;width:min(420px,100vw);z-index:7;transform:translateX(100%)}
  .peek2.abierto{transform:translateX(0)}
  .peek2-persona{
    display:block;flex:none;
    /* Mismo rótulo que el del otro panel: si los dos titulan un panel, los
       dos miden lo mismo — verlos a distinto tamaño diría que uno manda. */
    font-family:var(--fuente-titulo);font-size:var(--fs-dialogo);font-weight:700;
    line-height:1.2;margin:6px 0 0;letter-spacing:-.01em;
  }
}

/* ── ≤1024: la anchura manda sobre la barra, y el riel se pliega ───────
   El tercer peldaño de la escalera (ver el rótulo grande, arriba). Dos
   decisiones, y las dos son del ANCHO, no del usuario:

   1) LA BARRA VA SÓLO-ICONOS. La clase `compacta` la pone cabecera.php antes
      del primer pintado y app.js al redimensionar; aquí sólo muere el
      interruptor — en este rango el usuario no elige, así que un botón que
      no puede obedecer es peor que ninguno. Su cookie queda intacta y
      vuelve a mandar en cuanto el ancho regresa.

   2) EL RIEL SE PLIEGA A LA COLUMNA PRINCIPAL. En este rango panel + riel
      ya no caben como columnas (medido con `lado-cinco` a 1024: el panel
      caía a ~565px y los renglones de configuración envolvían de mala
      manera). El pliegue es el que pide el propio riel («no se esconde, se
      pliega»): pasa a ser el último bloque de la columna, a lo ancho.

   La mecánica del pliegue: el cascarón se vuelve REJILLA de dos columnas
   —barra | contenido— con el panel arriba y el riel abajo, y EL SCROLL SUBE
   AL CASCARÓN: una columna partida en dos scrolls apilados es la pantalla
   donde la rueda nunca hace lo que esperas (la misma razón que ya apiló la
   mesa y la bandeja). El panel y su molde sueltan el suyo; el molde además
   suelta el margen negativo del canal, porque su barra ya no existe — la
   del cascarón cae en el canto de la ventana, más allá del riel.

   TODO CUELGA DE `:has(> .app-lado)`: en las páginas sin riel no cambia un
   pixel — la sábana, el tablero y la bandeja conservan sus viewports. Es la
   misma mecánica `:has()` que ya reparte `lado-cinco`/`lado-seis`. */
@media (max-width:1024px){
  .sb-plegar{display:none}
  /* Y el tema VUELVE, por la misma razón por la que el interruptor se va: aquí
     la barra está compacta porque no cabe, no porque nadie lo pidiera, y sin
     forma de expandirla el botón de `cuenta.php` no alcanza a un operador. El
     apilado del pie —el que la barra contraída a mano ya no tiene— sigue vivo
     en este rango, y es el precio correcto: un control apilado se lee; uno que
     no existe, no. */
  .app-sidebar.compacta .sb-tema{display:flex}

  .app-shell:has(> .app-lado){
    display:grid;
    grid-template-columns:auto minmax(0,1fr);
    /* La segunda fila estira: con poco contenido, el gris del riel llega
       hasta abajo en vez de cortarse a media pantalla. */
    grid-template-rows:auto minmax(min-content,1fr);
    overflow-y:auto;overflow-x:clip;
    scrollbar-gutter:stable;
  }
  .app-shell:has(> .app-lado) > .app-panel{
    grid-column:2;grid-row:1;
    overflow:visible;height:auto;
  }
  .app-shell:has(> .app-lado) :is(.consola,.portada){
    flex:none;overflow:visible;
    scrollbar-gutter:auto;
    margin-right:0;padding-right:0;
    padding-bottom:32px;
  }
  .app-shell:has(> .app-lado) > .app-lado{
    grid-column:2;grid-row:2;
    width:auto;min-width:0;max-width:none;height:auto;
    overflow:visible;scrollbar-gutter:auto;
    border-left:0;border-top:1px solid var(--app-hairline);
    /* Plegado ya no acompaña a un titular: su aire de arriba es el suyo, y
       los costados son los del panel — por eso el token vive en el cascarón. */
    padding:18px var(--panel-pad-x) 32px;
  }
  /* Y LA CABEZA DEJA DE PEGARSE (2026-09-06, pedido de Arif: el sticky sólo de
     `lg` para arriba). Plegado, el riel ya no es un scroller propio: es el
     último bloque de la columna principal, dentro de SU scroll. Una cabeza
     pegajosa ahí no se queda sobre la lista del riel —se quedaría sobre la
     página entera, tapando contenido que no es suyo hasta que el bloque
     completo se fuera de pantalla—, que es justo lo contrario de lo que la
     pieza dice. El filo se queda: es la frontera de la cabeza con lo suyo, y
     eso no depende del ancho. */
  .app-shell:has(> .app-lado) > .app-lado > .lado-cabeza.pegada{
    position:static;z-index:auto;
    /* Sin el aire de arriba: existía para que la cabeza fija no tocara el canto
       de la ventana, y plegada no se fija. El riel pone aquí su propio aire
       (18px, arriba). */
    padding-top:0;
  }
  .app-shell:has(> .app-lado) > .app-lado > .lado-cabeza.pegada::after{content:none}
  /* Los PEEKS de una página plegada anclan al VIEWPORT, no al cascarón: su
     `position:absolute` medía contra un ancla que aquí es tan alta como todo
     el contenido — abierto, su cabeza quedaba en el tope del DOCUMENTO (a
     cientos de px de donde el usuario tocó el botón) y cerrado inflaba el
     scrollWidth con sus 390 fuera del canto (medido en el Inicio: 774 contra
     384). `fixed` con los mismos top/right/bottom lo pega a la ventana, que
     es lo que el peek siempre quiso decir. El 30 lo pone sobre la cabecera
     móvil pegajosa (20) y bajo el cajón del menú y su velo (58/57). */
  .app-shell:has(> .app-lado) .peek{position:fixed;z-index:30}
}
/* La barra, ANCLADA mientras el cascarón scrollea — sólo en el tramo con
   barra en flujo (769–1024): en móvil es un cajón fijo y esta regla le
   pisaría el `position`. `sticky` y no `fixed` porque en flujo sigue
   ocupando su columna de la rejilla. */
@media (min-width:769px) and (max-width:1024px){
  .app-shell:has(> .app-lado) > .app-sidebar{
    grid-column:1;grid-row:1 / -1;
    position:sticky;top:0;align-self:start;
    height:100vh;height:100dvh;
  }
}

/* ── ≤768: móvil — la cabecera propia y la barra como cajón ────────────
   El último peldaño. Una barra de 56px en un teléfono de 390 es el 14% de la
   pantalla gastado en chrome, así que la barra sale del flujo: se vuelve un
   CAJÓN fijo sobre un velo, y lo abre la única pieza que existe sólo en este
   rango — la cabecera móvil (`.cab-movil`: el botón de menú y la marca).

   El cajón abre SIEMPRE ANCHO, con rótulos: quien lo abre viene a leer a
   dónde ir, y un cajón de iconos obligaría a abrir Y adivinar. Por eso app.js
   le quita `compacta` en este rango aunque la cookie diga otra cosa (la
   cookie no se toca: es la elección de escritorio).

   El pliegue del riel de arriba SIGUE aplicando (768 < 1024); aquí sólo se
   re-colocan sus piezas a UNA columna y la cabecera se vuelve pegajosa — en
   las páginas con riel el cascarón scrollea, y perder el menú al fondo de la
   página sería perder la única navegación que queda.

   Los z-index: velo 57 y cajón 58 — encima de peeks (6/7) y del velo del
   filtro (55), debajo del diálogo (60): un diálogo abierto sigue siendo lo
   último que se contesta. */
.cab-movil{display:none}
.velo-menu{display:none}
@media (max-width:768px){
  .app-shell{
    display:grid;
    grid-template-columns:minmax(0,1fr);
    grid-template-rows:auto minmax(0,1fr);
  }
  .cab-movil{
    display:flex;align-items:center;gap:4px;
    grid-column:1;grid-row:1;
    padding:8px 12px;
    background:var(--app-sidebar-bg);
    border-bottom:1px solid var(--app-hairline);
  }
  /* El botón de menú es el `.sb-plegar` de este rango: mismo tamaño, misma
     conducta de hover, mismo icono. No comparte clase porque no comparte
     sitio ni estado (aquél colapsa, éste abre un cajón). */
  .cab-menu{
    width:34px;height:34px;flex:none;
    background:transparent;border:0;border-radius:var(--r-ctl);
    cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;
    color:var(--app-fg-secondary);
  }
  .cab-menu .icn{width:var(--icn-fila);height:var(--icn-fila);vertical-align:0}
  .cab-menu:hover{background:var(--app-pill);color:var(--app-fg)}
  .app-panel{grid-column:1;grid-row:2;min-height:0}

  /* Con riel: tres filas, un solo scroll (el del cascarón, heredado del
     pliegue), y la cabecera pegajosa para no perder el menú al fondo.

     El `min-height:auto` DESHACE el `min-height:0` de arriba, y sin él la
     página entera se encima (medido a 390: la fila del panel salía de 359px
     para una portada de 1237 — con `min-height:0` la contribución mínima del
     panel es CERO, y como el cascarón mide 100vh en firme, la fila `auto` se
     queda con el sobrante en vez de crecer con su contenido, que se derrama
     por encima del riel). Con `auto`, la fila mide lo que mide el contenido
     — que es todo el contrato del pliegue. */
  .app-shell:has(> .app-lado){
    grid-template-columns:minmax(0,1fr);
    grid-template-rows:auto auto minmax(min-content,1fr);
  }
  .app-shell:has(> .app-lado) > .cab-movil{position:sticky;top:0;z-index:20}
  .app-shell:has(> .app-lado) > .app-panel{grid-column:1;grid-row:2;min-height:auto}
  .app-shell:has(> .app-lado) > .app-lado{grid-column:1;grid-row:3}

  /* El cajón. `fixed` lo saca del flujo, así que la rejilla de arriba ni lo
     cuenta. Cerrado vive fuera del canto izquierdo; `.abierta` lo trae. La
     sombra sólo abierto: cerrada se colaba 40px de penumbra por el borde. */
  .app-sidebar{
    position:fixed;top:0;bottom:0;left:0;
    --sb-w:264px;
    height:100vh;height:100dvh;
    z-index:58;
    transform:translateX(-100%);
    transition:transform 200ms var(--app-ease);
  }
  .app-sidebar.abierta{
    transform:translateX(0);
    box-shadow:16px 0 40px var(--app-shadow);
  }
  /* El cajón abre ancho, así que recupera su marca (la media query de 1180
     la esconde para la barra de 180 — aquí la barra vuelve a medir 264). */
  .app-sidebar .sb-marca{display:inline}
  .velo-menu{
    position:fixed;inset:0;z-index:57;
    background:var(--app-velo);
    border:0;padding:0;margin:0;
  }
  .velo-menu.abierto{display:block;animation:dlg-velo .12s ease-out}
}
/* Repetida a propósito DESPUÉS del cajón: la de arriba (junto a la barra)
   pierde contra el `transition:transform` de móvil por puro orden del
   archivo — las media queries no suman especificidad. */
@media (prefers-reduced-motion:reduce){
  .app-sidebar{transition:none}
  .velo-menu.abierto{animation:none}
}

/* ── Puerta ─────────────────────────────────────────────────────────── */
.puerta{
  height:100vh;display:flex;align-items:center;justify-content:center;
  background:var(--app-bg);
}
/* EL LOGIN, Y SÓLO EL LOGIN. `.puerta` la comparten SEIS pantallas públicas
   —optin, agenda, baja, mi-cita, chat y el 404—, donde el centrado a media
   pantalla sobre papel es lo correcto: son una frase o un formulario corto
   que el visitante lee y cierra. El login es otra cosa —es la entrada a la
   app— y desde el 2026-08-26 se calca del de Controly: el peso ARRIBA y el
   gris de la barra lateral de fondo. «El login» son tres ranuras de la misma
   puerta: `interno/puerta.php`, `restablecer.php` y, desde el 2026-09-05, el
   consentimiento del MCP (`autorizar.php`), que el usuario ve justo después
   de teclear la clave y que a 304px sobre `.puerta` a secas medía 1,318px de
   alto en un viewport de 627: centrada y sin desplazar, el botón quedaba
   fuera. La caja de esa ranura es la misma de 304: se homologó al login.

   Las dos mitades tienen el mismo porqué. El bloque anclado arriba es donde
   una pantalla de entrada pone lo suyo (nadie la lee: se teclea y se pasa),
   y el gris de la barra es el primer color de la app —entrar deja de ser un
   corte de blanco a gris—. Va como MODIFICADOR y no editando `.puerta`
   porque las otras seis no quieren ninguna de las dos cosas. */
.puerta-login{
  align-items:flex-start;
  background:var(--app-sidebar-bg);
  /* Los 48px de Controly (su `mt-lg-5`) medidos en una pantalla de portátil,
     dichos como proporción para que no se peguen al canto en una alta ni
     empujen el botón fuera en una baja: 7vh da 47.7 en los 682px de viewport
     de esta Mac. El tope de 80 evita que en un monitor de 1440 de alto el
     bloque se despegue tanto que vuelva a leerse como centrado. */
  padding:clamp(40px,7vh,80px) 24px 40px;
  /* `.puerta` recorta a 100vh y el `body` no desplaza (`overflow:hidden`).
     Centrada eso no molestaba —el sobrante se reparte y se pierde arriba y
     abajo por igual—; anclada arriba, con el captcha encendido y una pantalla
     baja, el botón se saldría sin forma de alcanzarlo. */
  overflow-y:auto;
}
/* La frase bajo el rótulo (`.lede`), CENTRADA en este molde (pedido de Arif,
   2026-09-04, mirando `restablecer.php`): el rótulo y la marca ya van al
   centro, y una frase de dos líneas alineada a la izquierda bajo ellos se
   lee como un párrafo que se coló. Sólo aquí — en las seis públicas el
   `.lede` sigue como está. */
.puerta-login .lede{text-align:center}
.puerta-caja{
  /* 304 y no 300 desde el reCAPTCHA (2026-08-10): el widget de Google mide
     304px fijos y no se puede redimensionar. Se ajustó la caja para que quepa
     NATIVO, en vez de encogerlo con transform:scale — que lo deja borroso por
     ser un iframe. Con el captcha apagado la diferencia no se ve. */
  display:flex;flex-direction:column;gap:12px;width:304px;
}
/* El iframe del captcha es inline y arrastra el hueco de la línea base. */
.puerta-captcha{line-height:0}
/* AQUÍ VA EL LOGOTIPO COMPLETO (`.marca-logotipo`), no el símbolo suelto
   (2026-08-26, decisión de Arif). Estuvo nueve días diciendo la marca a dos
   piezas —símbolo + «CONTROLY CRM» escrito en Space Grotesk, 2026-08-17—; hoy
   es la imagen de la casa, la misma que Controly pone en su login, en las
   TRES pantallas sin cascarón (aquí, el 404 y el nav de `ui.php`). Dentro de
   la app manda `.marca-icono`: el logotipo no cabe en una fila de menú.
   El nombre escrito de la barra lateral se queda, porque ahí no es un
   logotipo suelto sino la fila de identidad del cascarón.

   `flex` y no `text-align:center` es herencia de cuando había dos piezas que
   alinear; se conserva porque centra igual y el día que vuelva el nombre no
   hay nada que rehacer.

   ── LA TIPOGRAFÍA SE QUEDA AUNQUE AQUÍ YA NO HAYA TEXTO, Y NO ES RESIDUO ──
   `.puerta-marca` NO es sólo del login: `optin.php`, `agenda.php` y
   `mi-cita.php` la usan como TITULAR DE TEXTO —el nombre del funnel o el tipo
   de cita—, que es lo único que identifica esas pantallas. Quitarle la familia
   y el cuerpo las deja con su titular en la letra del cuerpo a 16, por debajo
   del rótulo que lleva debajo: jerarquía invertida, y sin ningún error que lo
   delate. Pasó al hacer este cambio y se atrapó mirando `optin.php`; queda
   escrito para que no vuelva a pasar. Con sólo un icono dentro, estas cuatro
   declaraciones no hacen nada. */
.puerta-marca{
  display:flex;align-items:center;justify-content:center;gap:9px;
  font-family:var(--fuente-titulo);
  font-size:var(--fs-titulo);font-weight:700;margin-bottom:6px;letter-spacing:-.02em;
}
.puerta-titulo{
  text-align:center;color:var(--app-fg-secondary);font-size:var(--fs-base);
}
/* EL RÓTULO DEL LOGIN, EN LA FAMILIA DE LOS TÍTULOS (2026-08-26, pedido de
   Arif). Era la letra del cuerpo a 14 en el gris secundario — el peso justo
   cuando arriba había un logotipo escrito de 26px que mandaba. Sin él, esta
   línea es lo único que nombra la pantalla, así que sube a `--fs-dialogo`
   (15) en Space Grotesk y en negrita: exactamente el rótulo de una
   SUPERFICIE, que es lo que la escala reserva para ese tamaño.

   VA BAJO `.puerta-login` Y NO EN `.puerta-titulo` A SECAS: ese rótulo lo
   comparten las seis pantallas públicas («Déjanos tus datos», «Agenda tu
   cita», «Tu cita»…), que son del CLIENTE y no del producto, y nadie pidió
   tocarlas. El cambio se pidió para el login; se aplica al login.

   El COLOR se queda en el secundario y no sube al primario: la jerarquía ya
   la hacen la familia y el peso, y en el primario esta línea competiría con
   el símbolo por ser lo primero que se mira. */
.puerta-login .puerta-titulo{
  font-family:var(--fuente-titulo);font-size:var(--fs-dialogo);font-weight:700;
  letter-spacing:-.01em;
}
/* Desde los usuarios (2026-08-09) la puerta es el login de correo +
   contraseña: dos campos FUSIONADOS en un bloque —el gesto visual del login
   de Controly, dicho con los tokens de la casa (el input suelto en pill de la
   clave-por-cuenta murió con ella). El tratamiento —papel, filo, foco— es el
   de cualquier control; lo único propio es la fusión de esquinas, y el foco
   sube en z-index para que su halo no quede mordido por el campo hermano. */
.puerta-campos{display:flex;flex-direction:column}

/* El pie del login (2026-08-26): recuperar la clave y la firma de la marca,
   en ese orden — el mismo que el de Controly, del que se calca.

   `margin-top` ADEMÁS del `gap` de la caja: los 12px de la caja separan
   piezas del formulario entre sí, y esto ya no es formulario. Sumados dan
   los 24px que Controly pone entre su botón y el enlace.

   EL ENLACE NO LLEVA CLASE. Se ve con el `a` de la hoja —letra del cuerpo y
   subrayado permanente—, que es la regla del 2026-08-16: el azul quedó
   reservado a lo que escribe en la base, y este enlace no escribe nada. */
.puerta-pie{
  display:flex;flex-direction:column;align-items:center;gap:18px;
  margin-top:12px;text-align:center;
}
.puerta-pie a{font-size:var(--fs-base)}
/* La firma es lo que ACOMPAÑA —13px y terciario, como cualquier pie— y no
   usa `.marca-nombre`: el reparto de tintas de la marca
   está calibrado para decir el nombre en grande (la fila de arriba ya lo
   hace), y a 13px en versales navy competiría con el enlace que sí es
   accionable. */
.puerta-firma{margin:0;font-size:var(--fs-micro);color:var(--app-fg-tertiary)}

/* ── Campo ────────────────────────────────────────────────────────────────
   LA estructura de un campo en toda la app: la etiqueta ARRIBA, el control
   debajo, y NINGÚN placeholder. No es preferencia de estilo, es lo que hace
   que un formulario se pueda usar:

   · La etiqueta no se va. Un placeholder desaparece en cuanto tecleas, así
     que el campo que estás llenando es justo el único que ya no dice qué es
     — y al revisar antes de enviar, un formulario lleno no dice nada.
   · Se lee ANTES de entrar al campo. La etiqueta dentro obliga a un gesto
     (foco) para descubrir a dónde vas.
   · Sobrevive al autocompletado, al zoom y al lector de pantalla.

   Y DESDE EL 2026-09-03 ES «NINGÚN PLACEHOLDER» A SECAS (decisión de Arif),
   no «ninguno haciendo de etiqueta». La diferencia es la que se había estado
   pagando: el catálogo prohibía sustituir el rótulo pero bendecía el
   placeholder «cuando da un EJEMPLO», y por esa puerta entraron 26 atributos
   en 14 archivos — ejemplos («Familia Crosswell, El Fogón»), formatos («Sólo
   dígitos») y hasta reglas de negocio que no estaban dichas en ningún otro
   sitio («vacío = sin aviso; 0 = avisar al agotarse»).

   Los dos argumentos que lo tumban, y ninguno es de gusto:

   · ES EL PEOR SITIO PARA PONER ALGO QUE HAGA FALTA. Se borra al primer
     carácter, o sea exactamente cuando el usuario querría comprobar contra el
     ejemplo lo que acaba de escribir. Una regla que sólo se ve con el campo
     vacío es una regla que no se puede consultar.
   · MIENTRAS SE VE, MIENTE. Un campo vacío con texto dentro se lee como si el
     registro TUVIERA ese dato. Ese argumento ya estaba escrito en el proyecto
     —el peek borraba los marcadores al abrir en sólo lectura, por eso mismo—
     pero acotado a una pantalla; la regla lo generaliza y lo ataca en el
     origen, así que aquella limpieza se quedó sin trabajo y se retiró.

   DÓNDE VA ENTONCES LO QUE HAY QUE DECIR: en algo que no se borre — el propio
   rótulo, un `.dlg-nota` bajo el control, o el `.lede` del diálogo. Lo que NO
   se hace es devolverlo al placeholder porque «ahí se ve más limpio».

   LO QUE SOBREVIVE SON CADENAS CONTADAS, no «los casos donde se vale» — y esa
   es la mitad que vuelve la regla vigilable, porque un criterio («cuando el
   control vive suelto en una barra») lo interpreta cada autor a su manera y una
   lista de textos exactos no. Son dos, las dos nombran un control que no puede
   llevar rótulo, y las dos las fijó Arif el mismo día:

   · `Buscar…` — el buscador de cabecera, el de las tablas. Once en la app, y
     con elipsis: hasta ese día diez decían `Buscar` y uno `Buscar…`.
   · `Seleccionar…` — el combo sin elegir. Vive en UNA constante de `combo.js`,
     no en el marcado, porque los tres combos decían tres frases distintas para
     el mismo estado.

   Aparte, y sin ser marcadores: el mecanismo del cajón alto de la puerta
   (abajo — transparente, sólo para que exista `:placeholder-shown`), el
   compositor de un mensaje, y el ejemplo por país del teléfono, que
   `telefono.js` calcula para el aviso de dígitos y ya no pinta.

   El cajón alto con la etiqueta anidada (`.campo-flotante`, abajo) es la
   ÚNICA excepción y vive sólo en la puerta — el login y, desde el 2026-09-04,
   `restablecer.php`, que es la misma puerta por otra ranura. El catálogo ya lo decía —«se usa
   donde el campo tiene que sostenerse solo, hoy la puerta»— y aun así lo
   copiaron cinco páginas con 33 campos entre las cinco (2026-08-11): una
   recomendación no frena una copia, así que aquí queda como regla.

   LA FICHA DEL PEEK USA ESTO, sin clase propia (2026-08-11). Tuvo la suya
   —`.fx-campo`, el mismo campo con el hueco a 3px y la etiqueta en el
   terciario— y sobrevivió a su propia migración por ir después en la hoja:
   la ficha se quedó predicando lo contrario de esta sección hasta que se
   borró la variante. Hoy apila `.campo` en un `.campos` como cualquier otro
   formulario del sistema, y por eso no hay nada que sincronizar cuando esta
   regla cambie. */
.campo{display:flex;flex-direction:column;gap:5px;min-width:0}
/* La etiqueta va en el color del TEXTO, no en el terciario de los rótulos
   (corrección de Arif, 2026-08-11): es lo que le dice al usuario de qué es el
   campo que va a llenar, así que tiene que leerse sin esfuerzo — atenuarla es
   ahorrar tinta justo en la palabra que sostiene el formulario.

   Y A 14 DESDE EL 2026-08-17 (corrección de Arif mirando los modales). Estuvo
   a 13 y el argumento de entonces era que el tamaño ayudaba a separarla del
   valor. Lo que lo tumba no es el gusto: es que **el sistema ya se
   contradecía**. El label de una CASILLA (`.dlg-check`) siempre midió 14 por
   heredar `--fs-base`, así que la misma palabra —«Nombre», «Ciudad»,
   «Correo»— se pintaba a dos tamaños según qué control tuviera al lado.
   Medido en vivo antes de tocar nada: 86 rótulos de campo a 13 contra 14
   rótulos de casilla a 14, en las mismas pantallas y a menudo en el mismo
   diálogo.

   La jerarquía con el valor NO se pierde, porque nunca la sostuvo el tamaño:
   la hacen el **peso** (500 contra normal) y la **posición** — el rótulo está
   SIEMPRE encima de lo suyo y no compite con nada. Ese era el argumento del
   bloque de la escala («la jerarquía la lleva el COLOR, no el tamaño») y aquí
   se aplica hasta el final en vez de a medias. */
.campo > label{font-size:var(--fs-base);color:var(--app-fg);font-weight:500}
/* El control NO tiene que acordarse de pedir `.ctl`: dentro de un campo lo
   hereda por posición. Un campo que se ve crudo porque a alguien se le olvidó
   una clase es el fallo que este componente existe para no tener. */
.campo input,.campo select,.campo textarea{width:100%}
/* Bloque de campos apilados. Sustituye a `.puerta-campos` fuera de la puerta:
   aquél FUSIONA las esquinas de la pareja del login —un gesto de esa pantalla,
   no de un formulario— y con seis campos dejaba una tira sin junturas. */
.campos{display:flex;flex-direction:column;gap:10px}
.campos.dos{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.campos .ancho{grid-column:1 / -1}

/* ── Campo de etiqueta flotante ───────────────────────────────────────────
   El cajón alto donde la etiqueta VIVE DENTRO del campo: centrada mientras
   está vacío, y encogida arriba en cuanto hay foco o texto. Portado del login
   de Controly (2026-08-10), que lo consigue con .form-floating de Bootstrap 5;
   aquí se reimplementa con los tokens de la casa porque Bootstrap se retiró.

   El mecanismo es el de Bootstrap y conviene no reinventarlo: el input LLEVA
   placeholder real (que se pinta transparente) sólo para que exista el
   selector :placeholder-shown — que es lo que distingue "vacío" de "con
   texto" sin una línea de JS. Quitar el atributo placeholder rompe el efecto
   y no da ningún error. */
.campo-flotante{position:relative;display:flex;flex-direction:column}
.campo-flotante input{
  background:var(--ctl-bg);
  border:1px solid var(--ctl-filo);
  border-radius:var(--r-ctl);
  /* 56px de alto: la etiqueta encogida y el texto conviven sin apretarse */
  height:56px;padding:16px 14px 0;
  font-size:var(--fs-base);color:var(--app-fg);
  outline:none;font-family:inherit;box-sizing:border-box;
  transition:border-color var(--app-dur) var(--app-ease),
             box-shadow var(--app-dur) var(--app-ease),
             padding-top var(--app-dur) var(--app-ease);
}
/* transparente, no ausente: el selector lo necesita (ver arriba) */
.campo-flotante input::placeholder{color:transparent}
.campo-flotante label{
  position:absolute;top:0;left:0;
  padding:18px 14px;
  font-size:var(--fs-base);color:var(--app-fg-tertiary);
  pointer-events:none;
  /* La etiqueta NO cambia de color con el foco: es rótulo, no estado. Quien
     dice "estás aquí" es el filo teal del campo, y decirlo dos veces compite
     con él. Mismo criterio que el .form-floating de Controly. */
  transition:transform var(--app-dur) var(--app-ease),
             font-size var(--app-dur) var(--app-ease);
}
/* Con texto o con foco: el campo baja su texto y la etiqueta sube.
   HOY LA ETIQUETA NO ENCOGE, SÓLO SUBE (corregido el 2026-09-19): pasa de
   `--fs-base` a `--fs-micro`, y desde el 2026-09-08 los dos valen 14px, así
   que lo único que cambia es el `translateY(-10px)`. Este comentario decía que
   encogía de 14 a 13 con el token y que ese 13 era «el estado encogido» de un
   rótulo de 14; eso dejó de ocurrir cuando el piso subió a 14.

   Lo que sigue en pie es por qué el cambio va por el TOKEN y no con
   `transform:scale`: Bootstrap encoge con `scale(.85)`, que sobre 14px da
   11.9, por debajo del piso de 14 del sistema, y por una vía que ninguna
   auditoría de tamaños detecta, porque en la hoja no aparece ningún
   `font-size` chico. */
.campo-flotante input:focus,
.campo-flotante input:not(:placeholder-shown){padding-top:24px}
.campo-flotante input:focus ~ label,
.campo-flotante input:not(:placeholder-shown) ~ label{
  font-size:var(--fs-micro);transform:translateY(-10px);
}
.campo-flotante input:hover{border-color:var(--ctl-filo-hover)}
.campo-flotante input:focus{border-color:var(--ctl-foco);box-shadow:0 0 0 4px var(--ctl-halo)}
/* El halo lo sube el CONTENEDOR, no el input: así la etiqueta viaja con él y
   no queda mordida por el campo hermano de la pareja fusionada. */
.campo-flotante:focus-within{z-index:1}

/* Fusión de la pareja: mismo gesto de antes, ahora sobre el contenedor. */
.puerta-campos .campo-flotante:first-child input{border-bottom-left-radius:0;border-bottom-right-radius:0}
.puerta-campos .campo-flotante:last-child{margin-top:-1px}
.puerta-campos .campo-flotante:last-child input{border-top-left-radius:0;border-top-right-radius:0}

/* NOTA (2026-08-11): este bloque nació por la mañana para arreglar los selects
   crudos de la alta de cita y la config de agenda, y por la tarde se quedó SIN
   CONSUMIDOR — esos formularios pasaron al campo del sistema y la puerta, la
   única superficie que conserva el cajón alto, no tiene ni un select. Se
   conserva sólo esta explicación; el código se fue con ellos.
   Lo que decía, por si el cajón alto vuelve a necesitar uno: un select SIEMPRE
   tiene valor, así que no hay estado «vacío» ni `:placeholder-shown` que lo
   detecte — su etiqueta nace encogida y se queda ahí.
*/

/* ── Mostrar / ocultar contraseña ─────────────────────────────────────── */
.campo-flotante input[type="password"],
.campo-flotante input.es-clave{padding-right:46px}
.ver-clave{
  position:absolute;right:6px;top:50%;transform:translateY(-50%);
  display:flex;align-items:center;justify-content:center;
  width:34px;height:34px;padding:0;
  background:none;border:0;border-radius:50%;
  /* Secundario y no terciario: es el mismo escalón que el icono del menú
     (2026-08-10). El terciario es el gris de lo apagado, y este ojo es el
     único mando del campo — leerse como deshabilitado era justo lo contrario
     de lo que hace. */
  color:var(--app-fg-secondary);cursor:pointer;
  transition:color var(--app-dur) var(--app-ease),
             background var(--app-dur) var(--app-ease);
}
.ver-clave:hover{color:var(--app-fg);background:var(--ctl-borde)}
.ver-clave:focus-visible{outline:2px solid var(--ctl-foco);outline-offset:1px}

/* ── Defensa contra el autofill de Chrome ─────────────────────────────────
   Chrome pinta el campo autocompletado con SU amarillo y no hay propiedad que
   lo cambie: `background` no le gana. El único recurso es taparlo con una
   sombra interior enorme del color propio, y forzar el color de la letra con
   -webkit-text-fill-color (`color` tampoco le gana).
   Va con un token OPACO a propósito: --ctl-bg es translúcido en oscuro y
   dejaría pasar justo el amarillo que se quiere tapar.
   El `transition ... 5000s` no anima nada — es el truco conocido para que el
   repintado del fondo quede aplazado tanto que nunca ocurra.
   Y las dos últimas reglas son las que casi siempre se olvidan: al
   autocompletar, Chrome NO dispara :not(:placeholder-shown), así que sin
   ellas la etiqueta se quedaría encima del texto ya precargado. */
.campo-flotante input:-webkit-autofill,
.campo-flotante input:-webkit-autofill:hover,
.campo-flotante input:-webkit-autofill:focus{
  -webkit-box-shadow:0 0 0 1000px var(--ctl-bg-solido) inset;
  -webkit-text-fill-color:var(--app-fg);
  caret-color:var(--app-fg);
  transition:background-color 5000s ease-in-out 0s;
}
.campo-flotante input:-webkit-autofill{padding-top:24px}
.campo-flotante input:-webkit-autofill ~ label{font-size:var(--fs-micro);transform:translateY(-10px)}
.puerta-caja .btn-acento{padding:8px 14px}
/* EL BOTÓN DEL LOGIN: la ÚNICA excepción de tamaño del sistema (2026-08-26,
   pedido de Arif, y declarada como excepción a propósito).

   Todo botón de la app mide `--fs-base` (14). Éste va a 15 porque es el único
   botón de su pantalla y no compite con nada: no hay barra, ni tabla, ni un
   segundo control con el que tenga que medir igual — la misma razón por la
   que el catálogo ya llamaba a la puerta «lo único que rompe el alto y la
   forma». A 14 se leía por debajo del rótulo que tiene encima, que ahora
   también mide 15.

   Y `letter-spacing` POSITIVO, que en esta hoja no lo lleva nada más: los
   títulos van con tracking NEGATIVO (`-.01`/`-.02em`) porque aprietan cuerpos
   grandes. Aquí el efecto buscado es el contrario —abrir un texto corto dentro
   de una cápsula ancha— y `.02em` a 15px son 0.3px por letra: se nota como
   aplomo, no como separación. Subirlo más lo volvería un botón de versalitas,
   que es otra cosa.

   BAJO `.puerta-login`, por lo mismo que el rótulo: el `.puerta-caja` de
   arriba lo comparten las seis pantallas públicas y sus botones («Enviar»,
   «Agendar») siguen midiendo lo que mide toda la app. La excepción es de UNA
   pantalla, y el selector lo dice. */
.puerta-login .btn-acento{
  font-size:var(--fs-dialogo);letter-spacing:.02em;
}
/* .puerta-error se retiró (2026-08-08): el error de la puerta es una
   .alerta warning, como cualquier otro estado que vive en la página. */

/* ── Alerta ──────────────────────────────────────────────────────────────
   El equivalente del .alert de Bootstrap, dicho en el idioma de la casa
   (2026-08-08): tinte de badge de fondo, texto SIEMPRE el del cuerpo — aquí
   no hay letra de color; lo que antes se decía con una frase café o teal se
   dice con esto — y un icono del catálogo que sí lleva el token `-text` de
   su color: los `-text` ya no escriben, PINTAN GLIFOS. A diferencia del
   aviso (que sube, dice lo suyo y se va), la alerta VIVE en la página
   mientras la condición exista. Mismos modificadores que el badge: un solo
   vocabulario de estado.

   DENTRO DE UN DIÁLOGO (movido del catálogo `ui.php`, 2026-09-19; entró el
   2026-09-11): sí, pero sólo cuando la condición la CALCULA el servidor
   mientras llenas el formulario. En la hoja de confirmación no entra: su
   consecuencia es fija y ya la dice el `.lede`, y repetirla teñida sería la
   misma frase dos veces. El caso que la admite es el diálogo del horario, que
   pregunta al modelo qué días del rango marcado chocan con uno que ya existe:
   la respuesta aparece y desaparece según lo que marques, y es información
   que no estaba en la pantalla. Va donde iría una `.dlg-nota`, dentro del
   `.campo` y bajo el control del que habla. Va en INFO y no en ámbar: los días
   libres se guardan igual y el ocupado se queda como está, así que no hay nada
   que corregir; sube a `warning` sólo cuando TODOS los días marcados chocan,
   que es el caso en que el gesto no consigue nada. Medido ese día sobre el
   papel del diálogo: 14.36:1 el texto en claro y 9.94:1 en oscuro (ámbar:
   15.37 y 7.04).

   Y el CANTO (2026-08-10), lo único que faltaba del original: el `.alert` de
   Bootstrap se delimita con un filo del color de su estado, y la alerta se
   apoyaba sólo en el tinte — que llena pero no cierra. Va en `--canto-*`, derivado del
   `-text` de su estado: el mismo token que pinta el icono, porque tienen el
   mismo trabajo —leerse contra el relleno—. El relleno NO sube de alpha, que
   es donde se paga el contraste del texto. Del clon se tomó el filo y nada
   más: la letra de color de Bootstrap no entra —el texto sigue siendo el del
   cuerpo— y el info es el teal de la casa, no su cian. */
/* CONTRATO: un icono (el `::before`) y UN SOLO hijo. Es flex, así que si el
   mensaje mete `<strong>` o `<code>` sueltos, cada uno se vuelve una COLUMNA y
   la frase sale troceada en tiras verticales — medido el 2026-08-10 en el
   banner de `automatizacion.php`. Un mensaje con formato va envuelto en un
   `<span>`. */
.alerta{
  display:flex;align-items:flex-start;gap:9px;
  background:var(--badge-info);color:var(--app-fg);
  border:1px solid var(--canto-info);
  border-radius:var(--r-ctl);padding:10px 12px;
  font-size:var(--fs-base);line-height:1.45;
}
/* El icono de la alerta MARCA el bloque —es lo primero que se busca, y la
   frase viene después—, así que mide `--icn-fila` igual que el del menú y no
   el 16 de los que acompañan. Sin `margin-top`: a 16 había que empujarlo 2px
   para casarlo con la primera línea; a 20 su caja ya cubre los 20.3 de esa
   línea (14 × 1.45) y el empujón lo descolgaría.

   El COLOR sí se queda, y es la única excepción a la regla de iconos neutros
   (2026-08-10): aquí el color no decora, ES el estado — el mismo token `-text`
   que el tinte del fondo. La regla nueva mató las tintas que no significaban
   nada; ésta significa. */
.alerta::before{
  content:'';flex:none;width:var(--icn-fila);height:var(--icn-fila);
  background:var(--app-info-text);
  -webkit-mask:var(--icn-info) no-repeat center / contain;
          mask:var(--icn-info) no-repeat center / contain;
}
.alerta.warning{background:var(--badge-warning);border-color:var(--canto-warning)}
.alerta.warning::before{background:var(--app-atencion-text);-webkit-mask-image:var(--icn-atencion);mask-image:var(--icn-atencion)}
.alerta.danger{background:var(--badge-danger);border-color:var(--canto-danger)}
.alerta.danger::before{background:var(--app-peligro-text);-webkit-mask-image:var(--icn-fallo);mask-image:var(--icn-fallo)}
.alerta.success{background:var(--badge-success);border-color:var(--canto-success)}
.alerta.success::before{background:var(--app-exito-text);-webkit-mask-image:var(--icn-exito);mask-image:var(--icn-exito)}

/* ── Aviso (toast) ──────────────────────────────────────────────────────
   El MISMO tinte que los badges —info el normal, ámbar el de error— y el
   texto del cuerpo, oscuro en claro y claro en oscuro: un aviso es estado
   que flota, así que habla el idioma de los badges y no inventa una letra
   de color. Como el tinte es translúcido y el aviso flota SOBRE contenido,
   se compone encima del papel (background-image sobre background-color) —
   así el token del badge se reutiliza literal sin que la sábana se
   transparente por debajo. */
.aviso{
  position:fixed;left:50%;bottom:-60px;transform:translateX(-50%);
  background:var(--app-paper);
  background-image:linear-gradient(var(--badge-info),var(--badge-info));
  border:1px solid var(--app-hairline);color:var(--app-fg);
  border-radius:var(--r-caja);padding:8px 8px 8px 16px;font-size:var(--fs-base);font-weight:500;
  box-shadow:0 6px 24px var(--app-shadow);z-index:60;
  display:flex;align-items:center;gap:10px;max-width:min(560px,calc(100vw - 32px));
  opacity:0;transition:bottom var(--app-dur) var(--app-ease),opacity var(--app-dur) var(--app-ease);
  pointer-events:none;
}
.aviso.visible{bottom:76px;opacity:1}
.aviso.error{background-image:linear-gradient(var(--badge-warning),var(--badge-warning))}

/* LOS CONTROLES DEL AVISO (2026-09-03). El aviso pasó de 2600 a 7800 ms —el
   triple, pedido de Arif— y eso sólo es aceptable con una salida: un mensaje
   que se queda ocho segundos sin poder callarse es ruido, no confirmación. La
   × y la duración larga son la MISMA decisión y entraron juntas.

   `pointer-events` es lo único delicado del bloque. La caja sigue en `none`
   —flota centrada sobre el contenido y comerse los clics de lo que tapa sería
   pagar la × con una regresión en toda la app— y son los dos controles los que
   lo reactivan. Es el reparto correcto: clicable exactamente lo que se pulsa.

   La × repite el gesto del diálogo (`.dlg-x`) a propósito: es la misma promesa
   —«esto se cierra aquí»— y aprenderla dos veces sería el sistema desafinando.
   Lo que no repite es el tamaño: aquí vive dentro de un renglón de 8px de aire,
   así que va a la medida del texto y no a la del título de un diálogo. */
.aviso-texto{min-width:0}
.aviso-x,.aviso-accion{pointer-events:auto;font-family:inherit;cursor:pointer;flex:none}
/* Hereda el tratamiento de `.dlg-x` —relleno tenue, filo interior, píldora— y
   sólo cambia la MEDIDA: allí es el control de la esquina de una hoja y aquí
   vive en un renglón de 8px de aire, así que se calcula sobre `--fs-meta`. */
.aviso-x{
  width:calc(var(--fs-meta) * var(--btn-lh) + var(--btn-py) * 2);
  height:calc(var(--fs-meta) * var(--btn-lh) + var(--btn-py) * 2);
  border-radius:var(--r-pill);border:0;
  background:var(--app-relleno-tenue);color:var(--app-fg-secondary);
  box-shadow:inset 0 0 0 1px var(--app-borde-muted);
  display:flex;align-items:center;justify-content:center;
  font-size:16px;line-height:1;
  transition:background var(--app-dur) var(--app-ease),color var(--app-dur) var(--app-ease);
}
.aviso-x:hover{background:var(--app-chip);color:var(--app-fg)}
/* La ACCIÓN existe para el «Abrir» del contacto parecido, que vivía al pie del
   diálogo del alta y se quedaba sin sitio cuando el alta pasó a cerrarse al
   crear. Va con la letra de un ENLACE y no con la caja de un botón: dentro de
   un aviso, que ya es una caja flotante, un segundo botón con caja se lee como
   dos avisos pegados. */
.aviso-accion{
  border:0;background:none;color:inherit;font-size:inherit;font-weight:600;
  padding:0;text-decoration:underline;text-underline-offset:2px;
}
.aviso-accion:hover{text-decoration-thickness:2px}
.aviso-accion[hidden]{display:none}

/* ── Menús de la barra de selección ───────────────────────────────────
   CUELGAN HACIA ARRIBA, y eso es una consecuencia del sitio de la barra, no
   una preferencia: al pie, arriba es el único lado con sitio —abajo sólo
   quedan los 12px de padding del panel, que además recorta
   (`overflow:hidden`)—. Si la barra vuelve a subir, esto cambia a
   `top:34px` en el mismo movimiento; se probó el 2026-08-17 y volvió con
   ella. La cota (34) no depende del lado. */
.bs-accion.con-menu{position:relative}
/* `min-width` y no un ancho: el menú es `absolute`, así que su shrink-to-fit se
   mide contra el `.bs-accion` que lo ancla —83px— y sin piso quedaría del ancho
   de la palabra «Descartar». El piso subió de 190 a 200 el 2026-09-02, medido
   contra la opción más larga que el sistema tiene: «Pidió no ser contactado»
   envolvía a dos renglones en 190 y entra justa en 200. Lo que la hacía caber
   antes era el `.ancho` de 330 que el menú de descarte llevaba por su segundo
   renglón; al morir ese renglón, 330 dejaba 130px de aire a la derecha y el
   menú se leía como si esperara una columna que ya no existe. */
.menu-flotante{
  position:absolute;left:0;bottom:34px;min-width:200px;
  background:var(--app-paper);border:1px solid var(--app-hairline);
  border-radius:var(--r-caja);box-shadow:0 10px 36px var(--app-shadow);
  padding:5px;z-index:30;display:none;flex-direction:column;
  font-family:var(--fuente-titulo);font-weight:400;text-align:left;
}
.menu-flotante.ancho{min-width:330px}
.menu-flotante.abierto{display:flex}

/* ── EL SABOR `fixed`: el menú que vive fuera de su anfitrión (2026-09-03) ──
   Lo estrena el catálogo de etiquetas del peek, y la razón es la misma que ya
   obligó a `combo.js` y a `telefono.js` a colgar su lista del `<body>`: el
   cuerpo del peek es `overflow-y:auto` y el panel `overflow:hidden`, así que
   una capa `absolute` colgada ahí dentro se RECORTA. Aquí no cuelga de nadie —
   es hermana de `#peek` en el marcado— y se coloca contra el rectángulo del
   botón que la abre, recolocándose al desplazar.

   Las coordenadas las pone el JS (`colocarMenuEtq`, peek.js) porque dependen de
   dónde haya quedado ese botón tras el scroll; lo que sí es del CSS es que la
   lista tenga TECHO y su propio scroll: un catálogo de cien etiquetas no puede
   crecer hasta salirse de la pantalla. El techo real lo calcula el JS contra el
   hueco disponible, y este `max-height` es sólo el suelo del cálculo. */
/* El nombre era `pk-menu` hasta el 2026-09-09, y lo perdió al ganar su segundo
   consumidor: el menú de opciones de la columna del tablero, que no tiene nada
   de peek y necesita este sabor por lo mismo que aquél —su anfitrión recorta—.
   Un prefijo de componente en una pieza compartida obliga al segundo a
   escribir el nombre del primero, que es como una clase termina significando
   «el sitio donde nació» en vez de «lo que hace». */
.menu-flotante.fijo{position:fixed;bottom:auto;z-index:70}
.pk-menu-lista{
  display:flex;flex-direction:column;
  overflow-y:auto;min-height:0;
}
.menu-flotante .op{
  padding:6px 9px;border-radius:var(--r-ctl);cursor:pointer;
  display:flex;flex-direction:column;gap:1px;
}
.menu-flotante .op:hover{background:var(--app-chip)}
/* ── LA OPCIÓN QUE ADEMÁS ES UN BOTÓN (2026-09-09) ────────────────────
   Las `.op` del sistema nacieron `<span>` porque sus menús se abren y se
   recorren con el ratón. El del tablero no puede permitírselo: sustituye a los
   tres glifos de la cabecera, y uno de los cargos contra aquéllos era
   justamente que el teclado no los alcanzaba — devolver el gesto en un menú
   igual de inalcanzable habría arreglado la mitad del problema y escrito la
   otra mitad de nuevo. Como `<button>`, el foco, el Enter y el Espacio los da
   el navegador y no hay que programarlos.

   La forma de la regla es la que el sistema ya usa para esto: `button.sb-item`
   —la fila del riel, que también es un botón vestido de otra cosa— apaga
   exactamente lo mismo. `font` y `color` van con `inherit` porque el
   `.menu-flotante` fija su propia familia y su peso, y un botón no los hereda
   solo. Las `.op` que siguen siendo `<span>` no se tocan: allí el cambio no
   compra nada y tocarlas movería cuatro menús vivos. */
button.op{
  background:transparent;border:0;width:100%;
  font:inherit;color:inherit;text-align:left;
}
/* AQUÍ VIVIÓ `.menu-flotante .op em` —el segundo renglón de una opción, en
   Geist porque era explicación y no navegación—, retirado el 2026-09-02 con su
   único consumidor: la `ruta` del catálogo de motivos, que era el consejo del
   outreach. El `flex-direction:column` de `.op` es lo que queda de esa forma de
   dos renglones y se deja: no cambia nada con un solo hijo, y es lo que
   sostendría el segundo si alguna opción vuelve a necesitarlo. */

/* Caja del input dentro de un menú — hoy sólo la del etiquetado en lote.
   Sobrevivió al retiro del motor de filtro (2026-08-09): el resto de ese
   bloque (.menu-panel, .menu-suelto, .mp-titulo, .mv-y) se fue con él. */
.mv-caja{display:flex;align-items:center;gap:6px;padding:4px 5px 7px}
.mv-input{flex:1;min-width:0}

.menu-flotante .sep{height:1px;background:var(--app-hairline);margin:4px 6px;flex:none}
/* LA OPCIÓN DESTRUCTIVA DE UN MENÚ DE FILA (2026-09-19, plan del catálogo,
   etapa 6). Nace con el menú «···» de las filas del Webmaster: «Reiniciar» y
   «Eliminar» dejan de ser dos `.btn-segunda` iguales a «Plan» y bajan al menú,
   bajo un `.sep`, con el mismo tratamiento que `.bs-accion.danger` en la barra
   de selección —tinta `--app-peligro-text` y el tinte del badge al apuntar—,
   porque es la misma señal: una PALABRA que dice de qué clase es entre sus
   vecinas, no un botón de peligro. La pregunta de cada una sigue siendo
   obligatoria (su diálogo con la palabra). La condición de uso la fija
   ui.php → Las reglas → «Hasta dos gestos, botones; más, un menú». */
.menu-flotante .op.danger{color:var(--app-peligro-text)}
.menu-flotante .op.danger:hover{background:var(--badge-danger)}
/* La línea del menú que NO es una opción: explica por qué no hay opciones que
   ofrecer. La estrenó el menú del tablero (2026-08-15), cuando la selección
   mezcla leads de dos funnels y cada uno tiene su propio juego de etapas —
   ahí no hay una etapa buena que proponer, y un menú vacío no dice nada.
   Sin cursor ni hover: se lee, no se pulsa. Habla como una `.tramo-nota` y
   respeta el mismo margen lateral que las opciones, para que el bloque de
   texto arranque en la vertical de los rótulos que sustituye. */
.menu-flotante .op-nota{
  padding:6px 9px;font-size:var(--fs-meta);color:var(--app-fg-secondary);
  font-family:var(--fuente);line-height:1.45;max-width:34ch;
}

/* AQUÍ VIVIÓ `.canal-sel` —el select de canal del compositor de toques— y con
   ella `.registrar-toque input`, que sólo existía dentro de los selectores
   compartidos de más arriba. Las dos se fueron con `t_accion` el 2026-09-02 y
   se depuraron en la barrida final del desmontaje, que es lo que ese día quedó
   escrito aquí como pendiente: tocar tres listas de selectores largas de las
   que cuelga media app pedía medir el antes/después, y eso es lo que hizo la
   barrida —catorce controles vivos × dieciocho propiedades, en base, hover y
   foco, idénticos antes y después—. Su hermano `.bitacora-cargando` se había
   ido antes, sustituido por `.notas-vacio`. */

/* ── Enlace ─────────────────────────────────────────────────────────────
   Space Grotesk y SIEMPRE con su icono delante. El icono lo pone el CSS
   (SVG en máscara, pintado con currentColor), no el marcado: así es
   obligatorio por construcción —ningún enlace puede olvidarlo— y no hay
   librería de iconos que cargar ni mantener. Es la única letra de Space
   Grotesk por debajo de 20px: los enlaces son pocos, cortos, y el icono
   los ancla. */
/* Desde el 2026-08-16 NO lleva color propio: hereda la letra del cuerpo, como
   cualquier enlace (ver la regla de `a`, arriba). Lo que la sigue distinguiendo
   dentro de una frase es lo que siempre la distinguió de verdad: su flecha y su
   Space Grotesk. El azul que llevaba era la última excepción de «el azul
   escribe». */
/* EL SUBRAYADO EN REPOSO YA VA AL COLOR DE LA LETRA (2026-09-11, decisión de
   Arif). Con el azul retirado el 2026-08-16, la línea quedó siendo la señal
   principal de que una palabra se pulsa —y la dibujaba `--app-borde-muted`,
   el mismo filo tenue que un `<a>` suelto—. Ese tono estaba calculado para
   que un párrafo con tres enlaces no se lea rayado, pero `.enlace` no es el
   caso que lo justificaba: es escaso, corto y ya viene marcado por su flecha
   y su Space Grotesk, así que subirlo a `currentColor` no raya nada y le
   devuelve el peso que perdió al irse el color.
   El estado de reposo se come entonces lo que antes era el hover, y el hover
   necesita un escalón propio: lo da el GROSOR, no el color —a 2px, que es el
   mismo recurso y el mismo número con que `.aviso-accion` marca su hover—.
   Color para decir «esto se pulsa», grosor para decir «lo estás apuntando».
   La transición va aquí porque casi todo `.enlace` es un `<span>` y no hereda
   la de `a`; se anima el grosor, que es lo único que ahora cambia.
   Y el `offset` sube a 3px —uno más que el 2 de `a`— porque las dos cosas que
   acaban de cambiar empujan la línea contra las bajas: al color de la letra ya
   no se confunde con el descendente de una `j` o una `p`, la toca; y en hover
   el grosor crece HACIA ABAJO, así que el 2 que bastaba para una línea tenue
   se queda corto para una de 2px. El valor es propio de `.enlace` a propósito:
   el 2 de `a` está calculado para las bajas de la Geist y ésta es la única
   pieza que va en Space Grotesk por debajo de 20px. */

/* LA FLECHA DEJA DE SER UN ICONO Y PASA A SER TEXTO (2026-09-11, decisión de
   Arif tras ver la pieza en un titular). Con el icono delante, el subrayado
   arrancaba en la primera letra y la flecha quedaba fuera: `text-decoration`
   se propaga a los descendientes en línea pero NO se dibuja a través de una
   caja atómica, y un `::before` con `display:inline-block` lo es.

   SE INTENTÓ ANTES, Y FALLÓ, dibujar a mano el tramo que faltaba con un
   `linear-gradient` del ancho del icono, calzado contra el subrayado por
   medición (`baseline+3` a `+4`). En pantalla se veía el escalón entre las dos
   líneas, y el hover sólo engordaba la del texto. La lección es de forma, no
   de aritmética: **dos líneas que deben coincidir acaban no coincidiendo**, y
   ninguna medición lo arregla porque el subrayado se posiciona con las
   métricas de la fuente y el fondo con la caja — dos orígenes distintos que
   nada obliga a moverse juntos.

   La raíz es que la flecha estuviera FUERA del texto. Ahora es texto: un
   carácter (`↗`, U+2197) en el `::after`, dentro del mismo flujo, así que el
   subrayado lo cubre como cubre cualquier letra. Una sola línea: no puede
   desalinearse consigo misma, y el hover la engorda entera.

   TRES DETALLES QUE NO SON GRATUITOS:
     · **Va DETRÁS**, no delante (Arif). Delante, la flecha precede a la cosa
       que nombra; detrás, el enlace se lee y la flecha cierra diciendo a dónde
       lleva. Y es lo que permite el punto anterior sin un hueco inicial.
     · El espacio es un **nbsp dentro del `content`**, no un `margin`: un
       margen no se subraya y dejaba un corte de 3px antes de la flecha.
     · El `\FE0E` (VS15) fuerza la presentación de TEXTO del carácter; sin él
       un navegador puede pintarlo como emoji de color.

   LO QUE SE PIERDE, y se acepta a ojos abiertos: el glifo ya no es el SVG de
   la casa (retícula de 14, trazo 1.5) sino el de la fuente que lo resuelva, y
   ni Space Grotesk ni Geist traen U+2197 — medido, no supuesto: la «A» mide
   8.85 en Space Grotesk, 9.64 en Geist y 10.11 pidiendo una familia que no
   existe, o sea cada una pone su propio glifo; la flecha mide **14 en las
   tres**, incluida la inexistente, así que las tres caen en el mismo respaldo
   del sistema. Puede por tanto variar de un SO a otro. A cambio, la pieza deja de tener dos
   mitades que sincronizar. Sigue siendo obligatorio por construcción: lo pone
   el CSS, no el marcado, así que ningún enlace puede olvidarlo.

   El color no se declara: es el de la letra, como todo lo demás desde que el
   azul se retiró el 2026-08-16. */
.enlace{
  cursor:pointer;font-size:var(--fs-base);
  font-family:var(--fuente-titulo);font-weight:500;
  text-decoration:underline;text-decoration-color:currentColor;
  text-decoration-thickness:1px;text-underline-offset:3px;
  transition:text-decoration-thickness var(--app-dur) var(--app-ease);
}
.enlace::after{content:'\00A0\2197\FE0E'}
.enlace:hover{text-decoration-thickness:2px}

.imp-soltar{
  border:1.5px dashed var(--app-hairline);border-radius:var(--r-caja);
  padding:34px 24px;text-align:center;
  background:var(--app-group);transition:background var(--app-dur) var(--app-ease);
}
.imp-soltar.encima{background:var(--app-accent-tint);border-color:var(--app-accent)}
.imp-soltar-txt{font-size:var(--fs-base);font-weight:500}
.imp-soltar-nota{
  font-size:var(--fs-meta);color:var(--app-fg-tertiary);
  margin-top:7px;max-width:520px;margin-left:auto;margin-right:auto;line-height:1.5;
}
.imp-soltar-nota code{
  background:var(--app-chip);border-radius:var(--r-mini);padding:1px 5px;
  font-family:ui-monospace,monospace;font-size:.92em;color:var(--app-fg-secondary);
}

/* Se dice qué se detectó, no se asume en silencio: ver aquí que la
   codificación salió mal es más barato que descubrirlo con 1,200 Ã±. */
.imp-detectado{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:16px}
.imp-detectado .det{
  background:var(--app-chip);border-radius:var(--r-ctl);padding:4px 10px;
  font-size:var(--fs-meta);color:var(--app-fg-secondary);
}
.imp-detectado .det b{color:var(--app-fg);font-weight:600}

.imp-sel{
  background:transparent;border:0;font-family:inherit;
  font-size:var(--fs-base);color:inherit;outline:none;cursor:pointer;
  max-width:190px;
}
.imp-dest.auto .imp-marca{color:var(--app-fg-secondary)}

.imp-tarjetas{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-bottom:8px}
.imp-tarjeta{
  background:var(--app-group);border-radius:var(--r-ctl);padding:16px 18px;
}
.imp-tarjeta .num{font-family:var(--fuente-titulo);font-size:var(--fs-titulo);font-weight:700;line-height:1.1}
.imp-tarjeta .lbl{font-size:var(--fs-meta);color:var(--app-fg-secondary);margin-top:3px}
/* La variante tiñe la TARJETA, no el número: cifra de color = texto de
   color, y eso murió el 2026-08-08. El tinte es el de los badges. */
.imp-tarjeta.bien{background:var(--badge-info)}
/* `ojo` y no `aviso`: .aviso es el toast global (position:fixed, fuera de
   pantalla) y aplicárselo a una tarjeta la manda al limbo. Choque real,
   invisible en el DOM y evidente en la captura. */
.imp-tarjeta.ojo{background:var(--badge-warning)}

/* ── CRUD manual: etiquetas y papelera ──────────────────────────────── */

/* LA TIRA DE CHIPS DE LA FICHA. Fue `.fx-etiquetas`, un CONTENEDOR CON PAPEL Y
   FILO —«no es un input pero se comporta como uno, se teclea dentro»— y esa
   frase dejó de ser cierta el 2026-09-03: el campo de teclear se mudó al menú
   del catálogo, así que dentro ya no se teclea nada. Con él se fueron el fondo,
   el borde, el `min-height` y el `:focus-within`, que era el halo de un control
   sin control dentro.

   Lo que queda es lo único que había que conservar: envolver. Con ocho
   etiquetas la tira crece hacia abajo y no desborda a lo ancho — medido en el
   panel de 420 y a 375px de ventana. */
.pk-etq-tira{display:flex;flex-wrap:wrap;gap:5px;align-items:center;margin-top:7px}
/* `.fx-etq` SE RETIRÓ EL 2026-09-03. Era el chip de etiqueta del editor del
   peek, con `--r-pill`, fondo `--app-paper` y `background` declarado dos veces
   en conflicto; hoy ese sitio usa `.etiqueta.editable`, que es el mismo chip que
   la sábana. Con ella se fue el último uso de `--r-pill` que no era un botón.
   El porqué completo, arriba en el bloque del chip. */

/* LA × DE UN CHIP — la segunda familia de gestos, y por qué son DOS y no una
   (2026-08-16). Un gesto de RENGLÓN (`.cf-btn`) vive al lado de la pieza que
   opera y lleva su propia píldora; esta × vive DENTRO de la pieza que borra, y
   una píldora dentro de otra píldora es una caja dentro de una caja. Así que
   comparte todo lo demás —el terciario en reposo, el relleno al apuntarla— y
   cambia sólo la forma.

   ── SU RADIO PASÓ DE CÍRCULO A `--r-mini` EL 2026-09-03 ──
   Era `50%` porque vivía dentro de una píldora, y ahí un círculo es la única
   forma que no pelea con el contorno. Con el chip a `--r-ctl` (8px), un círculo
   dentro de una esquina de 8px se ve prestado: `--r-mini` (4px) es lo que la
   doctrina de radios de este archivo da a lo que mide menos de 20px, y es
   además el radio con el que el chip la aloja.

   Unifica lo que eran dos clases con dos radios (`.fx-etq-x` a 50% y `.hs-x` a
   un `3px` suelto que violaba la regla de los cinco radios). Y no se tiñe de
   ámbar, que es lo que hacía `.hs-x`: lo que avisa es la pregunta. */
/* ── SU MEDIDA: 24×24, Y ES LA CORRECCIÓN DE UN DEFECTO (2026-09-03) ──
   Era `padding:0 4px` sobre un glifo con `line-height:1`, o sea que NO TENÍA
   caja propia: su tamaño era el del carácter × más ocho píxeles. Medido en el
   navegador a 375px de ancho: **15×13**. Arif lo describió como
   «extremadamente pequeña», y el número le da la razón — el mínimo de
   WCAG 2.2 (Target Size Minimum) son 24×24 CSS px, así que el blanco de toque
   valía menos de un tercio del área exigida.

   Lo que lo mantuvo chico tanto tiempo fue heredar la lógica del CHIP: el chip
   mide 21px porque se diseñó para la fila de 36 de la sábana, y la × se ajustó
   a ese alto en vez de pedir el suyo. Pero **en la sábana no hay ×** —ahí el
   chip es de sólo lectura— así que esa restricción nunca gobernó a esta pieza:
   la × sólo existe donde el chip se EDITA, y ahí no hay fila de 36 que
   respetar. Se estaba pagando un límite de un sitio en el que no vive.

   `flex` y no `padding`, para que los 24 sean el ALTO Y EL ANCHO de verdad y
   no dependan del glifo: un carácter × no tiene métrica estable entre fuentes.
   El glifo se queda del tamaño que tenía (`--fs-micro`, el del chip); lo que
   crece es la caja que se pulsa, que es lo que estaba mal.

   NO ES EL ÚNICO CAMINO PARA QUITAR (movido del catálogo `ui.php`,
   2026-09-19): la misma etiqueta se desmarca en el catálogo que abre
   «Etiquetar», cuyo renglón mide 33px. No es un gesto duplicado: la × es el
   atajo sobre lo que ya se ve y el catálogo es la vía cómoda del mismo hecho. */
/* LA CAJA LA COMPARTEN DOS GESTOS desde el 2026-09-11: la × que quita y el
   lápiz que edita (`.chip-lapiz`, estrenado ese día en las DOS piezas del riel
   de la Agenda — la franja de horario y el renglón de bloqueo). Todo lo de abajo
   —los 24 de toque, el color susurrado, el radio y el hover— es del GESTO DENTRO
   DE UN CHIP, no de la ×; lo único que es suyo es el cuerpo del glifo, y por eso
   ésa es la única línea que se queda a solas. */
.chip-x,.chip-lapiz{
  display:inline-flex;align-items:center;justify-content:center;
  width:24px;height:24px;
  color:var(--app-fg-tertiary);cursor:pointer;
  border-radius:var(--r-mini);line-height:1;flex:none;
  /* Y EL CUERPO DEL GLIFO SUBE A 18, que es la MISMA regla que la × del
     diálogo tiene escrita unas líneas más abajo: «nada aquí es un número a ojo
     salvo el cuerpo del glifo, que es la única cota que no puede derivarse — el
     × necesita ir por encima del texto para que su tinta se lea como marca».
     Aquélla pone 20 en una caja de 26.89, o sea 0.74 de su caja; 18 en 24 es la
     misma proporción, así que las dos ×s de la app se ven de la misma familia
     aunque midan distinto.

     Sin esto, agrandar la caja no arregla la queja entera: con el glifo a 13 el
     × dejaba **7px de tinta** dentro de 24 de blanco, que se lee como un signo
     perdido. Y esos 7px no son casualidad — son exactamente el defecto que esta
     misma app ya diagnosticó el 2026-08-17 en `.bs-cerrar`, descrita entonces
     como «un signo suelto de 7px de tinta». La pieza se unificó aquel día y
     este chip volvió a caer en lo mismo por el otro lado: la caja.

     El lápiz no lo hereda: es un icono y llena su caja, así que su tinta la
     decide `--icn` y no el cuerpo de una fuente. Va a 14 —y no a los 16 de
     `--icn-texto`— para que su mancha iguale la de la ×, que con cuerpo 18
     dibuja ~13px de tinta: a 16 el lápiz pesaba más que la × que tiene enfrente
     y el par dejaba de leerse como dos gestos del mismo rango. */
  transition:background var(--app-dur) var(--app-ease), color var(--app-dur) var(--app-ease);
}
.chip-x{font-size:18px}
.chip-lapiz .icn{width:14px;height:14px;vertical-align:0}
/* `--app-pill` y no `--app-chip` al apuntarla: dentro de un chip que YA es
   `--app-chip`, el hover con el mismo tinte no se ve. Sube un escalón (6.4% →
   8.9%), que es la diferencia mínima que se lee sobre su propio fondo. */
.chip-x:hover,.chip-lapiz:hover{color:var(--app-fg);background:var(--app-pill)}
/* AQUÍ VIVIÓ `.fx-etq-nueva`, el campo de texto libre que iba al final de la
   tira — plano, sin caja propia, porque el contenedor le prestaba el filo y el
   foco. Se retiró el 2026-09-03 con el editor que lo alojaba. Lo que hacía
   —teclear una etiqueta— lo hace hoy el filtro del menú del catálogo, que
   además la ENCUENTRA si ya existe, que era la mitad que faltaba.

   Era el último de la familia de CONTROLES PLANOS, que se quedó en cero ese
   día (movido del catálogo `ui.php`, 2026-09-19): la línea de alta de lead y
   el editor de celda (`.ec-campo` dentro de `.editor-celda`) cayeron antes con
   sus editores. Lo que la familia deja escrito, por si vuelve: un control
   plano se justifica cuando su contenedor YA dibuja el borde y el foco, porque
   darle caja propia pondría dos marcos concéntricos; en cuanto el contenedor
   deja de ser un control, el plano no tiene dónde apoyarse. */

/* ── LA OPCIÓN CON ESTADO — la lista que dice dónde estás (2026-09-03) ──
   Dos semánticas en una sola pieza, porque el problema que resuelven es el
   mismo: un menú de lote que no dice qué tienen los seleccionados obliga a
   cerrarlo, mirar las filas y volver. TRES CONSUMIDORES: los dos menús de
   lote de `app.js` (etiquetas y etapa) y el catálogo de etiquetas de la ficha
   del peek (`peek.js`), donde el sujeto es uno y los tres estados colapsan a
   dos —`.puesta` o sin clase—: el caso degenerado de esta misma pieza, que
   se reusó sin estrenar una sola clase. Corregido el 2026-09-19: decía «DOS
   CONSUMIDORES», el recuento de antes de que llegara el peek.

     · ETIQUETAS (múltiple, marca CUADRADA con palomita): se pueden tener
       varias, así que el clic ALTERNA — pone lo que falta, quita lo que está;
       sobre un parcial COMPLETA (`app.js`, el menú de etiquetas en lote).
     · ETAPA (`.exclusiva`, marca REDONDA con punto): un contacto está en UNA
       etapa, así que el clic no alterna, ELIGE. La forma lo dice antes de
       leer nada: cuadrado se marca y se desmarca, redondo se elige — es la
       convención de checkbox contra radio, que no hace falta explicar.

   Nació sustituyendo al toggle `.me-modo` («Poner» / «Quitar») del menú de
   etiquetas.
   Aquel repartía el trabajo al revés: pedía elegir un VERBO y después un
   sustantivo, cuando lo que el usuario tiene en la cabeza es un ESTADO —«que
   estos cinco lleven "referido"»—. Con el estado a la vista, el verbo se cae
   solo: el clic pone lo que no está y quita lo que sí, y eso se lee ANTES de
   pulsar en vez de vivir en una píldora a 60px del sitio donde se decide.

   ── TRES ESTADOS, PORQUE EN LOTE «LA TIENE» NO ES UN BOOLEANO ──
   `.puesta` (todos), `.parcial` (algunos) y sin clase (ninguno). El parcial es
   la razón entera de que esta pieza exista: es lo único que el menú viejo NO
   podía decir, y sin él «quitar» sobre una selección mixta es una apuesta.

   ── LA MARCA ──
   Una caja con el filo del control y la palomita de la casa dentro
   (`--icn-palomita`, la misma del checklist de funnels). NO es un
   `input[type=checkbox]`: éste tiene un tercer estado que el nativo sólo da por
   JS y que aquí es de lectura, no de formulario — nada de esto se envía, cada
   clic escribe solo. El hueco se reserva SIEMPRE, también sin marca, para que
   los nombres alineen: una lista donde el texto salta según el estado se lee
   como dos listas.

   El fondo de `.puesta` es `--app-row-selected`, el MISMO tinte con el que la
   sábana marca su fila elegida y el selector de catálogo su opción actual. No
   se inventa un tono: «esto está elegido» ya tiene color en esta app. */
.op-estado{
  display:flex;align-items:center;gap:8px;
  padding:6px 9px;border-radius:var(--r-ctl);cursor:pointer;
  font-size:var(--fs-meta);
}
.op-estado:hover{background:var(--app-chip)}
.op-estado.puesta{background:var(--app-row-selected)}
.op-estado.puesta:hover{background:var(--app-pill)}
.op-estado .marca{
  flex:none;width:15px;height:15px;border-radius:var(--r-mini);
  box-shadow:inset 0 0 0 1px var(--app-borde-muted);
  display:flex;align-items:center;justify-content:center;
}
/* La palomita se pinta con el mismo mecanismo que los iconos de la app
   (máscara sobre `currentColor`), así hereda el tema sin un segundo dibujo. */
.op-estado.puesta .marca,
.op-estado.parcial .marca{box-shadow:inset 0 0 0 1px var(--app-fg-secondary)}
.op-estado.puesta .marca::after,
.op-estado.parcial .marca::after{
  content:'';width:11px;height:11px;background:var(--app-fg);
  -webkit-mask:var(--icn-palomita) no-repeat center / contain;
          mask:var(--icn-palomita) no-repeat center / contain;
}
/* El PARCIAL no cambia de glifo, se atenúa: una palomita a medio tono dice «a
   algunos» sin estrenar un segundo símbolo que haya que aprender. El guion que
   usan otras apps compite con la palomita en vez de graduarla. */
.op-estado.parcial .marca::after{opacity:.4}
.op-estado .txt{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* El conteo del parcial: terciario y en la letra chica, porque acompaña a la
   marca y no compite con el nombre. */
.op-estado .badge{
  flex:none;font-size:var(--fs-micro);color:var(--app-fg-tertiary);
  font-family:var(--fuente);
}
/* ── EL SABOR EXCLUSIVO: la etapa ──────────────────────────────────────
   Redonda y con un punto en vez de cuadrada con palomita, porque el gesto es
   otro: aquí no se marca y desmarca, se ELIGE uno de varios. Prestarle la
   palomita prometería que un segundo clic lo saca del tablero, y no lo hace —
   para eso está «Sin ubicar», que es una opción más de la misma lista.

   El parcial es aquí más útil que en etiquetas y sale gratis: como la etapa es
   exclusiva, LOS CONTEOS SUMAN EL TOTAL SELECCIONADO, así que el menú se lee de
   un vistazo como el reparto de la selección — «3 aquí, 1 allá, 2 sin ubicar»—
   sin que nadie tuviera que construir esa pantalla. */
.op-estado.exclusiva .marca{border-radius:50%}
.op-estado.exclusiva .marca::after{
  width:7px;height:7px;border-radius:50%;background:var(--app-fg);
  -webkit-mask:none;mask:none;
}
#menu-etiqueta #me-lista{display:flex;flex-direction:column;max-height:210px;overflow-y:auto}
/* El menú de etapa lleva el mismo techo con scroll: un funnel con doce etapas
   no debe empujar la barra de selección fuera de la pantalla. */
#menu-tablero{max-height:260px;overflow-y:auto}
/* La nota al pie de un menú flotante: hoy la usa el de etiquetas para decir
   que no hay ninguna. (Nació en el de papelera, que ya no existe — su opción
   destructiva se volvió una pregunta del sistema el 2026-08-16.) */
.mp-nota{
  font-style:normal;font-family:var(--fuente);font-size:var(--fs-micro);color:var(--app-fg-tertiary);
  padding:2px 9px 5px;line-height:1.4;
}

/* ── Papelera de contactos (personas-papelera.php) ────────────────────
   Tenía molde propio —`.pap-fila`, una rejilla de seis anchos escritos a
   mano— y desde el 2026-08-17 es la SÁBANA, como citas y envíos: mismas
   piezas, misma fila de 36 con su hover, misma cabecera teñida. Por eso este
   bloque se quedó en una sola regla: el gesto de la última columna. Todo lo
   demás lo pone `.sabana` y su `--rejilla`, que la página declara en el
   marcado porque aquí el juego de columnas es fijo. */
/* `.pap-restaurar` SE QUEDÓ SIN ESTILO el 2026-09-03 y hoy es sólo el gancho
   del JS: quien pinta ese gesto es `.cf-btn`, el de renglón. Tenía cuatro
   líneas propias —`--app-accent`, `font-weight:500` y subrayado al hover— para
   UN consumidor, y lo que dibujaban era un enlace disfrazado dentro de una
   fila: exactamente la excepción que el censo de gestos del 2026-08-16 dijo
   haber eliminado al plegar cinco clases en dos. Sobrevivió por estar en la
   lista de «escribe (azul)», que es una regla de BOTONES — un texto suelto en
   acento dentro de un renglón no es un botón, es el enlace azul que la app
   retiró ese mismo día de todas partes.

   Se conserva el nombre en el marcado porque el listener lo usa para distinguir
   ese clic del que abre el panel; darle `.cf-btn` al selector haría que
   cualquier gesto de renglón futuro restaurara. */

/* ── El panel en SOLO LECTURA (lo estrena la papelera) ─────────────────
   Sus dos caminos de escritura no están: `iniciar()` los retira del DOM. Lo
   que queda aquí es la única marca que el CSS puede poner — los campos de la
   ficha, que SON el panel y por eso se desactivan en vez de retirarse. Se ven
   como lo que son: dato en reposo, no un control que espera. El
   `-webkit-text-fill-color` es obligatorio en Safari/Chrome, que atenúan el
   texto de un `:disabled` por su cuenta y dejarían el valor ilegible. */
.peek.solo-lectura .fx-ctl:disabled{
  background:transparent;border-color:transparent;box-shadow:none;cursor:default;
  color:var(--app-fg);-webkit-text-fill-color:var(--app-fg);opacity:1;
  padding-left:0;resize:none;
}
/* El marcador de posición se quita en JS y no aquí: `::placeholder` sobre un
   control DESACTIVADO no se deja pintar de transparente en Chrome —el valor
   computado dice `rgba(0,0,0,0)` y el navegador lo sigue dibujando igual—, así
   que pelear la cascada era gastar reglas en algo que se resuelve borrando el
   atributo (ver `peek.js`, el pase de solo lectura). Y hay que quitarlo: «Lo
   que no cabe en un campo» es una INVITACIÓN a escribir, y en una pantalla de
   consulta se lee como si el lead tuviera ese dato. */

/* ── CRUD manual: alta rápida ───────────────────────────────────────── */

/* Barra propia sobre la sábana y NO una fila editable dentro de ella: el bucle
   de render reescribe #filas en cada scroll y se llevaría el input, el mismo
   motivo por el que el editor de celda es un elemento superpuesto. */
/* El alta es un `.dialogo` desde el 2026-08-11, así que casi todo su CSS se
   fue: la barra teñida (`.alta-rapida`), su input sin caja y la × de cerrar
   (`.alta-cerrar`) los sustituyen la hoja, el `.campo` y el pie del sistema.
   La × volvió el 2026-08-14, pero ya como pieza del sistema (`.dlg-x`, abajo)
   y para TODAS las hojas — no como el botón propio de esta capa.
   Queda lo único que el diálogo no sabe hacer solo: el aviso de parecido.
   Y el «dónde cae» —que es lo que convierte «el escenario declara los valores
   por defecto» en una promesa y no en una sorpresa— es ahora una `.dlg-nota`,
   sin clase propia. */
/* `.alta-aviso` se retiró el 2026-09-03: era el pie del diálogo del alta,
   donde vivían el veredicto del correo y el parecido de nombre. Con el alta
   cerrándose al crear se quedó sin sitio y los dos mensajes se mudaron al
   aviso flotante, que ese mismo día ganó duración larga, × y acción. */

/* ── CRUD manual: la ficha del peek ─────────────────────────────────── */

/* AQUÍ VIVÍA `.ficha-titulo` —el asa del acordeón— con su `cursor:pointer`, su
   `user-select:none`, su hover y el galón `.car` que giraba 90° con
   `.ficha.abierta`. Las cinco reglas se retiraron el 2026-09-03 (pedido de
   Arif) porque la ficha ya no se pliega: «Datos» es una sección más del panel y
   su cabecera es la anatomía compartida (`.pk-cab`, arriba), con
   `.peek-etiqueta` de rótulo. Un título con mano de pulsar sobre algo que no
   se pulsa prometería un gesto que no existe.

   Lo único que sobrevive del bloque es el CONTADOR, y sobrevive mejor: dejó de
   ser la excusa de la fila plegada («cuántos faltan SIN desplegar») para ser lo
   que era en el fondo, el dato de una sección que ya está abierta. */
/* EL DATO DE UNA CABECERA DE SECCIÓN — nombre de sistema desde el 2026-09-04,
   con sus consumidores segundo y tercero (el panel del cliente, en «Contactos»
   y en «Ventas»). Se llamó `.ficha-faltan` mientras su único consumidor era el
   contador «N sin llenar» de la ficha del contacto, y **el día que llegó el
   segundo el nombre pasó a mentir**: esto no habla de lo que falta, habla de lo
   que la sección tiene —«1 contacto», «1 venta · $3,730.00»—.

   Es el tercer renombrado de esta clase en la misma semana y por el mismo
   camino (`.pk-ente-cab` → `.pk-cab`, `.pk-ente-valor` → `.pk-valor`), así que
   la lección ya no es nueva: **una pieza nombrada por su primer consumidor
   caduca en cuanto llega el segundo**, y el aviso no llega solo — hay que ir a
   buscarlo al añadir el consumidor.

   QUÉ DECLARA: dice algo del bloque **sin robarle el renglón al contenido**, en
   la tinta terciaria y a `--fs-micro` porque acompaña al rótulo, no compite con
   él. El `margin-left:auto` es cinturón: `.pk-cab .peek-etiqueta` ya lleva
   `flex:1` y se come el hueco, pero con un tercer hijo en la fila —el dato Y un
   botón, que es el caso de «Datos»— el auto es lo que garantiza que el par
   cierre a la derecha pase lo que pase con el rótulo. */
.pk-cab-dato{
  margin-left:auto;font-size:var(--fs-micro);font-weight:500;
  color:var(--app-fg-tertiary);
}
.ficha-cuerpo{margin-top:7px}
/* EL PRIMER DATO ABRE A RAS (2026-09-04, con la mudanza a sólo lectura).
   `.peek-dato` trae 16px de aire arriba —el que lo separa de lo que tenga
   encima— y aquí lo de encima es su propia cabecera, que ya puso los 7 de
   `.ficha-cuerpo`: sumados dan 23 contra los 7 con que abren las otras tres
   secciones del panel. Es el mismo reparto que `.notas .nota:first-child` ya
   usa, y por el mismo motivo: el aire entre la cabecera y su contenido lo pone
   el contenedor, no el primer elemento de la lista. */
.ficha-cuerpo > .peek-dato:first-child{margin-top:0}

/* La ficha NO tiene CSS de disposición propio desde el 2026-08-11: apila
   `.campo` dentro de un `.campos`, que es lo que el sistema ya declara para
   campos apilados. Se fueron con la reescritura tres reglas que sostenían la
   forma anterior —`.fx-grupo`, `.fx-grupo-tit` y la rejilla de dos columnas—,
   porque hoy la ficha son LAS COLUMNAS de la tabla, en su orden y a renglón
   completo: sin grupos que titular y sin pares que cuadrar. Lo único suyo que
   queda es el aviso de dedup, aquí abajo. */
.fx-ctl{width:100%}

.fx-aviso{font-size:var(--fs-micro);line-height:1.4;padding-top:1px}
/* Choque con un UNIQUE: el guardado VA a fallar. Parecido: sólo una sospecha,
   nunca fusiona — así es como "Tacos El Güero Centro" se comería al "Sur".
   El choque es una mini-alerta: tinte ámbar + texto del cuerpo, no letra
   café (2026-08-08). */
.fx-choque{
  color:var(--app-fg);background:var(--badge-warning);
  border-radius:var(--r-mini);padding:1px 6px;display:inline-block;
}
.fx-parecido{color:var(--app-fg-tertiary)}
.fx-aviso a{margin-left:4px}

/* ── Diálogo ──────────────────────────────────────────────────────────
   La ÚNICA capa modal de la app, y nació dos veces: `.dialogo` para exportar
   —absoluta, con un velo transparente que sólo cazaba clics— y `.fn-capa` para
   el guion del agente —fija, con velo de verdad—. Entre las dos llegaron a
   CINCO consumidores (exportar, guion, alta de cita, los cuatro formularios de
   citas-config y los tres de automatización) con dos anatomías, dos sombras,
   dos anchos y dos formas de rotular Cancelar. Cinco es el segundo consumidor
   con creces, así que la capa se ganó su nombre y las dos anteriores murieron
   el 2026-08-11.

   Qué lo separa del PEEK, que también flota: el peek ACOMPAÑA —se abre al lado
   de la lista, se navega con J/K y puedes dejarlo abierto mientras trabajas—;
   el diálogo INTERRUMPE y no devuelve la pantalla hasta que respondes. Si lo
   que abres se puede dejar abierto, no es un diálogo.

   SIN LÍNEAS, ni bajo el título ni sobre el pie. Un separador divide zonas que
   se confundirían; dentro de una hoja de 560 con su aire, título, cuerpo y pie
   ya se distinguen solos — la línea no divide, parte. */
.dialogo{
  position:fixed;inset:0;z-index:60;padding:24px;
  display:flex;align-items:center;justify-content:center;
  background:var(--app-velo);
  animation:dlg-velo .12s ease-out;
}
/* Cinco medidas y ninguna a ojo: el marcado DICE cuál usa. `.dlg-caja` sola
   vale por la mediana —el default de la escala, como en Bootstrap— pero las
   cinco se declaran igual para que ninguna página tenga que deducirla.
      xs 340  una fecha, una confirmación
      sm 390  UN campo con su rótulo, y nada más
      md 440  el default: formulario corto de campos apilados
      lg 560  formulario con notas, resúmenes o una casilla
      xl 720  texto largo: un guion, el cuerpo de un correo
   El escalón de 390 entró el 2026-08-11, al mudar el alta de lead: con un solo
   campo, 440 dejaba una hoja más ancha que su contenido — y un diálogo que
   sobra por los lados se lee como un formulario al que le falta algo. Eran
   cuatro; el nombre nuevo se INSERTÓ y los demás corrieron, en vez de inventar
   un `xs+` que nadie sabría dónde colocar.
   Ninguna llega a los 800 de Bootstrap: a 14px de base, 720 ya son ~100
   caracteres por renglón — el techo de lo que se lee sin perder el renglón. */
.dlg-caja{
  /* `relative` = el ancla de la × (`.dlg-x`), que va absoluta en la esquina. */
  position:relative;
  width:100%;max-width:440px;max-height:calc(100vh - 48px);overflow-y:auto;
  /* ── LA HOJA VUELVE AL PAPEL EN CLARO, Y SE QUEDA EN LA BARRA EN OSCURO
     (2026-09-20, decisión de Arif tras verlo en pantalla) ──
     El problema medido era real y sigue siéndolo: `--app-paper` y `--ctl-bg`
     son los dos blanco puro en claro, así que hoja y campo dan 1.00:1 y el
     campo lo dibuja sólo su filo. Hay dos maneras de arreglarlo, y las dos se
     construyeron y se miraron el mismo día:

       · TEÑIR LA HOJA — los campos separan por RELLENO, que el ojo resuelve
         sin enfocar. Se probó con el tono del riel (rgb(247,247,242), 1.08:1)
         y con el de la barra (#F0EEE6, 1.16:1). **Arif rechazó los dos en
         claro**: «luce muy extraño». Lo que los números no enseñan es que la
         hoja quedaba más oscura que la página sobre la que flota, o sea la
         elevación al revés.
       · SUBIR EL FILO DEL CONTROL — separan por CONTORNO. Es lo que quedó:
         `--ctl-borde` pasó de 12.7% a 75% de tinta (#706858 sobre blanco), y
         el porqué vive allá porque es el token quien cambió.

     EN OSCURO SE QUEDA EL TONO DE LA BARRA, que es lo que Arif aprobó
     («el formato oscuro me encantó, mantenlo así»). Allá no hay elevación
     invertida —#252528 contra el papel #28282c es bajar, y el velo lo separa
     igual— y el campo, que es translúcido (`rgba(0,0,0,.22)`), se hunde sobre
     cualquiera de los dos. Por eso el fondo va en un token con dos valores y
     no en un literal: el claro y el oscuro llegaron a respuestas distintas, y
     la regla no tiene que saber en cuál está.

     AQUÍ VIVIÓ UN `--app-fg-tertiary` RECALIBRADO (#646056) mientras la hoja
     fue beige: el terciario está calibrado contra el papel y sobre aquel
     fondo caía a 4.58:1. Con la hoja de vuelta en papel el token rinde sus
     5.32:1 y el ajuste sobra, así que se retiró con lo que lo justificaba. */
  background:var(--dlg-hoja-bg);border-radius:var(--r-dialogo);
  /* Dos capas: la larga lo despega de la pantalla, la corta lo apoya. Y el
     filo va INSET —no `border`— para que no le sume 2px al ancho declarado. */
  box-shadow:0 24px 64px var(--app-shadow),0 6px 16px var(--app-shadow),
             inset 0 0 0 1px var(--app-hairline);
  /* 16 arriba y abajo, 18 a los lados. La ASIMETRÍA es la de siempre y sigue
     valiendo: el vertical baja más porque ahí el aire cuesta filas —el ancho es
     fijo, pero el alto lo decide el contenido, así que cada píxel vertical se lo
     quita al contenido cuando el diálogo lleva ocho campos—, mientras que el
     horizontal trabaja: separa el texto del canto y evita que un campo a todo lo
     ancho toque la pared. Lo que cambió es el NÚMERO, no el razonamiento.
     El costado bajó 22 → 24 → 20 (2026-08-11) → 18 (2026-08-28, decisión de
     Arif), y este último escalón no lo pidió una medición sino la escala: 18 es
     el relleno interior de la casa (`.seccion`, `.tarjeta`, `.app-lado`,
     `.peek`), y un diálogo es una pieza de primer nivel como las demás. Cuando
     el costado se elige pieza por pieza acaba habiendo cuatro números, que es
     exactamente lo que había hasta esta fecha.
     Va en TOKEN porque `.dlg-cuerpo` lo consume para anular el suyo — mismo
     acoplamiento que `.peek` / `.peek-cuerpo`, y por el mismo motivo se resuelve
     con una custom property en vez de con una advertencia en un comentario. */
  --dlg-pad-x:18px;
  padding:16px var(--dlg-pad-x);
  display:flex;flex-direction:column;gap:14px;
  animation:dlg-entra .14s ease-out;
}
.dlg-xs{max-width:340px}
.dlg-sm{max-width:390px}
.dlg-md{max-width:440px}
.dlg-lg{max-width:560px}
.dlg-xl{max-width:720px}

/* EL CUERPO CON SCROLL PROPIO — opcional, y el diálogo que lo necesita se
   declara (2026-08-12, a petición de Arif).

   Sin esto, un diálogo más alto que la pantalla scrollea ENTERO —la hoja es su
   propio contenedor de scroll— y se van con él el título y el pie: buscas el
   botón que escribe y ya no está. Es el `.modal-dialog-scrollable` de Bootstrap
   y el motivo es el mismo: lo que se lee (el título) y lo que se pulsa (el pie)
   son ANCLAS, no contenido.

   Es OPT-IN y no el comportamiento por defecto porque no todos los diálogos lo
   quieren: uno de confirmación de tres renglones no gana nada con un cuerpo
   scrolleable, y los que hoy caben de sobra no tienen por qué cambiar.
   Lo pide quien no cabe: el alta de lead, que puede crecer SIN TECHO —sus
   campos son las columnas que el usuario haya marcado, pueden ser veinte—, y
   el alta de cita, que con techo y todo (seis campos, uno de ellos un textarea
   de tres renglones) ya no cabe en una laptop.

   `min-height:0` es la pieza que lo hace funcionar y la que se olvida: un hijo
   flex NO encoge por debajo de su contenido salvo que se le diga, así que sin
   esta línea el cuerpo empuja la hoja y el scroll vuelve a salir donde no debe.

   Los márgenes negativos con el padding igualado son NETO CERO —el contenido
   queda exactamente donde estaba sin scroll— y compran dos cosas: la barra de
   desplazamiento se pega al canto de la hoja (como en Bootstrap) en vez de
   flotar a 20px, y el halo de foco de un control (4px por fuera) deja de
   recortarse contra el borde del contenedor de scroll.

   `overscroll-behavior` corta el encadenamiento: llegar al final del cuerpo no
   debe empezar a mover la sábana de detrás.

   ── LA SEÑAL NO ES UNA LÍNEA, SON DOS HALOS (2026-09-20, pedido de Arif) ──
   Hasta hoy el cuerpo que desbordaba encendía dos filos de `--app-hairline`,
   uno bajo el título y otro sobre el pie, con este razonamiento escrito: «el
   diálogo declara SIN LÍNEAS, pero con el cuerpo desplazándose la premisa deja
   de valer». La premisa seguía valiendo; lo que faltaba era la pieza correcta.
   La app ya tiene una señal para «arriba hay más» que NO es una línea —el halo
   del titular de página y el del cuerpo del peek—, y un diálogo con el título
   fijo sobre contenido que corre es exactamente la misma anatomía que un peek.
   Así que aquí va la misma pieza, y la regla del diálogo vuelve a cumplirse
   entera: ni bajo el título ni sobre el pie hay una línea.

   DOS Y NO UNO, y ésa es la única diferencia con el peek: el peek tiene algo
   fijo ARRIBA y nada abajo, y el diálogo tiene las dos cosas —título arriba,
   pie abajo—, así que necesita decir «hay más» en las dos direcciones. El de
   abajo es el mismo gradiente girado (`at 50% 100%`), no una pieza nueva.

   SE ENCIENDEN POR SEPARADO (`.halo-arriba` / `.halo-abajo`) y no con una sola
   clase «desborda»: son dos hechos distintos —hay contenido por encima, hay
   contenido por debajo— y al final del scroll el de abajo tiene que apagarse o
   miente. Los pone `app.js` en un solo sitio para toda la app, por el mismo
   oyente en captura que ya sirve al titular, al peek y a la cabeza del riel.

   `.desborda` SOBREVIVE con otro trabajo: ya no pinta nada, sólo dice «este
   cuerpo de verdad scrollea», y de eso cuelga el aire de abajo. */
/* El costado sale de `--dlg-pad-x` (lo declara `.dlg-caja`) y el derecho
   descuenta el canal del scroll: es la regla del riel lateral de `ui.php`
   aplicada a una capa. Este cuerpo desborda por diseño —para eso existe
   `.desborda`— y sin reservar el canal el contenido cerraba 6px adentro del
   rótulo que tiene encima, sólo en los diálogos largos. Reservado, la barra
   cae al canto de la hoja y el campo respira lo mismo que el título. */
/* EL AIRE DE ABAJO NO ES SIMÉTRICO (16 contra 4), y desde el 2026-09-08 es a
   propósito. Los 4px de arriba y de abajo nacieron como el respiro mínimo para
   que el filo del scroll no cortara un foco, y abajo se quedaban cortos: con el
   `margin:-5px` que los compensa, el último campo acababa a **14px del pie**
   —apenas 4 más que los 10 que separan un campo del siguiente—, así que el
   botón que GUARDA se leía como el final de la misma columna de campos. Lo cazó
   Arif en el alta de cliente, donde la dirección pasó a ser un textarea y su
   caja alta dejó el problema a la vista.

   Con 16 el hueco medido pasa a **26px**, dos veces y media el que hay entre
   campos: suficiente para que el pie se lea como OTRA zona. (No sale de sumar
   16+8: entre medias van el borde del scroll, el `-5` de margen y el `gap:14`
   de la caja; el número es el medido en pantalla, no el derivado.) Arriba se
   quedan los 4 — ahí no hay nada de lo que separarse, el título ya está fuera
   del área que scrollea.

   Desde el 2026-09-20 esos 16 CUELGAN DE `.desborda` y no del cuerpo a secas:
   el razonamiento de arriba vale para el diálogo que scrollea, que era el
   único que tenía cuerpo cuando se escribió. Ahora lo declaran los 58, y en
   uno que cabe entero no hay «columna de campos que sigue» de la que separar
   el pie — sólo habría 12px de aire que nadie pidió. */
/* COLUMNA CON EL MISMO AIRE QUE DABA LA CAJA (2026-09-20). La hoja es `flex`
   con `gap:14`, así que hasta hoy sus hijos —título, cada bloque del cuerpo,
   pie— se separaban solos. Al envolver el cuerpo, esos bloques dejan de ser
   hijos de la hoja y el aire entre ellos se habría perdido: el cuerpo lo
   reproduce con el mismo valor, así que envolver no mueve un píxel. Los 10
   diálogos que ya declaraban cuerpo no se enteran —envuelven un solo hijo—, y
   la lista de opciones (`.dlg-lista`, que también es cuerpo en dos diálogos)
   se queda con su `gap:1`: sus renglones se tocan a propósito.

   EL AIRE VA EN UN TOKEN porque los dos halos lo consumen. Son hijos flex como
   cualquier otro, así que el `gap` les pone 14px de separación a cada uno — y
   un halo que ocupa alto deja de ser una sombra y pasa a ser un margen. Su
   margen negativo tiene que absorber su propia altura MÁS el hueco, y sin el
   token ese número sería un 28 escrito a mano que se rompe el día que el aire
   cambie o que una lista con otro `gap` declare cuerpo. */
/* EL CUERPO TOCA AL TÍTULO Y AL PIE, y no es un ajuste estético: es lo que
   deja a los halos nacer pegados a la pieza que los proyecta (2026-09-20,
   corrección de Arif: «el halo del footer tiene espacio entre el halo y el
   footer»). El margen vertical era -5 contra un `gap:14` de la hoja, así que
   entre el borde del scroller y el pie quedaban **9px de papel** — medido en
   «Nuevo horario»: cuerpo acabando en y=542, pie empezando en 551. Un halo
   `sticky` no puede cruzarlos, porque el `overflow` de su propio scroller lo
   recorta. Con el margen igualado al `gap` el hueco es CERO y el halo queda
   contra el pie; el aire que el contenido necesita lo pone el padding, que
   sube lo mismo que bajó el margen — neto cero, ni un píxel se mueve.
   Es la misma cuenta que ya hacía `.peek-cuerpo` con su cabecera. */
.dlg-cuerpo{
  --dlg-cuerpo-gap:14px;
  --dlg-aire:13px;            /* del título al primer bloque */
  --dlg-aire-abajo:13px;      /* del último bloque al pie; con scroll, más */
  display:flex;flex-direction:column;gap:var(--dlg-cuerpo-gap);
  overflow-y:auto;min-height:0;overscroll-behavior:contain;scrollbar-gutter:stable;
  margin:-14px calc(var(--dlg-pad-x) * -1);
  padding:0 calc(var(--dlg-pad-x) - var(--canal-scroll)) 0 var(--dlg-pad-x);
}
/* EL AIRE VERTICAL NO PUEDE SER `padding` DEL SCROLLER, y ésta es la segunda
   corrección de Arif del 2026-09-20: «el halo del footer está mal ubicado, hay
   espacio entre el halo y el footer». Con `padding-bottom` puesto, el halo se
   quedaba **25px por encima del pie** — comprobado pintándolo de rojo, no
   deducido. El motivo es de spec: un `sticky` se pega al scrollport PERO queda
   clavado dentro de su bloque contenedor, que es el **content box** del padre,
   y el padding lo recorta justo esos 25px. Así que el padding vertical se va a
   cero y el aire se lo comen los propios halos en su margen negativo: cada uno
   descuenta su altura y el hueco del `gap`, y devuelve el aire que toca. El
   contenido queda donde estaba y el halo llega al canto.

   EL AIRE DE ABAJO ES MAYOR SÓLO CUANDO SCROLLEA. Antes eran 16 fijos, y con el cuerpo
   declarado por TODOS los diálogos —no sólo por los largos— esos 12px de más
   se los habrían comido los 48 que hoy caben de sobra, moviéndoles el pie sin
   que nadie lo pidiera. Cuelga de `.desborda`, que es justo «este cuerpo de
   verdad scrollea», así que el diálogo corto queda exactamente donde estaba. */
.dlg-cuerpo.desborda{--dlg-aire-abajo:25px}
/* LOS DOS HALOS. Copia literal del `.peek-cuerpo::before` —`sticky` dentro del
   scroller, margen negativo para no ocupar alto, y los costados en negativo
   espejando el padding del cuerpo (el derecho descuenta el canal de la barra,
   o el halo desborda 6px en horizontal: ese bug ya se pagó en el peek)—.
   El de abajo es el mismo gradiente anclado al otro borde. */
/* `flex:none` EN TODO HIJO, Y AQUÍ ESTÁ LA TRAMPA QUE COSTÓ UNA DEVOLUCIÓN
   (2026-09-20). Un hijo flex encoge por defecto, y este cuerpo desborda POR
   DISEÑO: al hacerlo columna flex, en cuanto el contenido pasaba del alto
   disponible el navegador encogía a todos sus hijos ANTES de dar el scroll.
   Los halos, que no tienen contenido que los sostenga, se aplastaban de 14px
   a 4 — así que el gradiente desaparecía y, peor, su margen negativo (que
   descuenta 14 fijos) tiraba del primer bloque 10px HACIA ARRIBA, metiéndolo
   debajo del título. Medido en «Nuevo horario»: el primer hijo arrancaba en
   y=62 con el título acabando en 63 y el cuerpo empezando en 72.
   Como bloque nunca pasó, porque un bloque no encoge: `flex:none` devuelve
   esa propiedad a los hijos y deja el `gap` como única diferencia. */
.dlg-cuerpo > *{flex:none}
.dlg-cuerpo::before,.dlg-cuerpo::after{
  content:'';position:sticky;z-index:2;display:block;pointer-events:none;
  flex:none;height:14px;
  margin-left:calc(var(--dlg-pad-x) * -1);
  margin-right:calc((var(--dlg-pad-x) - var(--canal-scroll)) * -1);
  opacity:0;transition:opacity var(--app-dur) var(--app-ease);
}
/* LAS DOS CLASES QUE TRAEN SU PROPIO AIRE lo declaran en el token, y no basta
   con que su regla gane la cascada: los halos LEEN el token para saber cuánto
   hueco absorber, así que un `gap` que no esté aquí los descuadra. `.campos`
   se declara además ANTES que este bloque, o sea que sin esta línea el cuerpo
   le pisaba sus 10px con 14 — que es el defecto que la muestra de `ui.php`
   destapó el 2026-09-20, el mismo día. */
.dlg-cuerpo.dlg-lista{--dlg-cuerpo-gap:1px}
.dlg-cuerpo.campos{--dlg-cuerpo-gap:10px}
.dlg-cuerpo::before{top:0;margin-bottom:calc(var(--dlg-aire) - 14px - var(--dlg-cuerpo-gap));
  background:radial-gradient(62% 100% at 50% 0%, var(--app-halo), transparent 72%)}
.dlg-cuerpo::after{bottom:0;margin-top:calc(var(--dlg-aire-abajo) - 14px - var(--dlg-cuerpo-gap));
  background:radial-gradient(62% 100% at 50% 100%, var(--app-halo), transparent 72%)}
.dlg-cuerpo.halo-arriba::before{opacity:1}
.dlg-cuerpo.halo-abajo::after{opacity:1}
@keyframes dlg-velo{from{opacity:0}to{opacity:1}}
@keyframes dlg-entra{from{opacity:0;transform:translateY(8px) scale(.985)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion:reduce){.dialogo,.dlg-caja,.rebote{animation:none}}

/* UN PUNTO SOBRE EL CUERPO (15), no los 20 con los que nació. Los copió del
   panel de detalle y eran dos trabajos distintos: allá se TITULA un registro
   —el nombre del lead es el dato principal de esa pantalla—, mientras que el
   título de un diálogo sólo ROTULA la pregunta que ya provocaste tú al pulsar.
   A 20 competía con su propio contenido y se comía una fila de alto.
   (El 2026-08-18 el peek bajó a estos mismos 15 y hoy comparten token: los dos
   rotulan una superficie. El contraste que originó este párrafo ya no existe;
   se conserva porque explica POR QUÉ 15 y no 14 ni 20, que sigue vigente.)
   La jerarquía la hacen el peso y la fuente de titular, que es justo lo que la
   regla «el tamaño no jerarquiza» dice desde el principio. UN punto sobre el
   cuerpo y no cero: a 14 clavados el rótulo se disolvía en su propio párrafo
   —probado en pantalla y corregido en el acto—, y ese punto basta para que se
   lea como encabezado sin volver a competir. Va como TOKEN y no como un 15
   suelto: la escala de texto tiene seis valores declarados y ninguno vive
   dentro de un componente. */
/* El `padding-right` deja el carril de la ×: su caja (el átomo de control) más
   6px de aire. Sin él, un título largo pasa por debajo del botón. Cuesta nada
   cuando no hay × —el título es corto y va a la izquierda—, así que no se
   condiciona; y se deriva igual que la × para que los dos crezcan juntos. */
.dlg-titulo{
  font-family:var(--fuente-titulo);font-size:var(--fs-dialogo);font-weight:700;letter-spacing:-.01em;
  padding-right:calc(var(--fs-base) * var(--btn-lh) + var(--btn-py) * 2 + 6px);
}

/* ── La × de cerrar ───────────────────────────────────────────────────
   Entró el 2026-08-14 a petición de Arif, y hay que decir qué la trajo: el
   diálogo NO la tenía por decisión —el alta de lead traía una (`.alta-cerrar`)
   y se retiró al mudarla al sistema el 2026-08-11, porque cerrar ya vivía en
   tres gestos: el velo, Cancelar y Escape—. Lo que faltaba no era una salida,
   era la SEÑAL de que las hay: los tres son invisibles hasta que se prueban, y
   el ojo busca la × arriba a la derecha en cualquier modal de la web. Un
   cuarto camino al mismo sitio no desafina el sistema; una capa que se ve
   cerrada sí.

   `absolute` y no una fila de cabecera con el título: así ninguna página
   reordena su marcado —basta insertar el botón dentro de la hoja, donde caiga—
   y el título conserva su `+ .lede` pegado, que es una adyacencia de CSS que
   un envoltorio rompería en todas las capas de una sentada.

   Lleva `data-cerrar` y nada más: cerrar lo resuelve `app.js` para toda la app,
   igual que el velo y Cancelar. Y `type="button"` es obligatorio porque cuatro
   hojas son un `<form>` — sin él, la × ENVÍA el formulario.

   ── ES UN BOTÓN DE SALIDA, ASÍ QUE SE PINTA COMO TAL (2026-08-14) ──
   Nació transparente hasta el hover, con la geometría copiada de la flecha del
   peek (26px sueltos). Dos problemas, y el segundo es el de fondo: sin relleno
   se leía como un adorno —a 17px el signo `×` deja apenas 7.2px de tinta, que
   es lo que mide su dibujo: la tipografía lo traza a la altura de la x
   minúscula porque es un operador matemático, no un icono—; y ese 26 era un
   número suelto heredado de otro número suelto, ciego a la escala.

   Las dos cosas se arreglan con lo que el sistema YA declara. La piel es la de
   `.btn-suave` —el Cancelar de su propio pie— y por el mismo motivo escrito
   allá: «una salida tiene que ser encontrable sin ser tentadora», relleno
   tenue con el contorno haciendo el trabajo, y se llena al apuntarla. La × y
   Cancelar son el MISMO gesto en la misma hoja; que se vieran distintos era la
   incoherencia. Y la caja se DERIVA del átomo de control (`--btn-py` +
   `--fs-base` × `--btn-lh` = los 26.89px que ya miden botón, input y
   flotante), así que la × mide exactamente lo que mide el Cancelar de al lado
   y sigue sola a la escala si el cuerpo cambia. Nada aquí es un número a ojo
   salvo el cuerpo del glifo, que es la única cota que no puede derivarse: el
   `×` necesita ir por encima del texto para que su tinta se lea como marca. */
/* ── Y ES LA × DE TODA LA APP, NO SÓLO LA DEL DIÁLOGO (2026-08-17) ──────
   La barra de selección tenía la suya —`.bs-cerrar`, transparente, con el
   padding de un ítem— porque nació antes que esta pieza y nadie volvió por
   ella. Eran el mismo gesto (salir de aquí) con dos pintas: la del diálogo
   rellena y redonda, la de la barra un signo suelto de 7px de tinta.
   Comparten regla, y el `.dlg-x` conserva en la suya lo ÚNICO que es de la
   hoja: dónde se ancla. Quien añada otra × la pide por esta clase. */
.dlg-x,.bs-cerrar,.peek-nav .flecha.cerrar{
  width:calc(var(--fs-base) * var(--btn-lh) + var(--btn-py) * 2);
  height:calc(var(--fs-base) * var(--btn-lh) + var(--btn-py) * 2);
  border-radius:var(--r-pill);border:0;
  background:var(--app-relleno-tenue);color:var(--app-fg-secondary);
  box-shadow:inset 0 0 0 1px var(--app-borde-muted);
  display:flex;align-items:center;justify-content:center;
  font-size:20px;line-height:1;cursor:pointer;
  transition:background var(--app-dur) var(--app-ease),color var(--app-dur) var(--app-ease);
}
.dlg-x{position:absolute;top:12px;right:12px;z-index:1}
.dlg-x:hover,.bs-cerrar:hover,.peek-nav .flecha.cerrar:hover{background:var(--app-chip);color:var(--app-fg)}
/* El párrafo que explica el diálogo es el `.lede` de la app sin su margen: el
   aire entre bloques lo pone el `gap` de la caja, y el ancho lo pone la medida
   elegida. Pegado al título, porque los dos son la misma frase en dos alturas. */
.dlg-caja .lede{margin:0;max-width:none}
.dlg-titulo + .lede{margin-top:-7px}
/* El textarea que justifica la medida grande. Entra al tratamiento único de
   control (papel + filo) como cualquier campo: hasta el 2026-08-11 el del
   guion lo pedía prestado a `.mv-input` —una clase del menú flotante— y los
   dos de automatización no lo pedían a nadie, así que se pintaban con el
   textarea crudo del navegador. Dos textareas del mismo sistema con dos
   aspectos, y ninguno lo había decidido. */
/* `textarea.dlg-area` y no `.dlg-area` a secas: dentro de un `.campo`, el
   selector `.campo textarea` del tratamiento común pesa más (0,1,1) y le
   imponía su min-height de 62 — el cuerpo de un correo quedaba en una rendija
   de tres renglones. Con el tipo delante empata en peso y gana por orden. */
textarea.dlg-area{width:100%;min-height:220px}
/* `.dlg-campo` murió el 2026-08-11: era un tercer campo (etiqueta a 14 en
   negrita) para decir lo mismo que `.campo`. Los diálogos usan el campo del
   sistema, como todo lo demás. */
.dlg-alcance{font-size:var(--fs-base);font-weight:500}
.dlg-sel{width:100%}
.dlg-nota{font-size:var(--fs-meta);color:var(--app-fg-tertiary);line-height:1.45}
/* El resumen cambia de tono cuando algo se queda fuera: la merma tiene que
   verse ANTES de descargar, no descubrirse contando renglones después. */
.dlg-resumen{
  background:var(--app-group);border-radius:var(--r-ctl);padding:10px 12px;
  font-size:var(--fs-base);line-height:1.45;
}
.dlg-resumen.con-merma{background:var(--app-amber-tint);color:var(--app-fg);font-weight:500}
/* ── Elegir UNO de N dentro de un diálogo (2026-08-15) ────────────────
   Es el ÁTOMO DE ÍTEM de 31px —el de la barra lateral y el del menú
   flotante— dicho en un diálogo: mismo alto, mismo relleno al pasar, misma
   fuente de titulares, porque hace exactamente el mismo trabajo, NAVEGAR. No
   se reutilizó `.sb-item` por el nombre: dice «barra lateral» y esto no lo es.

   Cada opción es un `<a href>` de verdad, no un botón: quien la usa cambia el
   SUJETO de la página y eso recarga, así que el enlace da gratis el teclado,
   el clic con rueda y la barra de navegación que app.js engancha a todo
   `a[href]`. El `<select>` que hubo antes navegaba con `location.href` desde
   JS y se perdía esa señal.

   `.actual` = el que está en pantalla, con `--app-row-selected`, el mismo tono
   con que la sábana dice «ésta es la fila elegida». El margen negativo alinea
   el texto con el título del diálogo y deja que el relleno del hover se
   desborde hacia los lados — el patrón de `.pm-item`. */
.dlg-lista{display:flex;flex-direction:column;gap:1px}
.dlg-opcion{
  display:flex;align-items:center;gap:9px;min-height:31px;
  padding:5px 9px;margin:0 -9px;border-radius:var(--r-ctl);
  font-family:var(--fuente-titulo);font-size:var(--fs-base);color:var(--app-fg);
  text-decoration:none;cursor:pointer;
}
.dlg-opcion:hover{background:var(--app-pill);color:var(--app-fg);text-decoration:none}
.dlg-opcion.actual{background:var(--app-row-selected)}
/* El glifo de la opción (2026-09-03, con el selector de catálogo): mismo
   tamaño y misma tinta que el de una fila de la barra, porque el diálogo lo
   abre una fila de la barra y lo que ofrece son los mismos destinos — leerse
   como otra cosa sería el error. No todas las opciones lo llevan: el selector
   de funnel elige entre N cosas del mismo tipo y ahí un icono repetido no
   distingue nada. */
.dlg-opcion .icn{width:var(--icn-fila);height:var(--icn-fila);color:var(--app-fg-secondary)}
/* El nombre cede ancho al badge, nunca al revés: un funnel apagado tiene que
   poder decirlo aunque su nombre no quepa. */
.dlg-opcion .txt{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.dlg-opcion .badge{flex:none}
/* EL DATO QUE EXPLICA POR QUÉ SALIÓ LA FILA (2026-09-09, con el buscador
   global): el teléfono cuando se buscó por dígitos, el correo cuando se buscó
   por correo, el folio cuando se buscó por folio. Buscar «6680001416» y ver
   sólo «Ramón Quiñónez» deja al usuario preguntándose qué casó.

   NO ES UN BADGE, y por eso no reusa esa pieza: el badge dice ESTADO —activo,
   apagado, vencido— y esto es un DATO. Lleva por tanto la voz del dato
   secundario de la casa, la misma de `.lado-fila-meta`:
   `--fs-meta` y tinta terciaria. Se distingue del nombre por tamaño y tono, no
   por color.

   El que cede ancho es el nombre (`.txt`, arriba) MIENTRAS el dato sea corto:
   un teléfono a medias no sirve de nada, y un nombre a medias sí se reconoce.
   PERO EL DATO TIENE TECHO —55% de la fila— desde la pasada visual del
   2026-09-09 (corrección del coordinador): con `flex:none` a secas, la razón
   social «Grupo Ferretero del Fuerte SA de CV» dejó al cliente en
   «Ferretería del F…», y ahí el dato se comió justo lo que la fila existe para
   decir. Un teléfono o un folio nunca llegan al techo (12 caracteres en una
   caja de 390 son ~30%), así que siguen enteros; sólo el dato largo —razón
   social, correo kilométrico, empresa— se recorta, y se recorta por la cola,
   que es la parte que menos identifica. */
.dlg-opcion .dato{
  flex:0 1 auto;min-width:0;max-width:55%;overflow:hidden;text-overflow:ellipsis;
  font-family:var(--fuente);font-size:var(--fs-meta);
  color:var(--app-fg-tertiary);white-space:nowrap;
}
/* EL RÓTULO DE GRUPO dentro de una `.dlg-lista` (2026-09-09, con el buscador
   global). Nace porque el buscador es la primera lista de diálogo con
   resultados de VARIOS tipos —contactos, clientes, ventas— y sin rótulo las
   filas se leen como una sola lista revuelta.

   ES LA VOZ DE `.sb-titulo`, el rótulo de grupo de la barra lateral, y lo es a
   propósito: hacen el mismo trabajo (partir una lista en secciones nombradas)
   y leerse distinto sería inventar una segunda manera de decir lo mismo. No
   reusa aquella clase porque su nombre dice «barra lateral» y esto no lo es —
   el mismo motivo por el que `.dlg-opcion` no reusó `.sb-item`.

   El primero no lleva aire arriba: el campo de búsqueda ya está encima. */
.dlg-grupo{
  font-size:var(--fs-micro);font-weight:600;letter-spacing:.07em;text-transform:uppercase;
  color:var(--app-fg-tertiary);padding:10px 0 4px;
}
.dlg-grupo:first-child{padding-top:2px}

/* La casilla del diálogo — la única del sistema que se ENVÍA con el resto del
   formulario. El `<label>` envuelve al `<input>`, que es lo que vuelve el rótulo
   entero zona de clic sin un `for` que mantener; `align-items:center` centra la
   caja contra un rótulo de dos renglones, que es el caso normal cuando el texto
   lleva su consecuencia detrás del guion, que es una de sus dos formas de
   rótulo.

   LAS DOS FORMAS DEL RÓTULO (movidas del catálogo `ui.php`, 2026-09-19, que
   era donde vivían). La LARGA es «marca — consecuencia», para cuando lo que la
   casilla provoca no se puede descubrir después: «Catálogo — borra además
   clientes, contactos…» en el reinicio de cuenta, que es destructivo y no
   tiene vuelta. La CORTA nombra la acción y ya: «Excluir los marcados "no
   contactar"» al exportar, y «Marcar como pagado» / «Marcar como cobrado» en
   las capturas de gasto y de venta. Estas dos empezaron largas y las acortó
   Arif el 2026-09-09, el día que nacieron, así que lo que la forma larga decía
   —que se salda el importe COMPLETO y con la fecha del DOCUMENTO— hoy no se
   lee en ninguna parte. Es la tensión viva de la pieza: si algún día esa
   ambigüedad estorba, lo que se repone es una `.dlg-nota` bajo la casilla, no
   un rótulo de dos renglones.

   NACE DESMARCADA siempre que añada una escritura o agrande un alcance: lo que
   sale por no mirar tiene que ser el caso conservador.

   DÓNDE VIVE: suelta entre el cuerpo y el pie del diálogo; dentro de
   `.campos`, pegada al campo del que habla (el gasto de contado, donde hereda
   la columna y el hueco de 10); o bajo un `.peek-div` en un panel (la venta de
   contado, donde la cifra de la que habla es el total y no un campo). */
.dlg-check{display:flex;align-items:center;gap:8px;font-size:var(--fs-base);cursor:pointer;user-select:none}
/* `input[type="checkbox"]` Y NO `input` A SECAS desde el 2026-09-11, y es la
   MISMA trampa que documenta `.radio` (§ El radio) con la misma cuenta: dentro
   de un `.campo`, el tratamiento común de los controles (`.ctl,.campo input,
   .campo select,.campo textarea`) empata en peso (0,1,1) y **gana por orden**,
   porque este bloque vive ~2,600 líneas más abajo. Le pondría a la casilla
   papel, filo, radio de control y `padding:4px 10px` de caja de texto. Con el
   atributo delante sube a (0,2,1) y el orden deja de decidir.

   NUNCA LO DESTAPÓ NADIE hasta ese día porque las cuatro casillas anteriores
   del proyecto —exportar, gasto, venta, oferta— viven sueltas en el diálogo,
   fuera de todo `.campo`: son decisiones del formulario, sin rótulo propio. La
   primera que vive DENTRO de un campo es el grupo de días del horario
   (`citas-config.php`), que sí tiene rótulo porque son siete y hay que decir de
   qué son.

   Las tres declaraciones de reposición son deliberadas y no cosméticas: sin
   ellas el navegador pinta la casilla nativa DENTRO de una caja de texto. */
.dlg-check input[type="checkbox"]{
  width:15px;height:15px;flex:none;accent-color:var(--app-accent);cursor:pointer;
  padding:0;border:0;background:none;border-radius:0;
}
/* ── Grupo de casillas (2026-09-11) ───────────────────────────────────────
   Lo que `.radios` es para el radio, con UNA diferencia de forma que sale de
   cuántas agrupa: aquél es `flex-wrap` porque junta dos —«Existente / Nuevo»—,
   y dos en una línea no tienen nada que alinear. Éste nació para SIETE (los
   días de la semana en el horario de la agenda) y ahí el ojo barre por columna:
   en fila envolvente, las casillas de la segunda línea caen donde caigan y hay
   que leer palabra por palabra para encontrar el jueves.

   UNA SOLA COLUMNA —una fila por casilla— desde el mismo día que nació, por
   pedido de Arif tras verlo en pantalla. Empezó con `auto-fit` de mínimo 112px,
   que en el `dlg-md` de 440 daba tres columnas: lunes-martes-miércoles,
   jueves-viernes-sábado, y domingo solo en una tercera fila coja. Los días de
   la semana ya tienen un orden que todo el mundo conoce, y repartirlo en
   columnas obliga a leerlo en zigzag para comprobar que están los siete.
   Apilados, el orden de la lista ES el de la semana y el hueco que falta se ve
   de un vistazo. Sale más alto —cuatro filas más que con tres columnas—, y eso
   lo absorbe la hoja: el diálogo del horario no declara `.dlg-cuerpo`, así que
   pasado el techo de `100vh − 48` scrollea entera.

   Sigue siendo grid y no un `flex-wrap` ni un `display:block`: la columna única
   es una DECISIÓN declarada, y el `gap` de 6 mantiene el ritmo vertical entre
   casillas sin heredar márgenes de nadie. */
.dlg-checks{display:grid;grid-template-columns:1fr;gap:6px}
/* El pie SIEMPRE termina en la derecha y SIEMPRE en el mismo orden: la salida
   (`.btn-suave` Cancelar) y luego la que escribe. Es el orden de macOS, el de
   Bootstrap y el de la web entera; Windows pone Aceptar primero, pero ésa es
   una convención de escritorio que ninguna app web sigue y mezclarlas sería
   tener dos órdenes en la misma pantalla. Cancelar es un BOTÓN, no un texto
   gris: hasta el 2026-08-11 cuatro capas lo pintaban como un `<span>`, que es
   la pieza que el sistema ya tenía resuelta y nadie usó.
   Lo que va a la IZQUIERDA es otra cosa —un enlace de contexto, un paso
   atrás— y se separa con `.hueco`, no con márgenes. */
.dlg-pie{display:flex;align-items:center;justify-content:flex-end;gap:8px;padding-top:8px}
.dlg-pie .hueco{flex:1}

/* LA PALABRA QUE HAY QUE TRANSCRIBIR — el segundo nivel de la confirmación
   (`CONFIRMAR.pedir` con `palabra`, en app.js). Es la única pieza nueva que
   pidió el componente: todo lo demás son las clases del diálogo tal cual.

   Monoespaciada y sobre chip como `.cfg-llave`, que es la otra cadena LITERAL
   del sistema —lo que importa es transcribirla carácter por carácter, no
   leerla—, pero sin su cursor ni su hover: aquélla se copia al pulsarla y ésta
   NO se puede copiar. El `user-select:none` no es un detalle de estilo: si se
   pudiera seleccionar, el paso que existe para obligar a TECLEAR se resolvería
   con dos atajos y dejaría de ser un freno. Lo mismo vale para el color: va en
   el del cuerpo (no hereda el `--app-fg-tertiary` de la llave) porque aquí es
   el dato que hay que leer bien, no una referencia de apoyo. */
.dlg-palabra{
  font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  font-size:var(--fs-micro);background:var(--app-chip);border-radius:var(--r-mini);
  padding:1px 6px;user-select:none;-webkit-user-select:none;
}

/* ── Funnels (funnels.php): UN RENGLÓN POR CAPA, dentro de un TRAMO ──
   Rehecho el 2026-08-12 y otra vez el 2026-08-15. Aquí vivían `.fn-ancho` y
   `.fn-fila`: una tabla de
   SIETE columnas (`minmax(140px,1fr) 316px 150px 54px 96px 72px 28px`) dentro de
   un contenedor de 1080px que la propia página sacaba de `.hoja` a la fuerza —
   la cuarta forma de página en un sistema que tiene tres, con la prosa a 880 y
   la tabla a 1080. Los comentarios que vivían aquí iban parcheando el reparto
   («el nombre lleva MÍNIMO, no 1fr, porque colapsaba a cero»), que era tratar el
   síntoma: el problema no era el reparto, era meter siete columnas donde caben
   tres. Medido antes de tocarlo: piso duro de 932px, o sea desborde bajo 1260px
   de ventana —por encima del umbral de 1200 que este sistema declara—, y la
   celda del agente en estado «apagado» desbordaba 27px y se encimaba con la
   columna de al lado a 1440px, sin estrechar nada.

   La página pasó a `.cf-bloque` (el molde que ya usaban automatizacion.php y
   citas-config.php) con un renglón por capa, así que lo único que hace falta
   aquí son DOS rejillas. Y las cinco clases que esta página había inventado para
   el gesto-texto en fila —`.fn-toggle`, `.fn-llave`, `.fn-copiar`, `.fn-guion`,
   `.fn-agente-toggle`— se retiraron: eran `.cf-btn` con el padding movido
   (2px 4px / 2px 5px / 3px 0), o sea tres versiones de la misma pieza.

   El 2026-08-15 el renglón se mudó dos veces: a un TRAMO de la mesa por la
   mañana y a la CONSOLA por la tarde, donde vive hoy como `.cfg-fila` (el
   molde y sus porqués, arriba). Aquí vivió `.fn-renglon` (la rejilla de 78px
   de rótulo) con sus reglas de enlace-sin-azul; se retiraron al quedarse sin
   consumidor — la decisión que valía la pena conservar viajó con la mudanza:
   el estado de un renglón no es una acción, así que jamás va en el azul del
   sistema, que es de lo que escribe. */
/* Los gestos del renglón y los del bloque comparten pieza: una tira de `.cf-btn`
   que NO se estira ni se recorta. Sin `flex:none` la última acción es la que
   cede y queda un botón a medias — que es exactamente cómo se rompía la celda
   del agente en el molde viejo. */
.fn-gestos{display:flex;gap:4px;align-items:center;flex:none}
/* El interruptor pide MÁS aire que los 4px que se dan dos píldoras entre sí: no
   es un gesto más de la tira, es el estado del renglón, y pegado a un `.cf-btn`
   se lee como el cuarto botón de un grupo. Va siempre el último, así que basta
   con separarlo por la izquierda. */
.fn-gestos > .sw{margin-left:6px}
/* NINGUNA × SE TIÑE DE ÁMBAR, y es decisión, no descuido (el párrafo vivía
   con `.nc-quitar`, retirada sin consumidor el 2026-09-19): el sistema dice
   que no hay botón de peligro y que lo que avisa antes de actuar es el texto
   de la pregunta. Un × que se tiñe era
   la excepción — y encima incoherente, porque «Eliminar» y «Cerrar», que son
   palabras y destruyen más, nunca se tiñeron. Las tres acciones que perdieron
   el ámbar ganaron su confirmación en el mismo cambio.

   AL DÍA (2026-08-28, precisado el 08-29): la acción que RETIRA lo seleccionado
   SÍ se tiñe hoy —`.bs-accion.danger`, y la condición es que sea la única
   teñida de su barra, no la única acción—, y esto sigue igual: un glifo suelto
   no se tiñe, y la confirmación no es opcional en ninguno de los dos casos. */

/* ── AQUÍ VIVIÓ LA REJILLA Y SU EDITOR DE CELDA (etapa 4 · 2026-09-02) ──
   Dos piezas: el cursor de celda (`.celda-cursor`, borde INTERIOR y no un halo
   —los 4px del anillo de Bootstrap sobre una celda de 36px son ruido—, y sin
   radio por lo mismo que las filas: sobre una cuadrícula, una esquina
   redondeada deja ver la línea del grid asomando por debajo) y el editor
   superpuesto (`.editor-celda` + `.ec-campo`), que iba en `position:absolute`
   con la MISMA altura de fila.

   **Esa altura no era estética**: un editor que crezca la fila rompe la
   aritmética índice×altura del scroll virtual, que es de donde sale qué ventana
   pintar. Es la regla que hay que recuperar si algún día vuelve a editarse
   dentro de una lista virtualizada; el porqué largo está en `activos/app.js`,
   donde estaba su motor.

   Se retiraron con la selección de columnas: la sábana es para leer y actuar en
   lote, y corregir un dato se hace en la ficha del peek
   (docs/DOC-semilla-crm.md §10). */

/* ── Agrupación: el sustituto real del kanban ─────────────────────────
   La cabecera de grupo es una fila MÁS de 36px dentro del arreglo aplanado.
   Ésa es la única forma de agrupar sin romper la aritmética del scroll
   virtual, que multiplica índice por altura para saber qué ventana pintar. */
/* La cabecera de grupo cruza TODAS las columnas, así que no lleva verticales
   —cortarla en celdas negaría lo que es— pero sí el horizontal, o el grid se
   vería interrumpido cada vez que aparece un grupo. */
.fila-grupo,.grupo-pegado{
  height:var(--app-row-h);
  display:flex;align-items:center;gap:8px;
  padding:0 10px;
  font-size:var(--fs-meta);font-weight:600;
  letter-spacing:.04em;text-transform:uppercase;
  color:var(--app-fg-secondary);
  border-bottom:1px solid var(--grid-linea);
}
.fila-grupo .fg-conteo,.grupo-pegado .fg-conteo{
  font-weight:500;letter-spacing:0;text-transform:none;
  color:var(--app-fg-tertiary);font-variant-numeric:tabular-nums;
}
/* Absoluta sobre la sábana, justo bajo la fila de cabecera. No dentro del
   viewport: ahí se iría con el scroll. El alto de la cabecera va por TOKEN
   (`--sabana-cab-h`) desde el 2026-08-13: era un 30 escrito a mano aquí y otro
   allá, o sea dos sitios que se separan solos el día que la cabecera cambie. */
.grupo-pegado{
  position:absolute;top:var(--sabana-cab-h);left:0;right:0;z-index:2;
  background:var(--app-bg);
  box-shadow:0 1px 0 var(--app-hairline);
  pointer-events:none;
}

/* La trazabilidad es lo que hace que "deshacer" tenga sentido; sin historial,
   un lote malo es irreversible. */
.imp-historial{margin-top:38px;border-top:1px solid var(--app-hairline);padding-top:18px}
.imp-hist-fila{
  display:grid;grid-template-columns:1fr 200px 90px 90px 70px;
  gap:12px;align-items:center;padding:7px 0;font-size:var(--fs-base);
}
.imp-hist-fila .hf-nombre{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500}
.imp-hist-fila .hf-meta,.imp-hist-fila .hf-estado{font-size:var(--fs-meta);color:var(--app-fg-tertiary)}
.imp-hist-fila .hf-num{font-size:var(--fs-meta);color:var(--app-fg-secondary);font-variant-numeric:tabular-nums}

/* ── La conversación (docs/DOC-conversacion.md) ──────────────────────────
   Un solo juego de clases para las DOS superficies: la página pública del
   visitante y el hilo del peek en el CRM. Lo único que cambia entre ellas es
   qué lado es `mia` — el punto de vista, no el diseño.

   EL RELLENO AZUL DE `.mia` NO PROMETE ESCRITURA (movido del bloque de las
   reglas, 2026-09-19): dice de quién es el mensaje, no que pulsar algo escriba
   en la base. Vive dentro de la excepción acotada del hilo —colores literales
   y radios fuera de la escala, declarada en `ui.php` → El color—, y por eso no
   rompe «El azul escribe». */
.chat-pagina{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.chat-caja{
  width:100%;max-width:560px;height:min(80vh,680px);display:flex;flex-direction:column;
  background:var(--app-bg);border:1px solid var(--app-borde);border-radius:var(--r-caja);overflow:hidden;
}
.chat-cab{
  padding:14px 18px;border-bottom:1px solid var(--app-hairline);
  font-family:'Space Grotesk',sans-serif;font-weight:600;font-size:var(--fs-seccion);
}
.chat-hilo{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:8px}
/* En el peek el hilo convive con la ficha y la bitácora: alto acotado y su
   propio scroll, para que no empuje al resto fuera de la vista. */
.chat-hilo-crm{max-height:280px;padding:10px 0;border:0}
.chat-vacio{margin:auto;color:var(--app-fg-tertiary);font-size:var(--fs-meta);text-align:center}
.chat-burbuja{
  max-width:78%;padding:8px 12px;border-radius:14px;font-size:var(--fs-base);
  white-space:pre-wrap;overflow-wrap:anywhere;
}
.chat-burbuja.mia{align-self:flex-end;background:var(--app-accent);color:#fff;border-bottom-right-radius:4px}
.chat-burbuja.suya{align-self:flex-start;background:var(--app-chip);border-bottom-left-radius:4px}
/* 14px (`--fs-micro`) y no los 10 con que nació (2026-08-13). La escala de
   esta casa tiene PISO DE 14 para todo lo que sea LETRA —bajo esa cota sólo
   glifos— y su propio preámbulo pone como ejemplo justo esto: los timestamps
   que otras apps de datos encogen por decorativos. La jerarquía la lleva el
   color (y aquí la opacidad), no el tamaño. La hora de un mensaje se lee para
   saber si vale la pena contestar ya o mañana, así que es dato. Corregido el
   2026-09-19: decía «13px» y «PISO DE 13», valores de antes de que el piso
   subiera a 14 el 2026-09-08. La `opacity:.65` choca con el criterio de
   `.conteo-total` (nunca una atenuación sobre un gris calibrado): anotada como
   deuda, no se cambió aquí. */
.chat-hora{display:block;margin-top:3px;font-size:var(--fs-micro);opacity:.65}
/* El evento de sistema NUNCA sale al visitante; sólo se ve aquí. Por eso es un
   chip centrado y no una burbuja: no es de nadie de los dos lados. */
.chat-evento{
  align-self:center;font-size:var(--fs-meta);color:var(--app-fg-tertiary);
  background:var(--app-chip);border-radius:10px;padding:3px 10px;
}
.chat-pie{display:flex;gap:8px;padding:12px;border-top:1px solid var(--app-hairline)}
.chat-pie .mv-input{flex:1;min-width:0}
/* Quién contesta el chat, arriba del compositor. Tono de meta y no de aviso:
   es estado, no alarma — a diferencia del bloqueo, que sí lo es. */
/* LA LISTA DE LECTURA DE LA FICHA — `.pk-renglones` (2026-09-24). Nació como
   `.pk-citas` para las citas del contacto y subió a nombre de sistema el día que
   la sección «Conversiones» pidió la misma forma: dos superficies, una pieza
   (ui.php → Peek). Renglón de dos líneas —lo que es y su dato— en tinta
   terciaria, porque lo que lista es historia; `.viva` destaca la única fila
   que pide algo (la cita futura). Un problema en el dato va con
   `.glifo.warning` en la segunda línea, no con otro color de letra. Sin gestos
   por renglón: la operación vive en su página. */
.pk-renglones{display:flex;flex-direction:column;gap:8px}
.pk-renglon{display:flex;flex-direction:column;gap:1px}
.pk-renglon-t{font-size:var(--fs-meta);color:var(--app-fg-tertiary)}
.pk-renglon.viva .pk-renglon-t{color:var(--app-fg);font-weight:500}
.pk-renglon-meta{font-size:var(--fs-meta);color:var(--app-fg-tertiary)}
/* Bajo un `.pk-cab` —que anula los 7px del rótulo— la lista los repone como
   `margin-top`, igual que `.ficha-cuerpo` y `.pk-valor`. Sin esto el primer
   renglón de «Conversiones» tocaba el rótulo (medido: 0px contra los 7 de
   Citas, que abre con el rótulo suelto). */
.pk-cab + .pk-renglones{margin-top:7px}
/* EL UMBRAL en la ficha del contacto (2026-09-02, docs/DOC-cliente.md §11.1;
   interruptor desde el 2026-09-03): un renglón de ESTADO con su control a la
   derecha — la misma forma que `.chat-quien` en el panel de la conversación.

   ES UN RÓTULO CON SU CONTROL, no una línea de meta, y por eso cambió el
   reparto de tinta el 2026-09-03: la fila entera iba en `--app-fg-tertiary`
   —correcto cuando era una frase de estado («Contacto no verificado»)— y hoy la
   primera palabra es el RÓTULO del interruptor. La regla del catálogo manda que
   una etiqueta vaya en `--app-fg`, «la palabra que sostiene el formulario»; el
   terciario se queda para el detalle, que es dato.

   QUIEN CEDE EL ANCHO ES EL DETALLE, no el rótulo: un `.sw` no se encoge y un
   rótulo partido en dos líneas deja el control huérfano de su palabra. */
.pk-persona{
  display:flex;align-items:center;gap:7px;
  margin-top:12px;font-size:var(--fs-meta);
}
.pk-persona > :first-child{flex:none;color:var(--app-fg);font-weight:500}
.pk-det{flex:1;min-width:0;color:var(--app-fg-tertiary)}

/* EL ENTE — su propio bloque desde el 2026-09-03 (pedido de Arif). Vivía como
   un segundo `.cf-btn` dentro del renglón de arriba, y ahí no se sabía a cuál
   de los dos estados pertenecía cada gesto.

   LLEVA `.peek-etiqueta` Y EL UMBRAL NO, y es deliberado: «Cliente» nombra un
   campo cuyo VALOR es un nombre de otra tabla —«ACME» a secas no dice en
   calidad de qué aparece—, mientras que el renglón del umbral ya es un rótulo
   con su control al lado. El valor va en `--fs-base` y no en meta porque es un
   dato que se lee, no una anotación; su ausencia («Sin agrupar») baja al
   terciario para que no pese lo mismo que un nombre.

   EL GESTO VA EN LA FILA DEL RÓTULO Y NO EN LA DEL VALOR (2026-09-03,
   reportado por Arif): compartiendo renglón con el nombre del cliente, un
   nombre largo empujaba al `.cf-btn` —que por contrato no se parte ni se
   encoge— y rompía la fila. Arriba, el botón no lo mueve el dato y el nombre se
   queda con el ancho entero. **Los 7px que separan rótulo y valor cambian de
   dueño, no desaparecen**: `.peek-etiqueta` los lleva como `margin-bottom` y
   dentro de la cabecera estirarían la fila del botón, así que ahí se anulan y
   los pone el valor como `margin-top`. El mismo aire, medido desde el otro
   lado.

   NO LLEVA `margin-top` PROPIO: lo da el `.peek-div` que abre el bloque (18
   arriba / 14 abajo, como todo separador de este panel). */
/* La DISPOSICIÓN de esta cabecera vive arriba, en `.pk-cab`, que es la de las
   cuatro secciones del panel. Aquí se quedan los porqués, que sí son de este
   bloque. */
/* `overflow-wrap:anywhere` y no elipsis: el nombre del ente es el dato de esta
   sección, así que cabe entero aunque tome dos líneas — recortarlo dejaría dos
   clientes distintos con el mismo texto en pantalla. */
/* EL VALOR DE UNA SECCIÓN DEL PEEK — lo que va debajo de un `.pk-cab` cuando el
   rótulo ya lo puso la cabecera y no hace falta repetirlo.

   SE LLAMÓ `.pk-ente-valor` HASTA EL 2026-09-04, por su único consumidor de
   entonces (el nombre del cliente). Ese día ganó el segundo —el CIERRE, que
   pasó a leerse y a editarse en su propio diálogo— y el nombre se quedó
   mintiendo: un token con nombre de UN consumidor deja de reconocerse en cuanto
   aparece el segundo, que es la lección que este proyecto ya pagó con
   `--fs-peek` y con `--btn-radio`. La regla del catálogo lo dice al derecho:
   una pieza sube cuando la usan dos superficies, y sube CON nombre de sistema.

   Los 7px son el mismo aire con que `.ficha-cuerpo` abre bajo su cabecera: el
   contenido de una sección arranca a 7 de su rótulo, lo ponga quien lo ponga. */
.pk-valor{margin-top:7px;font-size:var(--fs-base);overflow-wrap:anywhere}
.pk-valor.vacio{color:var(--app-fg-tertiary)}

/* ── Citas: operación (citas.php) y configuración (citas-config.php) ─────
   La CONFIGURACIÓN está en el molde consola (corregido el 2026-09-19: decía
   «sigue en el molde `.nc-fila`, como campos y funnels»). La
   OPERACIÓN se mudó a la SÁBANA el 2026-08-14 (pedido de Arif: «modela citas
   sobre leads»): mismas piezas —`.sabana`, `.sabana-cabecera`, `.fila`,
   `.fila-rejilla`— y por tanto cuadrícula completa, cabecera teñida y hoja a
   pantalla completa con scroll propio. La página no declara ni una medida de
   la tabla: su `--rejilla` va en el marcado, que es donde se sabe si hay
   columna Atiende.

   Con eso se fueron `.ct-fila` y su variante `.ct-solo`, y con ellas el defecto
   que arrastraban: cuatro anchos FIJOS (140+110+96+140) más dos mínimos daban
   un piso duro de 886px dentro de una `.hoja` de 880, así que la variante
   multi-agenda desbordaba SIEMPRE recortando contacto y servicio (destapado al
   sembrar una segunda agenda, 2026-08-12). Ya no hay hoja de 880 que desbordar
   —la sábana ocupa el ancho del panel— y las columnas ceden con `minmax`.
   También se fue `.ct-medio`: el medio de contacto dejó de ir bajo el nombre y
   tiene columna propia, que es la regla de la sábana (un dato por columna) y lo
   que permite la fila de una línea con elipsis.

   Lo que la página le quita a la sábana es el `flex` de la primera celda, que
   allá centra la caja del check y aquí rompería el elipsis de la fecha. Y
   desde el 2026-09-24, como abre con un dato, destapa el filo entre sus dos
   congeladas y corre el `left` de la segunda (junto a registros y prenómina,
   en el bloque del filo destapado).

   EL CURSOR DE MANO VOLVIÓ (2026-08-15). Vivió en `default` con un motivo
   escrito —«la fila de leads ABRE su peek al pulsarla y aquí no hay nada que
   abrir; un puntero que promete un gesto inexistente es peor que ninguno»— y
   ese motivo se acabó el día que la fila SÍ abre: el detalle de la cita, con el
   veredicto al pie (`.ct-peek`, citas.php). La regla no cambió; cambió el
   hecho, así que la excepción se retira en vez de quedarse contradiciendo al
   `cursor:pointer` que `.fila` trae de fábrica. */
/* Lo que sigue lo comparten las DOS páginas que adoptaron el molde —citas y
   envíos (2026-08-16)—, y por eso viven como lista de selectores en vez de
   como una segunda copia con los mismos números: un ajuste de la celda que
   lleva un badge o un botón es del MOLDE, no de la página. Si aparece un
   tercer consumidor, esto sube al catálogo (`ui.php` → «Las reglas»: una
   pieza sube cuando la usan dos superficies) con nombre de sistema. */
.ct-sabana .fila-rejilla > :nth-child(1),
.env-sabana .fila-rejilla > :nth-child(1),
/* La lista de ventas entró el 2026-08-18 por la misma puerta: su primera celda
   es el FOLIO, y el `flex` que allá centra la caja del check le rompería el
   elipsis.

   GASTOS ESTUVO AQUÍ Y SE FUE (2026-08-27 → 2026-08-28), y LA LISTA DE VENTAS
   detrás el mismo día. Entraron por lo mismo —abrían con el folio— y salieron
   al ganar su barra de selección: su primera columna es ahora el CHECK, o sea
   el caso del molde, y con el `display:block` la caja se quedaba pegada arriba
   de una celda estirada a toda la fila. Las tres reglas que esas sábanas traían
   por abrir con folio se retiraron juntas —ésta, el `left` del segundo congelado
   y el filo destapado—, porque las tres dependen del mismo hecho. **Queda
   `.rg-sabana` sola**, que es la única que sigue abriendo con un dato; el día
   que el libro de caja gane su barra, esta lista se queda vacía y el bloque
   entero se retira. */
/* Y la del libro de caja, el 2026-08-27: su primera celda es el folio del
   movimiento, el mismo caso exacto que las dos de arriba. */
.rg-sabana .fila-rejilla > :nth-child(1){display:block}
.ct-cuando,.env-cuando{font-weight:500}

/* ── LA CABECERA ALINEADA CON SUS FILAS ─────────────────────────────────
   El defecto (2026-08-16, medido en envíos al pedir Arif que se revisara la
   alineación tras quitar los gestos de la fila): la cabecera y las filas son
   DOS contenedores de scroll distintos y sólo el de las filas scrollea en
   vertical, así que en cuanto la lista pasa del alto de la hoja el viewport
   pierde el ancho de la barra —6px medidos en este Chrome, 15 en otros
   sistemas— y la cabecera no. Las columnas elásticas reparten lo que sobra, de
   modo que el canal se acumula de izquierda a derecha: 0 en la primera columna
   y el canal entero en la última, o sea el rótulo despegado de su columna. Es
   un defecto del MOLDE —lo tienen las siete sábanas que pintan sus filas en
   PHP— y sólo aflora con filas suficientes para scrollear.

   AQUÍ VIVIÓ SU PARCHE HASTA EL 2026-08-29: `scrollbar-gutter:stable` en los
   dos contenedores, en una lista de siete prefijos que fue creciendo módulo a
   módulo. Alineaba, sí, pero RESERVANDO EL CANAL SIEMPRE — y sin barra eso deja
   una franja de 6px sin pintar entre la última celda y el borde derecho del
   marco: la cabecera teñida y las líneas de las filas mueren antes del filo, y
   el borde se lee despegado de su tabla. Visible en cuanto la lista es corta,
   que es el caso normal de estas siete pantallas.

   Lo cazó Arif comparando contra leads, que nunca lo tuvo por el motivo que
   ahora rige: **mide el canal en vez de reservarlo**. Eso es hoy
   `window.CANAL_SABANA` (app.js, arriba del corte), que hace lo mismo para las
   siete y por el mismo camino que leads ya usaba en su `render()`. Con la
   medición no hay nada que reservar: sin barra la cabecera no lleva padding y
   el contenido llega al filo; con barra lo lleva y coinciden.

   NO QUEDA NADA QUE DECLARAR AQUÍ, y por eso esta nota se queda sin regla: la
   lista de siete prefijos —que la nota anterior ya iba anotando como señal de
   que esto pedía promoverse a nombre de sistema— desapareció entera. La
   promoción quedó hecha por otro camino: el motor mira `.sabana` y se excluye
   solo de la única que ya se mide, la del scroll virtual. */
/* EL RENGLÓN RETIRADO SE BAJA DE TONO en vez de esconderse: sigue siendo
   historia que explica por qué ese horario volvió a estar libre, por qué falta
   un folio o por qué ese contacto ya no trabaja. Lo usan siete sábanas y las
   filas de configuración.

   ── SE BAJA CON COLOR, NO CON `opacity` (2026-09-04, reportado por Arif:
      «¿en las filas desactivadas se deforma el estilo de las líneas de la
      celda?») ──
   Sí se deformaba, y el mecanismo estaba diagnosticado en esta misma hoja desde
   el 2026-09-03 sin que nadie lo aplicara AQUÍ: **`opacity` compone el subárbol
   entero y no se deshace desde un hijo**. Con `.5` sobre la fila caían con el
   texto tres cosas que no son texto:

     · **la línea horizontal del grid** (el `border-bottom` de `.fila`), que
       pasa de `alpha .108` a `.054` — la mitad de tinta— así que la cuadrícula
       se INTERRUMPE en cada fila retirada;
     · **las líneas verticales** de cada celda (`border-right`), por lo mismo;
     · **el fondo OPACO de las dos columnas congeladas**, que existe justamente
       para que no se vea correr el contenido por debajo — al 50% se transparenta
       y el texto de las columnas que viajan **se ve a través** de la fila.
       Medido con la sábana desplazada a 240px.

   El tercero es el que lo delata a simple vista y el que ninguna cantidad de
   «se ve más claro» explica: no es que la fila esté atenuada, es que está
   ROTA.

   HOY SE ATENÚA CON `color`, que es lo que este sistema hace en todas partes
   —`--app-fg-tertiary` es literalmente «esto pesa menos»— y lo que el velo
   nunca debió sustituir. Con eso las líneas, los fondos y **la casilla**
   conservan su tinta: la casilla importa porque es el único control de una fila
   retirada, el que la despierta, y bajo el velo era el elemento menos legible
   del renglón (medido el 2026-09-03 sobre la perilla: 4.96:1 caía a ~2.3).

   `> *` y el descendiente para las celdas que declaran su propio color
   (`.col-secundaria`, y las anidadas de items y clientes). **Se excluyen las
   piezas que llevan tinte propio** —badge y chip de etiqueta—: son DATO y su
   color significa algo; recolorearlas sería borrar un estado para decir otro. */
.ct-muerta{color:var(--app-fg-tertiary)}
.ct-muerta *:not(.badge):not(.etiqueta):not(.chip-x){color:inherit}
/* AQUÍ VIVÍA `.ct-acciones`, la celda del «Cancelar» de la fila, y se retiró
   el 2026-09-24 con el gesto: la fila de citas ya no lleva controles, como la de
   envíos (ver `.env-acciones` más abajo).
   La celda del estado lleva un badge y hereda el `line-height` de 35px que
   centra el TEXTO de la fila — con él, el badge crecía hasta tocar el de la
   fila de abajo. Se devuelve a 1 y el centrado lo hace el flex. */
.ct-estado,.env-estado{display:flex;align-items:center;line-height:1}
/* La alerta de envíos, entre el título y la tabla. Lo único que la clase añade
   a la pieza del sistema es su sitio: el margen que la separa de la hoja y el
   `flex:none` que pide `.app-panel` —columna flex—, sin el cual el bloque se
   aplasta cuando la tabla reclama alto. Ni un color ni un borde propios: el
   ámbar y el icono los pone `.alerta.warning`.

   `.ct-alerta` era su gemela en citas —el atraso de las citas sin cerrar— y se
   retiró con ella el 2026-08-29: la lista ya lo dice con el orden y el badge
   «Sin cerrar», y una banda de ancho completo para repetirlo le comía alto a la
   tabla (docs/DOC-agenda.md). */
.env-alerta{flex:none;margin-bottom:14px}
/* La nota del titular, en el mismo aire pero un escalón por debajo: es la
   advertencia de que la lista viene recortada, no trabajo pendiente. Mismo
   cuerpo y mismo tono que `.cf-nota`, la nota de su hermana de configuración;
   no se comparte la clase porque son dos páginas. */
.ct-nota,.env-nota{font-size:var(--fs-meta);color:var(--app-fg-tertiary);margin:0 0 10px;flex:none}

/* ── EL ALTA MANUAL: lo poco que el diálogo añade (interno/cita-nueva.php) ──
   Los dos selectores de día y hora son los de la reserva pública, compartidos
   por lista de selectores más abajo (`.ag-dias,.cn-dias`…). Aquí sólo vive lo
   que cambia por estar DENTRO de un campo de formulario en vez de suelto en
   una página: el aire entre los dos escalones y la línea que habla cuando no
   hay nada que ofrecer.

   `.cn-paso` no reusa `.ag-paso` (18px de separación entre pasos de una página
   pública) a propósito: dentro de un `.campo` el aire ya lo pone el `gap` de 5
   del campo y el de 10 de `.campos`, y sumarle 18 partiría el campo en dos
   bloques que se leen como secciones distintas. Es el mismo criterio que
   `.vn-renglon` aplica al redeclarar `--esp-opcion` en el peek: dentro de una
   capa el alto cuesta filas. */
.cn-paso{margin-top:8px}
/* EL AIRE ANTES DEL PIE (corrección de Arif, 2026-09-04: «crea un poco más de
   espacio entre el textarea de notas y el footer»). Medido antes: 22px entre el
   filo del textarea y el borde de «Crear» —4px del `padding` del cuerpo, 1 del
   filo transparente, 9 del margen negativo y 8 del `padding-top` del pie—.
   Suficiente para un `<input>`, corto para un textarea: la esquina de abajo a
   la derecha lleva la manija de `resize:vertical`, así que el gesto de estirar
   las notas y el de pulsar «Crear» quedaban a un pelo el uno del otro.

   +10, que NO es un número nuevo: es el `gap` de `.campos`, el mismo aire que
   separa dos campos entre sí. El pie es lo que sigue al último campo, así que
   se le da la misma distancia — es la regla del catálogo de que el aire es la
   estructura y sale de valores que ya existen, no de un ajuste a ojo.

   VA EN UNA CLASE DEL DIÁLOGO Y NO EN `.dlg-pie`, que es del sistema: la
   tensión sólo aparece cuando el último campo es un textarea, y de los diálogos
   de la app éste es el único así. El día que un segundo lo tenga, esto sube a
   `.dlg-cuerpo:has(> .campo:last-child textarea)` o al pie, y deja de ser de
   citas — pero con un consumidor eso sería legislar sobre un caso. */
.cn-cuerpo{padding-bottom:14px}
/* HABLA EN TRES MOMENTOS y por eso es un renglón fijo y no un estado vacío:
   mientras el motor calcula, cuando la oferta no tiene ni un día libre en toda
   su ventana, y cuando el día elegido se quedó sin huecos. Los tres son el
   mismo hecho —no hay nada que elegir aquí— y el `.estado-vacio` del sistema
   es una pieza de PÁGINA, con su icono y su título centrados: dentro de un
   campo de diálogo diría lo mismo ocupando ocho veces el alto. */
.cn-slots-nota{font-size:var(--fs-meta);color:var(--app-fg-tertiary);line-height:1.45}

/* ── LA CUENTA CONTABLE DERIVADA (`.gn-cuenta`, diálogo de nuevo gasto) ──
   NO es una «pista de ayuda» del formulario, y la distinción decide su tono:
   es un VALOR calculado que cambia con el selector de arriba —«Se carga a 5003
   · Gastos de venta»— y que nace diciendo la regla que lo gobierna. Por eso
   arranca en terciario, como una nota, y sube a secundario cuando ya dice un
   dato: `.puesta` es lo que separa «te explico cómo funciona» de «esto es lo
   que va a pasar».

   Va PEGADA a su control (4px) y no al aire de `.campos` (10px): pertenece al
   campo de la categoría, no es el siguiente renglón del formulario.

   NO SUBE AL CATÁLOGO todavía. La app no tiene una pieza «línea de ayuda bajo
   un campo» —se miró antes de escribir esto—, y ésta sería la candidata obvia;
   pero tiene UN consumidor y la regla de la casa pide dos. El día que un
   segundo formulario necesite decir algo debajo de un control, esto sube a
   `ui.php` con nombre de sistema en vez de copiarse con otro prefijo. */
.gn-cuenta{
  margin-top:-1px;font-size:var(--fs-micro);line-height:1.4;
  color:var(--app-fg-tertiary);
}
.gn-cuenta.puesta{color:var(--app-fg-secondary)}

/* El aviso de que faltan categorías: lo único que la alerta necesita aquí es
   separarse del formulario que condiciona. El tinte, el filo y el icono los
   pone `.alerta.warning`. */
.gn-falta{margin-bottom:14px}

/* ── EL PANEL DE LECTURA (`.ct-peek` en citas.php, `.env-peek` en envios.php)
   El panel lateral de leads con otro contenido: el cuerpo entero —fondo,
   filete, sombra, ancho, deslizamiento— lo pone `.peek`, y su nav, su scroll,
   sus etiquetas y su pie anclado los ponen las clases de esa familia. Lo de
   aquí es SÓLO lo que un panel de LECTURA necesita y el de leads no tiene,
   porque allá casi todo son controles con su propio espaciado.

   Nació para las citas el 2026-08-15 y lo estrenó su segundo consumidor el
   2026-08-16, cuando la cola de envíos adoptó el mismo molde: el detalle de un
   envío se lee exactamente igual —badge de estado, renglones etiqueta-dato,
   un bloque de prosa ajena y los gestos al pie—. Se comparte por LISTA DE
   SELECTORES y no copiando los números a un `.env-*` propio, que es como nacen
   dos piezas que se parecen y divergen. Con un tercer consumidor esto sube al
   catálogo con nombre de sistema (`.peek-dato`, `.peek-texto`…), que es la
   regla escrita en `ui.php` → «Las reglas».

   Nada de esto redefine una pieza de `.peek`: si un día el panel cambia de
   ancho o de sombra, estos dos lo siguen solos. */

/* EL RENGLÓN ETIQUETA-DATO — `.peek-dato`, pieza de SISTEMA desde el
   2026-08-18. Nació como `.ct-dato,.env-dato` con la promoción ya escrita
   arriba («con un tercer consumidor esto sube al catálogo con nombre de
   sistema»), y ese tercer consumidor llegó: el panel de la venta
   (`venta-lista.php`). Se renombró en vez de sumarle un `.vt-dato` a la lista,
   que es exactamente lo que la nota pedía no hacer.

   Sube SOLA y no el bloque entero: las otras piezas de esta familia —el badge
   de estado, la prosa ajena, la nota de procedencia, el pie de gestos— siguen
   con dos consumidores, y una pieza sube cuando la usan tres, no cuando su
   vecina subió.

   El aire va ARRIBA y no abajo para que el primero no se despegue de lo que
   tenga encima (el badge de estado, que ya trae el suyo). */
.peek-dato{margin-top:16px}
.peek-dato > div:last-child{font-size:var(--fs-base);color:var(--app-fg)}
/* EL VALOR AUSENTE, ATENUADO (2026-09-04). Lo estrena la ficha del contacto al
   pasar a lectura: sus campos se pintan SIEMPRE —el contador «N sin llenar» de
   su cabecera no serviría de nada si los vacíos se escondieran—, así que el
   hueco se dice con palabra («Sin dato», «Sin ubicar») y se baja de tono.

   Va como `.peek-dato > .vacio` y no como `.vacio` a secas por especificidad:
   la regla de arriba apunta al último hijo (0,1,1) y le ganaría a una clase
   suelta (0,1,0). Mismo tono y mismo nombre que `.pk-valor.vacio`, que es
   el otro sitio del panel donde una ausencia tiene que verse sin pesar lo que
   un dato.

   LOS OTROS SEIS CONSUMIDORES no cambian: esconden el bloque entero con
   `hidden` cuando el dato falta, y eso sigue siendo correcto ahí — en un panel
   de venta o de envío, un campo sin valor no es trabajo pendiente. */
.peek-dato > .vacio{color:var(--app-fg-tertiary)}
/* Oculto cuando su dato no está: el JS le pone `hidden` y esto lo hace valer
   sobre el `display` del bloque. */
.peek-dato[hidden]{display:none}

/* El badge de estado, pegado bajo el subtítulo: es lo primero que se mira al
   abrir —dice si esta cita todavía pide algo— y por eso va antes que el dato. */
.ct-peek-estado,.env-peek-estado{margin-top:14px}

/* LA PROSA RESPETA SUS SALTOS — nombre de sistema desde el 2026-09-04, con su
   tercer consumidor (la descripción del producto en su panel). Se llamó
   `.ct-peek-notas` / `.env-peek-texto`: **dos prefijos de página compartiendo
   una regla por lista de selectores**, que es la forma que tiene una pieza de
   sistema antes de que alguien le ponga el nombre. Mismo camino que `.pk-cab`,
   `.peek-dato`, `.peek-cifra`, `.pk-valor` y `.pk-cab-dato`.

   QUÉ DECLARA: es lo único del panel que **no escribió el sistema**. Quien
   reserva una cita escribe párrafos en las notas, el correo que salió de un
   drip los lleva por definición, y una descripción tecleada en un textarea de
   dos líneas los admite desde que ese campo dejó de ser un `<input>` — el
   mismo día que esta pieza ganó nombre. Colapsarlos en un bloque corrido
   convierte una lista de puntos en una frase ilegible.

   `pre-wrap` conserva el salto y sigue partiendo la línea larga;
   `overflow-wrap:anywhere` evita que una URL pegada —que en una nota de
   reserva, y más en un correo, es lo más normal— empuje el panel a lo ancho.

   LA REGLA PARA EL PRÓXIMO CAMPO: **si el usuario puede teclear un Enter ahí,
   lo que lo pinta lleva esta clase.** Es la comprobación que hay que hacer al
   convertir un `<input>` en `<textarea>`, y no llega sola. */
.peek-prosa{white-space:pre-wrap;overflow-wrap:anywhere;line-height:1.45}

/* Cuándo se reservó / cuándo se encoló. Cierra el cuerpo en la tipografía de
   las notas al pie: es procedencia, no dato de trabajo. */
.ct-peek-alta,.env-peek-alta{margin-top:20px;font-size:var(--fs-micro);color:var(--app-fg-tertiary)}

/* El pie con los gestos. `.peek-pie` ya trae el filete, el aire y el `flex:none`
   que lo mantiene fuera del scroll; lo de aquí es sólo cómo se reparten el
   ancho, y **desde el 2026-09-04 las dos pantallas lo reparten distinto**.

   El `[hidden]` se nombra porque `.peek-pie` es `display:flex`, y una regla de
   clase gana al `display:none` que el atributo trae del user-agent: sin esto,
   una cita ya cerrada —o un envío que ya salió— enseñaría un pie vacío con su
   filete. */
.ct-peek-pie[hidden],.env-peek-pie[hidden]{display:none}

/* ENVÍOS: mitades. Sus dos gestos —reintentar y cancelar— son dos respuestas a
   la misma pregunta, y darle a una más superficie que a la otra la recomienda.
   Es la regla que las dos pantallas compartían, y sigue siendo cierta AQUÍ. */
.env-peek-pie > button{flex:1}

/* ── CITAS: A LA IZQUIERDA SE EDITA, A LA DERECHA SE CIERRA ───────────────
   (2026-09-04, lo cazó Arif: «que el botón cancelar ya no sea tipo block
   100%».) El `flex:1` compartido se escribió para el caso de DOS gestos y el
   de uno cayó por el hueco: una cita futura ofrece sólo «Cancelar la cita», y
   estirada a todo el ancho del panel esa única acción destructiva se leía como
   la acción principal de la pantalla — justo lo contrario de lo que el catálogo
   pide para una salida destructiva («encontrable sin ser tentadora»).

   Con «Editar» al lado, además, el reparto por igual sería peor que ancho:
   **son dos preguntas distintas**. El veredicto CIERRA la cita («¿qué pasó?») y
   editar la deja abierta («¿la muevo?»). Tres botones del mismo tamaño en una
   tira se leen como tres opciones de una misma lista.

   Así que ninguno se estira y el `margin-left:auto` del segundo hijo abre el
   hueco: editar ancla a la izquierda, el veredicto —uno o dos— cierra a la
   derecha. Es el mismo recurso que `.pk-cab-dato` usa en la fila del rótulo del
   peek y que `.dlg-pie` declara con su `.hueco`, sin estrenar una pieza.

   `:nth-child(2)` y no una clase en el veredicto porque el orden ES el
   contrato: «Editar» lo pinta `citas.js` SIEMPRE primero cuando existe, y
   cuando no existe tampoco hay veredicto (los dos salen del mismo `confirmada`)
   — así que un pie con un segundo hijo es siempre un pie con editar delante. */
.ct-peek-pie > button{flex:none}
.ct-peek-pie > button:nth-child(2){margin-left:auto}

.cf-nota{font-size:var(--fs-meta);color:var(--app-fg-tertiary);margin:0 0 8px}
/* EL GESTO DE UN RENGLÓN — contorno de `.btn-suave`, fondo TRANSPARENTE
   (2026-08-16, decisión de Arif tras comparar cuatro tratamientos en vivo).

   ── QUÉ ESTABA MAL, y no era la falta de caja ──
   Nació sin contorno, en `--app-fg-tertiary` y a `--fs-meta`, que son
   EXACTAMENTE las tres propiedades de `.hs-metas`: en el renglón de la oferta,
   «20 min · descanso 30» (que no se pulsa) y «Editar» (que sí) eran los mismos
   píxeles. No se distinguía del dato que tenía al lado.

   ── EL CONTORNO SE COMPARTE; EL RELLENO ES LO QUE LOS SEPARA ──
   Lleva el filo y el radio de `.btn-suave`, así que el sistema tiene UNA
   familia contorneada en dos tamaños en vez de una pieza nueva. Pero va
   **transparente** y aquélla lleva `--app-relleno-tenue`, y esa diferencia es
   deliberada (corrección de Arif sobre la primera versión, que las igualaba):
   `.btn-suave` es la SALIDA del pie de un diálogo y su relleno existe para que
   sea encontrable; un gesto de renglón no tiene que encontrarse, tiene que
   estar. Igualarlas habría hecho que el «Cancelar» de un diálogo y el
   «Copiar» de una fila fueran el mismo objeto a dos tamaños.
   De paso el gesto vuelve a la regla de los cinco radios —«pill lo que se
   pulsa»— que llevaba incumpliendo con sus 4px desde que nació.

   Se descartó dejarlo sin caja subiendo sólo el color (la vía «ghost» de
   Material/Primer/Apple, que era la recomendación de Claude): con contorno el
   gesto se ve ANTES de apuntarlo, y en una página que es toda renglones eso
   pesa más que la discreción.

   ── DOS PIEZAS Y NO UNA (movido del catálogo `ui.php`, 2026-09-19) ──
   El censo del 2026-08-16 encontró cinco gestos sueltos con cuatro radios
   distintos (4, 8, un `3px` suelto y 50%) que eran en realidad dos piezas:
   `.cp-flecha`, `.nc-quitar` y `.chat-quien-btn` viven AL LADO de lo que
   operan y se plegaron en `.cf-btn`; `.fx-etq-x` y `.hs-x` viven DENTRO de lo
   que borran y se plegaron en `.chip-x`. Forzar la píldora del renglón dentro
   de un chip se ve mal al instante: la distinción es real.

   ── EL RÓTULO EMPIEZA EN MAYÚSCULA (mismo origen, 2026-08-16) ──
   Todo rótulo pulsable de la app empieza en mayúscula —botones, opciones de
   menú, acciones del titular, las dos respuestas de una confirmación—, y
   `.cf-btn` era el único en minúscula, en ~30 gestos de tres páginas: esos
   renglones se leían como de otro producto. Se alineó con el resto (petición
   de Arif). Sobrevivió porque el componente no estaba en el catálogo. */
.cf-btn{
  --cf-py:2px;
  display:inline-flex;align-items:center;justify-content:center;
  background:transparent;color:var(--app-fg-secondary);
  box-shadow:inset 0 0 0 1px var(--app-borde-muted);
  border-radius:var(--r-pill);padding:var(--cf-py) 9px;
  font-size:var(--fs-meta);line-height:var(--btn-lh);cursor:pointer;text-align:center;
  /* Un rótulo de gesto NO se parte ni se encoge. Lo traía `.chat-quien-btn` y
     se perdió al plegarla: «Quitar bloqueo» cayó a dos líneas y la píldora
     midió 42px de alto en vez de 23. Quien cede el ancho es el texto del
     renglón, que para eso envuelve. */
  white-space:nowrap;flex:none;
  transition:background var(--app-dur) var(--app-ease), color var(--app-dur) var(--app-ease);
}
.cf-btn:hover{background:var(--app-chip);color:var(--app-fg)}
/* LA FORMA GLIFO — un solo carácter (×, ↑, ↓) en vez de una palabra. Mismo
   alto y caja CUADRADA, y el cuadrado no es un número a ojo: sale del átomo de
   control, igual que la × del diálogo. Sin esto, la píldora de una palabra
   aplicada a un carácter da una pastilla ancha y ridícula.

   `inline-flex` en la pieza base y no aquí: un gesto tiene que centrar su
   contenido viva donde viva, y así el `min-width` funciona también fuera de un
   contenedor flex. */
.cf-btn.glifo{
  padding:var(--cf-py) 0;
  min-width:calc(var(--fs-meta) * var(--btn-lh) + var(--cf-py) * 2);
}
/* El aviso de Google: ámbar, que en este sistema significa «mira esto», no
   «error». No escribe letras de color — tiñe el fondo y el texto va en el color
   del cuerpo, que es la regla del ámbar. */
.cf-google-mal{
  font-size:var(--fs-micro);background:var(--app-amber-tint);color:var(--app-fg);
  border-radius:var(--r-mini);padding:1px 6px;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}

/* ── El riel de la agenda: la semana, no una tabla (2026-08-15) ────────
   citas-config pasó al molde consola+riel de funnels; las cuatro rejillas
   `.cf-*-fila` que vivían aquí murieron con sus filas-tabla. El horario se
   lee COMO SEMANA: siete renglones fijos —el día cerrado también habla— y
   cada franja es un chip de hora con su × susurrada, el mismo trato que los
   gestos raros de `.cfg-fila`. La forma fue y volvió el mismo día: chips por
   día → un rango por fila → chips por día; con el riel a 5/12 el día
   repetido en cada fila era lo que sobraba (decisión de Arif). Que dos
   franjas del mismo día no se encimen lo cuida el modelo (m-cita.php,
   guardar-horario), no la pantalla. Los bloqueos comparten molde debajo: un
   renglón por excepción. El alta de franja es el renglón punteado al pie.

   `.lado-cinco`: panel y riel a 7/12 y 5/12 del ancho tras la barra. Nació
   para ESTA página (corrección de Arif) — el horario es el dato que más se
   toca aquí y a 336px quedaba apretado — y desde el 2026-08-16 lo comparte
   **funnels**, que pedía lo mismo por otro motivo: su riel lleva la puesta en
   marcha (diez pasos rotulados) y el pulso, y a ancho fijo el reparto real
   caía en ~9/3, con los rótulos del checklist partiéndose en dos líneas.
   El `:has()` le da al panel su 7 sólo cuando el riel ancho está presente;
   las demás páginas ni se enteran.

   EL 5/12 LLEVA TOPE: 400px (2026-09-19, plan del catálogo, etapa 6). Sin él
   el riel crecía con la pantalla —481 a 1440, 681 a 1920— y la lista de la
   puesta en marcha, que empuja su «opcional» al canto, lo dejaba a 460px de
   su rótulo: dos palabras de la misma fila leídas como dos columnas. Medido
   en funnels.php a 1920: con 480 el hueco mayor era 259, con 420 era 199 y
   con 400 queda en 179, sin truncar ningún paso; se eligió 400 por el margen.
   A 1280 el 5/12 ya da 415, así que el tope apenas se nota; a 1440 y 1920 lo
   que el riel deja lo toma el panel. Plegado (≤1024) el tope se anula: ahí
   el riel es un bloque a lo ancho de la columna. */
.app-lado.lado-cinco{width:auto;flex:5 1 0;min-width:0;max-width:400px}
.app-shell:has(> .lado-cinco) .app-panel{flex:7 1 0}
/* AQUÍ VIVIÓ `.lado-seis` — panel y riel a MITADES (2026-08-16 · 2026-09-05).
   Era el reparto de AUTOMATIZACIÓN y su único consumidor: el riel de esa
   página no era un resumen sino el VISOR del recorrido del drip —tarjetas con
   el copy de cada paso—, y una tarjeta que enseña un correo necesita el ancho
   de leerlo.

   SE RETIRA CON SU CONSUMIDOR, y el porqué lo tenía escrito el catálogo antes
   de que pasara (`ui.php` → El riel lateral): *«si el riel de una página crece
   hasta pedir la mitad de la lectura, lo que hay que revisar no es su ancho
   sino si eso debería ser una página»*. Eso es exactamente lo que se hizo — el
   recorrido es hoy `drip.php`—, así que la mitad y mitad se queda sin nadie
   que la pida y un ancho sin consumidor es código muerto.

   La lección que deja escrita, por si vuelve a hacer falta: la mecánica es la
   del cinco —`width:auto;flex:6 1 0;min-width:0` en el riel y `flex:6 1 0` al
   panel bajo `:has(> .lado-seis)`—, y lo que hay que preguntarse ANTES de
   escribirla es si lo que pide la mitad no está pidiendo en realidad su propia
   pantalla. */
.hs-metas{font-size:var(--fs-meta);color:var(--app-fg-tertiary);white-space:nowrap;flex:none}
.hs-dia{display:flex;align-items:baseline;gap:10px;padding:5px 0;min-height:30px}
.hs-dia + .hs-dia{border-top:1px solid var(--app-hairline)}
.hs-dia-n{width:34px;flex:none;font-weight:600;font-size:var(--fs-meta)}
.hs-dia.vacio .hs-dia-n{color:var(--app-fg-tertiary);font-weight:500}
.hs-chips{display:flex;flex-wrap:wrap;gap:6px;min-width:0}
.hs-cerrado{font-size:var(--fs-micro);color:var(--app-fg-tertiary)}
/* EL CHIP DE FRANJA LLEVA UN GESTO EN CADA EXTREMO desde el 2026-09-11: el
   lápiz que edita a la izquierda y la × que quita a la derecha. Que no se toquen
   es el motivo del reparto, y es el mismo que el pie del panel de la cita ya
   tenía escrito (DOC-agenda §9: «a la izquierda se edita y a la derecha se
   cierra»): dos cajas de 24 pegadas, una de ellas destructiva, son una trampa de
   dedo. El precio, dicho: 27px de ancho permanente por chip —la caja se reserva
   aunque el gesto esté a opacidad 0, que es lo que impide que el chip crezca bajo
   el cursor— y con ellos la franja pasa de 134 a 161px. Se paga porque `.hs-chips`
   envuelve: lo que cede es una segunda línea en un día de dos franjas cuando el
   riel se estrecha, no un dato truncado. */
.hs-franja{
  display:inline-flex;align-items:center;gap:3px;
  font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:var(--fs-micro);
  font-variant-numeric:tabular-nums;color:var(--app-fg-secondary);
  background:var(--app-chip);border-radius:var(--r-mini);padding:2px 7px;white-space:nowrap;
}
/* Los gestos de estos dos SUSURRAN, y esa es la única diferencia que se conserva
   frente a la × del editor de etiquetas: allá se están gestionando etiquetas y la
   × es el trabajo; aquí la semana se LEE casi siempre, y veinte gestos permanentes
   sobre diez franjas son ruido. Que susurre es propiedad del CHIP, no del gesto
   —por eso la regla vive aquí y no en `.chip-x`—.

   LOS CUATRO SELECTORES SE PLEGARON EN DOS el 2026-09-11, al ganar el bloqueo su
   lápiz: la franja y el bloqueo llevan los mismos dos gestos y querían la misma
   regla, y escribirla por extenso eran cuatro líneas que hay que acordarse de
   tocar de a cuatro. `:is()` ya lo usa el cascarón. */
:is(.hs-franja,.hs-bloqueo) :is(.chip-x,.chip-lapiz){opacity:0;transition:opacity .12s}
:is(.hs-franja,.hs-bloqueo):is(:hover,:focus-within) :is(.chip-x,.chip-lapiz){opacity:1}
/* AQUÍ VIVIÓ `.hs-add` — el renglón punteado del alta de horario
   (2026-08-15 · 2026-09-05). Se retira con su único consumidor, que pasó a ser
   un `.btn-segunda`, y el porqué afina una regla del catálogo en vez de
   romperla: **el renglón punteado es el alta EN LÍNEA** —promete «aquí va a
   nacer uno» y el uno aparece ahí mismo—; **lo que abre una CAPA lleva cara**.
   Este alta abría un diálogo, así que llevaba las dos señales contradictorias
   a la vez.

   QUEDA UN CASO POR MIRAR con el mismo rasero, y se anota en vez de tocarse:
   `.fila-agregar` de `drip.php` («＋ Nuevo paso») también abre un cajón. Ahí el
   punteado dice además «el camino sigue aquí», que es un argumento que este
   alta no tenía — por eso no se cambió de oficio. */
/* AQUÍ VIVIÓ `.hs-alta`, el envoltorio del botón «Nuevo horario» al pie de la
   lista de días. Se retiró el 2026-09-06 con su único consumidor, cuando el
   alta subió a `.lado-cabeza` (el porqué, en `citas-config.php`): allí el
   empuje al canto y el ancho por contenido los pone `.lado-cabeza`, así que no
   quedaba nada que esta clase declarara. Lo único que hacía eran 10px de aire
   sobre un botón que ya no está debajo de nada. */
.hs-nada{font-size:var(--fs-meta);color:var(--app-fg-tertiary);line-height:1.5;margin:6px 0 0}
/* EL BLOQUEO LLEVA LOS MISMOS DOS GESTOS QUE LA FRANJA, uno por extremo
   (2026-09-11). Aquí el que cede ancho es el MOTIVO —es el único `flex:1` del
   renglón y ya elipsaba—, no las fechas, que van `flex:none` porque son la
   columna que se barre. Con el riel a 5/12 en 1440 el motivo se queda en 183px
   después de pagar el lápiz. */
.hs-bloqueo{display:flex;align-items:center;gap:8px;padding:5px 0;min-height:30px;font-size:var(--fs-meta)}
.hs-bloqueo + .hs-bloqueo{border-top:1px solid var(--app-hairline)}
.hs-bloqueo-rango{font-variant-numeric:tabular-nums;flex:none}
.hs-bloqueo-motivo{color:var(--app-fg-tertiary);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* ── Reserva pública de citas (agenda.php) ────────────────────────────────
   Se apoya en `.puerta` como sus hermanas públicas: la misma caja centrada, el
   mismo tipo de campos. Lo propio son los dos selectores —días y horas—, que
   son rejillas de botones. */
.ag-caja{width:min(520px,100%)}
.ag-paso{margin-top:18px}
.ag-rotulo{font-size:var(--fs-meta);font-weight:600;color:var(--app-fg-secondary);margin-bottom:8px}
.ag-tz{font-weight:400;color:var(--app-fg-tertiary)}
.ag-cargando{font-size:var(--fs-meta);color:var(--app-fg-tertiary)}
/* ── LOS DOS SELECTORES, COMPARTIDOS CON EL ALTA MANUAL (2026-09-04) ──────
   Decían «no existen en ningún otro sitio de la app», y esa frase caducó el día
   que el alta manual de `citas.php` dejó de preguntar la hora con un campo
   libre y pasó a la misma retícula: es EL MISMO motor de slots pintado dos
   veces, así que tiene que ser el mismo dibujo o son dos disponibilidades que
   se parecen y divergen.

   Se comparte por LISTA DE SELECTORES y no copiando los números a un `.cn-*`
   propio — el trato exacto que `.ct-*,.env-*` ya hace con la celda de la
   sábana, y por el mismo motivo: un ajuste del selector de días es del SELECTOR,
   no de la pantalla que lo enseña. Con un tercer consumidor esto sube al
   catálogo con nombre de sistema (`ui.php` → «Las reglas»).

   La única diferencia real entre las dos superficies NO está aquí: es que la
   pública etiqueta en la hora del visitante y el alta manual en la del negocio
   (docs/DOC-agenda.md §3). Eso lo decide quién llama al motor, no el CSS. */
/* Fila con scroll horizontal y no rejilla que crece: la ventana de reserva son
   semanas, así que la lista es larga y estrecha por naturaleza. */
.ag-dias,.cn-dias{display:flex;gap:6px;overflow-x:auto;padding-bottom:4px}
.ag-dia,.cn-dia{
  flex:none;display:flex;flex-direction:column;align-items:center;gap:1px;
  min-width:56px;padding:8px 4px;cursor:pointer;
  background:var(--app-chip);border:1px solid transparent;border-radius:var(--r-ctl);
  color:var(--app-fg);font:inherit;line-height:1.1;
}
.ag-dia:hover,.cn-dia:hover{border-color:var(--ctl-borde-hover)}
/* ── EL ELEGIDO VA EN TEAL, NO EN AZUL (2026-09-09) ───────────────────────
   Nació con `--app-accent` de relleno y letra blanca, y eso rompía la regla
   dura de los botones —«uno solo es azul: el que escribe en la base; si
   pulsarlo no escribe, no hay azul, ni de fondo ni de hover»— en el peor
   sitio posible: DENTRO del diálogo del alta, donde el botón que sí escribe
   («Crear») está a 20px de distancia. Medido en pantalla el 2026-09-09, el
   día elegido, la hora elegida y el botón de guardar pintaban el mismo
   #0068d7 y el diálogo ofrecía tres azules para una sola escritura.

   Elegir un día NO escribe: marca el sujeto del formulario y espera. Eso ya
   tiene color en esta app y es el teal informativo — el mismo que la sábana
   pone en su fila elegida, el menú de lote en su opción puesta y el selector
   de catálogo en su opción actual. No se inventa un tono.

   Del teal se toma el tinte de BADGE (16%) y no `--app-row-selected` (9%),
   que es lo que usan esos tres: aquellos marcan sobre papel y esto marca
   sobre `--app-chip`, un tinte neutro que ya vale 6.4% — a 9% la diferencia
   sería de 2.6 puntos y no se leería como elección. Es el caso exacto de
   `.env-chip.on`, el otro chip del sistema que se enciende, y lleva su mismo
   relleno.

   Y el canto es el aporte propio: `.env-chip.on` no reserva borde y estos sí
   —lo tienen transparente desde que nacieron, para el hover—, así que teñirlo
   sale gratis en medida y le da a la elección un segundo canal además del
   fondo. Gana al `:hover` por orden de fuente con la misma especificidad, que
   es lo correcto: pasar el ratón por encima del que ya elegiste no tiene nada
   nuevo que decir. */
.ag-dia.activo,.cn-dia.activo{background:var(--badge-info);border-color:var(--canto-info)}
.ag-dia-sem,.ag-dia-mes,.cn-dia-sem,.cn-dia-mes{font-size:var(--fs-micro);opacity:.7}
.ag-dia-num,.cn-dia-num{font-size:var(--fs-seccion);font-weight:600}
.ag-horas,.cn-horas{display:flex;flex-wrap:wrap;gap:6px}
.ag-hora,.cn-hora{
  cursor:pointer;padding:7px 12px;font:inherit;font-size:var(--fs-meta);
  background:var(--app-chip);border:1px solid transparent;border-radius:var(--r-ctl);color:var(--app-fg);
}
.ag-hora:hover,.cn-hora:hover{border-color:var(--ctl-borde-hover)}
.ag-hora.activo,.cn-hora.activo{background:var(--badge-info);border-color:var(--canto-info)}
.ag-nota{font-size:var(--fs-meta);color:var(--app-fg-tertiary);margin:6px 0 10px}
.ag-listo-titulo{font-family:var(--fuente-titulo);font-size:var(--fs-destacado);font-weight:700;margin-bottom:6px}
/* El enlace de gestión se parte donde sea: es un token de 64 caracteres y sin
   esto desborda la caja en móvil. */
.ag-enlace{display:block;word-break:break-all;font-size:var(--fs-meta);color:var(--app-accent)}

/* Gestión de la cita por el visitante (mi-cita.php) */
.mc-dato{display:flex;justify-content:space-between;gap:12px;padding:7px 0;border-bottom:1px solid var(--app-hairline)}
.mc-dato span{font-size:var(--fs-meta);color:var(--app-fg-tertiary)}
.mc-dato strong{font-weight:500;text-align:right}
.mc-cancelar{margin-top:16px}

/* ── Presupuestos (2026-09-04) ─────────────────────────────────────────
   EL ENLACE EN EL PANEL (`cotizaciones.php`). Un token de 64 hex no cabe en
   420 y sin `word-break` desborda el peek — el mismo motivo de `.ag-enlace`.
   Va en la tinta secundaria y en el tamaño meta: es un dato para copiar, no
   para leer, y el gesto que lo copia está en la cabecera de su bloque. */
.co-enlace{
  margin-top:7px;word-break:break-all;
  font-size:var(--fs-meta);color:var(--app-fg-secondary);
  font-variant-numeric:tabular-nums;
}

/* LA PÁGINA PÚBLICA DEL PRESUPUESTO (mi-cotizacion.php). Comparte `.puerta`
   con las otras seis públicas —centrado sobre papel, se lee y se cierra— pero
   es la única que enseña un DOCUMENTO con renglones, y los 304 de
   `.puerta-caja` (calibrados para el reCAPTCHA del login) parten un nombre de
   producto en tres líneas. Sube a 520 como máximo y cede en móvil; el alto ya
   no se centra a media pantalla porque un presupuesto de diez renglones no
   cabe en una, y un `.puerta` de 100vh lo recortaría. */
.mq-puerta{height:auto;min-height:100vh;align-items:flex-start;padding:32px 16px}
.mq-caja{width:100%;max-width:520px}
.mq-renglones{margin-top:8px}
.mq-total strong{font-size:var(--fs-destacado);font-family:var(--fuente-titulo);font-weight:700;letter-spacing:-.015em}
.mq-nota{margin:8px 0 0;font-size:var(--fs-meta);color:var(--app-fg-secondary);white-space:pre-line}
/* Los dos botones de respuesta, con el acento a la derecha —el gesto que el
   negocio espera— y el suave a la izquierda, como en el pie de un diálogo. */
.mq-acciones{display:flex;justify-content:flex-end;gap:8px;margin-top:16px}
.mq-pie{justify-content:center}

/* "Escribiendo…" de la página pública. Tres puntos y no la palabra: el chat es
   de un negocio, y la palabra obligaría a elegir entre mentir ("el asesor está
   escribiendo") y delatar al agente. Los puntos no afirman quién. */
.chat-escribiendo{align-self:flex-start;display:flex;gap:4px;padding:10px 12px}
.chat-escribiendo[hidden]{display:none}
.chat-escribiendo span{
  width:6px;height:6px;border-radius:50%;background:var(--app-fg-tertiary);
  animation:chat-latido 1.2s infinite ease-in-out;
}
.chat-escribiendo span:nth-child(2){animation-delay:.15s}
.chat-escribiendo span:nth-child(3){animation-delay:.3s}
@keyframes chat-latido{0%,60%,100%{opacity:.25}30%{opacity:.9}}
/* EL RÓTULO EN LA TINTA DEL TEXTO Y EL DETALLE EN LA TERCIARIA (2026-09-03,
   con el paso a interruptor). La fila entera iba en terciario —correcto cuando
   era una frase de estado— y hoy su primera parte es el RÓTULO de un control:
   la regla del catálogo manda que una etiqueta vaya en `--app-fg`. Es la misma
   anatomía que `.pk-persona`, que es su hermano exacto en el otro panel; el
   `.pk-det` del detalle se comparte tal cual, en vez de declarar un gemelo.
   `space-between` cede: quien tiene que ceder el ancho es el detalle, no el
   rótulo — un rótulo partido deja al control huérfano de su palabra. */
.chat-quien{
  display:flex;align-items:center;gap:7px;
  margin-top:10px;font-size:var(--fs-meta);
}
.chat-quien-rot{flex:none;color:var(--app-fg);font-weight:500}
/* Aquí vivía `.chat-quien-btn` — texto en peso medio que se subrayaba al
   apuntarlo—, y se retiró el 2026-08-16: «Tomar el chat» y «Quitar bloqueo»
   son gestos a la derecha de un renglón (`.chat-quien` y `.chat-bloqueo` son
   los dos un flex con el texto a la izquierda y la acción a la derecha), o sea
   exactamente lo que `.cf-btn` describe. Su subrayado era además el idioma de
   `.enlace`, que es de la prosa: dos idiomas para el mismo gesto según en qué
   pantalla cayera. Su marcado pide ahora `cf-btn`. */
/* LA FRANJA QUE SUSTITUYE AL COMPOSITOR: la frase de por qué no se escribe y su
   única salida a la derecha. La usan el contacto bloqueado («Quitar bloqueo») y,
   desde el 2026-09-25, la ventana de 24 h de WhatsApp vencida («Enviar
   plantilla», `.chat-ventana`, un gancho de JS sin estilo propio). Su botón es
   `.btn-segunda` porque abre un diálogo (ui.php → #regla-capa-con-cara), y no
   cede ancho: la frase es la que se parte. */
.chat-bloqueo{
  display:flex;align-items:center;justify-content:space-between;gap:10px;
  margin-top:10px;padding:8px 10px;border-radius:var(--r-ctl);
  font-size:var(--fs-meta);
  background:var(--badge-warning);color:var(--app-atencion-text);
}

/* El ancho ya no lo decide esta página: su capa lleva un textarea de cuerpo de
   correo, así que pide la medida `dlg-lg` del sistema y `.au-caja` murió. */
.au-fila2{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.au-vars{font-size:var(--fs-meta);color:var(--app-fg-tertiary);margin:8px 0 0}
/* La plantilla de WhatsApp de un aviso de cita (2026-09-23): el selector con
   su «Actualizar» al lado, y la vista previa del mensaje ya llenado con un
   ejemplo, en la caja tenue de las notas largas. */
.au-plantilla-fila{display:flex;gap:8px;align-items:center}
.au-plantilla-fila select{flex:1;min-width:0}
/* `.cv-previa` es la misma vista previa en el diálogo «Enviar plantilla» de la
   bandeja (2026-09-25): dos páginas, una regla. Con un tercer consumidor sube a
   un nombre de sistema. */
.au-previa,.cv-previa{
  margin-top:10px;padding:10px 12px;border-radius:var(--r-ctl);
  background:var(--app-chip);font-size:var(--fs-base);white-space:pre-wrap;
}
.au-previa:empty,.cv-previa:empty{display:none}
.au-vars code{
  background:var(--app-chip);border-radius:var(--r-mini);
  padding:1px 5px;margin-right:2px;font-size:var(--fs-micro);
}

/* ── LA COLA, EN LA SÁBANA (2026-08-16, pedido de Arif: «homóloga envíos con
   leads») ────────────────────────────────────────────────────────────────
   Segundo módulo que adopta el molde por el mismo camino que citas dos días
   antes: se toma la TABLA —cuadrícula completa, cabecera teñida, fila de 36 y
   hoja a pantalla completa con scroll propio— y NO el motor virtual, que lo
   pide una lista de miles y aquí la cola se sirve topada (`listaEnvios`).
   También se toma el panel lateral, que es donde vive ahora lo que no cabe en
   una línea.

   Con eso murió `.env-fila`, y con ella el defecto que traía de nacimiento:
   era una fila de DOS LÍNEAS por celda —nombre sobre dirección, motivo bajo el
   qué— con `height:auto`, que es exactamente lo que la regla de la casa
   prohíbe («la fila no crece»). Cada dato tiene ahora su columna, que es lo
   que permite la línea única con elipsis; se fueron con ella `.env-para` y
   `.env-que`, las dos celdas apiladas.

   La página no declara ni una medida de la tabla: su `--rejilla` va en el
   marcado, como en citas.

   Y con ella murió también `.env-acciones` (que compartía regla con
   `.ct-acciones`, retirada a su vez el 2026-09-24 por lo mismo): los dos gestos se mudaron al pie del panel el mismo día, así
   que la fila ya no lleva ningún control — el único gesto que ofrece es
   abrirse. La celda del estado quedó de `:last-child`, o sea sin el filo ni el
   relleno de 6px con que la rejilla separa columnas: el canto derecho de la
   tabla no separa nada. */

/* El motivo del fallo, en el ámbar que dice «mira esto» (nunca rojo: nada está
   roto del lado del que mira, y el rojo de esta casa es de estado terminal).
   En la FILA se recorta a una línea como cualquier celda de sábana —el `title`
   da la frase entera al pasar por encima— y ENTERA se lee en el panel, que es
   donde se arregla la causa. Vivía sin recortar dentro de una fila que crecía;
   lo que cambió no es la promesa (el motivo se lee sin abrir la base), es
   dónde se cumple. */
.env-motivo{color:var(--app-atencion-text)}

/* La nota del panel cuando todavía no hay texto que enseñar: un envío
   pendiente no tiene copy guardado porque se compone al salir. Decirlo evita
   la lectura de «esto está roto» ante tres renglones ausentes — que es el
   error que el `[hidden]` de `.peek-dato`, solo, provocaría. */
.env-peek-nota{margin-top:16px;font-size:var(--fs-meta);color:var(--app-fg-tertiary)}

/* `flex:none` desde que la barra es hija directa del panel (columna flex): sin
   él se estruja cuando la tabla reclama alto. Antes vivía dentro de `.hoja`,
   que no repartía nada. */
.env-pulso{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:16px;flex:none}
.env-chip{
  display:inline-flex;align-items:center;gap:6px;
  padding:4px 10px;border-radius:var(--r-ctl);
  background:var(--app-chip);color:var(--app-fg-secondary);
  font-size:var(--fs-meta);text-decoration:none;
}
.env-chip:hover{color:var(--app-fg)}
.env-chip.on{background:var(--badge-info);color:var(--app-fg);font-weight:600}
/* El único color fuerte de la barra, y sólo cuando hay algo roto: un fallo que
   no pide que lo mires es un fallo silencioso con otro nombre. */
.env-chip.alerta-roja{background:var(--badge-danger);color:var(--app-peligro-text);font-weight:600}
.env-n{font-variant-numeric:tabular-nums;font-weight:600}
/* El pie con el comando del reloj: debajo de la tabla y fuera de su reparto
   (`flex:none`), que es lo que le impide comerse alto cuando la cola es larga.
   Su aire bajó de 18 a 10 con la mudanza: la tabla ya no acaba donde acaban
   las filas, sino donde acaba el panel. */
.env-pie{margin-top:10px;flex:none}
.env-pie code{background:var(--app-chip);border-radius:var(--r-mini);padding:1px 5px;font-size:var(--fs-micro)}

/* ── La bandeja de conversaciones (conversaciones.php) ────────────────────
   Tres columnas de alto completo, y por eso la página le quita el relleno al
   panel: los filetes que las separan tienen que llegar a los cuatro bordes. Si
   el relleno se quedara arriba, cada línea arrancaría 16px por debajo del techo
   y se leerían como tres cajas dentro de una caja, no como un tablero.

   El relleno se lo queda cada columna. Ganancia lateral: la media query de
   1180px que lo reduce sigue aplicando sobre `.app-panel` y esto no depende de
   ella. */
.app-panel.cv-pleno{padding:0}
/* 3 · 5 · 4 — las mismas proporciones que la rejilla de 12 de la rueda
   (`funnel-chat.php`: col-xl-3 / col-xl-5 / col-xl-4). Se expresan en `fr` y no
   en píxeles fijos: el ancho disponible cambia con la barra lateral (206px,
   180px o 56px en modo compacto) y con `fr` las tres se reparten lo que haya
   conservando la proporción, sin una media query por cada estado de la barra.
   `minmax(0,…)` es lo que permite que una columna se encoja por debajo de su
   contenido — sin él, un hilo largo ensancha la columna 2 y desplaza a las
   otras dos. */
/* El reparto es UNA REJILLA DE 12, como la de Bootstrap, y por eso las tres
   cifras suman 12: se pueden pensar y pedir en «columnas» aunque aquí no haya
   framework que las provea. Nació 3 · 5 · 4 y pasó a 3.5 · 5 · 3.5 el
   2026-08-13 (pedido de Arif, en esos términos: «media columna más a la 1 y
   media menos a la 3»). El hilo no se toca: es la superficie que se lee.

   Por qué esa media columna estaba mal repartida: la bandeja lleva CUATRO
   datos por card —nombre, último mensaje, chips de canal y funnel— y todos se
   truncan; la ficha lleva pares etiqueta·valor alineados a los extremos, que
   es lo que mejor aguanta estrecharse. */
.cv-tablero{
  flex:1;min-height:0;
  display:grid;grid-template-columns:minmax(0,3.5fr) minmax(0,5fr) minmax(0,3.5fr);
}
/* `overflow:hidden` + `min-width:0` es lo que permite que los hijos con scroll
   se encojan dentro de la rejilla. Sin ellos, un hilo largo estira la columna y
   el scroll se va a la ventana entera. */
.cv-col{min-width:0;display:flex;flex-direction:column;overflow:hidden;padding:16px 20px 12px}
/* LOS DOS CANTOS DE FUERA SON EL RIEL DEL PANEL, NO LA CANALETA (2026-08-28).
   Los 20 de arriba nacieron aplicados a los cuatro costados y eso ponía el
   titular de esta página 12px a la izquierda del de TODAS las demás — la única
   pantalla que arranca en otro sitio, y encima desalineada consigo misma: su
   rama de estado vacío cae al `.app-panel` estándar y salta a 32 en cuanto se
   queda sin conversaciones.
   No se sube el 20 a 32 en las cuatro caras: el canto de FUERA y el hueco de
   DENTRO son dos medidas distintas. Fuera manda `--panel-pad-x`, como en todo
   el resto de la app; dentro se queda el 20+1+20 = 41 que ya tenía, que es el
   mismo orden que el hueco de la consola a su riel (20+1+18 = 39). Ponerle 32
   por las cuatro dispararía la canaleta a 65 y las tres columnas dejarían de
   leerse como un tablero. Coste medido: la bandeja pasa de 303 a 291px. */
.cv-col:first-child{padding-left:var(--panel-pad-x)}
.cv-col:last-child{padding-right:var(--panel-pad-x)}
.cv-col + .cv-col{border-left:1px solid var(--app-hairline)}

/* La cabecera de cada columna. Alto mínimo COMÚN a las tres: el titular de la
   primera es `--fs-titulo` y los rótulos de las otras dos son de meta, así que
   sin un piso compartido las tres bandas arrancarían a alturas distintas y la
   rejilla se leería desalineada justo en la línea que más se mira. */
.cv-cab{flex:none;display:flex;align-items:center;gap:10px;min-height:38px;margin-bottom:12px}
/* LA CABEZA QUE LLEVA EL TITULAR SE ALINEA POR LÍNEA BASE, NO POR CAJA
   (2026-08-28). El piso de 38px de arriba hace bien su trabajo —las tres
   columnas empiezan su contenido a la misma altura— pero `align-items:center`
   alinea CAJAS, y lo que el ojo lee en una fila de rótulos es la línea BASE.
   Medido: el h1 caía en 43.9 y su propio conteo en 40.0, o sea que esta
   cabecera rompía hasta la pareja título+conteo que `.panel-titulo` resuelve
   gratis con `baseline`; y las tres columnas se abrían 3.9px entre sí.
   Con `baseline`, el h1 aterriza en 40.5 —EXACTAMENTE la línea base del
   titular de cualquier otra página— y arrastra a su conteo con él; la
   separación entre las tres cae a 1.5px.
   Sólo la bandeja: las otras dos cabezas llevan rótulos de otro cuerpo
   (el nombre del contacto en `--fs-dialogo` y «Datos» en meta), y ahí `baseline` las pegaría al techo de la banda
   en vez de centrarlas. Tres rangos tipográficos distintos no comparten línea
   base sin ser hermanos del mismo contenedor, y volverlos hermanos costaría
   sacar el titular de su columna — que es justo lo que partiría los filetes
   verticales por los que existe este molde. */
.cv-bandeja .cv-cab{align-items:baseline}
.cv-cab h1{
  font-family:var(--fuente-titulo);font-size:var(--fs-titulo);font-weight:700;
  margin:0;line-height:1.2;letter-spacing:-.015em;
}
.cv-cab .conteo{font-size:var(--fs-base);color:var(--app-fg-tertiary)}
.cv-cab-txt{font-size:var(--fs-meta);font-weight:600;color:var(--app-fg-secondary)}

/* ── Columna 1: filtros y cards ── */
.cv-filtros{flex:none;display:flex;flex-direction:column;gap:8px;margin-bottom:10px}
.cv-filtros .mv-input{width:100%}
.cv-filtros .seg{align-self:flex-start}
.cv-lista{flex:1;min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:6px;margin:0 -6px;padding:2px 6px}
/* CARD CON BORDE (corrección de Arif, 2026-08-13). Nació sin filete —el aire
   separaba y el fondo marcaba la seleccionada, como en la fila de la sábana—
   pero una card sin contorno no se lee como card: en una columna con aire de
   sobra, dos bloques de texto sueltos parecen párrafos, no objetos elegibles.
   La fila de la sábana puede prescindir del borde porque la rejilla ya la
   encierra; aquí no hay rejilla.

   El separador sube a 6px con el borde: pegadas, dos líneas contiguas se leen
   como una tabla. */
.cv-card{
  padding:9px 10px;border-radius:var(--r-ctl);cursor:pointer;
  border:1px solid var(--ctl-borde-hover);background:var(--app-paper);
  display:flex;flex-direction:column;gap:2px;
}
/* EL BORDE ES EL DE HOVER, EN REPOSO (corrección de Arif, 2026-08-13). Nació con
   `--app-hairline` —el filete de la casa, .06 de alfa— que es el correcto para
   SEPARAR (una línea entre dos zonas) pero no para CONTORNEAR: a esa opacidad,
   cuatro lados sobre `--app-paper` no llegan a dibujar un objeto y la card
   seguía leyéndose como texto suelto. `--ctl-borde-hover` (.20 / .24) es el
   mismo filo con que un control dice «esto se puede tocar», que es justamente
   lo que una card de la bandeja es.

   El hover se queda entonces con el cambio de fondo, que es la señal de fila
   del resto de la app. No se inventó un cuarto nivel de borde para conservar el
   escalón: tres —reposo, hover, seleccionada— ya son los que la app usa. */
.cv-card:hover{background:var(--app-group)}
/* LA SELECCIONADA SE DICE SÓLO CON EL FONDO (corrección de Arif, 2026-08-13).
   Llevó un borde de acento y se retiró: el azul es el color de lo PULSABLE en
   esta app —enlaces, botón primario, foco— y usarlo para marcar estado le pone
   a una card de la lista el peso visual de la acción principal de la pantalla,
   compitiendo con el «Enviar» que está a dos columnas. El relleno alcanza:
   `--app-chip` queda por debajo del papel de las demás y por debajo del hover,
   así que los tres estados siguen ordenados. */
.cv-card.activa{background:var(--app-chip)}
.cv-card-arriba{display:flex;align-items:baseline;gap:8px}
.cv-card-nombre{
  flex:1;min-width:0;font-weight:600;font-size:var(--fs-base);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
.cv-card-cuando{flex:none;font-size:var(--fs-micro);color:var(--app-fg-tertiary)}
/* Una línea y punto. El fragmento ya viene aplanado desde PHP (los saltos se
   comen ahí), así que aquí sólo hay que cortarlo. */
.cv-card-frag{
  font-size:var(--fs-meta);color:var(--app-fg-tertiary);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
.cv-card-pie{display:flex;align-items:center;flex-wrap:wrap;gap:5px;margin-top:2px}
/* RENGLÓN PROPIO SIEMPRE (`flex-basis:100%`, 2026-08-13). Lo estuvo desde que
   nació, pero por accidente: el pie es `flex-wrap` y con la columna en 3/12 el
   funnel nunca cabía junto al chip. Al ensanchar la bandeja a 3.5 empezó a
   caber en unas cards y en otras no —según lo largo que fuera el nombre del
   funnel—, y la lista pasó a tener cards de dos alturas distintas por un motivo
   que no significa nada. Declarado, la altura es la misma para todas y el
   funnel se queda con el ancho entero para su elipsis. */
.cv-card-funnel{
  flex-basis:100%;
  font-size:var(--fs-micro);color:var(--app-fg-tertiary);text-transform:uppercase;
  letter-spacing:.04em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
.cv-nada{flex:none;padding:10px 4px;font-size:var(--fs-meta);color:var(--app-fg-tertiary)}

/* ── Columna 2: el hilo y el compositor ── */
/* Aquí vivió `.cv-auto`, el interruptor «Actualizar cada 10 s», hasta el
   2026-09-25: lo sustituyó el sondeo de la bandeja (conversaciones.js). */
/* «Respuestas rápidas» al canto derecho de la fila del nombre: el nombre
   cede ancho con su elipsis, el gesto no (#regla-gesto-en-la-fila-del-rotulo). */
.cv-conversacion .cv-cab > .btn-segunda{margin-left:auto;flex:none}
/* LA TARJETA DE RESPUESTA RÁPIDA reacomoda `.cfg-tarjetas` (pedido de Arif,
   2026-09-25): el «···» sube a la fila del título, el texto va debajo y «Usar»
   baja a su propia fila, al final y al canto derecho, como la acción de un
   pie. «Usar» lleva relieve (`.btn-segunda`) aunque no abre una capa
   —#regla-capa-con-cara lo reserva para eso—: es la acción de la tarjeta y se
   pidió que se viera como botón. */
.cfg-tarjetas .rr-fila .cfg-rotulo{grid-column:1;grid-row:1}
.cfg-tarjetas .rr-fila .fn-gestos{grid-column:2;grid-row:1}
.cfg-tarjetas .rr-fila .cfg-detalle{grid-column:1 / -1;grid-row:2}
.cfg-tarjetas .rr-fila .cfg-acciones{grid-column:1 / -1;grid-row:3;justify-self:end;margin-left:0}
/* El texto de una respuesta rápida en su tarjeta del peek, ENTERO (Arif,
   2026-09-25): al revés que la nota de la ficha, aquí se lee para decidir cuál
   usar, y cortarlo obligaría a abrirla para saber qué dice. Conserva sus saltos
   de línea. Pieza de una sola pantalla. */
.rr-texto{white-space:pre-wrap;overflow-wrap:anywhere;color:var(--app-fg-secondary)}
.cv-hilos{flex:1;min-height:0;display:flex;flex-direction:column}
/* Aquí el hilo ES la columna, así que se le quita el tope de 280px que necesita
   en el peek —donde convive con la ficha y la bitácora— igual que ya hace
   `.peek2 .chat-hilo-crm`. */
.cv-hilos .chat-hilo-crm{max-height:none;flex:1;min-height:0;padding:2px 2px 10px}
/* El separador de día: chip centrado, hermano de `.chat-evento` pero más
   callado — no es contenido del hilo, es una regla del calendario. */
.chat-dia{
  align-self:center;margin:8px 0 2px;
  font-size:var(--fs-micro);font-weight:600;letter-spacing:.04em;
  color:var(--app-fg-tertiary);text-transform:uppercase;
}
.cv-composers{flex:none;padding-top:8px;border-top:1px solid var(--app-hairline)}
.cv-composers .chat-bloqueo{margin-top:10px}
.chat-bloqueo > .btn-segunda{flex:none}

/* ── El compositor, clonado de la rueda (`funnel-chat.php`) ───────────────
   Una CAJA que contiene el textarea y el botón, no un campo con un botón al
   lado. `align-items:flex-end` es lo que ancla el botón abajo: la caja crece
   hacia arriba con el texto y el círculo se queda donde el pulgar lo dejó.

   `overflow:hidden` recorta el textarea contra el radio de 22px — sin él, el
   texto de la primera línea se asoma por fuera de la esquina redondeada.

   El color, el borde y el fondo NO se declaran aquí: los pone `.mv-input`, que
   la caja lleva también. Esta clase es sólo la forma. Por eso el foco tampoco
   se reimplementa: `:focus-within` reusa los mismos tokens que el `:focus` de
   cualquier control de la app, en vez del `#80bdff` de Bootstrap que la rueda
   tuvo que escribir a mano. */
.chat-input-wrap{
  display:flex;align-items:flex-end;gap:8px;
  height:auto;min-height:48px;
  padding:5px 6px 5px 16px;
  border-radius:22px;overflow:hidden;
}
.chat-input-wrap:focus-within{border-color:var(--ctl-foco);box-shadow:0 0 0 4px var(--ctl-halo)}
/* El textarea va TRANSPARENTE y sin borde: el control visible es la caja. Así
   su scroll —cuando pasa de tres renglones— cae por dentro, a la izquierda del
   botón, y no encima de él. */
.chat-input-txt{
  flex:1 1 auto;min-width:0;
  border:0;outline:0;background:transparent;color:inherit;
  font:inherit;line-height:1.4;
  resize:none;overflow-y:auto;
  /* EL TOPE SON 3 RENGLONES y luego scroll. Se calcula en vez de fijarse en
     píxeles (la rueda usa 77px, medidos a mano para su tipografía) para que
     siga valiendo si cambia `--fs-base`: 1.4em es el renglón y 14px es el
     relleno vertical de abajo. */
  max-height:calc(1.4em * 3 + 14px);
  padding:7px 0;
}
.chat-input-txt:focus{outline:0;box-shadow:none}
.chat-input-txt::placeholder{color:var(--app-fg-tertiary)}
/* Su barra fina la traía escrita aquí y se retiró el 2026-08-13: es la del
   reset, palabra por palabra. Y de paso se fue el `scrollbar-width:thin` que
   la acompañaba, que en Chrome estaba DESACTIVANDO sus propios pseudos — este
   textarea llevaba meses pintando la barra nativa de 11px, no la de 6 que su
   propio CSS describía. */
/* CÍRCULO, no un botón con la palabra «Enviar»: dentro de la caja, un rectángulo
   con texto compite con lo que se está escribiendo. La flecha sale del mismo
   juego de iconos que el resto de la app (`--icn-arriba`), no de una fuente de
   iconos que este proyecto no carga. */
.chat-input-enviar{
  flex:0 0 auto;width:38px;height:38px;padding:0;border:0;border-radius:50%;
  background:var(--app-accent-solido);color:var(--app-accent-fg);
  display:inline-flex;align-items:center;justify-content:center;cursor:pointer;
}
.chat-input-enviar:hover{filter:brightness(1.06)}
.chat-input-enviar:disabled{opacity:.45;cursor:default}

/* ── Columna 3: la ficha ── */
.cv-ficha-panel{display:flex;flex-direction:column;min-height:0;flex:1}
/* El nombre del contacto, título del hilo en la columna 2 (vivió en la cabeza
   de esta columna como `.cv-ficha-nombre` hasta el 2026-09-25). */
.cv-contacto-nombre{
  font-family:var(--fuente-titulo);font-size:var(--fs-dialogo);font-weight:700;
  line-height:1.2;letter-spacing:-.015em;
  min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.cv-ficha-cuerpo{flex:1;min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:16px}
/* El interruptor del agente: caja punteada, como todo lo que en esta app es un
   ESTADO que además se puede cambiar desde donde se mira. El tinte de atención
   sólo cuando el agente NO está contestando — es la situación que alguien tiene
   que recordar deshacer. */
.cv-agente{
  flex:none;display:flex;align-items:center;justify-content:space-between;gap:10px;
  padding:9px 11px;border-radius:var(--r-ctl);
  border:1px dashed var(--app-hairline);
}
.cv-agente.tomada{background:var(--badge-warning);border-style:solid;border-color:transparent;color:var(--app-atencion-text)}
.cv-agente.tomada .cv-agente-est{color:inherit}
.cv-agente-tit{font-size:var(--fs-meta);font-weight:600}
.cv-agente-est{font-size:var(--fs-micro);color:var(--app-fg-tertiary)}
/* Los datos: etiqueta a la izquierda, valor a la derecha. Es el molde de
   `.mc-dato` (la ficha de la cita pública) y se repite a propósito — dos listas
   de dato-valor que se vieran distintas serían dos idiomas para lo mismo. */
.cv-datos{margin:0;display:flex;flex-direction:column}
.cv-datos > div{display:flex;justify-content:space-between;gap:12px;padding:7px 0;border-bottom:1px solid var(--app-hairline)}
.cv-datos dt{font-size:var(--fs-meta);color:var(--app-fg-tertiary)}
.cv-datos dd{margin:0;font-size:var(--fs-meta);font-weight:500;text-align:right;min-width:0;overflow-wrap:anywhere}
/* EL DATO LARGO BAJA A SU PROPIA FILA (2026-09-25, pedido de Arif): el correo
   no cabe junto a su rótulo en los ~290px de la ficha y se partía. Rótulo
   arriba, valor debajo alineado a la izquierda. */
.cv-datos > .cv-dato-apilado{flex-direction:column;gap:2px}
.cv-datos > .cv-dato-apilado dd{text-align:left}
/* Las notas de la ficha se escriben en `#modal-nota` y se pintan con
   `.nota-card` (2026-09-25); ya no hay compositor en línea aquí, así que la
   regla de su textarea (`.cv-nota-txt`) se retiró con él. */

@media (max-width:1180px){
  /* La bandeja se lleva aquí los mismos +30 del reparto de arriba, pero NO se
     los quita a la ficha: se los quita al hilo, que es el `1fr` y absorbe sin
     que nada se estreche por debajo de su mínimo. Medido antes de decidirlo:
     la ficha por debajo de ~270px parte un correo típico en dos renglones
     (`marisol.herrera@gmail.com` pasa de 21 a 42px de alto), y en el reparto
     proporcional de arriba eso no pasa porque la columna es mucho más ancha.
     Arriba se pidió media columna de la 3 a la 1 y así se hizo; aquí las tres
     ya están en su mínimo, así que la media columna sale del elástico. */
  .cv-tablero{grid-template-columns:280px minmax(0,1fr) 280px}
  .cv-col{padding:16px 14px 12px}
}
/* POR DEBAJO DE 1024 NO CABEN TRES. Se apilan y cada bloque se queda con su
   propio alto: la rejilla deja de gobernar el scroll y lo gobierna la página,
   que es lo único que funciona cuando el ancho no da para columnas. El filete
   pasa de vertical a horizontal por el mismo motivo — separa igual, en el eje
   que ahora existe. */
@media (max-width:1024px){
  /* Apilada, el que scrollea es el panel: reserva su canal y las columnas lo
     descuentan del canto derecho, para que los dos cantos vuelvan a medir
     `--panel-pad-x`. Y aquí SÍ llevan riel por las dos caras: apiladas ya no
     hay canaleta que repartir, cada bloque ocupa el ancho entero. */
  .app-panel.cv-pleno{overflow-y:auto;scrollbar-gutter:stable}
  .cv-tablero{grid-template-columns:1fr;min-height:auto}
  .cv-tablero .cv-col{
    overflow:visible;
    padding-left:var(--panel-pad-x);
    padding-right:calc(var(--panel-pad-x) - var(--canal-scroll));
  }
  .cv-col + .cv-col{border-left:0;border-top:1px solid var(--app-hairline)}
  .cv-lista{max-height:340px}
  .cv-hilos .chat-hilo-crm{height:60vh}
  .cv-ficha-cuerpo{overflow:visible}
}
/* La hora DENTRO del chip de evento va en línea: `.chat-hora` es `display:block`
   porque en una burbuja se sienta debajo del texto, y ahí eso es correcto. En un
   chip de una línea partía el evento en dos renglones y le daba a un apunte
   interno el peso visual de un mensaje. */
.chat-evento .chat-hora{display:inline;margin:0 0 0 6px}

/* ── Las dos pieles del hilo: la bandeja y el peek (2026-09-25) ────────────
   El hilo se pinta según su CANAL (`canal-<canal>` en el marcado de
   conversaciones.php), para que el operador sepa por dónde contesta antes de
   leer una palabra. Pedido de Arif: *«al hacer clic en un chat de whatsapp si
   aplica el estilo actual pero en el chat web no»*.

   VAN ACOTADAS A LA BANDEJA (`.cv-hilos`) Y AL PANEL DE CONVERSACIÓN DEL PEEK
   (`.peek2`, pedido de Arif el mismo día: «aplica el mismo estilo al chat del
   peek»). `.chat-burbuja` es UN SOLO juego de clases para tres superficies, y
   la tercera —la página pública del visitante, `chat.php`— es de la marca del
   negocio: tocar la clase base la repintaría también. El peek pone la clase
   del canal en `pintarConversacion()` (peek.js).

   ── LA PIEL DE MICRO: la de todo canal que no sea WhatsApp ──
   Fondo liso cálido (`--app-secundario-bg`, el escalón entre la barra y el
   papel) y dos burbujas de la casa:
   · la del CONTACTO en papel con su línea de pelo — la hoja que llega;
   · la de MICRO (agente o humano) en el beige de la barra hundido con la
     tinta (`--cv-burbuja-micro`: 7% de `--app-tinta` sobre
     `--app-sidebar-bg`; en oscuro 45% sobre `--app-paper`), con la MISMA
     letra del cuerpo, como la verde clara de WhatsApp. No es azul porque no
     escribe en la base. Se probó antes la tinta sólida con letra blanca y
     Arif la descartó el mismo día: un bloque café oscuro pesaba más que lo
     que el contacto escribió. Y el beige exacto de la barra se fundía con el
     fondo. Se probó 12% y Arif lo pidió más claro: el 7% es el punto que
     todavía la separa (1.21:1 contra el fondo, con su línea de pelo).
     Medido en Chrome: la letra da 13.0:1 en claro y 11.4:1 en oscuro, su
     hora (al 65%) 4.7:1 y 5.8:1; la del contacto 17:1 y 12.5:1.
   Radios de la escala: 12 (lo que flota) con la esquina de quien habló en 4.
   La fecha va como texto, sin chip, y el evento de sistema como contorno sin
   relleno (`--app-borde`): con relleno se confundían con la burbuja de Micro.
   Los dos en tinta secundaria, no terciaria: sobre el escalón cálido la
   terciaria baja de 4.5 (medido: la secundaria da 6.9:1 en claro y 7.7:1 en
   oscuro). La hora dentro del evento va sin la atenuación de `.chat-hora`. En oscuro el fondo baja a `--app-bg` para que el papel de la
   burbuja (#28282c) se separe de él; la tinta no cambia, se lee igual. */
:is(.cv-hilos,.peek2) .chat-hilo-crm{
  background:var(--app-secundario-bg);
  border:1px solid var(--app-hairline);
  border-radius:var(--r-caja);
  padding:10px;
}
[data-theme="dark"] :is(.cv-hilos,.peek2) .chat-hilo-crm{background:var(--app-bg)}
:is(.cv-hilos,.peek2) .chat-burbuja{max-width:85%}
:is(.cv-hilos,.peek2) .chat-burbuja.suya{
  background:var(--app-paper);color:var(--app-fg);
  box-shadow:inset 0 0 0 1px var(--app-hairline);
  border-radius:var(--r-caja) var(--r-caja) var(--r-caja) var(--r-mini);
}
:is(.cv-hilos,.peek2) .chat-burbuja.mia{
  background:var(--cv-burbuja-micro);color:var(--app-fg);
  box-shadow:inset 0 0 0 1px var(--app-hairline);
  border-radius:var(--r-caja) var(--r-caja) var(--r-mini) var(--r-caja);
}
:is(.cv-hilos,.peek2){--cv-burbuja-micro:color-mix(in srgb,var(--app-tinta) 7%,var(--app-sidebar-bg))}
[data-theme="dark"] :is(.cv-hilos,.peek2){--cv-burbuja-micro:color-mix(in srgb,var(--app-tinta) 45%,var(--app-paper))}
:is(.cv-hilos,.peek2) .chat-dia{
  color:var(--app-fg-secondary);margin:10px 0 4px;
}
:is(.cv-hilos,.peek2) .chat-evento{
  background:transparent;color:var(--app-fg-secondary);max-width:85%;
  box-shadow:inset 0 0 0 1px var(--app-borde);
}
:is(.cv-hilos,.peek2) .chat-hilo-crm:not(.canal-whatsapp) .chat-evento .chat-hora{opacity:1}

/* ── LA PIEL DE WHATSAPP: sólo `canal-whatsapp` ──
   Clonada de Controly (`funnel-chat.php` + app-tema/app-dark), con sus mismos
   valores exactos: el doodle de fondo, el blanco de la entrante y el verde de
   la saliente. El operador ya lee chats así, y un hilo de WhatsApp que no se
   parece a WhatsApp obliga a leer el lado de la burbuja para saber quién habló.
   Colores literales y radios fuera de la escala: excepción declarada en ui.php
   (El color, «excepción acotada»). */
:is(.cv-hilos,.peek2) .chat-hilo-crm.canal-whatsapp{
  background:url("img/chat-fondo-whatsapp.jpg") repeat;
}
[data-theme="dark"] :is(.cv-hilos,.peek2) .chat-hilo-crm.canal-whatsapp{
  background:url("img/chat-fondo-whatsapp-dark.jpg") repeat;
}
/* La ESQUINA es la que apunta a quien habló — Controly usa un pico cuadrado
   (radio 0) y no el 4px redondeado de la clase base. */
:is(.cv-hilos,.peek2) .canal-whatsapp .chat-burbuja.suya{
  background:#fff;color:#1c1c1e;box-shadow:none;
  border-radius:12px 12px 12px 0;
}
:is(.cv-hilos,.peek2) .canal-whatsapp .chat-burbuja.mia{
  background:rgba(30,215,96,.3);color:#1c1c1e;
  border-radius:12px 12px 0 12px;
}
[data-theme="dark"] :is(.cv-hilos,.peek2) .canal-whatsapp .chat-burbuja.suya{background:#202c33;color:#ececf0}
[data-theme="dark"] :is(.cv-hilos,.peek2) .canal-whatsapp .chat-burbuja.mia{background:#005c4b;color:#ececf0}
/* SOBRE EL DOODLE NADA PUEDE SER TRANSLÚCIDO. `--app-chip` y `--app-pill` son
   un negro/blanco al 5-10% de alfa: sobre un fondo liso se leen, sobre un
   patrón dejan pasar los dibujos por debajo de las letras. Los dos chips se
   pintan con la MISMA superficie de la burbuja entrante —así pertenecen al
   hilo en vez de flotar encima— y con el color de texto normal.
   Se probó primero con `--app-paper` + `opacity:.92`; en oscuro el separador
   «AYER» y el veredicto del agente quedaban casi ilegibles sobre el doodle. */
:is(.cv-hilos,.peek2) .canal-whatsapp .chat-dia{background:#fff;color:#1c1c1e;border-radius:10px}
:is(.cv-hilos,.peek2) .canal-whatsapp .chat-evento{background:#fff;color:#1c1c1e}
[data-theme="dark"] :is(.cv-hilos,.peek2) .canal-whatsapp .chat-dia,
[data-theme="dark"] :is(.cv-hilos,.peek2) .canal-whatsapp .chat-evento{background:#202c33;color:#ececf0}
/* EL SCROLL DEL HILO fue el primero en arreglarse, con un `color-scheme:dark`
   acotado a esta caja: aquí la barra clara caía junto al doodle oscuro y era
   imposible de ignorar. Esa regla se retiró el 2026-08-13, cuando la nota que
   la acompañaba —«hacerlo global es su propia tarea»— se convirtió en la
   tarea: `color-scheme` y la barra fina viven ahora en el reset y valen para
   toda la app. Se deja escrito porque el camino importa: esta caja fue la
   prueba de concepto de lo que hoy es la regla de la casa. */

/* ══ EL RECORRIDO DEL DRIP (2026-08-16) ═══════════════════════════════
   El drip leído como lo vive el lead —entra, espera, recibe, espera, recibe—
   con el copy real de cada paso a la vista. Modela la vista del autorespondedor
   de la rueda (flujo vertical con chips de espera entre tarjetas), dicho en el
   idioma de esta casa: tokens en vez de grises sueltos, gestos susurrados en
   vez de tres botones permanentes, y la espina dibujada por CSS en vez de un
   SVG por hueco.

   NACIÓ COMO RIEL de `automatizacion.php` —el primero que ENSEÑABA CONTENIDO
   en vez de resumirlo— y el 2026-09-05 se volvió PÁGINA (`drip.php`). De estas
   reglas eso cambia UNA, y está al pie del bloque (`.dr-hoja`): el ancho no, la
   alineación sí.

   SE OPERA, como el riel de la agenda: cada tarjeta trae sus gestos y el alta
   es el renglón punteado al pie (el patrón de `.fila-agregar`). Editar abre el
   cajón; el interruptor de la secuencia y el remitente viven en la consola, que es
   donde se configura — esto enseña lo que la máquina va a mandar.

   Acotado a 560px aunque haya más sitio: una tarjeta que enseña un correo se
   lee como un correo, y a 700px el renglón pierde al ojo. El `margin-inline:auto`
   es de su vida anterior —centrado en el carril del riel, porque el flujo es un
   CAMINO y pegado a la izquierda con la espina al centro quedaba cojo— y hoy
   sólo aplica donde el anfitrión sea más ancho que el camino. */
.recorrido{max-width:560px;margin-inline:auto;padding-bottom:24px}
/* Los nodos: dónde empieza y dónde acaba el camino. Píldoras sobre papel para
   que corten la espina si el aire se encoge. El punto del arranque es verde
   —por ahí ENTRA gente—; el del fin, mudo. */
.rec-nodo{
  display:flex;align-items:center;gap:7px;width:max-content;margin:0 auto;
  font-size:var(--fs-micro);font-weight:600;
  background:var(--app-paper);border:1px solid var(--app-hairline);
  border-radius:var(--r-pill);padding:3px 12px;position:relative;z-index:1;
}
.rec-punto{width:7px;height:7px;border-radius:50%;background:var(--app-borde-muted);flex:none}
.rec-punto.ok{background:var(--app-exito)}
/* El tramo entre dos piezas: la espina (::before) y, montado en ella, el chip
   de espera. El chip lleva papel sólido a propósito — es lo que corta la línea
   a su paso, sin cálculos de altura. */
.rec-tramo{
  position:relative;display:flex;align-items:center;justify-content:center;
  min-height:38px;
}
.rec-tramo::before{
  content:'';position:absolute;top:0;bottom:0;left:50%;width:2px;
  margin-left:-1px;background:var(--app-borde-muted);opacity:.55;
}
.rec-espera{
  position:relative;z-index:1;
  font-size:var(--fs-micro);color:var(--app-fg-tertiary);
  font-variant-numeric:tabular-nums;
  background:var(--app-paper);border:1px solid var(--app-hairline);
  border-radius:var(--r-pill);padding:2px 10px;
}
/* La tarjeta de un paso: papel, canto y una sombra corta — flota apenas sobre
   el gris del riel, que ya es superficie teñida. El hover sube el canto y
   enciende los gestos, el mismo susurro de `.cfg-fila`. */
.rec-card{
  position:relative;background:var(--app-paper);
  border:1px solid var(--app-hairline);border-radius:var(--r-caja);
  padding:11px 14px;box-shadow:0 1px 3px var(--app-shadow);
  transition:border-color var(--app-dur) var(--app-ease);
}
.rec-card:hover{border-color:var(--ctl-borde-hover)}
/* LA TARJETA DEL PASO CONSERVA EL VELO, y es la única excepción a la regla de
   arriba (2026-09-04). Las tres cosas que el velo rompía en una fila de sábana
   —la línea horizontal del grid, las verticales de las celdas y el fondo opaco
   de las columnas congeladas— **aquí no existen**: una tarjeta no tiene
   cuadrícula que interrumpir ni nada congelado debajo. Lo que sí comparte es el
   riesgo de atenuar sus gestos, que aparecen al posarse; queda anotado como lo
   que hay que medir el día que alguien tenga un paso apagado a la vista. */
.rec-card.ct-muerta{opacity:.55}
/* EL PIE DE LA TARJETA DE PASO (2026-09-03): sus gestos bajaron aquí al entrar
   la regla de que junto al interruptor no va ningún otro control. Mismo reparto
   que `.cfg-acciones` en la fila de configuración —lo que abre una capa con
   cara, la × destructiva plana— y el mismo aire de arriba, para que se lea como
   otro registro y no como otra línea del contenido. */
.rec-acciones{
  display:flex;flex-wrap:wrap;gap:8px;align-items:center;
  margin-top:10px;
}
.rec-cab{display:flex;align-items:flex-start;gap:8px}
/* El canal, con el color de su familia: el correo habla en el teal de info y
   WhatsApp en el verde que ya usa el chat de conversaciones. */
.rec-canal{
  font-size:var(--fs-micro);font-weight:600;flex:none;
  border-radius:var(--r-mini);padding:1px 7px;margin-top:1px;
}
.rec-canal.correo{background:var(--badge-info);color:var(--app-info-text)}
.rec-canal.whatsapp{background:var(--badge-success);color:var(--app-exito-text)}
.rec-asunto{flex:1;min-width:0;font-weight:600;line-height:1.35;overflow-wrap:anywhere}
.rec-cab .badge{flex:none;margin-top:1px}
.rec-gestos{display:flex;align-items:center;gap:4px;flex:none}
.rec-gestos .cf-btn{opacity:0;transition:opacity .12s}
.rec-card:hover .cf-btn,
.rec-card:focus-within .cf-btn{opacity:1}
/* El copy del paso, tal cual va a salir. Acotado a ~9 renglones con un fundido
   al corte (la clase la pone el JS midiendo, no siempre: un correo corto con
   la última línea desvanecida parecería truncado). Los estilos internos son
   el mínimo para que el HTML del correo se lea — el correo real lleva el CSS
   que lleve el cliente de correo, no éste. */
.rec-cuerpo{
  margin-top:8px;font-size:var(--fs-meta);color:var(--app-fg-secondary);
  line-height:1.5;max-height:189px;overflow:hidden;
}
.rec-cuerpo.desborda{
  -webkit-mask-image:linear-gradient(#000 62%,transparent 96%);
  mask-image:linear-gradient(#000 62%,transparent 96%);
}
.rec-cuerpo p{margin:0 0 7px}
.rec-cuerpo p:last-child{margin-bottom:0}
.rec-cuerpo a{color:var(--app-accent)}
.rec-cuerpo ul,.rec-cuerpo ol{margin:6px 0;padding-left:20px}
/* El paso de WhatsApp sale por una plantilla APROBADA de Meta, no por un copy
   editable: la caja punteada dice justamente «esto no se escribe aquí». */
.rec-plantilla{
  margin-top:8px;font-size:var(--fs-meta);color:var(--app-fg-tertiary);
  border:1px dashed var(--app-borde-muted);border-radius:var(--r-ctl);
  padding:7px 10px;line-height:1.45;
}
/* EL RENGLÓN PUNTEADO DE ALTA — al pie de una lista, el gesto que le agrega
   uno más. Nació como `.rec-add` en la semana de la agenda y era de un módulo;
   el 2026-08-18 lo pidió su SEGUNDO consumidor (el detalle de la venta) y por
   eso perdió el prefijo en vez de copiarse. Es la lección que este repo ya
   tenía escrita y pagada: `.nc-envoltura` se llamaba así por «no contactar» y
   acabó en tres módulos que no eran ése. Lo que se comparte se nombra por lo
   que hace. */
.fila-agregar{
  display:flex;align-items:center;justify-content:center;
  min-height:32px;margin-top:14px;
  /* NO APAGADO (2026-08-18, corrección de Arif). Nació con el filo en
     `--app-borde-muted` y la letra en terciario, o sea vestido de «esto está
     deshabilitado». Y no lo está: es el único gesto que hace crecer la lista.
     El punteado ya dice «aquí todavía no hay nada»; no hace falta que además
     lo diga el color. Sube al filo del control y a la letra del cuerpo. */
  border:1px dashed var(--ctl-borde-hover);border-radius:var(--r-ctl);
  color:var(--app-fg);font-size:var(--fs-meta);cursor:pointer;
}
/* Pegada a lo que cierra la lista —el gran total—, que es lo que pidió Arif:
   entre el cierre y el gesto de agregar no hay nada que separar. Los 14px de
   arriba son para cuando cuelga de una lista suelta, como en el recorrido del drip. */
.fila-agregar.pegada{margin-top:4px}
.fila-agregar:hover{border-color:var(--app-fg-tertiary);background:var(--app-row-hover)}
/* El estado vacío del recorrido: qué falta y el gesto que lo crea, centrados en
   el carril — el mismo trabajo que hace `.estado-vacio` en un panel. */
.rec-vacio{
  max-width:400px;margin:40px auto 0;text-align:center;
  color:var(--app-fg-secondary);font-size:var(--fs-base);line-height:1.55;
}
.rec-vacio .btn-segunda{margin-top:14px}
/* ── EL RECORRIDO CON PÁGINA PROPIA (`drip.php`, 2026-09-05) ──────────
   Al salirse del riel, el camino cambia de anfitrión y con él la regla de
   alineación — no la de ancho, que es la misma y por el mismo motivo: una
   tarjeta que enseña un correo se lee como un correo.

   EN EL RIEL SE CENTRABA (`margin-inline:auto`) porque el carril era más ancho
   que el camino y pegado a la izquierda quedaba cojo. EN LA HOJA NO PUEDE:
   el molde de lectura tiene una regla propia y es la contraria —el contenido
   arranca donde arranca el titular—, y romperla es exactamente lo que hacía
   que el importador fuese la única pantalla que no parecía la misma app (ver
   `.hoja`). Así que aquí la hoja se acota al ancho del camino y el camino deja
   de centrarse: el resultado es UNA columna de 560 pegada al margen, con el
   lede, el titular y las tarjetas empezando en la misma vertical.

   Va como modificador de `.hoja` y no como ancho suelto porque lo que se
   estrecha es la HOJA: si el lede se quedara a 880 y sólo el camino a 560, la
   página tendría dos anchos de lectura y ninguno explicaría al otro. */
.hoja.dr-hoja{width:min(560px,100%)}
.dr-hoja .recorrido{margin-inline:0}

/* ── El CAJÓN: el diálogo dicho de lado (2026-08-16) ──────────────────
   Variante LATERAL de la capa de diálogo, para formularios largos que se
   escriben mirando la página — el paso del drip se redacta leyendo el camino
   que hay debajo (`drip.php`) y el aviso de cita y la regla de reactivación,
   la consola que los ordena; un modal centrado tapa las tres.
   Es el drawer de Controly (`modal-drawer-right`) dicho con las piezas de
   aquí: MISMA capa `.dialogo`, mismo velo, misma ×, mismos tres gestos de
   cierre de app.js — sólo cambia dónde se para la hoja y por dónde entra.
   La página lo pide poniendo `lateral` en la capa y `dlg-cajon` en la caja;
   quitarlos devuelve el diálogo centrado sin tocar el marcado de dentro.
   Sus dos consumidores lo piden de dos maneras a propósito (movido del
   catálogo `ui.php`, 2026-09-19): `automatizacion.js` elige la forma en
   caliente —su capa hospeda cinco formularios y sólo dos van de lado—, y
   `drip.php` la escribe en el marcado, porque ahí no hay otra forma que
   elegir. */
.dialogo.lateral{align-items:stretch;justify-content:flex-end;padding:0}
.dialogo.lateral .dlg-caja{
  max-height:none;height:100%;
  border-radius:var(--r-dialogo) 0 0 var(--r-dialogo);
  animation:dlg-cajon .16s ease-out;
}
/* El cuerpo empuja el pie al FONDO del cajón: en una hoja a toda la altura,
   Guardar flotando a media pantalla es un pie que parece parte del formulario.
   Es la anatomía del drawer de Controly (header · body · footer anclado). */
.dialogo.lateral .dlg-cuerpo{flex:1}
.dlg-cajon{max-width:560px}
@keyframes dlg-cajon{from{opacity:0;transform:translateX(28px)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion:reduce){.dialogo.lateral .dlg-caja{animation:none}}

/* ── Trumbowyg, vestido de la casa (2026-08-16) ───────────────────────
   El editor KISS de Controly (negrita, cursiva, enlace, lista), copiado con
   su dist y RE-VESTIDO por tokens: el vendor trae dos temas propios de grises
   sueltos («.trumbowyg-dark» en un ancestro) y esta app tematiza por
   `data-theme` con tokens, así que se le pisan los colores una vez y los dos
   temas salen gratis. El vendor carga ANTES de app.css (misma regla que
   intl-tel-input): igual especificidad, gana el orden. */
.trumbowyg-box{
  min-height:220px;margin:0;width:100%;
  background:var(--ctl-bg);border:1px solid var(--ctl-borde);
  border-radius:var(--r-ctl);overflow:hidden;
}
.trumbowyg-editor-box{border:none}
.trumbowyg-editor{padding:10px 12px;color:var(--app-fg);font:inherit;font-size:var(--fs-base);line-height:1.5}
.trumbowyg-editor a{color:var(--app-accent)}
.trumbowyg-textarea{background:transparent;color:var(--app-fg)}
.trumbowyg-button-pane{background:var(--app-group);border-bottom:1px solid var(--app-hairline);min-height:34px;padding:0 4px}
/* Sus dos rayas decorativas asumen filas de 35px exactos; fuera. */
.trumbowyg-button-pane::before,.trumbowyg-button-pane::after{content:none}
.trumbowyg-button-pane .trumbowyg-button-group::after{background:var(--app-hairline);height:22px;margin:6px 4px 0}
.trumbowyg-button-pane button{width:32px;height:30px;margin:2px 0;border-radius:var(--r-mini)}
.trumbowyg-box svg,.trumbowyg-modal svg{color:var(--app-fg-secondary);fill:var(--app-fg-secondary)}
.trumbowyg-button-pane button.trumbowyg-active,
.trumbowyg-button-pane button:not(.trumbowyg-disable):focus,
.trumbowyg-button-pane button:not(.trumbowyg-disable):hover{background-color:var(--app-pill)}
.trumbowyg-button-pane .trumbowyg-open-dropdown::after{border-top-color:var(--app-fg-tertiary)}
/* El desplegable del enlace (Insertar/Remover): papel de la casa, no el #fff
   del vendor — en oscuro era un bloque blanco. */
.trumbowyg-dropdown{border-color:var(--app-hairline);background:var(--app-paper);border-radius:var(--r-ctl);box-shadow:0 6px 16px var(--app-shadow)}
.trumbowyg-dropdown button{background:transparent;color:var(--app-fg);font-family:var(--fuente);font-size:var(--fs-base)}
.trumbowyg-dropdown button:focus,.trumbowyg-dropdown button:hover{background:var(--app-pill)}
/* El diálogo interno del enlace. */
.trumbowyg-modal-box{
  background:var(--app-paper);color:var(--app-fg);
  border-radius:var(--r-ctl);box-shadow:0 6px 16px var(--app-shadow);
  font-family:var(--fuente);
}
.trumbowyg-modal-box .trumbowyg-modal-title{
  font-family:var(--fuente-titulo);font-size:var(--fs-dialogo);font-weight:700;
  background:transparent;color:var(--app-fg);border-bottom:1px solid var(--app-hairline);
}
.trumbowyg-modal-box .trumbowyg-input-row{border-color:var(--ctl-borde)}
.trumbowyg-modal-box .trumbowyg-input-infos{background-color:var(--app-group);border-right-color:var(--ctl-borde)}
.trumbowyg-modal-box .trumbowyg-input-infos label{color:var(--app-fg-secondary)}
.trumbowyg-modal-box .trumbowyg-input-html input,
.trumbowyg-modal-box .trumbowyg-input-html select,
.trumbowyg-modal-box .trumbowyg-input-html textarea{background:var(--ctl-bg-solido);color:var(--app-fg)}
.trumbowyg-modal-box .trumbowyg-modal-button{border-radius:var(--r-ctl);font-family:var(--fuente)}
.trumbowyg-modal-box .trumbowyg-modal-button.trumbowyg-modal-submit,
.trumbowyg-modal-box .trumbowyg-modal-button.trumbowyg-modal-submit:focus,
.trumbowyg-modal-box .trumbowyg-modal-button.trumbowyg-modal-submit:hover{background:var(--app-accent-solido);color:#fff}
.trumbowyg-modal-box .trumbowyg-modal-button.trumbowyg-modal-reset,
.trumbowyg-modal-box .trumbowyg-modal-button.trumbowyg-modal-reset:focus,
.trumbowyg-modal-box .trumbowyg-modal-button.trumbowyg-modal-reset:hover{background:var(--app-chip);color:var(--app-fg)}
.trumbowyg-overlay{background-color:transparent}

/* ── El kardex del diálogo de existencia (items.php, 2026-08-18) ──────
   Los últimos movimientos de UN producto: los gestos del ledger más la fila
   de venta derivada (docs/DOC-existencia.md §7). Filas planas con hairline,
   no una tabla: es una narrativa que se ojea, no datos que se comparan. */
.kdx-cab{
  margin-top:16px;padding-top:10px;border-top:1px solid var(--app-hairline);
  font-size:var(--fs-micro);font-weight:600;color:var(--app-fg-secondary);
}
.kdx{max-height:300px;overflow-y:auto;margin-top:2px}
.kdx-fila{
  display:flex;gap:10px;align-items:baseline;
  padding:6px 0;border-bottom:1px solid var(--app-hairline);
  font-size:var(--fs-micro);
}
.kdx-fila:last-child{border-bottom:none}
.kdx-fecha{flex:none;width:132px;color:var(--app-fg-tertiary)}
.kdx-tipo{flex:none;width:64px;color:var(--app-fg-secondary)}
.kdx-cant{flex:none;width:72px;text-align:right;font-variant-numeric:tabular-nums;font-weight:600}
.kdx-cant.neg{color:var(--app-fg-secondary)}
.kdx-nota{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--app-fg-secondary)}
.kdx-quien{flex:none;max-width:110px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--app-fg-tertiary)}
.kdx-vacio{padding:10px 0;font-size:var(--fs-micro);color:var(--app-fg-tertiary)}

/* ── EL PANEL DEL PRODUCTO (2026-09-04) ───────────────────────────────────
   Casi todo lo pone el molde: `.peek`, `.peek-nav`, `.peek-cuerpo`,
   `.peek-dato`, `.peek-cifra` y las clases del kardex, que ya eran de sistema.
   Lo único suyo son estas dos reglas, y las dos salen de que el kardex ahora
   vive en DOS sitios con alturas distintas.

   EL KARDEX NO SCROLLEA DENTRO DEL PANEL. Sus 300px de techo son del DIÁLOGO,
   donde la hoja tiene alto fijo y el kardex va al final: sin ese techo, veinte
   movimientos empujarían el pie fuera de la pantalla. En el panel el que
   scrollea es `.peek-cuerpo`, y un scroller anidado dentro de otro es la
   trampa clásica —la rueda del ratón se come el interior y el de fuera no se
   entera de que hay más abajo—. Se anula aquí y no se quita allá: el techo
   sigue siendo correcto donde nació. */
.it-peek .kdx{max-height:none;overflow:visible}
/* El umbral, pegado a su cifra (4px) y no al aire de la sección: pertenece al
   disponible que tiene encima, no es el siguiente renglón del panel. Mismo
   tono y mismo cuerpo que `.gn-cuenta`, la otra línea que dice una regla bajo
   el dato que gobierna. */
/* AQUÍ VIVÍA `.it-peek-umbral`, el renglón «Avisa en 4» bajo la cifra del
   disponible, y se retiró el 2026-09-04 al subir ese texto a la fila del rótulo
   de su sección: allí lo viste `.pk-cab-dato`, que es la pieza del sistema para
   exactamente eso —decir algo del bloque sin robarle el renglón al contenido—.
   Se borra en vez de quedarse: una regla sin consumidor es CSS que nadie
   vuelve a mirar y que el próximo censo tiene que volver a descubrir. */

/* ── Ventas: la lista y su panel (venta-lista.php) ───────────────────────
   Cuarta página en adoptar el molde de la sábana server-rendered, después de
   citas, envíos y la papelera. Lo que trae de nuevo es lo que ninguna de las
   tres tenía: columnas de DINERO y un panel que enseña un DOCUMENTO —con su
   detalle y su línea de tiempo— en vez de una ficha.

   Casi todo lo pone el sistema: `.sabana` y `.fila-rejilla` la tabla, `.peek`
   el panel entero, `.peek-etiqueta` y `.peek-dato` sus renglones, `.badge` los
   chips. Lo de aquí es sólo lo que el dinero y el documento piden. */

/* EL DINERO SE ALINEA A LA DERECHA Y SE ESCANEA POR EL PUNTO. Es la única
   familia de columnas de la app que no se lee de izquierda a derecha: lo que
   se compara entre filas es la magnitud, y con las cifras alineadas al inicio
   «$9.00» y «$1,240.00» empiezan en el mismo sitio y engañan al ojo. Va junto
   con `.col-num` (tabular-nums), que es lo que impide que los dígitos bailen
   de ancho al ordenar. La cabecera lleva la MISMA clase: un rótulo alineado
   al contrario de su columna se lee como de la columna de al lado. */
/* `.gs-num` (gastos, 2026-08-27) comparte la lista y no copia la regla: la
   alineación del dinero es del MOLDE de una sábana con columnas de importe, no
   de la página que la estrena — el mismo criterio con el que `.ct-*`/`.env-*`
   comparten las suyas. Vive bajo el rótulo de Ventas porque es donde nació. */
/* `.rg-num` (registros, 2026-08-27) entra por la misma puerta, y con el caso
   que más lo pide de los tres: sus DOS columnas de dinero —«Entrada» y
   «Salida»— sólo se leen como un libro de caja si las cifras de cada una se
   apilan por su punto decimal. */
/* `.cl-num` (clientes, 2026-08-28) es el cuarto, y el que más columnas mete de
   una vez: comprado, pendiente y el valor mensual del ente. Entra por la misma
   puerta y por el mismo motivo — la alineación del dinero es del MOLDE, no de
   la página que la estrena. La lleva también su columna «Personas», que no es
   dinero pero SÍ es una cantidad que se compara entre filas: lo que pide la
   derecha es la magnitud, no la moneda. */
/* `.ca-num` (cartera, 2026-08-30) es el quinto y el que vuelve indiscutible la
   regla: sus CINCO columnas de dinero —las cuatro bandas de antigüedad más el
   total— existen para compararse entre sí Y entre filas, y sin el punto decimal
   apilado no se puede leer de un vistazo en qué tramo está el bulto. Entra por
   la misma puerta que las cuatro de arriba. */
/* `.it-num` (catálogo, 2026-09-03) es el sexto, y entra el día que la lista de
   items dejó de ser una consola de renglones para leerse en columnas. Son dos:
   el precio de lista y lo disponible. La segunda no es dinero —es una cantidad,
   con sus decimales— y entra por lo mismo que la columna «Personas» de
   clientes: lo que pide la derecha es la MAGNITUD, no la moneda; un catálogo se
   escanea buscando lo caro y lo que se está acabando, y las dos preguntas se
   contestan comparando cifras entre filas. */
/* `.co-num` (presupuestos, 2026-09-04) es el séptimo: una columna de dinero,
   la del importe, y entra por la misma puerta que las seis de arriba. */
.vt-num,.gs-num,.rg-num,.cl-num,.ca-num,.it-num,.co-num{text-align:right}
/* El folio es el número que la gente dice en voz alta, así que pesa como el
   nombre en la sábana de leads — es lo que identifica la fila. */
.vt-folio,.gs-folio,.rg-folio,.cl-folio,.co-folio{font-weight:500;font-variant-numeric:tabular-nums}
/* La cartera NO está en esa lista: retiró su columna «Folio» el 2026-08-30 para
   hacerle sitio a las bandas (el porqué, en `cartera.php`). En su lugar
   marca el TOTAL de la fila, que es la columna que cierra las cinco de al lado
   y a la que se llega leyendo en horizontal: sin peso propio se lee como una
   sexta banda, y con él se lee como su suma. Es el mismo trabajo que el folio
   hacía al principio de la fila —dar un ancla al ojo—, movido a donde ahora
   está el dato que manda. */
.ca-total-fila{font-weight:600}

/* EL PUNTO DE AVISO DEL PENDIENTE. `.glifo` es `display:flex` y por defecto
   alinea a la izquierda; aquí el grupo entero se empuja a la derecha para que
   **los importes sigan alineados por su punto decimal** — que es lo único que
   una columna de dinero pide. El punto queda a la izquierda del número y su x
   varía con el ancho de la cifra: eso está bien, lo que no puede moverse es
   el borde derecho. */
.vt-num .glifo,.gs-num .glifo,.cl-num .glifo,.ca-num .glifo,.it-num .glifo,.co-num .glifo{justify-content:flex-end}

/* EL FILO ENTRE FOLIO Y FECHA, destapado (2026-08-18, lo cazó Arif: «no veo la
   línea de celda entre folio y fecha»). El porqué del tapado está donde vive el
   mecanismo, arriba en `.fila-rejilla > :nth-child(1)`. Aquí sólo el hecho:
   esta sábana abre con el FOLIO, un dato, así que la cuadrícula completa que el
   proyecto promete —tipo hoja de cálculo, 2026-08-13— tiene que incluir ese
   filo. Va acotado a las sábanas que abren con folio a propósito: en leads la
   primera columna es el check y ahí Arif no lo quiere. */
/* PRENÓMINA entró el 2026-09-21 por el mismo criterio (lo cazó Arif: «entre
   la columna folio y trabajador no hay línea vertical»): abre con el folio y
   no tiene casilla. */
/* CITAS entró el 2026-09-24, otra vez por el mismo reporte («entre la primer y
   segunda columna no existe la línea vertical»): abre con CUÁNDO, un dato, y no
   tiene casilla. ENVÍOS, el mismo día y por el mismo motivo: abre con su
   CUÁNDO de 164px. */
.rg-sabana .fila-rejilla > :nth-child(1),
.pn-sabana .fila-rejilla > :nth-child(1),
.ct-sabana .fila-rejilla > :nth-child(1),
.env-sabana .fila-rejilla > :nth-child(1){z-index:2}
/* Y GASTOS y VENTAS salieron de esta lista el 2026-08-28 por el criterio de
   arriba, no por una excepción: desde que tienen barra de selección abren con
   el CHECK, y el criterio dice que la sábana que abre con el check NO se
   destapa el filo — un canal de 15px con línea propia se lee como una columna
   que no es. Es el mismo sitio donde está leads, y por la misma razón. */

/* EL SEGUNDO CONGELADO ARRANCA DONDE TERMINA EL PRIMERO. El `left:25px` del
   molde está calibrado para la sábana de leads, cuya primera columna es el
   check (15px + 10 de padding). Aquí la primera es el FOLIO y mide 78px
   pintados, así que con el valor heredado las dos congeladas se solaparían 53px
   al desplazarse en horizontal. Hoy no se ve —la rejilla está dimensionada para
   caber— y por eso se arregla ahora: es la clase de defecto que sólo aparece el
   día que alguien abre la app en una ventana angosta.

   El valor es una MEDIDA de la rejilla, no una constante del molde: nació
   compartido por tres sábanas que abrían con un folio de 68px y hoy le queda
   una.

   GASTOS Y VENTAS LO COMPARTÍAN Y DEJARON DE HACERLO (2026-08-28), que es la
   demostración de la frase de arriba: el 78 no era suyo por ser esas páginas,
   era suyo por abrir con un folio de 68px. Con la barra de selección abren con
   el check, así que su medida volvió a ser la del molde (`left:25px` = 10 de
   padding + 15 de check) y basta con no nombrarlas aquí.

   Y LA CARTERA LO COMPARTIÓ UNAS HORAS el 2026-08-30, mientras abrió con folio.
   Al retirarse esa columna dejó de tocarle, que es la mejor prueba de la frase:
   la medida es de la REJILLA y se va con ella. Su caso, abajo. */
.rg-sabana .fila-rejilla > :nth-child(2){left:78px}
/* Citas y envíos abren con CUÁNDO, de 164px (`$rejilla` de cada página): el
   segundo congelado arranca en 10 de padding + 164. Con el 25 del molde,
   la segunda columna (Contacto en citas, Para en envíos) se metería 149px bajo la fecha al desplazar en horizontal — y con
   la primera ya en `z-index:2` quedaría tapado, no encimado. */
.ct-sabana .fila-rejilla > :nth-child(2),
.env-sabana .fila-rejilla > :nth-child(2){left:174px}

/* ── LA CARTERA CONGELA UNA COLUMNA, NO DOS (2026-08-30) ──────────────────
   Es la única sábana que abre con una columna ELÁSTICA —el nombre del cliente,
   sin folio ni casilla delante— y eso rompe dos supuestos del molde a la vez:

   · **La celda 2 ya no es texto, es dinero.** Congelarla dejaría fija una
     columna de importes mientras las otras cinco corren por debajo: no
     identifica la fila, que es para lo que existe el congelado. Se libera con
     `position:static`, y con ella se van el `left`, el margen negativo que
     absorbía el canal y la sombra —que pasa a la primera, la que de verdad se
     queda—.
   · **`display:flex` en la celda 1 mataría el ellipsis.** El molde se lo pone
     porque ahí vive la casilla —una caja de 15px que hay que centrar—, y el
     propio molde advierte que en las demás columnas está prohibido: el texto es
     un nodo suelto y `text-overflow` no aplica a un hijo anónimo de flex. Un
     nombre largo se saldría de su columna en vez de truncarse.

   Lo que NO cambia es el `left:0` de la celda 1 ni su fondo opaco: los hereda
   del molde y son exactamente lo que hace falta. */
.ca-sabana .fila-rejilla > :nth-child(1){display:block;z-index:2}
.ca-sabana .fila-rejilla > :nth-child(2){position:static;left:auto;margin-left:0;padding-left:0}
.ca-sabana.desplazada .fila-rejilla > :nth-child(2){box-shadow:none}
.ca-sabana.desplazada .fila-rejilla > :nth-child(1){box-shadow:7px 0 7px -7px rgba(0,0,0,.45)}

/* ── EL PANEL ──
   LA CIFRA MANDA. Al abrir una venta la primera pregunta es «¿de cuánto?», y
   la respuesta va en el tamaño del titular, no en un renglón etiqueta-dato
   como los demás: un importe de documento es el dato, no un atributo suyo.
   Comparte fuente con el `h2` del panel (la de títulos) para que se lea como
   parte de la cabecera y no como el primer dato del cuerpo. */
/* `.cl-*` (clientes, 2026-08-28) comparte de aquí abajo TODO lo que este bloque
   declara —la cifra, la barra de cobro y los renglones del panel— y no copia una
   sola regla. El criterio es el que ya usaron `.gs-num` y `.rg-num` arriba: lo
   que se comparte no es «el estilo de ventas», es la anatomía de un PANEL QUE
   ENSEÑA UN DOCUMENTO O UN ENTE — cifra arriba, barra de saldo, renglones con
   nombre, meta e importe. Que naciera pintando una venta no la hace suya.
   Lo único que el panel del cliente NO reusa es el timeline, porque el ente no
   tiene línea de vida propia: la tienen sus ventas. */
/* `.ca-*` (cartera, 2026-08-30) comparte lo mismo y por el mismo criterio, con
   una diferencia que conviene decir: su cifra NO es el importe de un documento
   ni lo comprado por un ente — es lo que ese ente DEBE. Sigue siendo «el dato y
   no un atributo suyo», que es lo que este bloque declara, así que entra sin
   regla propia. Lo que no reusa es la barra de cobro: una barra de avance
   contesta «cuánto va cobrado», y esta pantalla existe para lo contrario. */
/* LA CIFRA QUE ABRE UN PANEL — nombre de sistema desde el 2026-09-04, con su
   CUARTO consumidor (el catálogo de productos). Se llamó `.vt-cifra` /
   `.cl-cifra` / `.ca-cifra`: tres prefijos de página compartiendo una regla por
   lista de selectores, que es la forma que tiene una pieza de sistema antes de
   que alguien le ponga el nombre. La regla del catálogo lo dice al derecho —una
   pieza sube cuando la usan dos superficies, y sube CON nombre de sistema— y es
   el mismo camino que ya recorrieron `.peek-dato` y `.pk-valor`.

   QUÉ DECLARA, que es lo que la hace reusable: **el dato principal del registro
   va primero y va grande**, porque es la pregunta con la que se abre el panel —
   cuánto vale la venta, cuánto ha comprado el ente, cuánto debe, cuánto cuesta
   el producto—. A su lado, y sólo si hay algo que decir, un `.badge`: el estado
   NORMAL no se etiqueta, porque teñirlo deja sin contraste al que no lo es. */
.peek-cifra{display:flex;align-items:center;gap:10px;margin-top:14px}
/* CON RÓTULO ENCIMA, EL AIRE LO PONE EL RÓTULO (2026-09-04). Los 14px de
   arriba separan la cifra de lo que tenga ENCIMA, y cuando lo de encima es su
   propio `.peek-etiqueta` —que ya pone 7 de `margin-bottom`— los dos se suman:
   **21px entre un rótulo y el número que nombra, contra los 7 con que abre
   cualquier `.peek-dato`**. A esa distancia el rótulo deja de leerse como suyo,
   que es justo lo que el rótulo vino a arreglar.

   Es el MISMO caso que `.ficha-cuerpo` ya resolvió y con la misma salida: el
   aire entre una cabecera y su contenido lo pone uno de los dos, no los dos.
   Se escribe como adyacencia y no como clase nueva porque la condición es
   posicional — «llevo rótulo» ES «tengo un `.peek-etiqueta` justo antes».

   TOCA A SUS DOS CONSUMIDORES, y está bien: el panel del cliente («Ventas de
   septiembre») y el de productos («Existencia»), que llevaba el par desde que
   nació y arrastraba los mismos 21. */
.peek-etiqueta + .peek-cifra{margin-top:0}
/* LA CIFRA LLEVA RÓTULO CUANDO EL SUJETO DEL PANEL NO LA NOMBRA (movido del
   catálogo `ui.php`, 2026-09-19). En una venta o en un moroso el número basta:
   el panel ya dice de qué habla. En un cliente no —un número solo puede ser lo
   comprado, lo que debe, lo del mes o su mensualidad—, y lo cazó Arif sobre la
   pantalla montada: «el importe es confuso». La pregunta que decide es «¿el
   sujeto de este panel tiene una sola cifra posible?», no si la cifra es
   obvia para quien la programó. El ejemplo del producto lo confirma: nació sin
   rótulo, y al ganar su segunda cifra (la existencia) la primera se leía como
   si le faltara algo. Cuando una regla trae ejemplos, manda el criterio: la
   lista de casos caduca con la primera pantalla nueva. */
.peek-cifra-n{
  font-family:var(--fuente-titulo);font-size:var(--fs-destacado);font-weight:700;
  letter-spacing:-.015em;line-height:1.1;font-variant-numeric:tabular-nums;
}

/* EL AVANCE DE UN SALDO — nombre de sistema desde el 2026-09-05, con su TERCER
   consumidor (el panel del gasto). Se llamó `.vt-saldo` / `.cl-saldo`: dos
   prefijos de página compartiendo una regla por lista de selectores, que es la
   forma que tiene una pieza de sistema antes de que alguien le ponga el nombre.
   Es el mismo camino de `.peek-cifra`, `.peek-prosa` y `.pk-cab-dato`, y la
   lección que aquí se aplicó en vez de volver a pagarla: **una pieza bautizada
   por su primer consumidor caduca en cuanto llega el segundo, y el aviso no
   llega solo — hay que ir a buscarlo al añadir el consumidor.**

   QUÉ DECLARA, que es lo que la hace reusable: **qué proporción de un documento
   está saldada**. La barra es lo que el «Resumen de saldo» de la rueda resuelve
   con una barra de progreso, y se conserva porque contesta de un vistazo lo que
   dos cifras obligan a restar.

   VERDE Y NO ACENTO, y la razón sobrevive al cambio de dirección del dinero: en
   la venta lo que cuenta es que el dinero ENTRÓ y en el gasto que la deuda se
   SALDÓ, y las dos son la misma señal — esto ya está resuelto. Es el mismo
   verde de la línea de tiempo de la venta y el del punto del pago vivo en el
   desglose, y dos señales del mismo hecho no pueden ir de colores distintos en
   la misma pantalla. */
.peek-saldo{margin-top:4px}
.peek-barra{
  height:6px;border-radius:var(--r-mini);margin-top:8px;overflow:hidden;
  background:var(--app-hairline);
}
.peek-barra-avance{display:block;height:100%;width:0;background:var(--app-exito)}
/* Las dos cifras a los extremos: lo saldado empuja desde la izquierda y lo
   pendiente cierra a la derecha, en la misma dirección en que la barra se
   llena. */
.peek-saldo-cifras{
  display:flex;justify-content:space-between;gap:12px;margin-top:6px;
  font-size:var(--fs-micro);color:var(--app-fg-secondary);
  font-variant-numeric:tabular-nums;
}

/* EL DETALLE. Filas planas con hairline y no una tabla, igual que el kardex
   del catálogo y por el mismo motivo: son tres o cuatro renglones que se ojean
   dentro de un panel de 420px, no una rejilla donde se comparan columnas.
   El nombre arriba con su meta debajo, y el importe anclado a la derecha —que
   es la cifra que se busca al recorrerlos. */
.vt-renglones,.cl-renglones,.ca-renglones{margin-top:2px}
.vt-renglon,.cl-renglon,.ca-renglon{
  display:grid;grid-template-columns:1fr auto;gap:2px 12px;
  padding:8px 0;border-bottom:1px solid var(--app-hairline);
}
.vt-renglon:last-child,.cl-renglon:last-child,.ca-renglon:last-child{border-bottom:none}
.vt-renglon-nombre,.cl-renglon-nombre,.ca-renglon-nombre{font-size:var(--fs-base);overflow-wrap:anywhere}
/* El importe del renglón comparte fila con el nombre y se alinea con los
   demás: es la columna que se escanea hacia abajo. */
.vt-renglon-importe,.cl-renglon-importe,.ca-renglon-importe{
  grid-column:2;grid-row:1;text-align:right;font-weight:600;
  font-size:var(--fs-base);font-variant-numeric:tabular-nums;white-space:nowrap;
}
/* «2 × $85.00 · Producto · Código 7». Cruza las dos columnas porque es la
   explicación del renglón entero, no un dato de la primera. */
.vt-renglon-meta,.cl-renglon-meta,.ca-renglon-meta{
  grid-column:1 / -1;display:flex;align-items:center;gap:6px;flex-wrap:wrap;
  font-size:var(--fs-micro);color:var(--app-fg-secondary);
}

/* EL GESTO DEL RENGLÓN (cartera, 2026-08-30) — el único renglón del panel que
   ADEMÁS de informar se pulsa. Va en su propia fila del grid, alineado a la
   derecha bajo el importe que va a mover.

   AQUÍ DECÍA «`btn-suave`, porque el azul del acento vive DENTRO del diálogo
   que abre», y las dos mitades de esa frase se separaron el 2026-09-04: la
   segunda sigue siendo cierta —pulsar aquí no escribe todavía— pero no lleva a
   `.btn-suave`, que es la SALIDA del pie de un diálogo. Lo que gobierna es la
   regla del catálogo, «lo que abre una capa lleva cara», así que el botón es
   `.btn-segunda`. El comentario se quedó atrás cuando el JS se corrigió; queda
   escrito porque el argumento que lo eligió mal es el que hay que no repetir.

   Es la diferencia entre este panel y el del cliente: allá los renglones son
   historia y aquí son trabajo pendiente. Sin el gesto, la cartera diría a quién
   cobrar y obligaría a ir a buscar la venta a otra lista para hacerlo. Desde el
   2026-09-04 el panel de la VENTA hace lo mismo en su bloque «Cobro», y no
   necesitó una regla nueva: allá el gesto cabe en `.pk-cab`, la cabecera de
   sección del peek, porque hay UN saldo y no N renglones. */
.ca-renglon .ca-cobrar{grid-column:2;justify-self:end;margin-top:4px}

/* LA META DE LA CARTERA FLUYE COMO PÁRRAFO, no como fila de items
   (2026-08-30). Es la única de las tres que se aparta del molde flex, y el
   motivo se vio en pantalla con una venta de concepto largo:

   Esta meta abre con un `.glifo` —la antigüedad teñida— y `.glifo` es
   `display:flex`, o sea un ITEM propio del contenedor. Con la meta en flex eso
   deja dos malos repartos y ninguno tercero: uniendo las piezas con `' · '` de
   texto, todo lo que sigue al glifo cae en un ÚNICO nodo y la segunda línea
   arranca con un `·` suelto; separándolas en elementos, cada pieza se vuelve
   inrompible y un concepto de seis palabras se lleva su renglón entero —tres
   líneas donde caben dos—.

   La cura no es elegir el menos malo: es que el contenedor deje de ser flex.
   Como bloque, el texto envuelve por PALABRA como cualquier párrafo y el corte
   cae donde toca; el glifo se suma al flujo con `inline-flex`, que es lo que le
   permite conservar su punto y su alineación sin ocupar una línea.

   Las otras dos metas se quedan en flex y NO se tocan: sus piezas son todas
   texto —ahí el `' · '` unido funciona— y la de ventas usa el `gap` para
   separar chips. Lo que cambia aquí no es el gusto, es que hay un elemento
   entre el texto. */
.ca-renglon-meta{display:block}
.ca-renglon-meta .glifo{display:inline-flex;vertical-align:baseline}

/* LA LÍNEA DE TIEMPO. Mismo molde de filas planas que el detalle — es una
   narrativa que se ojea. El tono lo lleva el rótulo del evento y no la fila
   entera: teñir el fondo de una fila de cinco convierte la lista en un
   semáforo y deja sin contraste a las que sí piden atención. */
.vt-timeline{margin-top:2px}
.vt-evento{padding:8px 0;border-bottom:1px solid var(--app-hairline)}
.vt-evento:last-child{border-bottom:none}
.vt-evento-cab{display:flex;align-items:baseline;justify-content:space-between;gap:12px}
.vt-evento-clase{font-size:var(--fs-base);font-weight:500}
.vt-evento-importe{
  font-size:var(--fs-micro);color:var(--app-fg-secondary);
  font-variant-numeric:tabular-nums;white-space:nowrap;
}
.vt-evento-meta{
  display:block;margin-top:2px;
  font-size:var(--fs-micro);color:var(--app-fg-tertiary);
}
/* Lo que ENTRÓ dinero. */
.vt-evento.vt-ok .vt-evento-clase{color:var(--app-exito-text)}

/* DOS TRATAMIENTOS Y NO UNO, corregido mirando el timeline de una venta con un
   cobro anulado (2026-08-18): la primera versión tachaba las tres clases de
   esta familia y dejaba «Cobro cancelado» tachado, o sea diciendo que la
   CANCELACIÓN está cancelada. No lo está — es un acto que ocurrió y se sostiene.

   · `.vt-anulado` — el hecho que dejó de valer: el COBRO que se anuló. Se tacha
     y se baja de tono. Sigue a la vista porque explica el saldo de hoy, que es
     el mismo criterio con el que `.ct-muerta` conserva la cita cancelada.
   · `.vt-revierte` — el ACTO de deshacer (cancelar la venta, cancelar el
     cobro). No se tacha: se tiñe en el rojo de la casa, que es lo que dice
     «esto quitó algo» sin negar que pasó. */
.vt-evento.vt-anulado{opacity:.6}
.vt-evento.vt-anulado .vt-evento-clase{text-decoration:line-through}
.vt-evento.vt-revierte .vt-evento-clase{color:var(--app-peligro-text)}

/* El hueco mientras el detalle viene por la red, y también el «sin eventos».
   Un panel que abre con dos secciones en blanco se lee como roto; una línea
   que dice qué pasa, no. */
/* Y el mismo tono para el hueco NOMBRADO del panel del cliente («nadie colgado
   todavía», «todavía no compra»): es la misma pieza — texto de servicio dentro
   de un bloque que espera contenido. */
.vt-cargando,.cl-cargando,.ca-cargando{padding:10px 0;font-size:var(--fs-micro);color:var(--app-fg-tertiary)}

/* ── Ventas: la captura en el panel (interno/venta-nueva.php) ────────────
   El panel que CAPTURA, hermano del que lee. Dos diferencias con él y las dos
   tienen la misma causa: aquí se construye un documento con renglones, y eso
   pide sitio.

   ── EL ANCHO: `peek-lg` ──
   La medida no se declara aquí — vive en la escala del panel, allá arriba con
   `.peek`, porque es del SISTEMA y no de esta pantalla. Lo que sí es de esta
   pantalla es de dónde salió el número: la línea de captura de abajo pide
   560 − 48 de padding = 512 útiles — 84 (cantidad) + 104 (precio) + ~110 de
   gestos + 24 de huecos le dejan ~190 al producto. A 420 el producto se
   quedaba en 50px, o sea en nada. Es el único ancho de la app que se ganó
   midiendo, y por eso es el ejemplo de qué hace falta para pedir el siguiente.
   (Hasta el 2026-08-18 esto era `.peek.ancho`, una excepción con nombre de
   adjetivo: ancho contra qué. Al volverse escala tomó el nombre de su rung.) */

/* ── LA LÍNEA DE CAPTURA ──
   Producto, cantidad, precio y el gesto, en una fila. Es la misma pieza para
   AGREGAR (al pie de la lista) y para EDITAR (en el sitio del renglón), porque
   son el mismo trabajo — y así no hay dos formularios que se separen.

   NO es un diálogo, y ése es el punto: agregar un renglón no es una decisión
   aparte sino la continuación de lo que ya estabas haciendo, así que no puede
   interrumpir ni tapar con un velo lo que llevas capturado. La regla la tiene
   escrita el catálogo: «si lo que abres se puede dejar abierto mientras
   trabajas, no es un diálogo». Hasta el 2026-08-18 esto era un `.dialogo`
   ENCIMA del panel, que además había que sacar fuera del `<aside>` para que el
   `overflow:hidden` del peek no lo recortara — cuando una capa hay que
   partirla en dos para que quepa, la capa sobra.

   LOS CONTROLES NO LLEVAN ETIQUETA, y eso no cambió: cuatro rótulos encima de
   cuatro controles de una fila serían más tinta que dato. Lo que sí cambió el
   2026-08-18 por la tarde es QUIÉN hace de rótulo. Hasta entonces el
   placeholder («Cant», «Precio»), que es el caso que el catálogo permite —el
   control vive suelto y no hay formulario que etiquetar—, pero que arrastra su
   defecto conocido: se BORRA justo cuando empiezas a escribir, así que el campo
   que estás llenando es el único que ya no dice qué es. Ahora NO hay
   placeholder: los dos números nacen con un CERO cargado como valor (`0` y
   `0.00`, puestos por `NUM.escribir` para que sigan al papel de cada campo),
   que enseña la forma esperada y no se desvanece; y el rótulo del precio lo da
   su afijo de moneda (`.ctl-moneda`, abajo), que tampoco. La cantidad se queda
   sin marca visible: es el control sin `$` de una fila de dos números, y su
   `aria-label` sostiene el nombre accesible. */
/* ── EL RENGLÓN YA CAPTURADO: los gestos bajan con los números (2026-08-18)
   `.cfg-fila` reparte 8px · nombre · gestos, con los números en una segunda
   fila bajo el nombre. Sirve en una página ancha y **estrangula al nombre en
   un panel de 420**: los dos botones se llevan 117px FIJOS de los 372 útiles,
   en todos los renglones y para siempre, dejando 222 al nombre del producto.

   No es un choque —la rejilla nunca deja que el nombre toque los botones—, es
   inanición. Medido con un nombre de 74 caracteres, que es un nombre de
   refacción cualquiera:

       hoy (gestos arriba)      222px de nombre · 3 líneas · fila de 98px
       gestos con los números   351px de nombre · 2 líneas · fila de 79px

   O sea que bajarlos gana ancho Y altura a la vez, que es raro: normalmente
   una de las dos se paga. La tercera opción medida —gestos en fila propia al
   final— da el mismo ancho y cuesta 102px, así que no compite.

   Sólo aplica al renglón de venta (`.vn-renglon`), no a `.cfg-fila`: esa clase
   la usan media docena de pantallas anchas donde el reparto de tres columnas
   es el correcto. */
/* EL RENGLÓN CAPTURADO SE DIBUJA COMO CAJA (2026-08-18, Arif): mismo punteado
   que el gesto de agregar, porque son las dos piezas de la misma lista — lo que
   ya hay y lo que falta por poner, dichos igual.
   `margin` sin la sangría de `.cfg-fila`: aquella lleva `0 -8px` para que el
   fondo del hover llegue al canto, y con borde eso dejaría al renglón 16px más
   ancho que el botón de abajo — dos cajas de la misma lista sin alinear se leen
   como error. Al perder la sangría gana su propio padding lateral, que es lo
   que mantiene el texto donde estaba.
   Los 4px de abajo separan una caja de la siguiente: pegadas, dos punteados
   contiguos se leen como una línea doble. */
.cfg-fila.vn-renglon{
  /* SIN LA COLUMNA DEL PUNTO. `.cfg-fila` abre con 8px para el semáforo de
     estado más 12 de hueco, y un renglón de venta NO tiene estado que marcar:
     ahí iba un `<span>` vacío. Eran 20px de espacio muerto a la izquierda —
     invisibles mientras la fila no tuvo caja, y descarados en cuanto se le puso
     el punteado: 29px de aire a la izquierda contra 9 a la derecha (medido).
     Quitada la columna, el padding de 8 manda por los dos lados. */
  grid-template-columns:minmax(0,1fr);
  border:1px dashed var(--ctl-borde-hover);
  margin:0 0 4px;
  /* EL AIRE DE LA CONSOLA NO GOBIERNA AQUÍ (2026-09-03). `--esp-opcion` subió
     a 14 para que la configuración respire, y esto no es configuración: es un
     renglón de captura dentro de un panel de 420px, donde el alto lo decide el
     contenido y cada píxel de más se lo quita a la lista. Se devuelve al 6 de
     siempre redeclarando el token, no escribiendo un `padding` — así el día que
     el de la consola cambie, éste sigue siendo explícitamente OTRO número y no
     una copia que se quedó atrás. */
  --esp-opcion:6px;
}
/* Con una sola columna, la segunda línea arranca en la primera y no en la 2ª —
   y el rótulo también, que desde el 2026-09-03 va anclado a la 2 por defecto. */
.vn-renglon .cfg-detalle,
.vn-renglon .cfg-rotulo{grid-column:1 / -1}
.vn-total .cfg-rotulo{grid-column:1}
/* ── LA FILA DEL GRAN TOTAL (2026-08-18) ──────────────────────────────────
   Tres cosas la separan de un renglón, y las tres las pidió Arif mirándola:

   · FILETE ARRIBA. Cierra la lista: lo de abajo ya no es un renglón más sino
     su suma. Va en `--ctl-borde`, y el token se eligió COMPARANDO los tres
     grises del sistema en pantalla, no de memoria: `--app-hairline` (.06) es
     el de separar bloques y aquí no se ve —el filete existía y no hacía su
     trabajo—; `--app-borde-muted` (.15) se lee pero pesa más que el número que
     está cerrando; `--ctl-borde` (.10) es el filo de un control, visible y
     callado, que es exactamente lo que se le pide a una raya de suma.
   · SIN HOVER. `.cfg-fila` se ilumina al pasar porque sus filas se pulsan o
     tienen gestos; ésta no hace nada, y un fondo que responde promete un clic
     que no existe.
   · SIN LA SANGRÍA de `.cfg-fila` (`margin:0`), por lo mismo que el renglón la
     perdió al volverse caja punteada: los tres bloques de esta lista —renglón,
     total y el gesto de agregar— tienen que compartir cantos. Ocho píxeles de
     desalineación entre piezas apiladas no se leen como intención.
   · Y es la ÚNICA en negrita: el importe de cada renglón bajó a peso normal
     (ver `.vn-imp`), así que el peso vuelve a significar algo en la columna.

   Acotada al panel de venta, con prefijo, porque hoy tiene UN consumidor. Si
   la cotización pide su total, sube y pierde el prefijo — el mismo camino que
   hizo `.rec-add` para volverse `.fila-agregar`. */
.cfg-fila.vn-total{
  /* Pierde la columna del punto por lo mismo que el renglón, y además POR él:
     dejarla aquí indentaría «Total» 20px respecto de los nombres de producto
     de arriba, que es la desalineación que este cambio venía a matar. */
  grid-template-columns:minmax(0,1fr) auto;
  /* El hueco ENTRE sus dos celdas, que `.cfg-fila` dejó de poner el 2026-09-19
     (allá lo llevan las piezas, para que la columna del punto colapse). Aquí
     no hay punto y sí dos celdas que separar: «Total» y su importe. */
  column-gap:12px;
  /* Y el aire de siempre, como su renglón hermano: esto vive en el peek. */
  --esp-opcion:6px;
  /* Filo LATERAL transparente: el renglón de arriba lleva borde en los cuatro
     lados y esta fila sólo arriba, así que sin esto su contenido quedaría 1px
     corrido respecto de los nombres de producto. Es invisible de mirar y
     visible de medir; el truco cuesta una declaración y deja la columna
     exacta. */
  border:1px solid transparent;
  border-top:1px solid var(--ctl-borde);
  margin:0;
}
.cfg-fila.vn-total:hover{background:transparent}
/* El importe del renglón, en peso normal. Era un `<strong>` heredado del
   marcado canónico de `.cfg-fila`, donde el valor sí es lo que se busca; en una
   lista de renglones que suman, poner en negrita CADA uno es no poner ninguno:
   el ojo no encuentra el total. */
.vn-renglon .cfg-detalle .vn-imp{font-weight:400}
/* `margin-left:auto` y no un separador vacío: `.cfg-detalle` ya es flex, así
   que el auto empuja los gestos al canto sin agregar un nodo que mantener. */
.vn-renglon .cfg-detalle .fn-gestos{margin-left:auto}

.vn-linea{
  display:grid;grid-template-columns:minmax(0,1fr) 84px 104px auto;
  gap:8px;align-items:center;
  padding:8px;margin:2px -8px 0;
  border-radius:var(--r-ctl);background:var(--app-group);
}
/* EN 420 LA LÍNEA SE PARTE EN DOS FILAS (2026-08-18) ────────────────────
   El panel de captura volvió a `peek-sm` y los cuatro controles en una sola
   fila no caben: 420 − 48 de padding dejan 372 útiles, y cantidad (84) +
   precio (104) + los gestos (~110) + tres huecos (24) se comen 322 — al
   producto le quedaban ~50px, que es donde estábamos antes de que el panel
   se ensanchara a 560.

   Se parte por donde manda el reparto de lo que cada cosa ES: el PRODUCTO
   es lo que se elige y necesita leerse entero, así que sube solo a la
   primera fila a todo lo ancho; cantidad, precio y el gesto son cifras
   cortas y un botón, y caben de sobra en la segunda (84 + 104 + gestos +
   huecos sobre los mismos 372).

   Es el MISMO recurso que usa `.nota-nueva` en el peek de contactos —y antes
   que ella `.registrar-toque`, de donde salió la lección—, y por la misma
   razón: en un panel angosto el que cede no puede ser el campo principal. Allá
   se resuelve con `flex-wrap` porque es flex; aquí con `grid-column` porque es
   rejilla — el gesto es idéntico.

   LOS GESTOS VAN AL CANTO DERECHO (corrección de Arif, 2026-08-18). La primera
   versión los dejó pegados al precio —tercera columna en `auto`— razonando que
   el gesto vive al lado de lo que opera. Manda la otra lectura: la fila de
   abajo cierra donde cierra el producto de arriba, y el borde derecho de la
   línea deja de verse dentado. La columna pasa a `1fr` y los botones se
   empaquetan al final. Es además lo que ya hace la línea a 560 sin pedirlo,
   donde el `1fr` del producto se come el sobrante: así las dos medidas
   terminan igual en vez de por accidente. */
.peek-sm .vn-linea{grid-template-columns:84px 104px 1fr}
.peek-sm .vn-linea .vn-l-caja{grid-column:1 / -1}
/* Los controles son los MISMOS del sistema (`.ctl`), sin variante reducida:
   este proyecto no tiene tamaños chicos y estrenar uno aquí sería el primero
   que alguien copia. Lo único propio es que los dos números van alineados a la
   derecha y en cifras tabulares — se comparan en columna con los importes de
   los renglones de arriba. */
/* `[data-num]` y no `input.ctl`: la regla se escribió cuando los tres campos de
   la línea eran números, y desde que el producto es un combo hay un input de
   TEXTO ahí dentro — con el selector viejo el nombre del producto se alineaba a
   la derecha, que es donde no se lee un nombre. El atributo ya marca cuáles son
   cifras (lo usa NUM para saber su papel), así que el selector correcto ya
   existía. */
.vn-linea input[data-num]{text-align:right;font-variant-numeric:tabular-nums}
/* `flex-end` los pega al canto derecho de su columna. A 560 la columna es
   `auto` y no sobra nada, así que ahí no cambia nada; a 420 es `1fr` y es lo
   que los lleva al borde. Una sola regla para las dos medidas. */
.vn-linea .vn-gestos{display:flex;gap:2px;align-items:center;white-space:nowrap;justify-content:flex-end}

/* ── EL SIGNO DE MONEDA DENTRO DEL CAMPO (2026-08-18) ────────────────────
   Un afijo, no un placeholder, y la diferencia es la que pidió Arif: un
   placeholder se BORRA justo cuando empiezas a escribir —o sea que el campo
   que estás llenando es el único que ya no dice qué es—, y el afijo se queda.
   Es la misma objeción que el catálogo ya tiene escrita contra el placeholder
   como rótulo; aquí el signo la resuelve en vez de esquivarla.

   VA A LA IZQUIERDA, que es donde lo pone el español —`$1,234.56`— y donde lo
   ponen las apps de dinero. El número sigue alineado a la DERECHA, así que
   entre los dos queda aire: es la disposición contable de toda la vida, el
   signo anclado al canto y las cifras cuadrando en columna por la derecha.

   Tres cosas lo hacen «no interferir con la captura», y las tres importan:
   · Vive en un `::before` del ENVOLTORIO, así que no es un nodo que el JS pueda
     confundir con un control ni algo que viaje en el `value` del input.
   · `pointer-events:none` — el clic sobre el signo atraviesa y enfoca el campo
     que hay debajo, que es lo que el usuario espera al apuntarle al campo.
   · El sitio se reserva con PADDING del input, no con margen del signo: si no,
     una cifra larga se le monta encima. 22px = los 10 del control + los ~8 del
     glifo + 4 de aire. Y cuando la cifra desborda, el input SCROLLEA dentro de
     su caja de contenido y el signo se queda quieto — comprobado con
     `9,128,940.75`, que se recorta por la izquierda sin tocarlo.

   El envoltorio hereda el sitio del input en la rejilla, así que el input pasa
   a `width:100%` — sin eso se encogería a su ancho intrínseco dentro de él.

   SUBE AL CATÁLOGO EL 2026-09-03 (pedido de Arif), y con eso cambia una
   decisión que este mismo archivo tenía escrita: el afijo existía «para las
   líneas de captura que no tienen etiqueta», y `gasto-nuevo.php` lo decía al
   revés —«sin afijo de moneda: aquí lo rotula su etiqueta»—. Lo que tumba ese
   argumento no es el gusto: **la etiqueta y el signo no dicen lo mismo**. Un
   rótulo se LEE («Valor mensual», «Importe del pago»); el `$` se RECONOCE sin
   leer, y es lo que hace que un campo de dinero se distinga de uno de cantidad
   de un vistazo, que es la confusión que de verdad cuesta —capturar 1500 en el
   campo de existencia y 12 en el de precio no da ningún error—.

   DOS CAMBIOS PARA QUE PUEDA VIVIR EN UN `.campo`:
   · El selector pasa de `> .ctl` a `> input`. Dentro de un `.campo` el control
     NO lleva la clase —la hereda por posición, que es el punto de esa pieza—,
     así que con `> .ctl` el `padding-left` no aplicaba y la cifra se montaba
     sobre el signo. Sigue casando el de la línea de venta, que sí es `.ctl`.
   · `tabular-nums`: las cifras miden todas igual, así que el número no se
     ensancha ni se angosta tecla a tecla. La línea de venta ya lo tenía en su
     propia regla; aquí deja de ser cosa de una pantalla.

   Lo que NO hace el afijo, y es la mitad de por qué se puede poner en todos:
   no toca el `value`. `NUM` sigue pintando el número sin `$` —está escrito en
   `app.js`— así que `NUM.leer()` y el `is_numeric()` de PHP no se enteran de
   que el signo existe.

   DÓNDE VA (movido del catálogo `ui.php`, 2026-09-19): en todo campo con
   `data-num="importe"`, que es el marcador que ya distingue dinero de
   cantidad, así que la lista no se mantiene a mano. El 2026-09-03 eran seis:
   precio de lista (`items.php`), valor mensual (`clientes.php`), importe
   (`gasto-nuevo.php`), importe del pago (`gasto-pagar.php`), importe del
   cobro (`venta-cobrar.php`) y precio del renglón (`venta-nueva.php`, de
   donde sale la pieza); los cinco primeros recibieron su envoltorio ese día.
   Los campos `data-num="cantidad"` NO lo llevan. Antes de envolver se
   verificó que el motor numérico no se rompe: `NUM` se engancha por
   delegación en `document` y reconoce el campo por su `data-num` sin mirar el
   padre, y los cinco se toman con `getElementById`, sin `parentNode` ni
   `closest()` alrededor. */
.ctl-moneda{position:relative;display:block;min-width:0}
.ctl-moneda > input{width:100%;padding-left:22px;font-variant-numeric:tabular-nums}
.ctl-moneda::before{
  content:"$";
  position:absolute;top:50%;left:10px;transform:translateY(-50%);
  font-size:var(--fs-base);line-height:1;color:var(--app-fg-tertiary);
  pointer-events:none;
}
/* EL SIGNO SE APAGA CON SU CAMPO. Sin esto el `$` se queda en el terciario
   mientras el número se va al gris del `:disabled` del navegador, y acaba
   leyéndose MÁS vivo que el dato al que acompaña — un afijo que pesa más que su
   cifra. `:has()` y no una clase: el estado ya lo dice el `disabled` del input,
   y pedir además una clase en el envoltorio es un segundo sitio que alguien va
   a olvidar sincronizar. */
.ctl-moneda:has(> input:disabled)::before{opacity:.55}

/* ── La página que no existe (404.php) ───────────────────────────────────
   Hermana de la puerta y no de una página de la app: son las dos únicas
   pantallas que se ven SIN el cascarón, así que comparten `.puerta` —el
   centrado a pantalla completa sobre papel— y su bloque de marca. Lo de aquí
   es sólo lo que cambia: una caja más ancha, porque lo que manda es una frase
   y no dos campos de 304px. */
.p404-caja{display:flex;flex-direction:column;align-items:center;width:min(420px,calc(100vw - 48px));text-align:center}
/* La cifra en la fuente de títulos y en el gris secundario: es el RÓTULO del
   error, no su mensaje — lo que se lee es la frase de abajo. En el tono del
   cuerpo competiría con ella, y en rojo diría «algo se rompió», que es
   justamente lo que un 404 no significa. */
.p404-cifra{
  font-family:var(--fuente-titulo);font-size:56px;font-weight:700;
  line-height:1;letter-spacing:-.03em;color:var(--app-fg-secondary);
  margin-top:10px;
}
.p404-titulo{font-family:var(--fuente-titulo);font-size:var(--fs-titulo);font-weight:600;letter-spacing:-.015em;margin-top:12px}
/* La ruta rota, en la tipografía del código: dice «esto es un dato literal».
   `overflow-wrap` no sobra — una ruta larga no tiene espacios donde partirse y
   se saldría de la caja. */
.p404-ruta{
  margin-top:10px;padding:5px 10px;border-radius:var(--r-ctl);
  background:var(--app-chip);color:var(--app-fg-secondary);
  font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:var(--fs-micro);
  max-width:100%;overflow-wrap:anywhere;
}
.p404-cuerpo{margin:14px 0 0;color:var(--app-fg-secondary);font-size:var(--fs-base);line-height:1.5}
.p404-boton{margin-top:20px}
