/*
 * app.css — hoja global del proyecto.
 *
 * Implementa el **sistema** de diseño de docs/DISENO.md, y los **valores por
 * defecto** de los tokens (§5.b). La dirección estética la fija el módulo de
 * plantilla activo, que redefine esos tokens en su propia hoja.
 *
 * OJO: un tema se SUPERPONE, no sustituye. Un token de color que el tema olvide
 * redefinir se queda con el valor de aquí y no da ningún error: sale una pantalla
 * mestiza. Lo comprueba tests/e2e/design.spec.js.
 *
 * ORDEN DE SECCIONES OBLIGATORIO (docs/DISENO.md §13):
 *   00 tokens · 01 base/reset · 02 tipografía · 03 layout
 *   04 utilidades · 05 componentes comunes · 06 navegación
 *
 * Dentro de cada sección, los componentes van en orden alfabético: sin build
 * tools, encontrar una regla rápido vale más que agrupar por afinidad.
 *
 * REGLAS: cero colores literales fuera de la sección 00 · cero medidas fuera de
 * la escala · cero !important · ningún estado comunicado solo con color.
 *
 * Presupuesto: ≤18 KB sin comprimir.
 */

/* ═══ 00. TOKENS ══════════════════════════════════════════════════════════════ */

:root {
    color-scheme: light dark;

    /* ── Color: tema claro ────────────────────────────────────────────────── */
    --fondo:               #FAF7F4;
    --superficie:          #FFFFFF;
    --superficie-alta:     #FFFFFF;
    --superficie-hundida:  #F1ECE7;

    --texto:               #1E1B19;
    --texto-suave:         #665D57;
    --texto-inverso:       #FAF7F4;

    --linea:               #E6E0DA;   /* decorativo: 1.31:1, NUNCA borde de control */
    --linea-fuerte:        #8E857D;   /* bordes de control: 3.62:1 sobre superficie */

    --acento:              #6D2E8F;
    --acento-fuerte:       #5B2678;
    --acento-intenso:      #4E2066;
    --acento-texto:        #FFFFFF;
    --acento-suave:        #F4EAF9;
    --acento-borde:        #9C68B8;

    --ok:                  #17663D;
    --ok-suave:            #E4F3EA;
    --error:               #B02418;
    --error-suave:         #FCEAE7;
    --aviso:               #7A5300;
    --aviso-suave:         #FBF0DC;
    --info:                #16558C;
    --info-suave:          #E7F0FA;

    /* Composiciones con alfa: los dos únicos usos legítimos de color literal. */
    --velo-foto:           linear-gradient(to top, rgba(0,0,0,.62), rgba(0,0,0,0) 62%);
    --velo-modal:          rgba(30,27,25,.55);

    /* ── Tipografía ───────────────────────────────────────────────────────── */
    --fuente: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue",
              Arial, "Noto Sans", sans-serif,
              "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji";

    /* Titulares. Por defecto la MISMA pila que el cuerpo, así que un tema que no la
       toque no cambia nada. Existe para que un tema pueda cambiar la tipografía de
       los titulares sin escribir una pila literal dentro de una regla de componente,
       que es el equivalente tipográfico de un color suelto. */
    --fuente-titulo: var(--fuente);

    --fuente-mono: ui-monospace, SFMono-Regular, Menlo, Consolas,
                   "Liberation Mono", monospace;

    --tipo-xs:    0.75rem;
    --tipo-s:     0.8125rem;
    --tipo-sm:    0.875rem;
    --tipo-base:  1rem;
    --tipo-m:     1.125rem;
    --tipo-l:     1.25rem;
    --tipo-xl:    1.5rem;
    --tipo-2xl:   1.875rem;
    --tipo-3xl:   clamp(2rem, 6vw, 2.75rem);

    --interlineado-apretado: 1.15;
    --interlineado-medio:    1.4;
    --interlineado-texto:    1.6;

    /* ── Espaciado (base 4px, ocho valores, ni uno más) ────────────────────── */
    --espacio-1: 0.25rem;
    --espacio-2: 0.5rem;
    --espacio-3: 0.75rem;
    --espacio-4: 1rem;
    --espacio-5: 1.5rem;
    --espacio-6: 2rem;
    --espacio-7: 3rem;
    --espacio-8: 4rem;

    /* ── Radios ───────────────────────────────────────────────────────────── */
    --radio-xs:      6px;
    --radio-s:      10px;
    --radio-m:      14px;
    --radio-l:      20px;
    --radio-xl:     28px;
    --radio-redondo: 999px;

    /* ── Elevación ────────────────────────────────────────────────────────── */
    --sombra-1: 0 1px 2px rgba(30,27,25,.06), 0 1px 1px rgba(30,27,25,.04);
    --sombra-2: 0 4px 12px rgba(30,27,25,.08), 0 1px 3px rgba(30,27,25,.06);
    --sombra-3: 0 12px 32px rgba(30,27,25,.14), 0 2px 8px rgba(30,27,25,.08);

    /* ── Anchos ───────────────────────────────────────────────────────────── */
    --ancho-app:       30rem;
    --ancho-texto:     38rem;
    --ancho-contenido: 62rem;
    --ancho-amplio:    75rem;

    /* ── Alturas y zonas táctiles ─────────────────────────────────────────── */
    --toque-min:      2.75rem;
    --toque-comodo:   3rem;
    --alto-cabecera:  3.5rem;

    /* ── Capas ────────────────────────────────────────────────────────────── */
    --z-cabecera: 20;
    --z-menu:     40;
    --z-aviso:    50;

    /* ── Movimiento ───────────────────────────────────────────────────────── */
    --dur-instantanea: 90ms;
    --dur-rapida:     140ms;
    --dur-media:      220ms;
    --dur-lenta:      320ms;
    --curva-salida:   cubic-bezier(.2, 0, 0, 1);
    --curva-entrada:  cubic-bezier(.4, 0, 1, 1);
    --curva-estandar: cubic-bezier(.2, 0, .2, 1);
    --curva-enfatica: cubic-bezier(.34, 1.28, .64, 1);

    /* ── Breakpoints (referencia; las media queries van a mano) ────────────── */
    --bp-sm: 30rem;
    --bp-md: 48rem;
    --bp-lg: 64rem;
    --bp-xl: 80rem;
}

