/* =========================================================================
   NÚCLEO — components.css
   Los componentes que el inventario de Fase 1 encontró REINVENTADOS de forma
   independiente tres veces o más. No es un catálogo de lo que podría hacer
   falta: cada bloque de aquí abajo existe porque el repositorio ya lo tiene
   escrito varias veces con nombres distintos.

   Prefijo `ds-` en todo. Es lo que mantiene el núcleo inerte mientras dura la
   migración: ninguna de estas clases existe hoy en las ~47.000 líneas de CSS
   de app, así que cargar este fichero no puede cambiar una pantalla sin
   migrar. Ver la cabecera de base.css.

   Reglas que se cumplen aquí sin excepción (DESIGN.md §2, §4, §5):
     - Ni un literal de color, tamaño de fuente o espaciado. Todo de tokens.
     - Ninguna sombra salvo en lo que flota de verdad (modal, panel lateral).
     - Ningún estado comunicado solo por color: siempre texto, y icono en los
       críticos.
     - El acento solo en navegación, acción primaria, fila seleccionada, foco
       y logotipo. Aquí eso significa: .ds-btn--primary, .ds-table
       tr[aria-selected] y el anillo de foco. En ningún borde decorativo.
   ========================================================================= */

/* =========================================================================
   1. SUPERFICIES
   Las tarjetas NO llevan sombra (DESIGN.md §1): se separan con borde y
   escalón de fondo. Reinventado en ~18 de los 20 CSS del repo.
   ========================================================================= */

.ds-panel {
  background: var(--n-0);
  border: var(--bd-strong);
  border-radius: var(--r-md);
}

.ds-panel__head {
  display: flex;
  align-items: center;
  gap: var(--s-5);
  padding: var(--s-5) var(--s-6);
  border-bottom: var(--bd);
}

.ds-panel__title {
  font-size: var(--fs-md);
  font-weight: 600;
  line-height: var(--lh-tight);
}

.ds-panel__body { padding: var(--s-6); }

/* El cuerpo de un panel que contiene una tabla no lleva relleno: la tabla
   llega hasta el borde y son sus celdas las que respiran. */
.ds-panel__body--flush { padding: 0; }

.ds-panel__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-5);
  padding: var(--s-5) var(--s-6);
  border-top: var(--bd);
  background: var(--n-25);
  border-radius: 0 0 var(--r-md) var(--r-md);
}

/* UN AVISO NO ES UNA COLUMNA AL LADO DEL BOTÓN: SE LLEVA SU RENGLÓN.
   El pie es una fila, y una fila coloca a su lado todo lo que le eches. En la
   ficha de tarea del doctor eso puso TRES columnas en un móvil —«queda 1
   presupuesto por validar», el botón «Pedir nuevo presupuesto» y «Atención al
   paciente lo creará…»— cada una de un tercio de 390px. El botón, que es
   `white-space: nowrap`, se sale de su caja antes que ceder ancho.

   Es EXACTAMENTE el defecto que ya estaba resuelto para la fila de acciones de
   un bloque (`.ds-b__accion` en `apps/coordinador.css`), con la misma cura y
   por el mismo motivo, y aquí faltaba: el pie del panel es el otro sitio donde
   el sistema pone un motivo escrito junto a un botón, porque §6 no admite que
   una información viva solo en un `title` —en un móvil no hay hover, así que
   un tooltip informa igual que el silencio—.

   `flex: 1 0 100%` en las dos anchuras y no bajo una media query: por encima de
   640px los botones son de ancho automático, así que dar el renglón entero al
   aviso no cambia nada mientras quepa, y por debajo es lo único que evita las
   columnas. `margin: 0` porque el hueco lo pone el `gap` del pie. */
.ds-panel__foot > .ds-help { flex: 1 0 100%; margin: 0; }

/* Cabecera de pantalla: título, contexto y la acción primaria a la derecha. */
/* BARRA DE TITULAR (CEO, 2026-10-24): título, una línea de contexto y las
   acciones, en una franja. Se trabaja en portátiles de 15″ y la cabecera se
   llevaba ~90 px antes del primer dato; ver DESIGN.md §6.5. */
.ds-pagehead {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--s-3) var(--s-6);
  padding-bottom: var(--s-4);
  border-bottom: var(--bd);
}

.ds-pagehead__title {
  font-size: var(--fs-lg);
  font-weight: 600;
  line-height: var(--lh-tight);
  letter-spacing: -0.01em;
}

.ds-pagehead__sub {
  margin-top: var(--s-1);
  max-width: 110ch;
  font-size: var(--fs-xs);
  color: var(--n-500);
}

.ds-pagehead__actions {
  margin-left: auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-3);
}

/* ⚠️ EL ARMAZÓN CEDE SU CABECERA A LA PANTALLA (2026-08-17, medido en la app del
   doctor).

   Un armazón de app instalada pinta su propia `ds-pagehead` —el nombre de dónde
   estás, que en un móvil no lo da ninguna barra superior—, y las pantallas ya
   migradas traen la suya dentro del contenido. Cuando coinciden pasan dos cosas,
   las dos medidas: **dos `<h1>` en el documento** —un lector de pantalla anuncia
   dos títulos de página— y **48 px de chrome muerto encima del pliegue**, que a
   390 × 664 es un cuarto de lo que se ve sin hacer scroll y es justo el alto que
   se está peleando pantalla por pantalla.

   Manda la de la PANTALLA, porque es la que dice algo: la del armazón es el
   nombre de la app. Y se resuelve por la ESTRUCTURA y no pantalla por pantalla:
   así vale para la que se añada el año que viene sin que nadie se acuerde de
   esto — que es exactamente cómo aparecieron las dos primeras.

   `display: none` y no `visibility`: hay que quitarlo del árbol de
   accesibilidad, no solo de la vista, o el lector sigue leyendo los dos.

   Acotado a los armazones que declaran barra inferior (`--con-nav-inferior`):
   son los que pintan cabecera propia. El `> ` es imprescindible — la cabecera de
   la pantalla vive dentro de `main`, y sin él se retirarían las dos. */
.ds-app--con-nav-inferior:has(main .ds-pagehead) > .ds-pagehead { display: none; }

/* La cabecera del armazón va FUERA del contenido de la pantalla, así que no
   hereda el margen lateral de ningún arquetipo: el título quedaba pegado al
   borde del teléfono mientras la lista, un palmo más abajo, llevaba sus 16 px
   (medido en «Mis tareas» del Coordinador, 2026-09-25: tres márgenes
   izquierdos distintos en la misma pantalla). El margen es el de `.ds-a__pad`,
   para que título y contenido caigan en la misma vertical. */
.ds-app--con-nav-inferior > .ds-pagehead {
  padding: var(--s-6) var(--s-6) var(--s-5);
}

/* =========================================================================
   2. BOTONES
   Tres pesos y no más: primaria (una por pantalla), normal y fantasma.
   ========================================================================= */

.ds-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-3);
  height: var(--ctl-h);
  padding: 0 var(--s-5);
  border: var(--bd-strong);
  border-radius: var(--r-sm);
  background: var(--n-0);
  color: var(--n-800);
  font-size: var(--fs-sm);
  font-weight: 500;
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease);
}

.ds-btn:hover { background: var(--n-50); }
.ds-btn:active { background: var(--n-100); }

/* ⚠️ UN BOTÓN QUE ES UN `<a>` PIERDE SU COLOR AL PASARLE EL RATÓN, y hay que
   reafirmarlo aquí (2026-08-17). Es el mismo choque que la cabecera de
   `base.css` describe entre el reset y la decisión, solo que con la hoja
   ANTIGUA y por el otro lado:

     - `static/css/base.css` declara `a:hover { color: var(--accent-600) }`,
       que es (0,1,1) —elemento + pseudo-clase—, y `base.html` la carga DESPUÉS
       de este fichero, a propósito, para que el CSS de las apps sin migrar
       siga ganando durante la migración.
     - Las variantes de botón son (0,1,0) y solo declaran su color en reposo.

   Así que (0,1,1) le ganaba a (0,1,0) y **el texto de CUALQUIER botón-enlace
   se volvía acento al pasar por encima**. En el primario eso es índigo sobre
   índigo: el rótulo desaparece justo en el momento de pulsarlo. Lo destapó
   «Empezar sin presupuesto» de Primera Visita, pero no era de esa pantalla —
   le pasaba a los tres pesos de botón en las 30 sub-apps, y al `--danger`, que
   además perdía lo único que dice que es destructivo.

   `.ds-btn:hover` es (0,2,0) y le gana a (0,1,1) sin `!important` y sin tocar
   el orden de carga, que es una decisión deliberada de `base.html`. El
   subrayado viene del mismo sitio (`.ds-app :where(a):hover`) y se quita por
   lo mismo: un botón no se subraya.

   Se retira el día que `css/base.css` deje de cargarse, o sea cuando no quede
   una sola pantalla sin migrar. */
.ds-btn:hover,
.ds-btn:active { color: var(--n-800); text-decoration: none; }

.ds-btn--primary:hover,
.ds-btn--primary:active { color: var(--n-0); }

.ds-btn--ghost:hover,
.ds-btn--ghost:active { color: var(--n-600); }

.ds-btn--danger:hover,
.ds-btn--danger:active { color: var(--risk-ink); }

.ds-btn:disabled:hover,
.ds-btn[aria-disabled="true"]:hover { color: var(--n-400); }

.ds-btn:disabled,
.ds-btn[aria-disabled="true"] {
  color: var(--n-400);
  background: var(--n-50);
  cursor: not-allowed;
}

/* La acción primaria. Es uno de los cinco sitios donde el acento aparece,
   así que como mucho una por pantalla: dos botones de acento es no haber
   decidido cuál es la siguiente acción. */
.ds-btn--primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--n-0);
}

.ds-btn--primary:hover  { background: var(--accent-600); border-color: var(--accent-600); }
.ds-btn--primary:active { background: var(--accent-600); }

.ds-btn--ghost {
  background: transparent;
  border-color: transparent;
  color: var(--n-600);
}

.ds-btn--ghost:hover { background: var(--n-100); }

/* Destructiva. Va en texto rojo sobre fondo neutro, no en rojo macizo: un
   botón rojo lleno compite con los badges de riesgo de la propia tabla. */
.ds-btn--danger {
  color: var(--risk-ink);
  border-color: var(--n-200);
}

.ds-btn--danger:hover {
  background: var(--risk-bg);
  border-color: var(--risk);
}

/* Solo icono. Lleva aria-label obligatorio; sin él es un botón mudo. */
.ds-btn--icon {
  width: var(--ctl-h);
  padding: 0;
}

.ds-btn--lg {
  height: calc(var(--ctl-h) + var(--s-4));
  padding: 0 var(--s-7);
  font-size: var(--fs-base);
}

/* El hermano pequeño de `--lg`, para una celda de acciones: tres botones a la
   altura de control completa dentro de un `<td>` empujan la fila al doble de
   alto y la tabla deja de leerse como una lista.

   ⚠️ Existía escrito en dos sitios y **no estaba definido en ninguno**
   (`coordinador-biometria.js` lo pinta desde JS desde hace meses): la clase
   viajaba en el HTML y no hacía nada. Es el fallo mudo de siempre en versión
   CSS — nadie ve un botón «que no se ha empequeñecido». */
.ds-btn--sm {
  height: calc(var(--ctl-h) - var(--s-4));
  padding: 0 var(--s-4);
  font-size: var(--fs-sm);
}

/* Grupo de botones pegados (vistas, rangos de fecha, densidad). */
.ds-btngroup { display: inline-flex; }

.ds-btngroup .ds-btn { border-radius: 0; }
.ds-btngroup .ds-btn:first-child { border-radius: var(--r-sm) 0 0 var(--r-sm); }
.ds-btngroup .ds-btn:last-child  { border-radius: 0 var(--r-sm) var(--r-sm) 0; }
.ds-btngroup .ds-btn + .ds-btn   { margin-left: -1px; }
.ds-btngroup .ds-btn[aria-pressed="true"] {
  background: var(--n-100);
  color: var(--n-900);
  font-weight: 600;
}

/* =========================================================================
   3. CAMPOS
   La etiqueta es una micro-etiqueta condensada: es la firma del sistema y
   además comprime el formulario, que es lo que necesita una pantalla densa.
   ========================================================================= */

/* La separación entre la etiqueta y su control **no es estética**: el anillo de
   foco se pinta 4px por fuera de la caja (`--focus`, ver base.css §5), así que
   con los 6px que había aquí antes el anillo pasaba a 2px del rótulo y lo
   rozaba. Con 8px queda a 4px, la misma holgura que el sistema deja en
   cualquier otro sitio. Medido el 2026-08-18 en el card del Coordinador. */
.ds-field {
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
}

.ds-field__label {
  font-family: var(--font-cond);
  font-size: var(--fs-micro);
  font-weight: 600;
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
  color: var(--n-500);
}

.ds-field__label--req::after {
  content: " *";
  color: var(--risk-ink);
}

.ds-input,
.ds-select,
.ds-textarea {
  width: 100%;
  min-height: var(--ctl-h);
  padding: var(--s-3) var(--s-4);
  border: var(--bd-strong);
  border-radius: var(--r-sm);
  background: var(--n-0);
  color: var(--n-800);
  font-size: var(--fs-base);
  line-height: var(--lh-base);
}

.ds-textarea { min-height: calc(var(--ctl-h) * 3); }

/* Tres renglones es el mínimo razonable para un texto libre, y de más para un
   campo que casi siempre tiene DOS líneas —los teléfonos de un contacto: un
   móvil y a veces un fijo—. El `rows="2"` del marcado no basta, porque el
   `min-height` de arriba lo pisa. Importa donde el alto es el recurso escaso:
   en el editor de la Libreta (§26) esos 28px son una fila de fichas menos en
   la lista que hay debajo, en cada pantalla y todo el rato. */
.ds-textarea--corta { min-height: calc(var(--ctl-h) * 2); }

/* Un control dentro de una BARRA (`.ds-cluster`, `.ds-pagehead__actions`) y no
   dentro de un formulario en columna. El `width: 100%` de arriba es correcto en
   un `.ds-field` —los campos de un formulario se alinean— y equivocado aquí: la
   barra envuelve, así que la etiqueta, el campo y el botón caen en tres
   renglones y una sola pregunta ocupa el alto de tres. Pasó en la Bandeja de
   Recepción (2026-08-17): el selector de ventana comía el espacio de la lista
   que hay debajo, en una pantalla grande.

   No se arregla quitando el 100 % de `.ds-select`: en un formulario en columna
   haría que cada campo midiese lo que mide su opción más larga, y la columna
   dejaría de estar alineada. Son dos sitios y dos anchos. */
.ds-input--auto,
.ds-select--auto { width: auto; }

.ds-input::placeholder,
.ds-textarea::placeholder { color: var(--n-400); }

.ds-input:disabled,
.ds-select:disabled,
.ds-textarea:disabled {
  background: var(--n-50);
  color: var(--n-500);
}

/* Solo lectura: un campo que no se puede escribir tiene que PARECER que no se
   puede escribir. Sin esto es idéntico a uno editable, así que quien lo tiene
   delante hace clic, teclea y no pasa nada — la pantalla no falla, miente.
   No es `:disabled`: el texto sigue leyéndose entero y se puede seleccionar y
   copiar, que es justo para lo que se usa (un campo que rellena la aplicación).
   De ahí que conserve el color del texto y solo pierda el fondo blanco. */
.ds-input[readonly],
.ds-textarea[readonly] {
  background: var(--n-50);
  cursor: default;
}

/* Importes y cantidades: alineados a la derecha y tabulares también dentro
   del input, o el número salta al pasar de edición a celda. */
.ds-input--num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* La ayuda de un campo es TEXTO, así que tiene medida: en un panel a sangre
   —el del plan de tratamiento mide 1.304px en un portátil— una sola frase se
   estiraba en un renglón de punta a punta de la tarjeta y dejaba de leerse como
   la nota de su campo. El tope no se nota en un formulario en columna, donde la
   columna ya es más estrecha que esto. */
.ds-field__help {
  max-width: 62ch;
  font-size: var(--fs-xs);
  color: var(--n-500);
}

/* Error: borde + texto. El borde rojo solo sería color (DESIGN.md §5). */
.ds-field--error .ds-input,
.ds-field--error .ds-select,
.ds-field--error .ds-textarea { border-color: var(--risk); }

.ds-field__error {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  font-size: var(--fs-xs);
  color: var(--risk-ink);
}

.ds-check {
  display: inline-flex;
  align-items: center;
  gap: var(--s-4);
  font-size: var(--fs-base);
  cursor: pointer;
}

.ds-check input { accent-color: var(--accent); }

/* LA CASILLA SUELTA (2026-08-17).
   `.ds-check` viste la pareja etiqueta + casilla, y eso cubre el marcado que
   escribe una plantilla. No cubre el que escribe **Django**: un
   `forms.CheckboxInput` rinde el `<input>` y nada más, así que la clase va en la
   casilla y no en un envoltorio que no existe. Sin esto, la regla de arriba no
   la alcanza nunca y el `input` sale con el azul por defecto del navegador — el
   mismo azul que DESIGN.md §1 prohíbe por escrito.

   Sube al núcleo porque ya se estaba usando: `apps/common/incidencias/forms.py`
   pone `class="ds-checkbox"` desde su migración y **ninguna hoja la declaraba**.
   Es el modo de fallo que este sistema tiene peor: una clase que nadie declara
   se ve casi bien —la casilla nativa funciona— y no falla nunca, así que nada
   avisa. Al pasar los 270 controles de `contabilidad/forms.py` y
   `stock/forms.py` se encontró el hueco, y arreglarlo ES la tarea: el contrato
   (§2) dice que si falta un componente se lleva al núcleo, no se escribe en la
   app.

   El objetivo táctil sigue el mismo criterio que el resto: 44px con puntero
   grueso, aunque el modo compacto esté activo (§6). Se hace con `outline` y no
   con caja para no mover la línea de base del renglón donde vive la casilla. */
.ds-checkbox {
  accent-color: var(--accent);
  width: var(--s-6);
  height: var(--s-6);
  margin: 0;
  flex: none;
  cursor: pointer;
}

@media (pointer: coarse) {
  .ds-app .ds-checkbox {
    width: var(--s-7);
    height: var(--s-7);
  }
}

/* EL CAMPO QUE ES UNA CASILLA (2026-09-14).
   Un formulario que se pinta en bucle (`{% for campo in form %}`) mete cada
   control en un `.ds-field`, que es una COLUMNA: rótulo arriba, control
   debajo. Con un booleano eso sale mal de dos maneras, y las dos se midieron en
   las seis pantallas de Marketing Central que lo tienen: la casilla se estira
   al ancho de la columna —846 px de caja— y el navegador dibuja el cuadrado
   centrado en ella, a media pantalla y 22 px por debajo de su texto; y el texto
   sale en versalitas como si nombrara un campo que no está.

   El marcado es el de `.ds-check` (la casilla dentro de su etiqueta) y este
   modificador solo arregla lo que la columna rompe: la etiqueta mide lo que
   mide su frase —no el ancho de la columna, que haría clicable media pantalla—
   y la ayuda y el error se sangran hasta donde empieza el texto, para que se
   lean como nota de ESA casilla y no como un párrafo suelto. El sangrado es
   el ancho de la casilla más el hueco de `.ds-check`, también en táctil. */
.ds-field--check { gap: var(--s-2); }

.ds-field--check > .ds-check { align-self: flex-start; }

.ds-field--check > :is(.ds-field__help, .ds-field__error) {
  padding-left: calc(var(--s-6) + var(--s-4));
}

@media (pointer: coarse) {
  .ds-app .ds-field--check > :is(.ds-field__help, .ds-field__error) {
    padding-left: calc(var(--s-7) + var(--s-4));
  }
}

/* Sección de formulario. Un formulario largo no es una lista de campos: son
   grupos con nombre, y sin ellos «Fecha prevista» y «Cubetas devueltas» pesan
   lo mismo. La rejilla reparte los campos en columnas que se adaptan al ancho,
   así que uno de veinte campos no obliga a bajar veinte veces.

   Sube al núcleo porque la primera app migrada ya lo necesitaba y lo resolvió
   con `<h3 style="...">` incrustado en la plantilla (stock/producto_form.html):
   eso es la señal de que faltaba el componente, no de que sobrara la sección. */
.ds-fieldset {
  border: 0;
  margin: 0;
  padding: 0;
  min-width: 0;
}

.ds-fieldset + .ds-fieldset { margin-top: var(--s-9); }

.ds-fieldset__legend {
  display: block;
  width: 100%;
  padding: 0 0 var(--s-4);
  margin-bottom: var(--s-6);
  border-bottom: var(--bd-strong);
  font-family: var(--font-cond);
  font-size: var(--fs-micro);
  font-weight: 600;
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
  color: var(--n-500);
}

.ds-formrow {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--s-6);
}

.ds-formrow + .ds-formrow { margin-top: var(--s-6); }

/* Campo que ocupa la fila entera: notas, direcciones, cualquier texto largo. */
.ds-formrow__full { grid-column: 1 / -1; }

/* UNA FILA DE PANELES NO ES UNA FILA DE CAMPOS. El `minmax(220px, …)` de
   arriba está medido para un `<input>` con su etiqueta encima. Cuando lo que
   se reparte son PANELES enteros —con su nota explicativa dentro— la misma
   rejilla mete cuatro columnas de 290px en una columna de 1.240 y la nota se
   lee en tiras de tres palabras: medido en `/configuracion/datalytics/` el
   2026-08-27, donde «¿Qué es ETL Hub?» ocupaba trece renglones.

   Un panel pide ancho de LECTURA, no de campo: dos por fila como mucho, y uno
   solo en cuanto la pantalla no da para los dos. 28rem es el mínimo con el que
   dos caben en la columna ancha de Configuración (1.240px) y ninguno baja del
   ancho al que su texto sigue siendo un párrafo. */
.ds-formrow:has(> .ds-panel) {
  grid-template-columns: repeat(auto-fit, minmax(28rem, 1fr));
}

/* Pie de formulario. La acción primaria a la izquierda —donde termina de leer
   quien acaba de rellenar— y la destructiva separada por el empujón, para que
   «Eliminar» no quede pegado a «Guardar». */
.ds-formfoot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-5);
  margin-top: var(--s-9);
  padding-top: var(--s-6);
  border-top: var(--bd-strong);
}

/* =========================================================================
   4. BADGE DE ESTADO
   SIEMPRE texto; icono en los críticos. Un badge que solo es color no lo
   lee ni un daltónico ni una impresión en blanco y negro, y esto es
   software clínico (DESIGN.md §5). El icono va con aria-hidden: lo que se
   lee en voz alta es el texto.
   ========================================================================= */

.ds-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-1) var(--s-4);
  border-radius: var(--r-pill);
  background: var(--idle-bg);
  color: var(--idle-ink);
  font-size: var(--fs-xs);
  font-weight: 600;
  line-height: var(--lh-tight);
  white-space: nowrap;
}

.ds-badge__i { font-size: var(--fs-micro); }

.ds-badge--ok   { background: var(--ok-bg);   color: var(--ok-ink); }
.ds-badge--warn { background: var(--warn-bg); color: var(--warn-ink); }
.ds-badge--risk { background: var(--risk-bg); color: var(--risk-ink); }
.ds-badge--info { background: var(--info-bg); color: var(--info-ink); }
.ds-badge--idle { background: var(--idle-bg); color: var(--idle-ink); }

/* Punto de estado para listas muy densas, donde el badge no cabe. Lleva
   siempre el texto al lado; el punto es refuerzo, no sustituto. */
.ds-dot {
  display: inline-block;
  width: var(--s-4);
  height: var(--s-4);
  border-radius: var(--r-pill);
  background: currentColor;
  flex: none;
}

/* =========================================================================
   5. TABLA
   El componente más usado de Lydent. La referencia real del repositorio es
   la columna numérica de Contabilidad (.importe-cell / .tes-num), no la de
   Analytics: es la única familia que ya alinea a la derecha, no envuelve y
   usa numeración tabular.
   ========================================================================= */

.ds-table {
  width: 100%;
  font-size: var(--cell-fs);
}

/* Cabecera: micro-etiqueta condensada sobre regla de un píxel. La firma. */
.ds-table thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  padding: var(--s-3) var(--cell-pad-x);
  background: var(--n-0);
  border-bottom: var(--bd-strong);
  font-family: var(--font-cond);
  font-size: var(--fs-micro);
  font-weight: 600;
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
  color: var(--n-500);
  text-align: left;
  white-space: nowrap;
}

.ds-table tbody td {
  height: var(--row-h);
  padding: var(--cell-pad-y) var(--cell-pad-x);
  border-bottom: var(--bd);
  vertical-align: middle;
}

.ds-table tbody tr:hover td { background: var(--n-50); }

/* Fila seleccionada: uno de los cinco sitios del acento. */
.ds-table tbody tr[aria-selected="true"] td { background: var(--accent-bg); }

/* LA FILA DE LA QUE SE SALIÓ, al volver del detalle o de la edición
   (`ds_url_ficha … ancla=`, `nucleo.js`). El querystring devuelve la lista con
   sus filtros y su página; esto devuelve el RENGLÓN, que en una tabla de 40
   filas es la mitad de «donde estaba».

   Se apaga sola a los pocos segundos y no es un estado: si se quedara fija se
   confundiría con la selección de arriba, que sí lo es y significa otra cosa.
   El destello no comunica nada por sí mismo —solo dice «mira aquí»—, así que
   no incumple la regla de no informar solo con color. */
.ds-table tbody tr:target td {
  animation: ds-fila-vuelta 2.4s var(--ease) 1;
}

@keyframes ds-fila-vuelta {
  0%, 55% { background: var(--accent-bg); }
  100% { background: transparent; }
}

@media (prefers-reduced-motion: reduce) {
  .ds-table tbody tr:target td { animation: none; background: var(--accent-bg); }
}

/* Zebra SOLO a partir de 8 columnas (DESIGN.md §4). Por debajo, la regla de
   un píxel ya guía el ojo y el zebreado solo mete ruido. */
.ds-table--zebra tbody tr:nth-child(even) td { background: var(--n-25); }
.ds-table--zebra tbody tr:hover td { background: var(--n-50); }

/* Columna numérica. Derecha, sin envolver, tabular, mismos decimales en toda
   la columna y el símbolo de moneda SOLO en la cabecera. */
