/* ============================================================================
   Controles de UI TRANSVERSALES
   ----------------------------------------------------------------------------
   Piezas visuales reutilizables por CUALQUIER vista de la app. No dependen de
   ningún módulo (a diferencia de work.css, que es del módulo de Trabajo) y no
   deben referenciar clases de una pantalla concreta.

   Convención: prefijo `ui-` + BEM (`ui-bloque__elemento`, estado `is-*`).
   Todo se apoya en variables del tema de Bootstrap (--bs-*), así que funciona
   en claro y oscuro sin overrides por tema.

   PROFUNDIDAD — LA REGLA QUE MÁS VECES SE HA ROTO EN ESTE REPO:
   el `#page-topbar` del tema es `position: fixed; z-index: 1`, así que CREA su
   propio contexto de apilado y hacia afuera vale 1 — su menú de notificaciones
   vale 1000 solo por DENTRO. Consecuencia: cualquier z-index >= 1 de contenido
   de página lo pisa, menú incluido (y un `stretched-link`, cuyo `::after` vale
   1, EMPATA y gana por orden de pintado).

   Por eso todo contenedor que eleve algo dentro de sí —controles sobre un
   enlace estirado, sticky, overlays internos— crea su propio contexto con
   `isolation: isolate` (o con `contain: paint`, que también lo crea) y NUNCA
   con `position: relative; z-index: N`, que eleva de verdad. Aislar contiene
   sin elevar. Se verifica con `elementFromPoint`, que es lo único que lo
   delata: a ojo el control se ve perfecto.

   ESTOS CONTROLES SON EL ESTÁNDAR DE ACABADO DE LA APP, no el detalle de una
   pantalla (decisión de Randolf, 2026-07-22: "que sea claro que en cualquier
   vista similar en la aplicación deberá verse así de pulido"). Cada regla de
   abajo nació de un defecto real detectado mirando la pantalla —áreas de clic
   diminutas, hover que borraba el estado, anillos recortados por las esquinas,
   ruedas de carga parpadeando—, así que USARLOS trae el acabado puesto.
   Reimplementar una lista con markup propio es la desviación que hay que
   justificar, no al revés. El resumen de las reglas y su porqué está en
   CLAUDE.md → "Listados: el acabado de `ui-list` es el ESTÁNDAR de la app".

   Índice
     1. Control segmentado (`ui-segmented`)
     2. Lista seleccionable (`ui-list`)
     3. Pastilla de metadato (`ui-pill`)
     4. Avatar de iniciales (`ui-avatar`)
     5. Menú contextual (`ui-menu`)
     6. Botón de ícono (`ui-icon-btn`)
     7. Popover anclado de ajustes (`ui-popover`)
     8. Captura rápida (`ui-capture`)
     9. Detalle en hoja de vidrio (`ui-detail`)
    10. Desplegable de filtro (`ui-select`) y su rótulo (`ui-field-label`)
    11. Conmutador de un eje booleano (`ui-toggle`)
    12. Tablero por columnas (`ui-board`)
    13. Repaso guiado (`ui-plan`)
    14. Campos de formulario: el interior va BLANCO (`.form-control`/`.form-select`)
    15. Zona de arrastre de archivo (`ui-dropzone`), fila de chips (`ui-chips`)
        y lista de errores por fila (`ui-errors`)
    16. Lista de definiciones (`ui-defs`) y esqueleto de carga (`ui-skeleton`)
    17. Hoja de tiempo semanal (`ui-sheet`)
    18. Páginas de estado — error y no encontrado (`status-page`)
    19. Inicio — capturar y elegir qué hacer ahora (`ui-home`)
    20. Espacios de trabajo — el tablero de plataforma (`ui-tenants`)
    21. User story map (`ui-storymap`)
   22. Piezas de un producto de software (`ui-domains`)
   23. Diálogos (`ui-dialog`)
   24. Portafolio de tarjetas (`ui-cards`, `ui-card`)
   25. Actores / personas (`ui-actor`) — avatar, franja del mapa y selector
   ==========================================================================*/


/* ── 16. Lista de definiciones y esqueleto ───────────────────────────────────
   `ui-defs` lista COSAS QUE SE DEFINEN (hoy los campos personalizados de un
   espacio). No es `ui-list`: aquello es una lista SELECCIONABLE con detalle y
   acciones de lote; esto es un inventario donde cada fila se edita o se borra y
   nada se marca. Compartir la clase habría traído el aparato de selección para
   nada.

   Cada ítem lleva su rótulo arriba con sus pastillas, y debajo la REGLA en una
   línea — sin ella hay que abrir el editor de cada campo para saber qué está
   pidiendo el espacio, que es la pregunta de la pantalla. */
