/* =========================================================================
   NÚCLEO — base.css
   Reset, tipografía, foco y utilidades mínimas.

   ---------------------------------------------------------------------
   POR QUÉ TODO CUELGA DE .ds-app
   ---------------------------------------------------------------------
   Este fichero se carga en templates/base.html, o sea en LAS 30 SUB-APPS A
   LA VEZ, y la migración es app por app durante meses. Un reset global aquí
   —`* { margin: 0 }`, `body { font-family: ... }`— cambiaría de golpe la
   tipografía y el interlineado de todo Lydent el día del deploy, que es
   exactamente el big-bang que el plan (§2.3) se negó a hacer con el color.

   Así que el núcleo es INERTE POR CONSTRUCCIÓN:

     - fonts.css declara familias y no pinta nada.
     - base.css solo actúa dentro de un contenedor con clase `ds-app`.
     - components.css solo usa clases `ds-*`, que no existen en el repo.

   Una pantalla se migra poniendo `ds-app` en su contenedor y cambiando su
   marcado a `ds-*`. Hasta ese día no le pasa nada. El día que no quede una
   sola app sin migrar, el scope se puede subir a :root de una tacada.

   ---------------------------------------------------------------------
   POR QUÉ EL RESET VA EN :where() Y LA DECISIÓN NO
   ---------------------------------------------------------------------
   Lo descubrió el piloto de Stock: el botón primario salía índigo sobre
   índigo —texto invisible— y el destructivo salía negro. Los dos por lo
   mismo: `.ds-app a` y `.ds-app button` tienen especificidad (0,1,1) y le
   ganaban a `.ds-btn--primary` y `.ds-btn--danger`, que son (0,1,0). O sea que
   el RESET le ganaba a la DECISIÓN, que es exactamente al revés de como tiene
   que ser.

   Regla del sistema, y no es cosmética:

     - Lo que es un DEFECTO (reset, herencia de fuente, color de enlace) se
       escribe `.ds-app :where(x)`. `:where()` aporta cero, así que la regla
       pesa lo que su elemento y cualquier clase de componente la gana.
     - Lo que es una DECISIÓN (la caja raíz, el anillo de foco, la densidad)
       conserva `.ds-app`, porque sí debe ganar.

   Si un componente necesita `!important` para imponerse a este fichero, el
   bug está aquí: falta un `:where()`.

   ---------------------------------------------------------------------
   PUNTOS DE RUPTURA (decisión cerrada 2026-08-03, §6 del plan)
   ---------------------------------------------------------------------
   640px y 1024px, los de DESIGN.md §6 — no los 576/768/992 de Bootstrap que
   arrastran las apps viejas. Se puede decidir así justamente porque el
   núcleo está scopeado: los dos juegos no conviven nunca en el mismo
   elemento, y cada app cambia de juego el día que se migra entera.
   CSS no admite var() dentro de @media, de modo que los dos números están
   escritos a mano en cada consulta. Son los únicos literales permitidos
   fuera de tokens.css y están todos en este fichero y en archetypes/.
   ========================================================================= */

/* =========================================================================
   1. RAÍZ DE PANTALLA MIGRADA
   ========================================================================= */