@media (prefers-color-scheme: dark) {
    :root {
        --fondo:              #151310;
        --superficie:         #1F1B18;
        --superficie-alta:    #282320;
        --superficie-hundida: #100E0C;

        --texto:              #EFEAE5;
        --texto-suave:        #A79E97;
        --texto-inverso:      #1E1B19;

        --linea:              #322C28;
        --linea-fuerte:       #787069;

        --acento:             #D8A9EE;
        --acento-fuerte:      #E4BFF5;
        --acento-intenso:     #C795DD;
        --acento-texto:       #2A1233;
        --acento-suave:       #2E2233;
        --acento-borde:       #8B7299;

        --ok:                 #6FD196;
        --ok-suave:           #16281E;
        --error:              #FF9C8B;
        --error-suave:        #2E1815;
        --aviso:              #E3B663;
        --aviso-suave:        #2B2113;
        --info:               #8FC5F7;
        --info-suave:         #152431;

        --velo-modal:         rgba(0,0,0,.7);

        /* En oscuro la elevación es color, no sombra. */
        --sombra-1: none;
        --sombra-2: 0 4px 14px rgba(0,0,0,.5);
        --sombra-3: 0 16px 40px rgba(0,0,0,.6);
    }
}

/* ═══ 01. BASE / RESET ════════════════════════════════════════════════════════ */

*, *::before, *::after { box-sizing: border-box; }

html {
    -webkit-text-size-adjust: 100%;
    scrollbar-gutter: stable;            /* evita el salto al aparecer el scroll */
}

body {
    margin: 0;
    /* dvh y no vh: con vh el contenido salta al aparecer/desaparecer la barra de
       direcciones del móvil. vh queda como respaldo. */
    min-height: 100vh;
    min-height: 100dvh;
    display: flex;
    flex-direction: column;
    background: var(--fondo);
    color: var(--texto);
    font-family: var(--fuente);
    font-size: var(--tipo-base);
    line-height: var(--interlineado-texto);
    accent-color: var(--acento);         /* checkbox y radio nativos, gratis */
    overscroll-behavior-y: none;         /* quita el rebote extraño en la TWA */
    -webkit-tap-highlight-color: transparent; /* obliga a definir :active propio */
    touch-action: manipulation;          /* mata el zoom por doble toque; el pellizco
                                            ya lo bloquea user-scalable=no del viewport */
}

img, svg, video { max-width: 100%; display: block; }
button, input, select, textarea { font: inherit; color: inherit; }

::selection { background: var(--acento-suave); color: var(--texto); }

/* SOLO en dispositivos táctiles (móvil/tablet, incluida la TWA): la selección de
   texto se apaga. En escritorio con ratón NO se toca —ahí seleccionar y copiar
   texto es útil y no choca con nada—. Dos motivos en táctil:
     · mantener pulsado un elemento debe disparar SU gesto —reportar un mensaje,
       seleccionar filas— y no el menú nativo de "copiar / seleccionar todo",
       que se colaba encima;
     · el texto de interfaz (rótulos, menús, estados vacíos) no es contenido que
       nadie copie, y poder arrastrarlo hace que la app se sienta como una web.
   `dialog` va aparte de `body` porque un `<dialog>` en el top layer no siempre
   hereda el `user-select` del `<body>`, y el chat vive en uno. `-webkit-touch-
   callout: none` apaga además el bocadillo de iOS al mantener pulsada una imagen
   (fotos de otras personas: no se guardan con un gesto). El prefijo
   `-webkit-user-select` lo exige el WebView de iOS.

   Se REACTIVA la selección solo donde el texto es contenido de verdad: campos
   de formulario, texto técnico (`code`/`pre`/`kbd`/`samp` — comandos y tokens
   de `apps/DocDev` y `apps/Diagnostics`), y cualquier elemento con
   `data-seleccionable` (el gancho para que un módulo declare "esto se copia":
   una biografía, un mensaje propio, un identificador). Ver docs/DISENO.md §6. */
@media (hover: none) and (pointer: coarse) {
    body,
    dialog {
        -webkit-touch-callout: none;
        -webkit-user-select: none;
        user-select: none;
    }

    input, textarea, select,
    [contenteditable]:not([contenteditable="false"]),
    code, pre, kbd, samp,
    [data-seleccionable] {
        -webkit-user-select: text;
        user-select: text;
    }
}

/* Foco visible en todo. Nunca `outline: none` a secas. */
:focus-visible {
    outline: 2px solid var(--acento);
    outline-offset: 2px;
    border-radius: var(--radio-xs);
}
:focus:not(:focus-visible) { outline: none; }

/* ═══ 02. TIPOGRAFÍA ══════════════════════════════════════════════════════════ */

h1, h2, h3, h4 {
    margin: 0 0 var(--espacio-3);
    font-family: var(--fuente-titulo);   /* = --fuente salvo que un tema la cambie */
    line-height: var(--interlineado-medio);
    text-wrap: balance;                  /* degrada sin ruido donde no se soporta */
    font-weight: 600;
}

h1 { font-size: var(--tipo-2xl); line-height: var(--interlineado-apretado); }
h2 { font-size: var(--tipo-xl); }
h3 { font-size: var(--tipo-l); }

p, li { text-wrap: pretty; }
p { margin: 0 0 var(--espacio-4); }
p:last-child { margin-bottom: 0; }

a {
    color: var(--acento);
    text-underline-offset: .15em;
}

strong { font-weight: 600; }

code, kbd, samp {
    font-family: var(--fuente-mono);
    font-size: .88em;
    background: var(--superficie-hundida);
    border: 1px solid var(--linea);
    border-radius: var(--radio-xs);
    padding: .1em .35em;
}