.ui-defs {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.ui-defs__item {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 1rem;
    padding: 0.75rem 0.9rem;
    border: 1px solid var(--bs-border-color);
    border-radius: 12px;
    background-color: var(--bs-body-bg);
}

/* Inactivo se ATENÚA, no se esconde: sigue existiendo y hay que poder reactivarlo. */
.ui-defs__item.is-inactive { opacity: 0.6; }

.ui-defs__main {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    min-width: 0;   /* sin esto un rótulo largo desborda el flex */
}

.ui-defs__head {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.35rem;
}

.ui-defs__label {
    font-weight: 650;
    font-size: 0.95rem;
}

/* El texto secundario va al 88 % del color del cuerpo y NO a
   `--bs-secondary-color`: ese token da 3.47:1 sobre el fondo de la tarjeta en
   oscuro — peor que el 4.12 que la app tiene de línea base, o sea una regresión
   de ESTA pantalla y no el límite heredado del tema. Al 88 % da 4.65:1 en oscuro
   y 8.45:1 en claro, ambos sobre el 4.5 que exige el texto pequeño. Mismo
   criterio que `ui-pill` y el "sin ritmo" del repaso. */
.ui-defs__rule {
    font-size: 0.82rem;
    color: color-mix(in srgb, var(--bs-body-color) 88%, transparent);
}

.ui-defs__key {
    font-size: 0.72rem;
    color: color-mix(in srgb, var(--bs-body-color) 88%, transparent);
    background: none;
    padding: 0;
}

/* Indivisible: media botonera en cada renglón es justo lo que se ve mal. */
.ui-defs__actions {
    display: flex;
    flex-wrap: nowrap;
    gap: 0.35rem;
    flex-shrink: 0;
}

/* El estado de carga tiene la FORMA del contenido, no una rueda centrada: el
   layout ya está puesto y solo se llena, así que la pantalla no salta al llegar
   los datos. ESTÁTICO a propósito — una animación infinita repinta sin parar, el
   defecto que este repo ya pagó una vez con el halo del checkbox. */
.ui-skeleton {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.ui-skeleton__row {
    height: 4.5rem;
    border: 1px solid var(--bs-border-color);
    border-radius: 12px;
    background-color: color-mix(in srgb, var(--bs-body-color) 3.5%, transparent);
}


/* ── 15. Zona de arrastre, chips y errores por fila ──────────────────────────
   Nacieron con la importación de clientes; son transversales a propósito, porque
   cualquier carga masiva futura necesita las mismas tres piezas.

   LA ZONA ES UN ÁREA GRANDE Y EL `<input type=file>` LA LLENA TRANSPARENTE: así
   el clic funciona en cualquier punto sin un botón que compita, y el arrastre lo
   resuelve el helper JS (`initDropzone`) alternando `is-over`. El input tapado
   con `opacity: 0` sigue recibiendo el clic —es el elemento de encima— pero NO
   el drop nativo: ahí el navegador abriría el archivo, por eso el helper reenvía
   `dataTransfer.files` a mano. */
.ui-dropzone {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.35rem;
    min-height: 8.5rem;
    padding: 1.25rem;
    border: 2px dashed color-mix(in srgb, var(--bs-body-color) 20%, transparent);
    border-radius: 14px;
    background-color: color-mix(in srgb, var(--bs-body-color) 2.5%, transparent);
    text-align: center;
    transition: border-color 130ms ease-out, background-color 130ms ease-out;
}

.ui-dropzone input[type="file"] {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    opacity: 0;
    cursor: pointer;
}

.ui-dropzone:hover,
.ui-dropzone.is-over {
    border-color: color-mix(in srgb, var(--bs-primary) 55%, transparent);
    background-color: color-mix(in srgb, var(--bs-primary) 6%, transparent);
}

/* Mientras sube, la zona deja de invitar: el cursor lo dice y el input está
   deshabilitado, así que un segundo archivo no puede pisar al que va en vuelo. */
.ui-dropzone.is-busy {
    border-style: solid;
    cursor: progress;
}

.ui-dropzone.is-busy input[type="file"] { cursor: progress; }

.ui-dropzone__icon {
    font-size: 1.5rem;
    color: color-mix(in srgb, var(--bs-primary) 70%, var(--bs-body-color));
}

.ui-dropzone__text {
    font-size: 0.9rem;
    font-weight: 550;
}

/* Fila de pastillas que envuelve sola. */
.ui-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
}

/* Errores por fila: la LISTA es un vistazo acotado, nunca el volcado completo
   — con doscientas filas malas una tabla es ilegible y el camino de corrección
   real es el archivo anotado. Cada bloque es una fila del origen. */
.ui-errors {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.ui-errors__row {
    padding: 0.55rem 0.75rem;
    border-radius: 10px;
    background-color: color-mix(in srgb, var(--bs-danger) 7%, transparent);
    border-left: 3px solid color-mix(in srgb, var(--bs-danger) 55%, transparent);
}

.ui-errors__line {
    display: block;
    font-size: 0.78rem;
    font-weight: 650;
    color: color-mix(in srgb, var(--bs-danger) 75%, #000);
}

[data-bs-theme="dark"] .ui-errors__line {
    color: color-mix(in srgb, var(--bs-danger) 85%, #fff);
}

.ui-errors__list {
    margin: 0.2rem 0 0;
    padding-left: 1.1rem;
    font-size: 0.82rem;
}


/* ── 14. Campos de formulario ────────────────────────────────────────────────
   EL INTERIOR DE UN CAMPO VA BLANCO, NO DEL GRIS DE LA SUPERFICIE (decisión de
   Randolf, 2026-07-27: "esos campos deben ser blancos, por dentro, porque si no
   todo queda monocromático").

   El tema compila Bootstrap con `.form-control { background: var(--bs-tertiary-bg) }`
   —un gris—, así que sobre una tarjeta clara el campo y su fondo se funden y el
   formulario entero se lee como un solo bloque plano: no se ve DÓNDE se escribe.
   Con el interior en el color del cuerpo, el borde deja de ser la única señal y
   el campo se lee como un hueco que espera texto.

   Va aquí y no en cada pantalla porque es acabado de la app entera. En OSCURO se
   conserva el tono del tema: allí el cuerpo ya es oscuro y un campo del mismo
   color perdería la separación que en claro gana.

   EL BORDE SUBE CON EL INTERIOR, y no es un ajuste cosmético: mientras el campo
   era gris, el relleno hacía todo el trabajo de decir "aquí se escribe" y el
   borde del tema podía quedarse en 1.21:1. Con el interior blanco sobre una
   tarjeta blanca, ese borde es lo ÚNICO que dibuja el campo — a 1.21:1
   desaparece. A 20 % del color del cuerpo da 1.44:1 (medido), el rango de un
   borde de formulario moderno: visible sin endurecer la pantalla. */
.form-control,
.form-select {
    background-color: var(--bs-body-bg);
    border-color: color-mix(in srgb, var(--bs-body-color) 20%, transparent);
}

.form-control:disabled,
.form-select:disabled {
    background-color: var(--bs-secondary-bg);
}

/* UN MENSAJE DE VALIDACIÓN ES ROJO SIEMPRE, sin depender de que quien escriba el
   campo se acuerde de ponerle `text-danger` (Randolf, 2026-07-27: "hay un
   estándar y es que estos mensajes siempre están en rojo").

   Ese estilo EXISTÍA —`app.css` declaraba `.validation-message { color: #e50000 }`—
   pero esa hoja está COMENTADA en `App.razor` desde la plantilla, así que nunca
   se cargó: los mensajes se veían rojos solo porque 115 de los 122
   `<ValidationMessage>` llevan `class="text-danger"` a mano, y los 7 que no la
   llevaban salían en NEGRO. Un estándar que depende de repetir una clase en cada
   sitio no es un estándar, es una disciplina que falla.

   EL TONO SE MIDIÓ, no se eligió a ojo: `--bs-danger` puro (#fd625e) da 2.9:1
   sobre blanco y esto es texto pequeño, que exige 4.5:1; el `*-text-emphasis`
   del tema da 11.22:1 pero es un vino tan oscuro que se lee NEGRO — que es el
   defecto que se estaba corrigiendo. El 75 % del danger sobre negro da 4.96:1 y
   sigue leyéndose rojo. En oscuro se aclara en vez de oscurecerse.

   Y `class="text-danger"` SE QUITÓ de los 115 `<ValidationMessage>` que lo
   traían, en vez de ganarle con `!important`: las utilidades de Bootstrap llevan
   `!important`, así que esa clase pisaba esta regla y dejaba DOS rojos distintos
   conviviendo. Con la clase fuera, el color vive en un solo sitio. */
.validation-message,
.field-validation-error {
    color: color-mix(in srgb, var(--bs-danger) 75%, #000);
    font-size: 0.8125rem;
}

[data-bs-theme="dark"] .validation-message,
[data-bs-theme="dark"] .field-validation-error {
    color: color-mix(in srgb, var(--bs-danger) 85%, #fff);
}

[data-bs-theme="dark"] .form-control,
[data-bs-theme="dark"] .form-select {
    background-color: var(--bs-tertiary-bg);
}


/* ── 1. Control segmentado ───────────────────────────────────────────────────
   Grupo de opciones MUTUAMENTE EXCLUSIVAS, pocas y siempre visibles (3 a 5).
   Se lee de un vistazo, a diferencia de un <select> que esconde las opciones
   hasta abrirlo. Para más opciones, o si son combinables, este control NO es
   el indicado.

   Uso:
     <div class="ui-segmented" role="group" aria-label="…">
       <button type="button" class="ui-segmented__item is-active" aria-pressed="true">
         <i class="fa-solid fa-inbox"></i>Inbox
         <span class="ui-segmented__count">3</span>
       </button>
       …
     </div>

   El estado activo lo decide el servidor (Blazor) escribiendo `is-active` y
   `aria-pressed`; el CSS no guarda estado — nada de :checked ni JS. */

.ui-segmented {
    /* Acento de marca: índigo del tema → violeta. Se declara AQUÍ (no se toma
       de work.css) para que el control no dependa de ningún módulo; es la
       misma fórmula, así que se ve idéntico al de la vista anterior. */
    --ui-accent-gradient:
        linear-gradient(135deg,
            var(--bs-primary),
            color-mix(in srgb, var(--bs-primary) 55%, #8b5cf6));

    display: inline-flex;
    gap: 2px;
    padding: 3px;
    border-radius: 11px;
    background: color-mix(in srgb, var(--bs-body-color) 6%, transparent);
    /* Con muchas opciones en pantalla angosta, se envuelve en vez de desbordar. */
    flex-wrap: wrap;
}

.ui-segmented__item {
    border: 0;
    background: transparent;
    color: var(--bs-secondary-color);
    font-size: 0.8rem;
    font-weight: 550;
    padding: 0.28rem 0.7rem;
    border-radius: 8px;
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    white-space: nowrap;
    transition: background-color 130ms ease-out, color 130ms ease-out;
}

.ui-segmented__item:hover { color: var(--bs-body-color); }

/* Anillo de foco explícito: los fondos tenues de este control se comen el
   estilo por defecto de Bootstrap, y sin esto el recorrido por teclado es
   invisible. */
.ui-segmented__item:focus-visible {
    outline: 2px solid var(--bs-primary);
    outline-offset: 1px;
}

/* La opción activa lleva el acento de marca: es el eje que decide QUÉ se está
   viendo, así que se lee de lejos. */
.ui-segmented__item.is-active {
    background: var(--ui-accent-gradient);
    color: #fff;
    font-weight: 650;
    box-shadow: 0 4px 10px -4px color-mix(in srgb, var(--bs-primary) 70%, transparent);
}

.ui-segmented__item.is-active:hover { color: #fff; }

/* CONMUTADOR DE VISTA: el mismo segmentado, pero con ENLACES en vez de botones
   —cada vista es una URL propia, así que tiene que poder abrirse en otra
   pestaña y marcarse como favorita—. Un `<a>` llega con subrayado y con el
   color del tema, que es lo único que hay que neutralizar para que se vea
   idéntico a las demás opciones del control. */
a.ui-segmented__item {
    text-decoration: none;
    color: var(--bs-secondary-color);
}

a.ui-segmented__item:hover { color: var(--bs-body-color); }

/* El nombre de la vista se pliega en pantallas angostas: el ícono basta, y así
   el conmutador no le roba ancho al título de la página. */
@media (max-width: 991.98px) {
    .ui-segmented--views .ui-segmented__name { display: none; }
}


/* Variante DISCRETA: el activo es una pastilla neutra en vez del acento. Para
   controles secundarios que no deben competir con el eje principal cuando hay
   varios segmentados en la misma barra. */
.ui-segmented--subtle .ui-segmented__item.is-active {
    background: var(--bs-body-bg);
    color: var(--bs-body-color);
    box-shadow: 0 1px 4px rgba(15, 23, 42, 0.14);
}

/* Contador opcional dentro de una opción (p. ej. cuántos ítems trae). */
/* PLEGADO: solo la opción activa dice su nombre; las demás se quedan en su
   ícono. El nombre no se BORRA —sigue en el árbol de accesibilidad y en el
   `title`—, se pliega hasta ancho cero.

   El pliegue usa `0fr → 1fr` de rejilla, no un `max-width` fijo: así anima
   EXACTAMENTE hasta el ancho de cada palabra, sin adivinar un número que dejaría
   la animación corta con "Entrada" y larga con "Realizables". Es el mismo truco
   con el que el detalle se abre a una altura desconocida, y con el mismo costo:
   rehace el layout por frame. Aquí es despreciable —son cuatro pastillas—, pero
   por eso mismo se mantiene corta.

   OJO AL ANCHO DEL CONTROL: al cambiar de opción crece o encoge según la palabra
   nueva, así que lo que venga a su derecha en la fila se corre. Es parte del
   efecto —el control respira— y no un defecto; si algún día estorba, la salida
   es fijarle un ancho al hueco del nombre, no quitar la animación. */
.ui-segmented--collapse .ui-segmented__item { gap: 0; }

.ui-segmented--collapse .ui-segmented__label {
    display: grid;
    grid-template-columns: 0fr;
    opacity: 0;
    transition: grid-template-columns 190ms cubic-bezier(0.05, 0.7, 0.1, 1),
                opacity 130ms ease-out;
}

.ui-segmented--collapse .ui-segmented__label > span {
    overflow: hidden;
    white-space: nowrap;
    /* El aire entre ícono y nombre vive AQUÍ y no en el `gap` del ítem: plegado,
       un gap dejaría un hueco fantasma al lado del ícono. */
    padding-inline-start: 0.35rem;
}

.ui-segmented--collapse .ui-segmented__item.is-active .ui-segmented__label {
    grid-template-columns: 1fr;
    opacity: 1;
}

/* El ícono no se encoge mientras el nombre de al lado se despliega. */
.ui-segmented--collapse .ui-segmented__item > i { flex-shrink: 0; }

@media (prefers-reduced-motion: reduce) {
    .ui-segmented--collapse .ui-segmented__label { transition: none; }
}


.ui-segmented__count {
    font-size: 0.68rem;
    font-weight: 650;
    line-height: 1;
    padding: 0.16rem 0.34rem;
    border-radius: 999px;
    background: color-mix(in srgb, var(--bs-body-color) 9%, transparent);
    color: var(--bs-secondary-color);
}

/* Sobre el acento, el contador se resuelve en blanco translúcido: mantener el
   índigo ahí lo dejaría ilegible. */
.ui-segmented__item.is-active .ui-segmented__count {
    background: rgba(255, 255, 255, 0.22);
    color: #fff;
}

.ui-segmented--subtle .ui-segmented__item.is-active .ui-segmented__count {
    background: color-mix(in srgb, var(--bs-primary) 14%, transparent);
    color: var(--bs-primary);
}

/* Variante PESTAÑAS: para secciones de un MISMO objeto (las del detalle de una
   tarea), no para filtrar una lista. La diferencia no es estética — un control
   segmentado dice "elige un modo" y unas pestañas dicen "el mismo objeto visto
   por otra cara"; usar el segmentado para lo segundo hacía que el panel pareciera
   tener filtros. Además, sobre una superficie ya tintada el activo de la variante
   discreta (una pastilla del color del fondo) se perdía: aquí el activo se marca
   con un subrayado del acento, que se lee sobre cualquier fondo.

   Se apoya en las MISMAS clases que emite el componente (`is-active`,
   `aria-pressed`): solo cambia la piel. */
.ui-segmented--tabs {
    display: flex;
    gap: 0.15rem;
    padding: 0;
    border-radius: 0;
    background: transparent;
    /* Riel bajo TODA la fila: da a las pestañas un suelo común y separa la
       cabecera del panel de su contenido sin meter otro filete. */
    box-shadow: inset 0 -1px 0 color-mix(in srgb, var(--bs-body-color) 10%, transparent);
}

.ui-segmented--tabs .ui-segmented__item {
    border-radius: 0;
    padding: 0.42rem 0.7rem;
    font-size: 0.82rem;
    /* Reserva del subrayado en TODAS: sin ella, activar una pestaña movía la
       fila un pixel. */
    box-shadow: inset 0 -2px 0 transparent;
    transition: color 130ms ease-out, box-shadow 130ms ease-out;
}

.ui-segmented--tabs .ui-segmented__item:hover {
    background: transparent;
    box-shadow: inset 0 -2px 0 color-mix(in srgb, var(--bs-body-color) 18%, transparent);
}

.ui-segmented--tabs .ui-segmented__item.is-active,
.ui-segmented--tabs .ui-segmented__item.is-active:hover {
    background: transparent;
    color: var(--bs-primary);
    font-weight: 650;
    box-shadow: inset 0 -2px 0 var(--bs-primary);
}

.ui-segmented--tabs .ui-segmented__item:focus-visible {
    outline-offset: -2px;
    border-radius: 6px;
}


/* ── 2. Lista seleccionable ──────────────────────────────────────────────────
   Listado de filas homogéneas donde UNA puede quedar seleccionada (el detalle
   se abre al lado o debajo). La superficie es una tarjeta con filetes entre
   filas; la selección se marca con el acento, no con un fondo saturado.

   Uso:
     <div class="ui-list">
       <div class="ui-list__item is-marked is-active">…</div>
       …
     </div>

   Cada fila es un <button> para que el teclado la alcance con Tab y la active
   con Enter/Espacio sin una sola línea de JS. */

/* ── ui-toggle ────────────────────────────────────────────────────────────
   Eje de alcance BOOLEANO ("solo mías"). No es un desplegable de dos opciones:
   un valor con dos estados se ALTERNA de un toque, y un `<select>` obligaría a
   abrir una lista para elegir entre "sí" y "no".

   Material del segmentado, y encendido toma su MISMO acento: así se lee que
   está acotando, con el vocabulario que la barra ya usa para "esto está
   activo". */
.ui-toggle {
    --ui-accent-gradient:
        linear-gradient(135deg,
            var(--bs-primary),
            color-mix(in srgb, var(--bs-primary) 55%, #8b5cf6));

    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.34rem 0.8rem;
    border: 1px solid transparent;
    border-radius: 10px;
    background-color: color-mix(in srgb, var(--bs-body-color) 6%, transparent);
    color: var(--bs-secondary-color);
    font-size: 0.8rem;
    font-weight: 550;
    white-space: nowrap;
    cursor: pointer;
    transition: background-color 130ms ease-out, color 130ms ease-out, box-shadow 130ms ease-out;
}

.ui-toggle:hover { background-color: color-mix(in srgb, var(--bs-body-color) 10%, transparent); }

.ui-toggle:focus-visible {
    outline: none;
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--bs-primary) 22%, transparent);
}

.ui-toggle.is-on {
    background: var(--ui-accent-gradient);
    color: #fff;
}

.ui-toggle.is-on:hover { filter: brightness(1.05); }

/* ── ui-select ────────────────────────────────────────────────────────────
   Desplegable con el MISMO material que el segmentado: fondo tintado del color
   del texto, esquinas suaves y sin borde duro. Los `form-select` de Bootstrap
   traían un borde gris y un radio distintos, así que en la misma fila convivían
   dos lenguajes — el control del sistema operativo al lado del control de la
   app.

   `appearance: none` quita el adorno nativo y el chevron se dibuja como imagen
   de fondo. No puede tomar `currentColor` (un `<select>` no admite
   pseudo-elementos), así que va con un gris neutro y su variante para el tema
   oscuro, que es como lo resuelve el propio Bootstrap. */
.ui-select {
    appearance: none;
    display: block;
    width: 100%;
    padding: 0.34rem 1.85rem 0.34rem 0.7rem;
    border: 1px solid transparent;
    border-radius: 10px;
    background-color: color-mix(in srgb, var(--bs-body-color) 6%, transparent);
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' d='M4 6.5 8 10.5 12 6.5'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 0.55rem center;
    background-size: 0.75rem;
    color: var(--bs-body-color);
    font-size: 0.8rem;
    font-weight: 550;
    line-height: 1.5;
    cursor: pointer;
    transition: background-color 130ms ease-out, box-shadow 130ms ease-out, border-color 130ms ease-out;
}

.ui-select:hover { background-color: color-mix(in srgb, var(--bs-body-color) 10%, transparent); }

/* Mismo anillo de foco que el resto de los controles de la pantalla. */
.ui-select:focus {
    outline: none;
    border-color: color-mix(in srgb, var(--bs-primary) 45%, transparent);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--bs-primary) 22%, transparent);
}

.ui-select:disabled { opacity: 0.55; cursor: default; }

[data-bs-theme="dark"] .ui-select {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%239ca3af' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' d='M4 6.5 8 10.5 12 6.5'/%3E%3C/svg%3E");
}

/* Las OPCIONES las pinta el sistema operativo: se les fija fondo y color del
   tema para que el desplegable abierto no salga en blanco sobre oscuro. */
.ui-select option {
    background-color: var(--bs-body-bg);
    color: var(--bs-body-color);
}

/* El RÓTULO del control, en el mismo tono discreto que el segmentado usa para
   sus opciones en reposo: nombra el eje sin competir con el valor. */
.ui-field-label {
    display: block;
    margin-bottom: 0.3rem;
    /* 85% del color del cuerpo y NO `--bs-secondary-color`: ese token mide 4.12
       en oscuro y esto es texto pequeño en negrita (exige 4.5). Medido en el
       banco; el defecto del token ya está documentado en el repo. Sube el
       contraste de todas las barras de filtro a la vez. */
    color: color-mix(in srgb, var(--bs-body-color) 85%, transparent);
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.01em;
}

/* ── CÍRCULO DE ESTADO (vocabulario compartido) ───────────────────────────────
   "Un círculo con un chulo" significa lo mismo en toda la pantalla: vacío,
   tocar para completar; verde macizo, ya está hecha (y tocarlo la reabre). Lo
   usan TRES superficies —la fila de la lista, la lista de subtareas de la hoja
   y la cabecera de la hoja—, así que la forma se declara UNA vez aquí y cada
   sitio añade solo lo suyo: el tamaño, y el apilado o el aire que necesite.

   Antes estaba copiado en dos bloques y ya había empezado a divergir (el chulo
   tenue era 30 % en la fila y 32 % en la hoja, sin ninguna razón). Va ANTES de
   sus consumidores a propósito: las reglas específicas de cada uno se declaran
   después y por tanto lo pisan cuando hace falta. */
.ui-list__complete,
.ui-detail__complete {
    flex-shrink: 0;
    padding: 0;
    border-radius: 50%;
    border: 1.5px solid color-mix(in srgb, var(--bs-body-color) 26%, transparent);
    background: transparent;
    color: color-mix(in srgb, var(--bs-body-color) 30%, transparent);   /* el chulo, tenue */
    display: inline-flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    transition: background-color 120ms ease-out, border-color 120ms ease-out, color 120ms ease-out, opacity 120ms ease-out;
}

/* HECHA: el mismo verde macizo, lo pinte la marca de la fila o el círculo de la
   hoja. La marca de la fila no tiene borde, así que el `border-color` solo lo
   estrena quien sí lo lleva. */
.ui-list__done,
.ui-detail__complete.is-done {
    border-color: var(--bs-success);
    background: var(--bs-success);
    color: #fff;
}


.ui-list {
    border: 1px solid var(--bs-border-color);
    border-radius: 14px;
    background: var(--bs-body-bg);
    overflow: hidden;

    /* ── MEDIDAS DE LA FILA, EN PÍXELES ENTEROS ───────────────────────────
       No es manía: son enteros PORQUE la guía del árbol debe caer en el mismo
       pixel que el chevrón.

         · Con medidas fraccionarias (0.9rem = 14.4px, 1.05rem = 16.8px…) el
           centro del chevrón y el de la línea se redondean POR SEPARADO —son
           una caja de elemento y un fondo— y se descuadraban hasta medio pixel,
           distinto en cada nivel de zoom.
         · Enteros REDUCEN además la costura entre filas: dos cajas adyacentes
           con frontera fraccionaria no teselan al rasterizar y dejan 1px por
           el que asoma el fondo del contenedor, que sobre la guía se ve como
           un corte blanco. No la eliminan del todo: bajo zoom fraccionario el
           propio borde de 1px se resuelve en fracción (medido: 0.667px) y la
           frontera vuelve a caer entre píxeles. Eliminarla por completo exige
           pintar la guía en UN elemento continuo, no fila por fila.

       De ahí que el relleno y la sangría vayan en px enteros. Al tocarlos,
       mantenerlos enteros. */
    --ui-list-pad-x: 14px;
    --ui-list-pad-y: 11px;

    /* Cuánto sangra cada nivel de jerarquía. Un solo sitio para afinarlo: lo
       consumen la fila y su detalle. */
    --ui-list-indent: 22px;

    /* Color de las guías del árbol: el ACENTO del producto, no un gris. Es lo
       que hace que una familia se lea como un grupo y no como filas con más
       margen. Atenuado para que agrupe sin competir con el contenido —las
       pastillas de vencimiento y los avatares ya ponen color— y resuelto contra
       el fondo del tema, así que funciona en claro y oscuro sin overrides.

       OPACO y no translúcido a propósito: la capa del arranque solapa a la de
       ancestros en la mitad inferior de una cabeza de familia, y con
       transparencia ese tramo saldría más oscuro que el resto de la línea. */
    --ui-list-guide: color-mix(in srgb, var(--bs-primary) 62%, var(--bs-body-bg));

    /* Tinte del BLOQUE de una familia. Muy bajo a propósito: junto con la guía
       basta para que el grupo se lea de un vistazo, y no compite con los
       estados de la fila (hover, marcada, completada), que se declaran después
       y por tanto lo pisan — que es justo lo que debe pasar. */
    --ui-list-family-tint: color-mix(in srgb, var(--bs-primary) 4%, transparent);

    /* Grosor de la guía. 2px se lee como una decisión; 1px, como un borde. */
    --ui-list-guide-width: 2px;

    /* Las guías como IMAGEN: una línea por nivel de ancestro. El número de
       líneas no está aquí — lo decide el `background-size` de quien la use
       (`nivel × sangría`), así que la misma imagen sirve a la fila y a su
       detalle. */
    --ui-list-guide-image: repeating-linear-gradient(
        to right,
        var(--ui-list-guide) 0 var(--ui-list-guide-width),
        transparent var(--ui-list-guide-width) var(--ui-list-indent));

    /* Columna RESERVADA al inicio de cada fila para el conteo de subtareas
       (histórica del chevrón — la pastilla heredó su sitio y su geometría). Se
       reserva SIEMPRE, tenga esa fila subtareas o no: si apareciera solo en los
       padres, los títulos bailarían entre filas — y, peor, la lista entera se
       desplazaría al completar la última subtarea.

       Es MÁS ANCHA que el botón: la diferencia es el hueco que separa el
       chevrón de la casilla. Sin ese hueco, el realce de la franja de la
       casilla —que se pinta DETRÁS de ella, así que solo se percibe por los
       márgenes que asoman— no tenía por dónde asomar a la izquierda y se leía
       como media pastilla pegada al borde derecho. */
    --ui-list-twisty: 28px;

    /* La pastilla de conteo (su alto y su ancho MÍNIMO — con dos dígitos crece
       hacia el hueco). Del mismo tamaño base que la casilla, para que las dos
       piezas del inicio de la fila se lean como pareja. */
    --ui-list-twisty-size: 16px;

    /* Hueco entre la pastilla y la casilla — lo que le queda a la columna una
       vez puesta la pastilla. Es por donde asoma el realce de la franja. */
    --ui-list-twisty-gap: calc(var(--ui-list-twisty) - var(--ui-list-twisty-size));

    /* Columna donde vive la guía: el CENTRO de la pastilla de conteo (no el de
       la columna). Así la línea del grupo nace del número que anuncia a las
       hijas — la lectura correcta ("esto cuelga de aquello"). */
    --ui-list-guide-x: calc(var(--ui-list-pad-x) + var(--ui-list-twisty-size) / 2);

    /* Borde IZQUIERDO de la guía — el valor que consumen de verdad todas las
       capas. Se calcula una sola vez para que la línea de la cabeza y la de sus
       hijas partan EXACTAMENTE de la misma x. */
    --ui-list-guide-col: calc(var(--ui-list-guide-x) - var(--ui-list-guide-width) / 2);

    /* Hueco entre las piezas de la fila (conteo · casilla · título · metas), y
       ancho de la casilla. Se declaran aquí porque el DETALLE los necesita para
       alinear su contenido EXACTAMENTE con el título de su fila: sin ellos el
       panel arrancaba 2 px antes y se leía como una capa ajena a la lista. */
    --ui-list-gap: 12px;
    --ui-list-check: var(--ui-list-twisty-size);
}

.ui-list__item {
    width: 100%;
    /* Ancla del radio de navegación, que se extiende sobre toda la fila. */
    position: relative;
    border: 0;
    border-top: 1px solid var(--bs-border-color);
    background: transparent;
    color: var(--bs-body-color);
    text-align: start;
    padding: var(--ui-list-pad-y) var(--ui-list-pad-x);

    /* SANGRÍA POR NIVEL de jerarquía. El nivel lo pone la fila como
       `--ui-list-level` (0 = raíz) y el espaciado lo decide AQUÍ, no el
       componente: así la fila, su franja sensible y el detalle se alinean solos.

       El tope de 6 niveles es defensivo: la jerarquía admite 8, y a partir de
       cierto punto seguir sangrando deja el título sin ancho útil en pantallas
       angostas sin aportar ya ninguna lectura. Se prefiere que dos niveles muy
       profundos se vean iguales antes que un título de tres palabras por línea.

       La franja de la casilla (`.ui-list__check::before`) se desplaza sola: su
       `left` es relativo a la casilla, así que arranca en el borde izquierdo de
       ESTA fila — que es justo lo que debe cubrir. */
    padding-left: calc(var(--ui-list-pad-x)
                     + var(--ui-list-twisty)
                     + min(var(--ui-list-level, 0), 6) * var(--ui-list-indent));

    display: flex;
    align-items: center;
    gap: var(--ui-list-gap);
    transition: background-color 120ms ease-out, color 120ms ease-out;

    /* AQUÍ VIVÍA `content-visibility: auto` (la "virtualización del navegador"),
       y SE QUITÓ porque SE COMÍA LOS TOQUES — bug real en móvil, 2026-07-26.

       Mientras el navegador considera que el contenido de la fila no es
       "relevante" todavía, el HIT-TESTING NO LLEGA A SUS HIJOS: el punto sobre
       la casilla o sobre el chulo de completar resuelve a la FILA. Medido en el
       banco con `elementFromPoint` sobre el markup real: con
       `content-visibility: auto` devuelve `DIV.ui-list__item` para los dos
       controles; con `contain: paint` devuelve `INPUT.ui-list__check` y
       `BUTTON.ui-list__complete`.

       Por qué con ratón nunca se notó y con el dedo siempre: para hacer clic,
       el puntero PASA ANTES por encima de la fila, y ese hover ya la vuelve
       relevante — cuando llega el clic, los hijos son alcanzables. El dedo no
       tiene ese paso previo: el primer contacto ES el toque, la fila todavía
       está saltada y el toque aterriza en ella. De ahí los dos síntomas
       reportados: en iOS la casilla exigía DOS toques (el primero solo la
       volvía relevante) y el chulo de completar se teñía (el `:hover` sí se
       aplica) sin completar nada, porque el clic era de la fila.

       Una optimización que impide tocar los controles no es una optimización, y
       menos en el dispositivo donde más falta hace. Se conserva `contain` —que
       es lo que `content-visibility` implicaba y de lo que sí depende el
       diseño—: cada fila sigue siendo frontera de PINTURA (ningún repintado se
       filtra a sus vecinas, y es lo que RECORTA al alto de la fila las franjas
       sensibles de la casilla y del chulo, que se estiran ±1-2rem a propósito)
       y de LAYOUT (lo de dentro no puede reflotar a la lista). El coste de
       pintar filas fuera de pantalla se lo compensa que la lista está acotada a
       500 filas (`MaxRows`) y que la fila es un flex de una línea. */
    contain: layout paint;

    /* Techo base solo para que la animación de salida (`is-leaving`) pueda
       colapsar HASTA 0 — no se puede transicionar `max-height` desde `none`.
       6rem es holgado: las filas son de una línea (título con elipsis, meta en
       una fila). El detalle abierto es un HERMANO (`.ui-list__detail`), no vive
       dentro de la fila, así que este techo no lo afecta. */
    max-height: 6rem;
}

.ui-list__item:first-child { border-top: 0; }

/* ── GUÍAS DEL ÁRBOL ──────────────────────────────────────────────────────
   Una familia (una tarea con sus subtareas) se lee como UN grupo gracias a una
   guía vertical con el acento del producto que baja desde la pastilla de conteo
   del padre y recorre a todas sus hijas. Sin ella, la sangría sola parece un margen
   accidental — que es exactamente como se veía.

   TODAS las guías se pintan como CAPAS DE FONDO de la fila. Dos razones, cada
   una de un bug real:

     · Un pseudo-elemento NO sirve: la fila lleva `contain: paint`, y eso
       RECORTA a sus hijos —pseudo-elementos incluidos— al borde INTERIOR. Como cada fila tiene un `border-top` de
       1px, la línea se cortaba en cada frontera y el árbol se veía punteado.
       Un fondo sí pinta bajo el borde (`background-origin/clip: border-box`).

     · Y las DOS piezas —el arranque en la cabeza y la línea de las hijas—
       tienen que usar la MISMA técnica. Cuando el arranque era un
       pseudo-elemento y la continuación un degradado, la misma x se rasterizaba
       con medio pixel de diferencia y la línea se veía TORCIDA justo en la
       primera hija (la que no tiene chevrón). Por eso ambas salen de
       `--ui-list-guide-col`, que se calcula una sola vez.

   Los estados (hover, marcada, completada) solo fijan `background-color`, así
   que no borran la imagen. */
.ui-list__item {
    /* Una línea por cada nivel de ANCESTRO: el número no está escrito en ningún
       sitio, sale del ancho de la capa (`nivel × sangría`) sobre un degradado
       repetido. A nivel 0 la capa mide 0 y no se pinta nada. */
    background-image: var(--ui-list-guide-image);
    background-origin: border-box;
    background-clip: border-box;
    background-repeat: no-repeat;
    background-size: calc(min(var(--ui-list-level, 0), 6) * var(--ui-list-indent)) 100%;
    background-position: var(--ui-list-guide-col) 0;
}

/* CABEZA DE FAMILIA con su familia DESPLEGADA (la cadena del detalle abierto):
   además de las guías de sus ancestros, el ARRANQUE de la suya. Es una capa más
   —media altura, anclada abajo— que empieza a media fila, justo bajo su pastilla
   de conteo (si empezara arriba la atravesaría), y baja hasta el borde inferior,
   donde engancha con la guía de su primera hija. Es lo que hace que el grupo
   empiece EN el padre y no debajo de él. Plegada (el estado por defecto) la capa
   no se pinta: la cabeza sola no encabeza nada visible y la línea quedaría
   colgando en el vacío.

   Usa LA MISMA IMAGEN que las otras guías y con el MISMO ancho de mosaico
   (`--ui-list-indent`, del que solo se ve la primera línea), no un degradado
   sólido de 2px. Es deliberado: en pantallas con escala fraccionaria, una capa
   de 2px y un mosaico de 22.4px NO redondean la misma x al mismo pixel físico,
   y la línea se veía TORCIDA justo al pasar de la cabeza a su primera hija.
   Misma función de imagen y mismo período ⇒ misma rasterización. */
.ui-list__item.is-parent.is-expanded {
    background-image:
        var(--ui-list-guide-image),
        var(--ui-list-guide-image);
    /* La capa del arranque cubre UNA COLUMNA MÁS que la de ancestros (la
       propia), a media altura y anclada abajo. */
    background-size:
        calc((min(var(--ui-list-level, 0), 6) + 1) * var(--ui-list-indent)) 50%,
        calc(min(var(--ui-list-level, 0), 6) * var(--ui-list-indent)) 100%;
    /* MISMO ORIGEN que todas las demás capas de la lista. Es el invariante que
       mantiene la línea recta y lo que estaba mal: la capa del arranque partía
       de `guide-col + nivel × sangría`, así que su mosaico se rasterizaba desde
       una x distinta que el de la hija —misma coordenada nominal, otra FASE— y
       en escala fraccionaria las dos líneas no caían en el mismo pixel físico. */
    background-position:
        var(--ui-list-guide-col) 100%,
        var(--ui-list-guide-col) 0;
}

/* CONTEO DE SUBTAREAS. Vive en la columna reservada al inicio de la fila (la
   del difunto chevrón) y va POSICIONADO ABSOLUTO a propósito: como solo se
   renderiza en las filas con subtareas, si fuera un elemento de flujo
   desplazaría casilla y título en esas filas y no en las demás. Así ocupa su
   columna sin mover nada.

   NO es un control: el plegado se deriva del detalle abierto, así que el clic
   burbujea a la fila y un solo gesto abre el detalle Y despliega la familia.
   Es la cabeza de la guía —de él baja la línea del grupo al desplegarse— y por
   eso se alinea con la misma columna que `--ui-list-guide-x`. */
.ui-list__count {
    position: absolute;
    left: calc(var(--ui-list-pad-x) + min(var(--ui-list-level, 0), 6) * var(--ui-list-indent));
    top: 50%;
    transform: translateY(-50%);
    min-width: var(--ui-list-twisty-size);
    height: var(--ui-list-twisty-size);
    padding: 0 4px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 999px;
    font-size: 0.6875rem;
    font-weight: 600;
    line-height: 1;
    font-variant-numeric: tabular-nums;
    color: var(--bs-secondary-color);
    /* Fondo OPACO a propósito (mezcla contra el fondo del tema, no contra
       transparente — mismo truco que `--ui-list-guide`): la guía del grupo se
       pinta como fondo de la fila justo bajo esta columna, y con un tinte
       translúcido la línea se veía ATRAVESANDO el círculo. Opaca, la línea
       queda detrás y nace visualmente en el borde inferior de la pastilla. */
    background-color: color-mix(in srgb, var(--bs-body-color) 8%, var(--bs-body-bg));
    /* Por encima del radio que cubre la fila, para que su tooltip responda; el
       clic atraviesa igual (burbujea hasta la fila). */
    z-index: 1;
    transition: background-color 120ms ease-out, color 120ms ease-out;
}

/* Familia desplegada: la pastilla se tiñe del acento — es de donde nace la guía
   que baja hacia las hijas, y el tinte los ata visualmente. Igual de OPACA. */
.ui-list__count.is-open {
    color: var(--bs-primary);
    background-color: color-mix(in srgb, var(--bs-primary) 14%, var(--bs-body-bg));
}

/* CHEVRÓN DE APERTURA de las HOJAS. En una fila sin subtareas la columna del
   conteo quedaba muda y nada decía que la fila se abre. Es DECORATIVO puro
   (sin eventos: el clic es de la fila, por eso `pointer-events: none`) y sigue
   el mismo patrón del chulo de completar: invisible en reposo, aparece al
   pasar el cursor por la fila, y en táctil —donde no hay hover— está siempre,
   tenue. Vocabulario clásico de disclosure: cerrado apunta a la DERECHA
   ("aquí hay algo que abrir"); con el detalle abierto se queda fijo, en acento
   y apuntando hacia ABAJO — hacia lo que se abrió. */
.ui-list__disclose {
    position: absolute;
    left: calc(var(--ui-list-pad-x) + min(var(--ui-list-level, 0), 6) * var(--ui-list-indent));
    top: 50%;
    width: var(--ui-list-twisty-size);
    height: var(--ui-list-twisty-size);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 0.62rem;
    color: var(--bs-secondary-color);
    pointer-events: none;
    opacity: 0;
    /* El ícono es chevron-down: -90° lo deja apuntando a la derecha. */
    transform: translateY(-50%) rotate(-90deg);
    transition: opacity 120ms ease-out, color 120ms ease-out, transform 160ms ease-out;
}

@media (hover: hover) and (pointer: fine) {
    .ui-list__item:hover .ui-list__disclose { opacity: 1; }
}

.ui-list__item.is-open .ui-list__disclose {
    opacity: 1;
    color: var(--bs-primary);
    transform: translateY(-50%);   /* abierto: gira a su orientación natural, abajo */
}

/* Sin hover (táctil) no hay forma de "revelarlo": queda siempre visible, tenue
   — mismo criterio que el chulo de completar. */
@media (hover: none) {
    .ui-list__disclose { opacity: 0.55; }
    .ui-list__item.is-open .ui-list__disclose { opacity: 1; }
}

/* Dentro de una familia NO se dibuja el filete de separación: el bloque se lee
   como una pieza y los cortes fuertes quedan entre familias. El borde se
   conserva en TRANSPARENTE (no se quita) por dos motivos: mantiene el alto de
   la fila idéntico al de las demás, y deja que la guía —que se pinta como fondo
   con `background-origin: border-box`— lo cruce sin interrupción. */
.ui-list__item.is-nested {
    border-top-color: transparent;
}

/* Las hijas ENTRAN con una aparición sutil al desplegarse su familia. Solo
   ellas: una fila de nivel 0 jamás se inserta por una expansión (la carga
   inicial nace plegada), así que la lista completa nunca anima. `@starting-style`
   aplica al INSERTARSE el elemento en el DOM — cero JS, cero clases toggleadas;
   `opacity`/`translate` componen en GPU y no tocan layout. */
.ui-list__item.is-nested {
    transition: background-color 120ms ease-out, color 120ms ease-out,
                opacity 180ms ease-out, translate 180ms ease-out;
}

@starting-style {
    .ui-list__item.is-nested {
        opacity: 0;
        translate: 0 -6px;
    }
}

@media (prefers-reduced-motion: reduce) {
    .ui-list__item.is-nested {
        transition: background-color 120ms ease-out, color 120ms ease-out;
    }
}

/* El BLOQUE de la familia DESPLEGADA —cabeza e hijas— comparte un tinte mínimo
   del acento. Es la segunda mitad del agrupamiento: la guía dice "esto cuelga
   de aquello" y el tinte dice "esto es un bloque". Plegada (el defecto), la
   cabeza es una fila más: sin tinte, o media lista quedaría teñida sin familia
   visible que lo explique. Va ANTES de las reglas de estado (hover, marcada,
   completada) a propósito: todas se declaran después y deben pisarlo, porque el
   estado de una fila importa más que a qué grupo pertenece. */
.ui-list__item.is-parent.is-expanded,
.ui-list__item.is-nested {
    background-color: var(--ui-list-family-tint);
}


/* EL HOVER DE LA LISTA SOLO EXISTE CON PUNTERO FINO (2026-07-26). En táctil el
   navegador aplica `:hover` al tocar y lo deja PEGADO hasta el toque siguiente,
   así que la fila (y sus controles) se quedaban resaltados como si estuvieran
   activos sin estarlo; y en iOS un `:hover` que cambia estilos sobre el
   elemento tocado se come el PRIMER toque, que es la segunda mitad del bug de
   "hay que tocar dos veces". El dedo no puede "pasar por encima": una pista de
   hover no le sirve de nada y sí le estorba. El `:focus-visible` (teclado) se
   deja SIEMPRE fuera de esta puerta. */
@media (hover: hover) and (pointer: fine) {
    .ui-list__item:hover { background-color: color-mix(in srgb, var(--bs-body-color) 4%, transparent); }
}

.ui-list__item:focus-visible {
    outline: 2px solid var(--bs-primary);
    outline-offset: -2px;
}

/* DOS estados distintos que pueden convivir en la misma fila, y por eso usan
   señales visuales que no se confunden:

     · MARCADA (`is-marked`, la casilla): pertenece al conjunto sobre el que
       actuará una acción de lote. Tinte del acento + filete lateral. Es un
       estado de CONJUNTO: puede haber muchas a la vez.
     · ACTIVA (`is-active`, un clic en la fila): la tarea que el usuario está
       mirando, la que abre el doble clic. Anillo alrededor de la fila. Es
       ÚNICA, y por eso se dibuja como un contorno y no como un relleno — así
       se distingue de un vistazo aunque esté además marcada.

   El filete lateral se prefiere al borde completo del diseño de referencia
   porque este último se lee mal en la primera y la última fila (choca con el
   radio de la tarjeta). */
.ui-list__item.is-marked {
    background-color: color-mix(in srgb, var(--bs-primary) 7%, transparent);
    box-shadow: inset 3px 0 0 var(--bs-primary);
}

/* Sin esto el hover neutro PISA el tinte (misma especificidad, declarado
   después) y la fila parece desmarcarse al pasar el cursor. */
@media (hover: hover) and (pointer: fine) {
    .ui-list__item.is-marked:hover {
        background-color: color-mix(in srgb, var(--bs-primary) 12%, transparent);
    }
}

/* SALIDA al completar (`is-leaving`): la fila se desvanece, se desliza y COLAPSA
   su alto, de modo que sus vecinas suben suave en vez de dar un salto al
   recargar. `opacity`/`transform` los compone la GPU; el colapso de alto es la
   única parte que toca layout, pero para las pocas filas de una acción es
   imperceptible y lo que hace que se lea "chévere". La duración debe coincidir
   con `LeaveAnimationMs` en la página.
   Requiere un `max-height` base en la fila (abajo) para poder animar HASTA 0. */
.ui-list__item.is-leaving {
    opacity: 0;
    max-height: 0;
    padding-block: 0;
    border-color: transparent;
    transform: translateX(1.5rem);
    overflow: hidden;
    pointer-events: none;
    transition:
        opacity 160ms ease-out,
        transform 180ms ease-out,
        max-height 200ms ease-out,
        padding 200ms ease-out;
}

/* BORRADO (`is-deleting`): la fila se DESGARRA hacia la izquierda —al lado
   contrario que la salida al completar— mientras se tiñe de rojo y colapsa.
   La dirección es lo que distingue las dos salidas de un vistazo: completar
   despacha la tarea hacia adelante; borrar la retira.

   El tinte va con `!important` porque los estados de fila (hover, marcada,
   completada, tinte de familia) fijan `background-color` y ganarían — y aquí el
   color ES el mensaje. Es la única regla de la lista que lo necesita.

   La duración debe coincidir con `DeleteAnimationMs` en la página. Requiere el
   `max-height` base de la fila para poder animar HASTA 0. */
.ui-list__item.is-deleting {
    opacity: 0;
    max-height: 0;
    padding-block: 0;
    border-color: transparent;
    transform: translateX(-2rem) scale(0.97);
    background-color: color-mix(in srgb, var(--bs-danger) 20%, transparent) !important;
    overflow: hidden;
    pointer-events: none;
    transition:
        opacity 190ms ease-in,
        transform 220ms cubic-bezier(0.4, 0, 1, 1),
        background-color 120ms ease-out,
        max-height 240ms ease-in,
        padding 240ms ease-in;
}

/* Con menos movimiento: se desvanece y colapsa, sin el desgarro lateral. */
@media (prefers-reduced-motion: reduce) {
    .ui-list__item.is-deleting { transform: none; }
}

/* VUELTA AL TRABAJO (`is-returning`): al DESCOMPLETAR una tarea desde el
   ARCHIVO, deja de pertenecer a ese listado y se va — hacia la IZQUIERDA, de
   donde vino, que es el gesto contrario al de completar.

   Con esto el vocabulario de direcciones queda cerrado y se lee sin explicación:
   a la DERECHA se despacha (completar), a la IZQUIERDA en rojo se retira
   (borrar), y a la IZQUIERDA en neutro se DEVUELVE (reabrir). Nada de rojo aquí:
   reabrir no destruye nada, solo mueve la tarea de sitio.

   La duración debe coincidir con `ReturnAnimationMs` en la página. Requiere el
   `max-height` base de la fila para poder animar HASTA 0. */
.ui-list__item.is-returning {
    opacity: 0;
    max-height: 0;
    padding-block: 0;
    border-color: transparent;
    transform: translateX(-1.5rem);
    overflow: hidden;
    pointer-events: none;
    transition:
        opacity 160ms ease-out,
        transform 180ms ease-out,
        max-height 200ms ease-out,
        padding 200ms ease-out;
}

@media (prefers-reduced-motion: reduce) {
    .ui-list__item.is-returning { transform: none; }
}

/* ENTRADA de una fila recién capturada (`is-captured`). Es el gemelo exacto de
   la salida de arriba, al revés: la fila DESPLIEGA su alto, entra deslizándose
   y se enciende un instante con el acento, que se apaga solo. Que se despliegue
   —y no aparezca de golpe— es lo que hace que la lista se lea como si la fila
   se hubiera hecho sitio, en vez de dar un salto.

   Va con `@starting-style`: anima al INSERTARSE en el DOM, sin una línea de JS
   ni un segundo render que encienda una clase (mismo mecanismo que la apertura
   del detalle). El destello es una animación aparte porque tiene que volver a
   su sitio sola: una transición se quedaría en el valor final.

   `opacity`/`translate` los compone la GPU; el despliegue del alto es lo único
   que toca layout, y para una fila es imperceptible. */
.ui-list__item.is-captured {
    animation: ui-list-capture-flash 1100ms ease-out;
    transition:
        opacity 200ms ease-out,
        translate 200ms ease-out,
        max-height 200ms ease-out;
}

@starting-style {
    .ui-list__item.is-captured {
        opacity: 0;
        translate: -0.75rem 0;
        max-height: 0;
    }
}

/* Destello del acento que se desvanece: dice "esta es la que acabas de crear"
   sin dejar un resaltado permanente que después habría que explicar. */
@keyframes ui-list-capture-flash {
    from { background-color: color-mix(in srgb, var(--bs-primary) 16%, transparent); }
    to   { background-color: transparent; }
}

/* Quien pidió menos movimiento recibe solo el destello: sigue viendo CUÁL es la
   fila nueva, sin desplazamiento ni despliegue. */
@media (prefers-reduced-motion: reduce) {
    .ui-list__item.is-captured { transition: none; }
    @starting-style {
        .ui-list__item.is-captured { opacity: 1; translate: none; max-height: 6rem; }
    }
}

/* El título de una fila marcada gana peso: con varias, el bloque elegido se
   distingue de un vistazo sin recurrir a un fondo saturado. */
.ui-list__item.is-marked .ui-list__title { font-weight: 600; }

.ui-list__item.is-active {
    box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--bs-primary) 55%, transparent);
    border-radius: 6px;
}

/* La tarjeta recorta sus esquinas (`overflow: hidden` + radio), así que el
   anillo de la PRIMERA fila se veía cortado por arriba y el de la ÚLTIMA por
   abajo. Se les da el radio INTERIOR de la tarjeta (su radio menos el borde)
   para que el anillo siga la curva en vez de chocar contra ella.

   Va sobre `.ui-list > :first/last-child` y no sobre `.ui-list__item` porque
   con un detalle abierto al final el último hijo es el panel, no la fila — así
   la curva la lleva siempre quien de verdad cierra la tarjeta. Y con
   propiedades lógicas, cada regla toca solo DOS esquinas: no se pisan entre sí
   cuando la lista tiene una sola fila. */
.ui-list > :first-child {
    border-start-start-radius: 13px;
    border-start-end-radius: 13px;
}

.ui-list > :last-child {
    border-end-start-radius: 13px;
    border-end-end-radius: 13px;
}

/* Marcada Y activa: las dos señales a la vez — el filete lateral se dibuja
   dentro del anillo, así que ninguna tapa a la otra. */
.ui-list__item.is-marked.is-active {
    box-shadow:
        inset 0 0 0 2px color-mix(in srgb, var(--bs-primary) 55%, transparent),
        inset 3px 0 0 var(--bs-primary);
}

/* Toda la fila es clicable (selecciona), no solo la casilla. */
.ui-list__item { cursor: pointer; }

/* Al seleccionar se arrastra la selección de texto del navegador, que deja los
   títulos resaltados en azul y ensucia el gesto — sobre todo con Shift+clic,
   que además extiende la selección nativa de texto. */
.ui-list__item { user-select: none; }

/* Detalle abierto DENTRO de la lista, justo debajo de su fila (patrón Things).
   Es hermano de la fila, no hijo: así el `contain: paint` de la fila no lo
   recorta y su alto libre no afecta al de la fila. */
.ui-list__detail {
    /* SANGRADO CON SU FILA: el detalle es hermano, no hijo, así que sin esto
       una subtarea abriría su ficha pegada al margen del primer nivel. El
       padding llega hasta la columna de la CASILLA; lo que falta para alcanzar
       el título lo pone `.ui-detail` (casilla + hueco), de modo que el mismo
       cálculo sirve a la banda de escritorio y a la hoja móvil sin duplicarse. */
    padding: 2px var(--ui-list-pad-x) 0
             calc(var(--ui-list-pad-x) + var(--ui-list-twisty)
                + min(var(--ui-list-level, 0), 6) * var(--ui-list-indent));

    /* Fondo LISO del color de la hoja. SIN tinte y SIN DEGRADADO: se probaron
       los dos y Randolf los rechazó (el velo del acento, "vuelve todo muy
       morado, muy monótono"; el degradado gris que lo sustituyó, tampoco). El
       razonamiento de fondo se sostiene: el índigo del producto ya está en las
       guías del árbol, en la fila activa y en las pestañas, así que sumarlo al
       fondo del detalle era ruido y encima apagaba los ÚNICOS colores que deben
       resaltar aquí, los de los datos. Chrome invisible, color en el dato.

       Dónde TERMINA el detalle se lee sin fondo propio: si abajo viene una
       hija, esa fila lleva el tinte de familia; si viene una fila suelta, su
       filete separador es opaco. */
    background-color: var(--bs-body-bg);

    /* El detalle es HERMANO de la fila, así que se mete ENTRE una cabeza de
       familia y sus hijas. Sin dibujar él también las guías, la línea del grupo
       desaparecía por completo mientras la ficha estuviera abierta — que es
       justo cuando más falta hace saber de qué cuelga lo que se está mirando.
       Misma imagen y misma columna que la fila: se lee como una sola línea. */
    background-image: var(--ui-list-guide-image);
    background-origin: border-box;
    background-clip: border-box;
    background-repeat: no-repeat;
    background-size: calc(min(var(--ui-list-level, 0), 6) * var(--ui-list-indent)) 100%;
    background-position: var(--ui-list-guide-col) 0;

    /* Transparente para no cortar la guía: la fila abierta ya renuncia a su
       borde inferior (`is-open`), así que fila y detalle se leen como una pieza
       y el cambio de fondo basta como separación. */
    border-top: 1px solid transparent;

    /* Apertura ANIMADA: el panel entra creciendo desde cero (~200 ms) y ese
       tiempo es el colchón de la consulta que viene en camino — el contenido
       normalmente aterriza antes de que la expansión termine, así que abrir con
       UN clic se siente instantáneo sin haberlo sido. El truco 0fr→1fr anima
       hasta la altura natural del contenido sin conocerla; `@starting-style`
       (abajo) aplica el 0fr AL INSERTARSE el elemento en el DOM, sin JS ni
       clases toggleadas. Navegador sin soporte: abre sin animación, y ya. */
    display: grid;
    grid-template-rows: 1fr;
    /* 160 ms y curva DECELERADA (arranque rápido, frenada suave), no la `ease`
       de antes, que arrancaba despacio: en una expansión el ojo juzga la
       velocidad por los primeros milisegundos, así que una curva que sale
       lenta se siente lenta aunque dure lo mismo. Es la corrección más barata
       posible — no cambia lo que cuesta el frame, cambia cuándo ocurre el
       movimiento.

       Y hay que saber lo que ESTO cuesta: `grid-template-rows` NO es una
       propiedad de compositor. Animar 0fr→1fr rehace el LAYOUT en cada frame
       (el del panel y el de todo lo que tiene debajo), a diferencia de
       `translate`/`opacity`, que el compositor resuelve sin repintar. Se paga a
       propósito porque es lo único que anima hasta una altura desconocida sin
       JS; a cambio se mantiene corta. Si algún día pesa de verdad, la salida no
       es afinar la curva: es dejar de animar el alto y entrar solo con
       opacidad. */
    transition: grid-template-rows 160ms cubic-bezier(0.05, 0.7, 0.1, 1);
}

@starting-style {
    .ui-list__detail { grid-template-rows: 0fr; }
}

/* Durante la apertura la fila del grid mide menos que su contenido; el hijo la
   acompaña recortando en vez de desbordar sobre las filas de abajo. */
.ui-list__detail > * {
    min-height: 0;
    overflow: hidden;
}

/* Mientras el contenido viene en camino, el hueco reserva altura para que el
   reemplazo pendiente→detalle AJUSTE en vez de saltar.

   Sube de 3.75 rem a 7 rem porque con la tarea sin cachear se encadenaban DOS
   expansiones —primero al hueco, después al contenido— y ese doble movimiento
   es lo que se percibía como lentitud (reportado por Randolf). 7 rem es la
   altura de un panel de pocos campos: el segundo tramo pasa a ser un ajuste
   corto, y en las tareas con poco contenido no llega a encoger. */
.ui-list__detail:has(> .ui-list__detail-pending) { min-height: 7rem; }

@media (prefers-reduced-motion: reduce) {
    .ui-list__detail { transition: none; }
}

/* Detalle de una cabeza de familia DESPLEGADA: la guía tiene que seguir hasta
   las hijas que vienen debajo, así que se pinta una columna MÁS que la de sus
   ancestros — la suya propia. */
.ui-list__detail.is-parent {
    background-size: calc((min(var(--ui-list-level, 0), 6) + 1) * var(--ui-list-indent)) 100%;
}

/* La fila con el detalle abierto pierde la separación inferior: fila y detalle
   se leen como UNA pieza. */
.ui-list__item.is-open { border-bottom: 0; }

/* Indicador de carga con APARICIÓN DIFERIDA: arranca invisible y solo se revela
   a los 250 ms. Si la respuesta llega antes —el caso normal, porque el detalle
   se precarga al seleccionar la fila— el indicador nunca se ve. Un destello de
   "cargando" más corto que un parpadeo se percibe como un defecto, no como
   información; es la razón por la que las guías de rendimiento recomiendan no
   mostrar nada hasta pasado ese umbral. */
.ui-list__detail-pending {
    opacity: 0;
    animation: ui-reveal-late 1ms linear 250ms forwards;

    /* Centrado en el hueco reservado del detalle. Como ítem de grid el span se
       blockifica, pero su tamaño explícito (spinner-border-sm) lo mantiene
       chico; esto solo decide DÓNDE queda. */
    justify-self: center;
    align-self: center;
}

@keyframes ui-reveal-late { to { opacity: 1; } }

/* Mismo criterio para la LISTA ENTERA mientras espera los datos del filtro que
   se acaba de elegir. Reserva alto para que la página no dé un salto cuando las
   filas entren, y se revela igual de tarde: en un cambio de filtro normal la
   entrega llega antes de los 250 ms y el usuario nunca ve un indicador. */
.ui-list-pending {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 4rem;

    opacity: 0;
    animation: ui-reveal-late 1ms linear 250ms forwards;
}

/* Título: ocupa el espacio libre y se recorta con puntos suspensivos — nunca
   empuja los metadatos ni parte la fila en dos líneas. */
.ui-list__title {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;

    /* Aire entre la franja de la casilla y el texto. Va como padding del TÍTULO
       y no como recorte de la franja: así el texto respira sin que el objetivo
       de clic pierda ni un píxel. */
    padding-inline-start: 0.4rem;
}

/* Metadatos al final de la fila (fechas, avatares): no se encogen. */
/* RENOMBRAR: el lápiz abre el modo y el chulo lo cierra guardando. Solo
   aparece al pasar por encima de la fila —o cuando el teclado lo enfoca— para
   no llenar la lista de controles: en reposo, una fila son datos, no botones.
   En táctil no hay hover y el lápiz no aparece nunca; ahí el título se edita en
   la hoja, que es donde se ve. */
.ui-list__rename-btn {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.75rem;
    height: 1.75rem;
    border: 0;
    border-radius: 7px;
    background: transparent;
    color: var(--bs-secondary-color);
    font-size: 0.8rem;
    opacity: 0;
    cursor: pointer;
    transition: opacity 120ms ease-out, background-color 120ms ease-out, color 120ms ease-out;
}

.ui-list__rename-btn:focus-visible { opacity: 0.65; }

@media (hover: hover) and (pointer: fine) {
    .ui-list__item:hover .ui-list__rename-btn { opacity: 0.65; }

    .ui-list__rename-btn:hover {
        opacity: 1;
        background-color: color-mix(in srgb, var(--bs-body-color) 8%, transparent);
        color: var(--bs-body-color);
    }
}

/* EN TÁCTIL EL LÁPIZ NO EXISTE, y tiene que dejar de OCUPAR SITIO: con
   `opacity: 0` seguía siendo un objetivo de 28 px perfectamente tocable —
   invisible— justo antes de la pastilla de vencimiento, así que un toque algo
   corto entraba a renombrar sin que nada lo anunciara. Ahí el título se edita
   en la hoja, que es donde se ve. */
@media (hover: none) {
    .ui-list__rename-btn:not(.is-on) { display: none; }
}

.ui-list__rename-btn:focus-visible {
    outline: none;
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--bs-primary) 25%, transparent);
}

/* GUARDANDO: deja de ser discreto — es la salida del modo y debe verse siempre,
   también con el dedo, sin depender del hover. */
.ui-list__rename-btn.is-on {
    opacity: 1;
    color: var(--bs-primary);
    background-color: color-mix(in srgb, var(--bs-primary) 12%, transparent);
}

/* EL CAMPO OCUPA EL SITIO EXACTO DEL TÍTULO: mismo tipo, mismo tamaño y el
   relleno compensado con margen negativo, así el texto no salta ni un píxel al
   entrar o salir del modo — y la banda del detalle, que se alinea con este
   texto, tampoco se mueve. */
.ui-list__rename {
    flex: 1 1 auto;
    min-width: 0;
    margin-inline-start: 0.4rem;   /* el aire que el título pone con padding */
    padding: 2px 6px;
    margin-block: -2px;
    border: 1px solid color-mix(in srgb, var(--bs-primary) 45%, transparent);
    border-radius: 7px;
    background-color: var(--bs-body-bg);
    color: inherit;
    font: inherit;
}

.ui-list__rename:focus {
    outline: none;
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--bs-primary) 22%, transparent);
}

.ui-list__meta {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    flex-shrink: 0;
}

/* Casilla de SELECCIÓN al inicio de la fila. Es un <input type="checkbox">
   REAL: así la barra espaciadora lo alterna de forma NATIVA cuando tiene el
   foco (sin scroll de página, sin preventDefault y sin una línea de JS), y Tab
   lo alcanza solo. Cualquier reemplazo por un <span> obligaría a reimplementar
   a mano todo ese comportamiento del navegador.

   CUADRADA a propósito: el círculo es el vocabulario universal de "tarea
   completada" (Things, Todoist, Apple Recordatorios), así que usarlo para
   seleccionar confundía las dos acciones. El cuadrado dice "elegir", y deja el
   círculo libre para cuando la fila gane su marca de completado. */
.ui-list__check {
    flex-shrink: 0;
    width: var(--ui-list-twisty-size);
    height: var(--ui-list-twisty-size);
    margin: 0;
    border-radius: 0.25rem;
    cursor: pointer;
}

/* Radio de NAVEGACIÓN: invisible pero CUBRIENDO la fila entera.
   No es la técnica sr-only de recortar a 1px, y la diferencia importa: el foco
   por ratón lo decide el `mousedown`, que aterriza en lo que esté BAJO EL
   CURSOR. Con el radio recortado fuera de la vista, el clic caía sobre el texto
   —que no es enfocable— y el foco se iba al `body`; el radio quedaba marcado
   pero SIN foco, así que las flechas no respondían hasta un segundo clic
   (comprobado en vivo: `document.activeElement` daba BODY).
   Extendido sobre la fila, el `mousedown` lo golpea a él: el navegador lo
   enfoca y lo marca de forma nativa. Ni `FocusAsync`, ni una referencia por
   fila, ni JS. Lo que se ve sigue siendo el anillo de la fila. */
.ui-list__radio {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    margin: 0;
    padding: 0;
    opacity: 0;
    cursor: pointer;
    /* Debajo de la casilla, que debe seguir recibiendo sus propios clics. */
    z-index: 0;
}

/* Como el control enfocado es invisible, el foco lo delata la FILA. Sin esto,
   navegar con las flechas no se vería. */
.ui-list__item:has(.ui-list__radio:focus-visible) {
    outline: 2px solid var(--bs-primary);
    outline-offset: -2px;
}

/* ÁREA SENSIBLE de la casilla = toda la franja izquierda de la fila, de borde
   a borde y de alto completo, no el cuadro de 17px. Un objetivo de 17px está
   muy por debajo del mínimo de 44px de las guías táctiles y obliga a apuntar
   con precisión también con el ratón.

   Se consigue con un pseudo-elemento estirado: los eventos de un pseudo-elemento
   pertenecen a su ELEMENTO, así que un clic en cualquier punto de la franja es
   un clic en la casilla — sin JS y sin envolver la casilla en otro contenedor
   que rompería la separación entre marcar y seleccionar.

   Los desbordes verticales generosos los recorta la propia fila (`contain:
   paint`), así que la franja termina
   midiendo exactamente el alto de la fila sin tener que calcularlo. */
.ui-list__check {
    position: relative;
    /* POR ENCIMA del radio de navegación, que cubre la fila entera: la franja
       de la casilla debe seguir recibiendo sus propios clics (marcar), no
       activar la fila. */
    z-index: 1;
}

.ui-list__check::before {
    content: "";
    position: absolute;
    top: -2rem;         /* recortado por la fila */
    bottom: -2rem;      /* recortado por la fila */
    /* La casilla se mudó al EXTREMO DERECHO, así que su franja mira hacia allá:
       hasta el borde de la tarjeta, y por la izquierda solo el hueco previo
       —sin llegar al avatar— para no marcar al tocarlo. Es exactamente la
       franja que tenía el chulo cuando ocupaba este sitio: al intercambiar los
       dos controles se intercambian también sus áreas, o el clic aterrizaría
       en el lado equivocado. */
    right: calc(-1 * var(--ui-list-pad-x));
    left: -0.3rem;
    z-index: -1;        /* detrás del cuadro, para que el realce no lo tape */
}

/* La señal del hover es un HALO pequeño alrededor de la casilla, no el tinte de
   la franja completa: teñir todo el rectángulo se leía como un subrayado extraño
   cruzando la columna del chevrón/conteo (reporte de Randolf, 2026-07-24). La
   franja SIGUE siendo el área de clic íntegra —eso no cambió—; como el `::before`
   pertenece a la casilla, pasar el cursor por CUALQUIER punto de la franja
   enciende el halo sobre la casilla: la pista apunta a lo que el clic hace. */
@media (hover: hover) and (pointer: fine) {
    .ui-list__check:hover {
        box-shadow: 0 0 0 5px color-mix(in srgb, var(--bs-body-color) 8%, transparent);
    }
}

/* Fila COMPLETADA que permanece en la vista (modo "al finalizar el día"): se
   HUNDE (recessed, patrón Things 3). Sigue ahí como registro del progreso del
   día y se limpia sola al terminar el día (o con "Limpiar ahora"), pero retrocede
   para no competir con lo que aún está por hacer: fondo levemente inset, texto en
   el tono secundario y tachado, metadatos atenuados. La transición de color de la
   fila (arriba) hace que el hundimiento sea suave cuando se completa en el sitio.

   `@key` por id conserva el nodo al recargar, así que la fila no se recrea: solo
   gana `is-completed` y transiciona. */
.ui-list__item.is-completed {
    background-color: color-mix(in srgb, var(--bs-body-color) 3.5%, transparent);
    color: var(--bs-secondary-color);
}

@media (hover: hover) and (pointer: fine) {
    .ui-list__item.is-completed:hover {
        background-color: var(--bs-body-bg);
    }
}

.ui-list__item.is-completed .ui-list__title {
    color: var(--bs-secondary-color);
    font-weight: 400;
    text-decoration: line-through;
    text-decoration-color: color-mix(in srgb, var(--bs-secondary-color) 55%, transparent);
}

/* Los metadatos (fecha, avatar) retroceden con la fila… menos la marca de HECHA,
   que debe quedar nítida como la única señal viva. La `opacity` se COMPONE con la
   del hijo, así que no se puede "des-atenuar" un hijo de un padre atenuado: por
   eso se atenúan los hermanos de la marca uno a uno, no el `ui-list__meta` entero. */
.ui-list__item.is-completed .ui-list__meta > :not(.ui-list__done) { opacity: 0.65; }

/* Marca de COMPLETADA, en el EXTREMO DERECHO de la fila. Selección a la izquierda
   (casilla CUADRADA = "elegir"), estado a la derecha (este CÍRCULO verde =
   "hecho"): forma distinta, color distinto y extremo distinto, para que las dos
   ideas NO puedan confundirse (era el punto: reutilizar la casilla las mezclaba).
   Sobre la fila recessed, el verde es el único acento vivo — el "check
   satisfactorio" del patrón Things. */
.ui-list__done {
    flex-shrink: 0;
    width: 1.15rem;
    height: 1.15rem;
    border-radius: 50%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 0.6rem;
    /* El verde lo pone la regla compartida del círculo de estado. */
}

/* Botón de COMPLETAR: el gemelo ACCIONABLE del chulo verde, en el mismo extremo
   derecho de una fila NO completada. Un círculo con el check tenue que, al pasar
   el cursor o con foco, se rellena de verde: "clic o toque para completar" — la
   versión con ratón/dedo del "marcar como realizada" del clic derecho. Mismo
   tamaño que `ui-list__done` para que la columna derecha no salte entre filas
   completadas y no. */
.ui-list__complete {
    /* La forma la pone el círculo de estado compartido (arriba); aquí solo va
       la medida de la fila y su apilado. */
    width: 1.15rem;
    height: 1.15rem;
    font-size: 0.58rem;
    /* Por encima del radio de navegación que cubre la fila, como la casilla. */
    position: relative;
    z-index: 1;
}

.ui-list__complete:focus-visible {
    border-color: var(--bs-success);
    background: var(--bs-success);
    color: #fff;
    outline: none;
}

/* El relleno verde es la PREVISUALIZACIÓN de "esto va a completar", y solo tiene
   sentido con un puntero que se posa antes de hacer clic. En táctil se aplicaba
   al tocar y se quedaba pegado: el chulo se ponía verde como si hubiera
   completado y la tarea seguía igual — exactamente lo reportado ("se activa el
   ícono y no pasa nada"). Ahí el estado real lo dice la fila (tachada y con su
   chulo verde), no un hover. */
@media (hover: hover) and (pointer: fine) {
    .ui-list__complete:hover {
        border-color: var(--bs-success);
        background: var(--bs-success);
        color: #fff;
    }
}

/* Botón de REABRIR: el chulo verde de una fila completada, ahora ACCIONABLE —lo
   "des-completa" (marca como no realizada). Hereda de `ui-list__done` el relleno
   verde y el tamaño; aquí van solo los reseteos de <button> y el estado
   interactivo. */
.ui-list__done--action {
    border: 0;
    padding: 0;
    cursor: pointer;
    position: relative;
    z-index: 1;
    transition: box-shadow 120ms ease-out;
}

/* Al pasar el cursor (o con foco) NO se pierde el verde. La versión anterior lo
   volvía un contorno transparente para "previsualizar" el des-completar, pero eso
   confundía JUSTO al completar: el ratón queda sobre el chulo recién hecho y el
   hover lo hacía parecer des-marcado (reportado por Randolf). El chulo sigue
   verde; solo un halo sutil delata que es accionable (un clic lo reabre). */
.ui-list__done--action:focus-visible {
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--bs-success) 32%, transparent);
    outline: none;
}