.ds-table th.ds-num,
.ds-table td.ds-num {
  text-align: right;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* Signo y color. El signo «−» es lo que comunica; el color solo refuerza. */
/* SOLO PARA MAGNITUDES CON SIGNO, y esto es una regla, no un consejo. El «-»
   —o el «+» que la plantilla escribe delante— es el segundo canal, y el color
   solo lo subraya. Usarlas por UMBRAL («win-rate por debajo de 50, en rojo»)
   deja el color como único canal, que es lo que §5 prohíbe: medido, --ok y
   --risk se separan solo ΔE 7,7 en deuteranopia, así que ese rojo y ese verde
   son el mismo color para bastante gente. Si lo que se quiere marcar es un
   umbral, es un badge con su palabra, no una cifra teñida.
   (Analytics, 2026-08-06: se retiraron 15 usos por umbral introducidos en su
   propia migración.) */
.ds-table td.ds-num--neg { color: var(--risk-ink); }
.ds-table td.ds-num--pos { color: var(--ok-ink); }

/* El cero no es un dato: se apaga para que las cifras reales destaquen. */
.ds-table .ds-num--zero { color: var(--n-300); }

/* Columna de totales y fila de resultado. */
.ds-table .ds-col--total {
  border-left: var(--bd-strong);
  font-weight: 600;
  background: var(--n-25);
}

.ds-table tfoot td,
.ds-table tr.ds-row--total td {
  height: var(--row-h);
  padding: var(--cell-pad-y) var(--cell-pad-x);
  border-top: var(--bd-strong);
  background: var(--n-50);
  font-weight: 600;
}

/* Primera columna fija en tablas anchas. Se activa a mano con --fija: no
   todas las tablas anchas la quieren, y fijarla sin querer parte el scroll. */
.ds-table--fija tbody th:first-child,
.ds-table--fija tbody td:first-child {
  position: sticky;
  left: 0;
  z-index: 1;
  background: var(--n-0);
}

.ds-table--fija tbody tr:hover td:first-child { background: var(--n-50); }

.ds-table--fija thead th:first-child {
  position: sticky;
  left: 0;
  z-index: 3;
}

/* Orden: el indicador se ve SIEMPRE en la columna ordenada, no solo al pasar
   el ratón — en tableta no hay ratón (DESIGN.md §4 y §6). */
.ds-table th[aria-sort] { cursor: pointer; }
.ds-table th[aria-sort]:hover { color: var(--n-700); }

/* ⚠️ EL TRIÁNGULO NO ERA UN SITIO DONDE SE PUDIERA PULSAR, Y ESO ES LO QUE EL
   CEO REPORTÓ COMO «LOS TRIÁNGULOS NO FUNCIONAN» (2026-08-21).

   El `<th>` declaraba `cursor: pointer` —o sea, prometía que la celda entera
   obedece— pero el único elemento navegable era el `<a>` del rótulo, y el
   indicador lo pintaba un `::after` **del `<th>`**: fuera del enlace por
   construcción. En las columnas de ancho justo era peor todavía, porque ahí el
   `margin-right` negativo lo empujaba al relleno de la celda, separado del
   texto: se veía una flecha, se pulsaba la flecha y no pasaba nada.

   Se arregla moviendo las dos cosas —el relleno de la celda y el indicador— al
   propio enlace: el `<a>` pasa a ser un bloque que ocupa el `<th>` entero
   (márgenes negativos que se comen el relleno de la celda y lo vuelven a poner
   como suyo), así que cualquier píxel de la cabecera, triángulos incluidos, es
   el enlace. No hace falta tocar `sort_th`: se selecciona por
   `th[aria-sort] > a`, y así las 108 columnas del portal lo heredan de golpe. */
/* El relleno se lo queda el ENLACE, no la celda: es la única forma de que el
   enlace llegue hasta el borde. Y `height: 100%` es la otra mitad — sin él el
   enlace mide lo que mide SU rótulo, y en una fila donde otra cabecera ocupa
   tres renglones («EN JUEGO €» partido) las cortas dejaban una franja muerta
   debajo del texto: pulsabas dentro de la celda, con la mano de «esto se
   pulsa» puesta, y no pasaba nada. La franja era el defecto original en
   pequeño. */
.ds-table thead th[aria-sort]:has(> a) {
  padding: 0;
  /* `height: 1px` es un MÍNIMO, no una altura: la celda sigue creciendo con su
     contenido. Está aquí para que sea una altura DECLARADA, que es lo que hace
     que el `height: 100%` del enlace resuelva contra la celda en vez de caer a
     `auto`. Sin ella el enlace vuelve a medir lo que mide su rótulo. */
  height: 1px;
}

.ds-table thead th[aria-sort] > a {
  position: relative;
  display: flex;
  align-items: center;
  height: 100%;
  /* Por la derecha, 14px de más: es el hueco del indicador (8 de triángulo +
     6 de aire). Reservarlo en el RELLENO y pintar el par en `absolute` es lo
     que impide que el indicador cuente como una palabra más del rótulo y se
     vaya él solo a un tercer renglón en una columna estrecha. */
  padding: var(--s-3) calc(var(--cell-pad-x) + 14px) var(--s-3) var(--cell-pad-x);
  color: inherit;
  text-decoration: none;
}

/* ⚠️ SON DOS TRIÁNGULOS, NO UNO — petición literal del CEO el mismo día: «dos
   triángulos, uno sobre otro (un rombo con espacio en medio), así puedes
   ordenar como quieras».

   Con un solo triángulo la cabecera enseñaba el orden VIGENTE y callaba el
   otro sentido, así que para saber si «Espera (máx.)» se podía poner al revés
   había que pulsarla y ver qué pasaba. Con el par, los dos sentidos están
   dibujados desde el primer pintado y el color dice cuál manda: gris los dos
   mientras la columna no ordena, y el que ordena en acento.

   Es la misma razón por la que la rama `none` ya estaba vestida —una columna
   ordenable tiene que distinguirse de una que no lo es sin pasar el ratón por
   encima—, llevada hasta el final. */
.ds-table thead th[aria-sort] > a::before,
.ds-table thead th[aria-sort] > a::after {
  content: "";
  position: absolute;
  top: 50%;
  width: 0;
  height: 0;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
}

/* El hueco del medio son los 4px que separan el `-7` del `+2`: sin él los dos
   triángulos se tocan y el par se lee como un rombo macizo, que es otra cosa. */
.ds-table thead th[aria-sort] > a::before {
  margin-top: -7px;
  border-bottom: 5px solid var(--n-300);
}

.ds-table thead th[aria-sort] > a::after {
  margin-top: 2px;
  border-top: 5px solid var(--n-300);
}

/* De gris a ACENTO en cuanto la columna ordena, y solo el sentido vigente.
   El `aria-sort` ya lo decía para el lector de pantalla; esto lo dice para el
   ojo. No es color como único canal: la posición del triángulo teñido —arriba
   o abajo— es el segundo. */
.ds-table thead th[aria-sort="ascending"]  > a::before { border-bottom-color: var(--accent); }
.ds-table thead th[aria-sort="descending"] > a::after  { border-top-color: var(--accent); }

/* Columna de texto: el par va DETRÁS del rótulo, en el hueco que le reserva el
   relleno de aquí arriba. */
.ds-table thead th[aria-sort] > a::before,
.ds-table thead th[aria-sort] > a::after {
  right: var(--cell-pad-x);
}

/* ⚠️ COLUMNA NUMÉRICA: EL PAR VA DELANTE, y esa es la otra mitad de lo que el
   CEO pidió («justifica a la derecha el texto Espera (máx.)»).

   `.ds-num` alinea el rótulo a la derecha para que caiga justo encima de sus
   cifras. Reservando el hueco del indicador por la derecha, ese rótulo quedaba
   14px por dentro y **la cabecera dejaba de caer sobre la columna que nombra**
   —el mismo defecto que `.ds-cell--acciones` documenta unas líneas más abajo—.
   Poniendo el par a la izquierda, el borde derecho del rótulo y el de los
   números vuelven a ser el mismo. */
.ds-table thead th.ds-num[aria-sort] > a {
  /* `justify-content` y no `text-align`: el rótulo es un item de flex y al
     `text-align` heredado de `.ds-num` no le llega mientras el item mida lo
     que mide su contenido. Los dos hacen falta —este coloca la CAJA, aquel
     alinea los renglones cuando el rótulo se parte en dos. */
  justify-content: flex-end;
  padding-right: var(--cell-pad-x);
  padding-left: calc(var(--cell-pad-x) + 14px);
}

.ds-table thead th.ds-num[aria-sort] > a::before,
.ds-table thead th.ds-num[aria-sort] > a::after {
  right: auto;
  left: var(--cell-pad-x);
}

/* ⚠️ LA CABECERA QUE NO TIENE ENLACE DENTRO — y no es un caso raro: es el
   `initTableSorting()` de `comercial.js`, que ordena EN CLIENTE sobre
   `table.sortable-table` y pone el `aria-sort` en un `<th data-sort>` pelado,
   con el manejador de clic en el propio `<th>`. Ahí el enlace no existe y no
   debe existir: el orden no cambia de URL.

   Así que ese caso pinta el par sobre el `<th>`, que es donde se pulsa.
   `:not(:has(> a))` es lo que separa a los dos sin marcar nada en la plantilla:
   una clase que hubiera que acordarse de poner dejaría veinte tablas sin
   indicador, que es de donde venimos. */
/* ⚠️ `sticky` Y NO `relative`, aunque aquí solo haga falta ser un bloque
   contenedor para el par de triángulos de abajo. `relative` lo era, y de paso
   PISABA el `position: sticky` con el que nace `.ds-table thead th` —este
   selector pesa más—: o sea que la fila de títulos dejaba de clavarse
   exactamente en las tablas que se pueden ordenar, que son las largas. `sticky`
   contiene a los absolutos igual de bien y conserva el `top: 0` y el `z-index`
   que la cabecera ya declara. Encontrado el 2026-09-01 con el CEO mirando
   Stock → Bajo mínimo: 60 filas y ningún título a partir de la séptima. */
.ds-table th[aria-sort]:not(:has(> a)) {
  position: sticky;
  padding-right: calc(var(--cell-pad-x) + 14px);
}

.ds-table th[aria-sort]:not(:has(> a))::before,
.ds-table th[aria-sort]:not(:has(> a))::after {
  content: "";
  position: absolute;
  top: 50%;
  right: var(--cell-pad-x);
  width: 0;
  height: 0;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
}

.ds-table th[aria-sort]:not(:has(> a))::before {
  margin-top: -7px;
  border-bottom: 5px solid var(--n-300);
}

.ds-table th[aria-sort]:not(:has(> a))::after {
  margin-top: 2px;
  border-top: 5px solid var(--n-300);
}

.ds-table th[aria-sort="ascending"]:not(:has(> a))::before { border-bottom-color: var(--accent); }
.ds-table th[aria-sort="descending"]:not(:has(> a))::after { border-top-color: var(--accent); }

/* Celda de acciones por fila: se reserva el ancho siempre, para que la fila
   no dé un salto lateral al aparecer los botones.

   Vale también para el `<th>`, y no es cosmética: sin él el rótulo de la
   columna se queda a la izquierda mientras sus botones están a la derecha, y
   una cabecera que no cae encima de lo que nombra se lee como desorden aunque
   los píxeles estén exactamente donde tienen que estar. Salió midiendo el
   «no sale ordenado» del buzón de imagen (2026-08-18): las cuatro columnas
   alineaban al píxel y aun así la tabla parecía descuadrada. */
.ds-table :is(th, td).ds-cell--acciones {
  width: 1%;
  white-space: nowrap;
  text-align: right;
}

/* Y los botones de esa celda NO ENVUELVEN. Lo destapó el Club al mirar sus
   listas en pantalla: con dos o tres acciones por fila metidas en un
   `.ds-cluster` —que es `flex-wrap: wrap`— el `width: 1%` de arriba las
   apilaba en vertical y cada fila pasaba a ocupar tres renglones; la tabla
   dejaba de ser una tabla. Envolver es lo correcto en una barra de acciones de
   pantalla, no dentro de una celda que ya se dimensiona por su contenido. */
.ds-table td.ds-cell--acciones .ds-cluster {
  flex-wrap: nowrap;
  /* Y SE PEGAN AL BORDE DERECHO. La celda ya declaraba `text-align: right`
     arriba, pero eso no coloca nada: dentro hay un `.ds-cluster`, que es un
     contenedor flex, y a un flex lo alinea `justify-content` — el `text-align`
     del padre no le llega. Así que la declaración de intención estaba escrita
     y no se cumplía.

     Qué se veía, y lo reportó el CEO mirando la bandeja de recepción
     (2026-08-18): la columna de acciones mide lo que mide la fila MÁS ancha
     —`width: 1%` la encoge al contenido, pero una columna de tabla es una sola
     anchura para todas—, así que la fila que además lleva el icono de
     conversación fija el ancho de la columna, y las que no lo llevan dejaban su
     botón azul **alineado a la izquierda**, medio botón por delante de los
     demás. En una lista de siete filas donde el icono va y viene, la acción
     primaria bailaba de sitio en cada renglón.

     Se alinea por la DERECHA y no por la izquierda porque la acción primaria es
     el ancla: es la que se busca, la que se pulsa y la única que está en todas
     las filas. Alinear por la izquierda dejaría cuadrado lo opcional y bailando
     lo importante. Es además lo que ya hace el mismo componente en móvil
     (`justify-content: flex-end`, §5.a), así que esto no estrena criterio:
     lo iguala. */
  justify-content: flex-end;
}

/* Cuando la celda de acciones lleva un formulario con campo —«canjear» pide el
   teléfono, «reenviar» el número nuevo—, ese campo NO cabe: la fila se sale de
   la pantalla y la acción principal queda fuera, que es el mismo defecto que
   §5 del arquetipo A ya corrigió en la barra de selección. Un campo dentro de
   la celda de acciones es la señal de que eso pide un modal. */

/* LA COLUMNA DE ANCHO PREDECIBLE MIDE SU CONTENIDO, NO LA SOBRA DE LA TABLA.
   Es la misma mecánica que `.ds-cell--acciones` de aquí arriba (`width: 1%` es
   «encógete a lo tuyo» en el reparto automático), pero para datos: una fecha,
   un importe, un contador de días.

   Por qué hace falta, y es medible (cola comercial, 2026-08-21, avisado por el
   CEO): en el reparto automático una columna nunca baja de su **contenido
   mínimo**, y en estas columnas lo más ancho no es el dato — es el RÓTULO, que
   `thead th` pinta en versalitas y `white-space: nowrap`. «Último movimiento»
   reservaba 152 px para pintar «16/07/2026», y «Espera (máx.)» 118 px para tres
   dígitos: 371 px de una tabla de 1.248 —el 30 %— para tres columnas cuyo dato
   más largo mide un tercio de eso. Lo que sobra se lo quitan las columnas que
   sí varían, y con ellas estranguladas la fila se parte en cuatro renglones:
   «Pendiente de aceptación» salía a palabra por línea. Medido a 1.280 px, con
   control negativo: las tres columnas pasan de 371 px a 262 —incluido el hueco
   que se le reserva a la flecha de orden, ver abajo—, la de «por qué» de 310 a
   405 y la fila alta de la lista de 109 px a 51.

   Por eso el rótulo SÍ puede partirse en dos renglones y el dato no: la
   cabecera es una línea más una sola vez, y la fila se repite cuarenta veces.
   Ese es todo el intercambio.

   ⚠️ **NO ES OPT-IN, Y ESA FUE LA CORRECCIÓN DEL CEO EL MISMO DÍA.** El defecto
   no es de la cola: lo tiene toda tabla con una columna de ancho fijo y rótulo
   largo, y son 354 plantillas. Una clase que hay que acordarse de poner arregla
   la pantalla que se está mirando y deja rotas las otras 353 — que es
   exactamente cómo se llegó aquí.

   Así que la regla la hereda `.ds-num`, que YA marca en todo el portal
   —2.654 celdas— lo mismo que esta clase declara: derecha, tabular, sin
   envolver, o sea **ancho predecible**. `ds-col--justa` se queda para lo que el
   sistema no puede saber: la fecha, el código, el «hace N días» — columnas
   igual de predecibles que no son un número.

   Lo que la automática NO hace es romper la tabla que va sobrada: `width: 1%`
   nunca baja del contenido mínimo, así que una columna numérica no se estrecha
   por debajo de su cifra más larga, y si TODAS las columnas son numéricas el
   reparto vuelve a ser el de antes porque no queda ninguna a la que dar la
   sobra. */
.ds-table :is(th, td):is(.ds-num, .ds-col--justa) { width: 1%; }

/* `.ds-num` ya lo trae de su propia regla; esto es para las demás. */
.ds-table :is(th, td).ds-col--justa { white-space: nowrap; }

.ds-table thead th:is(.ds-num, .ds-col--justa) { white-space: normal; }

/* EL INDICADOR DE ORDEN NO ES UNA PALABRA MÁS DEL RÓTULO. En cuanto la
   cabecera puede partirse, un triángulo que fuera contenido de la línea sería
   lo último de ella y en una columna estrecha no cabría: se iría él solo a un
   tercer renglón, debajo de un rótulo de dos. Un indicador de orden separado
   de la columna que ordena no se lee como su indicador.

   Por eso el par de triángulos de §5 va en `position: absolute` dentro del
   enlace, con su hueco reservado en el relleno: no cuenta para el salto de
   línea y queda centrado en la celda, que es donde el ojo lo busca cuando la
   fila de cabeceras mezcla rótulos de una y de dos líneas.

   Aquí ya no queda nada que declarar: el relleno de esas columnas lo pone la
   regla del enlace, y por eso el `padding-right` que vivía en este sitio se
   fue con ella. */

/* ------------------------------------------------------------------------
   5.a MÓVIL — LA TABLA SE VUELVE TARJETAS
   DESIGN.md §6 lo prohíbe expresamente: nunca scroll horizontal de tabla
   completa. En móvil se ven 3–4 columnas y el resto se despliega.

   Requisito de marcado: cada <td> lleva `data-label` con el texto de su
   cabecera, y las columnas que no entran en el resumen llevan `ds-col--sec`.

   Vivía en `archetypes/a-tabla.css` §5 y se mudó aquí el 2026-08-06 al migrar
   Analytics. El motivo es de fondo: esto es comportamiento del COMPONENTE
   tabla, no del armazón de una app de listas, y mientras vivió en el
   arquetipo A cualquier app que no fuese de tablas —pero que tuviera
   tablas— se quedaba sin él. Analytics tiene 84, y se abre en el móvil del
   gabinete tanto como en el PC del despacho.
   ------------------------------------------------------------------------ */

@media (max-width: 640px) {
  /* La tabla deja de ser tabla. Hace falta bajar `display:block` también a
     <table>, <tbody> y <tfoot>: con las filas en bloque pero la tabla
     todavía en `display:table`, las filas se encogen al ancho de su
     contenido y las tarjetas salen a media columna. */
  .ds-table--responsive,
  .ds-table--responsive tbody,
  .ds-table--responsive tfoot {
    display: block;
    width: 100%;
  }

  .ds-table--responsive thead { display: none; }

  .ds-table--responsive tbody tr {
    display: block;
    width: 100%;
    padding: var(--s-5) var(--s-6);
    border-bottom: var(--bd-strong);
  }

  .ds-table--responsive tbody td {
    display: flex;
    align-items: baseline;
    gap: var(--s-5);
    width: 100%;
    height: auto;
    padding: var(--s-2) 0;
    border: 0;
  }

  .ds-table--responsive tbody td::before {
    content: attr(data-label);
    flex: none;
    width: 38%;
    font-family: var(--font-cond);
    font-size: var(--fs-micro);
    font-weight: 600;
    letter-spacing: var(--track-micro);
    text-transform: uppercase;
    color: var(--n-500);
  }

  /* Casilla y acciones no son datos: no llevan rótulo y no gastan la
     columna de la etiqueta. La casilla encabeza la tarjeta y las acciones
     la cierran por la derecha. */
  .ds-table--responsive tbody td[data-label=""]::before { content: none; }

  /* El `width: 1%` que en escritorio impide que la columna de acciones robe
     ancho a los datos hay que deshacerlo aquí, o el botón se queda pegado a
     la izquierda de una celda de un píxel. */
  .ds-table--responsive tbody td.ds-cell--acciones {
    width: 100%;
    justify-content: flex-end;
    padding-top: var(--s-4);
  }

  /* Y por lo mismo el de las columnas de ancho predecible: en tarjeta no hay
     columnas que repartir —cada celda es un renglón con su rótulo delante—,
     así que un `width: 1%` aquí dejaría el dato aplastado contra el borde
     izquierdo. Medido con control negativo: la fecha en 4 px. */
  .ds-table--responsive tbody td:is(.ds-num, .ds-col--justa) { width: 100%; }

  /* La fila seleccionada se tiñe entera, no celda a celda. */
  .ds-table--responsive tbody tr[aria-selected="true"] { background: var(--accent-bg); }
  .ds-table--responsive tbody tr[aria-selected="true"] td { background: none; }
  .ds-table--responsive tbody tr:hover td { background: none; }

  .ds-table--responsive tfoot tr { display: block; padding: var(--s-5) var(--s-6); }
  .ds-table--responsive tfoot td { display: flex; justify-content: space-between; }

  /* Columnas secundarias: solo cuando la fila está desplegada. */
  .ds-table--responsive tbody tr:not([data-expanded="true"]) .ds-col--sec {
    display: none;
  }

  /* Y que se note que hay algo debajo. La regla de arriba llevaba desde
     2026-08-06 escondiendo columnas SIN que nada las pudiera revelar —el
     atributo no lo ponía nadie— y sin ninguna señal de que existieran: la
     tabla se veía correcta, solo que le faltaban datos. El disparador está en
     `nucleo.js` y es opt-in con `data-ds-expandible`; esto es su rótulo. */
  /* `:has()` porque una pantalla declara la tabla entera pero no todas sus
     filas esconden algo: sin esto, una fila sin columnas secundarias saldría
     con el cursor y la flecha de «pulsa aquí» y no pasaría nada al pulsar.
     Un rótulo que promete lo que no hay es peor que no ponerlo. */
  .ds-table--responsive[data-ds-expandible] tbody tr:has(.ds-col--sec) {
    cursor: pointer;
  }

  .ds-table--responsive[data-ds-expandible] tbody tr:has(.ds-col--sec)
    td:first-child::after {
    content: "▾";
    margin-left: var(--s-3);
    color: var(--n-400);
  }

  .ds-table--responsive[data-ds-expandible]
    tbody tr[data-expanded="true"]:has(.ds-col--sec) td:first-child::after {
    content: "▴";
  }

  /* El número deja de ir a la derecha: en tarjeta compite con su rótulo. */
  .ds-table--responsive td.ds-num { text-align: left; }
}

/* Una tabla de lectura analítica no siempre puede volverse tarjeta: la de
   detalle mensual son 12 filas × N áreas y en tarjetas ocuparía doce
   pantallas. Ahí la salida es la del propio DESIGN.md §6 —enseñar 3–4
   columnas— con las demás marcadas como secundarias, que es lo que hace
   `.ds-col--sec` fuera del modo tarjeta. Se declara aparte y no dentro del
   bloque de arriba porque aplica a tablas que NO son `--responsive`. */
@media (max-width: 640px) {
  .ds-table:not(.ds-table--responsive) .ds-col--sec { display: none; }
}

/* ------------------------------------------------------------------------
   5.a-quinquies TABLETA — LA COLUMNA QUE SOLO CABE EN EL ESCRITORIO (2026-10-14)

   §6 decía qué pasa en el móvil (tarjetas) y en el escritorio (todo), pero la
   tableta en vertical —800 px, el tamaño de un Galaxy Tab en el gabinete— no
   tenía regla: la tabla se pintaba entera, como en el escritorio, y lo que no
   cabía se iba por la derecha. Medido en Stock ese día: **6 listas** se salían
   del ancho (Productos 1.066 px de 800, Entradas y salidas 955, Movimientos
   888, Proveedores 864 con los botones de la fila fuera de la pantalla), y las
   que cabían lo hacían partiendo el nombre del producto en cinco o seis
   renglones porque las columnas de detalle se llevaban el ancho.

   Dos piezas, y se usan juntas:

     `ds-col--escritorio`  en el `<th>` y el `<td>` de una columna que en la
                           tableta sobra. Solo se esconde entre 641 y 1024 px:
                           en el móvil la tarjeta la enseña como un renglón más
                           (o la manda al desplegable si además es
                           `ds-col--sec`), y en el escritorio está siempre.
     `ds-sub--tableta`     un renglón gris DENTRO de la celda que identifica la
                           fila, que repite lo que se ha escondido («3M ·
                           REF-1010»). Solo existe en ese mismo tramo: en el
                           escritorio duplicaría su columna, y en el móvil la
                           tarjeta ya la enseña.

   La pregunta para elegir es la de §6.3: no «¿es importante?» —lo son todas—
   sino «¿hace falta para decidir sobre ESTA fila sin entrar en ella?».
   ------------------------------------------------------------------------ */

.ds-sub--tableta { display: none; }

@media (min-width: 641px) and (max-width: 1024px) {
  .ds-table .ds-col--escritorio { display: none; }

  /* Una tabla editable lleva un campo en casi cada celda, y a 800 px el
     relleno lateral de diez celdas son ~250 px que no lleva nadie: el campo
     «Lote» de Entradas y salidas se quedaba en 40 px útiles. */
  .ds-table--editable :is(th, td) { padding-inline: var(--s-4); }

  .ds-sub--tableta {
    display: block;
    margin-top: var(--s-1);
    font-size: var(--fs-xs);
    font-weight: 400;
    color: var(--n-500);
  }
}

/* ------------------------------------------------------------------------
   5.a-bis MÓVIL — LA TARJETA COMPACTA  (`ds-table--compacta`)

   La tarjeta de §5.a gasta UN RENGLÓN POR COLUMNA: con las 3–4 columnas que
   §6 permite, más la celda de acciones, una fila son cuatro o cinco renglones
   y un dedo de alto. Medido el 2026-08-17 en la bandeja del doctor del
   Coordinador (arnés `files/coordinador_mis_tareas/medir_mis_tareas.py`, 390 ×
   664 px, `pointer: coarse`, armazón real): **239 px por tarea y UNA tarea
   visible sin scroll** en la pantalla que es el `start_url` de la app
   instalada. DESIGN.md §1 pide justo lo contrario —Linear, densidad sin
   ruido, «cada píxel se gana ayudando a decidir o a actuar»—.

   Esta variante reparte la tarjeta en DOS LÍNEAS en vez de N:

     línea 1   la identidad de la fila (`ds-cell--titulo`) + la acción
     línea 2   el resto de las columnas visibles, EN LÍNEA

   Con eso la misma fila mide **82 px y caben 5** —cuatro cuando la cola es
   larga y se gana su línea de orden—, sin quitar ni un dato de la pantalla: lo
   que sale de la tarjeta va a `ds-col--sec`, o sea al desplegable que la fila ya
   tenía.

   Es un modificador de `--responsive`, no un sustituto: se declara junto a
   ella y solo cambia el estado PLEGADO. Al desplegar (`data-expanded="true"`)
   la fila vuelve a la tarjeta con rótulos de §5.a, que es donde se leen los
   detalles y donde el rótulo sí hace falta.

   CONTRATO DE MARCADO, y son tres cosas:

     1. `ds-cell--titulo` en la celda que identifica la fila. Su sitio en el
        marcado da igual —el reparto lo hace `order`— salvo si la tabla es
        `data-ds-expandible`: ahí va PRIMERA, porque el «▾» que avisa de que hay
        algo debajo lo pinta §5.a en `td:first-child` y tiene que caer junto al
        nombre.
     2. El rótulo de las demás celdas va EN LÍNEA (ya no es una columna del
        38 %), así que lo que se entiende solo se marca `data-label=""` y se
        ahorra el rótulo. Es el mecanismo que §5.a ya usa para la casilla y las
        acciones: aquí no cambia, solo se usa más. Y cuando el dato NO se
        entiende solo pero el rótulo tampoco cabe —«2 semanas»—, el rótulo se
        pone `ds-sr-only` dentro de la celda: va absoluto, así que el lector de
        pantalla lo oye y la tarjeta no paga un píxel.
     3. Lo que no quepa en dos líneas, `ds-col--sec`. Si hay que elegir, la
        pregunta es «¿esto hace falta para decidir si entro?» y no «¿es un dato
        importante?»: lo segundo lo son todos.

   ⚠️ SOLO SE COMPACTA LA FILA QUE TIENE TÍTULO (`:has(> .ds-cell--titulo)`), y
   por eso el selector lo repite en todas las reglas. En un `tbody` conviven
   filas que no son tarjetas —la de detalle que rellena un AJAX, la de «no hay
   resultados»—, y convertirlas en una línea de datos las rompe. Que la fila
   declare que es una tarjeta es más barato que ir tapando los casos.
   ------------------------------------------------------------------------ */

@media (max-width: 640px) {
  /* La separación entre datos es `--s-4` y no `--s-5` porque la segunda línea va
     JUSTA: a 390 px mide **326 px** y el caso normal de la bandeja del doctor
     pide 293 (tipo «Revisión de estudio» 122 + espera 68 + estado «En proceso»
     92, más las dos separaciones). Con 12 px sobraban 17 y con 8 sobran 33, que
     es lo que hace falta para que un rótulo un poco más largo no se lleve el
     estado a un tercer renglón. */
  .ds-table--compacta tbody tr:not([data-expanded="true"]):has(> .ds-cell--titulo) {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    column-gap: var(--s-4);
    padding: var(--s-4) var(--s-6);
  }

  /* La celda deja de ser un renglón: mide lo que mide su dato. */
  .ds-table--compacta tbody tr:not([data-expanded="true"]):has(> .ds-cell--titulo) > td {
    width: auto;
    padding: 0;
  }

  /* Y su rótulo deja de reservar el 38 % del ancho: va delante del dato, en la
     misma línea. Sigue siendo la micro-etiqueta condensada de la firma
     (DESIGN.md §1); lo único que cambia es que no monta columna. */
  .ds-table--compacta tbody tr:not([data-expanded="true"]):has(> .ds-cell--titulo) > td::before {
    width: auto;
  }

  /* LÍNEA 1 — LA IDENTIDAD. Se lleva el ancho que le deje la acción y nunca
     lleva rótulo: un nombre no necesita que le pongan «NOMBRE» delante.
     ⚠️ `flex-basis: 0` y no `auto`, y no es un detalle: en una línea que
     ENVUELVE, un ítem se baja de renglón cuando no cabe **por su tamaño base**,
     y solo después se reparte lo que falta. Con `auto` el nombre entero era el
     tamaño base, así que el botón de la acción se iba a un renglón propio —«
     Revisar plan» medido a 390 px: la fila pasaba de 78 px a 126—. Con base 0 el
     nombre pide cero, la acción cabe siempre y el nombre se queda todo lo demás.
     Es la misma trampa que `.ds-select[data-orden]` en la hoja del Coordinador. */
  .ds-table--compacta tbody tr:not([data-expanded="true"]):has(> .ds-cell--titulo) > td.ds-cell--titulo {
    flex: 1 1 0;
    min-width: 0;
    font-weight: 600;
  }

  .ds-table--compacta tbody
    tr:not([data-expanded="true"]):has(> .ds-cell--titulo) > td.ds-cell--titulo::before {
    content: none;
  }

  /* Lo que se pulsa en ella MIDE 44 (§6), y no cuesta un píxel de fila: la
     línea ya la fija la acción, que también mide 44. En la bandeja del doctor
     eran 24 enlaces de paciente a 42 px —un defecto anterior que se arregla al
     compactar, porque es el mismo renglón—. `min-height` y no `padding`: el
     valor sale de `--ctl-h`, que en táctil es 44 y en escritorio 28. */
  .ds-table--compacta tbody
    tr:not([data-expanded="true"]):has(> .ds-cell--titulo) > td.ds-cell--titulo a {
    display: inline-flex;
    align-items: center;
    gap: var(--s-3);
    min-height: var(--ctl-h);
  }

  /* La acción cierra la línea 1, con su rótulo puesto: no se convierte en un
     botón de icono, que es lo que obligaría a hacer si estuviera fuera del
     flujo. */
  .ds-table--compacta tbody tr:not([data-expanded="true"]):has(> .ds-cell--titulo) > td.ds-cell--acciones {
    width: auto;
    padding: 0;
  }

  /* EL CORTE DE LÍNEA ES EXPLÍCITO, y por eso hay un pseudo-elemento en medio.
     Un `flex-wrap` parte la línea donde deja de caber, o sea que la línea 2
     empezaría en un sitio distinto por fila según lo largo que sea el nombre.
     Un ítem de base 100 % y alto 0 entre los dos grupos —el título y la acción
     con `order` 0, las demás celdas con 2— parte SIEMPRE en el mismo sitio y
     no pinta nada. */
  .ds-table--compacta tbody tr:not([data-expanded="true"]):has(> .ds-cell--titulo)::after {
    content: "";
    order: 1;
    flex-basis: 100%;
    height: 0;
  }

  /* LÍNEA 2 — lo que hace falta para decidir, en línea y en orden de marcado. */
  .ds-table--compacta tbody tr:not([data-expanded="true"]):has(> .ds-cell--titulo)
    > td:not(.ds-cell--titulo):not(.ds-cell--acciones) {
    order: 2;
  }
}

/* ------------------------------------------------------------------------
   5.a-quater MÓVIL — LA FILA DE BANDEJA  (`ds-table--bandeja`)

   La cuarta variante, para las colas de TRABAJO: listas donde cada fila es
   algo que abrir, y abrirla es lo único que se hace desde la lista. Es el
   patrón de una bandeja de correo, y nació en «Mis tareas» del Coordinador
   (CEO, 2026-09-25, con la app instalada delante): *«la veo muy hecha
   trozos»*. Lo medido en aquella pantalla con la tarjeta compacta:

     · **Diez botones de acento llenos** en la misma vista —§2 pide uno como
       mucho—, y eran lo que partía la pantalla en bloques.
     · **Tres destinos en la línea 1**: el nombre abría Vevi y sacaba de la
       app, el hueco desplegaba la fila y el botón abría la tarea. Lo natural
       es tocar el nombre, y era justo lo que llevaba fuera.
     · **Filas de dos y de tres renglones** mezcladas, porque la línea 2
       pasaba por debajo del botón y envolvía cuando el tipo era largo.

   LA FORMA, y es siempre la misma:

     línea 1   la identidad (`ds-cell--titulo`) ········· el dato (`ds-cell--meta`)
     línea 2   el resto de celdas visibles, en línea ····· ›

   · **Toda la fila abre**: el primer hijo de `ds-cell--acciones` (el enlace
     o el botón de la acción principal) se estira sobre la fila con un
     `::after` absoluto, y se pinta como un «›». No hay JavaScript: el toque
     cae en el propio `<a>`/`<button>`, así que el `href`, el `click` de la app
     y el anclaje de vuelta siguen siendo los de siempre.
   · **Lo demás de la celda de acciones queda por ENCIMA** del estirado
     (`z-index`), así que una segunda acción —«Descartar»— se sigue pudiendo
     pulsar sin abrir la fila.
   · **Lo que se pulsa dentro de las celdas de datos, NO**: queda debajo del
     estirado. Es a propósito —era el enlace a Vevi en el nombre— y por eso
     su icono se esconde: un icono que promete un destino que no se alcanza
     miente. Ese enlace vive en la ficha que se abre.
   · **No despliega.** `ds-col--sec` se esconde y ya; lo que haya en esas
     celdas es el detalle de la ficha, a un toque. La tabla no debe llevar
     `data-ds-expandible` —el «▾» prometería un desplegable que el estirado
     tapa—, y aun así esta regla lo apaga por si alguien lo deja.
   · Las celdas de la línea 2 llevan su rótulo EN LÍNEA (el `data-label` en
     micro), como en §5.a-bis; lo que se entiende solo, `data-label=""`.

   ⚠️ Mismo contrato que la compacta: SOLO se transforma la fila con título
   (`:has(> .ds-cell--titulo)`). Un separador de grupo, una fila de detalle o
   la de «sin resultados» se quedan como estaban.

   Por encima de 640 px no existe: el escritorio sigue siendo la tabla.
   ------------------------------------------------------------------------ */

@media (max-width: 640px) {
  .ds-table--bandeja tbody tr:has(> .ds-cell--titulo) {
    position: relative;
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    column-gap: var(--s-4);
    row-gap: var(--s-2);
    padding: var(--s-5) var(--s-6);
  }

  .ds-table--bandeja tbody tr:has(> .ds-cell--titulo):active { background: var(--n-50); }

  /* El foco de teclado es del enlace estirado, que no se ve: se pinta en la
     fila entera, que es lo que abre. */
  .ds-table--bandeja tbody tr:has(> .ds-cell--acciones > :first-child:focus-visible) {
    outline: 2px solid var(--accent);
    outline-offset: -2px;
  }

  /* Cada celda es un bloque de la línea: su rótulo va delante, en línea, y
     si trae un segundo renglón propio (una nota `ds-help`) cae debajo de ella
     y no a su lado. */
  .ds-table--bandeja tbody tr:has(> .ds-cell--titulo) > td {
    order: 2;
    display: block;
    width: auto;
    padding: 0;
    font-size: var(--fs-sm);
    color: var(--n-600);
  }

  .ds-table--bandeja tbody tr:has(> .ds-cell--titulo) > td::before { margin-right: var(--s-3); }

  .ds-table--bandeja tbody tr:has(> .ds-cell--titulo) > td.ds-col--sec { display: none; }

  /* Sin «▾»: aquí no se despliega. */
  .ds-table--bandeja tbody tr:has(> .ds-cell--titulo) > td:first-child::after { content: none; }

  /* LÍNEA 1 — la identidad. `flex-basis: 0` por lo mismo que en §5.a-bis: el
     nombre pide cero y se queda con lo que deje el dato de la derecha.
     `order: -1` para que vaya delante del dato aunque en el marcado vaya
     detrás: hay tablas cuyo orden de celdas es contrato con su JavaScript
     (las necesidades de implante, con la fecha primera). */
  .ds-table--bandeja tbody tr:has(> .ds-cell--titulo) > td.ds-cell--titulo {
    order: -1;
    flex: 1 1 0;
    min-width: 0;
    font-size: var(--fs-base);
    font-weight: 600;
    color: var(--n-900);
  }

  .ds-table--bandeja tbody tr:has(> .ds-cell--titulo) > td.ds-cell--titulo::before,
  .ds-table--bandeja tbody tr:has(> .ds-cell--titulo) > td.ds-cell--meta::before {
    content: none;
  }

  .ds-table--bandeja tbody tr:has(> .ds-cell--titulo) > td.ds-cell--titulo a { color: inherit; }
  .ds-table--bandeja tbody tr:has(> .ds-cell--titulo) > td.ds-cell--titulo a > i { display: none; }

  /* …y a la derecha, el dato con el que se decide si entrar: la espera, la
     fecha. Como la hora de un correo. */
  .ds-table--bandeja tbody tr:has(> .ds-cell--titulo) > td.ds-cell--meta {
    order: 0;
    flex: none;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
  }

  /* El corte entre las dos líneas es explícito (§5.a-bis). */
  .ds-table--bandeja tbody tr:has(> .ds-cell--titulo)::after {
    content: "";
    order: 1;
    flex-basis: 100%;
    height: 0;
  }

  /* LÍNEA 2, al final: las acciones. */
  .ds-table--bandeja tbody tr:has(> .ds-cell--titulo) > td.ds-cell--acciones {
    order: 3;
    display: flex;
    align-items: center;
    gap: var(--s-4);
    margin-left: auto;
    padding: 0;
  }

  .ds-table--bandeja tbody tr:has(> .ds-cell--titulo) > td.ds-cell--acciones > * {
    position: relative;
    z-index: 1;
  }

  /* Las demás acciones, SOLO ICONO (44 × 44). Con su rótulo, «Descartar» en la
     línea 2 la envolvía a un tercer renglón (fila de 122 px medida en «Por
     revisar»). El texto no se quita del marcado: `font-size: 0` lo oculta a la
     vista y lo deja como nombre accesible del botón.

     Y el margen negativo en vertical es para que el objetivo táctil de 44 px
     (§6) no fije el alto de la línea: el botón se come el relleno de la fila
     en vez de estirarla. Sin él, la línea 2 medía 44 y la fila 98 px, por
     encima del tope de las demás. */
  .ds-table--bandeja tbody tr:has(> .ds-cell--titulo) > td.ds-cell--acciones > :not(:first-child) {
    width: var(--ctl-h);
    margin-block: calc((var(--s-6) - var(--ctl-h)) / 2);
    padding: 0;
    font-size: 0;
    color: var(--n-400);
  }

  .ds-table--bandeja tbody tr:has(> .ds-cell--titulo) > td.ds-cell--acciones > :not(:first-child) > i {
    font-size: var(--fs-base);
  }

  /* La acción principal: invisible como botón, estirada sobre la fila y
     pintada como «›». `position: static` para que su `::after` tome como
     caja la fila (que es la posicionada), no el propio botón. */
  /* `order: 1`: el «›» cierra SIEMPRE la fila, en la misma columna en todas
     las colas, aunque en el marcado vaya delante de las demás acciones. */
  .ds-table--bandeja tbody tr:has(> .ds-cell--titulo) > td.ds-cell--acciones > :first-child {
    order: 1;
    position: static;
    width: auto;
    height: auto;
    min-height: 0;
    padding: 0 var(--s-2);
    border: 0;
    background: none;
    color: var(--n-400);
    font-size: 0;
  }

  .ds-table--bandeja tbody tr:has(> .ds-cell--titulo) > td.ds-cell--acciones > :first-child > * {
    display: none;
  }

  .ds-table--bandeja tbody tr:has(> .ds-cell--titulo) > td.ds-cell--acciones > :first-child::before {
    content: "";
    display: block;
    width: var(--s-4);
    height: var(--s-4);
    border: solid currentColor;
    border-width: 0 2px 2px 0;
    transform: rotate(-45deg);
  }

  .ds-table--bandeja tbody tr:has(> .ds-cell--titulo) > td.ds-cell--acciones > :first-child::after {
    content: "";
    position: absolute;
    inset: 0;
  }
}

/* ------------------------------------------------------------------------
   5.a-ter MÓVIL — LA TARJETA QUE SE RELLENA  (`ds-table--editable`)

   La tercera variante, y existe porque las dos anteriores dan por supuesto que
   la tarjeta se LEE. Cuando la fila se ESCRIBE —una rejilla de `ds-cellinput`,
   que es lo que Stock estrenó y hoy usan también las líneas de «Pedir
   presupuesto» del Coordinador— la tarjeta de §5.a se comporta al revés de lo
   que hace falta:

     · El rótulo se lleva el **38 %** del ancho, así que el campo se queda con
       lo que sobra… y encima `.ds-cellinput` tiene `max-width: 5rem`, pensado
       para una COLUMNA de escritorio. Medido a 390 px en el modal del
       Coordinador (arnés `files/coordinador_tarea_detalle/medir_modal.py
       --lineas=5`): **campos de 80 px** en una tarjeta de 342.
     · Cada celda es un renglón, y un renglón con un control dentro mide 44 px
       (§6, objetivo táctil). Seis celdas más la de acciones = **368 px por
       línea** y **1.903 px las cinco**: media pantalla por tratamiento.

   Lo que cambia, y son tres cosas:

     1. **El campo se lleva el ancho que sobra** — se levanta el `max-width` de
        la celda de escritorio y el rótulo pasa a columna fija y estrecha.
     2. **La acción sube al primer renglón**, con el mismo mecanismo del corte
        explícito de §5.a-bis (`order` + un pseudo-elemento de base 100 %). Un
        renglón entero para una ✕ es el gasto más fácil de quitar.
     3. **`ds-col--mitad`**: dos celdas cortas y seguidas comparten renglón. Con
        el rótulo EN LÍNEA —no en columna— el campo sale más ancho que antes
        aunque la celda mida la mitad, así que esto no es un recorte: es un
        renglón menos y un campo mayor a la vez.

   Medido en el mismo sitio, a 390 × 664 con 5 líneas: la línea pasa de **368 a
   230 px**, las cinco de **1.903 a 1.171**, el campo de **80 a 119–238** y los
   nombres partidos en dos renglones de **5 de 5 a 1 de 5** (el que queda tiene
   51 caracteres: ese SÍ tiene que envolver). El campo sigue midiendo 44 px de
   alto, que es el objetivo táctil de §6 — la densidad no sale de encoger lo que
   se pulsa.

   NO toca el escritorio: todo va dentro de la `@media` de tarjeta. Ahí la fila
   sigue siendo una rejilla, que es lo correcto cuando hay ancho. Comprobado a
   1440 × 900: la línea se queda en 37 px y los campos suben de 58–75 a 63–80.

   ⚠️ Es **opt-in**: se declara junto a `--responsive` y ninguna de las ~230
   pantallas que ya la usan cambia. Lo vigila
   `coordinador/tests/test_modal_solicitud_compacto.py`, que falla si algún
   selector de este bloque deja de empezar por `.ds-table--editable` o si alguna
   regla se sale de la `@media`.

   ⚠️ 2026-08-19 — LOS CAMPOS EMPIEZAN TODOS EN LA MISMA VERTICAL, Y ESA
   VERTICAL LA MARCA EL RÓTULO MÁS LARGO.

   El CEO, con la ventana abierta en su móvil: *«el textbox de uds se alinearía
   a la izquierda con el de dientes y nota, en la posición que marque la palabra
   DIENTES como tope por la izquierda»*. Lo que tenía delante eran **tres
   arranques distintos** en la misma tarjeta: «Uds» a 58 px porque su rótulo va
   en línea y es corto, «Dientes» y «Nota» a 124 porque el suyo va en columna.
   Un borde izquierdo en escalera en un formulario de cinco renglones.

   El mecanismo es **una rejilla de cuatro columnas en la FILA**, no un ancho
   por celda:

     `max-content | minmax(0,1fr) | max-content | minmax(0,1fr)`

   La primera columna es el carril de los rótulos, y `max-content` la hace medir
   **el más ancho de los que caen dentro**. Quién cae dentro lo decide el
   marcado: las celdas con campo (`ds-col--campo`) se subrejillan
   (`grid-template-columns: subgrid`), así que su rótulo entra en esa columna y
   la sostiene; las de los renglones 1 y 2 ocupan la fila entera y **no**
   contribuyen —un ítem que cruza una pista flexible no cuenta para el tamaño
   intrínseco de las que cruza (CSS Grid §12.5)—, que es exactamente por lo que
   «TRATAMIENTO», más largo que «DIENTES», no mueve la línea.

   Por eso no hay ni un píxel medido a ojo: si mañana un rótulo se alarga, se
   traduce o se añade un campo, la columna se recalcula sola y los cuatro campos
   siguen alineados. El rótulo de una celda ya no manda sobre su propio campo;
   manda el carril.

   Las dos columnas flexibles son iguales entre sí, y de ahí sale lo otro que
   pidió el CEO: los dos campos del renglón 3 —«Uds» y «Dto %», media fila cada
   uno (`ds-col--mitad`)— **se reparten uniformes** lo que queda desde la línea
   hasta el borde derecho, con su rótulo en medio. Los campos de renglón entero
   se estiran hasta el borde (`grid-column: 2 / -1`), o sea que **crecen hacia
   la izquierda** hasta tocar la línea: se ensanchan, no se estrechan.

   La rejilla sustituye al `flex-wrap`, y con ella se va el pseudo-elemento que
   cortaba la línea: cada celda declara **cuántas columnas ocupa**, así que el
   renglón en el que cae no depende de dónde deje de caber.
   ------------------------------------------------------------------------ */

@media (max-width: 640px) {
  /* CUATRO COLUMNAS: el carril de los rótulos, el campo, el rótulo de en medio
     y el segundo campo. Las dos flexibles con `minmax(0, …)` a propósito: un
     `<input>` tiene ancho intrínseco propio (~177 px), y con el `auto` que trae
     `1fr` por defecto ese ancho sería el mínimo de la pista y la tarjeta
     desbordaría a lo ancho. */
  .ds-table--editable tbody tr {
    display: grid;
    grid-template-columns: max-content minmax(0, 1fr) max-content minmax(0, 1fr);
    align-items: center;
    column-gap: var(--s-5);
    padding: var(--s-4) var(--s-5);
  }

  /* Un dato por renglón; la separación entre renglones baja a `--s-1` porque
     el alto de la celda ya lo fija el control que lleva dentro. */
  .ds-table--editable tbody td {
    grid-column: span 4;
    order: 2;
    min-width: 0;
    gap: var(--s-4);
    padding: var(--s-1) 0;
  }

  /* El rótulo deja de ser el 38 % y pasa a una columna fija: 38 % de 342 px son
     130 y ningún rótulo de esta tabla los necesita. Es exactamente el ancho que
     se le devuelve al campo. Vale para los renglones que solo se LEEN —código y
     tratamiento—; el de los que se escriben lo fija la rejilla, más abajo. */
  .ds-table--editable tbody td::before { width: 5.5rem; }

  /* RENGLÓN 1 — la identidad de la fila y la acción que la quita, con `order: 0`
     para que la ✕ suba desde el final del marcado. Ya no hace falta cortar la
     línea con un pseudo-elemento: la identidad ocupa tres columnas y la acción
     la cuarta, así que el renglón se cierra por sí solo. */
  .ds-table--editable tbody td:first-child {
    grid-column: span 3;
    order: 0;
  }

  .ds-table--editable tbody td.ds-cell--acciones {
    grid-column: span 1;
    width: auto;
    order: 0;
    padding: 0;
  }

  /* Una celda con `colspan` (el detalle desplegado de la fila, «no hay
     productos») no es la identidad de la tarjeta aunque sea `:first-child`:
     ocupa la fila entera. Sin esto salía a tres cuartos de ancho. */
  .ds-table--editable tbody td[colspan] { grid-column: 1 / -1; }

  /* RENGLONES 3, 4 y 5 — LA LÍNEA ÚNICA. La celda se subrejilla en vez de
     maquetar por su cuenta: hereda las columnas de la fila, así que su rótulo
     cae en el carril común y su campo empieza donde empiezan todos. Es la
     ÚNICA declaración que fija la línea, y no dice ningún píxel. */
  .ds-table--editable tbody td.ds-col--campo {
    display: grid;
    grid-template-columns: subgrid;
    align-items: center;
  }

  /* El rótulo mide lo que mide su palabra; quien iguala es la columna. */
  .ds-table--editable tbody td.ds-col--campo::before { width: auto; }

  /* Dos celdas cortas y seguidas, un solo renglón: media rejilla cada una
     —rótulo y campo—, así que los dos campos salen del mismo tamaño. */
  .ds-table--editable tbody td.ds-col--mitad { grid-column: span 2; }

  /* El campo de un renglón entero llega hasta el borde derecho: crece hacia la
     izquierda hasta la línea, no al revés. */
  .ds-table--editable tbody td.ds-col--campo:not(.ds-col--mitad) .ds-cellinput {
    grid-column: 2 / -1;
  }

  /* EL CAMPO SE LLEVA LO QUE QUEDE. `max-width: 5rem` es de la celda de
     escritorio (§5.b), donde la columna manda; aquí manda la tarjeta. */
  .ds-table--editable tbody .ds-cellinput { max-width: none; }

  /* Un número se sigue leyendo por la derecha; un texto libre, no. En columna
     estrecha daba igual —todo cabía justo—, pero en un campo de 240 px un
     «1.6, 2.6» pegado al borde derecho se lee como si estuviera mal puesto. */
  .ds-table--editable tbody .ds-cellinput:not([type="number"]) {
    text-align: left;
  }

  /* LA CEBRA SE PINTA EN LA FILA, NO EN LA CELDA. `--zebra` tiñe los `td`, y
     eso vale mientras la celda ocupa el renglón entero; aquí la fila es un
     flex con hueco entre columnas y relleno propio, así que teñir las celdas
     dejaba la tarjeta a medio pintar —franjas blancas entre «Uds» y «Dto %» y
     alrededor del borde—. Es el mismo movimiento que §5.a ya hace con la fila
     seleccionada. */
  .ds-table--editable.ds-table--zebra tbody tr:nth-child(even) {
    background: var(--n-25);
  }

  .ds-table--editable.ds-table--zebra tbody tr:nth-child(even) td {
    background: none;
  }
}

/* =========================================================================
   5.b CELDA EDITABLE
   Descubierto en el piloto de Stock: su tabla de productos no es una lista,
   es una rejilla que se edita en el sitio (stock mínimo, uds. de pedido…),
   y eso no lo cubría ni `.ds-input` ni la celda normal. Lo reinventan también
   Gestión Taller y Contabilidad con su propio CSS en línea.

   Estrecho, tabular y alineado a la derecha como el resto de la columna
   numérica: si al entrar en edición el número se mueve o cambia de forma, la
   columna deja de escanearse.
   ========================================================================= */

.ds-cellinput {
  width: 100%;
  max-width: 5rem;
  height: calc(var(--row-h) - var(--s-5));
  padding: 0 var(--s-4);
  border: var(--bd-strong);
  border-radius: var(--r-sm);
  background: var(--n-0);
  font-size: var(--cell-fs);
  font-variant-numeric: tabular-nums;
  text-align: right;
}

.ds-cellinput:disabled {
  background: var(--n-50);
  color: var(--n-500);
}

/* ⚠️ UN NÚMERO QUE NO SE VE NO ES UN DATO.
   `max-width: 5rem` era el ancho PENSADO, pero con `width: 100%` el que manda
   es la celda, y la celda la encoge su rótulo («Uds. almac.»). Medido en
   /stock/productos/ el 2026-09-07, viewport 1920: los campos salían de 40 a
   49 px —nunca los 80 del máximo— y de ahí se van 16 px de padding y otros
   ~16 del stepper nativo, así que quedaban 8-20 px de texto. **14 de 200
   campos recortaban su propio valor**: un `100` se leía `10` y un `10` se leía
   `1`. Y el fallo es mudo en el peor sitio posible, porque la cifra recortada
   sigue siendo una cifra creíble.
   El suelo es el ancho que el componente ya declaraba. Medido después: 0
   recortados, todos a 80 px, y la tabla mide lo mismo (1.625 px) porque lo
   que crecen estas columnas lo ceden las flexibles. Fuera de una tabla no
   aplica: el salto de página del paginador declara su propio ancho. */
.ds-table td .ds-cellinput[type="number"] { min-width: 5rem; }

/* Confirmación de guardado. Es un acuse transitorio de una acción que el
   usuario acaba de hacer, no un estado de la fila, así que aquí el color sí
   basta: nadie lo lee sin haber pulsado antes. El error NO es transitorio y
   por eso lleva texto al lado (`.ds-field__error`). */
.ds-cellinput--saved { border-color: var(--ok); background: var(--ok-bg); }
.ds-cellinput--error { border-color: var(--risk); background: var(--risk-bg); }

/* Y el tercer estado, que faltaba: EDITADO Y TODAVÍA SIN GUARDAR. En una
   rejilla que guarda al salir del campo, es la diferencia entre «ya está» y
   «esto se pierde si te vas». Lo tenía Stock resuelto con un `#eab308` escrito
   a mano dentro del JavaScript —el amarillo del sistema anterior, que el tema
   no alcanza—; es un estado del componente, así que su sitio es este. */
.ds-cellinput--pendiente { border-color: var(--warn); background: var(--warn-bg); }

/* LA CELDA QUE DECIDE ALGO: un control y el botón que lo confirma, en la misma
   línea y sin excepciones.

   No es un `.ds-cluster`. El cluster ENVUELVE —`flex-wrap: wrap`, que es lo
   correcto en una barra de acciones de pantalla— y `.ds-input`/`.ds-select`
   miden el 100 %, así que dentro de una celda el control se come el renglón
   entero y el botón cae debajo: medido en el buzón de imagen del Coordinador
   el 2026-08-18, **9 de 9 filas** con el ✓ en el renglón de abajo y la fila a
   77px en vez de 37. El CEO lo describió como «el botón salta».

   Rejilla y no flex a propósito: `minmax(0, 1fr)` deja que el control se
   ENCOJA por debajo de su tamaño intrínseco, que es lo que hace que un
   desplegable con nombres largos no reviente la columna ni saque la tabla a
   scroll horizontal. Un flex con `width: 100%` no se encoge: desborda. */
/* ⚠️ VA EN EL HIJO DE LA CELDA, NUNCA EN EL `<td>`. Un `display: grid` (o
   `flex`) sobre un `<td>` lo saca de la maquetación de tabla: el navegador le
   da una celda anónima, la fila deja de medir lo que mide su contenido y el
   formulario se PINTA ENCIMA de las filas de abajo. Medido con Chrome el
   2026-09-06 en `/stock/proveedor/<pk>/productos-asociados/`, que lo llevaba en
   un `<td colspan="8">`: fila de **33px** para **60px** de contenido, o sea
   **31px** de la fila siguiente tapados por el campo de referencia; el CEO lo
   mandó como «error de css» con la captura. La forma correcta es la de
   `configuracion/_rows/carpeta_imagen.html`: `<td>` limpio y la clase en el
   `<form>`/`<div>` de dentro. Lo vigila
   `apps/common/core/tests/test_celda_no_es_el_layout.py`. */
.ds-cellform {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--s-3);
}