/* Todo lo que escribe el usuario: un nombre de 40 caracteres sin espacios no
   puede romper el layout. */
.texto-usuario { overflow-wrap: anywhere; }

/* Números que se actualizan, para que no bailen los dígitos. */
time, .cifra { font-variant-numeric: tabular-nums; }

/* ═══ 03. LAYOUT ══════════════════════════════════════════════════════════════ */

.contenedor {
    width: 100%;
    max-width: var(--ancho-contenido);
    margin-inline: auto;
    padding-inline: var(--espacio-4);
}

.contenedor--app   { max-width: var(--ancho-app); }
.contenedor--texto { max-width: var(--ancho-texto); }

.cabecera {
    position: sticky;
    top: 0;
    z-index: var(--z-cabecera);
    background: var(--superficie);
    border-bottom: 1px solid var(--linea);
    /* Respeta la muesca lateral en horizontal dentro de la TWA. */
    padding-inline: env(safe-area-inset-left) env(safe-area-inset-right);
    /* El texto de navegación no se selecciona: lo cubre ya la regla global de
       `body` (arriba). Antes vivía aquí, cuando era la única zona con el
       problema. */
}

.cabecera__interior {
    display: flex;
    align-items: center;
    gap: var(--espacio-3);
    flex-wrap: wrap;
    min-height: var(--alto-cabecera);
    padding-block: var(--espacio-2);
}

.marca {
    font-weight: 700;
    font-size: var(--tipo-m);
    letter-spacing: -.01em;
    text-decoration: none;
    color: var(--texto);
    margin-right: auto;
}

.principal {
    flex: 1;
    padding-block: var(--espacio-6) var(--espacio-8);
}

.pie {
    background: var(--superficie);
    border-top: 1px solid var(--linea);
    padding-block: var(--espacio-5);
    padding-bottom: calc(var(--espacio-5) + env(safe-area-inset-bottom));
    font-size: var(--tipo-sm);
}

.pie__texto { color: var(--texto-suave); margin: 0 0 var(--espacio-2); }

.titulo-pagina { margin-bottom: var(--espacio-5); }

/* ═══ 04. UTILIDADES ══════════════════════════════════════════════════════════ */
/* Diez, y se cierra la lista. Esto no es Tailwind con nombres en español:
   cualquier necesidad nueva se resuelve con un componente. */

.oculto { display: none; }

/* `hidden` es un atributo del HTML y su `display: none` viene de la hoja del
   navegador, así que CUALQUIER `display` de autor lo pisa: un botón con
   `class="boton" hidden` —el patrón de «solo el JS me enciende»— se veía
   igualmente por el `display: inline-flex` de `.boton`. El `!important` es la
   única forma de que un atributo gane a una clase, y aquí es lo correcto: si algo
   lleva `hidden`, no se ve. Para mostrarlo se quita el atributo, no se pisa el
   display. */
[hidden] { display: none !important; }

.oculto-visual {
    position: absolute; width: 1px; height: 1px;
    margin: -1px; padding: 0; border: 0;
    clip-path: inset(50%); overflow: hidden; white-space: nowrap;
}

.pila { display: flex; flex-direction: column; gap: var(--espacio-4); }
.fila { display: flex; align-items: center; gap: var(--espacio-2); }

/* Rompe el padding del contenedor para llevar una foto a los bordes. */
.sangrado { margin-inline: calc(var(--espacio-4) * -1); }

@media (min-width: 64rem) {
    .solo-movil { display: none; }
}
@media (max-width: 63.999rem) {
    .solo-escritorio { display: none; }
}

/* Enlace de salto: visible solo al tabular. */
.salto-contenido {
    position: absolute;
    left: -9999px;
}
.salto-contenido:focus {
    left: var(--espacio-4);
    top: var(--espacio-4);
    z-index: var(--z-aviso);
    background: var(--superficie);
    color: var(--texto);
    padding: var(--espacio-2) var(--espacio-3);
    border-radius: var(--radio-s);
    box-shadow: var(--sombra-2);
}

/* ═══ 05. COMPONENTES COMUNES ═════════════════════════════════════════════════ */

/* ── Aviso ────────────────────────────────────────────────────────────────────
   El estado NO se comunica solo con color: fondo tintado + icono + palabra
   ("Hecho:", "Error:"). docs/DISENO.md §8.7 */

.aviso {
    display: flex;
    gap: var(--espacio-3);
    background: var(--superficie);
    border: 1px solid var(--linea);
    border-left: 3px solid var(--linea-fuerte);
    border-radius: var(--radio-s);
    padding: var(--espacio-3) var(--espacio-4);
    margin: 0 0 var(--espacio-5);
}

.aviso__icono { flex: none; margin-top: .15em; color: var(--texto-suave); }
.aviso__cuerpo { min-width: 0; }
.aviso__titulo { font-weight: 600; margin: 0; }
.aviso__texto  { margin: 0; color: var(--texto-suave); }

/* El resumen de errores de un formulario largo (docs/DISENO.md §8.2) reutiliza
   .aviso__texto sobre un <ul>, un enlace por campo fallido: mismo color de
   texto que la versión en <p>, pero sin viñetas ni sangría de lista. */
ul.aviso__texto { list-style: none; padding: 0; display: flex; flex-direction: column; gap: var(--espacio-1); }

.aviso--ok     { background: var(--ok-suave);    border-color: var(--linea); border-left-color: var(--ok); }
.aviso--error  { background: var(--error-suave); border-color: var(--linea); border-left-color: var(--error); }
.aviso--atento { background: var(--aviso-suave); border-color: var(--linea); border-left-color: var(--aviso); }
.aviso--info   { background: var(--info-suave);  border-color: var(--linea); border-left-color: var(--info); }

.aviso--ok     .aviso__icono { color: var(--ok); }
.aviso--error  .aviso__icono { color: var(--error); }
.aviso--atento .aviso__icono { color: var(--aviso); }
.aviso--info   .aviso__icono { color: var(--info); }