@media (hover: hover) and (pointer: fine) {
    .ui-list__done--action:hover {
        box-shadow: 0 0 0 2px color-mix(in srgb, var(--bs-success) 32%, transparent);
    }
}

/* El círculo visible mide ~18 px: un objetivo pequeño para un dedo (guía: 44 px).
   Un pseudo-elemento estira el ÁREA SENSIBLE a lo ALTO de la fila y hasta el borde
   DERECHO de la tarjeta, sin agrandar el círculo. Los eventos del pseudo son del
   botón, así que tocar cerca del chulo completa igual. Se extiende a la derecha y
   al hueco previo —NO hacia el avatar (a 0.3rem, sin cruzar el gap de 0.4rem)—
   para no actuar por error al tocarlo. La fila (`contain: paint`) recorta el
   desborde vertical al alto exacto de la fila.
   Aplica a AMBOS: completar (fila no hecha) y reabrir (fila hecha). */
.ui-list__complete::before,
.ui-list__done--action::before {
    content: "";
    position: absolute;
    top: -1rem;        /* recortado por la fila → alto completo */
    bottom: -1rem;
    /* Ahora el chulo abre la fila, así que su franja se estira hacia la
       IZQUIERDA —quedándose la columna del chevrón cuando está vacía— y por la
       derecha llega justo antes del título: el hueco de la fila leído de la
       variable, que en el teléfono es más ceñido (con un valor fijo se pasaba
       sobre el texto y tocar la primera letra completaba la tarea). */
    /* SOLO EL HUECO PREVIO, nunca la columna del chevrón/conteo. Esa columna es
       de la FILA: el chevrón anuncia que se expande —y es `pointer-events:
       none` justo para que el clic burbujee— y la pastilla de conteo abre la
       familia. Estirarla hasta el borde de la tarjeta hacía que apuntar al
       chevrón COMPLETARA la tarea (bug reportado por Randolf, 2026-08-03);
       el hueco de 16 px que ese estirado pretendía recuperar no estaba muerto,
       era la columna de expandir. */
    left: -0.3rem;
    right: calc(-1 * var(--ui-list-gap));
}

/* Con ratón (puntero fino con hover) el chulo aparece al pasar por la FILA, para
   no llenar la lista de círculos en reposo. En TÁCTIL no hay hover —el dedo no
   puede "pasar por encima"—, así que ahí queda SIEMPRE visible (esta regla no
   aplica y gana el `opacity` por defecto). El foco por teclado también lo revela. */
/* AL INVERTIR LOS PAPELES SE INVIERTE TAMBIÉN QUIÉN SE VE EN REPOSO. El chulo
   pasó a ser el control PRIMARIO —lo que se hace en la inmensa mayoría de las
   filas— así que ya no se esconde: dejarlo con `opacity: 0` habría dejado la
   columna de la izquierda vacía en reposo y la lista sin su riel de anclaje.

   Quien se revela ahora es la CASILLA, que es lo excepcional: con ratón
   aparece al pasar por la fila, para no llenar la lista de cuadros que casi
   nadie usa. En TÁCTIL no hay hover —el dedo no puede "pasar por encima"—, así
   que ahí queda siempre visible (esta regla no aplica). Y una casilla MARCADA
   se ve SIEMPRE, con hover o sin él: esconder lo que está seleccionado dejaría
   al usuario sin saber sobre qué van a actuar las acciones de lote. */
@media (hover: hover) and (pointer: fine) {
    .ui-list__check { opacity: 0; }

    .ui-list__item:hover .ui-list__check,
    .ui-list__check:checked,
    .ui-list__check:focus-visible { opacity: 1; }
}


/* ── LA FILA EN EL TELÉFONO: todo el ancho es para el TÍTULO ──────────────────
   Decisión de Randolf (2026-07-26): en un teléfono la fila no da para leer la
   tarea. Medido en 390 px con la fila completa, al título le quedaban 142 px —
   media docena de palabras cortadas. El resto se lo repartían piezas que ahí no
   hacen falta:

     · El CHEVRÓN y el CONTADOR de subtareas viven en una columna reservada de
       28 px al inicio de TODAS las filas. Su trabajo es anunciar que la fila se
       abre y cuántas hijas tiene, y en el teléfono eso lo cuenta la propia hoja
       del detalle ("Subtareas 3", con la lista). Tocar la fila la abre igual:
       ninguno de los dos era el gesto, solo la pista.
     · La CASILLA de selección, ahora en el extremo derecho. Aquí solo quita
       ancho al texto: marcar filas para una acción de LOTE es un gesto de
       escritorio, y en el teléfono cada píxel es una letra más. El chulo de
       completar SÍ se queda —desde que se mudó al inicio es el control
       primario, y completar con el pulgar es justo lo que se hace en el
       móvil—, ocupando la columna que la casilla dejó libre: el título
       arranca donde arrancaba antes.

   Se quitan los dos extremos y se recorta el relleno lateral, que en escritorio
   da aire y aquí es texto perdido. La marca VERDE de completada se queda: es
   estado, no acción, y es lo que hace legible el archivo.

   Todo con `display: none`, no con opacidad: un control invisible pero tocable
   es la trampa que ya se corrigió en el lápiz de renombrar. */
@media (max-width: 767.98px) {

    .ui-list {
        /* La columna reservada del inicio desaparece con lo que la ocupaba. */
        --ui-list-twisty: 0px;
        /* Relleno lateral más ceñido (entero, por la regla de la guía). */
        --ui-list-pad-x: 10px;
        /* Y la sangría por nivel: 22 px por escalón se comen una línea entera a
           dos niveles de profundidad. 16 sigue leyéndose como jerarquía. */
        --ui-list-indent: 16px;
        /* Hueco entre las piezas de la fila: lo mismo, cada píxel es una letra. */
        --ui-list-gap: 8px;
    }

    .ui-list__disclose,
    .ui-list__count,
    .ui-list__check { display: none; }

    /* Y el LÁPIZ de renombrar, que se llevaba 28 px del título (lo cazó Randolf
       en una captura: "Indagar por el estado de la migración d…"). Aquí no es
       una pérdida de función: en el teléfono el título se edita en la HOJA, que
       es donde de verdad se lee. El chulo de GUARDAR se exceptúa —si alguien
       entra a renombrar por el menú de ⋯, tiene que poder salir— y por eso la
       regla lleva `:not(.is-on)`.

       Convive con la de `hover: none` de más arriba y no la reemplaza: aquella
       cubre una tablet ancha con dedo (sin hover que revele el lápiz), y esta,
       una ventana angosta con ratón. Son dos motivos distintos para lo mismo. */
    .ui-list__rename-btn:not(.is-on) { display: none; }

    /* El TÍTULO arranca antes: menos hueco entre piezas y menos aire propio.
       El aire sigue existiendo (el texto no se pega a la casilla), solo que
       medido para una pantalla en la que cada píxel es una letra más. */
    .ui-list__title { padding-inline-start: 0.2rem; }

    /* La FRANJA SENSIBLE de la casilla se reancla al borde de la fila: su
       `left` se calculaba desde la columna que acaba de desaparecer, así que
       sin esto el área de marcar se encogía justo al cuadro dibujado. */
    .ui-list__check::before,
    .ui-list__item.is-parent .ui-list__check::before {
        left: calc(-1 * var(--ui-list-pad-x));
    }
}


/* ── 3. Pastilla de metadato ─────────────────────────────────────────────────
   Dato corto y secundario dentro de una fila (una fecha, un conteo). El tono
   comunica urgencia; el neutro es el default. */

.ui-pill {
    /* inline-flex por el ícono opcional (DueDatePill y las pastillas del
       detalle): mantiene el flujo en línea y separa ícono/texto sin espacios
       fantasma del markup. */
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    font-size: 0.72rem;
    font-weight: 600;
    line-height: 1;
    padding: 0.28rem 0.5rem;
    border-radius: 999px;
    white-space: nowrap;
    background: color-mix(in srgb, var(--bs-body-color) 7%, transparent);
    /* NO `--bs-secondary-color`: MEDIDO sobre este mismo fondo tintado da 3.69:1
       en oscuro, y esto es texto pequeño en negrita (0.72rem), que exige 4.5:1.
       Es el mismo defecto que las variantes de color de abajo ya corrigieron con
       los tokens `*-text-emphasis`; la pastilla BASE se había quedado sin él.
       Al 90% del color del cuerpo pasa en los dos temas (8.3:1 claro, 4.6:1
       oscuro) conservando el tono apagado que la distingue del texto normal. */
    color: color-mix(in srgb, var(--bs-body-color) 90%, transparent);
}

/* Requiere atención hoy o ya se pasó.

   El texto NO usa `var(--bs-danger)` puro: MEDIDO sobre superficie blanca da
   2.96:1, y esta pastilla es texto pequeño en negrita (0.72rem), que exige
   4.5:1. `--bs-*-text-emphasis` son los tokens que Bootstrap calcula
   precisamente para texto sobre un fondo tintado del mismo color, y pasan en
   los dos temas (11.2:1 en claro, 6.4:1 en oscuro) sin perder el rojo. */
.ui-pill--danger {
    background: color-mix(in srgb, var(--bs-danger) 12%, transparent);
    color: var(--bs-danger-text-emphasis);
}

/* Algo anda mal pero no es una fecha incumplida: un proyecto que no se mueve,
   trabajo detenido. Ámbar = "míralo", frente al rojo = "ya se pasó". */
.ui-pill--warning {
    background: color-mix(in srgb, var(--bs-warning) 16%, transparent);
    /* Misma razón que el rojo: el ámbar puro sobre blanco mide 2.6:1. */
    color: var(--bs-warning-text-emphasis);
}

/* Informativo con peso: no es un problema, es algo que el sistema señala
   (toca revisar, esperando a un tercero). Toma el acento del producto. */
.ui-pill--accent {
    background: color-mix(in srgb, var(--bs-primary) 12%, transparent);
    color: color-mix(in srgb, var(--bs-primary) 80%, var(--bs-body-color));
}

/* En oscuro el índigo del acento se hunde contra el fondo: MEDIDO, la fórmula
   de arriba da 2.6:1 sobre la superficie de una tarjeta, muy por debajo del
   4.5:1 que exige un texto de 0.72rem. Se aclara hacia el blanco, que conserva
   el tinte del producto en vez de virarlo a gris. */
[data-bs-theme=dark] .ui-pill--accent {
    color: color-mix(in srgb, var(--bs-primary) 45%, #fff);
}


/* ── 4. Avatar de iniciales ──────────────────────────────────────────────────
   Identifica a una persona sin cargar una imagen. El color lo decide quien lo
   usa (variable --ui-avatar-bg) para que la misma persona conserve el suyo. */

.ui-avatar {
    --ui-avatar-bg: var(--bs-secondary);
    flex-shrink: 0;
    width: 1.6rem;
    height: 1.6rem;
    border-radius: 50%;
    background: var(--ui-avatar-bg);
    color: #fff;
    font-size: 0.62rem;
    font-weight: 700;
    letter-spacing: 0.02em;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    text-transform: uppercase;
}

/* Variante compacta, para cuando el avatar acompaña a un texto en una ficha
   (el DRI del panel de historia) y no debe competir con él. */
.ui-avatar--sm {
    width: 1.25rem;
    height: 1.25rem;
    font-size: 0.52rem;
}


/* ── 5. Menú contextual ──────────────────────────────────────────────────────
   Menú flotante que abre el clic derecho (evento nativo `@oncontextmenu` de
   Blazor; el CSS solo lo presenta). Va con un backdrop invisible de cuerpo
   completo que lo cierra al primer clic en cualquier parte.

   Uso:
     @if (MenuOpen)
     {
       <div class="ui-menu-backdrop" @onclick="Close"></div>
       <div class="ui-menu" style="left: {x}px; top: {y}px">
         <button class="ui-menu__item">…</button>
       </div>
     } */

.ui-menu-backdrop {
    position: fixed;
    inset: 0;
    z-index: 1050;
}

.ui-menu {
    position: fixed;
    z-index: 1051;
    min-width: 12rem;
    padding: 0.35rem;
    border-radius: 10px;
    background: var(--bs-body-bg);
    border: 1px solid var(--bs-border-color);
    box-shadow: 0 8px 24px -8px rgba(15, 23, 42, 0.35);
}

/* Variante ANCLADA: el mismo menú, colgando de su botón en vez de flotar en las
   coordenadas del puntero. Es lo que da un camino táctil a las acciones (en un
   móvil no hay clic derecho) sin JS de posicionamiento: se apoya en el mismo
   `ui-popover-anchor` del popover de ajustes. */
.ui-menu--anchored {
    position: absolute;
    top: calc(100% + 0.4rem);
    inset-inline-end: 0;
}

.ui-menu__item {
    width: 100%;
    border: 0;
    background: transparent;
    color: var(--bs-body-color);
    text-align: start;
    font-size: 0.85rem;
    padding: 0.45rem 0.6rem;
    border-radius: 7px;
    display: flex;
    align-items: center;
    gap: 0.55rem;
    cursor: pointer;
    transition: background-color 110ms ease-out;
}

.ui-menu__item:hover,
.ui-menu__item:focus-visible {
    background: color-mix(in srgb, var(--bs-body-color) 7%, transparent);
    outline: none;
}

/* Acción DESTRUCTIVA: en rojo y separada por un filete de las demás, para que
   no se confunda con ellas al recorrer el menú a la carrera. El filete solo
   aparece si hay algo encima (`:not(:first-child)`): con una sola acción en el
   menú, una línea suelta arriba sería ruido. */
.ui-menu__item--danger { color: var(--bs-danger); }

.ui-menu__item--danger:not(:first-child) {
    margin-top: 0.25rem;
    padding-top: 0.5rem;
    border-top: 1px solid var(--bs-border-color);
    border-radius: 0 0 7px 7px;
}

.ui-menu__item--danger:hover,
.ui-menu__item--danger:focus-visible {
    background: color-mix(in srgb, var(--bs-danger) 12%, transparent);
}

/* Explicación cuando no hay ninguna acción posible (p. ej. sin estado "Hecha"
   configurado): un texto tenue, no un botón muerto. */
.ui-menu__empty {
    display: block;
    padding: 0.45rem 0.6rem;
    font-size: 0.82rem;
    color: var(--bs-secondary-color);
}


/* ── 6. Botón de ícono ───────────────────────────────────────────────────────
   Disparador compacto y CUADRADO para una acción secundaria representada solo
   por un ícono (ajustes, "más"…). No es un `<button class="btn">` de Bootstrap a
   propósito: en una barra de acciones que se estira en móvil, un `.btn` se
   ensancharía a todo el ancho; este conserva su tamaño. */

.ui-icon-btn {
    flex-shrink: 0;
    width: 2.125rem;
    height: 2.125rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 9px;
    border: 1px solid var(--bs-border-color);
    background: var(--bs-body-bg);
    color: var(--bs-body-color);
    font-size: 0.95rem;
    cursor: pointer;
    transition:
        background-color 120ms ease-out,
        border-color 120ms ease-out,
        color 120ms ease-out;
}

.ui-icon-btn:hover {
    background: color-mix(in srgb, var(--bs-body-color) 5%, transparent);
}

.ui-icon-btn:focus-visible {
    outline: 2px solid var(--bs-primary);
    outline-offset: 1px;
}

/* Activo = su popover está abierto: tinte del acento, para que se lea que el
   panel que cuelga de él le pertenece. */
.ui-icon-btn.is-on {
    background: color-mix(in srgb, var(--bs-primary) 12%, var(--bs-body-bg));
    border-color: color-mix(in srgb, var(--bs-primary) 45%, transparent);
    color: var(--bs-primary);
}


/* ── 7. Popover anclado de ajustes ───────────────────────────────────────────
   Panel flotante que CUELGA de su disparador (un `ui-icon-btn`), SIN JS de
   posición: el disparador y el panel van dentro de un ancla `position: relative`
   y el panel se ancla con `position: absolute`. Cierra con el MISMO backdrop
   invisible del menú contextual (`ui-menu-backdrop`) — y, a diferencia de un
   menú, NO se cierra al elegir dentro: es un panel de AJUSTES, se queda abierto
   mientras se toca (patrón Linear/Todoist).

   Es un CONTENEDOR QUE CRECE: se compone de secciones (`ui-popover__section`),
   una por grupo de ajustes. Sumar un ajuste es una sección (o una fila) más,
   cero refactor.

   Anclaje/z-index calcados del menú contextual: el ancla no crea contexto de
   apilado (sin z-index/opacity/transform), así que el panel (z 1051) gana al
   backdrop fijo (z 1050) en el contexto raíz — mismo modelo verificado de
   `ui-menu` + `ui-menu-backdrop`.

   Uso:
     <div class="ui-popover-anchor">
       <button class="ui-icon-btn is-on" @onclick="Toggle"><i class="fa-solid fa-gear"></i></button>
       @if (Open)
       {
         <div class="ui-menu-backdrop" @onclick="Close"></div>
         <div class="ui-popover" role="dialog">
           <div class="ui-popover__head">…</div>
           <div class="ui-popover__section">…</div>
         </div>
       }
     </div> */

.ui-popover-anchor { position: relative; }

.ui-popover {
    position: absolute;
    top: calc(100% + 0.5rem);
    inset-inline-end: 0;              /* alineado al borde derecho del disparador */
    z-index: 1051;                    /* sobre el ui-menu-backdrop (1050) */
    width: 19rem;
    max-width: min(19rem, 90vw);      /* nunca se sale de la pantalla en móvil */
    padding: 0.35rem;
    border-radius: 13px;
    background: var(--bs-body-bg);
    border: 1px solid var(--bs-border-color);
    box-shadow: 0 18px 48px -18px rgba(15, 23, 42, 0.4);
}

.ui-popover__head {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 0.5rem 0.6rem;
}

.ui-popover__head-icon {
    color: var(--bs-secondary-color);
    font-size: 0.85rem;
}

.ui-popover__title {
    flex: 1 1 auto;
    font-size: 0.9rem;
    font-weight: 600;
}

.ui-popover__close {
    flex-shrink: 0;
    border: 0;
    background: transparent;
    color: var(--bs-secondary-color);
    line-height: 1;
    padding: 0.2rem;
    border-radius: 6px;
    cursor: pointer;
    transition: background-color 110ms ease-out, color 110ms ease-out;
}

.ui-popover__close:hover,
.ui-popover__close:focus-visible {
    background: color-mix(in srgb, var(--bs-body-color) 7%, transparent);
    color: var(--bs-body-color);
    outline: none;
}

/* Cada grupo de ajustes es una sección con su encabezado. El filete superior la
   separa de la cabecera del panel (y a las secciones entre sí). */
.ui-popover__section {
    padding: 0.6rem 0.5rem 0.4rem;
    border-top: 1px solid var(--bs-border-color);
}

.ui-popover__eyebrow {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    margin: 0 0 0.55rem;
    font-size: 0.68rem;
    font-weight: 650;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--bs-secondary-color);
}

.ui-popover__label {
    margin: 0 0 0.15rem;
    font-size: 0.9rem;
    font-weight: 550;
}

.ui-popover__help {
    margin: 0 0 0.6rem;
    font-size: 0.8rem;
    line-height: 1.45;
    color: var(--bs-secondary-color);
}


/* ── 8. Captura rápida ───────────────────────────────────────────────────────
   Campo de una línea para AGREGAR un elemento a la lista que tiene debajo:
   se escribe, Enter, y el foco se queda para encadenar capturas. Va siempre
   pegado a su lista (mismo ancho, mismo radio, mismo filete) para que se lea
   como la primera fila de esa lista y no como un formulario aparte.

   Uso:
     <div class="ui-capture">
       <i class="fa-solid fa-plus ui-capture__icon"></i>
       <input class="ui-capture__input" placeholder="…">
       <kbd class="ui-capture__kbd">↵</kbd>
     </div>

   El input NO lleva clase de Bootstrap a propósito: `.form-control` trae su
   propio borde, sombra y anillo de foco, y aquí quien los pinta es el
   CONTENEDOR (así el ícono y la pista ↵ quedan DENTRO del mismo campo, en vez
   de flotando al lado de una caja). */

.ui-capture {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    /* Mismo relleno horizontal que una fila de `ui-list`: el ícono cae en la
       columna de la casilla y el texto arranca donde arrancan los títulos. */
    padding: 10px var(--ui-capture-pad-x, 14px);
    margin-bottom: 0.75rem;
    border: 1px solid var(--bs-border-color);
    border-radius: 14px;
    background: var(--bs-body-bg);
    transition:
        border-color 130ms ease-out,
        box-shadow 130ms ease-out;
}

/* El foco se pinta en el CONTENEDOR (`:focus-within`), no en el input: el campo
   visible es toda la barra. Anillo del acento, mismo lenguaje que el resto. */
.ui-capture:focus-within {
    border-color: color-mix(in srgb, var(--bs-primary) 55%, transparent);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--bs-primary) 15%, transparent);
}

.ui-capture__icon {
    flex-shrink: 0;
    font-size: 0.8rem;
    color: var(--bs-secondary-color);
    transition: color 130ms ease-out;
}

.ui-capture:focus-within .ui-capture__icon { color: var(--bs-primary); }

.ui-capture__input {
    flex: 1 1 auto;
    min-width: 0;
    border: 0;
    padding: 0;
    background: transparent;
    color: var(--bs-body-color);
    font-size: 0.95rem;
    line-height: 1.4;
}

/* Sin anillo propio: lo pinta el contenedor (ver arriba). */
.ui-capture__input:focus { outline: none; }

.ui-capture__input::placeholder {
    color: var(--bs-secondary-color);
    opacity: 0.75;
}

/* Vista previa de la fecha reconocida en el texto. Violeta del acento y no el
   índigo primario: es una LECTURA de lo que el sistema entendió, no un estado
   del control, y así no compite con el anillo de foco. */
.ui-capture__date-chip {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    padding: 0.15rem 0.55rem;
    border-radius: 999px;
    font-size: 0.75rem;
    white-space: nowrap;
    color: #7c3aed;
    background: color-mix(in srgb, #7c3aed 13%, transparent);
}

/* Pista de que Enter captura. Aparece solo con texto, así que en reposo el
   campo queda limpio. */
.ui-capture__kbd {
    flex-shrink: 0;
    padding: 0.1rem 0.4rem;
    border-radius: 6px;
    border: 1px solid var(--bs-border-color);
    background: color-mix(in srgb, var(--bs-body-color) 5%, transparent);
    color: var(--bs-secondary-color);
    font-size: 0.72rem;
    line-height: 1.3;
}

/* Variante COMPACTA — la captura de subtareas dentro del panel de detalle.
   Misma anatomía y mismos estados (es el mismo control), en la escala del
   panel: no compite con la captura principal, que es el hero de la pantalla.
   El fondo sólido de la base (`--bs-body-bg`) queda a propósito: sobre el
   vidrio de la hoja móvil un campo de texto necesita asiento opaco, igual que
   la rejilla de campos. */
/* Variante BÚSQUEDA — mismo control, sin el margen inferior propio (su fila ya
   lo pone) y con la × de limpiar al final. Comparte anatomía con la captura a
   propósito: en el archivo ocupa su mismo sitio, así que tiene que medir igual. */
.ui-capture--search { margin-bottom: 0; }

.ui-capture__clear {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.5rem;
    height: 1.5rem;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: transparent;
    color: var(--bs-secondary-color);
    font-size: 0.75rem;
    cursor: pointer;
    transition: background-color 120ms ease-out, color 120ms ease-out;
}

.ui-capture__clear:hover {
    color: var(--bs-body-color);
    background-color: color-mix(in srgb, var(--bs-body-color) 10%, transparent);
}

.ui-capture__clear:focus-visible {
    outline: none;
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--bs-primary) 25%, transparent);
}

/* El aspa nativa del `type=search` sobraría: ya hay una, y la del navegador no
   avisa al servidor. */
.ui-capture__input::-webkit-search-cancel-button { display: none; }

.ui-capture--sub {
    margin-bottom: 0;
    padding: 7px 12px;
    border-radius: 11px;
}

.ui-capture--sub .ui-capture__input { font-size: 0.88rem; }
.ui-capture--sub .ui-capture__icon  { font-size: 0.72rem; }


/* ── 9. Detalle del ítem ─────────────────────────────────────────────────────
   El panel de detalle que se abre DENTRO de la lista (TaskDetailPanel y quien
   venga después). DOS presentaciones con el MISMO markup, decididas 100% por
   CSS — cero JS, cero clases toggleadas desde el servidor:

     · ESCRITORIO (≥md): el detalle NO es una tarjeta. Es una CONTINUACIÓN de
       la fila — banda de ancho COMPLETO, sin borde ni sombra propios, con el
       contenido alineado al TÍTULO de su fila y al borde derecho de sus metas.
       (La primera versión ponía aquí una tarjeta con borde y sombra DENTRO de
       la lista, que ya es una tarjeta: se leía superpuesta y su ancho no
       coincidía con nada — medido, 2px fuera por la izquierda y 14px por la
       derecha. Tarjeta dentro de tarjeta nunca; el detalle pertenece a la hoja.)

     · MÓVIL (<md): hoja de vidrio anclada abajo (bottom-sheet iOS 26), con el
       telón difuminando la lista y el aura detrás. Ahí SÍ es una superficie
       propia, porque flota sobre el contenido. Medidas del material tomadas de
       la especificación de Liquid Glass: radio 34px, tinte blanco 10 % en claro
       / negro 24 % en oscuro, realce especular en gradiente diagonal (25 %
       arriba-izquierda → 5 % abajo-derecha) y desenfoque de 40 px.

   El APROVECHAMIENTO DEL ANCHO es la otra mitad del rediseño (patrón del
   rediseño 2025 de Todoist y del panel de Linear): con notas, dos columnas
   —contenido a la izquierda, propiedades en una columna estrecha a la derecha—;
   sin notas, las propiedades fluyen en cuadrícula y llenan la banda. La
   condición la decide `:has()`, no un `@if` en el markup. */

.ui-detail {
    /* Rótulos, separadores y acentos del panel — un solo sitio donde afinarlos. */
    --ui-detail-rule: color-mix(in srgb, var(--bs-body-color) 10%, transparent);
    --ui-detail-flag: #b45309;
    --ui-detail-sheet-tint: rgba(255, 255, 255, 0.10);
    --ui-detail-specular: linear-gradient(135deg,
        rgba(255, 255, 255, 0.25), rgba(255, 255, 255, 0.05) 46%, rgba(255, 255, 255, 0) 70%);

    position: relative;
    /* ALINEACIÓN EXACTA con la fila: el contenido arranca donde arranca el
       TÍTULO (columna reservada + sangría del nivel + casilla + su hueco) y
       termina donde terminan sus metas. Sin esto el panel "cuelga" — es lo que
       hacía que se leyera como una capa ajena y no como parte de la fila. */
    padding: 0.15rem 0 1.15rem calc(var(--ui-list-check) + var(--ui-list-gap));
}

/* El ámbar de "Marcada" pierde contraste sobre fondo oscuro (regla AA ya
   aplicada en el módulo Work): se aclara. */
[data-bs-theme=dark] .ui-detail {
    --ui-detail-flag: #fbbf24;
    --ui-detail-sheet-tint: rgba(0, 0, 0, 0.24);
    --ui-detail-specular: linear-gradient(135deg,
        rgba(255, 255, 255, 0.14), rgba(255, 255, 255, 0.03) 46%, rgba(255, 255, 255, 0) 70%);
}

/* Encabezado: pertenece a la HOJA móvil. En escritorio el título ya se lee en
   la fila de arriba y repetirlo sería ruido. */
.ui-detail__head { display: none; }

.ui-detail__title {
    flex: 1;
    min-width: 0;
    margin: 0;
    font-size: 1.05rem;
    font-weight: 700;
    /* Tracking ligeramente negativo: a este tamaño el texto se compacta y se
       lee como titular, no como párrafo en negrita. */
    letter-spacing: -0.011em;
    line-height: 1.3;
}

.ui-detail__close {
    flex-shrink: 0;
    width: 2rem;
    height: 2rem;
    border: 0;
    border-radius: 50%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: color-mix(in srgb, var(--bs-body-color) 8%, transparent);
    color: var(--bs-secondary-color);
    transition: background-color 130ms ease-out, color 130ms ease-out;
}

.ui-detail__close:hover {
    background: color-mix(in srgb, var(--bs-body-color) 15%, transparent);
    color: var(--bs-body-color);
}

.ui-detail__close:focus-visible {
    outline: 2px solid var(--bs-primary);
    outline-offset: 1px;
}

/* Las pestañas van al ras del contenido, con aire suficiente para leerse como
   la cabecera del panel y no como un control suelto. */
.ui-detail__tabs { margin-bottom: 0.95rem; }

/* ── Cuerpo: contenido arriba, propiedades en cuadrícula ──────────────────
   Se probaron las dos columnas del rediseño de Todoist (contenido | barra
   lateral de atributos) y NO encajan aquí: sus tareas traen descripción larga,
   subtareas y comentarios, así que la columna izquierda sostiene la altura de
   la derecha. Las nuestras traen notas de dos líneas y siete propiedades — la
   barra lateral quedaba tres veces más alta que el contenido y dejaba medio
   panel vacío (medido en el banco de pruebas: 500 px de columna izquierda en
   blanco).

   El reparto correcto para ESTE contenido es al revés: las notas arriba, con
   medida tipográfica acotada, y las propiedades en una cuadrícula que llena el
   ancho. Un solo layout sirve con notas y sin ellas, así que tampoco hace falta
   que el CSS adivine con `:has()`. */

.ui-detail__body {
    display: flex;
    flex-direction: column;
    gap: 1rem;
}

/* SIN tope de ancho (decisión de Randolf, 2026-07-25: "hay espacio para que
   las notas estén en una sola fila y no lo aprovecha; eso no puede pasar").
   Se había puesto un `max-width: 72ch` por la regla tipográfica de los 45-75
   caracteres por renglón, pero esa regla es para TEXTO CORRIDO —un artículo,
   una ficha larga— y aquí lo que hay son notas de una o dos frases: el tope
   partía en dos una línea que cabía entera, y dejaba media banda vacía al lado.
   El ancho ya lo acota el propio detalle (sangría del nivel + relleno). */
.ui-detail__main { min-width: 0; }
.ui-detail__main:empty { display: none; }

/* Las notas llegan RENDERIZADAS desde Markdown (ver TaskNotesMarkdown), así que
   ya no llevan `pre-wrap`: los saltos vienen como <br> del propio render, y
   dejar ambos los duplicaría. Lo que sigue es la tipografía de ese contenido —
   compacta, porque son notas de trabajo dentro de un panel, no un artículo. */
.ui-detail__notes {
    margin: 0;
    font-size: 0.9rem;
    line-height: 1.6;
    color: var(--bs-body-color);
    overflow-wrap: break-word;
}

.ui-detail__notes > :first-child { margin-top: 0; }
.ui-detail__notes > :last-child  { margin-bottom: 0; }

.ui-detail__notes p,
.ui-detail__notes ul,
.ui-detail__notes ol,
.ui-detail__notes blockquote,
.ui-detail__notes pre,
.ui-detail__notes table { margin: 0 0 0.5rem; }