/* =========================================================================
   5.c PESTAÑAS DE SECCIÓN
   Otro hallazgo del piloto, y de los caros: la navegación por secciones está
   reinventada al menos seis veces con nombres distintos —`stock-nav`,
   `config-tabs__tab--active`, `coord-tab--active`, `caja-mes-tab--active`,
   `acceso-tab--active`, `mock-modal__tab--active`—. Dos niveles, porque
   Stock usa dos y no es el único.

   El ítem activo es uno de los cinco sitios donde el acento puede aparecer
   (DESIGN.md §5: «navegación, barra e ítem activo»).
   ========================================================================= */

/* ⚠️ 2026-08-17 — ESTE NIVEL TAMBIÉN ENVUELVE, y por el mismo motivo que el
   nivel 2 (ver el bloque de `.ds-tabs--sub` más abajo, que lo razona entero).

   Era un carril horizontal —`overflow-x: auto` con los ítems a `nowrap`—
   escrito sobre un supuesto que está a diez líneas de aquí: «arriba hay cuatro
   grupos y caben siempre». Medido: de las **22 barras de primer nivel del
   repositorio, 15 pasan de cuatro ítems**, y dos tienen quince. El supuesto era
   falso, y su fallo es mudo — con las barras de scroll superpuestas (el default
   de macOS y de buena parte de Windows 11) no hay NINGUNA señal de que haya
   más: la fila termina cortada y se lee como el final de la lista.

   Lo destapó Recepción, que no es la barra de una app sino la de un PUESTO y
   tiene ocho secciones de tres apps distintas. La última es «Conversaciones»,
   además con `ds-push`, o sea la primera en caerse por el borde: la pantalla
   donde recepción pasa el día quedaba fuera de su propia navegación.

   Envolver no esconde ni un ítem en ninguna resolución y baja sola a una fila
   cuando la barra es corta. `overflow-x: visible` porque envolver hace que no
   haya desbordamiento que gestionar, y dejar el `auto` reintroduciría el carril
   en cuanto un ítem fuera más ancho que la caja. */
.ds-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  padding: 0 var(--s-6);
  border-bottom: var(--bd-strong);
  background: var(--n-0);
  overflow-x: visible;
}

.ds-tabs__item {
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-5) var(--s-5);
  border-bottom: var(--bd-w-signal) solid transparent;
  color: var(--n-600);
  font-size: var(--fs-base);
  font-weight: 500;
  white-space: nowrap;
  text-decoration: none;
}

.ds-tabs__item:hover { color: var(--n-900); text-decoration: none; }

/* La pestaña también puede ser un `<button>` cuando no navega: los pasos de
   un asistente que cambian de sección sin recargar (`pagos/alta_asistente`).
   Sin esto sale con el marco y el fondo del botón del navegador. */
button.ds-tabs__item {
  background: none;
  border-top: 0; border-left: 0; border-right: 0;
  border-radius: 0;
  font-family: inherit;
  cursor: pointer;
}

.ds-tabs__item[aria-current] {
  color: var(--accent);
  border-bottom-color: var(--accent);
  font-weight: 600;
}

/* Segundo nivel: mismo vocabulario, menos peso. Va sobre fondo escalonado
   para que se lea como dependiente del primero y no como su competencia.

   ⚠️ ESTE NIVEL ENVUELVE, EL PRIMERO NO. Y es la diferencia entre los dos:
   arriba hay cuatro grupos y caben siempre; aquí hay tantos ítems como
   módulos tenga el grupo, y Aplicaciones tiene DIECIOCHO.

   Heredaba el `overflow-x: auto` del nivel 1, o sea que esos dieciocho vivían
   en un carril horizontal que en la mayoría de resoluciones no cabía. Tres
   cosas fallaban a la vez, y ninguna daba error:

   1. Con las barras de scroll superpuestas —el default de macOS y de buena
      parte de Windows 11— NO HAY NINGUNA SEÑAL de que haya más. La fila
      termina cortada a media palabra y se lee como el final de la lista.
   2. El módulo activo puede quedar fuera de la vista al cargar, así que la
      navegación no dice dónde estás, que es su otro trabajo.
   3. Con ratón, un carril horizontal no se desplaza: la rueda mueve la
      página. Hay que arrastrar la barra que además no se ve.

   La intención escrita en `includes/nav_modulos.html` era «preferimos que se
   vea que hay más a que haya que adivinarlo», y envolver la cumple mejor que
   el scroller: no esconde ni un ítem en ninguna resolución, y baja sola a una
   fila cuando el grupo es corto. `overflow-x: visible` porque envolver hace
   que no haya desbordamiento que gestionar, y dejar el `auto` heredado
   reintroduciría el carril en cuanto un ítem fuera más ancho que la caja. */
.ds-tabs--sub {
  flex-wrap: wrap;
  overflow-x: visible;
  gap: var(--s-1);
  row-gap: var(--s-2);
  padding: var(--s-3) var(--s-6);
  background: var(--n-50);
  border-bottom: var(--bd);
}

.ds-tabs--sub .ds-tabs__item {
  padding: var(--s-3) var(--s-4);
  border-bottom: 0;
  border-radius: var(--r-sm);
  font-size: var(--fs-sm);
}

.ds-tabs--sub .ds-tabs__item:hover { background: var(--n-100); }

.ds-tabs--sub .ds-tabs__item[aria-current] {
  background: var(--accent-bg);
  color: var(--accent-600);
}

/* TERCER NIVEL: LAS SECCIONES DE ESTA PANTALLA (2026-09-16).

   `--secciones` es `--sub` contestando OTRA pregunta. El nivel 2 dice **en qué
   app estás** y navega a otra pantalla; este dice **qué parte de esta pantalla
   estás mirando** y no sale de ella.

   Existe porque hasta hoy las dos se disputaban la misma fila: una pantalla de
   Configuración con secciones propias sustituía la lista de módulos por las
   suyas, y quien entraba a Comercial perdía el mapa para saltar a otra app
   (queja del CEO). Ahora conviven, y entonces hace falta que NO se parezcan:
   dos filas seguidas de píldoras grises son el «tres niveles no se leen» que
   se quería evitar.

   La diferencia se lleva al fondo y al ítem activo, no al tamaño: fondo blanco
   —la fila ya está dentro del lienzo de la pantalla— y activo subrayado en vez
   de relleno, que es el vocabulario del nivel 1 en pequeño. El escalón gris del
   nivel 2 queda arriba, y el ojo lee «grupo › app › sección» de arriba abajo.

   Hereda de `--sub` todo lo demás (envolver incluido): se escribe con las dos
   clases. */
.ds-tabs--secciones {
  background: var(--n-0);
  padding-top: var(--s-2);
  padding-bottom: 0;
}

.ds-tabs--secciones .ds-tabs__item {
  border-radius: 0;
  border-bottom: var(--bd-w-signal) solid transparent;
  padding-bottom: var(--s-2);
}

.ds-tabs--secciones .ds-tabs__item:hover {
  background: transparent;
  color: var(--n-900);
}

.ds-tabs--secciones .ds-tabs__item[aria-current],
.ds-tabs--secciones .ds-tabs__item.active {
  background: transparent;
  color: var(--accent);
  border-bottom-color: var(--accent);
  font-weight: 600;
}

/* LAS COLAS DE UNA BANDEJA CABEN EN UNA FILA, TAMBIÉN EN UN TELÉFONO
   (2026-08-17, con la bandeja del doctor del Coordinador).

   `--colas` no es «otra barra»: es `--sub` cuando sus ítems son las COLAS de una
   misma pantalla —destinos fijos, contados y conocidos de antemano—, y no los
   módulos de un grupo. Esa diferencia es la que permite lo contrario de lo que
   hace `--sub`: aquí **no se envuelve**. Envolver es correcto cuando no se sabe
   cuántos ítems hay (Aplicaciones tiene dieciocho); con cuatro fijos, envolver
   es gastar tres renglones del teléfono en la fila que menos información lleva
   de la pantalla.

   Medido a 390 × 664 px en el armazón del doctor: la barra de colas ocupaba
   **153 px de 664 en tres renglones** y la primera fila de datos empezaba en
   y=449 — dos tercios de lo que se ve sin scroll gastados en cromo.

   Y NO es un carril horizontal, a propósito. `flex: 1 1 auto` + `min-width: 0`
   reparte el ancho entre los ítems y los deja encogerse, así que **no hay
   desbordamiento que gestionar**: ni barra de scroll invisible que esconda un
   destino (§5.c ya lo pagó dos veces), ni una región con `overflow` que se queda
   el gesto del dedo cuando no desborda —eso costó que la pantalla del doctor no
   se pudiera scrollear (`apps/coordinador.css`)—. Lo único que se recorta es el
   ROTULO, y con puntos suspensivos, que sí se ve.

   El icono se retira porque es decoración (`aria-hidden`) y es lo que más ancho
   come por destino; el CONTADOR se queda, porque es el dato: dice si hay algo
   que hacer. */
@media (max-width: 640px) {
  .ds-tabs--colas {
    flex-wrap: nowrap;
    gap: var(--s-1);
    padding-left: var(--s-3);
    padding-right: var(--s-3);
  }

  .ds-tabs--colas .ds-tabs__item {
    flex: 1 1 auto;
    min-width: 0;
    justify-content: center;
    gap: var(--s-2);
    padding-left: var(--s-2);
    padding-right: var(--s-2);
    font-size: var(--fs-xs);
  }

  .ds-tabs--colas .ds-tabs__item > i { display: none; }

  /* El rótulo es lo único elástico de la fila. `min-width: 0` en el ítem no
     basta: sin esto el texto —`white-space: nowrap` heredado del ítem— se sale
     por encima del destino de al lado en vez de recortarse.
     Va scopeado a `--colas` a propósito: fuera de esta variante los ítems no se
     encogen, así que recortar el rótulo solo podría esconder texto sin motivo. */
  .ds-tabs--colas .ds-tabs__rotulo {
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .ds-tabs--colas .ds-badge {
    flex: none;
    padding-left: var(--s-3);
    padding-right: var(--s-3);
  }
}

/* =========================================================================
   5.d BUSCADOR TRANSVERSAL
   Sale del análisis de navegación de Stock: hay 17 cajas de búsqueda, cada
   una atada a su propia lista, y ninguna que cruce las entidades. Ése es el
   motivo real por el que se acaban teniendo cuatro pestañas del navegador
   abiertas — el usuario está supliendo a mano una vista que no existe.

   Vive en la cabecera de la app, no dentro de una lista, porque su trabajo es
   justamente saltarse la lista. Comercial ya resolvió esto mismo con su
   buscador universal de contactos; esto es el componente que allí faltó.
   ========================================================================= */

.ds-search {
  position: relative;
  flex: 1;
  max-width: 520px;
}

.ds-search__input {
  width: 100%;
  height: var(--ctl-h);
  padding: 0 var(--s-6) 0 var(--s-9);
  border: var(--bd-strong);
  border-radius: var(--r-sm);
  background: var(--n-0);
  font-size: var(--fs-base);
}

.ds-search__icono {
  position: absolute;
  left: var(--s-5);
  top: 50%;
  transform: translateY(-50%);
  color: var(--n-400);
  pointer-events: none;
}

/* Atajo de teclado a la derecha. Se enseña porque un buscador que hay que ir
   a buscar con el ratón no ahorra el viaje que venía a ahorrar. */
.ds-search__atajo {
  position: absolute;
  right: var(--s-4);
  top: 50%;
  transform: translateY(-50%);
  padding: var(--s-1) var(--s-3);
  border: var(--bd);
  border-radius: var(--r-sm);
  background: var(--n-50);
  font-family: var(--font-cond);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-micro);
  color: var(--n-500);
  pointer-events: none;
}

.ds-search__panel {
  position: absolute;
  top: calc(100% + var(--s-3));
  left: 0;
  right: 0;
  z-index: 40;
  max-height: 60vh;
  overflow-y: auto;
  background: var(--n-0);
  border: var(--bd-strong);
  border-radius: var(--r-md);
  box-shadow: var(--sh-overlay);
}

/* Los resultados van AGRUPADOS por tipo, con el tipo escrito. Una lista plana
   obligaría a deducir de qué es cada fila, que es el trabajo que el buscador
   viene a quitar. */