.ds-app {
  box-sizing: border-box;
  font-family: var(--font-ui);
  font-size: var(--fs-base);
  line-height: var(--lh-base);
  color: var(--n-800);
  background: var(--n-25);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

.ds-app *,
.ds-app *::before,
.ds-app *::after {
  box-sizing: inherit;
}

/* =========================================================================
   2. RESET
   Solo lo que estorba. No se resetea lo que el navegador ya hace bien.
   ========================================================================= */

.ds-app :where(h1), .ds-app :where(h2), .ds-app :where(h3),
.ds-app :where(h4), .ds-app :where(h5), .ds-app :where(h6),
.ds-app :where(p), .ds-app :where(figure), .ds-app :where(blockquote),
.ds-app :where(dl), .ds-app :where(dd) {
  margin: 0;
}

.ds-app :where(ul), .ds-app :where(ol) {
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Listas de prosa (ayuda, estados vacíos): recuperan sus marcas. */
.ds-app .ds-prose :where(ul, ol) {
  padding-left: var(--s-7);
  list-style: revert;
}

.ds-app :where(img),
.ds-app :where(svg),
.ds-app :where(video) {
  display: block;
  max-width: 100%;
}

.ds-app :where(table) {
  border-collapse: collapse;
  border-spacing: 0;
}

/* `hidden` tiene que ocultar, y punto. El navegador lo aplica con
   `display: none` de hoja de usuario, que pierde contra CUALQUIER regla de
   clase: un componente que se declara `display: flex` —el aviso de campo, el
   panel del buscador, una fila de rejilla— aparece en pantalla aunque la
   plantilla lo haya marcado `hidden`. Ocurrió de verdad: el formulario del
   trabajo enseñaba «esta ficha no existe todavía» sobre una ficha que existe.

   Va con `.ds-app` y no en `:where()` porque es una DECISIÓN y debe ganar:
   (0,2,0) le basta a cualquier componente de una sola clase. Esto es también
   lo que evita que `.ds-hidden` —la única `!important` del sistema— tenga que
   usarse cada vez que algo se oculta desde la plantilla.

   La primera mitad del selector cubre el caso en que lo oculto es la RAÍZ: un
   overlay compartido que lleva su propio `ds-app` porque lo incluyen tanto
   pantallas migradas como pantallas que aún no lo están (el modal de WhatsApp
   de Comercial). Aquí vivió hasta el 2026-08-18 una nota que decía que esa
   mitad «hoy no hace falta, porque la hoja de usuario de los navegadores
   actuales declara `[hidden]` con `!important`». Era FALSO —ni la especificación
   de HTML ni Chrome ponen ahí `!important`; comprobado en Chrome el 2026-08-18—
   y no fue una imprecisión inofensiva: mientras esa frase estuvo escrita, nadie
   puso el cinturón de abajo, porque la hoja decía que no hacía falta. Este
   repositorio ya se había comido el mismo fallo CUATRO veces antes de creérsela
   —`static/odontohr/css/login.css:182`, `static/cuadrante/css/cuadrante.css:787`,
   `static/odontohr/css/componentes.css:823` y el test
   `apps/common/configuracion/tests/test_modales.py`—. `[hidden]` de la hoja de
   usuario pierde contra CUALQUIER regla de clase, y punto. */
.ds-app[hidden],
.ds-app [hidden] { display: none; }

/* EL CINTURÓN DEL VELO, y no sobra por estar la regla de arriba: aquella está
   ACOTADA a `.ds-app`, y lo que flota vive fuera del armazón por definición. Un
   `.ds-modal-backdrop` escrito detrás del `</div>` de la pantalla conserva su
   `display: flex` —`position: fixed`, `inset: 0`, `z-index: 1100`— y **tapa la
   pantalla entera bloqueando todos los clics**, invisible salvo por el tinte;
   peor todavía, `cerrarModal()` hace `modal.hidden = true` sobre algo que ya lo
   es, así que la ✕, «Cancelar», el clic en el velo y Escape son los cuatro
   no-ops. Ocurrió de verdad: diez modales de Comercial desde el 9051287f5
   (2026-08-09), nueve días en producción.

   Sin `!important`: con `[hidden]` el selector sube a (0,2,0) y le gana de sobra
   a la clase sola de `components.css`. OdontoHR cerró este mismo fallo con
   `.o-velo[hidden]` en `static/odontohr/css/componentes.css:824`.

   El sitio de un modal sigue siendo DENTRO de `.ds-app` —fuera pierde también
   la tipografía de los controles, la regla de aquí abajo—; esto es el cinturón,
   no el permiso. */
.ds-modal-backdrop[hidden] { display: none; }

/* Los controles heredan la tipografía en lugar de la del sistema operativo. */
.ds-app :where(button),
.ds-app :where(input),
.ds-app :where(select),
.ds-app :where(textarea) {
  font: inherit;
  color: inherit;
}

/* Y los campos de texto tienen el aspecto del sistema aunque nadie les ponga
   `.ds-input`. Descubierto al retirar el CSS viejo de Stock: media docena de
   pantallas dejaban sus <input> y <select> sin clase, y sin esta regla salían
   con el aspecto nativo del sistema operativo dentro de una pantalla ya
   migrada — que es peor que antes, no mejor.

   Va en `:where()` porque es un DEFECTO: `.ds-input`, `.ds-cellinput` y
   `.ds-search__input` lo ganan sin esfuerzo. Los <button> NO entran aquí a
   propósito: un botón sin clase debe seguir viéndose neutro, o toda la app
   parecería llena de acciones primarias. */
.ds-app :where(input:not([type="checkbox"]):not([type="radio"]):not([type="file"])),
.ds-app :where(select),
.ds-app :where(textarea) {
  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);
  font-size: var(--fs-base);
}

.ds-app :where(textarea) {
  resize: vertical;
}

/* EL CAMPO DE FICHERO ES EL ÚNICO CONTROL QUE PINTA EL NAVEGADOR, Y SE NOTA.
   La regla de arriba lo excluye a propósito —un `[type=file]` no es una caja de
   texto—, pero excluirlo lo dejaba entero como lo dibuja el sistema operativo:
   dentro de una pantalla migrada aparece un «Seleccionar archivo» gris que no
   se parece a ningún otro botón del producto. Lo reportó el CEO en Stock →
   Importar factura el 2026-09-01, y no es de esa pantalla: es de todas, porque
   nadie viste este control.

   Se viste el control (la caja) y su botón (`::file-selector-button`, que es la
   única forma de llegar a él sin JavaScript ni el truco del `<label>` con el
   `<input>` escondido). El botón copia `.ds-btn` a mano porque un pseudo-elemento
   no puede llevar una clase; si `.ds-btn` cambia, esto se revisa. */
.ds-app :where(input[type="file"]) {
  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);
  font-size: var(--fs-sm);
  color: var(--n-600);
}