.aviso--ok     .aviso__texto,
.aviso--error  .aviso__texto,
.aviso--atento .aviso__texto,
.aviso--info   .aviso__texto { color: var(--texto); }

/* Un aviso puede traer acción propia. Hoy lo usa el aviso de versión nueva del
   service worker (public/assets/js/register-sw.js): botón «Recargar» dentro del
   cuerpo y botón de cerrar a la derecha. No es un componente nuevo, son dos reglas
   de colocación sobre el que ya existe. La separación entre los dos pulsables la
   da el `gap` del propio .aviso (12 px ≥ 8 px, docs/DISENO.md §3.1). */
.aviso__cuerpo > .boton { margin-top: var(--espacio-3); }
.aviso > .boton--icono  { flex: none; align-self: flex-start; margin-left: auto; }

/* ── Botón ────────────────────────────────────────────────────────────────────
   docs/DISENO.md §8.1. Un solo --principal por pantalla. */

.boton {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--espacio-2);
    /* min-height + padding, nunca height fija: las métricas de fuente cambian
       entre Android, iOS y escritorio (§3.1). */
    min-height: var(--toque-min);
    padding: var(--espacio-2) var(--espacio-4);
    border: 1px solid var(--linea-fuerte);
    border-radius: var(--radio-s);
    background: var(--superficie);
    color: var(--texto);
    font-size: var(--tipo-sm);
    font-weight: 500;
    text-decoration: none;
    cursor: pointer;
    /* `color` va en la lista igual que en `.menu__enlace`: hay botones que cambian
       el color de su contenido al cambiar de estado —el hamburguesa de la cabecera
       al abrirse— y sin esto el relleno se interpolaba y el icono daba un salto. */
    transition: background-color var(--dur-rapida) var(--curva-estandar),
                border-color var(--dur-rapida) var(--curva-estandar),
                color var(--dur-rapida) var(--curva-estandar),
                transform var(--dur-instantanea) var(--curva-estandar);
}

.boton__icono { flex: none; }

/* :active explícito: sustituye al tap-highlight que hemos desactivado y es lo
   que hace que se sienta nativo. */
.boton:active { transform: scale(.98); }

/* hover SOLO dentro de este media query: en la TWA no hay hover, y fuera de él
   Android deja el estilo "pegado" tras el toque. */
@media (hover: hover) {
    .boton:hover { background: var(--superficie-hundida); }
}

.boton--principal {
    min-height: var(--toque-comodo);
    padding-inline: var(--espacio-5);
    background: var(--acento);
    border-color: transparent;
    color: var(--acento-texto);
    font-size: var(--tipo-base);
    font-weight: 600;
}
.boton--principal:active { background: var(--acento-intenso); }
@media (hover: hover) {
    .boton--principal:hover { background: var(--acento-fuerte); }
}
/* Separa el anillo de foco del relleno del botón. */
.boton--principal:focus-visible { box-shadow: 0 0 0 2px var(--superficie); }

.boton--secundario { background: transparent; }

.boton--fantasma {
    background: transparent;
    border-color: transparent;
    color: var(--acento);
}

/* Sin relleno rojo en reposo: un rojo permanente en pantalla es ansiedad de
   fondo. El relleno se reserva para el botón de confirmar de un modal. */
.boton--destructivo { color: var(--error); border-color: var(--error); background: transparent; }

.boton--bloque { width: 100%; }

.boton--icono {
    width: var(--toque-min);
    padding: 0;
    border-radius: var(--radio-redondo);
}

.boton[disabled],
.boton[aria-disabled="true"] {
    background: var(--superficie-hundida);
    border-color: var(--linea);
    color: var(--linea-fuerte);
    cursor: not-allowed;
    transform: none;
    /* Nunca opacity: arrastra el contraste a valores imprevisibles. */
}

/* ── Campo de formulario ──────────────────────────────────────────────────────
   docs/DISENO.md §8.2. Etiqueta SIEMPRE visible: el placeholder no es una
   etiqueta —desaparece al escribir y tiene contraste bajo por definición. */

.campo {
    display: flex;
    flex-direction: column;
    gap: var(--espacio-2);
}

.campo__etiqueta {
    font-size: var(--tipo-sm);
    font-weight: 600;
}

.campo__control {
    /* Borde --linea-fuerte (3.62:1) y no --linea: un control cuyo único borde es
       decorativo incumple WCAG 1.4.11. */
    border: 1px solid var(--linea-fuerte);
    border-radius: var(--radio-s);
    background: var(--superficie);
    color: var(--texto);
    /* 1rem sin excepción: por debajo, iOS hace zoom al enfocar el campo. */
    font-size: var(--tipo-base);
    font-family: inherit;
    min-height: var(--toque-comodo);
    padding: var(--espacio-2) var(--espacio-3);
    width: 100%;
}

.campo__control:focus-visible { border-color: var(--acento); }

/* Campo de solo lectura: mismo ritmo vertical que uno editable —para que las dos
   clases de fila se alineen dentro del mismo formulario—, pero sin aspecto de
   control: nada de borde ni fondo, porque no hay nada que enfocar ni escribir. Un
   <input disabled> aquí sería el error: se salta del orden de tabulación y algunos
   lectores de pantalla no anuncian bien su valor. docs/DISENO.md §8.2. */
.campo__valor {
    display: flex;
    align-items: center;
    gap: var(--espacio-2);
    min-height: var(--toque-comodo);
    padding: var(--espacio-2) 0;
    margin: 0;
    color: var(--texto);
    font-size: var(--tipo-base);
}

/* Un campo con botón adjunto (mostrar contraseña). El input manda en el ancho. */
.campo__grupo {
    display: flex;
    align-items: center;
    gap: var(--espacio-2);
}

.campo__ayuda {
    color: var(--texto-suave);
    font-size: var(--tipo-sm);
    margin: 0;
}