.ds-search__grupo {
  padding: var(--s-3) var(--s-6) var(--s-2);
  font-family: var(--font-cond);
  font-size: var(--fs-micro);
  font-weight: 600;
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
  color: var(--n-500);
  border-top: var(--bd);
}

.ds-search__grupo:first-child { border-top: 0; }

.ds-search__res {
  display: flex;
  align-items: center;
  gap: var(--s-5);
  padding: var(--s-4) var(--s-6);
  text-decoration: none;
  color: var(--n-800);
}

.ds-search__res:hover,
.ds-search__res[aria-selected="true"] {
  background: var(--accent-bg);
  text-decoration: none;
}

.ds-search__res-meta {
  margin-left: auto;
  font-size: var(--fs-xs);
  color: var(--n-500);
  font-variant-numeric: tabular-nums;
}

/* Autocompletado. NO es el buscador transversal: es un campo de formulario o
   de filtro que sugiere mientras se teclea, y lo único que necesita del
   componente de arriba es el panel de resultados —de ahí que solo aporte el
   ancla posicionada y reutilice `.ds-search__panel` y `.ds-search__res`—.

   Se sube al núcleo porque estaba reimplementado en SIETE hojas de app
   (configuracion, datalytics, taller, retenciones, contabilidad,
   recordatorios2, odontohr), cada una con su `.autocomplete-results`. Lo
   destapó la segunda app migrada: al borrar `stock.css` el autocompletado de
   la ficha de producto se quedó sin estilo y nadie lo notó, porque el que lo
   pintaba era el CSS viejo de otra app.

   El resaltado del teclado viaja en `aria-selected` y no en una clase
   `active`: así lo anuncia el lector de pantalla, y el panel ya lo pinta. */
.ds-autocomplete { position: relative; }

.ds-autocomplete > .ds-input { width: 100%; }

/* El botón de vaciar va DENTRO del campo. Fuera se apila debajo del input
   —`.ds-field` es una columna— y deja de leerse como «borrar lo escrito»
   para parecer otra acción del formulario. */
.ds-autocomplete__x {
  position: absolute;
  top: 0;
  right: 0;
  height: var(--ctl-h);
  color: var(--n-400);
}

.ds-autocomplete__x:hover { color: var(--n-700); background: none; }

/* Dentro de un modal el cuerpo hace scroll (`overflow-y: auto`) y recorta lo
   absoluto: un panel de 60vh lo estiraría y obligaría a desplazar el modal
   para ver las opciones. Corto, y el panel hace scroll por su cuenta. */
.ds-modal .ds-autocomplete > .ds-search__panel { max-height: 14rem; }

/* Desplegable con texto predictivo (`select[data-ds-predictivo]`, lo arma
   `nucleo.js`): el <select> se queda dentro del ancla, oculto, porque es lo
   que se envía. El selector va más fino que `.ds-select` para que el
   `hidden` gane también fuera de un `.ds-app`. */
.ds-autocomplete > select[hidden] { display: none; }

.ds-autocomplete > .ds-search__panel .ds-search__res[role="option"] { cursor: pointer; }

.ds-search__res--vacio {
  font-size: var(--fs-xs);
  color: var(--n-500);
}

/* =========================================================================
   5.e BANDEJA DE TRABAJO
   La fila de «esto está pendiente y esto es lo que hay que hacer». Es el
   componente de la pantalla «Hoy» de Stock, y el mismo que reinventan las
   alertas de Comercial, las worklists de Coordinador y la cola de auditoría
   de Recordatorios2 — cada una con su CSS.

   Regla: toda fila termina en una ACCIÓN. Una bandeja que solo informa es un
   listado, y para eso ya está la tabla.
   ========================================================================= */

.ds-tasks {
  display: flex;
  flex-direction: column;
}

.ds-task {
  display: flex;
  align-items: center;
  gap: var(--s-5);
  padding: var(--s-3) var(--s-6);
  border-bottom: var(--bd);
  background: var(--n-0);
}

.ds-task:hover { background: var(--n-25); }

/* Cifra grande a la izquierda: cuántos son. Es lo que decide si esta fila se
   atiende ahora o después, así que va primero y en tabular. */
.ds-task__n {
  flex: none;
  min-width: 3.5rem;
  font-size: var(--fs-md);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  line-height: var(--lh-tight);
  text-align: right;
}

.ds-task__cuerpo { min-width: 0; flex: 1; }

.ds-task__titulo { font-weight: 600; }

.ds-task__detalle {
  margin-top: var(--s-1);
  font-size: var(--fs-xs);
  color: var(--n-500);
}

.ds-task__accion { margin-left: auto; flex: none; }

/* La urgencia tiñe SOLO la cifra, no la fila. Una bandeja con cuatro filas de
   colores distintos deja de leerse como una lista y pasa a ser un semáforo
   averiado; y el nivel va escrito además en el badge del detalle. */
.ds-task--risk .ds-task__n { color: var(--risk-ink); }
.ds-task--warn .ds-task__n { color: var(--warn-ink); }
.ds-task--ok   .ds-task__n { color: var(--ok-ink); }
.ds-task--idle .ds-task__n { color: var(--n-300); }

@media (max-width: 640px) {
  .ds-task { flex-wrap: wrap; }
  /* Sin base, `flex: 1` encoge el texto antes de bajar el botón de línea, y en
     un teléfono el título se partía en una columna de tres palabras (medido a
     390 px en la ficha del plan de pagos, 2026-10-25). */
  .ds-task__cuerpo { flex-basis: calc(100% - 3.5rem - var(--s-5)); }
  .ds-task__accion { margin-left: calc(3.5rem + var(--s-6)); }
}

/* ------------------------------------------------------------------------
   La bandeja EN REJILLA, y COMPACTA. Para cuando la lista de colas no es la
   pantalla sino su cabecera, y lo que hay debajo es el trabajo.

   Una columna es lo correcto cuando la bandeja ES la pantalla —se recorre de
   arriba abajo y se elige—. Deja de serlo en cuanto hay algo debajo que
   también necesita alto: siete colas apiladas en una pantalla ancha gastan
   media página en una columna de texto de 40 caracteres, y el listado de abajo
   se queda en una franja tan estrecha que ni se puede desplazar. Pasó en la
   Bandeja de Recepción (2026-08-17) con siete carriles y un monitor grande.

   Se pide a propósito con el modificador y no se hace sola: la rejilla cambia
   el orden de lectura —de una columna a filas de dos o tres— y eso solo es
   aceptable donde el orden de la lista es una prioridad blanda, no una
   secuencia. En rejilla cada cola se separa por marco y no por línea: sin
   marco, tres celdas contiguas se leen como una sola fila de tres columnas.

   ⚠️ **La rejilla sola no bastaba, y es el mismo problema medido otra vez**
   (CEO, 2026-08-17, sobre la pantalla ya en rejilla): «seguimos consumiendo
   todo el espacio en cards que podrían ser mucho más pequeños sin perder
   significado y nos quedamos sin espacio para las respuestas por resolver».
   Tres cosas gastaban ese alto, y ninguna era la cifra ni el título:

   1. **el párrafo de tres líneas** que explica de qué va la cola — se lee la
      primera vez y se vuelve a pintar las quinientas siguientes;
   2. **el botón en su propio renglón**, que suma un alto de control entero a
      cada celda;
   3. **celdas de 24rem**, que en un monitor ancho dan tres columnas y por
      tanto tres filas para siete colas.

   Ahora la celda es **una línea**: cifra · título · acción a la derecha.
   Medido con las siete colas reales, celda de 180 px → **60 px**:

   | Ancho del contenedor | Antes | Después | Devuelto |
   |---|---:|---:|---:|
   | 1516 px (monitor grande) | 520 px | 128 px | **392 px** |
   | 1280 px (portátil)       | 520 px | 168 px | **352 px** |
   | 1024 px (tablet)         | 690 px | 217 px | **473 px** |

   Tres cuartas partes del alto, y no se ha quitado ni una cifra ni un título:
   en un portátil es la diferencia entre ver tres filas de trabajo y no ver
   ninguna. La comprobación es reproducible y vive en
   `files/recepcion_bandeja_compacta/comparativa.html`, que enlaza estas
   mismas hojas y pinta las dos versiones una encima de otra.

   El texto **no se pierde**: sigue en el DOM (lo lee un lector de pantalla
   entero) y la plantilla lo repite en el `title` de la celda para el ratón.
   Recortar con `overflow` y no con `display:none` es justo esa diferencia.
   ------------------------------------------------------------------------ */
.ds-tasks--cols {
  display: grid;
  /* 21rem y no 24: la celda ya no sostiene un párrafo, pero sí un título de
     seis palabras y su botón. Estrecharla más mete cinco columnas y parte los
     títulos en dos líneas — se gana ancho y se paga en alto, que es lo que se
     venía a ahorrar. */
  grid-template-columns: repeat(auto-fit, minmax(21rem, 1fr));
  gap: var(--s-4);
}

.ds-tasks--cols .ds-task {
  align-items: center;
  flex-wrap: nowrap;
  gap: var(--s-4);
  padding: var(--s-4) var(--s-5);
  border: var(--bd);
  border-radius: var(--r-md);
}

/* La cifra deja de reservar 3,5rem: aquí no hay una columna de cifras que
   alinear —cada celda es una isla— y ese ancho salía del texto. */
.ds-tasks--cols .ds-task__n {
  min-width: 2.25rem;
  text-align: left;
}

.ds-tasks--cols .ds-task__cuerpo { flex: 1 1 auto; }

/* El párrafo sale de la vista, NO del documento: mismas propiedades que
   `.ds-sr-only` (base.css), así que un lector de pantalla lo sigue leyendo
   entero y el ratón lo tiene en el `title` de la celda.

   Se probó antes recortarlo a un renglón con `text-overflow`. No vale, y se
   ve en cuanto se mira: «Contestaron al recordatorio y el sist…» no informa
   de nada que el título no dijera ya, cuesta 20 px por celda y —al estrechar
   la columna para que quepa— parte en dos líneas los títulos, que son lo
   único que de verdad hay que leer aquí. */
.ds-tasks--cols .ds-task__detalle {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* La acción vuelve a la derecha, en la misma línea. Cuando la celda sostenía
   un párrafo esto le robaba el ancho al texto; sin párrafo, no hay a quién
   robárselo. */
.ds-tasks--cols .ds-task__accion {
  margin-left: var(--s-4);
  margin-top: 0;
  padding-left: 0;
  flex: none;
  flex-basis: auto;
}

@media (max-width: 640px) {
  /* En una sola columna la celda compacta sigue siendo lo correcto: el
     `flex-wrap` de la fila ancha devolvería el botón a su propio renglón, que
     es el alto que se viene a quitar. */
  .ds-tasks--cols .ds-task { flex-wrap: nowrap; }
  .ds-tasks--cols .ds-task__accion { margin-left: var(--s-4); }
}

/* ------------------------------------------------------------------------
   La bandeja con la columna de ACCIONES ALINEADA.

   En la fila flex cada botón mide lo que su texto: «Ver» 46 px y «Marcar
   entregado» 131 px, todos pegados a la derecha, así que el borde izquierdo de
   la columna de botones baila de fila en fila y la bandeja se lee como un
   montón y no como una lista (CEO, 2026-10-09, sobre la portada de
   Contabilidad).

   La bandeja pasa a ser UNA rejilla de tres columnas —cifra · cuerpo ·
   acción— y cada fila la hereda con `subgrid`. Así la columna de la acción la
   decide el botón MÁS ANCHO de toda la bandeja, sin escribir un ancho a mano
   que caduca con la primera etiqueta nueva, y los demás se estiran a él. El
   mínimo de 10rem es para que dos bandejas seguidas en la misma pantalla
   (Pendiente de resolver / Datos por subir) caigan en la misma columna
   mientras sus etiquetas quepan, que es lo habitual.

   Se pide con el modificador y no se hace sola: hay bandejas cuya fila no es
   cifra · cuerpo · acción, y a esas la rejilla les movería las piezas.
   ------------------------------------------------------------------------ */
.ds-tasks--alineada {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) minmax(10rem, max-content);
  column-gap: var(--s-6);
}

.ds-tasks--alineada .ds-task {
  display: grid;
  grid-column: 1 / -1;
  grid-template-columns: subgrid;
  align-items: center;
}

.ds-tasks--alineada .ds-task__n      { grid-column: 1; }
.ds-tasks--alineada .ds-task__cuerpo { grid-column: 2; }
.ds-tasks--alineada .ds-task__accion { grid-column: 3; margin-left: 0; }
.ds-tasks--alineada .ds-task__accion .ds-btn { width: 100%; }

/* Una bandeja sin cifras (los insumos) no reserva la columna vacía: el
   cuerpo arranca en el borde, como en la fila flex. */
.ds-tasks--alineada:not(:has(.ds-task__n)) .ds-task__cuerpo { grid-column: 1 / 3; }

@media (max-width: 640px) {
  /* En el móvil no hay ancho para tres columnas: la acción baja a su propio
     renglón, debajo del cuerpo y con su mismo ancho — el mismo sitio que le da
     la fila flex, pero ahora todos los botones iguales. */
  .ds-tasks--alineada { grid-template-columns: auto minmax(0, 1fr); }
  .ds-tasks--alineada .ds-task__accion { grid-column: 2; margin-left: 0; }
  .ds-tasks--alineada:not(:has(.ds-task__n)) .ds-task__accion { grid-column: 1 / -1; }
}

/* =========================================================================
   6. TARJETA KPI
   Rótulo en micro-etiqueta ENCIMA, cifra grande tabular, variación debajo en
   color semántico y con flecha (DESIGN.md §3-C).
   ========================================================================= */

.ds-kpis {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: var(--s-4);
}

/* LA FICHA DEL TAMAÑO DE UN BOTÓN (CEO, 2026-10-24). Rótulo y cifra en el
   mismo renglón y, si la hay, una línea de contexto debajo: ~46 px con ella,
   ~30 sin ella, donde la tarjeta hero se llevaba 90-115 px. *«Los KPI los haría
   del tamaño de los botones, no mucho más grandes»*: se trabaja en portátiles
   de 15″, y si el día a día ya ocupa la pantalla, para un error grave no queda
   nada más gordo que pintar. Deroga la cifra a --fs-2xl del arquetipo C
   (2026-08-07) y el recorte de `.ds-a`: ahora es UNA medida en toda la app.
   Ver DESIGN.md §6.5. */
.ds-kpi {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  column-gap: var(--s-4);
  flex: 0 1 auto;
  min-width: 0;
  max-width: 26rem;
  padding: var(--s-2) var(--s-5);
  background: var(--n-0);
  border: var(--bd-strong);
  border-radius: var(--r-md);
}

.ds-kpi__label {
  font-family: var(--font-cond);
  font-size: var(--fs-micro);
  font-weight: 600;
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
  color: var(--n-500);
}

.ds-kpi__value {
  font-size: var(--fs-md);
  font-weight: 600;
  line-height: var(--lh-tight);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.ds-kpi__unit {
  font-size: var(--fs-xs);
  font-weight: 500;
  color: var(--n-500);
  margin-left: var(--s-1);
}

/* El contexto baja a su renglón y no estira la ficha: se parte antes que
   ensanchar la tira. Lo mismo cualquier otro hijo que traiga una pantalla
   (una barra, una lista, un enlace): solo rótulo y cifra comparten renglón. */
.ds-kpi > :not(.ds-kpi__label, .ds-kpi__value) { flex: 1 0 100%; }

.ds-kpi__delta {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  margin-top: var(--s-1);
  font-size: var(--fs-xs);
  font-weight: 500;
  line-height: var(--lh-tight);
  font-variant-numeric: tabular-nums;
  color: var(--n-500);
}

.ds-kpi__delta--up   { color: var(--ok-ink); }
.ds-kpi__delta--down { color: var(--risk-ink); }

/* Hay métricas donde bajar es bueno (no-shows, cartera parada). El sentido
   lo decide la plantilla eligiendo la clase, no el signo del número. */
.ds-kpi__delta--good { color: var(--ok-ink); }
.ds-kpi__delta--bad  { color: var(--risk-ink); }

/* KPI QUE FILTRA. Lo pide el detalle de ejecución de Recordatorios2, donde las
   doce cifras de arriba no informan: cada una acota la tabla de abajo («ver
   solo los fallidos»). Allí estaba resuelto con `<div>` + un `click` colgado,
   o sea inalcanzable con el tabulador y mudo para el lector de pantalla.

   Va como VARIANTE y no como componente aparte porque el KPI no cambia de
   forma al volverse control: cambia de elemento (`<button>`) y gana estado.
   El estado viaja en `aria-pressed`, que es lo que lo anuncia además de
   pintarlo — una clase `activo` solo haría lo segundo. */
.ds-kpi--action {
  text-align: left;
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease);
}

/* Dentro de un <button> los hijos son inline por defecto y las tres líneas del
   KPI se pondrían en la misma. */
.ds-kpi--action { font: inherit; }

.ds-kpi--action:hover { border-color: var(--n-400); }
.ds-kpi--action[aria-pressed="true"] { border-color: var(--accent); background: var(--accent-bg); }

/* Sin nada que filtrar el KPI sigue diciendo su cifra, pero deja de ofrecerse
   como control: un botón que no hace nada al pulsarlo se lee como averiado. */
.ds-kpi--action:disabled { cursor: default; color: inherit; background: var(--n-0); }

/* KPI CON SIGNO SEMÁNTICO (Analytics, 2026-08-06).
   Hay cifras que no son neutras: «cartera parada 32.178 €» o «14 noches de
   ETL fallidas» son un riesgo, no un dato. La app lo resolvía con un borde
   izquierdo de color MÁS un fondo en degradado, y el degradado está prohibido
   (§1). Aquí queda solo el borde, que es el caso escrito de `--bd-w-signal`:
   el grueso que ES señal.

   El color NO es la información —§5 no lo permite—: es énfasis sobre un
   rótulo que ya dice lo que pasa, y las variantes de riesgo y aviso llevan
   además su icono en el rótulo. Si al quitarle el color el KPI deja de
   entenderse, el defecto está en el rótulo. */
.ds-kpi--ok   { border-left: var(--bd-w-signal) solid var(--ok); }
.ds-kpi--warn { border-left: var(--bd-w-signal) solid var(--warn); }
.ds-kpi--risk { border-left: var(--bd-w-signal) solid var(--risk); }
.ds-kpi--info { border-left: var(--bd-w-signal) solid var(--info); }
.ds-kpi--idle { border-left: var(--bd-w-signal) solid var(--n-300); }

.ds-kpi--ok   .ds-kpi__label i { color: var(--ok-ink); }
.ds-kpi--warn .ds-kpi__label i { color: var(--warn-ink); }
.ds-kpi--risk .ds-kpi__label i { color: var(--risk-ink); }
.ds-kpi--info .ds-kpi__label i { color: var(--info-ink); }

/* Un KPI cuyo valor es un NOMBRE, no una magnitud: «Doctor con más carga»,
   «Área líder». Baja de cuerpo y admite partir palabra: a --fs-2xl un nombre
   con dos apellidos no cabe en ninguna columna de la tira, y sin
   `overflow-wrap` se sale de la tarjeta en vez de envolver. La cifra
   numérica no necesita ninguna de las dos cosas. */
.ds-kpi__value--texto {
  white-space: normal;
  overflow-wrap: anywhere;
}

/* Un paso más para el valor que además es LARGO. Lo aplica `nucleo.js` por
   longitud del texto, no la plantilla: el caso que lo pidió es el nombre
   comercial de la clínica —«Clínica Carmona - Odontología y Medicina
   Estética»—, que a `--fs-2xl` ocupaba cinco renglones y empujaba a los otros
   tres KPIs de la tira. También suelta el interlineado: a este cuerpo el texto
   se lee como texto y `--lh-tight` lo apelmaza. */
.ds-kpi__value--largo {
  font-size: var(--fs-sm);
  white-space: normal;
  line-height: var(--lh-base);
}

/* KPI QUE ES UN ENLACE. La variante de arriba nació para un `<button>` que
   filtra la tabla de su misma pantalla; el Club destapó el otro caso, igual de
   común: la cifra lleva a OTRA lista ya filtrada («1 tarjeta sin registrar en
   Vevi» → esa lista). Como enlace heredaba el color de enlace, así que la cifra
   salía en acento — y §5 no se lo permite: el acento no va en los datos, y aquí
   además compite con los badges de la tabla de al lado. La tarjeta entera es el
   destino, así que el subrayado tampoco aporta. */
a.ds-kpi { color: inherit; text-decoration: none; }
/* El `color: inherit` se reafirma en `:hover` por lo mismo que en `.ds-btn`
   (ver el bloque largo de §2): `a:hover` de la hoja antigua es (0,1,1), se
   carga después y le ganaba a `a.ds-kpi` — así que la cifra se ponía en acento
   justo al apuntarla, que es lo que §5 no permite. */
a.ds-kpi:hover { border-color: var(--n-400); text-decoration: none; color: inherit; }

/* =========================================================================
   6.b BARRA DE PROGRESO
   «Cuánto de esto está hecho». La reinventan el cuadre del asistente de
   albaranes de Gestión Taller (`cuadre-bar`), el avance de importación y las
   barras de ocupación de Analytics, todas con su propio track+fill.

   El relleno va en NEUTRO OSCURO y no en acento: el acento tiene cinco sitios
   (DESIGN.md §5) y una barra de datos no es ninguno — si lo fuera, en una
   pantalla con cuatro barras el acento dejaría de señalar la acción primaria.
   Los modificadores semánticos existen para cuando el avance ES un veredicto
   (conciliado / descuadrado), y entonces la cifra va escrita al lado: la barra
   sola sería estado comunicado por color.
   ========================================================================= */

.ds-progress {
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
}

.ds-progress__track {
  height: var(--s-4);
  border-radius: var(--r-pill);
  background: var(--n-100);
  overflow: hidden;
}

.ds-progress__fill {
  height: 100%;
  background: var(--n-600);
  transition: width var(--dur-base) var(--ease);
}

.ds-progress--ok   .ds-progress__fill { background: var(--ok); }
.ds-progress--warn .ds-progress__fill { background: var(--warn); }
.ds-progress--risk .ds-progress__fill { background: var(--risk); }

.ds-progress__text {
  font-size: var(--fs-xs);
  color: var(--n-500);
  font-variant-numeric: tabular-nums;
}

/* -------------------------------------------------------------------------
   6.b-bis BARRA APILADA — «en qué se va esto», no «cuánto llevo»
   Añadida el 2026-08-18 con la pantalla de capacidad y reservas de Cortex,
   que es su primer caso real: un turno se reparte entre primeras visitas,
   revisiones, urgencias y producción, y las cuatro partes suman el total.

   NO es un caso de `.ds-progress` a secas: aquella contesta «cuánto de esto
   está hecho» con UN relleno. Esta contesta «de qué se compone», que es una
   pregunta distinta y hasta hoy no la tenía nadie en el núcleo — la había
   reinventado `cortex.css` como `.cm-sim-bar` para el simulador del cuadro
   de mando, que es exactamente el aviso de DESIGN.md §2: si una hoja de app
   escribe un componente, al núcleo le falta.

   Los tramos usan las RANURAS DE SERIE (`--viz-1..5`) y no los semánticos:
   «primeras visitas» no es un veredicto, es una categoría, y los cinco
   semánticos están reservados (DESIGN.md §5). El orden de las ranuras está
   validado por pares adyacentes y no se reordena.

   ⚠️ El color NO comunica solo: la leyenda lleva la palabra y los minutos van
   escritos en la tabla de al lado. La barra es el resumen visual, nunca el
   dato. La anchura de cada tramo llega por `data-pct` y la pinta
   `static/design/nucleo.js`, igual que en la barra simple — el contrato
   prohíbe `style=""` en la plantilla y el porcentaje es un DATO.
   ------------------------------------------------------------------------- */

.ds-progress--apilada .ds-progress__track { display: flex; }

.ds-progress--apilada .ds-progress__fill {
  flex: none;
  border-radius: 0;
}

/* El color de una serie, aplicable al tramo Y a su punto de leyenda: son la
   misma serie, así que es una sola declaración y no pueden divergir. */
.ds-serie--1 { background: var(--viz-1); }
.ds-serie--2 { background: var(--viz-2); }
.ds-serie--3 { background: var(--viz-3); }
.ds-serie--4 { background: var(--viz-4); }
.ds-serie--5 { background: var(--viz-5); }

/* El tramo que es «lo que queda», no una serie: gris, porque no compite con
   las categorías que sí se han decidido. */
.ds-serie--resto { background: var(--n-300); }

.ds-progress__leyenda {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2) var(--s-5);
  font-size: var(--fs-xs);
  color: var(--n-500);
}

.ds-progress__clave {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  font-variant-numeric: tabular-nums;
}

.ds-progress__punto {
  width: var(--s-4);
  height: var(--s-4);
  border-radius: var(--r-sm);
  flex: none;
}

/* =========================================================================
   7. BARRA DE FILTROS
   Fija, nunca hace scroll (arquetipo A). Colapsable en pantallas estrechas.
   ========================================================================= */

.ds-filters {
  position: sticky;
  top: 0;
  z-index: 4;
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--s-5);
  padding: var(--s-5) var(--s-6);
  background: var(--n-0);
  border-bottom: var(--bd-strong);
}

/* Un campo de filtro NO se dimensiona por su opción más larga. Lo destapó la
   lista de trabajos de Gestión Taller, que tiene nueve filtros: el <select> de
   doctores se estiraba hasta «Dra. Juana María Carmona García», la fila se iba
   de 1.280px y lo que quedaba fuera de pantalla era el botón de acción
   primaria —o sea que la pantalla perdía su acción, no solo un filtro—.
   `flex: 0 1 180px` los iguala y los deja encoger, así que la barra envuelve
   en vez de desbordarse. */
.ds-filters__field { flex: 1 1 130px; min-width: 120px; max-width: 180px; }

/* ENCOGER ANTES DE ENVOLVER. Con `flex: 0 1 180px` la barra partía la fila en
   cuanto los 180px no cabían: el navegador decide el salto de línea con el
   `flex-basis`, y encoger solo reparte lo que falta DENTRO de una fila ya
   partida — así que el `min-width: 140px` no se alcanzaba nunca. En Pendientes
   de pago (2026-09-14) a 1.100px «Vence hasta» bajaba de renglón con todos los
   desplegables a su ancho entero. Ahora la base es pequeña y el tope es el
   180px de siempre: en pantalla ancha se ve igual, y al estrecharse cada campo
   cede hasta 120px antes de que ninguno baje de fila.

   Una casilla no es un control de 36px de alto: con `align-items: flex-end`
   su texto quedaba por debajo del de los campos. Se le da la altura de un
   control para que se centre con ellos. */
.ds-filters__field > .ds-check { min-height: var(--ctl-h); }

/* EL BUSCADOR DE LA BARRA NO ES UN FILTRO DE 180px. El tope de arriba existe
   contra un `<select>` que se estira hasta su opción más larga; un campo donde
   se TECLEA tiene el problema contrario: a 180px el marcador de posición sale
   cortado a media palabra («…o código de pacien») y quien llega a la pantalla
   no llega a leer qué se espera que escriba. Medido en la entrada de Primera
   Visita, 2026-08-17.

   Crece pero no manda: `1 1 320px` deja que se estire con la barra y el tope
   impide que se coma el hueco de la nota y de la acción, que es lo que pasaba
   antes de que existiera el `flex-basis` de arriba. */
.ds-filters__field--ancho { flex: 1 1 220px; max-width: 30rem; }

.ds-filters__field > .ds-input,
.ds-filters__field > .ds-select { width: 100%; }

/* UN `<form>` DENTRO DEL CAMPO NO ES UNA CAJA TRANSPARENTE, así que lo que
   sobra no empuja: SE SALE. El campo mide 180px y no crece; el formulario de
   dentro es un flex cuyos hijos no bajan de su contenido (`min-width: auto`),
   de modo que un `<input>` de veinte caracteres más el botón «Buscar» miden
   260px y los ochenta que sobran se pintan ENCIMA del campo siguiente. Como la
   barra pinta en orden de documento, el que queda debajo es el botón:
   invisible, y sin poder pulsarse. El CEO lo vio en Stock → Pedidos el
   2026-09-01 —el buscador sin su botón, tapado por el `<select>` de
   proveedor—, y estaba igual en doce pantallas más de la misma app.

   Esto solo CONTIENE el daño: envolver es feo, tapar un botón es un fallo. La
   forma correcta es que el FORMULARIO SEA LA BARRA —`<form class="ds-filters">`
   con un `.ds-filters__field` por control—, y entonces el buscador pide su
   ancho con `--ancho` y cada filtro se queda dentro de su caja. Referencia:
   `templates/stock/inventario_almacen.html`. */
.ds-filters__field > form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-5);
}

/* Un filtro a veces tiene que explicarse (por qué 45 días y no 7), y ese texto
   NO cabe dentro del campo: la columna mide 180px, así que un párrafo de dos
   renglones se parte en seis y estira la barra entera —que es fija y le come
   ese alto a la tabla, justo lo que la barra no debe hacer—, dejando además
   vacío todo el ancho a su derecha. La nota sale del campo y ocupa ese hueco.
   El tope en `ch` es medida de lectura, no de caja: sin él la nota se estira a
   toda la fila y se lee como documentación, no como pie de un filtro.

   ⚠️ ES UN HIJO DE `.ds-filters`, NUNCA UNA SEGUNDA CLASE EN LA MISMA ETIQUETA.
   Planificación de Comercial la llevaba como `class="ds-filters
   ds-filters__nota"` y se comía media pantalla: fuera de la barra, el elemento
   es hijo directo de `.ds-a__main`, que es `flex-direction: column`, y ahí el
   `flex: 1 1 260px` de abajo mide ALTO — 260px de base más `flex-grow: 1`
   repartiéndose el hueco libre con `.ds-a__body`. Un `flex` solo significa lo
   que crees dentro del contenedor para el que se escribió. */
.ds-filters__nota {
  flex: 1 1 260px;
  max-width: 64ch;
  margin: 0;
  font-size: var(--fs-xs);
  line-height: 1.45;
  color: var(--n-500);
}

.ds-filters__actions {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: var(--s-4);
}

/* LA ACCIÓN PEGADA AL ÚLTIMO CAMPO. Con `margin-left: auto` el botón se va al
   borde derecho, y en una barra de dos fechas (un periodo) eso deja medio
   metro de vacío entre «Hasta» y «Ver periodo»: el botón deja de leerse como
   el que aplica ESAS fechas. Lo pidió el CEO en el panel de Marketing,
   2026-10-11. Para barras cortas cuyo botón aplica lo de su izquierda; en una
   barra de lista con acciones propias, el borde derecho sigue siendo lo suyo. */
.ds-filters__actions--junto { margin-left: 0; }

/* EN EL MÓVIL LA BARRA DE BOTONES ENVUELVE. Sin esto, cuatro botones
   («Buscar · Limpiar · Conexiones · + Nueva») no cabían en 384 px y el último
   —que suele ser el de crear— quedaba fuera de la pantalla y empujaba la
   página entera de lado. Medido en Stock el 2026-10-14: 409 y 398 px. */
@media (max-width: 640px) {
  .ds-filters__actions { flex-wrap: wrap; margin-left: 0; }
}

/* Filtro aplicado. Se ve el filtro puesto sin desplegar la barra, que es lo
   que evita el «no salen datos» de una tabla filtrada y olvidada. */
.ds-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-2) var(--s-4);
  border: var(--bd-strong);
  border-radius: var(--r-pill);
  background: var(--n-50);
  font-size: var(--fs-xs);
}

