/* MAKERS — sistema de diseño
   Valores de movimiento calibrados a partir del análisis de referencia
   (ver ~/Documents/clones/trionn.com/ANALISIS-REFERENCIA.md §2.6).
   Implementación original: ni assets, ni código, ni tipografía de la referencia. */

/* LA TIPOGRAFÍA VIVE AQUÍ, NO EN GOOGLE. Antes, las dos páginas abrían con un
   `<link>` a fonts.googleapis.com: un origen ajeno en la ruta crítica, que
   además tiene que resolverse ENTERO —CSS primero, woff2 después— antes de que
   `document.fonts.ready` (js/app.js:136) deje arrancar el troceado de títulos.
   Dos saltos de red, dos DNS y dos TLS delante de la primera letra, y en una red
   que bloquee ese dominio el arranque se queda esperando a algo que no va a
   llegar.

   Aquí están los dos pesos que usa la marca, subconjunto latino, servidos por el
   mismo origen que ya sirvió el HTML. Sólo el latino: el contenido en ES y EN no
   sale de U+0000–00FF más el guion largo y el euro, y las variantes thai y
   vietnamita de Google pesaban lo mismo sin pintar ni un glifo. La flecha «→»
   (U+2192) tampoco estaba en el subconjunto latino de Google, así que ya caía en
   la fuente del sistema antes de este cambio: no cambia nada.

   `swap` es deliberado y coincide con lo que servía Google: el texto se lee con
   la fuente de reserva mientras llega la buena. Los títulos se trocean después
   de `document.fonts.ready`, así que el corte en líneas se sigue midiendo sobre
   la tipografía definitiva. */
@font-face {
  font-family: "Bai Jamjuree";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../assets/fonts/bai-jamjuree-400-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Bai Jamjuree";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("../assets/fonts/bai-jamjuree-500-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

:root {
  /* --- dos mundos --- */
  --ink: #050505;
  --paper: #c9c7c4;
  --white: #fff;
  --font-light: #d8d8d8;
  --font-dark: #3a3a3a;
  --line-dark: #3a3a3a;
  --line-light: #d0cecb;

  /* FILETES TRANSLÚCIDOS. Los de arriba son la pareja OPACA de los dos mundos
     —papel y tinta— y no se tocan. Estos dos son los translúcidos, que estaban
     escritos a mano catorce veces con catorce valores distintos y en dos
     sintaxis (`#ffffffXX` y `color-mix`), sin que ninguna diferencia significara
     nada: 12, 14, 15 y 16 % dibujaban la misma línea.

     Sólo cubren ESTRUCTURA: separar y enmarcar. Deliberadamente NO cubren tres
     cosas que también son líneas translúcidas y que deben seguir siendo
     distintas entre sí:
       · el circuito del ecosistema, donde hilo (17 %) y nodo (24 %) son la
         leyenda del diagrama;
       · los bordes de control del configurador (28-35 %);
       · sus estados (35-45 %), donde el escalón reposo→hover es la señal.
     Un token que se los tragara a todos aplanaría distinciones que trabajan. */
  --line-sutil: color-mix(in srgb, var(--paper) 14%, transparent);
  --line-marco: color-mix(in srgb, var(--paper) 22%, transparent);

  /* CALIBRADO. Valor del sistema de diseño vigente —`makers-docs/07_design/
     design-principles.md`, revisado el 2026-07-28—: un acento y sólo uno.
     Estaba en #ff6a1a, un naranja inventado; el de la marca es este rojo.

     Y el documento no sólo da el color, da su función: el acento marca
     ESTRUCTURA —hairlines, numeración, conectores de diagrama, estados activos,
     bordes de hover— y rechaza expresamente el acento en fondos, tarjetas o
     titulares. Lo que ya se hacía aquí encaja: las escuadras del foco son una
     mira, los nodos del circuito son conectores, la brasa es emisión.

     NO se tocan las temperaturas de luz del isotipo (0xff2200 de la brasa,
     0xff4a10 de su disco): eso no es un token de interfaz, es una fuente de luz
     en una simulación calibrada, y cambiarla por un color de marca sería pintar
     la física. */
  --ember: #ff2d1a;

  /* --- ritmo --- */
  --sp: 0.25rem;
  --gut: clamp(1.25rem, 4vw, 3.5rem);
  --maxw: 80rem;

  /* --- movimiento: cuatro decisiones, repetidas --- */
  --dur: 0.62s;                              /* la duración de la casa */
  --dur-fast: 0.36s;
  --dur-slow: 1.1s;
  --ease: cubic-bezier(0.215, 0.61, 0.355, 1); /* equivalente a power3.out */
  --ease-expo: cubic-bezier(0.16, 1, 0.3, 1);
  --stagger: 0.05s;
  --delay: 0.3s;
}

/* ------------------------------------------------------ PLANO DE CAPÍTULO
   La mecánica es de TODOS los capítulos, no del hero. Un capítulo con recorrido
   mide una pantalla más su tramo, y lo que se ve dentro queda anclado arriba: el
   scroll no desplaza nada, sólo avanza `--p`.

   El recorrido NO se declara aquí. Lo escribe `capitulo.js` leyendo el
   `data-viaje` de cada sección, porque es un dato de esa escena —cuánto dura su
   historia— y no una constante del diseño. Ahí está el mando del ritmo: subir o
   bajar ese número alarga o acorta un capítulo sin tocar nada más.

   El atributo `data-fijado` lo pone el motor, y es lo que hace que sin
   JavaScript, o con movimiento reducido, todo esto sencillamente no exista y la
   sección se comporte como una sección. */
[data-capitulo] { --p: 0; }
[data-capitulo][data-fijado="si"] {
  display: block; padding: 0;
  min-height: 0; height: calc(100svh + var(--viaje));
}
[data-fijado="si"] [data-plano] {
  position: sticky; top: 0;
  height: 100svh; overflow: hidden;
}

*, *::before, *::after { box-sizing: border-box; }
html {
  -webkit-text-size-adjust: 100%;
  /* EL DESBORDAMIENTO, RECORTADO EN LA RAÍZ. En un móvil de 375 px el navegador
     medía el viewport en 450: algún elemento del recorrido se sale por la
     derecha, y Chrome ensancha el viewport de lo FIJO hasta cubrirlo. El menú,
     el velo y el visor de fotos se calculaban sobre 450 px y su borde derecho
     quedaba fuera de la pantalla. Medido: con `clip` aquí, 450 → 375.

     `clip` y no `hidden`: `hidden` convierte al elemento en contenedor de scroll
     y le quita el `position: sticky` a todos los capítulos que se anclan por
     dentro; `clip` recorta sin crear contenedor. */
  overflow-x: clip;
}

body {
  margin: 0;
  background: var(--ink);
  color: var(--font-light);
  font-family: "Bai Jamjuree", system-ui, sans-serif;
  font-weight: 400;
  overflow-x: clip;
}

/* Bloqueo de scroll mientras el preloader manda */
html:not([data-mk-ready]) body { overflow: hidden; height: 100vh; }

img, svg, canvas, video { display: block; max-width: 100%; }
a { color: inherit; text-decoration: none; }

/* ---------------------------------------------------------------
   TIPOGRAFÍA — una sola familia, tres bandas que no se solapan.
   El tamaño es la etiqueta semántica: por debajo de 13px sólo hay
   etiquetas, de 14 a 17 sólo cuerpo, de 18 arriba sólo display.
   --------------------------------------------------------------- */
.t-display, h1, h2 {
  font-weight: 400;                /* nunca bold: jerarquía por escala y aire */
  line-height: 0.9;
  letter-spacing: -0.06em;
  margin: 0;
  text-wrap: balance;
}
h1, .t-xxl { font-size: clamp(2.75rem, 6.25vw, 5.625rem); }  /* 90px @1440 */
/* UN SOLO VALOR POR NIVEL. Esto era `clamp(2.25rem, 5.5vw, 8.25rem)` y no era
   una escala: era una medida de DISPLAY —8,25rem son 132px— que `h2` heredaba de
   `.t-xl`, una clase con cero usos en todo el HTML.

   El resultado, medido a 1280: el mismo `<h2>` valía 70px o 35px según si su
   sección llevaba por casualidad la clase `.esc`. Y estaba invertido. Los
   titulares que cargan el argumento —«El fallo no está en ninguna de las piezas.
   Está en el relevo.»— salían a 35px, y rótulos de dos palabras como «Trabajo
   seleccionado» a 70. El tamaño decía lo contrario que el contenido.

   Ahora hay un valor y la jerarquía la hace el aire, que es lo que manda el
   principio 3: «antes de agrandar algo, se le da más aire». */
h2, .t-xl  { font-size: clamp(1.9rem, 3.1vw, 2.9rem); }
.t-l       { font-size: clamp(1.5rem, 2.6vw, 3.15rem); line-height: 1; letter-spacing: -0.04em; }

/* Cuerpo y etiquetas HEREDAN el color de la sección. Fijarlo aquí obligaba a
   un override por cada mundo, y el que faltara dejaba texto gris sobre gris. */
.t-body, p {
  font-size: 1.0125rem;            /* 16,2px */
  line-height: 1.6;
  letter-spacing: 0;
  max-width: 46ch;
}

/* La voz "sistema": mayúscula + tracking ancho + cifras tabulares.
   Sustituye a la monoespaciada sin romper la regla de familia única. */
.t-label, .label {
  font-size: 0.7875rem;            /* 12,6px */
  text-transform: uppercase;
  letter-spacing: 0.16em;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  opacity: 0.75;
}

/* DOS NIVELES, NO UNO. `.cta` era el único patrón de botón y hacía de todo: la
   llamada principal de la película era un enlace de 12,6px y 22px de alto,
   tipográficamente IDÉNTICO a `.t-label`, que es una etiqueta decorativa. La
   acción que decide si te contratan pesaba lo mismo que un rótulo, y en táctil
   ni siquiera llevaba subrayado, porque el subrayado sólo aparecía al pasar el
   ratón — un gesto que en un móvil no existe.

   Ahora `.cta` se queda como navegación terciaria y la conversión usa
   `.cta--primario`: pastilla rellena de 44px, que es el objetivo táctil mínimo
   y la forma que el sistema de diseño ya declara vigente («Botones: Pill»).
   Es además el vocabulario que ya usa el configurador (`.cfg-btn`), así que la
   web deja de hablar dos idiomas de botón. */
.cta {
  /* 44 px es el objetivo táctil mínimo, y lo necesitan LOS DOS niveles de botón:
     estaba sólo en `.cta--primario`, así que «Ver qué hacemos» o «Escríbenos»
     medían 22 y 26 px de alto — imposibles de acertar con el pulgar. */
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  gap: 0.6em;
  font-size: 0.7875rem;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  padding-bottom: 0.5em;
  position: relative;
}
.cta::after {
  content: "";
  position: absolute;
  left: 0; bottom: 0;
  width: 100%; height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur-fast) var(--ease);
}
.cta:hover::after, .cta:focus-visible::after { transform: scaleX(1); }