/* Tres señales en el error, no solo el color: borde, icono y texto (§8.2). El
   texto dice qué hacer, no qué está mal. Flex y no párrafo suelto: el icono y el
   texto van en la misma línea, con el icono arriba si el mensaje ocupa dos. */
.campo__error {
    display: flex;
    align-items: flex-start;
    gap: var(--espacio-1);
    color: var(--error);
    font-size: var(--tipo-sm);
    margin: 0;
}

/* 1rem y no el 1.25em de .icono: a --tipo-sm, 1.25em se ve más grande que el
   propio texto del error y descoloca la primera línea. */
.campo__error .icono {
    flex: none;
    width: 1rem;
    height: 1rem;
    margin-top: .125em;
}

.campo--error .campo__control { border-color: var(--error); }

/* Casilla + texto (aceptar términos, recordarme). El <input> ya sale coloreado
   gratis por accent-color en :root (§01); aquí solo falta el objetivo táctil: el
   propio checkbox mide menos de 44px, así que es la ETIQUETA la que lo alcanza,
   con el checkbox y el texto dentro. */
.campo__casilla {
    display: flex;
    align-items: flex-start;
    gap: var(--espacio-2);
    min-height: var(--toque-min);
    padding-block: var(--espacio-1);
    font-size: var(--tipo-sm);
}

.campo__casilla input { flex: none; margin-top: .1875rem; width: 1.25rem; height: 1.25rem; }

/* ── Medidor de fuerza de contraseña ──────────────────────────────────────────
   docs/DISENO.md §8.2 bis. Puramente informativo: la contraseña se acepta o no
   por CredentialService::policyError() (longitud), esto no bloquea nada, solo
   anima a algo mejor que el mínimo. Nace con `hidden` en la vista y lo revela
   app.js — sin JS, el campo sigue siendo una contraseña normal con sus reglas
   ya escritas en el .campo__ayuda de al lado.

   CUATRO niveles y ninguno solo por color: el texto dice "Floja"/"Regular"/
   "Buena"/"Fuerte", y el color es una capa más, no la única señal. */
.medidor-contrasena { margin-top: var(--espacio-1); }

.medidor-contrasena__barra {
    height: .25rem;
    border-radius: var(--radio-redondo);
    background: var(--superficie-hundida);
    overflow: hidden;
}

.medidor-contrasena__relleno {
    display: block;
    height: 100%;
    border-radius: var(--radio-redondo);
    /* Sin transición de color/anchura: es texto tecleado en vivo, la regla de
       §10 es que nada se retrase más de 100ms, y una barra que se anima detrás
       de cada tecla se queda descolocada de lo que se acaba de escribir. */
    background: var(--error);
    width: 25%;
}

.medidor-contrasena[data-nivel="regular"] .medidor-contrasena__relleno { background: var(--aviso); width: 50%; }
.medidor-contrasena[data-nivel="buena"]   .medidor-contrasena__relleno { background: var(--info);  width: 75%; }
.medidor-contrasena[data-nivel="fuerte"]  .medidor-contrasena__relleno { background: var(--ok);    width: 100%; }

.medidor-contrasena__texto {
    margin: var(--espacio-1) 0 0;
    font-size: var(--tipo-sm);
    color: var(--texto-suave);
}

/* ── Etiqueta (intereses, "nuevo", "verificado") ───────────────────────────── */

.etiqueta {
    display: inline-flex;
    align-items: center;
    gap: var(--espacio-1);
    padding: var(--espacio-1) var(--espacio-3);
    border: 1px solid var(--linea-fuerte);
    border-radius: var(--radio-redondo);
    background: var(--superficie);
    color: var(--texto-suave);
    font-size: var(--tipo-s);
    /* Sin uppercase: en español los acentos quedan mal y algunos lectores de
       pantalla lo deletrean. */
}

.etiqueta--acento { background: var(--acento-suave); border-color: var(--acento-borde); color: var(--acento); }
.etiqueta--ok     { background: var(--ok-suave);     border-color: var(--ok);           color: var(--ok); }

/* Contador (docs/DISENO.md §8.10 y §9): SOLO para algo accionable —hoy, las
   conversaciones sin leer de `apps/Social`—, nunca actividad sin más
   (§2.6.6). `--acento` y no `--error`: no es un aviso, es una cifra a
   resolver. `tabular-nums` para que el número no salte de ancho entre
   sondeos, y `min-width` (no `width`) para que crezca sin recortarse a partir
   de dos cifras — sigue siendo un `<span>` corto, sin límite artificial. */
.etiqueta--contador {
    justify-content: center;
    min-width: 1.25rem;
    padding-inline: var(--espacio-2);
    background: var(--acento-suave);
    border-color: var(--acento-borde);
    color: var(--acento);
    font-variant-numeric: tabular-nums;
    font-weight: 600;
}

/* ── Formulario ───────────────────────────────────────────────────────────────
   Envoltorio de un formulario vertical. Un campo por fila (docs/DISENO.md §8.2)
   y ancho de app: una línea de entrada de 60 rem no se lee ni se rellena bien. */

.formulario {
    display: flex;
    flex-direction: column;
    gap: var(--espacio-5);
    max-width: var(--ancho-app);
}

/* ── Icono ────────────────────────────────────────────────────────────────────
   Sprite propio en assets/icons/sprite.svg, INCRUSTADO una vez por el layout
   base: una vista escribe <use href="#icon-x">, sin URL (docs/DISENO.md §7).

   El `stroke: currentColor` lo trae cada <symbol> como atributo, así que el
   icono hereda el color del texto y funciona en los dos temas sin nada más.

   El GROSOR y los remates, en cambio, tienen que estar aquí y no en el sprite:
   el contenido que un <use> clona no lo alcanzan los selectores CSS —ni los del
   propio archivo del sprite—, pero sí hereda de este <svg>, que es su padre.
   Sin estas tres líneas el trazo se pinta con el valor inicial de SVG (1) y con
   los extremos cuadrados, no con el 1.75 redondeado que fija docs/DISENO.md §7. */