/* =========================================================================
   «POR QUÉ» — el motivo que encabeza una fila de worklist, y los demás
   nombrados debajo. Lo pidió la cola comercial (2026-08-20) y la razón es
   medible: la celda ENUMERABA los motivos y sus fichas —2.016 personas tienen
   más de una ficha y la peor tiene doce— y con doce revisiones vencidas
   escribía los doce códigos de área en un renglón corrido, de los que ninguno
   sirve para decidir si hay que llamar. La fila resume y el acordeón detalla.

   Es una REJILLA de dos columnas y no una lista suelta a propósito: los
   rótulos empiezan en la misma vertical **también entre filas distintas**, y
   eso es lo que permite barrer la columna en vertical buscando un motivo sin
   leer nada más. Con anchos que dependen del texto —píldoras, por ejemplo— esa
   lectura se pierde.

   El color va SOLO en el icono, y solo en el motivo filtrado: cuarenta filas
   con dos o tres píldoras son ochenta manchas compitiendo entre sí.
   ========================================================================= */
.ds-porque {
  display: grid;
  grid-template-columns: minmax(0, 14rem) minmax(0, 1fr);
  gap: var(--s-2) var(--s-5);
  align-items: baseline;
}

/* `display: contents` disuelve la línea en la rejilla del padre: sus dos hijos
   pasan a ser celdas de la misma fila. Sin esto haría falta `subgrid`, que no
   sirve aquí porque la celda no comparte rejilla con las de las otras filas. */
.ds-porque__linea { display: contents; }

.ds-porque__rotulo {
  display: flex;
  align-items: baseline;
  gap: var(--s-3);
  font-weight: 600;
}

.ds-porque__i { width: 1.1rem; text-align: center; color: var(--n-500); flex: none; }

/* El motivo por el que esta persona ha entrado en una cola FILTRADA. Color más
   palabra: el rótulo lleva al lado un `ds-sr-only` que lo dice, porque el color
   solo no informa (DESIGN.md §5). */
.ds-porque__i--filtrado { color: var(--accent); }

.ds-porque__obj { color: var(--n-700); font-size: var(--fs-sm); }

/* Cuántas fichas trae ese motivo: «×12». Tabular para que los números de filas
   consecutivas se alineen. */
.ds-porque__n { font-variant-numeric: tabular-nums; }

/* «Creado 11/08/2026 — Antigüedad: 36 días»: de cuándo es el presupuesto que la
   fila enseña. Tercera línea de la celda del PACIENTE, que es la única con
   hueco horizontal libre — el porqué está en `_cola_creado.html`.

   `block` porque es un renglón propio, y `nowrap` porque tiene que ser UNO: el
   coste de esta pantalla no es el ancho, son las 7.969 filas por las que se
   multiplica cada línea de más. Aquí el `nowrap` es seguro y dentro de «Por
   qué» no lo era: son ~22 caracteres en la celda más ancha de la tabla, la
   misma que aguanta «Cristobal Alfonso Lardín Sánchez». */
.ds-fila__creado {
  display: block;
  margin-top: var(--s-2);
  color: var(--n-500);
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* QUIÉN HABLÓ LA ÚLTIMA VEZ, en la columna «Último contacto» de la cola.

   Es un icono en un renglón que ya existía, y no una segunda línea: el coste de
   esa pantalla no es el ancho, son las 8.244 filas por las que se multiplica
   cada línea de más. El icono NO informa solo —DESIGN.md §5—: la celda lleva al
   lado un `title` con la frase entera y un `ds-sr-only` que la repite.

   Dos tonos y nada más. Una persona de la casa se pinta con el gris de texto de
   la tabla; el automatismo —el recordatorio de cita, Claudia— con el gris
   apagado, porque una fecha suya NO es una gestión comercial y la columna tiene
   que dejarlo ver de un vistazo. Medido en CDCPM el 2026-09-07: de 847 filas
   con contacto, 372 son eso. */
.ds-contacto__i {
  width: 1rem;
  margin-right: var(--s-2);
  text-align: center;
  color: var(--n-600);
}

.ds-contacto__i--auto { color: var(--n-400); }

/* «y 2 más — pendiente de aceptación, revisión vencida». Ocupa la fila entera
   de la rejilla: es un pie de la celda, no una tercera columna. */
.ds-porque__mas {
  grid-column: 1 / -1;
  font-size: var(--fs-xs);
  color: var(--n-600);
}

/* «Ya tiene cita: 21/09/2026 13:00 · 21/09/2026 13:30 — no es de estos
   presupuestos». Las citas futuras del paciente que no ejecutan ningún
   tratamiento de los presupuestos de la fila (cola comercial, 2026-09-09).

   Mismo sitio que `__mas` —la fila entera de la rejilla, como pie de la celda—
   porque es la misma clase de dato: algo de la PERSONA que no cabe dentro de
   ninguna de sus fichas. Un punto más de tinta que el pie de motivos y sin
   `nowrap`: aquí sí puede haber tres o cuatro fechas y partirlas es preferible
   a que se salgan por encima del responsable. */
.ds-porque__citas {
  grid-column: 1 / -1;
  font-size: var(--fs-xs);
  color: var(--n-700);
  font-variant-numeric: tabular-nums;
}

@media (max-width: 1200px) {
  /* Sin sitio para dos columnas, la línea se apila y el rótulo manda. */
  .ds-porque { grid-template-columns: minmax(0, 1fr); gap: var(--s-1); }
  .ds-porque__obj { padding-left: calc(1.1rem + var(--s-3)); }
}

/* El chip del motivo por el que esta fila ha entrado en una cola filtrada.
   El filtro acota QUIÉN sale, no qué se ve de él (cola comercial, 2026-08-20),
   así que la fila trae todos sus motivos y hace falta decir cuál contesta al
   filtro. Color **más** palabra: el chip lleva un `ds-sr-only` con «(el motivo
   filtrado)», que es lo que exige DESIGN.md §5 — el color solo no informa. */
.ds-chip--match {
  border-color: var(--accent);
  background: var(--accent-bg);
  color: var(--accent-600);
  font-weight: 600;
}

.ds-chip__x {
  display: inline-flex;
  border: 0;
  background: none;
  padding: 0;
  color: var(--n-500);
  cursor: pointer;
}

.ds-chip__x:hover { color: var(--n-800); }

/* El botón de desplegar solo existe donde la barra no cabe. */
.ds-filters__toggle { display: none; }

@media (max-width: 640px) {
  .ds-filters { align-items: stretch; flex-direction: column; }
  .ds-filters__toggle { display: inline-flex; }
  .ds-filters[data-collapsed="true"] .ds-filters__field { display: none; }

  /* Y el `flex-basis: 180px` de arriba DEJA DE VALER, porque al girar el eje
     principal esos 180px pasan a ser ALTO. Cada filtro pedía entonces 180px de
     altura para una etiqueta y un `<select>` de 36px, así que la barra se leía
     como un formulario con tres dedos de aire entre campos —el defecto que se
     reportó en el dashboard de Comercial el 2026-08-07—. `0 0 auto` los
     devuelve a su altura de contenido; el ancho ya lo da `align-items:
     stretch`. */
  .ds-filters__field { flex: 0 0 auto; max-width: none; }

  /* Mismo motivo para la nota: sus 260px de base serían alto. */
  .ds-filters__nota { flex: 0 0 auto; }
}

/* =========================================================================
   8. ESTADO VACÍO
   Dos variantes, porque son dos mensajes distintos con dos acciones
   distintas (DESIGN.md §4): no hay nada TODAVÍA (crear) frente a el filtro
   no devuelve nada (quitar el filtro). Confundirlas manda al usuario a
   crear un registro que ya existe.
   ========================================================================= */

.ds-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s-5);
  padding: var(--s-11) var(--s-6);
  text-align: center;
}

.ds-empty__icon {
  font-size: var(--fs-xl);
  color: var(--n-300);
}

.ds-empty__title {
  font-size: var(--fs-md);
  font-weight: 600;
}

.ds-empty__text {
  max-width: 46ch;
  font-size: var(--fs-base);
  color: var(--n-500);
}

/* =========================================================================
   9. MODAL
   Lo único, junto al panel lateral, que puede llevar sombra: es lo único
   que de verdad flota sobre el documento.
   ========================================================================= */

/* z-index 1100 y no 100: el modal flota sobre el DOCUMENTO, pero también sobre
   el armazón de la app, que todavía es el heredado. Medido en el portal real:
   `navigation.css` pone el menú lateral en 300, su velo en 250 y los menús
   desplegables de la action-bar en 1000, así que a 100 el modal salía DEBAJO
   del menú abierto. Baja a la altura del resto del sistema el día que el
   armazón se migre; hasta entonces esto es un hecho medido, no una holgura. */
.ds-modal-backdrop {
  position: fixed;
  inset: 0;
  z-index: 1100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--s-6);
  background: var(--scrim);
}

.ds-modal {
  width: 100%;
  max-width: 560px;
  max-height: 88vh;
  max-height: 88dvh;
  display: flex;
  flex-direction: column;
  background: var(--n-0);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-overlay);
}

.ds-modal--lg { max-width: 880px; }

/* Cabecera y pie fijos, el cuerpo es lo único que hace scroll. Para eso el
   cuerpo tiene que ser hijo FLEX de una columna con alto acotado: si un
   `<form>` envuelve cabecera, cuerpo y pie (18 modales lo hacen: el POST
   necesita llevarse el botón del pie), el form es un bloque que crece con su
   contenido, el cuerpo ya no encoge y el pie sale por debajo de la ventana sin
   que nada lo mueva —medido en «Editar el datáfono» a 609×938—. Por eso el
   `<form>` hijo directo hereda la columna del modal en vez de cortarla.
   `min-height: 0` es lo que deja encoger a un hijo flex por debajo de su
   contenido. */
.ds-modal > form {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}

.ds-modal__head {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  gap: var(--s-5);
  padding: var(--s-6);
  border-bottom: var(--bd);
}

.ds-modal__title {
  font-size: var(--fs-lg);
  font-weight: 600;
  line-height: var(--lh-tight);
}

.ds-modal__x { margin-left: auto; }

.ds-modal__body {
  flex: 1 1 auto;
  min-height: 0;
  padding: var(--s-6);
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* `flex-wrap: wrap` como en `.ds-formfoot`: un pie de modal no siempre son dos
   botones. Cuando además lleva estado a la izquierda —el modal de WhatsApp
   dice si la sesión está vinculada— en móvil no caben en una línea, y sin
   envolver el contenido se sale de la caja en vez de bajar. */
.ds-modal__foot {
  flex-shrink: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: var(--s-4);
  padding: var(--s-5) var(--s-6);
  border-top: var(--bd);
}

@media (max-width: 640px) {
  .ds-modal-backdrop { padding: 0; align-items: flex-end; }
  .ds-modal { max-width: none; max-height: 94vh; max-height: 94dvh; border-radius: var(--r-lg) var(--r-lg) 0 0; }
}

/* =========================================================================
   10. AVISO EN LÍNEA
   El mensaje del sistema y el aviso de contexto. Icono + texto, nunca solo
   color, y el borde izquierdo grueso porque ES la señal, no decoración.
   ========================================================================= */

/* Compacto desde el 2026-10-24 (DESIGN.md §6.5): un aviso es una franja, no
   un cartel. El amarillo es para lo que hay que hacer HOY y no impide seguir;
   lo que se repite cada día va a la bandeja de la pantalla (`.ds-tasks`), y el
   rojo se guarda para lo que invalida la cifra de la pantalla. */
.ds-note {
  display: flex;
  align-items: flex-start;
  gap: var(--s-4);
  padding: var(--s-3) var(--s-5);
  border-radius: var(--r-sm);
  border-left: var(--bd-w-signal) solid var(--idle);
  background: var(--idle-bg);
  font-size: var(--fs-sm);
  line-height: var(--lh-base);
}

.ds-note--ok   { border-left-color: var(--ok);   background: var(--ok-bg); }
.ds-note--warn { border-left-color: var(--warn); background: var(--warn-bg); }
.ds-note--risk { border-left-color: var(--risk); background: var(--risk-bg); }
.ds-note--info { border-left-color: var(--info); background: var(--info-bg); }

.ds-note__i { flex: none; margin-top: var(--s-1); }
.ds-note__title { font-weight: 600; }

/* El título es un `<span>` a propósito —hay notas donde el texto sigue en la
   MISMA línea («La ejecución falló: timeout»)—, así que cuando lo que viene
   detrás es un bloque hay que darle el aire que no trae por ser inline; si no,
   el rótulo se lee pegado al párrafo, como una primera línea en negrita. */
.ds-note__title + :is(p, div, ul, ol) { margin-top: var(--s-1); }

/* =========================================================================
   11. LA AYUDA: PÁGINA DE DOCUMENTACIÓN, MANUAL Y MAQUETAS
   Reconstruido de forma independiente TRES veces (Stock, Gestión Taller,
   Contabilidad) sin reutilizarse nunca: es el caso más claro de componente
   que faltaba en un núcleo. Son capturas dibujadas con CSS, no imágenes, y
   por eso envejecen con el producto en lugar de quedarse desfasadas.

   ---------------------------------------------------------------------
   CÓMO SE DIBUJA UNA MAQUETA (decisión de 2026-08-04)
   ---------------------------------------------------------------------
   Al medir las dos hojas de ayuda que había —`stock/css/ayuda.css` y
   `gestiontaller/css/taller-ayuda.css`— resultaron ser CLONES: las mismas
   `manual-*`, `mock-*` y `ui-mockup*`, con las mismas maquetas dibujadas dos
   veces. Y las dos tenían el mismo defecto de fondo: dentro de la maqueta
   había un juego de piezas FALSAS —`mock-btn`, `mock-input`, `mock-badge`,
   `mock-table`, `mock-kpi`— que imitaban a las de verdad. O sea un segundo
   sistema de diseño, sin mantenimiento, que envejece por su cuenta: en Stock
   está declarado por escrito que las maquetas «retratan la interfaz
   ANTERIOR».

   La regla es: **una maqueta se dibuja con los componentes de verdad**.
   Dentro de un `.ds-mock` van `.ds-table`, `.ds-badge`, `.ds-kpi`,
   `.ds-etapas`, `.ds-btn`… los mismos que pinta la pantalla que se está
   explicando. Así el dibujo no puede desfasarse: cuando el componente cambia,
   la maqueta cambia con él. Lo único que aporta `.ds-mock` es el marco
   discontinuo y el rótulo «Ejemplo», que es lo que impide confundir el dibujo
   con la pantalla — y por eso NO se le quita.

   Corolario: no se añaden aquí `ds-mock__btn` ni parientes. Si una maqueta
   necesita una pieza que el núcleo no tiene, es que falta el componente de
   verdad, no su imitación.
   ========================================================================= */

/* La página de ayuda entera. No es un arquetipo —los cinco arquetipos son para
   pantallas de trabajo— sino una forma de página que TODAS las apps tienen: un
   documento largo con índice, sus secciones y sus maquetas.

   Lo único que necesita del sistema es la anchura de lectura: a ancho completo
   de escritorio saldrían renglones de 200 caracteres. Es más ancha que
   `.ds-manual` (76ch) porque aquí conviven la prosa y tablas de tres columnas.
   Antes cada app lo resolvía con su propio `ayuda-container`/`manual-container`. */
.ds-doc { max-width: 92ch; }

.ds-manual { max-width: 76ch; }

.ds-manual__step {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--s-5);
  padding: var(--s-6) 0;
  border-bottom: var(--bd);
}

.ds-manual__n {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--s-9);
  height: var(--s-9);
  border-radius: var(--r-pill);
  background: var(--n-100);
  color: var(--n-600);
  font-size: var(--fs-sm);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.ds-manual__title { font-weight: 600; }

.ds-manual__text {
  margin-top: var(--s-3);
  color: var(--n-600);
}

/* La maqueta. Deliberadamente apagada y con el rótulo puesto: tiene que
   leerse como un dibujo de la pantalla, no confundirse con la pantalla. */
.ds-mock {
  margin-top: var(--s-5);
  padding: var(--s-5);
  border: 1px dashed var(--n-300);
  border-radius: var(--r-md);
  background: var(--n-25);
  font-size: var(--fs-xs);
  color: var(--n-500);
}

.ds-mock::before {
  content: "Ejemplo";
  display: block;
  margin-bottom: var(--s-4);
  font-family: var(--font-cond);
  font-size: var(--fs-micro);
  font-weight: 600;
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
  color: var(--n-400);
}

/* =========================================================================
   5.f SELECTOR VISUAL DE PRODUCTO
   Sale de la descripción del flujo real de Stock (2026-08-03): N personas por
   clínica usan la app SOLO para extraer material, muchas veces al día, y no
   se saben las referencias — reconocen la caja. Para ellas la foto no es
   adorno: es el identificador. Un buscador de texto les pide justo el dato
   que no tienen.

   Por eso la tarjeta se ordena: FOTO primero, nombre después, y debajo dónde
   está físicamente. Tocarla suma una unidad; no hay diálogo de cantidad,
   porque el gesto tiene que costar un toque.

   Reutilizable por la tienda del Club, que tiene el mismo problema.
   ========================================================================= */

.ds-picker {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: var(--s-5);
  padding: var(--s-6);
}

.ds-pcard {
  display: flex;
  flex-direction: column;
  border: var(--bd-strong);
  border-radius: var(--r-md);
  background: var(--n-0);
  overflow: hidden;
  cursor: pointer;
  text-align: left;
  padding: 0;
  transition: border-color var(--dur-fast) var(--ease);
}

.ds-pcard:hover { border-color: var(--n-400); }

/* Con unidades cogidas: el acento marca lo que llevas en la cesta, que es el
   único estado que esta pantalla necesita comunicar. */
.ds-pcard[data-cogidas]:not([data-cogidas="0"]) {
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
}

.ds-pcard__foto {
  position: relative;
  aspect-ratio: 4 / 3;
  background: var(--n-100);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--n-400);
  font-size: var(--fs-2xl);
}

/* `contain` y no `cover`. Estas fotos son packshots —el producto centrado
   sobre fondo blanco—, así que rellenar el hueco recortando corta justo la
   caja que hay que reconocer. Con `contain` se ve entera y conserva su
   proporción, que es lo único que la hace identificable de un vistazo.
   El fondo pasa a blanco porque el del packshot ya lo es: con el gris de la
   caja se veía el recuadro de la foto flotando dentro de la tarjeta. */
.ds-pcard__foto img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  padding: var(--s-4);
}

.ds-pcard:has(img) .ds-pcard__foto { background: var(--n-0); }

/* SIN FOTO NO ES UN ESTADO DEGRADADO.
   Medido en CDT: de los 437 productos que la clínica usa de verdad, 162 no
   tienen foto — y solo 3 tienen URL de la que bajarla. Los que llegan por
   comercial, sin web, no van a tenerla nunca. O sea que esto no es una deuda
   que se salda: es un tercio permanente del catálogo.

   Por eso la tarjeta sin foto NO lleva un hueco gris pidiendo una imagen. Una
   rejilla con un tercio de huecos grises se lee como una app rota, y encima
   castiga justo a los productos del comercial. La tarjeta sin foto cambia de
   identificador: manda EL NOMBRE, grande y legible, que es lo que hay. La
   cámara queda como gesto discreto en una esquina, para quien pueda y quiera.

   Las dos tarjetas son ciudadanas de primera; solo cambian de qué se agarra
   el ojo. */
.ds-pcard__titulo {
  /* El relleno inferior es mayor a propósito: deja sitio al gesto de la
     cámara, que si no se le come la última línea del nombre — y el nombre es
     justamente el identificador de esta tarjeta. */
  padding: var(--s-5) var(--s-5) var(--s-10);
  font-size: var(--fs-md);
  font-weight: 600;
  line-height: var(--lh-tight);
  color: var(--n-700);
  text-align: center;
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
}

.ds-pcard--texto .ds-pcard__foto { background: var(--n-50); }

/* Gesto para añadir la foto: discreto y opcional. Quien no encuentra el
   producto es quien lo tiene delante, así que se le ofrece —pero sin convertir
   su ausencia en el mensaje principal de la tarjeta. */
.ds-pcard__camara {
  position: absolute;
  bottom: var(--s-3);
  right: var(--s-3);
  width: var(--s-9);
  height: var(--s-9);
  border-radius: var(--r-pill);
  border: var(--bd-strong);
  background: var(--n-0);
  color: var(--n-500);
  font-size: var(--fs-sm);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}

.ds-pcard__camara:hover { color: var(--accent); border-color: var(--accent); }

/* Contador de lo cogido, encima de la foto. */
.ds-pcard__cont {
  position: absolute;
  top: var(--s-3);
  right: var(--s-3);
  min-width: var(--s-9);
  height: var(--s-9);
  padding: 0 var(--s-3);
  border-radius: var(--r-pill);
  background: var(--accent);
  color: var(--n-0);
  font-size: var(--fs-base);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  display: flex;
  align-items: center;
  justify-content: center;
}

.ds-pcard__cuerpo { padding: var(--s-4) var(--s-5) var(--s-5); }

.ds-pcard--texto .ds-pcard__nombre { display: none; }

.ds-pcard__nombre {
  font-size: var(--fs-sm);
  font-weight: 600;
  line-height: var(--lh-tight);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Dónde está físicamente. Es lo segundo que se lee y lo que evita la vuelta
   al almacén con las manos vacías. */
.ds-pcard__donde {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  margin-top: var(--s-3);
  padding: var(--s-1) var(--s-3);
  border-radius: var(--r-sm);
  background: var(--n-50);
  font-size: var(--fs-xs);
  color: var(--n-600);
}

.ds-pcard__quedan {
  margin-top: var(--s-3);
  font-size: var(--fs-xs);
  color: var(--n-500);
  font-variant-numeric: tabular-nums;
}

.ds-pcard__quedan--bajo { color: var(--warn-ink); font-weight: 600; }
.ds-pcard__quedan--cero { color: var(--risk-ink); font-weight: 600; }

/* Barra de la cesta, anclada abajo. Siempre visible en cuanto hay algo:
   confirmar no puede depender de recordar dónde estaba el botón. */
.ds-cesta {
  display: flex;
  align-items: center;
  gap: var(--s-6);
  padding: var(--s-5) var(--s-6);
  border-top: var(--bd-w-signal) solid var(--accent);
  background: var(--accent-bg);
}

.ds-cesta__n { font-weight: 600; font-variant-numeric: tabular-nums; }
.ds-cesta__acciones { margin-left: auto; display: flex; gap: var(--s-4); }

@media (max-width: 640px) {
  .ds-picker { grid-template-columns: repeat(auto-fill, minmax(132px, 1fr)); gap: var(--s-4); padding: var(--s-4); }
}

/* -------------------------------------------------------------------------
   5.f.2 TARJETA DE ESPECIFICACIÓN — cuando la foto no puede distinguir
   Descubierto midiendo el catálogo real (2026-08-03). Los 31 implantes en uso
   de CDT no tienen foto, y NO es dejadez: un BioHorizons 3,3×10,5 y uno
   3,8×12,0 son la misma fotografía. Ahí la foto no identifica nada.

   Pero el nombre tampoco, tal cual: «Implante BioHorizons Tappered Pro
   Conical 3,3x10.5mm Narrow» son 55 caracteres de los que solo discriminan
   los 15 últimos, y los otros 40 se repiten idénticos en las 31 filas. Una
   rejilla así obliga a leer la cola de cada tarjeta — y recortar el texto,
   que es lo natural, corta EXACTAMENTE por donde está el dato que importa.

   Solución: sacar el prefijo común a una cabecera de grupo y dejar en la
   tarjeta solo lo que varía, grande y en numeración tabular para que las
   medidas se comparen en vertical. El prefijo se calcula solo (prefijo común
   más largo del grupo); no hay que teclear nada por producto.
   ------------------------------------------------------------------------- */

/* Cabecera de familia dentro de la rejilla. Lleva el prefijo que se ha
   factorizado, para que nadie tenga que adivinar de qué son estas tarjetas. */
.ds-picker__familia {
  grid-column: 1 / -1;
  display: flex;
  align-items: baseline;
  gap: var(--s-4);
  padding: var(--s-6) var(--s-1) var(--s-2);
  border-bottom: var(--bd);
}

.ds-picker__familia-nombre {
  font-family: var(--font-cond);
  font-size: var(--fs-micro);
  font-weight: 600;
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
  color: var(--n-500);
}

.ds-picker__familia-n {
  margin-left: auto;
  font-size: var(--fs-xs);
  color: var(--n-400);
  font-variant-numeric: tabular-nums;
}

/* La medida manda: es lo único que distingue una caja de la de al lado. */
.ds-pcard--spec .ds-pcard__foto { background: var(--n-25); }

.ds-pcard__spec {
  padding: var(--s-5);
  font-size: var(--fs-lg);
  font-weight: 600;
  line-height: var(--lh-tight);
  color: var(--n-800);
  text-align: center;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}

/* Matiz de la variante (Narrow / Regular / Wide): distingue tanto como la
   medida, pero no se lee como número. */
.ds-pcard__spec-var {
  display: block;
  margin-top: var(--s-2);
  font-family: var(--font-cond);
  font-size: var(--fs-micro);
  font-weight: 600;
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
  color: var(--n-500);
}

.ds-pcard--spec .ds-pcard__nombre { display: none; }

/* =========================================================================
   12. VUELTA
   El sistema no tenía forma de volver. Ni un componente, ni un hueco donde
   ponerlo: `grep volver static/design/` no devolvía nada, así que las
   pantallas migradas heredaban la omisión una por una — y el contrato del
   arquetipo B la convertía en regla, retirando el botón «Volver» de la ficha
   porque competía con la acción de acento.

   Competir competía, y por eso esto NO es un botón. Es un enlace pequeño,
   gris y por encima del título: se lee antes que nada al entrar (arriba a la
   izquierda es donde el ojo empieza) y no disputa el peso visual de la
   siguiente acción, que sigue estando sola a la derecha. La regla de §3-B se
   mantiene entera; lo que se repara es la salida.

   Destino FIJO Y NOMBRADO («Todos los trabajos»), no `history.back()`:
   - `history.back()` miente cuando se llega por enlace directo desde
     WhatsApp, desde un correo o desde una pestaña nueva, que es justo como
     se llega a estas fichas.
   - Un destino nombrado se puede leer antes de pulsarlo. Es lo que hace que
     la app se pueda dibujar de memoria, y es la misma razón por la que el
     nivel 2 de la navegación se declara una sola vez.

   No sustituye a la navegación de arriba: la de arriba dice dónde estás en
   la app, esta dice de dónde vienes en este trabajo.
   ========================================================================= */

.ds-volver {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  font-size: var(--fs-sm);
  font-weight: 500;
  line-height: 1;
  color: var(--n-500);
  text-decoration: none;
  border-radius: var(--r-sm);
  transition: color var(--dur-fast) var(--ease);
}

.ds-volver:hover {
  color: var(--n-800);
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* El foco lo pinta `base.css`, pero solo dentro de `.ds-app`. Esta pieza se
   usa también en pantallas todavía sin migrar (es la salida de emergencia:
   negarle el anillo justo ahí sería negárselo a quien navega con teclado en
   la mitad del producto que aún no ha llegado al sistema). */
.ds-volver:focus-visible {
  outline: none;
  box-shadow: var(--focus);
}

/* La flecha no es decoración: sin ella el enlace se lee como un título de
   sección. Va en un span propio para que no herede el subrayado del hover,
   que sobre una flecha se ve como un tachón. */
.ds-volver__i {
  font-size: var(--fs-xs);
  line-height: 1;
}

.ds-volver:hover .ds-volver__i { text-decoration: none; }

/* Cuando va SOLO en su renglón, encima del título de una pantalla que NO es
   arquetipo B (el B reparte el espacio de su cabecera en `.ds-b__volver`).
   Sin el modificador, el enlace comparte barra con otros datos —el resumen de
   una lista— y ahí la separación la pone la barra. */
.ds-volver--bloque {
  display: flex;
  width: fit-content;
  margin-bottom: var(--s-3);
}

/* =========================================================================
   13. GRÁFICO
   Lo pide Analytics (arquetipo C, 2026-08-06), y no es una pieza suya: el
   contenedor de gráfico está reinventado en CUATRO hojas de app —analytics,
   cortex, contabilidad y padi— con el mismo nombre (`chart-container`) y
   tres alturas distintas. Por eso vive aquí y no en el arquetipo: Contabilidad
   es arquetipo A y también dibuja.

   Lo único que aporta es un ALTO RESERVADO. Sin él, la biblioteca de gráficos
   pinta sobre un contenedor de altura 0 y la página da un salto cuando llega
   el dibujo; con el alto puesto, el hueco ya está antes de que cargue nada.
   El color y la tipografía de las series NO se deciden aquí —las decide quien
   dibuja—, pero salen de estos mismos tokens (ver
   static/analytics/js/charts_paleta.js).
   ========================================================================= */

.ds-chart {
  position: relative;
  width: 100%;
  max-width: 100%;
  min-height: 260px;
  overflow: hidden;
}

/* Series muchas o categorías en el eje Y: el alto lo calcula quien dibuja y
   el contenedor deja de imponer el suyo. */
.ds-chart--auto { min-height: 0; }

/* Mapas de calor y barras horizontales con muchas categorías. */
.ds-chart--alto { min-height: 420px; }

/* La biblioteca escribe anchos en línea sobre su propio nodo al redimensionar
   y, si la ventana encoge, se queda con el ancho anterior y desborda al
   padre. Este es el único sitio del sistema donde hace falta contener a un
   tercero que escribe estilo en línea. */
.ds-chart > div { max-width: 100%; }
.ds-chart canvas,
.ds-chart svg,
.ds-chart foreignObject { max-width: 100%; }

/* PIE DEL GRÁFICO. No es decoración: es dónde se dice qué mide el eje y de
   dónde sale el dato, que en un panel analítico es la mitad de la lectura.
   Va DEBAJO y no en un `title`, porque en tableta el `title` no existe (§6). */
.ds-chart__pie {
  margin-top: var(--s-4);
  font-size: var(--fs-xs);
  line-height: var(--lh-base);
  color: var(--n-500);
}

/* Serie etiquetada a mano, para los gráficos que §3-C quiere SIN leyenda.
   Es un rótulo con su muestra de color, no una leyenda: se pone junto al
   dato, no en una esquina que obliga a mirar dos veces. */
.ds-serie {
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
  font-size: var(--fs-xs);
  color: var(--n-600);
}

.ds-serie__marca {
  width: 10px;
  height: 10px;
  flex: none;
  border-radius: var(--r-sm);
  background: var(--n-300);
}

/* La muestra toma el color de la MISMA ranura que la serie del gráfico, y por
   eso se elige por número y no con un `style` en línea: la tabla de debajo y
   el gráfico de arriba tienen que estar pintando lo mismo del mismo color, y
   un literal en el marcado no puede garantizarlo.

   Sin modificador se queda gris, que es lo correcto de la SEXTA en adelante:
   no hay ranura nueva que inventar (ver tokens.css, §series de gráfico). */
.ds-serie__marca--1 { background: var(--viz-1); }
.ds-serie__marca--2 { background: var(--viz-2); }
.ds-serie__marca--3 { background: var(--viz-3); }
.ds-serie__marca--4 { background: var(--viz-4); }
.ds-serie__marca--5 { background: var(--viz-5); }
/* De la sexta en adelante NO hay modificador, y por eso la muestra se queda
   gris: es el mismo «Otros» que devuelve `LydentViz.colores()`. Una tabla con
   nueve filas de colores distintos no se está leyendo, se está hojeando. */

/* En móvil el gráfico baja de alto: a 260px de alto y 340 de ancho, un
   gráfico ocupa media pantalla y empuja fuera del pliegue lo que explica. */
@media (max-width: 640px) {
  .ds-chart       { min-height: 220px; }
  .ds-chart--alto { min-height: 320px; }
}

/* =========================================================================
   14. CONTROL SEGMENTADO
   Elegir UNA de entre pocas opciones que no cambian de página: la métrica de
   un gráfico, el ámbito de un informe, la ventana de comparación. Está
   reinventado en tres hojas —analytics (`metrica-toggle`), comercial y padi—
   y las tres lo montan con `<input type="radio">` oculto y un `<span>`
   pintado, que es lo correcto: es un grupo de radios, y hacerlo con botones
   pierde las flechas del teclado y el anuncio del lector de pantalla.

   No es `.ds-tabs`: las pestañas navegan a otra pantalla y aquí no se
   navega. No es `.ds-btngroup` con `aria-pressed`: eso son acciones, y esto
   es un valor que se envía con el formulario.
   ========================================================================= */

.ds-seg {
  display: inline-flex;
  padding: var(--s-1);
  gap: var(--s-1);
  background: var(--n-100);
  border-radius: var(--r-md);
}

.ds-seg__op {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: calc(var(--ctl-h) - var(--s-2));
  padding: 0 var(--s-5);
  border-radius: var(--r-sm);
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--n-600);
  white-space: nowrap;
  cursor: pointer;
}

/* El radio de verdad sigue ahí y sigue recibiendo el foco: se saca de la
   vista sin `display:none`, que lo quitaría del orden de tabulación. */
.ds-seg__op input {
  position: absolute;
  opacity: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  cursor: pointer;
}

.ds-seg__op:hover { color: var(--n-900); }

.ds-seg__op:has(input:checked) {
  background: var(--n-0);
  color: var(--n-900);
  box-shadow: 0 0 0 1px var(--n-200);
}

.ds-seg__op:has(input:focus-visible) { box-shadow: var(--focus); }

/* A 375px cuatro métricas no caben en una fila: envuelve en vez de salirse. */
@media (max-width: 640px) {
  .ds-seg { flex-wrap: wrap; }
}

/* =========================================================================
   15. PISTA
   El icono de interrogación que explica cómo se calcula una cifra. Analytics
   tiene setenta, y estaban todos en `data-tooltip` sobre un `<i>`: en tableta
   —que es la mitad del uso en gabinete— no hay `hover`, o sea que esa
   explicación NO EXISTÍA (§6, y es la misma regla que ya se cobró en la
   columna de acciones del Club).

   Se arregla sin JS y sin dependencias: el disparador es un `<button>`, así
   que se puede TOCAR y TABULAR, y el texto aparece con `:hover`, `:focus-visible`
   y `:focus-within`. Un toque en el móvil enfoca el botón y muestra la pista;
   el siguiente toque fuera la cierra.

   El texto va en el marcado, no en un atributo: un `data-tooltip` no lo lee
   un lector de pantalla ni lo encuentra Ctrl+F.
   ========================================================================= */

.ds-hint { position: relative; display: inline-flex; }

.ds-hint__b {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.25rem;
  height: 1.25rem;
  padding: 0;
  border: 0;
  border-radius: var(--r-pill);
  background: none;
  color: var(--n-400);
  font-size: var(--fs-xs);
  line-height: 1;
  cursor: help;
}

.ds-hint__b:hover { color: var(--n-700); background: var(--n-100); }

.ds-hint__txt {
  position: absolute;
  top: calc(100% + var(--s-3));
  left: 50%;
  z-index: 30;
  width: max-content;
  max-width: min(34ch, 80vw);
  transform: translateX(-50%);
  padding: var(--s-4) var(--s-5);
  border-radius: var(--r-md);
  background: var(--n-800);
  color: var(--n-0);
  font-family: var(--font-ui);
  font-size: var(--fs-xs);
  font-weight: 400;
  line-height: var(--lh-base);
  letter-spacing: normal;
  text-align: left;
  text-transform: none;
  /* Dentro de un `<th>` (nowrap) la pista heredaba el `nowrap` y salía en una
     sola línea de 600 px fuera de su caja; dentro de un `.ds-scroll-x`
     estiraba la tabla de 1.401 a 1.912 px (Marketing, 2026-10-11). */
  white-space: normal;
  box-shadow: var(--sh-overlay);
  visibility: hidden;
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease);
}

