/*
 * Hato Santa Bárbara — sistema visual
 *
 * Escrito desde cero el 2026-08-08. Lo anterior eran 1.304 líneas que
 * reimplementaban Bootstrap a mano (btn, card, col-md-6, shadow-sm): los
 * defectos de Bootstrap sin sus ventajas. En una sola sesión mordió tres veces
 * con el mismo fallo — el hueco del grid sin descontar del ancho de la columna.
 *
 * Reorganizado en capas el 2026-08-09, por una decisión de Romfeli: «el diseño
 * y el frontend sólidos desde ahora, con capacidad de mejora y compatibilidad
 * con librerías que están en la vanguardia».
 *
 * Cuatro reglas que gobiernan todo lo de abajo:
 *
 * 1. EL TELÉFONO MANDA. No es que la página quepa en el móvil: es que se diseña
 *    para 360 px y el escritorio se adapta hacia arriba. La navegación va abajo,
 *    en la zona del pulgar, porque es ahí donde está la mano.
 *
 * 2. NADA HARDCODEADO. Todo color, tamaño, espacio y radio sale de un token. Un
 *    valor suelto es el que después nadie encuentra para cambiar.
 *
 * 3. LOS UMBRALES NO SE NEGOCIAN. 56 px de objetivo táctil, 7:1 de contraste,
 *    16 px en campos. Se usa con guantes y a pleno sol del llano; lo verifica
 *    scripts/evidencia_visual.py y no es opinión.
 *
 * 4. LA CASCADA LA MANDA LA CAPA, NO EL ORDEN DE CARGA. Ver abajo.
 */

/* ═══════════════════════════════════════════════════════════════════════════
   CAPAS — el orden se declara una vez, aquí, y ya no se discute
   ═══════════════════════════════════════════════════════════════════════════

   Esta línea es el contrato de todo el frontend. De menor a mayor autoridad:

     reset       normalización del navegador
     vendor      librerías de terceros (Leaflet hoy; gráficas, tablas, mañana)
     base        tokens y elementos HTML desnudos
     componentes lo nuestro con nombre propio (.ficha, .accion, .hilera)
     utilidades  el retoque puntual, que gana a todo lo demás

   `vendor` justo después del reset: una librería puede pisar la normalización
   —trae su propio mundo y es legítimo— y no puede tocar nuestros componentes.

   **Qué se compra con esto.** Hasta hoy `leaflet.css` se cargaba en `extra_head`
   —o sea, DESPUÉS de este archivo— y nuestros parches del mapa solo ganaban
   porque alguien fue escalando selectores a mano hasta superarlo:
   `.leaflet-touch .leaflet-bar a` para vencer a `.leaflet-bar a`. Eso aguanta
   con una librería y con alguien que recuerde por qué está escrito así. Con la
   segunda es una guerra de especificidad, y esa guerra termina siempre igual:
   alguien pone un `!important` y ya nadie puede razonar sobre la hoja.

   Ahora una librería entra en `vendor` y pierde contra nuestros componentes
   AUNQUE se cargue después y AUNQUE tenga más especificidad. Por eso los
   parches de Leaflet del final de este archivo pudieron bajar a una sola clase.

   **Cómo se mete una librería nueva** (la convención, en una línea): nunca con
   un `<link>` suelto, siempre con una regla de importación que termine en
   `layer(vendor)`. El ejemplo vivo es `vendor-mapa.css` — dos líneas, ábrelo.

   Aquí iba ese ejemplo escrito entero y hubo que quitarlo: `collectstatic`
   reescribe las rutas del CSS y no le importa que estén dentro de un
   comentario, así que se puso a buscar el archivo inventado del ejemplo y el
   despliegue reventó con `MissingFileError`. Es el mismo tropiezo que ya está
   documentado en `nucleo/tests/test_estructura_html.py`, donde un `</div>` de
   adorno dentro de un comentario engañaba al contador de etiquetas. Un
   comentario esconde el código de quien lee, no de las herramientas.

   Lo verifica `nucleo/tests/test_cascada.py` en un navegador de verdad: que el
   orden esté declarado, que Leaflet viva en `vendor`, y que una regla nuestra
   con MENOS especificidad le gane. Un `grep '@layer'` diría que sí y no
   probaría nada. */

@layer reset, vendor, base, componentes, utilidades;

/* Las dos fuentes, servidas desde el propio sitio. Rutas relativas a este
   archivo para que `collectstatic` las encuentre y las versione igual que el
   resto. Variables: un archivo por familia cubre todos los pesos. */
@font-face {
  font-family: 'Fraunces';
  src: url("../fonts/fraunces-5.3.0-latin-wght.eb2486760f75.woff2") format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Atkinson Hyperlegible Next';
  src: url("../fonts/atkinson-next-5.3.0-latin-wght.825fabef695f.woff2") format('woff2');
  font-weight: 200 800;
  font-style: normal;
  font-display: swap;
}

/* ═══════════════════════════════════════════════════════════════════════════
   TRANSICIÓN ENTRE PÁGINAS
   ═══════════════════════════════════════════════════════════════════════════

   Esto es Django sirviendo documentos completos: cada enlace es una petición y
   una página nueva. Con esta regla el navegador encadena la vista que sale con
   la que entra, y la aplicación deja de parpadear en blanco entre pantallas.

   Por qué importa aquí y no es adorno: con la conexión de un teléfono en el
   llano, ese blanco entre páginas es lo que hace dudar de si el toque se
   registró — y la duda acaba en un segundo toque y en un dato duplicado.

   Fuera de las capas a propósito: `@view-transition` es una regla de documento,
   no una declaración que compita en la cascada. Y el navegador que no la
   entienda la ignora entera, sin dejar nada a medias.

   Lo comprueba `nucleo/tests/test_componentes.py` navegando de verdad y
   mirando si el documento que entra trajo transición. */

@view-transition {
  navigation: auto;
}