/* La variante de conversión. Misma voz tipográfica, otro peso visual. */
.cta--primario {
  min-height: 44px;
  padding: 0.9em 1.9em;
  border: 1px solid var(--white);
  border-radius: 999px;
  background: var(--white);
  color: var(--ink);
  font-weight: 500;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
/* El subrayado de `.cta` sobra dentro de una pastilla rellena. */
.cta--primario::after { content: none; }
/* Y la sombra global de `main` también. Existe para separar el texto claro del
   3D; aquí el fondo es opaco y la letra oscura, así que no separa nada y sucia
   el trazo. Al pasar el cursor la pastilla se vacía y la letra vuelve a quedar
   sobre la película: ahí la sombra hace falta otra vez. */
.cta--primario { text-shadow: none; }
.cta--primario:hover, .cta--primario:focus-visible {
  text-shadow: 0 0 0.5em rgba(5, 5, 5, 0.92), 0 0 1.4em rgba(5, 5, 5, 0.7);
}
.cta--primario:hover, .cta--primario:focus-visible { background: transparent; color: var(--white); }
/* `--dur-fast` y no `--dur`: el subrayado de arriba y esta flecha son el mismo
   botón y tenían que moverse juntos. 0,62 s es el tiempo de la película; una
   microinteracción de UI que dura lo mismo se siente pesada. */
.cta .arrow { transition: transform var(--dur-fast) var(--ease); }
.cta:hover .arrow { transform: translateX(0.35em); }

/* ANILLO DE FOCO DE DOS TONOS. Uno solo no valía aquí: el fondo de esta web
   barre todo el rango de luminancia —de la brasa al negro, y del negro al papel
   del preloader—, así que el rojo medía 1,19:1 contra el gris medio de la escena
   y 2,21:1 sobre el papel. Un anillo de foco que no se ve no es un anillo de
   foco. Con el trazo negro y el blanco alrededor se lee sobre cualquier fondo.
   No toca el canon: el foco es «estado activo», que es de los sitios donde el
   documento dice expresamente que el acento SÍ debe ir. */
:focus-visible {
  outline: 2px solid var(--ember);
  outline-offset: 3px;
  box-shadow: 0 0 0 5px #050505, 0 0 0 7px rgba(255, 255, 255, 0.85);
}

/* La salida sin JavaScript. Va sobre el papel de la casa, no sobre el negro:
   sin el 3D detrás, un fondo negro con texto suelto parece una web rota. */
.nojs {
  position: relative; z-index: 9999;
  min-height: 100vh;
  display: flex; flex-direction: column; justify-content: center;
  gap: 1.1rem;
  padding: var(--gut);
  background: var(--paper); color: var(--ink);
}
.nojs h1 { font-size: clamp(2.25rem, 6vw, 4.5rem); font-weight: 500; letter-spacing: -0.03em; margin: 0; }
.nojs p { max-width: 46ch; margin: 0; }
.nojs a { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }
.nojs__nota { font-size: 0.85rem; opacity: 0.6; }

.skip {
  position: absolute; left: -9999px;
  background: var(--paper); color: var(--ink);
  padding: 1rem 1.5rem; z-index: 10000;
}
.skip:focus { left: 1rem; top: 1rem; }

/* --------------------------------------------------------------- LAYOUT */
.wrap { width: 100%; max-width: var(--maxw); margin-inline: auto; padding-inline: var(--gut); }

.section {
  position: relative;
  min-height: 100dvh;              /* la unidad de composición es la pantalla */
  padding-top: 6.25rem;            /* 100px arriba / 80px abajo: asimétrico a propósito */
  padding-bottom: 5rem;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
@media (min-width: 1024px) {
  .section { padding-top: 9.375rem; padding-bottom: 10rem; }
}

/* --------------------------------------------------------------- EL UNIVERSO
   REGLA ABSOLUTA: el universo no es el fondo del hero, es el fondo de la web.
   Fijo al viewport y vivo de la primera pantalla a la última. Nunca se sustituye
   por un color, nunca se interrumpe.

   Con esto desaparecen los DOS MUNDOS —tinta y papel— y las costuras que los
   cosían. Eran una buena decisión mientras la web era una web: dos fondos
   opacos alternándose y un degradado de 40vh para pasar de uno a otro. Pero un
   fondo opaco, por bonito que sea, TAPA el universo, y a partir de ahora eso es
   exactamente lo que no puede pasar. La jerarquía que daba el cambio de fondo
   ahora la tiene que dar la CÁMARA: no otro color, otro lugar. */
#universo {
  position: fixed; inset: 0; z-index: 0;
  pointer-events: none;              /* el contenido de encima sigue recibiendo el ratón */
  background: var(--ink);            /* respaldo si WebGL no arranca: negro, nunca blanco */
}
#universo canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }

/* EL VELO. Mismo z-index que el universo y después de él en el DOM: pinta encima
   del 3D y debajo del contenido sin alterar la pila de nadie.

   `--velo` la escribe js/velo.js desde el crucero. En reposo vale 0, así que si
   ese módulo no llegara a cargar la web se ve exactamente como antes.

   `--velo-max` es el mando: sube o baja la densidad de la tela sin tocar el JS. */
.velo {
  /* Más tela que antes (estaba en 0,32 y luego en 0,52). Con la nube a plena luz
     por detrás, el cuerpo de texto se leía sobre trozos brillantes en
     movimiento. Y desde que el isotipo es plástico con reflejos de estudio, los
     trozos devuelven bastante más luz que cuando este número se calibró: el
     fondo volvía a competir con el texto en las secciones de abajo. 0,72 deja la
     nube como una presencia detrás del contenido, que es su papel, sin llegar a
     taparla del todo. */
  --velo-max: 0.72;                  /* el mando: densidad de la tela */
  position: fixed; inset: 0; z-index: 0;
  pointer-events: none;
  background: var(--ink);
  opacity: var(--velo, 0);
  /* Sin transición: la opacidad la mueve el scroll fotograma a fotograma, y una
     transición encima la haría ir por detrás del dedo. */
}

/* Todo el contenido flota SOBRE el universo. Sin fondo propio: el fondo ya está
   puesto, y está vivo. */
main, footer, .hdr { position: relative; z-index: 1; }

/* Y POR ESO EL TEXTO LLEVA SOMBRA: el fondo está vivo, así que a veces pasa un
   trozo por detrás de una palabra.

   Está medido, porque la impresión era otra. Renderizando la escena a un
   objetivo propio y leyendo los píxeles bajo cada bloque de texto —con el ACES y
   el sRGB del OutputPass aplicados a mano, que si no los valores no son los de
   la pantalla— el contraste MEDIO sale entre 13 y 15:1. Altísimo. El fondo no
   está claro.

   El problema es local: sobre 35 muestras de los cuatro apartados de contenido,
   la fracción del área de texto que cae por debajo de 3:1 da mediana 1,15%,
   percentil 90 del 4,76% y máximo del 12,14%. Diecinueve de las treinta y cinco
   pasan del 1%. Y en esas manchas el contraste llega a 1,0:1 — letras que
   sencillamente no están. Un promedio sano escondiendo palabras que desaparecen.

   La sombra ataca eso y sólo eso: oscurece el halo del glifo, no pone un panel
   detrás del texto. Sobre negro es invisible, así que no toca el diseño de texto
   flotando sobre el universo; sobre una lámina iluminada devuelve la palabra.
   Dos capas: una ceñida y fuerte, otra ancha y suave.

   Ojo con el `em`: se resuelve AQUÍ, contra el cuerpo de `main`, y lo que se
   hereda ya son píxeles. El titular de 90 px recibe los mismos 8 y 22,4 que un
   pie de 12. Escribí lo contrario en este comentario y lo comprobé: la sombra
   computada de `.hero__h` son 8px y 22.4px, no proporcionales a su tamaño.

   Y está bien que sea así, aunque no fuera lo que pretendía. El glifo es opaco:
   el fondo no se ve A TRAVÉS de la letra, se ve ALREDEDOR. Lo que hay que
   oscurecer es el borde del trazo, y ese borde mide lo mismo tenga la letra 12
   px o 90. Quien se pierde sobre una mancha clara es el texto pequeño —en la
   medición, «de los sistemas se entregan»— y para ése 8 px son generosos.

   Coste medido en el hero, tres repeticiones alternando: mediana 59 fps sin
   sombra y 67 con ella, con los rangos solapados. No cuesta nada. */
main, footer, .hdr {
  text-shadow: 0 0 0.5em rgba(5, 5, 5, 0.92), 0 0 1.4em rgba(5, 5, 5, 0.7);
}

.head { display: flex; flex-direction: column; gap: 1.5rem; margin-bottom: 4rem; }

/* ------------------------------------------------------ TÍTULOS · REVELADO
   Cada línea sube desde detrás de su propia máscara. El troceado lo hace
   `js/titulos.js` midiendo el reflujo real, no partiendo por caracteres.

   EL DETALLE QUE SE OLVIDA: `overflow: hidden` en una caja de texto recorta los
   DESCENDENTES —la g, la j, la p— y el título aparece con las colas cortadas.
   Se arregla dándole a la máscara un colchón abajo y quitándoselo al flujo con
   un margen negativo del mismo tamaño: la caja crece para no cortar, pero no
   ocupa más. Sin esto el efecto parece un error de fuente. */
h1.tit, h2.tit { white-space: normal; }
.tit__linea { display: block; }
.tit__clip {
  display: inline-block; overflow: hidden; vertical-align: top;
  padding-bottom: 0.16em; margin-bottom: -0.16em;
}
.tit__clip > span { display: inline-block; will-change: transform, opacity; }
/* La palabra es la unidad del foco; la línea, la del revelado. Por eso hay un
   nivel más de span y no una lista plana. */
.tit__pal { display: inline-block; }

/* --------------------------------------------------------- TÍTULOS · FOCO
   De la tríada sólo una palabra está enfocada; las otras dos se van al fondo.
   Y se van con DESENFOQUE, no con opacidad: una palabra atenuada es la misma
   palabra más floja, una desenfocada está más lejos. Es profundidad de campo,
   y por eso no hay ni una sombra dibujada en todo el efecto.

   El desenfoque va en `em`, no en píxeles, y así se conserva cuando el titular
   encoge en móvil. En píxeles fijos, a tamaño pequeño la palabra se volvería
   ilegible y a tamaño grande el desenfoque desaparecería. */
.foco { position: relative; }
[data-foco] {
  /* Se anima el COLOR, no la opacidad, y no es indiferente: las palabras de la
     puerta ya traen su propia animación de entrada sobre `opacity` con
     `fill: both`, así que cualquier opacidad declarada aquí quedaría anulada
     por ella. El alfa del color no lo toca nadie. */
  transition: filter 0.5s var(--ease), color 0.5s var(--ease);
  /* SIN `filter: blur(0)` EN REPOSO, y la diferencia se mide. Declarar un filtro
     —aunque sea de valor cero— promociona el elemento a su propia capa de
     compositor, y aquí hay 124 palabras a la vez en pantalla: 124 capas
     sostenidas para un efecto que sólo existe al pasar el cursor. Medido en el
     tramo del Sistema, 98 fps con ellas y 104 sin ellas.

     La transición no se pierde: de `none` a `blur()` el navegador interpola
     tratando `none` como el filtro identidad, que es exactamente el blur(0) que
     estaba escrito aquí. El efecto es el mismo; lo que desaparece es el coste
     de mantenerlo preparado. */
}
/* `color-mix` con `currentColor` y no un color literal: el mismo titular vive
   sobre tinta y sobre papel, y un gris fijo se volvía invisible en uno de los
   dos mundos. Aquí el fondo es siempre el color de la sección, más apagado —
   la regla de que el cuerpo hereda el color de su mundo, aplicada al foco. */
[data-foco].is-lejos {
  filter: blur(0.05em);
  color: color-mix(in srgb, currentColor 42%, transparent);
}

/* El efecto es de hover, así que el titular tiene que poder recibir el cursor.
   Dentro del plano del viaje las escenas van con `pointer-events: none` para
   no secuestrar el estallido, y eso también apagaba el hover del titular. */
h1.tit, h2.tit { pointer-events: auto; }

/* El marco de esquinas persigue a la palabra enfocada. Sólo cuatro escuadras:
   un rectángulo completo encajonaría la palabra, y esto tiene que leerse como
   una mira que apunta, no como un borde. Mismo lenguaje que las cruces de las
   esquinas de la puerta y que los nodos del circuito. */
.foco__marco {
  position: absolute; top: 0; left: 0; opacity: 0;
  pointer-events: none;
  transition:
    transform 0.5s cubic-bezier(0.4, 0, 0.2, 1),
    width     0.5s cubic-bezier(0.4, 0, 0.2, 1),
    height    0.5s cubic-bezier(0.4, 0, 0.2, 1),
    opacity   0.3s var(--ease);
}
/* Las escuadras miden en `rem`, NO en `em`, y es una decisión de sistema, no un
   descuido: en `em` crecerían con cada titular y el marco competiría con la
   palabra que señala. A tamaño fijo y pequeño se leen como una mira —el mismo
   registro que las cruces de las esquinas de la puerta y los nodos de 1 px del
   circuito—, y funcionan igual sobre un h1 de 90 px que sobre un h2 de 36. */
.foco__esq {
  position: absolute; width: 0.34rem; height: 0.34rem;
  border: 0 solid var(--ember);
}
/* Van FUERA de la caja de la palabra: pegadas al canto tocarían las astas de
   las mayúsculas y se leerían como parte de la letra. */
.foco__esq--si { top: -0.16rem; left: -0.16rem;  border-top-width: 2px; border-left-width: 2px; }
.foco__esq--sd { top: -0.16rem; right: -0.16rem; border-top-width: 2px; border-right-width: 2px; }
.foco__esq--ii { bottom: -0.16rem; left: -0.16rem;  border-bottom-width: 2px; border-left-width: 2px; }
.foco__esq--id { bottom: -0.16rem; right: -0.16rem; border-bottom-width: 2px; border-right-width: 2px; }

/* --------------------------------------------------------------- HEADER */
.hdr {
  position: fixed; inset: 0 0 auto 0; z-index: 900;
  display: flex; align-items: center; justify-content: space-between;
  padding: 1.25rem var(--gut);
  color: #fff;
  /* La sombra de la casa, la misma que protege al resto del texto sobre el 3D. */
  text-shadow: 0 0 0.5em rgba(5, 5, 5, 0.92), 0 0 1.4em rgba(5, 5, 5, 0.7);
}

/* EL VELO DE LA CABECERA. Sin él, la barra fija imprimía su texto encima del
   contenido en TODAS las anchuras —no sólo en móvil—: «Aplicación textil» y
   «MAKERS» se mezclaban letra sobre letra en `#trabajo`, y lo mismo en cuanto
   una sección scrollea de verdad. Se notaba poco porque casi todos los
   capítulos centran su contenido verticalmente y rara vez llegan arriba.

   Y por eso se ha ido `mix-blend-mode: difference`. Estaba para que la barra se
   invirtiera sola sobre los fondos claros, pero tiene dos costes: con difference
   la navegación DESAPARECE sobre las láminas claras del etalonaje —el resultado
   de invertir un gris medio es otro gris medio—, y además haría inútil este
   velo, porque restar negro es la identidad y el degradado no se vería.

   Con el velo detrás, el blanco fijo se lee sobre cualquier fondo, claro u
   oscuro, sin depender de lo que haya debajo. Es menos ingenioso y funciona
   siempre, que es el orden correcto de prioridades.

   Degradado y no caja: se apaga hacia abajo para que no aparezca un canto duro
   a mitad de pantalla. No lleva acento y no es una tarjeta, así que no toca la
   regla del canon — bajar la luz no es pintar de color. */
.hdr::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: calc(100% + 3rem);
  z-index: -1;
  /* Opaco en la franja que ocupa la barra y apagándose sólo por debajo: si el
     degradado empieza a aclarar dentro del texto de la cabecera, el título que
     pasa por detrás se sigue leyendo entre las letras. */
  background: linear-gradient(to bottom, #050505 0%, #050505 62%, rgba(5, 5, 5, 0) 100%);
  pointer-events: none;
}
.hdr__nav { display: none; gap: 2rem; }
@media (min-width: 768px) { .hdr__nav { display: flex; } }
.hdr__mark { font-size: 0.7875rem; letter-spacing: 0.16em; text-transform: uppercase; }
.hdr__fin { display: flex; align-items: center; gap: 1.25rem; }

/* Interruptor de la música. Disco de 32 px con el fondo apenas insinuado, como
   la referencia: tiene que estar ahí sin pedir turno. */
.son {
  display: grid; place-items: center;
  width: 2rem; height: 2rem; flex-shrink: 0;
  border: 0; border-radius: 50%;
  background: rgb(255 255 255 / 0.1);
  color: inherit; cursor: pointer;
  transition: background var(--dur) var(--ease);
}
.son:hover { background: rgb(255 255 255 / 0.2); }
.son svg { grid-area: 1 / 1; width: 1rem; height: 0.875rem; fill: currentColor; stroke: currentColor; }
/* Sólo uno de los dos iconos a la vez, y manda el estado real del audio. */
.son__on { display: none; }
.son[aria-pressed="true"] .son__on { display: block; }
.son[aria-pressed="true"] .son__off { display: none; }

/* --------------------------------------------------------- ESCENAS DEL ACTO I
   El texto de una escena no es una sección: es un rótulo sobre una toma. Vive en
   una esquina, ocupa poco y deja el centro libre, porque lo que hay que mirar
   está detrás. La regla 02 en CSS: si el bloque de texto compite con la cámara,
   el bloque está mal. */
.esc { padding: 0; }
.esc__plano { display: flex; align-items: center; }
.esc__texto {
  max-width: 33rem;
  display: flex; flex-direction: column; align-items: flex-start;
  gap: 1.15rem;
}
/* Cada escena coloca su texto en un sitio distinto del cuadro. No es variedad
   por variedad: el travelling del 03 va de izquierda a derecha, así que el texto
   se queda a la izquierda y no persigue a la cámara; el 05 mira desde arriba y
   el peso visual cae al otro lado. */
.esc--centro .esc__texto { margin-inline: auto; align-items: center; text-align: center; }
.esc__texto--der { margin-left: auto; margin-right: 0; }

/* El titular cede tamaño para que lo primero que se recuerde sea la imagen.
   Regla: si un usuario recuerda una escena, primero la imagen, después las
   palabras. Un h2 a escala de portada invierte ese orden. */
/* Sin `font-size` propio: los titulares de escena usan el mismo nivel que el
   resto de `h2`. Lo que sí es suyo es la medida de línea — son frases largas
   dentro de un plano, y a 20ch rompen donde deben. */
.esc h2 { max-width: 20ch; }
.esc .t-body { max-width: 38ch; opacity: 0.66; }

/* El remate es la frase que cierra la escena y abre la siguiente. Va colgado de
   la línea de acento, igual que la tesis del hero: estructura, no adorno. */
.esc__remate {
  padding-left: 1rem; border-left: 1px solid var(--ember);
  font-size: clamp(1rem, 1.4vw, 1.2rem); line-height: 1.45;
  max-width: 34ch; white-space: pre-line;
}
.esc--centro .esc__remate { border-left: 0; border-top: 1px solid var(--ember); padding: 0.9rem 0 0; }
.esc__remate--grande { font-size: clamp(1.15rem, 1.9vw, 1.6rem); max-width: 30ch; }
.esc__h2 { font-size: clamp(1.3rem, 2.4vw, 2rem); line-height: 1.15; max-width: 24ch; letter-spacing: -0.03em; }

/* LOS TRES CASOS DEL COSTE. No es una lista de características: son tres escenas
   de un día cualquiera, y tienen que leerse de una en una. Sin viñeta —una
   viñeta las convierte en inventario— y colgadas de una hairline que las alinea
   sin agruparlas. Cada `li` trae su propio `--desde`, así que el escalonado lo
   pone el scroll y no una transición. */
.coste__casos { list-style: none; margin: 2rem 0 0; padding: 0; max-width: 40ch; }
.coste__casos li {
  padding: 0.9rem 0 0.9rem 1rem;
  border-left: 1px solid var(--line-sutil);
  font-size: clamp(0.95rem, 1.25vw, 1.0125rem); line-height: 1.5;
}

/* El pie cierra el descenso antes del remate: constata, no sentencia. Por eso
   no lleva el acento —ése es el papel del remate— y va a media opacidad. */
.esc__pie { margin-top: 2rem; max-width: 34ch; font-size: clamp(0.95rem, 1.25vw, 1.0125rem); opacity: 0.6; }

/* La salida no pertenece a su escena: es el puente a la siguiente. Voz de
   etiqueta para que se lea como rótulo de tránsito y no como una frase más. */
.esc__salida {
  margin-top: 2.5rem; max-width: 40ch;
  font-size: 0.7875rem; text-transform: uppercase; letter-spacing: 0.16em;
  line-height: 1.6; opacity: 0.5;
}

/* El inventario: cinco filas que se encienden con la cámara. Numeración a la
   izquierda y una hairline por fila — el lenguaje de diagramas de la casa. */
.piezas { list-style: none; margin: 0.35rem 0 0; padding: 0; display: grid; gap: 0; width: min(27rem, 100%); }
.pieza {
  display: grid; grid-template-columns: 2.5rem 1fr; gap: 0.15rem 1rem;
  padding: 0.85rem 0; border-top: 1px solid var(--line-sutil);
  opacity: var(--k, 1); transform: translateY(calc((1 - var(--k, 1)) * 0.7rem));
}
.pieza:last-child { border-bottom: 1px solid var(--line-sutil); }
.pieza__n { grid-row: 1 / 3; opacity: 0.5; padding-top: 0.2rem; }
.pieza__t { font-size: 0.98rem; letter-spacing: -0.01em; }
.pieza__d { font-size: 0.9rem; opacity: 0.6; }

/* Las cinco capas de la escena 07. SIN NUMERACIÓN, y no es una omisión: un número
   las ordena, y la escena entera dice que no hay orden. El inventario de la 03 sí
   va numerado porque allí la lista es el argumento; aquí lo es el conjunto. */
.sistema__hint { display: block; margin: 0 0 0.9rem; opacity: 0.5; }

/* Antes cada tiempo del recorrido era UNA palabra y cabía en 22rem. Ahora lleva
   quién lo hace, titular y descripción, así que la columna sube a 34rem — y se
   queda ahí a propósito: la mitad derecha del plano se deja libre para el
   gráfico de cada paso. El número va en --ember porque el sistema de diseño le
   asigna al acento la numeración, no los fondos. */
.capas { list-style: none; margin: 0.35rem 0 0; padding: 0; display: grid; gap: 0; width: min(34rem, 100%); }
.capa {
  padding: 1.1rem 0; border-top: 1px solid var(--line-sutil);
  letter-spacing: -0.01em;
}
.capa:last-child { border-bottom: 1px solid var(--line-sutil); }
.capa__cabeza { margin: 0; display: flex; align-items: baseline; gap: 0.75rem; }
.capa__cabeza .capa__n { color: var(--ember); }  /* .t-label fija color: sin subir
   especificidad, el número salía en gris y se perdía la numeración en acento,
   que es justo uno de los usos que el sistema de diseño le reserva a --ember. */
.capa__quien { opacity: 0.5; }
.capa__t {
  margin: 0.35rem 0 0; font-size: 1.3rem; font-weight: 500;
  line-height: 1.18; letter-spacing: -0.02em;
}
.capa__d {
  margin: 0.4rem 0 0; font-size: 0.95rem; line-height: 1.5;
  opacity: 0.7; max-width: 46ch;
}

/* ================================================== LA HOJA DE CONTACTOS
   El porfolio, por fin con imágenes. Marco uniforme y numerado, como una hoja
   de contactos de verdad: es el formato que el canon señala como puente entre
   la mitad audiovisual y la de automatización —«una rejilla de contactos y un
   diagrama de flujo son, visualmente, la misma familia»—.

   Las fotos vienen en siete proporciones distintas, de 9:16 a 2.39:1. Un marco
   fijo con `object-fit: cover` es lo que las convierte en una hoja en vez de en
   un mosaico irregular, y es además lo que hace la hoja de contactos física. */
.hoja {
  list-style: none;
  margin: 3.5rem 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
  gap: 1px;
  background: var(--line-sutil);
  border: 1px solid var(--line-sutil);
}
.hoja__f {
  position: relative;
  aspect-ratio: 4 / 5;
  overflow: hidden;
  background: var(--ink);
}
.hoja__f img {
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
  /* En reposo entra un punto apagada para que la rejilla se lea como conjunto;
     al señalarla recupera su color entero, que es lo que manda el canon: el
     contenido es la única fuente de color libre. */
  opacity: 0.72;
  transition: opacity var(--dur) var(--ease), transform var(--dur-slow) var(--ease);
}
.hoja__f:hover img, .hoja__f:focus-within img { opacity: 1; transform: scale(1.03); }

.hoja__n {
  position: absolute;
  left: 0.5rem; bottom: 0.4rem;
  font-size: 0.65rem;
  letter-spacing: 0.12em;
  font-variant-numeric: tabular-nums;
  color: var(--paper);
  opacity: 0.55;
  text-shadow: 0 0 0.5em rgba(5, 5, 5, 0.95);
  transition: color var(--dur-fast) var(--ease), opacity var(--dur-fast) var(--ease);
}
.hoja__f:hover .hoja__n { color: var(--ember); opacity: 1; }

@media (max-width: 767px) {
  .hoja { grid-template-columns: repeat(3, 1fr); margin-top: 2.5rem; }
}
@media (prefers-reduced-motion: reduce) {
  .hoja__f img { transition: none; }
  .hoja__f:hover img { transform: none; }
}

/* =============================================== EL ECOSISTEMA (cap. 03b)
   Las cinco capas, abiertas. El capítulo `sistema` las nombra; éste dice qué
   son, qué te llevas y cuánto cuestan.

   El acordeón es <details name> nativo. Cero JavaScript: exclusividad, teclado,
   lector de pantalla y el estado `open` para el CSS vienen de fábrica. Escribir
   esto a mano habría sido cien líneas peores.

   El circuito sigue el lenguaje de `07_design/visual-language.md`: esquina
   recta, 1px, ángulos rectos, una sola flecha, relleno = activa. El acento sólo
   toca estructura —numeración, hilos, nodo activo—, nunca fondos ni titulares. */
/* Este capítulo es el único de la película que se LEE en vez de leerse de un
   vistazo: dos mil píxeles de texto denso, no una frase. Las esquirlas del
   isotipo lo cruzan por detrás y el `text-shadow` global se queda corto cuando
   pasa un trozo iluminado justo debajo de un párrafo.

   El velo va enmascarado —se apaga hacia los bordes— para que no aparezca una
   caja con canto: sigue viéndose la película alrededor, y donde hay que leer
   baja el fondo. No es una tarjeta, y no lleva acento: el canon prohíbe el rojo
   en fondos, no prohíbe bajar la luz. */
/* El alto que compensa haber quitado `data-viaje`. Sin esto el capítulo mide lo
   que mide su contenido y su tramo del crucero se queda muy por debajo del de
   sus vecinos, que es lo que produce el latigazo de cámara documentado en
   `#datos`. Con 200vh el recorrido es comparable y el contenido no se recorta,
   porque aquí la sección crece en lugar de pinear. */
#ecosistema { min-height: 200vh; }

.eco { margin: 3rem 0 0; position: relative; isolation: isolate; }
.eco::before {
  content: "";
  position: absolute;
  inset: -3rem -4rem;
  z-index: -1;
  background: var(--ink);
  opacity: 0.74;
  -webkit-mask-image: radial-gradient(115% 92% at 50% 50%, #000 58%, transparent 100%);
  mask-image: radial-gradient(115% 92% at 50% 50%, #000 58%, transparent 100%);
}
.eco__intro { margin: 1.25rem 0 0; opacity: 0.7; }

.eco__circuito { display: block; width: 100%; max-width: 60rem; height: auto; margin: 2.5rem 0 3rem; }
.eco__hilo, .eco__retorno { fill: none; stroke: #ffffff2b; stroke-width: 1; }
.eco__retorno { stroke: var(--ember); opacity: 0.55; }
.eco__circuito marker path { fill: var(--ember); }
.eco__nodo rect {
  fill: none; stroke: #ffffff3d; stroke-width: 1;
  transition: fill var(--dur-fast) var(--ease), stroke var(--dur-fast) var(--ease);
}
.eco__nodo text {
  fill: #ffffff8a; font-size: 21px; font-weight: 500;
  font-variant-numeric: tabular-nums; text-anchor: middle;
  transition: fill var(--dur-fast) var(--ease);
}
/* Discontinuo = automatizado. Es la leyenda del lenguaje, no un efecto. */
.eco__nodo--auto rect { stroke-dasharray: 5 4; }

/* Relleno = activa. Cinco reglas explícitas en vez de un atributo por nodo:
   el orden del marcado ya dice cuál es cuál, y así no hay estado que sincronizar. */
.eco:has(.eco__capa:nth-child(1) .eco__det[open]) .eco__nodo:nth-of-type(1) rect,
.eco:has(.eco__capa:nth-child(2) .eco__det[open]) .eco__nodo:nth-of-type(2) rect,
.eco:has(.eco__capa:nth-child(3) .eco__det[open]) .eco__nodo:nth-of-type(3) rect,
.eco:has(.eco__capa:nth-child(4) .eco__det[open]) .eco__nodo:nth-of-type(4) rect,
.eco:has(.eco__capa:nth-child(5) .eco__det[open]) .eco__nodo:nth-of-type(5) rect {
  fill: var(--ember); stroke: var(--ember);
}
.eco:has(.eco__capa:nth-child(1) .eco__det[open]) .eco__nodo:nth-of-type(1) text,
.eco:has(.eco__capa:nth-child(2) .eco__det[open]) .eco__nodo:nth-of-type(2) text,
.eco:has(.eco__capa:nth-child(3) .eco__det[open]) .eco__nodo:nth-of-type(3) text,
.eco:has(.eco__capa:nth-child(4) .eco__det[open]) .eco__nodo:nth-of-type(4) text,
.eco:has(.eco__capa:nth-child(5) .eco__det[open]) .eco__nodo:nth-of-type(5) text {
  fill: var(--ink);
}

/* ---------------------------------------------------- las cinco filas */
.eco__capas { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--line-sutil); }
.eco__capa { border-bottom: 1px solid var(--line-sutil); transition: border-color var(--dur-fast) var(--ease); }

/* Cinco filas que se abren y ninguna decía al ratón que se podía pulsar. El
   acento va donde el canon lo autoriza —filete y estado, no el titular— y
   reaprovecha lo que ya hay: el borde de arriba y el signo, que vive a 0,7. */
@media (hover: hover) {
  .eco__capa:hover { border-bottom-color: var(--ember); }
  .eco__sum:hover .eco__signo::before,
  .eco__sum:hover .eco__signo::after { opacity: 1; }
}

.eco__sum {
  display: grid;
  grid-template-columns: 3rem minmax(0, 12rem) minmax(0, 1fr) 1.5rem;
  gap: 0.5rem 1.25rem;
  align-items: baseline;
  padding: 1.35rem 0;
  cursor: pointer;
  list-style: none;
}
.eco__sum::-webkit-details-marker { display: none; }
.eco__sum::marker { content: ""; }

.eco__n { color: var(--ember); opacity: 1; }
.eco__t {
  font-size: clamp(1.25rem, 2.2vw, 1.75rem); font-weight: 500;
  line-height: 1.1; letter-spacing: -0.025em; color: var(--white);
}
.eco__q { font-size: 0.95rem; line-height: 1.5; opacity: 0.6; max-width: 46ch; }

/* El signo es una cruz que gira a menos. Dice si está abierto sin escribirlo. */
.eco__signo { position: relative; justify-self: end; width: 14px; height: 14px; }
.eco__signo::before, .eco__signo::after {
  content: ""; position: absolute; inset: 50% 0 auto 0;
  height: 1px; background: currentColor; opacity: 0.7;
  transition: transform var(--dur) var(--ease);
}
.eco__signo::after { transform: rotate(90deg); }
.eco__det[open] .eco__signo::after { transform: rotate(180deg); }
.eco__det[open] .eco__q { opacity: 0; }

.eco__cuerpo { padding: 0 0 2.25rem; max-width: 52rem; }
.eco__d { margin: 0; }
.eco__caso {
  margin: 1.25rem 0 0; padding-left: 1.1rem;
  border-left: 2px solid var(--ember);
  font-size: 0.95rem; line-height: 1.55; opacity: 0.75; max-width: 52ch;
}
.eco__llevas { list-style: none; margin: 1.75rem 0 0; padding: 0; display: grid; gap: 0.6rem; }
.eco__llevas li {
  position: relative; padding-left: 1.2rem;
  font-size: 0.95rem; line-height: 1.5; opacity: 0.75; max-width: 60ch;
}
.eco__llevas li::before {
  content: ""; position: absolute; left: 0; top: 0.68em;
  width: 6px; height: 1px; background: var(--ember);
}
.eco__precio {
  margin: 1.9rem 0 1.4rem;
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem 0.75rem;
  max-width: none;
}
.eco__precio strong {
  font-size: 1.6rem; font-weight: 500; letter-spacing: -0.02em;
  color: var(--white); font-variant-numeric: tabular-nums;
}
.eco__cond { font-size: 0.85rem; opacity: 0.5; }
.eco__pie { margin-top: 3rem; }

/* Despliegue animado donde el navegador sabe interpolar `auto`. Donde no,
   abre de golpe: la información llega igual y nadie se queda fuera. */
@supports (interpolate-size: allow-keywords) {
  :root { interpolate-size: allow-keywords; }
  .eco__det::details-content {
    block-size: 0; overflow: hidden;
    transition: block-size var(--dur) var(--ease), content-visibility var(--dur) allow-discrete;
  }
  .eco__det[open]::details-content { block-size: auto; }
}
@media (prefers-reduced-motion: reduce) {
  .eco__det::details-content { transition: none; }
  .eco__signo::before, .eco__signo::after { transition: none; }
}

@media (max-width: 900px) {
  .eco__sum { grid-template-columns: 2.5rem minmax(0, 1fr) 1.25rem; }
  .eco__q { grid-column: 2 / -1; }
}

@media (max-width: 767px) {
  .esc__texto, .esc__texto--der { margin-inline: 0; max-width: none; }
  .esc h2 { max-width: none; }

  /* EL CIRCUITO A ANCHO DE TELÉFONO. El SVG se escala a ~0,35 sobre su viewBox,
     así que el trazo de 1 se dibujaba a 0,35 px —por debajo del píxel, o sea
     gris pálido o nada— y el número de 21 px se quedaba en 7.

     El trazo se arregla con `non-scaling-stroke`, que dibuja el ancho en píxeles
     de pantalla y no en unidades del viewBox: no hay que calcular ningún factor
     ni volver a tocarlo si cambia la caja. El cuerpo de letra sí hay que
     compensarlo a mano, porque no existe el equivalente.

     No se oculta el circuito, y es deliberado: `:686-700` enciende el nodo de la
     capa abierta, así que también en móvil es el indicador de posición del
     acordeón — cinco cuadros en fila con uno naranja se leen aun a 25 px. */
  .eco__hilo, .eco__retorno, .eco__nodo rect { vector-effect: non-scaling-stroke; }
  .eco__nodo text { font-size: 46px; }
}

/* --------------------------------------------------------------- ÍNDICE
   El precio de quitar los bordes entre apartados es que se pierde la única
   pista de "cuánto queda". Esto la devuelve, y por eso no es decoración: es el
   orientador que sustituye a las secciones que ya no se ven empezar.

   Registro de instrumento, no de menú: rayas de 1 px que sólo crecen y se
   encienden en la que estás. El nombre aparece al acercarse.

   LLEVABA `mix-blend-mode: difference`, y era el elemento más caro de la web.
   Medido en el tramo del Sistema: 73 fps con él y 102 sin él — un 40% más, y el
   único cambio era ése. La razón es que un elemento fijo con `difference`
   encima de un lienzo WebGL a pantalla completa obliga al compositor a mezclar
   contra todo lo que tiene debajo en cada fotograma, así que el lienzo no puede
   quedarse en su propia capa de GPU.

   Estaba para «cruzar del mundo tinta al mundo papel sin una regla de color por
   sección». Esos dos mundos ya no existen: desde que se unificó el fondo, todo
   lo que hay después de la cortina es oscuro, así que la inversión no invierte
   nada — salvo sobre el campo de fotos, donde sí hay imágenes claras.

   Para ese caso basta una sombra, que es lo que hace legible un rótulo blanco
   sobre cualquier fondo y no cuesta una recomposición: se pinta con el texto,
   en su capa, y no obliga a leer de vuelta lo que hay debajo. */
.indice {
  position: fixed; right: var(--gut); top: 50%;
  transform: translateY(-50%);
  z-index: 800;
  display: none; flex-direction: column; gap: 0.9rem;
  color: #fff; text-shadow: 0 1px 4px rgba(0, 0, 0, 0.7);
  opacity: 0; transition: opacity var(--dur) var(--ease);
}
/* EL ÍNDICE BAJA A TABLETA Y DEJA DE DEPENDER DE `hover`. Estaba condicionado a
   `(min-width: 1024px) and (hover: hover)`, así que el único orientador de una
   película de 22.000 px de scroll se apagaba justo en el dispositivo donde más
   falta hace y donde el scroll es más largo. Un índice que necesita ratón para
   existir no es un índice: es un adorno de escritorio.

   Se queda fuera sólo por debajo de 768, donde no cabe una columna de doce
   rótulos; ahí orienta la barra de progreso de abajo. */
@media (min-width: 768px) {
  .indice { display: flex; }
  html[data-mk-ready] .indice { opacity: 1; }   /* no existe hasta que se levanta la cortina */
}
/* Sin ratón el nombre no puede depender de rozar: se enseña siempre, atenuado. */
@media (hover: none) {
  .indice__marca span { opacity: 0.55; transform: none; }
}

/* LA BARRA DE PROGRESO. Es la orientación del móvil, donde el índice no cabe:
   dos píxeles arriba que dicen cuánto queda de película. Sin JavaScript — la
   línea de tiempo es el propio scroll del documento. Donde el navegador no la
   soporta, simplemente no aparece y no se pierde nada más que ella misma. */
.progreso {
  position: fixed; inset: 0 0 auto 0; z-index: 950;
  height: 2px; transform-origin: left; transform: scaleX(0);
  background: var(--ember);
  pointer-events: none;
  opacity: 0;
}
html[data-mk-ready] .progreso { opacity: 1; transition: opacity var(--dur) var(--ease); }
@supports (animation-timeline: scroll()) {
  @media (max-width: 767px) {
    .progreso { animation: progreso-crece linear both; animation-timeline: scroll(root block); }
  }
}
@keyframes progreso-crece { from { transform: scaleX(0); } to { transform: scaleX(1); } }
.indice__marca {
  display: flex; align-items: center; justify-content: flex-end;
  gap: 0.6rem; height: 0.75rem;
}
.indice__marca i {
  display: block; width: 1.25rem; height: 1px;
  /* `text-shadow` no alcanza a una raya, que no es texto: la suya va aquí. */
  background: currentColor; opacity: 0.35; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);
  transition: width var(--dur) var(--ease), opacity var(--dur) var(--ease);
}
.indice__marca span {
  font-size: 0.7rem; letter-spacing: 0.16em; text-transform: uppercase;
  opacity: 0; transform: translateX(0.4rem);
  transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease);
  white-space: nowrap;
}
.indice__marca.is-aqui i { width: 2.25rem; opacity: 1; }
.indice:hover .indice__marca span,
.indice__marca:focus-visible span { opacity: 0.75; transform: none; }
.indice__marca:hover i { opacity: 1; }

/* --------------------------------------------------------------- HERO */
/* Distribución del hero, medida sobre la referencia: titular arriba-izquierda,
   objeto al centro, y las tres piezas de servicio ancladas a las esquinas
   inferiores. La densidad informativa por pantalla es deliberadamente baja. */
/* --------------------------------------------------- EL VIAJE (hero → manifiesto)
   El hero mide una pantalla MÁS el recorrido. Lo que se ve queda pegado arriba
   durante todo ese tramo, así que el scroll no desplaza nada en pantalla: sólo
   avanza `--p`, y de `--p` cuelga todo lo demás.

   Toda la coreografía vive aquí, en calc, y no en JavaScript a razón de ocho
   estilos por fotograma. `viaje.js` escribe UNA propiedad y se calla. La ventaja
   no es el rendimiento —es que el reparto de tiempos se lee de un vistazo y se
   ajusta sin abrir un .js.

   Los dos tramos SE SOLAPAN a propósito (la escena A acaba de irse en 0,52 y la
   B ya está entrando desde 0,48). Un relevo sin solape deja un hueco negro en
   medio, y ese hueco es exactamente la sensación de "cambio de página" que
   había que eliminar. */
/* Lo genérico —el anclaje, el alto, el `--p`— vive en PLANO DE CAPÍTULO. Aquí
   queda sólo lo que es de ESTE capítulo: cómo se reparte su número. */
.hero__viaje {
  /* Los dos tramos están anclados a la DETONACIÓN (0,46 en iso3d.js), no al
     arco: el titular no se va porque la cámara haya llegado, se va porque la
     pieza le ha estallado debajo. Y el manifiesto no llega detrás del hueco que
     deja: llega ENTRE los trozos, mientras siguen abriéndose. */
  /* LA ESCENA A SALE DESPEDIDA, PERO NO ANTES DE TIEMPO. Arrancaba en 0,42 y
     terminaba en 0,60, cuando la detonación estaba en 0,46: el texto se
     desenfocaba a la vez que la pieza reventaba. Con el estallido movido al
     0,72, ese mismo 0,42 dejaba el titular borroso una pantalla ANTES de que
     pasara nada — se apagaba solo, sin causa a la vista.
     Ahora empieza EN la detonación: lo que desenfoca el texto es el golpe. */
  --a: clamp(0, (var(--p) - 0.72) / 0.14, 1);
  --b: clamp(0, (var(--p) - 0.58) / 0.34, 1);   /* la escena B se construye entre el polvo */
  perspective: 1400px;                           /* la profundidad del DOM, a juego con la de la cámara */
}

.escena { position: absolute; inset: 0; z-index: 2; pointer-events: none; }
.escena a, .escena button { pointer-events: auto; }
/* Lo que ya no está en escena tampoco se tabula ni se pulsa. */
[data-escena="manifiesto"] .escena--hero a,
[data-escena="hero"] .escena--manifiesto a { pointer-events: none; visibility: hidden; }

/* SALE DESPEDIDA. Sin transform propia a propósito: a cada pieza de texto la
   lanza `moverDOM()` en iso3d.js, con la misma física que a los trozos del
   isotipo —su empuje, su retardo, su frenado—. Poner aquí un desplazamiento de
   bloque encima lo enmascararía, y el titular dejaría de compartir la explosión
   para limitarse a acompañarla. Aquí sólo se apaga y se desenfoca. */
.escena--hero {
  /* SUELO EN LA RAMPA. Sin él, a mitad del recorrido el CTA principal quedaba a
     2,62:1 de contraste y el secundario a 1,60:1 — y los dos seguían siendo
     enfocables y pulsables. Un botón que se puede usar tiene que poder leerse.

     Con 0,45 el mínimo se queda en 0,55 (≈4,7:1, por encima del umbral) y la
     salida se sigue leyendo como salida, porque el desenfoque de la línea de
     abajo ya hace casi todo el trabajo expresivo. */
  opacity: calc(1 - var(--a) * 0.45);
  filter: blur(calc(var(--a) * 7px));
}

/* LLEGA desde la profundidad, por el lado contrario. */
.escena--manifiesto {
  display: flex; align-items: center;
  /* Deja pasar la cabecera fija: aquí el bloque se centra en la pantalla entera,
     no dentro de una sección con su propio padding, así que sin esto el
     antetítulo aterriza justo debajo del menú. */
  padding: 5.5rem 0 3rem;
  transform: translate3d(calc((1 - var(--b)) * 16vw), 0, calc((1 - var(--b)) * -560px));
  filter: blur(calc((1 - var(--b)) * 9px));
}
/* El titular ya no ocupa el ancho de la página: vive en una columna de medio
   ancho, en segundo plano y con el isotipo al lado. A escala de portada rompía
   en siete líneas y se salía por arriba. La jerarquía la sigue dando el tamaño;
   el tamaño lo da la columna, no la etiqueta. */
.escena--manifiesto h2 {
  font-size: clamp(1.75rem, 3.1vw, 3.4rem);
  /* LOS CORTES SON DEL AUTOR, NO DEL ANCHO. Este titular son tres golpes
     («Facturas más. / Trabajas aún más. / Vives menos.») y medido a 475px el
     navegador los junta en dos: «Facturas más. Trabajas / aún más. Vives
     menos.», que deshace el ritmo.

     `pre-line` hace que el \n del JSON valga. Y llega a tiempo: titulos.js mide
     los saltos por `offsetTop` ANTES de reconstruir el titular en spans, así que
     se queda con estos tres. Después se pone `.tit`, que vuelve a `normal`, y ya
     da igual porque las líneas ya son elementos. */
  white-space: pre-line;
}
.escena--manifiesto .head { margin-bottom: 2.5rem; gap: 1rem; }
/* La máscara descubre de ARRIBA abajo, en el mismo sentido que el escalonado de
   los hijos. Al revés —que es lo primero que sale— el párrafo y la llamada
   aparecían antes que el titular, y las dos mecánicas tiraban en direcciones
   opuestas: se leía como un fallo, no como un revelado.

   Va en el .wrap y no en la escena porque el desenfoque sangra fuera de la caja,
   y recortar por encima dejaría un canto duro justo donde el borde tiene que ser
   lo más blando de todo. */
.escena--manifiesto .wrap {
  margin-left: auto; margin-right: 0; max-width: min(46ch, 46%);
  clip-path: inset(0 0 calc((1 - var(--b)) * 100%) 0);
}
/* Escalonado interno: el bloque no aterriza de una pieza. Cada hijo declara su
   turno con --i y arranca un poco más tarde que el anterior. */
.escena--manifiesto .wrap > * {
  --k: clamp(0, (var(--b) * 1.5) - (var(--i, 0) * 0.16), 1);
  opacity: var(--k);
  transform: translateY(calc((1 - var(--k)) * 2.2rem));
}
/* La niebla es la atmósfera del hero; al salir de él, se sale de ella. */
/* La niebla YA NO se apaga con el hero. Antes era la atmósfera de una sección y
   se iba con ella; ahora es la atmósfera del universo y el universo no termina. */

@media (max-width: 767px) {
  /* En vertical no hay tercio lateral que ceder, así que la cámara sube la
     pieza (ver iso3d.js) y el texto entra por abajo, a ancho completo. Y el
     desenfoque desaparece — es el filtro más caro de la coreografía y el que
     primero tira los 60 fps.
     El acortado del recorrido ya NO se hace aquí: lo aplica `capitulo.js` a
     todos los capítulos con un solo factor, para que el ritmo de la web en
     vertical se recalibre en un sitio y no medio en CSS y medio en JS. */
  .escena--manifiesto { align-items: flex-end; padding-bottom: 10vh; }
  .escena--manifiesto .wrap { margin-inline: auto; max-width: none; }
  .escena--hero, .escena--manifiesto { filter: none; }
}

/* Sin movimiento: no hay viaje. Las dos escenas vuelven a ser dos bloques que
   se leen uno detrás de otro, que es lo que eran antes de todo esto. */
@media (prefers-reduced-motion: reduce) {
  .hero { height: auto; min-height: 100dvh; }
  .hero__viaje { position: static; height: auto; overflow: visible; perspective: none; }
  /* Sin el plano fijado, `inset: 0` estira el lienzo sobre las DOS escenas y el
     isotipo queda de fondo del manifiesto. Aquí sólo le toca la primera. */
  .escena { position: static; pointer-events: auto; }
  .escena--hero { min-height: 100dvh; position: relative; }
  .escena--hero, .escena--manifiesto { opacity: 1; transform: none; filter: none; }
  .escena--manifiesto { padding: 6.25rem 0; }
  .escena--manifiesto .wrap { clip-path: none; margin-inline: auto; max-width: var(--maxw); }
  .escena--manifiesto .wrap > * { opacity: 1; transform: none; }
  /* El disparador del isotipo y su instrucción se van con el viaje. Bajo esta
     preferencia `viaje.js` no registra el gesto, así que el botón quedaba
     enfocable, etiquetado y sin responder a nada, y `#blast-hint` seguía
     describiendo un gesto imposible. `display: none` y no `remove()` desde JS:
     así sale del orden de tabulación y del árbol de accesibilidad, vive donde ya
     está el resto del desmontaje, y se reevalúa si la preferencia cambia en
     caliente — el `matchMedia` de viaje.js se lee una sola vez al cargar. */
  .hero__blast, .hero__hint { display: none; }
}

.hero__head { position: absolute; top: 12%; left: var(--gut); right: var(--gut); z-index: 2; }


/* El titular lleva su propio salto: la frase son dos golpes y el segundo remata.
   Dejarlo al reflujo natural rompería el paralelismo justo donde vive el chiste. */
.hero__h { white-space: pre-line; }

/* La propuesta de valor va DEBAJO del titular, no en una esquina. El recorrido
   es titular → qué hacéis → llamada, en una sola columna y sin saltos de ojo.
   Antes vivía en la tarjeta inferior derecha, a tamaño de cuerpo y pisada por
   las pistas de interacción. */
/* El subtítulo ya NO es una enumeración. Era «Marca. Contenido. IA.
   Automatización.» en su propia línea, y por eso llevaba `pre-line` y un
   `::first-line` con la voz de sistema: se leía como el inventario del
   ecosistema. Ahora es una sola frase de prosa —las dos mitades y quién las
   hace— así que las dos reglas se van con ella. Sin `pre-line` el texto se
   recompone solo al estrecharse, que es lo que quiere una frase. */
.hero__sub {
  margin-top: 1.75rem; max-width: 46ch;
  font-size: clamp(1rem, 1.35vw, 1.2rem); line-height: 1.5;
}


/* El CTA de cabecera. Mismo trato que el secundario del hero —texto y filete
   al pasar el cursor—, un punto más pequeño: en la cabecera acompaña, no llama.
   Sin padding propio: el filete de `.cta` se apoya en el `padding-bottom` de la
   base, y añadir relleno aquí lo despegaría del texto. */
/* AQUÍ VIVÍA `.cta--nav`, la pastilla de «Contacto» de la cabecera. Ahora
   Contacto es un enlace más del menú —mismo tamaño, mismas versalitas, mismo
   subrayado al pasar— y el rincón derecho queda para el sonido y el idioma, que
   antes se apretaban contra él. */

.hero__acciones { display: flex; flex-wrap: wrap; gap: 1.75rem 2.5rem; margin-top: 2.5rem; }
/* AQUÍ HABÍA UN `.cta--tenue { opacity: 0.6 }` sobre el secundario. Escrito
   cuando las dos llamadas del hero eran el mismo enlace y la única forma de
   distinguirlas era la tinta —«dos llamadas con el mismo énfasis son cero
   llamadas»—, que era cierto entonces.

   Dejó de serlo, y se volvió un fallo de contraste. Ese 0,6 no vivía solo: se
   multiplicaba por el suelo de la escena (0,55, arriba en `.escena--hero`).
   0,55 × 0,6 = 0,33, y el gris de texto heredado —#d8d8d8, no blanco— a ese
   alfa sobre #050505 da 2,33:1 en un cuerpo de 12,6 px. Con el enlace todavía
   enfocable y pulsable, que es la parte grave: se puede usar y no se puede
   leer. Y no había número que lo salvara — para llegar a 4,5:1 haría falta un
   alfa efectivo de 0,537, o sea `.cta--tenue` ≥ 0,976: no es atenuación, es
   nada.

   La jerarquía la lleva ahora la FORMA, que es más fuerte y no cuesta
   contraste: el principal es una pastilla blanca rellena de 44 px y el
   secundario un enlace pelado con subrayado al hover. No hacen falta dos
   opacidades para decir cuál es cuál. */

/* --------------------------------------------- APARICIÓN PROGRESIVA
   Cada pieza entra en su punto del recorrido, no todas al cargar. `--desde` lo
   declara el marcado porque es una decisión de guion —cuándo se cuenta cada
   cosa— y no de estilo.

   El titular NO lleva `data-fase` a propósito: manda desde el primer fotograma.
   Lo que se descubre es el argumento, no la afirmación. */
[data-fase] {
  --f: clamp(0, (var(--p) - var(--desde, 0)) / 0.11, 1);
  opacity: var(--f);
  transform: translateY(calc((1 - var(--f)) * 1.1rem));
}

.hero__scroll { position: absolute; left: var(--gut); bottom: 6%; z-index: 2; }
.hero__hint {
  position: absolute; left: 50%; bottom: 5%;
  transform: translateX(-50%); z-index: 2;
  text-align: center; line-height: 1.7;
  white-space: pre-line;              /* el copy trae el salto de línea */
}
/* `.hero__card` y `.hero__est` vivían aquí: la tarjeta de «Desde 2020». Se fue
   del hero por contradecir al archivo, y sus reglas con ella. */

@media (max-width: 767px) {
  .hero__head { top: 14%; }
  .hero__h { white-space: normal; }               /* en vertical manda el ancho, no el salto */
  .hero__sub { margin-top: 1.25rem; }
  .hero__card, .hero__scroll { display: none; }   /* en móvil, una idea por pantalla */
  .hero__hint { bottom: 8%; }
}
/* A SANGRE, como la referencia: el canvas ocupa el hero entero. Los hilos
   cruzan toda la página en vez de morir en el borde de una caja, y el negro del
   lienzo ES el fondo, así que no hay rectángulo que disimular. La cámara
   ortográfica va corregida por aspecto (ver iso3d.js), de modo que el objeto
   conserva su tamaño calibrado respecto a la ALTURA y el ancho sólo añade aire. */
/* Ya no contiene el lienzo —eso es #universo— sino sólo la zona del gesto. */
.hero__stage { position: absolute; inset: 0; z-index: 0; }


.hero .wrap { position: relative; z-index: 1; pointer-events: none; }
.hero .wrap a, .hero .wrap button { pointer-events: auto; }

/* El disparador del "blast": invisible pero focalizable. La interacción oculta
   de la referencia no tiene equivalente por teclado; ésta sí. También delimita
   dónde empieza a ser "el logo" para el sonido de roce. */
.hero__blast {
  position: absolute; inset: 50% auto auto 50%;
  transform: translate(-50%, -50%);
  width: min(38vmin, 22rem); aspect-ratio: 1;
  background: none; border: 0; border-radius: 50%;
  cursor: pointer; color: inherit;
}
.sr {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}
/* El titular pasó de dos golpes cortos a una frase con argumento, y a la escala
   de portada (90px, 14ch) rompía en seis líneas y se comía la pantalla. Baja de
   cuerpo y gana ancho: sigue mandando, pero deja sitio a lo que va llegando
   debajo. La jerarquía la da la escala relativa, no el tamaño absoluto. */
.hero__h { max-width: 20ch; font-size: clamp(2.1rem, 4.4vw, 4rem); }
.hero__foot { display: flex; justify-content: space-between; align-items: flex-end; margin-top: 4rem; gap: 2rem; flex-wrap: wrap; }

/* --------------------------------------------------------------- PASOS */
/* ----------------------------------------------- CIRCUITO DEL SISTEMA
   El lenguaje de diagramas de 07_design/visual-language.md, por fin construido.
   Sin sombras, sin degradados, sin curvas decorativas: 1px y ángulos rectos.
   Los estados son los especificados y no se inventa ninguno. */
/* FIJO. Sin esto el circuito se va por arriba mientras aún se leen las fases,
   y el cierre del bucle —que es el pago de la sección— ocurriría fuera de
   pantalla.

   El fondo era un panel opaco de papel, porque la lista pasa por debajo y había
   que taparla. Ahora eso está prohibido: un panel opaco tapa el universo. En su
   lugar, un DEGRADADO que se desvanece — oscurece lo justo para que la lista
   desaparezca al pasar por detrás, y deja ver el espacio a través. Es
   atmósfera, no una tarjeta. */
.circuito {
  position: sticky; top: 0; z-index: 2;
  background: linear-gradient(180deg,
    var(--ink) 0%, var(--ink) 62%,
    color-mix(in srgb, var(--ink) 82%, transparent) 84%,
    transparent 100%);
  /* El relleno superior es la altura de la cabecera: así el fondo del circuito
     cubre TODA la franja de arriba y las filas desaparecen detrás de él antes
     de llegar al header. Con `top: 5.5rem` quedaba una banda al descubierto y
     el texto de la lista chocaba con la navegación. */
  padding: 5.5rem 0 1.75rem;
  margin-bottom: 2rem;
}
.circuito__svg { width: 100%; height: auto; overflow: visible; }

/* Pendiente: sólo borde. */
.circuito__nodo rect {
  fill: none; stroke: var(--line-dark); stroke-width: 1;
  transition: fill 0.45s var(--ease, ease), stroke 0.45s var(--ease, ease);
}
.circuito__nodo text {
  fill: var(--font-light); font-size: 13px; letter-spacing: 0.14em; text-transform: uppercase;
  transition: fill 0.45s var(--ease, ease);
}
.circuito__n { opacity: 0.5; font-variant-numeric: tabular-nums; }

/* Automatizada: discontinuo. Es la leyenda del sistema, no un adorno. */
.circuito__nodo.es-auto rect { stroke-dasharray: 5 4; }

/* Recorrida: el borde se afirma. */
.circuito__nodo.es-hecha rect { stroke: var(--font-light); }

/* Activa: relleno. El único estado que invierte el texto. */
.circuito__nodo.es-activa rect { fill: var(--font-light); stroke: var(--font-light); }
.circuito__nodo.es-activa text { fill: var(--ink); }

.circuito__enlace {
  stroke: var(--line-light); stroke-width: 1;
  transition: stroke 0.45s var(--ease, ease);
}
.circuito__enlace.es-hecha { stroke: var(--ink); }

/* El retorno es lo que convierte la fila en bucle: va en brasa, y es lo único
   naranja de la sección. Se dibuja, no aparece. */
.circuito__retorno {
  fill: none; stroke: var(--ember); stroke-width: 1.5;
  transition: stroke-dashoffset 0.12s linear;
}
.circuito__flecha {
  fill: none; stroke: var(--ember); stroke-width: 1.5;
  stroke-linecap: round; stroke-linejoin: round;
  opacity: 0; transition: opacity 0.35s ease;
}
.circuito.es-cerrado .circuito__flecha { opacity: 1; }

/* Cola de scroll: la última fila tiene que poder subir hasta la altura de
   lectura con la sección todavía en pantalla, o el bucle no llega a cerrarse. */
#sistema .steps { padding-bottom: 34vh; }
.steps { border-top: 1px solid var(--line-light); }
/* La fila activa se afirma; las demás no se apagan del todo — se siguen
   leyendo. Atenuar contenido para dirigir la vista es una tentación cara. */
.step { transition: opacity 0.45s ease; opacity: 0.55; }
.step--activa { opacity: 1; }
.step {
  display: grid; gap: 0.5rem 2rem;
  grid-template-columns: 3rem 1fr;
  padding: 1.75rem 0;
  border-bottom: 1px solid var(--line-light);
}
@media (min-width: 768px) {
  .step { grid-template-columns: 4rem 16rem 1fr; align-items: baseline; }
}
.step__n { font-variant-numeric: tabular-nums; opacity: 0.6; }

/* --------------------------------------------------------------- DATOS */
/* Aquí había `background: var(--line-light)`. Es el truco de usar el hueco de la
   rejilla como filete: el fondo del contenedor asoma por `gap` y dibuja las
   líneas entre celdas. Sólo funciona si las celdas tienen fondo OPACO, y `.fact`
   es transparente — así que el #d0cecb no salía por el hueco de un píxel, salía
   por el bloque entero.

   Sobre ese gris, unas cifras heredadas del texto de la página (#d8d8d8) daban
   1,10:1 de contraste. El mínimo para texto grande es 3:1: «18+», «2» y «100%»
   —los titulares del apartado que se llama PRUEBA— eran invisibles.

   Se quita el fondo en vez de dar fondo opaco a las celdas porque las celdas ya
   traen su propio `border`, así que ese fondo no dibujaba ninguna línea que no
   estuviera ya dibujada. `--line-light` es un color de filete; en el resto de la
   hoja se usa siempre como `border`, y éste era el único sitio que lo usaba de
   relleno. */
.facts { display: grid; gap: 1px; grid-template-columns: 1fr; }
@media (min-width: 768px) { .facts { grid-template-columns: repeat(3, 1fr); } }

/* HOLGURA PARA EL MENÚ. Al fijar esta escena, la regla de fijado le puso
   `padding: 0` a la sección, y con ello desaparecieron los 100 px que `.section`
   le daba por arriba. En horizontal no se nota —sobra sitio— pero en vertical su
   contenido mide 744 px dentro de un plano de 812: quedan 34 de holgura contra
   los 72 que ocupa el `hdr`, que es fijo y sin fondo. Medido en un Pixel 8
   emulado, el label «PRUEBA» caía en 34..47 y la marca «MAKERS» en 28..44. Se
   pisaban.

   Va sobre el plano y no sobre la sección porque la sección ya no es una caja de
   maquetación, es la pista del scroll: su padding movería el sticky.

   Sólo esta escena la necesita, y está comprobado: de las nueve fijadas, las
   seis `.esc` tienen entre 113 y 272 px de holgura y `estudio` 240. La general
   —dar esta reserva a TODOS los planos— también funcionaría, pero bajaría 36 px
   la composición de seis escenas ya aprobadas para arreglar una.

   Y sólo en vertical. En horizontal esta misma escena tiene 196 px de holgura,
   así que la reserva no arregla nada allí y en cambio baja la composición 36 px.
   Un arreglo de móvil que mueve el escritorio no es un arreglo. */
@media (max-width: 767px) {
  #datos [data-plano] { padding-top: 4.5rem; }
}

/* Y LA RESERVA SOLA NO BASTABA, porque el problema de fondo no es dónde empieza
   el contenido sino que MIDE DEMASIADO: 744 px en un plano de 812. Con la
   reserva puesta, el label subía de 34 a 70 — el menú acaba en 72, así que
   seguía rozando.

   Las tres fichas apiladas aportan 240 px sólo de relleno vertical. A 1,5rem son
   144, y con esos 96 px el contenido baja a 648: entra con holgura y el label cae
   en 118. Sólo en vertical — en horizontal las fichas van en fila, su relleno no
   se suma y el aire de 2,5rem es el aprobado.

   Y va DESPUÉS de `.fact`, no antes: las dos reglas tienen la misma
   especificidad, así que decide el orden. Puesta arriba compilaba, casaba la
   media query y no servía de nada — el `padding` seguía en 40 px. */
/* `--line-light` y no `--line-dark`: esta ficha vive sobre la página de tinta, y
   el filete del mundo papel sobre fondo oscuro daba 1,79:1 — un borde que no se
   ve. La pareja `-light`/`-dark` nombra el MUNDO al que pertenece cada color,
   igual que `--font-light`/`--font-dark`, no su claridad. */
.fact { padding: 2.5rem 2rem; border: 1px solid var(--line-light); }
@media (max-width: 767px) {
  .fact { padding: 1.5rem 2rem; }
}
/* Sólo el SUELO del clamp. A 375 px, 3rem dejaba la cifra en 48 px contra los
   33,6 del h1: el texto más grande de la web era un dato de apoyo, no el
   titular. Con 2rem cae a 32 px y queda por debajo. Por encima de 686 px ya
   mandaba el 7vw, así que ahí no se mueve nada. */
.fact__n { font-size: clamp(2rem, 7vw, 6rem); line-height: 0.9; letter-spacing: -0.06em; }

/* --------------------------------------------------------------- MATERIAL */
.material__stage { position: relative; aspect-ratio: 16/9; background: #0a0a0a; overflow: hidden; }
.material__stage canvas { width: 100%; height: 100%; display: block; }
/* Las etiquetas van sobre un degradado propio: el plano etalonado tiene
   altas luces y sin scrim se pierden justo cuando la costura pasa por debajo. */
.material__side {
  position: absolute; bottom: 0; padding: 1rem;
  font-size: var(--t-label-size, 0.7rem); letter-spacing: 0.16em; text-transform: uppercase;
  font-variant-numeric: tabular-nums; color: #fff; pointer-events: none;
}
.material__side--final { left: 0; color: var(--ember); }
.material__side--log { right: 0; opacity: 0.8; }
.material__stage::after {
  content: ""; position: absolute; inset: auto 0 0; height: 5rem; pointer-events: none;
  background: linear-gradient(0deg, rgba(0,0,0,0.65), transparent);
}

/* --------------------------------------------------------------- FOOTER */
/* EL CIERRE. Orden de lectura y de peso: oferta (titular) → qué mide → qué te
   llevas → botón → cuándo respondemos → qué pasa después → salvedad → promesa.
   Todo lo que va detrás del botón está a media voz: quien ya decidió no lo lee,
   y quien duda encuentra ahí exactamente lo que le frena. */
.foot__llevas {
  margin-top: 1.75rem; max-width: 46ch;
  padding-left: 1rem; border-left: 1px solid var(--ember);
  font-size: clamp(0.95rem, 1.25vw, 1.0125rem); line-height: 1.6;
}
.foot__micro { margin-top: 1rem; max-width: 42ch; font-size: 0.7875rem; letter-spacing: 0.02em; opacity: 0.55; }
.foot__despues { list-style: none; margin: 2.5rem 0 0; padding: 0; max-width: 44ch; display: grid; gap: 0.75rem; }
.foot__despues li { font-size: 0.7875rem; line-height: 1.55; opacity: 0.6; padding-left: 1rem; border-left: 1px solid #ffffff14; }
.foot__continuidad { margin-top: 1.5rem; font-size: 0.7875rem; letter-spacing: 0.02em; opacity: 0.6; }
/* La salvedad es la que más convierte de todo el bloque: dice que la llamada
   puede terminar en «no te hace falta». Por eso lleva el acento y no se esconde
   entre las demás notas. */
.foot__salvedad {
  margin-top: 2rem; max-width: 44ch;
  padding-left: 1rem; border-left: 1px solid var(--ember);
  font-size: clamp(0.95rem, 1.25vw, 1.0125rem); line-height: 1.5; opacity: 0.85;
}
/* La promesa del umbral vuelve a cerrar la película: se abre y se cierra con la
   misma frase, y en medio se ha explicado qué significa. */
.foot__cierre {
  margin-top: 3.5rem; max-width: none;
  font-size: clamp(1.3rem, 2.4vw, 2rem); line-height: 1.15; letter-spacing: -0.03em; opacity: 0.9;
}

.foot { border-top: 1px solid var(--line-light); padding-top: 3rem; margin-top: 4rem; }
.foot__grid { display: flex; flex-wrap: wrap; gap: 2rem 4rem; justify-content: space-between; }

/* --------------------------------------------------------------- PRELOADER */
#pl {
  position: fixed; inset: 0; z-index: 9050;
  background: var(--paper); color: var(--ink);
  display: grid; place-items: center;
}
#pl[hidden] { display: none; }

/* Las cintas: se escalan en Y de forma escalonada. Son también la cortina
   de las transiciones, por eso viven fuera del contenido del preloader. */
.pl__belts { position: absolute; inset: 0; display: flex; }
/* LA FIRMA. Las barras entran desde fuera del cuadro viajando en la dirección
   de su propio trazo, se cruzan en el centro dibujando el isotipo, y después
   ENGORDAN hasta que no queda papel entre ellas.

   Engordar y no escalar es la clave. La versión anterior enmascaraba la marca
   con su SVG y la escalaba: pero al escalar una máscara, el papel entre los
   trazos escala con ella, así que por mucho que creciera acababa en franjas.
   Nunca podía cerrar. Una barra que crece de grosor sí cierra, por construcción.

   Dos mitades recortadas: el cuerpo baja a la derecha y el brazo a la izquierda.
   Sin el recorte, las diagonales de un lado cruzarían al otro y se perdería
   la V. */
.pl__firma { position: absolute; inset: 0; overflow: hidden; }
/* La unión NO va al 50 %. Partido por la mitad sale un zigzag simétrico —un
   espigado— y el isotipo no es eso: las `\` dominan y las `/` son minoría, así
   que la V tiene un lado largo y otro corto. Ese desequilibrio es lo que hace
   que se lea UNA V protagonista en vez de una repetición.
   El 65/35 es la proporción de la marca. Cada lado recorta en esa línea, y ahí
   es donde las diagonales de un sentido se encuentran con las del otro. */
.pl__lado { position: absolute; inset: 0; overflow: hidden; }

/* La línea donde se encuentran las dos familias NO es vertical. Con un corte
   recto todos los vértices caen en la misma x y sale un zigzag repetido —un
   espigado—, que es justo lo que el isotipo no es. En la marca ese encuentro
   baja en diagonal, así que los vértices van en escalera y el conjunto se lee
   como UNA V y no como un patrón.

   El corte va a 45° exactos: el desplazamiento horizontal entre el borde
   superior y el inferior es `100vh`, es decir la altura del cuadro en píxeles.
   Mezclar % y vh en el polígono es lo que mantiene el ángulo en cualquier
   proporción de pantalla; con porcentajes puros el ángulo cambiaría al
   redimensionar. */
/* LOS DOS RECORTES SE SOLAPAN 2 px, y no es un margen de seguridad cualquiera.
   Con la misma diagonal exacta en los dos, los lados se TOCAN sin compartir un
   solo píxel. Y un `clip-path` se suaviza: cada borde reparte su píxel de
   frontera entre lo que hay dentro y lo que hay fuera, por su cuenta y sin saber
   del otro. En esa fila ninguno de los dos llega al 100%, así que al inundar en
   negro quedaba una costura diagonal de esquina a esquina — visible justo cuando
   la cortina tenía que estar completamente cerrada.

   Extendiendo el lado izquierdo 2 px hacia la derecha, se mete DEBAJO del otro y
   la fila queda cubierta dos veces. No se ve la diferencia en la firma: la línea
   de encuentro sigue estando donde estaba, porque quien la dibuja es el borde
   del lado derecho, que no se toca. */
.pl__lado--izq {
  clip-path: polygon(0 0, calc(58% + 50vh + 2px) 0, calc(58% - 50vh + 2px) 100%, 0 100%);
}
.pl__lado--der {
  clip-path: polygon(calc(58% + 50vh) 0, 100% 0, 100% 100%, calc(58% - 50vh) 100%);
}

/* Más larga que cualquier pantalla: girada 45° tiene que seguir cruzando el
   cuadro de esquina a esquina, y el sobrante se lo come el recorte del lado. */
.pl__raya {
  position: absolute; left: 50%; top: 50%;
  width: 320vmax; height: var(--grosor);
  margin: calc(var(--grosor) / -2) 0 0 -160vmax;
  background: var(--ink);
  transform-origin: 50% 50%;
  will-change: transform;
}

.pl__inner { position: relative; z-index: 2; text-align: center; padding: var(--gut); }
/* Aquí vivían `.pl__words` y `.pl__w`, la tríada del umbral y su foco. Las
   sustituye `.pl__promesa`, unas líneas más abajo. */

/* LA PROMESA DEL UMBRAL. Sustituye a la tríada, y hereda su sitio y su escala —
   la barra de carga mide el ancho de este bloque igual que medía el de las tres
   palabras, porque `.pl__inner` sigue siendo un ítem de grid que se ajusta al
   contenido.

   Dos pesos y nada más: la promesa manda y el descriptor la sitúa. Ninguno de
   los dos se anima ni espera a nada — es la regla que echó a la tríada de aquí.

   El ancho NO va en `ch` sobre el contenedor: `ch` se resuelve contra el
   font-size del PROPIO elemento, y este bloque hereda los 16 px de la raíz — 22ch
   daban 176 px y partían el titular en cuatro líneas. Va en `rem`, que no
   depende de dónde se declare. */
.pl__promesa { max-width: 36rem; margin-inline: auto; }
.pl__h {
  margin: 0;
  font-size: clamp(1.75rem, 5vw, 3.6rem);
  line-height: 1.02; letter-spacing: -0.03em;
  /* El corte de línea viene del JSON, igual que en el hero. */
  white-space: pre-line;
}
.pl__sub {
  margin: 1.25rem auto 0;
  max-width: 34ch;
  font-size: clamp(0.9rem, 1.5vw, 1.0125rem);
  line-height: 1.5; opacity: 0.66;
}
/* La puerta: mientras se espera el gesto sólo están las palabras y la
   invitación. La barra de carga aparece cuando hay algo que medir. */
.pl__entrar {
  display: none; margin: 2.5rem auto 0;
  border: 0; background: none; color: inherit; cursor: pointer;
  font: inherit; font-size: 0.7875rem; letter-spacing: 0.16em; text-transform: uppercase;
  opacity: 0.55;
}
.pl--espera { cursor: pointer; }
.pl--espera .pl__entrar { display: block; animation: pl-latido 2.4s ease-in-out infinite; }
.pl--espera .pl__bar, .pl--espera .pl__pct { visibility: hidden; }
@keyframes pl-latido { 0%, 100% { opacity: 0.4; } 50% { opacity: 0.9; } }

/* La barra abarca EXACTAMENTE las tres palabras. `.pl__inner` es un ítem de
   grid, así que su ancho es el del contenido — la fila de palabras. Con 100 %
   la línea va de ATRAER a ESCALAR. Los 18rem fijos de antes se quedaban cortos
   y, al no llevar margen automático, además caían descentrados. */
.pl__bar { margin: 2.5rem auto 0; width: 100%; height: 1px; background: #00000026; position: relative; }
.pl__fill { position: absolute; inset: 0 auto 0 0; width: 0%; background: var(--ink); }
.pl__pct { margin-top: 0.75rem; text-align: center; font-variant-numeric: tabular-nums; letter-spacing: 0.16em; font-size: 0.7875rem; }

/* Las cruces de esquina: el motivo que cose el preloader con las secciones */
.pl__x { position: absolute; width: 13px; height: 13px; opacity: 0.55; }
.pl__x::before, .pl__x::after { content: ""; position: absolute; background: var(--ink); }
.pl__x::before { inset: 6px 0 auto 0; height: 1px; }
.pl__x::after { inset: 0 auto 0 6px; width: 1px; }

/* --------------------------------------------------------------- REVELADOS */
.rv { opacity: 0; transform: translateY(1.25rem); }
html[data-mk-ready] .rv--in { opacity: 1; transform: none; transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease); }

/* ---------------------------------------------------------------
   MOVIMIENTO REDUCIDO — la referencia no lo hace; aquí es de origen.
   Colapsa los pines, apaga el movimiento y deja todo en estado final.
   --------------------------------------------------------------- */
/* ---------------------------------------------- VENTANAS BAJAS (el portátil)
   La película se dirigió a 1440×900 y se adaptó por ANCHURA, pero su variable
   real es la ALTURA: el plano fijado mide 100svh y lo que no cabe se recorta,
   arriba y abajo, sin forma de llegar a ello.

   Medido a 1280×800 —un MacBook Air, no un móvil—: el cierre se pasaba 87 px y
   se comía el correo de contacto. Aquí se aprieta el ritmo vertical del cierre
   en lugar de desfijarlo, porque su final está coreografiado con la
   recomposición del isotipo y desfijarlo cambiaría la última escena. */
@media (max-height: 900px) {
  #contacto .foot__despues { margin-top: 1.5rem; }
  #contacto .foot__salvedad { margin-top: 1.25rem; }
  #contacto .foot__cierre { margin-top: 2rem; margin-bottom: 1rem; }
  #contacto .foot { margin-top: 2.25rem; }
  #contacto .t-body { margin-block: 0.75rem; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
  .rv { opacity: 1 !important; transform: none !important; }

  /* LA PELÍCULA SE QUEDABA SIN TEXTO. `[data-fase]` arranca en `opacity: 0` y
     se revela con `--f`, que lo escribe el motor de capítulos a partir del
     avance del plano fijado. Pero con movimiento reducido `capitulo.js` desfija
     los planos —hace lo correcto— y entonces `--f` no se escribe nunca: 24 de
     las 27 piezas de copy se quedaban invisibles PARA SIEMPRE, los dos botones
     del hero incluidos. La web entera era un fondo negro con un titular.

     Misma solución que ya se usaba dos líneas arriba para `.rv`. Es la línea de
     CSS que separa «premium» de «inservible para quien marca esa casilla». */
  [data-fase] { opacity: 1 !important; transform: none !important; }
}


/* ================================================== SERVICIOS · seis tarjetas
   Portadas de makers-site, donde viven en un slider horizontal ANCLADO que
   secuestra el scroll de la página. Aquí no puede: el scroll ya gobierna el
   recorrido de cámara y los dos se pelearían por el mismo gesto. Se resuelve
   con una tira propia —desplazamiento horizontal dentro de la tira, con imán
   de parada— que deja el scroll vertical a la película.

   `#servicios` no lleva `data-viaje` (ver el comentario de su marcado), así que
   necesita alto propio para que el crucero de la cámara no se le quede corto.
   Misma solución que `#ecosistema`. */
#servicios { min-height: 150vh; }

.svc__intro { max-width: 52ch; margin: 1.5rem 0 0; }

.svc {
  list-style: none; margin: 3rem 0 0; padding: 0 0 1.5rem;
  display: flex; gap: 1.75rem;
  overflow-x: auto; scroll-snap-type: x mandatory;
  /* Sangrado hasta el borde: la tira se lee como una hilera que sigue fuera de
     pantalla, no como una caja. Sin esto la última tarjeta parece la última. */
  margin-inline: calc(var(--gut) * -1); padding-inline: var(--gut);
  scrollbar-width: thin;
}
.svc__card {
  flex: 0 0 clamp(15rem, 22vw, 19rem);
  scroll-snap-align: start;
}
.svc__card .escena { margin: 0; }
.svc__h {
  margin: 1.1rem 0 0; font-size: 1.05rem; font-weight: 500;
  line-height: 1.25; letter-spacing: -0.015em; max-width: 22ch;
}
.svc__tag {
  display: block; margin-top: 0.55rem; color: var(--ember);
  /* `.t-label` pone versalitas a todo. En origen el hashtag se lee tal cual se
     escribe —#agentesdeIA, con su camel case—, y en mayúsculas se pierde. */
  text-transform: none;
}

@media (max-width: 640px) {
  .svc__card { flex-basis: 76vw; }
}


/* ================================================== CATÁLOGO DE SERVICIOS
   Diez servicios en cuatro fases, portados de makers-site/services.html. La
   fase es una cabecera con su frase a la derecha; cada servicio, un <details>
   que abre media a la izquierda y texto a la derecha.

   `#servicios` no lleva `data-viaje` — con recorrido el motor fija el plano a
   100svh y esto crece al abrir fichas, así que se recortaría solo. Misma
   decisión, y por la misma razón medida, que `#ecosistema`. */
#servicios { min-height: 180vh; }

.svcl { margin-top: 3.5rem; }
.svcl__fase + .svcl__fase { margin-top: 3.5rem; }

.svcl__cab {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 2rem; flex-wrap: wrap;
  padding-bottom: 0.9rem; border-bottom: 1px solid var(--line-marco);
}
.svcl__id { margin: 0; display: flex; align-items: baseline; gap: 0.9rem; }
.svcl__n { color: var(--ember); }
.svcl__t { font-size: 1.15rem; font-weight: 500; letter-spacing: -0.015em; }
.svcl__q { margin: 0; max-width: 44ch; text-align: right; font-size: 0.9rem;
  line-height: 1.5; opacity: 0.6; }

.svcl__svc { border-bottom: 1px solid var(--line-sutil); }
.svcl__bar {
  display: flex; align-items: center; gap: 1.4rem;
  padding: 1.35rem 0; cursor: pointer; list-style: none;
}
.svcl__bar::-webkit-details-marker { display: none; }
.svcl__ix { opacity: 0.45; }
.svcl__nom { font-size: 1.45rem; font-weight: 500; letter-spacing: -0.02em; flex: 1; }
/* El signo es el mismo lenguaje que el de `ecosistema`: raya que se cruza. */
.svcl__signo { position: relative; width: 14px; height: 14px; flex: none; opacity: 0.6; }
.svcl__signo::before, .svcl__signo::after {
  content: ""; position: absolute; inset: 50% 0 auto 0; height: 1px;
  background: currentColor; transition: transform var(--dur-fast) var(--ease);
}
.svcl__signo::after { transform: rotate(90deg); }
details[open] .svcl__signo::after { transform: rotate(0deg); }

/* LA APERTURA. `<details>` nativo no tiene transición: el contenido pasa de no
   existir a existir y salta. `::details-content` da acceso a esa caja, y
   `interpolate-size: allow-keywords` permite animar HASTA `auto` — sin eso
   habría que medir el alto con JavaScript en cada apertura y volver a medirlo
   en cada cambio de ancho, que es la solución que envejece mal.

   `content-visibility` va en la transición con `allow-discrete` porque es una
   propiedad discreta: sin declararla ahí, el contenido se oculta de golpe al
   cerrar y el alto anima sobre una caja ya vacía.

   `--dur` es la duración de la casa, la misma que gasta la película. */
.svcl { interpolate-size: allow-keywords; }
.svcl__svc::details-content {
  block-size: 0;
  overflow: hidden;
  opacity: 0;
  transition:
    block-size var(--dur) var(--ease),
    opacity var(--dur-fast) var(--ease),
    content-visibility var(--dur) allow-discrete;
}
.svcl__svc[open]::details-content { block-size: auto; opacity: 1; }

/* Quien pide menos movimiento no quiere que una ficha crezca durante medio
   segundo: abre y ya. */
@media (prefers-reduced-motion: reduce) {
  .svcl__svc::details-content { transition: none; }
}

.svcl__cuerpo {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 2.5rem; padding: 0 0 2.25rem;
}
.svcl__media { position: relative; aspect-ratio: 4 / 3; overflow: hidden;
  border: 1px solid var(--line-marco); background: #0a0a0a; }
.svcl__vid { width: 100%; height: 100%; object-fit: cover; display: block; }
.svcl__texto { display: flex; flex-direction: column; align-items: flex-start;
  gap: 1.6rem; justify-content: center; }
.svcl__texto .t-body { margin: 0; max-width: 46ch; }

@media (max-width: 780px) {
  .svcl__cuerpo { grid-template-columns: 1fr; gap: 1.5rem; }
  .svcl__q { text-align: left; }
  .svcl__nom { font-size: 1.2rem; }
}


/* ================================================== EL SELECTOR DE IDIOMA
   Mismo lenguaje que el de makers-site: dos siglas y una barra, la activa en
   blanco y la otra a media luz. Aquí es un <button> porque no navega — el
   contenido vive en `content/<lang>.json` y cambiar de idioma es recargar
   datos.

   Hereda el color y la sombra de `.hdr`, así que sobre el 3D se lee igual que
   el resto de la barra. */
.lang {
  background: none; border: 0; padding: 0.35em 0.1em; margin-right: 0.9rem;
  color: inherit; font: inherit; letter-spacing: 0.16em; cursor: pointer;
  display: inline-flex; align-items: baseline; gap: 0.25em;
}
.lang__sep { opacity: 0.35; }
/* La que NO está puesta va a media luz. El intercambio lo hace el JS con
   `data-lang="en"` en el propio botón, no reescribiendo el texto: así el
   marcado no depende del idioma que se esté leyendo. */
.lang__off { opacity: 0.45; transition: opacity var(--dur-fast) var(--ease); }
.lang__on  { opacity: 1; }
.lang[data-lang="en"] .lang__on  { opacity: 0.45; }
.lang[data-lang="en"] .lang__off { opacity: 1; }
.lang:hover .lang__off, .lang:focus-visible .lang__off { opacity: 0.8; }
.lang[data-lang="en"]:hover .lang__on { opacity: 0.8; }


/* ================================================== LOS SALTOS SON DEL AUTOR
   Seis titulares del sitio llevan su corte escrito a mano en el JSON. Medido:
   sólo se respetaban en dos sitios. En el resto el navegador los ignoraba y
   partía por donde le cabía — `ecosistema.h` y `footer.h` salían en UNA línea
   larga, y `hero.h` partía bien por casualidad, porque coincidía el ancho.

   `pre-line` hace que el \n valga, y llega a tiempo: titulos.js mide los saltos
   por `offsetTop` ANTES de reconstruir el titular en spans, así que se queda con
   los del autor. Después añade `.tit`, que vuelve a `normal`, y ya da igual
   porque las líneas ya son elementos.

   Sólo se aplica a titulares con corte escrito. El cuerpo de texto NO lo lleva:
   una frase larga tiene que recomponerse sola al estrecharse. */
.escena--hero h1, .escena--hero h2,
#ecosistema h2[data-titulo],
#contacto h2[data-titulo] {
  white-space: pre-line;
}


/* ================================================== EL FORMULARIO DE CONTACTO
   Mismo lenguaje de campo que el configurador (`.cfg-campo` en
   presupuesto.css): fondo transparente, sin caja, filete inferior y etiqueta en
   versalitas. No se comparte el CSS entre los dos porque viven en páginas y
   hojas distintas, y el del configurador está acoplado a su propio recorrido de
   pasos; se repite el TRATO, no el componente. */
.fcon { margin-top: 4.5rem; max-width: none; }
.fcon__h { margin: 0.9rem 0 0; font-size: clamp(2.4rem, 5vw, 4.4rem); font-weight: 400; line-height: 0.9; letter-spacing: -0.06em; max-width: 18ch; text-wrap: balance; }
.fcon__intro { margin: 0.75rem 0 0; opacity: 0.72; max-width: 46ch; }

.fcon__campos {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.75rem;
  margin-top: 2.25rem;
}
.fcon__campo { display: flex; flex-direction: column; gap: 0.5rem; }
.fcon__campo--ancho { grid-column: 1 / -1; }
.fcon__campo input,
.fcon__campo textarea {
  font: inherit; font-size: 1.0625rem;
  color: var(--white); background: transparent;
  border: 0; border-bottom: 1px solid var(--line-marco);
  padding: 0.8em 0; resize: vertical;
}
.fcon__campo input:focus-visible,
.fcon__campo textarea:focus-visible {
  outline: none; border-bottom-color: var(--ember);
}
/* El aviso de campo vacío no es sólo color: el filete cambia de grosor. Quien
   no distingue el rojo tiene que poder ver cuál falta. */
.fcon__campo[data-error] input,
.fcon__campo[data-error] textarea { border-bottom-width: 2px; border-bottom-color: var(--ember); }

.fcon__enviar { margin-top: 2.25rem; }

/* La trampa de robots se saca de la vista SIN `display:none` ni
   `visibility:hidden`: hay bots que sólo rellenan lo que está en el flujo, y
   ocultarlo del todo la delata. */
.fcon__trampa {
  position: absolute; left: -9999px; width: 1px; height: 1px;
  opacity: 0; pointer-events: none;
}

.fcon__estado { margin: 1rem 0 0; font-size: 0.9rem; min-height: 1.4em; }
.fcon__estado[data-ok] { color: var(--paper); }
.fcon__estado[data-mal] { color: var(--ember); }

@media (max-width: 640px) {
  .fcon__campos { grid-template-columns: 1fr; gap: 1.4rem; }
}

/* --------------------------------------------------------------- PORFOLIO
   Traído de makers-site/trabajo.html. Sin fondos: flota sobre el universo como
   el resto. El acento sólo en estructura (la etiqueta «Ejemplo»). */


/* -------------------------------------------------------- EL PORFOLIO · CAMPO
   Ni rejilla ni columnas: las piezas van sueltas por la sección, del tamaño de
   un fotograma de contacto, a la deriva y en blanco y negro. Lo que se acerca
   al cursor se enciende y crece.

   El RÓTULO va arriba y el campo debajo, cada uno en su sitio: antes el campo era
   absoluto sobre la sección entera y el texto se leía con fotos pasando por
   detrás. Ahora el campo es un bloque con ALTO PROPIO —sus piezas van absolutas y
   no empujan nada, así que sin alto mediría cero— y `overflow: hidden` en la
   sección para que una pieza del borde, al crecer, no saque barra horizontal. */
#portfolio-makers {
  position: relative;
  overflow: hidden;
  /* Sin `min-height` propio: el alto lo pone el rótulo más el campo. */
  padding-bottom: 2rem;
}
.pm__campo { position: relative; height: 340svh; margin-top: 2.5rem; }
.pm__head { position: relative; z-index: 2; }

.pm__pieza {
  position: absolute;
  margin: 0;
  /* La pieza se coloca por su CENTRO, no por su esquina: el reparto del JS
     razona en centros y el imán tira del centro. */
  translate: -50% -50%;
  will-change: transform;
}
.pm__pieza img, .pm__pieza video {
  display: block; width: 100%; height: auto;
  border: 1px solid var(--line-sutil);
  box-shadow: 0 0.35rem 1.1rem rgba(0, 0, 0, 0.55);   /* separa las que se solapan */
  /* En reposo son archivo: grises y algo apagadas, para que el campo tenga
     profundidad y no compita con el titular. */
  filter: grayscale(1) contrast(1.02);
  opacity: 0.62;
  transition: filter 0.45s var(--ease), opacity 0.45s var(--ease),
              box-shadow 0.45s var(--ease);
}
.pm__pieza { z-index: var(--z, 1); }
.pm__pieza--abre { cursor: zoom-in; }
.pm__pieza.is-cerca { z-index: 999; }
.pm__pieza.is-cerca img, .pm__pieza.is-cerca video {
  filter: none;
  opacity: 1;
  box-shadow: 0 1rem 3rem rgba(5, 5, 5, 0.75);
}

@media (max-width: 860px) { .pm__campo { height: 330svh; margin-top: 1.5rem; } }
@media (prefers-reduced-motion: reduce) {
  .pm__pieza img, .pm__pieza video { transition: none; }
}

/* LAS PIEZAS DENTRO DE LA CAPA. Venían de su propia sección y ahora viven en el
   cuerpo de cada capa, entre lo que te llevas y el precio. Se separan con un
   filete y su rótulo: sin eso, diez desplegables seguidos detrás de una lista
   de entregables se leen como más de lo mismo. */
.eco__piezas {
  margin: 1.5rem 0;
  padding-top: 1.25rem;
  border-top: 1px solid var(--line-sutil);
}
.eco__piezasT { opacity: 0.6; margin: 0 0 0.75rem; }
/* La línea de debajo del botón: quita el miedo a pulsar, así que va pegada a
   él y no al cuerpo de texto de arriba. */
/* ------------------------------------------------------------------ EL VISOR */
.visor {
  position: fixed; inset: 0; z-index: 9000;
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; grid-template-rows: minmax(0, 1fr);
  align-items: center; gap: 1rem; padding: 4rem 1.25rem 3.5rem;
  /* OPACO DEL TODO. Estaba a 0,95, y ese 5% dejaba pasar el botón blanco que
     hay debajo: a pantalla completa en un móvil se veía como una mancha
     flotando sobre la foto. `var(--ink)` es el mismo 5,5,5 de antes, sólo que
     sin dejar pasar nada. */
  background: var(--ink);
  opacity: 0; transition: opacity 0.25s var(--ease);
  /* En táctil, deja el gesto al deslizamiento y no al scroll de la página. */
  touch-action: none;
}
.visor[hidden] { display: none; }   /* `display: grid` de arriba anularía el atributo */
.visor.is-abierto { opacity: 1; }
.visor__fig { margin: 0; min-height: 0; height: 100%; display: grid; grid-template-rows: minmax(0, 1fr) auto; justify-items: center; gap: 0.75rem; }
.visor__img { max-width: 100%; max-height: 100%; min-height: 0; object-fit: contain; transition: opacity 0.2s var(--ease); }
.visor__img.is-cambiando { opacity: 0.25; }
.visor__pie { margin: 0; font-size: 0.85rem; line-height: 1.4; opacity: 0.75; text-align: center; max-width: 60ch; min-height: 1.4em; }
.visor__x, .visor__flecha {
  width: 3rem; height: 3rem; border-radius: 50%; padding: 0;
  background: none; color: inherit; border: 1px solid var(--line-marco);
  font-size: 1.25rem; line-height: 1; cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
.visor__x:hover, .visor__flecha:hover, .visor__x:focus-visible, .visor__flecha:focus-visible { border-color: var(--paper); background: rgba(255, 255, 255, 0.08); }
.visor__x { position: absolute; top: 1rem; right: 1rem; }
.visor__cuenta { position: absolute; left: 0; right: 0; bottom: 1.1rem; margin: 0; text-align: center; font-size: 0.8rem; letter-spacing: 0.14em; opacity: 0.6; font-variant-numeric: tabular-nums; }
@media (max-width: 640px) {
  .visor { grid-template-columns: minmax(0, 1fr); padding: 4rem 0.75rem 5rem; }
  /* Las flechas bajan a los lados del contador: a los lados de la foto le comían
     un tercio del ancho a una pantalla de 375 px. */
  .visor__flecha { position: absolute; bottom: 0.6rem; }
  .visor__prev { left: 1rem; } .visor__sig { right: 1rem; }
}
@media (prefers-reduced-motion: reduce) { .visor, .visor__img { transition: none; } }

/* ===== SLIDESHOW · vídeo, branding, diseño web (dentro de #portfolio-makers)
   Tres a la vista y el resto fuera de pantalla. Misma voz que el resto: rótulo
   en versalitas con filete fino, sin cajas ni acento, imagen a sangre. */
.sl { position: relative; z-index: 2; margin-top: clamp(2.5rem, 5vw, 4rem); --sl-gap: 1.25rem; }
.sl__cab { display: flex; align-items: flex-end; justify-content: space-between; gap: 1rem; padding-top: 1rem; margin-bottom: 1.25rem; border-top: 1px solid var(--line-sutil); }
/* BLOQUE Y NO FLEX EN COLUMNA, y esto arregla un título partido. Con `flex-
   direction: column` cada hijo cae en su propia fila — y el efecto de foco
   envuelve CADA PALABRA en su span, así que «Diseño web» salía en dos líneas
   con los 0,9rem del `gap` metidos entre las dos palabras. Se notaba sólo en
   este rótulo porque es el único del carrusel con más de una palabra.
   Como bloque, la rayita se coloca con su propio margen y el título fluye. */
.sl__t { display: block; font-size: clamp(1.75rem, 3vw, 2.75rem); font-weight: 400; line-height: 0.95; letter-spacing: -0.05em; }
.sl__t::before { content: ""; display: block; width: 6px; height: 1px; margin-bottom: 0.9rem; background: var(--ember); }
.sl__ctl { display: flex; gap: 0.5rem; }
.sl__b { width: 44px; height: 44px; border-radius: 50%; border: 1px solid var(--line-marco); background: transparent; color: inherit; font: inherit; cursor: pointer; transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease); }
.sl__b:hover, .sl__b:focus-visible { background: var(--white); color: var(--ink); border-color: var(--ember); }
.sl__pista { display: flex; gap: var(--sl-gap); overflow-x: auto; scroll-snap-type: x mandatory; scrollbar-width: none; padding: 0; margin: 0; list-style: none; overscroll-behavior-x: contain; }
.sl__pista::-webkit-scrollbar { display: none; }
.sl__pista:focus-visible { outline: 1px solid var(--ember); outline-offset: 6px; }
.sl { --sl-n: 3; }
.sl--web { --sl-n: 2; }
.sl--cuadro { --sl-n: 1; }
.sl__item { flex: 0 0 calc((100% - (var(--sl-n) - 1) * var(--sl-gap)) / var(--sl-n)); scroll-snap-align: start; overflow: hidden; background: color-mix(in srgb, var(--paper) 5%, transparent); }
.sl__item img { display: block; width: 100%; height: 100%; }
.sl--video .sl__item { aspect-ratio: 16 / 9; }
.sl--video .sl__item img { object-fit: cover; }
.sl--cuadro .sl__item { aspect-ratio: 16 / 10; }
.sl--cuadro .sl__item img { object-fit: contain; }
.sl--web .sl__item { aspect-ratio: 1; }
.sl--web .sl__item img { object-fit: contain; }
.sl__vid { position: relative; display: block; width: 100%; height: 100%; padding: 0; border: 0; background: #000; cursor: pointer; }
.sl__vid iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
.sl__play { position: absolute; left: 50%; top: 50%; width: 56px; height: 56px; margin: -28px 0 0 -28px; border-radius: 50%; border: 1px solid var(--white); background: rgba(5,5,5,.45); transition: background var(--dur-fast) var(--ease); }
.sl__play::after { content: ""; position: absolute; left: 22px; top: 17px; border-left: 15px solid var(--white); border-top: 10px solid transparent; border-bottom: 10px solid transparent; }
.sl__vid:hover .sl__play, .sl__vid:focus-visible .sl__play { background: var(--ember); border-color: var(--ember); }
.sl__vid:focus-visible { outline: 1px solid var(--ember); outline-offset: -4px; }
@media (max-width: 860px) { .sl__item, .sl--web .sl__item, .sl--cuadro .sl__item { flex-basis: 78%; } .sl--cuadro .sl__item { aspect-ratio: 1; } }

/* ===== PLANES · misma gramática: filete fino, rótulo, cuerpo heredado ===== */
.pl { position: relative; }
.pl__vent, .pl__grid { display: grid; gap: 0 clamp(1.25rem, 3vw, 2.5rem); padding: 0; margin: 0; list-style: none; grid-template-columns: repeat(3, 1fr); }
.pl__vent { grid-template-columns: repeat(4, 1fr); }
.pl__v, .pl__card, .pl__web { position: relative; display: flex; flex-direction: column; gap: 0.75rem; padding: 1.25rem 0 2.25rem; border-top: 1px solid var(--line-sutil); }
.pl__v h3::before, .pl__web h3::before { content: ""; display: block; width: 6px; height: 1px; margin-bottom: 0.9rem; background: var(--ember); }
.pl__v h3, .pl__web h3 { font-weight: 500; font-size: 1.05rem; line-height: 1.25; letter-spacing: -0.015em; }
.pl__v p, .pl__web p { font-size: 0.95rem; opacity: 0.8; max-width: 32ch; }
.pl__n { display: flex; flex-direction: column; gap: 0.6rem; font-weight: 400; font-size: clamp(1.5rem, 2.2vw, 2rem); line-height: 1; letter-spacing: -0.04em; }
.pl__num { color: var(--ember); opacity: 1; }
.pl__num, .pl__cinta { font-size: 0.7875rem; text-transform: uppercase; letter-spacing: 0.16em; line-height: 1; opacity: 0.75; letter-spacing: 0.16em; }
.pl__cinta { position: absolute; top: 1.25rem; right: 0; opacity: 1; color: var(--ember); }
.pl__card--rec { border-top: 2px solid var(--ember); padding-top: calc(1.25rem - 1px); }
.pl__card, .pl__web, .pl__v { transition: border-color var(--dur-fast) var(--ease); }
.pl__card:hover, .pl__web:hover, .pl__v:hover { border-top-color: var(--ember); }
.pl__lista { display: grid; gap: 0; padding: 0; margin: 0.5rem 0 0; list-style: none; font-size: 0.95rem; flex: 1; align-content: start; }
.pl__lista li { position: relative; padding: 0.55rem 0 0.55rem 1.1rem; border-top: 1px solid var(--line-sutil); }
.pl__lista li::before { content: ""; position: absolute; left: 0; top: 50%; width: 6px; height: 1px; background: var(--ember); }
.pl__lista li:first-child { border-top: 0; }
.pl__sel { align-self: flex-start; margin-top: 1rem; }
.pl__sel:not(.cta--primario) { border: 1px solid var(--line-marco); border-radius: 999px; padding: 0.9em 1.9em; min-height: 44px; }
.pl__grid--web { grid-template-columns: repeat(4, 1fr); }
.pl__plazo { color: var(--ember); margin-top: auto; padding-top: 0.5rem; font-size: 0.7875rem !important; text-transform: uppercase; letter-spacing: 0.16em; opacity: 0.75 !important; }
/* EL PRECIO, COMO EN EL PDF. Dos líneas discretas y ningún número gigante:
   el mensual arriba en gris y el semestral debajo en blanco. Ése es el orden
   del PDF y es el que cuenta la historia sola — primero lo que cuesta sin
   comprometerse, después lo que cuesta comprometiéndose, y la resta la hace
   quien lee.

   El mensual va además un punto más pequeño. No es decoración: es el precio
   que NO queremos que ancle la lectura, y en tipografía el tamaño es lo
   primero que ordena la jerarquía, antes que el color.

   `tabular-nums` alinea las cifras entre las tres tarjetas; sin eso, 350 y 750
   bailan y la escalera deja de verse de un vistazo. */
.pl__precio { margin: 0.9rem 0 0; max-width: none; font-variant-numeric: tabular-nums; }
.pl__precio > span { display: block; }
.pl__precio-mes { font-size: 0.875rem; opacity: 0.55; }
.pl__precio-sem { margin-top: 0.15rem; font-size: 1rem; color: var(--white); }
/* El IVA, pegado a cada cifra y no sólo en la letra pequeña del final: un
   precio que se anuncia sin impuesto tiene que decirlo donde se lee el
   precio. */
.pl__iva { display: inline; font-size: 0.78rem; opacity: 0.5; }
.pl__nota { margin-top: 1.5rem; font-size: 0.95rem; opacity: 0.75; max-width: 70ch; }
.pl__pedir { margin-top: 2rem; }
.pl__faq { max-width: none; }
.pl__q { border-top: 1px solid var(--line-sutil); }
.pl__q:last-child { border-bottom: 1px solid var(--line-sutil); }
.pl__q summary { position: relative; cursor: pointer; list-style: none; padding: 1.1rem 2rem 1.1rem 0; font-weight: 500; letter-spacing: -0.015em; }
.pl__q summary::-webkit-details-marker { display: none; }
.pl__q summary::after { color: var(--ember); content: "+"; position: absolute; right: 0; top: 1.1rem; font-weight: 400; }
.pl__q[open] summary::after { content: "\2013"; }
.pl__q summary:focus-visible { outline: 1px solid var(--ember); outline-offset: 2px; }
.pl__q[open] { border-left: 1px solid var(--ember); padding-left: 1rem; }
.pl__q div { padding: 0 0 1.25rem; opacity: 0.85; line-height: 1.6; max-width: 70ch; }
.pl__q div ul { padding-left: 1.1rem; }
@media (max-width: 1100px) { .pl__grid--web { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 960px) { .pl__vent, .pl__grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 620px) { .pl__vent, .pl__grid, .pl__grid--web { grid-template-columns: 1fr; } }

/* Cabeceras de bloque: las mismas que las de sección (`.head` + label + h2), más grandes. */
.pl__head { margin: clamp(3rem, 6vw, 5rem) 0 clamp(1.75rem, 3vw, 2.5rem); gap: 1.25rem; }
.pl .head:first-child { margin-top: 0; }
#planes h2, .pm__head h2 { font-size: clamp(2.4rem, 5vw, 4.4rem); max-width: 18ch; }
.pl__head p, .pm__head .head > p { font-size: clamp(1.05rem, 1.35vw, 1.25rem); max-width: 44ch; opacity: 0.9; }
.pl__n { font-size: clamp(1.9rem, 3vw, 2.75rem); }
.pl__v h3, .pl__web h3 { font-size: 1.3rem; }
.pl__v p, .pl__web p, .pl__lista, .pl__mas p, .pl__nota, .pl__q div { font-size: 1.0125rem; line-height: 1.6; }
/* Planes y packs web, en cajas. */
.pl__grid { gap: clamp(1rem, 2vw, 1.5rem); }
.pl__card, .pl__web { padding: clamp(1.5rem, 2.4vw, 2.25rem); border: 1px solid var(--line-marco); background: color-mix(in srgb, var(--paper) 4%, transparent); transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease); }
.pl__card:hover, .pl__web:hover { border-top-color: var(--ember); }
.pl__card--rec { border-top: 2px solid var(--ember); padding-top: calc(clamp(1.5rem, 2.4vw, 2.25rem) - 1px); }
.pl__cinta { top: clamp(1.5rem, 2.4vw, 2.25rem); right: clamp(1.5rem, 2.4vw, 2.25rem); }
.pl__card .pl__lista, .pl__web .pl__lista { margin-top: 0.75rem; }
/* «Seleccionar»: sin relleno blanco. Contorno y letra blancos; en el recomendado, contorno rojo. Al pasar, relleno rojo. */
.pl__sel, .pl__sel.cta--primario { background: transparent; color: var(--white); border: 1px solid var(--line-marco); text-shadow: none; }
.pl__sel:hover, .pl__sel:focus-visible, .pl__sel.cta--primario:hover, .pl__sel.cta--primario:focus-visible { background: transparent; border-color: var(--ember); color: var(--ember); text-shadow: none; }

/* Negrita = peso 500, el único que la familia tiene además del 400. Mismo criterio que el resto: nombres, cifras y lo que decide. */
.pl strong { font-weight: 500; color: var(--white); }
.pl__n { font-weight: 500; letter-spacing: -0.035em; }
.pl__lista li strong { letter-spacing: -0.01em; }
.pl__plazo strong { font-weight: 500; }

/* Detalle desplegable de cada plan: mismo lenguaje que las preguntas frecuentes. */
.pl__mas { border-top: 1px solid var(--line-sutil); margin-top: 0.5rem; }
.pl__mas summary { position: relative; cursor: pointer; list-style: none; padding: 0.9rem 2rem 0.9rem 0; font-size: 0.7875rem; text-transform: uppercase; letter-spacing: 0.16em; line-height: 1; }
.pl__mas summary::-webkit-details-marker { display: none; }
.pl__mas summary::after { content: "+"; position: absolute; right: 0; top: 0.8rem; font-size: 1.1rem; color: var(--ember); letter-spacing: 0; }
.pl__mas[open] summary::after { content: "\2013"; }
.pl__mas summary:focus-visible { outline: 1px solid var(--ember); outline-offset: 2px; }
.pl__mas div { padding-bottom: 0.75rem; }
.pl__mas p { opacity: 0.85; max-width: none; }
.pl__mas p + p { margin-top: 0.75rem; }
.pl__mas strong { opacity: 1; }

/* El foco por cursor también en los títulos sin revelado: tienen que poder recibir el ratón dentro de planos con `pointer-events: none`. */
.esc__t, .mk-eyebrow, .sl__t, .pl__v h3, .pl__n, .pl__web h3, .fcon__h { pointer-events: auto; }
.pl__n .foco__marco, .sl__t .foco__marco { pointer-events: none; }

/* Un solo trazo tipográfico: los botones no heredan la familia por defecto y salían en Arial. */
button, input, textarea, select { font-family: inherit; }
/* Un solo escalón de tracking para títulos: display -0.06em, nombres en 500 -0.02em. */
.sl__t { letter-spacing: -0.06em; line-height: 0.9; }
.pl__n, .pl__v h3, .pl__web h3 { letter-spacing: -0.02em; }

#planes, #portfolio-makers { padding-top: clamp(5rem, 8vw, 7rem); }
.pl__q summary { font-size: 1.0125rem; }
/* Familias de packs sueltos: un rótulo por familia dentro del mismo bloque. */
.pl__fam { margin: clamp(2rem, 4vw, 3rem) 0 0.5rem; font-size: clamp(1.3rem, 2vw, 1.75rem); font-weight: 400; letter-spacing: -0.03em; }
.pl__fam + .pl__sub { margin-bottom: 1.25rem; }

/* Cierres de sección: el sitio donde alguien acaba de ver algo y puede actuar. */
.pl__cierre { margin-top: clamp(2.5rem, 5vw, 4rem); display: flex; flex-wrap: wrap; align-items: center; gap: 1.25rem 2rem; }
.pl__cierre p { margin: 0; max-width: 46ch; }
.pl__cierre-faq { margin-top: 2rem; display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem 1.25rem; }
.fcon__promesa { margin-top: 1.25rem; max-width: 52ch; opacity: 0.75; font-size: 0.95rem; }

/* ================= PLANES · LA MISMA GRAMÁTICA QUE EL RESTO
   Las familias de packs —Reels Power, Vídeo Pro, Foto Pro— son el mismo nivel
   que las filas del porfolio (Vídeo, Branding, Diseño web), así que se escriben
   igual: rayita roja encima, tamaño de rótulo y el tracking apretado de los
   titulares. Estaban a 24 px, sin marca y con la mitad de tracking: se leían
   como un párrafo en negrita en medio de la sección. */
.pl__fam {
  display: flex; flex-direction: column; gap: 0.9rem;
  margin: clamp(3rem, 6vw, 4.5rem) 0 0.9rem;
  font-size: clamp(1.75rem, 3vw, 2.75rem);
  font-weight: 400; line-height: 0.9; letter-spacing: -0.06em;
}
.pl__fam::before { content: ""; width: 6px; height: 1px; background: var(--ember); }
/* Y su bajada. Clase PROPIA y no `.pl__sub`: ese nombre ya era del preloader
   —con su `margin: auto` centrado— y al reutilizarlo aquí las bajadas de Planes
   heredaban un centrado que nada más en la sección tiene. Dos componentes
   distintos no pueden compartir nombre por el prefijo. */
.pl__bajada { margin: 0 0 1.5rem; max-width: 44ch; font-size: clamp(1.05rem, 1.35vw, 1.25rem); line-height: 1.6; opacity: 0.9; text-align: left; }

/* Los cierres son momentos de ACCIÓN, y el rojo marca justo eso en esta web:
   el filete los separa de lo que viene antes y los anuncia. */
.pl__cierre { padding-top: 1.5rem; border-top: 1px solid var(--line-sutil); position: relative; }
.pl__cierre::before { content: ""; position: absolute; top: -1px; left: 0; width: 56px; height: 1px; background: var(--ember); }
.pl__cierre p { font-size: clamp(1.05rem, 1.35vw, 1.25rem); opacity: 0.9; }
.pl__cierre-faq { font-size: 1.0125rem; }
/* La nota de entrega es letra pequeña de contrato: se queda discreta y se separa. */
.pl__nota { padding-top: 1.25rem; border-top: 1px solid var(--line-sutil); }
/* Toda la sección se alinea a la izquierda, como el resto de la web: la bajada
   de familia heredaba el centrado de un contenedor y quedaba descolgada. */
.pl__fam, .pl__nota, .pl__cierre, .pl__cierre-faq { text-align: left; }


/* ================= TÁCTIL Y MÓVIL · lo que el dedo tiene que poder acertar
   Auditado a 375, 390, 768 y 1024. La web no desborda en ninguno, pero varios
   controles se quedaban muy por debajo del objetivo táctil mínimo. */

/* La cabecera: marca, sonido e idioma medían 16, 32 y 31 px de alto. */
.hdr__mark { display: inline-flex; align-items: center; min-height: 44px; }
.son { width: 44px; height: 44px; }
.lang { min-height: 44px; padding: 0 0.25rem; }
/* Y los enlaces del menú, 13 px a 768. */
.hdr__nav a { display: inline-flex; align-items: center; min-height: 44px; }

/* EL MENÚ EN MÓVIL. Por debajo de 768 se ocultaba y no había sustituto: en un
   documento de 27.000 px de alto, la única forma de ir a Planes o a Contacto era
   bajar a dedo. Ahora se despliega bajo la cabecera con el botón de la derecha. */
.hdr__menu {
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; padding: 0; border: 0; border-radius: 50%;
  background: transparent; color: inherit; cursor: pointer;
}
.hdr__menu span, .hdr__menu span::before, .hdr__menu span::after {
  content: ""; display: block; width: 18px; height: 1px; background: currentColor;
  transition: transform var(--dur-fast) var(--ease), opacity var(--dur-fast) var(--ease);
}
.hdr__menu span { position: relative; }
.hdr__menu span::before { position: absolute; top: -6px; }
.hdr__menu span::after { position: absolute; top: 6px; }
.hdr__menu[aria-expanded="true"] span { background: transparent; }
.hdr__menu[aria-expanded="true"] span::before { transform: translateY(6px) rotate(45deg); }
.hdr__menu[aria-expanded="true"] span::after { transform: translateY(-6px) rotate(-45deg); }
@media (min-width: 768px) { .hdr__menu { display: none; } }
@media (max-width: 767px) {
  .hdr { flex-wrap: wrap; }
  .hdr__nav {
    display: none; order: 3; width: 100%; flex-direction: column; gap: 0;
    padding-top: 0.5rem; border-top: 1px solid var(--line-sutil); margin-top: 0.75rem;
  }
  .hdr__nav.is-abierto { display: flex; }
  .hdr__nav a { min-height: 48px; width: 100%; }

  /* CON EL MENÚ ABIERTO, LA CABECERA NECESITA FONDO, y esto era un fallo de
     verdad: los cuatro enlaces caían encima del párrafo del hero y del 3D en
     movimiento, y no se leía ni una cosa ni la otra. Comprobado a 375 px,
     «PLANES» y «CONTACTO» se cruzaban con la línea «clientes, y agentes de IA
     que los atienden».

     La cabecera no tiene fondo a propósito —vive sobre el 3D y se apoya en la
     sombra de texto—, y esa sombra basta para una fila de rótulos sueltos. No
     basta para un panel de cuatro enlaces, que es otra cosa.

     Va la regla al contenedor y también al propio panel: si `:has()` no
     estuviera disponible, el panel sigue siendo legible por su cuenta. */
  .hdr:has(.hdr__nav.is-abierto),
  .hdr__nav.is-abierto {
    background: color-mix(in srgb, var(--ink) 97%, transparent);
  }

  /* EL TÍTULO DEL CARRUSEL, EN SU PROPIA LÍNEA. `space-between` deja al título
     y a las flechas repartiéndose 375 px, y «Diseño web» a 1,75rem no cabe:
     parte en dos y las flechas quedan colgando a media altura. Apilados, el
     título ocupa el ancho que necesita y las flechas se van a la derecha
     debajo, que además las deja más cerca del pulgar. */
  .sl__cab { flex-wrap: wrap; align-items: flex-start; }
  .sl__cab .sl__t { width: 100%; }
  .sl__cab > :not(.sl__t) { margin-left: auto; }

  /* UN ENLACE DE CORREO DE 13 px DE ALTO NO SE PUEDE PULSAR. Es texto suelto,
     así que su caja es la de la línea; aquí se le da el alto de un objetivo
     táctil sin cambiar cómo se ve. */
  .t-label[href^="mailto:"], .t-label[href^="tel:"] {
    display: inline-flex; align-items: center; min-height: 44px;
  }

  /* Y LOS DESPLEGABLES DE LOS PACKS SE QUEDABAN EN 41. */
  .pl__mas summary { min-height: 44px; display: flex; align-items: center; }
}

/* El botón que abre el visor quedaba DEBAJO de las fotos que flotan sobre él:
   dos de cada tres puntos del botón devolvían una imagen al pulsar. */
.pm__ver { position: relative; z-index: 100; }

/* Las fichas del Sistema y el correo del pie: 13-28 px de alto en táctil. */
#sistema .tiene-ficha { min-height: 44px; }
.fpie a.t-label { display: inline-flex; align-items: center; min-height: 44px; }

/* Suelo de 13 px reales en las versalitas: 12,6 se queda corto para leer. */
.t-label, .label { font-size: 0.8125rem; }

/* Y la cabecera se quita de en medio mientras el visor está abierto: se
   transparentaba sobre la foto, con «MAKERS» y «ES / EN» encima de la imagen. */
body:has(.visor.is-abierto) .hdr { opacity: 0; pointer-events: none; }