.ds-hint:hover .ds-hint__txt,
.ds-hint:focus-within .ds-hint__txt {
  visibility: visible;
  opacity: 1;
}

/* Pegada al borde derecho de la pantalla, una pista centrada se sale. En
   móvil se ancla al borde del disparador y no al centro. */
@media (max-width: 640px) {
  .ds-hint__txt {
    left: auto;
    right: 0;
    transform: none;
  }
}

/* La pista del TÍTULO de un panel, de la cabecera de página o de la etiqueta
   de un KPI se abre hacia la DERECHA, anclada al borde izquierdo del «?».
   Esos «?» van pegados a la izquierda de su caja, y centrada la pista salía
   por la izquierda y se montaba encima (o debajo) del menú lateral, que es
   `fixed` con z-index 300 (Marketing → Panel, «Por línea», 2026-10-22). Solo
   por encima de 640 px: en el móvil manda la regla de arriba. Va ANTES de
   `ds-hint--fin` (misma especificidad), que sigue ganando. */
@media (min-width: 641px) {
  :is(.ds-panel__title, .ds-pagehead__title, .ds-kpi__label) .ds-hint__txt {
    left: 0;
    right: auto;
    transform: none;
  }
  /* El ÚLTIMO KPI de una fila está pegado al borde derecho: abierta hacia la
     derecha se saldría de la pantalla, así que se abre hacia la izquierda. */
  .ds-kpi:last-child .ds-kpi__label .ds-hint__txt {
    left: auto;
    right: 0;
  }
}

/* La pista de un botón de la ÚLTIMA columna de una tabla (el teléfono de la
   cola de Comercial): centrada se sale por la derecha a cualquier ancho, no
   solo en móvil, así que se ancla siempre al borde del disparador. */
.ds-hint--fin .ds-hint__txt {
  left: auto;
  right: 0;
  transform: none;
}

/* =========================================================================
   16. SELECCIÓN MÚLTIPLE CON BÚSQUEDA
   Elegir varias cosas de una lista larga: las áreas que se comparan en un
   informe, los destinatarios de un envío. Sube al núcleo el 2026-08-06, al
   cerrar la migración de Analytics: vivía en `analytics.css` como `.ms-*` y
   esa hoja se borra, pero el componente no es de esa app —Comercial tiene su
   propia versión— y dejarlo en `apps/analytics.css` habría sido esconder la
   duplicación en otro sitio.

   El `<select multiple hidden>` de verdad sigue en el marcado y es la fuente
   que se envía: esto lo viste y lo maneja, no lo sustituye. Si el JavaScript
   falla, el formulario sigue funcionando.
   ========================================================================= */

.ds-multi { position: relative; flex: 0 0 300px; min-width: 260px; }

.ds-multi__trigger {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  width: 100%;
  min-height: var(--ctl-h);
  padding: var(--s-2) var(--s-5);
  border: var(--bd-strong);
  border-radius: var(--r-sm);
  background: var(--n-0);
  font: inherit;
  font-size: var(--fs-sm);
  text-align: left;
  cursor: pointer;
}

.ds-multi__trigger:hover { border-color: var(--n-400); }
.ds-multi__trigger[aria-expanded="true"] { border-color: var(--accent); }

/* Lo elegido, dentro del propio disparador: sin esto hay que abrir el panel
   para saber qué está puesto, y un filtro olvidado es una tabla que parece
   vacía. */
.ds-multi__pills {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-2);
  flex: 1;
  min-height: 22px;
}

.ds-multi__pills--empty { color: var(--n-400); }

.ds-multi__pill {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-1) var(--s-3);
  border-radius: var(--r-pill);
  background: var(--n-100);
  font-size: var(--fs-xs);
  font-weight: 600;
}

/* La pastilla toma el color de la serie que le corresponde en el gráfico. Lo
   pone el JS con `--pill-color`, que es un DATO —qué área es la tercera— y no
   una decisión de estilo: por eso viaja en una variable y no en una clase. */
.ds-multi__pill[style*="--pill-color"] { background: var(--pill-color); color: var(--n-0); }

.ds-multi__pill-x {
  display: inline-flex;
  border: 0;
  padding: 0;
  background: none;
  color: inherit;
  font-size: var(--fs-base);
  line-height: 1;
  cursor: pointer;
  opacity: 0.8;
}

.ds-multi__pill-x:hover { opacity: 1; }

.ds-multi__counter {
  flex: none;
  font-size: var(--fs-micro);
  font-variant-numeric: tabular-nums;
  color: var(--n-500);
}

.ds-multi__caret { flex: none; color: var(--n-400); font-size: var(--fs-xs); }

.ds-multi__panel {
  position: absolute;
  top: calc(100% + var(--s-2));
  left: 0;
  z-index: 20;
  width: 100%;
  min-width: 280px;
  padding: var(--s-4);
  border: var(--bd-strong);
  border-radius: var(--r-md);
  background: var(--n-0);
  box-shadow: var(--sh-overlay);
}

.ds-multi__search { width: 100%; margin-bottom: var(--s-4); }

.ds-multi__options { max-height: 260px; overflow-y: auto; }

.ds-multi__option {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  padding: var(--s-3) var(--s-4);
  border-radius: var(--r-sm);
  font-size: var(--fs-sm);
  cursor: pointer;
}

.ds-multi__option:hover { background: var(--n-50); }

/* Al llegar al tope no se ocultan las demás: se apagan. Que desaparezcan hace
   creer que la lista se ha filtrado sola. */
.ds-multi__option[data-tope="true"] { opacity: 0.45; cursor: not-allowed; }

.ds-multi__foot {
  display: flex;
  justify-content: space-between;
  gap: var(--s-4);
  margin-top: var(--s-4);
  padding-top: var(--s-4);
  border-top: var(--bd);
}

@media (max-width: 640px) {
  .ds-multi { flex: 1 1 100%; max-width: none; }
  .ds-multi__panel { position: static; box-shadow: none; margin-top: var(--s-3); }
}


/* =========================================================================
   17 y 18 — vinieron de `archetypes/b-flujo.css` el 2026-08-06, al migrar
   Comercial. Se escribieron allí porque las pidió la ficha de producto de
   Stock (un expediente), pero no son propias de un expediente: la FICHA DEL
   PACIENTE es arquetipo C y necesita las dos —el bloque «pasado» se pliega y
   el historial de contactos es una cronología de lectura—, y de B no hereda
   nada. Mismo criterio con el que Analytics subió `.ds-table--responsive`.
   ========================================================================= */

/* =========================================================================
   17. SECCIÓN PLEGABLE
   «Todo lo demás se pliega». Con <details>/<summary> nativos: lo que hace el
   navegador, lo hace el navegador (DESIGN.md §2).
   ========================================================================= */

.ds-bloque {
  border: var(--bd-strong);
  border-radius: var(--r-md);
  background: var(--n-0);
}

.ds-bloque > summary {
  display: flex;
  align-items: center;
  gap: var(--s-5);
  padding: var(--s-5) var(--s-6);
  cursor: pointer;
  list-style: none;
  font-weight: 600;
}

.ds-bloque > summary::-webkit-details-marker { display: none; }

.ds-bloque > summary::before {
  content: "";
  width: 0;
  height: 0;
  border-top: 4px solid transparent;
  border-bottom: 4px solid transparent;
  border-left: 5px solid var(--n-400);
  transition: transform var(--dur-fast) var(--ease);
}

.ds-bloque[open] > summary::before { transform: rotate(90deg); }

.ds-bloque > summary:hover { background: var(--n-25); }

/* Recuento a la derecha del título: se lee sin desplegar, que es justo lo que
   permite dejarlo plegado sin perder información. */
.ds-bloque__cuenta {
  margin-left: auto;
  font-size: var(--fs-xs);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--n-500);
}

.ds-bloque__cuerpo {
  padding: var(--s-6);
  border-top: var(--bd);
}

.ds-bloque__cuerpo--flush { padding: 0; }

/* LAS ACCIONES DE UN BLOQUE VAN DEBAJO DE SU CONTENIDO, NO SOLO CUANDO ESTÁ
   VACÍO. El arquetipo B pliega cada bloque con su recuento, y el cuerpo va a
   sangre cuando lo que lleva dentro es una tabla — que entonces no deja sitio
   donde poner un «añadir otro». La consecuencia medida en Stock (ficha de
   producto, 2026-09-06): el botón de dar de alta un proveedor solo se pintaba
   en el estado vacío, así que un producto con UNA referencia ya no admitía la
   segunda desde su ficha.

   Es el pie del bloque, no el de la pantalla: `.ds-b__accion` es la acción del
   expediente entero y vive arriba, junto al título. */
.ds-bloque__pie {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-5);
  padding: var(--s-5) var(--s-6);
  border-top: var(--bd);
}

/* =========================================================================
   18. CRONOLOGÍA
   El cuerpo del arquetipo. La semilla real del repositorio es `episodio.css`
   de Coordinador; aquí se generaliza y se le corrige el defecto que el
   inventario le encontró: sus puntos comunicaban el estado solo con color.
   ========================================================================= */

.ds-crono {
  list-style: none;
  margin: 0;
  padding: 0;
}

.ds-crono__hito {
  position: relative;
  padding: 0 0 var(--s-7) var(--s-8);
  border-left: 1px solid var(--n-200);
}

.ds-crono__hito:last-child { border-left-color: transparent; padding-bottom: 0; }

.ds-crono__punto {
  position: absolute;
  left: calc(var(--s-2) * -1 - 1px);
  top: var(--s-1);
  width: var(--s-5);
  height: var(--s-5);
  border-radius: var(--r-pill);
  background: var(--n-0);
  border: 2px solid var(--n-300);
}

.ds-crono__hito--ok   .ds-crono__punto { border-color: var(--ok); background: var(--ok); }
.ds-crono__hito--warn .ds-crono__punto { border-color: var(--warn); background: var(--warn); }
.ds-crono__hito--risk .ds-crono__punto { border-color: var(--risk); background: var(--risk); }

.ds-crono__fecha {
  font-family: var(--font-cond);
  font-size: var(--fs-micro);
  font-weight: 600;
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
  color: var(--n-500);
  font-variant-numeric: tabular-nums;
}

.ds-crono__texto { margin-top: var(--s-2); }
.ds-crono__quien { margin-top: var(--s-2); font-size: var(--fs-xs); color: var(--n-500); }

/* =========================================================================
   19. VISTA PREVIA DE MENSAJE
   Lo que se va a mandar, ya con las variables de la plantilla sustituidas.
   No es un campo más del formulario: es la única oportunidad de leer el
   mensaje como lo va a leer el paciente, y por eso se saca del flujo de
   campos y se enmarca.

   Deliberadamente NO imita a WhatsApp: la versión anterior copiaba el papel
   pintado (#ECE5DD), la burbuja blanca y el pico, y eso es marca ajena dentro
   de una interfaz que tiene un solo acento (DESIGN.md §5). Lo que hace falta
   comunicar —«esto es un mensaje, no un input»— lo dice el marco.

   Sirve para cualquier canal (WhatsApp, SMS, correo, chat): el canal lo dice
   el título del modal, no el color de la caja.
   ========================================================================= */

.ds-msgprev {
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
}

.ds-msgprev__label {
  font-family: var(--font-cond);
  font-size: var(--fs-micro);
  font-weight: 600;
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
  color: var(--n-500);
}

/* `pre-wrap` porque los saltos de línea del mensaje son contenido: una
   plantilla con dos párrafos llega con dos párrafos. El ancho se acota en
   caracteres, no en píxeles, porque lo que se está previsualizando es texto
   corrido y en un modal de 880 px una línea de 120 caracteres no se parece
   en nada a la que va a ver el paciente en el móvil. */
.ds-msgprev__texto {
  max-width: 48ch;
  padding: var(--s-4) var(--s-5);
  border: var(--bd-strong);
  border-radius: var(--r-sm);
  background: var(--n-25);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  line-height: var(--lh-base);
}

/* LA IMAGEN QUE VIAJA CON EL MENSAJE (2026-08-09, con las campañas de
   fidelización de Comercial, donde lo que se envía ES una tarjeta con su pie).
   Se acota al mismo ancho que el texto: es la misma pieza, y verla más grande
   que el mensaje que la acompaña invierte el peso de lo que se está revisando.
   Va aquí y no en `apps/comercial.css` porque previsualizar un adjunto es del
   canal, no de la app: Mensajería y Recordatorios mandan imágenes igual. */
.ds-msgprev__img {
  max-width: min(100%, 48ch);
  height: auto;
  border: var(--bd-strong);
  border-radius: var(--r-sm);
  display: block;
}

/* LA CARA DE WHATSAPP, A PETICIÓN (CEO, 2026-10-26). El párrafo de arriba
   sigue valiendo como DEFECTO: la vista previa neutra sirve a cualquier canal.
   Pero en el alta de un plan, quien lo prepara tiene que reconocer de un
   vistazo qué le va a llegar al paciente al móvil —«el mensaje de whatsapp que
   imite a whatsapp»—, y ahí el canal no lo dice ningún título: lo dice la
   cara. Por eso es un MODIFICADOR que pide la pantalla, con sus colores en
   `--wa-*` (tokens, «superficie ajena»), y nunca el aspecto por defecto.

   Marcado: `.ds-msgprev.ds-msgprev--wa` > `.ds-wa__cab` (avatar · nombre ·
   número) + `.ds-wa__hilo` > `.ds-wa__burbuja` > `.ds-msgprev__texto` +
   `.ds-wa__meta`. Las negritas `*así*` las pinta el JS de quien la use
   (`pagos/js/mensaje_editar.js`, con `data-formato="whatsapp"`). */
.ds-msgprev--wa {
  gap: 0;
  max-width: 26rem;
  border: var(--bd-strong);
  border-radius: var(--r-md);
  overflow: hidden;
  background: var(--wa-fondo);
}

.ds-wa__cab {
  display: flex;
  align-items: center;
  gap: var(--s-5);
  padding: var(--s-4) var(--s-5);
  background: var(--wa-cab);
  color: var(--wa-tinta);
}

.ds-wa__avatar {
  display: grid;
  place-items: center;
  flex: none;
  width: var(--s-9);
  height: var(--s-9);
  border-radius: var(--r-pill);
  background: var(--wa-avatar);
  color: var(--wa-tenue);
  font-weight: 600;
}

.ds-wa__nombre { font-weight: 600; font-size: var(--fs-base); }
.ds-wa__num { font-size: var(--fs-xs); color: var(--wa-tenue); }

.ds-wa__hilo {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  padding: var(--s-7) var(--s-6);
}

.ds-wa__burbuja {
  max-width: 92%;
  padding: var(--s-4) var(--s-5) var(--s-3);
  border-radius: var(--r-md) 0 var(--r-md) var(--r-md);
  background: var(--wa-burbuja);
  color: var(--wa-tinta);
}

.ds-msgprev--wa .ds-msgprev__texto {
  max-width: none;
  padding: 0;
  border: 0;
  background: none;
  font-size: var(--fs-base);
}

.ds-wa__enlace { color: var(--wa-enlace); overflow-wrap: anywhere; }

.ds-wa__meta {
  display: block;
  margin-top: var(--s-2);
  text-align: right;
  font-size: var(--fs-micro);
  color: var(--wa-tenue);
}

.ds-wa__meta i { margin-left: var(--s-2); color: var(--wa-tick); }

/* =========================================================================
   19 BIS. RESUMEN ANTES DE CONFIRMAR
   Lo pidió el último paso del alta de un plan (CEO, 2026-10-26): ocho pares
   etiqueta/valor seguidos en una rejilla «parecen texto que se ha caído por
   la pantalla». Lo que se revisa antes de guardar no es una lista: son
   PREGUNTAS —quién, cuánto, cuándo, adónde va—, y cada una se contesta en su
   tarjeta, con la cifra que manda más grande que su contexto.

   Sin color de adorno (DESIGN.md §5): lo que separa las tarjetas es el borde,
   el escalón de fondo de la cabecera y el icono en gris.
   ========================================================================= */
.ds-resumen {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--s-5);
}

.ds-resumen__card {
  border: var(--bd-strong);
  border-radius: var(--r-md);
  background: var(--n-0);
  overflow: hidden;
}

.ds-resumen__titulo {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  padding: var(--s-4) var(--s-5);
  background: var(--n-50);
  border-bottom: var(--bd);
  font-family: var(--font-cond);
  font-size: var(--fs-micro);
  font-weight: 600;
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
  color: var(--n-600);
}

.ds-resumen__titulo i { color: var(--n-500); }

.ds-resumen__dl {
  display: flex;
  flex-direction: column;
  gap: var(--s-5);
  padding: var(--s-5);
}

.ds-resumen__dt { font-size: var(--fs-xs); color: var(--n-500); }
.ds-resumen__dd { margin: var(--s-1) 0 0; font-weight: 500; overflow-wrap: anywhere; }

/* La cifra que contesta la tarjeta (el total, el día): se lee primero. */
.ds-resumen__dd--cifra {
  font-size: var(--fs-lg);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* =========================================================================
   20. MANDO DE SIMULADOR
   Lo pidió el cuadro de mando de Cortex, que es el primer simulador del
   producto —cuatro ejes: PV, revisiones, urgencias y precio—. No es suyo: el
   simulador de negocio de Analytics y el de escenarios de pagos piden lo
   mismo, así que vive aquí y no en `apps/cortex.css`.

   `input[type=range]` no estaba vestido en ninguna parte del núcleo, así que
   hasta hoy salía con el aspecto por defecto del navegador — que es distinto
   en cada uno.
   ========================================================================= */

.ds-mando {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  padding-block: var(--s-3);
}

/* El rótulo y su valor comparten renglón: la cifra es lo que se mira al
   arrastrar, así que no puede estar lejos del pulgar. */
.ds-mando__label {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-3);
  font-family: var(--font-cond);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
  color: var(--n-600);
}

.ds-mando__val {
  font-family: var(--font-ui);
  font-size: var(--fs-md);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0;
  text-transform: none;
  color: var(--n-900);
}

.ds-mando__input {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 1.25rem;
  background: transparent;
  cursor: pointer;
}

/* El carril y el pulgar hay que declararlos por separado en cada motor: no
   existe una propiedad que valga para los dos. */
.ds-mando__input::-webkit-slider-runnable-track {
  height: 0.25rem;
  border-radius: var(--r-1);
  background: var(--n-200);
}
.ds-mando__input::-moz-range-track {
  height: 0.25rem;
  border-radius: var(--r-1);
  background: var(--n-200);
}

.ds-mando__input::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 1rem;
  height: 1rem;
  margin-top: -0.375rem;
  border-radius: 50%;
  background: var(--accent);
  border: 2px solid var(--n-0);
}
.ds-mando__input::-moz-range-thumb {
  width: 1rem;
  height: 1rem;
  border-radius: 50%;
  background: var(--accent);
  border: 2px solid var(--n-0);
}

.ds-mando__input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

.ds-mando__input:disabled { cursor: not-allowed; opacity: 0.5; }


/* =========================================================================
   21. ESCALA — un valor frente a su referencia
   «Yo» contra «la media de mis compañeros» sobre una regla. Lo pidió el
   cuadro de mando de Cortex, donde vivía como `cm-peers-bar` con los
   marcadores posicionados por `style="left: calc(...)"` ESCRITO EN LA
   PLANTILLA, o sea CSS en línea calculado en el servidor.

   Sigue haciendo falta pasar la posición desde el servidor —es un dato, no
   una decisión de estilo—, pero ahora va por una variable propia
   (`--ds-escala-pos`) en vez de por una propiedad de layout: la plantilla
   dice DÓNDE está el valor y la hoja decide cómo se dibuja eso.
   ========================================================================= */

.ds-escala {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
}

.ds-escala__track {
  position: relative;
  height: 0.375rem;
  border-radius: var(--r-1);
  background: var(--n-100);
}

.ds-escala__marca {
  position: absolute;
  top: 50%;
  left: var(--ds-escala-pos, 0%);
  width: 0.75rem;
  height: 0.75rem;
  margin-left: -0.375rem;
  border-radius: 50%;
  transform: translateY(-50%);
  border: 2px solid var(--n-0);
}

.ds-escala__marca--propio { background: var(--accent); z-index: 1; }
.ds-escala__marca--ref { background: var(--n-400); }

.ds-escala__pies {
  display: flex;
  justify-content: space-between;
  font-size: var(--fs-xs);
  color: var(--n-500);
}

/* La leyenda lleva PALABRA, no solo el punto de color: dos círculos que solo
   se distinguen por el tono no los lee un daltónico (§5). */
.ds-escala__leyenda {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
}

.ds-escala__punto {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  display: inline-block;
}
.ds-escala__punto--propio { background: var(--accent); }
.ds-escala__punto--ref { background: var(--n-400); }


/* =========================================================================
   22. IMPRESIÓN
   Añadido el 2026-08-07, con el guion de grabación de Claudia Voz — que es una
   pantalla cuyo destino ES el papel: se lee en voz alta delante de un
   micrófono, y con el móvil en la mano no se graba.

   Sube al núcleo, y no se queda en la hoja de esa app, porque no tiene nada de
   suyo: apagar el armazón al imprimir vale para las trece. Era además la
   tercera vez que hacía falta —el dossier de PV y los documentos PDF lo
   resuelven cada uno por su cuenta— aunque esos dos son otra cosa: los genera
   WeasyPrint desde el servidor y no pasan por aquí. Esto es para las pantallas
   que se imprimen DESDE EL NAVEGADOR.

   `body:has(.ds-app)` mantiene la regla inerte donde el sistema todavía no se
   ha pedido (§2): una pantalla sin migrar sigue imprimiéndose como siempre.
   Sin `!important` a propósito —la única excepción escrita sigue siendo
   `.ds-hidden`—; el peso lo da la especificidad, y por eso este bloque va al
   final del fichero.
   ========================================================================= */

@media print {
  body:has(.ds-app) .sidebar-universal,
  body:has(.ds-app) .sidebar-overlay,
  body:has(.ds-app) .top-bar,
  body:has(.ds-app) .action-bar,
  body:has(.ds-app) .skip-link,
  .ds-app .ds-tabs,
  .ds-app .ds-filters,
  .ds-app .ds-volver { display: none; }

  /* Lo que la pantalla declara que no se imprime: avisos, cifras de contexto y
     la propia acción de imprimir. Es marcado y no una lista de componentes
     porque solo la pantalla sabe qué de lo suyo sobra en papel.

     EL SEGUNDO SELECTOR NO SOBRA, y se comprobó midiendo: los arquetipos cargan
     DESPUÉS de este fichero, así que `.ds-b__accion { display: flex }` empataba
     en especificidad y ganaba por posición — el botón de imprimir se imprimía—.
     Con `.ds-app` delante sube a (0,2,0) y gana sin `!important`, cuya única
     excepción escrita sigue siendo `.ds-hidden`. Los componentes de ESTE
     fichero no necesitaban el refuerzo (van antes y pierden por posición), pero
     se cubren igual: la diferencia no se puede recordar al escribir marcado. */
  .ds-no-imprimir,
  .ds-app .ds-no-imprimir { display: none; }

  /* Las regiones con scroll dejan de recortar: en papel no hay scroll, y lo
     que no cabe en la caja simplemente no se imprimiría. */
  .ds-app .ds-a__body,
  .ds-app .ds-b__cuerpo,
  .ds-app .ds-c__body { overflow: visible; height: auto; }

  /* Ni sombras ni fondos de pantalla: tinta gastada en decoración. */
  .ds-app .ds-panel { border-color: var(--n-300); box-shadow: none; }
}

/* =========================================================================
   14. CONFIRMACIÓN ESCRITA
   La pieza que `archetypes/e-formulario.css` dejó anotada como pendiente:
   §3-E pide que «las acciones destructivas o de alcance global pidan
   confirmación escrita», y ahí se decidió que su sitio era el núcleo —es un
   componente, no una regla de layout— y su caso, la app Configuración,
   «donde hay más de uno».

   ESCRITO EL 2026-08-09, con ese caso ya en la mesa: el panel de Seguridad
   tiene un botón «Limpiar todo» que borra TODOS los registros de intentos
   fallidos, y su única defensa era un `confirm()` del navegador. Un
   `confirm()` se despacha con la barra espaciadora sin haberlo leído; teclear
   una palabra obliga a mirar qué se va a borrar.

   Marcado:

     <form method="post" action="…" class="ds-confirm" data-confirm="BORRAR">
       {% csrf_token %}
       <p class="ds-confirm__aviso">Esto borra … y no se puede deshacer.</p>
       <label class="ds-field__label" for="x">Escribe BORRAR para continuar</label>
       <input class="ds-input ds-confirm__campo" id="x" autocomplete="off">
       <button class="ds-btn ds-btn--danger ds-confirm__ok" disabled>Borrar</button>
     </form>

   El botón nace `disabled` en el HTML a propósito: si el JS no llega a
   cargarse, la acción no se puede disparar. La alternativa —habilitado y que
   el JS lo deshabilite— deja la puerta abierta justo cuando algo ha fallado.
   ========================================================================= */

.ds-confirm {
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
  padding: var(--s-5);
  border: var(--bd-w-signal) solid var(--risk);
  border-radius: var(--r-sm);
  background: var(--risk-bg);
}

.ds-confirm__aviso {
  font-size: var(--fs-sm);
  color: var(--risk-ink);
}

.ds-confirm__campo { max-width: 22ch; }

/* Deshabilitado NO es solo un color más flojo: el cursor también lo dice, que
   es lo que se nota antes de leer. */
.ds-app .ds-confirm__ok:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* =========================================================================
   23. ZONA DE SOLTAR UN FICHERO
   Sube al núcleo el 2026-08-09, con PADI. El criterio de §2 —un componente
   entra cuando el repositorio ya lo tiene escrito varias veces— se cumple con
   holgura: hay SEIS implementaciones independientes del mismo gesto
   (`padi_dropzone.js`, `contabilidad.js`, `mensajeria.js`, los dos asistentes
   de Gestión Taller y el formulario de laboratorio de Configuración), cada una
   con su nombre de clase y su color de resalte.

   Gestión Taller ya la había resuelto dentro del sistema nuevo con dos reglas
   en su hoja de app (`.taller-drop`), y PADI la necesitaba idéntica: es el
   momento exacto en que copiar habría sido la segunda bifurcación. Se
   generaliza aquí y Taller pasa a consumirla — el mismo movimiento que hizo
   `includes/pagination_ds.html` al llegar su segundo consumidor.

   Se monta SOBRE `.ds-empty`, que ya centra icono, título y texto: lo único
   que aporta esto es el marco discontinuo —que es lo que dice «aquí cabe algo
   que todavía no está»— y el acuse del arrastre.

   El elemento es un `<label for="…">` con su `<input type="file" hidden>`
   dentro: así se abre el selector con un clic Y con el teclado sin escribir
   una línea de JS. El JS solo hace falta para el arrastre, que no tiene
   equivalente nativo.
   ========================================================================= */

.ds-drop {
  border: 1px dashed var(--n-300);
  border-radius: var(--r-md);
  cursor: pointer;
}

/* Encima del sitio, con el fichero cogido. Es acuse de un gesto en curso, no
   un estado de la pantalla, así que aquí el acento sí vale: nadie lo ve sin
   estar arrastrando algo. */
.ds-drop--activa {
  border-color: var(--accent);
  background: var(--accent-bg);
}

/* Los ficheros elegidos, debajo del texto. Va en tabular porque suelen ser
   nombres con fechas y números de consulta que se comparan de un vistazo. */
.ds-drop__ficheros {
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
  color: var(--n-600);
  overflow-wrap: anywhere;
}

/* =========================================================================
   24. ACUSE FLOTANTE
   Añadido el 2026-08-09, con Mensajería. Una pantalla que actúa sin recargar
   —enviar un mensaje, guardar una etiqueta, vincular a un seguimiento— no
   tiene dónde poner el «hecho» de Django, así que cada app que lo necesitaba
   se inventaba el suyo. Aquí no es del arquetipo D: lo pide cualquier
   pantalla que escriba por AJAX.

   Dos reglas que lo hacen honesto:
   - **El error no se cuenta aquí.** Un acuse se va solo a los tres segundos,
     así que solo puede llevar lo que da igual perderse. Un fallo que exige
     hacer algo va en `.ds-note` dentro de la pantalla, donde se queda. La
     variante `--risk` existe para el fallo que ya no tiene arreglo («no se
     pudo enviar»), no para el que lo tiene.
   - **`aria-live`**, o para quien usa lector de pantalla el acuse no existe.
   ========================================================================= */