.ui-detail__notes ul,
.ui-detail__notes ol { padding-inline-start: 1.25rem; }

/* Los títulos dentro de una nota son SEPARADORES de secciones cortas, no
   titulares: apenas más grandes que el texto, en semibold. */
.ui-detail__notes h1,
.ui-detail__notes h2,
.ui-detail__notes h3,
.ui-detail__notes h4,
.ui-detail__notes h5,
.ui-detail__notes h6 {
    margin: 0.75rem 0 0.35rem;
    font-size: 0.95rem;
    font-weight: 650;
    line-height: 1.4;
}

/* El COLOR se fija a propósito: sin él, `code` hereda el rosa de Bootstrap
   (3.82:1 en claro) y `pre` el gris FIJO que el tema compila (1.29:1 en
   oscuro — invisible). La caja es lo que distingue al código, no su tinte. */
.ui-detail__notes code {
    padding: 0.1em 0.35em;
    border-radius: 5px;
    color: var(--bs-body-color);
    background-color: color-mix(in srgb, var(--bs-body-color) 8%, transparent);
    font-size: 0.85em;
}

.ui-detail__notes pre {
    padding: 0.6rem 0.75rem;
    border-radius: 8px;
    color: var(--bs-body-color);
    background-color: color-mix(in srgb, var(--bs-body-color) 6%, transparent);
    overflow-x: auto;   /* el código ancho desplaza dentro de su caja, jamás el panel */
}

.ui-detail__notes pre > code { padding: 0; background: none; }

.ui-detail__notes blockquote {
    padding-inline-start: 0.75rem;
    border-inline-start: 3px solid color-mix(in srgb, var(--bs-body-color) 18%, transparent);
    color: var(--bs-secondary-color);
}

.ui-detail__notes table { border-collapse: collapse; }

.ui-detail__notes th,
.ui-detail__notes td {
    padding: 0.25rem 0.6rem;
    border: 1px solid var(--bs-border-color-translucent);
    text-align: start;
}

/* Las listas de chequeo (`- [ ]`) sin la viñeta duplicada al lado de la casilla. */
.ui-detail__notes .contains-task-list { list-style: none; padding-inline-start: 0.25rem; }
.ui-detail__notes .task-list-item input { margin-inline-end: 0.4rem; }

/* Las propiedades van en REJILLA, y es la rejilla la que da la coherencia: como
   todos los campos se pintan siempre (tengan valor o no), cada dato cae en el
   MISMO sitio en todas las tareas del espacio y el ojo lo encuentra sin releer
   los rótulos.

   La unidad base es estrecha (9 rem) y los campos que necesitan más piden DOS
   celdas (`--wide`). Esa es la diferencia con la primera versión, que daba a
   todos el mismo ancho: entonces "Baja" desperdiciaba 170 px de los 200 que
   recibía mientras "22/07/2026 · Randolf Rincón Fadul" se partía en dos con los
   mismos 200. Con base estrecha + dos tamaños, ni se desperdicia ni se parte, y
   la posición sigue siendo estable — que es lo que una rejilla da y un reparto
   por contenido no puede dar. */
.ui-detail__side {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
    gap: 0.95rem 1.75rem;
    align-content: start;
}

/* Los campos con listas dentro (contextos, personas, adjuntos) o con textos
   largos (fechas con autor, recurrencia) ocupan DOS celdas. No la fila entera:
   eso los dejaba solos con media banda vacía al lado. */
.ui-detail__field--wide { grid-column: span 2; }

/* Campo SIN valor: el guion queda tenue para que la lectura salte por encima
   de lo vacío y se detenga en lo que sí tiene dato. */
.ui-detail__empty { color: var(--bs-secondary-color); opacity: 0.55; }

/* Filete de separación: solo cuando ARRIBA hay contenido que separar. */
.ui-detail__main:not(:empty) + .ui-detail__side {
    padding-top: 1rem;
    border-top: 1px solid var(--ui-detail-rule);
}

/* ── Campos ── */

.ui-detail__field {
    display: grid;
    gap: 0.3rem;
    min-width: 0;
    /* Anclado ARRIBA: sin esto el rótulo y su valor se separaban repartiéndose
       el alto de la fila y dos campos vecinos quedaban desalineados. */
    align-content: start;
}

/* Rótulo: versalitas pequeñas con tracking amplio. Da jerarquía sin recurrir al
   peso (que competiría con el valor) ni al color fuerte. */
.ui-detail__label {
    font-size: 0.65rem;
    font-weight: 650;
    text-transform: uppercase;
    letter-spacing: 0.07em;
    color: var(--bs-secondary-color);
    line-height: 1.2;
}

.ui-detail__value {
    font-size: 0.86rem;
    line-height: 1.4;
    min-width: 0;
    word-break: break-word;
}

/* Fila de valores que son pastillas o avatares (etiquetas, asignados). */
.ui-detail__chips {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    align-items: center;
}

.ui-detail__person {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    font-size: 0.84rem;
}

/* El ARCHIVO no es una lista de trabajo: un filete lo aparta de las tres del
   ciclo GTD, para que el control no sugiera que hay cuatro sitios donde buscar
   qué hacer. Mismo gesto que el separador del Logbook en Things. */
.ui-segmented--with-archive .ui-segmented__item:last-child {
    margin-left: 0.3rem;
    padding-left: calc(0.3rem + 0.65rem);
    border-left: 1px solid var(--bs-border-color);
    border-top-left-radius: 0;
    border-bottom-left-radius: 0;
}

/* ── Edición en el sitio (`ui-editable`) ──
   Tocar un valor lo vuelve editable. La afordancia tiene que ser DISCRETA —el
   panel se lee mucho más de lo que se edita, así que no puede parecer un
   formulario— pero DESCUBRIBLE: al pasar por encima, el valor se asienta sobre
   una superficie tenue y aparece un lápiz. En táctil no hay hover con el que
   revelar nada, así que ahí el lápiz queda visible en tenue (misma solución
   que el chevrón de las filas). */

.ui-editable { min-width: 0; }

/* El disparador NO parece un botón: es el mismo texto de siempre, alineado
   igual, para que la lectura del panel no cambie por ser editable. */
.ui-editable__display {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    width: 100%;
    padding: 2px 6px;
    margin: -2px -6px;   /* el relleno no puede correr el valor de su sitio */
    border: 0;
    border-radius: 7px;
    background: transparent;
    color: inherit;
    font: inherit;
    text-align: inherit;
    cursor: text;        /* dice "aquí se escribe", no "aquí se navega" */
    transition: background-color 120ms ease-out;
}

.ui-editable__display.is-static {
    cursor: default;
    padding: 0;
    margin: 0;
}

.ui-editable__display:hover {
    background-color: color-mix(in srgb, var(--bs-body-color) 6%, transparent);
}

.ui-editable__display:focus-visible {
    outline: none;
    background-color: color-mix(in srgb, var(--bs-primary) 10%, transparent);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--bs-primary) 45%, transparent);
}

.ui-editable__shown { min-width: 0; }

.ui-editable__pen {
    flex-shrink: 0;
    margin-left: auto;
    font-size: 0.68rem;
    color: var(--bs-secondary-color);
    opacity: 0;
    transition: opacity 120ms ease-out;
}

.ui-editable__display:hover .ui-editable__pen,
.ui-editable__display:focus-visible .ui-editable__pen { opacity: 0.8; }

/* Sin hover (táctil) el lápiz no se podría revelar: queda tenue y siempre. */
@media (hover: none) {
    .ui-editable__pen { opacity: 0.45; }
}

/* El editor hereda la escala del panel: al abrirse, el valor no salta de
   tamaño ni desplaza a sus vecinos de la rejilla. */
.ui-editable__input {
    width: 100%;
    padding: 3px 7px;
    border: 1px solid color-mix(in srgb, var(--bs-primary) 55%, transparent);
    border-radius: 7px;
    background: var(--bs-body-bg);
    color: var(--bs-body-color);
    font: inherit;
    line-height: 1.4;
}

.ui-editable__input:focus {
    outline: none;
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--bs-primary) 15%, transparent);
}

.ui-editable__input--area {
    resize: vertical;
    min-height: 5rem;
    white-space: pre-wrap;
}

/* El editor compuesto de fecha: día y hora caben en la misma línea y envuelven
   cuando el campo es estrecho. */
.ui-editable__pair {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
}

.ui-editable__pair .ui-editable__input { width: auto; flex: 1 1 8.5rem; }
.ui-editable__input--time { flex: 0 1 7rem; }

/* Las pestañas y la bandera comparten renglón: las pestañas ocupan lo que haya
   y la bandera CIERRA la línea en su propia columna, sin encogerse nunca. */
.ui-detail__tabs-row {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.ui-detail__tabs-row > .ui-segmented { flex: 1 1 auto; min-width: 0; }

/* En la hoja móvil el título es EDITABLE y ocupa el ancho; la ✕ lo cierra. */
.ui-detail__head > .ui-editable { flex: 1 1 auto; min-width: 0; }

/* CHULO DE COMPLETAR de la cabecera de la hoja (móvil). Vive dentro de
   `.ui-detail__head`, que en escritorio no se pinta, así que no hace falta
   ninguna media query: donde el título ya está en la fila, este botón no
   existe. 44 px porque es la guía táctil y porque es la acción principal de la
   pantalla; el círculo y el verde son los MISMOS de la fila. */
.ui-detail__complete {
    /* La forma la pone el círculo de estado compartido; aquí, la medida táctil
       (44 px, la guía) y el aire para alinearlo con la primera línea del título. */
    width: 44px;
    height: 44px;
    margin-top: -0.15rem;
    font-size: 1rem;
}

.ui-detail__complete:focus-visible {
    outline: none;
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--bs-success) 32%, transparent);
}

/* La previsualización en verde solo con puntero: en táctil el hover se queda
   pegado y el círculo parecería completado sin estarlo (la misma trampa que se
   corrigió en el chulo de la fila). */
@media (hover: hover) and (pointer: fine) {
    .ui-detail__complete:not(.is-done):hover {
        border-color: var(--bs-success);
        background: var(--bs-success);
        color: #fff;
    }
}

/* Objetivo GRANDE y a la mano: marcar es un gesto de priorización que se hace
   al vuelo, y con el dedo. En reposo es discreto (sin borde ni relleno) para no
   competir con el título; encendido toma el ámbar de "marcada". */
.ui-detail__flag-btn {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.125rem;
    height: 2.125rem;
    border: 0;
    border-radius: 9px;
    background: transparent;
    color: var(--bs-secondary-color);
    font-size: 0.95rem;
    opacity: 0.5;
    cursor: pointer;
    transition: opacity 120ms ease-out, background-color 120ms ease-out, color 120ms ease-out;
}

.ui-detail__flag-btn:hover { opacity: 1; background-color: color-mix(in srgb, var(--bs-body-color) 6%, transparent); }

.ui-detail__flag-btn:focus-visible {
    outline: none;
    opacity: 1;
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--bs-primary) 25%, transparent);
}

/* MARCADA: el mismo ámbar que ya usa el resto del panel para la prioridad. */
.ui-detail__flag-btn.is-on {
    opacity: 1;
    color: var(--ui-detail-flag, #b45309);
    background-color: color-mix(in srgb, var(--ui-detail-flag, #b45309) 12%, transparent);
}

/* Sin permiso es una LECTURA, no un control. */
.ui-detail__flag-btn.is-static { cursor: default; }

/* El TÍTULO encabeza el contenido: es el único valor del panel con peso propio. */
/* Notas vacías: invitan a escribir sin fingir que hay contenido. */
.ui-detail__notes.is-placeholder {
    color: var(--bs-secondary-color);
    opacity: 0.7;
    font-style: italic;
}

/* ── Valores de MUCHOS: etiquetas y personas ──
   No abren editor (no hay UN valor que reemplazar): cada pastilla trae su × y
   al final va el control para agregar otra. La × se revela al pasar por encima
   de SU pastilla — mostrarlas todas a la vez convertiría el campo en una fila
   de botones de borrar. */

.ui-detail__chip-x {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 14px;
    height: 14px;
    margin-left: 0.15rem;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: transparent;
    color: inherit;
    font-size: 0.6rem;
    line-height: 1;
    cursor: pointer;
    opacity: 0;
    transition: opacity 120ms ease-out, background-color 120ms ease-out;
}

.ui-detail__tag:hover .ui-detail__chip-x,
.ui-detail__person:hover .ui-detail__chip-x,
.ui-detail__chip-x:focus-visible { opacity: 0.75; }

.ui-detail__chip-x:hover {
    opacity: 1;
    background-color: color-mix(in srgb, currentColor 18%, transparent);
}

/* Sin hover (táctil) no hay forma de revelarla: queda tenue y siempre. */
@media (hover: none) {
    .ui-detail__chip-x { opacity: 0.5; }
}

/* El control de "agregar otra": discreto, del tamaño de las pastillas que
   acompaña, para que el campo se siga leyendo como una lista y no como un
   formulario. */
.ui-detail__chip-add {
    min-width: 0;
    max-width: 11rem;
    padding: 2px 8px;
    border: 1px dashed var(--bs-border-color);
    border-radius: 999px;
    background: transparent;
    color: var(--bs-secondary-color);
    font-size: 0.78rem;
    line-height: 1.5;
}

.ui-detail__chip-add:focus {
    outline: none;
    border-style: solid;
    border-color: color-mix(in srgb, var(--bs-primary) 55%, transparent);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--bs-primary) 15%, transparent);
    color: var(--bs-body-color);
}

/* ── Adjuntos (`ui-attach`) ──
   El último campo de la rejilla, y el único que no es un dato sino un ESPACIO:
   se le sueltan archivos encima o se hace clic para elegirlos. Cierra el panel
   ocupando la fila entera. */

/* La FILA ENTERA, siempre. Es el último campo y el único que crece con el uso:
   con dos celdas se quedaba en ~415px —donde no caben dos tarjetas— y cada
   adjunto se comía un renglón dejando el resto del ancho vacío. */
.ui-attach {
    grid-column: 1 / -1;
    display: grid;
    gap: 0.6rem;
}

/* Lo YA subido, en rejilla fluida: varios por renglón cuando hay ancho, uno
   cuando no, sin una sola media query. Cada uno se reconoce por el ícono de su
   tipo antes de leer el nombre. */
/* Rejilla de DOCE columnas, y cada adjunto ocupa TRES (decisión de Randolf,
   2026-07-25): cuatro por renglón en escritorio y dos en móvil (seis de doce).
   Ancho FIJO y no por contenido — medir el contenido hacía que "Fondo.jpg"
   ocupara una tarjeta diminuta y el renglón quedara desparejo; y por debajo de
   tres columnas no cabe nada legible. Un nombre que no quepa se recorta y se
   lee entero en el tooltip. */
.ui-attach__list {
    display: grid;
    grid-template-columns: repeat(12, minmax(0, 1fr));
    gap: 0.4rem;
    margin: 0;
    padding: 0;
    list-style: none;
}

/* EN EL PANEL DE UNA HISTORIA van DOS por renglón, no cuatro: su diálogo mide
   ~640 px (contra la columna ancha del inspector de tareas), así que con cuatro
   el nombre se quedaba en 14-26 px — medido en el banco (2026-08-04). Y el
   nombre es justamente lo que dice qué es el archivo. */
.ui-attach--story .ui-attach__item { grid-column: span 6; }

.ui-attach__item {
    grid-column: span 3;
    min-width: 0;
    display: flex;
    align-items: center;
    gap: 0.5rem;
    min-width: 0;
    padding: 0.35rem 0.5rem;
    border: 1px solid var(--bs-border-color);
    border-radius: 10px;
    background: var(--bs-body-bg);
    transition: border-color 120ms ease-out, background-color 120ms ease-out;
}

.ui-attach__item:hover {
    border-color: color-mix(in srgb, var(--bs-primary) 40%, var(--bs-border-color));
}

.ui-attach__link {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    min-width: 0;
    flex: 1 1 auto;
    color: inherit;
    text-decoration: none;
}

.ui-attach__icon {
    flex-shrink: 0;
    font-size: 1rem;
    color: var(--bs-secondary-color);
}

/* El nombre es lo que se busca, así que se queda con todo el ancho sobrante y
   se recorta con puntos suspensivos — nunca parte la tarjeta en dos renglones.
   El nombre completo vive en el `title`. */
.ui-attach__name {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 0.84rem;
}

.ui-attach__size {
    flex-shrink: 0;
    font-size: 0.72rem;
    color: var(--bs-secondary-color);
    font-variant-numeric: tabular-nums;
}

/* Botón PROPIO, no la × minúscula de las pastillas: aquella medía 14px y se
   revelaba por hover —invisible con ratón e imposible de acertar con el dedo—.
   Aquí es un objetivo de 28px y ROJO DESDE EL REPOSO (decisión de Randolf):
   borrar un adjunto es destructivo, así que el color lo dice desde el primer
   vistazo, sin esperar a que alguien pase el cursor —cosa que en táctil no
   ocurre nunca—. Al apuntarlo sube al danger PLENO (fondo sólido, glifo
   blanco): el color ya avisaba, y el hover confirma que se va a ejecutar. */
.ui-attach__remove {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.75rem;
    height: 1.75rem;
    padding: 0;
    border: 0;
    border-radius: 7px;
    background-color: color-mix(in srgb, var(--bs-danger) 12%, transparent);
    /* `--bs-danger` puro da 2.6:1 sobre el tinte de su propio fondo — medido en
       el banco (2026-08-04) al estrenar el bloque en el panel de historia. Es la
       MISMA fórmula que ya corrigió los mensajes de validación: oscurecer en
       claro y ACLARAR en oscuro, porque un `color-mix` no rinde igual en los dos
       temas. El rojo se sigue leyendo rojo, que es lo que tiene que decir. */
    color: color-mix(in srgb, var(--bs-danger) 75%, #000);
    font-size: 0.8rem;
    line-height: 1;
    cursor: pointer;
    transition: background-color 120ms ease-out, color 120ms ease-out;
}

.ui-attach__remove:hover {
    color: #fff;
    background-color: var(--bs-danger);
}

/* El teclado llega al mismo sitio que el ratón: mismo danger pleno, con su
   anillo para que se vea DÓNDE está el foco. */
.ui-attach__remove:focus-visible {
    outline: none;
    color: #fff;
    background-color: var(--bs-danger);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--bs-danger) 30%, transparent);
}

/* LA ZONA. Punteada en reposo (dice "aquí cabe algo") y encendida en el acento
   mientras hay un arrastre encima. El <input> la cubre entera con opacidad 0:
   el clic en cualquier punto abre el diálogo, sin un botón que compita. */
.ui-attach__drop {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    padding: 0.85rem 1rem;
    border: 1.5px dashed var(--bs-border-color);
    border-radius: 12px;
    color: var(--bs-secondary-color);
    font-size: 0.82rem;
    text-align: center;
    cursor: pointer;
    transition: border-color 130ms ease-out, background-color 130ms ease-out, color 130ms ease-out;
}

.ui-attach__drop:hover,
.ui-attach__drop:focus-within {
    border-color: color-mix(in srgb, var(--bs-primary) 55%, transparent);
    color: var(--bs-body-color);
}

/* `is-dropping` la pone el helper de JS mientras el arrastre está encima — es
   la única señal que el CSS no puede darse solo (no existe `:drag-over`). */
.ui-attach__drop.is-dropping {
    border-color: var(--bs-primary);
    border-style: solid;
    background-color: color-mix(in srgb, var(--bs-primary) 10%, transparent);
    color: var(--bs-primary);
}

.ui-attach__input {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    opacity: 0;
    cursor: pointer;
}

/* LA COLA: lo escogido y aún sin subir. */
.ui-attach__queue {
    display: grid;
    gap: 0.35rem;
    margin: 0;
    padding: 0;
    list-style: none;
}

.ui-attach__pending {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
    padding: 0.35rem 0.5rem;
    border: 1px dashed color-mix(in srgb, var(--bs-primary) 40%, var(--bs-border-color));
    border-radius: 10px;
}

.ui-attach__pending.is-invalid {
    border-color: color-mix(in srgb, var(--bs-danger) 55%, transparent);
    background-color: color-mix(in srgb, var(--bs-danger) 6%, transparent);
}

/* El nombre editable y su extensión BLOQUEADA se leen como un solo campo: así
   queda claro que lo que se cambia es el nombre, no el tipo de archivo. */
.ui-attach__rename {
    display: flex;
    align-items: center;
    flex: 1 1 12rem;
    min-width: 0;
    border: 1px solid var(--bs-border-color);
    border-radius: 8px;
    background: var(--bs-body-bg);
    overflow: hidden;
}

.ui-attach__rename:focus-within {
    border-color: color-mix(in srgb, var(--bs-primary) 55%, transparent);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--bs-primary) 15%, transparent);
}

.ui-attach__rename-input {
    flex: 1 1 auto;
    min-width: 0;
    border: 0;
    padding: 3px 7px;
    background: transparent;
    color: var(--bs-body-color);
    font-size: 0.84rem;
}

.ui-attach__rename-input:focus { outline: none; }

.ui-attach__ext {
    flex-shrink: 0;
    padding: 3px 7px;
    background: color-mix(in srgb, var(--bs-body-color) 6%, transparent);
    color: var(--bs-secondary-color);
    font-size: 0.8rem;
}