.icono {
    width: 1.25em;
    height: 1.25em;
    flex: none;
    stroke-width: 1.75;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* ── Estado vacío / de error de página ────────────────────────────────────── */

.estado-vacio {
    text-align: center;
    max-width: var(--ancho-texto);
    margin-inline: auto;
    padding-block: var(--espacio-7);
}

.estado-vacio__icono { color: var(--texto-suave); width: 3rem; height: 3rem; margin-inline: auto; }
.estado-vacio__titulo { margin-top: var(--espacio-4); }
.estado-vacio__texto { color: var(--texto-suave); }
.estado-vacio__codigo {
    font-size: var(--tipo-3xl);
    line-height: var(--interlineado-apretado);
    font-weight: 700;
    color: var(--linea-fuerte);
    margin: 0;
}

/* Traza de excepción: solo aparece con APP_DEBUG=true. */
.traza {
    text-align: left;
    background: var(--superficie);
    border: 1px solid var(--error);
    border-radius: var(--radio-m);
    padding: var(--espacio-4);
    margin-top: var(--espacio-5);
}
.traza__titulo    { font-weight: 700; margin: 0 0 var(--espacio-1); color: var(--error); }
.traza__mensaje   { margin: 0 0 var(--espacio-2); overflow-wrap: anywhere; }
.traza__ubicacion { color: var(--texto-suave); font-size: var(--tipo-sm); margin: 0 0 var(--espacio-3); overflow-wrap: anywhere; }
.traza__pila {
    overflow-x: auto;
    font-family: var(--fuente-mono);
    font-size: var(--tipo-xs);
    background: var(--superficie-hundida);
    padding: var(--espacio-3);
    border-radius: var(--radio-xs);
    margin: 0;
}

/* ── Portada ──────────────────────────────────────────────────────────────── */

.portada {
    text-align: center;
    max-width: var(--ancho-texto);
    margin-inline: auto;
    padding-block: var(--espacio-6) var(--espacio-4);
}

.portada__titulo {
    font-size: var(--tipo-3xl);
    line-height: var(--interlineado-apretado);
    font-weight: 700;
    letter-spacing: -.02em;
    margin-bottom: var(--espacio-3);
}

.portada__entradilla {
    font-size: var(--tipo-m);
    color: var(--texto-suave);
    margin-bottom: var(--espacio-5);
}

.portada__acciones {
    display: flex;
    flex-wrap: wrap;
    gap: var(--espacio-3);
    justify-content: center;
}

/* ── Pasos numerados ──────────────────────────────────────────────────────── */

.pasos {
    max-width: var(--ancho-texto);
    padding-left: var(--espacio-5);
    margin: 0 0 var(--espacio-5);
}
.pasos > li { margin-bottom: var(--espacio-3); }
.pasos > li::marker { color: var(--acento); font-weight: 600; }

/* ── Tabla ────────────────────────────────────────────────────────────────── */

.tabla-envoltorio { overflow-x: auto; }   /* la tabla scrollea, no el body */

.tabla {
    width: 100%;
    border-collapse: collapse;
    background: var(--superficie);
    border: 1px solid var(--linea);
    border-radius: var(--radio-m);
    overflow: hidden;
    text-align: left;
    font-size: var(--tipo-sm);
}

.tabla td, .tabla th {
    padding: var(--espacio-3);
    border-top: 1px solid var(--linea);
    vertical-align: top;
}
.tabla tr:first-child td, .tabla tr:first-child th { border-top: 0; }

.tabla__estado  { width: 2.5rem; text-align: center; }
.tabla__nombre  { width: 13rem; font-weight: 600; }
.tabla__detalle {
    color: var(--texto-suave);
    font-family: var(--fuente-mono);
    font-size: var(--tipo-s);
    overflow-wrap: anywhere;
}

/* En móvil la fila se apila. Con tres columnas en 360px, el ancho fijo de
   .tabla__nombre dejaba el detalle en una columna de ocho caracteres y el texto
   monoespaciado se rompía palabra a palabra. */
@media (max-width: 47.999rem) {
    .tabla,
    .tabla tbody,
    .tabla tr,
    .tabla td,
    .tabla th {
        display: block;
        width: auto;
    }

    .tabla tr {
        border-top: 1px solid var(--linea);
        padding: var(--espacio-3);
    }
    .tabla tr:first-child { border-top: 0; }

    .tabla td,
    .tabla th {
        border-top: 0;
        padding: 0;
        text-align: left;
    }

    /* Estado y nombre en la misma línea, el detalle debajo a todo el ancho.
       El selector lleva .tabla delante a propósito: `.tabla td` de arriba tiene
       especificidad (0,1,1) y ganaría a una clase sola (0,1,0), dejando el icono
       de estado en su propia línea. */
    .tabla .tabla__estado {
        display: inline-block;
        width: auto;
        margin-right: var(--espacio-2);
        vertical-align: baseline;
    }
    /* El reset pone display:block a todo svg; aquí debe fluir con el texto. */
    .tabla .tabla__estado .icono {
        display: inline-block;
        vertical-align: -0.2em;
    }
    .tabla .tabla__nombre {
        display: inline;
    }
    .tabla .tabla__detalle {
        margin-top: var(--espacio-2);
    }
}

.es-ok    { color: var(--ok); }
.es-error { color: var(--error); }

/* ── Tarjeta ──────────────────────────────────────────────────────────────── */

.tarjetas {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
    gap: var(--espacio-4);
    margin-top: var(--espacio-6);
}

.tarjeta {
    background: var(--superficie);
    border: 1px solid var(--linea);
    border-radius: var(--radio-m);
    padding: var(--espacio-5);
    box-shadow: var(--sombra-1);
}

.tarjeta__icono  { color: var(--acento); width: 1.75rem; height: 1.75rem; margin-bottom: var(--espacio-3); }
.tarjeta__titulo { font-size: var(--tipo-l); margin-bottom: var(--espacio-2); }
.tarjeta__texto  { color: var(--texto-suave); font-size: var(--tipo-sm); margin: 0; }

/* ═══ 06. NAVEGACIÓN ══════════════════════════════════════════════════════════ */

/* ── Menú ─────────────────────────────────────────────────────────────────── */

.menu__lista {
    display: flex;
    gap: var(--espacio-1);
    list-style: none;
    margin: 0;
    padding: 0;
    flex-wrap: wrap;
}

.menu__enlace {
    display: inline-flex;
    align-items: center;
    gap: var(--espacio-2);
    min-height: var(--toque-min);
    padding: var(--espacio-2) var(--espacio-3);
    border-radius: var(--radio-s);
    text-decoration: none;
    color: var(--texto-suave);
    font-size: var(--tipo-sm);
    font-weight: 500;
    transition: background-color var(--dur-rapida) var(--curva-estandar),
                color var(--dur-rapida) var(--curva-estandar);
}

@media (hover: hover) {
    .menu__enlace:hover { background: var(--superficie-hundida); color: var(--texto); }
}

/* El activo no se marca solo con color: además lleva aria-current="page" en el
   marcado, y peso 600. */
.menu__enlace--activo {
    background: var(--acento-suave);
    color: var(--acento);
    font-weight: 600;
}

/* Entrada que es una acción (salir): un <form> con un <button> dentro. El botón
   reutiliza .menu__enlace y solo deshace lo que el navegador le pone de serie. */
.menu__accion { display: flex; }

.menu__enlace--accion {
    background: none;
    border: 0;
    font-family: inherit;
    cursor: pointer;
}

.menu__icono { flex: none; }

/* Submenú. Se abre por hover en escritorio y por foco con teclado. */
.menu__elemento--con-hijos { position: relative; }

.menu__submenu {
    display: none;
    position: absolute;
    top: 100%;
    left: 0;
    min-width: 12rem;
    background: var(--superficie-alta);
    border: 1px solid var(--linea);
    border-radius: var(--radio-s);
    box-shadow: var(--sombra-2);
    list-style: none;
    margin: 0;
    padding: var(--espacio-1);
    z-index: var(--z-menu);
}

.menu__elemento--con-hijos:hover .menu__submenu,
.menu__elemento--con-hijos:focus-within .menu__submenu { display: block; }

/* Menú del pie: más discreto y sin fondo en el activo. */
.menu--footer .menu__lista { gap: 0; }
.menu--footer .menu__enlace {
    min-height: auto;
    padding: var(--espacio-1) var(--espacio-2);
    font-size: var(--tipo-sm);
    font-weight: 400;
}
.menu--footer .menu__enlace--activo { background: transparent; }

/* ── Navegación de la cabecera: un panel, dos menús ────────────────────────────
   docs/DISENO.md §9. El panel `#menu-cabecera` contiene los DOS menús —'main' con
   las secciones del producto y 'user' con la cuenta— cada uno en su <nav>. En
   escritorio es una fila más de la cabecera; por debajo de 64rem se colapsa detrás
   del botón. El HTML es el mismo en los dos breakpoints: no hay segundo pintado,
   así que no hay dos copias del menú que puedan discrepar.

   Los selectores de aquí abajo NO nombran a `.menu--user`, y eso es deliberado:
   valen para cualquier <nav class="menu"> que caiga dentro del panel, así que el
   día que `matching` registre su entrada en 'main' el panel la coloca sin una
   línea de CSS nueva. Nombrar el menú concreto era lo que había antes, cuando el
   panel solo contenía uno.

   Ojo con el orden de la cascada, que aquí decide dos cosas:

   1. En escritorio el botón no se ve. Lo dice `.solo-movil` en el marcado, pero
      `.boton { display: inline-flex }` tiene la MISMA especificidad y va ANTES en
      este archivo, así que ganaba por orden y el botón salía también en
      escritorio. De ahí la regla explícita de abajo, que va después.
   2. El colapso depende de `--colapsable`, que pone el JS. Sin JS no hay clase,
      no hay colapso y las listas se ven como en escritorio. Un `display: none` a
      secas en móvil dejaría toda la navegación inalcanzable en cuanto app.js
      fallara, con el botón además invisible por su `hidden`. */

.cabecera__navegacion {
    /* Ancla del panel desplegado, que se posiciona respecto a este contenedor. */
    position: relative;
    display: flex;
    align-items: center;
}

/* En escritorio los dos <nav> son una fila. --espacio-4 y no el --espacio-1 de
   `.menu__lista`: ese hueco separa entradas del MISMO menú, y aquí lo que hay que
   distinguir es dónde acaba la navegación del producto y empieza la de la cuenta.
   Sin esta separación, «Avisos» y «Mi cuenta» se leen como la misma lista.

   En móvil el `flex-direction` pasa a columna dentro de la media query de abajo. */
.cabecera__panel {
    display: flex;
    align-items: center;
    gap: var(--espacio-4);
}

/* Los DOS iconos del botón viajan en el marcado y aquí se elige cuál se ve.

   Viene de cuando el sprite se enlazaba como archivo externo y reescribir el
   `href` del <use> obligaba a resolverlo otra vez por red en el instante del
   clic. Desde que el sprite se incrusta (Renderer::iconSprite()) eso ya no pasa,
   pero esto se queda: un `display` sigue siendo más barato que rehacer el árbol
   de un <use>, y el estado tiene una sola fuente —el aria-expanded— sin que el JS
   toque marcado. Ver public/assets/js/app.js.

   `display: none` y no `visibility`/`opacity`: un elemento con `display: none` no
   es un ítem de flex, así que el icono escondido no deja ni hueco ni `gap` dentro
   del botón. */
.cabecera__hamburguesa[aria-expanded="false"] .cabecera__icono-menu--cerrar,
.cabecera__hamburguesa[aria-expanded="true"]  .cabecera__icono-menu--abrir { display: none; }

/* Abierto: el botón se marca como lo que es, la pestaña de un panel abierto. TRES
   señales y ninguna exclusiva del color: la equis del icono, el aria-label «Cerrar
   menú» y el relleno.

   El relleno es la señal que menos pesa, a propósito: --acento-suave está a 1,17:1
   de la superficie de la cabecera (1,13:1 en oscuro) y en un tema monocromo puede
   no verse. Quien sostiene el borde del control es --acento-borde —4,13:1 claro /
   4,04:1 oscuro sobre --superficie, por encima del 3:1 de 1.4.11— y el icono queda
   a 7,38:1 / 7,78:1 sobre su propio relleno.

   Los dos temas activos lo llevan a inversión, cada uno con su gesto; esto es el
   valor por defecto para cualquier otro. Sin transición nueva: `.boton` ya
   interpola relleno, borde y color, no se desplaza nada y el bloque de movimiento
   reducido no necesita una línea más. */
.cabecera__hamburguesa[aria-expanded="true"] {
    background: var(--acento-suave);
    border-color: var(--acento-borde);
    color: var(--acento);
}

@media (min-width: 64rem) {
    .cabecera__hamburguesa { display: none; }
}

@media (max-width: 63.999rem) {
    /* --toque-comodo (48px) y no --toque-min (44px): es el único acceso a toda la
       navegación en móvil y se pulsa con el pulgar en la esquina superior, que es
       la zona más incómoda de la pantalla (§4.6). El `.boton--icono` del marcado
       deja ambos en --toque-min. */
    .cabecera__hamburguesa {
        width: var(--toque-comodo);
        min-height: var(--toque-comodo);
    }

    /* Colapsado. Se esconde el PANEL entero y no cada lista: dentro hay dos <nav>
       y esconderlos uno a uno dejaría sus contenedores ocupando sitio en la
       cabecera. El botón es hermano del panel, no hijo, así que sigue visible.

       Solo con el JS vivo: la clase la pone app.js. Sin JS no hay clase, no hay
       colapso, y las dos listas se ven en la cabecera como en escritorio. */
    .cabecera__panel--colapsable { display: none; }

    /* Desplegado: panel flotante bajo el botón, con la misma superficie, el mismo
       borde y la misma sombra que `.menu__submenu` de escritorio, porque es lo
       mismo abierto de otra manera. La forma sí cambia: --radio-m y no --radio-s.
       Esto flota SOBRE el contenido y ocupa media pantalla —es una hoja, no un
       control—, y así cada tema le da su curva sin escribir una regla: 4 px en
       Grafito, 18 px en Verbena.

       La caja flotante es el PANEL y no la lista de cada menú, que es lo que era
       cuando aquí dentro solo había un <nav>: con dos, una caja por menú serían
       dos tarjetas flotantes superpuestas bajo el mismo botón.

       Sin transición ni animación a propósito: es un cambio de `display`, que no
       se puede animar, y así no hay nada que desactivar con prefers-reduced-motion
       (§10). */
    .cabecera__panel--desplegado {
        display: flex;
        flex-direction: column;
        align-items: stretch;
        /* --espacio-2 y no el --espacio-4 de escritorio: aquí los dos bloques ya
           los separa el filete de `.menu + .menu`, y este hueco solo tiene que
           cumplir los ≥8 px entre adyacentes (§16). */
        gap: var(--espacio-2);
        position: absolute;
        top: calc(100% + var(--espacio-2));
        inset-inline-end: 0;
        min-width: 12rem;
        /* A 320px de ancho un panel de 12rem cabe, pero un texto largo de una
           entrada podría estirarlo fuera de la pantalla. */
        max-width: calc(100vw - var(--espacio-4) * 2);
        padding: var(--espacio-2);
        background: var(--superficie-alta);
        border: 1px solid var(--linea);
        border-radius: var(--radio-m);
        box-shadow: var(--sombra-2);
        z-index: var(--z-menu);
    }

    /* Las entradas de cada menú, en columna y a ancho completo. */
    .cabecera__panel--desplegado .menu__lista {
        flex-direction: column;
        align-items: stretch;
        flex-wrap: nowrap;
        /* Ver arriba: 4 px es un fallo de puntería con el pulgar (§16). */
        gap: var(--espacio-2);
    }

    /* Dónde acaba la navegación del producto y empieza la de la cuenta. El
       selector adyacente hace que el filete exista SOLO si de verdad hay dos
       menús: con 'main' vacío —que es el estado de hoy— no se pinta ninguna línea
       suelta encima de una lista única.

       Es una separación de forma y no de color: quien no distinga el gris del
       filete sigue viendo el hueco de --espacio-2 a cada lado (§2.6.3). */
    .cabecera__panel--desplegado .menu + .menu {
        border-top: 1px solid var(--linea);
        padding-top: var(--espacio-2);
    }

    /* Cada fila del panel es un objetivo táctil de ancho completo y 48px de alto:
       en un desplegable vertical, apuntar solo al texto es incómodo. */
    .cabecera__panel--desplegado .menu__elemento,
    .cabecera__panel--desplegado .menu__accion { display: flex; }

    .cabecera__panel--desplegado .menu__enlace {
        flex: 1;
        justify-content: flex-start;
        min-height: var(--toque-comodo);
    }

    /* «Salir» es un <form class="menu__accion"> con el <button class="menu__enlace">
       dentro (partials/menu.php): el flex item real de .menu__elemento es el
       <form>, no el botón, así que el `flex: 1` de arriba —que sí llega al
       botón, un descendiente cualquiera— no bastaba para que el FORM ocupara
       la fila entera. Sin esto, el fondo de :hover del botón solo cubría su
       contenido, más estrecho que el resto de filas del panel. */
    .cabecera__panel--desplegado .menu__accion {
        flex: 1;
    }
}

/* ═══ MOVIMIENTO REDUCIDO ═════════════════════════════════════════════════════
   Con esta preferencia activa, nada se desplaza. Se conservan los cambios de
   color, que no provocan mareo. */

@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: .01ms;
        animation-iteration-count: 1;
        transition-duration: .01ms;
        scroll-behavior: auto;
    }
    .boton:active { transform: none; }
}