.ds-toast {
  position: fixed;
  left: 50%;
  bottom: var(--s-8);
  z-index: 1200;
  max-width: 44ch;
  padding: var(--s-5) var(--s-7);
  border-radius: var(--r-md);
  background: var(--n-800);
  color: var(--n-0);
  font-size: var(--fs-base);
  box-shadow: var(--sh-overlay);
  opacity: 0;
  transform: translateX(-50%) translateY(var(--s-8));
  transition: opacity var(--dur-base) var(--ease),
              transform var(--dur-base) var(--ease);
  pointer-events: none;
}

.ds-toast--visible {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

/* El fallo lleva icono además de color, como todo estado del sistema (§5). */
.ds-toast--risk { background: var(--risk); }

/* =========================================================================
   25. NAVEGACIÓN INFERIOR — el armazón de una app instalada en el móvil

   La escribe el núcleo y no una app porque ya se había reconstruido DOS veces
   por separado (`static/club_dental/css/club_dental.css` y su gemelo de
   estética), que es el mismo camino por el que la página de ayuda acabó
   escrita tres veces (§11). La tercera es la del Coordinador, y aquí se para.

   Es chrome de MÓVIL, no un componente de pantalla: por encima del punto de
   ruptura de móvil desaparece y manda la fila de secciones (`ds-tabs`), que es
   donde el sistema pone la navegación cuando hay ancho. Una barra inferior en
   un monitor de 27" no es una decisión, es un descuido.

   ⚠️ EL RELEVO — `.ds-nav-relevo` (2026-08-17, con la app del doctor)

   Esa frase de arriba —«manda la fila de secciones»— era una intención, no un
   mecanismo: el núcleo esconde la barra inferior por encima de 640 px y **nadie
   enseñaba la otra**. Cada armazón tenía que pintar las DOS a la vez, así que en
   el teléfono se pintaban las dos: en la app del doctor, los mismos cuatro
   destinos arriba y abajo, y ese alto es justo el que falta con el teclado
   abierto (DESIGN.md §6.1). Y al retirar la de arriba, medido en el armazón
   real, quedaban **0 destinos alcanzables a 1.440 px** —el armazón del doctor se
   parte por ROL, así que un doctor con ordenador es un caso real, no
   hipotético—.

   Lo que faltaba era la mitad complementaria de la pareja, y va **aquí**, dentro
   de este mismo `@media`: el armazón marca su fila de ancho con
   `ds-nav-relevo` y el sistema la esconde **exactamente donde enseña la barra
   inferior**. Una sola condición de anchura gobierna las dos, escrita una vez y
   en el núcleo. Un armazón que copiase el literal `641px` en su hoja de app
   tendría el umbral escrito dos veces —y DESIGN.md §6 solo se lo permite a
   `base.css` y a `archetypes/`—: el día que el punto de ruptura se moviera, las
   dos barras aparecerían juntas otra vez, o ninguna.

   No declara `display` en su estado visible **a propósito**: se compone con lo
   que sea la fila (hoy siempre `ds-tabs`, que ya es `flex` y envuelve), así que
   el relevo no puede romper su maquetación ni duplicar su valor. Es solo la
   condición.
   ========================================================================= */

.ds-nav-inferior { display: none; }

@media (max-width: 640px) {
  .ds-nav-inferior {
    position: fixed;
    inset: auto 0 0 0;
    z-index: 900;
    display: flex;
    /* El indicador de inicio de iOS se come la última fila si no se aparta. */
    padding-bottom: env(safe-area-inset-bottom, 0px);
    border-top: var(--bd);
    background: var(--n-0);
  }

  /* La otra mitad de la pareja: donde entra la barra inferior, se retira la
     fila de ancho del armazón. Va en este mismo bloque para que las dos
     dependan de UNA condición (ver el porqué en la cabecera de la sección).
     Gana a `.ds-tabs { display: flex }` por posición —misma especificidad, y
     esta sección va después—; no se sube a `!important` por eso mismo. */
  .ds-nav-relevo { display: none; }

  .ds-nav-inferior__item {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--s-2);
    /* 44px de objetivo táctil (§6), aunque el modo compacto esté activo. */
    min-height: var(--s-11);
    padding: var(--s-3) var(--s-2);
    color: var(--n-500);
    font-family: var(--font-cond);
    font-size: var(--fs-micro);
    font-weight: 600;
    letter-spacing: var(--track-micro);
    text-transform: uppercase;
    text-decoration: none;
  }

  .ds-nav-inferior__item[aria-current="page"] { color: var(--accent); }

  /* El acento no basta: el color solo no es una señal accesible (§5). */
  .ds-nav-inferior__item[aria-current="page"]::before {
    content: "";
    position: absolute;
    top: 0;
    width: var(--s-9);
    height: var(--bd-w-signal);
    background: var(--accent);
  }

  .ds-nav-inferior__item { position: relative; }

  .ds-nav-inferior__i { font-size: var(--fs-lg); }

  .ds-nav-inferior__n {
    position: absolute;
    top: var(--s-2);
    left: 50%;
    min-width: var(--s-6);
    margin-left: var(--s-3);
    padding: 0 var(--s-2);
    border-radius: var(--r-pill);
    background: var(--risk);
    color: var(--n-0);
    font-size: var(--fs-micro);
    line-height: var(--s-6);
    text-align: center;
  }

  /* Sin esto la barra tapa la última fila de la pantalla, que suele ser la
     acción. El alto se declara aquí y no en cada app. */
  .ds-app--con-nav-inferior {
    padding-bottom: calc(var(--s-11) + var(--s-6) + env(safe-area-inset-bottom, 0px));
  }
}

/* =========================================================================
   26. DIRECTORIO — la ficha de un contacto
   Una rejilla de personas: nombre, cómo llamarles y una nota. No es una
   tabla y no debe serlo, y la diferencia es de USO: una tabla se recorre de
   arriba abajo comparando la misma columna —cuánto, cuándo, quién— y aquí no
   se compara nada. Se BUSCA a alguien concreto, se le mira el teléfono y se
   sale. Para eso, cinco columnas con cuatro celdas medio vacías cada una es
   la peor forma posible: obliga a leer en horizontal para reunir tres datos
   que van juntos.

   Nació en la Libreta de Recepción (2026-08-17), que es la agenda de los
   teléfonos que NO son de pacientes —doctores que derivan, proveedores,
   mensajería—. Está aquí y no en una hoja de app porque «un directorio de
   personas» lo pide cualquiera: el equipo en OdontoHR, los colaboradores en
   DentalPartners, los socios del Club.

   Tres decisiones, y las tres son de fondo:

   1. **La inicial en un disco, no una foto.** Aquí no hay fotos y no las va a
      haber; un hueco gris con un icono genérico repetido veinte veces es
      ruido, y la inicial sí discrimina —le da a cada ficha una marca distinta
      con la que el ojo vuelve al sitio— sin inventarse un dato.
   2. **El teléfono es el contenido, no un metadato.** Es a lo que se viene:
      va en su renglón, en numeración tabular para que dos números se comparen
      en vertical, y con `tel:` para que se pueda pulsar.
   3. **Las acciones no compiten.** Editar y quitar van arriba a la derecha,
      en fantasma y con `aria-label`: siempre visibles —revelarlas al pasar el
      ratón deja la ficha sin acciones en una tableta— pero sin peso visual.
   ========================================================================= */

.ds-directorio {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
  gap: var(--s-5);
}

.ds-contacto {
  display: flex;
  align-items: flex-start;
  gap: var(--s-5);
  padding: var(--s-5) var(--s-6);
  border: var(--bd);
  /* `--r-md` y no `--r-lg`: el grande está reservado a modales y hojas
     laterales (tokens.css), y una tarjeta con radio de modal se lee como algo
     que se ha abierto encima. */
  border-radius: var(--r-md);
  background: var(--n-0);
  transition: border-color var(--dur-fast) var(--ease);
}

.ds-contacto:hover { border-color: var(--n-300); }

/* La ficha que se está editando ARRIBA. Con el formulario fuera de la rejilla
   (ver el editor, abajo) ésta es la única señal de a quién se está tocando: un
   anillo de acento, que es lo mismo que dice una fila seleccionada en una
   tabla. No se tiñe el fondo —el disco de la inicial ya es `--accent-bg` y se
   perdería dentro—. */
.ds-contacto--activo {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent);
}

.ds-contacto__avatar {
  flex: none;
  display: grid;
  place-items: center;
  width: 2.5rem;
  height: 2.5rem;
  border-radius: var(--r-pill);
  background: var(--accent-bg);
  color: var(--accent-600);
  font-size: var(--fs-md);
  font-weight: 600;
  line-height: 1;
  text-transform: uppercase;
}

.ds-contacto__cuerpo { min-width: 0; flex: 1; }

.ds-contacto__nombre {
  font-weight: 600;
  color: var(--n-800);
  overflow-wrap: anywhere;
}

/* Categoría y demás rótulos, pegados al nombre. */
.ds-contacto__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-3);
  margin-top: var(--s-2);
}

.ds-contacto__tels {
  margin-top: var(--s-4);
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
}

.ds-contacto__tel {
  display: inline-flex;
  align-items: center;
  gap: var(--s-4);
  color: var(--n-700);
  font-variant-numeric: tabular-nums;
}

.ds-contacto__tel:hover { color: var(--accent-600); }

.ds-contacto__tel i {
  color: var(--n-400);
  font-size: var(--fs-xs);
}

.ds-contacto__nota {
  margin-top: var(--s-4);
  font-size: var(--fs-xs);
  color: var(--n-500);
  overflow-wrap: anywhere;
}

.ds-contacto__acciones {
  flex: none;
  display: flex;
  gap: var(--s-2);
}

/* ---- EL EDITOR: uno solo, en el mismo sitio siempre ----------------------
   La primera versión editaba EN SU CELDA —el formulario ocupaba la fila
   entera de la rejilla, donde estaba la ficha—. Se usó y falla por tres
   sitios (CEO, 2026-08-17, sobre una libreta de 101 contactos):

   1. **El formulario aparece EN MEDIO de las fichas.** Entre la fila 2 y la 4
      no se lee como un formulario: se lee como una tarjeta rota. Y para
      llegar a él hay que buscarlo, que es justo lo que un editor no puede
      pedir.
   2. **El alta iba a todo lo ancho**: cuatro campos de 350px de una punta a
      otra de un monitor de 1.500. La etiqueta y su campo quedan a dos palmos
      y el ojo vuelve a empezar en cada uno — es el mismo motivo por el que
      el arquetipo E acota en `--form-max`, y aquí no se aplicaba.
   3. **Editar movía el formulario de sitio**: al pulsar el lápiz, la caja en
      la que se iba a escribir aparecía en otra parte de la página.

   Hoy hay UN editor, y está siempre donde estaba: una banda clavada
   (`sticky`) arriba de la región que scrollea, con la caja dentro acotada.
   Pulsar «editar» **no mueve nada** — cambia lo que hay dentro de la caja que
   ya estaba ahí, y la lista le sigue pasando por debajo.

   Dos decisiones que no son cosmética:

   - **Lleva sombra, y es de las poquísimas cosas fuera de un modal que la
     lleva** (tokens.css: la sombra dice que algo FLOTA sobre el documento).
     Aquí eso es literalmente cierto: las fichas pasan por debajo. Sin ella,
     una banda del mismo blanco que la lista se lee como la primera fila.
   - **En el móvil deja de estar clavada.** Debajo de 641px la región no es el
     scrollport —scrollea la página, ver `a-tabla.css`— y un formulario fijo
     de 200px sobre una pantalla de 640 se come un tercio de la lista.
   ------------------------------------------------------------------------ */

/* -------------------------------------------------------------------------
   DOS COLUMNAS: EL FORMULARIO AL LADO, NO ENCIMA (2026-09-01)

   El editor era una banda clavada ARRIBA y la rejilla iba debajo. Con la
   pantalla ya cargada de capas —cabecera, descripción, pestañas, barra de
   filtros— al llegar a las fichas no quedaba alto: en un portátil se veía
   media fila de contactos y todo lo demás era formulario. Lo reportó el CEO
   con la captura delante: «con la construcción en capas llega un momento que
   no queda espacio para los contactos».

   La forma es un grid de dos columnas dentro de la región que ya scrollea, con
   el formulario **sticky**: la lista se recorre entera y el editor se queda
   donde está. No se anida un segundo scrollport, y no es pereza — anidar uno
   dentro de `.ds-a__body` exige un alto definido que este armazón no cede, y
   es exactamente el taponamiento que documenta `.ds-b__lista-scroll` en
   `b-flujo.css`: la región deja de bajar y el gesto se queda muerto.

   Debajo de 1024px vuelven a apilarse: dos columnas en 900px dejan las fichas
   en 500px y la rejilla ya se auto-ajusta a 20rem.
   ------------------------------------------------------------------------- */

.ds-directorio-cols {
  display: grid;
  gap: var(--s-6);
  align-items: start;
}

@media (min-width: 1024px) {
  .ds-directorio-cols {
    grid-template-columns: minmax(0, 22rem) minmax(0, 1fr);
  }

  /* SIN FORMULARIO NO HAY DOS COLUMNAS. El editor solo se pinta en la libreta
     y cuando se está editando; en la pestaña de pacientes —y en el filtro de
     personal sin nada abierto— el grid seguía reservando sus 22rem y metía
     TODAS las fichas en esa franja, con media pantalla en blanco al lado. Lo
     reportó el CEO con la captura: «cuando muestras pacientes sí que puedes
     mostrar toda la pantalla de cards».

     Se resuelve con `:has()` y no con una clase condicional en la plantilla
     porque quien sabe si hay formulario es el DOM: el marcado ya lo decide una
     vez con su `{% if %}`, y repetirlo en una clase es la segunda copia de la
     misma condición esperando a divergir. `:has()` es técnica ya asentada en
     este proyecto — el armazón cede el alto con `:has(.ds-a)`. */
  .ds-directorio-cols:not(:has(> .ds-directorio-editor)) {
    grid-template-columns: minmax(0, 1fr);
  }

  /* Al lado, el editor deja de ser una banda: ni fondo escalonado, ni sombra,
     ni borde inferior a lo ancho. Es una caja más. */
  .ds-directorio-cols > .ds-directorio-editor {
    position: sticky;
    top: var(--s-5);
    padding: 0;
    border-bottom: 0;
    background: none;
    box-shadow: none;
  }

  .ds-directorio-cols > .ds-directorio-editor .ds-directorio-editor__caja {
    max-width: none;
  }

  /* La cabecera del panel es una fila de flex y en 22rem no cabe: el título,
     el distintivo de a quién se edita y los dos botones se salen de la caja
     por la derecha. Aquí envuelve. `--s-3` de `row-gap` porque al envolver el
     hueco vertical entre dos filas de controles se lee como separación de
     bloques si es el mismo `--s-5` que el horizontal.

     Y `min-width: 0` en el distintivo: un flex item no baja de su contenido
     por defecto, así que un nombre largo empujaba a los botones fuera aunque
     la fila envolviera. */
  .ds-directorio-cols .ds-panel__head {
    flex-wrap: wrap;
    row-gap: var(--s-3);
  }

  .ds-directorio-cols .ds-panel__head > .ds-badge {
    min-width: 0;
  }

  /* `.ds-push` empuja el grupo de botones a la derecha con `margin-left:auto`.
     Al envolver, eso los deja colgando en una fila propia contra el borde: se
     retira para que caigan a la izquierda, alineados con el título. */
  .ds-directorio-cols .ds-panel__head > .ds-push {
    margin-left: 0;
  }
}

.ds-directorio-editor {
  position: sticky;
  top: 0;
  z-index: 2;
  padding: var(--s-5) var(--s-6);
  border-bottom: var(--bd-strong);
  /* El escalón de fondo es lo que separa la banda de la lista (DESIGN.md §1:
     las superficies se distinguen con borde y escalón, no con sombra). `--n-50`
     y no `--n-25`: la lista de fichas es blanca sobre blanco y el medio tono
     de más arriba no llegaba a leerse como otra superficie. */
  background: var(--n-50);
  box-shadow: var(--sh-overlay);
}

.ds-directorio-editor__caja {
  max-width: var(--form-max);
  margin-inline: auto;
}

/* Editando: marco de acento y cabecera teñida. Es el único aviso de que ese
   formulario NO va a crear un contacto nuevo, y el estado no se comunica solo
   por color (DESIGN.md §5): el título dice «Editar» y la cabecera lleva el
   nombre de quien se está editando. */
.ds-directorio-editor--abierto .ds-directorio-editor__caja { border-color: var(--accent); }

.ds-directorio-editor--abierto .ds-panel__head {
  background: var(--accent-bg);
  border-radius: var(--r-md) var(--r-md) 0 0;
}

@media (max-width: 640px) {
  .ds-directorio-editor {
    position: static;
    box-shadow: none;
  }
}

/* =========================================================================
   5.g FILA DE DETALLE — «¿por qué está esta persona en la lista?»
   Marcado: `<table data-ds-detalle>` y, DETRÁS de cada fila de resumen, una
   `<tr class="ds-fila-detalle" hidden>` con un único `<td colspan>` dentro.
   El disparador está en `nucleo.js`.

   NO es lo mismo que `data-ds-expandible` (§5.a), y por eso son dos cosas:
   aquel revela las celdas `.ds-col--sec` **solo dentro del media query de
   móvil** —es «enseñar las columnas que la tarjeta esconde»—, y aquí lo que
   hay que enseñar no está en ninguna columna. La cola de Comercial es el caso
   que lo pide: la fila dice «tres motivos distintos» y el detalle dice CUÁLES,
   con su estado y su acción. Ponerlo en columnas costaría tres renglones por
   persona en una tabla que existe justamente para tener una fila por persona.

   Se conserva la convención del otro mecanismo entera —`data-expanded`,
   `aria-expanded`, `tabindex`, e ignorar los clics sobre lo interactivo— para
   no tener un tercer vocabulario de «pulsa aquí» en el producto.

   La fila de detalle es un `<tr>` de la MISMA tabla y no un panel flotante:
   así hereda el ancho de las columnas, se imprime, y el navegador la anuncia
   dentro de la fila a la que pertenece.
   ========================================================================= */

.ds-table[data-ds-detalle] tbody tr:has(+ tr.ds-fila-detalle) {
  cursor: pointer;
}

/* La señal de que hay algo debajo. Sin ella el detalle existe y no lo
   encuentra nadie, que es el fallo mudo que ya costó §5.a. */
.ds-table[data-ds-detalle] tbody tr:has(+ tr.ds-fila-detalle) td:first-child::after {
  content: "▾";
  margin-left: var(--s-3);
  color: var(--n-400);
}

.ds-table[data-ds-detalle]
  tbody tr[data-expanded="true"]:has(+ tr.ds-fila-detalle) td:first-child::after {
  content: "▴";
}

.ds-table[data-ds-detalle] tbody tr[data-expanded="true"] { background: var(--n-25); }

.ds-fila-detalle > td {
  background: var(--n-25);
  padding: var(--s-5) var(--s-6);
  border-bottom: var(--bd);
}

.ds-detalle {
  display: flex;
  flex-direction: column;
  gap: var(--s-5);
}

.ds-detalle__lista {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
}

.ds-detalle__item {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-4);
  padding: var(--s-4) var(--s-5);
  background: var(--n-0);
  border: var(--bd-strong);
  border-radius: var(--r-md);
}

.ds-detalle__titulo { font-weight: 600; }

/* El renglón secundario de un item: área, estado, responsable, antigüedad.
   Va en la misma línea mientras quepa y baja entero cuando no. */
.ds-detalle__meta {
  color: var(--n-500);
  font-size: var(--fs-xs);
}

/* Las acciones se van al final de la línea, nunca al principio: lo primero
   que hay que leer es de qué va esto. */
.ds-detalle__acciones {
  margin-left: auto;
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
}

@media (max-width: 640px) {
  /* En modo tarjeta la celda ya es un bloque; lo que sobra es el rótulo
     fantasma que `--responsive` añade con `::before`. Se suprime con
     `data-label=""`, igual que en la columna de acciones. */
  .ds-detalle__acciones { margin-left: 0; width: 100%; }
}

/* El pie del detalle: acciones que son de LA PERSONA y no de una de sus
   fichas — hoy solo el opt-out comercial de la cola única. Separado por un
   filete y alineado a la derecha para que no se lea como una acción más de la
   última ficha de la lista, que es justo el error que costaría caro aquí: lo
   que hay debajo es irreversible. */
.ds-detalle__pie {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--s-3);
  padding-top: var(--s-4);
  border-top: var(--bd);
}

/* =========================================================================
   27. LA CONVERSACIÓN
   Añadido el 2026-08-07, con el chat de Claudia. El fichero nació con el ETL
   Hub, que es «el estado de algo que se mueve solo», y por eso hasta ahora
   solo tenía panel de salud: semáforo, piezas, avisos. Pero DESIGN.md §3-D
   describe DOS formas —«el contenido crece por abajo y el compositor está
   anclado», «indicadores de estado de sesión»— y esa segunda no existía en
   ninguna parte del núcleo. Es la unidad de trabajo de Mensajería y de
   Claudia: LA CONVERSACIÓN.

   Se escribe aquí y no en la hoja de Claudia porque Mensajería la va a pedir
   igual, y la lección de las trece bifurcaciones es exactamente esa. Va sin
   prefijo de arquetipo —`ds-chat`, no `ds-d__chat`— por el mismo criterio que
   `.ds-etapas` en el B: es una pieza reutilizable, no la estructura de la
   pantalla.

   LO QUE NO SE CONSERVA DEL CHAT ANTERIOR, y es el motivo de la mitad de este
   bloque: era una **terminal negra** con el verde de marca de prompt. §3-D lo
   prohíbe por escrito —«fondo neutro, no oscuro, para no romper con el resto
   de la plataforma»— y no era una excusa estética: la consola vivía en el
   mismo portal claro que el resto y se leía como una ventana ajena incrustada.
   Lo que sí se conserva es la voz monoespaciada de los turnos, que aquí SÍ
   informa: se está leyendo lo que un modelo genera palabra a palabra, y la
   anchura fija es lo que deja comparar dos respuestas.

   ⚠️ VIVIÓ EN `archetypes/d-tiempo-real.css` HASTA EL 2026-08-31, con el
   párrafo de arriba —«es una pieza reutilizable, no la estructura de la
   pantalla»— desmintiendo dónde estaba. Se mudó aquí el día que hizo falta en
   otro arquetipo: la curación del banco de pruebas es **C** —se lee un caso,
   no se vigila algo que se mueve— y `base_eval.html` carga UNA hoja de
   arquetipo, la que nombra la vista. La conversación que hay que juzgar no
   podía pintarse como conversación, y el fallo habría sido el mudo de siempre:
   las clases se escriben, no dan error y no pintan.
   ========================================================================= */

.ds-chat {
  display: flex;
  flex-direction: column;
  min-height: 0;
  height: 100%;
  background: var(--n-0);
}

/* LA CONVERSACIÓN COMO CONTENIDO, no como panel vivo. En el panel dividido el
   hilo es la única región que scrollea y el compositor se queda anclado abajo;
   eso es correcto cuando la pantalla ES la conversación y falso cuando la
   conversación es una PRUEBA dentro de un documento que se lee entero. Sin
   este modificador, un hilo dentro de un panel de arquetipo C se encierra en
   su propia caja con su propia barra, y leer cuatro mensajes cuesta dos
   scrolls anidados —el de la página y el suyo—, que es la forma más segura de
   que nadie llegue al último turno.

   Solo apaga el scroll: el relleno del hilo, las burbujas y la alternancia
   izquierda/derecha son los mismos. Quien lo use pone el cuerpo del panel a
   sangre (`ds-panel__body--flush`), o el relleno se cuenta dos veces. */
.ds-chat--estatico { height: auto; }

.ds-chat--estatico .ds-chat__hilo {
  flex: none;
  overflow: visible;
}

/* Cabecera: quién contesta y en qué estado está la sesión. */
.ds-chat__cab {
  display: flex;
  align-items: center;
  gap: var(--s-5);
  padding: var(--s-5) var(--s-6);
  border-bottom: var(--bd-strong);
}

.ds-chat__quien {
  font-weight: 600;
  display: flex;
  align-items: baseline;
  gap: var(--s-4);
  min-width: 0;
}

/* Quien contesta puede llamarse «María Dolores Sánchez Peñalver»: sin tope, en
   una cabecera flex el nombre parte por palabras y en móvil se comía cuatro
   renglones, empujando fuera de pantalla el botón de la derecha. Se trunca. */
.ds-chat__quien > * {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Estado de sesión CON TEXTO, no solo el punto: §3-D lo pide expresamente y
   §5 lo exige. El punto reutiliza `.ds-d__punto`, que ya distingue por forma. */
.ds-chat__estado {
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
  font-family: var(--font-cond);
  font-size: var(--fs-micro);
  font-weight: 600;
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
  color: var(--n-500);
}

/* EL HILO. La única región que hace scroll, y crece por abajo. */
.ds-chat__hilo {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: var(--s-6);
  display: flex;
  flex-direction: column;
  gap: var(--s-5);
}

.ds-chat__turno {
  max-width: 76ch;
  padding: var(--s-5) var(--s-6);
  border-radius: var(--r-md);
  background: var(--n-50);
  font-size: var(--fs-base);
  line-height: var(--lh-base);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* LA VOZ MONOESPACIADA ES UN MODIFICADOR, NO EL DEFECTO (2026-08-09, con
   Mensajería). El turno nació en mono con Claudia y el motivo estaba escrito:
   «se lee lo que un modelo genera palabra a palabra, y la anchura fija es lo
   que deja comparar dos respuestas». Eso es cierto de una consola de modelo y
   falso de una conversación con un paciente, que es texto humano y se lee
   mejor proporcional. Como es una propiedad de TODO el hilo y no de un turno
   suelto, el modificador va en el contenedor: `.ds-chat--maquina`. La traza
   técnica conserva la mono siempre, porque ahí sigue informando. */
.ds-chat--maquina .ds-chat__turno,
.ds-chat--maquina .ds-chat__texto,
.ds-chat__traza {
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
}

/* Lo que escribe la persona va a la derecha y en acento suave: es uno de los
   cinco sitios del acento —la selección— y aquí hace de «esto lo he dicho yo».
   La alternancia izquierda/derecha es lo que permite recorrer un hilo largo
   sin leer quién habla en cada turno. */
.ds-chat__turno--propio {
  align-self: flex-end;
  background: var(--accent-bg);
}

.ds-chat__turno--sistema {
  align-self: center;
  max-width: 68ch;
  background: none;
  color: var(--n-500);
  font-size: var(--fs-xs);
  text-align: center;
}

/* El coste de la respuesta (tiempo, tokens/s) y cualquier otra anotación de
   máquina: se lee si se busca y no compite con el texto. */
.ds-chat__meta {
  align-self: flex-start;
  display: flex;
  align-items: center;
  gap: var(--s-3);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
  color: var(--n-500);
}

/* Volcado técnico dentro del hilo: la traza de una decisión, un JSON. Conserva
   los saltos y no se estira: es para leerlo, no para escanearlo. */
.ds-chat__traza {
  align-self: stretch;
  max-width: none;
  background: var(--n-25);
  border: var(--bd-strong);
}

.ds-chat__fila { display: flex; gap: var(--s-5); }
.ds-chat__rotulo { flex: none; font-weight: 600; color: var(--n-600); }
.ds-chat__fila--tenue { color: var(--n-500); }
.ds-chat__fila--salida { color: var(--ok-ink); }
.ds-chat__fila--fallo  { color: var(--risk-ink); }

/* EL COMPOSITOR, ANCLADO ABAJO. Es el otro requisito literal de §3-D. */
.ds-chat__compositor {
  /* Posicionado porque los popover del compositor (§29) se anclan a él con
     `bottom: 100%`. Sin esto su ancestro posicionado más cercano es el panel
     entero y la caja de emoticonos se abre FUERA de la pantalla, por arriba. */
  position: relative;
  display: flex;
  align-items: flex-end;
  gap: var(--s-5);
  padding: var(--s-5) var(--s-6);
  border-top: var(--bd-strong);
  background: var(--n-25);
}

.ds-chat__texto {
  flex: 1;
  min-height: var(--ctl-h);
  max-height: 8rem;
  resize: none;
  font-size: var(--fs-base);
}

/* Barra de contexto entre el hilo y el compositor: los parámetros con los que
   se está simulando. No es parte del hilo —no es algo que alguien dijera— así
   que va fuera de la región que scrollea. */
.ds-chat__contexto {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-5);
  padding: var(--s-4) var(--s-6);
  border-top: var(--bd);
  background: var(--n-25);
  font-size: var(--fs-xs);
}

/* En pantalla estrecha la cabecera y el compositor envuelven. Es el mismo
   defecto que DESIGN.md §3-B documenta para el expediente —cabecera `nowrap`
   correcta en escritorio que en móvil expulsa la acción fuera de la pantalla—
   y aquí se daba dos veces: el nombre partía en cuatro renglones y el
   compositor dejaba la caja de escribir en 60px de ancho. Medido a 390px. */
@media (max-width: 640px) {
  .ds-chat__hilo { padding: var(--s-5); }
  .ds-chat__turno { max-width: 100%; }
  .ds-chat__compositor { padding: var(--s-5); flex-wrap: wrap; }
  .ds-chat__cab { flex-wrap: wrap; }

  /* La caja de escribir sube a su propio renglón y los botones quedan debajo:
     en un teléfono el ancho de la caja importa más que la fila de iconos. */
  .ds-chat__texto {
    order: -1;
    flex: 1 1 100%;
  }
}

/* =========================================================================
   28. LA CONVERSACIÓN HUMANA
   Lo que le faltaba a `ds-chat` (§27) para servir a Mensajería: separador de
   día, entrega del mensaje y meta dentro de la burbuja.
   ========================================================================= */

/* El turno entrante ocupa la izquierda con fondo blanco y contorno: en un hilo
   de dos voces, blanco contra `--accent-bg` separa más que dos grises. */
.ds-chat__turno--entrante {
  align-self: flex-start;
  background: var(--n-0);
  border: var(--bd-strong);
}

.ds-chat__turno--seleccionable { cursor: pointer; }

.ds-chat__turno--seleccionado {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

/* EL TURNO BAJO ESCRUTINIO, que no es lo mismo que un turno seleccionado: no
   lo ha elegido nadie con el ratón, es el motivo de que la pantalla exista —el
   mensaje que hay que etiquetar en la curación, el que disparó una alerta—. Va
   con `--foco` y NUNCA solo con el contorno: encima lleva su rótulo
   (`.ds-chat__dia--foco`), que es la palabra que §5 exige al lado del color.

   El acento es el mismo de la selección porque es el mismo trabajo —«este de
   aquí»— y el sistema tiene un solo acento. El contorno se separa dos píxeles
   en vez de uno: el turno en foco convive con burbujas propias, que ya son
   `--accent-bg`, y a un píxel el contorno se leía pegado a ellas. */
.ds-chat__turno--foco {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* LA RÁFAGA: varios mensajes seguidos que son UN turno.
   Añadido el 2026-09-04 con la curación, y va en el núcleo y no en la hoja de
   la app porque una ráfaga es un hecho de `mensajeria` —lo que alguien escribe
   seguido, `rafagas.VENTANA_RAFAGA`— y la bandeja la va a pedir igual.

   El defecto que corrige: la pantalla de etiquetar cosía la ráfaga en UNA
   burbuja con UNA hora, y **210 de los 493 casos de la primera tanda eran
   ráfagas de 2+**. Quien etiquetó no podía ver que eran tres mensajes ni con
   qué ritmo se escribieron, y el ritmo cambia el sentido: «perdón por las
   molestias» pegado detrás de «ayer no pude ir» es una disculpa.

   Con UN solo mensaje esto no pinta nada —ni línea, ni hueco, ni separación
   extra—, que es el otro requisito: la mayoría de los casos son de uno y no se
   les puede añadir ruido. */
.ds-chat__rafaga {
  align-self: flex-start;
  max-width: 100%;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--s-2);
}

/* EL RESALTE ABARCA LA RÁFAGA ENTERA, no cada burbuja.
   `--foco` (arriba) marca UN turno bajo escrutinio; cuando el turno son tres
   mensajes, tres contornos a dos píxeles se tocan y se leen como tres cosas
   distintas — justo lo contrario de lo que hay que ver. El contorno se sube al
   grupo, y la palabra sigue estando encima (`.ds-chat__dia--foco`): las dos
   cosas y no solo el color, que es lo que exige §5. */
.ds-chat__rafaga--foco {
  outline: 2px solid var(--accent);
  outline-offset: var(--s-4);
  border-radius: var(--r-md);
}

/* EL NEXO: la línea que encadena dos mensajes de la ráfaga, y cuya ALTURA dice
   cuánto tiempo pasó entre ellos.

   ⚠️ La altura sale de `data-hueco`, nunca de un `style="height:…"` en el
   marcado: el tramo lo decide el servicio (`curacion.TRAMOS_DEL_HUECO`) y la
   escala vive aquí, que es donde el tema puede alcanzarla.

   ⚠️ Y NO es una escala lineal. El hueco está acotado en 3 minutos por la
   ventana de la ráfaga, así que a tamaño real 2 s es invisible y 170 s rompe
   la pantalla. Son cuatro tramos con cuatro alturas, y cada uno se lee también
   **en cifra** (`__nexo-hueco`) y **en palabra** (el `title`): una señal que
   solo es geometría no la lee quien va rápido. */
.ds-chat__nexo {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  margin-left: var(--s-7);
  height: var(--ds-nexo-h, var(--s-4));
}

.ds-chat__nexo-linea {
  width: var(--s-1);
  height: 100%;
  background: var(--n-300);
}

.ds-chat__nexo-hueco {
  font-family: var(--font-cond);
  font-size: var(--fs-micro);
  font-variant-numeric: tabular-nums;
  color: var(--n-500);
}

.ds-chat__nexo[data-hueco="seguido"]  { --ds-nexo-h: var(--s-4);  }
.ds-chat__nexo[data-hueco="segundos"] { --ds-nexo-h: var(--s-7);  }
.ds-chat__nexo[data-hueco="medio"]    { --ds-nexo-h: var(--s-9);  }
.ds-chat__nexo[data-hueco="largo"]    { --ds-nexo-h: var(--s-11); }

/* Separador de día. No es un turno —nadie lo dijo— así que no lleva burbuja. */
.ds-chat__dia {
  align-self: center;
  padding: var(--s-2) var(--s-5);
  border-radius: var(--r-pill);
  background: var(--n-100);
  font-family: var(--font-cond);
  font-size: var(--fs-micro);
  font-weight: 600;
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
  color: var(--n-600);
}

/* POR QUÉ LÍNEA SALIÓ ESTE MENSAJE — la variante del separador de día.
   Añadido el 2026-09-11, con el desplegable de línea de Mensajería.

   Es el mismo hecho que el separador de día: algo cambió A PARTIR DE AQUÍ y no
   lo dijo nadie, así que no puede ser una burbuja. Reutiliza la pastilla
   centrada y solo añade el punto del color de la línea, que llega como DATO en
   una custom property (`--msg-instancia`) — el nombre va escrito al lado, que
   es lo que §5 exige.

   ⚠️ Lleva `.ds-chat__dia` para heredar la pastilla, así que **quien recorra
   los separadores de DÍA tiene que excluir este modificador**: en Mensajería
   son dos lectores (el día del último bloque pintado y el salto de la búsqueda
   por fecha) y sin el `:not()` los dos se creen que un cambio de línea es un
   cambio de día. */
.ds-chat__dia--linea {
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
  background: var(--n-50);
  border: var(--bd-strong);
  text-transform: none;
  letter-spacing: normal;
}

/* Pie de la burbuja: canal, hora y entrega. */
.ds-chat__pie {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--s-3);
  margin-top: var(--s-3);
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
  color: var(--n-500);
}

/* LA LÍNEA, dentro del pie. Un dato más del envío, al lado del canal y la
   hora: el canal dice «por WhatsApp» y esto dice «por CUÁL de los números».

   No se pinta la burbuja con el color de la línea, y es deliberado: el fondo
   ya significa dirección (propio/entrante) y un tercer eje de color destruye
   los dos primeros. Aquí el peso lo lleva el nombre escrito; el punto solo
   acompaña, así que la burbuja se sigue leyendo en escala de grises. Los
   mensajes sin línea conocida no pintan nada —es «no consta», y un hueco es
   honesto donde inventar no lo es—. */
.ds-chat__linea {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
}

/* ENTREGA — y aquí está la corrección de un defecto real, no de estilo.
   «Entregado» y «leído» pintaban los dos `✓✓` y se distinguían SOLO por el
   color del segundo, que §5 prohíbe y que con deuteranopia no existe. Ahora la
   escala se lee por FORMA y por PALABRA: pendiente (reloj), enviado (un tick),
   entregado (dos ticks), leído (dos ticks + la palabra) y fallido (icono + «no
   enviado»). El color se queda como refuerzo del que ya lo dice todo sin él. */
.ds-chat__entrega {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
}

.ds-chat__entrega--leido  { color: var(--info-ink); }
.ds-chat__entrega--fallo  { color: var(--risk-ink); font-weight: 600; }

/* Adjunto dentro de la burbuja. */
.ds-chat__adjunto {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  margin-bottom: var(--s-4);
  padding: var(--s-4) var(--s-5);
  border-radius: var(--r-sm);
  background: var(--n-100);
  font-size: var(--fs-sm);
}

/* BUSCAR DENTRO DEL HILO. Una barra bajo la cabecera, no un popover: mientras
   se busca hay que seguir viendo la conversación, y lo que se teclea acota lo
   que se está mirando. Es del núcleo y no de Mensajería porque la bandeja de
   Claudia tiene el mismo hilo y va a pedir lo mismo. */
.ds-chat__buscar {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  padding: var(--s-4) var(--s-6);
  border-bottom: var(--bd-strong);
  background: var(--n-25);
}

/* `.ds-input` es `width: 100%`: dentro de la barra el texto se reparte lo que
   sobra y la fecha se queda con su ancho natural, o el selector nativo se
   estruja hasta no poder pulsarlo. */
.ds-chat__buscar-q     { flex: 1; min-width: 8rem; }
.ds-chat__buscar-fecha { flex: none; width: auto; }

/* El contador y las tres teclas son una pieza y envuelven juntas. */
.ds-chat__buscar-nav {
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--s-4);
  margin-left: auto;
}