.ds-app input[type="file"]::file-selector-button {
  margin: 0 var(--s-5) 0 0;
  padding: var(--s-2) var(--s-5);
  border: var(--bd-strong);
  border-radius: var(--r-sm);
  background: var(--n-0);
  color: var(--n-800);
  font: inherit;
  font-size: var(--fs-sm);
  font-weight: 500;
  line-height: 1.6;
  cursor: pointer;
}

.ds-app input[type="file"]::file-selector-button:hover { background: var(--n-50); }

/* Las casillas y radios se tiñen del acento. Sin esto salen del azul por
   defecto del navegador, que es justamente el `#0d6efd` que DESIGN.md §1
   prohíbe — y se cuela sin que nadie lo escriba.

   Esto es DECISIÓN, no defecto, así que va con `.ds-app` y no en `:where()`:
   `static/css/base.css` tiñe TODAS las casillas del verde antiguo con
   `input[type="checkbox"]`, que pesa lo mismo (0,1,1) y se carga después que
   el núcleo. Con `:where()` ganaba el verde y la casilla de una pantalla
   migrada seguía siendo del color viejo. */
.ds-app input[type="checkbox"],
.ds-app input[type="radio"] {
  accent-color: var(--accent);
}

/* =========================================================================
   3. TIPOGRAFÍA
   La escala vive en tokens.css; aquí solo se le pone nombre.
   ========================================================================= */

.ds-app :where(h1), .ds-h1 {
  font-size: var(--fs-xl);
  line-height: var(--lh-tight);
  font-weight: 600;
  letter-spacing: -0.01em;
}

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

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

/* LA FIRMA (DESIGN.md §1). Cabecera de columna, rótulo de KPI, etiqueta de
   campo. Condensada, en mayúscula, con tracking y sobre regla de un píxel.
   Si esto se usa para texto corrido, el sistema deja de reconocerse. */
.ds-micro {
  font-family: var(--font-cond);
  font-size: var(--fs-micro);
  font-weight: 600;
  line-height: var(--lh-tight);
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
  color: var(--n-500);
}

.ds-help {
  font-size: var(--fs-xs);
  line-height: var(--lh-base);
  color: var(--n-500);
}

.ds-muted { color: var(--n-500); }
.ds-strong { font-weight: 600; }

/* Numeración tabular. Obligatoria en toda columna numérica y todo KPI
   (DESIGN.md §4). Sin esto las columnas de importes bailan al escanear. */