/* Quien pidió menos movimiento no recibe ninguno. Hace falta decirlo aparte
   porque los pseudo-elementos de la transición no son hijos de nadie: el
   `*` del bloque de `prefers-reduced-motion` del final de este archivo no los
   alcanza, y sin esto la página seguiría deslizándose para quien pidió que no.
   La navegación sigue funcionando; lo único que se quita es la animación. */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   RESET
   ═══════════════════════════════════════════════════════════════════════════ */

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

  html {
    -webkit-text-size-adjust: 100%;
    /* Un enlace a un ancla dejaba su destino justo debajo de la cabecera fija,
       que lo tapaba. Este margen le reserva el hueco al desplazarse. */
    scroll-padding-top: 4.5rem;
  }

  body {
    margin: 0;
  }

  img {
    max-width: 100%;
    height: auto;
    display: block;
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   BASE — tokens y elementos desnudos
   ═══════════════════════════════════════════════════════════════════════════ */

@layer base {
  /* ── Color: «Llano y tierra» (2026-10-22) ──
     La paleta deja de ser un verde sobre gris de oficina y pasa a ser la del
     hato: arena de sabana de fondo, verde de potrero como color principal,
     tierra cocida de acento y agua para el clima. Romfeli eligió la dirección
     el 2026-10-22, entre tres propuestas.

     Lo que no cambió es la regla: **todo texto a 7:1 o más** sobre su fondo, y
     lo mide `nucleo/tests/test_tema.py` sobre la página pintada. Cada par se
     calculó antes de escribirlo (tinta 15,5 · tinta-media 9,4 · verde 8,0 ·
     tierra 7,6 · agua 7,8 · rojo 7,9, sobre papel). `--tinta-suave` da 5:1 y
     por eso no se usa para texto que haya que leer: ejes, adornos, divisores.

     Los grises tiran a marrón (tono 60) y no a azul: junto a la arena, un gris
     frío parece sucio. El hexadecimal de al lado es la conversión exacta. */
  :root {
    --tinta: oklch(22% 0.018 60);               /* #211912 */
    --tinta-media: oklch(37% 0.026 58);         /* #4b3d32 */
    --tinta-suave: oklch(52% 0.026 62);         /* #74665a */

    --papel: oklch(96.4% 0.016 84);             /* #f8f2e7 */
    --superficie: oklch(99.2% 0.006 84);        /* #fefcf8 */
    --superficie-alt: oklch(93.6% 0.020 82);    /* #f1e9db */

    --borde: oklch(88% 0.024 80);               /* #e0d6c6 */
    --borde-fuerte: oklch(79% 0.030 76);        /* #c6b8a6 */

    --verde: oklch(40% 0.075 150);              /* #265331 */
    --verde-oscuro: oklch(34% 0.065 150);       /* #1c4125 */
    --verde-claro: oklch(93.5% 0.028 140);      /* #e0eedd */

    --tierra: oklch(43% 0.100 52);              /* #793d11 */
    --tierra-clara: oklch(94% 0.028 70);        /* #f8e8d8 */
    --rojo: oklch(45% 0.160 27);                /* #9b1f1d */
    --rojo-claro: oklch(95.5% 0.020 20);        /* #feebeb */
    --agua: oklch(41% 0.075 236);               /* #18506d */
    --agua-clara: oklch(94% 0.020 236);         /* #dfeef7 */

    /* El sol del paisaje y los destellos. Decorativo: nunca lleva texto. */
    --sol: oklch(82% 0.140 82);                 /* #f1ba4b */

    /* El texto que va ENCIMA de un color fuerte. */
    --sobre-fuerte: oklch(99.2% 0.006 84);

    /* ── Tipografía ──
       Hasta el 2026-10-22 era `system-ui` y ninguna fuente web, por los datos
       móviles del llano. Se cambia a sabiendas, con el coste medido: las dos
       fuentes suman 70 KB en woff2 (solo el subconjunto latino, que cubre el
       español entero) y el trabajador de servicio las guarda tras la primera
       visita, así que se pagan una vez por teléfono y no en cada página.

       · Fraunces, para los títulos: una serifa cálida, con carácter de libro
         de finca. Se usa poco y grande, que es donde luce.
       · Atkinson Hyperlegible Next, para todo lo demás: diseñada para leerse
         con poca visión —distingue 1/l/I y 0/O—, que es leer un número de
         crotal a pleno sol con la pantalla al mínimo.

       `font-display: swap`: el texto sale en la fuente del sistema mientras
       llega la web; nunca hay una pantalla en blanco esperando una letra. */
    --letra: 'Atkinson Hyperlegible Next', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
    --letra-titulo: 'Fraunces', Georgia, 'Times New Roman', serif;

    --t-menor: 0.8125rem;   /* 13px — solo para lo accesorio */
    --t-base: 1rem;         /* 16px — el mínimo de un campo en iOS */
    --t-medio: 1.125rem;    /* 18px */
    --t-titulo: 1.375rem;   /* 22px */
    --t-grande: 1.75rem;    /* 28px */
    --t-cifra: 2.25rem;     /* 36px — los números que se leen de un vistazo */

    /* ── Espacio ── Escala de 4 px: todo múltiplo, sin valores intermedios. */
    --e1: 0.25rem;
    --e2: 0.5rem;
    --e3: 0.75rem;
    --e4: 1rem;
    --e5: 1.5rem;
    --e6: 2rem;
    --e7: 3rem;

    /* ── Forma ── */
    --radio-sm: 0.5rem;
    --radio: 0.875rem;
    --radio-lg: 1.25rem;

    /* La sombra se mezcla desde la tinta en vez de llevar un `rgba()` escrito a
       mano. `in srgb` y no `in oklch` a propósito: para mezclar CON transparente
       lo que importa es el canal alfa, y srgb da el resultado previsible. */
    --sombra:
      0 1px 2px color-mix(in srgb, var(--tinta) 4%, transparent),
      0 2px 8px color-mix(in srgb, var(--tinta) 5%, transparent);
    --sombra-alta:
      0 2px 4px color-mix(in srgb, var(--tinta) 6%, transparent),
      0 8px 24px color-mix(in srgb, var(--tinta) 8%, transparent);

    /* ── Medidas del layout ── */
    --barra-inferior: 4.5rem;   /* 72px: cabe un objetivo de 56 con aire */
    --lateral: 15rem;
    --transicion: 0.18s ease;

    /* ── Objetivo táctil ──
       44 es el AAA de WCAG y el mínimo para un dedo desnudo. 56 es el nuestro:
       un guante de trabajo no tiene esa puntería.

       Esto lo decía este comentario desde el primer día y el token valía 44: solo
       los campos llegaban a 56, y cada botón de la interfaz salía como aviso en
       `evidencia_visual.py --perfil campo` sin que nadie lo mirara. La deriva
       entre lo que un documento afirma y lo que hace se resuelve en favor de lo
       afirmado, que era además la decisión tomada (Romfeli, 2026-08-09).

       Dos nombres para un número: hoy coinciden, pero un botón y un campo no son
       lo mismo y pueden separarse mañana sin buscar el valor por toda la hoja. */
    --tactil: 3.5rem;
    --tactil-campo: var(--tactil);

    /* La marca visual de un radio o una casilla, que NO es su objetivo táctil.
       El objetivo lo da el `<label>` que los envuelve —ahí se pulsa, y ahí se
       mide—; esto es solo el círculo que hay que ver y al que se apunta.

       32 y no 56 (2026-08-11): un círculo de 56 px junto a texto de 16 llevaría
       el panel de capas de 113 a 145 px sobre un mapa de 326, y el objetivo ya
       cumple sin eso. 32 es lo que hace falta para distinguirlo a pleno sol y
       para que el guante acierte sin apuntar fino. Por debajo nunca: 24 es el
       mínimo legal de WCAG 2.2 SC 2.5.8. */
    --tactil-marca: 2rem;

    /* ── Paleta del tema de sol ──
       Los valores viven aquí, junto a los normales, y los bloques de abajo solo
       los reasignan. Así ningún color está escrito dos veces: si mañana el
       verde de sol se retoca, se retoca en una línea y no en dos sitios que
       pueden divergir sin que nadie lo note.

       Todos bajan de luminosidad hasta pasar de 10:1 sobre el papel. El umbral
       lo fija `nucleo/tests/test_tema.py`, que lo mide sobre la página pintada
       en vez de creerse estos comentarios. */
    --sol-tinta: oklch(14% 0.006 248);
    --sol-tinta-media: oklch(27% 0.020 261);
    --sol-tinta-suave: oklch(33% 0.020 264);
    --sol-papel: oklch(100% 0 0);
    --sol-superficie: oklch(100% 0 0);
    --sol-superficie-alt: oklch(95% 0.010 93);
    --sol-borde: oklch(62% 0.020 86);
    --sol-borde-fuerte: oklch(42% 0.020 85);
    --sol-verde: oklch(32% 0.075 153);
    --sol-verde-oscuro: oklch(26% 0.062 151);
    --sol-verde-claro: oklch(94% 0.020 139);
    --sol-tierra: oklch(34% 0.090 84);
    --sol-tierra-clara: oklch(95% 0.026 92);
    --sol-rojo: oklch(34% 0.150 27);
    --sol-rojo-claro: oklch(96% 0.014 17);

    /* ── Paleta de noche llanera (2026-10-22) ──
       No es la clara invertida: cada valor se eligió para el fondo oscuro y
       se midió igual que los del día. Texto a 7:1 o más (tinta 15,5 · verde
       8,7 · tierra 8,2 · agua 9,1 · rojo 7,1 sobre la superficie). Los fondos
       son marrón de tierra de noche, no gris: con la arena del día, un gris
       azulado parecería otra aplicación. */
    --noche-tinta: oklch(94% 0.012 80);
    --noche-tinta-media: oklch(84% 0.018 78);
    --noche-tinta-suave: oklch(70% 0.020 76);
    --noche-papel: oklch(19% 0.014 60);
    --noche-superficie: oklch(23.5% 0.016 60);
    --noche-superficie-alt: oklch(28% 0.018 60);
    --noche-borde: oklch(34% 0.020 62);
    --noche-borde-fuerte: oklch(44% 0.022 62);
    --noche-verde: oklch(78% 0.110 148);
    --noche-verde-oscuro: oklch(86% 0.090 148);
    --noche-verde-claro: oklch(27% 0.045 150);
    --noche-tierra: oklch(78% 0.110 62);
    --noche-tierra-clara: oklch(28% 0.040 62);
    --noche-rojo: oklch(75% 0.130 25);
    --noche-rojo-claro: oklch(28% 0.050 25);
    --noche-agua: oklch(80% 0.080 236);
    --noche-agua-clara: oklch(27% 0.040 236);
    --noche-sobre-fuerte: oklch(19% 0.014 60);
  }

  /* ── El tema de sol, encendido de dos maneras ──

     1. `[data-tema='sol']` — alguien pulsó el interruptor. El atributo lo pinta
        el servidor desde una cookie (ver `nucleo/tema.py`), así que llega antes
        que el primer píxel y no parpadea.

     2. `prefers-contrast: more` — el sistema operativo ya lo pedía. Quien tiene
        el teléfono configurado en alto contraste no debería tener que
        descubrir un botón para que le hagamos caso.

     Y el tercer estado, que es el que obliga a que esto sean dos reglas y no
     una: `[data-tema='claro']` significa «lo he apagado yo» y tiene que ganarle
     a la detección automática. Por eso el `:not()`.

     Lo que cambia además del color: desaparecen las sombras. A pleno sol una
     sombra suave no se ve —no separa nada— y lo que sí separa es un borde con
     contraste, que es lo que suben `--borde` y `--borde-fuerte`.

     Las dos listas de abajo son idénticas y no se pueden fundir: una cuelga de
     un atributo y la otra de una consulta de medio, y CSS no deja compartir un
     bloque entre ambas. Lo que sí se evitó es repetir los COLORES — las dos
     listas son reasignaciones, y el valor de cada uno está escrito una sola vez
     arriba. */
  [data-tema='sol'] {
    --tinta: var(--sol-tinta);
    --tinta-media: var(--sol-tinta-media);
    --tinta-suave: var(--sol-tinta-suave);
    --papel: var(--sol-papel);
    --superficie: var(--sol-superficie);
    --superficie-alt: var(--sol-superficie-alt);
    --borde: var(--sol-borde);
    --borde-fuerte: var(--sol-borde-fuerte);
    --verde: var(--sol-verde);
    --verde-oscuro: var(--sol-verde-oscuro);
    --verde-claro: var(--sol-verde-claro);
    --tierra: var(--sol-tierra);
    --tierra-clara: var(--sol-tierra-clara);
    --rojo: var(--sol-rojo);
    --rojo-claro: var(--sol-rojo-claro);
    --sombra: none;
    --sombra-alta: none;
  }

  @media (prefers-contrast: more) {
    :root:not([data-tema='claro']) {
      --tinta: var(--sol-tinta);
      --tinta-media: var(--sol-tinta-media);
      --tinta-suave: var(--sol-tinta-suave);
      --papel: var(--sol-papel);
      --superficie: var(--sol-superficie);
      --superficie-alt: var(--sol-superficie-alt);
      --borde: var(--sol-borde);
      --borde-fuerte: var(--sol-borde-fuerte);
      --verde: var(--sol-verde);
      --verde-oscuro: var(--sol-verde-oscuro);
      --verde-claro: var(--sol-verde-claro);
      --tierra: var(--sol-tierra);
      --tierra-clara: var(--sol-tierra-clara);
      --rojo: var(--sol-rojo);
      --rojo-claro: var(--sol-rojo-claro);
      --sombra: none;
      --sombra-alta: none;
    }
  }

  /* ── El tema de noche, encendido de dos maneras ──
     Igual que el de sol: `[data-tema='noche']` cuando alguien lo eligió, o el
     modo oscuro del teléfono cuando nadie ha elegido nada. Cualquier elección
     explícita —claro, sol o noche— gana a la del sistema: por eso el
     `:not([data-tema])` y no un `:not([data-tema='claro'])`. */
  [data-tema='noche'] {
    --tinta: var(--noche-tinta);
    --tinta-media: var(--noche-tinta-media);
    --tinta-suave: var(--noche-tinta-suave);
    --papel: var(--noche-papel);
    --superficie: var(--noche-superficie);
    --superficie-alt: var(--noche-superficie-alt);
    --borde: var(--noche-borde);
    --borde-fuerte: var(--noche-borde-fuerte);
    --verde: var(--noche-verde);
    --verde-oscuro: var(--noche-verde-oscuro);
    --verde-claro: var(--noche-verde-claro);
    --tierra: var(--noche-tierra);
    --tierra-clara: var(--noche-tierra-clara);
    --rojo: var(--noche-rojo);
    --rojo-claro: var(--noche-rojo-claro);
    --agua: var(--noche-agua);
    --agua-clara: var(--noche-agua-clara);
    --sobre-fuerte: var(--noche-sobre-fuerte);
    --sombra: 0 1px 2px oklch(0% 0 0 / 0.3), 0 2px 8px oklch(0% 0 0 / 0.25);
    --sombra-alta: 0 2px 4px oklch(0% 0 0 / 0.35), 0 8px 24px oklch(0% 0 0 / 0.35);
    color-scheme: dark;
  }

  @media (prefers-color-scheme: dark) {
    :root:not([data-tema]) {
      --tinta: var(--noche-tinta);
      --tinta-media: var(--noche-tinta-media);
      --tinta-suave: var(--noche-tinta-suave);
      --papel: var(--noche-papel);
      --superficie: var(--noche-superficie);
      --superficie-alt: var(--noche-superficie-alt);
      --borde: var(--noche-borde);
      --borde-fuerte: var(--noche-borde-fuerte);
      --verde: var(--noche-verde);
      --verde-oscuro: var(--noche-verde-oscuro);
      --verde-claro: var(--noche-verde-claro);
      --tierra: var(--noche-tierra);
      --tierra-clara: var(--noche-tierra-clara);
      --rojo: var(--noche-rojo);
      --rojo-claro: var(--noche-rojo-claro);
      --agua: var(--noche-agua);
      --agua-clara: var(--noche-agua-clara);
      --sobre-fuerte: var(--noche-sobre-fuerte);
      --sombra: 0 1px 2px oklch(0% 0 0 / 0.3), 0 2px 8px oklch(0% 0 0 / 0.25);
      --sombra-alta: 0 2px 4px oklch(0% 0 0 / 0.35), 0 8px 24px oklch(0% 0 0 / 0.35);
      color-scheme: dark;
    }
  }

  body {
    font-family: var(--letra);
    font-size: var(--t-base);
    line-height: 1.55;
    color: var(--tinta);
    background: var(--papel);
    -webkit-font-smoothing: antialiased;
  }

  a {
    color: var(--verde);
    text-decoration: none;
  }

  a:hover {
    color: var(--verde-oscuro);
    text-decoration: underline;
  }

  /* El foco se ve SIEMPRE y en todo. Un `outline: none` heredado deja la página
     navegable a ciegas, que es de los fallos más caros y menos visibles. */
  :focus-visible {
    outline: 3px solid var(--verde);
    outline-offset: 2px;
    border-radius: var(--radio-sm);
  }

  h1, h2, h3, h4 {
    margin: 0 0 var(--e2);
    line-height: 1.2;
    font-weight: 650;
    letter-spacing: -0.01em;
    /* Reparte las líneas de un titular para que ninguna quede con una palabra
       suelta. Se nota sobre todo en el teléfono, que es donde un título de
       cinco palabras cae en tres líneas desiguales. */
    text-wrap: balance;
  }

  /* En un párrafo no se equilibra —eso descoloca textos largos—, solo se evita
     que la última línea quede con una palabra huérfana. */
  p, dd, .encabezado__sub {
    text-wrap: pretty;
  }

  /* Las casillas y los radios se pintan con el verde del sistema en vez del
     azul del navegador. Una línea, y deja de haber un color que no es nuestro
     en mitad de un formulario. */
  input[type='checkbox'],
  input[type='radio'] {
    accent-color: var(--verde);
  }

  /* Fraunces solo en los tres niveles grandes: es una letra para leer poco y
     grande. `h4` y todo lo pequeño siguen en la de texto, que se lee mejor. */
  h1, h2, h3 {
    font-family: var(--letra-titulo);
    font-weight: 600;
    font-optical-sizing: auto;
    letter-spacing: -0.015em;
  }
  h1 { font-size: var(--t-grande); }
  h2 { font-size: var(--t-titulo); }
  h3 { font-size: var(--t-medio); }

  p { margin: 0 0 var(--e3); }

  ul, ol { margin: 0 0 var(--e3); padding-left: var(--e5); }
  li { margin-bottom: var(--e1); }

  dl { margin: 0; }
  dt {
    font-size: var(--t-menor);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--tinta-media);
    margin-top: var(--e3);
  }
  dd { margin: 0; font-size: var(--t-base); }

  /* Un enlace dentro de una lista de definiciones es navegación —lleva a la ficha
     de la madre o del padre— y se toca con el pulgar. Sin esto medía 138×17 px.
     La regla ya existía en el sistema anterior y se perdió al reescribirlo: al
     rehacer algo, lo primero que se cae son las correcciones ganadas a golpes.
     (El enlace dentro de un párrafo se deja en paz: ahí engordar la caja rompe el
     flujo del texto sin ganar nada.) */
  dd a {
    display: inline-flex;
    align-items: center;
    min-height: var(--tactil);
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   COMPONENTES
   ═══════════════════════════════════════════════════════════════════════════ */

@layer componentes {

  /* ── Estructura: el teléfono primero ── */

  .app {
    min-height: 100vh;
    min-height: 100dvh;   /* dvh: descuenta la barra del navegador móvil */
    display: flex;
    flex-direction: column;
  }

  .cabecera {
    position: sticky;
    top: 0;
    z-index: 20;
    display: flex;
    align-items: center;
    gap: var(--e3);
    min-height: 3.5rem;
    padding: var(--e2) var(--e4);
    background: var(--superficie);
    border-bottom: 1px solid var(--borde);
    view-transition-name: cabecera;   /* se queda quieta, como la navegación */
  }

  .cabecera__marca {
    display: flex;
    align-items: center;
    gap: var(--e2);
    font-family: var(--letra-titulo);
    font-weight: 650;
    font-size: var(--t-medio);
    color: var(--tinta);
    letter-spacing: -0.02em;
    min-height: var(--tactil);
    /* Sin esto, un hijo de flex no baja de su ancho de contenido y empuja al
       resto de la barra en vez de encogerse. */
    min-width: 0;
  }

  /* El nombre de la finca se recorta, no se parte. Con el interruptor del tema
     en la barra, «Hato Santa Bárbara» pasaba a dos líneas en un teléfono de
     360 px y subía la cabecera de 56 a 73 px — alto que se pierde en todas las
     pantallas, porque la cabecera es `sticky`.

     Recortar y no esconder: con varias fincas, este texto es lo que dice en
     cuál estás. Vale más medio nombre que ninguno. */
  .cabecera__marca span:last-of-type {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    min-width: 0;
  }

  .cabecera__marca:hover { text-decoration: none; color: var(--verde); }

  /* Los interruptores de tema empujan hacia la derecha todo lo que viene detrás,
     y por eso el `auto` está aquí y no en `.cabecera__usuario`: si lo llevaran
     los dos, el espacio sobrante se repartiría entre ellos y el icono quedaría
     flotando en mitad de la barra. */
  .cabecera__temas {
    margin-left: auto;
    display: flex;
    gap: var(--e1);
  }

  /* Un botón que solo lleva icono es cuadrado: sin texto que lo ensanche, el
     relleno lateral de `.accion` lo dejaba más ancho que alto sin motivo. */
  .accion--icono {
    min-width: var(--tactil);
    padding-inline: var(--e2);
  }

  .accion--icono[aria-pressed='true'] {
    background: var(--verde-claro);
    color: var(--verde-oscuro);
  }

  .cuenta { position: relative; }
  .cuenta__boton { list-style: none; }
  .cuenta__boton::-webkit-details-marker { display: none; }
  .cuenta__inicial {
    display: grid;
    place-items: center;
    width: 2.25rem;
    height: 2.25rem;
    border-radius: 999px;
    background: var(--verde);
    color: var(--sobre-fuerte);
    font-family: var(--letra-titulo);
    font-weight: 650;
    font-size: var(--t-medio);
  }
  .cuenta__menu {
    position: absolute;
    right: 0;
    top: calc(100% + var(--e2));
    z-index: 30;
    min-width: 14rem;
    padding: var(--e4);
    border: 1px solid var(--borde);
    border-radius: var(--radio);
    background: var(--superficie);
    box-shadow: var(--sombra-alta);
  }
  .cuenta__nombre { margin: 0 0 var(--e3); font-weight: 600; }

  .cabecera__usuario {
    display: flex;
    align-items: center;
    gap: var(--e2);
    font-size: var(--t-menor);
    color: var(--tinta-media);
  }

  .contenido {
    flex: 1;
    width: 100%;
    max-width: 68rem;
    margin: 0 auto;
    padding: var(--e4);
    /* Hueco para que la barra inferior no tape el último botón de la página. */
    padding-bottom: calc(var(--barra-inferior) + var(--e5));
    min-width: 0;   /* deja que las tablas hagan scroll en vez de estirar */

    /* El contenido se declara CONTENEDOR, y con eso los componentes de dentro
       pueden preguntar por el sitio que tienen en vez de por el tamaño de la
       ventana. No es lo mismo, y la diferencia se pintaba mal todos los días:
       desde 60rem hay una barra lateral de 15rem, así que en una ventana de
       1000 px aquí dentro caben 760 — y la lista de animales enseñaba su tabla
       «de pantalla ancha» porque la ventana pasaba de 768, aunque el hueco real
       no llegara. Ver `nucleo/tests/test_componentes.py`. */
    container: contenido / inline-size;
  }

  .pie {
    padding: var(--e4);
    padding-bottom: calc(var(--barra-inferior) + var(--e4));
    text-align: center;
    font-size: var(--t-menor);
    color: var(--tinta-media);
    border-top: 1px solid var(--borde);
  }

  /* ── Navegación: abajo en el teléfono ──
     El patrón nativo del móvil y, sobre todo, la zona del pulgar. La barra
     lateral de antes era un patrón de escritorio escondido tras un botón. */
  .nav {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 30;
    display: flex;
    background: var(--superficie);
    border-top: 1px solid var(--borde);
    padding-bottom: env(safe-area-inset-bottom);   /* iPhone con barra de gestos */

    /* Con nombre propio, la barra no participa en el fundido general: se queda
       quieta mientras el contenido cambia por detrás. Es lo que separa una
       transición que parece una aplicación de una que parece una diapositiva —
       si todo se desvanece a la vez, incluida la navegación, el ojo pierde el
       punto de referencia. */
    view-transition-name: navegacion;
  }

  .nav__enlace {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    min-height: var(--barra-inferior);
    padding: var(--e2) var(--e1);
    color: var(--tinta-media);
    font-size: var(--t-menor);
    font-weight: 500;
    text-align: center;
  }

  .nav__enlace:hover { text-decoration: none; color: var(--verde); }

  /* El estado activo NO se dice solo con color: lleva peso, una barra y
     `aria-current` en el HTML. El color como único portador está prohibido
     (WCAG 1.4.1) y además a pleno sol dos verdes no se distinguen. */
  .nav__enlace[aria-current='page'] {
    /* `--verde-oscuro` y no `--verde`: sobre el fondo claro del propio enlace, el
       verde normal da 6,80:1 y el perfil de campo pide 7. Medido, no elegido. */
    color: var(--verde-oscuro);
    font-weight: 700;
    box-shadow: inset 0 3px 0 var(--verde);
    background: var(--verde-claro);
  }

  .nav__icono {
    width: 22px;
    height: 22px;
    flex-shrink: 0;
  }

  /* ── Estructura: escritorio ── */

  @media (min-width: 60rem) {
    .app {
      display: grid;
      grid-template-columns: var(--lateral) 1fr;
      grid-template-rows: auto 1fr auto;

      /* La franja del lateral la pinta el CONTENEDOR, no la barra.
         La barra mide una ventana de alto —tiene que quedarse quieta al hacer
         scroll, y para eso `sticky` necesita altura propia—, así que en una
         página larga su fondo blanco terminaba a media pantalla y debajo
         aparecía el color del papel: un rectángulo cortado a la altura exacta de
         la ventana. El fondo y la línea divisoria se dibujan aquí, sobre la
         columna entera, y así llegan hasta el pie sin añadir un solo elemento. */
      background:
        linear-gradient(
          to right,
          var(--superficie) 0 calc(var(--lateral) - 1px),
          var(--borde) calc(var(--lateral) - 1px) var(--lateral),
          var(--papel) var(--lateral)
        );
    }

    .nav {
      position: sticky;
      top: 0;
      left: auto;
      right: auto;
      bottom: auto;
      grid-row: 1 / -1;
      grid-column: 1;
      height: 100vh;
      height: 100dvh;
      overflow-y: auto;
      flex-direction: column;
      align-items: stretch;
      padding: var(--e4) var(--e2);
      gap: var(--e1);
      background: transparent;   /* lo pinta `.app`, hasta el fondo de la página */
      border-top: none;
    }

    .nav__enlace {
      flex: 0 0 auto;
      flex-direction: row;
      justify-content: flex-start;
      gap: var(--e3);
      min-height: var(--tactil);
      padding: var(--e2) var(--e3);
      border-radius: var(--radio-sm);
      font-size: var(--t-base);
      text-align: left;
    }

    .nav__enlace[aria-current='page'] {
      box-shadow: inset 3px 0 0 var(--verde);
    }

    .cabecera { grid-column: 2; }
    .contenido {
      grid-column: 2;
      padding: var(--e6);
      padding-bottom: var(--e6);
    }
    .pie { grid-column: 2; padding-bottom: var(--e4); }
  }

  /* ── Encabezado de página ── */

  .encabezado {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: var(--e3);
    margin-bottom: var(--e5);
  }

  .encabezado__texto { flex: 1 1 15rem; min-width: 0; }
  .encabezado h1 { margin-bottom: var(--e1); }

  .encabezado__sub {
    margin: 0;
    color: var(--tinta-media);
    font-size: var(--t-base);
  }

  /* ── Ficha: el contenedor de todo ── */

  .ficha {
    background: var(--superficie);
    border: 1px solid var(--borde);
    border-radius: var(--radio);
    box-shadow: var(--sombra);
    margin-bottom: var(--e4);
    overflow: hidden;
  }

  .ficha__cuerpo { padding: var(--e4); }
  .ficha__cuerpo--ajustado { padding: var(--e3) var(--e4); }

  /* Una pantalla de un solo propósito —entrar— no se lee mejor por ser más ancha:
     un formulario de dos campos estirado a 68rem obliga a barrer la vista de
     lado a lado para nada. Se centra y se le pone techo. */
  .ficha--estrecha {
    max-width: 24rem;
    margin-left: auto;
    margin-right: auto;
  }

  .ficha__titulo {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--e2);
    margin-bottom: var(--e3);
  }

  .ficha__titulo h2 { margin: 0; font-size: var(--t-medio); }

  /* ── Cifra: los números del panel ── */

  /* `auto-fit` con `minmax` y no columnas fijas: así el hueco ya está descontado
     por el propio grid y no hace falta restarlo a mano. Es exactamente el fallo
     que tenía el sistema anterior, tres veces. */
  .cifras {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(9rem, 100%), 1fr));
    gap: var(--e3);
    margin-bottom: var(--e5);
  }

  .cifra {
    background: var(--superficie);
    border: 1px solid var(--borde);
    border-radius: var(--radio);
    box-shadow: var(--sombra);
    padding: var(--e4);
  }

  .cifra__etiqueta {
    margin: 0 0 var(--e1);
    font-size: var(--t-menor);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--tinta-media);
    font-weight: 600;
  }

  .cifra__valor {
    margin: 0;
    font-family: var(--letra-titulo);
    font-size: var(--t-cifra);
    font-weight: 600;
    line-height: 1;
    color: var(--verde);
    letter-spacing: -0.03em;
    font-variant-numeric: tabular-nums;
  }

  .cifra__pie {
    margin: var(--e1) 0 0;
    font-size: var(--t-menor);
    color: var(--tinta-media);
  }

  /* ── Acciones ── */

  .acciones {
    display: flex;
    flex-wrap: wrap;
    gap: var(--e2);
  }

  .accion {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--e2);
    min-height: var(--tactil);
    padding: var(--e2) var(--e4);
    border: 1px solid var(--borde-fuerte);
    border-radius: var(--radio-sm);
    background: var(--superficie);
    color: var(--tinta);
    font-family: inherit;
    font-size: var(--t-base);
    font-weight: 600;
    cursor: pointer;
    transition: background var(--transicion), border-color var(--transicion);
  }

  .accion:hover {
    background: var(--superficie-alt);
    border-color: var(--tinta-suave);
    text-decoration: none;
    color: var(--tinta);
  }

  .accion--principal {
    background: var(--verde);
    border-color: var(--verde);
    color: var(--sobre-fuerte);
  }

  .accion--principal:hover {
    background: var(--verde-oscuro);
    border-color: var(--verde-oscuro);
    color: var(--sobre-fuerte);
  }

  .accion--peligro {
    color: var(--rojo);
    border-color: var(--rojo);
  }

  .accion--peligro:hover {
    background: var(--rojo);
    border-color: var(--rojo);
    color: var(--sobre-fuerte);
  }

  .accion--sutil {
    border-color: transparent;
    background: transparent;
    font-weight: 500;
  }

  /* Un botón que vive en la misma fila que un campo tiene que MEDIR lo que mide
     un campo. El objetivo táctil de un botón suelto es 44 px y el de un campo son
     56: puestos uno al lado del otro en la fila de filtros, el botón quedaba doce
     píxeles más bajo y la fila se leía torcida. No es una altura nueva inventada
     aquí, es el mismo token del campo al que acompaña. */
  .accion--campo {
    min-height: var(--tactil-campo);
  }

  /* Cuando el botón ES la acción de la pantalla —«Entrar» en el login— ocupa el
     ancho de su contenedor. Antes lo pedía un `w-100` de Bootstrap que en este
     sistema no existe, así que no ocupaba nada. */
  .accion--ancho {
    width: 100%;
  }

  /* ── Hileras: la lista en el teléfono ── */

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

  .hilera {
    display: block;
    padding: var(--e3) var(--e4);
    border-bottom: 1px solid var(--borde);
    min-height: var(--tactil-campo);
    color: inherit;
  }

  .hilera:last-child { border-bottom: none; }
  .hilera:hover { background: var(--superficie-alt); text-decoration: none; color: inherit; }

  .hilera__principal {
    display: flex;
    align-items: baseline;
    gap: var(--e2);
    flex-wrap: wrap;
  }

  .hilera__clave {
    font-weight: 700;
    font-size: var(--t-medio);
    color: var(--verde);
    font-variant-numeric: tabular-nums;
  }

  .hilera__nombre { font-weight: 500; }

  .hilera__datos {
    margin: var(--e1) 0 0;
    font-size: var(--t-menor);
    color: var(--tinta-media);
    display: flex;
    flex-wrap: wrap;
    gap: var(--e1) var(--e3);
  }

  /* ── Etiquetas de estado ── */

  /* Cada estado lleva SU PALABRA además de su color. Impreso en blanco y negro
     tiene que seguir entendiéndose (UX.md §3.8). */
  .etiqueta {
    display: inline-flex;
    align-items: center;
    padding: 2px var(--e2);
    border-radius: 999px;
    font-size: var(--t-menor);
    font-weight: 600;
    background: var(--superficie-alt);
    color: var(--tinta-media);
    border: 1px solid var(--borde);
  }

  .etiqueta--activo { background: var(--verde-claro); color: var(--verde-oscuro); border-color: var(--verde); }
  /* El texto de aviso se deriva del propio token de tierra en vez de llevar un
     `#6b5210` escrito a mano: oscurecerlo mezclando con la tinta mantiene el
     tono y da el contraste que pide el perfil de campo. */
  .etiqueta--aviso {
    background: var(--tierra-clara);
    color: color-mix(in oklch, var(--tierra) 72%, var(--tinta));
    border-color: var(--tierra);
  }
  .etiqueta--baja { background: var(--rojo-claro); color: var(--rojo); border-color: var(--rojo); }

  /* ── Tabla: solo desde tableta ── */

  .tabla-envoltura { overflow-x: auto; }

  .tabla {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--t-base);
  }

  .tabla th,
  .tabla td {
    padding: var(--e3) var(--e4);
    text-align: left;
    border-bottom: 1px solid var(--borde);
    vertical-align: middle;
  }

  /* La cabecera de la tabla se queda a la vista al bajar por la lista. Sin
     esto, a la décima fila ya nadie sabe qué columna está mirando y hay que
     subir a comprobarlo.

     `top` no es 0 sino el alto de la cabecera de la página, que también es
     `sticky`: si no, las dos se pisan y los títulos de columna quedan debajo.

     Aquí se descartó a propósito `content-visibility: auto` para las filas.
     Habría sido la técnica de moda, pero la lista viene paginada de veinte en
     veinte: no hay nada que ahorrar, y sí un riesgo real de romper la búsqueda
     del navegador y el recuento de controles alcanzables. */
  .tabla th {
    position: sticky;
    top: 3.5rem;
    z-index: 1;
    font-size: var(--t-menor);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--tinta-media);
    font-weight: 700;
    background: var(--superficie-alt);
    white-space: nowrap;
  }

  .tabla tbody tr:hover { background: var(--superficie-alt); }
  .tabla tbody tr:last-child td { border-bottom: none; }

  /* El enlace de una celda es un objetivo táctil, y su ancho depende del texto:
     un sector llamado «as» daba 18 px de ancho. `min-width` le pone un suelo para
     que un nombre corto no lo vuelva inalcanzable con el pulgar. */
  .tabla td a {
    display: inline-flex;
    align-items: center;
    justify-content: flex-start;
    min-height: var(--tactil);
    min-width: var(--tactil);
    font-weight: 600;
  }

  .tabla caption {
    padding: var(--e3) var(--e4);
    text-align: left;
    font-size: var(--t-menor);
    color: var(--tinta-media);
    caption-side: top;
  }

  /* ── Formularios ── */

  .campo { margin-bottom: var(--e4); }

  .campo__etiqueta {
    display: block;
    margin-bottom: var(--e1);
    font-weight: 600;
    font-size: var(--t-base);
  }

  /* Atado al ELEMENTO y no solo a una clase: `{{ form.campo }}` de Django
     renderiza el <input> sin clase ninguna, y una regla de clase no lo alcanza.
     Así, cualquier formulario que alguien pinte sin clases nace cumpliendo. */
  /* Los `:not()` van dentro de `:where()` para que no cuenten en la
     especificidad. Escritos sueltos sumaban cuatro selectores de atributo y
     dejaban esta regla en (0,4,1) — más fuerte que `input:user-invalid`, que es
     (0,1,1). El resultado: el aviso de campo mal rellenado no se veía nunca, y
     no porque faltara, sino porque perdía contra el estilo normal del campo.

     Una regla de base no debe ganar por acumulación de paréntesis. Con
     `:where()` la especificidad es la de `input` a secas y cualquier estado
     —foco, error— la puede matizar. */
  input:where(:not([type='checkbox'], [type='radio'], [type='hidden'], [type='submit'])),
  select,
  textarea {
    display: block;
    width: 100%;
    min-height: var(--tactil-campo);
    padding: var(--e2) var(--e3);
    /* 16px exactos: por debajo, Safari en iOS hace zoom al enfocar y descuadra
       la página entera. No es estética, rompe el formulario. */
    font-size: var(--t-base);
    font-family: inherit;
    color: var(--tinta);
    background: var(--superficie);
    border: 1px solid var(--borde-fuerte);
    border-radius: var(--radio-sm);
    transition: border-color var(--transicion), box-shadow var(--transicion);
  }

  input:focus,
  select:focus,
  textarea:focus {
    border-color: var(--verde);
    box-shadow: 0 0 0 3px var(--verde-claro);
  }

  /* El cuadro de notas crece con lo que se escribe, hasta un techo. Escribir a
     ciegas dentro de tres líneas fijas, con el resto del texto desplazado hacia
     arriba, es de lo que más molesta al registrar en el corral. Donde
     `field-sizing` no exista, queda el `min-height` de siempre. */
  textarea {
    min-height: 5rem;
    resize: vertical;
    field-sizing: content;
    max-height: 40vh;
  }

  /* Un campo obligatorio no se pinta de error hasta que alguien lo deja vacío.
     Con `:invalid` a secas, un formulario recién abierto sale con todos sus
     campos obligatorios en rojo —antes de que nadie escriba nada—, y eso enseña
     a ignorar el rojo. `:user-invalid` espera a que la persona haya pasado por
     el campo o haya intentado enviar. */
  input:user-invalid,
  select:user-invalid,
  textarea:user-invalid {
    border-color: var(--rojo);
    box-shadow: 0 0 0 3px var(--rojo-claro);
  }

  .campo__ayuda {
    display: block;
    margin-top: var(--e1);
    font-size: var(--t-menor);
    color: var(--tinta-media);
  }

  .campo__error {
    margin-top: var(--e1);
    font-size: var(--t-menor);
    font-weight: 600;
    color: var(--rojo);
  }

  .obligatorio { color: var(--rojo); }

  /* ── Avisos ── */

  .aviso {
    display: flex;
    align-items: flex-start;
    gap: var(--e3);
    padding: var(--e3) var(--e4);
    margin-bottom: var(--e4);
    border: 1px solid var(--borde);
    border-left: 4px solid var(--tinta-suave);
    border-radius: var(--radio-sm);
    background: var(--superficie);
  }

  .aviso--exito { border-left-color: var(--verde); background: var(--verde-claro); }
  .aviso--error { border-left-color: var(--rojo); background: var(--rojo-claro); }
  .aviso--atencion { border-left-color: var(--tierra); background: var(--tierra-clara); }

  .aviso__texto { flex: 1; min-width: 0; }
  .aviso p:last-child { margin-bottom: 0; }

  .aviso__cerrar {
    flex-shrink: 0;
    min-width: var(--tactil);
    min-height: var(--tactil);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: none;
    background: transparent;
    color: var(--tinta-media);
    font-size: var(--t-medio);
    cursor: pointer;
    border-radius: var(--radio-sm);
  }

  .aviso__cerrar:hover {
    background: color-mix(in srgb, var(--tinta) 6%, transparent);
    color: var(--tinta);
  }

  /* ── Diálogo ── */

  /* Un `<dialog>` trae un estilo por defecto de 1998: borde biselado, ancho
     pegado al contenido y pegado al borde superior. Se le da la forma del
     resto del sistema y se centra.

     En el teléfono se ancla abajo, en la zona del pulgar, y ocupa el ancho:
     una confirmación centrada en mitad de una pantalla de 6 pulgadas obliga a
     estirar la mano justo para decir «sí, bórralo». */
  .dialogo {
    padding: 0;
    border: 1px solid var(--borde);
    border-radius: var(--radio-lg) var(--radio-lg) 0 0;
    background: var(--superficie);
    color: var(--tinta);
    box-shadow: var(--sombra-alta);
    width: 100%;
    max-width: 32rem;
    margin: auto auto 0;
  }

  @media (min-width: 40rem) {
    .dialogo {
      border-radius: var(--radio-lg);
      margin: auto;
    }
  }

  .dialogo__cuerpo { padding: var(--e5) var(--e4); }
  .dialogo h2 { margin-bottom: var(--e3); }
  .dialogo .acciones { margin-top: var(--e5); }

  /* El fondo de detrás. `::backdrop` es del navegador, no un div nuestro: no
     hay forma de que se quede colgado si algo falla a medias. */
  .dialogo::backdrop {
    background: color-mix(in srgb, var(--tinta) 55%, transparent);
  }

  /* ── Barra de progreso: la completitud de una ficha ── */

  .progreso {
    height: 0.5rem;
    background: var(--superficie-alt);
    border: 1px solid var(--borde);
    border-radius: 999px;
    overflow: hidden;
  }

  .progreso__relleno {
    height: 100%;
    background: var(--verde);
    border-radius: 999px;
  }

  /* ── Rejilla ── */

  /* `auto-fit` + `minmax`: las columnas se acomodan solas y el hueco lo descuenta
     el grid. Sin porcentajes que sumen 100% MÁS el gap, que es lo que rompía el
     sistema anterior. */
  .rejilla {
    display: grid;
    gap: var(--e4);
    grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr));
    margin-bottom: var(--e4);
  }

  .rejilla--filtros {
    grid-template-columns: repeat(auto-fit, minmax(min(12rem, 100%), 1fr));
    gap: var(--e3);
    align-items: end;
  }

  /* ── Mapa ── */

  .mapa {
    height: 60vh;
    min-height: 20rem;
    border-radius: var(--radio);
    overflow: hidden;
  }

  .mapa--bajo { height: 45vh; min-height: 16rem; }

  /* Leaflet trae sus propios estilos y no cumplen nuestros umbrales: radios de
     13×13 px y una atribución en gris que da 4,94:1. Se corrige desde aquí y no
     parcheando la librería, que se pierde en la siguiente actualización.

     **Estos selectores adelgazaron el 2026-08-09** y conviene saber por qué:
     antes decían `.leaflet-touch .leaflet-bar a` y `.leaflet-container .leaflet-
     control-attribution`, con la clase de contexto puesta ahí para ganarle en
     especificidad a la librería. Ya no hace falta: Leaflet entra por
     `vendor-mapa.css` en la capa `vendor` y esta capa gana por orden declarado,
     no por tener más clases. Una clase menos por regla es una regla que se lee. */
  /* El objetivo táctil de estas dos filas es el label entero, no el círculo:
     medido a 360 px daba 93×56 y ya cumplía el perfil `campo`. El
     `padding-inline` es lo que faltaba — sin él, el label empezaba justo en el
     borde del radio y un dedo que se iba unos píxeles a la izquierda caía en el
     panel, no en la opción. */
  .leaflet-control-layers-list label {
    display: flex;
    align-items: center;
    min-height: var(--tactil);
    padding-inline: var(--e2);
    font-size: var(--t-base);
    cursor: pointer;
  }

  .leaflet-control-layers-list input[type='radio'],
  .leaflet-control-layers-list input[type='checkbox'] {
    width: var(--tactil-marca);
    height: var(--tactil-marca);
    min-height: var(--tactil-marca);
    margin-right: var(--e2);
  }

  .leaflet-bar a {
    width: var(--tactil);
    height: var(--tactil);
    line-height: var(--tactil);
    font-size: var(--t-medio);
  }

  .leaflet-control-attribution,
  .leaflet-control-attribution a {
    color: var(--tinta);
    background: color-mix(in srgb, var(--superficie) 90%, transparent);
  }

  /* El enlace de la atribución medía 50×13 px: por debajo de los 24×24 que
     WCAG 2.5.8 exige como mínimo legal —el que no cambia con el perfil— y que
     `evidencia_visual.py` marca como fallo, no como aviso.

     No se lleva a los 56 px del resto: es un crédito legal que hay que poder
     pulsar, no una acción del trabajo diario, y un objetivo de 56 px ahí
     taparía el mapa. Se le dan los 24 que la norma pide. */
  .leaflet-control-attribution a {
    display: inline-flex;
    align-items: center;
    min-height: 24px;
    padding-inline: var(--e1);
  }

  .leaflet-draw-toolbar a.leaflet-disabled,
  .leaflet-draw-actions a.leaflet-disabled {
    color: var(--tinta-media);
    background-color: var(--superficie-alt);
  }

  .leaflet-draw-actions a {
    color: var(--tinta);
    background-color: var(--superficie);
    min-height: var(--tactil);
    line-height: var(--tactil);
    font-size: var(--t-base);
  }

  .leaflet-draw-tooltip {
    background: color-mix(in srgb, var(--tinta) 95%, transparent);
    border-color: color-mix(in srgb, var(--tinta) 95%, transparent);
    color: var(--sobre-fuerte);
    font-size: var(--t-base);
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   UTILIDADES — las mínimas, y todas con token
   ═══════════════════════════════════════════════════════════════════════════ */

@layer utilidades {
  /* Solo para lectores de pantalla. Se recorta a 1×1 px en vez de ocultarse con
     `display`, porque tiene que seguir siendo legible para el lector. */
  .solo-lectura {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }

  /* Con menos de 48rem de SITIO la tabla se esconde y manda la lista de
     hileras: en ese ancho, cinco columnas son scroll horizontal o texto de
     11 px.

     `@container` y no `@media`: la pregunta correcta es «cuánto espacio tengo»,
     no «qué pantalla es esta». La misma lista metida mañana en una columna, en
     un panel partido o dentro de un diálogo seguirá acertando sin tocar nada.
     El contenedor se declara en `.contenido`. */
  .solo-ancho { display: none; }
  .solo-estrecho { display: block; }

  @container contenido (min-width: 48rem) {
    .solo-ancho { display: block; }
    .solo-estrecho { display: none; }
  }

  /* El atajo al contenido: fuera de la pantalla hasta que se le enfoca. No se
     esconde con `display: none` —eso lo sacaría del orden de tabulación y lo
     dejaría inservible— sino desplazándolo fuera del área visible. */
  .atajo {
    position: fixed;
    top: var(--e2);
    left: var(--e2);
    z-index: 100;
    transform: translateY(-200%);
    padding: var(--e3) var(--e4);
    min-height: var(--tactil);
    display: inline-flex;
    align-items: center;
    background: var(--verde);
    color: var(--sobre-fuerte);
    border-radius: var(--radio-sm);
    font-weight: 600;
    box-shadow: var(--sombra-alta);
  }

  .atajo:focus {
    transform: translateY(0);
    color: var(--sobre-fuerte);
    text-decoration: none;
  }

  /* El `<main>` recibe el foco al usar el atajo y no debe dibujar el recuadro
     de foco: no es un control, solo el destino de un salto. El anillo sí se
     mantiene en todo lo que se puede pulsar. */
  .contenido:focus {
    outline: none;
  }

  .texto-sutil { color: var(--tinta-media); }
  .texto-menor { font-size: var(--t-menor); }
  .texto-centro { text-align: center; }
  .peso-fuerte { font-weight: 700; }
  .numero { font-variant-numeric: tabular-nums; }

  .sin-margen { margin: 0; }
  .margen-arriba { margin-top: var(--e4); }
  .margen-abajo { margin-bottom: var(--e4); }

  .vacio {
    padding: var(--e6) var(--e4);
    text-align: center;
    color: var(--tinta-media);
  }

  .paginacion {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    gap: var(--e3);
    margin-top: var(--e4);
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   GRÁFICAS Y BARRAS — añadido el 2026-10-22
   ═══════════════════════════════════════════════════════════════════════════

   Dos piezas, y ninguna necesita librería:

   · `.grafica` envuelve una tabla que `graficas.js` dibuja como barras SVG. Sin
     el script, la tabla se ve tal cual. Con él, la tabla queda para el lector de
     pantalla y un botón la vuelve a enseñar.
   · `.barras` son barras horizontales pintadas en el servidor con CSS: cada fila
     es un enlace, y el ancho llega en `--proporcion` desde la vista.

   Una sola tinta de datos —el verde— porque cada gráfica es una sola serie: un
   segundo color diría que hay dos cosas distintas, y no las hay. */

@layer componentes {
  .grafica { margin: var(--e4) 0 0; }

  .grafica--viva:not(.grafica--con-tabla) table {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
  }

  .grafica__lienzo {
    position: relative;
    border-radius: var(--radio-sm);
    touch-action: manipulation;
  }
  .grafica__lienzo:focus-visible {
    outline: 3px solid var(--verde);
    outline-offset: 4px;
  }
  .grafica__lienzo svg { display: block; width: 100%; height: auto; overflow: visible; }

  .grafica__guia { stroke: var(--borde); stroke-width: 1; stroke-dasharray: 2 4; }
  .grafica__base { stroke: var(--borde-fuerte); stroke-width: 1; }
  .grafica__eje {
    fill: var(--tinta-suave);
    font: 500 var(--t-menor) / 1 var(--letra);
    font-variant-numeric: tabular-nums;
  }

  .grafica__zona { fill: transparent; cursor: pointer; }
  .grafica__barra {
    fill: color-mix(in oklch, var(--verde) 55%, var(--superficie));
    transition: fill var(--transicion);
  }
  /* El mes que se está mirando, a tinta llena: es el que cuentan las cifras
     de arriba, y así se encuentra sin leer el eje. */
  .grafica__columna--actual .grafica__barra { fill: var(--verde); }
  .grafica__columna--elegida .grafica__zona { fill: var(--verde-claro); }
  .grafica__columna--elegida .grafica__barra { fill: var(--verde-oscuro); }
  .grafica__columna--elegida .grafica__eje { fill: var(--tinta); font-weight: 700; }

  .grafica__globo {
    position: absolute;
    top: 0;
    transform: translate(-50%, -100%);
    display: grid;
    gap: 2px;
    padding: var(--e2) var(--e3);
    border-radius: var(--radio-sm);
    background: var(--tinta);
    color: var(--superficie);
    font-size: var(--t-menor);
    white-space: nowrap;
    pointer-events: none;
    box-shadow: var(--sombra-alta);
    z-index: 2;
  }
  /* `hidden` pierde contra cualquier `display` de autor; hay que repetirlo. */
  .grafica__globo[hidden] { display: none; }
  .grafica__globo strong { font-size: var(--t-medio); }
  .grafica__globo-mes { opacity: 0.85; }

  .grafica__alternar { margin-top: var(--e2); }

  .tabla--compacta th,
  .tabla--compacta td { padding: var(--e2) var(--e3); }
  .tabla--compacta td { text-align: right; }

  .barras { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--e1); }

  .barras__fila {
    display: grid;
    grid-template-columns: minmax(5.5rem, auto) 1fr 2.5rem;
    align-items: center;
    gap: var(--e3);
    min-height: var(--tactil);
    padding: 0 var(--e2);
    margin: 0 calc(-1 * var(--e2));
    border-radius: var(--radio-sm);
    color: inherit;
    text-decoration: none;
    transition: background var(--transicion);
  }
  .barras__fila:hover,
  .barras__fila:focus-visible { background: var(--superficie-alt); text-decoration: none; color: inherit; }

  .barras__pista {
    height: 0.75rem;
    border-radius: 999px;
    background: var(--superficie-alt);
    overflow: hidden;
  }
  .barras__relleno {
    display: block;
    height: 100%;
    width: var(--proporcion, 0%);
    border-radius: inherit;
    background: var(--verde);
    transform-origin: left;
    animation: barras-crecen 0.6s cubic-bezier(0.2, 0.8, 0.2, 1) both;
  }
  .barras__valor { text-align: right; font-weight: 700; }

  @keyframes barras-crecen {
    from { transform: scaleX(0); }
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   FILTRO VIVO DE LA LISTA — añadido el 2026-10-22
   ═══════════════════════════════════════════════════════════════════════════

   Chips de especie en una sola fila que se desliza con el pulgar, en vez de un
   <select>: se ve de un golpe qué especies hay y cuántas, y elegir es un toque
   en lugar de tres. Cada chip mide lo mismo que cualquier control: 56 px. */

@layer componentes {
  .filtros {
    display: flex;
    gap: var(--e3);
    align-items: end;
  }
  .filtros__campo { flex: 1; }

  .con-js .solo-sin-js { display: none; }

  .chips {
    display: flex;
    gap: var(--e2);
    margin: var(--e4) calc(-1 * var(--e4)) 0;
    padding: 0 var(--e4) var(--e1);
    overflow-x: auto;
    scroll-snap-type: x proximity;
    /* Sin esto el imán alinea el primer chip al borde del carril y se come el
       relleno: quedaba pegado al canto de la ficha, fuera de la línea del campo. */
    scroll-padding-inline: var(--e4);
    scrollbar-width: none;
  }
  .chips::-webkit-scrollbar { display: none; }

  .chip {
    flex: none;
    display: inline-flex;
    align-items: center;
    gap: var(--e2);
    min-height: var(--tactil);
    padding: 0 var(--e4);
    border: 1px solid var(--borde-fuerte);
    border-radius: 999px;
    background: var(--superficie);
    color: var(--tinta);
    font-weight: 600;
    text-decoration: none;
    scroll-snap-align: start;
    transition: background var(--transicion), border-color var(--transicion), color var(--transicion);
  }
  /* Solo con ratón: en iOS el `:hover` se queda pegado tras el toque y el chip
     que se acaba de soltar parece elegido a medias. */
  @media (hover: hover) {
    .chip:hover { background: var(--superficie-alt); text-decoration: none; color: var(--tinta); }
  }
  .chip[aria-current='true'] {
    background: var(--verde);
    border-color: var(--verde);
    color: var(--sobre-fuerte);
  }
  .chip__n {
    min-width: 1.5rem;
    padding: 0 var(--e1);
    border-radius: 999px;
    background: var(--superficie-alt);
    color: var(--tinta-media);
    font-size: var(--t-menor);
    font-variant-numeric: tabular-nums;
    text-align: center;
  }
  .chip[aria-current='true'] .chip__n {
    background: color-mix(in oklch, var(--sobre-fuerte) 22%, transparent);
    color: var(--sobre-fuerte);
  }

  .resultados { transition: opacity var(--transicion); }
  /* Mientras llega la lista nueva, la vieja se apaga un poco: se ve que algo
     pasa sin tapar lo que había con una ruedita. `htmx-request` lo pone htmx
     en el elemento de `hx-indicator` mientras dura la petición. */
  .resultados.htmx-request { opacity: 0.55; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   EL LLANO — la cabecera del panel (2026-10-22)
   ═══════════════════════════════════════════════════════════════════════════

   Un paisaje en SVG: cielo, sol o luna según la hora, la mata en el horizonte,
   la sabana y tres moriches. Pesa 2 KB y no es una foto a propósito: una foto
   del llano serían 200 KB y se vería igual a mediodía que de noche.

   Sus colores son tokens propios y decorativos: ningún texto se apoya en
   ellos, así que no entran en la regla de 7:1. El texto va debajo, sobre la
   superficie lisa de la tarjeta. */

@layer componentes {
  .llano {
    --cielo-alto: oklch(78% 0.075 232);
    --cielo-bajo: oklch(93% 0.040 88);
    --astro: var(--sol);
    --mata: oklch(46% 0.070 142);
    --sabana-lejos: oklch(71% 0.090 112);
    --sabana: oklch(80% 0.105 92);
    --moriche: oklch(32% 0.050 125);

    margin-bottom: var(--e5);
    border: 1px solid var(--borde);
    border-radius: var(--radio-lg);
    background: var(--superficie);
    box-shadow: var(--sombra);
    overflow: hidden;
  }

  .llano--tarde {
    --cielo-alto: oklch(74% 0.085 245);
    --cielo-bajo: oklch(87% 0.090 62);
    --astro: oklch(80% 0.150 62);
  }

  .llano--noche {
    --cielo-alto: oklch(24% 0.050 262);
    --cielo-bajo: oklch(38% 0.060 290);
    --astro: oklch(93% 0.030 90);
    --mata: oklch(22% 0.030 150);
    --sabana-lejos: oklch(27% 0.030 120);
    --sabana: oklch(31% 0.035 100);
    --moriche: oklch(15% 0.020 120);
  }

  /* Con el tema de noche, también el llano de día se apaga un poco: un cielo
     celeste a pleno brillo en una pantalla oscura deslumbra. */
  [data-tema='noche'] .llano:not(.llano--noche) .llano__paisaje { filter: brightness(0.72) saturate(0.9); }
  @media (prefers-color-scheme: dark) {
    :root:not([data-tema]) .llano:not(.llano--noche) .llano__paisaje { filter: brightness(0.72) saturate(0.9); }
  }

  .llano__paisaje {
    display: block;
    width: 100%;
    height: clamp(7.5rem, 24vw, 11rem);
  }
  .llano__cielo-alto { stop-color: var(--cielo-alto); }
  .llano__cielo-bajo { stop-color: var(--cielo-bajo); }
  .llano__astro { fill: var(--astro); }
  .llano__estrellas { fill: var(--astro); opacity: 0.8; }
  .llano__mata { fill: var(--mata); }
  .llano__sabana-lejos { fill: var(--sabana-lejos); }
  .llano__sabana { fill: var(--sabana); }
  .llano__moriche path {
    fill: none;
    stroke: var(--moriche);
    stroke-width: 2.2;
    stroke-linecap: round;
  }
  .llano__moriche path:first-child { stroke-width: 3; }

  .llano__texto { padding: var(--e4) var(--e4) var(--e2); }

  .llano__fecha {
    margin: 0;
    font-size: var(--t-menor);
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--tierra);
  }

  .llano__saludo {
    margin: var(--e1) 0;
    font-size: clamp(1.75rem, 6.5vw, 2.5rem);
    line-height: 1.1;
  }

  .llano__finca { margin: 0; color: var(--tinta-media); }

  /* ── El agua del año ── */
  .agua {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr));
    gap: var(--e3) var(--e4);
    margin: var(--e3) 0 0;
    padding: var(--e4);
    border-top: 1px solid var(--borde);
    background: color-mix(in oklch, var(--agua-clara) 55%, var(--superficie));
  }
  .agua__dato dt {
    font-size: var(--t-menor);
    font-weight: 600;
    color: var(--tinta-media);
  }
  .agua__dato dd { margin: var(--e1) 0 0; color: var(--tinta-media); }
  .agua__cifra {
    font-family: var(--letra-titulo);
    font-size: var(--t-grande);
    font-weight: 600;
    line-height: 1;
    color: var(--agua);
  }
  .agua__pie { display: block; font-size: var(--t-menor); }
  .agua__fuente {
    margin: 0;
    padding: var(--e2) var(--e4) var(--e3);
    font-size: var(--t-menor);
    color: var(--tinta-media);
    background: color-mix(in oklch, var(--agua-clara) 55%, var(--superficie));
  }
}

/* Respeta a quien pidió menos movimiento en su sistema operativo. Fuera de toda
   capa a propósito: es una renuncia del usuario, no una decisión de diseño, y
   tiene que ganarle a cualquier capa —incluidas las de librerías que animen por
   su cuenta— sin depender del orden que hayamos declarado arriba. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    scroll-behavior: auto !important;
  }
}