.ds-chat__buscar-n {
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* LA COINCIDENCIA. Todas se marcan con fondo; la que se está mirando lleva
   ADEMÁS contorno y negrita, que es forma: distinguir «la activa» de «las
   demás» solo por el matiz del fondo es exactamente lo que §5 no permite. */
.ds-chat__match {
  background: var(--warn-bg);
  color: inherit;
  border-radius: var(--r-sm);
}

.ds-chat__match--activa {
  outline: 2px solid var(--warn);
  outline-offset: 1px;
  font-weight: 600;
}

/* El separador del día al que ha saltado la búsqueda por fecha: sin esto, un
   salto a un día sin coincidencias de texto no deja ninguna señal de haber
   ocurrido y parece que el control no hace nada. */
.ds-chat__dia--diana {
  background: var(--warn-bg);
  color: var(--warn-ink);
}

/* El rótulo del turno en foco. Misma pieza que el separador de día —una
   pastilla centrada, que no es un turno porque nadie la dijo— y en el acento,
   para que la palabra y el contorno de la burbuja de debajo digan lo mismo. */
.ds-chat__dia--foco {
  background: var(--accent-bg);
  color: var(--accent);
}

/* A un ancho de móvil no caben los dos campos y las teclas en un renglón: el
   texto se queda con el suyo entero y abajo van la fecha y la navegación. */
@media (max-width: 640px) {
  .ds-chat__buscar { flex-wrap: wrap; padding: var(--s-4) var(--s-5); }
  .ds-chat__buscar-q { flex-basis: 100%; }
}

/* =========================================================================
   29. EL POPOVER DEL COMPOSITOR
   Emoticonos y plantillas: dos cajas que se abren sobre el compositor y se
   cierran al elegir. No es un modal —no bloquea, no vela la pantalla y no
   pide confirmación— y por eso no reutiliza `.ds-modal`.
   ========================================================================= */

.ds-chat__pop {
  position: absolute;
  bottom: 100%;
  left: 0;
  right: 0;
  z-index: 20;
  max-height: 18rem;
  overflow-y: auto;
  border: var(--bd-strong);
  border-radius: var(--r-md) var(--r-md) 0 0;
  background: var(--n-0);
  box-shadow: var(--sh-overlay);
}

.ds-chat__pop-cab {
  position: sticky;
  top: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  padding: var(--s-4) var(--s-5);
  border-bottom: var(--bd);
  background: var(--n-0);
  font-family: var(--font-cond);
  font-size: var(--fs-micro);
  font-weight: 600;
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
  color: var(--n-500);
}

.ds-chat__pop-grupo { border-bottom: var(--bd); }
.ds-chat__pop-grupo:last-child { border-bottom: none; }

.ds-chat__pop-grupo-tit {
  padding: var(--s-3) var(--s-5);
  background: var(--n-25);
  font-family: var(--font-cond);
  font-size: var(--fs-micro);
  font-weight: 600;
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
  color: var(--n-500);
}

.ds-chat__pop-item {
  display: block;
  width: 100%;
  padding: var(--s-4) var(--s-5);
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}

.ds-chat__pop-item:hover { background: var(--n-50); }

.ds-chat__emojis {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(2.25rem, 1fr));
  gap: var(--s-2);
  padding: var(--s-4);
}

.ds-chat__emoji {
  aspect-ratio: 1;
  border: 0;
  border-radius: var(--r-sm);
  background: none;
  font-size: var(--fs-lg);
  line-height: 1;
  cursor: pointer;
}

.ds-chat__emoji:hover { background: var(--n-50); }


/* =========================================================================
   25. LA OPCIÓN QUE SE RECONOCE POR SU LOGO
   Sube al núcleo el 2026-09-03, al cortar el puente de Contabilidad.

   Es el mismo problema que `.ds-picker` resolvió para el almacén: **hay listas
   donde el identificador no es el texto**. Nadie lee «Banco Santander, cuenta
   terminada en 4900»: se reconoce el logo rojo. Un `<select>` nativo no admite
   imágenes dentro de un `<option>`, y de ahí que Contabilidad tuviera
   construido a mano un desplegable entero desde JavaScript —`icono-dropdown*`,
   19 reglas y ~120 líneas—, con la nota escrita en su plantilla: «el núcleo no
   tiene ese componente; queda anotado como hueco del núcleo». Esto lo cierra.

   Son DOS, y la diferencia importa:

   · `.ds-selicono` — **elegir de una lista larga**: cuentas bancarias, tarjetas,
     métodos de cobro. Viste a un `<select>` de verdad, que sigue en el marcado y
     es lo que se envía: si el JavaScript no corre, el `<select>` nativo queda
     visible y operativo. No lo sustituye, lo viste.
   · `.ds-iconos` — **elegir el icono en sí**, de un juego corto y conocido de
     antemano (una docena de logos de banco). Ahí no hay lista que desplegar: se
     ven todos a la vez, son `<input type="radio">` de verdad y el foco y el
     teclado los da el navegador.

   ⚠️ El icono NUNCA va solo. En `.ds-selicono` el nombre viaja al lado en el
   menú, y en `.ds-iconos` debajo de cada casilla: un logotipo es un color y una
   forma, o sea justo lo que §5 no admite como única señal. Y la opción vacía se
   dice con `—` y borde discontinuo, no solo con la ausencia de imagen.
   ========================================================================= */

/* ---- Elegir de una lista: el <select> vestido ------------------------- */

.ds-selicono { position: relative; display: inline-block; vertical-align: middle; }

/* El <select> nativo, una vez construido el desplegable: fuera de la vista pero
   dentro del formulario. `clip` y no `display: none` para que siga siendo la
   fuente de verdad y el respaldo sin JavaScript. */
.ds-selicono__native {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  opacity: 0;
  pointer-events: none;
  clip-path: inset(50%);
}

.ds-selicono__trigger {
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
  max-width: 100%;
  min-height: var(--ctl-h);
  padding: 0 var(--s-4);
  border: var(--bd-strong);
  border-radius: var(--r-sm);
  background: var(--n-0);
  line-height: 1;
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease);
}

.ds-selicono__trigger:hover { border-color: var(--n-400); }
.ds-selicono__trigger[aria-expanded="true"] { border-color: var(--accent); }
.ds-selicono__trigger:focus-visible { box-shadow: var(--focus); outline: 0; }

/* Sin elegir: el borde discontinuo lo dice sin depender del color, igual que
   `.ds-drop` dice «aquí se suelta algo». */
.ds-selicono__trigger--vacio { border-style: dashed; }

.ds-selicono__img { display: block; width: 22px; height: 22px; border-radius: var(--r-sm); }
.ds-selicono__fa { width: 20px; text-align: center; color: var(--n-500); }
.ds-selicono__none { padding: 0 var(--s-2); font-weight: 600; color: var(--n-400); }

.ds-selicono__caret {
  margin-left: auto;
  padding-left: var(--s-2);
  font-size: var(--fs-xs);
  color: var(--n-400);
}

.ds-selicono__menu {
  position: absolute;
  top: calc(100% + var(--s-2));
  left: 0;
  z-index: 1080;
  min-width: 200px;
  max-height: 320px;
  overflow-y: auto;
  padding: var(--s-2);
  border: var(--bd-strong);
  border-radius: var(--r-lg);
  background: var(--n-0);
  box-shadow: var(--sh-overlay);
  display: none;
}

.ds-selicono[data-abierto] > .ds-selicono__menu { display: block; }

.ds-selicono__item {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  width: 100%;
  padding: var(--s-3) var(--s-4);
  border: 0;
  border-radius: var(--r-sm);
  background: none;
  color: var(--n-700);
  font: inherit;
  font-size: var(--fs-sm);
  text-align: left;
  cursor: pointer;
}

.ds-selicono__item:hover { background: var(--n-50); }

/* El elegido: fondo Y peso. Solo con el fondo, en un menú de logos de colores,
   no se distingue del que tienes debajo del ratón. */
.ds-selicono__item[aria-selected="true"] {
  background: var(--accent-bg);
  color: var(--accent-600);
  font-weight: 600;
}

.ds-selicono__label { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }

/* En una ficha el desplegable ocupa el ancho del campo, no el de su contenido. */
.ds-selicono--ancho,
.ds-selicono--ancho > .ds-selicono__trigger { display: flex; width: 100%; }

/* ---- Elegir el icono: el juego corto, todo a la vista ----------------- */

.ds-iconos {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-5);
}

/* 72px y no 56: el ancho lo manda el ROTULO, no el icono. Con 56 no cabía
   «Automático» sin partirse por la mitad («Automátic / o»), y ninguna de las dos
   salidas fáciles vale — recortar con puntos suspensivos deja «Banco Sa…» en
   Santander y en Sabadell, o sea el rótulo dejando de desambiguar justo entre
   las dos casillas que más se parecen. */
.ds-iconos__opt {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 72px;
  cursor: pointer;
}

/* El radio de verdad, invisible pero presente: el foco, las flechas del teclado
   y el envío del formulario los sigue dando el navegador. */
.ds-iconos__opt > input {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  opacity: 0;
  clip-path: inset(50%);
}

.ds-iconos__tile {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  border: 2px solid var(--n-200);
  border-radius: var(--r-lg);
  background: var(--n-0);
  color: var(--n-400);
  transition: border-color var(--dur-fast) var(--ease);
}

.ds-iconos__tile > img { width: 30px; height: 30px; }

.ds-iconos__opt > input:checked ~ .ds-iconos__tile {
  border-color: var(--accent);
  background: var(--accent-bg);
}

.ds-iconos__opt > input:focus-visible ~ .ds-iconos__tile { box-shadow: var(--focus); }

/* El nombre debajo, siempre: ver el aviso de la cabecera.

   ENVUELVE, no recorta, y es lo único que lo hace servir para algo. Con
   `text-overflow: ellipsis` sobre 56px, «Banco Santander» y «Banco Sabadell»
   salían los dos como «Banco Sa…»: el rótulo dejaba de desambiguar justo entre
   las dos casillas que más se parecen, y entonces lo único que las distingue es
   el color del logo — que es exactamente lo que el aviso de la cabecera dice
   que no puede pasar. Dos renglones cuestan 12px y se leen. */
.ds-iconos__nombre {
  max-width: 72px;
  margin-top: var(--s-2);
  font-size: var(--fs-micro);
  line-height: var(--lh-tight);
  color: var(--n-500);
  text-align: center;
  overflow-wrap: break-word;
}

.ds-iconos__opt > input:checked ~ .ds-iconos__nombre {
  color: var(--accent-600);
  font-weight: 600;
}


/* =========================================================================
   30. EL MENÚ DESPLEGABLE
   Un disparador que abre una lista de opciones sobre el documento. Sube al
   núcleo el 2026-09-11, con el desplegable de línea de Mensajería.

   Faltaba, y se notaba: el núcleo tenía `.ds-multi` (§16) —que es un FILTRO
   con búsqueda y selección múltiple— y `.ds-chat__pop` (§29) —que se ancla al
   compositor y es suyo—, pero no la pieza corriente de «elegir una cosa de
   entre pocas». Sin ella, quien la necesitara la habría escrito en su hoja de
   app, que es el camino por el que se llegó a las trece bifurcaciones.

   LO QUE ESTE COMPONENTE NO HACE, y es su razón de ser: **esconder lo que no
   se puede elegir**. Una opción ausente no explica nada, y quien está en el
   mostrador no se queda mirando el hueco — coge el móvil. Por eso la opción
   inservible se queda EN LA LISTA, apagada y con el motivo escrito debajo, y
   por eso `aria-disabled` en vez de `disabled`: un botón `disabled` no se
   puede enfocar, así que con teclado y lector de pantalla el motivo —que es
   justo lo que hay que oír— no existiría. Quien escribe el menú ignora el
   clic; el sistema deja leerlo.
   ========================================================================= */

.ds-menu { position: relative; display: inline-flex; }

/* El disparador se COMPONE con lo que ya sea —un `.ds-badge`, un `.ds-btn`—;
   aquí solo se le quita el cromado de botón del navegador. */
.ds-menu__trigger { border: 0; cursor: pointer; }

.ds-menu__caret {
  font-size: var(--fs-micro);
  transition: transform var(--dur-fast) var(--ease);
}

.ds-menu__trigger[aria-expanded="true"] .ds-menu__caret { transform: rotate(180deg); }

/* Se abre hacia abajo y crece hacia la IZQUIERDA (`right: 0`): el disparador
   vive en cabeceras donde a su derecha ya hay acciones, y un panel que creciera
   hacia allí se saldría del panel —que en el arquetipo D recorta con `clip`—.
   El tope en `vw` es para el teléfono: 24rem son 384px y no caben en un móvil
   de 390 sin comerse los dos márgenes. */
.ds-menu__panel {
  position: absolute;
  top: calc(100% + var(--s-2));
  right: 0;
  z-index: 30;
  min-width: 17rem;
  max-width: min(24rem, 86vw);
  max-height: 22rem;
  overflow-y: auto;
  border: var(--bd-strong);
  border-radius: var(--r-md);
  background: var(--n-0);
  box-shadow: var(--sh-overlay);
}

.ds-menu__op {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--s-2);
  width: 100%;
  padding: var(--s-4) var(--s-5);
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}

.ds-menu__op + .ds-menu__op { border-top: var(--bd); }
.ds-menu__op:hover { background: var(--n-50); }

/* La elegida en acento —es la «fila seleccionada» de DESIGN.md §5, uno de los
   cinco sitios del acento—, y NUNCA solo en acento: quien la escribe le pone
   además su marca de comprobada. */
.ds-menu__op[aria-checked="true"] { background: var(--accent-bg); }

/* Apagada, no ausente. Se enfoca y se lee; lo único que no hace es responder. */
.ds-menu__op[aria-disabled="true"] { color: var(--n-500); cursor: not-allowed; }
.ds-menu__op[aria-disabled="true"]:hover { background: none; }

.ds-menu__op-tit {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  width: 100%;
  font-weight: 600;
}

/* El badge de estado se va al canto derecho de su renglón. */
.ds-menu__op-tit .ds-badge { margin-left: auto; }

/* El MOTIVO. Envuelve —`white-space: normal`, porque dentro de una cabecera
   `nowrap` lo heredaría y saldría una línea de 400 caracteres— y es lo único
   que convierte una opción apagada en información. */
.ds-menu__op-nota {
  font-size: var(--fs-xs);
  line-height: var(--lh-base);
  color: var(--n-500);
  white-space: normal;
}

/* =========================================================================
   31. TABLERO POR COLUMNAS — «dónde está cada cosa», de solo lectura
   Añadido el 2026-09-15 con el tablero de leads de Analytics → Marketing
   (T2 de docs/marketing_embudo_leads_maestro.md), que es su primer caso real:
   cinco fases de un embudo, una columna por fase y una tarjeta por lead.

   NO es `.ds-etapas` (b-flujo.css §3): aquella es UN expediente y la etapa en
   la que está. Esto son MUCHOS elementos repartidos entre etapas. Tampoco es
   un kanban: las tarjetas no se arrastran, porque lo que las mueve son los
   datos. Si algún día una pantalla quiere mover tarjetas a mano, eso es otra
   pieza con otro contrato (confirmación, deshacer), no un modificador de esta.

   Contrato:
   - Cada columna lleva SU NOMBRE y su total escritos; la tarjeta, su estado
     en `.ds-badge` con icono y palabra. Nada se comunica solo con color.
   - El total y la suma de la columna son de la columna ENTERA: si la pantalla
     recorta tarjetas (tope), lo dice en `.ds-tablero__pie`.
   - Una tarjeta puede ser `<a>` (navegable) o `<div>`; se ven igual salvo el
     `hover`, que solo tiene la navegable.
   - Escritorio y tableta: columnas lado a lado con desplazamiento horizontal
     PROPIO del tablero (nunca de la página). Por debajo de 640px se apilan.
   ========================================================================= */

.ds-tablero {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(12rem, 1fr);
  gap: var(--s-5);
  align-items: start;
  overflow-x: auto;
  padding-bottom: var(--s-2);
}

.ds-tablero__col {
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
  min-width: 0;
  padding: var(--s-5);
  border-top: var(--bd-w-signal) solid var(--n-200);
  border-radius: var(--r-md);
  background: var(--n-50);
}

.ds-tablero__cab {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
}

.ds-tablero__nombre {
  margin: 0;
  font-family: var(--font-cond);
  font-size: var(--fs-micro);
  font-weight: 600;
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
  color: var(--n-600);
}

.ds-tablero__total {
  margin: 0;
  font-size: var(--fs-lg);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--n-800);
}

.ds-tablero__suma,
.ds-tablero__pie,
.ds-tablero__vacio {
  margin: 0;
  font-size: var(--fs-xs);
  color: var(--n-500);
  font-variant-numeric: tabular-nums;
}

.ds-tablero__lista {
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
  margin: 0;
  padding: 0;
  list-style: none;
}

.ds-tablero__tarjeta {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  padding: var(--s-4) var(--s-5);
  border: var(--bd-strong);
  border-radius: var(--r-md);
  background: var(--n-0);
  font-size: var(--fs-sm);
  color: inherit;
}

a.ds-tablero__tarjeta { text-decoration: none; }
a.ds-tablero__tarjeta:hover { border-color: var(--n-400); color: inherit; text-decoration: none; }

.ds-tablero__tit {
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.ds-tablero__meta {
  font-size: var(--fs-xs);
  color: var(--n-500);
  overflow-wrap: anywhere;
}

.ds-tablero__cifras {
  font-size: var(--fs-xs);
  color: var(--n-700);
  font-variant-numeric: tabular-nums;
}

.ds-tablero__marcas {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
}

@media (max-width: 640px) {
  .ds-tablero {
    grid-auto-flow: row;
    grid-auto-columns: auto;
    overflow-x: visible;
  }
}

/* =========================================================================
   32. LOS MENSAJES DE DJANGO — una tarjeta, no una franja por mensaje
   Añadido el 2026-10-11 tras el CEO: «es un error general de lydent.es».
   Una acción en «Configuración → Marketing» deja 7–15 mensajes, y cada
   armazón los pintaba uno debajo de otro, cada uno una franja con su relleno:
   **empujaban el contenido hacia abajo** y la vista quedaba inútil hasta que se
   desvanecían. Y además había CINCO maneras de pintarlos —`alert` de app.css,
   `ds-note`, `cc-message`, `aseso-message`, una sin botón de cerrar—, que es
   el sitio donde una se rompe y no se nota.

   Contrato (marcado en `templates/includes/ds_avisos.html`, el único que la
   escribe; comportamiento en `nucleo.js` → `prepararAvisos`):
   - **Flota, no empuja.** `position: fixed` arriba a la derecha, bajo la barra
     superior. Por encima del contenido, del menú lateral (300) y de la barra
     inferior del móvil (900); por DEBAJO de los desplegables del armazón
     (1000), de los modales (1100) y del acuse (1200): un menú que abres o un
     modal que te pide algo mandan sobre un «guardado».
   - **Un renglón por mensaje**, en `--fs-sm`, y la lista desplaza dentro de la
     tarjeta a partir del 60 % del alto: quince mensajes no tapan la pantalla.
   - **Cabecera con la cuenta y UNA X** que cierra la tarjeta entera. Con un
     solo mensaje no hay cabecera: el mensaje y su X.
   - **Tono por renglón con icono además de color** (y la palabra para el
     lector de pantalla), y el canto de la tarjeta lleva el del PEOR mensaje:
     un error entre diez «hecho» no puede leerse verde.
   - **Solo se cierra sola si todo es éxito o información.** Un aviso o un
     error que se va solo es un aviso que nadie leyó.
   ========================================================================= */

.ds-avisos {
  position: fixed;
  top: calc(var(--topbar-h) + var(--s-4));
  right: var(--s-6);
  z-index: 950;
  display: flex;
  flex-direction: column;
  width: min(30rem, 100vw - 2 * var(--s-6));
  max-height: 60vh;
  border: var(--bd-strong);
  border-left: var(--bd-w-signal) solid var(--info);
  border-radius: var(--r-md);
  background: var(--n-0);
  box-shadow: var(--sh-overlay);
  font-size: var(--fs-sm);
  line-height: var(--lh-tight);
  color: var(--n-800);
  transition: opacity var(--dur-base) var(--ease),
              transform var(--dur-base) var(--ease);
}

/* `display: flex` le gana al atributo `hidden` del navegador. */
.ds-avisos[hidden] { display: none; }

.ds-avisos--ok   { border-left-color: var(--ok); }
.ds-avisos--info { border-left-color: var(--info); }
.ds-avisos--warn { border-left-color: var(--warn); }
.ds-avisos--risk { border-left-color: var(--risk); }

/* La salida: se desliza un poco y se apaga antes de desaparecer del DOM. */
.ds-avisos--saliendo {
  opacity: 0;
  transform: translateY(calc(-1 * var(--s-4)));
}

.ds-avisos__cab {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  flex: none;
  padding: var(--s-2) var(--s-2) var(--s-2) var(--s-5);
  border-bottom: var(--bd);
}

.ds-avisos__cuenta {
  font-weight: 600;
  color: var(--n-700);
  font-variant-numeric: tabular-nums;
}

.ds-avisos__cerrar {
  flex: none;
  margin-left: auto;
  font-size: var(--fs-md);
}

.ds-avisos__lista {
  margin: 0;
  padding: 0;
  list-style: none;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.ds-avisos__item {
  display: flex;
  align-items: flex-start;
  gap: var(--s-4);
  padding: var(--s-3) var(--s-5);
}

.ds-avisos__item + .ds-avisos__item { border-top: var(--bd); }

/* Con un solo mensaje la X va en el renglón: todo se centra en él y la X se
   arrima al borde. */
.ds-avisos__item:has(> .ds-avisos__cerrar) { align-items: center; }
.ds-avisos__item:has(> .ds-avisos__cerrar) .ds-avisos__i { margin-top: 0; }
.ds-avisos__item .ds-avisos__cerrar { margin-right: calc(-1 * var(--s-3)); }

.ds-avisos__i {
  flex: none;
  display: inline-grid;
  place-items: center;
  width: var(--s-7);
  height: var(--s-7);
  margin-top: calc(-1 * var(--s-1));
  border-radius: var(--r-pill);
  font-size: var(--fs-micro);
  font-weight: 700;
  line-height: 1;
}

.ds-avisos__item--ok   .ds-avisos__i { color: var(--ok-ink);   background: var(--ok-bg); }
.ds-avisos__item--info .ds-avisos__i { color: var(--info-ink); background: var(--info-bg); }
.ds-avisos__item--warn .ds-avisos__i { color: var(--warn-ink); background: var(--warn-bg); }
.ds-avisos__item--risk .ds-avisos__i { color: var(--risk-ink); background: var(--risk-bg); }

/* El error se lee también en el texto: es lo único que no se cierra solo y lo
   primero que hay que encontrar entre quince renglones. */
.ds-avisos__item--risk .ds-avisos__texto { color: var(--risk-ink); }

.ds-avisos__texto {
  flex: 1;
  min-width: 0;
  overflow-wrap: anywhere;
}

@media (max-width: 640px) {
  .ds-avisos {
    right: var(--s-6);
    left: var(--s-6);
    width: auto;
  }
}

/* =========================================================================
   33. LISTA DE DATOS DE UNA FICHA (`.ds-a__dl`)
   Vivía en `archetypes/a-tabla.css` porque nació en su panel de detalle, pero
   la usan también ~20 fichas de arquetipo B —el mandato de pagos, el niño de
   PADI, el trabajo del taller, la traza de Claudia— y las que no cargaban la
   hoja del A la pintaban SIN ESTILO: cuatro rótulos apilados en texto llano
   que se comían diez renglones encima de la tabla (medido en la ficha del
   plan 38 de CDCPM, CEO 2026-10-25). Sube al núcleo con su nombre, que ya
   llevan las plantillas.
   ========================================================================= */

/* Lista de datos del detalle: rótulo condensado encima del valor. Es la
   misma firma de la cabecera de columna, para que el detalle se lea como
   una fila puesta en vertical. */
.ds-a__dl {
  display: grid;
  gap: var(--s-6);
}

.ds-a__dt {
  font-family: var(--font-cond);
  font-size: var(--fs-micro);
  font-weight: 600;
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
  color: var(--n-500);
}

.ds-a__dd {
  margin-top: var(--s-2);
  font-size: var(--fs-base);
}

.ds-a__dd--num { font-variant-numeric: tabular-nums; }

/* LA MISMA LISTA, EN REJILLA. El `.ds-a__dl` de arriba apila porque nació en
   `.ds-a__detail`, que es un panel lateral estrecho: ahí una sola columna es lo
   correcto. Dentro de una `tr.ds-fila-detalle` la caja es el ANCHO DE LA TABLA,
   y apilar seis rótulos ahí gasta seis renglones para pintar seis palabras —el
   detalle acaba siendo más alto que las diez filas que lo rodean.

   Sube al arquetipo y no a la hoja de una app (DESIGN.md §2) porque no es de
   una app: las seis colas de Comercial lo piden a la vez, y `altas`, `pagos`,
   `padi`, `etl_hub` y `centralita` ya dejaron escrito en su cabecera que usan
   este mismo componente. La primera que lo escribiera en su hoja obligaría a
   las otras cinco a copiarlo, que es la bifurcación que este sistema cura.

   `auto-fit` y no un número de columnas: el detalle de una cola tiene cuatro
   campos y el de otra siete, y en una tableta caben dos. Un `repeat(3, …)`
   fijo sería un punto de ruptura escrito a mano fuera de donde §6 lo permite. */
.ds-a__dl--rejilla {
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: var(--s-5) var(--s-6);
}