.ds-num {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

/* Código, identificadores y volcados técnicos. Los importes NO van aquí:
   van en Sans con .ds-num, que ya alinea sin cambiar de voz. */
.ds-code {
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
}

.ds-truncate {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* TEXTO GUARDADO CON SUS SALTOS DE LÍNEA (2026-08-09, con el modal de guiones
   de Comercial). Un guión de llamada, una plantilla de mensaje o una nota se
   escribieron con párrafos, y esos saltos son CONTENIDO: sin esto llegan al
   navegador y se colapsan en un párrafo corrido.

   Sube al núcleo porque el repositorio ya lo tenía escrito seis veces con
   cinco nombres distintos —`taller-mensaje`, `r2-texto`, `r2-log`, dos en
   `claudia_voz.css` y el `.ds-msgprev__texto` del propio núcleo—, que es el
   criterio de §2. El ancho NO va aquí: cada sitio acota el suyo (`ch` en un
   panel, el ancho del modal en un modal). */
.ds-prewrap {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* VOLCADO TÉCNICO EN BLOQUE (2026-08-28, con la traza de Claudia). Un
   system-prompt de 9.500 tokens, un borrador crudo del modelo, un log. Se
   compone con `.ds-code .ds-prewrap`, que ya ponen la voz y los saltos; lo que
   añade esto es la CAJA — y la caja es lo que impide que una línea de 300
   caracteres empuje el cuerpo de la página a la derecha (§3: nada scrollea la
   página en horizontal; scrollea el bloque).

   Sube al núcleo y no a la hoja de la app porque el techo de 40 líneas de §2
   dice exactamente esto: si una app necesita un componente, la tarea es
   llevarlo al núcleo. Y no es de Claudia — cualquier pantalla que enseñe la
   salida de una máquina lo pide igual. */
.ds-dump {
  max-height: 24rem;
  overflow: auto;
  line-height: var(--lh-tight);
  background: var(--n-50);
  border: var(--bd-strong);
  border-radius: var(--r-sm);
  padding: var(--s-5);
  margin: var(--s-4) 0 0;
}

/* =========================================================================
   4. ENLACES
   ========================================================================= */

.ds-app :where(a) {
  color: var(--accent);
  text-decoration: none;
}

.ds-app :where(a):hover {
  text-decoration: underline;
}

/* En prosa el enlace va subrayado siempre: dentro de un párrafo el color
   solo no basta para distinguirlo (DESIGN.md §5: nada se comunica solo por
   color). En una tabla o una barra de acciones el contexto ya lo dice. */
.ds-app .ds-prose :where(a) {
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* =========================================================================
   5. FOCO
   Siempre visible. Nunca outline:none sin sustituto (DESIGN.md §8).
   ========================================================================= */

.ds-app :focus-visible {
  outline: 2px solid transparent; /* superviviente en modo alto contraste */
  outline-offset: 2px;
  box-shadow: var(--focus);
}

/* El ratón no necesita el anillo; el teclado sí. */
.ds-app :focus:not(:focus-visible) {
  outline: none;
}

/* EL ANILLO NECESITA SITIO, y ningún componente se lo reservaba.

   `--focus` son dos sombras —2px del color de fondo y 2px del acento—, o sea
   que se pinta **4px POR FUERA** de la caja del control. Un título pegado a un
   campo se lo come: medido el 2026-08-18 en el card del Coordinador, la
   etiqueta «Añadir tipo de subtarea» terminaba exactamente donde empieza el
   campo (holgura **−4px**), así que al escribir el anillo TACHABA el título. El
   CEO lo vio en producción y lo describió tal cual: «remarca con borde azul,
   que está todo tan apretado que sobreescribe al título».

   No se arregla con un margen en esa plantilla: el patrón «un `<h6>` y debajo
   el formulario de añadir» se repite en **19 pantallas** de Configuración, así
   que el defecto es del componente y el arreglo va aquí. 12px menos los 4 del
   anillo dejan 8px de aire, que es lo mismo que separa dos campos.

   El `<p>` entra por el mismo motivo y no por simetría: un campo que va
   directamente detrás de su texto de ayuda —`.ds-field__help`— es el otro sitio
   donde esto pasa, y en este mismo card lo hacía el buscador de tarifas. */
.ds-app :is(h1, h2, h3, h4, h5, h6, p)
      + :is(.ds-field, .ds-formrow, .ds-autocomplete,
            .ds-input, .ds-select, .ds-textarea) {
  margin-top: var(--s-5);
}

.ds-app ::selection {
  background: var(--accent-bg);
  color: var(--n-900);
}

/* =========================================================================
   6. DENSIDAD RESPONSIVA
   El modo compacto es de escritorio. Por debajo de 1024px se relaja solo
   (DESIGN.md §6) y por debajo de 640px se garantiza el objetivo táctil de
   44px, aunque el usuario tenga fijado el modo compacto.
   ========================================================================= */

/* Se nombra también al contenedor que baja la densidad a mano. Sin esa
   segunda mitad la relajación no llega: `[data-density="compact"]` se declara
   sobre el elemento anidado, y una declaración propia gana siempre a un valor
   heredado del ancestro por muy específico que sea el ancestro. O sea que una
   tabla de detalle marcada como compacta se quedaría en celdas de 32px y
   controles de 28 justo en la tableta, que es donde el dedo necesita 44.
   (Analytics, 2026-08-06: es su tabla de detalle mensual.) */
@media (max-width: 1024px) {
  .ds-app,
  .ds-app [data-density="compact"] {
    --row-h:      40px;
    --cell-pad-y: var(--s-4);
    --cell-pad-x: var(--s-6);
    --cell-fs:    var(--fs-base);
    --ctl-h:      34px;
  }
}

@media (pointer: coarse) {
  .ds-app,
  .ds-app [data-density="compact"] {
    --row-h: 44px;
    --ctl-h: 44px;
  }
}

/* =========================================================================
   7. UTILIDADES MÍNIMAS
   Deliberadamente pocas. Esto no es un framework de utilidades: si algo se
   repite, es un componente y su sitio es components.css.
   ========================================================================= */

/* Pila vertical con separación uniforme. */
.ds-stack       { display: flex; flex-direction: column; gap: var(--s-6); }
.ds-stack--tight{ gap: var(--s-4); }
.ds-stack--wide { gap: var(--s-9); }

/* Fila que envuelve. Para barras de acciones y grupos de metadatos. */
.ds-cluster {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-5);
}

.ds-cluster--tight { gap: var(--s-3); }

/* UN CAMPO Y EL BOTÓN QUE ACTÚA SOBRE ÉL no se centran: se apoyan en la misma
   línea de base. El cluster centra —correcto para una barra de metadatos, donde
   todo mide lo mismo—, pero un `.ds-field` es una COLUMNA (rótulo + control) y
   centrarla contra un botón deja el botón flotando a media altura del control:
   medido en el panel «Fecha de inicio» del plan de tratamiento, el botón 8px
   por encima del input y 17px por encima de su base. Es la misma alineación que
   ya usa la barra de filtros (`.ds-filters`, components.css §7), que es el otro
   sitio donde conviven rótulo-con-campo y botón. */
.ds-cluster--pie { align-items: flex-end; }

/* Empuja lo que venga después al extremo contrario de una .ds-cluster. */
.ds-push { margin-left: auto; }

/* Único sitio donde se permite scroll horizontal, y siempre explícito. */
.ds-scroll-x {
  overflow-x: auto;
  overscroll-behavior-x: contain;
}

/* Contenido accesible solo para lector de pantalla. */
.ds-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Única `!important` del sistema, y es deliberada: DESIGN.md §2 lo prohíbe
   porque se usa para ganar peleas de cascada que no deberían existir, pero
   ganar la pelea ES el trabajo de esta regla —oculta un componente que se
   declara a sí mismo `display: flex`—. Si aparece una segunda, es un bug. */
.ds-hidden { display: none !important; }

/* Alineación. Son las dos únicas utilidades de posición del sistema y existen
   porque una celda de texto que va a la derecha no es numérica —para eso está
   `.ds-num`, que además tabula—. Cualquier otra alineación es cosa del
   componente, no de una clase suelta. */
.ds-right  { text-align: right; }
.ds-center { text-align: center; }