[data-bs-theme="dark"] .ui-attach__remove {
    color: color-mix(in srgb, var(--bs-danger) 85%, #fff);
}

.ui-attach__error {
    flex-basis: 100%;
    font-size: 0.75rem;
    /* Mismo motivo y misma fórmula que la × de arriba: 2.79:1 medido. */
    color: color-mix(in srgb, var(--bs-danger) 75%, #000);
}

[data-bs-theme="dark"] .ui-attach__error {
    color: color-mix(in srgb, var(--bs-danger) 85%, #fff);
}

.ui-attach__actions { display: flex; align-items: center; gap: 0.25rem; }

/* Guardado rechazado: rojo del tema, dentro del panel. */
.ui-detail__edit-error {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    margin: 0;
    padding: 0.4rem 0.6rem;
    border-radius: 8px;
    font-size: 0.82rem;
    color: var(--bs-danger);
    background: color-mix(in srgb, var(--bs-danger) 10%, transparent);
}

/* ── Subtareas: la captura dentro del panel ──
   Último bloque del cuerpo, separado con el mismo filete que la rejilla de
   campos. En escritorio queda pegado al borde inferior de la banda — justo
   encima de donde las filas hijas se pintan — y en la hoja móvil es el remate
   del desplazamiento. */
.ui-detail__subtasks {
    padding-top: 1rem;
    border-top: 1px solid var(--ui-detail-rule);
    display: grid;
    gap: 0.5rem;
}

/* Reusa la tipografía de rótulo de la rejilla (versalitas); el conteo va como
   pastilla mínima — es el MISMO número de la pastilla de la fila, así que no
   puede mentir. */
.ui-detail__subtasks-title {
    display: flex;
    align-items: center;
    gap: 0.45rem;
    margin: 0;
}

.ui-detail__subtasks-count {
    padding: 0.05rem 0.45rem;
    border-radius: 999px;
    font-size: 0.68rem;
    font-weight: 650;
    letter-spacing: 0;
    color: var(--bs-secondary-color);
    background: color-mix(in srgb, var(--bs-body-color) 8%, transparent);
}

/* LISTA DE SUBTAREAS DEL PANEL — solo existe en la HOJA MÓVIL.
   En escritorio las hijas se despliegan como FILAS justo debajo de la banda del
   detalle, así que repetirlas aquí sería decir dos veces lo mismo a treinta
   píxeles; en el teléfono la hoja tapa la lista y esta es la ÚNICA forma de
   verlas y de llegar a ellas. Se oculta con `display: none` (no se deja de
   renderizar) porque el servidor no conoce el ancho de la pantalla y esta
   pantalla no tiene JS propio; el coste es el DOM de las hijas directas de UNA
   fila, la abierta. */
.ui-detail__subtasks-list { display: none; }

/* Feedback de la captura (fallo o "no pertenece a esta lista"): DENTRO del
   panel, porque en móvil la hoja tapa la página y un aviso lejano no existe. */
.ui-detail__subtasks-feedback {
    margin: 0;
    font-size: 0.8rem;
    color: var(--bs-secondary-color);
}

.ui-detail__subtasks-feedback.is-error { color: var(--bs-danger); }

/* ── Pastillas propias del detalle ── */

/* Estado con SU color (lo pone el markup en --ui-state-color). El punto lleva
   un halo del mismo color: se lee como un indicador encendido, no como un
   círculo pegado al texto. */
.ui-detail__state {
    background: color-mix(in srgb, var(--ui-state-color) 13%, transparent);
    color: color-mix(in srgb, var(--ui-state-color) 68%, var(--bs-body-color));
}

.ui-detail__state-dot {
    width: 0.5rem;
    height: 0.5rem;
    border-radius: 50%;
    background: var(--ui-state-color);
    box-shadow: 0 0 0 2.5px color-mix(in srgb, var(--ui-state-color) 20%, transparent);
}

/* Terminal (hecha/cancelada): el estado deja de reclamar atención. */
.ui-detail__state.is-terminal {
    background: color-mix(in srgb, var(--bs-body-color) 7%, transparent);
    color: var(--bs-secondary-color);
}

.ui-detail__state.is-terminal .ui-detail__state-dot {
    background: var(--bs-secondary-color);
    box-shadow: none;
}

.ui-detail__flag {
    background: color-mix(in srgb, #d97706 13%, transparent);
    color: var(--ui-detail-flag);
}

/* Etiqueta tintada con su propio color (--ui-tag-color del markup). */
.ui-detail__tag {
    background: color-mix(in srgb, var(--ui-tag-color) 14%, transparent);
    color: color-mix(in srgb, var(--ui-tag-color) 66%, var(--bs-body-color));
}

/* ── Adjuntos ── */


/* ── Historial ── */

.ui-detail__timeline {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
}

/* Los eventos se hilvanan con una línea continua que baja por los íconos: el
   historial se lee como una secuencia, no como tres filas sueltas. */
.ui-detail__event {
    position: relative;
    display: flex;
    align-items: flex-start;
    gap: 0.7rem;
    padding-bottom: 0.85rem;
}

.ui-detail__event:last-child { padding-bottom: 0; }

.ui-detail__event:not(:last-child)::before {
    content: "";
    position: absolute;
    left: 0.85rem;
    top: 1.75rem;
    bottom: 0.15rem;
    width: 1px;
    background: var(--ui-detail-rule);
}

.ui-detail__event-icon {
    flex-shrink: 0;
    width: 1.7rem;
    height: 1.7rem;
    border-radius: 50%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 0.66rem;
    background: color-mix(in srgb, var(--bs-primary) 11%, var(--bs-body-bg));
    color: var(--bs-primary);
    /* Aro del color del fondo: la línea de la secuencia pasa por detrás y el
       ícono la interrumpe limpiamente. */
    box-shadow: 0 0 0 3px var(--bs-body-bg);
}

.ui-detail__event-sentence {
    font-size: 0.85rem;
    line-height: 1.35;
}

.ui-detail__event-meta {
    font-size: 0.72rem;
    color: var(--bs-secondary-color);
    margin-top: 0.1rem;
}

/* Misma aparición diferida (250 ms) del resto de la pantalla: en una consulta
   normal el indicador nunca llega a verse. */
.ui-detail__pending {
    display: block;
    margin: 0.4rem auto;
    opacity: 0;
    animation: ui-reveal-late 1ms linear 250ms forwards;
}

/* El telón solo existe en la hoja móvil; en escritorio ni ocupa espacio. */
.ui-detail-scrim { display: none; }

/* CONFIRMACIONES POR ENCIMA DE LA HOJA. La hoja móvil se apila en 1070 y un
   `.modal` de Bootstrap en 1055: cualquier confirmación disparada DESDE la hoja
   —completar arrastrando subtareas, eliminar— se habría pintado DEBAJO, con el
   usuario esperando una respuesta que no se ve. Lo notó el camino nuevo de
   completar desde el detalle en móvil; el número solo tiene que quedar por
   encima de la hoja y por debajo del snackbar (1080), que es el único elemento
   que debe seguir viéndose. */
.ui-modal--over-sheet { z-index: 1075; }

/* ── Hoja móvil ──────────────────────────────────────────────────────────────
   Bajo md el contenedor del detalle deja de ser una banda de la lista y se
   vuelve overlay: el usuario está a la mitad de una lista larga y el detalle
   tiene que quedar EN PANTALLA, no donde haya caído su fila. */
@media (max-width: 767.98px) {

    .ui-list__detail {
        position: fixed;
        inset: 0;
        /* Overlay transitorio, como los menús de vidrio. Vive dentro del
           `isolation` del layout, así que su apilado es asunto suyo. */
        z-index: 1070;
        display: flex;
        flex-direction: column;
        justify-content: flex-end;
        padding: 0;
        margin: 0;
        border-top: 0;
        /* Telón: separa la hoja del fondo sin apagarlo. Al 46 % de azul-noche
           (la primera versión) el desenfoque de la hoja mezclaba ESE gris con
           el blanco del vidrio y el resultado era un gris muerto — el material
           se veía sucio en vez de luminoso. Con 34 % y un neutro puro, el
           vidrio conserva su claridad y el fondo sigue leyéndose detrás.

           SIN `backdrop-filter` (rendimiento, 2026-07-25): tenía uno propio y
           eran DOS desenfoques de pantalla completa apilados —el del telón y el
           de la hoja— recalculándose a la vez mientras la hoja se desplazaba.
           El del telón era el prescindible: la hoja tapa tres cuartos de la
           pantalla, así que solo se percibía en la franja de arriba, y ahí un
           oscurecimiento plano separa igual de bien. Un color semitransparente
           no cuesta composición; capturar y difuminar el fondo, sí. */
        background-color: color-mix(in srgb, #1e2430 34%, transparent);
        background-image: none;
        transition: none;
    }

    /* En el overlay no aplican los recortes del grid de escritorio. */
    .ui-list__detail > * { overflow: visible; }

    .ui-detail-scrim {
        display: block;
        position: absolute;
        inset: 0;
    }

    /* Mientras el contenido viene en camino, el indicador flota centrado. */
    .ui-list__detail > .ui-list__detail-pending { margin: auto; }

    .ui-detail {
        margin-top: auto;
        /* ALTURA FIJA, no "hasta un máximo" (decisión de Randolf, 2026-07-25):
           con `max-height` la hoja se ajustaba al contenido, así que al pasar
           de "Detalles" a "Actividad" —que casi siempre trae menos— se encogía
           de golpe y el panel daba un salto bajo el dedo. Un contenedor que
           cambia de tamaño al cambiar de pestaña obliga a reubicar la vista
           cada vez.

           75 dvh: entre los dos puntos de anclaje que usan las hojas nativas
           (medio ~50 % y grande ~93 %). El medio deja el detalle a medias y el
           grande tapa la lista por completo, con lo que se pierde el contexto
           de dónde se estaba. `dvh` y no `vh` porque en el móvil la barra del
           navegador aparece y desaparece: con `vh` la hoja cambiaría de alto
           justo al desplazar. */
        height: 75dvh;
        /* La hoja NO desplaza: lo hace su cuerpo (regla de abajo). Así el
           título y las pestañas quedan fijos POR CONSTRUCCIÓN — como en una
           hoja nativa— en vez de con `position: sticky` y un velo que los
           tapara a medias: al desplazar, el texto se leía A TRAVÉS del título
           y las pestañas se perdían de vista, que es justo la acción principal
           del panel. */
        display: flex;
        flex-direction: column;
        overflow: hidden;
        /* 34px: el radio de hoja de la especificación de Liquid Glass. */
        border-radius: 34px 34px 0 0;
        border: 0;
        padding: 0.35rem 1.15rem 0;
        /* VIDRIO: tinte del material sobre el fondo del tema + desenfoque real
           de lo que pasa por detrás (la lista y el aura). El `saturate` evita
           que el desenfoque deslave los colores de debajo.

           El material va CLARO (88 %, no 72 %): con menos opacidad, lo que se
           filtraba del telón teñía toda la hoja de un gris plano y apagado. La
           especificación de Liquid Glass pone el tinte en blanco 8–14 % sobre
           el fondo, no un velo translúcido a medias.

           DOS capas de luz: el realce especular diagonal arriba (25 % → 5 %,
           como la especificación) y un degradado vertical que aclara la cabeza
           de la hoja y la deja caer hacia el pie. Eso es lo que le da volumen —
           sin ellos la hoja es un rectángulo de un solo tono. */
        background-color: color-mix(in srgb, var(--bs-body-bg) 88%, transparent);
        background-image:
            var(--ui-detail-specular),
            linear-gradient(180deg, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0) 14rem);
        background-repeat: no-repeat, no-repeat;
        background-size: 100% 10rem, 100% 100%;
        /* 24 px y no 40 (rendimiento): el coste del desenfoque crece con el
           radio, y pasado cierto punto no se gana nada — a 24 px lo de detrás
           ya es irreconocible, que es todo lo que el material necesita. */
        backdrop-filter: blur(24px) saturate(1.6);
        -webkit-backdrop-filter: blur(24px) saturate(1.6);
        /* Canto del vidrio: filo luminoso arriba (el realce especular de la
           especificación) y un aro tenue que define el borde de la pieza. */
        box-shadow:
            inset 0 1px 0 rgba(255, 255, 255, 0.42),
            inset 0 0 0 1px rgba(255, 255, 255, 0.10),
            0 -24px 70px -20px rgba(2, 6, 23, 0.55);

        /* Entrada one-shot: la hoja sube al insertarse.
           `translate` y `opacity` son las DOS únicas propiedades que el
           compositor resuelve sin volver a pintar — por eso se anima con ellas
           y no con `top`/`height`.

           EL DESENFOQUE NO PARTICIPA DEL MOVIMIENTO, y ahí estaba el peso real
           (reportado por Randolf: "se siente lenta, pesada"): con
           `backdrop-filter` activo, cada pixel que la hoja se desplaza obliga
           al navegador a recapturar el fondo y rehacer el desenfoque — 60 veces
           por segundo, sobre tres cuartos de pantalla. En un teléfono de gama
           media eso solo se come el presupuesto de 16 ms por frame.

           El truco es una transición de duración CERO con retardo igual al del
           movimiento: durante la entrada el desenfoque vale 0 (composición
           barata) y aparece de golpe cuando la hoja ya está quieta, que es
           cuando el fondo deja de cambiar y el navegador puede cachearlo. */
        transition:
            translate 200ms cubic-bezier(0.2, 0.9, 0.24, 1),
            opacity 160ms ease-out,
            backdrop-filter 0s linear 200ms,
            -webkit-backdrop-filter 0s linear 200ms;
    }

    [data-bs-theme=dark] .ui-list__detail {
        background-color: color-mix(in srgb, #05070c 52%, transparent);
    }

    [data-bs-theme=dark] .ui-detail {
        background-color: color-mix(in srgb, var(--bs-body-bg) 82%, transparent);
        background-image:
            var(--ui-detail-specular),
            linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0) 14rem);
        box-shadow:
            inset 0 1px 0 rgba(255, 255, 255, 0.14),
            inset 0 0 0 1px rgba(255, 255, 255, 0.05),
            0 -24px 70px -20px rgba(0, 0, 0, 0.72);
    }

    @starting-style {
        .ui-detail {
            translate: 0 28px;
            opacity: 0.35;
            /* Sin desenfoque mientras entra — ver la transición de arriba. */
            backdrop-filter: blur(0px);
            -webkit-backdrop-filter: blur(0px);
        }
    }

    /* Asa de hoja: afordancia iOS. No arrastra — comunica "esto es una hoja". */
    .ui-detail::before {
        content: "";
        display: block;
        width: 2.4rem;
        height: 0.3rem;
        border-radius: 999px;
        margin: 0.5rem auto 0.75rem;
        background: color-mix(in srgb, var(--bs-body-color) 20%, transparent);
    }

    /* En la hoja el título SÍ va: es lo primero que se lee al abrir, y no hay
       una fila arriba que lo esté mostrando. Queda fijo porque vive FUERA del
       área que desplaza (ver `.ui-detail`), sin sticky ni velos. */
    .ui-detail__head {
        display: flex;
        align-items: flex-start;
        gap: 0.75rem;
        flex-shrink: 0;
        margin: 0 0 0.85rem;
    }

    /* Lo único que desplaza. `min-height: 0` es obligatorio: un hijo flex no
       encoge por debajo de su contenido sin él, y la hoja crecería en vez de
       desplazar. El relleno inferior respeta el notch. */
    .ui-detail__body {
        overflow-y: auto;
        overscroll-behavior: contain;
        min-height: 0;
        /* AIRE AL FINAL: el pie de la app (`.footer` del tema, `position:
           absolute`, 60 px) vive FUERA del contexto de apilado que crea el aura
           del módulo, así que se pinta SOBRE la hoja por más z-index que ésta
           tenga — elevar el layout no es opción, es el bug ya conocido de
           contenido pisando el topbar. Sin este colchón el último campo se lee
           a medias detrás del pie; con él, el desplazamiento siempre lo deja
           por encima. Se suma al `safe-area` del notch, que es otra cosa. */
        padding-bottom: calc(5.5rem + env(safe-area-inset-bottom));
        /* El contenido se DESVANECE al pasar bajo las pestañas en vez de
           cortarse a media línea…

           …pero el desvanecido tiene que caer sobre ESPACIO VACÍO, no sobre el
           primer campo. La versión anterior daba por hecho que "el primer
           renglón ya empieza más abajo" y no era cierto: sin relleno superior
           el contenido arrancaba en el pixel 0 del área enmascarada, así que
           ESTADO y ETIQUETAS se veían difuminados de entrada (lo reportó
           Randolf con captura). El relleno de arriba mide MÁS que el
           desvanecido, de modo que a plena vista el fundido cae sobre el hueco
           y solo muerde el contenido cuando éste sube al desplazar — que es
           justo para lo que está. De paso da el aire que faltaba bajo las
           pestañas. */
        padding-top: 1.15rem;
        mask-image: linear-gradient(180deg, transparent 0, #000 14px);
        -webkit-mask-image: linear-gradient(180deg, transparent 0, #000 14px);
    }

    .ui-detail__tabs {
        flex-shrink: 0;
        /* El aire bajo las pestañas lo pone el relleno del cuerpo (que además
           tiene que absorber el desvanecido); duplicarlo aquí lo dejaba todo
           demasiado separado. */
        margin-bottom: 0;
    }

    /* En un teléfono ancho, una sola columna de pares rótulo/valor desperdicia
       media pantalla; en uno estrecho, dos columnas parten los valores. El
       mínimo de 8.5rem deja que el navegador lo decida por sí solo. */
    /* En el teléfono, dos columnas: una sola desperdicia media pantalla y tres
       parten los valores.

       Y sobre un PANEL SÓLIDO del color de la hoja (blanco en claro; en oscuro
       resuelve al fondo del tema, nada de blanco quemado). El vidrio deja pasar
       lo que hay detrás, y aunque eso es lo que le da vida a la hoja, bajo una
       rejilla de rótulos y valores se convierte en ruido: el panel opaco los
       asienta y los deja legibles. El contenido de arriba —las notas— se queda
       sobre el vidrio a propósito: así se distingue de un vistazo lo que la
       tarea DICE de lo que la tarea ES.

       Lo mismo vale para el HISTORIAL (`__panel` lo envuelve en la pestaña de
       actividad): una secuencia de eventos sobre vidrio se lee igual de mal que
       una rejilla de datos. El aro que llevan los íconos de la línea de tiempo
       es de este mismo color, así que sobre el panel sigue interrumpiendo la
       línea igual que antes. */
    .ui-detail__side,
    .ui-detail__panel {
        background: var(--bs-body-bg);
        border-radius: 14px;
        padding: 1rem 1.05rem 1.1rem;
    }

    .ui-detail__side {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 0.9rem 1.25rem;
    }

    /* `--wide` NO toma la fila entera aquí: con solo dos columnas, "ancho"
       equivale a "toda la fila", y como la mitad de los campos son anchos en
       escritorio, el panel se convertía en una columna de trece filas con media
       pantalla en blanco a la derecha de cada una (lo reportó Randolf con
       captura). Aquí cada campo ocupa UNA celda y, si su valor no cabe, usa dos
       renglones — dos líneas de texto en media columna desperdician menos que
       una línea ocupando el ancho completo. */
    .ui-detail__field--wide { grid-column: auto; }

    /* La excepción: los campos que llevan una LISTA CON CONTENIDO dentro
       (personas con avatar, etiquetas, adjuntos). Esos sí necesitan la fila —
       en media columna los chips se apilan uno por renglón. Se detectan por lo
       que TIENEN, no por una clase: si la lista está vacía y solo lleva el
       guion, el campo se queda en su celda y no gasta una fila en nada. */
    .ui-detail__field:has(.ui-detail__person),
    .ui-detail__field:has(.ui-detail__tag) { grid-column: 1 / -1; }

    /* Los adjuntos pasan a SEIS de doce: dos por renglón. Con tres quedarían
       cuatro tarjetas en el ancho de un teléfono y no cabría ni el nombre. */
    .ui-attach__item { grid-column: span 6; }

    /* PERO en el panel de una HISTORIA ocupan la fila ENTERA. Medido a 390 px:
       con dos por renglón el nombre se queda en 36 px —muestra tres letras y
       puntos suspensivos— porque el ícono y el tamaño se llevan el resto de la
       tarjeta. Una historia tiene pocos adjuntos y su nombre es lo que dice qué
       es ("mockup del panel" vs "especificación"), así que la fila completa
       cambia poco el desplazamiento y mucho la legibilidad. */
    .ui-attach--story .ui-attach__item { grid-column: 1 / -1; }

    /* ── SUBTAREAS: aquí SÍ se listan (la hoja tapa la lista) ──
       Sobre el mismo panel sólido que las propiedades: una lista de tareas
       sobre el vidrio se lee tan mal como una rejilla de datos. Cada renglón
       tiene un objetivo cómodo (44 px de alto) porque es el único sitio del
       teléfono desde donde se alcanza una subtarea. */
    .ui-detail__subtasks-list {
        display: flex;
        flex-direction: column;
        list-style: none;
        margin: 0;
        padding: 0.35rem 0.9rem;
        background: var(--bs-body-bg);
        border-radius: 14px;
    }

    .ui-detail__subtask {
        display: flex;
        align-items: center;
        gap: 0.6rem;
        min-height: 44px;
    }

    .ui-detail__subtask:not(:first-child) {
        border-top: 1px solid var(--ui-detail-rule);
    }

    /* El título ocupa todo el renglón: el objetivo de "abrir" es la línea
       entera, no las palabras. */
    .ui-detail__subtask-open {
        flex: 1 1 auto;
        min-width: 0;
        border: 0;
        background: none;
        padding: 0.55rem 0;
        text-align: start;
        color: inherit;
        font: inherit;
        cursor: pointer;
    }

    .ui-detail__subtask-title {
        display: block;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        font-size: 0.9rem;
    }

    /* Completada: mismo vocabulario que la fila (tachado + chulo verde), para
       que la hoja y la lista digan lo mismo de la misma forma. */
    .ui-detail__subtask.is-completed .ui-detail__subtask-title {
        color: var(--bs-secondary-color);
        text-decoration: line-through;
        text-decoration-color: color-mix(in srgb, var(--bs-secondary-color) 55%, transparent);
    }

    /* El chulo de completar de la fila, reutilizado tal cual: aquí NO hay hover
       que lo revele, así que se ve siempre (su `opacity: 0` vive tras la puerta
       de puntero fino) y su franja sensible se recorta sola contra el renglón. */
    .ui-detail__subtask .ui-list__complete,
    .ui-detail__subtask .ui-list__done { flex-shrink: 0; }

    /* SIEMPRE VISIBLE aquí. En la lista, el chulo se revela al pasar por encima
       de la FILA para no llenarla de círculos en reposo; en la hoja no hay tal
       fila, así que con puntero fino (un escritorio angosto, una tablet con
       ratón) se quedaba invisible y aun así tocable — justo el objetivo
       fantasma que se acaba de eliminar del lápiz. Son tres o cuatro renglones,
       no una lista de 500: aquí el círculo se ve desde el principio. */
    .ui-detail__subtask .ui-list__complete { opacity: 1; }

    /* Su franja sensible se REACOTA al renglón: la de la lista se estira ±1rem
       y hasta el borde de la tarjeta contando con que la FILA la recorta
       (`contain: paint`), y aquí no hay tal recorte — sin esto, el área del
       chulo invadiría los renglones vecinos y completaría la subtarea
       equivocada. Sigue siendo un objetivo holgado (~34 × 30 px) sin tocar al
       de arriba ni al de abajo. */
    .ui-detail__subtask .ui-list__complete::before,
    .ui-detail__subtask .ui-list__done--action::before {
        top: -0.35rem;
        bottom: -0.35rem;
        left: -0.5rem;
        right: -0.5rem;
    }

}

@media (prefers-reduced-motion: reduce) {
    .ui-detail { transition: none; }
}


/* ── 12. Tablero por columnas ────────────────────────────────────────────────
   Una columna por estado y una tarjeta por tarea. Las columnas se desplazan
   HORIZONTALMENTE como un carrusel: con seis estados no caben en un iPad, y
   encogerlas hasta que quepan deja tarjetas ilegibles — mejor pocas columnas
   bien leídas y el resto a un deslizamiento.

   El alto lo fija el viewport (no el contenido): así el desplazamiento vertical
   ocurre DENTRO de la columna y el encabezado de cada una queda siempre a la
   vista, que es lo que permite saber dónde se está soltando una tarjeta.

   No se ofrece bajo md — ver `.work-board-narrow`. */

.ui-board {
    display: flex;
    gap: 14px;
    align-items: flex-start;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    padding-bottom: 0.5rem;
    /* Cada columna se detiene en su sitio al deslizar: un tablero a medio
       desplazar entre dos columnas no se lee. */
    scroll-snap-type: x proximity;
}

.ui-board__col {
    flex: 0 0 clamp(15rem, 22vw, 19rem);
    scroll-snap-align: start;
    display: flex;
    flex-direction: column;
    max-height: min(72dvh, 40rem);
    border: 1px solid var(--bs-border-color);
    border-radius: 14px;
    background: var(--bs-body-bg);
    transition: background-color 120ms ease-out, border-color 120ms ease-out;
}

/* La columna bajo el dedo mientras se arrastra: se tiñe del color del estado
   que representa, que es la respuesta a "¿dónde va a caer esto?". */
.ui-board__col.is-drop {
    border-color: color-mix(in srgb, var(--ui-board-accent, var(--bs-primary)) 60%, transparent);
    background: color-mix(in srgb, var(--ui-board-accent, var(--bs-primary)) 7%, var(--bs-body-bg));
}

.ui-board__head {
    display: flex;
    align-items: center;
    gap: 0.45rem;
    padding: 0.7rem 0.85rem;
    border-bottom: 1px solid var(--bs-border-color);
    flex-shrink: 0;
}

/* El punto toma el color que el tenant le dio al estado: es el mismo lenguaje
   de la pastilla de estado del detalle. */
.ui-board__dot {
    width: 0.55rem;
    height: 0.55rem;
    border-radius: 50%;
    flex-shrink: 0;
    background: var(--ui-board-accent, var(--bs-secondary-color));
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--ui-board-accent, var(--bs-secondary-color)) 18%, transparent);
}

.ui-board__name {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 0.82rem;
    font-weight: 650;
}

/* El conteo son las tarjetas de ESTA columna, contadas en el propio render: no
   puede mentir porque no viene de otra consulta. */
.ui-board__count {
    flex-shrink: 0;
    padding: 0.05rem 0.45rem;
    border-radius: 999px;
    font-size: 0.7rem;
    font-weight: 650;
    color: var(--bs-secondary-color);
    background: color-mix(in srgb, var(--bs-body-color) 8%, transparent);
}

.ui-board__body {
    overflow-y: auto;
    overscroll-behavior-y: contain;
    min-height: 4rem;      /* una columna vacía sigue siendo un destino donde soltar */
    padding: 0.6rem;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.ui-board__card {
    display: block;
    text-decoration: none;
    color: inherit;
    padding: 0.6rem 0.7rem;
    border: 1px solid var(--bs-border-color);
    border-radius: 11px;
    background: var(--bs-body-bg);
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
    transition: box-shadow 120ms ease-out, border-color 120ms ease-out;
    /* EL GESTO VERTICAL ES NUESTRO, no del navegador: sin esto, en táctil el
       primer movimiento del dedo desplaza la columna y el arrastre nunca
       empieza. Es la línea que hace que el tablero se pueda usar con el dedo. */
    touch-action: none;
    /* Y el texto no se selecciona al mantener pulsado, que en táctil dispara la
       lupa de selección en mitad del arrastre. */
    user-select: none;
    -webkit-user-select: none;
    /* Y el arrastre NATIVO de enlaces e imágenes queda apagado también en
       WebKit, que es donde vive el iPad. */
    -webkit-user-drag: none;
}

.ui-board__card:hover {
    border-color: color-mix(in srgb, var(--bs-primary) 35%, var(--bs-border-color));
    box-shadow: 0 3px 10px -4px rgba(0, 0, 0, 0.18);
}

.ui-board__card:focus-visible {
    outline: 2px solid var(--bs-primary);
    outline-offset: 1px;
}

/* La original se queda en su sitio, atenuada: es el hueco de donde salió, y
   verlo ayuda a entender que la que se mueve es una copia. */
.ui-board__card.is-dragging { opacity: 0.35; }

/* Mientras el servidor confirma el movimiento. Es un estado breve y honesto: la
   tarjeta YA está en su columna nueva (optimista) y esto solo dice "en curso". */
.ui-board__card.is-moving { opacity: 0.6; }

.ui-board__card-title {
    display: block;
    font-size: 0.85rem;
    line-height: 1.35;
    font-weight: 550;
    /* Dos líneas y elipsis: una tarjeta que crece sin límite rompe la columna. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.ui-board__card-meta {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    margin-top: 0.45rem;
    flex-wrap: wrap;
}

/* EL FANTASMA que sigue al dedo. Va en el <body> y se mueve solo con
   `transform`, que el compositor resuelve sin repintar: arrastrar no cuesta ni
   un reflow. `pointer-events: none` es obligatorio — si capturara el puntero,
   `elementFromPoint` devolvería el propio fantasma y nunca la columna de
   debajo. */
.ui-board__ghost {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 1085;
    pointer-events: none;
    margin: 0;
    opacity: 0.95;
    rotate: 1.5deg;
    box-shadow: 0 12px 28px -10px rgba(2, 6, 23, 0.45);
    cursor: grabbing;
}

/* Mientras se arrastra, el cursor no cambia por cada elemento que pisa. */
.ui-board.is-dragging { cursor: grabbing; }


/* El tablero NO se ofrece en un teléfono: con dos columnas de 40 px no se lee
   ni un título. En vez de encogerlo, se dice — y se da el camino a la lista,
   que muestra exactamente las mismas tareas con los mismos filtros. */
.work-board-narrow {
    text-align: center;
    padding: 2.5rem 1.25rem;
    color: var(--bs-secondary-color);
}

.work-board-narrow__icon {
    font-size: 1.6rem;
    display: block;
    margin-bottom: 0.75rem;
    opacity: 0.5;
}


/* ── 13. Repaso guiado ───────────────────────────────────────────────────────
   Un WORKFLOW, no un tablero: una decisión a la vez, con sus salidas al lado.
   De ahí las dos reglas que gobiernan todo el bloque:

     · La tarjeta pone el TRABAJO y sus SALIDAS en el mismo renglón. Una
       decisión que exige desplazar para ver el botón no se toma — por eso el
       repaso pide ancho (bajo lg la pantalla lo dice en vez de encogerse).
     · Lo ya despachado se ATENÚA, no desaparece. Ver el recorrido es lo que
       convierte una lista en un repaso; si las tarjetas se fueran, el paso
       parecería vaciarse por arte de magia y no habría cómo volver sobre una.

   Sin animaciones infinitas ni nada que repinte de fondo (regla del repo: una
   optimización o un adorno que repinta para siempre no se paga). */

.ui-plan {
    max-width: 62rem;
    /* CONTENEDOR de consulta: la fila de campos decide su reparto por el ancho
       REAL del contenido, no por el del viewport. Es lo único que cubre el caso
       de una tableta con el menú lateral abierto, donde el viewport es ancho y el
       contenido no (la lección del breakpoint de /tasks). */
    container-type: inline-size;
    container-name: plan;
}


/* ── Portada y cierre ───────────────────────────────────────────────────────
   Los dos extremos del ritual comparten forma: un solo bloque centrado, sin
   nada más en pantalla. Empezar y terminar son momentos de decisión única. */
.ui-plan__hero {
    text-align: center;
    /* El relleno inferior deja el mismo aire que el pie de los pasos: la portada y
       el cierre acaban en un botón grande y no pueden quedar contra el pie. */
    padding: 2.5rem 1.25rem 6rem;
    max-width: 34rem;
    margin-inline: auto;
}

.ui-plan__hero-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 4rem;
    height: 4rem;
    border-radius: 50%;
    font-size: 1.6rem;
    margin-bottom: 1rem;
    color: var(--bs-primary);
    background: color-mix(in srgb, var(--bs-primary) 12%, transparent);
}

.ui-plan__hero-icon.is-done {
    color: var(--bs-success);
    background: color-mix(in srgb, var(--bs-success) 14%, transparent);
}

.ui-plan__hero-title {
    font-size: 1.35rem;
    font-weight: 600;
    margin-bottom: 0.35rem;
}

.ui-plan__hero-body {
    color: var(--bs-secondary-color);
    margin-bottom: 1.5rem;
}

/* La cadencia va ANTES del cuerpo: un repaso vale por su ritmo, y cuánto lleva
   sin hacerse es la mitad del empujón para empezarlo. */
.ui-plan__cadence {
    font-size: 0.85rem;
    font-weight: 550;
    color: var(--bs-primary);
    margin-bottom: 0.75rem;
}

.ui-plan__summary {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 0.35rem 1.25rem;
    font-size: 0.9rem;
    color: var(--bs-secondary-color);
    margin-bottom: 1rem;
}

.ui-plan__streak {
    font-weight: 600;
    color: var(--bs-warning-text-emphasis, #997404);
    margin-bottom: 1.5rem;
}


/* ── Progreso del recorrido ─────────────────────────────────────────────────
   Los pasos como puntos: es lo que convierte una secuencia de pantallas en un
   recorrido con final. Son ENLACES (cada paso es una URL), así que se puede
   saltar a cualquiera y el botón atrás del navegador recorre el repaso. */
.ui-plan__steps {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    margin-bottom: 1.1rem;
}

.ui-plan__pip {
    width: 1.6rem;
    height: 0.4rem;
    border-radius: 999px;
    background: color-mix(in srgb, var(--bs-body-color) 12%, transparent);
    transition: background-color 140ms ease, width 140ms ease;
}

.ui-plan__pip.is-done { background: color-mix(in srgb, var(--bs-primary) 45%, transparent); }

.ui-plan__pip.is-active {
    width: 2.6rem;
    background: var(--bs-primary);
}

@media (hover: hover) and (pointer: fine) {
    .ui-plan__pip:hover { background: color-mix(in srgb, var(--bs-primary) 65%, transparent); }
}

.ui-plan__pip:focus-visible {
    outline: 2px solid var(--bs-primary);
    outline-offset: 2px;
}

.ui-plan__steps-label {
    margin-inline-start: auto;
    font-size: 0.78rem;
    font-weight: 550;
    color: var(--bs-secondary-color);
}

.ui-plan__title {
    font-size: 1.15rem;
    font-weight: 600;
    margin-bottom: 0.2rem;
}

.ui-plan__hint {
    color: var(--bs-secondary-color);
    font-size: 0.88rem;
    margin-bottom: 1rem;
}

/* Un paso SIN nada pendiente es una buena noticia y se lee como tal: es la
   señal de "esto ya está bajo control", el punto del repaso entero. */
.ui-plan__clear {
    color: var(--bs-success-text-emphasis, #0f5132);
    background: color-mix(in srgb, var(--bs-success) 8%, transparent);
    border-radius: 12px;
    padding: 0.85rem 1rem;
    font-weight: 550;
}


/* ── Tarjetas ───────────────────────────────────────────────────────────────
   El trabajo a la izquierda, las salidas a la derecha, en un renglón. */
.ui-plan__list {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    margin-bottom: 1rem;
}

.ui-plan__card,
.ui-plan__project {
    display: flex;
    align-items: flex-start;
    flex-wrap: wrap;
    gap: 0.5rem 0.75rem;
    padding: 0.7rem 0.85rem;
    border-radius: 12px;
    background: color-mix(in srgb, var(--bs-body-color) 3.5%, transparent);
    border: 1px solid color-mix(in srgb, var(--bs-body-color) 7%, transparent);
    /* Cada tarjeta es frontera de pintura: un repintado no se filtra a las
       vecinas cuando una se atenúa o cambia de estado. */
    contain: layout paint;
}

/* El editor de aclarar APILA: es el único que lleva campos, y el título arriba
   con el editor debajo se lee como una sola decisión. */
.ui-plan__card--clarify { flex-direction: column; align-items: stretch; }

/* Y al apilar, el eje principal pasa a ser VERTICAL: el `flex-basis` del
   contenido —pensado para repartir el ANCHO cuando la tarjeta es una fila— se
   convertiría en 16rem de ALTO, que es exactamente el bloque vacío enorme que
   se veía. En columna el contenido mide lo que ocupa. */
.ui-plan__card--clarify .ui-plan__card-main { flex: 0 0 auto; }

/* Apilada, las salidas dejan de ir "a la derecha del trabajo" y pasan a ser el
   pie del editor: alineadas con la columna de campos y con aire propio, que es
   como se lee un formulario. */
.ui-plan__card--clarify .ui-plan__card-actions {
    margin-inline-start: 0;
    margin-top: 0.25rem;
}

/* ATENUADO, no borrado: se ve lo que ya se despachó sin que compita con lo que
   falta. Nada de opacidad sobre controles vivos — cuando está procesada, las
   acciones ya no se pintan (la tarjeta cambia de contenido, no de opacidad). */
.ui-plan__card.is-processed,
.ui-plan__project.is-processed {
    opacity: 0.55;
    background: transparent;
}

.ui-plan__project.is-stalled {
    border-color: color-mix(in srgb, var(--bs-danger) 35%, transparent);
    background: color-mix(in srgb, var(--bs-danger) 5%, transparent);
}

.ui-plan__card-main {
    flex: 1 1 16rem;
    min-width: 0;
}

.ui-plan__card-title {
    display: block;
    font-weight: 550;
    color: var(--bs-body-color);
    text-decoration: none;
    /* Un título largo no parte la tarjeta en cuatro renglones. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

@media (hover: hover) and (pointer: fine) {
    .ui-plan__card-title:hover { color: var(--bs-primary); }
}

.ui-plan__card-meta {
    display: flex;
    flex-wrap: wrap;
    gap: 0.3rem;
    margin-top: 0.3rem;
}

.ui-plan__chip {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    font-size: 0.75rem;
    font-weight: 550;
    padding: 0.1rem 0.45rem;
    border-radius: 999px;
    color: var(--bs-secondary-color);
    background: color-mix(in srgb, var(--bs-body-color) 6%, transparent);
}

.ui-plan__chip.is-overdue {
    color: var(--bs-danger-text-emphasis, #842029);
    background: color-mix(in srgb, var(--bs-danger) 12%, transparent);
}

.ui-plan__card-done {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    font-size: 0.8rem;
    font-weight: 550;
    color: var(--bs-success-text-emphasis, #0f5132);
}

.ui-plan__card-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
    /* Las salidas se alinean a la derecha del renglón del trabajo. */
    margin-inline-start: auto;
}


/* ── Campos del editor de aclarar ───────────────────────────────────────────*/

/* El título ES un campo aquí (aclarar es reescribir), así que se ve como texto de
   título y no como un input de formulario: sin borde en reposo, y con el marco
   solo al enfocarlo. */
.ui-plan__title-input {
    font-weight: 550;
    font-size: 1rem;
    padding-inline: 0.4rem;
    border-color: transparent;
    background: transparent;
}

.ui-plan__notes-input {
    resize: vertical;
    min-height: 3.2rem;
}

@media (hover: hover) and (pointer: fine) {
    .ui-plan__title-input:hover:not(:focus) {
        border-color: color-mix(in srgb, var(--bs-body-color) 12%, transparent);
    }
}

/* REJILLA de 12 columnas, NO flex por contenido. Con flex, cada control medía lo
   que decía su texto —"Todos los clientes" ancho, "Sin proyecto" angosto— así que
   el mismo campo cambiaba de tamaño según su valor y, al envolver, dejaba media
   fila vacía. Con la rejilla el tamaño de cada campo está DEFINIDO por su span:
   no depende de lo que diga dentro, y los cinco caben en una línea.

   `stretch` (no `flex-end`/`start`): cada campo es rótulo + control, así que
   alinear los CAMPOS por su borde dejaba los CONTROLES desalineados en cuanto uno
   medía distinto. Estirados, cada campo ancla su control abajo y todos comparten
   la línea por construcción. */
.ui-plan__fields {
    display: grid;
    grid-template-columns: repeat(12, 1fr);
    gap: 0.5rem 0.75rem;
    align-items: stretch;
}

/* Los spans SUMAN 12: la fila se llena completa, sin un hueco al final. El
   proyecto se lleva la columna extra porque sus nombres son los más largos. */
.ui-plan__field--customer { grid-column: span 3; }
.ui-plan__field--project  { grid-column: span 4; }
.ui-plan__field--date     { grid-column: span 2; }
.ui-plan__field--flag     { grid-column: span 1; }

/* Sin cascada (sin permiso de clientes) el proyecto ABSORBE las columnas del
   cliente en vez de dejar tres vacías al final de la fila. */
.ui-plan__fields:not(:has(.ui-plan__field--customer)) .ui-plan__field--project {
    grid-column: span 7;
}

/* El control llena su celda: es la celda la que define el ancho, no el texto. */
.ui-plan__fields select,
.ui-plan__fields input { width: 100%; }

/* Con el contenido estrecho (una tableta, o un escritorio con el menú abierto) la
   fila se parte en DOS renglones conservando las proporciones — nunca un campo
   suelto ocupando el ancho completo, y nunca un control recortado: medido, por
   debajo de ~680 px de contenido el input de fecha ya no cabe en 2/12. */
@container plan (max-width: 680px) {
    .ui-plan__fields { grid-template-columns: repeat(6, minmax(0, 1fr)); }

    /* 3 + 3 arriba, 2 + 2 + 2 abajo: los dos renglones quedan llenos. */
    .ui-plan__field--project { grid-column: span 3; }
    .ui-plan__field--flag    { grid-column: span 2; }

    /* Sin cascada, el proyecto se lleva el primer renglón completo. */
    .ui-plan__fields:not(:has(.ui-plan__field--customer)) .ui-plan__field--project {
        grid-column: span 6;
    }
}

/* La bandera no tiene un input debajo del rótulo sino un botón: se estira para
   ocupar el mismo alto que los controles vecinos, en vez de flotar. */
.ui-plan__field--flag { min-width: 0; }

.ui-plan__draft-flag {
    /* Alto como los inputs de al lado (así la fila queda a una sola caja) y el
       ancho de su celda: es la celda la que define el tamaño, como en el resto de
       la fila — un botón que mida su ícono dejaría un hueco a su derecha. */
    align-self: end;
    width: 100%;
    min-height: 2rem;
}

.ui-plan__field {
    display: flex;
    flex-direction: column;
    /* El rótulo arriba y el control PEGADO ABAJO: con los campos estirados a la
       misma altura, es esto lo que deja todos los controles en la misma línea. */
    justify-content: flex-end;
    gap: 0.15rem;
    /* Sin esto una celda de rejilla se niega a encogerse por debajo del contenido
       de su control, y la fila desborda. */
    min-width: 0;
}

.ui-plan__due,
.ui-plan__defer {
    /* La misma altura que los `ui-select` de la fila (Bootstrap los hace 4 px más
       bajos): así los controles no solo comparten la base, sino la caja. El ANCHO
       lo pone la celda de la rejilla, no un tope propio. */
    min-height: 2rem;
}


/* ── Proyectos ──────────────────────────────────────────────────────────────*/
.ui-plan__project { flex-direction: column; align-items: stretch; }

.ui-plan__project-head {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
}

.ui-plan__project-dot {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    flex-shrink: 0;
}

.ui-plan__project-name {
    font-weight: 600;
    color: var(--bs-body-color);
    text-decoration: none;
}

/* El ícono de "se abre en otra pestaña": discreto en reposo, y con el enlace
   apuntado toma el acento junto al texto — la señal de que ahí se puede ir. */
.ui-plan__link-icon {
    font-size: 0.65em;
    margin-inline-start: 0.3rem;
    opacity: 0.45;
}

@media (hover: hover) and (pointer: fine) {
    a.ui-plan__project-name:hover,
    a.ui-plan__next-title:hover { color: var(--bs-primary); }

    a.ui-plan__project-name:hover .ui-plan__link-icon,
    a.ui-plan__next-title:hover .ui-plan__link-icon { opacity: 1; }
}

a.ui-plan__next-title {
    color: var(--bs-body-color);
    text-decoration: none;
}

.ui-plan__badge {
    font-size: 0.7rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.02em;
    padding: 0.1rem 0.4rem;
    border-radius: 6px;
}

.ui-plan__badge.is-due {
    color: var(--bs-warning-text-emphasis, #997404);
    background: color-mix(in srgb, var(--bs-warning) 18%, transparent);
}

.ui-plan__badge.is-blocked {
    color: var(--bs-info-text-emphasis, #055160);
    background: color-mix(in srgb, var(--bs-info) 16%, transparent);
}

/* Sin avance: la señal que destapa el proyecto MUERTO que se ve sano por tener
   tareas. Va en el tono de advertencia, no de error: el trabajo existe. */
.ui-plan__badge.is-stale {
    color: var(--bs-warning-text-emphasis, #997404);
    background: color-mix(in srgb, var(--bs-warning) 18%, transparent);
}

.ui-plan__badge.is-overdue {
    color: var(--bs-danger-text-emphasis, #842029);
    background: color-mix(in srgb, var(--bs-danger) 14%, transparent);
}

/* `is-reviewed` (ya lo revisaste) e `is-ok` (llega a tiempo) son cosas distintas
   con la misma piel: se declaran juntas para que no puedan divergir. */
.ui-plan__badge.is-reviewed,
.ui-plan__badge.is-ok {
    color: var(--bs-success-text-emphasis, #0f5132);
    background: color-mix(in srgb, var(--bs-success) 14%, transparent);
}

.ui-plan__project-progress {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

/* El anillo es un SVG con `viewBox` y SIN medida propia: dentro de un contenedor
   que le da todo el ancho se estira a cientos de píxeles. Aquí se acota. */
.ui-plan__project-progress svg {
    flex: 0 0 auto;
    /* 3rem es el suelo para que el porcentaje de dentro quepa sin recortarse. */
    width: 3rem;
    height: 3rem;
}

.ui-plan__project-progress {
    align-items: flex-start;   /* el ritmo añade una segunda línea al lado del anillo */
}

.ui-plan__project-progress-body {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    min-width: 0;
}

.ui-plan__project-progress-label {
    font-size: 0.8rem;
    color: var(--bs-secondary-color);
}

/* ¿VA A LLEGAR? El ritmo y la fecha proyectada, al lado del progreso: uno dice
   dónde va el proyecto y el otro si llega. */
.ui-plan__project-pace {
    display: inline-flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.35rem;
    font-size: 0.78rem;
    color: var(--bs-body-color);
}

/* Sin ritmo que medir el texto es una explicación, no un dato: se atenúa —con la
   cursiva y un tono más bajo— para que no compita con la fecha real de los
   proyectos que sí la tienen.

   NO `--bs-secondary-color`: MEDIDO sobre el fondo tintado de esta tarjeta da
   3.91:1 en oscuro, bajo el 4.5 que exige el texto normal. Al 86 % del color del
   cuerpo pasa en los dos temas (7.6:1 claro, 4.6:1 oscuro) sin dejar de leerse
   como secundario. */
.ui-plan__project-pace.is-muted {
    color: color-mix(in srgb, var(--bs-body-color) 88%, transparent);
    font-style: italic;
}

/* ── Las próximas acciones del proyecto ─────────────────────────────────────
   Revisar un proyecto es juzgar SUS acciones, no ver un conteo: por eso van con
   su título completo, y cada una con lo que hace falta para juzgarla (si espera a
   un tercero, cuándo vence, cuánto pesa). */
/* OJO con el nombre: `.ui-plan__next` es el BOTÓN "Siguiente" del pie desde el
   primer día. Llamar así a este bloque le colgó encima relleno, fondo y radio y
   rompió el botón — la misma colisión de nombres que ya había pasado con
   `.ui-plan__project`. Al nombrar una clase dentro de un bloque BEM hay que
   buscar el nombre EXACTO en el markup antes de usarlo. */
.ui-plan__next-actions {
    border-radius: 10px;
    padding: 0.5rem 0.65rem;
    background: color-mix(in srgb, var(--bs-body-color) 3%, transparent);
}

.ui-plan__next-label {
    font-size: 0.7rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.02em;
    color: var(--bs-secondary-color);
    margin: 0 0 0.3rem;
}

.ui-plan__next-row {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.3rem 0.5rem;
    padding: 0.2rem 0;
}

.ui-plan__next-title {
    flex: 1 1 12rem;
    min-width: 0;
    font-weight: 500;
    /* El título completo: una acción cortada a la mitad no se puede juzgar. */
}

.ui-plan__next-more {
    margin: 0.3rem 0 0;
    font-size: 0.75rem;
    color: var(--bs-secondary-color);
}

.ui-plan__chip.is-waiting {
    color: var(--bs-info-text-emphasis, #055160);
    background: color-mix(in srgb, var(--bs-info) 14%, transparent);
}


/* EL HALLAZGO del paso: un proyecto activo sin una sola acción abierta. Se
   resuelve donde se descubre, así que el bloque incluye la captura. */
.ui-plan__stalled {
    border-radius: 10px;
    padding: 0.6rem 0.75rem;
    background: color-mix(in srgb, var(--bs-danger) 7%, transparent);
}

.ui-plan__stalled-head {
    margin-bottom: 0.15rem;
    color: var(--bs-danger-text-emphasis, #842029);
}

.ui-plan__stalled-hint {
    font-size: 0.82rem;
    color: var(--bs-secondary-color);
    margin-bottom: 0.5rem;
}

.ui-plan__project-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: 0.5rem 0.75rem;
}

.ui-plan__reviewed { margin-inline-start: auto; }


/* ── Mapa de carga de los próximos días ─────────────────────────────────────
   La pregunta del paso es "¿esto cabe?", y un listado no la responde. Cada día es
   una celda con una BARRA proporcional a las horas estimadas contra la capacidad
   del día, coloreada por nivel: el día que se va a desbordar se ve antes de
   llegar a él.

   Los días son botones (cero JS) y el color son clases. El rojo/ámbar salen de las
   variables del tema, así que funcionan en claro y oscuro sin overrides. */
.ui-plan__map { margin-bottom: 1rem; }

.ui-plan__map-days {
    display: grid;
    /* Catorce días SIEMPRE del mismo ancho: la comparación entre días es el dato,
       y con celdas de ancho variable no se puede comparar de un vistazo. */
    grid-template-columns: repeat(14, minmax(0, 1fr));
    gap: 0.35rem;
}

.ui-plan__map-day {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.15rem;
    padding: 0.4rem 0.15rem 0.35rem;
    border: 1px solid transparent;
    border-radius: 10px;
    background: color-mix(in srgb, var(--bs-body-color) 4%, transparent);
    color: var(--bs-secondary-color);
    font-size: 0.72rem;
    line-height: 1.1;
    transition: background-color 140ms ease, border-color 140ms ease;
}

.ui-plan__map-dow {
    text-transform: uppercase;
    letter-spacing: 0.02em;
    opacity: 0.75;
}

.ui-plan__map-num {
    font-weight: 600;
    font-size: 0.9rem;
    color: var(--bs-body-color);
}

/* HOY se marca con el acento: es el ancla del mapa. */
.ui-plan__map-day.is-today .ui-plan__map-num { color: var(--bs-primary); }

.ui-plan__map-bar {
    display: flex;
    align-items: flex-end;
    width: 100%;
    height: 2.25rem;
    border-radius: 5px;
    overflow: hidden;
    background: color-mix(in srgb, var(--bs-body-color) 7%, transparent);
}

.ui-plan__map-fill {
    width: 100%;
    border-radius: 5px 5px 0 0;
    background: var(--bs-secondary-color);
    transition: height 160ms ease;
}

.ui-plan__map-day.is-light  .ui-plan__map-fill { background: var(--bs-success); }
.ui-plan__map-day.is-busy   .ui-plan__map-fill { background: var(--bs-warning); }
.ui-plan__map-day.is-over   .ui-plan__map-fill { background: var(--bs-danger); }

/* El nivel también tiñe la celda: la barra sola es difícil de comparar de reojo. */
.ui-plan__map-day.is-busy { background: color-mix(in srgb, var(--bs-warning) 10%, transparent); }
.ui-plan__map-day.is-over { background: color-mix(in srgb, var(--bs-danger) 12%, transparent); }

.ui-plan__map-hours { font-variant-numeric: tabular-nums; }

/* Las tareas sin estimar: el día puede estar peor de lo que el color dice. */
.ui-plan__map-unknown {
    font-size: 0.65rem;
    font-weight: 600;
    padding: 0 0.25rem;
    border-radius: 999px;
    color: var(--bs-body-color);
    background: color-mix(in srgb, var(--bs-body-color) 10%, transparent);
}

.ui-plan__map-day.is-selected {
    border-color: var(--bs-primary);
    background: color-mix(in srgb, var(--bs-primary) 10%, transparent);
}

@media (hover: hover) and (pointer: fine) {
    .ui-plan__map-day:hover { border-color: color-mix(in srgb, var(--bs-primary) 45%, transparent); }
}

.ui-plan__map-day:focus-visible {
    outline: 2px solid var(--bs-primary);
    outline-offset: 2px;
}

.ui-plan__map-legend {
    display: flex;
    align-items: center;
    gap: 0.3rem;
    flex-wrap: wrap;
    margin: 0.5rem 0 0;
    font-size: 0.72rem;
    color: var(--bs-secondary-color);
}

.ui-plan__map-key {
    width: 0.6rem;
    height: 0.6rem;
    border-radius: 3px;
    margin-inline-start: 0.5rem;
}

.ui-plan__map-key:first-child { margin-inline-start: 0; }
.ui-plan__map-key.is-light { background: var(--bs-success); }
.ui-plan__map-key.is-busy  { background: var(--bs-warning); }
.ui-plan__map-key.is-over  { background: var(--bs-danger); }

.ui-plan__map-capacity { margin-inline-start: auto; }


/* ── El día abierto: sus tareas, con a dónde moverlas ───────────────────────*/
.ui-plan__day {
    margin-top: 0.85rem;
    padding: 0.75rem 0.85rem;
    border-radius: 12px;
    background: color-mix(in srgb, var(--bs-body-color) 3.5%, transparent);
    border: 1px solid color-mix(in srgb, var(--bs-body-color) 7%, transparent);
}

.ui-plan__day-title {
    display: flex;
    align-items: baseline;
    gap: 0.5rem;
    flex-wrap: wrap;
    font-size: 0.95rem;
    font-weight: 600;
    margin-bottom: 0.5rem;
    text-transform: capitalize;
}

.ui-plan__day-sum {
    font-size: 0.78rem;
    font-weight: 500;
    color: var(--bs-secondary-color);
    text-transform: none;
}

.ui-plan__day-warning {
    font-size: 0.82rem;
    color: var(--bs-danger-text-emphasis, #842029);
    background: color-mix(in srgb, var(--bs-danger) 8%, transparent);
    border-radius: 8px;
    padding: 0.4rem 0.6rem;
    margin-bottom: 0.6rem;
}

/* La fila del día APILA: título, descripción y —al pie— los datos con las
   salidas. Reprogramar es una decisión, y no se decide sobre un título cortado a
   la mitad ni sin el contexto que dice por qué esto era para ese día. */
.ui-plan__day-row {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    padding: 0.6rem 0;
    border-top: 1px solid color-mix(in srgb, var(--bs-body-color) 6%, transparent);
}

.ui-plan__day-row:first-of-type { border-top: 0; }

.ui-plan__day-row.is-processed { opacity: 0.5; }

/* A fila completa y SIN recortar: el título entero, envolviendo si hace falta. */
.ui-plan__day-name {
    font-weight: 550;
    margin: 0;
}

.ui-plan__day-notes {
    margin: 0;
    font-size: 0.85rem;
    color: var(--bs-secondary-color);
    /* Dos líneas: es el contexto para decidir, no el detalle completo (que vive en
       la lista). Una descripción larga no puede empujar el resto del día. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

/* El último renglón: a la izquierda lo que la tarea ES, a la derecha a dónde se
   mueve. Envuelve entero si el ancho no alcanza, nunca a media botonera. */
.ui-plan__day-foot {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.4rem 0.75rem;
    margin-top: 0.15rem;
}

.ui-plan__day-meta {
    display: flex;
    gap: 0.3rem;
    flex-wrap: wrap;
    /* Los chips ceden ancho pero NO crecen: creciendo le robaban unos píxeles a la
       botonera y el selector de fecha se caía a un segundo renglón. */
    flex: 0 1 auto;
    min-width: 0;
}

/* Un estimado ausente se marca: es lo que hace que el mapa mienta por defecto. */
.ui-plan__chip.is-unknown {
    color: var(--bs-warning-text-emphasis, #997404);
    background: color-mix(in srgb, var(--bs-warning) 14%, transparent);
}

.ui-plan__day-actions {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    /* La botonera NUNCA se parte por dentro: o entra completa en el renglón de los
       chips, o baja ENTERA al siguiente. Media botonera en cada línea es lo que se
       veía y lo que no puede pasar. */
    flex-wrap: nowrap;
    flex: 0 0 auto;
    margin-inline-start: auto;
}

.ui-plan__move-date { max-width: 9.5rem; }




/* ── Pie de navegación ──────────────────────────────────────────────────────*/
.ui-plan__more { margin-bottom: 1rem; }

.ui-plan__footer {
    display: flex;
    justify-content: space-between;
    gap: 0.75rem;
    margin-top: 1.25rem;
    padding-top: 1rem;
    border-top: 1px solid color-mix(in srgb, var(--bs-body-color) 8%, transparent);
    /* AIRE al final: el pie de la app es `position: absolute` de 60 px, así que
       sin colchón los botones de navegación quedan pegados a él y la pantalla se
       lee sin remate. El espacio en blanco es parte del diseño, no sobra. */
    margin-bottom: 4rem;
}

/* En el primer paso solo hay "siguiente": que no se pegue a la izquierda. */
.ui-plan__next,
.ui-plan__finish-step { margin-inline-start: auto; }


/* ============================================================================
   17. HOJA DE TIEMPO SEMANAL — `ui-sheet`
   ----------------------------------------------------------------------------
   La unidad no es el evento, es LA SEMANA: total contra la meta, los siete días
   como barras, y el reparto de en qué se fue. Es el consenso de Harvest y Toggl
   y responde las preguntas con las que uno abre la pantalla, que una tabla de
   eventos en orden inverso no responde.

   Los siete días se pintan SIEMPRE, incluidos los que están en cero: un día en
   blanco es justo la información que se busca.
   ==========================================================================*/

.ui-sheet {
    display: flex;
    flex-direction: column;
    gap: 1rem;
}

/* ── El encabezado: cuánto llevo ──────────────────────────────────────────── */

.ui-sheet__head {
    background: var(--bs-body-bg);
    border: 1px solid color-mix(in srgb, var(--bs-body-color) 16%, transparent);
    border-radius: 14px;
    padding: 1.1rem 1.25rem;
}

.ui-sheet__nav {
    display: flex;
    align-items: center;
    gap: .4rem;
    margin-bottom: .75rem;
}

.ui-sheet__step {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* 34 px: alcanzable con el dedo sin ser un botón enorme. */
    inline-size: 34px;
    block-size: 34px;
    border-radius: 10px;
    color: var(--bs-body-color);
    text-decoration: none;
    transition: background-color 120ms ease;
}

.ui-sheet__range {
    font-weight: 600;
    font-size: .95rem;
    min-inline-size: 9rem;
    text-align: center;
}

.ui-sheet__today {
    margin-inline-start: .35rem;
    font-size: .8rem;
    font-weight: 550;
    color: var(--bs-primary);
    text-decoration: none;
}

.ui-sheet__total {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: .5rem;
}

.ui-sheet__total-label {
    font-size: .8rem;
    /* 88 % del color del cuerpo: --bs-secondary-color cae a 3.47:1 sobre una
       tarjeta en oscuro, peor que la línea base de la app. */
    color: color-mix(in srgb, var(--bs-body-color) 88%, transparent);
}

.ui-sheet__total-value {
    font-size: 1.9rem;
    font-weight: 650;
    line-height: 1;
    /* Los dígitos no bailan al recalcularse el total. */
    font-variant-numeric: tabular-nums;
}

.ui-sheet__total-goal {
    font-size: .85rem;
    /* 85 %: al 70 % daba 3.79:1 en oscuro — texto pequeño exige 4.5. */
    color: color-mix(in srgb, var(--bs-body-color) 85%, transparent);
}

.ui-sheet__bar {
    margin-top: .6rem;
    block-size: 8px;
    border-radius: 999px;
    background: color-mix(in srgb, var(--bs-body-color) 8%, transparent);
    overflow: hidden;
}

.ui-sheet__bar-fill {
    display: block;
    block-size: 100%;
    border-radius: inherit;
    background: var(--bs-primary);
}

.ui-sheet__running {
    margin: .6rem 0 0;
    font-size: .82rem;
    color: color-mix(in srgb, var(--bs-body-color) 82%, transparent);
}

/* ── Los siete días ───────────────────────────────────────────────────────── */

.ui-sheet__days {
    display: grid;
    /* Siete columnas iguales: la comparación entre días es lo que se mira, y
       con anchos por contenido un día de "8h 30m" se vería más grande que otro
       de "2h" sin que eso signifique nada. */
    /* minmax(0, 1fr) y no 1fr: una pista `1fr` tiene min-width AUTO, así que un
       contenido que no encoge la infla y el grid desborda el contenedor. */
    grid-template-columns: repeat(7, minmax(0, 1fr));
    gap: .5rem;
}

.ui-sheet__day {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: .3rem;
    padding: .7rem .3rem .6rem;
    border: 1px solid color-mix(in srgb, var(--bs-body-color) 10%, transparent);
    border-radius: 12px;
    background: var(--bs-body-bg);
    color: var(--bs-body-color);
    text-decoration: none;
    transition: border-color 120ms ease, background-color 120ms ease;
}

.ui-sheet__day-name {
    font-size: .72rem;
    text-transform: uppercase;
    letter-spacing: .03em;
    color: color-mix(in srgb, var(--bs-body-color) 85%, transparent);
}

.ui-sheet__day-num {
    font-size: 1.05rem;
    font-weight: 600;
    line-height: 1;
    font-variant-numeric: tabular-nums;
}

.ui-sheet__day-bar {
    display: flex;
    align-items: flex-end;
    /* La barra crece desde abajo, como una columna: es la lectura natural de
       "cuánto se llenó el día". */
    inline-size: 8px;
    block-size: 44px;
    border-radius: 999px;
    background: color-mix(in srgb, var(--bs-body-color) 8%, transparent);
    overflow: hidden;
}

.ui-sheet__day-fill {
    display: block;
    inline-size: 100%;
    border-radius: inherit;
    background: var(--bs-primary);
}

.ui-sheet__day-hours {
    font-size: .74rem;
    font-variant-numeric: tabular-nums;
    color: color-mix(in srgb, var(--bs-body-color) 85%, transparent);
}

/* Hoy se marca con un aro; el día ABIERTO se tinta. Son dos cosas distintas y
   pueden coincidir, así que no comparten señal. */
.ui-sheet__day.is-today { border-color: color-mix(in srgb, var(--bs-primary) 55%, transparent); }

.ui-sheet__day.is-selected {
    background: color-mix(in srgb, var(--bs-primary) 10%, var(--bs-body-bg));
    border-color: color-mix(in srgb, var(--bs-primary) 45%, transparent);
}

/* Un día que aún no llega no es un día vacío: se atenúa para que no compita con
   los que sí se pueden registrar. */
.ui-sheet__day.is-future { opacity: .55; }

.ui-sheet__day.is-weekend .ui-sheet__day-name,
.ui-sheet__day.is-weekend .ui-sheet__day-num {
    color: color-mix(in srgb, var(--bs-body-color) 60%, transparent);
}

.ui-sheet__capacity {
    margin: 0;
    font-size: .78rem;
    text-align: end;
    color: color-mix(in srgb, var(--bs-body-color) 85%, transparent);
}

/* ── Lo que quedó sin registrar ───────────────────────────────────────────── */

.ui-sheet__missing {
    display: flex;
    align-items: center;
    gap: .6rem;
    padding: .7rem .95rem;
    border-radius: 12px;
    background: color-mix(in srgb, var(--bs-warning) 12%, var(--bs-body-bg));
    border: 1px solid color-mix(in srgb, var(--bs-warning) 35%, transparent);
    font-size: .87rem;
}

.ui-sheet__missing i { color: var(--bs-warning-text-emphasis); }

.ui-sheet__missing-go {
    margin-inline-start: auto;
    font-weight: 550;
    color: var(--bs-primary);
    text-decoration: none;
    white-space: nowrap;
}

/* ── Los dos paneles ──────────────────────────────────────────────────────── */

.ui-sheet__cols {
    display: grid;
    /* Dos columnas mientras quepan; el reparto por CONTENIDO (container query)
       y no por viewport, que con el menú lateral abierto miente. */
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 1rem;
    align-items: start;
}

.ui-sheet__panel {
    background: var(--bs-body-bg);
    border: 1px solid color-mix(in srgb, var(--bs-body-color) 16%, transparent);
    border-radius: 14px;
    padding: 1rem 1.15rem;
}

.ui-sheet__panel-title {
    margin: 0 0 .85rem;
    font-size: .78rem;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: color-mix(in srgb, var(--bs-body-color) 75%, transparent);
}

.ui-sheet__empty {
    margin: 0;
    font-size: .87rem;
    color: color-mix(in srgb, var(--bs-body-color) 85%, transparent);
}

/* ── El reparto ───────────────────────────────────────────────────────────── */

.ui-sheet__slice + .ui-sheet__slice { margin-top: .9rem; }

.ui-sheet__slice-head {
    display: flex;
    align-items: baseline;
    gap: .5rem;
}

.ui-sheet__slice-name {
    font-weight: 550;
    font-size: .9rem;
    /* Un nombre larguísimo no puede empujar las horas fuera de la tarjeta. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.ui-sheet__slice-hours {
    margin-inline-start: auto;
    font-size: .85rem;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.ui-sheet__slice-bar {
    margin: .3rem 0 .4rem;
    block-size: 6px;
    border-radius: 999px;
    background: color-mix(in srgb, var(--bs-body-color) 8%, transparent);
    overflow: hidden;
}

.ui-sheet__slice-fill {
    display: block;
    block-size: 100%;
    border-radius: inherit;
    background: color-mix(in srgb, var(--bs-primary) 70%, transparent);
}

.ui-sheet__tasks {
    margin: 0;
    padding: 0;
    list-style: none;
}

.ui-sheet__tasks li {
    display: flex;
    gap: .5rem;
    padding: .15rem 0;
    font-size: .82rem;
    color: color-mix(in srgb, var(--bs-body-color) 82%, transparent);
}

.ui-sheet__task-title {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.ui-sheet__task-hours {
    margin-inline-start: auto;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* ── El día abierto ───────────────────────────────────────────────────────── */

.ui-sheet__entries {
    margin: 0 0 .75rem;
    padding: 0;
    list-style: none;
}

.ui-sheet__entries li {
    display: flex;
    align-items: center;
    gap: .6rem;
    padding: .5rem 0;
    border-bottom: 1px solid color-mix(in srgb, var(--bs-body-color) 8%, transparent);
}

.ui-sheet__entries li:last-child { border-bottom: 0; }

.ui-sheet__entry-main {
    display: flex;
    flex-direction: column;
    min-inline-size: 0;
    flex: 1 1 auto;
}

.ui-sheet__entry-title {
    font-size: .88rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.ui-sheet__entry-notes {
    font-size: .78rem;
    color: color-mix(in srgb, var(--bs-body-color) 85%, transparent);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.ui-sheet__entry-time,
.ui-sheet__entry-hours {
    font-size: .82rem;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.ui-sheet__entry-hours { font-weight: 550; }

.ui-sheet__entries li.is-running .ui-sheet__entry-hours { color: var(--bs-primary); }

/* Quitar un registro es destructivo: el objetivo se dimensiona para el dedo y
   se ve SIN hover, que en táctil no ocurre nunca. */
.ui-sheet__entry-remove {
    flex: 0 0 auto;
    inline-size: 28px;
    block-size: 28px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 0;
    border-radius: 8px;
    background: transparent;
    color: color-mix(in srgb, var(--bs-danger) 75%, #000);
    transition: background-color 120ms ease, color 120ms ease;
}

[data-bs-theme="dark"] .ui-sheet__entry-remove {
    color: color-mix(in srgb, var(--bs-danger) 85%, #fff);
}

.ui-sheet__day-total {
    display: flex;
    gap: .5rem;
    margin: 0 0 .9rem;
    padding-top: .5rem;
    border-top: 1px solid color-mix(in srgb, var(--bs-body-color) 12%, transparent);
    font-size: .85rem;
}

.ui-sheet__day-total strong {
    margin-inline-start: auto;
    font-variant-numeric: tabular-nums;
}

/* ── La captura ───────────────────────────────────────────────────────────── */

.ui-sheet__capture {
    display: grid;
    /* Tamaños DECLARADOS, no por contenido: el mismo campo no puede cambiar de
       ancho según el texto que le toque mostrar (la lección del editor de
       aclarar del repaso). */
    grid-template-columns: minmax(0, 1fr) 6.5rem 6.5rem minmax(0, 1fr) auto;
    gap: .5rem;
    align-items: end;
    padding-top: .85rem;
    border-top: 1px solid color-mix(in srgb, var(--bs-body-color) 12%, transparent);
}

.ui-sheet__capture .form-label { margin-bottom: .15rem; }

.ui-sheet__hint {
    margin: .5rem 0 0;
    font-size: .78rem;
    color: color-mix(in srgb, var(--bs-body-color) 85%, transparent);
}

/* ── Reparto por ANCHO DE CONTENIDO ───────────────────────────────────────── */

/* El contenedor de la página ya declara `container-type: inline-size` para
   /tasks; aquí se reacciona al mismo ancho de CONTENIDO, no al viewport: con el
   menú lateral abierto el viewport es ancho y el contenido no. */
@container work-page (max-width: 820px) {
    .ui-sheet__cols { grid-template-columns: minmax(0, 1fr); }
}

@media (max-width: 820px) {
    .ui-sheet__cols { grid-template-columns: minmax(0, 1fr); }
}

@media (max-width: 620px) {
    /* Los siete días no caben legibles en una fila estrecha: pasan a dos filas
       antes que encogerse a columnas de 30 px. */
    .ui-sheet__days { grid-template-columns: repeat(4, minmax(0, 1fr)); }

    .ui-sheet__capture { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
    .ui-sheet__capture-task,
    .ui-sheet__capture-notes { grid-column: 1 / -1; }
    .ui-sheet__capture button { grid-column: 1 / -1; }
}


/* ============================================================================
   18. PÁGINAS DE ESTADO — `status-page`
   ----------------------------------------------------------------------------
   Lo que se muestra cuando algo ya salió mal: un 500 (/Error) o un 404
   (/not-found). Las dos aparecen en el peor momento del usuario, así que se
   leen de un vistazo y SIEMPRE ofrecen una salida — las de la plantilla eran
   texto suelto en inglés y sin ningún enlace de vuelta.
   ==========================================================================*/

.status-page {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    /* Centrado en el alto disponible sin llegar a ser una pantalla completa:
       el shell (topbar y menú) sigue visible y navegable, que es parte de la
       salida. */
    justify-content: center;
    min-block-size: 55vh;
    padding: 2rem 1.5rem;
    gap: .35rem;
}

.status-page__icon {
    font-size: 2.5rem;
    margin-bottom: .5rem;
    color: color-mix(in srgb, var(--bs-body-color) 45%, transparent);
}

.status-page__icon--danger { color: var(--bs-danger); }

.status-page__title {
    margin: 0;
    font-size: 1.35rem;
    font-weight: 600;
}

.status-page__body {
    margin: 0 0 1.25rem;
    max-inline-size: 32rem;
    /* 85 % del color del cuerpo: el token secundario del tema cae por debajo de
       4.5:1 en oscuro. */
    color: color-mix(in srgb, var(--bs-body-color) 85%, transparent);
}

/* El identificador de la petición: útil para soporte, secundario para el ojo. */
.status-page__reference {
    margin: 0 0 1.25rem;
    font-size: .82rem;
    color: color-mix(in srgb, var(--bs-body-color) 85%, transparent);
}

    .status-page__reference code {
        display: inline-block;
        margin-inline-start: .35rem;
        padding: .15rem .45rem;
        border-radius: 6px;
        background: color-mix(in srgb, var(--bs-body-color) 8%, transparent);
        /* Un identificador se copia a mano o se lee por teléfono: que no baile. */
        font-variant-numeric: tabular-nums;
        word-break: break-all;
    }


/* ============================================================================
   19. INICIO — `ui-home`
   ----------------------------------------------------------------------------
   La superficie de EJECUCIÓN del método (el "engage" de GTD), no un tablero de
   reportes: capturar arriba, elegir qué hacer en la columna principal, y el
   radar —hábitos y señales— al lado.

   RESPONSIVE POR CONTENIDO, NO POR VIEWPORT. `.ui-home` es un CONTENEDOR de
   consulta, así que el reparto responde al ancho REAL disponible: colapsar el
   menú lateral devuelve columnas sin que la ventana cambie de tamaño. Con
   `@media` (lo que esta pantalla tenía antes) el viewport miente — a 1280 px
   con el sidebar abierto el contenido son ~1030, y esa es la lección que
   /tasks ya pagó una vez.

   Y el reparto es POR CONTENIDO, no en columnas iguales: la columna principal
   pide el ancho en el que se lee una acción y el radar se lleva lo que sobre;
   cuando ya no caben lado a lado, el radar ENVUELVE y sus tarjetas se
   reacomodan solas en una rejilla `auto-fit` — sin un solo breakpoint.
   ==========================================================================*/

.ui-home {
    container-type: inline-size;
    container-name: ui-home;
    display: flex;
    flex-direction: column;
    gap: 1rem;
}

/* La captura no se esconde NUNCA, ni en el teléfono: es el primer hábito del
   método y el motivo por el que esta pantalla se abre a media mañana. */
.ui-home__capture { margin: 0; }

.ui-home__grid {
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
    align-items: flex-start;
}

/* La base es el ancho en el que una acción SE LEE (título + contexto +
   estimado); el `grow` alto es lo que hace que se quede con el espacio sobrante
   en vez de repartirlo a partes iguales con el radar. */
.ui-home__main {
    flex: 999 1 30rem;
    min-inline-size: 0;
    display: flex;
    flex-direction: column;
    gap: 1rem;
}

/* Envuelto (debajo), el radar deja de ser una columna y se convierte en una
   rejilla de tarjetas que se reacomoda sola — sin media query. */
.ui-home__side {
    flex: 1 1 19rem;
    min-inline-size: 0;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
    gap: 1rem;
    align-content: start;
}

/* ── Superficies ──────────────────────────────────────────────────────────── */

.ui-home__panel {
    position: relative;
    background: var(--bs-body-bg);
    border: 1px solid color-mix(in srgb, var(--bs-body-color) 14%, transparent);
    border-radius: 16px;
    padding: 1.15rem 1.25rem;
    /* Cada panel es su propia frontera de pintura: repintar una fila al pasar
       el cursor no puede costar el repintado del resto de la pantalla. */
    contain: paint;
}

/* El bloque protagonista se distingue por un filete del acento, no por un
   fondo distinto: teñir la tarjeta entera compite con el color que SÍ
   significa algo (la pastilla de vencido, el rojo de una señal). */
.ui-home__panel--now::before {
    content: "";
    position: absolute;
    inset-block-start: 0;
    inset-inline: 0;
    block-size: 3px;
    border-start-start-radius: 16px;
    border-start-end-radius: 16px;
    background: linear-gradient(90deg,
        var(--bs-primary),
        color-mix(in srgb, var(--bs-primary) 35%, #8b5cf6));
}

.ui-home__panel--tile { padding: 1rem 1.1rem; }

.ui-home__head {
    display: flex;
    align-items: baseline;
    gap: .6rem;
    margin-bottom: .85rem;
}

.ui-home__title {
    margin: 0;
    font-size: .78rem;
    font-weight: 650;
    text-transform: uppercase;
    letter-spacing: .04em;
    /* 85 % del color del cuerpo y no `--bs-secondary-color`: ese token da 3.47:1
       sobre el fondo de la tarjeta en oscuro, y esto es texto pequeño. */
    color: color-mix(in srgb, var(--bs-body-color) 85%, transparent);
}

.ui-home__count {
    font-size: .78rem;
    font-weight: 650;
    font-variant-numeric: tabular-nums;
    color: color-mix(in srgb, var(--bs-body-color) 85%, transparent);
}

.ui-home__link {
    margin-inline-start: auto;
    font-size: .8rem;
    /* El primary PURO da 2.03:1 sobre el fondo oscuro — texto pequeño exige 4.5.
       El token `*-text-emphasis` es el que el tema resuelve claro en oscuro y
       oscuro en claro. */
    color: var(--bs-primary-text-emphasis);
    text-decoration: none;
    white-space: nowrap;
}

.ui-home__link:hover { text-decoration: underline; }

.ui-home__empty,
.ui-home__body {
    margin: 0 0 .85rem;
    font-size: .87rem;
    color: color-mix(in srgb, var(--bs-body-color) 85%, transparent);
}

/* ── Los lentes ───────────────────────────────────────────────────────────── */

.ui-home__lenses {
    display: flex;
    flex-wrap: wrap;
    gap: .85rem 1.4rem;
    margin-bottom: 1rem;
    padding-bottom: .9rem;
    border-bottom: 1px solid color-mix(in srgb, var(--bs-body-color) 10%, transparent);
}

.ui-home__lens {
    display: flex;
    flex-direction: column;
    gap: .3rem;
    min-inline-size: 0;
}

.ui-home__chips {
    display: flex;
    flex-wrap: wrap;
    gap: .3rem;
}

/* Una pastilla de lente. `--ui-home-accent` la trae el markup cuando la
   etiqueta tiene color propio; sin ella, el tinte es el del acento del tema. */
.ui-home__chip {
    --ui-home-accent: var(--bs-primary);
    display: inline-flex;
    align-items: center;
    /* 32 px de alto: por debajo de eso no es un objetivo para el dedo. */
    min-block-size: 32px;
    padding: .3rem .7rem;
    border: 1px solid color-mix(in srgb, var(--bs-body-color) 14%, transparent);
    border-radius: 999px;
    background-color: color-mix(in srgb, var(--bs-body-color) 4%, transparent);
    color: color-mix(in srgb, var(--bs-body-color) 88%, transparent);
    font-size: .8rem;
    font-weight: 550;
    line-height: 1;
    white-space: nowrap;
    cursor: pointer;
    transition: background-color .12s ease, border-color .12s ease;
}

.ui-home__chip:hover {
    background-color: color-mix(in srgb, var(--bs-body-color) 9%, transparent);
}

.ui-home__chip.is-on {
    border-color: color-mix(in srgb, var(--ui-home-accent) 55%, transparent);
    background-color: color-mix(in srgb, var(--ui-home-accent) 14%, transparent);
    color: var(--bs-body-color);
    font-weight: 650;
}

/* El foco tiene que VERSE sobre estos fondos: el anillo por defecto de
   Bootstrap se pierde contra el tinte del chip encendido. */
.ui-home__chip:focus-visible {
    outline: 2px solid var(--bs-primary);
    outline-offset: 2px;
}

/* ── Las acciones ─────────────────────────────────────────────────────────── */

.ui-home__tasks,
.ui-home__signals {
    margin: 0;
    padding: 0;
    list-style: none;
}

.ui-home__task-row {
    display: flex;
    align-items: center;
    gap: .6rem;
    padding: .55rem 0;
    border-bottom: 1px solid color-mix(in srgb, var(--bs-body-color) 8%, transparent);
}

.ui-home__task-row:last-child { border-bottom: 0; }

/* Sale hacia la DERECHA al completarse: es el mismo vocabulario de dirección
   del resto de la app (derecha = se despacha). */
.ui-home__task-row.is-going {
    opacity: .35;
    transform: translateX(12px);
    transition: opacity .18s ease, transform .18s ease;
}

@media (prefers-reduced-motion: reduce) {
    .ui-home__task-row.is-going { transform: none; }
}

/* El chulo de completar: círculo vacío que se llena al apuntarlo — el mismo
   vocabulario que la lista de tareas. 28 px de dibujo, pero el objetivo real
   llega al alto de la fila con el pseudo-elemento. */
/* EL BOTÓN ES EL ÁREA y el ::after es el dibujo: estirándolo al alto de su
   fila (`align-self: stretch`) el objetivo llega a los 44 px de la guía táctil
   sea cual sea el alto de la fila, y sin invadir a la vecina — que es lo que sí
   hacía una franja con un inset fijo cuando la fila era baja. */
.ui-home__check {
    display: flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    align-self: stretch;
    inline-size: 26px;
    padding: 0;
    border: 0;
    background: transparent;
    cursor: pointer;
}

.ui-home__check::after {
    content: "";
    inline-size: 20px;
    block-size: 20px;
    border: 2px solid color-mix(in srgb, var(--bs-body-color) 32%, transparent);
    border-radius: 50%;
    transition: border-color .12s ease, background-color .12s ease;
}

.ui-home__check:hover::after {
    border-color: var(--bs-success);
    background-color: color-mix(in srgb, var(--bs-success) 18%, transparent);
}

.ui-home__check:focus-visible {
    outline: 2px solid var(--bs-primary);
    outline-offset: -2px;
    border-radius: 8px;
}

.ui-home__task {
    flex: 1 1 auto;
    min-inline-size: 0;
    color: var(--bs-body-color);
    text-decoration: none;
    /* El objetivo llega al ALTO COMPLETO de su fila: el relleno lo estira y el
       margen negativo lo devuelve a su sitio, así que el reparto no cambia. */
    padding-block: .55rem;
    margin-block: -.55rem;
}

.ui-home__task-title {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: .92rem;
    font-weight: 550;
}

.ui-home__task-meta {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: .3rem .5rem;
    margin-top: .15rem;
    font-size: .76rem;
    color: color-mix(in srgb, var(--bs-body-color) 85%, transparent);
}

.ui-home__task-meta:empty { display: none; }

.ui-home__task-project {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-inline-size: 14rem;
}

/* El contexto se pinta del color de su etiqueta: es lo que permite reconocerlo
   sin leerlo cuando ya se conoce el sistema propio. */
.ui-home__task-tag {
    --ui-home-accent: var(--bs-primary);
    padding: .05rem .4rem;
    border-radius: 999px;
    background-color: color-mix(in srgb, var(--ui-home-accent) 12%, transparent);
    color: var(--bs-body-color);
    font-weight: 550;
    white-space: nowrap;
}

.ui-home__task-est {
    display: inline-flex;
    align-items: center;
    gap: .2rem;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.ui-home__task-tail {
    display: flex;
    align-items: center;
    gap: .5rem;
    flex: 0 0 auto;
}

.ui-home__flag {
    flex: 0 0 auto;
    font-size: .75rem;
    /* El danger PURO no llega a 3:1 como elemento gráfico sobre el fondo de la
       tarjeta; el token de énfasis sí, y sigue leyéndose rojo. */
    color: var(--bs-danger-text-emphasis);
}

/* ── El radar ─────────────────────────────────────────────────────────────── */

.ui-home__figure {
    margin: 0 0 .1rem;
    font-size: 2rem;
    font-weight: 700;
    line-height: 1.1;
    font-variant-numeric: tabular-nums;
}

/* Bandeja vacía: el verde dice "esto está resuelto" sin gastar un número. */
.ui-home__figure--calm {
    font-size: 1.5rem;
    color: var(--bs-success-text-emphasis);
}

.ui-home__signals li {
    display: flex;
    align-items: center;
    gap: .5rem;
    padding: .45rem 0;
    border-bottom: 1px solid color-mix(in srgb, var(--bs-body-color) 8%, transparent);
}

.ui-home__signals li:last-child { border-bottom: 0; }

.ui-home__signal {
    flex: 1 1 auto;
    min-inline-size: 0;
    color: var(--bs-body-color);
    text-decoration: none;
    padding-block: .45rem;
    margin-block: -.45rem;
}

.ui-home__signal-name {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: .87rem;
}

/* ── La semana ────────────────────────────────────────────────────────────── */

.ui-home__week {
    margin: 0 0 .5rem;
    font-size: 1.05rem;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}

.ui-home__bar {
    block-size: 8px;
    margin-bottom: .85rem;
    border-radius: 999px;
    background: color-mix(in srgb, var(--bs-body-color) 8%, transparent);
    overflow: hidden;
}

.ui-home__bar-fill {
    display: block;
    block-size: 100%;
    border-radius: inherit;
    background: var(--bs-primary);
}

/* ── Ajustes por ANCHO DE CONTENIDO ───────────────────────────────────────── */

/* Angosto de verdad (teléfono, o el panel del inicio dentro de poco espacio):
   los lentes se apilan a ancho completo y la meta de una acción deja de
   competir por el renglón. */
@container ui-home (max-width: 34rem) {
    .ui-home__panel { padding: .95rem 1rem; }

    .ui-home__lenses {
        flex-direction: column;
        gap: .7rem;
    }

    .ui-home__lens { inline-size: 100%; }

    /* Los chips scrollean en su propia línea antes que envolver en cuatro
       renglones y empujar la lista fuera de la primera pantalla. */
    .ui-home__chips {
        flex-wrap: nowrap;
        overflow-x: auto;
        padding-bottom: .2rem;
        scrollbar-width: none;
    }

    .ui-home__chips::-webkit-scrollbar { display: none; }

    .ui-home__task-project { max-inline-size: 9rem; }

    /* La fila envuelve y el TÍTULO se queda con el renglón: es lo que se lee
       para decidir, y con la cola al lado bajaba a 156 px. La base descuenta el
       chulo para que el título no se vaya solo a la segunda línea. */
    .ui-home__task-row { flex-wrap: wrap; }

    .ui-home__task { flex: 1 1 calc(100% - 1.7rem); }

    /* Sangrada bajo el título, no pegada al borde: se lee como parte de su
       fila y no como una fila nueva. */
    .ui-home__task-tail { margin-inline-start: calc(26px + .6rem); }
}


/* ============================================================================
   20. ESPACIOS DE TRABAJO — `ui-tenants`
   ----------------------------------------------------------------------------
   El tablero del dueño del producto. Se apoya en `ui-list` (que ya trae filas,
   separadores y estados) y solo añade el reparto de la fila: qué es a la
   izquierda, la señal en el medio, cuándo se usó por última vez a la derecha.
   ==========================================================================*/

.ui-tenants__summary {
    margin-bottom: .85rem;
    font-size: .88rem;
    color: var(--bs-danger-text-emphasis);
}

    /* El resumen tiene que hablar el MISMO idioma que sus pastillas: en rojo
       sobre una lista de pastillas ámbar se lee como si fueran dos cosas
       distintas. Lo usa el listado de personas, donde lo que se cuenta son
       altas a medias y no dinero perdido. */
    .ui-tenants__summary--warn { color: var(--bs-warning-text-emphasis); }

.ui-tenants__row {
    display: flex;
    align-items: center;
    gap: .85rem;
    inline-size: 100%;
    text-align: start;
}

.ui-tenants__main {
    /* Se queda con el espacio libre y recorta: la señal y la fecha nunca se
       salen de la tarjeta por muy largo que sea el nombre. */
    flex: 1 1 auto;
    min-inline-size: 0;
}

.ui-tenants__name,
.ui-tenants__meta {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.ui-tenants__name {
    font-size: .92rem;
    font-weight: 500;
    color: var(--bs-body-color);
}

.ui-tenants__meta {
    font-size: .8rem;
    /* 85 % y no 62: a 12.8 px esto es texto pequeño y exige 4.5:1. El 62 % daba
       3.88 en claro; el 75 % lo arreglaba en claro (5.7) y seguía en 4.1 en
       OSCURO — los porcentajes de `color-mix` no se comportan igual en los dos
       temas, hay que medir ambos. */
    color: color-mix(in srgb, var(--bs-body-color) 85%, transparent);
}

/* El color propio de la fila (el de un producto de software, p. ej.): un punto
   antes del nombre. Va con `background` INLINE porque el color es un dato de la
   fila, no un token del tema. No se recorta con el nombre (`flex: 0 0 auto`):
   el ellipsis se lo lleva el texto, nunca la señal de color. */
.ui-tenants__dot {
    display: inline-block;
    flex: 0 0 auto;
    inline-size: .6rem;
    block-size: .6rem;
    border-radius: 50%;
    margin-inline-end: .45rem;
    vertical-align: baseline;
}

/* La lista DENTRO de un bloque que ya pone su propia superficie y su relleno
   (la de dominios en la cabina del producto): sin marco ni fondo propios, para
   no dibujar una caja dentro de otra. */
.ui-tenants--flush {
    background: none;
    border: 0;
    border-radius: 0;
    box-shadow: none;
}

.ui-tenants--flush > .ui-list__item {
    padding-inline: 0;
}

/* Un control que tiene que quedar POR ENCIMA del enlace estirado de su fila.
   z-index 2 y no 1: el ::after del `stretched-link` ya usa 1, y empatar deja
   ganar al que se pinta después — el control se vería perfecto y sería
   inalcanzable (medido con elementFromPoint en las cards del mapa). */
.ui-tenants__over {
    position: relative;
    z-index: 2;
}

.ui-tenants__tools {
    display: inline-flex;
    flex: 0 0 auto;
    gap: .35rem;
}

/* Los controles no ceden ancho NI lo heredan de su glifo: medido, el botón de
   eliminar quedaba en 18 px (su icono es más angosto que el del lápiz), por
   debajo del mínimo táctil — y es el destructivo. Un objetivo se dimensiona
   para el dedo, no para el ancho que le toque al icono. */
.ui-tenants__tools > * {
    flex: 0 0 auto;
    min-inline-size: 1.75rem;
}

.ui-tenants__when {
    flex: 0 0 auto;
    font-size: .8rem;
    font-variant-numeric: tabular-nums;
    color: color-mix(in srgb, var(--bs-body-color) 85%, transparent);
}

/* En el teléfono la fecha sobra: lo que hay que leer es el nombre y su señal. */
@media (max-width: 576px) {
    .ui-tenants__when { display: none; }

    /* Y con ella el tipo de cambio del historial de versiones: es apoyo, y en
       390 px compite por el sitio con la marca de "vigente", que es lo que uno
       viene a buscar. El tipo sigue en el detalle. */
    .ui-versions__type { display: none; }
}


/* ── 21. User story map ──────────────────────────────────────────────────────
   El mapa de historias de un dominio (/software/{producto}/domains/{dominio}):
   columnas = funcionalidades agrupadas por MÓDULO (cabecera sticky), bandas
   horizontales por RELEASE + "Sin planear" al final, cards apiladas por celda.

   Decisiones que no son estéticas:
   · El mapa es SU PROPIO contenedor de scroll 2D — el contenido ancho desplaza
     dentro de su `overflow`, el body nunca (regla del proyecto).
   · `isolation: isolate` y NUNCA `position: relative; z-index: N` en el
     contenedor: los sticky internos llevan z-index y sin el aislamiento
     pisarían el topbar del shell, que vive en z-index 1 (regla permanente,
     dos incidentes documentados).
   · Las cabeceras (módulos + funcionalidades) son sticky DENTRO del scroll,
     con ALTURAS FIJAS por variable: el `top` de la segunda fila depende de la
     altura de la primera, y una altura por contenido las descuadraría.
   · El ancho de columna es FIJO (`--ui-storymap-col`): en una rejilla el
     tamaño lo define la rejilla, no el texto que le toque a cada celda
     (regla del editor de aclarar).
   · El filete superior de 2px codifica el TIPO de card, no su estado
     (patrón Avion): índigo = funcionalidad, magenta = historia. El ESTADO
     vive SOLO en su badge, que es sólido y salta más que un filete; con el
     filete haciendo de estado, funcionalidad e historia se leían como la
     misma cosa y había que fijarse en el contenido para distinguirlas.
   · El COLOR opcional de la historia es un tinte del fondo, nunca del filete:
     mezclarlos haría ilegible al uno o al otro.
*/
/* LA TIPOGRAFÍA DEL MAPA — la misma que Avion (Assistant, licencia OFL),
   servida en LOCAL: el repo no carga ni una webfont de texto desde un CDN y
   esta no va a ser la primera. Es una fuente VARIABLE, así que un solo
   archivo por subconjunto cubre 400/600/700 (33 KB los dos).

   Por qué hacía falta: el tema declara `"IBM Plex Sans"` en
   `--bs-font-sans-serif` pero NO la carga, así que el mapa venía cayendo a la
   fuente del sistema — distinta en cada máquina y distinta de la referencia.
   Se aplica SOLO al mapa; subirla a toda la app es cambiar esa variable. */
@font-face {
    font-family: "Assistant";
    font-style: normal;
    font-weight: 200 800;
    font-display: swap;
    src: url("../fonts/assistant-latin.woff2") format("woff2");
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
                   U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
                   U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
    font-family: "Assistant";
    font-style: normal;
    font-weight: 200 800;
    font-display: swap;
    src: url("../fonts/assistant-latin-ext.woff2") format("woff2");
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
                   U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF,
                   U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

.ui-storymap {
    font-family: "Assistant", var(--bs-font-sans-serif);

    /* LA CARD MIDE SIEMPRE LO MISMO (190px, Avion) y la COLUMNA se deriva de
       ella más su aire. Son dos medidas distintas a propósito: la última
       columna se estira para que la banda de release llegue hasta el borde,
       pero sus cards NO — una card de mil píxeles no es una card. */
    --ui-storymap-card:   190px;
    --ui-storymap-col:    calc(var(--ui-storymap-card) + 2 * var(--ui-storymap-gap));
    /* Gobierna DOS cosas: el hueco entre cards y el aire hasta el borde de la
       celda. Subido de 10 a 14 px a pedido de Randolf — con 10 la flecha entre
       dos cards apiladas medía justo eso y quedaba casi toda punta. El JS lee
       el `row-gap` para dimensionar el hueco del arrastre, así que se adapta
       solo. */
    --ui-storymap-gap:    14px;
    --ui-storymap-mod-h:  38px;
    /* La funcionalidad es una CARD, no un rótulo: su fila mide la card más su
       aire. Sigue siendo una altura FIJA — el `top` de esta fila sticky
       depende de la anterior y una altura por contenido las descuadraría. */
    --ui-storymap-feat-h: 96px;

    /* El filete que dice QUÉ es cada card. El magenta es fijo (no sale del
       tema) porque distingue un TIPO, no un estado: si cambiara con el tema,
       la misma card se leería como otra cosa en claro y en oscuro. */
    --ui-storymap-accent-feature: var(--bs-primary);
    --ui-storymap-accent-story:   #e82e72;

    /* LA PALETA DE LAS FLECHAS VIVE AQUÍ, no en el JS, porque DEPENDE DEL
       TEMA: medida en banco, una paleta fija dejaba casi todos los colores
       bajo el 3:1 que exige un elemento gráfico en oscuro (hasta 1.81:1). El
       JS lee estas variables; el tema manda. El rojo del conflicto está
       aparte porque lo leen dos sitios (el trazo y la punta que el JS pinta a
       mano — `context-stroke` de SVG no es universal). */
    --ui-storymap-dep-1: #4338ca;
    --ui-storymap-dep-2: #15803d;
    --ui-storymap-dep-3: #7e22ce;
    --ui-storymap-dep-4: #0b5ed7;
    --ui-storymap-dep-5: #b45309;
    --ui-storymap-dep-6: #0e7490;
    --ui-storymap-dep-conflict: color-mix(in srgb, var(--bs-danger) 78%, black);

    isolation: isolate;
    /* Ancla del overlay de flechas — SIN z-index: el `isolation` de arriba ya
       contiene el apilado, y un z-index aquí pisaría el topbar (la regla que
       este módulo tiene documentada). */
    position: relative;
    overflow: auto;
    max-height: calc(100dvh - 220px);   /* deja aire para header y footer del shell */
    border: 1px solid var(--bs-border-color);
    border-radius: 12px;
    /* El área de trabajo es GRIS y las cards blancas flotan encima (Avion);
       el backbone se pinta del color del cuerpo, y ese contraste es lo que
       separa "la estructura" de "el trabajo" sin una sola línea extra. */
    background: var(--bs-tertiary-bg);
}

.ui-storymap__grid {
    display: grid;
    /* Las columnas las pone el markup — el número solo se conoce con el mapa
       cargado, y la ÚLTIMA es elástica (ver RebuildLayout). */
    gap: 0;
    /* `width: 100%` hace que el mapa llene el ancho disponible cuando las
       columnas no llegan al borde (la banda de release terminaba a media
       pantalla y dejaba una franja muerta). El `min-width` que lo desborda
       cuando sobran columnas lo pone el MARKUP en píxeles (N × ancho de
       columna) y NO es `max-content`: con max-content, la última pista —que
       es elástica— se inflaba hasta el ancho de su texto y salía 32 px más
       gorda que sus hermanas (medido: 250 contra 218). */
    width: 100%;
}

/* ── Cabeceras sticky ── */
.ui-storymap__module {
    position: sticky;
    top: 0;
    z-index: 3;
    height: var(--ui-storymap-mod-h);
    display: flex;
    align-items: center;
    padding: 0 var(--ui-storymap-gap);
    background: var(--bs-body-bg);
    /* SIN filete inferior: el backbone (módulos + funcionalidades) es una sola
       superficie blanca y quien la cierra es la fila de funcionalidades. */
    font-weight: 600;
    font-size: 1.05rem;
    letter-spacing: -.02em;
    color: color-mix(in srgb, var(--bs-body-color) 78%, transparent);
    white-space: nowrap;
    overflow: hidden;
}

/* EL CONTENIDO NO SE ESTIRA AUNQUE LA COLUMNA SÍ. La última pista del grid es
   elástica (`minmax(col, 1fr)`) para que la banda de release llegue al borde;
   sin este tope, el "+" del último módulo —anclado a la derecha— aterrizaba en
   el extremo de la pantalla, a media pantalla de las cards que agrega. El ancho
   lógico son sus columnas menos el relleno lateral de la cabecera, con lo que
   el "+" cae exactamente sobre el borde derecho de la última card. */
.ui-storymap__module-inner {
    display: flex;
    align-items: center;
    gap: .5rem;
    inline-size: 100%;
    max-inline-size: calc(
        var(--ui-storymap-span, 1) * var(--ui-storymap-col) - 2 * var(--ui-storymap-gap));
    min-inline-size: 0;
    overflow: hidden;
}

/* El filete del acento que identifica al módulo (patrón Avion). */
.ui-storymap__module-inner::before {
    content: "";
    flex: 0 0 3px;
    height: 1.1rem;
    border-radius: 2px;
    background: var(--ui-storymap-accent-feature);
}

/* El número del módulo ("2.") va del mismo color que su nombre — es parte del
   nombre, no un adorno: es como se le llama a la columna al hablar de ella. */
.ui-storymap__module-num {
    font-variant-numeric: tabular-nums;
}

.ui-storymap__module-name {
    overflow: hidden;
    text-overflow: ellipsis;
}

/* La celda de la funcionalidad: sticky bajo los módulos, con la CARD dentro
   y su aire debajo. Cierra el backbone con el único filete horizontal de la
   zona — de ahí para abajo empiezan las bandas. */
.ui-storymap__feature {
    position: sticky;
    top: var(--ui-storymap-mod-h);
    z-index: 3;
    height: var(--ui-storymap-feat-h);
    display: flex;
    align-items: stretch;
    padding: 0 var(--ui-storymap-gap) var(--ui-storymap-gap);
    background: var(--bs-body-bg);
    border-bottom: 1px solid var(--bs-border-color);
}

.ui-storymap__feature-card {
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: .25rem;
}

.ui-storymap__feature-top {
    display: flex;
    align-items: center;
    gap: .25rem;
}

/* Las flechas de reordenar se van al extremo, lejos del número. */
.ui-storymap__feature-tools {
    display: inline-flex;
    align-items: center;
    margin-inline-start: auto;
}

/* El título ABRE la funcionalidad y se estira sobre toda la card
   (`stretched-link`): un solo elemento enfocable, con nombre accesible de
   verdad — el patrón de ProjectCard. Resets de botón porque lo es. */
.ui-storymap__feature-name {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    padding: 0;
    border: 0;
    background: transparent;
    text-align: start;
    color: inherit;
    font-size: .9rem;
    font-weight: 600;
    line-height: 1.3;
    letter-spacing: -.01em;
    cursor: pointer;
}

/* Las flechas de reordenar se elevan SOBRE el enlace estirado, o serían
   INALCANZABLES (el `stretched-link` cubre la card entera).
   z-index 2 y NO 1: el `::after` de Bootstrap ya usa 1, así que con 1 empatan
   y gana el que se pinta después — el enlace. Medido con elementFromPoint,
   que es lo único que lo delata: a ojo la flecha se ve perfectamente. */
.ui-storymap__feature-tools,
.ui-storymap__feature-card .ui-storymap__num {
    position: relative;
    z-index: 2;
}

.ui-storymap__feature-name:focus-visible,
.ui-storymap__module-name:focus-visible {
    outline: 2px solid var(--bs-primary);
    outline-offset: 2px;
}

/* El nombre del módulo también abre su detalle. */
.ui-storymap__module-name {
    padding: 0;
    border: 0;
    background: transparent;
    color: inherit;
    font: inherit;
    text-align: start;
    cursor: pointer;
}

/* El nombre del release también abre su detalle. */
button.ui-storymap__band-name {
    padding: 0;
    border: 0;
    background: transparent;
    color: inherit;
    font: inherit;
    text-align: start;
    cursor: pointer;
}

button.ui-storymap__band-name:focus-visible {
    outline: 2px solid var(--bs-primary);
    outline-offset: 2px;
}

.ui-storymap__module-name:hover,
.ui-storymap__feature-name:hover,
button.ui-storymap__band-name:hover {
    text-decoration: underline;
    text-underline-offset: 2px;
}

/* El módulo SIN funcionalidades no finge una card: dice que está vacío y
   ofrece el "+" de su cabecera. Una card de mentira invitaría a clicarla. */
.ui-storymap__feature-empty {
    align-self: center;
    font-size: .84rem;
    color: color-mix(in srgb, var(--bs-body-color) 60%, transparent);
}

/* EL NÚMERO JERÁRQUICO ("2.3"): identifica la columna al hablar de ella, así
   que se lee sin competir con el título — pastilla tenue, cifras tabulares. */
.ui-storymap__num {
    flex-shrink: 0;
    padding: 0 .25rem;
    border-radius: 3px;
    background: color-mix(in srgb, var(--bs-body-color) 10%, transparent);
    color: color-mix(in srgb, var(--bs-body-color) 78%, transparent);
    font-size: .64rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    letter-spacing: 0;
}

/* Separador vertical sutil entre MÓDULOS (no entre funcionalidades): la
   agrupación se lee sin contar columnas. */
.ui-storymap__module,
.ui-storymap__feature,
.ui-storymap__cell {
    border-inline-end: 1px solid transparent;
}

.ui-storymap__module.is-module-end,
.ui-storymap__feature.is-module-end,
.ui-storymap__cell.is-module-end {
    border-inline-end-color: var(--bs-border-color);
}

/* ── Banda de release ── */
.ui-storymap__band {
    grid-column: 1 / -1;
    /* 2px como Avion: la banda separa dos zonas del mapa, no dos filas de una
       tabla — con 1px se leía como una fila más. */
    border-bottom: 2px solid var(--bs-border-color);
}

/* La fila ocupa el ancho del CONTENIDO (es grid-column 1/-1), y sus dos
   extremos se anclan al área VISIBLE con sticky: el nombre a la izquierda y
   las fechas a la derecha. Sin esto, al desplazar el mapa a la derecha se
   perdía el nombre del release y las fechas no se alcanzaban nunca. Es CSS
   puro a propósito — el JS del mapa solo se inicializa con permiso de
   gestión, así que atarlo a él dejaría la banda rota en solo lectura. */
.ui-storymap__band-inner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1.5rem;
    padding: .4rem var(--ui-storymap-gap);
    white-space: nowrap;
}

.ui-storymap__band-lead {
    position: sticky;
    left: 0;
    display: inline-flex;
    align-items: center;
    gap: .45rem;
    min-width: 0;
}

.ui-storymap__band-dates {
    position: sticky;
    right: 0;
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    flex-shrink: 0;
}

/* Nombre, stats y fechas comparten voz: versalita ancha en mayúsculas, que es
   lo que hace que la banda se lea como un RÓTULO de sección y no como texto
   del mapa. */
.ui-storymap__band-name,
.ui-storymap__band-meta,
.ui-storymap__band-dates {
    font-size: .8rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: -.01em;
}

.ui-storymap__band-name {
    color: color-mix(in srgb, var(--bs-body-color) 88%, transparent);
    overflow: hidden;
    text-overflow: ellipsis;
}

/* 75% y no 68%: sobre el gris de la banda, el 68% daba 4.48:1 — a dos
   centésimas del mínimo de un texto de 12,8 px (medido en el banco). */
.ui-storymap__band-meta,
.ui-storymap__band-dates {
    color: color-mix(in srgb, var(--bs-body-color) 75%, transparent);
}

/* El punto que separa nombre y stats (Avion). Decorativo: aria-hidden. */
.ui-storymap__band-sep {
    color: color-mix(in srgb, var(--bs-body-color) 40%, transparent);
}

/* El chevrón de colapso — botón de verdad, con su foco visible. */
.ui-storymap__collapse {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    border: 0;
    border-radius: 8px;
    background: transparent;
    color: inherit;
}

.ui-storymap__collapse:focus-visible {
    outline: 2px solid var(--bs-primary);
    outline-offset: 1px;
}

.ui-storymap__collapse i {
    transition: transform 160ms cubic-bezier(0.05, 0.7, 0.1, 1);
}

.ui-storymap__collapse.is-collapsed i {
    transform: rotate(-90deg);
}

/* ── Celdas ── */
.ui-storymap__cell {
    display: flex;
    flex-direction: column;
    gap: var(--ui-storymap-gap);
    align-content: start;
    /* `start` y no el `stretch` por defecto: en la última columna —que es
       elástica— el stretch estiraba las cards a todo el ancho sobrante. */
    align-items: start;
    min-height: 72px;
    padding: var(--ui-storymap-gap);
    border-bottom: 1px solid var(--bs-border-color-translucent);
}

/* ── La card ──
   Historia y funcionalidad son LA MISMA card (mismo material, mismo ancho):
   lo único que las distingue es el filete de 2px de su borde superior. Es lo
   que hace que el mapa se lea como una sola pieza y no como dos tablas
   pegadas. */
.ui-storymap__card,
.ui-storymap__feature-card {
    /* Ancho FIJO: lo que se estira es la columna, nunca la card. */
    width: var(--ui-storymap-card);
    flex: 0 0 auto;
    position: relative;
    border: 0;
    border-radius: 5px;
    padding: .5rem .55rem .45rem;
    background: var(--bs-body-bg);
    box-shadow: 0 1px 4px color-mix(in srgb, var(--bs-body-color) 15%, transparent);
    contain: paint;   /* frontera de pintura por card — nada sangra a la vecina */
    font-size: .84rem;
}

/* En OSCURO la sombra no separa (una sombra negra sobre fondo oscuro no se
   ve): el relieve lo da un anillo. Va como `box-shadow` y no como `border`
   para no mover ni un píxel del contenido ni del filete. */
[data-bs-theme="dark"] .ui-storymap__card,
[data-bs-theme="dark"] .ui-storymap__feature-card {
    box-shadow: 0 0 0 1px var(--bs-border-color), 0 1px 4px rgba(0, 0, 0, .45);
}

/* EL FILETE DEL TIPO. Va como pseudo-elemento y no como `border-top` para que
   el contenido no se desplace y el color de la card no lo corte. */
.ui-storymap__card::before,
.ui-storymap__feature-card::before {
    content: "";
    position: absolute;
    inset-block-start: 0;
    inset-inline: 0;
    height: 2px;
}

.ui-storymap__feature-card::before { background: var(--ui-storymap-accent-feature); }
.ui-storymap__card::before         { background: var(--ui-storymap-accent-story); }

/* Tinte del COLOR opcional de la historia, nunca sobre el filete del tipo. */
.ui-storymap__card[style*="--story-color"] {
    background: color-mix(in srgb, var(--story-color) 12%, var(--bs-body-bg));
}

.ui-storymap__card-top {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .5rem;
    margin-bottom: .25rem;
}

.ui-storymap__card-title {
    /* El título ENVUELVE (la card es para leer la historia), con un tope de
       líneas para que una novela no se coma la celda. Mismo cuerpo y peso que
       el de la funcionalidad: en el mapa son la misma voz. */
    display: -webkit-box;
    -webkit-line-clamp: 4;
    -webkit-box-orient: vertical;
    overflow: hidden;
    font-size: .9rem;
    font-weight: 600;
    line-height: 1.35;
    letter-spacing: -.01em;
}

.ui-storymap__card-tasks {
    margin-top: .3rem;
    font-size: .76rem;
    /* 92% y no 85%: sobre una card TINTADA por el color de la historia, el 85%
       daba 4.41:1 en oscuro (los porcentajes de color-mix no rinden igual en
       los dos temas — MEDIR ambos, regla del repo). */
    color: color-mix(in srgb, var(--bs-body-color) 92%, transparent);
}

.ui-storymap__points {
    flex-shrink: 0;
    font-variant-numeric: tabular-nums;
    font-size: .7rem;
    font-weight: 700;
    color: color-mix(in srgb, var(--bs-body-color) 78%, transparent);
}

/* ── El tiempo de la historia (panel de detalle) ──
   Lista compacta de entradas con quién, cuándo y cuánto. */
.ui-story-time__total {
    font-variant-numeric: tabular-nums;
    font-weight: 700;
    font-size: .82rem;
    /* 85% y no menos: es texto pequeño en negrita, así que exige 4.5:1 — el
       token secundario del tema se queda corto en oscuro (regla del repo). */
    color: color-mix(in srgb, var(--bs-body-color) 85%, transparent);
}

.ui-story-time__form {
    margin-block: .5rem .75rem;
    padding: .6rem;
    border-radius: 10px;
    background: color-mix(in srgb, var(--bs-body-color) 4%, transparent);
}

.ui-story-time__entries {
    list-style: none;
    margin: 0;
    padding: 0;
}

.ui-story-time__entries > li {
    display: flex;
    align-items: center;
    gap: .5rem;
    padding-block: .3rem;
    font-size: .8rem;
    border-block-start: 1px solid color-mix(in srgb, var(--bs-body-color) 8%, transparent);
}

.ui-story-time__entries > li:first-child { border-block-start: 0; }

/* El cronómetro abierto se marca por COLOR y no solo por el texto: es el
   único renglón sobre el que hay algo que hacer. */
.ui-story-time__entries > li.is-running .ui-story-time__hours {
    color: var(--bs-danger-text-emphasis);
    font-weight: 700;
}

.ui-story-time__when {
    color: color-mix(in srgb, var(--bs-body-color) 85%, transparent);
    white-space: nowrap;
}

/* Las notas ceden el ancho: son el contexto, no el dato. */
.ui-story-time__notes {
    flex: 1 1 auto;
    min-inline-size: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: color-mix(in srgb, var(--bs-body-color) 75%, transparent);
}

.ui-story-time__hours {
    margin-inline-start: auto;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* Objetivo destructivo: se dimensiona para el dedo (≥28px) y se ve sin hover
   — regla del repo, aprendida con la × de los adjuntos. */
.ui-story-time__remove {
    flex-shrink: 0;
    inline-size: 28px;
    block-size: 28px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 0;
    border-radius: 8px;
    background: transparent;
    color: var(--bs-danger-text-emphasis);
}

.ui-story-time__remove:hover,
.ui-story-time__remove:focus-visible {
    background: var(--bs-danger);
    color: #fff;
}

/* El tiempo dedicado, junto al progreso del checklist en el pie de la card.
   Hereda el color del pie (que ya está medido en los dos temas); solo separa
   su ícono del número. */
.ui-storymap__tracked {
    margin-inline-start: .6rem;
    white-space: nowrap;
}

.ui-storymap__tracked > i { margin-inline-end: .25rem; }

/* Lo que va pegado al borde DERECHO del encabezado de la card: los puntos y,
   al final, el DRI. Existe porque el card-top reparte con `space-between` y
   sin un grupo los puntos se irían al centro al aparecer el avatar. */
.ui-storymap__card-end {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    gap: .35rem;
}

/* ── El DRI ──
   Las iniciales de quien RESPONDE por la historia, arriba a la derecha (patrón
   Avion). Más pequeño que el avatar transversal: la card mide 190 px y el
   círculo comparte renglón con el badge de estado. Decorativo — el clic es de
   la card—, y el nombre completo vive en su `title`. */
.ui-storymap__dri {
    width: 22px;
    height: 22px;
    font-size: .58rem;
    /* Aro del fondo de la card: la despega del tinte de color de la historia
       sin dibujar un borde que compita con el badge. */
    box-shadow: 0 0 0 2px var(--bs-body-bg);
}

/* ── El badge de ESTADO ──
   Desde que el filete dice el TIPO, el badge es lo ÚNICO que dice el estado,
   así que es SÓLIDO y no pastel (patrón Avion): a un vistazo se ve qué está
   hecho sin leer una palabra. Los colores están MEDIDOS contra su fondo en
   los dos temas — el verde vivo de Avion con texto blanco da 2.2:1 y aquí no
   pasaría; el nuestro sale del token del tema, que sí llega. */
.ui-storymap__badge {
    display: inline-flex;
    align-items: center;
    padding: .1rem .35rem;
    border-radius: 100px;
    font-size: .62rem;
    font-weight: 700;
    line-height: 1.35;
    text-transform: uppercase;
    letter-spacing: .02em;
    white-space: nowrap;
}

/* "Sin preparar" es el estado por defecto y el más frecuente: tenue a
   propósito, o el mapa entero gritaría. */
.ui-storymap__badge--notready {
    background: color-mix(in srgb, var(--bs-body-color) 10%, transparent);
    color: color-mix(in srgb, var(--bs-body-color) 80%, transparent);
}

.ui-storymap__badge--ready      { background: var(--bs-primary); color: #fff; }
.ui-storymap__badge--inprogress { background: var(--bs-warning); color: #000; }

/* El verde del tema con texto blanco da 2.62:1 — MEDIDO, y es exactamente el
   defecto que tiene el badge de Avion (2.2:1). Oscurecido al 70% llega a
   4.97:1 y sigue leyéndose igual de verde. */
.ui-storymap__badge--done {
    background: color-mix(in srgb, var(--bs-success) 70%, black);
    color: #fff;
}

/* ── Ajustes de OSCURO ──
   Los porcentajes de `color-mix` NO rinden igual en los dos temas (regla del
   repo, y aquí lo volvió a demostrar el banco): con los valores de claro, en
   oscuro el nombre del módulo daba 4.31, el número 3.67, las stats de la
   banda 4.21 y el badge tenue 3.77 — todos por debajo del 4.5 que exige un
   texto pequeño. Y el filete índigo se quedaba en 2.03 contra el 3:1 de un
   elemento gráfico: sobre una card oscura, un índigo saturado no separa. */
[data-bs-theme="dark"] .ui-storymap {
    --ui-storymap-accent-feature: color-mix(in srgb, var(--bs-primary) 70%, white);
    --ui-storymap-accent-story:   color-mix(in srgb, #e82e72 85%, white);
}

[data-bs-theme="dark"] .ui-storymap__module,
[data-bs-theme="dark"] .ui-storymap__num,
[data-bs-theme="dark"] .ui-storymap__points {
    color: color-mix(in srgb, var(--bs-body-color) 95%, transparent);
}

[data-bs-theme="dark"] .ui-storymap__band-meta,
[data-bs-theme="dark"] .ui-storymap__band-dates {
    color: color-mix(in srgb, var(--bs-body-color) 88%, transparent);
}

[data-bs-theme="dark"] .ui-storymap__badge--notready {
    color: var(--bs-body-color);
}

/* ── Captura por celda ── */
/* La captura vive en TODAS las celdas, así que su caja compite con las cards:
   una celda vacía se veía como un formulario. El campo queda sin borde en
   reposo y lo estrena al apuntarlo o enfocarlo — pero su placeholder SIGUE
   visible y el campo sigue siendo tocable, que es lo que la regla táctil
   exige (Avion lo esconde tras el hover y en iPad eso es una trampa). */
.ui-storymap__capture {
    /* El ancho de una card, no el de la celda: en la columna elástica un
       100% dejaba un campo de captura larguísimo bajo cards de 190. */
    width: var(--ui-storymap-card);
    flex: 0 0 auto;
    border: 1px dashed transparent;
    border-radius: 5px;
    padding: .3rem .5rem;
    font-size: .8rem;
    background: transparent;
    color: var(--bs-body-color);
}

.ui-storymap__capture:hover {
    border-color: color-mix(in srgb, var(--bs-body-color) 25%, transparent);
}

.ui-storymap__capture::placeholder {
    color: color-mix(in srgb, var(--bs-body-color) 58%, transparent);
}

.ui-storymap__capture:focus {
    outline: none;
    border-style: solid;
    border-color: var(--bs-primary);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--bs-primary) 18%, transparent);
    background: var(--bs-body-bg);
}

/* Botones de estructura dentro de cabeceras (+ funcionalidad). */
.ui-storymap__add {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    margin-inline-start: auto;
    border: 0;
    border-radius: 6px;
    background: transparent;
    color: color-mix(in srgb, var(--bs-body-color) 60%, transparent);
    flex-shrink: 0;
}

.ui-storymap__add:hover {
    background: color-mix(in srgb, var(--bs-body-color) 8%, transparent);
    color: var(--bs-body-color);
}

.ui-storymap__add:focus-visible {
    outline: 2px solid var(--bs-primary);
    outline-offset: 1px;
}

/* La card es un BOTÓN (abre su detalle): resets de botón + foco visible —
   los fondos de tarjeta pisan el estilo de foco por defecto (regla del repo). */
button.ui-storymap__card {
    display: block;
    /* El ancho de la card, NO el de la celda: este selector es más específico
       que el del bloque compartido, así que un `100%` de aquí ganaba y
       estiraba la historia a lo ancho de la columna elástica. */
    width: var(--ui-storymap-card);
    text-align: start;
    font: inherit;
    color: inherit;
    cursor: pointer;
}

.ui-storymap__card:focus-visible {
    outline: 2px solid var(--bs-primary);
    outline-offset: 2px;
}

/* ── Reorganizar (fase 4) ── */

/* Las flechas de estructura comparten el material del "+" (siempre visibles:
   revelar por hover sería una trampa en iPad — regla del repo). */
.ui-storymap__reorder {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    border: 0;
    border-radius: 6px;
    background: transparent;
    color: color-mix(in srgb, var(--bs-body-color) 45%, transparent);
    flex-shrink: 0;
    font-size: .7rem;
}

.ui-storymap__reorder:hover {
    background: color-mix(in srgb, var(--bs-body-color) 8%, transparent);
    color: var(--bs-body-color);
}

.ui-storymap__reorder:focus-visible {
    outline: 2px solid var(--bs-primary);
    outline-offset: 1px;
}

/* Con permiso de historias, la card se puede ARRASTRAR: sin `touch-action`
   el primer movimiento del dedo sería scroll del mapa y el gesto no
   empezaría nunca (lección del kanban). */
.ui-storymap--draggable .ui-storymap__card { touch-action: none; cursor: grab; }
.ui-storymap.is-dragging,
.ui-storymap.is-dragging .ui-storymap__card { cursor: grabbing; }

/* El original de la card agarrada se ESCONDE: su sitio lo ocupa el hueco
   vivo que story-map.js mueve con el puntero. */
.ui-storymap__card.is-drag-origin { display: none; }

/* EL HUECO VIVO: mientras se arrastra, las cards se APARTAN con transform
   (compositor — cero layout por frame) y el deslizamiento es la transición.
   El padding de la celda (el único cambio de layout, una vez por cambio de
   índice) anima a juego para que las bandas de abajo no salten. */
.ui-storymap.is-dragging .ui-storymap__cell > * { transition: transform .15s ease; }
.ui-storymap.is-dragging .ui-storymap__cell     { transition: padding-bottom .15s ease; }

@media (prefers-reduced-motion: reduce) {
    .ui-storymap.is-dragging .ui-storymap__cell > *,
    .ui-storymap.is-dragging .ui-storymap__cell { transition: none; }
}

/* El fantasma vive en el <body> y se mueve con transform (compositor, cero
   repintado); `pointer-events: none` es lo que deja a elementFromPoint ver
   la celda de debajo. */
.ui-storymap__ghost {
    position: fixed;
    inset: 0 auto auto 0;
    margin: 0;
    z-index: 1080;
    pointer-events: none;
    opacity: .92;
    box-shadow: 0 10px 24px rgba(0, 0, 0, .22);
}

.ui-storymap__cell.is-drop {
    background: color-mix(in srgb, var(--bs-primary) 7%, transparent);
}

/* ── Flechas de dependencia (fase 5) ── */

/* El overlay vive DENTRO del contenedor de scroll y mide lo que mide el
   CONTENIDO, así las flechas viajan con el mapa al desplazar sin re-medir.
   `pointer-events: none` es lo que deja seguir clicando las cards de debajo. */
/* LAS FLECHAS VAN POR ENCIMA (como en Avion): detrás de las cards se perdían
   —solo asomaban en los canales— y una dependencia entre cards apiladas era
   invisible. Son finas y de color, así que no compiten con el texto, y
   `pointer-events: none` deja seguir clicando las cards de debajo. El z-index
   vive dentro del `isolation` del mapa: nunca pisa el topbar. */
.ui-storymap__deps {
    position: absolute;
    inset-block-start: 0;
    inset-inline-start: 0;
    pointer-events: none;
    z-index: 2;            /* sobre las cards (1), bajo la cabecera sticky (3) */
    overflow: visible;
}

.ui-storymap__card { z-index: 1; }

/* El color de cada línea lo pone el JS (paleta estable por dependencia): es
   lo que permite seguir una entre varias que se cruzan. */
.ui-storymap__dep {
    fill: none;
    stroke-width: 1.75;
    stroke-linecap: round;
    opacity: .85;
}

/* EN CONFLICTO = la bloqueadora no está terminada y va DESPUÉS de quien
   depende de ella: el plan promete algo que su prerequisito no va a cumplir.
   Es la única flecha que grita.

   El rojo PURO da 2.96:1 sobre el fondo del mapa — por debajo del 3:1 que
   exige un elemento gráfico (medido en el banco, mismo defecto que ya
   corrigieron las pastillas y los mensajes de validación). Se oscurece en
   claro y se aclara en oscuro; los porcentajes de color-mix no rinden igual
   en los dos temas, así que van medidos aparte. */
.ui-storymap__dep.is-conflicted {
    stroke: var(--ui-storymap-dep-conflict);
    stroke-width: 2.5;
    opacity: 1;            /* la única que no cede protagonismo */
}

/* En oscuro la paleta se INVIERTE a tonos claros: los mismos colores oscuros
   que se leen bien sobre blanco desaparecen sobre el fondo del tema. */
[data-bs-theme="dark"] .ui-storymap {
    --ui-storymap-dep-1: #a5b4fc;
    --ui-storymap-dep-2: #6ee7b7;
    --ui-storymap-dep-3: #d8b4fe;
    --ui-storymap-dep-4: #7dd3fc;
    --ui-storymap-dep-5: #fdba74;
    --ui-storymap-dep-6: #67e8f9;
    --ui-storymap-dep-conflict: color-mix(in srgb, var(--bs-danger) 85%, white);
}

/* Mientras se arrastra, las flechas apuntarían a posiciones viejas: se
   apagan y vuelven al soltar (lo redibuja el propio JS al aterrizar). */
.ui-storymap.is-dragging .ui-storymap__deps { display: none; }

/* ── Detalle de historia (panel sobre el mapa) ── */
.ui-storydetail__md { font-size: .9rem; }

/* Los títulos del Markdown son separadores de sección de la historia, no
   titulares — apenas mayores que el texto (criterio de las notas de tarea). */
.ui-storydetail__md :is(h1, h2, h3, h4) {
    font-size: 1rem;
    font-weight: 650;
    margin: .6rem 0 .3rem;
}

.ui-storydetail__md p { margin-bottom: .4rem; }

/* El código se lee del color del CUERPO y su caja es lo que lo distingue:
   el tema compila `pre` con un gris FIJO (rgb(33,37,41)) que en oscuro da
   1.29:1 — invisible —, y el rosa de `--bs-code-color` da 3.82:1 en claro.
   Medido en el banco en ambos temas. */
.ui-storydetail__md code {
    color: var(--bs-body-color);
    background: color-mix(in srgb, var(--bs-body-color) 8%, transparent);
    padding: .1em .35em;
    border-radius: 5px;
}

.ui-storydetail__md pre {
    overflow-x: auto;   /* el código ancho desplaza DENTRO de su caja */
    padding: .5rem .6rem;
    border-radius: 8px;
    color: var(--bs-body-color);
    background: color-mix(in srgb, var(--bs-body-color) 6%, transparent);
}

.ui-storydetail__md pre > code { padding: 0; background: none; }

.ui-storydetail__md img { max-width: 100%; }
.ui-storydetail__md :last-child { margin-bottom: 0; }

.ui-storydetail__task {
    display: flex;
    align-items: center;
    gap: .5rem;
    padding: .3rem 0;
    border-bottom: 1px solid var(--bs-border-color-translucent);
}

.ui-storydetail__task label {
    flex: 1 1 auto;
    min-width: 0;
    margin: 0;
}

.ui-storydetail__task-actions {
    display: inline-flex;
    gap: .15rem;
    flex-shrink: 0;
}

.ui-storydetail__dep {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: .5rem;
    padding: .35rem 0;
    border-bottom: 1px solid var(--bs-border-color-translucent);
}

/* El aviso de conflicto de planeación — la señal accionable del detalle. */
.ui-storydetail__conflict {
    font-size: .78rem;
    color: var(--bs-warning-text-emphasis);
}

/* ── 22. Piezas de un producto de software (`ui-domains`) ────────────────────
   Los dominios de la cabina: un MOSAICO, no una lista de texto. Un producto se
   entiende por sus piezas —la app móvil, su API, la base de datos— y una pieza
   se reconoce por su ícono antes que por el nombre que cada equipo le puso.
   Son pocas por producto, así que el espacio de una tarjeta se paga solo. */

.ui-domains {
    display: grid;
    /* `min(20rem, 100%)` de suelo y NO 15rem: medido con el panel ancho, a 15rem
       entraban cuatro columnas de ~260 px y el nombre del dominio se recortaba
       ("Sistema Bio…") aunque hubiera sitio de sobra al lado — una tarjeta que
       lleva ícono, nombre, tipo y dos botones necesita 20rem para leerse. El
       `min(…, 100%)` es lo que impide que ese suelo desborde en el teléfono.

       Y el TECHO de 26rem existe porque con un solo dominio la pista `1fr` lo
       estiraba a 569 px: una tarjeta sola no debe deformarse para llenar la
       fila. Con él mide 416 en cualquier ancho y la rejilla crece por número de
       columnas, que es como se lee una rejilla de tarjetas. */
    grid-template-columns: repeat(auto-fill, minmax(min(20rem, 100%), 26rem));
    gap: .75rem;
}

.ui-domains__card {
    position: relative;
    /* AÍSLA SU APILADO, y esto NO es opcional. Dentro de la tarjeta hay cosas
       elevadas —el `::after` del enlace estirado (z 1) y los controles que
       tienen que quedar por encima de él (z 2)—, y sin un contexto propio esos
       z-index compiten contra TODA la página. El `#page-topbar` del tema es
       `position: fixed; z-index: 1`, así que crea su propio contexto y hacia
       afuera vale 1: cualquier z-index ≥ 1 de contenido posterior lo pisa, y
       con él su menú de notificaciones (que por dentro vale 1000, pero eso solo
       cuenta dentro del topbar). Medido con `elementFromPoint`: el botón de la
       tarjeta ganaba el punto que le pertenecía al desplegable.

       `isolation: isolate` crea el contexto SIN elevar nada — la regla que este
       repo ya fijó para el layout y los takeovers de tareas, aplicada a la
       pieza que de verdad eleva controles: la tarjeta. */
    isolation: isolate;
    display: flex;
    align-items: flex-start;
    gap: .75rem;
    padding: .85rem;
    border: 1px solid var(--bs-border-color);
    border-radius: 12px;
    background: var(--bs-body-bg);
    transition: border-color .15s ease, box-shadow .15s ease;
}

/* El hover solo con puntero FINO: en táctil se queda pegado tras el toque
   (regla ya pagada en la lista de tareas). */
@media (hover: hover) and (pointer: fine) {
    .ui-domains__card:hover {
        border-color: color-mix(in srgb, var(--bs-primary) 45%, var(--bs-border-color));
        box-shadow: 0 2px 10px rgb(0 0 0 / .06);
    }
}

/* Lo que exige mirarse se ve ANTES de leer: filete del color de la alarma. */
.ui-domains__card.is-attention {
    border-color: color-mix(in srgb, var(--bs-danger) 40%, var(--bs-border-color));
    box-shadow: inset 3px 0 0 var(--bs-danger);
}

.ui-domains__icon {
    display: grid;
    place-items: center;
    flex: 0 0 auto;
    inline-size: 2.6rem;
    block-size: 2.6rem;
    border-radius: 10px;
    font-size: 1.1rem;
    /* Tinte del acento por defecto; cada tipo lo pisa abajo con el suyo, para
       que dos piezas distintas no se lean como la misma. */
    color: var(--bs-primary-text-emphasis);
    background: color-mix(in srgb, var(--bs-primary) 12%, transparent);
}

/* El color por tipo: refuerza al ícono sin sustituirlo (el nombre del tipo
   también se escribe — un ícono solo es un acertijo, y un lector de pantalla
   no lo ve). Los tonos salen de los tokens del tema, así que siguen claro y
   oscuro sin overrides. */
.ui-domains__icon--web            { color: var(--bs-primary-text-emphasis);   background: color-mix(in srgb, var(--bs-primary) 12%, transparent); }
.ui-domains__icon--mobile         { color: var(--bs-success-text-emphasis);   background: color-mix(in srgb, var(--bs-success) 14%, transparent); }
.ui-domains__icon--desktop        { color: var(--bs-info-text-emphasis);      background: color-mix(in srgb, var(--bs-info) 14%, transparent); }
.ui-domains__icon--api            { color: var(--bs-warning-text-emphasis);   background: color-mix(in srgb, var(--bs-warning) 16%, transparent); }
.ui-domains__icon--microservice   { color: var(--bs-primary-text-emphasis);   background: color-mix(in srgb, var(--bs-primary) 16%, transparent); }
.ui-domains__icon--database       { color: var(--bs-info-text-emphasis);      background: color-mix(in srgb, var(--bs-info) 18%, transparent); }
.ui-domains__icon--integration    { color: var(--bs-warning-text-emphasis);   background: color-mix(in srgb, var(--bs-warning) 12%, transparent); }
.ui-domains__icon--backgroundjobs { color: var(--bs-secondary-color);         background: color-mix(in srgb, var(--bs-body-color) 8%, transparent); }
.ui-domains__icon--infrastructure { color: var(--bs-secondary-color);         background: color-mix(in srgb, var(--bs-body-color) 10%, transparent); }
.ui-domains__icon--unspecified    { color: var(--bs-secondary-color);         background: color-mix(in srgb, var(--bs-body-color) 6%, transparent); }

.ui-domains__body {
    display: block;
    min-inline-size: 0;              /* sin esto un nombre largo desborda la tarjeta */
    flex: 1 1 auto;
}

.ui-domains__name {
    display: block;
    font-weight: 550;
    font-size: .95rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.ui-domains__kind {
    display: block;
    font-size: .72rem;
    font-weight: 600;
    letter-spacing: .02em;
    text-transform: uppercase;
    /* 85 % del color del cuerpo: a 11.5 px esto es texto pequeño y exige 4.5:1,
       y `--bs-secondary-color` da 4.1 en oscuro (medido). */
    color: color-mix(in srgb, var(--bs-body-color) 85%, transparent);
}

.ui-domains__counts {
    display: block;
    margin-block-start: .2rem;
    font-size: .8rem;
    color: color-mix(in srgb, var(--bs-body-color) 85%, transparent);
}

.ui-domains__signals {
    display: flex;
    flex-wrap: wrap;
    gap: .3rem;
    margin-block-start: .45rem;
}

.ui-domains__tools {
    display: inline-flex;
    flex: 0 0 auto;
    gap: .35rem;
}

/* Igual que en las filas: el objetivo se dimensiona para el dedo, no para el
   ancho que le toque al glifo del icono. */
.ui-domains__tools > * {
    flex: 0 0 auto;
    min-inline-size: 1.75rem;
}

/* ── El selector de tipo del modal ───────────────────────────────────────────
   Con los MISMOS íconos con que se verá en la cabina: elegir de una lista de
   palabras y luego encontrarse un símbolo obliga a aprender la equivalencia
   dos veces. */

.ui-domains__picker {
    display: flex;
    flex-wrap: wrap;
    gap: .4rem;
}

.ui-domains__option {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    padding: .35rem .6rem;
    font-size: .8rem;
    color: var(--bs-body-color);
    background: color-mix(in srgb, var(--bs-body-color) 6%, transparent);
    border: 1px solid transparent;
    border-radius: 999px;
    cursor: pointer;
}

@media (hover: hover) and (pointer: fine) {
    .ui-domains__option:hover {
        background: color-mix(in srgb, var(--bs-body-color) 10%, transparent);
    }
}

.ui-domains__option.is-on {
    color: var(--bs-primary-text-emphasis);
    background: color-mix(in srgb, var(--bs-primary) 14%, transparent);
    border-color: color-mix(in srgb, var(--bs-primary) 45%, transparent);
}

/* En OSCURO el mismo tinte al 14 % subía la luminancia del fondo y la opción
   elegida caía a 4.34 (medido) — a 12.8 px esto es texto pequeño y exige 4.5.
   Con menos tinte, el borde sigue diciendo cuál está elegida. */
[data-bs-theme="dark"] .ui-domains__option.is-on {
    background: color-mix(in srgb, var(--bs-primary) 8%, transparent);
}

.ui-domains__option:focus-visible {
    outline: 2px solid var(--bs-primary);
    outline-offset: 2px;
}

/* ── 23. Diálogos (`ui-dialog`) ──────────────────────────────────────────────
   El chrome de cualquier ventana modal. Reemplaza al markup de Bootstrap que
   cada pantalla repetía: aquí el centrado es real (grid), el velo cierra al
   clicarse y el foco entra al abrir para que Escape llegue.

   NO se llama `ui-modal`: ese prefijo ya lo usa `ui-modal--over-sheet` sobre
   los `.modal` de Bootstrap del módulo de tareas, y compartirlo dejaría dos
   cosas distintas con el mismo nombre. */

.ui-dialog {
    position: fixed;
    inset: 0;
    z-index: 1055;                      /* el del modal de Bootstrap: convive con lo no migrado */
    display: grid;
    place-items: center;                /* centrado de verdad, sin márgenes calculados */
    padding: 1rem;
    background: rgb(0 0 0 / .38);
    /* El desenfoque separa del fondo sin oscurecerlo tanto que la página
       desaparezca — se sigue viendo DÓNDE está uno. */
    backdrop-filter: blur(2px);
    -webkit-backdrop-filter: blur(2px);
    overflow-y: auto;
}

.ui-dialog:focus { outline: none; }     /* el contenedor recibe el foco solo para oír el teclado */

.ui-dialog__panel {
    inline-size: min(28rem, 100%);
    max-block-size: calc(100dvh - 2rem);
    display: flex;
    flex-direction: column;
    background: var(--bs-body-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: 14px;
    box-shadow: 0 18px 48px rgb(0 0 0 / .28);
    overflow: hidden;
}

.ui-dialog__panel--wide { inline-size: min(40rem, 100%); }

/* Entrada corta y una sola vez: `@starting-style` anima al INSERTARSE, sin JS
   ni un segundo render que encienda una clase. */
@starting-style {
    .ui-dialog__panel {
        opacity: 0;
        transform: translateY(8px) scale(.985);
    }
}

.ui-dialog__panel {
    transition: opacity .16s ease, transform .16s cubic-bezier(.05, .7, .1, 1);
}

@media (prefers-reduced-motion: reduce) {
    .ui-dialog__panel { transition: none; }
}

.ui-dialog__head {
    display: flex;
    align-items: flex-start;
    gap: .75rem;
    padding: 1.05rem 1.25rem .75rem;
}

.ui-dialog__title {
    flex: 1 1 auto;
    margin: 0;
    font-size: 1.02rem;
    font-weight: 600;
    color: var(--bs-body-color);
}

.ui-dialog__close {
    flex: 0 0 auto;
    inline-size: 2rem;
    block-size: 2rem;                   /* objetivo para el dedo, no el tamaño del glifo */
    display: grid;
    place-items: center;
    padding: 0;
    border: 0;
    border-radius: 8px;
    background: none;
    /* 85 % del color del cuerpo: `--bs-secondary-color` da 4.1 en oscuro. */
    color: color-mix(in srgb, var(--bs-body-color) 85%, transparent);
    cursor: pointer;
}

@media (hover: hover) and (pointer: fine) {
    .ui-dialog__close:hover {
        background: color-mix(in srgb, var(--bs-body-color) 8%, transparent);
        color: var(--bs-body-color);
    }
}

.ui-dialog__close:focus-visible {
    outline: 2px solid var(--bs-primary);
    outline-offset: 2px;
}

.ui-dialog__body {
    padding: 0 1.25rem 1.1rem;
    overflow-y: auto;                   /* el cuerpo desplaza; cabecera y pie quedan fijos */
    color: var(--bs-body-color);
}

/* Un pie cuyo contenido es condicional (p. ej. "Eliminar" solo con permiso)
   se queda sin nada que mostrar: se esconde en vez de dejar una franja
   con borde y sin botones. */
.ui-dialog__foot:empty { display: none; }

.ui-dialog__foot {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: .5rem;
    padding: .9rem 1.25rem 1.1rem;
    border-block-start: 1px solid var(--bs-border-color);
    background: color-mix(in srgb, var(--bs-body-color) 3%, transparent);
}

/* Sobre la hoja móvil del detalle de tarea, que se apila en 1070 — si no, la
   confirmación se pinta DEBAJO y el usuario toca sin que pase nada visible.
   (Reemplaza a `ui-modal--over-sheet`, que hacía lo mismo sobre los `.modal`
   de Bootstrap.) */
.ui-dialog--over-sheet { z-index: 1075; }

/* Un título que es un control ocupa el ancho y trae su propia tipografía. */
.ui-dialog__title--custom { min-inline-size: 0; font-weight: inherit; font-size: inherit; }


/* ── 24. CHIPS DE DOMINIO ──────────────────────────────────────────────────
   Los dominios de un producto en su fila del portafolio: qué ES el producto
   (una app, su API, su base de datos) de un vistazo. Reemplazan al conteo de
   historias, que no cambiaba ninguna decisión al entrar y costaba recorrer
   todo el historial del tenant.

   Van bajo el nombre y NO dentro de `ui-tenants__name`, que es `nowrap` con
   ellipsis: ahí un chip se recortaría (la trampa ya documentada con la marca
   "Vigente" del historial de versiones). */
.ui-domain-chips {
    display: flex;
    flex-wrap: wrap;
    gap: .3rem;
    margin-top: .25rem;
    /* Sobre el enlace estirado de la fila: el 1 ya lo usa su ::after. */
    position: relative;
    z-index: 2;
    pointer-events: none;          /* decorativos: el clic sigue siendo de la fila */
}

.ui-domain-chips__item {
    display: inline-flex;
    align-items: center;
    gap: .3rem;
    padding: .1rem .45rem;
    border-radius: 6px;
    font-size: .75rem;
    line-height: 1.5;
    white-space: nowrap;
    max-inline-size: 12rem;
    overflow: hidden;
    color: color-mix(in srgb, var(--bs-body-color) 85%, transparent);
    background: color-mix(in srgb, var(--bs-body-color) 6%, transparent);
}

/* El nombre va en su PROPIO elemento y el recorte vive aquí, no en el chip.
   `text-overflow: ellipsis` no alcanza al nodo de texto suelto de un
   `inline-flex` —el motor lo envuelve en una caja anónima— así que el nombre
   largo se cortaba en seco, sin los puntos que avisan de que hay más. Medido
   en el banco. */
.ui-domain-chips__label {
    overflow: hidden;
    text-overflow: ellipsis;
    min-width: 0;
}

.ui-domain-chips__item i {
    font-size: .8em;
    opacity: .85;
}


/* ── 24. Portafolio de tarjetas (`ui-cards`, `ui-card`) ──────────────────────
   La rejilla y el MARCO de una tarjeta de portafolio. Nació en `/projects`
   (donde vivía como `work-project-card`) y subió aquí al aparecer su segundo
   consumidor, `/software`: es la regla del repo — si dos pantallas necesitan el
   mismo marco no se copia, se comparte. Cada módulo añade DENTRO lo suyo (el
   anillo y las señales de un proyecto, los dominios de un producto); lo que
   está aquí es solo la caja.

   TODA LA TARJETA ES CLICABLE SIN SER UN BOTÓN GIGANTE: el NOMBRE es el enlace
   y se estira con `.stretched-link` de Bootstrap. Así hay UN solo elemento
   enfocable por tarjeta —y con nombre accesible de verdad, no un
   `div role=button`— y cualquier control que necesite su propio clic se eleva
   con `z-index: 2` (el 1 ya lo usa el `::after` del enlace estirado: empatar lo
   deja INALCANZABLE, defecto ya pagado en este repo). */

.ui-cards {
    display: grid;
    /* auto-fill + minmax: la rejilla decide cuántas columnas caben, sin media
       queries propias. 20rem es el ancho por debajo del cual el título y el
       contenido de la tarjeta empiezan a truncarse de más. */
    grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
    gap: 1rem;
    align-items: start;
}

.ui-card {
    position: relative;              /* ancla del stretched-link */
    /* AÍSLA SU APILADO, y esto NO es opcional. Dentro de la tarjeta hay cosas
       elevadas —el `::after` del enlace estirado (z 1) y los controles que
       tienen que quedar por encima de él (z 2)—, y sin un contexto propio esos
       z-index compiten contra TODA la página. El `#page-topbar` del tema es
       `position: fixed; z-index: 1`, así que crea su propio contexto y hacia
       afuera vale 1: cualquier z-index ≥ 1 de contenido posterior lo pisa, y
       con él su menú de notificaciones (que por dentro vale 1000, pero eso solo
       cuenta dentro del topbar). Medido con `elementFromPoint`: el botón de la
       tarjeta ganaba el punto que le pertenecía al desplegable.

       `isolation: isolate` crea el contexto SIN elevar nada — la regla que este
       repo ya fijó para el layout y los takeovers de tareas, aplicada a la
       pieza que de verdad eleva controles: la tarjeta. */
    isolation: isolate;
    display: flex;
    flex-direction: column;
    gap: 0.7rem;
    padding: 1rem 1.1rem;
    border: 1px solid var(--bs-border-color);
    /* El color de la entidad es su IDENTIDAD y va donde el ojo entra a la
       tarjeta. `--card-accent` lo escribe el componente por fila. */
    border-inline-start: 4px solid var(--card-accent, var(--bs-primary));
    border-radius: 14px;
    background: var(--bs-body-bg);
    transition: border-color 140ms ease, box-shadow 140ms ease, transform 140ms ease;
}

@media (hover: hover) and (pointer: fine) {
    .ui-card:hover {
        border-color: color-mix(in srgb, var(--bs-primary) 45%, var(--bs-border-color));
        box-shadow: 0 6px 20px -10px color-mix(in srgb, var(--bs-body-color) 45%, transparent);
        transform: translateY(-1px);
    }
}

/* El foco de teclado vive en el enlace estirado, así que el anillo lo pinta la
   TARJETA con :focus-within — si no, el anillo del <a> se dibujaría sobre toda
   la superficie estirada y se vería como un rectángulo suelto. */
.ui-card:focus-within {
    outline: 2px solid var(--bs-primary);
    outline-offset: 2px;
}

.ui-card:focus-within .stretched-link:focus-visible {
    outline: none;
}

@media (prefers-reduced-motion: reduce) {
    .ui-card { transition: none; }
    .ui-card:hover { transform: none; }
}

/* La cabecera: identidad a la izquierda, lo que quepa a la derecha. */
.ui-card__head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 0.75rem;
}

/* min-width:0 es lo que permite que el nombre se recorte: sin él, un hijo de
   flex se niega a encoger por debajo de su contenido y el recorte no ocurre. */
.ui-card__ident {
    min-width: 0;
    flex: 1;
}

.ui-card__title {
    display: block;
    font-size: 1rem;
    font-weight: 650;
    line-height: 1.3;
    color: var(--bs-body-color);
    text-decoration: none;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

@media (hover: hover) and (pointer: fine) {
    .ui-card__title:hover { color: var(--bs-primary); }
}

.ui-card__meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.4rem;
    font-size: 0.8rem;
    /* 85% del color del cuerpo y no `--bs-secondary-color`: ese token da 4.1:1
       en oscuro, por debajo del 4.5 que exige un texto pequeño (medido). */
    color: color-mix(in srgb, var(--bs-body-color) 85%, transparent);
    min-width: 0;
}

.ui-card__meta-item {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-inline-size: 100%;
}

/* Encabezado de GRUPO: se lee como una sección, no como una etiqueta suelta —
   por eso el filete se extiende hasta el final de la fila. */
.ui-cards__group {
    margin-block: 1.75rem 0.85rem;
    display: flex;
    align-items: baseline;
    gap: 0.6rem;
}

.ui-cards__group:first-child {
    margin-block-start: 0;
}

.ui-cards__group-name {
    font-size: 0.95rem;
    font-weight: 650;
    color: var(--bs-body-color);
}

.ui-cards__group-count {
    font-size: 0.78rem;
    color: color-mix(in srgb, var(--bs-body-color) 85%, transparent);
}

.ui-cards__group-rule {
    flex: 1;
    height: 1px;
    background: var(--bs-border-color);
}

/* ESQUELETO de carga con la FORMA de las tarjetas, no una rueda centrada: así
   el layout ya está puesto y lo único que cambia es que se llena. Estático a
   propósito — una animación infinita repinta sin parar, defecto ya corregido
   una vez en este repo. */
.ui-card-skeleton {
    border: 1px solid var(--bs-border-color);
    border-inline-start: 4px solid var(--bs-border-color);
    border-radius: 14px;
    padding: 1rem 1.1rem;
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
}

.ui-card-skeleton__line {
    display: block;
    height: 0.7rem;
    border-radius: 4px;
    background: color-mix(in srgb, var(--bs-body-color) 8%, transparent);
}

.ui-card-skeleton__line--title { height: 1rem; width: 60%; }
.ui-card-skeleton__line--meta  { width: 40%; }
.ui-card-skeleton__line--body  { width: 85%; }

/* La línea de resumen ("5 productos"). Clase propia y no `.text-muted`: esa
   utilidad de Bootstrap resuelve a `--bs-secondary-color`, que mide 4.12 en
   oscuro — texto pequeño exige 4.5— y lleva `!important`, así que no se puede
   corregir sin tocarla globalmente. */
.ui-cards__summary {
    font-size: 0.8rem;
    color: color-mix(in srgb, var(--bs-body-color) 85%, transparent);
}


/* ── 25. Actores / personas (`ui-actor`) ─────────────────────────────────────
   El AVATAR de un actor: círculo de color con su ícono de persona en blanco —
   la lectura de un vistazo de "quién ejecuta esto" (referencia funcional: las
   personas de Avion). El color es un DATO del actor y viaja inline
   (`--actor-color`); el catálogo de íconos vive en Shared (ActorIcons) para
   que todas las superficies pinten lo mismo.

   Tres tamaños: el de gestión (cabina, 32px), el de la franja del mapa (28px)
   y la mini-carita de la card (18px). El glifo blanco sobre el color de la
   paleta curada mide ≥4.6:1 en sus tonos más claros — y es un elemento
   GRÁFICO (3:1), no texto. */

.ui-actor {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    border-radius: 50%;
    background: var(--actor-color, var(--bs-primary));
    color: #fff;
    font-size: .85rem;
    flex-shrink: 0;
}

.ui-actor--sm { width: 28px; height: 28px; font-size: .75rem; }

/* La mini-carita de la CARD: se apilan solapadas (patrón avatar-stack) con un
   aro del color del fondo de la card para separarse entre sí. */
.ui-actor--mini {
    width: 18px;
    height: 18px;
    font-size: .55rem;
    box-shadow: 0 0 0 2px var(--bs-body-bg);
}

/* ── La fila de gestión (cabina): avatar + nombre + presencia + herramientas ─ */
.ui-actors {
    display: flex;
    flex-direction: column;
    gap: .35rem;
}

/* LA FILA ENVUELVE, y eso NO es un detalle estético (reportado por Randolf,
   2026-08-04: "en pantalla grande no se ven los nombres de los actores").
   Mete cuatro cosas en una línea —avatar, nombre, el toggle de presencia y dos
   botones— y el NOMBRE era lo único que podía ceder, porque es lo único con
   `min-width: 0`. Medido con el sidebar puesto: a 1280 de viewport la columna
   queda en 341 px y el nombre en 25 — ilegible.

   El arreglo NO es un breakpoint: `col-lg-4` ya decide por VIEWPORT ignorando
   que el sidebar se lleva 250 px (la trampa que /tasks documentó), así que otro
   umbral heredaría el mismo error. Con `flex-wrap` + un suelo de ancho para el
   nombre, el reparto se resuelve solo EN CUALQUIER ancho: cuando cabe todo va en
   una línea, y cuando no, los controles bajan a la segunda. Sin umbral que
   mantener. */
.ui-actors__row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .6rem;
    padding: .4rem .5rem;
    border: 1px solid var(--bs-border-color);
    border-radius: 10px;
    background: var(--bs-body-bg);
}

.ui-actors__name {
    font-weight: 600;
    font-size: .875rem;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* EL NOMBRE NO SE ENCOGE — esa es toda la regla, y no un umbral. Con
   `flex-shrink` el navegador lo estruja antes que envolver, y así es como
   acababa en 25 px (medido con el sidebar puesto: dos letras y puntos
   suspensivos). Con `0 0 auto` su base es el ancho del texto y no cede: si el
   conmutador y los botones ya no caben al lado, BAJAN ellos.

   Un suelo fijo (probé 10rem y 9rem) no sirve para esto: apila filas que caben
   de sobra —un "Webmaster" quedaba en dos renglones— y aun así recorta los
   nombres que superan el umbral. Sin número que calibrar, el reparto sale bien
   en cualquier ancho: corto → una línea; largo → dos, con el nombre entero.

   `max-width: 100%` es el tope del caso extremo: un nombre kilométrico ocupa su
   línea completa y ahí sí se recorta con ellipsis, en vez de desbordar. */
.ui-actors__grow { flex: 0 0 auto; min-width: 0; max-width: 100%; }

.ui-actors__tools {
    display: inline-flex;
    gap: .25rem;
    flex-shrink: 0;
}

/* ── La franja del mapa: los actores del sistema, clicables como LENTE ──────
   Cada avatar es un <button>: clic = atenuar las historias que ese actor NO
   ejecuta (el criterio lente de /tasks — atenuar, no quitar: quitar movería
   celdas y stats debajo del usuario). */
/* La fila de LENTES del mapa: separa "cómo se ve" (aquí) de "qué se crea"
   (las acciones del header). El display lo ponen las utilidades d-none
   d-md-flex — la fila solo existe donde existe el mapa. */
.ui-storymap-filters {
    align-items: center;
    gap: .75rem;
    flex-wrap: wrap;
    margin-block-end: .75rem;
}

.ui-actor-strip {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: .35rem;
}

.ui-actor-strip__btn {
    border: 2px solid transparent;
    border-radius: 50%;
    padding: 1px;
    background: none;
    line-height: 0;
    cursor: pointer;
}

.ui-actor-strip__btn:focus-visible {
    outline: 2px solid var(--bs-primary);
    outline-offset: 2px;
}

/* El lente activo se marca con un ARO del acento — el avatar no cambia de
   color (su color es identidad, no estado). */
.ui-actor-strip__btn.is-on {
    border-color: var(--bs-primary);
}

/* La card que el lente deja fuera: presente pero atenuada (criterio /tasks). */
.ui-storymap__card.is-lens-out { opacity: .35; }

/* Las mini-caritas dentro de la card, al pie: solapadas hacia la izquierda. */
.ui-actor-stack {
    display: inline-flex;
    padding-inline-start: 4px;
}

.ui-actor-stack .ui-actor--mini { margin-inline-start: -6px; }

/* ── El selector de avatar del modal (rejilla de íconos, patrón del selector
      de tipo de dominio): el elegido lleva el aro del acento. ─────────────── */
.ui-actor-picker {
    display: flex;
    flex-wrap: wrap;
    gap: .4rem;
}

.ui-actor-picker__opt {
    border: 2px solid transparent;
    border-radius: 50%;
    padding: 1px;
    background: none;
    line-height: 0;
    cursor: pointer;
}

.ui-actor-picker__opt.is-on { border-color: var(--bs-primary); }

.ui-actor-picker__opt:focus-visible {
    outline: 2px solid var(--bs-primary);
    outline-offset: 2px;
}

/* ── LA CABINA DE UN PRODUCTO (`ui-cockpit`) ───────────────────────────────
   Dos zonas: la de REFERENCIA (ficha del producto y sus actores) y la de
   CONTENIDO (los dominios, que es a lo que se entra).

   NO usa `col-lg-4 / col-lg-8`, y por una razón medida (reportado por Randolf
   con captura, 2026-08-04: "todo ese espacio y eso tan básico no se hizo"). Las
   columnas de Bootstrap reparten por PORCENTAJE del viewport, así que con el
   sidebar puesto la de referencia caía a ~340 px: la descripción se partía en
   cuatro renglones, el texto de ayuda en seis y la fila del actor se apilaba
   entera — todo ello con media pantalla vacía al lado, porque el bloque de
   dominios se quedaba con 2/3 aunque tuviera UNA tarjeta.

   Aquí el reparto lo decide el CONTENIDO: la referencia pide el ancho en el que
   sus campos y sus filas se leen (`flex-basis`), y el contenido se lleva TODO lo
   que sobre (`flex-grow` desproporcionado). Cuando ya no caben las dos, envuelven
   solas — sin un umbral de viewport que mantener, que es el error que este
   layout tenía y que /tasks ya había documentado. */

.ui-cockpit {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: 1rem;
}

/* El ancho en el que la ficha y las filas de actor se leen de verdad: por
   debajo de 24rem el nombre del actor vuelve a competir con su conmutador, y
   por encima de 32rem la referencia le robaría sitio a lo que de verdad se
   consulta. */
.ui-cockpit__aside {
    flex: 1 1 24rem;
    max-width: 32rem;
    min-width: 0;
}

/* `flex-grow` desproporcionado: mientras compartan renglón, el sobrante entero
   se va a los dominios y la referencia se queda en su base. Al envolver, ambos
   toman el 100 % por su `flex-basis`. */
.ui-cockpit__main {
    flex: 999 1 26rem;
    min-width: 0;
}

/* ── AURA AMBIENTAL DE PANTALLA (`ui-ambient`) ─────────────────────────
   El fondo de la app. Nació en /work/tasks como `work-next-layout` y para
   cuando se mudó aquí ya lo usaban trece pantallas de CUATRO módulos
   (tareas, proyectos, clientes y parametrización): era transversal de facto
   con nombre de módulo, que es justo lo que este archivo existe para evitar.
   Se sube al pedirlo Randolf para el módulo de software (2026-08-04) — mismo
   camino que siguió `ui-card` cuando le apareció el segundo consumidor.

   Lo de abajo es el porqué original, que no cambia: ────────────────────────────────────────
   `isolation` crea el stacking context SIN elevar nada: el aura (::before con
   z-index -1) queda detrás del contenido DENTRO del layout, y el contenido
   conserva z-index auto — jamás puede pintar encima del topbar/sidebar del
   shell (bug reportado por Randolf 2026-07-18: la primera versión elevaba las
   áreas del grid con z-index:1 y al hacer scroll el contenido se superponía).

   El aura es un FONDO DE PANTALLA, no decoración del contenido: `position:
   fixed` la ancla al VIEWPORT, así que no crece ni encoge según cuánto
   contenido haya, y las superficies de vidrio (backdrop-filter) la difuminan
   al pasar por encima durante el scroll — el frosted real. El ::before es
   estático: nada anima. */

.ui-ambient {
    position: relative;
    isolation: isolate;
}

.ui-ambient::before {
    content: "";
    position: fixed;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    background:
        radial-gradient(42rem 26rem at 24% 8%,
            color-mix(in srgb, var(--bs-primary) 10%, transparent), transparent 70%),
        radial-gradient(36rem 22rem at 86% 14%,
            color-mix(in srgb, #8b5cf6 8%, transparent), transparent 70%),
        radial-gradient(32rem 20rem at 52% 44%,
            color-mix(in srgb, #38bdf8 6%, transparent), transparent 72%),
        radial-gradient(26rem 16rem at 78% 60%,
            color-mix(in srgb, #fb7185 5%, transparent), transparent 70%),
        radial-gradient(38rem 24rem at 26% 88%,
            color-mix(in srgb, #8b5cf6 7%, transparent), transparent 70%),
        radial-gradient(32rem 20rem at 84% 92%,
            color-mix(in srgb, var(--bs-primary) 7%, transparent), transparent 70%);
}

[data-bs-theme=dark] .ui-ambient::before {
    background:
        radial-gradient(42rem 26rem at 24% 8%,
            color-mix(in srgb, var(--bs-primary) 14%, transparent), transparent 70%),
        radial-gradient(36rem 22rem at 86% 14%,
            color-mix(in srgb, #8b5cf6 11%, transparent), transparent 70%),
        radial-gradient(32rem 20rem at 52% 44%,
            color-mix(in srgb, #38bdf8 6%, transparent), transparent 72%),
        radial-gradient(26rem 16rem at 78% 60%,
            color-mix(in srgb, #fb7185 6%, transparent), transparent 70%),
        radial-gradient(38rem 24rem at 26% 88%,
            color-mix(in srgb, #8b5cf6 9%, transparent), transparent 70%),
        radial-gradient(32rem 20rem at 84% 92%,
            color-mix(in srgb, var(--bs-primary) 11%, transparent), transparent 70%);
}
